toolnexus 0.21.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/builtin.js CHANGED
@@ -14,11 +14,12 @@ import { compareCodePoints, sortEntriesByName, toPosixPath } from "./order.js";
14
14
  import path from "node:path";
15
15
  import { pending } from "./types.js";
16
16
  import { mediaTypeFor } from "./content.js";
17
+ import { bashOutput, killShell, runWebSearch, editNotebook, goJsonIndent, } from "./builtin_extras.js";
17
18
  /**
18
- * How long a job gets between "please stop" and "stop". Fixed, and identical in
19
- * every port, so a timeout means the same thing everywhere.
19
+ * How long a job gets between "please stop" and "stop" when `killGraceMs` is
20
+ * not set. Identical in every port, so a timeout means the same thing everywhere.
20
21
  */
21
- const KILL_GRACE_MS = 2000;
22
+ const DEFAULT_KILL_GRACE_MS = 2000;
22
23
  const WIN_RESERVED = new Set([
23
24
  "CON", "PRN", "AUX", "NUL",
24
25
  "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
@@ -68,8 +69,16 @@ class BuiltinEnv {
68
69
  shellError;
69
70
  baseDir;
70
71
  confine;
72
+ killGraceMs;
73
+ background;
74
+ webSearch;
75
+ notebookEdit;
71
76
  constructor(cfg) {
72
77
  const obj = typeof cfg === "object" && cfg !== null ? cfg : {};
78
+ this.background = obj.background;
79
+ this.webSearch = obj.webSearch;
80
+ this.notebookEdit = obj.notebookEdit === true;
81
+ this.killGraceMs = typeof obj.killGraceMs === "number" && obj.killGraceMs > 0 ? obj.killGraceMs : DEFAULT_KILL_GRACE_MS;
73
82
  this.baseDir = obj.baseDir ?? "";
74
83
  this.confine = obj.confineToBaseDir === true;
75
84
  if (obj.shell && obj.shell.length > 0) {
@@ -276,14 +285,21 @@ function walkFiles(root) {
276
285
  // individual tools
277
286
  // ---------------------------------------------------------------------------
278
287
  function bashTool(env) {
288
+ const properties = {
289
+ command: { type: "string", description: "The shell command to run" },
290
+ workdir: { type: "string", description: "Working directory (default: process cwd)" },
291
+ timeout: { type: "number", description: "Timeout in milliseconds (default 60000)" },
292
+ description: { type: "string", description: "Human-readable description of the command" },
293
+ };
294
+ if (env.background) {
295
+ properties.run_in_background = {
296
+ type: "boolean",
297
+ description: "Start the command and return at once with a background shell id; read it with bash_output, stop it with kill_shell. No timeout applies.",
298
+ };
299
+ }
279
300
  return builtin("bash", "Run a shell command and return its combined stdout+stderr. Non-zero exit is an error.", {
280
301
  type: "object",
281
- properties: {
282
- command: { type: "string", description: "The shell command to run" },
283
- workdir: { type: "string", description: "Working directory (default: process cwd)" },
284
- timeout: { type: "number", description: "Timeout in milliseconds (default 60000)" },
285
- description: { type: "string", description: "Human-readable description of the command" },
286
- },
302
+ properties,
287
303
  required: ["command"],
288
304
  additionalProperties: false,
289
305
  }, (args, ctx) => new Promise((resolve) => {
@@ -301,6 +317,10 @@ function bashTool(env) {
301
317
  }
302
318
  const timeout = typeof args.timeout === "number" ? args.timeout : 60_000;
303
319
  const meta = { shell: env.shellLabel };
320
+ if (env.background && args.run_in_background === true) {
321
+ const j = env.background.start(argv, command, workdir, env.killGraceMs);
322
+ return resolve(ok(`Started background shell ${j.id}. Read its output with bash_output, stop it with kill_shell.`, { ...meta, backgroundId: j.id }));
323
+ }
304
324
  // NOT `shell: true`: on Windows that silently means cmd.exe while every
305
325
  // other port fails loudly, and it hides WHICH interpreter ran. `detached`
306
326
  // puts the child in its own process group so a kill reaches the command
@@ -362,7 +382,7 @@ function bashTool(env) {
362
382
  graceTimer = setTimeout(() => {
363
383
  if (killJob(false))
364
384
  killedTree = true;
365
- }, KILL_GRACE_MS);
385
+ }, env.killGraceMs);
366
386
  };
367
387
  let graceTimer;
368
388
  const timer = setTimeout(() => stop("timeout"), timeout);
@@ -459,7 +479,16 @@ function readTool(env) {
459
479
  const offset = typeof args.offset === "number" ? Math.max(1, Math.trunc(args.offset)) : 1;
460
480
  const start = offset - 1;
461
481
  const limit = typeof args.limit === "number" ? Math.max(0, Math.trunc(args.limit)) : lines.length - start;
462
- return ok(lines.slice(start, start + limit).join("\n"));
482
+ const end = Math.min(Math.max(start, start + limit), lines.length);
483
+ const out = lines.slice(start, start + limit).join("\n");
484
+ // A window that stops before the end of the file says so in metadata, with the offset to
485
+ // continue from (SPEC §4C). The text stays byte-identical. A trailing newline is not a line.
486
+ let total = lines.length;
487
+ if (total > 0 && lines[total - 1] === "")
488
+ total--;
489
+ if (end < total)
490
+ return ok(out, { truncated: true, totalLines: total, nextOffset: end + 1 });
491
+ return ok(out);
463
492
  });
464
493
  }
465
494
  function writeTool(env) {
@@ -483,10 +512,11 @@ function writeTool(env) {
483
512
  catch (e) {
484
513
  return err(`write: ${e instanceof Error ? e.message : String(e)}`);
485
514
  }
515
+ const before = await readIfExists(full);
486
516
  await fs.mkdir(path.dirname(path.resolve(full)), { recursive: true });
487
517
  await fs.writeFile(full, content, "utf8");
488
518
  const bytes = Buffer.byteLength(content, "utf8");
489
- return ok(`Wrote ${bytes} bytes to ${p}`, { bytes });
519
+ return ok(`Wrote ${bytes} bytes to ${p}`, { bytes, ...editMetadata(env, [{ path: full, before, after: content }]) });
490
520
  });
491
521
  }
492
522
  function editTool(env) {
@@ -530,9 +560,10 @@ function editTool(env) {
530
560
  return err(`edit: oldString is not unique in ${p} (${count} occurrences); use replaceAll`);
531
561
  next = content.replace(oldString, newString);
532
562
  }
533
- await fs.writeFile(p, next, "utf8");
563
+ await fs.writeFile(full, next, "utf8");
534
564
  return ok(`Edited ${p} (${args.replaceAll === true ? count : 1} replacement${(args.replaceAll === true ? count : 1) === 1 ? "" : "s"})`, {
535
565
  replacements: args.replaceAll === true ? count : 1,
566
+ ...editMetadata(env, [{ path: full, before: content, after: next }]),
536
567
  });
537
568
  });
538
569
  }
@@ -544,6 +575,9 @@ function grepTool(env) {
544
575
  path: { type: "string", description: "Directory to search (default: process cwd)" },
545
576
  include: { type: "string", description: "Glob filter for file names" },
546
577
  limit: { type: "number", description: "Maximum number of matches (default 100)" },
578
+ context: { type: "number", description: "Lines of context before and after each match" },
579
+ before: { type: "number", description: "Lines of context before each match (overrides context)" },
580
+ after: { type: "number", description: "Lines of context after each match (overrides context)" },
547
581
  },
548
582
  required: ["pattern"],
549
583
  additionalProperties: false,
@@ -571,6 +605,14 @@ function grepTool(env) {
571
605
  // CONTENT and the order of the results were whatever the walk happened to reach first.
572
606
  // Now: collect every match, order by relative path (code point) then line number ascending,
573
607
  // and truncate last. The emitted path and the sort key are the same string (A28).
608
+ let before = 0, after = 0;
609
+ if (typeof args.context === "number" && args.context > 0)
610
+ before = after = Math.trunc(args.context);
611
+ if (typeof args.before === "number" && args.before >= 0)
612
+ before = Math.trunc(args.before);
613
+ if (typeof args.after === "number" && args.after >= 0)
614
+ after = Math.trunc(args.after);
615
+ const fileLines = new Map(); // only filled when context is asked for
574
616
  const hits = [];
575
617
  for (const file of walkFiles(root)) {
576
618
  const rel = toPosixPath(path.relative(root, file), path.sep);
@@ -584,14 +626,58 @@ function grepTool(env) {
584
626
  continue;
585
627
  }
586
628
  const lines = text.split("\n");
629
+ if (before > 0 || after > 0)
630
+ fileLines.set(rel, lines);
587
631
  for (let i = 0; i < lines.length; i++) {
588
632
  if (re.test(lines[i]))
589
633
  hits.push({ rel, line: i + 1, text: lines[i] });
590
634
  }
591
635
  }
592
636
  hits.sort((a, b) => compareCodePoints(a.rel, b.rel) || a.line - b.line);
593
- const matches = hits.slice(0, limit).map((h) => `${h.rel}:${h.line}:${h.text}`);
594
- return ok(matches.join("\n"), { count: matches.length });
637
+ const total = hits.length;
638
+ const kept = hits.slice(0, limit);
639
+ let outLines;
640
+ if (before === 0 && after === 0) {
641
+ outLines = kept.map((h) => `${h.rel}:${h.line}:${h.text}`);
642
+ }
643
+ else {
644
+ // ripgrep-style: match lines path:N:text, context lines path-N-text, "--" between
645
+ // groups that do not touch.
646
+ outLines = [];
647
+ const matchAt = new Map();
648
+ for (const h of kept) {
649
+ if (!matchAt.has(h.rel))
650
+ matchAt.set(h.rel, new Set());
651
+ matchAt.get(h.rel).add(h.line);
652
+ }
653
+ let lastRel = "", lastLine = 0;
654
+ for (const h of kept) {
655
+ const lines = fileLines.get(h.rel) ?? [];
656
+ let n = lines.length;
657
+ if (n > 0 && lines[n - 1] === "")
658
+ n--; // a trailing newline is not a line
659
+ let from = Math.max(1, h.line - before);
660
+ const to = Math.min(n, h.line + after);
661
+ if (h.rel === lastRel && from <= lastLine + 1)
662
+ from = lastLine + 1;
663
+ else if (lastRel !== "")
664
+ outLines.push("--");
665
+ for (let k = from; k <= to; k++) {
666
+ const sep = matchAt.get(h.rel).has(k) ? ":" : "-";
667
+ outLines.push(`${h.rel}${sep}${k}${sep}${lines[k - 1]}`);
668
+ }
669
+ if (to > lastLine || h.rel !== lastRel)
670
+ lastLine = to;
671
+ lastRel = h.rel;
672
+ }
673
+ }
674
+ const meta = { count: kept.length };
675
+ // Say the list was cut, and by how much, in metadata (SPEC §4C). The text is unchanged.
676
+ if (total > kept.length) {
677
+ meta.truncated = true;
678
+ meta.total = total;
679
+ }
680
+ return ok(outLines.join("\n"), meta);
595
681
  });
596
682
  }
597
683
  function globTool(env) {
@@ -872,21 +958,26 @@ function applyPatchTool(env) {
872
958
  // Stage every write/delete first; only touch the filesystem once all hunks apply.
873
959
  const writes = [];
874
960
  const deletes = [];
961
+ const changes = [];
875
962
  try {
876
963
  for (const op of ops) {
877
964
  if (op.type === "add") {
878
965
  if (existsSync(op.path))
879
966
  throw new Error(`file already exists: ${op.path}`);
880
967
  writes.push({ path: op.path, content: op.content });
968
+ changes.push({ path: op.path, before: undefined, after: op.content });
881
969
  }
882
970
  else if (op.type === "delete") {
883
971
  if (!existsSync(op.path))
884
972
  throw new Error(`file not found: ${op.path}`);
885
973
  deletes.push(op.path);
974
+ changes.push({ path: op.path, before: await fs.readFile(op.path, "utf8"), after: undefined });
886
975
  }
887
976
  else {
888
977
  const content = await fs.readFile(op.path, "utf8");
889
- writes.push({ path: op.path, content: applyUpdate(content, op.body) });
978
+ const next = applyUpdate(content, op.body);
979
+ writes.push({ path: op.path, content: next });
980
+ changes.push({ path: op.path, before: content, after: next });
890
981
  }
891
982
  }
892
983
  }
@@ -904,9 +995,119 @@ function applyPatchTool(env) {
904
995
  added: ops.filter((o) => o.type === "add").length,
905
996
  updated: ops.filter((o) => o.type === "update").length,
906
997
  deleted: ops.filter((o) => o.type === "delete").length,
998
+ ...editMetadata(env, changes),
907
999
  });
908
1000
  });
