@anna-ai/cli 0.1.14 → 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.
@@ -0,0 +1,136 @@
1
+ import { canonicalHost, getAccount } from "./credentials-BTv2IfUZ.js";
2
+ import { resolve } from "node:path";
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { createHash } from "node:crypto";
5
+
6
+ //#region src/executa/host_upload.ts
7
+ var HostUploadBridge = class {
8
+ mocks = [];
9
+ cachedSession = null;
10
+ constructor(opts) {
11
+ this.opts = opts;
12
+ if (opts.mode === "mock" && opts.mockFile) {
13
+ const path = resolve(opts.mockFile);
14
+ if (existsSync(path)) for (const line of readFileSync(path, "utf8").split(/\r?\n/)) {
15
+ if (!line.trim() || line.startsWith("#")) continue;
16
+ try {
17
+ this.mocks.push(JSON.parse(line));
18
+ } catch {}
19
+ }
20
+ }
21
+ }
22
+ async call(method, params) {
23
+ if (method !== "host/uploadFile") throw withCode(new Error(`unsupported host method: ${method}`), -32601);
24
+ if (this.opts.mode === "off") throw withCode(new Error("host upload disabled — rerun without `--no-upload`"), -32201);
25
+ if (this.opts.mode === "mock") return this.mockCall(params);
26
+ return this.realCall(params);
27
+ }
28
+ mockCall(params) {
29
+ const mode = String(params.mode ?? "inline");
30
+ const filename = String(params.filename ?? "");
31
+ const candidates = this.mocks.filter((m) => m.ns === "host" && m.method === "uploadFile");
32
+ const matched = candidates.find((m) => (!m.match?.modeEquals || m.match.modeEquals === mode) && (!m.match?.filenameIncludes || filename.includes(m.match.filenameIncludes))) ?? candidates[0];
33
+ if (matched && matched.result) return matched.result;
34
+ if (mode === "inline") {
35
+ const b64 = String(params.content_b64 ?? "");
36
+ const mime = String(params.mime_type ?? "application/octet-stream");
37
+ const url = `data:${mime};base64,${b64}`;
38
+ const r2Key = `mock/${hashShort(filename + ":" + b64)}/${filename || "blob"}`;
39
+ return {
40
+ download_url: url,
41
+ r2_key: r2Key,
42
+ size_bytes: Math.floor(b64.length * .75),
43
+ expires_at: new Date(Date.now() + 3600 * 1e3).toISOString()
44
+ };
45
+ }
46
+ if (mode === "negotiate") {
47
+ const key = `mock/${hashShort(filename + Date.now())}/${filename || "blob"}`;
48
+ return {
49
+ put_url: `https://mock.local/${key}?signature=mock`,
50
+ headers: { "content-type": String(params.mime_type ?? "") },
51
+ r2_key: key,
52
+ expires_at: new Date(Date.now() + 600 * 1e3).toISOString()
53
+ };
54
+ }
55
+ if (mode === "confirm") {
56
+ const key = String(params.r2_key ?? "");
57
+ return {
58
+ download_url: `https://mock.local/${key}`,
59
+ r2_key: key,
60
+ size_bytes: 0,
61
+ expires_at: new Date(Date.now() + 3600 * 1e3).toISOString()
62
+ };
63
+ }
64
+ throw withCode(new Error(`unknown upload mode: ${mode}`), -32203);
65
+ }
66
+ account() {
67
+ const acc = getAccount(this.opts.account);
68
+ if (!acc) throw withCode(new Error("no PAT on disk — run `anna-app login --host <nexus-url>` first (or pass `--mock-upload <fixture>` / `--no-upload`)"), -32201);
69
+ if (acc.expires_at && acc.expires_at < Math.floor(Date.now() / 1e3)) throw withCode(new Error("PAT expired — run `anna-app login` again"), -32201);
70
+ return acc;
71
+ }
72
+ async mint() {
73
+ if (this.cachedSession && this.cachedSession.expiresAt - 30 > Math.floor(Date.now() / 1e3)) return this.cachedSession;
74
+ const acc = this.account();
75
+ if (!this.opts.appSlug) throw withCode(new Error("host upload bridge has no app_slug — pass `--app-slug <slug>`"), -32203);
76
+ const url = `${canonicalHost(acc.host)}/api/v1/anna-apps/dev/session/mint`;
77
+ const res = await fetch(url, {
78
+ method: "POST",
79
+ headers: { "content-type": "application/json" },
80
+ body: JSON.stringify({
81
+ pat: acc.pat,
82
+ kind: "complete",
83
+ app_slug: this.opts.appSlug
84
+ })
85
+ });
86
+ if (!res.ok) {
87
+ const text = await res.text().catch(() => "");
88
+ throw withCode(new Error(`session.mint failed: HTTP ${res.status} ${text}`), -32207);
89
+ }
90
+ const body = await res.json();
91
+ this.cachedSession = {
92
+ appSessionToken: body.app_session_token,
93
+ expiresAt: Math.floor(Date.now() / 1e3) + (body.expires_in || 600)
94
+ };
95
+ return this.cachedSession;
96
+ }
97
+ async realCall(params) {
98
+ const acc = this.account();
99
+ const session = await this.mint();
100
+ const mode = String(params.mode ?? "inline");
101
+ const path = mode === "negotiate" ? "/api/v1/copilot/app/upload/negotiate" : mode === "confirm" ? "/api/v1/copilot/app/upload/confirm" : "/api/v1/copilot/app/upload";
102
+ const res = await fetch(`${canonicalHost(acc.host)}${path}`, {
103
+ method: "POST",
104
+ headers: {
105
+ "content-type": "application/json",
106
+ authorization: `Bearer ${session.appSessionToken}`
107
+ },
108
+ body: JSON.stringify(params)
109
+ });
110
+ if (!res.ok) {
111
+ const text = await res.text().catch(() => "");
112
+ throw withCode(new Error(`HTTP ${res.status}: ${text}`), httpToUploadCode(res.status));
113
+ }
114
+ return res.json();
115
+ }
116
+ };
117
+ function withCode(err, code) {
118
+ err.rpcCode = code;
119
+ return err;
120
+ }
121
+ function hashShort(s) {
122
+ return createHash("sha256").update(s).digest("hex").slice(0, 12);
123
+ }
124
+ function httpToUploadCode(status) {
125
+ if (status === 403) return -32201;
126
+ if (status === 429) return -32202;
127
+ if (status === 400) return -32203;
128
+ if (status === 413) return -32204;
129
+ if (status === 415) return -32205;
130
+ if (status === 404) return -32212;
131
+ if (status === 504) return -32208;
132
+ return -32207;
133
+ }
134
+
135
+ //#endregion
136
+ export { HostUploadBridge };
@@ -0,0 +1,131 @@
1
+ import { canonicalHost, getAccount } from "./credentials-BTv2IfUZ.js";
2
+ import { resolve } from "node:path";
3
+ import { existsSync, readFileSync } from "node:fs";
4
+
5
+ //#region src/executa/image.ts
6
+ var ImageBridge = class {
7
+ mocks = [];
8
+ cachedSession = null;
9
+ constructor(opts) {
10
+ this.opts = opts;
11
+ if (opts.mode === "mock" && opts.mockFile) {
12
+ const path = resolve(opts.mockFile);
13
+ if (existsSync(path)) for (const line of readFileSync(path, "utf8").split(/\r?\n/)) {
14
+ if (!line.trim() || line.startsWith("#")) continue;
15
+ try {
16
+ this.mocks.push(JSON.parse(line));
17
+ } catch {}
18
+ }
19
+ }
20
+ }
21
+ async generate(req) {
22
+ if (this.opts.mode === "off") throw withCode(new Error("image generation disabled — rerun without `--no-image`"), -32101);
23
+ if (this.opts.mode === "mock") return this.pickMock("generate", req.prompt);
24
+ return this.realCall("/api/v1/copilot/app/image/generate", req);
25
+ }
26
+ async edit(req) {
27
+ if (this.opts.mode === "off") throw withCode(new Error("image edit disabled — rerun without `--no-image`"), -32101);
28
+ if (this.opts.mode === "mock") return this.pickMock("edit", req.prompt);
29
+ return this.realCall("/api/v1/copilot/app/image/edit", req);
30
+ }
31
+ pickMock(method, prompt) {
32
+ const wantedMethod = method === "generate" ? "generate" : "edit";
33
+ const candidates = this.mocks.filter((m) => m.ns === "image" && m.method === wantedMethod);
34
+ const matched = candidates.find((m) => m.match?.promptIncludes && prompt.includes(m.match.promptIncludes)) ?? candidates[0];
35
+ if (matched && matched.result) return normaliseImageResult(matched.result);
36
+ return {
37
+ images: [{
38
+ url: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=",
39
+ mimeType: "image/png"
40
+ }],
41
+ model: "mock-image-model"
42
+ };
43
+ }
44
+ account() {
45
+ const acc = getAccount(this.opts.account);
46
+ if (!acc) throw withCode(new Error("no PAT on disk — run `anna-app login --host <nexus-url>` first (or pass `--mock-image <fixture>` / `--no-image`)"), -32101);
47
+ if (acc.expires_at && acc.expires_at < Math.floor(Date.now() / 1e3)) throw withCode(new Error("PAT expired — run `anna-app login` again"), -32101);
48
+ return acc;
49
+ }
50
+ async mint() {
51
+ if (this.cachedSession && this.cachedSession.expiresAt - 30 > Math.floor(Date.now() / 1e3)) return this.cachedSession;
52
+ const acc = this.account();
53
+ if (!this.opts.appSlug) throw withCode(new Error("image bridge has no app_slug — pass `--app-slug <slug>` to `anna-app executa dev` so nexus can attribute the spend"), -32104);
54
+ const url = `${canonicalHost(acc.host)}/api/v1/anna-apps/dev/session/mint`;
55
+ const res = await fetch(url, {
56
+ method: "POST",
57
+ headers: { "content-type": "application/json" },
58
+ body: JSON.stringify({
59
+ pat: acc.pat,
60
+ kind: "complete",
61
+ app_slug: this.opts.appSlug
62
+ })
63
+ });
64
+ if (!res.ok) {
65
+ const text = await res.text().catch(() => "");
66
+ throw withCode(new Error(`session.mint failed: HTTP ${res.status} ${text}`), -32103);
67
+ }
68
+ const body = await res.json();
69
+ this.cachedSession = {
70
+ appSessionToken: body.app_session_token,
71
+ expiresAt: Math.floor(Date.now() / 1e3) + (body.expires_in || 600)
72
+ };
73
+ return this.cachedSession;
74
+ }
75
+ async realCall(path, body) {
76
+ const acc = this.account();
77
+ const session = await this.mint();
78
+ const res = await fetch(`${canonicalHost(acc.host)}${path}`, {
79
+ method: "POST",
80
+ headers: {
81
+ "content-type": "application/json",
82
+ authorization: `Bearer ${session.appSessionToken}`
83
+ },
84
+ body: JSON.stringify(body)
85
+ });
86
+ if (!res.ok) {
87
+ const text = await res.text().catch(() => "");
88
+ throw withCode(
89
+ new Error(`HTTP ${res.status}: ${text}`),
90
+ // Map common HTTP statuses to canonical image codes; otherwise
91
+ // let the host's body propagate.
92
+ httpToImageCode(res.status)
93
+ );
94
+ }
95
+ const out = await res.json();
96
+ return normaliseImageResult(out);
97
+ }
98
+ };
99
+ function withCode(err, code) {
100
+ err.rpcCode = code;
101
+ return err;
102
+ }
103
+ function httpToImageCode(status) {
104
+ if (status === 403) return -32101;
105
+ if (status === 429) return -32102;
106
+ if (status === 400) return -32104;
107
+ if (status === 504) return -32105;
108
+ return -32103;
109
+ }
110
+ function normaliseImageResult(raw) {
111
+ if (!raw || typeof raw !== "object") return {
112
+ images: [],
113
+ model: "unknown"
114
+ };
115
+ const o = raw;
116
+ const images = Array.isArray(o.images) ? o.images.map((img) => ({
117
+ url: String(img.url ?? ""),
118
+ mimeType: img.mimeType ? String(img.mimeType) : void 0,
119
+ width: typeof img.width === "number" ? img.width : void 0,
120
+ height: typeof img.height === "number" ? img.height : void 0
121
+ })) : [];
122
+ return {
123
+ images,
124
+ model: typeof o.model === "string" ? o.model : void 0,
125
+ quota_used: o.quota_used && typeof o.quota_used === "object" ? o.quota_used : void 0,
126
+ _meta: o._meta && typeof o._meta === "object" ? o._meta : void 0
127
+ };
128
+ }
129
+
130
+ //#endregion
131
+ export { ImageBridge };
@@ -8,7 +8,10 @@ const HOST_INITIALIZE_PARAMS = {
8
8
  client_capabilities: {
9
9
  sampling: {},
10
10
  agent: {},
11
- storage: {}
11
+ storage: {},
12
+ image: {},
13
+ "image.edit": {},
14
+ upload: {}
12
15
  },
13
16
  client_info: {
14
17
  name: "anna-app-cli",
@@ -248,6 +251,46 @@ var ExecutaRunner = class {
248
251
  }
249
252
  return;
250
253
  }
254
+ if (method === "image/generate" || method === "image/edit") {
255
+ if (!this.opts.image) {
256
+ respond({ error: {
257
+ code: -32101,
258
+ message: "image not configured (rerun without `--no-image`, or pass `--mock-image <fixture>` / `--app-slug`)"
259
+ } });
260
+ return;
261
+ }
262
+ try {
263
+ const result = method === "image/generate" ? await this.opts.image.generate(params) : await this.opts.image.edit(params);
264
+ respond({ result });
265
+ } catch (e) {
266
+ const code = e.rpcCode ?? -32103;
267
+ respond({ error: {
268
+ code,
269
+ message: e.message
270
+ } });
271
+ }
272
+ return;
273
+ }
274
+ if (method === "host/uploadFile") {
275
+ if (!this.opts.hostUpload) {
276
+ respond({ error: {
277
+ code: -32201,
278
+ message: "host upload not configured (rerun without `--no-upload`, or pass `--mock-upload <fixture>` / `--app-slug`)"
279
+ } });
280
+ return;
281
+ }
282
+ try {
283
+ const result = await this.opts.hostUpload.call(method, params);
284
+ respond({ result });
285
+ } catch (e) {
286
+ const code = e.rpcCode ?? -32207;
287
+ respond({ error: {
288
+ code,
289
+ message: e.message
290
+ } });
291
+ }
292
+ return;
293
+ }
251
294
  if (method.startsWith("storage/") || method.startsWith("files/")) {
252
295
  if (!this.opts.storage) {
253
296
  respond({ error: {
@@ -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.14",
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",