@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 +382 -6
- package/dist/dashboard.html +1 -1
- package/dist/{dev-DImwL-ql.js → dev-D-Tru6gP.js} +2 -2
- package/dist/{doctor-yxWmMSJc.js → doctor-BmR0POfL.js} +1 -1
- package/dist/{server-BZULnh6k.js → server-gl345fFN.js} +2 -2
- package/package.json +1 -1
- package/templates/minimal/bundle/index.html +1 -1
- package/templates/minimal/executas/__SLUG__/{plugin.py → __SLUG_PY___plugin.py} +6 -2
- package/templates/minimal/executas/__SLUG__/pyproject.toml +6 -3
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
|
-
|
|
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(
|
|
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-
|
|
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-
|
|
488
|
+
const { runDoctor } = await import("./doctor-BmR0POfL.js");
|
|
113
489
|
const code = await runDoctor({ matrixNexusRoot: opts.matrixNexusRoot });
|
|
114
490
|
process.exit(code);
|
|
115
491
|
});
|
package/dist/dashboard.html
CHANGED
|
@@ -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-
|
|
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
|
@@ -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
|
-
|
|
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 = ["
|
|
3
|
-
build-backend = "
|
|
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__" = "
|
|
12
|
+
"__TOOL_ID__" = "__SLUG_PY___plugin:main"
|
|
13
|
+
|
|
14
|
+
[tool.setuptools]
|
|
15
|
+
py-modules = ["__SLUG_PY___plugin"]
|