@a-company/gnomon 0.0.0-stage → 8.0.2

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.
Files changed (71) hide show
  1. package/README.md +126 -2
  2. package/dist/agent.d.ts +28 -0
  3. package/dist/agent.js +45708 -0
  4. package/dist/build-id.json +1 -0
  5. package/dist/cli/gnomon.js +53946 -0
  6. package/dist/cli/gnomond.js +47744 -0
  7. package/dist/gnomon-local/cli.js +897 -0
  8. package/dist/index.d.ts +3477 -0
  9. package/dist/index.js +13698 -0
  10. package/dist/loops.d.ts +440 -0
  11. package/dist/loops.js +4706 -0
  12. package/dist/signer-CQ3Bpe7G.d.ts +158 -0
  13. package/dist/transport/gnomon-web.js +688 -0
  14. package/gnomon-mods/.purpose +143 -0
  15. package/gnomon-mods/constellation/README.md +27 -0
  16. package/gnomon-mods/constellation/fixtures/map-the-idea.golden.json +19 -0
  17. package/gnomon-mods/constellation/fixtures/map-the-idea.json +3 -0
  18. package/gnomon-mods/constellation/fixtures/summarize-the-session.golden.json +20 -0
  19. package/gnomon-mods/constellation/fixtures/summarize-the-session.json +3 -0
  20. package/gnomon-mods/constellation/fixtures/two-blocks.golden.json +4 -0
  21. package/gnomon-mods/constellation/fixtures/two-blocks.json +3 -0
  22. package/gnomon-mods/constellation/gnomon-manifest.json +68 -0
  23. package/gnomon-mods/constellation/skills/constellation-desk/SKILL.md +35 -0
  24. package/gnomon-mods/ledger/README.md +10 -0
  25. package/gnomon-mods/ledger/fixtures/breaker.golden.json +28 -0
  26. package/gnomon-mods/ledger/fixtures/breaker.json +80 -0
  27. package/gnomon-mods/ledger/fixtures/compacted.golden.json +42 -0
  28. package/gnomon-mods/ledger/fixtures/compacted.json +103 -0
  29. package/gnomon-mods/ledger/fixtures/over-budget.golden.json +24 -0
  30. package/gnomon-mods/ledger/fixtures/over-budget.json +86 -0
  31. package/gnomon-mods/ledger/fixtures/strip-reduced.golden.json +23 -0
  32. package/gnomon-mods/ledger/fixtures/strip-reduced.json +76 -0
  33. package/gnomon-mods/ledger/fixtures/strip.golden.json +18 -0
  34. package/gnomon-mods/ledger/fixtures/strip.json +68 -0
  35. package/gnomon-mods/ledger/gnomon-manifest.json +16 -0
  36. package/gnomon-mods/ledger/index.mjs +77 -0
  37. package/gnomon-mods/ledger/model.mjs +237 -0
  38. package/gnomon-mods/neurons/README.md +31 -0
  39. package/gnomon-mods/neurons/fixtures/capture-a-decision.golden.json +20 -0
  40. package/gnomon-mods/neurons/fixtures/capture-a-decision.json +3 -0
  41. package/gnomon-mods/neurons/fixtures/capture-a-learning.golden.json +19 -0
  42. package/gnomon-mods/neurons/fixtures/capture-a-learning.json +3 -0
  43. package/gnomon-mods/neurons/fixtures/search-the-brain.golden.json +19 -0
  44. package/gnomon-mods/neurons/fixtures/search-the-brain.json +3 -0
  45. package/gnomon-mods/neurons/fixtures/undeclared-kind.golden.json +9 -0
  46. package/gnomon-mods/neurons/fixtures/undeclared-kind.json +3 -0
  47. package/gnomon-mods/neurons/gnomon-manifest.json +87 -0
  48. package/gnomon-mods/neurons/skills/neurons-desk/SKILL.md +44 -0
  49. package/gnomon-mods/paradigm-context/README.md +9 -0
  50. package/gnomon-mods/paradigm-context/fixtures/empty.golden.json +28 -0
  51. package/gnomon-mods/paradigm-context/fixtures/empty.json +48 -0
  52. package/gnomon-mods/paradigm-context/fixtures/full-dark.golden.json +497 -0
  53. package/gnomon-mods/paradigm-context/fixtures/full-dark.json +134 -0
  54. package/gnomon-mods/paradigm-context/fixtures/full.golden.json +497 -0
  55. package/gnomon-mods/paradigm-context/fixtures/full.json +134 -0
  56. package/gnomon-mods/paradigm-context/fixtures/stale.golden.json +205 -0
  57. package/gnomon-mods/paradigm-context/fixtures/stale.json +69 -0
  58. package/gnomon-mods/paradigm-context/fixtures/unfocused.golden.json +28 -0
  59. package/gnomon-mods/paradigm-context/fixtures/unfocused.json +14 -0
  60. package/gnomon-mods/paradigm-context/gnomon-manifest.json +15 -0
  61. package/gnomon-mods/paradigm-context/index.mjs +64 -0
  62. package/gnomon-mods/paradigm-context/model.mjs +137 -0
  63. package/gnomon-mods/workshop/README.md +12 -0
  64. package/gnomon-mods/workshop/fixtures/strip-reduced.golden.json +56 -0
  65. package/gnomon-mods/workshop/fixtures/strip-reduced.json +16 -0
  66. package/gnomon-mods/workshop/fixtures/strip.golden.json +56 -0
  67. package/gnomon-mods/workshop/fixtures/strip.json +16 -0
  68. package/gnomon-mods/workshop/gnomon-manifest.json +14 -0
  69. package/gnomon-mods/workshop/index.mjs +73 -0
  70. package/gnomon-mods/workshop/model.mjs +152 -0
  71. package/package.json +80 -4
