@bricnbrac-purl/cli
v0.1.2
Published
Purl Atelier Map capture CLI for Codex and Claude Code
Maintainers
Readme
@bricnbrac-purl/cli
The purl CLI connects a local Git repository to Purl's Atelier Map. It replaces the developer-local
Python bootstrap in tools/atelier_capture/ with a distributable,
agent-neutral bridge that captures both Codex and Claude Code activity.
The public npm package connects to https://atelierapi.bricnbrac.com by default.
Verified on macOS (this development machine): packed-tarball install, purl connect <code>,
purl connect merging into a repository with pre-existing Codex/Claude Code hooks (unrelated
handlers preserved byte-for-byte), both providers' hooks firing for real Claude Code activity,
MapBootstrap ingestion, offline hook capture + purl flush recovery, and purl disconnect
(hooks removed with backups, backend installation revoked, OS Keychain entry deleted). Windows has
not yet been run on real hardware - see the handoff's Windows validation note before shipping.
Install from npm
Install globally, then connect from the Git repository you want Purl to capture:
npm install --global @bricnbrac-purl/cli
cd /path/to/your/repository
purl connectOr run it without a global installation:
cd /path/to/your/repository
npx @bricnbrac-purl/cli connectIf Purl Web gave you a pairing code, append it to either command, for example:
purl connect ABCD-1234
# or
npx @bricnbrac-purl/cli connect ABCD-1234Build a local package
cd cli
npm install
npm run build
npm test
npm run type-check
npm packThen, from a target repository:
npm install /path/to/bricnbrac-purl-cli-0.1.0.tgz --prefix ./some-local-install
./some-local-install/node_modules/.bin/purl connectConfiguration
| Variable | Default | Purpose |
|---|---|---|
| PURL_API_URL | https://atelierapi.bricnbrac.com | Base URL the pairing client and uploader talk to. Set http://localhost:8080 explicitly for local backend development. |
purl connect accepts --insecure-file-store as an explicit opt-in fallback: if the OS credential
store (macOS Keychain / Windows Credential Manager / Linux Secret Service) is unavailable, the CLI
fails with a recovery message unless this flag is passed, in which case the capture token is written
to a single 0600-permission file under the Purl data directory instead. There is no silent
plaintext fallback. The stable hook runner carries the current platform's native keyring package,
so background capture uses the same OS credential store without requiring a file copy.
Commands
purl connect [--agent codex,claude-code] [--insecure-file-store]
# start pairing, open the browser
purl connect ABCD-1234 # consume a short-lived one-time code shown by Purl Web
purl status # show paired project, providers, last capture activity
purl disconnect # remove Purl's hooks and local credentials for this repo
purl doctor # diagnose missing credentials, stale hooks, backend reachability
purl flush [--root <path>] # retry the local spool now
purl map init [--agent claude-code|codex] [--timeout-ms N] [--max-budget-usd N]
# drive a headless, read-only Claude Code or Codex session to deeply
# index the repo and replace the shallow bootstrap feature tree (below)
purl help # same as --help/-h
purl version # same as --version/-v
purl atelier hook --provider <codex|claude-code>
# internal: invoked BY agent hooks, not by a human
purl atelier mcp # internal: invoked BY agent hooks as an MCP serverpurl map init
purl connect's initial project-view feature tree is built from a single shallow LLM call over a
static snapshot (file tree, manifests, a handful of file excerpts) - it never reads git history or
real code, so it tends to be generic on anything but a trivial repo. purl map init fixes this at
the source: it shells out to a headless, read-only coding-agent session that actually explores
the codebase and cross-references real routing/registration code before naming a capability, then
uploads the resulting feature tree through the same event pipeline purl connect uses.
Two providers are supported, selected with --agent (defaults to claude-code). Both pin a
specific model and medium reasoning effort rather than deferring to whatever the user's local CLI
default happens to be, so cost and output quality stay predictable across machines:
claude-code(default) -claude -p, modelclaude-haiku-4-5-20251001(the full id, not thehaikualias - see the module docstring incli/src/agents/claude-code/headless-index.tsfor why),--effort medium, restricted to theRead/Grep/Globtools only, with a hard dollar cap via--max-budget-usd(default $2).codex-codex exec, modelgpt-5.6-terrawith-c model_reasoning_effort="medium", run inside Codex's--sandbox read-only(it explores via shell commands likecat/grep/find, but any write/mutating command is sandboxed to fail rather than executing).execis already non-interactive so there's no approval prompt to hang on. Codex'sexechas no dollar-budget flag equivalent to Claude Code's - only--timeout-msbounds a Codex run.
Both providers can never edit or run anything mutating in your repo - read-only is enforced by the agent's own tool/sandbox mechanism, not just by prompt instructions.
Prerequisites:
- Run
purl connectfirst -map initaugments an existing pairing, it doesn't replace it. - The chosen provider's CLI must be installed and already authenticated on your
PATH(claude --version/codex --versionshould work, andcodex loginfor Codex).purl map initchecks this up front and fails with an actionable message if not.
This is a foreground command you actively wait on - it can take a few minutes on a large repository
and spends real agent usage. It fails loudly on any error (missing binary, timeout, unparseable
output) rather than silently falling back to the shallow tree, so a real problem is never masked. If
indexing succeeds but the upload fails, the result is safely queued in the local spool - retry with
purl flush.
Adding a third provider means implementing the HeadlessIndexer interface
(cli/src/agents/headless-indexer.ts) in a new cli/src/agents/<provider>/headless-index.ts and
registering it in cli/src/commands/map-init.ts - the shared JSON schema
(cli/src/agents/feature-tree-schema.ts), prompt builder (cli/src/agents/indexing-prompt.ts),
loose JSON parser (cli/src/agents/json-envelope.ts), and subprocess runner
(cli/src/agents/subprocess.ts) are provider-agnostic and should be reused as-is.
Re-running purl connect
Running purl connect (no pairing code) in a repository that's already connected skips the
browser/device-code flow entirely - it reuses the stored capture token and just re-registers hooks
and the MCP server (already idempotent no-ops when nothing's missing, so this doubles as a repair
if something got deleted locally). What's printed differs by whether the CLI package itself
changed since the last connect here:
- same version -
This repository is already connected to "<project>" - no need to run "purl connect" again here. - different version -
purl CLI updated (0.1.0 -> 0.2.0) - re-synced hooks and the MCP server for "<project>" in <repoRoot>.
Passing an explicit pairing code (purl connect ABCD-1234) always runs the full pairing exchange
regardless of local state - that's the escape hatch for re-pairing to a different project or after
purl disconnect. The reported CLI version is also sent to the backend and shown per device in
Purl Web's Connected Devices panel.
MCP server (purl atelier mcp)
purl connect also registers a local MCP server, purl-atelier-map, with each connected agent
(Claude Code: ~/.claude.json's per-repo mcpServers; Codex: ~/.codex/config.toml's
[mcp_servers.purl-atelier-map]). It exposes three tools an agent can call during a session -
report_decision, report_activity, report_verification - that feed the backend's distillation
step as a labeled, advisory hint alongside the passively-captured file/tool evidence; nothing
reported through it is ever auto-trusted over what actually changed in the repo.
To make an agent aware the server exists without editing any file in your repository (no
CLAUDE.md/AGENTS.md changes), the SessionStart hook emits one hookSpecificOutput.additionalContext
JSON line on stdout - the same mechanism both Claude Code and Codex use to inject text into a
session's context from a hook - naming the server and its three tools as available, optional
capabilities. This only fires once per session, and only when Purl's MCP server is actually
registered for the repo; every other hook event still writes nothing to stdout, matching the
capture pipeline's normal silent behavior. See src/agents/nudge.ts for the exact wording and
src/commands/hook.ts for where it's gated. It's reversible the same way the rest of a purl
connect install is - purl disconnect removes the MCP registration along with the hooks, so the
nudge stops firing.
Architecture
Codex native event -------\
> provider adapter -> canonical local event -> spool -> uploader
Claude Code native event -/src/capture/types.ts- the canonical event model andProviderAdapterinterface shared by both providers.src/agents/types.ts- theAgentIntegrationinterface each provider's hook-merge module implements (install/uninstall/isInstalled), plus the pre-resolved runner-command context the CLI core hands it.src/agents/codex/- Codex.codex/hooks.jsonadapter and merge logic, plusheadless-index.ts(codex exec) forpurl map init --agent codex.src/agents/claude-code/- Claude Code.claude/settings.local.jsonadapter and merge logic, plusheadless-index.ts(claude -p) forpurl map init(the default agent).src/agents/headless-indexer.ts,feature-tree-schema.ts,indexing-prompt.ts,json-envelope.ts,subprocess.ts- the provider-neutralHeadlessIndexercontract and the schema/prompt/parsing/process-running logic bothmap initproviders share.src/pairing/- HTTP client for the backend's device-pairing endpoints (backend/APIs.md"Device pairing" section).src/storage/- OS credential store (@napi-rs/keyring), local installation metadata under the platform Purl data directory (paths.tsresolves it per-OS;PURL_CLI_DATA_DIRoverrides it for tests only), and the stable per-user runner copy hooks invoke (runner.ts).src/capture/- spool (spool.ts), uploader (uploader.ts), redaction/capping (redact.ts), git-backed baseline/workspace-delta/bootstrap helpers (git.ts,diff.ts) - all ported fromtools/atelier_capture/atelier_capture.py.src/commands/- one module per CLI command.src/util/- Git-root resolution, device label/fingerprint, browser opening, and the hook-only diagnostic log file.
Security
- The long-lived capture token never appears in a command, shell history, hook JSON, browser
storage, or log line. It is exchanged once during pairing and stored only in the OS credential
store (
@napi-rs/keyring; macOS Keychain / Windows Credential Manager / Linux Secret Service). - A bare reconnect validates that token with the API before changing hooks. A revoked token starts fresh browser approval; a network/server error fails clearly and is never reported as success.
- Hooks invoke a stable local runner (a per-user copy of the bundled CLI installed at
connecttime), nevernpxor an ephemeral cache path. - Hook installation always backs up the existing config, writes atomically, validates the result, and preserves every unrelated handler.
See ../backend/APIs.md for the pairing/ingestion HTTP contract and ../tools/atelier_capture/README.md for the retention and redaction policy this CLI preserves.
