@anna-ai/cli 0.1.9 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -48,17 +48,62 @@ Layered fail-fast checks: JSON Schema → `ui` static → cross-file `tool_id`
48
48
  linter (with Levenshtein-1 typo detection) → `--strict` host_api ACL grep
49
49
  of bundle JS/TS.
50
50
 
51
- ### `anna-app dev [--manifest …] [--bundle …] [--port 5180] [--matrix-nexus-root <path>]`
51
+ ### `anna-app dev [--manifest …] [--bundle …] [--port 5180] [--matrix-nexus-root <path>] [--executa <spec>…]`
52
52
 
53
53
  Boots the local harness:
54
54
 
55
55
  - Spawns the Python `anna-app-bridge` (production dispatcher reused via
56
56
  `WindowStoreProtocol`).
57
57
  - Serves a mock dashboard at `http://localhost:<port>/`.
58
- - Auto-discovers `<manifest-dir>/executas/<name>/pyproject.toml` and
59
- registers them in the in-process `ExecutaPool`.
58
+ - Auto-discovers `<manifest-dir>/executas/<name>/` plugins (Python /
59
+ Node.js / Go / pre-built binary) and registers them in the in-process
60
+ `ExecutaPool`. See "Multi-language executas" below.
60
61
  - Hot-reloads the bundle on disk changes (use `--no-watch` to disable).
61
62
 
