The CLI client for the AgentRoot protocol. Standalone, MIT-licensed. No publish pipeline yet: release strategy (registry, signing, provenance, cadence) is still being decided. Do not add a release workflow, a prepublishOnly hook, or any other publish-adjacent automation without explicit sign-off.
src/
index.ts Entry point: parseArgs + command router
cli/ Argument parsing, fatal exits, spinners, prompts
parse-args.ts POSIX argv parser (kebab/camel, --key=value, --, --no-X)
fatal.ts Single error sink with JSON envelope + sysexits codes
exit-codes.ts Sysexits-style EXIT constants + exitCodeName()
colors.ts picocolors wrapper, honors NO_COLOR + --no-color + TTY
streams.ts stderr-routing helpers (note, comment) + --quiet wiring
env-preamble.ts Mutates process.env before picocolors initialises
spinner.ts nanospinner wrapper that respects quiet + non-TTY
suggest.ts Levenshtein "Did you mean?" for unknown commands
confirm.ts Inquirer-backed yes/no with --yes / CI auto-confirm
commands/ One file per CLI command (thin handlers only)
resolve.ts DNS lookup -> manifest fetch -> records
search.ts /api/records (primary) + legacy fallbacks
install.ts install / install-interactive helpers
list.ts read ~/.agentroot/installed.json
update.ts re-fetch + hash-compare an installed record
uninstall.ts remove an installed record + tool symlinks
init.ts scaffold .well-known/agentroot.json
validate.ts validateManifest against a local JSON file
config.ts ~/.agentroot/config.json get/set/unset
health.ts /api/health reader (status + db)
manifests.ts /api/manifests paginated browser
collections.ts /api/collections list + detail
submit.ts POST /api/submit + DNS probe + TXT instructions
version.ts version subcommand + printShortVersion()
completion.ts Static bash/zsh/fish/pwsh completion scripts
help.ts Per-command help pages (15 commands + helpCompletion in completion.ts)
constants/ Module-level lookup tables (RECORD_TYPES, etc.)
services/ Stateful work, grouped by concern
config/ ~/.agentroot/config.json read/write
dns/ DNS TXT resolution + manifest URL parsing
http/ fetch wrapper + User-Agent + postJSON (for submit)
install/ Skill / MCP / agent / a2a install pipeline
types/ Shared TypeScript interfaces
utils/ Pure helpers (formatRecord, hashContent)
bin/agentroot.ts Shebang entry compiled to dist/bin/agentroot.js
tests/ vitest unit tests (mirror src/ layout)
docs/screenshots/ README screenshots (generated by scripts/)
scripts/ Build-time tooling (screenshots, etc.)
.planning/ Local-only working notes (gitignored)
vendor/ Workaround tarball for stale npm core (gitignored)
commands/search.ts calls /api/records first (the same paginated
endpoint the web UI uses). Earlier versions called /api/discover and
looked for data.results, a field that does not exist in that
endpoint's response shape, the bug silently dropped every agent/MCP/A2A
hit. The legacy searchWithFallback chain (find-skills -> manifest
probe by TLD) is preserved but only fires when page 1 of /api/records
returns zero rows. --json now returns the full envelope
(results, total, page, pages, limit) instead of a bare array.
Files: keep under 300 lines; if a file grows past that, split per responsibility.
Imports, then types, then constants, then exports, then private helpers. Constants and lookup tables go at module top, not buried inside functions. Functions read top-down: callers above callees.
Conditionals: if/else-if chains over 2 branches should be replaced with a Record<key, handler> map or strategy dispatch. See commands/install.ts (RECORD_TYPE_HANDLERS) for the canonical pattern.
const by default, let only when mutation is required. No var. One variable per declaration (no let a = 1, b = 2).
No any. Use unknown + type guards, or a real interface. as <Type> casts should be rare; prefer runtime checks.
Document every empty catch {} with a 2 or 3 line comment explaining what condition is being swallowed and why that's the right behavior.
Behavior parity with the monorepo CLI. The standalone is a 1:1 functional copy of packages/cli/ from d3-inc/agentroot. Output must match byte-for-byte (modulo timestamps).
Commits are authored by the human who wrote them. Do NOT add Co-Authored-By: trailers for Claude, Copilot, Cursor, ChatGPT, or any other AI assistant. Do NOT add "🤖 Generated with..." footers. The developer or contributor sending the PR is the sole author; AI tooling that helped is not co-credited. This applies to every commit and every PR, no exceptions. (DCO sign-off via git commit -s is still required and adds only the contributor's own Signed-off-by: line.)
Exit codes are sysexits. Every fatal() call must pass an EXIT.* constant from src/cli/exit-codes.ts (the single source of truth: OK=0, GENERIC=1, USAGE=2, NOINPUT=66, NOHOST=68, UNAVAILABLE=69, PROTOCOL=76, NOPERM=77, CONFIG=78). The default is GENERIC (1); pick the more specific code if it applies. JSON errors put the symbolic name in error.code via exitCodeName(). Document new exit codes in per-command help pages and in the README's "Exit codes" table.
Streams: stdout is data, stderr is conversation. Spinners, comments, progress notes, prompts all go to stderr. Only pipeable output (results tables, JSON envelopes) goes to stdout. cmd --json | jq must work without 2>/dev/null. Use note() / comment() from src/cli/streams.ts for any non-data line; never raw console.log for chatter.
vitest + setup file at tests/setup.ts that silences console.log/console.error.
pnpm test # one-shot run
pnpm test:watch # re-run on save
pnpm test:coverage # html coverage report in ./coverage/Every PR that adds or changes runtime behavior must include at least one test that fails without the change. See CONTRIBUTING.md for carve-outs (docs:, chore(deps):, pure refactors).
pnpm install # frozen-lockfile by default in CI
pnpm build # tsc to dist/
pnpm type-check # tsc --noEmit
pnpm test # 45/45 must passSmoke test recipe lives in TESTING.md.
.planning/PERF-AUDIT.md(local-only): performance review + applied fixes. Read before adding new I/O code paths..planning/CODE-QUALITY-AUDIT.md(local-only): readability audit referencing Google TS Style Guide, Clean Code TypeScript, refactoring.guru. Documents 25 findings and which were applied vs. deferred..planning/audit-trail.md(local-only): per-task log of the original extraction work.
Whenever you change CLI surface, you must update docs in the same commit. Surface = anything a user sees or scripts against:
- A new command, flag, alias, or environment variable → add it to:
README.md(Commands table, Help section verbatim text, Usage subsection if it deserves one)src/index.tsshowHelp()(the help text users see)CLAUDE.mdProject layout if it's a new fileTESTING.mdsmoke recipe
- A change to command output (column count, JSON envelope, error messages) → regenerate the relevant screenshot:
The recipe is in
pnpm run screenshots
scripts/capture-screenshots.mjs. Each PNG is committed todocs/screenshots/. - A change to default behavior (e.g. switching the search backend) → call it out in
## [Unreleased]ofCHANGELOG.md. - A change to required Node version, supported tools, or platforms → update README's Install section and
package.jsonengines.
Rule of thumb: if a user could be surprised by your change without reading the diff, the README must reflect it.
A class of bug we've already shipped twice and reverted: changing a primary thing (package name, version, repo URL) and dragging related external references along without checking they resolve. Concrete examples:
- README badges that fetch from npm — broke when we renamed
agent-root→@agent-root/clibefore the scoped package was published. - README "Version & environment" sample that hardcoded
agent-root 0.2.0— went stale the momentpackage.json.versionbumped. - Issue-template
config.ymlcontact link to GitHub Discussions — pretended Discussions was live while it was disabled on the repo.
When you change any of:
package.jsonname / version / repository.url / bugs.url- The repo's GitHub URL (org or repo rename)
- Any feature toggle on GitHub (Discussions, Wiki, Pages)
- Any deployment URL
…then grep for every reference to the old value in README.md, CHANGELOG.md, .github/, and the rest of the docs, and verify each new reference resolves to a real thing (or document why it doesn't yet). For external URLs (badges, package pages, registry endpoints), either confirm the URL returns 200 or replace with a static value until the dependency is published.
Static badge fallback when an external service isn't ready yet:
[](https://github.com/owner/repo/releases)is always better than a dynamic badge that renders "not found".
- Version:
package.jsonversion. Read at runtime viasrc/services/http/package-info.ts. Never hardcode a version anywhere else. - API base URL:
@agent-root/core'sAPI_BASEconstant. User override via~/.agentroot/config.json api-urlorAGENTROOT_API_BASEenv var. Never hardcodeagentroot.ioin CLI source. - Record types:
src/constants/record-types.ts. UseRECORD_TYPESandlabelForTypeeverywhere; do not repeat the string literals. - Tool names:
claude,cursor,codex,gemini,agents. These come from@agent-root/coredetectTools(). Do not redefine the set in the CLI. - Skill dir paths:
@agent-root/coreresolveToolDir(). Do not hand-write~/.claude/skills/etc.
If you add a new repeated string (x appears in 3+ files), extract it to src/constants/.
When making changes:
- Read the file before editing it; preserve its existing structural pattern (imports, types, constants, exports, then private helpers).
- Run
pnpm build && pnpm testafter every batch of changes. Don't accumulate untested edits. - If you add a new command, mirror an existing one in
commands/(e.g.,commands/resolve.tsis a good template). - New helpers go in
cli/,utils/, or the appropriateservices/<concern>/folder, never incommands/. - The
vendor/agent-root-core-0.2.0.tgzis a local workaround because the published@agent-root/core@0.2.0is stale. When@agent-root/corenext publishes (with the manifest/install-state/records APIs), switchpackage.jsonback to a semver range andgit rmthe vendor tarball. - Don't add npm dependencies without a clear need. Current runtime deps:
@agent-root/core,@inquirer/prompts,nanospinner,picocolors. Dev deps:vitest,playwright,ansi-to-html,typescript,@types/node.