@anna-ai/cli
Anna App developer CLI — scaffold, validate, run a local dev harness, drive
fixture-based regression tests, and program against the harness from
vitest/pytest. Phases 2–9 (MVP) are done.
Install
End-user install (after first npm publish):
npm i -g @anna-ai/cli
anna-app --help
The dev and fixture replay commands transparently fetch the pinned Python
runtime via uvx (no matrix-nexus checkout required). Install
uv once:
curl -LsSf https://astral.sh/uv/install.sh | sh
Local dev (this repo):
pnpm install
pnpm build
node dist/cli.js --help
Commands
anna-app init <dir> [--slug <slug>] [--template minimal] [--force]
Scaffolds a new Anna App project (manifest + bundle + a stdio plugin
template). __TOOL_ID__ becomes tool-dev-<slug>.
anna-app init my-app --slug my-app
cd my-app
anna-app validate
anna-app validate [--manifest manifest.json] [--bundle bundle] [--strict]
Layered fail-fast checks: legacy normalization (a schema 1–3 manifest is
folded to schema 4 and its retired fields — permissions,
host_capabilities, the ui.host_api method lists — are reported as a
warning) → JSON Schema → ui static → cross-file tool_id linter (with
Levenshtein-1 typo detection) → browser-feature usage vs
ui.browser_permissions (warning: e.g. getUserMedia without
"microphone" is denied by the Dashboard's Permissions-Policy — forum #395)
→ --strict allowed_tools literals vs ui.host_api.agent.tools.
There is no method-level Host API allow-list: installing an App is the
user's consent, and every anna.<ns>.<method> is available to an installed
App. A manifest only declares the three capabilities that involve a second
principal:
| declaration | meaning |
|---|---|
ui.host_api.credentials: ["google"] | third-party accounts the App may read via credentials.* |
ui.host_api.agent.tools: ["fs_read_file"] | host-kit tools (fs_* / exec_* / browser_*) a sandbox agent session may use; ["*"] = all |
storage.scopes: {"user": "rw"} | cross-App data (user / tool / another App's app bucket); the App's own app scope needs no declaration |
anna-app dev [--manifest …] [--bundle …] [--port 5180] [--matrix-nexus-root <path>] [--executa <spec>…]
Boots the local harness:
- Spawns the Python
anna-app-bridge(production dispatcher reused viaWindowStoreProtocol). - Serves a mock dashboard at
http://localhost:<port>/. - Auto-discovers
<manifest-dir>/executas/<name>/plugins (Python / Node.js / Go / pre-built binary) and registers them in the in-processExecutaPool. See "Multi-language executas" below. - Hot-reloads the bundle on disk changes (use
--no-watchto disable). - Mocks the realtime voice session (
anna.audio.session, SDK ≥ 0.21.0) in the dashboard page: ASR via the browser'sSpeechRecognition(Chrome), the LLM turn via the localagent.session, TTS viaspeechSynthesis(notts_audioframes — the app's player stays idle). Events /setContext/say/ barge-in /end()follow the production protocol. - Applies the same Permissions-Policy gates as production: the iframe
allowattribute and the bundlePermissions-Policyheader both come from the runtime (ui.browser_permissions∪ impliedaudio: ["session"]⇒microphone∪frame-src⇒ playback). An app that records without declaring"microphone"fails locally exactly like it fails on the Dashboard —validatetells you why. - Mirrors the host theme / locale (SDK ≥ 0.24.0): the iframe URL carries
?theme=&themeMode=&lang=and the titlebar ☀/🌙 and EN/中 buttons broadcasttheme.changed/locale.changedevents, soanna.onThemeChange()/anna.onLocaleChange()can be exercised without a production host. - Forwards
agent.session.runargs verbatim to the nexus account, so the optional per-runmaxTokens(dispatcher ≥ 0.30.0 / SDK ≥ 0.26.0) and the additiverun_meta.max_output_tokens/task_complete.{stop_reason,truncated}frames behave exactly as in production.
Multi-language executas
Each subdirectory of <manifest-dir>/executas/ is launched according to
the first sentinel that matches:
| # | Sentinel | Type | Default launch |
|---|---|---|---|
| 0 | executa.json | (any) | command field, else type-specific default below |
| 1 | pyproject.toml | python | uv run --project <dir> <tool_id> |
| 2 | package.json | node | node <bin[tool_id] | bin | main | module> |
| 3 | go.mod (alone) | go | requires executa.json declaring type: "go" |
| 4 | bin/<dirname> exec | binary | runs the executable directly |
executa.json (recommended for clarity, required for Go and pre-built
binary tools):
{
"tool_id": "tool-yourhandle-foo-abcd1234",
"type": "python" | "node" | "go" | "binary",
"command": ["…"], // optional; full override of type defaults
"enabled": true // optional; default true. Set false to skip
// (useful when shipping multiple language
// flavours of the same tool_id).
}
The --executa <spec> flag (repeatable) registers an out-of-tree
executa, or fully overrides auto-discovery for the run. Spec syntax:
# Auto-detect from the dir (same rules as in-tree discovery):
anna-app dev --executa dir=./vendor/external-tool
# Force type when there's no executa.json:
anna-app dev --executa dir=./executas/foo,type=go
# Fully explicit:
anna-app dev --executa dir=./executas/foo,tool_id=tool-h-foo-12345678,command="node plugin.js"
For the full discovery / executa.json reference, see
anna-executa-examples/docs/multi-language-anna-apps.md.
Two runtime modes (auto-selected):
| Mode | When | Command |
|---|---|---|
uvx (default) | No matrix-nexus checkout nearby | uvx --from anna-app-runtime-local@<pinned> anna-app-bridge |
nexus-source | --matrix-nexus-root set, $ANNA_NEXUS_ROOT set, or running from inside a checkout | uv run --project <root> python -m anna_app_runtime_local.bridge |
The pinned wheel version is exported as PINNED_RUNTIME_VERSION from
src/harness/bridge.ts — bump in lock-step with
packages/VERSIONS.md in matrix-nexus.
anna-app doctor [--matrix-nexus-root <path>]
Environment sanity check: uv on PATH, uvx cache state for the pinned
runtime, optional matrix-nexus checkout, dev signing key permissions.
Exit 0 if all required checks pass.
anna-app fixture verify <file.jsonl>
Strict syntactic + semantic validation of a recorded fixture (envelope ordering, response⇄request pairing).
anna-app fixture summarize <file.jsonl>
Pretty stats: counts per (ns, method), error breakdown, event totals.
anna-app fixture replay <file.jsonl> [--manifest …]
Dry-run replay (MVP): re-executes the recorded request sequence against
a fresh LocalDispatcherSession and diffs each response. Live executa
re-invocation is deferred to a future minor.
Programmatic harness (vitest)
import { mountBundle } from "@anna-ai/cli/test";
const h = await mountBundle({
manifest: "./examples/hello/manifest.json",
bundle: "./examples/hello/bundle",
});
await h.call("storage", "set", { key: "k", value: 1 });
const events = h.drainEvents();
await h.close();
See src/test/index.ts for the full surface. Calls are
checked against the manifest's capability declarations exactly like
production (tools.invoke → declared executas, non-self storage scopes →
storage.scopes, credentials.* → ui.host_api.credentials); there is no
method-level ACL.
Pytest plugin (anna-executa-test)
For executa authors. Lives in matrix-nexus at
packages/anna-executa-test/. Provides executa_session /
executa_invoke fixtures — see that package's README.
Schema bundle
@anna-ai/app-schema
is a regular npm dependency. Set ANNA_APP_SCHEMA_DIR to point at a
local checkout (e.g. matrix-nexus/packages/anna-app-schema/) to
iterate on the protocol without publishing.
Browser SDK
End-user app bundles load AnnaAppRuntime from
/static/anna-apps/_sdk/<version>/index.js. Both the matrix-nexus
production server and this CLI's harness serve that URL from the
public npm package @anna-ai/app-runtime,
which is declared as a normal dependency. No vendored copy, no sync
step.
Persistent storage (APS)
Anna 1.2+ exposes a per-user JSON-RPC storage surface under the
storage/* namespace. Every installed Executa receives a storage_token
for its own tool scope — install is consent, nothing to grant. To also
touch the user-wide user scope:
- Declare it in the plugin's describe manifest — structured
"storage": {"scopes": {"user": "rw"}}(or the legacyhost_capabilities: ["aps.scope.user.read", "aps.scope.user.write"]strings;aps.kv/aps.filesonly ever meant the self scope). - Negotiate
client_capabilities.storage = {}duringinitialize.
anna-app executa dev --storage real mints tokens with exactly these
scopes (tool + declared), mirroring the platform minter.
See the protocol & best-practice guide at
anna-executa-examples/docs/persistent-storage.md
for wire format, error codes, and a worked OCR-cache example. The
local dev harness mocks APS by default so end-to-end tests do not
need network access.
Roadmap
| Phase | Status | Scope |
|---|---|---|
2 — init + validate | ✅ MVP | Schema/ACL checks |
3 — dev harness | ✅ MVP | Stdio bridge, dashboard, hot-reload |
4 — @anna-ai/cli/test | ✅ MVP | Programmatic mountBundle |
5 — anna-executa-test (pytest) | ✅ MVP | Plugin shipped from matrix-nexus |
6 — fixture {verify,summarize,replay} | ✅ MVP (dry-run replay) | Live re-invoke deferred |
| 7 — Publish & distribution | ✅ done | uvx default + full [email protected] extraction; end-users no longer need a matrix-nexus checkout |
8 — manifest.dev block + init template + 4 docs | ✅ done | See matrix-nexus/docs/developers/apps/: local-dev.md, testing-bundle.md, testing-plugin.md, recording-replay.md |
| 9 — Reference example upgraded | ✅ done | anna-executa-examples/examples/anna-app-focus-flow ships tests/{bundle,plugin}/, fixtures, and .github/workflows/anna-app.yml |
Pinned versions
PINNED_RUNTIME_VERSION = "0.2.0a1"(src/harness/bridge.ts)- See
matrix-nexus/packages/VERSIONS.mdfor the full toolchain matrix.
Tests
pnpm test