@anna-ai/cli 0.1.11 → 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-DUmINbo4.js +372 -0
- package/dist/apps-BEJUn9Ws.js +44 -0
- package/dist/bridge-C0DWb5eQ.js +3 -0
- package/dist/cli.js +81 -10
- package/dist/{credentials-ggdaz_-7.js → credentials-BTv2IfUZ.js} +1 -1
- package/dist/credentials-DDqx6XMQ.js +3 -0
- package/dist/dashboard.html +8 -4
- package/dist/{dev-BPIUX2Nh.js → dev-BfLGxpiT.js} +52 -7
- package/dist/dev-C81H9c9_.js +3 -0
- package/dist/dev-account-DCyjamBa.js +44 -0
- package/dist/dev-app-cache-C3D1Sp_V.js +93 -0
- package/dist/dev-app-cache-CZ8lIKiw.js +4 -0
- package/dist/{doctor-R1pjmBDG.js → doctor-B3u0edUg.js} +1 -1
- package/dist/executa-dev-BhouP8jh.js +212 -0
- package/dist/executa-init-COEmKDOE.js +68 -0
- package/dist/executa-register-66WKIwQQ.js +47 -0
- package/dist/{login-D8cmvBb6.js → login-CsIVbrmf.js} +1 -1
- package/dist/{logout-P6L9VU4W.js → logout-gfmKQxMj.js} +1 -1
- package/dist/mascot-wlYTJqMs.js +218 -0
- package/dist/runner-Bral1LFW.js +279 -0
- package/dist/sampling-3EfSlDHM.js +155 -0
- package/dist/{server-Cd5Lo-2v.js → server-q6nKCeEV.js} +10 -4
- package/dist/storage-CnWTZqq_.js +316 -0
- package/dist/{whoami-jqlQwe7Z.js → whoami-BS5wy-Nh.js} +1 -1
- package/package.json +3 -3
- package/templates/executa/go/README.md +10 -0
- package/templates/executa/go/executa.json +4 -0
- package/templates/executa/go/go.mod +3 -0
- package/templates/executa/go/main.go +148 -0
- package/templates/executa/node/README.md +12 -0
- package/templates/executa/node/executa.json +4 -0
- package/templates/executa/node/package.json +12 -0
- package/templates/executa/node/plugin.mjs +126 -0
- package/templates/executa/node/sampling-fixture.jsonl +1 -0
- package/templates/executa/python/README.md +23 -0
- package/templates/executa/python/__SLUG_PY___plugin.py +146 -0
- package/templates/executa/python/executa.json +4 -0
- package/templates/executa/python/pyproject.toml +15 -0
- package/templates/executa/python/sampling-fixture.jsonl +4 -0
- package/dist/bridge-BIO7ilgO.js +0 -3
- /package/dist/{bridge-Cpm3D2Wk.js → bridge-D6YyP9DM.js} +0 -0
- /package/dist/{fixture-RceUUd84.js → fixture-CATHyLLI.js} +0 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import kleur from "kleur";
|
|
2
|
+
|
|
3
|
+
//#region src/mascot.ts
|
|
4
|
+
const FACE = [
|
|
5
|
+
255,
|
|
6
|
+
227,
|
|
7
|
+
234
|
|
8
|
+
];
|
|
9
|
+
const EYE = [
|
|
10
|
+
122,
|
|
11
|
+
90,
|
|
12
|
+
101
|
|
13
|
+
];
|
|
14
|
+
const GEO = (() => {
|
|
15
|
+
const VX0 = 252, VY0 = 280, VW = 520;
|
|
16
|
+
const n = (x, y) => [(x - VX0) / VW, (y - VY0) / VW];
|
|
17
|
+
const r = (v) => v / VW;
|
|
18
|
+
const [fcx, fcy] = n(512, 540);
|
|
19
|
+
const [lex, ley] = n(462, 520);
|
|
20
|
+
const [rex, rey] = n(562, 520);
|
|
21
|
+
return {
|
|
22
|
+
face: {
|
|
23
|
+
cx: fcx,
|
|
24
|
+
cy: fcy,
|
|
25
|
+
r: r(260)
|
|
26
|
+
},
|
|
27
|
+
eyeL: {
|
|
28
|
+
cx: lex,
|
|
29
|
+
cy: ley,
|
|
30
|
+
r: r(34)
|
|
31
|
+
},
|
|
32
|
+
eyeR: {
|
|
33
|
+
cx: rex,
|
|
34
|
+
cy: rey,
|
|
35
|
+
r: r(34)
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
})();
|
|
39
|
+
/** Sample the SVG at normalized (x, y) ∈ [0, 1]². Returns null for empty. */
|
|
40
|
+
function sampleSvg(x, y) {
|
|
41
|
+
const inCircle = (cx, cy, r) => {
|
|
42
|
+
const dx = x - cx, dy = y - cy;
|
|
43
|
+
return dx * dx + dy * dy <= r * r;
|
|
44
|
+
};
|
|
45
|
+
let c = null;
|
|
46
|
+
if (inCircle(GEO.face.cx, GEO.face.cy, GEO.face.r)) c = FACE;
|
|
47
|
+
return c;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 2×2 supersampled pixel using **majority vote** (not averaging).
|
|
51
|
+
* Anti-aliases silhouette edges (filled vs empty) while keeping every
|
|
52
|
+
* pixel a pure palette color — no muddy blends between the pink face and
|
|
53
|
+
* the dark eyes. Ties resolve in favor of the topmost layer encountered.
|
|
54
|
+
*/
|
|
55
|
+
function pixel(px, py, w, h) {
|
|
56
|
+
const counts = new Map();
|
|
57
|
+
let nulls = 0;
|
|
58
|
+
for (const oy of [.25, .75]) for (const ox of [.25, .75]) {
|
|
59
|
+
const s = sampleSvg((px + ox) / w, (py + oy) / h);
|
|
60
|
+
if (!s) {
|
|
61
|
+
nulls++;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const key = `${s[0]},${s[1]},${s[2]}`;
|
|
65
|
+
const entry = counts.get(key);
|
|
66
|
+
if (entry) entry.n++;
|
|
67
|
+
else counts.set(key, {
|
|
68
|
+
color: s,
|
|
69
|
+
n: 1
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
if (counts.size === 0) return null;
|
|
73
|
+
let best = null;
|
|
74
|
+
for (const e of counts.values()) if (!best || e.n > best.n) best = e;
|
|
75
|
+
if (nulls > (best?.n ?? 0)) return null;
|
|
76
|
+
return best.color;
|
|
77
|
+
}
|
|
78
|
+
const FG = (c) => `\x1b[38;2;${c[0]};${c[1]};${c[2]}m`;
|
|
79
|
+
const BG = (c) => `\x1b[48;2;${c[0]};${c[1]};${c[2]}m`;
|
|
80
|
+
const BG_DEFAULT = "\x1B[49m";
|
|
81
|
+
const RESET = "\x1B[0m";
|
|
82
|
+
function eq(a, b) {
|
|
83
|
+
if (a === b) return true;
|
|
84
|
+
if (!a || !b) return false;
|
|
85
|
+
return a[0] === b[0] && a[1] === b[1] && a[2] === b[2];
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Rasterize the face into a list of half-block lines.
|
|
89
|
+
* `cols` is character width; pixel rows are derived from the cell aspect
|
|
90
|
+
* so each pixel covers an (approximately) square area on screen.
|
|
91
|
+
*/
|
|
92
|
+
function renderFace(cols, cellAspect) {
|
|
93
|
+
const W = cols;
|
|
94
|
+
const charRows = Math.max(1, Math.round(cols / cellAspect));
|
|
95
|
+
const H = charRows * 2;
|
|
96
|
+
const eyeRow = Math.round(GEO.eyeL.cy * charRows - .5);
|
|
97
|
+
const eyeColL = Math.max(0, Math.round(GEO.eyeL.cx * W) - 1);
|
|
98
|
+
const eyeColR = Math.max(0, Math.round(GEO.eyeR.cx * W) - 1);
|
|
99
|
+
const isEyeCell = (cx, cy) => cy === eyeRow && (cx >= eyeColL && cx < eyeColL + 2 || cx >= eyeColR && cx < eyeColR + 2);
|
|
100
|
+
const rows = [];
|
|
101
|
+
for (let cy = 0; cy < charRows; cy++) {
|
|
102
|
+
let line = "";
|
|
103
|
+
let lastFg = null;
|
|
104
|
+
let lastBg = null;
|
|
105
|
+
let lastBgDefault = true;
|
|
106
|
+
for (let cx = 0; cx < W; cx++) {
|
|
107
|
+
if (isEyeCell(cx, cy)) {
|
|
108
|
+
if (!eq(lastFg, EYE)) {
|
|
109
|
+
line += FG(EYE);
|
|
110
|
+
lastFg = EYE;
|
|
111
|
+
}
|
|
112
|
+
if (lastBgDefault || !eq(lastBg, FACE)) {
|
|
113
|
+
line += BG(FACE);
|
|
114
|
+
lastBg = FACE;
|
|
115
|
+
lastBgDefault = false;
|
|
116
|
+
}
|
|
117
|
+
line += "█";
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const top = pixel(cx, cy * 2, W, H);
|
|
121
|
+
const bot = pixel(cx, cy * 2 + 1, W, H);
|
|
122
|
+
if (!top && !bot) {
|
|
123
|
+
if (!lastBgDefault) {
|
|
124
|
+
line += BG_DEFAULT;
|
|
125
|
+
lastBgDefault = true;
|
|
126
|
+
lastBg = null;
|
|
127
|
+
}
|
|
128
|
+
line += " ";
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (top && bot) {
|
|
132
|
+
if (!eq(lastFg, top)) {
|
|
133
|
+
line += FG(top);
|
|
134
|
+
lastFg = top;
|
|
135
|
+
}
|
|
136
|
+
if (lastBgDefault || !eq(lastBg, bot)) {
|
|
137
|
+
line += BG(bot);
|
|
138
|
+
lastBg = bot;
|
|
139
|
+
lastBgDefault = false;
|
|
140
|
+
}
|
|
141
|
+
line += "▀";
|
|
142
|
+
} else if (top) {
|
|
143
|
+
if (!eq(lastFg, top)) {
|
|
144
|
+
line += FG(top);
|
|
145
|
+
lastFg = top;
|
|
146
|
+
}
|
|
147
|
+
if (!lastBgDefault) {
|
|
148
|
+
line += BG_DEFAULT;
|
|
149
|
+
lastBgDefault = true;
|
|
150
|
+
lastBg = null;
|
|
151
|
+
}
|
|
152
|
+
line += "▀";
|
|
153
|
+
} else {
|
|
154
|
+
if (!eq(lastFg, bot)) {
|
|
155
|
+
line += FG(bot);
|
|
156
|
+
lastFg = bot;
|
|
157
|
+
}
|
|
158
|
+
if (!lastBgDefault) {
|
|
159
|
+
line += BG_DEFAULT;
|
|
160
|
+
lastBgDefault = true;
|
|
161
|
+
lastBg = null;
|
|
162
|
+
}
|
|
163
|
+
line += "▄";
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
rows.push(line + RESET);
|
|
167
|
+
}
|
|
168
|
+
return rows;
|
|
169
|
+
}
|
|
170
|
+
function mascotEnabled() {
|
|
171
|
+
if (process.env.ANNA_CLI_NO_MASCOT) return false;
|
|
172
|
+
if (process.env.NO_COLOR) return false;
|
|
173
|
+
if (process.env.ANNA_CLI_FORCE_MASCOT) return true;
|
|
174
|
+
return Boolean(process.stdout.isTTY);
|
|
175
|
+
}
|
|
176
|
+
function pickWidth() {
|
|
177
|
+
const override = Number.parseInt(process.env.ANNA_CLI_MASCOT_WIDTH ?? "", 10);
|
|
178
|
+
if (Number.isFinite(override) && override >= 10 && override <= 120) return override;
|
|
179
|
+
const cols = process.stdout.columns || 80;
|
|
180
|
+
if (cols < 60) return 10;
|
|
181
|
+
if (cols >= 120) return 16;
|
|
182
|
+
return 13;
|
|
183
|
+
}
|
|
184
|
+
function pickAspect() {
|
|
185
|
+
const v = Number.parseFloat(process.env.ANNA_CLI_MASCOT_ASPECT ?? "");
|
|
186
|
+
if (Number.isFinite(v) && v >= 1.2 && v <= 3) return v;
|
|
187
|
+
return 2.1;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Render the mascot as a multi-line string. Always returns the colored art
|
|
191
|
+
* (callers decide whether to print based on `mascotEnabled`).
|
|
192
|
+
*/
|
|
193
|
+
function renderMascot(message) {
|
|
194
|
+
const cols = pickWidth();
|
|
195
|
+
const aspect = pickAspect();
|
|
196
|
+
const face = renderFace(cols, aspect);
|
|
197
|
+
const pad = " ".repeat(2);
|
|
198
|
+
const label = kleur.magenta().bold("Anna");
|
|
199
|
+
const tagline = kleur.gray("Anna App developer CLI");
|
|
200
|
+
const textLines = [`${label} ${tagline}`];
|
|
201
|
+
if (message) textLines.push(`${kleur.gray("›")} ${message}`);
|
|
202
|
+
const start = Math.max(0, Math.floor((face.length - textLines.length) / 2));
|
|
203
|
+
const lines = [];
|
|
204
|
+
for (let i = 0; i < face.length; i++) {
|
|
205
|
+
const tIdx = i - start;
|
|
206
|
+
const text = tIdx >= 0 && tIdx < textLines.length ? " " + textLines[tIdx] : "";
|
|
207
|
+
lines.push(pad + face[i] + text);
|
|
208
|
+
}
|
|
209
|
+
return lines.join("\n");
|
|
210
|
+
}
|
|
211
|
+
/** Print the mascot to stderr if enabled. Stderr keeps stdout clean for piping. */
|
|
212
|
+
function printMascot(message) {
|
|
213
|
+
if (!mascotEnabled()) return;
|
|
214
|
+
process.stderr.write(renderMascot(message) + "\n\n");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
//#endregion
|
|
218
|
+
export { printMascot };
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
|
|
4
|
+
//#region src/executa/runner.ts
|
|
5
|
+
/** Spec describing where the host advertises its capabilities. */
|
|
6
|
+
const HOST_INITIALIZE_PARAMS = {
|
|
7
|
+
protocolVersion: "2.0",
|
|
8
|
+
client_capabilities: {
|
|
9
|
+
sampling: {},
|
|
10
|
+
agent: {},
|
|
11
|
+
storage: {}
|
|
12
|
+
},
|
|
13
|
+
client_info: {
|
|
14
|
+
name: "anna-app-cli",
|
|
15
|
+
version: "executa-dev"
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
var ExecutaRunner = class {
|
|
19
|
+
proc = null;
|
|
20
|
+
nextId = 1;
|
|
21
|
+
pending = new Map();
|
|
22
|
+
exitWaiters = [];
|
|
23
|
+
closed = false;
|
|
24
|
+
/** Resolved by start() after the v2 handshake (or v1 fallback) succeeds. */
|
|
25
|
+
initialized = null;
|
|
26
|
+
constructor(opts) {
|
|
27
|
+
this.opts = opts;
|
|
28
|
+
}
|
|
29
|
+
/** Spawn the subprocess and perform the initialize handshake. */
|
|
30
|
+
async start() {
|
|
31
|
+
if (this.proc) {
|
|
32
|
+
if (!this.initialized) throw new Error("executa already starting");
|
|
33
|
+
return this.initialized;
|
|
34
|
+
}
|
|
35
|
+
const [bin, ...args] = this.opts.command;
|
|
36
|
+
if (!bin) throw new Error("ExecutaRunner: empty command");
|
|
37
|
+
const env = {
|
|
38
|
+
...process.env,
|
|
39
|
+
...this.opts.env
|
|
40
|
+
};
|
|
41
|
+
env.ANNA_EXECUTA_DEV = "1";
|
|
42
|
+
this.proc = spawn(bin, args, {
|
|
43
|
+
cwd: this.opts.cwd,
|
|
44
|
+
stdio: [
|
|
45
|
+
"pipe",
|
|
46
|
+
"pipe",
|
|
47
|
+
"pipe"
|
|
48
|
+
],
|
|
49
|
+
env
|
|
50
|
+
});
|
|
51
|
+
const stderrSink = this.opts.onStderr ?? ((l) => process.stderr.write(`[executa] ${l}\n`));
|
|
52
|
+
createInterface({ input: this.proc.stderr }).on("line", stderrSink);
|
|
53
|
+
this.proc.on("exit", (code) => {
|
|
54
|
+
this.closed = true;
|
|
55
|
+
const waiters = this.exitWaiters.splice(0);
|
|
56
|
+
for (const w of waiters) w(code);
|
|
57
|
+
const err = new Error(`executa exited (code=${code ?? "null"})`);
|
|
58
|
+
for (const p of this.pending.values()) p.reject(err);
|
|
59
|
+
this.pending.clear();
|
|
60
|
+
});
|
|
61
|
+
this.proc.on("error", (e) => {
|
|
62
|
+
this.closed = true;
|
|
63
|
+
for (const p of this.pending.values()) p.reject(e);
|
|
64
|
+
this.pending.clear();
|
|
65
|
+
});
|
|
66
|
+
createInterface({ input: this.proc.stdout }).on("line", (line) => {
|
|
67
|
+
this.handleLine(line, stderrSink);
|
|
68
|
+
});
|
|
69
|
+
const initTimeout = this.opts.initTimeoutMs ?? 8e3;
|
|
70
|
+
try {
|
|
71
|
+
const raw = await this.callWithTimeout("initialize", HOST_INITIALIZE_PARAMS, initTimeout);
|
|
72
|
+
const ver = String(raw.protocolVersion ?? "1.0");
|
|
73
|
+
this.initialized = {
|
|
74
|
+
protocolVersion: ver === "2.0" ? "2.0" : "1.0",
|
|
75
|
+
raw
|
|
76
|
+
};
|
|
77
|
+
} catch (e) {
|
|
78
|
+
const rpc = e.rpc;
|
|
79
|
+
if (rpc && rpc.code === -32601) this.initialized = {
|
|
80
|
+
protocolVersion: "1.0",
|
|
81
|
+
raw: {}
|
|
82
|
+
};
|
|
83
|
+
else {
|
|
84
|
+
await this.stop();
|
|
85
|
+
throw e;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return this.initialized;
|
|
89
|
+
}
|
|
90
|
+
/** Send a JSON-RPC request and await the response. */
|
|
91
|
+
call(method, params = {}) {
|
|
92
|
+
if (this.closed || !this.proc) return Promise.reject(new Error("executa not running"));
|
|
93
|
+
const id = this.nextId++;
|
|
94
|
+
const env = {
|
|
95
|
+
jsonrpc: "2.0",
|
|
96
|
+
id,
|
|
97
|
+
method,
|
|
98
|
+
params
|
|
99
|
+
};
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
this.pending.set(id, {
|
|
102
|
+
resolve: (v) => resolve(v),
|
|
103
|
+
reject
|
|
104
|
+
});
|
|
105
|
+
this.proc.stdin.write(`${JSON.stringify(env)}\n`);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
/** Convenience: `describe → MANIFEST`. */
|
|
109
|
+
describe() {
|
|
110
|
+
return this.call("describe");
|
|
111
|
+
}
|
|
112
|
+
/** Convenience: `invoke {tool, arguments}`. */
|
|
113
|
+
invoke(tool, args) {
|
|
114
|
+
return this.call("invoke", {
|
|
115
|
+
tool,
|
|
116
|
+
arguments: args
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
/** Convenience: `health → {status}`. */
|
|
120
|
+
health() {
|
|
121
|
+
return this.call("health");
|
|
122
|
+
}
|
|
123
|
+
/** Close stdin (graceful EOF shutdown) then SIGTERM after a grace period. */
|
|
124
|
+
async stop(graceMs = 1500) {
|
|
125
|
+
if (!this.proc) return;
|
|
126
|
+
this.closed = true;
|
|
127
|
+
const p = this.proc;
|
|
128
|
+
try {
|
|
129
|
+
p.stdin.end();
|
|
130
|
+
} catch {}
|
|
131
|
+
const waitExit = new Promise((resolve) => {
|
|
132
|
+
this.exitWaiters.push(resolve);
|
|
133
|
+
if (p.exitCode != null) resolve(p.exitCode);
|
|
134
|
+
});
|
|
135
|
+
const timer = new Promise((resolve) => {
|
|
136
|
+
setTimeout(() => resolve("timeout"), graceMs);
|
|
137
|
+
});
|
|
138
|
+
const winner = await Promise.race([waitExit, timer]);
|
|
139
|
+
if (winner === "timeout") try {
|
|
140
|
+
p.kill("SIGTERM");
|
|
141
|
+
} catch {}
|
|
142
|
+
this.proc = null;
|
|
143
|
+
}
|
|
144
|
+
async callWithTimeout(method, params, ms) {
|
|
145
|
+
const id = this.nextId++;
|
|
146
|
+
const env = {
|
|
147
|
+
jsonrpc: "2.0",
|
|
148
|
+
id,
|
|
149
|
+
method,
|
|
150
|
+
params
|
|
151
|
+
};
|
|
152
|
+
return new Promise((resolve, reject) => {
|
|
153
|
+
const to = setTimeout(() => {
|
|
154
|
+
this.pending.delete(id);
|
|
155
|
+
reject(new Error(`executa ${method} timed out after ${ms}ms`));
|
|
156
|
+
}, ms);
|
|
157
|
+
this.pending.set(id, {
|
|
158
|
+
resolve: (v) => {
|
|
159
|
+
clearTimeout(to);
|
|
160
|
+
resolve(v);
|
|
161
|
+
},
|
|
162
|
+
reject: (e) => {
|
|
163
|
+
clearTimeout(to);
|
|
164
|
+
reject(e);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
this.proc.stdin.write(`${JSON.stringify(env)}\n`);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
handleLine(line, stderrSink) {
|
|
171
|
+
if (!line.trim()) return;
|
|
172
|
+
let env;
|
|
173
|
+
try {
|
|
174
|
+
env = JSON.parse(line);
|
|
175
|
+
} catch {
|
|
176
|
+
stderrSink(`non-json from executa stdout: ${line}`);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (typeof env.method === "string") {
|
|
180
|
+
this.handleReverse(env);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const id = env.id;
|
|
184
|
+
if (typeof id !== "number") return;
|
|
185
|
+
const pending = this.pending.get(id);
|
|
186
|
+
if (!pending) return;
|
|
187
|
+
this.pending.delete(id);
|
|
188
|
+
if (env.error) {
|
|
189
|
+
const e = env.error;
|
|
190
|
+
const err = new Error(`[${e.code}] ${e.message}`);
|
|
191
|
+
err.rpc = e;
|
|
192
|
+
pending.reject(err);
|
|
193
|
+
} else pending.resolve(env.result);
|
|
194
|
+
}
|
|
195
|
+
async handleReverse(env) {
|
|
196
|
+
const id = env.id;
|
|
197
|
+
const method = env.method;
|
|
198
|
+
const params = env.params ?? {};
|
|
199
|
+
const respond = (body) => {
|
|
200
|
+
if (id === void 0 || id === null) return;
|
|
201
|
+
if (!this.proc) return;
|
|
202
|
+
const out = {
|
|
203
|
+
jsonrpc: "2.0",
|
|
204
|
+
id,
|
|
205
|
+
...body
|
|
206
|
+
};
|
|
207
|
+
try {
|
|
208
|
+
this.proc.stdin.write(`${JSON.stringify(out)}\n`);
|
|
209
|
+
} catch {}
|
|
210
|
+
};
|
|
211
|
+
if (method === "sampling/createMessage") {
|
|
212
|
+
if (!this.opts.sampling) {
|
|
213
|
+
respond({ error: {
|
|
214
|
+
code: -32008,
|
|
215
|
+
message: "sampling not configured (use `anna-app login` and rerun without `--no-sampling`, or pass `--mock-sampling`)"
|
|
216
|
+
} });
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
const result = await this.opts.sampling.createMessage(params);
|
|
221
|
+
respond({ result });
|
|
222
|
+
} catch (e) {
|
|
223
|
+
const code = e.rpcCode ?? -32e3;
|
|
224
|
+
respond({ error: {
|
|
225
|
+
code,
|
|
226
|
+
message: e.message
|
|
227
|
+
} });
|
|
228
|
+
}
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (method.startsWith("agent/")) {
|
|
232
|
+
if (!this.opts.agent) {
|
|
233
|
+
respond({ error: {
|
|
234
|
+
code: -32041,
|
|
235
|
+
message: "agent reverse-RPC not configured (rerun without `--no-agent`, or pass `--mock-agent <fixture>`)"
|
|
236
|
+
} });
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
const result = await this.opts.agent.call(method, params);
|
|
241
|
+
respond({ result });
|
|
242
|
+
} catch (e) {
|
|
243
|
+
const code = e.rpcCode ?? -32e3;
|
|
244
|
+
respond({ error: {
|
|
245
|
+
code,
|
|
246
|
+
message: e.message
|
|
247
|
+
} });
|
|
248
|
+
}
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (method.startsWith("storage/") || method.startsWith("files/")) {
|
|
252
|
+
if (!this.opts.storage) {
|
|
253
|
+
respond({ error: {
|
|
254
|
+
code: -32021,
|
|
255
|
+
message: "storage not configured (rerun with `--storage memory|mock|real`)"
|
|
256
|
+
} });
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
const result = await this.opts.storage.call(method, params);
|
|
261
|
+
respond({ result });
|
|
262
|
+
} catch (e) {
|
|
263
|
+
const code = e.rpcCode ?? -32e3;
|
|
264
|
+
respond({ error: {
|
|
265
|
+
code,
|
|
266
|
+
message: e.message
|
|
267
|
+
} });
|
|
268
|
+
}
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
respond({ error: {
|
|
272
|
+
code: -32601,
|
|
273
|
+
message: `unknown reverse method: ${method}`
|
|
274
|
+
} });
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
//#endregion
|
|
279
|
+
export { ExecutaRunner };
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { canonicalHost, getAccount } from "./credentials-BTv2IfUZ.js";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
|
|
5
|
+
//#region src/executa/sampling.ts
|
|
6
|
+
var SamplingBridge = class {
|
|
7
|
+
mocks = [];
|
|
8
|
+
cachedSession = null;
|
|
9
|
+
constructor(opts) {
|
|
10
|
+
this.opts = opts;
|
|
11
|
+
if (opts.mode === "mock" && opts.mockFile) {
|
|
12
|
+
const path = resolve(opts.mockFile);
|
|
13
|
+
if (existsSync(path)) for (const line of readFileSync(path, "utf8").split(/\r?\n/)) {
|
|
14
|
+
if (!line.trim() || line.startsWith("#")) continue;
|
|
15
|
+
try {
|
|
16
|
+
this.mocks.push(JSON.parse(line));
|
|
17
|
+
} catch {}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async createMessage(req) {
|
|
22
|
+
if (this.opts.mode === "off") throw withCode(new Error("sampling disabled — rerun without `--no-sampling`"), -32008);
|
|
23
|
+
if (this.opts.mode === "mock") return this.mockMessage(req);
|
|
24
|
+
return this.realMessage(req);
|
|
25
|
+
}
|
|
26
|
+
mockMessage(req) {
|
|
27
|
+
const content = collectContent(req);
|
|
28
|
+
const entry = this.mocks.find((m) => m.ns === "sampling" && m.method === "createMessage" || m.ns === "llm" && m.method === "complete") ?? null;
|
|
29
|
+
const matched = entry && (!entry.match?.contentIncludes || content.includes(entry.match.contentIncludes)) ? entry : this.mocks.find((m) => m.ns === "sampling" && m.method === "createMessage" || m.ns === "llm" && m.method === "complete");
|
|
30
|
+
if (matched && matched.result) return normaliseSamplingResult(matched.result, "mock-model");
|
|
31
|
+
return {
|
|
32
|
+
role: "assistant",
|
|
33
|
+
content: {
|
|
34
|
+
type: "text",
|
|
35
|
+
text: "(mock) no fixture matched"
|
|
36
|
+
},
|
|
37
|
+
model: "mock-model",
|
|
38
|
+
stopReason: "endTurn"
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
account() {
|
|
42
|
+
const acc = getAccount(this.opts.account);
|
|
43
|
+
if (!acc) throw withCode(new Error("no PAT on disk — run `anna-app login --host <nexus-url>` first (or pass `--mock-sampling <fixture>` / `--no-sampling`)"), -32001);
|
|
44
|
+
if (acc.expires_at && acc.expires_at < Math.floor(Date.now() / 1e3)) throw withCode(new Error("PAT expired — run `anna-app login` again"), -32001);
|
|
45
|
+
return acc;
|
|
46
|
+
}
|
|
47
|
+
async mint() {
|
|
48
|
+
if (this.cachedSession && this.cachedSession.expiresAt - 30 > Math.floor(Date.now() / 1e3)) return this.cachedSession;
|
|
49
|
+
const acc = this.account();
|
|
50
|
+
if (!this.opts.appSlug) throw withCode(new Error("sampling bridge has no app_slug — pass `--app-slug <slug>` to `anna-app executa dev` so nexus can attribute the spend"), -32602);
|
|
51
|
+
const url = `${canonicalHost(acc.host)}/api/v1/anna-apps/dev/session/mint`;
|
|
52
|
+
const res = await fetch(url, {
|
|
53
|
+
method: "POST",
|
|
54
|
+
headers: { "content-type": "application/json" },
|
|
55
|
+
body: JSON.stringify({
|
|
56
|
+
pat: acc.pat,
|
|
57
|
+
kind: "complete",
|
|
58
|
+
app_slug: this.opts.appSlug
|
|
59
|
+
})
|
|
60
|
+
});
|
|
61
|
+
if (!res.ok) {
|
|
62
|
+
const text = await res.text().catch(() => "");
|
|
63
|
+
throw withCode(new Error(`session.mint failed: HTTP ${res.status} ${text}`), -32e3);
|
|
64
|
+
}
|
|
65
|
+
const body = await res.json();
|
|
66
|
+
this.cachedSession = {
|
|
67
|
+
appSessionToken: body.app_session_token,
|
|
68
|
+
expiresAt: Math.floor(Date.now() / 1e3) + (body.expires_in || 600)
|
|
69
|
+
};
|
|
70
|
+
return this.cachedSession;
|
|
71
|
+
}
|
|
72
|
+
async realMessage(req) {
|
|
73
|
+
const acc = this.account();
|
|
74
|
+
const session = await this.mint();
|
|
75
|
+
const body = mcpToCompleteBody(req);
|
|
76
|
+
const res = await fetch(`${canonicalHost(acc.host)}/api/v1/copilot/app/complete`, {
|
|
77
|
+
method: "POST",
|
|
78
|
+
headers: {
|
|
79
|
+
"content-type": "application/json",
|
|
80
|
+
authorization: `Bearer ${session.appSessionToken}`
|
|
81
|
+
},
|
|
82
|
+
body: JSON.stringify(body)
|
|
83
|
+
});
|
|
84
|
+
if (!res.ok) {
|
|
85
|
+
const text = await res.text().catch(() => "");
|
|
86
|
+
throw withCode(new Error(`HTTP ${res.status}: ${text}`), -32e3);
|
|
87
|
+
}
|
|
88
|
+
const out = await res.json();
|
|
89
|
+
return normaliseSamplingResult(out, out.model ?? "unknown");
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
function withCode(err, code) {
|
|
93
|
+
err.rpcCode = code;
|
|
94
|
+
return err;
|
|
95
|
+
}
|
|
96
|
+
/** Collect a single string view of a sampling request — used for mock matching. */
|
|
97
|
+
function collectContent(req) {
|
|
98
|
+
const parts = [];
|
|
99
|
+
if (req.systemPrompt) parts.push(req.systemPrompt);
|
|
100
|
+
for (const m of req.messages ?? []) {
|
|
101
|
+
const c = m.content;
|
|
102
|
+
if (typeof c === "string") parts.push(c);
|
|
103
|
+
else if (c && typeof c === "object" && "text" in c) parts.push(c.text);
|
|
104
|
+
}
|
|
105
|
+
return parts.join("\n");
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Translate an MCP `sampling/createMessage` request to the nexus
|
|
109
|
+
* `/api/v1/copilot/app/complete` body. Mapping is intentionally lossy —
|
|
110
|
+
* we forward only the fields nexus understands.
|
|
111
|
+
*/
|
|
112
|
+
function mcpToCompleteBody(req) {
|
|
113
|
+
const messages = (req.messages ?? []).map((m) => ({
|
|
114
|
+
role: m.role,
|
|
115
|
+
content: typeof m.content === "string" ? m.content : m.content.text
|
|
116
|
+
}));
|
|
117
|
+
const out = { messages };
|
|
118
|
+
if (req.systemPrompt) out.system = req.systemPrompt;
|
|
119
|
+
if (req.maxTokens != null) out.max_tokens = req.maxTokens;
|
|
120
|
+
if (req.temperature != null) out.temperature = req.temperature;
|
|
121
|
+
if (req.stopSequences) out.stop = req.stopSequences;
|
|
122
|
+
const hint = req.modelPreferences?.hints?.[0]?.name;
|
|
123
|
+
if (hint) out.model_hint = hint;
|
|
124
|
+
if (req.metadata) out.metadata = req.metadata;
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
/** Coerce arbitrary nexus / fixture output into a `SamplingResult`. */
|
|
128
|
+
function normaliseSamplingResult(raw, fallbackModel) {
|
|
129
|
+
if (!raw || typeof raw !== "object") return {
|
|
130
|
+
role: "assistant",
|
|
131
|
+
content: {
|
|
132
|
+
type: "text",
|
|
133
|
+
text: String(raw ?? "")
|
|
134
|
+
},
|
|
135
|
+
model: fallbackModel,
|
|
136
|
+
stopReason: "endTurn"
|
|
137
|
+
};
|
|
138
|
+
const r = raw;
|
|
139
|
+
if (r.role === "assistant" && r.content && typeof r.content === "object") return r;
|
|
140
|
+
const text = typeof r.text === "string" && r.text || (typeof r.content === "string" ? r.content : "") || (typeof r.message === "string" ? r.message : "") || "";
|
|
141
|
+
return {
|
|
142
|
+
role: "assistant",
|
|
143
|
+
content: {
|
|
144
|
+
type: "text",
|
|
145
|
+
text
|
|
146
|
+
},
|
|
147
|
+
model: r.model ?? fallbackModel,
|
|
148
|
+
stopReason: r.stop_reason ?? r.stopReason ?? "endTurn",
|
|
149
|
+
usage: r.usage ?? void 0,
|
|
150
|
+
_meta: { source: "nexus" }
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
//#endregion
|
|
155
|
+
export { SamplingBridge };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { canonicalHost, getAccount } from "./credentials-
|
|
1
|
+
import { canonicalHost, getAccount } from "./credentials-BTv2IfUZ.js";
|
|
2
2
|
import { dirname, join, normalize, resolve } from "node:path";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { createReadStream, existsSync, readFileSync, statSync, watch } from "node:fs";
|
|
@@ -48,12 +48,12 @@ var LlmBridge = class {
|
|
|
48
48
|
const cached = this.mintedAuto.get(windowUuid);
|
|
49
49
|
if (cached && cached.expiresAt - 30 > Math.floor(Date.now() / 1e3)) return cached;
|
|
50
50
|
const acc = this.account();
|
|
51
|
+
const slug = this.requireAppSlug();
|
|
51
52
|
const body = {
|
|
52
53
|
pat: acc.pat,
|
|
53
54
|
kind: "complete",
|
|
54
|
-
|
|
55
|
+
app_slug: slug
|
|
55
56
|
};
|
|
56
|
-
if (!body.app_id) body.app_id = 1;
|
|
57
57
|
const minted = await this.callMint(acc.host, body);
|
|
58
58
|
const ms = {
|
|
59
59
|
appSessionToken: minted.app_session_token,
|
|
@@ -68,13 +68,14 @@ var LlmBridge = class {
|
|
|
68
68
|
/** Mint a kind=agent session — called from `agent.session.create`. */
|
|
69
69
|
async mintAgent(args) {
|
|
70
70
|
const acc = this.account();
|
|
71
|
+
const slug = this.requireAppSlug();
|
|
71
72
|
const submode = args.submode ?? "auto";
|
|
72
73
|
const body = {
|
|
73
74
|
pat: acc.pat,
|
|
74
75
|
kind: "agent",
|
|
75
76
|
submode,
|
|
76
77
|
fixed_client_id: args.fixed_client_id ?? args.fixedClientId ?? null,
|
|
77
|
-
|
|
78
|
+
app_slug: slug,
|
|
78
79
|
label: args.label ?? "anna-app dev",
|
|
79
80
|
quota_caps: args.quotaCaps ?? null
|
|
80
81
|
};
|
|
@@ -89,6 +90,11 @@ var LlmBridge = class {
|
|
|
89
90
|
this.mintedAgent.set(ms.appSessionUuid, ms);
|
|
90
91
|
return ms;
|
|
91
92
|
}
|
|
93
|
+
/** Throws a friendly error if no appSlug was wired into the bridge. */
|
|
94
|
+
requireAppSlug() {
|
|
95
|
+
if (!this.opts.appSlug) throw new Error("llm bridge has no app_slug — the harness must register the dev app via POST /api/v1/anna-apps/dev/apps/register before minting sessions. (Run `anna-app login` once, then `anna-app dev` will auto-register the manifest slug.)");
|
|
96
|
+
return this.opts.appSlug;
|
|
97
|
+
}
|
|
92
98
|
async callMint(host, body) {
|
|
93
99
|
const url = `${canonicalHost(host)}/api/v1/anna-apps/dev/session/mint`;
|
|
94
100
|
const res = await fetch(url, {
|