@anna-ai/cli 0.1.16 → 0.1.17

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.
@@ -1,5 +1,5 @@
1
1
  import { getAccount } from "./credentials-BTv2IfUZ.js";
2
- import { listDevApps } from "./dev-app-cache-DAHcq46m.js";
2
+ import { listDevApps } from "./dev-app-cache-3Pfesngr.js";
3
3
  import { bold, cyan, dim, green, red, yellow } from "kleur/colors";
4
4
 
5
5
  //#region src/commands/apps.ts
@@ -0,0 +1,3 @@
1
+ import { BridgeRequestError, PINNED_RUNTIME_VERSION, PythonBridge } from "./bridge-Dffh9JUd.js";
2
+
3
+ export { PINNED_RUNTIME_VERSION, PythonBridge };
@@ -9,17 +9,39 @@ 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.0a3";
12
+ const PINNED_RUNTIME_VERSION = "0.2.0a4";
13
+ /**
14
+ * Throwable from a {@link RequestHandler} to send a structured JSON-RPC
15
+ * error back to the python bridge with a stable string ``code`` (e.g.
16
+ * ``"llm_disabled"``, ``"provider_error"``). The python side decodes the
17
+ * code from ``error.data.errorCode``.
18
+ */
19
+ var BridgeRequestError = class extends Error {
20
+ constructor(code, message, details) {
21
+ super(message);
22
+ this.code = code;
23
+ this.details = details;
24
+ }
25
+ };
13
26
  var PythonBridge = class {
14
27
  proc = null;
15
28
  nextId = 1;
16
29
  pending = new Map();
30
+ requestHandlers = new Map();
17
31
  readyPromise = null;
18
32
  closed = false;
19
33
  constructor(opts) {
20
34
  this.opts = opts;
21
35
  }
22
36
  /**
37
+ * Register a handler for an outbound python→node RPC method (e.g.
38
+ * ``host.llm.complete``). Subsequent registrations for the same method
39
+ * replace the previous handler.
40
+ */
41
+ onRequest(method, handler) {
42
+ this.requestHandlers.set(method, handler);
43
+ }
44
+ /**
23
45
  * Build the default launch command from `opts.mode`. uvx by default; for
24
46
  * `nexus-source` mode, `matrixNexusRoot` must be set.
25
47
  */
@@ -93,6 +115,10 @@ var PythonBridge = class {
93
115
  resolve$1();
94
116
  return;
95
117
  }
118
+ if (typeof env$1.method === "string") {
119
+ this.handleIncomingRequest(env$1);
120
+ return;
121
+ }
96
122
  this.handleResponse(env$1);
97
123
  });
98
124
  this.proc.on("error", (e) => reject(e));
@@ -102,6 +128,58 @@ var PythonBridge = class {
102
128
  });
103
129
  await this.readyPromise;
104
130
  }
131
+ async handleIncomingRequest(env) {
132
+ const method = env.method;
133
+ const id = env.id;
134
+ if (id == null) return;
135
+ const params = env.params ?? {};
136
+ const handler = this.requestHandlers.get(method);
137
+ if (!handler) {
138
+ this.writeFrame({
139
+ jsonrpc: "2.0",
140
+ id,
141
+ error: {
142
+ code: -32601,
143
+ message: `method not found: ${method}`
144
+ }
145
+ });
146
+ return;
147
+ }
148
+ try {
149
+ const result = await handler(params);
150
+ this.writeFrame({
151
+ jsonrpc: "2.0",
152
+ id,
153
+ result: result ?? {}
154
+ });
155
+ } catch (e) {
156
+ if (e instanceof BridgeRequestError) this.writeFrame({
157
+ jsonrpc: "2.0",
158
+ id,
159
+ error: {
160
+ code: -32e3,
161
+ message: e.message,
162
+ data: {
163
+ errorCode: e.code,
164
+ ...e.details ?? {}
165
+ }
166
+ }
167
+ });
168
+ else this.writeFrame({
169
+ jsonrpc: "2.0",
170
+ id,
171
+ error: {
172
+ code: -32603,
173
+ message: e.message,
174
+ data: { errorCode: "internal_error" }
175
+ }
176
+ });
177
+ }
178
+ }
179
+ writeFrame(env) {
180
+ if (!this.proc) return;
181
+ this.proc.stdin.write(`${JSON.stringify(env)}\n`);
182
+ }
105
183
  handleResponse(env) {
106
184
  const id = env.id;
107
185
  if (id == null) return;
@@ -143,4 +221,4 @@ var PythonBridge = class {
143
221
  };
144
222
 
145
223
  //#endregion
146
- export { PINNED_RUNTIME_VERSION, PythonBridge };
224
+ export { BridgeRequestError, PINNED_RUNTIME_VERSION, PythonBridge };
package/dist/cli.js CHANGED
@@ -450,8 +450,8 @@ program.command("validate").description("Run schema + ACL checks on a manifest+b
450
450
  const code = printResult(result);
451
451
  process.exit(code);
452
452
  });
