openmausbot 0.1.69 → 0.1.71

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 (66) hide show
  1. package/dist/assets/{index-BmHupgVJ.js → index-CJtyBonr.js} +1 -1
  2. package/dist/assets/index-CYLdYtYp.css +1 -0
  3. package/dist/assets/index-Di4ovwu5.js +289 -0
  4. package/dist/index.html +2 -2
  5. package/dist-server/container-mcp.js +6 -2
  6. package/dist-server/drivers/agents-proxy.js +305 -16
  7. package/dist-server/drivers/pi-mcp-extension.ts +1 -1
  8. package/dist-server/index.js +3141 -1124
  9. package/dist-server/local-computer-proxy.js +2 -0
  10. package/dist-server/local-computer.js +2 -1
  11. package/dist-server/mcp-gate.js +543 -0
  12. package/dist-server/mcp-server.js +38 -0
  13. package/dist-server/openmausbot.js +772 -260
  14. package/dist-server/pair-cli.js +772 -260
  15. package/dist-server/prepare-cloudflared.js +20 -3
  16. package/dist-server/proxy-paths.js +1 -0
  17. package/dist-server/server/agent-tool-policy.js +29 -0
  18. package/dist-server/server/bot-package.js +55 -2
  19. package/dist-server/server/browser-live.js +12 -7
  20. package/dist-server/server/config.js +40 -4
  21. package/dist-server/server/delegations.js +132 -74
  22. package/dist-server/server/drivers/acp/core.js +2 -0
  23. package/dist-server/server/drivers/acp/cursor.js +1 -1
  24. package/dist-server/server/drivers/acp/grok.js +5 -1
  25. package/dist-server/server/drivers/agents-proxy.js +324 -16
  26. package/dist-server/server/drivers/boxagent.js +1 -1
  27. package/dist-server/server/drivers/claude-login-auth.js +102 -9
  28. package/dist-server/server/drivers/claude.js +298 -21
  29. package/dist-server/server/drivers/codex.js +13 -4
  30. package/dist-server/server/drivers/pi.js +1 -1
  31. package/dist-server/server/engine-install.js +137 -0
  32. package/dist-server/server/env-path.js +12 -0
  33. package/dist-server/server/harness/bus.js +1 -1
  34. package/dist-server/server/harness/registry.js +31 -6
  35. package/dist-server/server/index.js +860 -215
  36. package/dist-server/server/local-computer.js +1 -1
  37. package/dist-server/server/mcp-gate-config.js +56 -0
  38. package/dist-server/server/mcp-gate.js +227 -0
  39. package/dist-server/server/mcp-trim.js +182 -0
  40. package/dist-server/server/memory-journal.js +440 -0
  41. package/dist-server/server/memory-store.js +308 -0
  42. package/dist-server/server/message-db.js +90 -3
  43. package/dist-server/server/package-export.js +25 -0
  44. package/dist-server/server/peer-approval.js +1 -1
  45. package/dist-server/server/peer-provenance.js +6 -2
  46. package/dist-server/server/proxy-paths.js +1 -0
  47. package/dist-server/server/redact.js +15 -0
  48. package/dist-server/server/resume-recovery.js +46 -0
  49. package/dist-server/server/routines.js +116 -55
  50. package/dist-server/server/screen-frame-gate.js +9 -6
  51. package/dist-server/server/skill-library.js +5 -5
  52. package/dist-server/server/steer-queue.js +20 -1
  53. package/dist-server/server/store.js +29 -7
  54. package/dist-server/server/system-prompt.js +14 -1
  55. package/dist-server/server/team-backup.js +53 -4
  56. package/dist-server/server/tool-summary.js +39 -0
  57. package/dist-server/server/tts/speech-text.js +3 -1
  58. package/dist-server/server/turn-context.js +18 -0
  59. package/dist-server/server/workspace.js +321 -22
  60. package/dist-server/shared/approval-mode.js +1 -1
  61. package/dist-server/shared/team-backup.js +14 -2
  62. package/dist-server/vps-container-mcp.js +6 -2
  63. package/package.json +1 -1
  64. package/skills/create-verification-skill/SKILL.md +2 -3
  65. package/dist/assets/index-COp5lMnv.js +0 -279
  66. package/dist/assets/index-Jk7TIeyW.css +0 -1
