audacity-cli
v0.1.0
Published
Audacity Claude-Code-style coding-agent CLI (talks only to Audacity infra)
Readme
audacity
A local, Claude-Code-style CLI coding agent whose "brain" is your own Audacity
infrastructure — never an external Claude/OpenAI endpoint. It talks only to the
Audacity web host's OpenAI-compatible chat endpoints, authenticated with a personal
audacity_api_… key.
audacity # interactive REPL
audacity "fix the failing test" # run one prompt to completion, then exitHow it works
- Phase 1 —
/rag false(default): a full coding agent. Each turn streams fromPOST {AUDACITY_API_URL}/v1/chat/completions, and the model can call local tools (read_file,list_files,grep,edit_file,write_file,run_bash). The conversation is held in memory and resent on every call (the endpoint is stateless). - Phase 2 —
/rag true(coming soon): routes toPOST {AUDACITY_API_URL}/v1/agent/chat/completions, the Mastra RAG agent (server-side memory + company/fund/doc tools). Same key, same wire format. The/rag trueslash command already toggles session state; sending in RAG mode currently prints a "Phase 2" notice.
All transport/auth lives in src/model.ts (the only integration seam). The public web
host handles the litellm-proxy JWT and Cloud Run IAM downstream — the CLI only ever
sends your audacity_api_… key. No gcloud, no service token.
Setup
bun install
bun run build # → dist/index.js (executable, with shebang)Run it:
bun run dev # dev (bun --watch src/index.ts)
node dist/index.js # after buildInstall globally (recommended)
cd apps/audacity
npm install -g . # builds dist/ via the prepare hook, then links `audacity` onto your PATH
audacity # now works from any directorynpm install -g . runs the prepare script (tsup), so the self-contained
dist/index.js is built at install time — no separate build step needed. (bun link
or npm link also work for local development.)
On first run, audacity prompts for your production key and a model, then saves
them to ~/.audacity/config.json — so it's remembered across every session, shell,
and terminal on the machine. You only enter the key once.
To unregister the local global install later:
npm uninstall -g audacity-cli # removes the `audacity` command from your PATHPublishing to npm
The package is self-contained — no workspace:* deps, only commander at runtime —
so it publishes straight from apps/audacity with no monorepo build orchestration.
You don't need to build first. The
preparelifecycle script (tsup && chmod +x) runs automatically onnpm publish, so the shippeddist/index.jsis always freshly built from the currentsrc/.
One-time setup:
npm login # authenticate to the npm registryPublish:
cd apps/audacity
npm publish --dry-run # inspect the tarball WITHOUT publishing — do this first
npm publish # ships audacity-cli@<version> to the public registryThe dry run prints exactly what ships. Expect only three files —
dist/index.js, package.json, README.md (controlled by the files: ["dist"]
whitelist; README.md is always included automatically). No src/, no tests.
After publishing, anyone can install it:
npm install -g audacity-cli
audacityName note: the package name is
audacity-cli(the bareaudacityname is taken on npm). If you ever want to namespace it under the org instead, rename to a scoped name like@audacity-investments/cliinpackage.jsonand publish withnpm publish --access public(scoped packages default to private otherwise).
Updating the published package
You can never republish the same version number, and unpublishing is restricted (only within 72h). The fix for any bad release is always to roll forward — publish a higher version. Never delete; just bump.
cd apps/audacity
npm version patch # 0.1.0 → 0.1.1 (bug fixes)
# npm version minor # 0.1.1 → 0.2.0 (new features, backwards-compatible)
# npm version major # 0.2.0 → 1.0.0 (breaking changes)
npm publishnpm version bumps package.json and creates a git commit + tag. Then npm publish
rebuilds via prepare and ships the new version. Users upgrade with:
npm install -g audacity-cli@latestWhile the version stays under 1.0.0, semver convention treats every release as
potentially breaking — fine for early development. Cut 1.0.0 once the wire format and
CLI surface are stable.
Configuration
audacity is self-contained: the only source of configuration is
~/.audacity/config.json. It depends on no environment variables and nothing else on
the machine — a key entered once is remembered globally on that device, across every
shell, directory, and session.
On first interactive run, audacity prompts for your production key (pasted, not
echoed) and a model, defaults the base URL to production
(https://portal.audacityinvestments.com), and auto-saves to
~/.audacity/config.json (chmod 600) — no "save?" prompt. Requests go to the public
prod host, which routes them through the production litellm-proxy downstream. Run /key
anytime to change the saved key. One-shot mode (audacity "…") never prompts — it reads
the saved config and errors clearly if no key is saved.
Optional field: compactModel — the model /compact uses to summarize. Summarizing
is an easy task, so it defaults to a cheap, fast model (gemini-2.5-flash) independent of
your chat model; set compactModel to override it.
{
"apiUrl": "https://portal.audacityinvestments.com",
"apiKey": "audacity_api_…",
"model": "claude-opus-4-8",
"compactModel": "gemini-2.5-flash"
}REPL commands
| Command | Action |
|---------|--------|
| /model [id] | Pick a model (no arg → numbered picker; sourced from apps/litellm/litellm_config.yaml) |
| /rag true\|false | Toggle RAG mode for the session (Phase 2 for true) |
| /auto | Toggle auto-approve for mutating tools |
| /key | Re-enter and save your API key |
| /cwd | Show the working directory |
| /compact | Summarize older turns into one message, keep the last 2 verbatim (shrinks context) |
| /clear | Reset the conversation |
| /exit | Quit |
Permissions
read_file, list_files, grep run without prompting. edit_file, write_file,
run_bash ask for y/N approval first (showing exactly what will run) — bypass with
the --yes/-y flag or the /auto toggle. In one-shot mode, mutating tools require
-y.
Testing prompt caching
The harness is a realistic way to exercise OpenAI prompt caching: it resends
the whole conversation on every turn (the endpoint is stateless), so a large,
stable prefix repeats call after call — exactly what the provider cache rewards.
Cheaper cached input tokens are captured server-side as
litellm_usage_log.cached_input_tokens (see apps/litellm-proxy/README.md for
the full metering/verification story).
Prerequisites
- Point at an environment where the capture is deployed. Prompt-cache
metering lives on the UAT proxy first. Set
~/.audacity/config.json:{ "apiUrl": "https://web-uat-qpe5npob6a-uk.a.run.app", "apiKey": "audacity_api_…your UAT key…", "model": "gpt-5.5" } - Use an OpenAI model (
gpt-5.5,gpt-5.4, …). OpenAI caches automatically; other providers need explicit cache markers that aren't wired yet.
⚠️ Cache population is asynchronous
The cache isn't guaranteed warm on the very next turn — an early turn can still
report cached_tokens = 0, and hits land reliably only after the prefix has been
seen a few times. Run several turns in one session and don't /clear.
Good sessions to trigger hits
Load a large file early (so the prefix crosses OpenAI's ~1024-token floor), then ask follow-ups that reuse that context:
read apps/litellm-proxy/src/routes/api/chat/completions/index.ts
walk me through the streaming success path
where exactly is user cost computed vs fmv cost?
now explain the non-streaming path and how it differsor a multi-file exploration:
read src/agent.ts, src/model.ts, and src/tools.ts
explain how a tool call flows from the model back into the loop
which tools require approval and where is that enforced?Bad cache tests (expect no cache): short one-liners with no file context,
/clear between questions, or jumping topics each turn.
Observe the hits
The harness streams and doesn't print usage, so watch the proxy logs
(the stream variant fires for harness traffic):
gcloud logging read 'resource.type="cloud_run_revision" AND resource.labels.service_name="litellm-proxy-uat" AND textPayload:"[DEV:prompt-cache]"' \
--project=audacity-uat --limit=10 --freshness=10m --format='value(textPayload)'Expect cached_input_tokens to climb turn over turn, e.g.
[DEV:prompt-cache] stream model=gpt-5.5 prompt_tokens=4450 cached_input_tokens=4224 CACHE HIT.
Test
bun test # tool-layer unit tests (no API key needed)
bun run typecheck