@freesyntax/notch-cli 0.5.19 → 0.5.21

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 (42) hide show
  1. package/dist/{apply-patch-EBZ5VLO7.js → apply-patch-D5PDUXUC.js} +0 -1
  2. package/dist/{auth-S3FIB42I.js → auth-JQX6MHJG.js} +0 -1
  3. package/dist/builtins/archimedes.toml +18 -0
  4. package/dist/builtins/awaiter.toml +18 -0
  5. package/dist/builtins/euclid.toml +18 -0
  6. package/dist/builtins/hypatia.toml +18 -0
  7. package/dist/builtins/kepler.toml +18 -0
  8. package/dist/builtins/plato.toml +18 -0
  9. package/dist/builtins/ptolemy.toml +18 -0
  10. package/dist/builtins/pythagoras.toml +18 -0
  11. package/dist/chunk-443G6HCC.js +543 -0
  12. package/dist/chunk-GFVLHUSS.js +155 -0
  13. package/dist/chunk-MMBFNIKE.js +509 -0
  14. package/dist/chunk-OSWUX6TC.js +167 -0
  15. package/dist/{chunk-6M6CXXWR.js → chunk-PKZKVOAN.js} +209 -1
  16. package/dist/chunk-QKM27RHS.js +198 -0
  17. package/dist/chunk-TU465P2P.js +3106 -0
  18. package/dist/compression-SQAIQ2UU.js +32 -0
  19. package/dist/{edit-FXWXOFAF.js → edit-JEFEK43H.js} +0 -1
  20. package/dist/{git-XVWI2BT7.js → git-5T5TSQTX.js} +0 -1
  21. package/dist/{github-DOZ2MVQE.js → github-DWRGWX6U.js} +0 -1
  22. package/dist/{glob-XSBN4MDB.js → glob-BI3P4C7Q.js} +0 -1
  23. package/dist/{grep-2A42QPWM.js → grep-VZ3I5GNW.js} +0 -1
  24. package/dist/index.js +5049 -1113
  25. package/dist/{lsp-WUEGBQ3F.js → lsp-UPY6I3L7.js} +0 -1
  26. package/dist/{notebook-5U6PAF6M.js → notebook-FXJBTSPA.js} +0 -1
  27. package/dist/ollama-bench-QQHBIG2D.js +190 -0
  28. package/dist/ollama-launch-2ASVER3S.js +18 -0
  29. package/dist/ollama-usage-2WPCZJJI.js +69 -0
  30. package/dist/{plugins-GJIUZCJ5.js → plugins-OG2P75K5.js} +0 -1
  31. package/dist/{read-LY2VGCZY.js → read-OVJG2XKW.js} +0 -1
  32. package/dist/{server-4JRQH3DT.js → server-7UQKCB2Z.js} +15 -17
  33. package/dist/session-index-SSGOOZXK.js +21 -0
  34. package/dist/{shell-RGXMLRLH.js → shell-4X545EVN.js} +0 -1
  35. package/dist/{task-VIJ3N5EB.js → task-OS3E5F3X.js} +0 -1
  36. package/dist/{tools-XKVTMNR5.js → tools-7WAWS6V4.js} +3 -2
  37. package/dist/{web-fetch-XOH5PUCP.js → web-fetch-KNIV3Z3W.js} +0 -1
  38. package/dist/{write-DOLDW7HM.js → write-NNHLOTYK.js} +0 -1
  39. package/package.json +6 -4
  40. package/dist/chunk-3RG5ZIWI.js +0 -10
  41. package/dist/chunk-75K7DQVI.js +0 -630
  42. package/dist/compression-LPFNGAV6.js +0 -17
