@anna-ai/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,260 @@
1
+ //#region src/test/host-api-acl.ts
2
+ const NAMESPACED = {
3
+ tools: true,
4
+ chat: true,
5
+ artifact: true,
6
+ llm: true,
7
+ fs: true,
8
+ storage: true,
9
+ prefs: true,
10
+ window: true
11
+ };
12
+ function deriveAcl(manifest) {
13
+ const ha = manifest.ui?.host_api ?? {};
14
+ const allowed = new Set();
15
+ const allowedTools = new Set();
16
+ let toolsWildcard = false;
17
+ for (const ref of ha.tools ?? []) {
18
+ if (ref === "required:*" || ref === "optional:*" || ref === "*") {
19
+ toolsWildcard = true;
20
+ continue;
21
+ }
22
+ const bare = ref.includes(":") ? ref.split(":", 2)[1] : ref;
23
+ allowedTools.add(bare);
24
+ }
25
+ if (allowedTools.size > 0 || toolsWildcard) allowed.add("tools.invoke");
26
+ for (const ns of Object.keys(NAMESPACED)) {
27
+ if (ns === "tools") continue;
28
+ for (const method of ha[ns] ?? []) allowed.add(`${ns}.${method}`);
29
+ }
30
+ return {
31
+ allowed,
32
+ allowedTools,
33
+ toolsWildcard
34
+ };
35
+ }
36
+ function isToolAllowed(acl, toolId) {
37
+ if (acl.toolsWildcard) return true;
38
+ return acl.allowedTools.has(toolId);
39
+ }
40
+
41
+ //#endregion
42
+ //#region src/test/runtime.ts
43
+ const DEFAULT_TOKEN_TTL_MS = 5 * 60 * 1e3;
44
+ function randHex(n) {
45
+ let s = "";
46
+ for (let i = 0; i < n; i += 1) s += Math.floor(Math.random() * 16).toString(16);
47
+ return s;
48
+ }
49
+ var CallLogImpl = class {
50
+ records = [];
51
+ push(r) {
52
+ this.records.push(r);
53
+ }
54
+ all() {
55
+ return this.records.slice();
56
+ }
57
+ byNs(prefix) {
58
+ return this.records.filter((r) => `${r.ns}.${r.method}` === prefix || r.ns === prefix);
59
+ }
60
+ last() {
61
+ return this.records[this.records.length - 1] ?? null;
62
+ }
63
+ lastOf(prefix) {
64
+ const xs = this.byNs(prefix);
65
+ return xs[xs.length - 1] ?? null;
66
+ }
67
+ clear() {
68
+ this.records.length = 0;
69
+ }
70
+ };
71
+ var EventBusImpl = class {
72
+ listeners = new Map();
73
+ wildcard = new Set();
74
+ emit(name, payload) {
75
+ for (const fn of this.listeners.get(name) ?? []) try {
76
+ fn(payload);
77
+ } catch {}
78
+ for (const fn of this.wildcard) try {
79
+ fn(name, payload);
80
+ } catch {}
81
+ }
82
+ on(name, fn) {
83
+ let set = this.listeners.get(name);
84
+ if (!set) {
85
+ set = new Set();
86
+ this.listeners.set(name, set);
87
+ }
88
+ set.add(fn);
89
+ return () => set.delete(fn);
90
+ }
91
+ /** Internal: tap all events for the auth.refresh handler etc. */
92
+ onAny(fn) {
93
+ this.wildcard.add(fn);
94
+ return () => this.wildcard.delete(fn);
95
+ }
96
+ };
97
+ /**
98
+ * Default mock implementations for namespaces that have safe in-memory
99
+ * behavior. Bundle authors can override any of these via `mocks`.
100
+ */
101
+ function makeDefaultMocks(state) {
102
+ return {
103
+ "storage.get": (args) => {
104
+ const key = args.key ?? "";
105
+ return state.storage.get(key) ?? null;
106
+ },
107
+ "storage.set": (args) => {
108
+ const { key, value } = args;
109
+ state.storage.set(key, value);
110
+ return null;
111
+ },
112
+ "storage.delete": (args) => {
113
+ const key = args.key ?? "";
114
+ state.storage.delete(key);
115
+ return null;
116
+ },
117
+ "prefs.get": (args) => {
118
+ const key = args.key ?? "";
119
+ return state.prefs.get(key) ?? null;
120
+ },
121
+ "prefs.set": (args) => {
122
+ const { key, value } = args;
123
+ state.prefs.set(key, value);
124
+ return null;
125
+ },
126
+ "chat.write_message": () => null,
127
+ "window.set_title": () => null
128
+ };
129
+ }
130
+ async function mountBundle(opts) {
131
+ const acl = deriveAcl(opts.manifest);
132
+ const calls = new CallLogImpl();
133
+ const events = new EventBusImpl();
134
+ const startedAt = Date.now();
135
+ const ttl = opts.tokenTtlMs ?? DEFAULT_TOKEN_TTL_MS;
136
+ const wid = opts.wid ?? `harness-${randHex(8)}`;
137
+ let token = opts.token ?? randHex(24);
138
+ const state = {
139
+ storage: new Map(),
140
+ prefs: new Map()
141
+ };
142
+ const mocks = {
143
+ ...makeDefaultMocks(state),
144
+ ...opts.mocks ?? {}
145
+ };
146
+ events.onAny((name, payload) => {
147
+ if (name === "auth.refresh") {
148
+ const next = payload?.token;
149
+ if (typeof next === "string" && next) token = next;
150
+ }
151
+ });
152
+ let seq = 0;
153
+ async function dispatch(ns, method, args) {
154
+ seq += 1;
155
+ const rec = {
156
+ seq,
157
+ t: Date.now() - startedAt,
158
+ ns,
159
+ method,
160
+ args,
161
+ outcome: null
162
+ };
163
+ calls.push(rec);
164
+ const key = `${ns}.${method}`;
165
+ if (!acl.allowed.has(key)) {
166
+ rec.outcome = "denied";
167
+ rec.errorCode = "DENIED";
168
+ rec.errorMessage = `host API '${key}' not declared in manifest.ui.host_api`;
169
+ throw new HostApiError(rec.errorCode, rec.errorMessage);
170
+ }
171
+ if (ns === "tools" && method === "invoke") {
172
+ const toolId = args.tool_id ?? "";
173
+ if (!isToolAllowed(acl, toolId)) {
174
+ rec.outcome = "denied";
175
+ rec.errorCode = "DENIED";
176
+ rec.errorMessage = `tool '${toolId}' not declared in manifest.ui.host_api.tools`;
177
+ throw new HostApiError(rec.errorCode, rec.errorMessage);
178
+ }
179
+ }
180
+ const handler = mocks[key];
181
+ if (!handler) {
182
+ rec.outcome = "error";
183
+ rec.errorCode = "NO_MOCK";
184
+ rec.errorMessage = `no mock registered for '${key}' (add via opts.mocks or harness.mock)`;
185
+ throw new HostApiError(rec.errorCode, rec.errorMessage);
186
+ }
187
+ try {
188
+ const result = await Promise.resolve(handler(args, {
189
+ ns,
190
+ method
191
+ }));
192
+ rec.outcome = "ok";
193
+ rec.result = result;
194
+ return result;
195
+ } catch (e) {
196
+ rec.outcome = "error";
197
+ const err = e;
198
+ rec.errorCode = err.code ?? "MOCK_ERROR";
199
+ rec.errorMessage = err.message ?? String(e);
200
+ throw e;
201
+ }
202
+ }
203
+ const facet = (ns) => (method) => (args) => dispatch(ns, method, args ?? {});
204
+ const runtime = {
205
+ get hello() {
206
+ return {
207
+ wid,
208
+ t: token,
209
+ user_id: opts.userId ?? 1
210
+ };
211
+ },
212
+ call: dispatch,
213
+ on: (name, fn) => events.on(name, fn),
214
+ tools: { invoke: (args) => dispatch("tools", "invoke", args) },
215
+ storage: {
216
+ get: (key) => dispatch("storage", "get", { key }),
217
+ set: (key, value) => dispatch("storage", "set", {
218
+ key,
219
+ value
220
+ })
221
+ },
222
+ chat: { write_message: (text, optsArg) => dispatch("chat", "write_message", {
223
+ text,
224
+ ...optsArg ?? {}
225
+ }) },
226
+ artifact: { create: (args) => dispatch("artifact", "create", args) },
227
+ llm: { complete: (args) => dispatch("llm", "complete", args) },
228
+ fs: new Proxy({}, { get: (_t, prop) => facet("fs")(prop) }),
229
+ prefs: {
230
+ get: (key) => dispatch("prefs", "get", { key }),
231
+ set: (key, value) => dispatch("prefs", "set", {
232
+ key,
233
+ value
234
+ })
235
+ },
236
+ window: { set_title: (title) => dispatch("window", "set_title", { title }) }
237
+ };
238
+ return {
239
+ runtime,
240
+ calls,
241
+ events,
242
+ acl,
243
+ mock(key, handler) {
244
+ mocks[key] = handler;
245
+ },
246
+ wait: (ms) => new Promise((res) => setTimeout(res, ms))
247
+ };
248
+ }
249
+ /** Error class thrown for ACL denials or missing mocks. */
250
+ var HostApiError = class extends Error {
251
+ code;
252
+ constructor(code, message) {
253
+ super(message);
254
+ this.name = "HostApiError";
255
+ this.code = code;
256
+ }
257
+ };
258
+
259
+ //#endregion
260
+ export { HostApiError, deriveAcl, isToolAllowed, mountBundle };
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@anna-ai/cli",
3
+ "version": "0.1.0",
4
+ "description": "Anna App developer CLI: scaffold, validate, harness (Phase 2 MVP: init + validate).",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "anna-app": "./dist/cli.js"
9
+ },
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/cli.d.ts",
13
+ "import": "./dist/cli.js"
14
+ },
15
+ "./test": {
16
+ "types": "./dist/test/index.d.ts",
17
+ "import": "./dist/test/index.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist/",
22
+ "templates/",
23
+ "vendor/",
24
+ "README.md"
25
+ ],
26
+ "scripts": {
27
+ "build": "tsdown",
28
+ "dev": "tsdown --watch",
29
+ "test": "vitest run",
30
+ "test:watch": "vitest",
31
+ "lint": "tsc --noEmit",
32
+ "sync:schema": "node scripts/sync-schema.mjs",
33
+ "check:runtime-pin": "node scripts/check-runtime-pin.mjs",
34
+ "prepublishOnly": "pnpm lint && pnpm test && pnpm build"
35
+ },
36
+ "dependencies": {
37
+ "ajv": "^8.17.1",
38
+ "ajv-formats": "^3.0.1",
39
+ "commander": "^12.1.0",
40
+ "kleur": "^4.1.5",
41
+ "ws": "^8.18.0"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^22.10.0",
45
+ "@types/ws": "^8.5.13",
46
+ "tsdown": "^0.9.6",
47
+ "typescript": "^5.6.3",
48
+ "vitest": "^2.1.8"
49
+ },
50
+ "engines": {
51
+ "node": ">=22"
52
+ }
53
+ }
@@ -0,0 +1,9 @@
1
+ # __SLUG__
2
+
3
+ Scaffolded by `anna-app init`. Next steps:
4
+
5
+ ```bash
6
+ anna-app validate # static checks
7
+ anna-app validate --strict # + host_api ACL coverage
8
+ anna-app dev # local harness — opens http://127.0.0.1:5180/dev/<wid>?t=<dev-token>
9
+ ```
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "__SLUG__",
3
+ "tagline": "TODO",
4
+ "description": "TODO",
5
+ "category": "productivity",
6
+ "pricing_model": "free"
7
+ }
@@ -0,0 +1,36 @@
1
+ // Minimal Anna App bundle entry. Replace with real logic.
2
+ const TOOL_ID = "__TOOL_ID__";
3
+
4
+ async function main() {
5
+ const status = document.getElementById("status");
6
+ const btn = document.getElementById("primary-btn");
7
+ if (!status || !btn) return;
8
+
9
+ let anna;
10
+ try {
11
+ anna = await AnnaAppRuntime.connect();
12
+ } catch (e) {
13
+ status.textContent = "Standalone preview (no host).";
14
+ return;
15
+ }
16
+
17
+ await anna.window.set_title({ title: "__SLUG__" });
18
+ status.textContent = "Ready.";
19
+
20
+ btn.addEventListener("click", async () => {
21
+ status.textContent = "Running…";
22
+ try {
23
+ const out = await anna.tools.invoke({
24
+ tool_id: TOOL_ID,
25
+ method: "ping",
26
+ args: {},
27
+ });
28
+ await anna.storage.set({ key: "__SLUG__:last", value: Date.now() });
29
+ status.textContent = JSON.stringify(out, null, 2);
30
+ } catch (e) {
31
+ status.textContent = "Error: " + e.message;
32
+ }
33
+ });
34
+ }
35
+
36
+ main();
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>__SLUG__</title>
6
+ <script src="/static/anna-apps/_sdk/0.1.0/index.js" defer></script>
7
+ <script src="./app.js" type="module" defer></script>
8
+ </head>
9
+ <body>
10
+ <h1>__SLUG__</h1>
11
+ <button id="primary-btn">Run</button>
12
+ <pre id="status">…</pre>
13
+ </body>
14
+ </html>
@@ -0,0 +1,60 @@
1
+ """Minimal stdio plugin scaffold for the __SLUG__ Anna App."""
2
+
3
+ import json
4
+ import sys
5
+
6
+ MANIFEST = {
7
+ "name": "__TOOL_ID__",
8
+ "version": "0.1.0",
9
+ "tools": [
10
+ {
11
+ "name": "ping",
12
+ "description": "Smoke-test method.",
13
+ "parameters": {
14
+ "type": "object",
15
+ "properties": {},
16
+ "additionalProperties": False,
17
+ },
18
+ }
19
+ ],
20
+ }
21
+
22
+
23
+ def invoke(method: str, args: dict) -> dict:
24
+ if method == "ping":
25
+ return {"pong": True}
26
+ raise ValueError(f"unknown method: {method}")
27
+
28
+
29
+ def main() -> None:
30
+ for line in sys.stdin:
31
+ line = line.strip()
32
+ if not line:
33
+ continue
34
+ req = json.loads(line)
35
+ try:
36
+ if req.get("method") == "describe":
37
+ result = MANIFEST
38
+ elif req.get("method") == "health":
39
+ result = {"status": "ok"}
40
+ elif req.get("method") == "invoke":
41
+ result = invoke(req["params"]["tool"], req["params"].get("arguments", {}))
42
+ else:
43
+ raise ValueError(f"unknown rpc: {req.get('method')}")
44
+ sys.stdout.write(json.dumps({"jsonrpc": "2.0", "id": req.get("id"), "result": result}) + "\n")
45
+ except Exception as e: # noqa: BLE001
46
+ sys.stdout.write(
47
+ json.dumps(
48
+ {
49
+ "jsonrpc": "2.0",
50
+ "id": req.get("id"),
51
+ "error": {"code": -32601, "message": str(e)},
52
+ }
53
+ )
54
+ + "\n"
55
+ )
56
+ sys.stdout.flush()
57
+
58
+
59
+ if __name__ == "__main__":
60
+ main()
@@ -0,0 +1,12 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "__TOOL_ID__"
7
+ version = "0.1.0"
8
+ description = "Executa for the __SLUG__ Anna App"
9
+ requires-python = ">=3.10"
10
+
11
+ [project.scripts]
12
+ "__TOOL_ID__" = "__SLUG___executa.plugin:main"
@@ -0,0 +1,38 @@
1
+ {
2
+ "schema": 2,
3
+ "permissions": ["tools.invoke", "storage.read", "storage.write"],
4
+ "required_executas": [
5
+ {
6
+ "tool_id": "__TOOL_ID__"
7
+ }
8
+ ],
9
+ "tags": ["__SLUG__"],
10
+ "ui": {
11
+ "bundle": {
12
+ "format": "static-spa",
13
+ "entry": "index.html",
14
+ "external_origins": []
15
+ },
16
+ "views": [
17
+ {
18
+ "name": "main",
19
+ "title": "__SLUG__",
20
+ "default": true,
21
+ "default_size": { "w": 480, "h": 360 },
22
+ "min_size": { "w": 320, "h": 240 }
23
+ }
24
+ ],
25
+ "host_api": {
26
+ "tools": ["__TOOL_ID__"],
27
+ "storage": ["get", "set"],
28
+ "window": ["set_title", "ready"]
29
+ },
30
+ "csp_overrides": {},
31
+ "state_merge": "last_writer_wins"
32
+ },
33
+ "dev": {
34
+ "fixtures": ["fixtures/*.jsonl"],
35
+ "seed_storage": {},
36
+ "user_id": 1
37
+ }
38
+ }
@@ -0,0 +1,22 @@
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
+ ```
@@ -0,0 +1,38 @@
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
+ }