@anna-ai/cli 0.1.4 → 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/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-DImwL-ql.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-BZULnh6k.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,9 +1,9 @@
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";
6
- import { createRequire } from "node:module";
7
7
  import { WebSocketServer } from "ws";
8
8
 
9
9
  //#region src/harness/server.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anna-ai/cli",
3
- "version": "0.1.4",
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",
@@ -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"]