This file provides guidance to AI coding agents when working with code in this repository.
npm run build # Compile TypeScript to dist/ (uses tsconfig.build.json, excludes tests)
npm run dev # Watch mode compilation (uses tsconfig.build.json, excludes tests)
npm test # Run all tests (vitest)
npm run test:watch # Run tests in watch mode
npm run type-check # Type check without emitting (uses tsconfig.json, includes tests)
npm run lint # Fix lint issues (oxlint) + format (oxfmt)
npm run lint:check # Check lint + formatting (for CI)The project uses two tsconfig files: tsconfig.json includes test files for type-checking, while tsconfig.build.json extends it and excludes colocated *.test.ts and *.spec.ts files (plus src/__mocks__) so test-only code is not compiled into dist/.
Tests are colocated next to the module they cover (for example src/commands/thread/thread.test.ts or src/lib/refs.test.ts). Shared Vitest manual mocks for npm packages live in src/__mocks__/ (e.g. chalk.ts).
vitest.config.ts lists @doist/cli-core in server.deps.inline so vi.mock('@doist/cli-core', …) and vi.doMock('node:fs/promises', …) reach cli-core's compiled imports. Without it, vitest treats the package as external and Node's native resolver bypasses the mock substitution, breaking the auth / config / spinner suites.
Run a single test file:
npx vitest run src/lib/refs.test.tsRun the CLI locally:
node dist/index.js <command>
# or after build:
./dist/index.js <command>This is a TypeScript CLI (tdc) for Comms messaging, built with Commander.js.
Entry point: src/index.ts registers all commands with Commander.
Commands (src/commands/): Commands with multiple subcommands use a folder-based structure (src/commands/<entity>/index.ts) where the index file exports a register*Command(program) function and wires Commander subcommands to handler functions in sibling files. Single-command files remain as flat files (src/commands/<entity>.ts). Each subcommand handler file exports one async action function and imports from ../../lib/. An optional helpers.ts holds shared constants/utilities used by multiple subcommands. Commands support --json, --ndjson, and --full flags for machine-readable output.
Lib (src/lib/):
api.ts- Singleton CommsApi client from@doist/comms-sdk, workspace/user cachingrefs.ts- Reference parsing: accepts IDs (id:123or bare123), Comms URLs, or fuzzy names for workspaces/usersoutput.ts- JSON/NDJSON formatting with essential field filtering per entity typeconfig.ts- Persists config to~/.config/comms-cli/config.jsonauth.ts- Token loading/saving/clearing (env var or config file)markdown.ts- Terminal markdown rendering viamarked+marked-terminalcompletion.ts- Commander tree-walker + completion helpers for shell tab completion
Reference system: The CLI accepts flexible references throughout - numeric IDs, id: prefixed IDs, full Comms URLs (parsed via parseCommsUrl), or fuzzy name matching for workspaces/users.
-
Implicit view subcommand:
tdc thread <ref>defaults totdc thread view <ref>via Commander's{ isDefault: true }. Same forconversationandmsg. Edge case: if a ref matches a subcommand name (e.g., "reply"), the subcommand wins — user must usetdc thread view reply -
Named flag aliases: Where commands accept positional
[workspace-ref], the--workspaceflag is also accepted. Error if both positional and flag are provided -
JSON output on mutating commands: Mutating commands (create, update, delete, archive) should support
--jsonoutput where it provides scripting value. Commands that return an object from the API (create/update) should also support--full. Commands where the API returns void should output a minimal status object (e.g.{ id, deleted: true }or{ id, isArchived: true }). ExtendMutationOptionsinsrc/lib/options.ts(which already includesjsonandfull) rather than adding these fields ad hoc. UseformatJson()fromsrc/lib/output.tsfor the output. Seesrc/commands/away.tsas the reference implementation. -
Spinner messages: When adding new SDK method calls, add a corresponding entry in the
API_SPINNER_MESSAGESmap insrc/lib/api.ts. Every user-facing API call should have a spinner message so the CLI shows progress feedback. -
Batch API responses: When calling
client.batch(...), never access.datadirectly on a batch result. Use these helpers fromsrc/lib/api.ts:assertBatchData(response, label)— single result; throwsCliErroron any failure (includingdata: null). Use for primary data the command can't render without (e.g. the thread itself).getOptionalBatchData(response, label)— tolerant single result; returnsdata(ornull) on success, throws only on real API errors (code >= 400). Use whendata: nullwith a success code is a valid empty state (e.g.getUnreadreturning null for "no unread threads"). Callers typically chain?? []/?? defaultValue.buildBatchNameMap(ids, responses, label)— strict parallel lookup; use when every id must resolve (e.g. channels).buildOptionalBatchNameMap(ids, responses, label)— tolerant parallel lookup; skips entries withdata: nulland a success code (e.g. deleted users) but still throws on real API errors. Callers must fall back viauserMap.get(id) ?? \user:${id}``. Use for user lookups so a single missing user doesn't abort the whole command.
See
src/commands/inbox.tsandsrc/commands/channel/threads.ts(mix of strict primary + tolerant unread) andsrc/commands/thread/view.ts(tolerant user map) for reference.
- Always use
CliErrorfromsrc/lib/errors.tsinstead of barethrow new Error(...),console.error() + process.exit(1), orconsole.error() + process.exitCode = 1. This ensures structured error output in--jsonmode and consistent formatting in text mode. The global error handler insrc/index.tscatches all errors, formats them appropriately, and sets the exit code. The same handler also matchesBaseCliError(re-exported fromsrc/lib/errors.ts) so errors thrown by@doist/cli-corehelpers route through the same formatter.
import { CliError } from '../../lib/errors.js'
throw new CliError('ERROR_CODE', 'User-facing message', ['Optional hint'])- When adding a new error code, add it to the
ErrorCodetype insrc/lib/errors.tsunder the appropriate category. - Never use
process.exit(1)in command handlers. It terminates immediately without runningfinallyblocks, which leaves the early loading spinner stuck in the terminal. Usethrow new CliError(...)instead — this lets the process exit cleanly after spinner cleanup.
Lefthook runs type-check, oxlint, and oxfmt on pre-commit, tests on pre-push.
The file src/lib/skills/content.ts exports SKILL_CONTENT — a comprehensive command reference that gets installed into AI agent skill directories via tdc skill install. This is the source of truth that agents use to understand available CLI commands.
Whenever commands, subcommands, flags, or options are added, updated, or removed in src/commands/, the SKILL_CONTENT in src/lib/skills/content.ts must be updated to match. This includes:
- Adding new commands or subcommands with usage examples
- Adding, removing, or renaming flags and options
- Updating the Quick Reference section when new top-level commands are introduced
- Keeping examples accurate and consistent with actual CLI behavior
After updating SKILL_CONTENT:
- Run
npm run build && npm run sync:skillto regenerateskills/comms-cli/SKILL.md(the standalone skill file used bynpx skills add) - Run
tdc skill update claude-code(and any other installed agents) to propagate changes to installed skill files
A CI check (npm run check:skill-sync) runs on pull requests and will fail if skills/comms-cli/SKILL.md is out of sync with content.ts.