453
- 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) => {
454
- const { runDev, parseExecutaSpec } = await import("./dev-BRlFgo2I.js");
453
+ 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)").option("--storage <mode>", "Storage backend: \"legacy\" (in-memory runtime_state, default) or \"aps\" (real nexus APS via /api/v1/storage/* — requires `anna-app login`).", "legacy").action(async (opts) => {
454
+ const { runDev, parseExecutaSpec } = await import("./dev-Bi6rkb1x.js");
455
455
  const cwd = opts.cwd ?? process.cwd();
456
456
  let executas;
457
457
  if (opts.executa && opts.executa.length > 0) {
@@ -479,7 +479,8 @@ program.command("dev").description("Run a local harness (in-process dispatcher +
479
479
  noLlm: opts.llm === false,
480
480
  mockLlm: opts.mockLlm,
481
481
  llmAccount: opts.llmAccount,
482
- llmAppSlug: opts.llmAppSlug
482
+ llmAppSlug: opts.llmAppSlug,
483
+ storageMode: opts.storage
483
484
  });
484
485
  process.exit(code);
485
486
  });
@@ -509,7 +510,7 @@ fixture.command("replay <file>").description("Dry-run replay of a harness record
509
510
  process.exit(code);
510
511
  });
511
512
  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) => {
512
- const { runDoctor } = await import("./doctor-C8MWfLt8.js");
513
+ const { runDoctor } = await import("./doctor-Dxkx0eqv.js");
513
514
  const code = await runDoctor({ matrixNexusRoot: opts.matrixNexusRoot });
514
515
  process.exit(code);
515
516
  });
@@ -535,7 +536,7 @@ program.command("whoami").description("Show the current account (and any others)
535
536
  process.exit(code);
536
537
  });