909
1001
  }
1002
+ async function readIfExists(p) {
1003
+ try {
1004
+ return await fs.readFile(p, "utf8");
1005
+ }
1006
+ catch {
1007
+ return undefined;
1008
+ }
1009
+ }
1010
+ /** Lines of a file's text; a trailing newline does not make an extra empty line. */
1011
+ function diffLines(text) {
1012
+ if (!text)
1013
+ return [];
1014
+ const lines = text.split("\n");
1015
+ if (lines[lines.length - 1] === "")
1016
+ lines.pop();
1017
+ return lines;
1018
+ }
1019
+ /** Line diff by longest common subsequence — minimal, so the counts agree across ports. */
1020
+ function lineDiff(a0, b0) {
1021
+ // Common prefix/suffix first: an edit touches a few lines, so the LCS table stays small.
1022
+ let pre = 0;
1023
+ while (pre < a0.length && pre < b0.length && a0[pre] === b0[pre])
1024
+ pre++;
1025
+ let suf = 0;
1026
+ while (suf < a0.length - pre && suf < b0.length - pre && a0[a0.length - 1 - suf] === b0[b0.length - 1 - suf])
1027
+ suf++;
1028
+ const head = a0.slice(0, pre).map((line) => ({ kind: " ", line }));
1029
+ const tail = a0.slice(a0.length - suf).map((line) => ({ kind: " ", line }));
1030
+ const a = a0.slice(pre, a0.length - suf), b = b0.slice(pre, b0.length - suf);
1031
+ const n = a.length, m = b.length;
1032
+ const lcs = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
1033
+ for (let i = n - 1; i >= 0; i--) {
1034
+ for (let j = m - 1; j >= 0; j--) {
1035
+ lcs[i][j] = a[i] === b[j] ? lcs[i + 1][j + 1] + 1 : Math.max(lcs[i + 1][j], lcs[i][j + 1]);
1036
+ }
1037
+ }
1038
+ const ops = [];
1039
+ let i = 0, j = 0;
1040
+ while (i < n && j < m) {
1041
+ if (a[i] === b[j]) {
1042
+ ops.push({ kind: " ", line: a[i] });
1043
+ i++;
1044
+ j++;
1045
+ }
1046
+ else if (lcs[i + 1][j] >= lcs[i][j + 1])
1047
+ ops.push({ kind: "-", line: a[i++] });
1048
+ else
1049
+ ops.push({ kind: "+", line: b[j++] });
1050
+ }
1051
+ while (i < n)
1052
+ ops.push({ kind: "-", line: a[i++] });
1053
+ while (j < m)
1054
+ ops.push({ kind: "+", line: b[j++] });
1055
+ return [...head, ...ops, ...tail];
1056
+ }
1057
+ /** Unified hunks with 3 lines of context. */
1058
+ function unifiedHunks(ops) {
1059
+ const CTX = 3;
1060
+ const out = [];
1061
+ const changed = ops.map((o, k) => (o.kind === " " ? -1 : k)).filter((k) => k >= 0);
1062
+ if (!changed.length)
1063
+ return out;
1064
+ // group change indices whose gap of context is <= 2*CTX
1065
+ const groups = [];
1066
+ for (const k of changed) {
1067
+ const g = groups[groups.length - 1];
1068
+ if (g && k - g[1] <= 2 * CTX)
1069
+ g[1] = k;
1070
+ else
1071
+ groups.push([k, k]);
1072
+ }
1073
+ // old/new line number before each op
1074
+ const oldAt = [], newAt = [];
1075
+ let o = 1, nn = 1;
1076
+ for (const op of ops) {
1077
+ oldAt.push(o);
1078
+ newAt.push(nn);
1079
+ if (op.kind !== "+")
1080
+ o++;
1081
+ if (op.kind !== "-")
1082
+ nn++;
1083
+ }
1084
+ for (const [s0, e0] of groups) {
1085
+ const s = Math.max(0, s0 - CTX), e = Math.min(ops.length - 1, e0 + CTX);
1086
+ const slice = ops.slice(s, e + 1);
1087
+ const oldLen = slice.filter((x) => x.kind !== "+").length;
1088
+ const newLen = slice.filter((x) => x.kind !== "-").length;
1089
+ const oldStart = oldLen ? oldAt[s] : oldAt[s] - 1;
1090
+ const newStart = newLen ? newAt[s] : newAt[s] - 1;
1091
+ out.push(`@@ -${oldStart},${oldLen} +${newStart},${newLen} @@`);
1092
+ for (const x of slice)
1093
+ out.push(x.kind + x.line);
1094
+ }
1095
+ return out;
1096
+ }
1097
+ function editMetadata(env, changes) {
1098
+ const files = [];
1099
+ const diff = [];
1100
+ for (const c of changes) {
1101
+ const rel = toPosixPath(env.baseDir ? path.relative(path.resolve(env.baseDir), path.resolve(c.path)) : c.path, path.sep);
1102
+ const ops = lineDiff(diffLines(c.before), diffLines(c.after));
1103
+ const additions = ops.filter((x) => x.kind === "+").length;
1104
+ const deletions = ops.filter((x) => x.kind === "-").length;
1105
+ const status = c.before === undefined ? "added" : c.after === undefined ? "deleted" : "modified";
1106
+ files.push({ path: rel, status, additions, deletions });
1107
+ diff.push(`--- ${c.before === undefined ? "/dev/null" : `a/${rel}`}`, `+++ ${c.after === undefined ? "/dev/null" : `b/${rel}`}`, ...unifiedHunks(ops));
1108
+ }
1109
+ return { files, diff: diff.length ? diff.join("\n") + "\n" : "" };
1110
+ }
910
1111
  /**
911
1112
  * Build the ten built-in tools (each source:"builtin"). The order is fixed
912
1113
  * for parity: bash, read, write, edit, grep, glob, webfetch, question,
@@ -914,7 +1115,7 @@ function applyPatchTool(env) {
914
1115
  */