@@ -0,0 +1,3106 @@
1
+ import {
2
+ Rollout
3
+ } from "./chunk-QKM27RHS.js";
4
+ import {
5
+ grepTool
6
+ } from "./chunk-6CZCFY6H.js";
7
+ import {
8
+ globTool
9
+ } from "./chunk-6U3ZAGYA.js";
10
+ import {
11
+ webFetchTool
12
+ } from "./chunk-FFB7GK3Y.js";
13
+ import {
14
+ githubTool
15
+ } from "./chunk-GBZGR6ID.js";
16
+ import {
17
+ lspTool
18
+ } from "./chunk-TH6GKC7E.js";
19
+ import {
20
+ notebookTool
21
+ } from "./chunk-KZAS754V.js";
22
+ import {
23
+ taskTool
24
+ } from "./chunk-UR4XL6OM.js";
25
+ import {
26
+ pluginManager
27
+ } from "./chunk-3QUV4JEX.js";
28
+ import {
29
+ loadCredentials
30
+ } from "./chunk-FIFC4V2R.js";
31
+ import {
32
+ readTool
33
+ } from "./chunk-CQMAVWLJ.js";
34
+ import {
35
+ writeTool
36
+ } from "./chunk-O3WZW7GS.js";
37
+ import {
38
+ editTool
39
+ } from "./chunk-YAYPQTOU.js";
40
+ import {
41
+ applyPatchTool
42
+ } from "./chunk-C4CPDDMN.js";
43
+ import {
44
+ shellTool
45
+ } from "./chunk-W4FAGQFL.js";
46
+ import {
47
+ gitTool
48
+ } from "./chunk-FAULT7VE.js";
49
+
50
+ // src/tools/index.ts
51
+ import { tool } from "ai";
52
+
53
+ // src/tools/code-mode.ts
54
+ import vm from "vm";
55
+ import { z } from "zod";
56
+ var DEFAULT_YIELD_TIME_MS = 3e4;
57
+ var MAX_YIELD_TIME_MS = 3e5;
58
+ var DEFAULT_MAX_OUTPUT_TOKENS = 1e3;
59
+ var CHARS_PER_TOKEN = 4;
60
+ var PRAGMA_PREFIX = "// @exec:";
61
+ var EXCLUDED_TOOLS = /* @__PURE__ */ new Set(["code_exec", "code_wait"]);
62
+ var CELLS = /* @__PURE__ */ new Map();
63
+ function nextCellId() {
64
+ return `cell_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
65
+ }
66
+ function parsePragma(input) {
67
+ const firstNewline = input.indexOf("\n");
68
+ if (firstNewline < 0) return { code: input };
69
+ const firstLine = input.slice(0, firstNewline).trimStart();
70
+ if (!firstLine.startsWith(PRAGMA_PREFIX)) return { code: input };
71
+ const rest = input.slice(firstNewline + 1);
72
+ const directive = firstLine.slice(PRAGMA_PREFIX.length).trim();
73
+ if (!directive) return { code: rest };
74
+ try {
75
+ const parsed = JSON.parse(directive);
76
+ if (!parsed || typeof parsed !== "object") return { code: rest };
77
+ const obj = parsed;
78
+ const out = { code: rest };
79
+ if (typeof obj.yield_time_ms === "number" && Number.isFinite(obj.yield_time_ms)) {
80
+ out.yieldTimeMs = obj.yield_time_ms;
81
+ }
82
+ if (typeof obj.max_output_tokens === "number" && Number.isFinite(obj.max_output_tokens)) {
83
+ out.maxOutputTokens = obj.max_output_tokens;
84
+ }
85
+ return out;
86
+ } catch {
87
+ return { code: rest };
88
+ }
89
+ }
90
+ function toJsIdentifier(name) {
91
+ return name.replace(/[^A-Za-z0-9_]/g, "_");
92
+ }
93
+ function buildContext(args) {
94
+ const { realTools, store, stdoutBuf, notifBuf, input, onYield, log } = args;
95
+ const wrappedTools = {};
96
+ for (const [name, tool2] of Object.entries(realTools)) {
97
+ if (EXCLUDED_TOOLS.has(name)) continue;
98
+ const jsName = toJsIdentifier(name);
99
+ const exec = tool2.execute;
100
+ wrappedTools[jsName] = async (params) => {
101
+ try {
102
+ const result = await exec(params ?? {});
103
+ if (!result) return "";
104
+ if (typeof result === "string") return result;
105
+ return result.content ?? "";
106
+ } catch (err) {
107
+ return `Error: ${err.message ?? String(err)}`;
108
+ }
109
+ };
110
+ }
111
+ const captureConsole = (level) => (...a) => {
112
+ const line = a.map((x) => {
113
+ if (x === null || x === void 0) return String(x);
114
+ if (typeof x === "string") return x;
115
+ try {
116
+ return JSON.stringify(x);
117
+ } catch {
118
+ return String(x);
119
+ }
120
+ }).join(" ");
121
+ stdoutBuf.value += (level === "log" ? "" : `[${level}] `) + line + "\n";
122
+ };
123
+ const sandboxConsole = {
124
+ log: captureConsole("log"),
125
+ info: captureConsole("info"),
126
+ warn: captureConsole("warn"),
127
+ error: captureConsole("error"),
128
+ debug: captureConsole("debug")
129
+ };
130
+ const globals = {
131
+ console: sandboxConsole,
132
+ Buffer,
133
+ URL,
134
+ URLSearchParams,
135
+ TextEncoder,
136
+ TextDecoder,
137
+ // Timers
138
+ setTimeout,
139
+ clearTimeout,
140
+ setInterval,
141
+ clearInterval,
142
+ setImmediate,
143
+ clearImmediate,
144
+ // Network
145
+ fetch: globalThis.fetch,
146
+ // WebCrypto
147
+ crypto: globalThis.crypto,
148
+ // Primitives that are mandatory for useful JS
149
+ Promise,
150
+ JSON,
151
+ Math,
152
+ Date,
153
+ Array,
154
+ Object,
155
+ String,
156
+ Number,
157
+ Boolean,
158
+ RegExp,
159
+ Error,
160
+ TypeError,
161
+ RangeError,
162
+ // Tool surface
163
+ tools: wrappedTools,
164
+ ALL_TOOLS: Object.keys(wrappedTools).map((n) => ({ name: n, description: "" })),
165
+ // Per-cell kv
166
+ store: (key, value) => {
167
+ store.set(key, value);
168
+ },
169
+ load: (key) => store.get(key),
170
+ // Extras
171
+ notify: (msg) => {
172
+ const text = typeof msg === "string" ? msg : (() => {
173
+ try {
174
+ return JSON.stringify(msg);
175
+ } catch {
176
+ return String(msg);
177
+ }
178
+ })();
179
+ notifBuf.push(text);
180
+ log(`[code_exec notify] ${text}`);
181
+ },
182
+ yield_control: () => {
183
+ onYield();
184
+ },
185
+ exit: () => {
186
+ throw new __CodeModeExitSignal();
187
+ },
188
+ input
189
+ };
190
+ return vm.createContext(globals, { name: "notch-code-mode" });
191
+ }
192
+ var __CodeModeExitSignal = class extends Error {
193
+ constructor() {
194
+ super("__CODE_MODE_EXIT__");
195
+ this.name = "__CodeModeExitSignal";
196
+ }
197
+ };
198
+ async function runScript(args) {
199
+ const { code, yieldTimeMs, context, wireYield, registerPending } = args;
200
+ const wrapped = `(async () => {
201
+ ${code}
202
+ })()`;
203
+ let script;
204
+ try {
205
+ script = new vm.Script(wrapped, { filename: "code_exec.js" });
206
+ } catch (err) {
207
+ return { status: "error", error: err.message };
208
+ }
209
+ const scriptPromise = (async () => {
210
+ try {
211
+ const p = script.runInContext(context, { timeout: yieldTimeMs });
212
+ return await p;
213
+ } catch (err) {
214
+ if (err instanceof __CodeModeExitSignal || err?.name === "__CodeModeExitSignal") {
215
+ return void 0;
216
+ }
217
+ throw err;
218
+ }
219
+ })();
220
+ scriptPromise.catch(() => {
221
+ });
222
+ registerPending?.(scriptPromise);
223
+ const outcome = await new Promise((resolve) => {
224
+ let settled = false;
225
+ const finish = (o) => {
226
+ if (settled) return;
227
+ settled = true;
228
+ resolve(o);
229
+ };
230
+ wireYield(() => finish({ kind: "yielded" }));
231
+ const timer = setTimeout(() => finish({ kind: "timeout" }), yieldTimeMs);
232
+ scriptPromise.then((value) => {
233
+ clearTimeout(timer);
234
+ finish({ kind: "done", value });
235
+ }).catch((error) => {
236
+ clearTimeout(timer);
237
+ finish({ kind: "error", error });
238
+ });
239
+ });
240
+ if (outcome.kind === "done") {
241
+ let rv;
242
+ if (outcome.value !== void 0) {
243
+ try {
244
+ rv = typeof outcome.value === "string" ? outcome.value : JSON.stringify(outcome.value);
245
+ } catch {
246
+ rv = String(outcome.value);
247
+ }
248
+ }
249
+ return { status: "completed", returnValue: rv };
250
+ }
251
+ if (outcome.kind === "yielded") return { status: "yielded" };
252
+ if (outcome.kind === "timeout") return { status: "timeout" };
253
+ return { status: "error", error: outcome.error.message };
254
+ }
255
+ var CODE_EXEC_DESCRIPTION = `Run JavaScript code to orchestrate/compose tool calls in one round-trip.
256
+
257
+ - Evaluates the provided JavaScript as an async module in a node:vm sandbox.
258
+ - All nested tools are available on the global \`tools\` object, e.g. \`await tools.read({ path: 'foo.ts' })\`. Tool names are normalized JS identifiers (slashes/dots become underscores, so \`tools.mcp__ologs__get_profile(...)\`).
259
+ - Nested tools accept an object and return a string.
260
+ - Accepts raw JavaScript source text \u2014 not JSON, not markdown fences, not a quoted string.
261
+ - Optional first-line pragma: \`// @exec: {"yield_time_ms": 10000, "max_output_tokens": 1000}\`. Pragma values override the structured params.
262
+ - \`yield_time_ms\` asks \`code_exec\` to yield early (status "yielded") if the script is still running. Max 300000ms.
263
+ - \`max_output_tokens\` caps the stdout returned to the model. Default 1000 tokens (~4000 chars).
264
+ - Top-level await is supported.
265
+
266
+ Globals available in the sandbox:
267
+ - \`tools.<name>(args)\`: call any other notch-cli tool.
268
+ - \`console.log/info/warn/error\`: captured and returned as stdout.
269
+ - \`store(key, value)\` / \`load(key)\`: per-cell kv scoped to the current \`cell_id\`.
270
+ - \`notify(msg)\`: log a message to the user terminal without waiting.
271
+ - \`yield_control()\`: yield current stdout back to the model while the script keeps running \u2014 resume with \`code_wait\`.
272
+ - \`exit()\`: end the script immediately.
273
+ - \`fetch\`, \`URL\`, \`URLSearchParams\`, \`crypto\`, \`TextEncoder/Decoder\`, \`Buffer\`, timers \u2014 standard runtime primitives.
274
+ - \`input\`: optional string provided by \`code_wait\` when resuming.
275
+ - \`ALL_TOOLS\`: array of \`{name, description}\` for every tool exposed on \`tools.*\`.
276
+
277
+ Sandbox limits (NOT a hard isolate \u2014 this is node:vm, not V8 isolate):
278
+ - No \`require\`, no \`process\`, no \`fs\`, no module system \u2014 all filesystem access must go through wrapped tools (tools.read / tools.write / tools.edit / ...).
279
+ - \`fetch\` IS exposed; assume arbitrary network egress is possible.
280
+ - A tight sync loop CAN exceed \`yield_time_ms\` (vm timeout catches sync code, but async work is only bounded by an outer race).
281
+
282
+ Return shape: \`{cell_id, status, stdout, return_value?, error?}\`. If status is "yielded" or "timeout", use \`code_wait\` with the same \`cell_id\` to resume.`;
283
+ var CODE_WAIT_DESCRIPTION = `Resume a previously yielded or timed-out \`code_exec\` cell.
284
+
285
+ - Use only after \`code_exec\` returned status "yielded" or "timeout".
286
+ - \`cell_id\` must match the id returned by the original \`code_exec\` call.
287
+ - \`additional_input\` (optional) is injected as the global \`input\` string inside the resumed script.
288
+ - Returns the same shape as \`code_exec\`: \`{cell_id, status, stdout, return_value?, error?}\`.
289
+ - If the cell has already completed, \`code_wait\` returns the final result and forgets the cell.`;
290
+ function clampYield(ms) {
291
+ const v = ms ?? DEFAULT_YIELD_TIME_MS;
292
+ if (!Number.isFinite(v) || v <= 0) return DEFAULT_YIELD_TIME_MS;
293
+ return Math.min(v, MAX_YIELD_TIME_MS);
294
+ }
295
+ function truncate(s, maxChars) {
296
+ if (s.length <= maxChars) return s;
297
+ return s.slice(0, maxChars) + `
298
+
299
+ [...truncated ${s.length - maxChars} chars]`;
300
+ }
301
+ function formatResult(cell, maxChars, extraNotifs) {
302
+ const lines = [];
303
+ lines.push(`cell_id: ${cell.id}`);
304
+ lines.push(`status: ${cell.status}`);
305
+ if (cell.returnValue !== void 0) {
306
+ lines.push(`return_value: ${cell.returnValue.slice(0, 2e3)}`);
307
+ }
308
+ if (cell.error) {
309
+ lines.push(`error: ${cell.error}`);
310
+ }
311
+ if (extraNotifs.length > 0) {
312
+ lines.push(`notifications:
313
+ ${extraNotifs.map((n) => ` - ${n}`).join("\n")}`);
314
+ }
315
+ lines.push(`stdout:
316
+ ${truncate(cell.stdout, maxChars)}`);
317
+ if (cell.status === "yielded" || cell.status === "timeout") {
318
+ lines.push(`
319
+ [cell still running \u2014 resume with code_wait({cell_id: "${cell.id}"})]`);
320
+ }
321
+ return lines.join("\n");
322
+ }
323
+ var execParams = z.object({
324
+ code: z.string().describe("Raw JavaScript source. May begin with `// @exec: {...}` pragma."),
325
+ yield_time_ms: z.number().optional().describe("Yield back to the model after this many ms. Max 300000."),
326
+ max_output_tokens: z.number().optional().describe("Cap stdout returned to the model. Default 1000."),
327
+ cell_id: z.string().optional().describe("Reuse an existing cell_id (rarely needed \u2014 exec allocates one).")
328
+ });
329
+ var codeModeExecTool = {
330
+ name: "code_exec",
331
+ description: CODE_EXEC_DESCRIPTION,
332
+ parameters: execParams,
333
+ execute: async (params, ctx) => {
334
+ const parsed = parsePragma(params.code);
335
+ const yieldTimeMs = clampYield(parsed.yieldTimeMs ?? params.yield_time_ms);
336
+ const maxTokens = parsed.maxOutputTokens ?? params.max_output_tokens ?? DEFAULT_MAX_OUTPUT_TOKENS;
337
+ const maxChars = Math.max(512, maxTokens * CHARS_PER_TOKEN);
338
+ const cellId = params.cell_id ?? nextCellId();
339
+ const realTools = buildToolMap(ctx);
340
+ const stdoutBuf = { value: "" };
341
+ const notifBuf = [];
342
+ const store = /* @__PURE__ */ new Map();
343
+ const cell = {
344
+ id: cellId,
345
+ stdout: "",
346
+ store,
347
+ pending: null,
348
+ status: "error",
349
+ settled: false,
350
+ context: null,
351
+ notifications: notifBuf
352
+ };
353
+ const context = buildContext({
354
+ realTools,
355
+ store,
356
+ stdoutBuf,
357
+ notifBuf,
358
+ onYield: () => cell.yieldResolver?.(),
359
+ log: ctx.log
360
+ });
361
+ cell.context = context;
362
+ CELLS.set(cellId, cell);
363
+ let outcome;
364
+ try {
365
+ outcome = await runScript({
366
+ cellId,
367
+ code: parsed.code,
368
+ yieldTimeMs,
369
+ maxChars,
370
+ context,
371
+ stdoutBuf,
372
+ notifBuf,
373
+ store,
374
+ wireYield: (r) => {
375
+ cell.yieldResolver = r;
376
+ },
377
+ registerPending: (p) => {
378
+ cell.pending = p;
379
+ p.then((value) => {
380
+ cell.stdout = stdoutBuf.value;
381
+ if (!cell.settled) {
382
+ cell.status = "completed";
383
+ if (value !== void 0) {
384
+ try {
385
+ cell.returnValue = typeof value === "string" ? value : JSON.stringify(value);
386
+ } catch {
387
+ cell.returnValue = String(value);
388
+ }
389
+ }
390
+ cell.settled = true;
391
+ }
392
+ }).catch((err) => {
393
+ cell.stdout = stdoutBuf.value;
394
+ if (!cell.settled) {
395
+ cell.status = "error";
396
+ cell.error = err.message;
397
+ cell.settled = true;
398
+ }
399
+ });
400
+ }
401
+ });
402
+ } catch (err) {
403
+ outcome = { status: "error", error: err.message };
404
+ }
405
+ cell.stdout = stdoutBuf.value;
406
+ cell.status = outcome.status;
407
+ if (outcome.returnValue !== void 0) cell.returnValue = outcome.returnValue;
408
+ if (outcome.error) cell.error = outcome.error;
409
+ cell.settled = outcome.status === "completed" || outcome.status === "error";
410
+ if (cell.settled) CELLS.delete(cellId);
411
+ const notifsForThisCall = notifBuf.splice(0, notifBuf.length);
412
+ return {
413
+ content: formatResult(cell, maxChars, notifsForThisCall),
414
+ isError: outcome.status === "error"
415
+ };
416
+ }
417
+ };
418
+ var waitParams = z.object({
419
+ cell_id: z.string().describe("The cell_id returned by code_exec."),
420
+ additional_input: z.string().optional().describe("Injected as the global `input` string inside the resumed script."),
421
+ yield_time_ms: z.number().optional(),
422
+ max_output_tokens: z.number().optional(),
423
+ terminate: z.boolean().optional().describe("If true, stop the cell instead of waiting for more output.")
424
+ });
425
+ var codeModeWaitTool = {
426
+ name: "code_wait",
427
+ description: CODE_WAIT_DESCRIPTION,
428
+ parameters: waitParams,
429
+ execute: async (params, _ctx) => {
430
+ const cell = CELLS.get(params.cell_id);
431
+ if (!cell) {
432
+ return {
433
+ content: `cell_id: ${params.cell_id}
434
+ status: unknown
435
+ error: no running cell with that id (it may have already completed)`,
436
+ isError: true
437
+ };
438
+ }
439
+ const maxTokens = params.max_output_tokens ?? DEFAULT_MAX_OUTPUT_TOKENS;
440
+ const maxChars = Math.max(512, maxTokens * CHARS_PER_TOKEN);
441
+ const yieldTimeMs = clampYield(params.yield_time_ms);
442
+ if (params.terminate) {
443
+ cell.status = "completed";
444
+ cell.error = cell.error ?? "terminated by code_wait";
445
+ cell.settled = true;
446
+ CELLS.delete(cell.id);
447
+ const notifs2 = cell.notifications.splice(0, cell.notifications.length);
448
+ return { content: formatResult(cell, maxChars, notifs2) };
449
+ }
450
+ if (params.additional_input !== void 0) {
451
+ try {
452
+ vm.runInContext(
453
+ `globalThis.input = ${JSON.stringify(params.additional_input)};`,
454
+ cell.context,
455
+ { timeout: 1e3 }
456
+ );
457
+ } catch {
458
+ }
459
+ }
460
+ if (cell.settled) {
461
+ const notifs2 = cell.notifications.splice(0, cell.notifications.length);
462
+ CELLS.delete(cell.id);
463
+ return {
464
+ content: formatResult(cell, maxChars, notifs2),
465
+ isError: cell.status === "error"
466
+ };
467
+ }
468
+ const outcome = await new Promise((resolve) => {
469
+ let settled = false;
470
+ const finish = (v) => {
471
+ if (settled) return;
472
+ settled = true;
473
+ resolve(v);
474
+ };
475
+ cell.yieldResolver = () => finish("yielded");
476
+ const timer = setTimeout(() => finish("timeout"), yieldTimeMs);
477
+ if (cell.pending) {
478
+ cell.pending.then(() => {
479
+ clearTimeout(timer);
480
+ finish("done");
481
+ }).catch(() => {
482
+ clearTimeout(timer);
483
+ finish("done");
484
+ });
485
+ }
486
+ });
487
+ if (outcome === "done") {
488
+ cell.settled = true;
489
+ CELLS.delete(cell.id);
490
+ } else if (outcome === "yielded") {
491
+ cell.status = "yielded";
492
+ } else {
493
+ cell.status = "timeout";
494
+ }
495
+ const notifs = cell.notifications.splice(0, cell.notifications.length);
496
+ return {
497
+ content: formatResult(cell, maxChars, notifs),
498
+ isError: cell.status === "error"
499
+ };
500
+ }
501
+ };
502
+
503
+ // src/tools/skill-manage.ts
504
+ import { z as z2 } from "zod";
505
+ var API_BASE_ENV = "NOTCH_API_URL";
506
+ var DEFAULT_API_BASE = "https://freesyntax.dev";
507
+ var AGENT_ID_ENV = "NOTCH_AGENT_ID";
508
+ var ParamsSchema = z2.object({
509
+ action: z2.enum(["create", "edit", "patch", "delete", "toggle", "list"]),
510
+ /**
511
+ * Target agent id. If omitted, falls back to the `NOTCH_AGENT_ID` env var.
512
+ * In Notch Cloud the task-runner sets this env var when the task was
513
+ * created from an agent. For standalone CLI use, pass it explicitly.
514
+ */
515
+ agent_id: z2.string().uuid().optional(),
516
+ /** Required for action=create | edit (optional for edit, keeps existing). */
517
+ name: z2.string().min(1).max(40).optional(),
518
+ label: z2.string().min(1).max(80).optional(),
519
+ category: z2.string().max(40).optional(),
520
+ /** Body of the SKILL.md. Required for create/edit; ignored otherwise. */
521
+ content: z2.string().min(1).max(16 * 1024).optional(),
522
+ /** For patch: appended to the existing body with a `---` separator. */
523
+ appendContent: z2.string().min(1).max(16 * 1024).optional(),
524
+ /** For edit/create/toggle. */
525
+ enabled: z2.boolean().optional(),
526
+ /** For edit/patch/delete/toggle: which existing skill to operate on. */
527
+ skill_id: z2.string().uuid().optional()
528
+ });
529
+ var DESCRIPTION = `Create, edit, patch, delete, toggle, or list durable "skills" (procedural memory) attached to the current agent. Use this when a non-trivial workflow, error-recovery pattern, or learned convention is worth persisting so future sessions inherit it. All writes are scanned server-side; prompt-override/secret/exfil patterns are rejected.
530
+
531
+ Actions:
532
+ - create: new skill from scratch. Requires name, label, content. Optional category.
533
+ - edit: full replace. Requires skill_id + content. Other fields optional.
534
+ - patch: append to existing. Requires skill_id + appendContent.
535
+ - delete: remove. Requires skill_id.
536
+ - toggle: enable/disable without rewriting. Requires skill_id + enabled.
537
+ - list: enumerate the agent's current skills.
538
+
539
+ SKILL.md body should be terse, imperative, and action-oriented. Include triggers ("when X") and the procedure ("then Y"). Avoid prose.`;
540
+ var skillManageTool = {
541
+ name: "skill_manage",
542
+ description: DESCRIPTION,
543
+ parameters: ParamsSchema,
544
+ execute: async (params, ctx) => {
545
+ const creds = await loadCredentials();
546
+ if (!creds) {
547
+ return {
548
+ isError: true,
549
+ content: "Not authenticated. Run `notch login` first so skills can be persisted to your workspace."
550
+ };
551
+ }
552
+ const agentId = params.agent_id ?? process.env[AGENT_ID_ENV];
553
+ if (!agentId) {
554
+ return {
555
+ isError: true,
556
+ content: `No agent id available. Pass agent_id explicitly, or ensure the task-runner set ${AGENT_ID_ENV}. For standalone CLI use there is no agent to attach skills to \u2014 use a ~/.notch/skills/ local file instead.`
557
+ };
558
+ }
559
+ const base = process.env[API_BASE_ENV] ?? DEFAULT_API_BASE;
560
+ const url = `${base.replace(/\/+$/, "")}/api/agents/${agentId}/skills/manage`;
561
+ const body = buildRequestBody(params);
562
+ if (!body) {
563
+ return {
564
+ isError: true,
565
+ content: `Action "${params.action}" is missing required fields. Check the parameter docs.`
566
+ };
567
+ }
568
+ try {
569
+ const res = await fetch(url, {
570
+ method: "POST",
571
+ headers: {
572
+ Authorization: `Bearer ${creds.token}`,
573
+ "Content-Type": "application/json",
574
+ "User-Agent": "notch-cli/skill_manage"
575
+ },
576
+ body: JSON.stringify(body)
577
+ });
578
+ const text = await res.text();
579
+ let parsed = {};
580
+ try {
581
+ parsed = JSON.parse(text);
582
+ } catch {
583
+ }
584
+ if (!res.ok) {
585
+ const summary = parsed["summary"] ?? String(parsed["error"] ?? res.statusText);
586
+ ctx.log?.(`[skill_manage] ${params.action} \u2192 HTTP ${res.status}: ${summary}`);
587
+ const findings = Array.isArray(parsed["findings"]) ? parsed["findings"] : [];
588
+ return {
589
+ isError: true,
590
+ content: JSON.stringify({
591
+ ok: false,
592
+ status: res.status,
593
+ action: params.action,
594
+ summary,
595
+ findings
596
+ }, null, 2)
597
+ };
598
+ }
599
+ ctx.log?.(`[skill_manage] ${params.action} ok`);
600
+ return {
601
+ content: typeof text === "string" && text.length > 0 ? text : JSON.stringify({ ok: true })
602
+ };
603
+ } catch (err) {
604
+ const message = err instanceof Error ? err.message : String(err);
605
+ return {
606
+ isError: true,
607
+ content: `Network error calling skill_manage: ${message}`
608
+ };
609
+ }
610
+ }
611
+ };
612
+ function buildRequestBody(p) {
613
+ switch (p.action) {
614
+ case "create": {
615
+ if (!p.name || !p.label || !p.content) return null;
616
+ return {
617
+ action: "create",
618
+ name: p.name,
619
+ label: p.label,
620
+ category: p.category ?? null,
621
+ content: p.content,
622
+ enabled: p.enabled ?? true
623
+ };
624
+ }
625
+ case "edit": {
626
+ if (!p.skill_id || !p.content) return null;
627
+ return {
628
+ action: "edit",
629
+ skillId: p.skill_id,
630
+ name: p.name,
631
+ label: p.label,
632
+ category: p.category ?? null,
633
+ content: p.content,
634
+ enabled: p.enabled
635
+ };
636
+ }
637
+ case "patch": {
638
+ if (!p.skill_id || !p.appendContent) return null;
639
+ return {
640
+ action: "patch",
641
+ skillId: p.skill_id,
642
+ appendContent: p.appendContent
643
+ };
644
+ }
645
+ case "delete": {
646
+ if (!p.skill_id) return null;
647
+ return { action: "delete", skillId: p.skill_id };
648
+ }
649
+ case "toggle": {
650
+ if (!p.skill_id || typeof p.enabled !== "boolean") return null;
651
+ return { action: "toggle", skillId: p.skill_id, enabled: p.enabled };
652
+ }
653
+ case "list":
654
+ return { action: "list" };
655
+ }
656
+ }
657
+
658
+ // src/tools/events.ts
659
+ import { z as z3 } from "zod";
660
+
661
+ // src/permissions/handlers/bash-classifier.ts
662
+ var DESTRUCTIVE_PATTERNS = [
663
+ /\brm\s+(-[a-zA-Z]*[rf][a-zA-Z]*|--recursive|--force)/i,
664
+ /\brm\s+-rf?\s+\//i,
665
+ /\bsudo\s+rm\b/i,
666
+ /\bdd\s+if=/i,
667
+ /\bmkfs\./i,
668
+ /\bshred\b/i,
669
+ /\bchmod\s+-R\s+/i,
670
+ /\bchown\s+-R\s+/i,
671
+ /:\(\)\s*\{\s*:\|:&\s*\};:/,
672
+ // fork bomb
673
+ /\b>\s*\/dev\/sd[a-z]/i,
674
+ /\bgit\s+push\s+(?:.*\s)?(?:-f|--force|--force-with-lease)\b/i,
675
+ /\bgit\s+reset\s+--hard\b/i,
676
+ /\bgit\s+clean\s+-[a-zA-Z]*[fd]/i,
677
+ /\bgit\s+checkout\s+--\s+\./,
678
+ /\bnpm\s+publish\b/i,
679
+ /\byarn\s+publish\b/i,
680
+ /\bpnpm\s+publish\b/i,
681
+ /\bdocker\s+(?:rm|rmi|system\s+prune|volume\s+rm)\b/i,
682
+ /\bkubectl\s+delete\b/i,
683
+ /\bterraform\s+destroy\b/i,
684
+ /\bmv\s+\/\s+/i,
685
+ /\bfind\s+.*\s-delete\b/i,
686
+ /\bfind\s+.*-exec\s+rm\b/i,
687
+ /\btruncate\s+-s\s+0/i,
688
+ /\b(poweroff|shutdown|reboot|halt)\b/i,
689
+ /\bkillall?\s+-9/i
690
+ ];
691
+ var NETWORK_PATTERNS = [
692
+ /\bcurl\b/i,
693
+ /\bwget\b/i,
694
+ /\bhttpie?\b/i,
695
+ /\bnc\b/i,
696
+ /\bnetcat\b/i,
697
+ /\bssh\b/i,
698
+ /\bscp\b/i,
699
+ /\brsync\s+.*::?/i,
700
+ /\bftp\b/i,
701
+ /\bsftp\b/i,
702
+ /\btelnet\b/i,
703
+ /\bnmap\b/i,
704
+ /\bping\b/i,
705
+ /\bdig\b/i,
706
+ /\bnslookup\b/i,
707
+ /\bhost\s+[a-z0-9.-]+\.[a-z]+/i,
708
+ /\bgit\s+(?:clone|fetch|pull|push)\b/i,
709
+ /\bnpm\s+(?:install|i|update|audit|fund)\b/i,
710
+ /\bpip\s+install\b/i,
711
+ /\bapt(?:-get)?\s+(?:install|update|upgrade)\b/i,
712
+ /\bbrew\s+(?:install|update|upgrade)\b/i,
713
+ /\b(?:python|python3|node|bun)\s+-c\s+.*(?:urllib|requests|fetch|http)/i,
714
+ /\bcurl.*\|\s*(?:sh|bash|zsh|fish)\b/i,
715
+ // explicit "pipe to shell"
716
+ /\bwget.*\|\s*(?:sh|bash|zsh|fish)\b/i
717
+ ];
718
+ var SAFE_PATTERNS = [
719
+ /^\s*ls(\s|$)/i,
720
+ /^\s*pwd(\s|$)/,
721
+ /^\s*whoami(\s|$)/,
722
+ /^\s*id(\s|$)/,
723
+ /^\s*echo\s/i,
724
+ /^\s*printf\s/i,
725
+ /^\s*cat\s/i,
726
+ /^\s*head\s/i,
727
+ /^\s*tail\s/i,
728
+ /^\s*wc\s/i,
729
+ /^\s*file\s/i,
730
+ /^\s*stat\s/i,
731
+ /^\s*du\s/i,
732
+ /^\s*df\s/i,
733
+ /^\s*which\s/i,
734
+ /^\s*type\s/i,
735
+ /^\s*env(\s|$)/i,
736
+ /^\s*date(\s|$)/i,
737
+ /^\s*uname/i,
738
+ /^\s*hostname(\s|$)/i,
739
+ /^\s*uptime(\s|$)/i,
740
+ /^\s*history(\s|$)/i,
741
+ /^\s*git\s+(status|log|diff|show|branch|blame|config\s+--get|remote\s+-v|stash\s+list|tag\s+-l|ls-files)\b/i,
742
+ /^\s*node\s+--version/i,
743
+ /^\s*npm\s+(ls|list|outdated|view|config\s+get|--version|-v|root)\b/i,
744
+ /^\s*yarn\s+(list|--version)\b/i,
745
+ /^\s*(python|python3)\s+--version/i,
746
+ /^\s*pip\s+(list|show|--version)\b/i,
747
+ /^\s*(rg|ripgrep)\s/i,
748
+ /^\s*grep\s/i,
749
+ /^\s*find\s+[^|;&]*(?<!-delete)\s*$/i,
750
+ // find without -delete/-exec rm
751
+ /^\s*tree(\s|$)/i,
752
+ /^\s*jq\s/i,
753
+ /^\s*awk\s/i,
754
+ /^\s*sed\s+-n\s/i,
755
+ // sed in print-only mode
756
+ /^\s*(make|cargo|go|npm|bun|pnpm|yarn)\s+(test|check|vet|fmt\s+--check|build\s+--dry-run)\b/i,
757
+ /^\s*(cargo|rustc)\s+--version/i,
758
+ /^\s*docker\s+(ps|images|logs|inspect|version)\b/i,
759
+ /^\s*kubectl\s+(get|describe|logs|version|config\s+current-context)\b/i
760
+ ];
761
+ function classifyBashCommand(command) {
762
+ const cmd = command.trim();
763
+ if (!cmd) return { category: "safe" };
764
+ const head = cmd.split(/[\s;|&]/)[0] ?? "";
765
+ for (const p of DESTRUCTIVE_PATTERNS) {
766
+ if (p.test(cmd)) {
767
+ return { category: "destructive", matchedPattern: p.source, head };
768
+ }
769
+ }
770
+ for (const p of NETWORK_PATTERNS) {
771
+ if (p.test(cmd)) {
772
+ return { category: "network", matchedPattern: p.source, head };
773
+ }
774
+ }
775
+ for (const p of SAFE_PATTERNS) {
776
+ if (p.test(cmd)) {
777
+ return { category: "safe", matchedPattern: p.source, head };
778
+ }
779
+ }
780
+ return { category: "unknown", head };
781
+ }
782
+
783
+ // src/permissions/handlers/interactive.ts
784
+ var SESSION_RECENT_MS = 3e4;
785
+ var recentApprovals = /* @__PURE__ */ new Map();
786
+ function fingerprintArgs(args) {
787
+ const keys = Object.keys(args).sort();
788
+ const parts = [];
789
+ for (const k of keys) {
790
+ const v = args[k];
791
+ const s = typeof v === "string" ? v : JSON.stringify(v);
792
+ parts.push(`${k}=${s.slice(0, 200)}`);
793
+ }
794
+ return parts.join("|");
795
+ }
796
+ function makeKey(sessionId, toolName, args) {
797
+ return `${sessionId}\0${toolName}\0${fingerprintArgs(args)}`;
798
+ }
799
+ function recordInteractiveApproval(sessionId, toolName, args) {
800
+ const key = makeKey(sessionId, toolName, args);
801
+ recentApprovals.set(key, { key, expires: Date.now() + SESSION_RECENT_MS });
802
+ }
803
+ function wasRecentlyApproved(sessionId, toolName, args) {
804
+ const key = makeKey(sessionId, toolName, args);
805
+ const hit = recentApprovals.get(key);
806
+ if (!hit) return false;
807
+ if (hit.expires < Date.now()) {
808
+ recentApprovals.delete(key);
809
+ return false;
810
+ }
811
+ return true;
812
+ }
813
+ var interactiveHandler = {
814
+ surface: "interactive",
815
+ check: async (toolName, args, baseDecision, ctx) => {
816
+ if (baseDecision === "allow") {
817
+ if (toolName === "shell" && typeof args.command === "string") {
818
+ const pending = async () => {
819
+ const cls = classifyBashCommand(args.command);
820
+ if (cls.category === "destructive") {
821
+ return {
822
+ outcome: "prompt",
823
+ reason: `classifier flagged destructive command: ${cls.matchedPattern}`
824
+ };
825
+ }
826
+ return { outcome: "allow" };
827
+ };
828
+ return { outcome: "allow", pendingClassifierCheck: pending };
829
+ }
830
+ return { outcome: "allow" };
831
+ }
832
+ if (baseDecision === "deny") {
833
+ return { outcome: "deny", reason: "denied by permission config" };
834
+ }
835
+ if (wasRecentlyApproved(ctx.sessionId, toolName, args)) {
836
+ return {
837
+ outcome: "allow",
838
+ silent: true,
839
+ reason: "session-recent approval (within 30s)"
840
+ };
841
+ }
842
+ return { outcome: "prompt" };
843
+ }
844
+ };
845
+
846
+ // src/permissions/handlers/one-shot.ts
847
+ var oneShotHandler = {
848
+ surface: "one-shot",
849
+ check: async (toolName, _args, baseDecision, ctx) => {
850
+ if (baseDecision === "allow") return { outcome: "allow" };
851
+ if (baseDecision === "deny") {
852
+ return { outcome: "deny", reason: "denied by permission config" };
853
+ }
854
+ if (ctx.autoConfirm) {
855
+ return {
856
+ outcome: "allow",
857
+ silent: true,
858
+ reason: "--yes flag set; auto-confirming one-shot"
859
+ };
860
+ }
861
+ const count = (ctx.denialCounter.get(toolName) ?? 0) + 1;
862
+ ctx.denialCounter.set(toolName, count);
863
+ return {
864
+ outcome: "deny",
865
+ silent: true,
866
+ reason: "one-shot mode denies prompt-level tools without --yes",
867
+ tailNotification: `one-shot: denied ${toolName} (pass --yes to auto-approve)`
868
+ };
869
+ }
870
+ };
871
+
872
+ // src/permissions/handlers/coordinator.ts
873
+ var COORDINATOR_ALLOWED = /* @__PURE__ */ new Set([
874
+ "agent_spawn",
875
+ "agent_send_message",
876
+ "agent_stop",
877
+ "read",
878
+ "grep",
879
+ "glob"
880
+ ]);
881
+ var coordinatorHandler = {
882
+ surface: "coordinator",
883
+ check: async (toolName, _args, baseDecision, _ctx) => {
884
+ if (COORDINATOR_ALLOWED.has(toolName)) {
885
+ if (baseDecision === "deny") {
886
+ return { outcome: "deny", reason: "coordinator tool explicitly denied by config" };
887
+ }
888
+ return { outcome: "allow", silent: true };
889
+ }
890
+ if (baseDecision === "deny") {
891
+ return { outcome: "deny", reason: "denied by permission config" };
892
+ }
893
+ return {
894
+ outcome: "deny",
895
+ silent: true,
896
+ reason: "Coordinator must delegate write operations to a worker.",
897
+ tailNotification: `coordinator: blocked ${toolName} (delegate to worker)`
898
+ };
899
+ }
900
+ };
901
+
902
+ // src/permissions/handlers/subagent.ts
903
+ var subagentHandler = {
904
+ surface: "subagent",
905
+ check: async (toolName, _args, baseDecision, ctx) => {
906
+ if (baseDecision === "allow") return { outcome: "allow" };
907
+ if (baseDecision === "deny") {
908
+ return { outcome: "deny", reason: "denied by permission config" };
909
+ }
910
+ const count = (ctx.denialCounter.get(toolName) ?? 0) + 1;
911
+ ctx.denialCounter.set(toolName, count);
912
+ const id = ctx.subagentId ?? "unknown";
913
+ return {
914
+ outcome: "deny",
915
+ silent: true,
916
+ reason: "subagent has no interactive UI to prompt",
917
+ tailNotification: `subagent ${id}: denied ${toolName} (no UI to prompt)`
918
+ };
919
+ }
920
+ };
921
+
922
+ // src/permissions/handlers/auto-mode.ts
923
+ var AUTO_IMPLICIT_WRITE_KEY = "__auto_mode_implicit_writes__";
924
+ var AUTO_WRITE_NOTIFY_THRESHOLD = 10;
925
+ var WRITE_TOOLS = /* @__PURE__ */ new Set(["write", "edit", "apply_patch", "shell", "git"]);
926
+ function hardDenyReason(toolName, args) {
927
+ if (toolName === "git") {
928
+ const op = typeof args.operation === "string" ? args.operation : "";
929
+ const flags = typeof args.flags === "string" ? args.flags : "";
930
+ const argsStr = typeof args.args === "string" ? args.args : "";
931
+ const combined = `${op} ${flags} ${argsStr}`.toLowerCase();
932
+ if (combined.includes("push") && /(-f\b|--force\b|--force-with-lease\b)/.test(combined)) {
933
+ return "force-push is never auto-approved";
934
+ }
935
+ if (combined.includes("reset") && combined.includes("--hard")) {
936
+ return "git reset --hard is never auto-approved";
937
+ }
938
+ }
939
+ if (toolName === "shell" && typeof args.command === "string") {
940
+ const cmd = args.command;
941
+ if (/\bnpm\s+publish\b/i.test(cmd) || /\byarn\s+publish\b/i.test(cmd) || /\bpnpm\s+publish\b/i.test(cmd)) {
942
+ return "package publish is never auto-approved";
943
+ }
944
+ if (/\bgit\s+push\b.*\b(?:-f|--force|--force-with-lease)\b/i.test(cmd)) {
945
+ return "force-push is never auto-approved";
946
+ }
947
+ if (/\b(?:rm\s+-rf?|dd\s+if=|mkfs\.|shred)\b/i.test(cmd)) {
948
+ return "classifier-flagged destructive shell command";
949
+ }
950
+ }
951
+ return null;
952
+ }
953
+ var autoModeHandler = {
954
+ surface: "auto-mode",
955
+ check: async (toolName, args, baseDecision, ctx) => {
956
+ if (baseDecision === "deny") {
957
+ return { outcome: "deny", reason: "denied by permission config" };
958
+ }
959
+ const reason = hardDenyReason(toolName, args);
960
+ if (reason) {
961
+ return {
962
+ outcome: "deny",
963
+ silent: false,
964
+ reason,
965
+ tailNotification: `auto-mode: refused ${toolName} \u2014 ${reason}`
966
+ };
967
+ }
968
+ if (toolName === "shell" && typeof args.command === "string") {
969
+ const pending = async () => {
970
+ const cls = classifyBashCommand(args.command);
971
+ if (cls.category === "destructive") {
972
+ return {
973
+ outcome: "deny",
974
+ silent: false,
975
+ reason: `classifier flagged destructive command: ${cls.matchedPattern}`,
976
+ tailNotification: `auto-mode: refused destructive shell command (${cls.head ?? "shell"})`
977
+ };
978
+ }
979
+ return baseDecision === "allow" ? { outcome: "allow", silent: true } : { outcome: "allow", silent: true, reason: "auto-mode implicit approval" };
980
+ };
981
+ if (baseDecision === "allow") return { outcome: "allow", pendingClassifierCheck: pending };
982
+ return { outcome: "allow", silent: true, pendingClassifierCheck: pending };
983
+ }
984
+ if (baseDecision === "allow") return { outcome: "allow" };
985
+ if (WRITE_TOOLS.has(toolName)) {
986
+ const writes = (ctx.denialCounter.get(AUTO_IMPLICIT_WRITE_KEY) ?? 0) + 1;
987
+ ctx.denialCounter.set(AUTO_IMPLICIT_WRITE_KEY, writes);
988
+ if (writes === AUTO_WRITE_NOTIFY_THRESHOLD) {
989
+ return {
990
+ outcome: "allow",
991
+ silent: true,
992
+ reason: "auto-mode implicit approval",
993
+ tailNotification: `auto-mode: ${writes} unattended writes this session \u2014 consider reviewing`
994
+ };
995
+ }
996
+ if (writes > AUTO_WRITE_NOTIFY_THRESHOLD && writes % 10 === 0) {
997
+ return {
998
+ outcome: "allow",
999
+ silent: true,
1000
+ reason: "auto-mode implicit approval",
1001
+ tailNotification: `auto-mode: ${writes} unattended writes this session`
1002
+ };
1003
+ }
1004
+ }
1005
+ return { outcome: "allow", silent: true, reason: "auto-mode implicit approval" };
1006
+ }
1007
+ };
1008
+
1009
+ // src/permissions/handlers/json-mode.ts
1010
+ var jsonModeHandler = {
1011
+ surface: "json-mode",
1012
+ check: async (toolName, _args, baseDecision, ctx) => {
1013
+ if (baseDecision === "allow") return { outcome: "allow" };
1014
+ if (baseDecision === "deny") {
1015
+ return { outcome: "deny", reason: "denied by permission config" };
1016
+ }
1017
+ const count = (ctx.denialCounter.get(toolName) ?? 0) + 1;
1018
+ ctx.denialCounter.set(toolName, count);
1019
+ const payload = JSON.stringify({
1020
+ type: "permission_denied",
1021
+ tool: toolName,
1022
+ reason: "json mode requires explicit allowlist"
1023
+ });
1024
+ return {
1025
+ outcome: "deny",
1026
+ silent: true,
1027
+ reason: "json mode requires explicit allowlist",
1028
+ tailNotification: payload
1029
+ };
1030
+ }
1031
+ };
1032
+
1033
+ // src/permissions/dispatcher.ts
1034
+ var HANDLERS = {
1035
+ interactive: interactiveHandler,
1036
+ "one-shot": oneShotHandler,
1037
+ coordinator: coordinatorHandler,
1038
+ subagent: subagentHandler,
1039
+ "auto-mode": autoModeHandler,
1040
+ "json-mode": jsonModeHandler
1041
+ };
1042
+ function getActiveHandler(surface) {
1043
+ return HANDLERS[surface];
1044
+ }
1045
+ async function runPermissionCheck(toolName, args, baseDecision, surface, ctx) {
1046
+ const handler = getActiveHandler(surface);
1047
+ const initial = await handler.check(toolName, args, baseDecision, ctx);
1048
+ if (initial.tailNotification) {
1049
+ pushTailNotification(initial.tailNotification);
1050
+ }
1051
+ if (!initial.pendingClassifierCheck) return initial;
1052
+ try {
1053
+ const refined = await initial.pendingClassifierCheck();
1054
+ if (refined.tailNotification) pushTailNotification(refined.tailNotification);
1055
+ return {
1056
+ outcome: refined.outcome,
1057
+ reason: refined.reason ?? initial.reason,
1058
+ silent: refined.silent ?? initial.silent,
1059
+ tailNotification: refined.tailNotification ?? initial.tailNotification
1060
+ };
1061
+ } catch {
1062
+ return {
1063
+ outcome: initial.outcome,
1064
+ reason: initial.reason,
1065
+ silent: initial.silent,
1066
+ tailNotification: initial.tailNotification
1067
+ };
1068
+ }
1069
+ }
1070
+ var tailQueue = [];
1071
+ function pushTailNotification(message) {
1072
+ tailQueue.push(message);
1073
+ }
1074
+ function recordAutoModeDenial(decision) {
1075
+ if (decision.outcome !== "deny") return;
1076
+ const msg = decision.tailNotification ?? decision.reason ?? "auto-mode: tool denied";
1077
+ pushTailNotification(msg);
1078
+ }
1079
+ function drainTailNotifications() {
1080
+ const out = tailQueue.splice(0, tailQueue.length);
1081
+ return out;
1082
+ }
1083
+ var currentSurface = "interactive";
1084
+ function setCurrentSurface(surface) {
1085
+ currentSurface = surface;
1086
+ }
1087
+ function createHandlerContext(options) {
1088
+ return {
1089
+ cwd: options.cwd,
1090
+ sessionId: options.sessionId,
1091
+ denialCounter: /* @__PURE__ */ new Map(),
1092
+ log: options.log,
1093
+ autoConfirm: options.autoConfirm,
1094
+ guardianEnabled: options.guardianEnabled,
1095
+ subagentId: options.subagentId
1096
+ };
1097
+ }
1098
+
1099
+ // src/events/router.ts
1100
+ var SEVERITY_RANK = {
1101
+ info: 0,
1102
+ success: 1,
1103
+ warn: 2,
1104
+ error: 3
1105
+ };
1106
+ var EventRouter = class {
1107
+ events = [];
1108
+ capacity;
1109
+ tailNotifyMin;
1110
+ maxAgeMs;
1111
+ filter;
1112
+ subscribers = /* @__PURE__ */ new Set();
1113
+ constructor(opts = {}) {
1114
+ this.capacity = opts.capacity ?? 256;
1115
+ this.tailNotifyMin = opts.tailNotifyMin ?? "warn";
1116
+ this.maxAgeMs = opts.maxAgeMs ?? 6 * 60 * 60 * 1e3;
1117
+ this.filter = opts.filter;
1118
+ }
1119
+ /**
1120
+ * Emit an event. Returns the stored event (with normalized id/ts) or
1121
+ * null if it was dropped by filtering.
1122
+ */
1123
+ emit(partial) {
1124
+ const ts = partial.ts ?? Date.now();
1125
+ const id = partial.id ?? `${partial.source}:${partial.type}:${ts}`;
1126
+ const ev = {
1127
+ id,
1128
+ source: partial.source,
1129
+ type: partial.type,
1130
+ ts,
1131
+ severity: partial.severity,
1132
+ title: partial.title,
1133
+ payload: partial.payload,
1134
+ ackRequired: partial.ackRequired,
1135
+ drained: false
1136
+ };
1137
+ if (!this.passesFilter(ev)) return null;
1138
+ this.pruneOld();
1139
+ const existing = this.events.findIndex((e) => e.id === ev.id && !e.drained);
1140
+ if (existing >= 0) {
1141
+ this.events[existing] = ev;
1142
+ } else {
1143
+ this.events.push(ev);
1144
+ if (this.events.length > this.capacity) {
1145
+ this.events.splice(0, this.events.length - this.capacity);
1146
+ }
1147
+ }
1148
+ if (SEVERITY_RANK[ev.severity] >= SEVERITY_RANK[this.tailNotifyMin]) {
1149
+ try {
1150
+ pushTailNotification(`${ev.source}: ${ev.title}`);
1151
+ } catch {
1152
+ }
1153
+ }
1154
+ for (const sub of this.subscribers) {
1155
+ try {
1156
+ sub(ev);
1157
+ } catch {
1158
+ }
1159
+ }
1160
+ return ev;
1161
+ }
1162
+ /**
1163
+ * Subscribe to future emits. Returns an unsubscribe function.
1164
+ */
1165
+ subscribe(fn) {
1166
+ this.subscribers.add(fn);
1167
+ return () => this.subscribers.delete(fn);
1168
+ }
1169
+ /**
1170
+ * Return all pending (un-drained) events, optionally filtered, newest
1171
+ * first. Does NOT mark them drained — use {@link drain} to ack.
1172
+ */
1173
+ pending(filter) {
1174
+ this.pruneOld();
1175
+ const f = filter ?? this.filter;
1176
+ return this.events.filter((e) => !e.drained).filter((e) => this.eventMatchesFilter(e, f)).sort((a, b) => b.ts - a.ts);
1177
+ }
1178
+ /**
1179
+ * Drain (acknowledge) pending events matching an optional filter.
1180
+ * Returns the set drained — useful when an agent explicitly consumes
1181
+ * queued events as part of a step. Events remain in the ring buffer
1182
+ * marked `drained: true` for audit until aged out.
1183
+ */
1184
+ drain(filter) {
1185
+ const matched = this.pending(filter);
1186
+ const now = Date.now();
1187
+ for (const ev of matched) {
1188
+ ev.drained = true;
1189
+ ev.drainedAt = now;
1190
+ }
1191
+ return matched;
1192
+ }
1193
+ /** All events in the ring buffer, including drained ones. */
1194
+ history() {
1195
+ return [...this.events];
1196
+ }
1197
+ /** Clear everything. Primarily useful in tests. */
1198
+ clear() {
1199
+ this.events = [];
1200
+ }
1201
+ /** Count of un-drained events. */
1202
+ pendingCount() {
1203
+ return this.pending().length;
1204
+ }
1205
+ passesFilter(ev) {
1206
+ if (!this.filter) return true;
1207
+ return this.eventMatchesFilter(ev, this.filter);
1208
+ }
1209
+ eventMatchesFilter(ev, f) {
1210
+ if (!f) return true;
1211
+ if (f.sources && !f.sources.includes(ev.source)) return false;
1212
+ if (f.types && !f.types.includes(ev.type)) return false;
1213
+ if (f.minSeverity && SEVERITY_RANK[ev.severity] < SEVERITY_RANK[f.minSeverity]) return false;
1214
+ return true;
1215
+ }
1216
+ pruneOld() {
1217
+ if (!Number.isFinite(this.maxAgeMs)) return;
1218
+ const cutoff = Date.now() - this.maxAgeMs;
1219
+ this.events = this.events.filter((e) => e.ts >= cutoff);
1220
+ }
1221
+ };
1222
+ var globalRouter = null;
1223
+ function getGlobalRouter() {
1224
+ if (!globalRouter) globalRouter = new EventRouter();
1225
+ return globalRouter;
1226
+ }
1227
+
1228
+ // src/tools/events.ts
1229
+ var parameters = z3.object({
1230
+ mode: z3.enum(["list", "drain"]).describe("`list` peeks at pending events. `drain` acknowledges + returns them."),
1231
+ sources: z3.array(z3.string()).optional().describe('Only include events from these sources (e.g. ["git", "ci"]).'),
1232
+ types: z3.array(z3.string()).optional().describe('Only include events of these types (e.g. ["new-commit"]).'),
1233
+ min_severity: z3.enum(["info", "success", "warn", "error"]).optional().describe("Only include events at or above this severity."),
1234
+ limit: z3.number().int().min(1).max(50).optional().default(20).describe("Max number of events to return (1-50, default 20).")
1235
+ });
1236
+ function formatEvent(ev) {
1237
+ const when = new Date(ev.ts).toISOString();
1238
+ const sev = ev.severity.toUpperCase().padEnd(7);
1239
+ const title = ev.title.replace(/\n/g, " ").slice(0, 200);
1240
+ const body = ev.payload ? ` \u2014 ${JSON.stringify(ev.payload).slice(0, 180)}` : "";
1241
+ return `[${when}] [${sev}] ${ev.source}/${ev.type}: ${title}${body}`;
1242
+ }
1243
+ var eventsTool = {
1244
+ name: "events",
1245
+ description: "Inspect or consume queued out-of-band events (git state, GitHub notifications, CI, custom webhooks). Use `list` to peek without consuming; use `drain` when you are ready to act on them and want them cleared. Events do NOT enter your conversation context automatically \u2014 you must call this tool to see them.",
1246
+ parameters,
1247
+ async execute(params) {
1248
+ const router = getGlobalRouter();
1249
+ const filter = {
1250
+ sources: params.sources,
1251
+ types: params.types,
1252
+ minSeverity: params.min_severity
1253
+ };
1254
+ const events = params.mode === "drain" ? router.drain(filter) : router.pending(filter);
1255
+ const limited = events.slice(0, params.limit);
1256
+ if (limited.length === 0) {
1257
+ const verb = params.mode === "drain" ? "drained" : "pending";
1258
+ return { content: `No ${verb} events matching filter.` };
1259
+ }
1260
+ const header = params.mode === "drain" ? `Drained ${limited.length} event(s) (cleared from pending queue):` : `${limited.length} pending event(s) (NOT yet acknowledged \u2014 call again with mode=drain to clear):`;
1261
+ const body = limited.map(formatEvent).join("\n");
1262
+ return { content: `${header}
1263
+ ${body}` };
1264
+ }
1265
+ };
1266
+
1267
+ // src/mcp/client.ts
1268
+ import { z as z4 } from "zod";
1269
+
1270
+ // src/mcp/transport.ts
1271
+ function detectTransport(config) {
1272
+ if (config.transport) return config.transport;
1273
+ if (config.url) return "http";
1274
+ return "stdio";
1275
+ }
1276
+
1277
+ // src/mcp/stdio-transport.ts
1278
+ import { spawn } from "child_process";
1279
+ var StdioTransport = class {
1280
+ constructor(config, name) {
1281
+ this.config = config;
1282
+ this.name = name;
1283
+ }
1284
+ config;
1285
+ process = null;
1286
+ requestId = 0;
1287
+ pendingRequests = /* @__PURE__ */ new Map();
1288
+ buffer = "";
1289
+ name;
1290
+ async connect() {
1291
+ if (!this.config.command) {
1292
+ throw new Error(`Stdio transport requires 'command' in config for server ${this.name}`);
1293
+ }
1294
+ this.process = spawn(this.config.command, this.config.args ?? [], {
1295
+ stdio: ["pipe", "pipe", "pipe"],
1296
+ env: { ...process.env, ...this.config.env },
1297
+ cwd: this.config.cwd
1298
+ });
1299
+ this.process.stdout?.setEncoding("utf-8");
1300
+ this.process.stdout?.on("data", (data) => {
1301
+ this.buffer += data;
1302
+ this.processBuffer();
1303
+ });
1304
+ this.process.on("error", (err) => {
1305
+ for (const [id, pending] of this.pendingRequests) {
1306
+ pending.reject(new Error(`MCP server ${this.name} error: ${err.message}`));
1307
+ this.pendingRequests.delete(id);
1308
+ }
1309
+ });
1310
+ this.process.on("exit", (code) => {
1311
+ for (const [id, pending] of this.pendingRequests) {
1312
+ pending.reject(new Error(`MCP server ${this.name} exited with code ${code}`));
1313
+ this.pendingRequests.delete(id);
1314
+ }
1315
+ });
1316
+ }
1317
+ disconnect() {
1318
+ if (this.process) {
1319
+ this.process.stdin?.end();
1320
+ this.process.kill();
1321
+ this.process = null;
1322
+ }
1323
+ this.pendingRequests.clear();
1324
+ }
1325
+ get isAlive() {
1326
+ return this.process !== null && this.process.exitCode === null && !this.process.killed;
1327
+ }
1328
+ sendRequest(method, params) {
1329
+ return new Promise((resolve, reject) => {
1330
+ const id = ++this.requestId;
1331
+ const msg = { jsonrpc: "2.0", id, method, params };
1332
+ this.pendingRequests.set(id, { resolve, reject });
1333
+ const data = JSON.stringify(msg);
1334
+ const header = `Content-Length: ${Buffer.byteLength(data)}\r
1335
+ \r
1336
+ `;
1337
+ this.process?.stdin?.write(header + data);
1338
+ setTimeout(() => {
1339
+ if (this.pendingRequests.has(id)) {
1340
+ this.pendingRequests.delete(id);
1341
+ reject(new Error(`MCP request ${method} timed out`));
1342
+ }
1343
+ }, 3e4);
1344
+ });
1345
+ }
1346
+ sendNotification(method, params) {
1347
+ const msg = { jsonrpc: "2.0", method, params };
1348
+ const data = JSON.stringify(msg);
1349
+ const header = `Content-Length: ${Buffer.byteLength(data)}\r
1350
+ \r
1351
+ `;
1352
+ this.process?.stdin?.write(header + data);
1353
+ }
1354
+ processBuffer() {
1355
+ while (this.buffer.length > 0) {
1356
+ const headerEnd = this.buffer.indexOf("\r\n\r\n");
1357
+ if (headerEnd === -1) break;
1358
+ const header = this.buffer.slice(0, headerEnd);
1359
+ const lengthMatch = header.match(/Content-Length:\s*(\d+)/i);
1360
+ if (!lengthMatch) {
1361
+ const nlIdx = this.buffer.indexOf("\n");
1362
+ if (nlIdx === -1) break;
1363
+ const line = this.buffer.slice(0, nlIdx).trim();
1364
+ this.buffer = this.buffer.slice(nlIdx + 1);
1365
+ if (line) this.handleMessage(line);
1366
+ continue;
1367
+ }
1368
+ const contentLength = parseInt(lengthMatch[1], 10);
1369
+ const messageStart = headerEnd + 4;
1370
+ if (this.buffer.length < messageStart + contentLength) break;
1371
+ const body = this.buffer.slice(messageStart, messageStart + contentLength);
1372
+ this.buffer = this.buffer.slice(messageStart + contentLength);
1373
+ this.handleMessage(body);
1374
+ }
1375
+ }
1376
+ handleMessage(raw) {
1377
+ try {
1378
+ const msg = JSON.parse(raw);
1379
+ if (msg.id !== void 0 && this.pendingRequests.has(msg.id)) {
1380
+ const pending = this.pendingRequests.get(msg.id);
1381
+ this.pendingRequests.delete(msg.id);
1382
+ if (msg.error) {
1383
+ pending.reject(new Error(`MCP error: ${msg.error.message}`));
1384
+ } else {
1385
+ pending.resolve(msg.result);
1386
+ }
1387
+ }
1388
+ } catch {
1389
+ }
1390
+ }
1391
+ };
1392
+
1393
+ // src/mcp/http-transport.ts
1394
+ var HttpTransport = class {
1395
+ requestId = 0;
1396
+ connected = false;
1397
+ name;
1398
+ baseUrl;
1399
+ headers;
1400
+ constructor(config, name) {
1401
+ this.name = name;
1402
+ if (!config.url) {
1403
+ throw new Error(`HTTP transport requires 'url' in config for server ${name}`);
1404
+ }
1405
+ this.baseUrl = config.url.replace(/\/$/, "");
1406
+ this.headers = {
1407
+ "Content-Type": "application/json",
1408
+ ...config.headers
1409
+ };
1410
+ }
1411
+ async connect() {
1412
+ try {
1413
+ const response = await fetch(`${this.baseUrl}`, {
1414
+ method: "POST",
1415
+ headers: this.headers,
1416
+ body: JSON.stringify({
1417
+ jsonrpc: "2.0",
1418
+ id: ++this.requestId,
1419
+ method: "initialize",
1420
+ params: {
1421
+ protocolVersion: "2024-11-05",
1422
+ capabilities: {},
1423
+ clientInfo: { name: "notch-cli", version: "0.4.8" }
1424
+ }
1425
+ }),
1426
+ signal: AbortSignal.timeout(15e3)
1427
+ });
1428
+ if (!response.ok) {
1429
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1430
+ }
1431
+ this.sendNotification("notifications/initialized", {});
1432
+ this.connected = true;
1433
+ } catch (err) {
1434
+ throw new Error(`MCP HTTP server ${this.name} unreachable: ${err.message}`);
1435
+ }
1436
+ }
1437
+ disconnect() {
1438
+ this.connected = false;
1439
+ }
1440
+ get isAlive() {
1441
+ return this.connected;
1442
+ }
1443
+ async sendRequest(method, params) {
1444
+ const id = ++this.requestId;
1445
+ const body = { jsonrpc: "2.0", id, method, params };
1446
+ const response = await fetch(this.baseUrl, {
1447
+ method: "POST",
1448
+ headers: this.headers,
1449
+ body: JSON.stringify(body),
1450
+ signal: AbortSignal.timeout(3e4)
1451
+ });
1452
+ if (!response.ok) {
1453
+ throw new Error(`MCP HTTP error (${this.name}): ${response.status} ${response.statusText}`);
1454
+ }
1455
+ const json = await response.json();
1456
+ if (json.error) {
1457
+ throw new Error(`MCP error (${this.name}): ${json.error.message}`);
1458
+ }
1459
+ return json.result;
1460
+ }
1461
+ sendNotification(method, params) {
1462
+ void fetch(this.baseUrl, {
1463
+ method: "POST",
1464
+ headers: this.headers,
1465
+ body: JSON.stringify({ jsonrpc: "2.0", method, params }),
1466
+ signal: AbortSignal.timeout(1e4)
1467
+ }).catch(() => {
1468
+ });
1469
+ }
1470
+ };
1471
+
1472
+ // src/mcp/sse-transport.ts
1473
+ var SSETransport = class {
1474
+ requestId = 0;
1475
+ pendingRequests = /* @__PURE__ */ new Map();
1476
+ abortController = null;
1477
+ connected = false;
1478
+ name;
1479
+ baseUrl;
1480
+ messageEndpoint;
1481
+ sseEndpoint;
1482
+ headers;
1483
+ constructor(config, name) {
1484
+ this.name = name;
1485
+ if (!config.url) {
1486
+ throw new Error(`SSE transport requires 'url' in config for server ${name}`);
1487
+ }
1488
+ this.baseUrl = config.url.replace(/\/$/, "");
1489
+ this.sseEndpoint = `${this.baseUrl}/sse`;
1490
+ this.messageEndpoint = `${this.baseUrl}/message`;
1491
+ this.headers = {
1492
+ "Content-Type": "application/json",
1493
+ ...config.headers
1494
+ };
1495
+ }
1496
+ async connect() {
1497
+ this.abortController = new AbortController();
1498
+ const ssePromise = this.startSSEListener();
1499
+ await new Promise((resolve) => setTimeout(resolve, 500));
1500
+ const initResult = await this.sendRequest("initialize", {
1501
+ protocolVersion: "2024-11-05",
1502
+ capabilities: {},
1503
+ clientInfo: { name: "notch-cli", version: "0.4.8" }
1504
+ });
1505
+ if (!initResult) {
1506
+ throw new Error(`MCP SSE server ${this.name} failed to initialize`);
1507
+ }
1508
+ this.sendNotification("notifications/initialized", {});
1509
+ this.connected = true;
1510
+ ssePromise.catch(() => {
1511
+ this.connected = false;
1512
+ });
1513
+ }
1514
+ disconnect() {
1515
+ this.abortController?.abort();
1516
+ this.abortController = null;
1517
+ this.connected = false;
1518
+ for (const [id, pending] of this.pendingRequests) {
1519
+ pending.reject(new Error(`MCP SSE transport disconnected`));
1520
+ this.pendingRequests.delete(id);
1521
+ }
1522
+ }
1523
+ get isAlive() {
1524
+ return this.connected;
1525
+ }
1526
+ async sendRequest(method, params) {
1527
+ const id = ++this.requestId;
1528
+ const body = { jsonrpc: "2.0", id, method, params };
1529
+ const resultPromise = new Promise((resolve, reject) => {
1530
+ this.pendingRequests.set(id, { resolve, reject });
1531
+ setTimeout(() => {
1532
+ if (this.pendingRequests.has(id)) {
1533
+ this.pendingRequests.delete(id);
1534
+ reject(new Error(`MCP SSE request ${method} timed out`));
1535
+ }
1536
+ }, 3e4);
1537
+ });
1538
+ const response = await fetch(this.messageEndpoint, {
1539
+ method: "POST",
1540
+ headers: this.headers,
1541
+ body: JSON.stringify(body),
1542
+ signal: AbortSignal.timeout(1e4)
1543
+ });
1544
+ if (!response.ok) {
1545
+ this.pendingRequests.delete(id);
1546
+ throw new Error(`MCP SSE POST error (${this.name}): ${response.status} ${response.statusText}`);
1547
+ }
1548
+ return resultPromise;
1549
+ }
1550
+ sendNotification(method, params) {
1551
+ const body = { jsonrpc: "2.0", method, params };
1552
+ void fetch(this.messageEndpoint, {
1553
+ method: "POST",
1554
+ headers: this.headers,
1555
+ body: JSON.stringify(body),
1556
+ signal: AbortSignal.timeout(1e4)
1557
+ }).catch(() => {
1558
+ });
1559
+ }
1560
+ /**
1561
+ * Start listening for Server-Sent Events.
1562
+ * Parses the SSE stream and resolves pending requests.
1563
+ */
1564
+ async startSSEListener() {
1565
+ const response = await fetch(this.sseEndpoint, {
1566
+ headers: {
1567
+ Accept: "text/event-stream",
1568
+ ...this.headers
1569
+ },
1570
+ signal: this.abortController?.signal
1571
+ });
1572
+ if (!response.ok || !response.body) {
1573
+ throw new Error(`SSE connection failed: ${response.status}`);
1574
+ }
1575
+ const reader = response.body.getReader();
1576
+ const decoder = new TextDecoder();
1577
+ let buffer = "";
1578
+ try {
1579
+ while (true) {
1580
+ const { done, value } = await reader.read();
1581
+ if (done) break;
1582
+ buffer += decoder.decode(value, { stream: true });
1583
+ const events = buffer.split("\n\n");
1584
+ buffer = events.pop() ?? "";
1585
+ for (const event of events) {
1586
+ const lines = event.split("\n");
1587
+ let data = "";
1588
+ for (const line of lines) {
1589
+ if (line.startsWith("data: ")) {
1590
+ data += line.slice(6);
1591
+ }
1592
+ }
1593
+ if (data) {
1594
+ this.handleSSEMessage(data);
1595
+ }
1596
+ }
1597
+ }
1598
+ } catch (err) {
1599
+ if (err.name !== "AbortError") {
1600
+ this.connected = false;
1601
+ }
1602
+ }
1603
+ }
1604
+ handleSSEMessage(raw) {
1605
+ try {
1606
+ const msg = JSON.parse(raw);
1607
+ if (msg.id !== void 0 && this.pendingRequests.has(msg.id)) {
1608
+ const pending = this.pendingRequests.get(msg.id);
1609
+ this.pendingRequests.delete(msg.id);
1610
+ if (msg.error) {
1611
+ pending.reject(new Error(`MCP SSE error: ${msg.error.message}`));
1612
+ } else {
1613
+ pending.resolve(msg.result);
1614
+ }
1615
+ }
1616
+ } catch {
1617
+ }
1618
+ }
1619
+ };
1620
+
1621
+ // src/mcp/client.ts
1622
+ function createTransport(config, name) {
1623
+ const type = detectTransport(config);
1624
+ switch (type) {
1625
+ case "http":
1626
+ return new HttpTransport(config, name);
1627
+ case "sse":
1628
+ return new SSETransport(config, name);
1629
+ case "stdio":
1630
+ default:
1631
+ return new StdioTransport(config, name);
1632
+ }
1633
+ }
1634
+ var MCPClient = class {
1635
+ transport;
1636
+ serverName;
1637
+ _tools = [];
1638
+ constructor(config, serverName) {
1639
+ this.serverName = serverName;
1640
+ this.transport = createTransport(config, serverName);
1641
+ }
1642
+ /**
1643
+ * Start the MCP server and initialize the connection.
1644
+ */
1645
+ async connect() {
1646
+ await this.transport.connect();
1647
+ await this.transport.sendRequest("initialize", {
1648
+ protocolVersion: "2024-11-05",
1649
+ capabilities: {},
1650
+ clientInfo: { name: "notch-cli", version: "0.4.8" }
1651
+ });
1652
+ this.transport.sendNotification("notifications/initialized", {});
1653
+ const result = await this.transport.sendRequest("tools/list", {});
1654
+ this._tools = result.tools ?? [];
1655
+ }
1656
+ /**
1657
+ * Get discovered tools from this server.
1658
+ */
1659
+ get tools() {
1660
+ return this._tools;
1661
+ }
1662
+ /**
1663
+ * Check if the MCP server is still alive.
1664
+ */
1665
+ get isAlive() {
1666
+ return this.transport.isAlive;
1667
+ }
1668
+ /**
1669
+ * Call a tool on the MCP server. Auto-reconnects if the server has crashed.
1670
+ */
1671
+ async callTool(name, args) {
1672
+ if (!this.isAlive) {
1673
+ try {
1674
+ await this.transport.connect();
1675
+ } catch (err) {
1676
+ throw new Error(`MCP server ${this.serverName} is down and could not reconnect: ${err.message}`);
1677
+ }
1678
+ }
1679
+ return this.transport.sendRequest("tools/call", { name, arguments: args });
1680
+ }
1681
+ /**
1682
+ * Disconnect from the MCP server.
1683
+ */
1684
+ disconnect() {
1685
+ this.transport.disconnect();
1686
+ }
1687
+ };
1688
+ function mcpToolsToNotch(client, serverName) {
1689
+ return client.tools.map((toolDef) => {
1690
+ const params = z4.record(z4.unknown()).describe(
1691
+ toolDef.description || `MCP tool from ${serverName}`
1692
+ );
1693
+ const notchTool = {
1694
+ name: `mcp_${serverName}_${toolDef.name}`,
1695
+ description: `[MCP/${serverName}] ${toolDef.description}`,
1696
+ parameters: params,
1697
+ async execute(args, _ctx) {
1698
+ try {
1699
+ const result = await client.callTool(toolDef.name, args);
1700
+ const mcpResult = result;
1701
+ if (mcpResult?.content && Array.isArray(mcpResult.content)) {
1702
+ const text = mcpResult.content.filter((c) => c.type === "text").map((c) => c.text).join("\n");
1703
+ return { content: text || JSON.stringify(result) };
1704
+ }
1705
+ return { content: typeof result === "string" ? result : JSON.stringify(result, null, 2) };
1706
+ } catch (err) {
1707
+ return { content: `MCP error (${serverName}/${toolDef.name}): ${err.message}`, isError: true };
1708
+ }
1709
+ }
1710
+ };
1711
+ return notchTool;
1712
+ });
1713
+ }
1714
+ function parseMCPConfig(config) {
1715
+ const servers = config?.mcpServers;
1716
+ if (!servers || typeof servers !== "object") return {};
1717
+ const result = {};
1718
+ for (const [name, cfg] of Object.entries(servers)) {
1719
+ const c = cfg;
1720
+ if (c?.command || c?.url) {
1721
+ result[name] = {
1722
+ command: c.command,
1723
+ args: c.args,
1724
+ env: c.env,
1725
+ cwd: c.cwd,
1726
+ transport: c.transport,
1727
+ url: c.url,
1728
+ headers: c.headers
1729
+ };
1730
+ }
1731
+ }
1732
+ return result;
1733
+ }
1734
+
1735
+ // src/safety/guardian.ts
1736
+ import fs from "fs/promises";
1737
+ import { fileURLToPath } from "url";
1738
+ import path from "path";
1739
+ import { generateText } from "ai";
1740
+ import { z as z5 } from "zod";
1741
+
1742
+ // src/safety/guardian-policy.ts
1743
+ var GUARDIAN_POLICY_EMBEDDED = `# Notch Guardian Risk Policy
1744
+
1745
+ You are the Notch Guardian \u2014 an independent second-opinion safety reviewer. You do
1746
+ not write code, answer the user's question, or plan the task. Your only job is to
1747
+ score a proposed tool call against the taxonomy below and return a JSON verdict.
1748
+
1749
+ Assume the caller is a trusted developer working on their own machine. Do not
1750
+ inflate severity because the action is "large", "long-running", or touches paths
1751
+ outside a writable workspace root \u2014 those facts alone are not risk signals. Focus
1752
+ on evidence of **actual** data exfiltration, credential theft, persistent security
1753
+ weakening, or irreversible damage.
1754
+
1755
+ ## Environment Assumptions
1756
+
1757
+ - Single-developer laptop or CI runner. No multi-tenant blast radius.
1758
+ - The user's own project files, \`.git\`, \`node_modules\`, build outputs, and
1759
+ \`~/.notch\` are **local** and non-sensitive by default.
1760
+ - Trusted network destinations: \`github.com\`, \`gitlab.com\`, \`npmjs.com\`,
1761
+ \`pypi.org\`, \`crates.io\`, \`modal.run\`, the active Notch model endpoints, any
1762
+ host mentioned in \`package.json\` / \`.gitmodules\` / committed config, and
1763
+ \`localhost\` / \`127.0.0.1\` / \`0.0.0.0\` on any port.
1764
+ - Untrusted by default: pastebins, webhook catchers, URL shorteners,
1765
+ transfer.sh, termbin, \`0x0.st\`, \`ix.io\`, arbitrary raw IPs, \`*.onion\`, and
1766
+ any domain whose sole purpose is receiving posted data.
1767
+ - \`git push\` to a remote already listed in \`git remote -v\` is trusted. A push
1768
+ to a brand-new remote URL introduced in the same command is not.
1769
+
1770
+ ## Risk Taxonomy
1771
+
1772
+ ### 1. Data Exfiltration
1773
+
1774
+ Private workspace content, environment variables, secrets, or org data being
1775
+ sent to an untrusted destination.
1776
+
1777
+ - **low** \u2014 reading public docs, fetching a package from a package registry,
1778
+ curl to a documented API that the user's code already talks to, uploading an
1779
+ artifact to the user's own cloud bucket named in committed config, \`gh api\`
1780
+ calls against the user's own repos, \`git push\` to an existing tracked
1781
+ remote, telemetry the user explicitly opted into.
1782
+ - **medium** \u2014 uploading a build artifact to a first-party storage service not
1783
+ yet in committed config, opening a tunnel (\`ngrok\`, \`cloudflared\`) that only
1784
+ exposes a specific local port the user asked about, \`rsync\` to a host listed
1785
+ in \`~/.ssh/config\`.
1786
+ - **high** \u2014 \`curl -X POST\` with \`@file\` from inside the repo to a domain not
1787
+ in the trusted list, piping \`cat ~/.aws/credentials\` or \`.env\` through any
1788
+ network command, \`scp\`/\`rsync\` of the whole repo to a fresh remote, posting
1789
+ stdout/stderr containing private paths to a pastebin, base64-encoding a
1790
+ secret and shipping it as a query string.
1791
+ - **critical** \u2014 exfiltrating \`.ssh/id_*\`, \`.aws/credentials\`, \`.npmrc\` with
1792
+ \`_authToken\`, \`.pypirc\`, browser cookie databases, keychains, or any
1793
+ SSO/OAuth refresh token to **any** external destination, including "trusted"
1794
+ ones. Shipping \`.env*\` files to pastebins or attacker-controlled webhooks.
1795
+ Mass-uploading \`**/*.pem\`, \`**/*.key\`, \`**/id_rsa*\` anywhere off-host.
1796
+
1797
+ Override: sending secrets to a third-party destination is **critical** even if
1798
+ the user authorized the tool call broadly \u2014 demand explicit per-destination
1799
+ consent.
1800
+
1801
+ ### 2. Credential Probing
1802
+
1803
+ Harvesting secrets from places they weren't handed to you.
1804
+
1805
+ - **low** \u2014 reading \`process.env.SOMETHING\` the user's code already references,
1806
+ using a token the user passed in via flag/env, calling \`aws sts
1807
+ get-caller-identity\` with the ambient profile, \`gh auth status\`.
1808
+ - **medium** \u2014 \`env | grep -i\` for patterns like \`TOKEN|KEY|SECRET\` without a
1809
+ clear reason, shelling out to \`security find-generic-password\` on macOS
1810
+ keychain for a single named entry.
1811
+ - **high** \u2014 recursive scan of \`$HOME\` for \`id_rsa\`, \`credentials\`, \`.env\`,
1812
+ \`*.pem\`, \`*.keystore\`; dumping browser cookie/login SQLite DBs (Chrome
1813
+ \`Login Data\`, Firefox \`logins.json\`, Safari keychain); reading
1814
+ \`/etc/shadow\`, \`/etc/sudoers\`; calling \`lsof -i -nP\` followed by file reads
1815
+ on anything that looks like a session; enumerating 1Password/Bitwarden
1816
+ vault files on disk.
1817
+ - **critical** \u2014 active credential dumping tools (\`mimikatz\`, \`lazagne\`,
1818
+ \`SharpChrome\`, \`credhistview\`), decrypting DPAPI blobs, extracting saved
1819
+ WiFi passwords with \`netsh wlan show profile key=clear\`, scripting against
1820
+ the gnome-keyring / kwallet D-Bus APIs to enumerate all entries.
1821
+
1822
+ Override: reading a single, user-named credential file the task needs (e.g.
1823
+ \`cat $HOME/.config/gh/hosts.yml\` when the user asked to configure gh) is
1824
+ **low**. Shelling into macOS keychain for a single user-specified item is
1825
+ **low**.
1826
+
1827
+ ### 3. Persistent Security Weakening
1828
+
1829
+ Changes that outlive the session and open future attack surface.
1830
+
1831
+ - **low** \u2014 setting a local git hook for the user's own repo, adding \`127.0.0.1
1832
+ myapp.test\` to \`/etc/hosts\`, narrowly scoped temporary cert pin override in a
1833
+ dev-only config file.
1834
+ - **medium** \u2014 disabling TLS verification for one command (\`curl -k\`, \`git -c
1835
+ http.sslVerify=false fetch\`), chmod \`644\` on a previously \`600\` config file,
1836
+ turning on a single feature-flag that lowers a check for this session only.
1837
+ - **high** \u2014 \`chmod -R 777\` on directories, disabling SELinux / AppArmor /
1838
+ Gatekeeper globally, flipping \`insecure_skip_verify: true\` in committed
1839
+ infra config, adding a permissive \`AllowUsers *\` to \`sshd_config\`,
1840
+ installing a new trusted root CA, committing \`NODE_TLS_REJECT_UNAUTHORIZED=0\`
1841
+ to \`.env\` or CI.
1842
+ - **critical** \u2014 installing a persistence mechanism (launchd/systemd unit,
1843
+ scheduled task, \`~/.bashrc\` backdoor line, cron entry) that runs arbitrary
1844
+ network code; disabling endpoint protection (\`spctl --master-disable\`, Defender
1845
+ off); creating an administrative account; adding an SSH key the user did not
1846
+ supply to \`authorized_keys\`; writing a malicious \`npm prepare\` script into
1847
+ \`package.json\`.
1848
+
1849
+ Override: a session-scoped \`export NODE_TLS_REJECT_UNAUTHORIZED=0\` used once
1850
+ to reproduce a cert bug is **medium**, not high.
1851
+
1852
+ ### 4. Destructive Actions
1853
+
1854
+ Irreversible or costly-to-reverse mutation.
1855
+
1856
+ - **low** \u2014 \`rm\` on a file the user just created, \`git clean -fd\` inside a
1857
+ worktree where \`git status\` shows only untracked scratch files, truncating a
1858
+ log file under the project, dropping a local sqlite dev DB, deleting
1859
+ \`node_modules\`/\`dist\`/\`.next\` build output.
1860
+ - **medium** \u2014 \`git reset --hard HEAD~N\` on a feature branch the user owns,
1861
+ \`git push --force-with-lease\` to a non-default branch, \`rm -rf\` on a
1862
+ directory with tracked files but uncommitted changes, \`DROP TABLE\` on a
1863
+ local dev DB, \`docker system prune -af\` on a dev machine.
1864
+ - **high** \u2014 \`rm -rf\` with a broad glob (\`/\`, \`$HOME\`, \`~/*\`, \`**/*\`), \`git
1865
+ push --force\` (no \`-with-lease\`) to a non-default branch, deleting a
1866
+ non-local git branch via refspec push (\`git push origin :branch\`), \`DROP
1867
+ DATABASE\` / \`TRUNCATE\` on anything named like \`staging\`/\`prod\`, factory
1868
+ resetting a cloud resource the user owns.
1869
+ - **critical** \u2014 \`git push --force\` or \`+refs/heads/*\` to \`main\`/\`master\`/a
1870
+ protected default branch; \`rm -rf /\`, \`:(){ :|:& };:\`, \`dd if=/dev/zero
1871
+ of=/dev/sda\`; \`terraform destroy\` / \`kubectl delete ns\` / database drops
1872
+ against anything production-shaped; wiping \`.git\` in a repo with unpushed
1873
+ commits.
1874
+
1875
+ Override: \`git push\` to an already-tracked remote with a normal refspec is
1876
+ **low**, even if the branch is \`main\`. \`git push --force\` or \`--force-with-lease\`
1877
+ to \`main\`/\`master\` is **critical**. \`git push --force-with-lease\` to a
1878
+ feature branch the user owns is **medium**. \`git commit\` is always **low**.
1879
+
1880
+ ### 5. Low-Risk Actions
1881
+
1882
+ A catch-all reminder \u2014 do not inflate severity for these.
1883
+
1884
+ - Reading any file the user or task references, regardless of path.
1885
+ - Running the project's test suite, linter, formatter, typechecker, or build.
1886
+ - Installing a dependency from \`package.json\`/\`requirements.txt\`/\`Cargo.toml\`
1887
+ via the declared package manager.
1888
+ - Fetching public documentation, RFCs, MDN pages, man pages, Stack Overflow,
1889
+ GitHub issue threads \u2014 regardless of frequency.
1890
+ - Sandbox retries / re-runs after a transient error.
1891
+ - Any action whose only "risk" is that it takes a long time or writes outside
1892
+ the initial \`cwd\`.
1893
+ - Creating / checking out / merging git branches the user owns.
1894
+ - Reading env vars, even sensitive-looking ones, into the current process
1895
+ when the task clearly needs them.
1896
+
1897
+ ## Output Contract
1898
+
1899
+ Return **only** a single JSON object matching this schema, with no prose, no
1900
+ code fences, no leading text:
1901
+
1902
+ \`\`\`
1903
+ {
1904
+ "category": "Data Exfiltration" | "Credential Probing" | "Persistent Security Weakening" | "Destructive Actions" | "Low-Risk Actions",
1905
+ "severity": "low" | "medium" | "high" | "critical",
1906
+ "justification": "one sentence, <= 240 chars, explaining the severity",
1907
+ "recommended_action": "auto-allow" | "prompt" | "deny",
1908
+ "specific_concerns": ["short bullet", "short bullet"]
1909
+ }
1910
+ \`\`\`
1911
+
1912
+ Mapping rule for \`recommended_action\`:
1913
+ - \`low\` \u2192 \`auto-allow\`
1914
+ - \`medium\` or \`high\` \u2192 \`prompt\`
1915
+ - \`critical\` \u2192 \`deny\`
1916
+
1917
+ If genuinely uncertain, pick \`medium\` / \`prompt\` \u2014 that surfaces the action to
1918
+ the human without blocking it.
1919
+ `;
1920
+
1921
+ // src/safety/guardian.ts
1922
+ var AssessmentSchema = z5.object({
1923
+ category: z5.string().min(1).max(120),
1924
+ severity: z5.enum(["low", "medium", "high", "critical"]),
1925
+ justification: z5.string().min(1).max(600),
1926
+ recommended_action: z5.enum(["auto-allow", "prompt", "deny"]),
1927
+ specific_concerns: z5.array(z5.string().max(240)).max(12).default([])
1928
+ });
1929
+ var cachedPolicy = null;
1930
+ async function loadPolicy() {
1931
+ if (cachedPolicy) return cachedPolicy;
1932
+ try {
1933
+ const here = fileURLToPath(import.meta.url);
1934
+ const mdPath = path.join(path.dirname(here), "guardian-policy.md");
1935
+ const raw = await fs.readFile(mdPath, "utf-8");
1936
+ if (raw.length > 200) {
1937
+ cachedPolicy = raw;
1938
+ return raw;
1939
+ }
1940
+ } catch {
1941
+ }
1942
+ cachedPolicy = GUARDIAN_POLICY_EMBEDDED;
1943
+ return cachedPolicy;
1944
+ }
1945
+ var sessionCache = /* @__PURE__ */ new Map();
1946
+ function cacheKey(input) {
1947
+ return JSON.stringify({ tool: input.tool, args: input.args });
1948
+ }
1949
+ var TIMEOUT_MS = 5e3;
1950
+ function timeoutFallback(reason) {
1951
+ return {
1952
+ category: "Low-Risk Actions",
1953
+ severity: "medium",
1954
+ justification: reason,
1955
+ recommended_action: "prompt",
1956
+ specific_concerns: []
1957
+ };
1958
+ }
1959
+ var OUTPUT_INSTRUCTION = `
1960
+ ## Your Response
1961
+
1962
+ Return ONLY a single JSON object \u2014 no markdown fences, no prose before or after.
1963
+ The object MUST match this TypeScript type exactly:
1964
+
1965
+ type Assessment = {
1966
+ category: "Data Exfiltration" | "Credential Probing" | "Persistent Security Weakening" | "Destructive Actions" | "Low-Risk Actions";
1967
+ severity: "low" | "medium" | "high" | "critical";
1968
+ justification: string; // one sentence, <= 240 chars, cite the specific rule you applied
1969
+ recommended_action: "auto-allow" | "prompt" | "deny";
1970
+ specific_concerns: string[]; // 0\u20136 short bullets, each <= 240 chars
1971
+ };
1972
+
1973
+ Severity \u2192 recommended_action mapping (you MUST obey this):
1974
+ - "low" \u2192 "auto-allow"
1975
+ - "medium" \u2192 "prompt"
1976
+ - "high" \u2192 "prompt"
1977
+ - "critical" \u2192 "deny"
1978
+
1979
+ If you are uncertain, return "medium" + "prompt". Do NOT invent new severity or
1980
+ action values. Do NOT wrap the JSON in markdown. Output the raw object only.
1981
+ `;
1982
+ function extractJson(raw) {
1983
+ const trimmed = raw.trim();
1984
+ const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
1985
+ const body = fenced?.[1] ?? trimmed;
1986
+ const start = body.indexOf("{");
1987
+ const end = body.lastIndexOf("}");
1988
+ if (start < 0 || end <= start) {
1989
+ return JSON.parse(body);
1990
+ }
1991
+ return JSON.parse(body.slice(start, end + 1));
1992
+ }
1993
+ async function assessRisk(input, model) {
1994
+ const key = cacheKey(input);
1995
+ const cached = sessionCache.get(key);
1996
+ if (cached) return cached;
1997
+ let policy;
1998
+ try {
1999
+ policy = await loadPolicy();
2000
+ } catch {
2001
+ policy = GUARDIAN_POLICY_EMBEDDED;
2002
+ }
2003
+ const system = `${policy}
2004
+ ${OUTPUT_INSTRUCTION}`;
2005
+ const userPayload = {
2006
+ tool: input.tool,
2007
+ args: input.args,
2008
+ context: input.context ?? null
2009
+ };
2010
+ const user = `Score this proposed tool call:
2011
+
2012
+ \`\`\`json
2013
+ ${JSON.stringify(
2014
+ userPayload,
2015
+ null,
2016
+ 2
2017
+ )}
2018
+ \`\`\``;
2019
+ const controller = new AbortController();
2020
+ const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
2021
+ let rawText;
2022
+ try {
2023
+ const result = await generateText({
2024
+ model,
2025
+ system,
2026
+ prompt: user,
2027
+ maxTokens: 512,
2028
+ temperature: 0,
2029
+ abortSignal: controller.signal
2030
+ });
2031
+ rawText = result.text ?? "";
2032
+ } catch (err) {
2033
+ clearTimeout(timer);
2034
+ const reason = err?.name === "AbortError" || controller.signal.aborted ? "Guardian timeout \u2014 defaulting to prompt." : `Guardian unavailable (${err?.message ?? "unknown error"}) \u2014 defaulting to prompt.`;
2035
+ const fallback = timeoutFallback(reason);
2036
+ return fallback;
2037
+ } finally {
2038
+ clearTimeout(timer);
2039
+ }
2040
+ let parsed;
2041
+ try {
2042
+ parsed = extractJson(rawText);
2043
+ } catch {
2044
+ return timeoutFallback(
2045
+ "Guardian returned non-JSON \u2014 defaulting to prompt."
2046
+ );
2047
+ }
2048
+ const validated = AssessmentSchema.safeParse(parsed);
2049
+ if (!validated.success) {
2050
+ return timeoutFallback(
2051
+ "Guardian returned a malformed verdict \u2014 defaulting to prompt."
2052
+ );
2053
+ }
2054
+ const assessment = normalizeAction(validated.data);
2055
+ sessionCache.set(key, assessment);
2056
+ return assessment;
2057
+ }
2058
+ function normalizeAction(a) {
2059
+ let recommended = a.recommended_action;
2060
+ switch (a.severity) {
2061
+ case "low":
2062
+ recommended = "auto-allow";
2063
+ break;
2064
+ case "critical":
2065
+ recommended = "deny";
2066
+ break;
2067
+ case "medium":
2068
+ case "high":
2069
+ if (recommended !== "prompt") recommended = "prompt";
2070
+ break;
2071
+ }
2072
+ return { ...a, recommended_action: recommended };
2073
+ }
2074
+
2075
+ // src/coordinator/tools.ts
2076
+ import { z as z6 } from "zod";
2077
+
2078
+ // src/agent/subagent.ts
2079
+ import { streamText } from "ai";
2080
+
2081
+ // src/agent/builtins/index.ts
2082
+ import { readdirSync, readFileSync, existsSync } from "fs";
2083
+ import { dirname, join } from "path";
2084
+ import { fileURLToPath as fileURLToPath2 } from "url";
2085
+ import { parse as parseToml } from "smol-toml";
2086
+ var cache = null;
2087
+ function resolveBuiltinsDir() {
2088
+ const here = dirname(fileURLToPath2(import.meta.url));
2089
+ const candidates = [
2090
+ here,
2091
+ // src/agent/builtins (dev) or wherever co-located
2092
+ join(here, "builtins"),
2093
+ // dist layout with copied builtins/ subdir
2094
+ join(here, "..", "builtins"),
2095
+ // one level up (e.g. dist/ adjacent to builtins/)
2096
+ join(here, "..", "..", "src", "agent", "builtins")
2097
+ // from dist/ back into src/
2098
+ ];
2099
+ for (const dir of candidates) {
2100
+ try {
2101
+ if (existsSync(dir) && readdirSync(dir).some((f) => f.endsWith(".toml"))) {
2102
+ return dir;
2103
+ }
2104
+ } catch {
2105
+ }
2106
+ }
2107
+ return null;
2108
+ }
2109
+ function asString(v, field, file) {
2110
+ if (typeof v !== "string" || v.trim() === "") {
2111
+ throw new Error(`builtin agent ${file}: field "${field}" must be a non-empty string`);
2112
+ }
2113
+ return v;
2114
+ }
2115
+ function asStringArray(v, field, file) {
2116
+ if (!Array.isArray(v) || !v.every((x) => typeof x === "string")) {
2117
+ throw new Error(`builtin agent ${file}: field "${field}" must be an array of strings`);
2118
+ }
2119
+ return v;
2120
+ }
2121
+ function asPositiveInt(v, field, file, fallback) {
2122
+ if (v === void 0 || v === null) return fallback;
2123
+ if (typeof v === "number" && Number.isInteger(v) && v > 0) return v;
2124
+ if (typeof v === "bigint" && v > 0n) return Number(v);
2125
+ throw new Error(`builtin agent ${file}: field "${field}" must be a positive integer`);
2126
+ }
2127
+ function parseBuiltinFile(filePath, fileName) {
2128
+ let raw;
2129
+ try {
2130
+ raw = readFileSync(filePath, "utf8");
2131
+ } catch (err) {
2132
+ console.warn(` [builtins] could not read ${fileName}: ${err.message}`);
2133
+ return null;
2134
+ }
2135
+ let doc;
2136
+ try {
2137
+ doc = parseToml(raw);
2138
+ } catch (err) {
2139
+ console.warn(` [builtins] malformed TOML in ${fileName}: ${err.message}`);
2140
+ return null;
2141
+ }
2142
+ const a = doc.agent;
2143
+ if (!a || typeof a !== "object") {
2144
+ console.warn(` [builtins] ${fileName}: missing [agent] table`);
2145
+ return null;
2146
+ }
2147
+ try {
2148
+ const name = asString(a.name, "agent.name", fileName).toLowerCase();
2149
+ const description = asString(a.description, "agent.description", fileName);
2150
+ const tools = asStringArray(a.tools, "agent.tools", fileName);
2151
+ const maxIterations = asPositiveInt(a.max_iterations, "agent.max_iterations", fileName, 20);
2152
+ const prompt = asString(
2153
+ a.prompt?.developer_instructions,
2154
+ "agent.prompt.developer_instructions",
2155
+ fileName
2156
+ );
2157
+ return { name, description, tools, maxIterations, prompt };
2158
+ } catch (err) {
2159
+ console.warn(` [builtins] ${err.message}`);
2160
+ return null;
2161
+ }
2162
+ }
2163
+ function loadBuiltinAgents() {
2164
+ if (cache) return cache;
2165
+ const dir = resolveBuiltinsDir();
2166
+ const map = /* @__PURE__ */ new Map();
2167
+ if (!dir) {
2168
+ console.warn(" [builtins] could not locate builtins directory; no built-in agents loaded");
2169
+ cache = map;
2170
+ return map;
2171
+ }
2172
+ let entries = [];
2173
+ try {
2174
+ entries = readdirSync(dir).filter((f) => f.endsWith(".toml"));
2175
+ } catch (err) {
2176
+ console.warn(` [builtins] could not list ${dir}: ${err.message}`);
2177
+ cache = map;
2178
+ return map;
2179
+ }
2180
+ for (const file of entries) {
2181
+ const agent = parseBuiltinFile(join(dir, file), file);
2182
+ if (!agent) continue;
2183
+ if (map.has(agent.name)) {
2184
+ console.warn(` [builtins] duplicate agent name "${agent.name}" in ${file} (keeping first)`);
2185
+ continue;
2186
+ }
2187
+ map.set(agent.name, agent);
2188
+ }
2189
+ cache = map;
2190
+ return map;
2191
+ }
2192
+ function getBuiltinAgent(name) {
2193
+ return loadBuiltinAgents().get(name.toLowerCase());
2194
+ }
2195
+ function listBuiltinAgents() {
2196
+ return [...loadBuiltinAgents().values()].sort((a, b) => a.name.localeCompare(b.name));
2197
+ }
2198
+
2199
+ // src/agent/subagent.ts
2200
+ function subagentRolloutId(parentSessionId, subagentId) {
2201
+ return `${parentSessionId}.sub.${subagentId}`;
2202
+ }
2203
+ async function openSubagentRollout(parentSessionId, subagentId, kind, prompt, model) {
2204
+ if (!parentSessionId) return null;
2205
+ try {
2206
+ const id = subagentRolloutId(parentSessionId, subagentId);
2207
+ const roll = new Rollout(id);
2208
+ await roll.openNew({
2209
+ project: `subagent:${kind}:parent=${parentSessionId}`,
2210
+ model: model.modelId ?? "unknown"
2211
+ });
2212
+ await roll.append({
2213
+ type: "user-message",
2214
+ msgId: `${subagentId}.u0`,
2215
+ payload: { role: "user", content: prompt }
2216
+ });
2217
+ return roll;
2218
+ } catch {
2219
+ return null;
2220
+ }
2221
+ }
2222
+ async function recordSubagentStep(roll, subagentId, iteration, assistantText, toolCalls, toolResults) {
2223
+ if (!roll) return;
2224
+ try {
2225
+ await roll.append({
2226
+ type: "assistant-message",
2227
+ msgId: `${subagentId}.a${iteration}`,
2228
+ payload: { text: assistantText, toolCallCount: toolCalls.length }
2229
+ });
2230
+ for (const tc of toolCalls) {
2231
+ await roll.append({
2232
+ type: "tool-call",
2233
+ payload: { id: tc.toolCallId, name: tc.toolName, args: tc.args }
2234
+ });
2235
+ }
2236
+ for (const tr of toolResults) {
2237
+ await roll.append({
2238
+ type: "tool-result",
2239
+ payload: { id: tr.toolCallId, name: tr.toolName, result: tr.result }
2240
+ });
2241
+ }
2242
+ } catch {
2243
+ }
2244
+ }
2245
+ async function closeSubagentRollout(roll, outcome, finalText, errorMsg) {
2246
+ if (!roll) return;
2247
+ try {
2248
+ if (outcome === "error") {
2249
+ await roll.append({ type: "error", payload: { message: errorMsg ?? "unknown" } });
2250
+ }
2251
+ await roll.append({
2252
+ type: "turn-end",
2253
+ payload: { outcome, finalText, ts: (/* @__PURE__ */ new Date()).toISOString() }
2254
+ });
2255
+ await roll.close();
2256
+ } catch {
2257
+ }
2258
+ }
2259
+ var SUBAGENT_PROMPTS = {
2260
+ explore: `You are an exploration agent. Your job is to quickly search and analyze a codebase to answer questions.
2261
+ You have access to read, grep, and glob tools. Use them efficiently \u2014 search broadly first, then narrow down.
2262
+ Be thorough but concise. Return your findings as a structured summary.
2263
+ Do NOT modify any files. Only read and search.`,
2264
+ plan: `You are a planning agent. Your job is to design implementation strategies.
2265
+ Analyze the codebase, identify the files that need changes, and produce a step-by-step plan.
2266
+ Consider edge cases, existing patterns, and potential risks.
2267
+ Do NOT implement anything. Only plan and recommend.
2268
+ Output a structured plan with:
2269
+ 1. Summary of approach
2270
+ 2. Files to modify (with specific changes)
2271
+ 3. New files to create (if any)
2272
+ 4. Testing strategy
2273
+ 5. Risks and considerations`,
2274
+ general: `You are a general-purpose worker agent. Complete the task assigned to you.
2275
+ You have full access to all tools. Work autonomously and return results when done.
2276
+ Be efficient \u2014 use grep/glob to find things before reading entire files.
2277
+ If you encounter errors, try to resolve them yourself before reporting back.`
2278
+ };
2279
+ var EXPLORE_TOOL_FILTER = /* @__PURE__ */ new Set(["read", "grep", "glob", "web_fetch"]);
2280
+ var PLAN_TOOL_FILTER = /* @__PURE__ */ new Set(["read", "grep", "glob", "git", "web_fetch"]);
2281
+ var subagentCounter = 0;
2282
+ async function spawnSubagent(config) {
2283
+ const { id, type, prompt, model, toolContext, maxIterations = 15 } = config;
2284
+ if (type === "builtin") {
2285
+ return {
2286
+ id,
2287
+ type,
2288
+ text: "",
2289
+ toolCalls: 0,
2290
+ iterations: 0,
2291
+ error: "use spawnBuiltinAgent() for built-in agents, not spawnSubagent()"
2292
+ };
2293
+ }
2294
+ config.onStatus?.(id, `Starting ${type} agent...`);
2295
+ const subagentCtx = {
2296
+ ...toolContext,
2297
+ permissionSurface: "subagent",
2298
+ subagentId: id,
2299
+ requireConfirm: false
2300
+ };
2301
+ const allTools = buildToolMap(subagentCtx);
2302
+ const tools = {};
2303
+ if (type === "explore") {
2304
+ for (const [name, tool2] of Object.entries(allTools)) {
2305
+ if (EXPLORE_TOOL_FILTER.has(name)) tools[name] = tool2;
2306
+ }
2307
+ } else if (type === "plan") {
2308
+ for (const [name, tool2] of Object.entries(allTools)) {
2309
+ if (PLAN_TOOL_FILTER.has(name)) tools[name] = tool2;
2310
+ }
2311
+ } else {
2312
+ Object.assign(tools, allTools);
2313
+ }
2314
+ const systemPrompt = `${SUBAGENT_PROMPTS[type]}
2315
+
2316
+ ## Working Directory
2317
+ ${toolContext.cwd}
2318
+
2319
+ ## Available Tools
2320
+ ${Object.keys(tools).map((n) => `- ${n}`).join("\n")}`;
2321
+ const messages = [
2322
+ { role: "user", content: prompt }
2323
+ ];
2324
+ let iterations = 0;
2325
+ let totalToolCalls = 0;
2326
+ const roll = await openSubagentRollout(config.parentSessionId, id, type, prompt, model);
2327
+ try {
2328
+ while (iterations < maxIterations) {
2329
+ iterations++;
2330
+ config.onStatus?.(id, `Iteration ${iterations}/${maxIterations}...`);
2331
+ const result = streamText({
2332
+ model,
2333
+ system: systemPrompt,
2334
+ messages,
2335
+ tools,
2336
+ maxSteps: 1
2337
+ });
2338
+ let fullText = "";
2339
+ const toolCalls = [];
2340
+ const toolResults = [];
2341
+ for await (const event of result.fullStream) {
2342
+ if (event.type === "text-delta") {
2343
+ fullText += event.textDelta;
2344
+ } else if (event.type === "tool-call") {
2345
+ toolCalls.push({
2346
+ toolCallId: event.toolCallId,
2347
+ toolName: event.toolName,
2348
+ args: event.args
2349
+ });
2350
+ config.onStatus?.(id, `${event.toolName}(...)`);
2351
+ }
2352
+ const evt = event;
2353
+ if (evt.type === "tool-result") {
2354
+ toolResults.push({
2355
+ toolCallId: evt.toolCallId,
2356
+ toolName: toolCalls.find((tc) => tc.toolCallId === evt.toolCallId)?.toolName ?? "unknown",
2357
+ result: evt.result
2358
+ });
2359
+ }
2360
+ }
2361
+ totalToolCalls += toolCalls.length;
2362
+ await recordSubagentStep(roll, id, iterations, fullText, toolCalls, toolResults);
2363
+ if (toolCalls.length > 0) {
2364
+ messages.push({
2365
+ role: "assistant",
2366
+ content: [
2367
+ ...fullText ? [{ type: "text", text: fullText }] : [],
2368
+ ...toolCalls.map((tc) => ({
2369
+ type: "tool-call",
2370
+ toolCallId: tc.toolCallId,
2371
+ toolName: tc.toolName,
2372
+ args: tc.args
2373
+ }))
2374
+ ]
2375
+ });
2376
+ messages.push({
2377
+ role: "tool",
2378
+ content: toolResults.map((tr) => ({
2379
+ type: "tool-result",
2380
+ toolCallId: tr.toolCallId,
2381
+ toolName: tr.toolName,
2382
+ result: tr.result
2383
+ }))
2384
+ });
2385
+ continue;
2386
+ }
2387
+ config.onStatus?.(id, "Complete");
2388
+ await closeSubagentRollout(roll, "complete", fullText);
2389
+ return {
2390
+ id,
2391
+ type,
2392
+ text: fullText,
2393
+ toolCalls: totalToolCalls,
2394
+ iterations
2395
+ };
2396
+ }
2397
+ config.onStatus?.(id, "Max iterations reached");
2398
+ await closeSubagentRollout(roll, "max-iterations", "[Subagent reached max iterations]");
2399
+ return {
2400
+ id,
2401
+ type,
2402
+ text: "[Subagent reached max iterations]",
2403
+ toolCalls: totalToolCalls,
2404
+ iterations
2405
+ };
2406
+ } catch (err) {
2407
+ config.onStatus?.(id, `Error: ${err.message}`);
2408
+ await closeSubagentRollout(roll, "error", "", err.message);
2409
+ return {
2410
+ id,
2411
+ type,
2412
+ text: "",
2413
+ toolCalls: totalToolCalls,
2414
+ iterations,
2415
+ error: err.message
2416
+ };
2417
+ }
2418
+ }
2419
+ function nextSubagentId(type) {
2420
+ return `${type}-${++subagentCounter}`;
2421
+ }
2422
+ async function spawnBuiltinAgent(config) {
2423
+ const { agentName, prompt, model, toolContext } = config;
2424
+ const agent = getBuiltinAgent(agentName);
2425
+ const id = config.id ?? `${agentName.toLowerCase()}-${++subagentCounter}`;
2426
+ if (!agent) {
2427
+ return {
2428
+ id,
2429
+ type: "builtin",
2430
+ text: "",
2431
+ toolCalls: 0,
2432
+ iterations: 0,
2433
+ error: `unknown built-in agent "${agentName}"`
2434
+ };
2435
+ }
2436
+ const maxIterations = config.maxIterations ?? agent.maxIterations;
2437
+ config.onStatus?.(id, `Starting built-in agent ${agent.name}...`);
2438
+ const subagentCtx = {
2439
+ ...toolContext,
2440
+ permissionSurface: "subagent",
2441
+ subagentId: id,
2442
+ requireConfirm: false
2443
+ };
2444
+ const allTools = buildToolMap(subagentCtx);
2445
+ const allowed = new Set(agent.tools);
2446
+ const tools = {};
2447
+ for (const [name, t] of Object.entries(allTools)) {
2448
+ if (allowed.has(name)) tools[name] = t;
2449
+ }
2450
+ const systemPrompt = `${agent.prompt}
2451
+
2452
+ ## Working Directory
2453
+ ${toolContext.cwd}
2454
+
2455
+ ## Available Tools
2456
+ ${Object.keys(tools).map((n) => `- ${n}`).join("\n")}`;
2457
+ const messages = [{ role: "user", content: prompt }];
2458
+ let iterations = 0;
2459
+ let totalToolCalls = 0;
2460
+ const roll = await openSubagentRollout(config.parentSessionId, id, `builtin:${agentName}`, prompt, model);
2461
+ try {
2462
+ while (iterations < maxIterations) {
2463
+ iterations++;
2464
+ config.onStatus?.(id, `Iteration ${iterations}/${maxIterations}...`);
2465
+ const result = streamText({
2466
+ model,
2467
+ system: systemPrompt,
2468
+ messages,
2469
+ tools,
2470
+ maxSteps: 1
2471
+ });
2472
+ let fullText = "";
2473
+ const toolCalls = [];
2474
+ const toolResults = [];
2475
+ for await (const event of result.fullStream) {
2476
+ if (event.type === "text-delta") {
2477
+ fullText += event.textDelta;
2478
+ } else if (event.type === "tool-call") {
2479
+ toolCalls.push({
2480
+ toolCallId: event.toolCallId,
2481
+ toolName: event.toolName,
2482
+ args: event.args
2483
+ });
2484
+ config.onStatus?.(id, `${event.toolName}(...)`);
2485
+ }
2486
+ const evt = event;
2487
+ if (evt.type === "tool-result") {
2488
+ toolResults.push({
2489
+ toolCallId: evt.toolCallId,
2490
+ toolName: toolCalls.find((tc) => tc.toolCallId === evt.toolCallId)?.toolName ?? "unknown",
2491
+ result: evt.result
2492
+ });
2493
+ }
2494
+ }
2495
+ totalToolCalls += toolCalls.length;
2496
+ await recordSubagentStep(roll, id, iterations, fullText, toolCalls, toolResults);
2497
+ if (toolCalls.length > 0) {
2498
+ messages.push({
2499
+ role: "assistant",
2500
+ content: [
2501
+ ...fullText ? [{ type: "text", text: fullText }] : [],
2502
+ ...toolCalls.map((tc) => ({
2503
+ type: "tool-call",
2504
+ toolCallId: tc.toolCallId,
2505
+ toolName: tc.toolName,
2506
+ args: tc.args
2507
+ }))
2508
+ ]
2509
+ });
2510
+ messages.push({
2511
+ role: "tool",
2512
+ content: toolResults.map((tr) => ({
2513
+ type: "tool-result",
2514
+ toolCallId: tr.toolCallId,
2515
+ toolName: tr.toolName,
2516
+ result: tr.result
2517
+ }))
2518
+ });
2519
+ continue;
2520
+ }
2521
+ config.onStatus?.(id, "Complete");
2522
+ await closeSubagentRollout(roll, "complete", fullText);
2523
+ return {
2524
+ id,
2525
+ type: "builtin",
2526
+ text: fullText,
2527
+ toolCalls: totalToolCalls,
2528
+ iterations
2529
+ };
2530
+ }
2531
+ config.onStatus?.(id, "Max iterations reached");
2532
+ await closeSubagentRollout(roll, "max-iterations", "[Built-in agent reached max iterations]");
2533
+ return {
2534
+ id,
2535
+ type: "builtin",
2536
+ text: "[Built-in agent reached max iterations]",
2537
+ toolCalls: totalToolCalls,
2538
+ iterations
2539
+ };
2540
+ } catch (err) {
2541
+ config.onStatus?.(id, `Error: ${err.message}`);
2542
+ await closeSubagentRollout(roll, "error", "", err.message);
2543
+ return {
2544
+ id,
2545
+ type: "builtin",
2546
+ text: "",
2547
+ toolCalls: totalToolCalls,
2548
+ iterations,
2549
+ error: err.message
2550
+ };
2551
+ }
2552
+ }
2553
+
2554
+ // src/coordinator/runtime.ts
2555
+ var registry = /* @__PURE__ */ new Map();
2556
+ function registerAgent(entry) {
2557
+ registry.set(entry.agentId, entry);
2558
+ }
2559
+ function getAgent(agentId) {
2560
+ return registry.get(agentId);
2561
+ }
2562
+ function pendingCount() {
2563
+ let n = 0;
2564
+ for (const entry of registry.values()) {
2565
+ if (!entry.delivered) n++;
2566
+ }
2567
+ return n;
2568
+ }
2569
+ function formatTaskNotification(agentId, toolUseId, status, summary, result) {
2570
+ const MAX_RESULT = 8 * 1024;
2571
+ const truncated = result.length > MAX_RESULT ? result.slice(0, MAX_RESULT) + `
2572
+
2573
+ [truncated ${result.length - MAX_RESULT} chars]` : result;
2574
+ const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2575
+ return `<task-notification>
2576
+ <task-id>${esc(agentId)}</task-id>
2577
+ <tool-use-id>${esc(toolUseId)}</tool-use-id>
2578
+ <status>${status}</status>
2579
+ <summary>${esc(summary)}</summary>
2580
+ <result>${esc(truncated)}</result>
2581
+ </task-notification>`;
2582
+ }
2583
+ async function awaitOneCompletion() {
2584
+ const pending = [];
2585
+ for (const entry of registry.values()) {
2586
+ if (!entry.delivered) pending.push(entry);
2587
+ }
2588
+ if (pending.length === 0) return null;
2589
+ const alreadySettled = pending.find((e) => e.result !== void 0);
2590
+ const winner = alreadySettled ? alreadySettled : await Promise.race(
2591
+ pending.map(
2592
+ (entry) => entry.completionPromise.then(() => entry).catch(() => entry)
2593
+ )
2594
+ );
2595
+ winner.delivered = true;
2596
+ const result = winner.result;
2597
+ const status = winner.status;
2598
+ const summary = status === "completed" ? `Agent "${winner.description}" completed` : status === "failed" ? `Agent "${winner.description}" failed: ${result?.error ?? "unknown error"}` : status === "killed" ? `Agent "${winner.description}" was stopped` : `Agent "${winner.description}" ended (${status})`;
2599
+ const text = result?.text ?? result?.error ?? "";
2600
+ const xml = formatTaskNotification(
2601
+ winner.agentId,
2602
+ winner.toolUseId,
2603
+ status,
2604
+ summary,
2605
+ text
2606
+ );
2607
+ return {
2608
+ agentId: winner.agentId,
2609
+ toolUseId: winner.toolUseId,
2610
+ status,
2611
+ summary,
2612
+ result: text,
2613
+ xml
2614
+ };
2615
+ }
2616
+ function injectCompletionAsUserMessage(messages, completion) {
2617
+ messages.push({
2618
+ role: "user",
2619
+ content: completion.xml
2620
+ });
2621
+ }
2622
+ async function pollPendingAgents(messages) {
2623
+ if (pendingCount() === 0) return false;
2624
+ const completion = await awaitOneCompletion();
2625
+ if (!completion) return false;
2626
+ injectCompletionAsUserMessage(messages, completion);
2627
+ return true;
2628
+ }
2629
+
2630
+ // src/coordinator/agent-resolver.ts
2631
+ var ROLE_HINTS = [
2632
+ {
2633
+ name: "archimedes",
2634
+ concepts: ["performance", "speed", "latency", "memory", "cpu", "throughput", "cost"],
2635
+ tasks: ["optimize", "benchmark", "measure", "slow", "faster", "hot path", "profiling"],
2636
+ strong: ["profile", "bench", "o(n", "big-o", "hotspot"]
2637
+ },
2638
+ {
2639
+ name: "euclid",
2640
+ concepts: ["proof", "invariant", "correctness", "theorem", "algorithm"],
2641
+ tasks: ["prove", "verify", "derive", "axiom", "formal"],
2642
+ strong: ["property test", "invariant", "induction", "lemma"]
2643
+ },
2644
+ {
2645
+ name: "hypatia",
2646
+ concepts: ["math", "formula", "statistics", "probability", "linear algebra", "numerical"],
2647
+ tasks: ["compute", "calculate", "regression", "distribution", "matrix"],
2648
+ strong: ["eigenvalue", "gradient", "ODE", "PDE", "markov"]
2649
+ },
2650
+ {
2651
+ name: "kepler",
2652
+ concepts: ["orbits", "pattern", "trend", "time series", "dataset"],
2653
+ tasks: ["analyze", "pattern", "forecast", "anomaly"],
2654
+ strong: ["time-series", "anomaly detection", "seasonality"]
2655
+ },
2656
+ {
2657
+ name: "plato",
2658
+ concepts: ["architecture", "design", "tradeoff", "principle", "philosophy"],
2659
+ tasks: ["design", "refactor", "tradeoff", "pattern", "architect"],
2660
+ strong: ["architectural decision", "adr", "design doc"]
2661
+ },
2662
+ {
2663
+ name: "ptolemy",
2664
+ concepts: ["map", "survey", "inventory", "catalog", "codebase", "dependency"],
2665
+ tasks: ["survey", "map out", "enumerate", "list all", "find every"],
2666
+ strong: ["dependency graph", "module map", "inventory"]
2667
+ },
2668
+ {
2669
+ name: "pythagoras",
2670
+ concepts: ["test", "assertion", "edge case", "spec", "contract"],
2671
+ tasks: ["test", "write tests", "cover", "assert", "spec"],
2672
+ strong: ["unit test", "integration test", "coverage gap"]
2673
+ },
2674
+ {
2675
+ name: "awaiter",
2676
+ concepts: ["wait", "poll", "watch", "monitor", "long-running"],
2677
+ tasks: ["wait for", "watch the", "poll", "until"],
2678
+ strong: ["long running", "deployment watch", "cron"]
2679
+ }
2680
+ ];
2681
+ var MIN_CONFIDENCE = 2;
2682
+ function scoreRole(description, hint) {
2683
+ const lower = description.toLowerCase();
2684
+ let score = 0;
2685
+ const reasons = [];
2686
+ for (const kw of hint.strong) {
2687
+ if (lower.includes(kw)) {
2688
+ score += 3;
2689
+ reasons.push(`strong:"${kw}"`);
2690
+ }
2691
+ }
2692
+ for (const kw of hint.tasks) {
2693
+ if (lower.includes(kw)) {
2694
+ score += 2;
2695
+ reasons.push(`task:"${kw}"`);
2696
+ }
2697
+ }
2698
+ for (const kw of hint.concepts) {
2699
+ if (lower.includes(kw)) {
2700
+ score += 1;
2701
+ reasons.push(`concept:"${kw}"`);
2702
+ }
2703
+ }
2704
+ return { score, reasons };
2705
+ }
2706
+ function resolveAgent(description) {
2707
+ const agents = listBuiltinAgents();
2708
+ const byName = /* @__PURE__ */ new Map();
2709
+ for (const a of agents) byName.set(a.name, a);
2710
+ const ranked = [];
2711
+ for (const hint of ROLE_HINTS) {
2712
+ const agent = byName.get(hint.name);
2713
+ if (!agent) continue;
2714
+ const { score, reasons } = scoreRole(description, hint);
2715
+ if (score > 0) ranked.push({ agent, score, reasons });
2716
+ }
2717
+ ranked.sort((a, b) => b.score - a.score);
2718
+ const best = ranked[0] ?? null;
2719
+ const confident = best !== null && best.score >= MIN_CONFIDENCE;
2720
+ return { best, ranked, confident };
2721
+ }
2722
+
2723
+ // src/coordinator/tools.ts
2724
+ var spawnParameters = z6.object({
2725
+ subagent_type: z6.enum(["explore", "plan", "general", "builtin"]).describe(
2726
+ "Worker profile: explore (read-only recon), plan (planning without edits), general (full tool access), builtin (named TOML-defined agent \u2014 provide builtin_name)."
2727
+ ),
2728
+ prompt: z6.string().describe(
2729
+ "Self-contained spec for the worker. Include purpose, file paths, acceptance criteria, and what to report back. Workers cannot see this conversation."
2730
+ ),
2731
+ description: z6.string().describe(
2732
+ "Short human-readable label for this worker, shown in status updates and in <task-notification> summaries."
2733
+ ),
2734
+ builtin_name: z6.string().optional().describe('Required when subagent_type is "builtin" \u2014 the name of the TOML-defined built-in agent to run.')
2735
+ });
2736
+ var agentSpawnTool = {
2737
+ name: "agent_spawn",
2738
+ description: 'Spawn a worker agent to execute a delegated task. Returns { agent_id, status: "spawned" } immediately \u2014 the worker runs in the background and eventually reports back via a <task-notification> user message. Use subagent_type to pick the worker profile.',
2739
+ parameters: spawnParameters,
2740
+ async execute(params, ctx) {
2741
+ if (!ctx.coordinatorWorkerModel) {
2742
+ return {
2743
+ content: "agent_spawn is unavailable: coordinator mode is active but no worker model was supplied to the tool context. This is a configuration bug \u2014 coordinator mode must set coordinatorWorkerModel.",
2744
+ isError: true
2745
+ };
2746
+ }
2747
+ const { subagent_type, prompt, description, builtin_name } = params;
2748
+ if (subagent_type === "builtin" && !builtin_name) {
2749
+ const hint = resolveAgent(`${description}
2750
+ ${prompt}`);
2751
+ const suggestion = hint.confident ? ` Suggested: builtin_name="${hint.best.agent.name}" (matched ${hint.best.reasons.slice(0, 2).join(", ")}).` : "";
2752
+ return {
2753
+ content: `agent_spawn: builtin_name is required when subagent_type is "builtin".${suggestion}`,
2754
+ isError: true
2755
+ };
2756
+ }
2757
+ const agentId = subagent_type === "builtin" ? `${(builtin_name ?? "builtin").toLowerCase()}-${nextSubagentId("general").split("-")[1]}` : nextSubagentId(subagent_type);
2758
+ const abortController = new AbortController();
2759
+ const workerCtx = { ...ctx, coordinatorMode: false };
2760
+ const completionPromise = subagent_type === "builtin" ? spawnBuiltinAgent({
2761
+ id: agentId,
2762
+ agentName: builtin_name,
2763
+ prompt,
2764
+ model: ctx.coordinatorWorkerModel,
2765
+ toolContext: workerCtx
2766
+ }) : spawnSubagent({
2767
+ id: agentId,
2768
+ type: subagent_type,
2769
+ prompt,
2770
+ model: ctx.coordinatorWorkerModel,
2771
+ toolContext: workerCtx
2772
+ });
2773
+ const toolUseId = `spawn-${agentId}`;
2774
+ const entry = {
2775
+ agentId,
2776
+ toolUseId,
2777
+ description,
2778
+ completionPromise,
2779
+ abortController,
2780
+ status: "spawned",
2781
+ delivered: false,
2782
+ startedAt: Date.now()
2783
+ };
2784
+ registerAgent(entry);
2785
+ completionPromise.then((result) => {
2786
+ entry.result = result;
2787
+ if (abortController.signal.aborted) {
2788
+ entry.status = "killed";
2789
+ } else if (result.error) {
2790
+ entry.status = "failed";
2791
+ } else {
2792
+ entry.status = "completed";
2793
+ }
2794
+ }).catch((err) => {
2795
+ entry.result = {
2796
+ id: agentId,
2797
+ type: subagent_type === "builtin" ? "builtin" : subagent_type,
2798
+ text: "",
2799
+ toolCalls: 0,
2800
+ iterations: 0,
2801
+ error: err?.message ?? String(err)
2802
+ };
2803
+ entry.status = abortController.signal.aborted ? "killed" : "failed";
2804
+ });
2805
+ return {
2806
+ content: JSON.stringify({
2807
+ agent_id: agentId,
2808
+ status: "spawned",
2809
+ description,
2810
+ note: "Worker is running. You will receive a <task-notification> when it finishes."
2811
+ })
2812
+ };
2813
+ }
2814
+ };
2815
+ var sendMessageParameters = z6.object({
2816
+ to: z6.string().describe("The agent_id returned by agent_spawn."),
2817
+ message: z6.string().describe("Follow-up instructions for the worker.")
2818
+ });
2819
+ var agentSendMessageTool = {
2820
+ name: "agent_send_message",
2821
+ description: "Continue an existing worker with a follow-up message. NOTE: Notch workers are currently one-shot \u2014 once a worker has finished, this tool cannot resume it. For follow-up work, spawn a fresh worker with a synthesized prompt that includes the previous worker's findings.",
2822
+ parameters: sendMessageParameters,
2823
+ async execute(params, _ctx) {
2824
+ const { to, message: _message } = params;
2825
+ const entry = getAgent(to);
2826
+ if (!entry) {
2827
+ return {
2828
+ content: `agent_send_message: no agent with id "${to}" in the registry. Check the agent_id you got back from agent_spawn.`,
2829
+ isError: true
2830
+ };
2831
+ }
2832
+ if (entry.status === "completed" || entry.status === "failed" || entry.status === "killed") {
2833
+ return {
2834
+ content: JSON.stringify({
2835
+ status: "unsupported",
2836
+ agent_id: to,
2837
+ agent_status: entry.status,
2838
+ reason: "Notch workers are one-shot. This agent has already finished and cannot be continued. Spawn a fresh worker with a prompt that carries forward the relevant context from the previous worker's result."
2839
+ }),
2840
+ isError: true
2841
+ };
2842
+ }
2843
+ return {
2844
+ content: JSON.stringify({
2845
+ status: "unsupported",
2846
+ agent_id: to,
2847
+ agent_status: entry.status,
2848
+ reason: "Notch workers cannot receive mid-flight messages in the current runtime. Wait for the <task-notification> for this worker, then spawn a fresh worker with a synthesized follow-up prompt. Alternatively, call agent_stop and respawn with the corrected instructions."
2849
+ }),
2850
+ isError: true
2851
+ };
2852
+ }
2853
+ };
2854
+ var stopParameters = z6.object({
2855
+ id: z6.string().describe("The agent_id of the worker to stop.")
2856
+ });
2857
+ var agentStopTool = {
2858
+ name: "agent_stop",
2859
+ description: 'Abort a running worker. Use when you realize a worker is going in the wrong direction or the user has changed requirements. The worker will still emit a <task-notification> with status="killed".',
2860
+ parameters: stopParameters,
2861
+ async execute(params, _ctx) {
2862
+ const entry = getAgent(params.id);
2863
+ if (!entry) {
2864
+ return {
2865
+ content: `agent_stop: no agent with id "${params.id}" in the registry.`,
2866
+ isError: true
2867
+ };
2868
+ }
2869
+ if (entry.status === "completed" || entry.status === "failed" || entry.status === "killed") {
2870
+ return {
2871
+ content: JSON.stringify({
2872
+ status: "already_finished",
2873
+ agent_id: params.id,
2874
+ agent_status: entry.status
2875
+ })
2876
+ };
2877
+ }
2878
+ entry.abortController.abort();
2879
+ return {
2880
+ content: JSON.stringify({
2881
+ status: "abort_signaled",
2882
+ agent_id: params.id,
2883
+ note: 'Abort signal sent. The worker will finish its current step and then report back with status="killed".'
2884
+ })
2885
+ };
2886
+ }
2887
+ };
2888
+ var COORDINATOR_TOOLS = [
2889
+ agentSpawnTool,
2890
+ agentSendMessageTool,
2891
+ agentStopTool
2892
+ ];
2893
+ var COORDINATOR_TOOL_NAMES = new Set(
2894
+ COORDINATOR_TOOLS.map((t) => t.name)
2895
+ );
2896
+ var COORDINATOR_ALLOWED_TOOL_NAMES = /* @__PURE__ */ new Set([
2897
+ ...COORDINATOR_TOOL_NAMES,
2898
+ "read",
2899
+ "grep",
2900
+ "glob"
2901
+ ]);
2902
+
2903
+ // src/tools/index.ts
2904
+ var BUILTIN_TOOLS = [
2905
+ readTool,
2906
+ writeTool,
2907
+ editTool,
2908
+ applyPatchTool,
2909
+ shellTool,
2910
+ gitTool,
2911
+ githubTool,
2912
+ grepTool,
2913
+ globTool,
2914
+ webFetchTool,
2915
+ lspTool,
2916
+ notebookTool,
2917
+ taskTool,
2918
+ codeModeExecTool,
2919
+ codeModeWaitTool,
2920
+ skillManageTool,
2921
+ eventsTool
2922
+ ];
2923
+ var mcpClients = /* @__PURE__ */ new Map();
2924
+ var mcpTools = [];
2925
+ async function initMCPServers(config) {
2926
+ const servers = parseMCPConfig(config);
2927
+ let toolCount = 0;
2928
+ for (const [name, serverConfig] of Object.entries(servers)) {
2929
+ try {
2930
+ const client = new MCPClient(serverConfig, name);
2931
+ await client.connect();
2932
+ mcpClients.set(name, client);
2933
+ const tools = mcpToolsToNotch(client, name);
2934
+ mcpTools.push(...tools);
2935
+ toolCount += tools.length;
2936
+ } catch (err) {
2937
+ console.error(` MCP server '${name}' failed to connect: ${err.message}`);
2938
+ }
2939
+ }
2940
+ return toolCount;
2941
+ }
2942
+ function disconnectMCPServers() {
2943
+ for (const [, client] of mcpClients) {
2944
+ client.disconnect();
2945
+ }
2946
+ mcpClients.clear();
2947
+ mcpTools = [];
2948
+ }
2949
+ var COORDINATOR_ONLY_TOOLS = [
2950
+ agentSpawnTool,
2951
+ agentSendMessageTool,
2952
+ agentStopTool
2953
+ ];
2954
+ function getAllTools(ctx) {
2955
+ const coordinator = !!ctx?.coordinatorMode;
2956
+ if (coordinator) {
2957
+ const allowed = [];
2958
+ for (const t of BUILTIN_TOOLS) {
2959
+ if (COORDINATOR_ALLOWED_TOOL_NAMES.has(t.name)) allowed.push(t);
2960
+ }
2961
+ allowed.push(...COORDINATOR_ONLY_TOOLS);
2962
+ return allowed;
2963
+ }
2964
+ return [...BUILTIN_TOOLS, ...mcpTools, ...pluginManager.getTools()];
2965
+ }
2966
+ function buildToolMap(ctx) {
2967
+ const map = {};
2968
+ for (const t of getAllTools(ctx)) {
2969
+ map[t.name] = tool({
2970
+ description: t.description,
2971
+ parameters: t.parameters,
2972
+ execute: async (params) => {
2973
+ if (ctx.checkPermission) {
2974
+ await ctx.runHook?.("permission-request", { tool: t.name, args: params });
2975
+ const argRecord = params;
2976
+ const baseLevel = ctx.checkPermission(t.name, argRecord);
2977
+ const surface = ctx.permissionSurface ?? "interactive";
2978
+ const handlerCtx = createHandlerContext({
2979
+ cwd: ctx.cwd,
2980
+ sessionId: ctx.permissionSessionId ?? "session",
2981
+ log: ctx.log,
2982
+ autoConfirm: ctx.autoConfirm,
2983
+ guardianEnabled: !!ctx.guardianModel,
2984
+ subagentId: ctx.subagentId
2985
+ });
2986
+ const decision = await runPermissionCheck(t.name, argRecord, baseLevel, surface, handlerCtx);
2987
+ if (decision.outcome === "deny") {
2988
+ if (surface === "auto-mode") recordAutoModeDenial(decision);
2989
+ await ctx.runHook?.("permission-denied", {
2990
+ tool: t.name,
2991
+ args: params,
2992
+ reason: decision.reason ?? "denied-by-handler",
2993
+ surface
2994
+ });
2995
+ return {
2996
+ content: decision.silent ? `Permission denied: ${decision.reason ?? t.name + " blocked by " + surface + " handler"}` : `Permission denied: ${t.name} \u2014 ${decision.reason ?? "denied by permission handler"}.`,
2997
+ isError: true
2998
+ };
2999
+ }
3000
+ if (decision.outcome === "prompt") {
3001
+ let guardian = null;
3002
+ if (ctx.guardianModel) {
3003
+ try {
3004
+ guardian = await assessRisk(
3005
+ { tool: t.name, args: argRecord },
3006
+ ctx.guardianModel
3007
+ );
3008
+ } catch {
3009
+ guardian = null;
3010
+ }
3011
+ if (guardian?.recommended_action === "deny") {
3012
+ await ctx.runHook?.("permission-denied", {
3013
+ tool: t.name,
3014
+ args: params,
3015
+ reason: "guardian-denied",
3016
+ guardian
3017
+ });
3018
+ return {
3019
+ content: `Guardian blocked this action (${guardian.severity}): ${guardian.justification}`,
3020
+ isError: true
3021
+ };
3022
+ }
3023
+ }
3024
+ const guardianCleared = guardian?.recommended_action === "auto-allow" && guardian.severity === "low";
3025
+ if (!guardianCleared) {
3026
+ if (!ctx.requireConfirm) {
3027
+ pushTailNotification(`${surface}: prompt required for ${t.name} but no UI (denied)`);
3028
+ await ctx.runHook?.("permission-denied", {
3029
+ tool: t.name,
3030
+ args: params,
3031
+ reason: "no-ui-to-prompt",
3032
+ surface
3033
+ });
3034
+ return {
3035
+ content: `Permission denied: ${t.name} requires a prompt but no interactive UI is attached.`,
3036
+ isError: true
3037
+ };
3038
+ }
3039
+ const paramSummary = Object.entries(argRecord).map(([k, v]) => `${k}=${String(v).slice(0, 80)}`).join(", ");
3040
+ const basePrompt = `Tool ${t.name}(${paramSummary}) requires approval. Proceed?`;
3041
+ const guardianTag = guardian ? `
3042
+ (Guardian: ${guardian.severity} \u2014 ${guardian.justification.slice(0, 200)})` : "";
3043
+ const confirmed = await ctx.confirm(basePrompt + guardianTag);
3044
+ if (!confirmed) {
3045
+ await ctx.runHook?.("permission-denied", { tool: t.name, args: params, reason: "cancelled-by-user" });
3046
+ return { content: "Cancelled by user.", isError: true };
3047
+ }
3048
+ if (surface === "interactive") {
3049
+ recordInteractiveApproval(
3050
+ ctx.permissionSessionId ?? "session",
3051
+ t.name,
3052
+ argRecord
3053
+ );
3054
+ }
3055
+ }
3056
+ }
3057
+ }
3058
+ if (ctx.dryRun && ["write", "edit", "apply_patch", "shell", "git"].includes(t.name)) {
3059
+ const paramSummary = JSON.stringify(params, null, 2).slice(0, 500);
3060
+ return {
3061
+ content: `[DRY RUN] Would execute ${t.name}:
3062
+ ${paramSummary}`
3063
+ };
3064
+ }
3065
+ await ctx.runHook?.("pre-tool", { tool: t.name, args: params });
3066
+ const result = await t.execute(params, ctx);
3067
+ await ctx.runHook?.("post-tool", {
3068
+ tool: t.name,
3069
+ args: params,
3070
+ result: result.content.slice(0, 500),
3071
+ isError: result.isError ?? false
3072
+ });
3073
+ return result;
3074
+ }
3075
+ });
3076
+ }
3077
+ return map;
3078
+ }
3079
+ function listToolNames(ctx) {
3080
+ return getAllTools(ctx).map((t) => t.name);
3081
+ }
3082
+ function describeTools2(ctx) {
3083
+ return getAllTools(ctx).map(
3084
+ (t) => `- **${t.name}**: ${t.description}`
3085
+ ).join("\n");
3086
+ }
3087
+ function mcpToolCount() {
3088
+ return mcpTools.length;
3089
+ }
3090
+
3091
+ export {
3092
+ drainTailNotifications,
3093
+ setCurrentSurface,
3094
+ MCPClient,
3095
+ parseMCPConfig,
3096
+ listBuiltinAgents,
3097
+ spawnSubagent,
3098
+ nextSubagentId,
3099
+ pollPendingAgents,
3100
+ initMCPServers,
3101
+ disconnectMCPServers,
3102
+ buildToolMap,
3103
+ listToolNames,
3104
+ describeTools2 as describeTools,
3105
+ mcpToolCount
3106
+ };