537
538
  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) => {
538
- const { runAppsList } = await import("./apps-Bt9CT5Sl.js");
539
+ const { runAppsList } = await import("./apps-ClgEOdKD.js");
539
540
  const code = await runAppsList({
540
541
  account: opts.account,
541
542
  json: opts.json
@@ -570,7 +571,7 @@ executa.command("register").description("Register a standalone executa with nexu
570
571
  process.exit(code);
571
572
  });
572
573
  executa.command("dev").description("Run one Executa plugin in isolation (REPL or one-shot describe/invoke)").option("--dir <path>", "Executa project dir (default: CWD)").option("--spec <spec>", "Override discovery: comma-separated key=value (tool_id=...,type=...,command=\"...\")").option("--describe", "Print MANIFEST and exit", false).option("--health", "Print health and exit", false).option("--invoke <tool>", "Invoke one tool and exit").option("--args <json>", "JSON object passed as tool arguments", "{}").option("--json", "One-shot: emit compact JSON (no banners)", false).option("--no-sampling", "Hard-disable sampling reverse RPC (returns sampling_disabled)").option("--mock-sampling <fixture>", "Serve canned sampling responses from a JSONL fixture (offline)").option("--app-slug <slug>", "Forward sampling to nexus on behalf of this dev AnnaApp slug").option("--sampling-account <host>", "Saved account host for nexus sampling (default: current)").option("--no-agent", "Hard-disable agent reverse RPC (returns agent_not_granted)").option("--mock-agent <fixture>", "Serve canned agent/* responses from a JSONL fixture (offline)").option("--agent-account <host>", "Saved account host for nexus agent (default: --sampling-account or current)").option("--storage <mode>", "Storage backend: off | memory | mock | real (default: memory)").option("--mock-storage <fixture>", "Serve canned storage/* + files/* responses from a JSONL fixture").option("--storage-account <host>", "Saved account host for nexus storage (default: --sampling-account or current)").option("--storage-scopes <list>", "Comma-separated scopes for real storage tokens (default: user,app,tool)").option("--no-image", "Hard-disable image reverse RPC (returns image_not_granted)").option("--mock-image <fixture>", "Serve canned image/generate + image/edit responses from a JSONL fixture").option("--image-account <host>", "Saved account host for nexus image (default: --sampling-account or current)").option("--no-upload", "Hard-disable host/uploadFile reverse RPC (returns upload_not_granted)").option("--mock-upload <fixture>", "Serve canned host/uploadFile responses from a JSONL fixture").option("--upload-account <host>", "Saved account host for nexus uploads (default: --sampling-account or current)").action(async (opts) => {
573
- const { runExecutaDev } = await import("./executa-dev-4FZ7AJHR.js");
574
+ const { runExecutaDev } = await import("./executa-dev-BzhSd_A2.js");
574
575
  const storageMode = opts.storage === void 0 ? void 0 : (() => {
575
576
  const m = opts.storage;
576
577
  if (m === "off" || m === "memory" || m === "mock" || m === "real") return m;
@@ -31,8 +31,20 @@ async function runDev(opts) {
31
31
  }
32
32
  const matrixNexusRoot = await resolveMatrixNexusRoot(opts.matrixNexusRoot, cwd);
33
33
  const mode = matrixNexusRoot ? "nexus-source" : "uvx";
34
- const { PythonBridge, PINNED_RUNTIME_VERSION } = await import("./bridge-AJilXBw2.js");
35
- const { HarnessServer } = await import("./server-D8R6ppOp.js");
34
+ const storageMode = opts.storageMode === "aps" ? "aps" : "legacy";
35
+ if (opts.storageMode && opts.storageMode !== "legacy" && opts.storageMode !== "aps") {
36
+ console.error(red(`✗ --storage must be "legacy" or "aps", got "${opts.storageMode}"`));
37
+ return 2;
38
+ }
39
+ if (storageMode === "aps") {
40
+ if (opts.noLlm || opts.mockLlm) {
41
+ console.error(red("✗ --storage aps requires a real LLM bridge (PAT on disk); drop --no-llm / --mock-llm or switch back to --storage legacy."));
42
+ return 2;
43
+ }
44
+ process.env.ANNA_APP_RUNTIME_STORAGE_MODE = "aps";
45
+ }
46
+ const { PythonBridge, PINNED_RUNTIME_VERSION } = await import("./bridge-B1vq1oG3.js");
47
+ const { HarnessServer } = await import("./server-6WHNkydc.js");
36
48
  const bridge = new PythonBridge({
37
49
  mode,
38
50
  matrixNexusRoot: matrixNexusRoot ?? void 0,
@@ -75,7 +87,9 @@ async function runDev(opts) {
75
87
  manifest
76
88
  });
77
89
  if (llm === null) return 2;
90
+ llm.storageMode = storageMode;
78
91
  console.log(` llm bridge ${dim(llm.mode === "off" ? "disabled (--no-llm)" : llm.mode === "mock" ? `mock (${opts.mockLlm})` : `real${opts.llmAccount ? ` [${opts.llmAccount}]` : ""} → app_slug=${llm.appSlug}`)}`);
92
+ console.log(` storage backend ${dim(storageMode === "aps" ? "aps (real nexus APS via /api/v1/storage/*)" : "legacy (in-memory runtime_state)")}`);
79
93
  const server = new HarnessServer({
80
94
  slug,
81
95
  manifest,
@@ -123,7 +137,7 @@ async function runDev(opts) {
123
137
  */
124
138
  async function resolveRealLlm(args) {
125
139
  const { getAccount } = await import("./credentials-DDqx6XMQ.js");
126
- const { ensureDevAppRegistered } = await import("./dev-app-cache-DGF2Kuzd.js");
140
+ const { ensureDevAppRegistered } = await import("./dev-app-cache-CZ1UjMz0.js");
127
141
  const acc = getAccount(args.account);
128
142
  if (!acc) {
129
143
  console.error(red("✗ no developer PAT on disk — run `anna-app login --host <nexus-url>` first.\n (or use `--no-llm` / `--mock-llm <fixture>` to develop offline.)"));
@@ -134,22 +148,35 @@ async function resolveRealLlm(args) {
134
148
  return null;
135
149
  }
136
150
  const manifest = args.manifest;
151
+ const registerInput = {
152
+ slug: args.appSlug,
153
+ name: manifest.name,
154
+ category: manifest.category,
155
+ tagline: manifest.tagline
156
+ };
137
157
  try {
138
158
  const entry = await ensureDevAppRegistered({
139
159
  cwd: args.cwd,
140
160
  host: acc.host,
141
161
  pat: acc.pat,
142
- input: {
143
- slug: args.appSlug,
144
- name: manifest.name,
145
- category: manifest.category,
146
- tagline: manifest.tagline
147
- }
162
+ input: registerInput
148
163
  });
149
164
  return {
150
165
  mode: "real",
151
166
  account: args.account,
152
- appSlug: entry.slug
167
+ appSlug: entry.slug,
168
+ onAppSlugNotFound: async () => {
169
+ const { invalidateDevAppCache } = await import("./dev-app-cache-CZ1UjMz0.js");
170
+ invalidateDevAppCache(args.cwd);
171
+ const fresh = getAccount(args.account);
172
+ if (!fresh) throw new Error("PAT not found while re-registering dev app — run `anna-app login` again");
173
+ await ensureDevAppRegistered({
174
+ cwd: args.cwd,
175
+ host: fresh.host,
176
+ pat: fresh.pat,
177
+ input: registerInput
178
+ });
179
+ }
153
180
  };
154
181
  } catch (e) {
155
182
  console.error(red(`✗ failed to register dev app on nexus: ${e.message}`));
@@ -0,0 +1,3 @@
1
+ import { parseExecutaSpec, runDev } from "./dev-BUetXnfG.js";
2
+
3
+ export { parseExecutaSpec, runDev };
@@ -1,6 +1,6 @@
1
1
  import { canonicalHost } from "./credentials-BTv2IfUZ.js";
2
2
  import { dirname, join, resolve } from "node:path";
3
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
4
4
 
5
5
  //#region src/dev-app-cache.ts
6
6
  const CACHE_DIR = ".anna";
@@ -28,6 +28,20 @@ function writeDevAppCache(cwd, entry) {
28
28
  mkdirSync(dirname(p), { recursive: true });
29
29
  writeFileSync(p, JSON.stringify(entry, null, 2) + "\n", "utf-8");
30
30
  }
31
+ /** Delete the local dev-app cache file.
32
+ *
33
+ * Used by the LLM bridge's recovery flow: when `/dev/session/mint`
34
+ * returns 404 with "app slug ... not found", the cache is stale (the
35
+ * nexus DB no longer has the row this cache asserts is registered).
36
+ * Invalidating forces the next `ensureDevAppRegistered` call to
37
+ * re-POST `/dev/apps/register`. Idempotent: a missing file is a no-op.
38
+ */
39
+ function invalidateDevAppCache(cwd) {
40
+ const p = cachePath(cwd);
41
+ if (existsSync(p)) try {
42
+ unlinkSync(p);
43
+ } catch {}
44
+ }
31
45
  /** Call POST /api/v1/anna-apps/dev/apps/register. Idempotent server-side. */
32
46
  async function registerDevApp(args) {
33
47
  const url = `${canonicalHost(args.host)}/api/v1/anna-apps/dev/apps/register`;
@@ -172,4 +186,4 @@ async function ensureDevExecutaRegistered(args) {
172
186
  }
173
187
 
174
188
  //#endregion
175
- export { ensureDevAppRegistered, ensureDevExecutaRegistered, listDevApps, readDevAppCache, readDevExecutaCache, registerDevApp, registerDevExecuta, writeDevAppCache, writeDevExecutaCache };
189
+ export { ensureDevAppRegistered, ensureDevExecutaRegistered, invalidateDevAppCache, listDevApps, readDevAppCache, readDevExecutaCache, registerDevApp, registerDevExecuta, writeDevAppCache, writeDevExecutaCache };
@@ -0,0 +1,4 @@
1
+ import "./credentials-BTv2IfUZ.js";
2
+ import { ensureDevAppRegistered, ensureDevExecutaRegistered, invalidateDevAppCache, listDevApps, readDevAppCache, readDevExecutaCache, registerDevApp, registerDevExecuta, writeDevAppCache, writeDevExecutaCache } from "./dev-app-cache-3Pfesngr.js";
3
+
4
+ export { ensureDevAppRegistered, ensureDevExecutaRegistered, invalidateDevAppCache };
@@ -1,4 +1,4 @@
1
- import { PINNED_RUNTIME_VERSION } from "./bridge-nqQ3-j-t.js";
1
+ import { PINNED_RUNTIME_VERSION } from "./bridge-Dffh9JUd.js";
2
2
  import { dirname, isAbsolute, resolve } from "node:path";
3
3
  import { existsSync, statSync } from "node:fs";
4
4
  import { spawnSync } from "node:child_process";
@@ -1,4 +1,4 @@
1
- import { parseExecutaSpec } from "./dev-C6v5yRV2.js";
1
+ import { parseExecutaSpec } from "./dev-BUetXnfG.js";
2
2
  import { isAbsolute, resolve } from "node:path";
3
3
  import { existsSync } from "node:fs";
4
4
  import { bold, cyan, dim, green, red, yellow } from "kleur/colors";
@@ -34,7 +34,7 @@ async function runExecutaDev(opts) {
34
34
  const oneShot = !!(opts.describe || opts.health || opts.invoke);
35
35
  const quiet = oneShot && (opts.json ?? false);
36
36
  const { getAccount } = await import("./credentials-DDqx6XMQ.js");
37
- const { ensureDevExecutaRegistered } = await import("./dev-app-cache-DGF2Kuzd.js");
37
+ const { ensureDevExecutaRegistered } = await import("./dev-app-cache-CZ1UjMz0.js");
38
38
  const needsRealMint = !opts.noSampling && !opts.mockSampling || !opts.noAgent && !opts.mockAgent || !opts.noImage && !opts.mockImage || !opts.noUpload && !opts.mockUpload || opts.storage === "real";
39
39
  let effectiveAppSlug = opts.appSlug;
40
40
  let autoRegistered = false;
@@ -1,4 +1,5 @@
1
1
  import { canonicalHost, getAccount } from "./credentials-BTv2IfUZ.js";
2
+ import { BridgeRequestError } from "./bridge-Dffh9JUd.js";
2
3
  import { dirname, join, normalize, resolve } from "node:path";
3
4
  import { createRequire } from "node:module";
4
5
  import { createReadStream, existsSync, readFileSync, statSync, watch } from "node:fs";
@@ -12,6 +13,10 @@ import { setTimeout as setTimeout$1 } from "node:timers/promises";
12
13
  var LlmBridge = class {
13
14
  mintedAuto = new Map();
14
15
  mintedAgent = new Map();
16
+ /** Single shared storage_token per LlmBridge instance — scope is per
17
+ * ``(user, app)`` server-side, not per-window, so caching one is
18
+ * correct. Re-minted automatically before expiry. */
19
+ mintedStorage = null;
15
20
  mocks = [];
16
21
  streamCounter = 0;
17
22
  constructor(opts) {
@@ -30,10 +35,30 @@ var LlmBridge = class {
30
35
  }
31
36
  }
32
37
  /** Returns true iff this bridge handles `(ns, method)` (i.e. the harness
33
- * should NOT forward it to the in-process Python dispatcher). */
38
+ * should NOT forward it to the in-process Python dispatcher).
39
+ *
40
+ * Storage handling depends on ``storageMode``:
41
+ * - ``legacy`` (default): the bridge does NOT claim storage; the Python
42
+ * dispatcher implements ``anna.storage.*`` against the in-memory
43
+ * ``runtime_state`` dict. Pre-APS parity, works fully offline.
44
+ * - ``aps``: the bridge claims storage and forwards to real nexus APS
45
+ * via ``/api/v1/storage/*`` with a Bearer ``storage_token``. */
46
+ handles(ns, method) {
47
+ if (ns === "llm" && method === "complete") return true;
48
+ if (ns === "agent" && method.startsWith("session.")) return true;
49
+ if (ns === "image" && (method === "generate" || method === "edit")) return true;
50
+ if (ns === "upload" && (method === "inline" || method === "negotiate" || method === "confirm")) return true;
51
+ if (ns === "storage" && this.opts.storageMode === "aps" && this.opts.mode === "real" && (method === "get" || method === "set" || method === "delete" || method === "list")) return true;
52
+ return false;
53
+ }
54
+ /** Back-compat static alias — older code paths still call
55
+ * ``LlmBridge.handles(ns, method)`` without an instance. Returns the
56
+ * legacy (no-storage) decision since options aren't available here. */
34
57
  static handles(ns, method) {
35
58
  if (ns === "llm" && method === "complete") return true;
36
59
  if (ns === "agent" && method.startsWith("session.")) return true;
60
+ if (ns === "image" && (method === "generate" || method === "edit")) return true;
61
+ if (ns === "upload" && (method === "inline" || method === "negotiate" || method === "confirm")) return true;
37
62
  return false;
38
63
  }
39
64
  /** Resolve the active account or throw with a friendly message. */
@@ -90,12 +115,65 @@ var LlmBridge = class {
90
115
  this.mintedAgent.set(ms.appSessionUuid, ms);
91
116
  return ms;
92
117
  }
118
+ /** Mint (or reuse) a storage_token for APS forwarding.
119
+ *
120
+ * Server-side ``/dev/storage/mint`` issues a JWT scoped to
121
+ * ``(user, app)`` with ``allowed_scopes ⊆ {user, app, tool}`` and a
122
+ * configurable TTL (default 600s) + ``max_calls`` budget (default 200).
123
+ * We re-mint when within 30s of expiry; budget exhaustion currently
124
+ * surfaces as an HTTP 401/403 which propagates back to the iframe.
125
+ *
126
+ * Per-app, not per-window: the token doesn't carry a window or
127
+ * session uuid — storage scopes are ``user``/``app``/``tool``, not
128
+ * ``window``. Caching one per LlmBridge instance is correct. */
129
+ async mintStorage() {
130
+ const cached = this.mintedStorage;
131
+ if (cached && cached.expiresAt - 30 > Math.floor(Date.now() / 1e3)) return cached;
132
+ const acc = this.account();
133
+ const slug = this.requireAppSlug();
134
+ const body = {
135
+ pat: acc.pat,
136
+ app_slug: slug,
137
+ ttl_seconds: 600,
138
+ max_calls: 200
139
+ };
140
+ const url = `${canonicalHost(acc.host)}/api/v1/anna-apps/dev/storage/mint`;
141
+ const res = await fetch(url, {
142
+ method: "POST",
143
+ headers: { "content-type": "application/json" },
144
+ body: JSON.stringify(body)
145
+ });
146
+ if (!res.ok) {
147
+ const text = await res.text().catch(() => "");
148
+ throw new Error(`storage.mint failed: HTTP ${res.status} ${text}`);
149
+ }
150
+ const payload = await res.json();
151
+ const ms = {
152
+ storageToken: payload.storage_token,
153
+ expiresAt: Math.floor(Date.now() / 1e3) + (payload.expires_in || 600),
154
+ allowedScopes: payload.allowed_scopes ?? [
155
+ "user",
156
+ "app",
157
+ "tool"
158
+ ]
159
+ };
160
+ this.mintedStorage = ms;
161
+ return ms;
162
+ }
93
163
  /** Throws a friendly error if no appSlug was wired into the bridge. */
94
164
  requireAppSlug() {
95
165
  if (!this.opts.appSlug) throw new Error("llm bridge has no app_slug — the harness must register the dev app via POST /api/v1/anna-apps/dev/apps/register before minting sessions. (Run `anna-app login` once, then `anna-app dev` will auto-register the manifest slug.)");
96
166
  return this.opts.appSlug;
97
167
  }
98
168
  async callMint(host, body) {
169
+ return this._callMintOnce(
170
+ host,
171
+ body,
172
+ /*retryOnAppNotFound=*/
173
+ true
174
+ );
175
+ }
176
+ async _callMintOnce(host, body, retryOnAppNotFound) {
99
177
  const url = `${canonicalHost(host)}/api/v1/anna-apps/dev/session/mint`;
100
178
  const res = await fetch(url, {
101
179
  method: "POST",
@@ -104,6 +182,19 @@ var LlmBridge = class {
104
182
  });
105
183
  if (!res.ok) {
106
184
  const text = await res.text().catch(() => "");
185
+ if (retryOnAppNotFound && res.status === 404 && /app slug .* not found/.test(text) && typeof this.opts.onAppSlugNotFound === "function") {
186
+ try {
187
+ await this.opts.onAppSlugNotFound();
188
+ } catch (hookErr) {
189
+ throw new Error(`session.mint failed: HTTP 404 ${text}\n recovery hook failed: ${hookErr.message}`);
190
+ }
191
+ return this._callMintOnce(
192
+ host,
193
+ body,
194
+ /*retryOnAppNotFound=*/
195
+ false
196
+ );
197
+ }
107
198
  throw new Error(`session.mint failed: HTTP ${res.status} ${text}`);
108
199
  }
109
200
  return await res.json();
@@ -290,6 +381,121 @@ var LlmBridge = class {
290
381
  };
291
382
  }
292
383
  }
384
+ if (args.ns === "image" && (args.method === "generate" || args.method === "edit")) {
385
+ const ms = await this.mintComplete(args.windowUuid);
386
+ const result = await this.postJson(`${canonicalHost(acc.host)}/api/v1/copilot/app/image/${args.method}`, ms.appSessionToken, args.args);
387
+ return {
388
+ ok: true,
389
+ result
390
+ };
391
+ }
392
+ if (args.ns === "upload") {
393
+ const ms = await this.mintComplete(args.windowUuid);
394
+ const path = args.method === "inline" ? "/api/v1/copilot/app/upload" : args.method === "negotiate" ? "/api/v1/copilot/app/upload/negotiate" : args.method === "confirm" ? "/api/v1/copilot/app/upload/confirm" : null;
395
+ if (path != null) {
396
+ const result = await this.postJson(`${canonicalHost(acc.host)}${path}`, ms.appSessionToken, args.args);
397
+ return {
398
+ ok: true,
399
+ result
400
+ };
401
+ }
402
+ }
403
+ if (args.ns === "storage") {
404
+ const sm = await this.mintStorage();
405
+ const a = args.args;
406
+ const base = `${canonicalHost(acc.host)}/api/v1/storage`;
407
+ if (args.method === "get") {
408
+ const qs = new URLSearchParams();
409
+ qs.set("key", String(a.key ?? ""));
410
+ if (a.scope) qs.set("scope", String(a.scope));
411
+ const res = await fetch(`${base}/kv?${qs.toString()}`, {
412
+ method: "GET",
413
+ headers: { authorization: `Bearer ${sm.storageToken}` }
414
+ });
415
+ if (res.status === 404) return {
416
+ ok: true,
417
+ result: {
418
+ value: null,
419
+ exists: false
420
+ }
421
+ };
422
+ if (!res.ok) {
423
+ const text = await res.text().catch(() => "");
424
+ throw new Error(`storage.get HTTP ${res.status}: ${text}`);
425
+ }
426
+ const entry = await res.json();
427
+ if (entry.exists === void 0) entry.exists = true;
428
+ return {
429
+ ok: true,
430
+ result: entry
431
+ };
432
+ }
433
+ if (args.method === "set") {
434
+ const qs = new URLSearchParams();
435
+ if (a.scope) qs.set("scope", String(a.scope));
436
+ const body = {
437
+ key: a.key,
438
+ value: a.value
439
+ };
440
+ if (a.if_match !== void 0) body.if_match = a.if_match;
441
+ if (a.metadata !== void 0) body.metadata = a.metadata;
442
+ if (a.tags !== void 0) body.tags = a.tags;
443
+ if (a.ttl_seconds !== void 0) body.ttl_seconds = a.ttl_seconds;
444
+ const res = await fetch(`${base}/kv?${qs.toString()}`, {
445
+ method: "PUT",
446
+ headers: {
447
+ "content-type": "application/json",
448
+ authorization: `Bearer ${sm.storageToken}`
449
+ },
450
+ body: JSON.stringify(body)
451
+ });
452
+ if (!res.ok) {
453
+ const text = await res.text().catch(() => "");
454
+ throw new Error(`storage.set HTTP ${res.status}: ${text}`);
455
+ }
456
+ return {
457
+ ok: true,
458
+ result: await res.json()
459
+ };
460
+ }
461
+ if (args.method === "delete") {
462
+ const qs = new URLSearchParams();
463
+ qs.set("key", String(a.key ?? ""));
464
+ if (a.if_match !== void 0) qs.set("if_match", String(a.if_match));
465
+ if (a.scope) qs.set("scope", String(a.scope));
466
+ const res = await fetch(`${base}/kv?${qs.toString()}`, {
467
+ method: "DELETE",
468
+ headers: { authorization: `Bearer ${sm.storageToken}` }
469
+ });
470
+ if (!res.ok) {
471
+ const text = await res.text().catch(() => "");
472
+ throw new Error(`storage.delete HTTP ${res.status}: ${text}`);
473
+ }
474
+ return {
475
+ ok: true,
476
+ result: await res.json()
477
+ };
478
+ }
479
+ if (args.method === "list") {
480
+ const qs = new URLSearchParams();
481
+ if (a.prefix !== void 0) qs.set("prefix", String(a.prefix));
482
+ if (a.cursor !== void 0) qs.set("cursor", String(a.cursor));
483
+ if (a.limit !== void 0) qs.set("limit", String(a.limit));
484
+ if (a.scope) qs.set("scope", String(a.scope));
485
+ const res = await fetch(`${base}/list?${qs.toString()}`, {
486
+ method: "GET",
487
+ headers: { authorization: `Bearer ${sm.storageToken}` }
488
+ });
489
+ if (!res.ok) {
490
+ const text = await res.text().catch(() => "");
491
+ throw new Error(`storage.list HTTP ${res.status}: ${text}`);
492
+ }
493
+ return {
494
+ ok: true,
495
+ result: await res.json()
496
+ };
497
+ }
498
+ }
293
499
  return {
294
500
  ok: false,
295
501
  error: {
@@ -434,6 +640,100 @@ var HarnessServer = class {
434
640
  this.cfg = cfg;
435
641
  this.bridge = bridge;
436
642
  this.llmBridge = cfg.llm ? new LlmBridge(cfg.llm) : null;
643
+ const HOST_OUTBOUND_ROUTES = [
644
+ [
645
+ "host.llm.complete",
646
+ "llm",
647
+ "complete"
648
+ ],
649
+ [
650
+ "host.agent.session.create",
651
+ "agent",
652
+ "session.create"
653
+ ],
654
+ [
655
+ "host.agent.session.run",
656
+ "agent",
657
+ "session.run"
658
+ ],
659
+ [
660
+ "host.agent.session.cancel",
661
+ "agent",
662
+ "session.cancel"
663
+ ],
664
+ [
665
+ "host.agent.session.history",
666
+ "agent",
667
+ "session.history"
668
+ ],
669
+ [
670
+ "host.agent.session.delete",
671
+ "agent",
672
+ "session.delete"
673
+ ],
674
+ [
675
+ "host.image.generate",
676
+ "image",
677
+ "generate"
678
+ ],
679
+ [
680
+ "host.image.edit",
681
+ "image",
682
+ "edit"
683
+ ],
684
+ [
685
+ "host.upload.inline",
686
+ "upload",
687
+ "inline"
688
+ ],
689
+ [
690
+ "host.upload.negotiate",
691
+ "upload",
692
+ "negotiate"
693
+ ],
694
+ [
695
+ "host.upload.confirm",
696
+ "upload",
697
+ "confirm"
698
+ ],
699
+ [
700
+ "host.storage.get",
701
+ "storage",
702
+ "get"
703
+ ],
704
+ [
705
+ "host.storage.set",
706
+ "storage",
707
+ "set"
708
+ ],
709
+ [
710
+ "host.storage.delete",
711
+ "storage",
712
+ "delete"
713
+ ],
714
+ [
715
+ "host.storage.list",
716
+ "storage",
717
+ "list"
718
+ ]
719
+ ];
720
+ for (const [hostMethod, ns, dispatchMethod] of HOST_OUTBOUND_ROUTES) this.bridge.onRequest(hostMethod, async (params) => {
721
+ if (this.llmBridge == null) throw new BridgeRequestError("llm_disabled", "harness started without an LlmBridge (use --no-llm to suppress this path or `anna-app login` for a real bridge)");
722
+ const out = await this.llmBridge.dispatch({
723
+ windowUuid: this.sessionId ?? "harness",
724
+ ns,
725
+ method: dispatchMethod,
726
+ args: params,
727
+ onEvent: (kind, payload) => {
728
+ this.llmEventQueue.push({
729
+ event: kind,
730
+ payload
731
+ });
732
+ }
733
+ });
734
+ if (out.ok) return out.result;
735
+ throw new BridgeRequestError(out.error.code, out.error.message);
736
+ });
437
737
  }
438
738
  async listen() {
439
739
  if (this.cfg.executas && this.cfg.executas.length > 0) await this.bridge.call("executas.register", { executas: this.cfg.executas.map((e) => ({
@@ -577,7 +877,7 @@ var HarnessServer = class {
577
877
  }
578
878
  });
579
879
  }
580
- if (this.llmBridge != null && LlmBridge.handles(parsed.ns, parsed.method)) {
880
+ if (this.llmBridge != null && this.llmBridge.handles(parsed.ns, parsed.method)) {
581
881
  const out = await this.llmBridge.dispatch({
582
882
  windowUuid: this.sessionId ?? "harness",
583
883
  ns: parsed.ns,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anna-ai/cli",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "Anna App developer CLI: scaffold, validate, harness (Phase 2 MVP: init + validate).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,3 +0,0 @@
1
- import { PINNED_RUNTIME_VERSION, PythonBridge } from "./bridge-nqQ3-j-t.js";
2
-
3
- export { PINNED_RUNTIME_VERSION, PythonBridge };
@@ -1,3 +0,0 @@
1
- import { parseExecutaSpec, runDev } from "./dev-C6v5yRV2.js";
2
-
3
- export { parseExecutaSpec, runDev };
@@ -1,4 +0,0 @@
1
- import "./credentials-BTv2IfUZ.js";
2
- import { ensureDevAppRegistered, ensureDevExecutaRegistered, listDevApps, readDevAppCache, readDevExecutaCache, registerDevApp, registerDevExecuta, writeDevAppCache, writeDevExecutaCache } from "./dev-app-cache-DAHcq46m.js";
3
-
4
- export { ensureDevAppRegistered, ensureDevExecutaRegistered };