915
1116
  export function createBuiltinTools(cfg) {
916
1117
  const env = new BuiltinEnv(cfg);
917
- return [
1118
+ const tools = [
918
1119
  bashTool(env),
919
1120
  readTool(env),
920
1121
  writeTool(env),
@@ -926,6 +1127,103 @@ export function createBuiltinTools(cfg) {
926
1127
  applyPatchTool(env),
927
1128
  todowriteTool(),
928
1129
  ];
1130
+ if (env.background)
1131
+ tools.push(bashOutputTool(env.background), killShellTool(env.background));
1132
+ if (env.webSearch)
1133
+ tools.push(webSearchTool(env.webSearch));
1134
+ if (env.notebookEdit)
1135
+ tools.push(notebookEditTool(env));
1136
+ for (const t of tools)
1137
+ t.hints = { ...BUILTIN_HINTS[t.name] };
1138
+ return tools;
1139
+ }
1140
+ const hint = (readOnly, destructive, openWorld) => ({ readOnly, destructive, openWorld });
1141
+ /**
1142
+ * Hints on every builtin, for host permission gates (SPEC §4C). bash can do anything, so it is
1143
+ * mutating, destructive and open-world. todowrite and question only touch the agent's own state
1144
+ * or ask the human, so they count as read-only for the host's files and systems.
1145
+ */
1146
+ const BUILTIN_HINTS = {
1147
+ bash: hint(false, true, true),
1148
+ read: hint(true, false, false),
1149
+ write: hint(false, true, false),
1150
+ edit: hint(false, true, false),
1151
+ grep: hint(true, false, false),
1152
+ glob: hint(true, false, false),
1153
+ webfetch: hint(true, false, true),
1154
+ question: hint(true, false, false),
1155
+ apply_patch: hint(false, true, false),
1156
+ todowrite: hint(true, false, false),
1157
+ bash_output: hint(true, false, false),
1158
+ kill_shell: hint(false, true, false),
1159
+ websearch: hint(true, false, true),
1160
+ notebook_edit: hint(false, true, false),
1161
+ };
1162
+ const idSchema = {
1163
+ type: "object",
1164
+ properties: { id: { type: "string", description: "The background shell id bash returned" } },
1165
+ required: ["id"],
1166
+ additionalProperties: false,
1167
+ };
1168
+ function bashOutputTool(bg) {
1169
+ return builtin("bash_output", "Read the output a background bash command (started with run_in_background) produced since the last read, and whether it is still running.", idSchema, async (args) => bashOutput(bg, typeof args.id === "string" ? args.id : ""));
1170
+ }
1171
+ function killShellTool(bg) {
1172
+ return builtin("kill_shell", "Stop a background bash command and everything it started.", idSchema, async (args) => killShell(bg, typeof args.id === "string" ? args.id : ""));
1173
+ }
1174
+ function webSearchTool(search) {
1175
+ return builtin("websearch", "Search the web. Returns numbered results with title, URL and snippet.", {
1176
+ type: "object",
1177
+ properties: {
1178
+ query: { type: "string", description: "The search query" },
1179
+ limit: { type: "number", description: "Maximum number of results (default 5)" },
1180
+ },
1181
+ required: ["query"],
1182
+ additionalProperties: false,
1183
+ }, async (args, ctx) => runWebSearch(search, args, ctx?.signal));
1184
+ }
1185
+ function notebookEditTool(env) {
1186
+ return builtin("notebook_edit", "Edit a Jupyter notebook (.ipynb) cell: replace its source, insert a new cell after it, or delete it. Replacing a code cell clears its outputs.", {
1187
+ type: "object",
1188
+ properties: {
1189
+ path: { type: "string", description: "Path to the .ipynb file" },
1190
+ cell_id: { type: "string", description: "The cell's id (or use cell_index)" },
1191
+ cell_index: { type: "number", description: "0-based cell position (used when cell_id is absent)" },
1192
+ new_source: { type: "string", description: "The new cell source (replace/insert)" },
1193
+ cell_type: { type: "string", enum: ["code", "markdown"], description: "Cell type for insert (default code) or to change on replace" },
1194
+ edit_mode: { type: "string", enum: ["replace", "insert", "delete"], description: "Default replace. insert adds the new cell AFTER the given cell, or first when none is given" },
1195
+ },
1196
+ required: ["path"],
1197
+ additionalProperties: false,
1198
+ }, async (args) => {
1199
+ const p = typeof args.path === "string" ? args.path : "";
1200
+ if (!p)
1201
+ return err("notebook_edit: path is required");
1202
+ let full;
1203
+ let raw;
1204
+ try {
1205
+ full = env.resolvePath(p);
1206
+ raw = await fs.readFile(full, "utf8");
1207
+ }
1208
+ catch (e) {
1209
+ return err(`notebook_edit: ${e instanceof Error ? e.message : String(e)}`);
1210
+ }
1211
+ let nb;
1212
+ try {
1213
+ nb = JSON.parse(raw);
1214
+ if (!nb || typeof nb !== "object" || Array.isArray(nb))
1215
+ throw new Error("not a JSON object");
1216
+ }
1217
+ catch (e) {
1218
+ return err(`notebook_edit: ${p} is not a notebook: ${e instanceof Error ? e.message : String(e)}`);
1219
+ }
1220
+ const r = editNotebook(nb, args, p);
1221
+ if (!("summary" in r))
1222
+ return r;
1223
+ const out = goJsonIndent(nb) + "\n";
1224
+ await fs.writeFile(full, out, "utf8");
1225
+ return ok(r.summary, { cells: nb.cells.length, ...editMetadata(env, [{ path: full, before: raw, after: out }]) });
1226
+ });
929
1227
  }
930
1228
  /**
931
1229
  * The interpreter the builtins resolved for `bash` — what a host prints, and
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Opt-in builtins for coding-agent hosts (SPEC §4C): background shells (`bash`
3
+ * run_in_background + `bash_output` + `kill_shell`), `websearch` over a host-supplied provider,
4
+ * and `notebook_edit`. None is on by default: a host that sets nothing sees the same ten builtins.
5
+ */
6
+ import { type ChildProcess } from "node:child_process";
7
+ import type { ToolResult } from "./types.js";
8
+ /** One background command, for a host UI. */
9
+ export interface BackgroundShellInfo {
10
+ id: string;
11
+ command: string;
12
+ started: Date;
13
+ running: boolean;
14
+ /** Valid when not running; -1 when killed by a signal. */
15
+ exitCode: number;
16
+ killed: boolean;
17
+ }
18
+ interface BgJob {
19
+ id: string;
20
+ command: string;
21
+ started: Date;
22
+ child: ChildProcess;
23
+ pgid?: number;
24
+ grace: number;
25
+ out: string;
26
+ read: number;
27
+ running: boolean;
28
+ exitCode: number;
29
+ killed: boolean;
30
+ done: Promise<void>;
31
+ }
32
+ /**
33
+ * Owns the commands `bash` started with `run_in_background`. Pass one as
34
+ * `builtins.background`, and call `closeAll()` when the host shuts down — a background command
35
+ * is NOT tied to the turn that started it, so nothing else stops it.
36
+ */
37
+ export declare class BackgroundShells {
38
+ private next;
39
+ private readonly jobs;
40
+ /** @internal Start `command` under `argv` in its own process group. */
41
+ start(argv: string[], command: string, workdir: string, grace: number): BgJob;
42
+ /** @internal */
43
+ get(id: string): BgJob | undefined;
44
+ private signal;
45
+ /** @internal Stop the job's whole process group: SIGTERM, the grace window, SIGKILL.
46
+ * Resolves whether it was still running. */
47
+ kill(j: BgJob): Promise<boolean>;
48
+ static info(j: BgJob): BackgroundShellInfo;
49
+ /** Every background command, oldest first. */
50
+ list(): BackgroundShellInfo[];
51
+ /** Stop one background command's process group. Unknown id ⇒ throws. */
52
+ killShell(id: string): Promise<void>;
53
+ /** Stop every background command still running, concurrently. */
54
+ closeAll(): Promise<void>;
55
+ }
56
+ export declare function bgStatus(i: BackgroundShellInfo): string;
57
+ /** `bash_output(id)`. */
58
+ export declare function bashOutput(bg: BackgroundShells, id: string): ToolResult;
59
+ /** `kill_shell(id)`. */
60
+ export declare function killShell(bg: BackgroundShells, id: string): Promise<ToolResult>;
61
+ /** One hit from a host search provider. */
62
+ export interface WebSearchResult {
63
+ title: string;
64
+ url: string;
65
+ snippet?: string;
66
+ }
67
+ /** The host's search provider. The library ships none. */
68
+ export type WebSearchFunc = (query: string, limit: number, ctx: {
69
+ signal?: AbortSignal;
70
+ }) => Promise<WebSearchResult[]>;
71
+ export declare function runWebSearch(search: WebSearchFunc, args: Record<string, unknown>, signal?: AbortSignal): Promise<ToolResult>;
72
+ /** Split text into nbformat's list of lines, each keeping its "\n". */
73
+ export declare function notebookSource(s: string): string[];
74
+ /** Go's json.MarshalIndent(v, "", " ") with sorted keys, plus Go's HTML escaping. */
75
+ export declare function goJsonIndent(v: unknown): string;
76
+ /** Apply one notebook_edit to the parsed notebook, in place. Returns the summary, or an error result. */
77
+ export declare function editNotebook(nb: Record<string, any>, args: Record<string, unknown>, p: string): {
78
+ summary: string;
79
+ } | ToolResult;
80
+ export {};