slackcli/├── src/│ ├── index.ts CLI entry point: welcome screen or parse, update notice│ ├── program.ts createProgram(): registers command groups, hooks and help│ ├── version.ts App version (build-time define, else package.json)│ ├── commands/ One file per command group│ │ ├── auth.ts│ │ ├── canvas.ts│ │ ├── conversations.ts│ │ ├── files.ts│ │ ├── messages.ts│ │ ├── saved.ts│ │ ├── search.ts│ │ └── update.ts│ ├── lib/ All logic; tests live beside each file│ └── types/index.ts Shared interfaces├── scripts/build.ts Compile wrapper: checks Bun >= 1.4.1, injects __APP_VERSION__├── scripts/release-announcement.ts Builds the Discord release announcement payload for release.yml├── .github/workflows/ CI, tests, SonarQube Cloud scan, release, policy checks├── .github/ISSUE_TEMPLATE/ Issue forms; blank issues are disabled├── .github/PULL_REQUEST_TEMPLATE.md Linked-issue reference + checklist├── .github/requirements/ Hash-locked pip requirements for CI (pre-commit)├── .pre-commit-config.yaml Local checks mirroring CI├── sonar-project.properties SonarQube Cloud analysis settings (sonar.yml)├── CLAUDE.md Repository constitution + architecture notes├── CONTRIBUTING.md Contribution policy├── RELEASING.md Release schedule, versioning and deprecation policy└── dist/ Build output (gitignored)src/commands/
Section titled “src/commands/”Each file exports a create<Group>Command(): Command factory that
createProgram() in src/program.ts registers. They parse flags, call into src/lib/, print, and set the exit code.
They hold no Slack API knowledge.
| File | Subcommands |
|---|---|
auth.ts |
login, login-browser, login-auto, whoami, list, set-default, remove, logout, extract-tokens, parse-curl |
canvas.ts |
list, read, sections, create, edit |
conversations.ts |
list, read, get, unread, mark-read, members list/add/remove, join, leave |
files.ts |
info, read, download |
logs.ts |
path, show, clear |
messages.ts |
send, react, unreact, edit, delete, draft, list-drafts, send-draft, delete-draft, schedule, list-scheduled, delete-scheduled |
saved.ts |
list, add, update, complete, reopen, remove |
search.ts |
messages, channels, people |
update.ts |
(default action), check |
src/lib/
Section titled “src/lib/”| Module | Responsibility |
|---|---|
slack-client.ts |
The Slack API abstraction. Dispatches every call to standardRequest() or browserRequest() by auth type, through the shared rate limiter, retries browser-auth calls per retry.ts, logs each attempt’s method, auth type, duration and outcome, and rethrows an authentication failure as a SlackAuthError (auth-errors.ts). A call that got no usable HTTP response throws SlackTransportError on both auth types. |
auth.ts |
Login orchestration; returns a configured SlackClient. The only place that decides a token is valid, and the one place the workspace selector (--workspace, SLACKCLI_WORKSPACE, stored default) is resolved. checkIdentity() is the auth whoami lookup: one auth.test for the selected profile, returned as a typed ok / auth_failed / unreachable result that keeps the profile key. checkAllProfiles() runs it over every stored profile for auth list --check, turning any per-profile failure into a result, and buildProfileList() shapes the auth list --json object. |
workspaces.ts |
Multi-workspace persistence, profile-key derivation and resolution. |
secret-store.ts |
Credential storage seam: the SecretStore interface, the inline FileSecretStore and macOS MacOSKeychainSecretStore backends, RoutingSecretStore, and helpers that split a config into metadata and secrets. |
browser-auth.ts |
Captures xoxd/xoxc from a signed-in browser; pure extractors are exported for tests. |
browser-launcher.ts |
Finds and launches a local Chromium-family browser; owns the profile directory. |
cdp-client.ts |
Minimal Chrome DevTools Protocol client over Bun’s WebSocket. |
curl-parser.ts |
Extracts tokens from a DevTools cURL command. |
curl-input.ts |
Resolves auth parse-curl’s input source (argument → --from-clipboard → piped stdin → interactive prompt) into a typed result; clipboard, stdin and TTY are injectable for tests. |
slack-url-parser.ts |
Slack URL / permalink / timestamp normalisation. |
name-resolver.ts |
Channel names (#general) and user handles / emails → IDs. parseNameReference() is pure, so an ID or URL never reaches Slack; resolveIdentifier() is the per-argument entry point commands call after slack-url-parser.ts (it also drops the @/# from a prefixed ID), and resolveUserList() resolves a <users...> list with one shared users.list scan. Exact, case-insensitive matches only; nothing found throws NotFoundError, several matches InvalidInputError. lazyClient() lets write commands create a client only when a lookup needs one, so their no-auth confirmation refusal is unchanged. |
message-input.ts |
Resolves the text of messages send/edit/draft from --message, --message-file <path> or --message-file - (stdin). The stdin read is bounded (1 MB, 5 minutes, a zero-length chunk ends it); stdin and the TTY check are injectable for tests. |
mrkdwn.ts |
Slack mrkdwn → rich_text blocks (drafts). |
schedule-time.ts |
parseScheduleTime(): the --at / --in of messages schedule → Slack’s post_at (Unix seconds). Strict regexes instead of Date.parse or a date library; refuses impossible dates, skipped local times, the past and anything beyond MAX_SCHEDULE_DAYS (120). Pure, with an injected clock. parseDueTime(): the same --at formats for `saved add |
drafts.ts |
Validates draft-list limits, extracts text from rich_text, and projects undocumented responses into the public command contract. |
canvas-parser.ts |
Slack canvas HTML → Markdown. |
canvas-sections.ts |
canvas sections’s lookup criteria (buildSectionCriteria(): --type / --contains, at least one, validated before any Slack call) and the section IDs read back from canvases.sections.lookup (sectionsOf()). |
canvas-write.ts |
canvas create / canvas edit’s input: readCanvasMarkdown() (a file or - for stdin, bounded, with Slack’s 1,048,576-character limit), checkEditOptions() / buildCanvasChange() (what each canvases.edit operation needs and allows, checked before any Slack call), and isDestructiveOperation() (replace/delete ask for confirmation). |
canvas-read.ts |
canvas read’s work: resolves the canvas ID (explicit or a channel’s canvas), downloads its HTML, and resolves <@U…> / <#C…> mentions. Expected failures throw CanvasReadError carrying their exit code. |
rate-limiter.ts |
Concurrency cap and minimum interval shared by every Slack API call. Logs waits at debug. |
auth-errors.ts |
Pure classifier for Slack’s five authentication codes (invalid_auth, token_expired, token_revoked, not_authed, account_inactive): the meaning and fix for a profile, the three-line message, the SlackAuthError that SlackClient.request() throws for them on both auth paths, and the separate wording for a token rejected during login. |
retry.ts |
Pure retry policy for browser-auth calls: which failures are retried (429 always, 5xx/network errors for the READ_METHODS allowlist only), Retry-After parsing, backoff with jitter, and the attempt/wait caps. |
logger.ts |
Logging configuration: log directory and level resolution, the rotating file and verbose stderr sinks, the session_start environment header, and the exit override that logs Commander usage errors. Called once from src/program.ts; libs log via LogTape’s getLogger directly. |
log-redaction.ts |
The token/cookie/JWT redaction patterns applied to every log line. |
logs.ts |
logs command work: lists the log file and its rotations oldest first, reads them line by line, redacts each line again, groups records by run_id, selects runs, formats them as text, and deletes only the log files. Also exports redactText(), the sink’s patterns applied to one string, which command-errors.ts uses for the --json error object’s message and hint. |
tildify.ts |
Home directory as ~ in log records: tildify() for a path, tildifyText() for free text, errorMessageForLog() for an error’s message. Its own module so libs can use it without importing logger.ts. |
process-errors.ts |
Last-resort unhandledRejection / uncaughtException handlers: log the error with its stack, print the message (the --json error object when the command has --json), exit 1. Installed from src/program.ts. |
cli-errors.ts |
The closed set of --json error codes (ERROR_CODES) and the typed errors the CLI raises itself: CliError, InvalidInputError, NotFoundError, UnsupportedAuthTypeError, ConfirmationRequiredError. No imports, so any module can throw them. |
command-errors.ts |
classifyError() (any thrown value → {code, message, hint?, retryable, slack_error?}, by type and Slack code, credentials redacted) and failCommand(), the one failure path of a command: the usual text, or the JSON error object on stderr under --json; sets exit code 1. |
mark-read.ts |
conversations mark-read: reads the conversation’s current read cursor (last_read of conversations.info, best effort), makes the conversations.mark write, and returns { channel_id, ts, previous_last_read }. |
message.ts |
Fetch one message by channel + timestamp, per auth type; delete one, treating message_not_found as already deleted; remove a reaction, treating no_reaction as already removed. |
poll.ts |
conversations read’s polling helpers: exact Slack ts comparison, the ts > --oldest and --exclude-self filters, the next_oldest cursor, and resolving the authenticated identity (stored user_id, or one auth.test). |
saved.ts |
Resolves saved-item pointers into messages, channels, and users; the Later-list writes (saved add, update, complete, reopen, remove), turning their flags into one of the saved.update changes the Slack web client sends. |
unread.ts |
Fetches and normalises unread channel data across both auth types, plus the workspace-wide thread summary (threads of client.counts, browser auth only) and each unread conversation’s read cursors (cursors, kept off the list so the default output does not change). Also the pure helpers of the conversations unread action: filterByTypes() (--types) and unreadSummaryLine() (the spinner’s closing line). |
unread-messages.ts |
conversations unread --messages (browser auth only): flag validation, the cut at a conversation’s last_read (reusing poll.ts), the conversation and per-conversation caps, normalising and paging Slack’s undocumented thread view (a failure degrades to the summary), and fetchUnreadDetails(), which makes the calls one at a time. Logs IDs, counts and durations only, never message content. |
formatter.ts |
Chalk-coloured renderers, status helpers, and writeJson(). |
dry-run.ts |
--dry-run on the Slack writes: the DRY_RUN_FLAG option, buildPreview() (workspace identity, target with a best-effort #channel / @user name lookup, payload without undefined fields) and emitDryRun() (one JSON object under --json, else formatDryRun()’s text; logs the action and a field count only). |
help.ts |
The --help layout: describeCommand() (summary, description, Examples, Notes, standard notes for --json, browser-only, confirmation and --dry-run), the root command tree and footer, and the --help pointer after usage errors. |
clipboard.ts |
Cross-platform clipboard read (pbpaste / PowerShell / xclip / xsel). |
interactive-input.ts |
Multi-line terminal input (double-Enter or Ctrl-D). |
json-fields.ts |
--fields on the read commands: parseFields() / fieldsOption() (validation, --json required), projectFields() (dot-path projection of each list item or of a single record, envelope kept) and FIELDS_LIST_KEYS, the one table naming each command’s main list. applyFields() is what a command calls just before writeJson() |
updater.ts |
Self-update via GitHub releases, with SHA-256 verification. |
banner.ts |
The welcome screen bare slackcli prints in a terminal: the pure renderBanner() (logo, version, star call-to-action, issue link; narrow-terminal, no-colour and no-Unicode fallbacks), shouldShowBanner() (no arguments and stdout a TTY), supportsUnicode(), shouldUseColor() (chalk’s level plus NO_COLOR) and the repository URL constants. Called from src/index.ts before program.parse(). |
src/types/index.ts
Section titled “src/types/index.ts”Every shared interface: AuthType, TokenType, StandardAuthConfig,
BrowserAuthConfig, WorkspaceConfig, WorkspacesData, SlackChannel,
SlackUser, SlackFile, SlackMessage, SlackDraft, DraftSummary, SlackScheduledMessage, ScheduledMessageSummary, SlackAuthTestResponse, SavedItem, SavedItemUpdate,
SearchMatch, ChannelSearchResult, PeopleSearchResult, UnreadChannel, UnreadCursor, UnreadThread, UnreadThreads, UnreadSummary,
SlackCanvas, and the per-command option interfaces.
WorkspaceConfig is a discriminated union on auth_type — narrowing it is what
makes the dual-auth split type-safe rather than a runtime string check.
Tests sit beside the code they cover (src/lib/curl-parser.test.ts next to
src/lib/curl-parser.ts). See testing.