@@ -135,9 +135,11 @@ function windowsKnownDirs() {
135
135
  var cached = null;
136
136
  var probed = false;
137
137
  var loginShellPath = null;
138
+ var registeredDirs = [];
138
139
  function augmentedPath() {
139
140
  if (cached === null) {
140
141
  cached = mergePaths([
142
+ ...registeredDirs.filter((d) => existsSync(d)),
141
143
  ...process.env.OMB_EXTRA_PATH ? process.env.OMB_EXTRA_PATH.split(delimiter) : [],
142
144
  ...process.env.PATH ? process.env.PATH.split(delimiter) : [],
143
145
  // Keep the last successful login-shell result while a rescan starts a
@@ -22,6 +22,7 @@ var SPAWNED_PROXIES = {
22
22
  agents: resolveProxy("drivers/agents-proxy"),
23
23
  dweb: resolveProxy("drivers/dweb-proxy"),
24
24
  connectors: resolveProxy("connector-proxy"),
25
+ mcpGate: resolveProxy("mcp-gate"),
25
26
  phone: resolveProxy("drivers/phone-proxy"),
26
27
  // Loaded by the external `pi` process via `-e`, not by this server — but
27
28
  // resolved through the same single source of truth so the packaged layout
@@ -216,7 +217,7 @@ function readCuaConnection({
216
217
  candidates.push(join2(home, "Library", "Application Support", directory, "cua-connection.json"));
217
218
  }
218
219
  }
219
- for (const file of [...new Set(candidates)]) {
220
+ for (const file of new Set(candidates)) {
220
221
  try {
221
222
  const raw = JSON.parse(readFileSync(file, "utf8"));
222
223
  if (platform === "linux") {
@@ -0,0 +1,543 @@
1
+ // server/mcp-gate.ts
2
+ import { spawn as spawn2 } from "node:child_process";
3
+ import { mkdirSync, readdirSync as readdirSync2, rmSync, statSync as statSync2, writeFileSync } from "node:fs";
4
+ import { join as join2 } from "node:path";
5
+ import { createInterface } from "node:readline";
6
+
7
+ // server/env-path.ts
8
+ import { execFile } from "node:child_process";
9
+ import { closeSync, existsSync, openSync, readFileSync, readSync, statSync, readdirSync } from "node:fs";
10
+ import { homedir } from "node:os";
11
+ import { basename, delimiter, dirname, extname, join } from "node:path";
12
+ function nvmBinDirs() {
13
+ try {
14
+ const base = join(homedir(), ".nvm", "versions", "node");
15
+ return readdirSync(base).filter((v) => v.startsWith("v")).sort((a, b) => b.localeCompare(a, void 0, { numeric: true })).map((v) => join(base, v, "bin"));
16
+ } catch {
17
+ return [];
18
+ }
19
+ }
20
+ function knownDirs() {
21
+ const home = homedir();
22
+ return [
23
+ join(home, ".local", "bin"),
24
+ // claude installer default
25
+ join(home, ".npm-global", "bin"),
26
+ // npm prefix ~/.npm-global (claude, opencode)
27
+ join(home, ".kimi-code", "bin"),
28
+ // kimi-code installer
29
+ join(home, ".grok", "bin"),
30
+ // x.ai installer
31
+ join(home, ".opencode", "bin"),
32
+ // opencode installer
33
+ join(home, ".claude", "local"),
34
+ // claude "local install"
35
+ "/opt/homebrew/bin",
36
+ // brew, Apple silicon
37
+ "/usr/local/bin",
38
+ // brew Intel / classic installs
39
+ join(home, ".volta", "bin"),
40
+ join(home, ".bun", "bin"),
41
+ join(home, ".asdf", "shims"),
42
+ join(home, ".deno", "bin"),
43
+ join(home, "bin"),
44
+ ...nvmBinDirs()
45
+ ];
46
+ }
47
+ function windowsKnownDirs() {
48
+ const home = homedir();
49
+ const appData = process.env.APPDATA ?? join(home, "AppData", "Roaming");
50
+ const localAppData = process.env.LOCALAPPDATA ?? join(home, "AppData", "Local");
51
+ return [
52
+ join(appData, "npm"),
53
+ // npm -g shims: claude, codex
54
+ join(home, ".grok", "bin"),
55
+ // x.ai installer
56
+ join(localAppData, "agy", "bin"),
57
+ // Antigravity installer
58
+ join(home, ".local", "bin"),
59
+ // claude native installer
60
+ join(home, ".claude", "local"),
61
+ join(home, "bin"),
62
+ // Factory droid installer (%USERPROFILE%\bin)
63
+ join(home, ".bun", "bin"),
64
+ join(home, ".deno", "bin"),
65
+ join(home, "go", "bin")
66
+ ];
67
+ }
68
+ var cached = null;
69
+ var probed = false;
70
+ var loginShellPath = null;
71
+ var registeredDirs = [];
72
+ function augmentedPath() {
73
+ if (cached === null) {
74
+ cached = mergePaths([
75
+ ...registeredDirs.filter((d) => existsSync(d)),
76
+ ...process.env.OMB_EXTRA_PATH ? process.env.OMB_EXTRA_PATH.split(delimiter) : [],
77
+ ...process.env.PATH ? process.env.PATH.split(delimiter) : [],
78
+ // Keep the last successful login-shell result while a rescan starts a
79
+ // fresh asynchronous probe. Otherwise resetPathCache() would make
80
+ // rc-only CLIs disappear again for the response that triggered it.
81
+ ...loginShellPath ? loginShellPath.split(delimiter) : [],
82
+ // Both platforms scan their standard install locations; only the
83
+ // login-shell probe below stays unix-only, since Windows has no
84
+ // equivalent rc file to source.
85
+ ...(process.platform === "win32" ? windowsKnownDirs() : knownDirs()).filter((d) => existsSync(d))
86
+ ]);
87
+ }
88
+ if (!probed && !process.env.VITEST && process.platform !== "win32") {
89
+ probed = true;
90
+ probeLoginShellPath();
91
+ }
92
+ return cached;
93
+ }
94
+ function mergePaths(parts) {
95
+ return [...new Set(parts.filter(Boolean))].join(delimiter);
96
+ }
97
+ function probeLoginShellPath() {
98
+ const shell = process.env.SHELL || "/bin/zsh";
99
+ execFile(
100
+ shell,
101
+ ["-l", "-i", "-c", 'printf "__OMB_PATH__%s" "$PATH"'],
102
+ { timeout: 5e3 },
103
+ (err, stdout) => {
104
+ if (err || !stdout) return;
105
+ const m = /__OMB_PATH__([^\n]*)/.exec(stdout);
106
+ if (!m || !m[1]) return;
107
+ loginShellPath = m[1];
108
+ cached = mergePaths([...(cached ?? "").split(delimiter), ...m[1].split(delimiter)]);
109
+ }
110
+ );
111
+ }
112
+ function splitCliString(cli) {
113
+ const out = [];
114
+ let cur = "";
115
+ let quote = null;
116
+ for (const ch of cli.trim()) {
117
+ if (quote) {
118
+ if (ch === quote) quote = null;
119
+ else cur += ch;
120
+ } else if (ch === '"' || ch === "'") {
121
+ quote = ch;
122
+ } else if (/\s/.test(ch)) {
123
+ if (cur) out.push(cur);
124
+ cur = "";
125
+ } else {
126
+ cur += ch;
127
+ }
128
+ }
129
+ if (cur) out.push(cur);
130
+ return out;
131
+ }
132
+ function isFile(p) {
133
+ try {
134
+ return statSync(p, { throwIfNoEntry: false })?.isFile() ?? false;
135
+ } catch {
136
+ return false;
137
+ }
138
+ }
139
+ function whichWin(cli) {
140
+ const exts = (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean);
141
+ const probe = (base) => {
142
+ const order = extname(base) ? [base, ...exts.map((e) => base + e)] : [...exts.map((e) => base + e), base];
143
+ return order.find(isFile) ?? null;
144
+ };
145
+ if (/[\\/]/.test(cli) || /^[a-zA-Z]:/.test(cli)) return probe(cli);
146
+ for (const dir of augmentedPath().split(delimiter)) {
147
+ if (!dir) continue;
148
+ const hit = probe(join(dir, cli));
149
+ if (hit) return hit;
150
+ }
151
+ return null;
152
+ }
153
+ function nodeExe(near) {
154
+ const local = join(near, "node.exe");
155
+ if (isFile(local)) return local;
156
+ const onPath = whichWin("node.exe");
157
+ if (onPath && extname(onPath).toLowerCase() === ".exe") return onPath;
158
+ return process.versions.electron ? null : process.execPath;
159
+ }
160
+ function parseCmdShim(shim) {
161
+ let text;
162
+ try {
163
+ text = readFileSync(shim, "utf8");
164
+ } catch {
165
+ return null;
166
+ }
167
+ const dir = dirname(shim);
168
+ const targets = [...text.matchAll(/"%~?dp0%?\\?([^"]+)"/g)].map((m) => join(dir, m[1])).filter((p) => isFile(p) && basename(p).toLowerCase() !== "node.exe");
169
+ const script = targets.find((p) => /\.[cm]?js$/i.test(p));
170
+ if (script) {
171
+ const node = nodeExe(dir);
172
+ if (node) return { command: node, args: [script] };
173
+ }
174
+ const exe = targets.find((p) => extname(p).toLowerCase() === ".exe");
175
+ return exe ? { command: exe, args: [] } : null;
176
+ }
177
+ function parseNodeShebang(file) {
178
+ let head = "";
179
+ let fd = null;
180
+ try {
181
+ fd = openSync(file, "r");
182
+ const buf = Buffer.alloc(128);
183
+ const n = readSync(fd, buf, 0, buf.length, 0);
184
+ head = buf.subarray(0, n).toString("utf8").split("\n", 1)[0];
185
+ } catch {
186
+ return null;
187
+ } finally {
188
+ if (fd !== null) {
189
+ try {
190
+ closeSync(fd);
191
+ } catch {
192
+ }
193
+ }
194
+ }
195
+ if (!/^#!.*\bnode(\.exe)?\b/.test(head)) return null;
196
+ const node = nodeExe(dirname(file));
197
+ return node ? { command: node, args: [file] } : null;
198
+ }
199
+ function resolveWord(cli, args) {
200
+ if (process.platform !== "win32") return { command: cli, args };
201
+ const file = whichWin(cli);
202
+ if (!file) return { command: cli, args };
203
+ const ext = extname(file).toLowerCase();
204
+ if (ext === ".cmd" || ext === ".bat") {
205
+ const direct = parseCmdShim(file);
206
+ return direct ? { command: direct.command, args: [...direct.args, ...args] } : { command: file, args };
207
+ }
208
+ if (ext === ".exe" || ext === ".com") return { command: file, args };
209
+ const viaNode = parseNodeShebang(file);
210
+ return viaNode ? { command: viaNode.command, args: [...viaNode.args, ...args] } : { command: file, args };
211
+ }
212
+ function resolveCliSpawn(cli, args) {
213
+ const trimmed = cli.trim();
214
+ if (trimmed.includes(" ") && existsSync(trimmed)) return resolveWord(trimmed, args);
215
+ const split = splitCliString(cli);
216
+ if (split.length > 1 || split[0] !== trimmed) {
217
+ const [head, ...fixed] = split;
218
+ if (!head) return { command: cli, args };
219
+ return resolveWord(head, [...fixed, ...args]);
220
+ }
221
+ return resolveWord(cli, args);
222
+ }
223
+
224
+ // server/mcp-trim.ts
225
+ var DEFAULT_RESULT_BUDGET = 8e3;
226
+ var MIN_BUDGET = 512;
227
+ var fmt = (n) => n.toLocaleString("en-US");
228
+ function serialize(value) {
229
+ try {
230
+ const json = JSON.stringify(value);
231
+ return typeof json === "string" ? json : null;
232
+ } catch {
233
+ return null;
234
+ }
235
+ }
236
+ function fitArray(items, budget) {
237
+ const kept = [];
238
+ let used = 2;
239
+ for (const item of items) {
240
+ const json = serialize(item);
241
+ if (json === null) break;
242
+ const cost = json.length + (kept.length ? 1 : 0);
243
+ if (kept.length && used + cost > budget) break;
244
+ kept.push(item);
245
+ used += cost;
246
+ }
247
+ return kept;
248
+ }
249
+ function arrayFields(value) {
250
+ if (Array.isArray(value)) return [{ key: "", items: value }];
251
+ if (!value || typeof value !== "object") return [];
252
+ return Object.entries(value).filter((entry) => Array.isArray(entry[1])).map(([key, items]) => ({ key, items }));
253
+ }
254
+ function cutAt(text, chars) {
255
+ const cut = text.slice(0, Math.max(0, chars));
256
+ const last = cut.charCodeAt(cut.length - 1);
257
+ return last >= 55296 && last <= 56319 ? cut.slice(0, -1) : cut;
258
+ }
259
+ function marker(input) {
260
+ const counts = input.dropped.filter((d) => d.kept < d.total).map((d) => `${d.key || "items"} ${fmt(d.kept)} of ${fmt(d.total)}`).join(", ");
261
+ const what = input.structural ? counts ? ` Kept ${counts}.` : "" : " Cut mid-text, so what is above may be incomplete JSON.";
262
+ const where = input.spillHint && input.spillPath ? ` The whole result is at ${JSON.stringify(input.spillPath)}; reading it costs as much as not trimming, so narrow the call first.` : " If you need more, call the tool again with a narrower query, a filter, or the next page.";
263
+ return `
264
+
265
+ [OpenMausBot trimmed this tool result to fit the conversation: ${fmt(input.originalChars)} \u2192 ${fmt(input.keptChars)} characters.${what}${where}]`;
266
+ }
267
+ function trimStructured(parsed, budget) {
268
+ const room = Math.max(MIN_BUDGET, budget);
269
+ const current = serialize(parsed);
270
+ if (current !== null && current.length <= room) return null;
271
+ const fields = arrayFields(parsed);
272
+ if (!fields.length) return null;
273
+ const skeleton = Array.isArray(parsed) ? 2 : serialize({ ...parsed, ...Object.fromEntries(fields.map((f) => [f.key, []])) })?.length ?? Infinity;
274
+ const share = Math.floor((room - skeleton) / fields.length);
275
+ if (!Number.isFinite(skeleton) || share <= 0) return null;
276
+ const dropped = [];
277
+ let value;
278
+ if (Array.isArray(parsed)) {
279
+ const kept = fitArray(parsed, share);
280
+ dropped.push({ key: "", kept: kept.length, total: parsed.length });
281
+ value = kept;
282
+ } else {
283
+ const next = { ...parsed };
284
+ for (const field of fields) {
285
+ const kept = fitArray(field.items, share);
286
+ dropped.push({ key: field.key, kept: kept.length, total: field.items.length });
287
+ next[field.key] = kept;
288
+ }
289
+ value = next;
290
+ }
291
+ if (!dropped.some((d) => d.kept < d.total)) return null;
292
+ const json = serialize(value);
293
+ return json !== null && json.length <= room ? { value, dropped } : null;
294
+ }
295
+ function trimResultText(input) {
296
+ const { text } = input;
297
+ const budget = Math.max(MIN_BUDGET, input.budget ?? DEFAULT_RESULT_BUDGET);
298
+ if (text.length <= budget) return { text, trimmed: false, originalChars: text.length };
299
+ const room = Math.max(MIN_BUDGET, budget - 400);
300
+ let parsed;
301
+ try {
302
+ parsed = JSON.parse(text);
303
+ } catch {
304
+ parsed = void 0;
305
+ }
306
+ const fields = parsed === void 0 ? [] : arrayFields(parsed);
307
+ if (fields.length) {
308
+ const skeleton = Array.isArray(parsed) ? 2 : serialize({ ...parsed, ...Object.fromEntries(fields.map((f) => [f.key, []])) })?.length ?? Infinity;
309
+ const share = Math.floor((room - skeleton) / fields.length);
310
+ if (Number.isFinite(skeleton) && share > 0) {
311
+ const dropped = [];
312
+ let value;
313
+ if (Array.isArray(parsed)) {
314
+ const kept = fitArray(parsed, share);
315
+ dropped.push({ key: "", kept: kept.length, total: parsed.length });
316
+ value = kept;
317
+ } else {
318
+ const next = { ...parsed };
319
+ for (const field of fields) {
320
+ const kept = fitArray(field.items, share);
321
+ dropped.push({ key: field.key, kept: kept.length, total: field.items.length });
322
+ next[field.key] = kept;
323
+ }
324
+ value = next;
325
+ }
326
+ const json = serialize(value);
327
+ if (json !== null && json.length <= room) {
328
+ return {
329
+ text: json + marker({ originalChars: text.length, keptChars: json.length, dropped, spillPath: input.spillPath, spillHint: input.spillHint, structural: true }),
330
+ trimmed: true,
331
+ originalChars: text.length
332
+ };
333
+ }
334
+ }
335
+ }
336
+ const cut = cutAt(text, room);
337
+ return {
338
+ text: cut + marker({ originalChars: text.length, keptChars: cut.length, dropped: [], spillPath: input.spillPath, spillHint: input.spillHint, structural: false }),
339
+ trimmed: true,
340
+ originalChars: text.length
341
+ };
342
+ }
343
+
344
+ // server/procs.ts
345
+ import {
346
+ spawn,
347
+ execFile as execFile2
348
+ } from "node:child_process";
349
+ function killCliTree(child2, timeoutMs = 5e3) {
350
+ const pid = child2.pid;
351
+ if (!pid || child2.exitCode !== null || child2.signalCode !== null) return Promise.resolve(true);
352
+ return new Promise((resolve) => {
353
+ let timer;
354
+ const done = (stopped) => {
355
+ clearTimeout(timer);
356
+ child2.off("close", closed);
357
+ resolve(stopped);
358
+ };
359
+ const closed = () => done(true);
360
+ child2.once("close", closed);
361
+ timer = setTimeout(() => done(false), timeoutMs);
362
+ timer.unref?.();
363
+ if (process.platform === "win32") {
364
+ execFile2("taskkill", ["/PID", String(pid), "/T", "/F"], { windowsHide: true }, (err) => {
365
+ if (!err) return;
366
+ try {
367
+ child2.kill();
368
+ } catch {
369
+ }
370
+ });
371
+ return;
372
+ }
373
+ try {
374
+ process.kill(-pid, "SIGTERM");
375
+ } catch {
376
+ try {
377
+ child2.kill("SIGTERM");
378
+ } catch {
379
+ }
380
+ }
381
+ });
382
+ }
383
+
384
+ // server/mcp-gate.ts
385
+ var NAME = process.env.OMB_GATE_NAME || "mcp";
386
+ var SPILL_DIR = process.env.OMB_GATE_SPILL_DIR || "";
387
+ var BUDGET = Number(process.env.OMB_GATE_BUDGET) > 0 ? Number(process.env.OMB_GATE_BUDGET) : DEFAULT_RESULT_BUDGET;
388
+ var SPILL_MAX_AGE_MS = 24 * 60 * 6e4;
389
+ var SPILL_HINT = process.env.OMB_GATE_SPILL_HINT === "1";
390
+ var GATE_ENV_KEYS = ["OMB_GATE_NAME", "OMB_GATE_SPILL_DIR", "OMB_GATE_BUDGET", "OMB_GATE_UPSTREAM", "OMB_GATE_SPILL_HINT"];
391
+ function fail(message) {
392
+ process.stderr.write(`mcp-gate(${NAME}): ${message}
393
+ `);
394
+ process.exit(1);
395
+ }
396
+ function upstreamSpec() {
397
+ let parsed;
398
+ try {
399
+ parsed = JSON.parse(process.env.OMB_GATE_UPSTREAM ?? "");
400
+ } catch {
401
+ fail("OMB_GATE_UPSTREAM is not valid JSON");
402
+ }
403
+ const spec2 = parsed;
404
+ if (!spec2 || typeof spec2 !== "object" || typeof spec2.command !== "string" || !spec2.command) {
405
+ fail("OMB_GATE_UPSTREAM needs a command");
406
+ }
407
+ return spec2;
408
+ }
409
+ function sweepSpill(dir) {
410
+ let entries;
411
+ try {
412
+ entries = readdirSync2(dir);
413
+ } catch {
414
+ return;
415
+ }
416
+ const cutoff = Date.now() - SPILL_MAX_AGE_MS;
417
+ for (const entry of entries) {
418
+ try {
419
+ const path = join2(dir, entry);
420
+ if (statSync2(path).mtimeMs < cutoff) rmSync(path, { force: true });
421
+ } catch {
422
+ }
423
+ }
424
+ }
425
+ var spilled = 0;
426
+ function spill(tool, text) {
427
+ if (!SPILL_DIR) return void 0;
428
+ const safeTool = tool.replace(/[^A-Za-z0-9_.-]/g, "-").slice(0, 60) || "tool";
429
+ const path = join2(SPILL_DIR, `${Date.now()}-${process.pid}-${spilled++}-${safeTool}.json`);
430
+ try {
431
+ mkdirSync(SPILL_DIR, { recursive: true, mode: 448 });
432
+ writeFileSync(path, text, { mode: 384 });
433
+ return path;
434
+ } catch (error) {
435
+ process.stderr.write(`mcp-gate(${NAME}): could not save the full result: ${String(error)}
436
+ `);
437
+ return void 0;
438
+ }
439
+ }
440
+ function trimCallResult(result, tool) {
441
+ const content = result.content;
442
+ if (!Array.isArray(content)) return false;
443
+ const textBlocks = content.filter(
444
+ (block) => Boolean(block) && typeof block === "object" && block.type === "text" && typeof block.text === "string"
445
+ );
446
+ const total = textBlocks.reduce((sum, block) => sum + block.text.length, 0);
447
+ if (!textBlocks.length || total <= BUDGET) return false;
448
+ const share = Math.floor(BUDGET / textBlocks.length);
449
+ const path = spill(tool, textBlocks.map((block) => block.text).join("\n"));
450
+ let trimmed = false;
451
+ for (const block of textBlocks) {
452
+ const outcome = trimResultText({ text: block.text, budget: share, spillPath: path, spillHint: SPILL_HINT, toolName: tool });
453
+ if (!outcome.trimmed) continue;
454
+ block.text = outcome.text;
455
+ trimmed = true;
456
+ }
457
+ if (trimmed && result.structuredContent && typeof result.structuredContent === "object") {
458
+ const structured = trimStructured(result.structuredContent, BUDGET);
459
+ if (structured) result.structuredContent = structured.value;
460
+ }
461
+ return trimmed;
462
+ }
463
+ var spec = upstreamSpec();
464
+ if (SPILL_DIR) sweepSpill(SPILL_DIR);
465
+ var childEnv = { ...process.env, ...spec.env };
466
+ for (const key of GATE_ENV_KEYS) delete childEnv[key];
467
+ var resolved = resolveCliSpawn(spec.command, spec.args ?? []);
468
+ var child = spawn2(resolved.command, resolved.args, {
469
+ stdio: ["pipe", "pipe", "pipe"],
470
+ env: childEnv,
471
+ shell: false,
472
+ // a console app spawned from the desktop shell flashes a window otherwise
473
+ ...process.platform === "win32" ? { windowsHide: true } : {}
474
+ });
475
+ var reaping = false;
476
+ var reapChild = () => {
477
+ if (reaping) return;
478
+ reaping = true;
479
+ void killCliTree(child);
480
+ };
481
+ process.on("SIGTERM", () => {
482
+ reapChild();
483
+ process.exit(0);
484
+ });
485
+ process.on("SIGINT", () => {
486
+ reapChild();
487
+ process.exit(0);
488
+ });
489
+ process.on("exit", reapChild);
490
+ child.on("error", (error) => {
491
+ process.stderr.write(`mcp-gate(${NAME}): could not start ${resolved.command}: ${String(error)}
492
+ `);
493
+ process.exit(1);
494
+ });
495
+ child.stderr.pipe(process.stderr);
496
+ var pending = /* @__PURE__ */ new Map();
497
+ createInterface({ input: process.stdin }).on("line", (line) => {
498
+ if (line.trim()) {
499
+ try {
500
+ const message = JSON.parse(line);
501
+ if (message.method === "tools/call" && message.id !== void 0 && message.id !== null) {
502
+ const params = message.params;
503
+ pending.set(String(message.id), typeof params?.name === "string" ? params.name : "tool");
504
+ }
505
+ } catch {
506
+ }
507
+ }
508
+ child.stdin.write(`${line}
509
+ `);
510
+ });
511
+ process.stdin.on("end", () => child.stdin.end());
512
+ createInterface({ input: child.stdout }).on("line", (line) => {
513
+ if (!line.trim()) return;
514
+ let message;
515
+ try {
516
+ message = JSON.parse(line);
517
+ } catch {
518
+ process.stdout.write(`${line}
519
+ `);
520
+ return;
521
+ }
522
+ const id = message.id === void 0 || message.id === null ? void 0 : String(message.id);
523
+ const tool = id === void 0 ? void 0 : pending.get(id);
524
+ if (id !== void 0) pending.delete(id);
525
+ const result = message.result;
526
+ if (tool && result && typeof result === "object" && !Array.isArray(result)) {
527
+ try {
528
+ if (trimCallResult(result, tool)) {
529
+ process.stderr.write(`mcp-gate(${NAME}): trimmed ${tool} to ${BUDGET} chars
530
+ `);
531
+ }
532
+ } catch (error) {
533
+ process.stderr.write(`mcp-gate(${NAME}): could not trim ${tool}: ${String(error)}
534
+ `);
535
+ process.stdout.write(`${line}
536
+ `);
537
+ return;
538
+ }
539
+ }
540
+ process.stdout.write(`${JSON.stringify(message)}
541
+ `);
542
+ });
543
+ child.on("exit", (code, signal) => process.exit(code ?? (signal ? 1 : 0)));
@@ -375,6 +375,22 @@ var TOOLS = [
375
375
  },
376
376
  annotations: MUTATING
377
377
  },
378
+ {
379
+ name: "edit_bot_message",
380
+ description: "Edit an earlier user message, which forks the conversation from that point and answers again. This is the rewind a person performs in the composer, and the only mapped way to make the harness rebuild a thread's context instead of resuming the provider's own session. Refused while the thread is working.",
381
+ inputSchema: {
382
+ type: "object",
383
+ properties: {
384
+ bot_id: { type: "string", description: "The ID of the bot." },
385
+ message_id: { type: "string", description: "The user message to replace." },
386
+ text: { type: "string", description: "What that message should say instead." },
387
+ task_id: { type: "string", description: "Optional owned task/thread ID. Defaults to the bot's selected task." }
388
+ },
389
+ required: ["bot_id", "message_id", "text"],
390
+ additionalProperties: false
391
+ },
392
+ annotations: MUTATING
393
+ },
378
394
  {
379
395
  name: "list_available_models",
380
396
  description: "List configured model instances and capabilities without exposing local executable paths.",
@@ -1014,6 +1030,28 @@ async function handleToolCall(name, args, baseFetcher = request, signal) {
1014
1030
  });
1015
1031
  return { success: true, bot: projectBot(res.bot) };
1016
1032
  }
1033
+ case "edit_bot_message": {
1034
+ const botId = idArg(args, "bot_id");
1035
+ const messageId = idArg(args, "message_id");
1036
+ const text = String(args.text ?? "").trim();
1037
+ if (!text) throw new Error("text is required");
1038
+ const current = await fleet(fetcher);
1039
+ const bot = records(current.bots).find((candidate) => candidate.id === botId);
1040
+ if (!bot) throw new Error(`Bot not found: ${botId}`);
1041
+ let threadId;
1042
+ if (args.task_id !== void 0) {
1043
+ threadId = idArg(args, "task_id");
1044
+ if (!taskBelongsTo(bot, threadId)) throw new Error(`Task '${threadId}' does not belong to bot '${botId}'`);
1045
+ if (botTaskState(bot, threadId).busy) throw new Error("Interrupt the task or let it finish before editing a message");
1046
+ } else if (bot.busy) {
1047
+ throw new Error("Interrupt the bot or let it finish before editing a message");
1048
+ }
1049
+ const res = await fetcher(
1050
+ `/api/bots/${encodeURIComponent(botId)}/messages/${encodeURIComponent(messageId)}/edit`,
1051
+ { method: "POST", body: JSON.stringify({ text, ...threadId ? { threadId } : {} }) }
1052
+ );
1053
+ return { success: true, botId, message: res?.message ?? null };
1054
+ }
1017
1055
  case "list_available_models": {
1018
1056
  const res = await fetcher("/api/instances");
1019
1057
  return {