@anna-ai/cli 0.1.1 → 0.1.9

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
@@ -25,7 +25,6 @@ Local dev (this repo):
25
25
 
26
26
  ```bash
27
27
  pnpm install
28
- pnpm sync:schema # vendors @anna-ai/app-schema from sibling matrix-nexus checkout
29
28
  pnpm build
30
29
  node dist/cli.js --help
31
30
  ```
@@ -115,14 +114,21 @@ For executa authors. Lives in matrix-nexus at
115
114
  `packages/anna-executa-test/`. Provides `executa_session` /
116
115
  `executa_invoke` fixtures — see that package's README.
117
116
 
118
- ## Schema bundle sync
117
+ ## Schema bundle
119
118
 
120
- The CLI vendors `@anna-ai/app-schema` until the npm bundle ships:
119
+ [`@anna-ai/app-schema`](https://www.npmjs.com/package/@anna-ai/app-schema)
120
+ is a regular npm dependency. Set `ANNA_APP_SCHEMA_DIR` to point at a
121
+ local checkout (e.g. `matrix-nexus/packages/anna-app-schema/`) to
122
+ iterate on the protocol without publishing.
121
123
 
122
- ```bash
123
- pnpm sync:schema # default: ../matrix-nexus
124
- pnpm sync:schema -- --from /path/to/... # override
125
- ```
124
+ ## Browser SDK
125
+
126
+ End-user app bundles load `AnnaAppRuntime` from
127
+ `/static/anna-apps/_sdk/<version>/index.js`. Both the matrix-nexus
128
+ production server and this CLI's harness serve that URL from the
129
+ public npm package [`@anna-ai/app-runtime`](https://www.npmjs.com/package/@anna-ai/app-runtime),
130
+ which is declared as a normal dependency. No vendored copy, no sync
131
+ step.
126
132
 
127
133
  ## Roadmap
128
134
 
package/dist/cli.js CHANGED
@@ -1,9 +1,12 @@
1
1
  #!/usr/bin/env node
2
+ import { dirname, extname, join, relative, resolve } from "node:path";
3
+ import { createRequire } from "node:module";
2
4
  import { Command } from "commander";
3
5
  import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
4
- import { dirname, join, resolve } from "node:path";
5
6
  import { fileURLToPath } from "node:url";
6
7
  import kleur from "kleur";
8
+ import Ajv from "ajv/dist/2020.js";
9
+ import addFormats from "ajv-formats";
7
10
 
8
11
  //#region src/commands/init.ts
9
12
  const here = dirname(fileURLToPath(import.meta.url));
@@ -13,13 +16,14 @@ function templateRoot(template) {
13
16
  }
14
17
  function substitute(content, slug) {
15
18
  const toolId = `tool-dev-${slug}`;
16
- return content.replace(/__SLUG__/g, slug).replace(/__TOOL_ID__/g, toolId);
19
+ const slugPy = slug.replace(/-/g, "_");
20
+ return content.replace(/__SLUG_PY__/g, slugPy).replace(/__SLUG__/g, slug).replace(/__TOOL_ID__/g, toolId);
17
21
  }
18
22
  function copyDirWithSubst(src, dst, slug) {
19
23
  mkdirSync(dst, { recursive: true });
20
24
  for (const entry of readdirSync(src, { withFileTypes: true })) {
21
25
  const s = join(src, entry.name);
22
- const d = join(dst, entry.name);
26
+ const d = join(dst, substitute(entry.name, slug));
23
27
  if (entry.isDirectory()) copyDirWithSubst(s, d, slug);
24
28
  else if (entry.isFile()) {
25
29
  const stat = statSync(s);
@@ -54,10 +58,371 @@ function runInit(opts) {
54
58
  return 0;
55
59
  }
56
60
 
61
+ //#endregion
62
+ //#region src/schema-bundle.ts
63
+ function resolveSchemaDir() {
64
+ if (process.env.ANNA_APP_SCHEMA_DIR) return process.env.ANNA_APP_SCHEMA_DIR;
65
+ const req = createRequire(import.meta.url);
66
+ const methodsAbs = req.resolve("@anna-ai/app-schema/methods");
67
+ return resolve(dirname(methodsAbs), "..");
68
+ }
69
+ function loadSchemaBundle() {
70
+ let dir;
71
+ try {
72
+ dir = resolveSchemaDir();
73
+ } catch (e) {
74
+ throw new Error(`Could not locate @anna-ai/app-schema: ${e.message}\nRun \`pnpm install\` (or set ANNA_APP_SCHEMA_DIR to a local checkout).`);
75
+ }
76
+ if (!existsSync(resolve(dir, "dispatcher_version.txt"))) throw new Error(`@anna-ai/app-schema bundle is missing expected files at ${dir}`);
77
+ const read = (rel) => readFileSync(resolve(dir, rel), "utf-8");
78
+ const readJson = (rel) => JSON.parse(read(rel));
79
+ const dispatcherVersion = read("dispatcher_version.txt").trim();
80
+ const appManifestSchema = readJson("manifest/AppManifest.json");
81
+ const uiSectionSchema = readJson("manifest/UiManifestSection.json");
82
+ const methodsTable = readJson("host_api/methods.json");
83
+ const eventsSchema = readJson("events/AnnaAppEvent.json");
84
+ return {
85
+ dir,
86
+ dispatcherVersion,
87
+ appManifestSchema,
88
+ uiSectionSchema,
89
+ methods: methodsTable.methods,
90
+ events: eventsSchema.properties.kind.enum
91
+ };
92
+ }
93
+
94
+ //#endregion
95
+ //#region src/validate/manifest-schema.ts
96
+ function validateManifestSchema(bundle, manifest) {
97
+ const ajv = new Ajv({
98
+ allErrors: true,
99
+ strict: false
100
+ });
101
+ addFormats(ajv);
102
+ const validate = ajv.compile(bundle.appManifestSchema);
103
+ const ok = validate(manifest);
104
+ if (ok) return [];
105
+ return (validate.errors ?? []).map((e) => ({
106
+ path: e.instancePath || "(root)",
107
+ message: `${e.message ?? "invalid"}${e.params ? " " + JSON.stringify(e.params) : ""}`
108
+ }));
109
+ }
110
+ const ANNA_CALL_RE = /\banna\.([a-z]+)\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g;
111
+ function scanHostApiCalls(files) {
112
+ const out = [];
113
+ for (const f of files) {
114
+ const lines = f.content.split("\n");
115
+ for (let i = 0; i < lines.length; i++) {
116
+ const line = lines[i];
117
+ ANNA_CALL_RE.lastIndex = 0;
118
+ let m;
119
+ while ((m = ANNA_CALL_RE.exec(line)) !== null) out.push({
120
+ file: f.path,
121
+ line: i + 1,
122
+ ns: m[1],
123
+ method: m[2]
124
+ });
125
+ }
126
+ }
127
+ return out;
128
+ }
129
+ /**
130
+ * Mirror of `anna_app_runtime_service.host_api_allows` (matrix-nexus).
131
+ * Keep these in sync — divergence here means the CLI lies about what
132
+ * production accepts.
133
+ */
134
+ function hostApiAllows(manifest, ns, method) {
135
+ if (!manifest.ui) return false;
136
+ if (ns === "window") return true;
137
+ const grants = manifest.ui.host_api ?? {};
138
+ const methods = grants[ns];
139
+ if (!methods || methods.length === 0) return false;
140
+ if (methods.includes("*")) return true;
141
+ if (methods.includes(method)) return true;
142
+ if (ns === "tools" && (method === "invoke" || method === "list")) return true;
143
+ return false;
144
+ }
145
+ function checkHostApiAllowance(usages, manifest, bundleMethods) {
146
+ const errors = [];
147
+ const noAuth = new Set(bundleMethods.filter((m) => m.no_auth).map((m) => `${m.namespace}.${m.method}`));
148
+ const known = new Set(bundleMethods.map((m) => `${m.namespace}.${m.method}`));
149
+ for (const u of usages) {
150
+ const fq = `${u.ns}.${u.method}`;
151
+ if (!known.has(fq)) continue;
152
+ if (noAuth.has(fq)) continue;
153
+ if (!hostApiAllows(manifest, u.ns, u.method)) errors.push(`${u.file}:${u.line} calls anna.${fq} but manifest.ui.host_api.${u.ns} does not grant it`);
154
+ }
155
+ return errors;
156
+ }
157
+
158
+ //#endregion
159
+ //#region src/validate/ui-section.ts
160
+ /**
161
+ * Static validation of `manifest.ui` — direct port of
162
+ * `src/services/anna_app_validator.py::validate_ui_section_static` from
163
+ * matrix-nexus. Keep these algorithms byte-equivalent: any divergence here
164
+ * means `anna-app validate` will lie about what production accepts.
165
+ */
166
+ const VIEW_NAME_PATTERN = /^[a-z0-9_-]{1,40}$/;
167
+ const BUNDLE_PATH_PATTERN = /^[A-Za-z0-9_./\-]+$/;
168
+ const HOST_API_TOOL_REF_PATTERN = /^(?:required:\*|optional:\*|required:[A-Za-z0-9_.\-]+|optional:[A-Za-z0-9_.\-]+|[A-Za-z0-9_.\-]+)$/;
169
+ const CSP_OVERRIDABLE_DIRECTIVES = new Set([
170
+ "connect-src",
171
+ "img-src",
172
+ "media-src",
173
+ "font-src",
174
+ "style-src",
175
+ "script-src"
176
+ ]);
177
+ function validateUiSectionStatic(manifest) {
178
+ const ui = manifest.ui;
179
+ if (!ui) return [];
180
+ const errors = [];
181
+ const bundle = ui.bundle;
182
+ if (bundle.format && bundle.format !== "static-spa") errors.push(`ui.bundle.format 当前仅支持 'static-spa': ${bundle.format}`);
183
+ const entry = bundle.entry ?? "";
184
+ const entryClean = entry.split("#")[0].split("?")[0];
185
+ if (!BUNDLE_PATH_PATTERN.test(entryClean)) errors.push(`ui.bundle.entry 包含非法字符: ${entry}`);
186
+ if (entry.includes("..") || entry.includes("\\") || entry.includes("//")) errors.push(`ui.bundle.entry 路径穿越禁止: ${entry}`);
187
+ for (const origin of bundle.external_origins ?? []) if (!origin.startsWith("https://") || origin.includes("*")) errors.push(`ui.bundle.external_origins 必须为 https:// 前缀且不含 '*': ${origin}`);
188
+ const views = ui.views ?? [];
189
+ if (views.length < 1 || views.length > 16) errors.push("ui.views 数量必须在 1..16 之间");
190
+ const defaults = views.filter((v) => v.default).length;
191
+ if (defaults > 1) errors.push("ui.views 中只能有一个 default=true");
192
+ const seen = new Set();
193
+ for (const v of views) {
194
+ if (!VIEW_NAME_PATTERN.test(v.name)) errors.push(`非法 view name: ${v.name}`);
195
+ if (seen.has(v.name)) errors.push(`重复 view name: ${v.name}`);
196
+ seen.add(v.name);
197
+ if (v.min_size && v.default_size) {
198
+ if (v.default_size.w < v.min_size.w || v.default_size.h < v.min_size.h) errors.push(`view '${v.name}' default_size 小于 min_size`);
199
+ }
200
+ if (v.max_size && v.default_size) {
201
+ if (v.default_size.w > v.max_size.w || v.default_size.h > v.max_size.h) errors.push(`view '${v.name}' default_size 大于 max_size`);
202
+ }
203
+ }
204
+ const requiredIds = new Set((manifest.required_executas ?? []).map((r) => r.tool_id));
205
+ const optionalIds = new Set((manifest.optional_executas ?? []).map((r) => r.tool_id));
206
+ for (const ref of ui.host_api?.tools ?? []) {
207
+ if (!HOST_API_TOOL_REF_PATTERN.test(ref)) {
208
+ errors.push(`host_api.tools 非法引用: ${ref}`);
209
+ continue;
210
+ }
211
+ if (ref === "required:*" || ref === "optional:*") continue;
212
+ const bare = ref.includes(":") ? ref.split(":", 2)[1] : ref;
213
+ if (!requiredIds.has(bare) && !optionalIds.has(bare)) errors.push(`host_api.tools 引用未在 manifest 中声明的 tool_id: ${ref}`);
214
+ }
215
+ const cspOverrides = ui.csp_overrides ?? {};
216
+ const badDirectives = Object.keys(cspOverrides).filter((d) => !CSP_OVERRIDABLE_DIRECTIVES.has(d)).sort();
217
+ if (badDirectives.length) errors.push(`csp_overrides 含不允许的 directive: ${JSON.stringify(badDirectives)}`);
218
+ for (const d of ["script-src", "style-src"]) for (const v of cspOverrides[d] ?? []) if (v !== "'self'" && !v.startsWith("'sha256-") && !v.startsWith("'nonce-")) errors.push(`csp_overrides[${d}] 仅允许 'self' / 'sha256-...' / 'nonce-...': ${v}`);
219
+ return errors;
220
+ }
221
+
222
+ //#endregion
223
+ //#region src/validate/tool-id-linter.ts
224
+ const TOOL_ID_RE = /\b(?:tool|skill)-[a-z0-9][a-z0-9-]{1,80}\b/g;
225
+ const SCAN_EXTENSIONS = new Set([
226
+ ".py",
227
+ ".toml",
228
+ ".json",
229
+ ".js",
230
+ ".ts",
231
+ ".jsx",
232
+ ".tsx",
233
+ ".mjs",
234
+ ".cjs"
235
+ ]);
236
+ const SCAN_SKIP_DIRS = new Set([
237
+ "node_modules",
238
+ "dist",
239
+ "build",
240
+ ".git",
241
+ ".venv",
242
+ "venv",
243
+ "__pycache__",
244
+ ".pytest_cache",
245
+ "coverage",
246
+ "fixtures"
247
+ ]);
248
+ function walkAndScan(rootDir, fs) {
249
+ const occurrences = [];
250
+ const stack = [rootDir];
251
+ while (stack.length) {
252
+ const current = stack.pop();
253
+ let entries;
254
+ try {
255
+ entries = fs.readdirSync(current);
256
+ } catch {
257
+ continue;
258
+ }
259
+ for (const entry of entries) {
260
+ const full = join(current, entry.name);
261
+ if (entry.isDirectory()) {
262
+ if (entry.name.startsWith(".") && entry.name !== ".github") continue;
263
+ if (SCAN_SKIP_DIRS.has(entry.name)) continue;
264
+ stack.push(full);
265
+ } else if (entry.isFile()) {
266
+ const ext = extname(entry.name);
267
+ if (!SCAN_EXTENSIONS.has(ext) && entry.name !== "pyproject.toml") continue;
268
+ let content;
269
+ try {
270
+ const st = statSync(full);
271
+ if (st.size > 1024 * 1024) continue;
272
+ content = readFileSync(full, "utf-8");
273
+ } catch {
274
+ continue;
275
+ }
276
+ const rel = relative(rootDir, full);
277
+ const lines = content.split("\n");
278
+ for (let i = 0; i < lines.length; i++) {
279
+ const line = lines[i];
280
+ TOOL_ID_RE.lastIndex = 0;
281
+ let m;
282
+ while ((m = TOOL_ID_RE.exec(line)) !== null) occurrences.push({
283
+ file: rel,
284
+ line: i + 1,
285
+ toolId: m[0]
286
+ });
287
+ }
288
+ }
289
+ }
290
+ }
291
+ return occurrences;
292
+ }
293
+ function levenshtein(a, b) {
294
+ if (a === b) return 0;
295
+ if (!a.length) return b.length;
296
+ if (!b.length) return a.length;
297
+ const prev = new Array(b.length + 1);
298
+ const curr = new Array(b.length + 1);
299
+ for (let j = 0; j <= b.length; j++) prev[j] = j;
300
+ for (let i = 1; i <= a.length; i++) {
301
+ curr[0] = i;
302
+ for (let j = 1; j <= b.length; j++) {
303
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
304
+ curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
305
+ }
306
+ for (let j = 0; j <= b.length; j++) prev[j] = curr[j];
307
+ }
308
+ return prev[b.length];
309
+ }
310
+ function analyzeToolIds(input) {
311
+ const byId = new Map();
312
+ for (const o of input.occurrences) {
313
+ if (!byId.has(o.toolId)) byId.set(o.toolId, []);
314
+ byId.get(o.toolId).push(o);
315
+ }
316
+ const errors = [];
317
+ const warnings = [];
318
+ input.declaredManifestIds;
319
+ const ids = Array.from(byId.keys());
320
+ for (let i = 0; i < ids.length; i++) for (let j = i + 1; j < ids.length; j++) {
321
+ const a = ids[i];
322
+ const b = ids[j];
323
+ if (Math.abs(a.length - b.length) > 1) continue;
324
+ if (levenshtein(a, b) > 1) continue;
325
+ const aLocs = byId.get(a);
326
+ const bLocs = byId.get(b);
327
+ const aFiles = new Set(aLocs.map((o) => o.file));
328
+ const bFiles = new Set(bLocs.map((o) => o.file));
329
+ const isolated = aFiles.size === 1 || bFiles.size === 1;
330
+ if (isolated) {
331
+ const minor = aFiles.size === 1 ? aLocs : bLocs;
332
+ const major = aFiles.size === 1 ? bLocs : aLocs;
333
+ errors.push(`Likely typo: "${minor[0].toolId}" (only at ${minor.map((o) => `${o.file}:${o.line}`).join(", ")}) differs by ≤1 char from "${major[0].toolId}" (used in ${major.length} place(s))`);
334
+ }
335
+ }
336
+ return {
337
+ occurrences: input.occurrences,
338
+ byId,
339
+ errors,
340
+ warnings
341
+ };
342
+ }
343
+
344
+ //#endregion
345
+ //#region src/commands/validate.ts
346
+ function readManifest(path) {
347
+ if (!existsSync(path)) throw new Error(`manifest not found: ${path}`);
348
+ return JSON.parse(readFileSync(path, "utf-8"));
349
+ }
350
+ function readBundleSources(dir) {
351
+ if (!existsSync(dir)) return [];
352
+ const out = [];
353
+ const stack = [dir];
354
+ while (stack.length) {
355
+ const cur = stack.pop();
356
+ let entries;
357
+ try {
358
+ entries = readdirSync(cur, { withFileTypes: true });
359
+ } catch {
360
+ continue;
361
+ }
362
+ for (const e of entries) {
363
+ const full = `${cur}/${e.name}`;
364
+ if (e.isDirectory()) {
365
+ if (e.name === "node_modules" || e.name.startsWith(".")) continue;
366
+ stack.push(full);
367
+ } else if (/\.(js|ts|jsx|tsx|mjs|cjs)$/.test(e.name)) try {
368
+ out.push({
369
+ path: full,
370
+ content: readFileSync(full, "utf-8")
371
+ });
372
+ } catch {}
373
+ }
374
+ }
375
+ return out;
376
+ }
377
+ function runValidate(opts) {
378
+ const errors = [];
379
+ const warnings = [];
380
+ const bundle = loadSchemaBundle();
381
+ console.log(kleur.gray(`[validate] @anna-ai/app-schema dispatcher_version=${bundle.dispatcherVersion} (${bundle.dir})`));
382
+ const manifest = readManifest(opts.manifestPath);
383
+ const schemaIssues = validateManifestSchema(bundle, manifest);
384
+ for (const i of schemaIssues) errors.push(`schema ${i.path}: ${i.message}`);
385
+ for (const e of validateUiSectionStatic(manifest)) errors.push(`ui: ${e}`);
386
+ const occ = walkAndScan(opts.cwd, { readdirSync: (p) => readdirSync(p, { withFileTypes: true }).map((e) => ({
387
+ name: e.name,
388
+ isDirectory: () => e.isDirectory(),
389
+ isFile: () => e.isFile()
390
+ })) });
391
+ const declared = [...(manifest.required_executas ?? []).map((r) => r.tool_id), ...(manifest.optional_executas ?? []).map((r) => r.tool_id)];
392
+ const idReport = analyzeToolIds({
393
+ occurrences: occ,
394
+ declaredManifestIds: declared
395
+ });
396
+ for (const e of idReport.errors) errors.push(`tool-id: ${e}`);
397
+ for (const w of idReport.warnings) warnings.push(`tool-id: ${w}`);
398
+ if (opts.strict) {
399
+ const bundleDir = opts.bundleDir ?? resolve(opts.cwd, "bundle");
400
+ const sources = readBundleSources(bundleDir);
401
+ const usages = scanHostApiCalls(sources);
402
+ const aclErrs = checkHostApiAllowance(usages, manifest, bundle.methods);
403
+ for (const e of aclErrs) errors.push(`host-api: ${e}`);
404
+ }
405
+ return {
406
+ errors,
407
+ warnings
408
+ };
409
+ }
410
+ function printResult(result) {
411
+ for (const w of result.warnings) console.warn(kleur.yellow(`⚠ ${w}`));
412
+ for (const e of result.errors) console.error(kleur.red(`✗ ${e}`));
413
+ if (result.errors.length === 0) {
414
+ console.log(kleur.green(`✓ validate passed${result.warnings.length ? ` (${result.warnings.length} warning(s))` : ""}`));
415
+ return 0;
416
+ }
417
+ console.error(kleur.red(`✗ validate failed: ${result.errors.length} error(s)`));
418
+ return 1;
419
+ }
420
+
57
421
  //#endregion
58
422
  //#region src/cli.ts
423
+ const pkg = createRequire(import.meta.url)("../package.json");
59
424
  const program = new Command();
60
- program.name("anna-app").description("Anna App developer CLI (scaffold, validate, harness)").version("0.1.0");
425
+ program.name("anna-app").description("Anna App developer CLI (scaffold, validate, harness)").version(pkg.version);
61
426
  program.command("init <dir>").description("Scaffold a new Anna App project").option("--slug <slug>", "App slug (lowercase, hyphens)", "").option("--template <name>", "Template to use", "minimal").option("--force", "Overwrite existing dir if non-empty", false).action((dir, opts) => {
62
427
  const slug = opts.slug || dir.split(/[\\/]/).pop() || "my-anna-app";
63
428
  const code = runInit({
@@ -68,8 +433,19 @@ program.command("init <dir>").description("Scaffold a new Anna App project").opt
68
433
  });
69
434
  process.exit(code);
70
435
  });
436
+ program.command("validate").description("Run schema + ACL checks on a manifest+bundle").option("--manifest <path>", "manifest.json path", "manifest.json").option("--bundle <dir>", "bundle directory (default: ./bundle)").option("--strict", "Enable strict checks (host_api ACL grep)", false).action(async (opts) => {
437
+ const cwd = process.cwd();
438
+ const result = runValidate({
439
+ cwd,
440
+ manifestPath: resolve(cwd, opts.manifest),
441
+ bundleDir: opts.bundle ? resolve(cwd, opts.bundle) : null,
442
+ strict: opts.strict
443
+ });
444
+ const code = printResult(result);
445
+ process.exit(code);
446
+ });
71
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) => {
72
- const { runDev } = await import("./dev-rBqMTjjY.js");
448
+ const { runDev } = await import("./dev-D-Tru6gP.js");
73
449
  const code = await runDev({
74
450
  cwd: opts.cwd ?? process.cwd(),
75
451
  manifestPath: opts.manifest,
@@ -109,7 +485,7 @@ fixture.command("replay <file>").description("Dry-run replay of a harness record
109
485
  process.exit(code);
110
486
  });
111
487
  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) => {
112
- const { runDoctor } = await import("./doctor-yxWmMSJc.js");
488
+ const { runDoctor } = await import("./doctor-BmR0POfL.js");
113
489
  const code = await runDoctor({ matrixNexusRoot: opts.matrixNexusRoot });
114
490
  process.exit(code);
115
491
  });
@@ -181,7 +181,7 @@
181
181
  <span id="iframe-src">awaiting session…</span>
182
182
  <span class="size" id="size"></span>
183
183
  </div>
184
- <iframe id="app" sandbox="allow-scripts allow-forms"></iframe>
184
+ <iframe id="app" sandbox="allow-scripts allow-same-origin allow-forms allow-popups"></iframe>
185
185
  </div>
186
186
  </main>
187
187
  <aside>
@@ -1,5 +1,5 @@
1
- import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
2
1
  import { dirname, isAbsolute, resolve } from "node:path";
2
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
3
3
  import { bold, cyan, dim, green, red, yellow } from "kleur/colors";
4
4
 
5
5
  //#region src/commands/dev.ts
@@ -31,7 +31,7 @@ async function runDev(opts) {
31
31
  const matrixNexusRoot = await resolveMatrixNexusRoot(opts.matrixNexusRoot, cwd);
32
32
  const mode = matrixNexusRoot ? "nexus-source" : "uvx";
33
33
  const { PythonBridge, PINNED_RUNTIME_VERSION } = await import("./bridge-BDBECvV1.js");
34
- const { HarnessServer } = await import("./server-B6-Qdluv.js");
34
+ const { HarnessServer } = await import("./server-gl345fFN.js");
35
35
  const bridge = new PythonBridge({
36
36
  mode,
37
37
  matrixNexusRoot: matrixNexusRoot ?? void 0,
@@ -1,6 +1,6 @@
1
1
  import { PINNED_RUNTIME_VERSION } from "./bridge-CBcQUQGU.js";
2
- import { existsSync, statSync } from "node:fs";
3
2
  import { dirname, isAbsolute, resolve } from "node:path";
3
+ import { existsSync, statSync } from "node:fs";
4
4
  import { bold, dim, green, red, yellow } from "kleur/colors";
5
5
  import { spawnSync } from "node:child_process";
6
6
  import { homedir } from "node:os";
@@ -1,5 +1,6 @@
1
- import { createReadStream, statSync, watch } from "node:fs";
2
1
  import { dirname, join, normalize, resolve } from "node:path";
2
+ import { createRequire } from "node:module";
3
+ import { createReadStream, statSync, watch } from "node:fs";
3
4
  import { fileURLToPath } from "node:url";
4
5
  import { readFile } from "node:fs/promises";
5
6
  import { createServer } from "node:http";
@@ -197,11 +198,16 @@ var HarnessServer = class {
197
198
  }
198
199
  }
199
200
  async serveSdk(pathname, res) {
200
- const root = this.cfg.matrixNexusRoot;
201
- if (!root) return this.text(res, 404, "SDK assets not available in uvx mode");
202
- const rel = pathname.replace(/^\/static\//, "");
203
- const abs = resolve(root, "static", rel);
204
- if (!abs.startsWith(resolve(root, "static"))) return this.text(res, 403, "forbidden");
201
+ const sdkRel = pathname.replace(/^\/static\/anna-apps\/_sdk\/[^/]+\//, "");
202
+ let distRoot;
203
+ try {
204
+ const req = createRequire(import.meta.url);
205
+ distRoot = dirname(req.resolve("@anna-ai/app-runtime"));
206
+ } catch (e) {
207
+ return this.text(res, 500, `@anna-ai/app-runtime is not installed: ${e.message}`);
208
+ }
209
+ const abs = resolve(distRoot, sdkRel);
210
+ if (!abs.startsWith(distRoot)) return this.text(res, 403, "forbidden");
205
211
  return this.serveFile(abs, res);
206
212
  }
207
213
  async serveBundleAsset(rel, res) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anna-ai/cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.9",
4
4
  "description": "Anna App developer CLI: scaffold, validate, harness (Phase 2 MVP: init + validate).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -20,7 +20,6 @@
20
20
  "files": [
21
21
  "dist/",
22
22
  "templates/",
23
- "vendor/",
24
23
  "README.md"
25
24
  ],
26
25
  "scripts": {
@@ -29,11 +28,12 @@
29
28
  "test": "vitest run",
30
29
  "test:watch": "vitest",
31
30
  "lint": "tsc --noEmit",
32
- "sync:schema": "node scripts/sync-schema.mjs",
33
31
  "check:runtime-pin": "node scripts/check-runtime-pin.mjs",
34
32
  "prepublishOnly": "pnpm lint && pnpm test && pnpm build"
35
33
  },
36
34
  "dependencies": {
35
+ "@anna-ai/app-runtime": "^0.1.0",
36
+ "@anna-ai/app-schema": "^0.1.0",
37
37
  "ajv": "^8.17.1",
38
38
  "ajv-formats": "^3.0.1",
39
39
  "commander": "^12.1.0",
@@ -4,7 +4,7 @@
4
4
  <meta charset="utf-8" />
5
5
  <title>__SLUG__</title>
6
6
  <script src="/static/anna-apps/_sdk/0.1.0/index.js" defer></script>
7
- <script src="./app.js" type="module" defer></script>
7
+ <script src="./app.js" defer></script>
8
8
  </head>
9
9
  <body>
10
10
  <h1>__SLUG__</h1>
@@ -21,9 +21,13 @@ MANIFEST = {
21
21
 
22
22
 
23
23
  def invoke(method: str, args: dict) -> dict:
24
+ # Tool methods MUST return the dispatcher contract envelope:
25
+ # {"success": True, "data": <payload-dict>}
26
+ # {"success": False, "error": "<reason>"}
27
+ # Anything else surfaces to the iframe as `tool_failed`.
24
28
  if method == "ping":
25
- return {"pong": True}
26
- raise ValueError(f"unknown method: {method}")
29
+ return {"success": True, "data": {"pong": True}}
30
+ return {"success": False, "error": f"unknown method: {method}"}
27
31
 
28
32
 
29
33
  def main() -> None:
@@ -1,6 +1,6 @@
1
1
  [build-system]
2
- requires = ["hatchling"]
3
- build-backend = "hatchling.build"
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "__TOOL_ID__"
@@ -9,4 +9,7 @@ description = "Executa for the __SLUG__ Anna App"
9
9
  requires-python = ">=3.10"
10
10
 
11
11
  [project.scripts]
12
- "__TOOL_ID__" = "__SLUG___executa.plugin:main"
12
+ "__TOOL_ID__" = "__SLUG_PY___plugin:main"
13
+
14
+ [tool.setuptools]
15
+ py-modules = ["__SLUG_PY___plugin"]
@@ -1,22 +0,0 @@
1
- # @anna/app-schema (v0.1.0)
2
-
3
- Versioned schema bundle for the Anna App platform. Generated by
4
- `scripts/export_app_schema.py` in matrix-nexus; do not edit by hand.
5
-
6
- ## Contents
7
-
8
- * `manifest/AppManifest.json` — JSON Schema for the app manifest.
9
- * `manifest/UiManifestSection.json` — JSON Schema for the `ui` block.
10
- * `host_api/methods.json` — Flat `(namespace, method)` table mirroring
11
- the dispatcher's `_DISPATCH` map. `no_auth=true` methods skip the
12
- `host_api` ACL gate.
13
- * `events/AnnaAppEvent.json` — SSE event union (the `kind` enum).
14
- * `dispatcher_version.txt` — Single source-of-truth bundle version;
15
- harnesses must refuse to start when their copy disagrees.
16
-
17
- ## Regenerate
18
-
19
- ```bash
20
- uv run python scripts/export_app_schema.py # write
21
- uv run python scripts/export_app_schema.py --check # CI gate
22
- ```
@@ -1 +0,0 @@
1
- 0.1.0
@@ -1,38 +0,0 @@
1
- {
2
- "$id": "https://schemas.anna.partners/anna-app-event.json",
3
- "$schema": "https://json-schema.org/draft/2020-12/schema",
4
- "additionalProperties": true,
5
- "description": "SSE envelope union for `event: data_model/AnnaAppEvent` frames. Bundle consumers should treat unknown `kind` values as forward-compatible additions and ignore them.",
6
- "properties": {
7
- "by_client_id": {
8
- "type": [
9
- "string",
10
- "null"
11
- ]
12
- },
13
- "kind": {
14
- "enum": [
15
- "artifact_appended",
16
- "chat_message_from_app",
17
- "close_view",
18
- "geometry_changed",
19
- "open_view",
20
- "ping",
21
- "runtime_state_synced",
22
- "status_changed",
23
- "title_changed",
24
- "window_focus_changed"
25
- ],
26
- "type": "string"
27
- },
28
- "window_uuid": {
29
- "type": "string"
30
- }
31
- },
32
- "required": [
33
- "kind"
34
- ],
35
- "title": "AnnaAppEvent",
36
- "type": "object",
37
- "version": "0.1.0"
38
- }