@anna-ai/cli 0.1.9 → 0.1.12

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.
@@ -114,6 +114,24 @@ function makeDefaultMocks(state) {
114
114
  state.storage.delete(key);
115
115
  return null;
116
116
  },
117
+ "storage.list": (args) => {
118
+ const { prefix = "", cursor, limit = 100 } = args ?? {};
119
+ const keys = [...state.storage.keys()].filter((k) => k.startsWith(prefix)).sort();
120
+ const startIdx = cursor ? keys.findIndex((k) => k > cursor) : 0;
121
+ const slice = startIdx >= 0 ? keys.slice(startIdx, startIdx + limit) : [];
122
+ const next_cursor = startIdx >= 0 && startIdx + limit < keys.length ? slice[slice.length - 1] : null;
123
+ return {
124
+ items: slice.map((k) => ({
125
+ key: k,
126
+ etag: null,
127
+ size_bytes: JSON.stringify(state.storage.get(k) ?? null).length,
128
+ metadata: null,
129
+ tags: null,
130
+ updated_at: null
131
+ })),
132
+ next_cursor
133
+ };
134
+ },
117
135
  "prefs.get": (args) => {
118
136
  const key = args.key ?? "";
119
137
  return state.prefs.get(key) ?? null;
@@ -124,7 +142,33 @@ function makeDefaultMocks(state) {
124
142
  return null;
125
143
  },
126
144
  "chat.write_message": () => null,
127
- "window.set_title": () => null
145
+ "window.set_title": () => null,
146
+ "llm.complete": (args) => {
147
+ const messages = args.messages ?? [];
148
+ const firstUser = messages.find((m) => m?.role === "user");
149
+ const content = firstUser?.content;
150
+ let text = "";
151
+ if (typeof content === "string") text = content;
152
+ else if (content && typeof content === "object" && "text" in content) text = String(content.text ?? "");
153
+ const truncated = text.length > 60 ? text.slice(0, 60) + "…" : text;
154
+ const out = `<MOCK LLM: ${truncated}>`;
155
+ const inputTokens = Math.max(1, Math.ceil(text.length / 4));
156
+ const outputTokens = Math.max(1, Math.ceil(out.length / 4));
157
+ return {
158
+ role: "assistant",
159
+ content: {
160
+ type: "text",
161
+ text: out
162
+ },
163
+ model: "mock-model",
164
+ stopReason: "endTurn",
165
+ usage: {
166
+ inputTokens,
167
+ outputTokens,
168
+ totalTokens: inputTokens + outputTokens
169
+ }
170
+ };
171
+ }
128
172
  };
129
173
  }
130
174
  async function mountBundle(opts) {
@@ -0,0 +1,43 @@
1
+ import { credentialsAreLooselyPermissioned, maskPat, readCredentials } from "./credentials-BTv2IfUZ.js";
2
+
3
+ //#region src/commands/whoami.ts
4
+ async function runWhoami(opts = {}) {
5
+ const data = readCredentials();
6
+ if (Object.keys(data.accounts).length === 0) {
7
+ if (opts.json) process.stdout.write(JSON.stringify({
8
+ accounts: [],
9
+ current: null
10
+ }) + "\n");
11
+ else console.log("(no accounts — run `anna-app login --host <nexus-url>`)");
12
+ return 0;
13
+ }
14
+ if (opts.json) {
15
+ const out = {
16
+ current: data.current,
17
+ accounts: Object.entries(data.accounts).map(([k, v]) => ({
18
+ host: k,
19
+ user_id: v.user_id,
20
+ pat_preview: maskPat(v.pat),
21
+ issued_at: v.issued_at,
22
+ expires_at: v.expires_at,
23
+ scopes: v.scopes
24
+ }))
25
+ };
26
+ process.stdout.write(JSON.stringify(out, null, 2) + "\n");
27
+ return 0;
28
+ }
29
+ if (credentialsAreLooselyPermissioned()) console.warn("⚠ ~/.config/anna/credentials.json has loose permissions; run `chmod 600` on it.");
30
+ for (const [k, v] of Object.entries(data.accounts)) {
31
+ const tag = k === data.current ? " (current)" : "";
32
+ const expDays = Math.max(0, Math.round((v.expires_at - Date.now() / 1e3) / 86400));
33
+ console.log(`• ${k}${tag}`);
34
+ console.log(` pat: ${maskPat(v.pat)}`);
35
+ console.log(` user_id: ${v.user_id ?? "(unknown)"}`);
36
+ console.log(` scopes: ${v.scopes ?? "(none)"}`);
37
+ console.log(` expires_in: ~${expDays}d`);
38
+ }
39
+ return 0;
40
+ }
41
+
42
+ //#endregion
43
+ export { runWhoami };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anna-ai/cli",
3
- "version": "0.1.9",
3
+ "version": "0.1.12",
4
4
  "description": "Anna App developer CLI: scaffold, validate, harness (Phase 2 MVP: init + validate).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -32,12 +32,13 @@
32
32
  "prepublishOnly": "pnpm lint && pnpm test && pnpm build"
33
33
  },
