@anna-ai/cli 0.1.4 → 0.1.11
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 +64 -3
- package/dist/{bridge-BDBECvV1.js → bridge-BIO7ilgO.js} +1 -1
- package/dist/{bridge-CBcQUQGU.js → bridge-Cpm3D2Wk.js} +1 -1
- package/dist/cli.js +427 -12
- package/dist/credentials-ggdaz_-7.js +122 -0
- package/dist/dashboard.html +1 -1
- package/dist/dev-BPIUX2Nh.js +366 -0
- package/dist/{doctor-yxWmMSJc.js → doctor-R1pjmBDG.js} +3 -3
- package/dist/login-D8cmvBb6.js +102 -0
- package/dist/logout-P6L9VU4W.js +23 -0
- package/dist/server-Cd5Lo-2v.js +678 -0
- package/dist/test/index.js +45 -1
- package/dist/whoami-jqlQwe7Z.js +43 -0
- package/package.json +2 -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/dev-DImwL-ql.js +0 -163
- package/dist/server-BZULnh6k.js +0 -261
- /package/dist/{fixture-BGjMtqWA.js → fixture-RceUUd84.js} +0 -0
package/README.md
CHANGED
|
@@ -48,17 +48,62 @@ Layered fail-fast checks: JSON Schema → `ui` static → cross-file `tool_id`
|
|
|
48
48
|
linter (with Levenshtein-1 typo detection) → `--strict` host_api ACL grep
|
|
49
49
|
of bundle JS/TS.
|
|
50
50
|
|
|
51
|
-
### `anna-app dev [--manifest …] [--bundle …] [--port 5180] [--matrix-nexus-root <path>]`
|
|
51
|
+
### `anna-app dev [--manifest …] [--bundle …] [--port 5180] [--matrix-nexus-root <path>] [--executa <spec>…]`
|
|
52
52
|
|
|
53
53
|
Boots the local harness:
|
|
54
54
|
|
|
55
55
|
- Spawns the Python `anna-app-bridge` (production dispatcher reused via
|
|
56
56
|
`WindowStoreProtocol`).
|
|
57
57
|
- Serves a mock dashboard at `http://localhost:<port>/`.
|
|
58
|
-
- Auto-discovers `<manifest-dir>/executas/<name
|
|
59
|
-
registers them in the in-process
|
|
58
|
+
- Auto-discovers `<manifest-dir>/executas/<name>/` plugins (Python /
|
|
59
|
+
Node.js / Go / pre-built binary) and registers them in the in-process
|
|
60
|
+
`ExecutaPool`. See "Multi-language executas" below.
|
|
60
61
|
- Hot-reloads the bundle on disk changes (use `--no-watch` to disable).
|
|
61
62
|
|
|
63
|
+
#### Multi-language executas
|
|
64
|
+
|
|
65
|
+
Each subdirectory of `<manifest-dir>/executas/` is launched according to
|
|
66
|
+
the first sentinel that matches:
|
|
67
|
+
|
|
68
|
+
| # | Sentinel | Type | Default launch |
|
|
69
|
+
| - | ----------------------- | -------- | ------------------------------------------------------------- |
|
|
70
|
+
| 0 | `executa.json` | (any) | `command` field, else type-specific default below |
|
|
71
|
+
| 1 | `pyproject.toml` | `python` | `uv run --project <dir> <tool_id>` |
|
|
72
|
+
| 2 | `package.json` | `node` | `node <bin[tool_id] \| bin \| main \| module>` |
|
|
73
|
+
| 3 | `go.mod` (alone) | `go` | requires `executa.json` declaring `type: "go"` |
|
|
74
|
+
| 4 | `bin/<dirname>` exec | `binary` | runs the executable directly |
|
|
75
|
+
|
|
76
|
+
`executa.json` (recommended for clarity, required for Go and pre-built
|
|
77
|
+
binary tools):
|
|
78
|
+
|
|
79
|
+
```jsonc
|
|
80
|
+
{
|
|
81
|
+
"tool_id": "tool-yourhandle-foo-abcd1234",
|
|
82
|
+
"type": "python" | "node" | "go" | "binary",
|
|
83
|
+
"command": ["…"], // optional; full override of type defaults
|
|
84
|
+
"enabled": true // optional; default true. Set false to skip
|
|
85
|
+
// (useful when shipping multiple language
|
|
86
|
+
// flavours of the same tool_id).
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The `--executa <spec>` flag (repeatable) registers an out-of-tree
|
|
91
|
+
executa, or fully overrides auto-discovery for the run. Spec syntax:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# Auto-detect from the dir (same rules as in-tree discovery):
|
|
95
|
+
anna-app dev --executa dir=./vendor/external-tool
|
|
96
|
+
|
|
97
|
+
# Force type when there's no executa.json:
|
|
98
|
+
anna-app dev --executa dir=./executas/foo,type=go
|
|
99
|
+
|
|
100
|
+
# Fully explicit:
|
|
101
|
+
anna-app dev --executa dir=./executas/foo,tool_id=tool-h-foo-12345678,command="node plugin.js"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
For the full discovery / `executa.json` reference, see
|
|
105
|
+
[`anna-executa-examples/docs/multi-language-anna-apps.md`](https://github.com/openclaw/anna-executa-examples/blob/main/docs/multi-language-anna-apps.md).
|
|
106
|
+
|
|
62
107
|
Two runtime modes (auto-selected):
|
|
63
108
|
|
|
64
109
|
| Mode | When | Command |
|
|
@@ -130,6 +175,22 @@ public npm package [`@anna-ai/app-runtime`](https://www.npmjs.com/package/@anna-
|
|
|
130
175
|
which is declared as a normal dependency. No vendored copy, no sync
|
|
131
176
|
step.
|
|
132
177
|
|
|
178
|
+
## Persistent storage (APS)
|
|
179
|
+
|
|
180
|
+
Anna 1.2+ exposes a per-user JSON-RPC storage surface under the
|
|
181
|
+
`storage/*` namespace. Plugins authored with this CLI can opt in by:
|
|
182
|
+
|
|
183
|
+
1. Declaring `storage.user` (or `.app`/`.tool`) in the manifest's
|
|
184
|
+
`host_capabilities` array.
|
|
185
|
+
2. Negotiating `client_capabilities.storage = {}` during `initialize`.
|
|
186
|
+
3. Asking the user to grant storage in the Anna admin panel.
|
|
187
|
+
|
|
188
|
+
See the protocol & best-practice guide at
|
|
189
|
+
[anna-executa-examples/docs/persistent-storage.md](https://github.com/openclaw/anna-executa-examples/blob/main/docs/persistent-storage.md)
|
|
190
|
+
for wire format, error codes, and a worked OCR-cache example. The
|
|
191
|
+
local `dev` harness mocks APS by default so end-to-end tests do not
|
|
192
|
+
need network access.
|
|
193
|
+
|
|
133
194
|
## Roadmap
|
|
134
195
|
|
|
135
196
|
| Phase | Status | Scope |
|
|
@@ -9,7 +9,7 @@ import { createInterface } from "node:readline";
|
|
|
9
9
|
* `uvx <pkg>@<version>` so end users always run the dispatcher version
|
|
10
10
|
* the CLI was tested against.
|
|
11
11
|
*/
|
|
12
|
-
const PINNED_RUNTIME_VERSION = "0.2.
|
|
12
|
+
const PINNED_RUNTIME_VERSION = "0.2.0a2";
|
|
13
13
|
var PythonBridge = class {
|
|
14
14
|
proc = null;
|
|
15
15
|
nextId = 1;
|
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,10 +433,34 @@ program.command("init <dir>").description("Scaffold a new Anna App project").opt
|
|
|
68
433
|
});
|
|
69
434
|
process.exit(code);
|
|
70
435
|
});
|
|
71
|
-
program.command("
|
|
72
|
-
const
|
|
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
|
+
});
|
|
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)").option("--executa <spec>", "Explicit executa registration; repeatable. Spec: comma-separated key=value (dir=<path>[,tool_id=<id>][,type=python|node|go|binary][,command=\"<argv>\"]). When only `dir=` is given, the executa is auto-detected from executa.json / pyproject.toml / package.json / go.mod. Overrides directory auto-discovery under <manifest-dir>/executas/.", (val, prev) => prev ? [...prev, val] : [val]).option("--no-llm", "Disable LLM bridge (anna.llm/agent return llm_disabled)", false).option("--mock-llm <fixture>", "Serve canned LLM responses from a JSONL fixture").option("--llm-account <host>", "Saved account host to use (default: current)").option("--llm-app-id <id>", "app_id passed to /dev/session/mint").action(async (opts) => {
|
|
448
|
+
const { runDev, parseExecutaSpec } = await import("./dev-BPIUX2Nh.js");
|
|
449
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
450
|
+
let executas;
|
|
451
|
+
if (opts.executa && opts.executa.length > 0) {
|
|
452
|
+
executas = [];
|
|
453
|
+
for (const spec of opts.executa) {
|
|
454
|
+
const r = parseExecutaSpec(spec, cwd);
|
|
455
|
+
if (r instanceof Error) {
|
|
456
|
+
console.error(`✗ --executa: ${r.message}`);
|
|
457
|
+
process.exit(2);
|
|
458
|
+
}
|
|
459
|
+
executas.push(r);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
73
462
|
const code = await runDev({
|
|
74
|
-
cwd
|
|
463
|
+
cwd,
|
|
75
464
|
manifestPath: opts.manifest,
|
|
76
465
|
bundleDir: opts.bundle,
|
|
77
466
|
slug: opts.slug,
|
|
@@ -79,13 +468,18 @@ program.command("dev").description("Run a local harness (in-process dispatcher +
|
|
|
79
468
|
matrixNexusRoot: opts.matrixNexusRoot,
|
|
80
469
|
port: Number.parseInt(opts.port, 10),
|
|
81
470
|
userId: Number.parseInt(opts.userId, 10),
|
|
82
|
-
noWatch: opts.watch === false
|
|
471
|
+
noWatch: opts.watch === false,
|
|
472
|
+
executas,
|
|
473
|
+
noLlm: opts.llm === false,
|
|
474
|
+
mockLlm: opts.mockLlm,
|
|
475
|
+
llmAccount: opts.llmAccount,
|
|
476
|
+
llmAppId: opts.llmAppId ? Number.parseInt(opts.llmAppId, 10) : void 0
|
|
83
477
|
});
|
|
84
478
|
process.exit(code);
|
|
85
479
|
});
|
|
86
480
|
const fixture = program.command("fixture").description("Inspect / replay harness recordings (Phase 6)");
|
|
87
481
|
fixture.command("verify <file>").description("Schema + invariant checks on a harness JSONL recording").option("--json", "Emit machine-readable JSON", false).action(async (file, opts) => {
|
|
88
|
-
const { runFixtureVerify } = await import("./fixture-
|
|
482
|
+
const { runFixtureVerify } = await import("./fixture-RceUUd84.js");
|
|
89
483
|
const code = await runFixtureVerify({
|
|
90
484
|
file,
|
|
91
485
|
json: opts.json
|
|
@@ -93,7 +487,7 @@ fixture.command("verify <file>").description("Schema + invariant checks on a har
|
|
|
93
487
|
process.exit(code);
|
|
94
488
|
});
|
|
95
489
|
fixture.command("summarize <file>").description("Print a human-readable digest of a harness recording").option("--json", "Emit machine-readable JSON", false).action(async (file, opts) => {
|
|
96
|
-
const { runFixtureSummarize } = await import("./fixture-
|
|
490
|
+
const { runFixtureSummarize } = await import("./fixture-RceUUd84.js");
|
|
97
491
|
const code = await runFixtureSummarize({
|
|
98
492
|
file,
|
|
99
493
|
json: opts.json
|
|
@@ -101,7 +495,7 @@ fixture.command("summarize <file>").description("Print a human-readable digest o
|
|
|
101
495
|
process.exit(code);
|
|
102
496
|
});
|
|
103
497
|
fixture.command("replay <file>").description("Dry-run replay of a harness recording (Phase 6 MVP)").option("--manifest <path>", "manifest.json path", "manifest.json").action(async (file, opts) => {
|
|
104
|
-
const { runFixtureReplay } = await import("./fixture-
|
|
498
|
+
const { runFixtureReplay } = await import("./fixture-RceUUd84.js");
|
|
105
499
|
const code = await runFixtureReplay({
|
|
106
500
|
file,
|
|
107
501
|
manifest: opts.manifest
|
|
@@ -109,10 +503,31 @@ fixture.command("replay <file>").description("Dry-run replay of a harness record
|
|
|
109
503
|
process.exit(code);
|
|
110
504
|
});
|
|
111
505
|
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-
|
|
506
|
+
const { runDoctor } = await import("./doctor-R1pjmBDG.js");
|
|
113
507
|
const code = await runDoctor({ matrixNexusRoot: opts.matrixNexusRoot });
|
|
114
508
|
process.exit(code);
|
|
115
509
|
});
|
|
510
|
+
program.command("login").description("Device-flow login against a nexus host; saves a PAT to ~/.config/anna/credentials.json").requiredOption("--host <url>", "nexus base URL, e.g. https://nexus.example.com").option("--no-browser", "Do not open a browser window automatically", false).action(async (opts) => {
|
|
511
|
+
const { runLogin } = await import("./login-D8cmvBb6.js");
|
|
512
|
+
const code = await runLogin({
|
|
513
|
+
host: opts.host,
|
|
514
|
+
noBrowser: opts.browser === false
|
|
515
|
+
});
|
|
516
|
+
process.exit(code);
|
|
517
|
+
});
|
|
518
|
+
program.command("logout").description("Remove a saved PAT entry").option("--host <url>", "Account to remove (default: current)").option("--all", "Remove every saved account", false).action(async (opts) => {
|
|
519
|
+
const { runLogout } = await import("./logout-P6L9VU4W.js");
|
|
520
|
+
const code = await runLogout({
|
|
521
|
+
host: opts.host,
|
|
522
|
+
all: opts.all
|
|
523
|
+
});
|
|
524
|
+
process.exit(code);
|
|
525
|
+
});
|
|
526
|
+
program.command("whoami").description("Show the current account (and any others)").option("--json", "Emit machine-readable JSON", false).action(async (opts) => {
|
|
527
|
+
const { runWhoami } = await import("./whoami-jqlQwe7Z.js");
|
|
528
|
+
const code = await runWhoami({ json: opts.json });
|
|
529
|
+
process.exit(code);
|
|
530
|
+
});
|
|
116
531
|
program.parseAsync(process.argv).catch((e) => {
|
|
117
532
|
console.error(e);
|
|
118
533
|
process.exit(2);
|