63
+ #### Multi-language executas
64
+
65
+ Each subdirectory of `<manifest-dir>/executas/` is launched according to
66
+ the first sentinel that matches:
67
+
68
+ | # | Sentinel | Type | Default launch |
69
+ | - | ----------------------- | -------- | ------------------------------------------------------------- |
70
+ | 0 | `executa.json` | (any) | `command` field, else type-specific default below |
71
+ | 1 | `pyproject.toml` | `python` | `uv run --project <dir> <tool_id>` |
72
+ | 2 | `package.json` | `node` | `node <bin[tool_id] \| bin \| main \| module>` |
73
+ | 3 | `go.mod` (alone) | `go` | requires `executa.json` declaring `type: "go"` |
74
+ | 4 | `bin/<dirname>` exec | `binary` | runs the executable directly |
75
+
76
+ `executa.json` (recommended for clarity, required for Go and pre-built
77
+ binary tools):
78
+
79
+ ```jsonc
80
+ {
81
+ "tool_id": "tool-yourhandle-foo-abcd1234",
82
+ "type": "python" | "node" | "go" | "binary",
83
+ "command": ["…"], // optional; full override of type defaults
84
+ "enabled": true // optional; default true. Set false to skip
85
+ // (useful when shipping multiple language
86
+ // flavours of the same tool_id).
87
+ }
88
+ ```
89
+
90
+ The `--executa <spec>` flag (repeatable) registers an out-of-tree
91
+ executa, or fully overrides auto-discovery for the run. Spec syntax:
92
+
93
+ ```bash
94
+ # Auto-detect from the dir (same rules as in-tree discovery):
95
+ anna-app dev --executa dir=./vendor/external-tool
96
+
97
+ # Force type when there's no executa.json:
98
+ anna-app dev --executa dir=./executas/foo,type=go
99
+
100
+ # Fully explicit:
101
+ anna-app dev --executa dir=./executas/foo,tool_id=tool-h-foo-12345678,command="node plugin.js"
102
+ ```
103
+
104
+ For the full discovery / `executa.json` reference, see
105
+ [`anna-executa-examples/docs/multi-language-anna-apps.md`](https://github.com/openclaw/anna-executa-examples/blob/main/docs/multi-language-anna-apps.md).
106
+
62
107
  Two runtime modes (auto-selected):
63
108
 
64
109
  | Mode | When | Command |
@@ -130,6 +175,22 @@ public npm package [`@anna-ai/app-runtime`](https://www.npmjs.com/package/@anna-
130
175
  which is declared as a normal dependency. No vendored copy, no sync
131
176
  step.
132
177
 
178
+ ## Persistent storage (APS)
179
+
180
+ Anna 1.2+ exposes a per-user JSON-RPC storage surface under the
181
+ `storage/*` namespace. Plugins authored with this CLI can opt in by:
182
+
183
+ 1. Declaring `storage.user` (or `.app`/`.tool`) in the manifest's
184
+ `host_capabilities` array.
185
+ 2. Negotiating `client_capabilities.storage = {}` during `initialize`.
186
+ 3. Asking the user to grant storage in the Anna admin panel.
187
+
188
+ See the protocol & best-practice guide at
189
+ [anna-executa-examples/docs/persistent-storage.md](https://github.com/openclaw/anna-executa-examples/blob/main/docs/persistent-storage.md)
190
+ for wire format, error codes, and a worked OCR-cache example. The
191
+ local `dev` harness mocks APS by default so end-to-end tests do not
192
+ need network access.
193
+
133
194
  ## Roadmap
134
195
 
135
196
  | Phase | Status | Scope |
@@ -0,0 +1,44 @@
1
+ import { getAccount } from "./credentials-BTv2IfUZ.js";
2
+ import { listDevApps } from "./dev-app-cache-BMfOlTHd.js";
3
+ import { bold, cyan, dim, green, red, yellow } from "kleur/colors";
4
+
5
+ //#region src/commands/apps.ts
6
+ async function runAppsList(opts) {
7
+ const acc = getAccount(opts.account);
8
+ if (!acc) {
9
+ console.error(red("✗ no PAT on disk — run `anna-app login --host <nexus-url>` first."));
10
+ return 2;
11
+ }
12
+ let apps;
13
+ try {
14
+ apps = await listDevApps({
15
+ host: acc.host,
16
+ pat: acc.pat
17
+ });
18
+ } catch (e) {
19
+ console.error(red(`✗ ${e.message}`));
20
+ return 2;
21
+ }
22
+ if (opts.json) {
23
+ console.log(JSON.stringify({
24
+ host: acc.host,
25
+ apps
26
+ }, null, 2));
27
+ return 0;
28
+ }
29
+ console.log(bold(cyan("dev apps installed for")) + " " + cyan(acc.host));
30
+ if (apps.length === 0) {
31
+ console.log(dim(" (none — run `anna-app dev` in a project to register one)"));
32
+ return 0;
33
+ }
34
+ for (const a of apps) {
35
+ const tag = a.is_dev ? yellow("[dev]") : green("[prod]");
36
+ const enabled = a.is_enabled ? green("✓") : red("✗");
37
+ console.log(` ${tag} ${enabled} ${bold(a.slug)} ${dim(`(app_id=${a.app_id}, v=${a.installed_version})`)}`);
38
+ if (a.name && a.name !== a.slug) console.log(` ${dim(a.name)}`);
39
+ }
40
+ return 0;
41
+ }
42
+
43
+ //#endregion
44
+ export { runAppsList };
@@ -0,0 +1,3 @@
1
+ import { PINNED_RUNTIME_VERSION, PythonBridge } from "./bridge-BQUo6ehX.js";
2
+
3
+ export { PINNED_RUNTIME_VERSION, PythonBridge };
@@ -9,7 +9,7 @@ import { createInterface } from "node:readline";
9
9
  * `uvx <pkg>@<version>` so end users always run the dispatcher version
10
10
  * the CLI was tested against.
11
11
  */
12
- const PINNED_RUNTIME_VERSION = "0.2.0a1";
12
+ const PINNED_RUNTIME_VERSION = "0.2.0a2";
13
13
  var PythonBridge = class {
14
14
  proc = null;
15
15
  nextId = 1;
package/dist/cli.js CHANGED
@@ -444,10 +444,23 @@ program.command("validate").description("Run schema + ACL checks on a manifest+b
444
444
  const code = printResult(result);
445
445
  process.exit(code);
446
446
  });
447
- program.command("dev").description("Run a local harness (in-process dispatcher + iframe + SSE relay)").option("--manifest <path>", "manifest.json path", "manifest.json").option("--bundle <dir>", "bundle directory (default: ./bundle)").option("--slug <slug>", "App slug (overrides manifest.slug/name)").option("--view <name>", "View name to open (default: manifest default)").option("--matrix-nexus-root <path>", "matrix-nexus checkout (auto-detected if omitted; can also use $ANNA_NEXUS_ROOT)").option("--port <number>", "HTTP port", "5180").option("--user-id <id>", "Harness user_id", "1").option("--cwd <dir>", "Project root (default: cwd)").option("--no-watch", "Disable bundle file watcher (default: enabled)").action(async (opts) => {
448
- const { runDev } = await import("./dev-D-Tru6gP.js");
447
+ program.command("dev").description("Run a local harness (in-process dispatcher + iframe + SSE relay)").option("--manifest <path>", "manifest.json path", "manifest.json").option("--bundle <dir>", "bundle directory (default: ./bundle)").option("--slug <slug>", "App slug (overrides manifest.slug/name)").option("--view <name>", "View name to open (default: manifest default)").option("--matrix-nexus-root <path>", "matrix-nexus checkout (auto-detected if omitted; can also use $ANNA_NEXUS_ROOT)").option("--port <number>", "HTTP port", "5180").option("--user-id <id>", "Harness user_id", "1").option("--cwd <dir>", "Project root (default: cwd)").option("--no-watch", "Disable bundle file watcher (default: enabled)").option("--executa <spec>", "Explicit executa registration; repeatable. Spec: comma-separated key=value (dir=<path>[,tool_id=<id>][,type=python|node|go|binary][,command=\"<argv>\"]). When only `dir=` is given, the executa is auto-detected from executa.json / pyproject.toml / package.json / go.mod. Overrides directory auto-discovery under <manifest-dir>/executas/.", (val, prev) => prev ? [...prev, val] : [val]).option("--no-llm", "Disable LLM bridge (anna.llm/agent return llm_disabled)").option("--mock-llm <fixture>", "Serve canned LLM responses from a JSONL fixture").option("--llm-account <host>", "Saved account host to use (default: current)").option("--llm-app-slug <slug>", "Override the manifest slug used to register / look up the dev AnnaApp (default: manifest.slug)").action(async (opts) => {
448
+ const { runDev, parseExecutaSpec } = await import("./dev-DoY58pBM.js");
449
+ const cwd = opts.cwd ?? process.cwd();
450
+ let executas;
451
+ if (opts.executa && opts.executa.length > 0) {
452
+ executas = [];
453
+ for (const spec of opts.executa) {
454
+ const r = parseExecutaSpec(spec, cwd);
455
+ if (r instanceof Error) {
456
+ console.error(`✗ --executa: ${r.message}`);
457
+ process.exit(2);
458
+ }
459
+ executas.push(r);
460
+ }
461
+ }
449
462
  const code = await runDev({
450
- cwd: opts.cwd ?? process.cwd(),
463
+ cwd,
451
464
  manifestPath: opts.manifest,
452
465
  bundleDir: opts.bundle,
453
466
  slug: opts.slug,
@@ -455,13 +468,18 @@ program.command("dev").description("Run a local harness (in-process dispatcher +
455
468
  matrixNexusRoot: opts.matrixNexusRoot,
456
469
  port: Number.parseInt(opts.port, 10),
457
470
  userId: Number.parseInt(opts.userId, 10),
458
- noWatch: opts.watch === false
471
+ noWatch: opts.watch === false,
472
+ executas,
473
+ noLlm: opts.llm === false,
474
+ mockLlm: opts.mockLlm,
475
+ llmAccount: opts.llmAccount,
476
+ llmAppSlug: opts.llmAppSlug
459
477
  });
460
478
  process.exit(code);
461
479
  });
462
480
  const fixture = program.command("fixture").description("Inspect / replay harness recordings (Phase 6)");
463
481
  fixture.command("verify <file>").description("Schema + invariant checks on a harness JSONL recording").option("--json", "Emit machine-readable JSON", false).action(async (file, opts) => {
464
- const { runFixtureVerify } = await import("./fixture-BGjMtqWA.js");
482
+ const { runFixtureVerify } = await import("./fixture-BEu4LXLG.js");
465
483
  const code = await runFixtureVerify({
466
484
  file,
467
485
  json: opts.json
@@ -469,7 +487,7 @@ fixture.command("verify <file>").description("Schema + invariant checks on a har
469
487
  process.exit(code);
470
488
  });
471
489
  fixture.command("summarize <file>").description("Print a human-readable digest of a harness recording").option("--json", "Emit machine-readable JSON", false).action(async (file, opts) => {
472
- const { runFixtureSummarize } = await import("./fixture-BGjMtqWA.js");
490
+ const { runFixtureSummarize } = await import("./fixture-BEu4LXLG.js");
473
491
  const code = await runFixtureSummarize({
474
492
  file,
475
493
  json: opts.json
@@ -477,7 +495,7 @@ fixture.command("summarize <file>").description("Print a human-readable digest o
477
495
  process.exit(code);
478
496
  });
479
497
  fixture.command("replay <file>").description("Dry-run replay of a harness recording (Phase 6 MVP)").option("--manifest <path>", "manifest.json path", "manifest.json").action(async (file, opts) => {
480
- const { runFixtureReplay } = await import("./fixture-BGjMtqWA.js");
498
+ const { runFixtureReplay } = await import("./fixture-BEu4LXLG.js");
481
499
  const code = await runFixtureReplay({
482
500
  file,
483
501
  manifest: opts.manifest
@@ -485,10 +503,39 @@ fixture.command("replay <file>").description("Dry-run replay of a harness record
485
503
  process.exit(code);
486
504
  });
487
505
  program.command("doctor").description("Check environment for `anna-app dev` (uv, matrix-nexus, dev key)").option("--matrix-nexus-root <path>", "matrix-nexus checkout (optional)").action(async (opts) => {
488
- const { runDoctor } = await import("./doctor-BmR0POfL.js");
506
+ const { runDoctor } = await import("./doctor-DP2UB10l.js");
489
507
  const code = await runDoctor({ matrixNexusRoot: opts.matrixNexusRoot });
490
508
  process.exit(code);
491
509
  });
510
+ program.command("login").description("Device-flow login against a nexus host; saves a PAT to ~/.config/anna/credentials.json").requiredOption("--host <url>", "nexus base URL, e.g. https://nexus.example.com").option("--no-browser", "Do not open a browser window automatically", false).action(async (opts) => {
511
+ const { runLogin } = await import("./login-dl1Zfny8.js");
512
+ const code = await runLogin({
513
+ host: opts.host,
514
+ noBrowser: opts.browser === false
515
+ });
516
+ process.exit(code);
517
+ });
518
+ program.command("logout").description("Remove a saved PAT entry").option("--host <url>", "Account to remove (default: current)").option("--all", "Remove every saved account", false).action(async (opts) => {
519
+ const { runLogout } = await import("./logout-DablvlFs.js");
520
+ const code = await runLogout({
521
+ host: opts.host,
522
+ all: opts.all
523
+ });
524
+ process.exit(code);
525
+ });
526
+ program.command("whoami").description("Show the current account (and any others)").option("--json", "Emit machine-readable JSON", false).action(async (opts) => {
527
+ const { runWhoami } = await import("./whoami-giXOY415.js");
528
+ const code = await runWhoami({ json: opts.json });
529
+ process.exit(code);
530
+ });
531
+ program.command("apps:list").description("List dev apps installed for the current PAT").option("--account <host>", "Saved account host (default: current)").option("--json", "Emit machine-readable JSON", false).action(async (opts) => {
532
+ const { runAppsList } = await import("./apps-CDe6Fjq2.js");
533
+ const code = await runAppsList({
534
+ account: opts.account,
535
+ json: opts.json
536
+ });
537
+ process.exit(code);
538
+ });
492
539
  program.parseAsync(process.argv).catch((e) => {
493
540
  console.error(e);
494
541
  process.exit(2);
@@ -0,0 +1,122 @@
1
+ import { createRequire } from "module";
2
+ import { dirname, join } from "node:path";
3
+ import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+
6
+ //#region rolldown:runtime
7
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
8
+
9
+ //#endregion
10
+ //#region src/credentials.ts
11
+ /** Resolve credentials file path. Honours $XDG_CONFIG_HOME. */
12
+ function credentialsPath() {
13
+ const xdg = process.env.XDG_CONFIG_HOME;
14
+ const base = xdg && xdg.length > 0 ? xdg : join(homedir(), ".config");
15
+ return join(base, "anna", "credentials.json");
16
+ }
17
+ /** Normalise a host URL to its canonical key (https://x.example.com). */
18
+ function canonicalHost(input) {
19
+ let url;
20
+ try {
21
+ url = new URL(input);
22
+ } catch {
23
+ url = new URL(`https://${input}`);
24
+ }
25
+ const port = url.port ? `:${url.port}` : "";
26
+ return `${url.protocol}//${url.hostname}${port}`;
27
+ }
28
+ /** Read credentials file (returns empty container if missing or unreadable). */
29
+ function readCredentials() {
30
+ const path = credentialsPath();
31
+ if (!existsSync(path)) return {
32
+ version: 1,
33
+ current: null,
34
+ accounts: {}
35
+ };
36
+ try {
37
+ const raw = readFileSync(path, "utf8");
38
+ const parsed = JSON.parse(raw);
39
+ return {
40
+ version: 1,
41
+ current: parsed.current ?? null,
42
+ accounts: parsed.accounts ?? {}
43
+ };
44
+ } catch {
45
+ return {
46
+ version: 1,
47
+ current: null,
48
+ accounts: {}
49
+ };
50
+ }
51
+ }
52
+ /** Atomically write credentials file (chmod 600). */
53
+ function writeCredentials(data) {
54
+ const path = credentialsPath();
55
+ mkdirSync(dirname(path), {
56
+ recursive: true,
57
+ mode: 448
58
+ });
59
+ const tmp = `${path}.tmp.${process.pid}`;
60
+ writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 384 });
61
+ try {
62
+ __require("node:fs").renameSync(tmp, path);
63
+ } catch (e) {
64
+ try {
65
+ unlinkSync(tmp);
66
+ } catch {}
67
+ throw e;
68
+ }
69
+ try {
70
+ chmodSync(path, 384);
71
+ } catch {}
72
+ }
73
+ /** Set or replace the entry for `host`; mark as `current`. */
74
+ function saveAccount(rec) {
75
+ const data = readCredentials();
76
+ const key = canonicalHost(rec.host);
77
+ data.accounts[key] = {
78
+ ...rec,
79
+ host: key
80
+ };
81
+ data.current = key;
82
+ writeCredentials(data);
83
+ }
84
+ /** Remove one account; if it was current, switch to any remaining one. */
85
+ function removeAccount(host) {
86
+ const data = readCredentials();
87
+ const key = canonicalHost(host);
88
+ if (!(key in data.accounts)) return false;
89
+ delete data.accounts[key];
90
+ if (data.current === key) {
91
+ const next = Object.keys(data.accounts);
92
+ data.current = next.length > 0 ? next[0] ?? null : null;
93
+ }
94
+ writeCredentials(data);
95
+ return true;
96
+ }
97
+ /** Look up an account by host (or current account if `host` omitted). */
98
+ function getAccount(host) {
99
+ const data = readCredentials();
100
+ const key = host ? canonicalHost(host) : data.current;
101
+ if (!key) return null;
102
+ return data.accounts[key] ?? null;
103
+ }
104
+ /** Mask a PAT for safe display: ``eyJh…aZQp (90d, scopes=aps:dev)``. */
105
+ function maskPat(pat) {
106
+ if (pat.length <= 12) return "***";
107
+ return `${pat.slice(0, 4)}…${pat.slice(-4)}`;
108
+ }
109
+ /** Permission check — warn callers if file mode is loose. */
110
+ function credentialsAreLooselyPermissioned() {
111
+ const path = credentialsPath();
112
+ if (!existsSync(path)) return false;
113
+ try {
114
+ const st = statSync(path);
115
+ return (st.mode & 63) !== 0;
116
+ } catch {
117
+ return false;
118
+ }
119
+ }
120
+
121
+ //#endregion
122
+ export { canonicalHost, credentialsAreLooselyPermissioned, credentialsPath, getAccount, maskPat, readCredentials, removeAccount, saveAccount, writeCredentials };
@@ -0,0 +1,3 @@
1
+ import { canonicalHost, credentialsAreLooselyPermissioned, credentialsPath, getAccount, maskPat, readCredentials, removeAccount, saveAccount, writeCredentials } from "./credentials-BTv2IfUZ.js";
2
+
3
+ export { getAccount };
@@ -337,20 +337,24 @@
337
337
  }
338
338
 
339
339
  function relayEventToIframe(ev) {
340
- // ev = { kind:"event", user_id, kind:..., payload, ts } from server.ts
340
+ // ev arrives from the WS server as { kind:"event", event:"<name>", payload }
341
+ // (see server.ts: `ws.send({ kind: "event", ...ev })`). The SDK keys its
342
+ // handlers off `event` (e.g. "rpc.stream", "auth.refresh"), so we MUST
343
+ // forward `ev.event` — not `ev.kind`, which is always the literal
344
+ // string "event" and would drop every frame on the floor.
341
345
  if (!iframe.contentWindow) return;
342
346
  const env = {
343
347
  wid: windowUuid,
344
348
  kind: "event",
345
- event: ev.kind,
349
+ event: ev.event,
346
350
  payload: ev.payload,
347
351
  };
348
352
  iframe.contentWindow.postMessage(env, "*");
349
353
  logLine(
350
354
  "event",
351
- `← event <span class="pill">${escapeHtml(ev.kind)}</span> ${escapeHtml(JSON.stringify(ev.payload))}`,
355
+ `← event <span class="pill">${escapeHtml(ev.event)}</span> ${escapeHtml(JSON.stringify(ev.payload))}`,
352
356
  );
353
- recPush("event", { event: ev.kind, payload: ev.payload });
357
+ recPush("event", { event: ev.event, payload: ev.payload });
354
358
  }
355
359
 
356
360
  // postMessage RPC bridge: iframe → POST /api/session/call → result