34
34
  "dependencies": {
35
- "@anna-ai/app-runtime": "^0.1.0",
36
- "@anna-ai/app-schema": "^0.1.0",
35
+ "@anna-ai/app-runtime": "^0.2.0",
36
+ "@anna-ai/app-schema": "^0.4.0",
37
37
  "ajv": "^8.17.1",
38
38
  "ajv-formats": "^3.0.1",
39
39
  "commander": "^12.1.0",
40
40
  "kleur": "^4.1.5",
41
+ "smol-toml": "^1.6.1",
41
42
  "ws": "^8.18.0"
42
43
  },
43
44
  "devDependencies": {
@@ -1,3 +0,0 @@
1
- import { PINNED_RUNTIME_VERSION, PythonBridge } from "./bridge-CBcQUQGU.js";
2
-
3
- export { PINNED_RUNTIME_VERSION, PythonBridge };
@@ -1,163 +0,0 @@
1
- import { dirname, isAbsolute, resolve } from "node:path";
2
- import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
3
- import { bold, cyan, dim, green, red, yellow } from "kleur/colors";
4
-
5
- //#region src/commands/dev.ts
6
- async function runDev(opts) {
7
- const cwd = resolve(opts.cwd);
8
- const manifestPath = isAbsolute(opts.manifestPath) ? opts.manifestPath : resolve(cwd, opts.manifestPath);
9
- if (!existsSync(manifestPath)) {
10
- console.error(red(`✗ manifest not found: ${manifestPath}`));
11
- return 2;
12
- }
13
- let manifest;
14
- try {
15
- manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
16
- } catch (e) {
17
- console.error(red(`✗ manifest is not valid json: ${e.message}`));
18
- return 2;
19
- }
20
- const slug = opts.slug ?? deriveSlug(manifest, manifestPath);
21
- const bundleDir = opts.bundleDir ? resolve(cwd, opts.bundleDir) : resolve(dirname(manifestPath), "bundle");
22
- const bundleEntry = manifest.ui?.bundle?.entry ?? "index.html";
23
- if (!existsSync(bundleDir)) {
24
- console.error(red(`✗ bundle dir not found: ${bundleDir}`));
25
- return 2;
26
- }
27
- if (!existsSync(resolve(bundleDir, bundleEntry))) {
28
- console.error(red(`✗ bundle entry "${bundleEntry}" not found under ${bundleDir}`));
29
- return 2;
30
- }
31
- const matrixNexusRoot = await resolveMatrixNexusRoot(opts.matrixNexusRoot, cwd);
32
- const mode = matrixNexusRoot ? "nexus-source" : "uvx";
33
- const { PythonBridge, PINNED_RUNTIME_VERSION } = await import("./bridge-BDBECvV1.js");
34
- const { HarnessServer } = await import("./server-gl345fFN.js");
35
- const bridge = new PythonBridge({
36
- mode,
37
- matrixNexusRoot: matrixNexusRoot ?? void 0,
38
- onStderr: (line) => process.stderr.write(dim(`[bridge] ${line}\n`))
39
- });
40
- console.log(bold(cyan("anna-app dev")));
41
- console.log(` manifest ${dim(manifestPath)}`);
42
- console.log(` bundle ${dim(`${bundleDir}/${bundleEntry}`)}`);
43
- if (mode === "nexus-source") {
44
- console.log(` matrix-nexus root ${dim(matrixNexusRoot)}`);
45
- console.log(` runtime ${dim("nexus-source (uv run)")}`);
46
- } else console.log(` runtime ${dim(`uvx anna-app-runtime-local@${PINNED_RUNTIME_VERSION}`)}`);
47
- console.log(` spawning python bridge…`);
48
- try {
49
- await bridge.start();
50
- } catch (e) {
51
- console.error(red(`✗ bridge failed to start: ${e.message}`));
52
- if (mode === "uvx") console.error(dim(" (try: install uv from https://docs.astral.sh/uv/, or pass --matrix-nexus-root to use a checkout)"));
53
- else console.error(dim(" (try: cd matrix-nexus && uv sync; ensure `uv` is on PATH; or unset --matrix-nexus-root to use uvx)"));
54
- return 2;
55
- }
56
- try {
57
- const pong = await bridge.call("ping");
58
- if (!pong.pong) throw new Error("bridge ping returned non-pong");
59
- } catch (e) {
60
- console.error(red(`✗ bridge ping failed: ${e.message}`));
61
- await bridge.stop();
62
- return 2;
63
- }
64
- console.log(green(" ✓ bridge ready"));
65
- const executas = opts.executas ?? autoDiscoverExecutas(dirname(manifestPath));
66
- if (executas.length > 0) console.log(` executas ${dim(executas.map((e) => e.tool_id).join(", "))}`);
67
- const server = new HarnessServer({
68
- slug,
69
- manifest,
70
- bundleDir,
71
- bundleEntry,
72
- view: opts.view,
73
- matrixNexusRoot,
74
- userId: opts.userId,
75
- port: opts.port,
76
- executas,
77
- watch: !opts.noWatch
78
- }, bridge);
79
- try {
80
- await server.listen();
81
- } catch (e) {
82
- const msg = e.code === "EADDRINUSE" ? `port ${opts.port} already in use` : e.message;
83
- console.error(red(`✗ harness failed to listen: ${msg}`));
84
- await bridge.stop();
85
- return 2;
86
- }
87
- console.log(` ${green("✓")} dashboard ${cyan(`http://localhost:${opts.port}/`)}`);
88
- console.log(yellow(" press Ctrl+C to stop"));
89
- const shutdown = async () => {
90
- console.log(dim("\n shutting down…"));
91
- try {
92
- await server.close();
93
- await bridge.stop();
94
- } finally {
95
- process.exit(0);
96
- }
97
- };
98
- process.once("SIGINT", shutdown);
99
- process.once("SIGTERM", shutdown);
100
- await new Promise(() => {});
101
- return 0;
102
- }
103
- function deriveSlug(manifest, path) {
104
- const fromMan = manifest.slug ?? manifest.name;
105
- if (typeof fromMan === "string" && fromMan) return fromMan;
106
- return dirname(path).split(/[\\/]/).pop() || "anna-app-dev";
107
- }
108
- /**
109
- * Look in `<manifest-dir>/executas/<name>/pyproject.toml` and infer
110
- * `tool_id` from the first `[project.scripts]` entry. We grep with a tiny
111
- * regex instead of pulling in a TOML parser — the pyproject script line
112
- * always looks like `"tool-anna-foo-xxxx" = "foo_plugin:main"`.
113
- */
114
- function autoDiscoverExecutas(manifestDir) {
115
- const root = resolve(manifestDir, "executas");
116
- if (!existsSync(root)) return [];
117
- const out = [];
118
- for (const name of readdirSync(root)) {
119
- const dir = resolve(root, name);
120
- let st;
121
- try {
122
- st = statSync(dir);
123
- } catch {
124
- continue;
125
- }
126
- if (!st.isDirectory()) continue;
127
- const py = resolve(dir, "pyproject.toml");
128
- if (!existsSync(py)) continue;
129
- let body;
130
- try {
131
- body = readFileSync(py, "utf-8");
132
- } catch {
133
- continue;
134
- }
135
- const m = body.match(/\[project\.scripts\][\s\S]*?"([^"]+)"\s*=/);
136
- if (!m || !m[1]) continue;
137
- out.push({
138
- tool_id: m[1],
139
- project_dir: dir
140
- });
141
- }
142
- return out;
143
- }
144
- async function resolveMatrixNexusRoot(explicit, cwd) {
145
- const candidates = [explicit, process.env.ANNA_NEXUS_ROOT];
146
- let dir = cwd;
147
- while (true) {
148
- candidates.push(dir);
149
- const parent = dirname(dir);
150
- if (parent === dir) break;
151
- dir = parent;
152
- }
153
- candidates.push(resolve(cwd, "..", "matrix-nexus"));
154
- for (const c of candidates) {
155
- if (!c) continue;
156
- const abs = isAbsolute(c) ? c : resolve(cwd, c);
157
- if (existsSync(resolve(abs, "packages/anna-app-runtime-local/pyproject.toml"))) return abs;
158
- }
159
- return null;
160
- }
161
-
162
- //#endregion
163
- export { runDev };
@@ -1,261 +0,0 @@
1
- import { dirname, join, normalize, resolve } from "node:path";
2
- import { createRequire } from "node:module";
3
- import { createReadStream, statSync, watch } from "node:fs";
4
- import { fileURLToPath } from "node:url";
5
- import { readFile } from "node:fs/promises";
6
- import { createServer } from "node:http";
7
- import { WebSocketServer } from "ws";
8
-
9
- //#region src/harness/server.ts
10
- const __filename = fileURLToPath(import.meta.url);
11
- const __dirname = dirname(__filename);
12
- const MIME = {
13
- ".html": "text/html; charset=utf-8",
14
- ".js": "application/javascript; charset=utf-8",
15
- ".mjs": "application/javascript; charset=utf-8",
16
- ".css": "text/css; charset=utf-8",
17
- ".json": "application/json; charset=utf-8",
18
- ".svg": "image/svg+xml",
19
- ".png": "image/png",
20
- ".jpg": "image/jpeg",
21
- ".jpeg": "image/jpeg",
22
- ".gif": "image/gif",
23
- ".woff": "font/woff",
24
- ".woff2": "font/woff2",
25
- ".map": "application/json"
26
- };
27
- var HarnessServer = class {
28
- server = createServer((req, res) => this.handle(req, res));
29
- wss = null;
30
- sessionId = null;
31
- liveSockets = new Set();
32
- watchers = [];
33
- reloadDebounce = null;
34
- constructor(cfg, bridge) {
35
- this.cfg = cfg;
36
- this.bridge = bridge;
37
- }
38
- async listen() {
39
- if (this.cfg.executas && this.cfg.executas.length > 0) await this.bridge.call("executas.register", { executas: this.cfg.executas.map((e) => ({
40
- tool_id: e.tool_id,
41
- project_dir: e.project_dir,
42
- command: e.command ?? null
43
- })) });
44
- await new Promise((res, rej) => this.server.listen(this.cfg.port, () => res()).once("error", rej));
45
- this.wss = new WebSocketServer({ noServer: true });
46
- this.server.on("upgrade", (req, socket, head) => {
47
- const url = new URL(req.url ?? "/", "http://localhost");
48
- if (url.pathname !== "/ws") {
49
- socket.destroy();
50
- return;
51
- }
52
- this.wss.handleUpgrade(req, socket, head, (ws) => {
53
- const sid = url.searchParams.get("session_id");
54
- if (!sid || sid !== this.sessionId) {
55
- ws.close(1008, "unknown session_id");
56
- return;
57
- }
58
- this.liveSockets.add(ws);
59
- const timer = setInterval(async () => {
60
- try {
61
- const out = await this.bridge.call("session.drain_events", { session_id: sid });
62
- for (const ev of out.events) ws.send(JSON.stringify({
63
- kind: "event",
64
- ...ev
65
- }));
66
- } catch (e) {
67
- ws.close(1011, `drain failed: ${e.message}`);
68
- clearInterval(timer);
69
- }
70
- }, 200);
71
- ws.on("close", () => {
72
- this.liveSockets.delete(ws);
73
- clearInterval(timer);
74
- });
75
- });
76
- });
77
- if (this.cfg.watch) this.startWatcher();
78
- }
79
- async close() {
80
- for (const w of this.watchers) w.close();
81
- this.watchers = [];
82
- if (this.reloadDebounce) clearTimeout(this.reloadDebounce);
83
- if (this.wss) {
84
- for (const c of this.wss.clients) c.terminate();
85
- this.wss.close();
86
- }
87
- await new Promise((res) => this.server.close(() => res()));
88
- }
89
- startWatcher() {
90
- const broadcastReload = (path) => {
91
- if (this.reloadDebounce) clearTimeout(this.reloadDebounce);
92
- this.reloadDebounce = setTimeout(() => {
93
- const env = JSON.stringify({
94
- kind: "reload",
95
- path
96
- });
97
- for (const ws of this.liveSockets) if (ws.readyState === ws.OPEN) ws.send(env);
98
- }, 100);
99
- };
100
- try {
101
- this.watchers.push(watch(this.cfg.bundleDir, { recursive: true }, (_evt, filename) => {
102
- if (filename) broadcastReload(`bundle/${filename}`);
103
- }));
104
- } catch (e) {
105
- process.stderr.write(`[harness] watcher failed to attach to ${this.cfg.bundleDir}: ${e.message}\n`);
106
- }
107
- }
108
- async handle(req, res) {
109
- try {
110
- const url = new URL(req.url ?? "/", "http://localhost");
111
- const method = req.method ?? "GET";
112
- if (method === "GET" && (url.pathname === "/" || url.pathname === "/dashboard")) return await this.serveDashboard(res);
113
- if (method === "GET" && url.pathname === "/api/config") return this.json(res, 200, {
114
- app_slug: this.cfg.slug,
115
- view: this.cfg.view ?? null,
116
- bundle_base: `/anna-apps/${this.cfg.slug}/dev/${this.cfg.bundleEntry}`,
117
- executas: (this.cfg.executas ?? []).map((e) => e.tool_id),
118
- watch: !!this.cfg.watch
119
- });
120
- if (method === "POST" && url.pathname === "/api/session/create") return await this.createSession(res);
121
- if (method === "POST" && url.pathname === "/api/session/call") return await this.proxyCall(req, res);
122
- if (method === "POST" && url.pathname === "/api/session/refresh-token") return await this.refreshToken(res);
123
- if (method === "GET" && url.pathname.startsWith("/static/anna-apps/_sdk/")) return await this.serveSdk(url.pathname, res);
124
- if (method === "GET" && url.pathname.startsWith(`/anna-apps/${this.cfg.slug}/dev/`)) {
125
- const rel = url.pathname.replace(`/anna-apps/${this.cfg.slug}/dev/`, "");
126
- return await this.serveBundleAsset(rel, res);
127
- }
128
- this.text(res, 404, `not found: ${url.pathname}`);
129
- } catch (e) {
130
- this.text(res, 500, `harness error: ${e.message}`);
131
- }
132
- }
133
- async serveDashboard(res) {
134
- const file = join(__dirname, "dashboard.html");
135
- const html = await readFile(file, "utf-8");
136
- res.writeHead(200, {
137
- "content-type": MIME[".html"],
138
- "cache-control": "no-store"
139
- });
140
- res.end(html);
141
- }
142
- async createSession(res) {
143
- if (this.sessionId) {
144
- try {
145
- await this.bridge.call("session.close", { session_id: this.sessionId });
146
- } catch {}
147
- this.sessionId = null;
148
- }
149
- const out = await this.bridge.call("session.create", {
150
- user_id: this.cfg.userId,
151
- manifest: this.cfg.manifest,
152
- view: this.cfg.view,
153
- entry_payload: this.cfg.entryPayload ?? {},
154
- app_slug: this.cfg.slug
155
- });
156
- this.sessionId = out.session_id;
157
- this.json(res, 200, out);
158
- }
159
- async proxyCall(req, res) {
160
- const body = await readBody(req);
161
- let parsed;
162
- try {
163
- parsed = JSON.parse(body);
164
- } catch {
165
- return this.json(res, 400, {
166
- ok: false,
167
- error: {
168
- code: "bad_request",
169
- message: "invalid json body"
170
- }
171
- });
172
- }
173
- try {
174
- const out = await this.bridge.call("session.call", {
175
- session_id: parsed.session_id,
176
- ns: parsed.ns,
177
- method: parsed.method,
178
- args: parsed.args ?? {}
179
- });
180
- this.json(res, 200, out);
181
- } catch (e) {
182
- this.json(res, 200, {
183
- ok: false,
184
- error: {
185
- code: "transport",
186
- message: e.message
187
- }
188
- });
189
- }
190
- }
191
- async refreshToken(res) {
192
- if (!this.sessionId) return this.json(res, 400, { error: "no active session" });
193
- try {
194
- const out = await this.bridge.call("session.refresh_token", { session_id: this.sessionId });
195
- this.json(res, 200, out);
196
- } catch (e) {
197
- this.json(res, 500, { error: e.message });
198
- }
199
- }
200
- async serveSdk(pathname, res) {
201
- const sdkRel = pathname.replace(/^\/static\/anna-apps\/_sdk\/[^/]+\//, "");
202
- let distRoot;
203
- try {
204
- const req = createRequire(import.meta.url);
205
- distRoot = dirname(req.resolve("@anna-ai/app-runtime"));
206
- } catch (e) {
207
- return this.text(res, 500, `@anna-ai/app-runtime is not installed: ${e.message}`);
208
- }
209
- const abs = resolve(distRoot, sdkRel);
210
- if (!abs.startsWith(distRoot)) return this.text(res, 403, "forbidden");
211
- return this.serveFile(abs, res);
212
- }
213
- async serveBundleAsset(rel, res) {
214
- const abs = resolve(this.cfg.bundleDir, normalize(rel));
215
- if (!abs.startsWith(resolve(this.cfg.bundleDir))) return this.text(res, 403, "forbidden");
216
- return this.serveFile(abs, res);
217
- }
218
- async serveFile(abs, res) {
219
- let stat;
220
- try {
221
- stat = statSync(abs);
222
- } catch {
223
- return this.text(res, 404, `not found: ${abs}`);
224
- }
225
- if (!stat.isFile()) return this.text(res, 404, "not a file");
226
- const ext = abs.slice(abs.lastIndexOf("."));
227
- res.writeHead(200, {
228
- "content-type": MIME[ext] ?? "application/octet-stream",
229
- "cache-control": "no-store",
230
- "content-length": String(stat.size)
231
- });
232
- createReadStream(abs).pipe(res);
233
- }
234
- json(res, status, body) {
235
- const text = JSON.stringify(body);
236
- res.writeHead(status, {
237
- "content-type": MIME[".json"],
238
- "content-length": String(Buffer.byteLength(text)),
239
- "cache-control": "no-store"
240
- });
241
- res.end(text);
242
- }
243
- text(res, status, body) {
244
- res.writeHead(status, {
245
- "content-type": "text/plain; charset=utf-8",
246
- "content-length": String(Buffer.byteLength(body))
247
- });
248
- res.end(body);
249
- }
250
- };
251
- function readBody(req) {
252
- return new Promise((resolve$1, reject) => {
253
- const chunks = [];
254
- req.on("data", (c) => chunks.push(c));
255
- req.on("end", () => resolve$1(Buffer.concat(chunks).toString("utf-8")));
256
- req.on("error", reject);
257
- });
258
- }
259
-
260
- //#endregion
261
- export { HarnessServer };