@anna-ai/cli 0.1.9 → 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 +47 -8
- package/dist/credentials-ggdaz_-7.js +122 -0
- package/dist/dev-BPIUX2Nh.js +366 -0
- package/dist/{doctor-BmR0POfL.js → doctor-R1pjmBDG.js} +2 -2
- 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/dist/dev-D-Tru6gP.js +0 -163
- package/dist/server-gl345fFN.js +0 -261
- /package/dist/{fixture-BGjMtqWA.js → fixture-RceUUd84.js} +0 -0
package/dist/test/index.js
CHANGED
|
@@ -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-ggdaz_-7.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.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "Anna App developer CLI: scaffold, validate, harness (Phase 2 MVP: init + validate).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -38,6 +38,7 @@
|
|
|
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": {
|
package/dist/dev-D-Tru6gP.js
DELETED
|
@@ -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 };
|
package/dist/server-gl345fFN.js
DELETED
|
@@ -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 };
|
|
File without changes
|