@@ -0,0 +1,897 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { readFileSync } from "node:fs";
5
+
6
+ // src/rpc.ts
7
+ import { realpathSync } from "node:fs";
8
+
9
+ // src/agent.ts
10
+ import { createHash } from "node:crypto";
11
+
12
+ // src/compat.ts
13
+ var preset = (o) => Object.freeze({ api: "openai-completions", developerRole: false, store: false, maxTokensField: "max_tokens", thinkingKwarg: false, slots: false, platforms: Object.freeze(["darwin", "win32", "linux"]), ...o });
14
+ var COMPAT_ALLOWLIST = Object.freeze({
15
+ "llama.cpp": preset({ thinkingKwarg: true, slots: true }),
16
+ ollama: preset({}),
17
+ "lm-studio": preset({}),
18
+ vllm: preset({ platforms: Object.freeze(["linux", "win32"]) }),
19
+ "mlx-lm": preset({ platforms: Object.freeze(["darwin"]) })
20
+ });
21
+ function surfaceTools(tools, surface) {
22
+ if (surface === "none") return [];
23
+ if (surface === "full") return tools.map((t) => ({ ...t }));
24
+ return tools.map((t) => {
25
+ const props = t.parameters.properties ?? {};
26
+ const compact = {};
27
+ for (const [k, v] of Object.entries(props)) compact[k] = { type: typeof v?.type === "string" ? v.type : "string" };
28
+ return { name: t.name, description: t.description.split("\n")[0].slice(0, 120), parameters: { type: "object", properties: compact, required: t.parameters.required ?? [] } };
29
+ });
30
+ }
31
+ function buildRequest(profile, i) {
32
+ const messages = i.messages.map((m) => ({ ...m, role: m.role === "developer" ? "system" : m.role }));
33
+ const body = { model: i.model, messages, [profile.maxTokensField]: i.maxTokens, stream: i.stream, temperature: 0 };
34
+ if (i.stream) body.stream_options = { include_usage: true };
35
+ const tools = i.nativeTools ? surfaceTools(i.tools, i.surface) : [];
36
+ if (tools.length) body.tools = tools.map((t) => ({ type: "function", function: t }));
37
+ if (profile.thinkingKwarg && i.thinking !== void 0) body.chat_template_kwargs = { enable_thinking: i.thinking };
38
+ return body;
39
+ }
40
+
41
+ // src/endpoint.ts
42
+ import net from "node:net";
43
+ var ANTHROPIC = /(^|\.)(anthropic\.com|claude\.ai|claude\.com)$/i;
44
+ function v4Class(h) {
45
+ const [a, b] = h.split(".").map(Number);
46
+ if (a === 127) return "loopback";
47
+ if (a === 0) return "unspecified";
48
+ if (a === 10 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168) return "lan";
49
+ if (a === 169 && b === 254) return "link-local";
50
+ return "public";
51
+ }
52
+ function v6Class(h) {
53
+ const x = h.toLowerCase();
54
+ if (x === "::1") return "loopback";
55
+ if (x === "::") return "unspecified";
56
+ const mapped = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/.exec(x);
57
+ if (mapped) return v4Class(mapped[1]);
58
+ const hex = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(x);
59
+ if (hex) {
60
+ const hi = parseInt(hex[1], 16);
61
+ const lo = parseInt(hex[2], 16);
62
+ return v4Class(`${hi >> 8}.${hi & 255}.${lo >> 8}.${lo & 255}`);
63
+ }
64
+ if (/^f[cd][0-9a-f]{2}:/.test(x)) return "lan";
65
+ if (/^fe[89ab][0-9a-f]:/.test(x)) return "link-local";
66
+ return "public";
67
+ }
68
+ function classifyEndpoint(base, forbiddenPorts = []) {
69
+ let u;
70
+ try {
71
+ u = new URL(base);
72
+ } catch {
73
+ return { ok: false, why: "malformed", host: null };
74
+ }
75
+ const host = u.hostname.replace(/^\[|\]$/g, "");
76
+ if (u.protocol !== "http:" && u.protocol !== "https:") return { ok: false, why: "scheme", host };
77
+ if (u.username || u.password) return { ok: false, why: "userinfo", host };
78
+ const port = u.port ? Number(u.port) : u.protocol === "https:" ? 443 : 80;
79
+ let cls;
80
+ const ip = net.isIP(host);
81
+ if (ip === 4) cls = v4Class(host);
82
+ else if (ip === 6) cls = v6Class(host);
83
+ else if (host.toLowerCase() === "localhost") cls = "loopback";
84
+ else return { ok: false, why: ANTHROPIC.test(host) ? "anthropic-host" : "hostname", host };
85
+ if (cls === "link-local" || cls === "unspecified" || cls === "public") return { ok: false, why: cls, host };
86
+ if (!Number.isInteger(port) || port <= 0 || port > 65535 || forbiddenPorts.includes(port)) return { ok: false, why: "port", host };
87
+ const path2 = u.pathname.replace(/\/+$/, "");
88
+ return { ok: true, base: `${u.protocol}//${u.host}${path2}`, host, port, class: cls };
89
+ }
90
+
91
+ // src/client.ts
92
+ var CAPS = Object.freeze({ sseLine: 1 << 20, content: 1 << 20, toolArgs: 256 * 1024, errorBody: 4 * 1024, body: 2 << 20, stream: 8 << 20 });
93
+ var EndpointRefused = class extends Error {
94
+ constructor(why) {
95
+ super(`endpoint refused: ${why} (loopback/LAN only, law 7)`);
96
+ this.why = why;
97
+ }
98
+ why;
99
+ };
100
+ async function* chunksOf(res) {
101
+ for await (const part of res.body) yield part;
102
+ }
103
+ async function readCapped(res, cap) {
104
+ const dec = new TextDecoder();
105
+ let out = "";
106
+ let n = 0;
107
+ for await (const part of chunksOf(res)) {
108
+ n += part.length;
109
+ if (n > cap) {
110
+ out += dec.decode(part.subarray(0, Math.max(0, part.length - (n - cap))));
111
+ break;
112
+ }
113
+ out += dec.decode(part, { stream: true });
114
+ }
115
+ return out;
116
+ }
117
+ var usageOf = (u) => u && (typeof u.prompt_tokens === "number" || typeof u.completion_tokens === "number") ? { input: u.prompt_tokens ?? 0, output: u.completion_tokens ?? 0 } : null;
118
+ async function complete(base, body, o = {}) {
119
+ const v = classifyEndpoint(base, o.forbiddenPorts);
120
+ if (!v.ok) throw new EndpointRefused(v.why);
121
+ const ctl = new AbortController();
122
+ const outer = o.signal;
123
+ const onOuter = () => ctl.abort(outer?.reason);
124
+ if (outer?.aborted) ctl.abort(outer.reason);
125
+ outer?.addEventListener("abort", onOuter, { once: true });
126
+ const idleMs = o.idleMs ?? 12e4;
127
+ let idle = setTimeout(() => ctl.abort(new Error(`idle > ${idleMs} ms`)), idleMs);
128
+ const touch = () => {
129
+ clearTimeout(idle);
130
+ idle = setTimeout(() => ctl.abort(new Error(`idle > ${idleMs} ms`)), idleMs);
131
+ };
132
+ try {
133
+ return await completeInner(v.base, body, o, ctl.signal, touch);
134
+ } finally {
135
+ clearTimeout(idle);
136
+ outer?.removeEventListener("abort", onOuter);
137
+ }
138
+ }
139
+ async function completeInner(vbase, body, o, signal, touch) {
140
+ const res = await fetch(`${vbase}/v1/chat/completions`, {
141
+ method: "POST",
142
+ headers: { "content-type": "application/json", accept: body.stream ? "text/event-stream" : "application/json" },
143
+ body: JSON.stringify(body),
144
+ redirect: "error",
145
+ signal
146
+ });
147
+ touch();
148
+ if (!res.ok) throw new Error(`completion failed: HTTP ${res.status} ${(await readCapped(res, CAPS.errorBody).catch(() => "")).slice(0, 200)}`);
149
+ if (!body.stream) {
150
+ const j = JSON.parse(await readCapped(res, CAPS.body));
151
+ if (j.error !== void 0) throw new Error(`completion error: ${JSON.stringify(j.error).slice(0, 200)}`);
152
+ const m = j.choices?.[0]?.message;
153
+ return {
154
+ content: m?.content ?? "",
155
+ toolCalls: (m?.tool_calls ?? []).map((t, i) => ({ id: t.id ?? `call-${i + 1}`, name: t.function?.name ?? "", arguments: typeof t.function?.arguments === "string" ? t.function.arguments : JSON.stringify(t.function?.arguments ?? {}) })),
156
+ usage: usageOf(j.usage),
157
+ finishReason: j.choices?.[0]?.finish_reason ?? null
158
+ };
159
+ }
160
+ const out = { content: "", toolCalls: [], usage: null, finishReason: null };
161
+ const byIndex = /* @__PURE__ */ new Map();
162
+ const decoder = new TextDecoder();
163
+ let buf = "";
164
+ const onData = (data) => {
165
+ if (data === "[DONE]") return;
166
+ let c;
167
+ try {
168
+ c = JSON.parse(data);
169
+ } catch {
170
+ return;
171
+ }
172
+ if (c.error !== void 0) throw new Error(`stream error: ${JSON.stringify(c.error).slice(0, 200)}`);
173
+ const u = usageOf(c.usage);
174
+ if (u) out.usage = u;
175
+ const ch = c.choices?.[0];
176
+ if (!ch) return;
177
+ if (ch.finish_reason) out.finishReason = ch.finish_reason;
178
+ const d = ch.delta;
179
+ if (d?.content) {
180
+ if (out.content.length + d.content.length > CAPS.content) throw new Error("content cap exceeded");
181
+ out.content += d.content;
182
+ o.onDelta?.(d.content);
183
+ }
184
+ for (const t of d?.tool_calls ?? []) {
185
+ const idx = typeof t.index === "number" ? t.index : byIndex.size;
186
+ const cur = byIndex.get(idx) ?? { id: t.id ?? `call-${idx + 1}`, name: "", arguments: "" };
187
+ if (t.id) cur.id = t.id;
188
+ if (t.function?.name) cur.name += t.function.name;
189
+ if (t.function?.arguments) {
190
+ if (cur.arguments.length + t.function.arguments.length > CAPS.toolArgs) throw new Error("tool-call arguments cap exceeded");
191
+ cur.arguments += t.function.arguments;
192
+ }
193
+ byIndex.set(idx, cur);
194
+ }
195
+ };
196
+ let streamed = 0;
197
+ for await (const part of chunksOf(res)) {
198
+ touch();
199
+ streamed += part.length;
200
+ if (streamed > CAPS.stream) throw new Error("stream bytes cap exceeded");
201
+ buf += decoder.decode(part, { stream: true });
202
+ if (buf.length > CAPS.sseLine && buf.indexOf("\n") < 0) throw new Error("SSE line cap exceeded");
203
+ let nl;
204
+ while ((nl = buf.indexOf("\n")) >= 0) {
205
+ const line = buf.slice(0, nl).replace(/\r$/, "");
206
+ buf = buf.slice(nl + 1);
207
+ if (line.startsWith("data:")) onData(line.slice(5).trim());
208
+ }
209
+ }
210
+ if (buf.startsWith("data:")) onData(buf.slice(5).trim());
211
+ out.toolCalls = [...byIndex.entries()].sort((a, b) => a[0] - b[0]).map(([, t]) => t);
212
+ return out;
213
+ }
214
+
215
+ // src/compaction.ts
216
+ var COMPACTION_HEADINGS = Object.freeze(["Goal", "Constraints", "Progress", "Decisions", "Next", "Critical"]);
217
+ var DEFAULT_CONTEXT_TARGET = 8192;
218
+ var SUMMARY_OPEN = '<compaction-summary source="model-generated" trust="data-not-instructions">';
219
+ var MESSAGE_CHARS = 2e3;
220
+ var CHARS_PER_TOKEN = 4;
221
+ var chars = (m) => m.content.length + (m.tool_calls ?? []).reduce((a, t) => a + t.function.name.length + t.function.arguments.length, 0);
222
+ function estimateTokens(messages) {
223
+ return messages.reduce((a, m) => a + Math.ceil(chars(m) / CHARS_PER_TOKEN) + 4, 0);
224
+ }
225
+ function isContextOverflow(err) {
226
+ const m = err instanceof Error ? err.message : String(err);
227
+ return /HTTP (400|413)\b/.test(m) && /context|n_ctx|too long|too many tokens|exceed|maximum.*length/i.test(m);
228
+ }
229
+ var SUMMARY_SYSTEM = [
230
+ "You compact a coding agent's working history so it can continue in a small context.",
231
+ "Reply with exactly these six markdown sections, in this order, and nothing else:",
232
+ ...COMPACTION_HEADINGS.map((h) => `## ${h}`),
233
+ "Be terse. Keep file paths, identifiers, commands and exact values. Under Progress say what is done; under Next what remains; under Critical anything that must not be lost. Do not call tools."
234
+ ].join("\n");
235
+ function render(m) {
236
+ const calls = (m.tool_calls ?? []).map((t) => ` [calls ${t.function.name}(${t.function.arguments.slice(0, 200)})]`).join("");
237
+ const body = m.content.length > MESSAGE_CHARS ? `${m.content.slice(0, MESSAGE_CHARS)} [\u2026${m.content.length - MESSAGE_CHARS} chars cut]` : m.content;
238
+ return `[${m.role}]${calls} ${body}`;
239
+ }
240
+ function summaryMessages(history, target) {
241
+ const body = history.filter((m) => m.role !== "system").map(render);
242
+ const cap = Math.floor(target * CHARS_PER_TOKEN * 0.6);
243
+ let kept = body;
244
+ if (body.join("\n").length > cap && body.length > 1) {
245
+ const head = body[0].slice(0, Math.floor(cap / 4));
246
+ const tail = [];
247
+ let n = head.length;
248
+ for (let i = body.length - 1; i >= 1; i--) {
249
+ if (n + body[i].length + 1 > cap) break;
250
+ tail.unshift(body[i]);
251
+ n += body[i].length + 1;
252
+ }
253
+ const elided = body.length - 1 - tail.length;
254
+ kept = [head, ...elided > 0 ? [`[\u2026 ${elided} messages elided \u2026]`] : [], ...tail];
255
+ }
256
+ return [
257
+ { role: "system", content: SUMMARY_SYSTEM },
258
+ { role: "user", content: `Working history to compact:
259
+
260
+ ${kept.join("\n").slice(0, cap)}` }
261
+ ];
262
+ }
263
+ function normaliseSummary(text, maxChars = DEFAULT_CONTEXT_TARGET) {
264
+ const sections = new Map(COMPACTION_HEADINGS.map((h) => [h, []]));
265
+ let cur = null;
266
+ const loose = [];
267
+ const NAME = "(goal|constraints|progress|decisions|next|critical)";
268
+ const FORMS = [new RegExp(`^\\s{0,3}#{1,6}\\s*${NAME}\\s*:?\\s*$`, "i"), new RegExp(`^\\s*\\*\\*${NAME}\\s*:?\\*\\*\\s*:?\\s*(.*)$`, "i"), new RegExp(`^\\s*${NAME}\\s*:\\s*(.*)$`, "i")];
269
+ for (const line of text.split(/\r?\n/)) {
270
+ const m = FORMS.map((f) => f.exec(line)).find((x) => x !== null);
271
+ if (m) {
272
+ cur = COMPACTION_HEADINGS.find((h) => h.toLowerCase() === m[1].toLowerCase());
273
+ if (m[2]) sections.get(cur).push(m[2]);
274
+ continue;
275
+ }
276
+ (cur ? sections.get(cur) : loose).push(line);
277
+ }
278
+ if (loose.join("").trim()) sections.get("Progress").unshift(...loose);
279
+ const out = COMPACTION_HEADINGS.map((h) => {
280
+ const body = sections.get(h).join("\n").trim().replace(/&/g, "&amp;").replace(/</g, "&lt;");
281
+ return `## ${h}
282
+ ${body || "(none)"}`;
283
+ }).join("\n\n");
284
+ return out.length > maxChars ? `${out.slice(0, maxChars)}
285
+ [summary cut]` : out;
286
+ }
287
+ function compactedHistory(history, summary, tailTokens) {
288
+ const system = history[0]?.role === "system" ? history[0] : null;
289
+ const rest = system ? history.slice(1) : history.slice();
290
+ let start = rest.length;
291
+ let sum = 0;
292
+ for (let i = rest.length - 1; i >= 0; i--) {
293
+ sum += estimateTokens([rest[i]]);
294
+ if (sum > tailTokens) break;
295
+ if (rest[i].role === "user" && !rest[i].content.startsWith("<tool_result") || rest[i].role === "assistant") start = i;
296
+ }
297
+ const tail = rest.slice(start);
298
+ const kept = tail.length;
299
+ const block = `${SUMMARY_OPEN}
300
+ ${summary}
301
+ </compaction-summary>`;
302
+ const lead = tail[0]?.role === "user" ? { role: "user", content: `${block}
303
+
304
+ ${tail.shift().content}` } : { role: "user", content: `${block}
305
+
306
+ Continue from Next.` };
307
+ return { messages: [...system ? [system] : [], lead, ...tail], kept };
308
+ }
309
+
310
+ // src/repair.ts
311
+ function tryObject(s) {
312
+ try {
313
+ let v = JSON.parse(s);
314
+ if (typeof v === "string") v = JSON.parse(v);
315
+ return v !== null && typeof v === "object" && !Array.isArray(v) ? v : void 0;
316
+ } catch {
317
+ return void 0;
318
+ }
319
+ }
320
+ function balance(s) {
321
+ const stack = [];
322
+ let inStr = false;
323
+ let esc = false;
324
+ for (const ch of s) {
325
+ if (inStr) {
326
+ if (esc) esc = false;
327
+ else if (ch === "\\") esc = true;
328
+ else if (ch === '"') inStr = false;
329
+ continue;
330
+ }
331
+ if (ch === '"') inStr = true;
332
+ else if (ch === "{") stack.push("}");
333
+ else if (ch === "[") stack.push("]");
334
+ else if ((ch === "}" || ch === "]") && stack[stack.length - 1] === ch) stack.pop();
335
+ }
336
+ return (inStr ? `${s}"` : s) + stack.reverse().join("");
337
+ }
338
+ var STEPS = [
339
+ ["fence", (s) => s.replace(/^\s*```[a-z]*\s*/i, "").replace(/\s*```\s*$/, "")],
340
+ ["smart-quotes", (s) => s.replace(/[“”]/g, '"').replace(/[‘’]/g, "'")],
341
+ ["single-quotes", (s) => s.replace(/'((?:[^'\\]|\\.)*)'/g, (_m, inner) => JSON.stringify(inner.replace(/\\'/g, "'")))],
342
+ ["unquoted-keys", (s) => s.replace(/([{,]\s*)([A-Za-z_$][\w$-]*)\s*:/g, '$1"$2":')],
343
+ ["trailing-commas", (s) => s.replace(/,\s*([}\]])/g, "$1")],
344
+ ["balance", balance]
345
+ ];
346
+ function repairArguments(raw) {
347
+ if (raw !== null && typeof raw === "object" && !Array.isArray(raw)) return { ok: true, value: raw, repaired: false, steps: [] };
348
+ if (typeof raw !== "string") return { ok: false, repaired: false, steps: [] };
349
+ const direct = tryObject(raw.trim() === "" ? "{}" : raw);
350
+ if (direct) return { ok: true, value: direct, repaired: false, steps: [] };
351
+ let s = raw.trim();
352
+ const steps = [];
353
+ for (const [name, f] of STEPS) {
354
+ const next = f(s);
355
+ if (next !== s) {
356
+ steps.push(name);
357
+ s = next;
358
+ }
359
+ const v = tryObject(s);
360
+ if (v) return { ok: true, value: v, repaired: true, steps };
361
+ }
362
+ return { ok: false, repaired: false, steps };
363
+ }
364
+ function textToolPrompt(tools) {
365
+ if (!tools.length) return "";
366
+ const lines = tools.map((t) => `- ${t.name}: ${t.description.split("\n")[0]} \u2014 arguments: ${JSON.stringify(t.parameters.properties ?? {})}`);
367
+ return [
368
+ "You can call tools. To call one, reply with exactly one block per call:",
369
+ '<tool_call>{"name": "<tool name>", "arguments": { ... }}</tool_call>',
370
+ "Tool results come back in <tool_result> blocks. Available tools:",
371
+ ...lines
372
+ ].join("\n");
373
+ }
374
+ function parseTextToolCalls(text) {
375
+ const calls = [];
376
+ const rest = text.replace(/<tool_call>([\s\S]*?)(<\/tool_call>|$)/g, (_m, body) => {
377
+ const r = repairArguments(body);
378
+ if (r.ok && typeof r.value?.name === "string") calls.push({ id: `text-${calls.length + 1}`, name: r.value.name, arguments: r.value.arguments ?? r.value.args ?? {} });
379
+ return "";
380
+ });
381
+ return { calls, rest: rest.trim() };
382
+ }
383
+
384
+ // src/tools.ts
385
+ import { constants, promises as fsp } from "node:fs";
386
+ import * as path from "node:path";
387
+
388
+ // src/protect.ts
389
+ var PROTECTED_PREFIX = "protected-input:";
390
+ var ProtectedRefused = class extends Error {
391
+ };
392
+ function protectGlob(glob) {
393
+ const g = glob.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
394
+ let re = "";
395
+ let wild = false;
396
+ for (let i = 0; i < g.length; i++) {
397
+ const c = g[i];
398
+ if (c === "*") {
399
+ wild = true;
400
+ if (g[i + 1] === "*") {
401
+ i++;
402
+ if (g[i + 1] === "/") {
403
+ i++;
404
+ re += "(?:.*/)?";
405
+ } else re += ".*";
406
+ } else re += "[^/]*";
407
+ } else if (c === "?") {
408
+ wild = true;
409
+ re += "[^/]";
410
+ } else if (c === "{" && g.indexOf("}", i) > i) {
411
+ wild = true;
412
+ const end = g.indexOf("}", i);
413
+ re += `(?:${g.slice(i + 1, end).split(",").map((s) => s.replace(/[.+^${}()|[\]\\*?]/g, "\\$&")).join("|")})`;
414
+ i = end;
415
+ } else re += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
416
+ }
417
+ return new RegExp(wild ? `^${re}$` : `^${re}(?:/.*)?$`, "i");
418
+ }
419
+ function protectedHit(rel, globs) {
420
+ if (rel.split("/").some((c) => c.toLowerCase().replace(/[. ]+$/, "") === "node_modules")) return true;
421
+ return globs.some((re) => re.test(rel));
422
+ }
423
+
424
+ // src/tools.ts
425
+ var MAX_READ = 256 * 1024;
426
+ var PathRefused = class extends Error {
427
+ };
428
+ function isDotGit(component) {
429
+ const c = component.toLowerCase().replace(/[. ]+$/, "");
430
+ return c === ".git" || c === "git~1";
431
+ }
432
+ var str = (v, name) => {
433
+ if (typeof v !== "string" || v.length === 0) throw new Error(`${name} must be a non-empty string`);
434
+ return v;
435
+ };
436
+ var PATH_CASE_INSENSITIVE = Object.freeze({ win32: true });
437
+ var PATH_SEP = Object.freeze({ win32: "\\", darwin: "/", linux: "/" });
438
+ var fold = (p, platform) => PATH_CASE_INSENSITIVE[platform] ? p.toLowerCase() : p;
439
+ function insideRoots(p, roots, platform = process.platform) {
440
+ const fp = fold(p, platform);
441
+ for (const r of roots) {
442
+ const fr = fold(r, platform);
443
+ const sep2 = PATH_SEP[platform] ?? path.sep;
444
+ if (fp === fr || fp.startsWith(fr.endsWith(sep2) ? fr : fr + sep2)) return r;
445
+ }
446
+ return null;
447
+ }
448
+ var inside = (p, roots) => insideRoots(p, roots);
449
+ async function canonRoots(roots) {
450
+ return Promise.all(roots.map((r) => fsp.realpath(r).catch(() => r)));
451
+ }
452
+ async function resolveRead(raw, ctx) {
453
+ const real = await fsp.realpath(path.resolve(ctx.cwd, raw));
454
+ if (!inside(real, await canonRoots(ctx.roots))) throw new PathRefused(`path is outside the session roots: ${raw.slice(0, 200)}`);
455
+ const st = await fsp.stat(real);
456
+ if (st.isFile() && st.nlink > 1) throw new PathRefused("a file with more than one hard link is refused");
457
+ return real;
458
+ }
459
+ async function resolveWrite(raw, ctx) {
460
+ const abs = path.resolve(ctx.cwd, raw);
461
+ let anc = abs;
462
+ const rest = [];
463
+ for (; ; ) {
464
+ try {
465
+ anc = await fsp.realpath(anc);
466
+ break;
467
+ } catch {
468
+ const parent = path.dirname(anc);
469
+ if (parent === anc) throw new PathRefused("no existing ancestor");
470
+ rest.unshift(path.basename(anc));
471
+ anc = parent;
472
+ }
473
+ }
474
+ const target = path.join(anc, ...rest);
475
+ const root = inside(target, await canonRoots(ctx.roots));
476
+ if (!root || target === root) throw new PathRefused(`write outside the session roots is refused (slice 1): ${raw.slice(0, 200)}`);
477
+ if (target.slice(root.length).split(/[\\/]/).some(isDotGit)) throw new PathRefused("writes under .git are refused");
478
+ if (target.slice(root.length).split(/[\\/]/).some((c) => /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(c) || Buffer.byteLength(c, "utf8") > 255)) throw new PathRefused("a path component with control, format or bidi characters, or over 255 bytes, is refused");
479
+ if (ctx.protect) {
480
+ const rel = target.slice(root.length).split(/[\\/]/).filter(Boolean).join("/");
481
+ if (protectedHit(rel, ctx.protect)) throw new ProtectedRefused(`${PROTECTED_PREFIX} ${rel.slice(0, 200)} is a protected goal input of this loop (no edit, no card)`);
482
+ }
483
+ try {
484
+ const st = await fsp.lstat(target);
485
+ if (st.isSymbolicLink()) throw new PathRefused("the final path component is a symlink");
486
+ if (st.nlink > 1) throw new PathRefused("a file with more than one hard link is refused");
487
+ } catch (err) {
488
+ if (err instanceof PathRefused || err instanceof ProtectedRefused) throw err;
489
+ }
490
+ return target;
491
+ }
492
+ var TOOLS = Object.freeze([
493
+ {
494
+ name: "read_file",
495
+ description: "Read a UTF-8 text file (path relative to the working directory).",
496
+ parameters: { type: "object", properties: { path: { type: "string" } }, required: ["path"] },
497
+ run: async (a, ctx) => {
498
+ const fh = await fsp.open(await resolveRead(str(a.path, "path"), ctx), constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
499
+ try {
500
+ const buf = Buffer.alloc(MAX_READ);
501
+ const { bytesRead } = await fh.read(buf, 0, MAX_READ, 0);
502
+ return { content: buf.subarray(0, bytesRead).toString("utf8"), isError: false };
503
+ } finally {
504
+ await fh.close();
505
+ }
506
+ }
507
+ },
508
+ {
509
+ name: "write_file",
510
+ description: "Write a UTF-8 text file, creating parent directories (path relative to the working directory).",
511
+ parameters: { type: "object", properties: { path: { type: "string" }, content: { type: "string" } }, required: ["path", "content"] },
512
+ run: async (a, ctx) => {
513
+ if (typeof a.content !== "string") throw new Error("content must be a string");
514
+ const p = await resolveWrite(str(a.path, "path"), ctx);
515
+ await fsp.mkdir(path.dirname(p), { recursive: true });
516
+ if (!inside(await fsp.realpath(path.dirname(p)), await canonRoots(ctx.roots))) throw new PathRefused("the parent moved outside the session roots");
517
+ const fh = await fsp.open(p, constants.O_WRONLY | constants.O_CREAT | (constants.O_NOFOLLOW ?? 0), 420);
518
+ try {
519
+ if ((await fh.stat()).nlink > 1) throw new PathRefused("a file with more than one hard link is refused");
520
+ await fh.truncate(0);
521
+ await fh.writeFile(a.content);
522
+ } finally {
523
+ await fh.close();
524
+ }
525
+ return { content: `wrote ${Buffer.byteLength(a.content)} bytes to ${path.relative(ctx.cwd, p) || p}`, isError: false };
526
+ }
527
+ },
528
+ {
529
+ name: "list_dir",
530
+ description: 'List a directory (path relative to the working directory; default ".").',
531
+ parameters: { type: "object", properties: { path: { type: "string" } }, required: [] },
532
+ run: async (a, ctx) => {
533
+ const p = await resolveRead(typeof a.path === "string" && a.path ? a.path : ".", ctx);
534
+ const ents = await fsp.readdir(p, { withFileTypes: true });
535
+ return { content: ents.map((e) => e.isDirectory() ? `${e.name}/` : e.name).sort().join("\n"), isError: false };
536
+ }
537
+ }
538
+ ]);
539
+ async function runTool(name, args, ctx) {
540
+ const t = TOOLS.find((x) => x.name === name && (!ctx.allow || ctx.allow.includes(x.name)));
541
+ if (!t) return { content: `unknown tool ${name.slice(0, 64)}`, isError: true };
542
+ try {
543
+ return await t.run(args, ctx);
544
+ } catch (err) {
545
+ return { content: err.message.slice(0, 500), isError: true };
546
+ }
547
+ }
548
+
549
+ // src/agent.ts
550
+ var TARGET_HEADROOM = 0.85;
551
+ var MAX_TOOL_CALLS_PER_TURN = 32;
552
+ var Agent = class {
553
+ #o;
554
+ /** the tools offered (all, or a loop act session's allowed ones) */
555
+ #tools;
556
+ #messages = [];
557
+ #steer = [];
558
+ #follow = [];
559
+ #abort = null;
560
+ #running = false;
561
+ /** sticky until the run ends: an abort during tools or steering is never lost */
562
+ #aborted = false;
563
+ #turn = 0;
564
+ #compactions = 0;
565
+ constructor(o) {
566
+ this.#o = o;
567
+ this.#tools = o.allowTools ? TOOLS.filter((t) => o.allowTools.includes(t.name)) : TOOLS;
568
+ const sys = [o.systemPrompt ?? "You are a careful coding agent working in the user's project.", !o.nativeTools && o.surface !== "none" ? textToolPrompt(this.#tools) : ""].filter(Boolean).join("\n\n");
569
+ this.#messages.push({ role: "system", content: sys });
570
+ }
571
+ get running() {
572
+ return this.#running;
573
+ }
574
+ state() {
575
+ return { running: this.#running, turns: this.#turn, messages: this.#messages.length, compactions: this.#compactions, contextTarget: this.#target, queued: { steer: this.#steer.length, followUp: this.#follow.length } };
576
+ }
577
+ get #target() {
578
+ return this.#o.contextTarget ?? DEFAULT_CONTEXT_TARGET;
579
+ }
580
+ /** tokens the history + reply may use: target × 0.85 minus the tools schema sent natively */
581
+ get #budget() {
582
+ const schema = this.#o.nativeTools ? Math.ceil(JSON.stringify(surfaceTools(this.#tools, this.#o.surface)).length / 4) : 0;
583
+ return Math.floor(this.#target * TARGET_HEADROOM) - schema;
584
+ }
585
+ #request() {
586
+ this.#abort = new AbortController();
587
+ const body = buildRequest(this.#o.profile, { model: this.#o.model, messages: this.#messages, tools: this.#tools, nativeTools: this.#o.nativeTools, surface: this.#o.surface, maxTokens: this.#o.maxTokens, stream: true });
588
+ return complete(this.#o.base, body, { signal: this.#abort.signal, onDelta: (delta) => this.#o.emit({ type: "message_update", delta }), ...this.#o.forbiddenPorts ? { forbiddenPorts: this.#o.forbiddenPorts } : {} });
589
+ }
590
+ /** One summarising completion, then the history is replaced by system + summary + a safe tail (compaction.ts). */
591
+ async #compact(reason, total) {
592
+ const tokensBefore = estimateTokens(this.#messages);
593
+ this.#o.emit({ type: "compaction_start", reason, tokensBefore });
594
+ this.#abort = new AbortController();
595
+ const budget = Math.max(this.#budget, 256);
596
+ const body = buildRequest(this.#o.profile, { model: this.#o.model, messages: summaryMessages(this.#messages, budget), tools: [], nativeTools: false, surface: "none", maxTokens: Math.min(1024, Math.max(256, Math.floor(budget / 8))), stream: false });
597
+ let c;
598
+ try {
599
+ c = await complete(this.#o.base, body, { signal: this.#abort.signal, ...this.#o.forbiddenPorts ? { forbiddenPorts: this.#o.forbiddenPorts } : {} });
600
+ } catch (err) {
601
+ this.#o.emit({ type: "compaction_end", reason, tokensBefore, error: err.message.slice(0, 500) });
602
+ throw err;
603
+ }
604
+ this.#abort = null;
605
+ if (c.usage) {
606
+ total.input += c.usage.input;
607
+ total.output += c.usage.output;
608
+ }
609
+ const summary = normaliseSummary(c.content, budget);
610
+ const { messages, kept } = compactedHistory(this.#messages, summary, Math.floor(budget / 4));
611
+ this.#messages.splice(0, this.#messages.length, ...messages);
612
+ this.#compactions++;
613
+ this.#o.emit({ type: "compaction_end", reason, tokensBefore, tokensAfter: estimateTokens(this.#messages), kept, usage: c.usage, summaryHash: `sha256:${createHash("sha256").update(summary).digest("hex")}`, summaryBytes: Buffer.byteLength(summary, "utf8") });
614
+ }
615
+ steer(text) {
616
+ this.#steer.push(text);
617
+ }
618
+ followUp(text) {
619
+ if (this.#running) this.#follow.push(text);
620
+ else void this.prompt(text);
621
+ }
622
+ abort() {
623
+ if (this.#running) this.#aborted = true;
624
+ this.#abort?.abort();
625
+ }
626
+ async prompt(text) {
627
+ if (this.#running) throw new Error("agent is busy (use steer or follow_up)");
628
+ this.#running = true;
629
+ this.#aborted = false;
630
+ this.#messages.push({ role: "user", content: text });
631
+ const total = { input: 0, output: 0 };
632
+ let stopReason = "stop";
633
+ let error;
634
+ this.#o.emit({ type: "agent_start" });
635
+ try {
636
+ for (let step = 0; ; step++) {
637
+ if (this.#aborted) {
638
+ stopReason = "aborted";
639
+ break;
640
+ }
641
+ if (step >= this.#o.maxSteps) {
642
+ stopReason = "max_steps";
643
+ break;
644
+ }
645
+ const turn = ++this.#turn;
646
+ this.#o.emit({ type: "turn_start", turn });
647
+ let compacted = false;
648
+ if (this.#messages.length > 2 && estimateTokens(this.#messages) + this.#o.maxTokens > this.#budget) {
649
+ await this.#compact("threshold", total);
650
+ compacted = true;
651
+ if (estimateTokens(this.#messages) + this.#o.maxTokens > this.#budget) throw new Error(`history is still over the context budget after compaction (${estimateTokens(this.#messages)} + ${this.#o.maxTokens} > ${this.#budget} tokens)`);
652
+ }
653
+ let c;
654
+ try {
655
+ c = await this.#request();
656
+ } catch (err) {
657
+ if (this.#aborted || !isContextOverflow(err)) throw err;
658
+ if (compacted) throw new Error(`the endpoint still refuses the prompt as too long after this step's compaction: ${err.message.slice(0, 200)}`);
659
+ await this.#compact("overflow", total);
660
+ c = await this.#request();
661
+ }
662
+ this.#abort = null;
663
+ const textCalls = !this.#o.nativeTools && this.#o.surface !== "none" ? parseTextToolCalls(c.content) : { calls: [], rest: c.content };
664
+ const calls = this.#o.surface === "none" ? [] : this.#o.nativeTools ? c.toolCalls.map((t) => ({ id: t.id, name: t.name, arguments: t.arguments })) : textCalls.calls;
665
+ if (c.usage) {
666
+ total.input += c.usage.input;
667
+ total.output += c.usage.output;
668
+ }
669
+ this.#messages.push({
670
+ role: "assistant",
671
+ content: c.content,
672
+ ...this.#o.nativeTools && calls.length ? { tool_calls: calls.map((t) => ({ id: t.id, type: "function", function: { name: t.name, arguments: typeof t.arguments === "string" ? t.arguments : JSON.stringify(t.arguments) } })) } : {}
673
+ });
674
+ this.#o.emit({ type: "message_end", message: { role: "assistant", content: c.content, toolCalls: calls.map((t) => ({ id: t.id, name: t.name })) }, usage: c.usage, finishReason: c.finishReason });
675
+ if (calls.length > MAX_TOOL_CALLS_PER_TURN) throw new Error(`${calls.length} tool calls in one turn (cap ${MAX_TOOL_CALLS_PER_TURN})`);
676
+ const results = await Promise.all(
677
+ calls.map(async (t) => {
678
+ const r = repairArguments(t.arguments);
679
+ if (r.repaired) this.#o.emit({ type: "model_repair", toolCallId: t.id, toolName: t.name, steps: r.steps });
680
+ this.#o.emit({ type: "tool_execution_start", toolCallId: t.id, toolName: t.name, args: r.ok ? r.value : null });
681
+ const res = r.ok ? await runTool(t.name, r.value, { cwd: this.#o.cwd, roots: this.#o.roots, ...this.#o.allowTools ? { allow: this.#o.allowTools } : {}, ...this.#o.protect ? { protect: this.#o.protect } : {} }) : { content: "arguments are not a JSON object (repair failed)", isError: true };
682
+ this.#o.emit({ type: "tool_execution_end", toolCallId: t.id, toolName: t.name, isError: res.isError, result: res.content.slice(0, 4096) });
683
+ return { t, res };
684
+ })
685
+ );
686
+ for (const { t, res } of results) {
687
+ if (this.#o.nativeTools) this.#messages.push({ role: "tool", tool_call_id: t.id, content: res.content });
688
+ else this.#messages.push({ role: "user", content: `<tool_result name="${t.name}"${res.isError ? ' error="true"' : ""}>
689
+ ${res.content}
690
+ </tool_result>` });
691
+ }
692
+ this.#o.emit({ type: "turn_end", turn, usage: c.usage });
693
+ const steering = this.#steer.splice(0);
694
+ for (const s of steering) this.#messages.push({ role: "user", content: s });
695
+ if (!calls.length && !steering.length) break;
696
+ }
697
+ } catch (err) {
698
+ this.#abort = null;
699
+ stopReason = this.#aborted || err.name === "AbortError" ? "aborted" : "error";
700
+ error = err.message.slice(0, 500);
701
+ }
702
+ if (this.#aborted) stopReason = "aborted";
703
+ this.#running = false;
704
+ this.#aborted = false;
705
+ this.#o.emit({ type: "agent_end", stopReason, usage: total, ...error ? { error } : {} });
706
+ const next = this.#follow.shift();
707
+ if (next !== void 0) await this.prompt(next);
708
+ }
709
+ };
710
+
711
+ // src/rpc.ts
712
+ var ConfigError = class extends Error {
713
+ };
714
+ var SURFACES = ["none", "compact", "full"];
715
+ function parseSessionConfig(text) {
716
+ let c;
717
+ try {
718
+ c = JSON.parse(text);
719
+ } catch {
720
+ throw new ConfigError("config is not the JSON subset of YAML");
721
+ }
722
+ const ep = c.endpoint;
723
+ const caps = c.capabilities;
724
+ if (c.schemaVersion !== "gnomon-local-session:v1") throw new ConfigError("schemaVersion must be gnomon-local-session:v1");
725
+ if (typeof c.session !== "string" || typeof c.cwd !== "string") throw new ConfigError("session and cwd are required");
726
+ if (!ep || typeof ep.base !== "string" || typeof ep.model !== "string" || typeof ep.compat !== "string") throw new ConfigError("endpoint{base, model, compat} is required");
727
+ if (!Object.hasOwn(COMPAT_ALLOWLIST, ep.compat)) throw new ConfigError(`compat ${ep.compat.slice(0, 32)} is not in the local allowlist`);
728
+ const forbiddenPorts = Array.isArray(c.forbiddenPorts) ? c.forbiddenPorts.filter((p) => Number.isInteger(p)) : [];
729
+ const v = classifyEndpoint(ep.base, forbiddenPorts);
730
+ if (!v.ok) throw new ConfigError(`endpoint refused: ${v.why} (loopback/LAN only, law 7)`);
731
+ if (!caps || typeof caps.tools !== "boolean") throw new ConfigError("capabilities{tools} is required");
732
+ const surface = typeof c.surface === "string" && SURFACES.includes(c.surface) ? c.surface : "compact";
733
+ const real = (p) => {
734
+ try {
735
+ return realpathSync.native(p);
736
+ } catch {
737
+ throw new ConfigError(`root ${p.slice(0, 200)} does not exist`);
738
+ }
739
+ };
740
+ const rawRoots = Array.isArray(c.roots) && c.roots.length ? c.roots : [c.cwd];
741
+ if (rawRoots.some((r) => typeof r !== "string" || !r.startsWith("/") && !/^[A-Za-z]:[\\/]/.test(r))) throw new ConfigError("roots must be absolute paths");
742
+ const roots = rawRoots.map(real);
743
+ const int = (x, d, max) => Number.isInteger(x) && x > 0 ? Math.min(x, max) : d;
744
+ const maxTokens = int(c.maxTokens, 1024, 32768);
745
+ const contextTarget = Math.max(int(c.contextTarget, 8192, 131072), 1024);
746
+ if (maxTokens >= 0.85 * contextTarget) throw new ConfigError(`maxTokens ${maxTokens} leaves no room under 0.85 x contextTarget ${contextTarget}`);
747
+ return {
748
+ schemaVersion: "gnomon-local-session:v1",
749
+ session: c.session,
750
+ cwd: c.cwd,
751
+ roots,
752
+ endpoint: { base: v.base, model: ep.model, compat: ep.compat },
753
+ capabilities: { tools: caps.tools, nCtx: typeof caps.nCtx === "number" ? caps.nCtx : null },
754
+ surface,
755
+ maxSteps: int(c.maxSteps, 16, 128),
756
+ maxTokens,
757
+ contextTarget,
758
+ forbiddenPorts,
759
+ ...typeof c.systemPrompt === "string" ? { systemPrompt: c.systemPrompt } : {},
760
+ ...strList(c.tools, "tools", (t) => TOOLS.some((x) => x.name === t)),
761
+ ...strList(c.protect, "protect", (g) => g.length > 0 && g.length <= 512)
762
+ };
763
+ }
764
+ function strList(v, key, ok) {
765
+ if (v === void 0) return {};
766
+ if (!Array.isArray(v) || v.length > 4096 || v.some((x) => typeof x !== "string" || !ok(x))) throw new ConfigError(`${key} must be a list of valid strings`);
767
+ return { [key]: [...v] };
768
+ }
769
+ function rpcSession(cfg, emit2) {
770
+ const agent = new Agent({
771
+ base: cfg.endpoint.base,
772
+ model: cfg.endpoint.model,
773
+ profile: COMPAT_ALLOWLIST[cfg.endpoint.compat],
774
+ nativeTools: cfg.capabilities.tools,
775
+ surface: cfg.surface,
776
+ cwd: cfg.cwd,
777
+ roots: cfg.roots,
778
+ maxSteps: cfg.maxSteps,
779
+ maxTokens: cfg.maxTokens,
780
+ contextTarget: cfg.contextTarget,
781
+ forbiddenPorts: cfg.forbiddenPorts,
782
+ ...cfg.systemPrompt !== void 0 ? { systemPrompt: cfg.systemPrompt } : {},
783
+ ...cfg.tools ? { allowTools: cfg.tools } : {},
784
+ ...cfg.protect ? { protect: cfg.protect.map(protectGlob) } : {},
785
+ emit: emit2
786
+ });
787
+ emit2({ type: "model_capabilities", backend: "local", model: cfg.endpoint.model, compat: cfg.endpoint.compat, tools: cfg.capabilities.tools, ctx: cfg.capabilities.nCtx, surface: cfg.surface, textTools: !cfg.capabilities.tools && cfg.surface !== "none", contextTarget: cfg.contextTarget });
788
+ const onLine = (line) => {
789
+ let cmd;
790
+ try {
791
+ cmd = JSON.parse(line);
792
+ } catch {
793
+ emit2({ type: "response", command: "unknown", success: false, error: "malformed line" });
794
+ return;
795
+ }
796
+ const id = typeof cmd.id === "string" ? { id: cmd.id } : {};
797
+ const command = typeof cmd.type === "string" ? cmd.type : "unknown";
798
+ const ok = (data) => emit2({ type: "response", ...id, command, success: true, ...data !== void 0 ? { data } : {} });
799
+ const no = (error) => emit2({ type: "response", ...id, command, success: false, error });
800
+ const msg = typeof cmd.message === "string" && cmd.message.length > 0 ? cmd.message : null;
801
+ switch (command) {
802
+ case "prompt":
803
+ if (!msg) return no("message is required");
804
+ if (agent.running) return no("agent is busy (use steer or follow_up)");
805
+ ok();
806
+ void agent.prompt(msg);
807
+ return;
808
+ case "steer":
809
+ if (!msg) return no("message is required");
810
+ if (!agent.running) return no("agent is idle (nothing to steer)");
811
+ agent.steer(msg);
812
+ return ok();
813
+ case "follow_up":
814
+ if (!msg) return no("message is required");
815
+ agent.followUp(msg);
816
+ return ok();
817
+ case "abort":
818
+ agent.abort();
819
+ return ok();
820
+ case "get_state":
821
+ return ok(agent.state());
822
+ default:
823
+ return no(`unknown command ${command.slice(0, 32)}`);
824
+ }
825
+ };
826
+ return { onLine, agent };
827
+ }
828
+
829
+ // src/cli.ts
830
+ var emit = (ev) => {
831
+ process.stdout.write(`${JSON.stringify(ev)}
832
+ `);
833
+ };
834
+ var Fatal = class extends Error {
835
+ constructor(error, code) {
836
+ super(error);
837
+ this.error = error;
838
+ this.code = code;
839
+ }
840
+ error;
841
+ code;
842
+ };
843
+ var RUNNER_MARKER = "gnomon-local-runner:v1";
844
+ function fatal(error, code) {
845
+ throw new Fatal(error, code);
846
+ }
847
+ function main(argv) {
848
+ const args = /* @__PURE__ */ new Map();
849
+ for (let i = 0; i < argv.length; i += 2) {
850
+ const k = argv[i];
851
+ const v = argv[i + 1];
852
+ if (!["--mode", "--config", "--session"].includes(k) || v === void 0 || args.has(k)) fatal(`bad argv near ${k.slice(0, 32)} (gnomon-local --mode rpc --config <local.yaml> --session <hsid>; ${RUNNER_MARKER})`, 2);
853
+ args.set(k, v);
854
+ }
855
+ if (args.get("--mode") !== "rpc") fatal("only --mode rpc is served", 2);
856
+ const cfgPath = args.get("--config");
857
+ if (!cfgPath) fatal("--config is required", 2);
858
+ let cfg;
859
+ try {
860
+ cfg = parseSessionConfig(readFileSync(cfgPath, "utf8"));
861
+ } catch (err) {
862
+ fatal(err instanceof ConfigError ? err.message : `cannot read config: ${err.message.slice(0, 200)}`, 3);
863
+ }
864
+ if (args.has("--session") && args.get("--session") !== cfg.session) fatal("--session does not match the config", 2);
865
+ const { onLine, agent } = rpcSession(cfg, emit);
866
+ let buf = "";
867
+ let ended = false;
868
+ process.stdin.setEncoding("utf8");
869
+ process.stdin.on("data", (d) => {
870
+ buf += d;
871
+ let nl;
872
+ while ((nl = buf.indexOf("\n")) >= 0) {
873
+ const line = buf.slice(0, nl).trim();
874
+ buf = buf.slice(nl + 1);
875
+ if (line) onLine(line);
876
+ }
877
+ });
878
+ process.stdin.on("end", () => {
879
+ ended = true;
880
+ });
881
+ const t = setInterval(() => {
882
+ if (ended && !agent.running) {
883
+ clearInterval(t);
884
+ process.exitCode = 0;
885
+ process.stdin.destroy();
886
+ }
887
+ }, 50);
888
+ }
889
+ try {
890
+ main(process.argv.slice(2));
891
+ } catch (err) {
892
+ emit({ type: "fatal", error: err instanceof Fatal ? err.error : err.message.slice(0, 200) });
893
+ process.exitCode = err instanceof Fatal ? err.code : 1;
894
+ }
895
+ export {
896
+ RUNNER_MARKER
897
+ };