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.
- package/dist/assets/{index-BmHupgVJ.js → index-CJtyBonr.js} +1 -1
- package/dist/assets/index-CYLdYtYp.css +1 -0
- package/dist/assets/index-Di4ovwu5.js +289 -0
- package/dist/index.html +2 -2
- package/dist-server/container-mcp.js +6 -2
- package/dist-server/drivers/agents-proxy.js +305 -16
- package/dist-server/drivers/pi-mcp-extension.ts +1 -1
- package/dist-server/index.js +3141 -1124
- package/dist-server/local-computer-proxy.js +2 -0
- package/dist-server/local-computer.js +2 -1
- package/dist-server/mcp-gate.js +543 -0
- package/dist-server/mcp-server.js +38 -0
- package/dist-server/openmausbot.js +772 -260
- package/dist-server/pair-cli.js +772 -260
- package/dist-server/prepare-cloudflared.js +20 -3
- package/dist-server/proxy-paths.js +1 -0
- package/dist-server/server/agent-tool-policy.js +29 -0
- package/dist-server/server/bot-package.js +55 -2
- package/dist-server/server/browser-live.js +12 -7
- package/dist-server/server/config.js +40 -4
- package/dist-server/server/delegations.js +132 -74
- package/dist-server/server/drivers/acp/core.js +2 -0
- package/dist-server/server/drivers/acp/cursor.js +1 -1
- package/dist-server/server/drivers/acp/grok.js +5 -1
- package/dist-server/server/drivers/agents-proxy.js +324 -16
- package/dist-server/server/drivers/boxagent.js +1 -1
- package/dist-server/server/drivers/claude-login-auth.js +102 -9
- package/dist-server/server/drivers/claude.js +298 -21
- package/dist-server/server/drivers/codex.js +13 -4
- package/dist-server/server/drivers/pi.js +1 -1
- package/dist-server/server/engine-install.js +137 -0
- package/dist-server/server/env-path.js +12 -0
- package/dist-server/server/harness/bus.js +1 -1
- package/dist-server/server/harness/registry.js +31 -6
- package/dist-server/server/index.js +860 -215
- package/dist-server/server/local-computer.js +1 -1
- package/dist-server/server/mcp-gate-config.js +56 -0
- package/dist-server/server/mcp-gate.js +227 -0
- package/dist-server/server/mcp-trim.js +182 -0
- package/dist-server/server/memory-journal.js +440 -0
- package/dist-server/server/memory-store.js +308 -0
- package/dist-server/server/message-db.js +90 -3
- package/dist-server/server/package-export.js +25 -0
- package/dist-server/server/peer-approval.js +1 -1
- package/dist-server/server/peer-provenance.js +6 -2
- package/dist-server/server/proxy-paths.js +1 -0
- package/dist-server/server/redact.js +15 -0
- package/dist-server/server/resume-recovery.js +46 -0
- package/dist-server/server/routines.js +116 -55
- package/dist-server/server/screen-frame-gate.js +9 -6
- package/dist-server/server/skill-library.js +5 -5
- package/dist-server/server/steer-queue.js +20 -1
- package/dist-server/server/store.js +29 -7
- package/dist-server/server/system-prompt.js +14 -1
- package/dist-server/server/team-backup.js +53 -4
- package/dist-server/server/tool-summary.js +39 -0
- package/dist-server/server/tts/speech-text.js +3 -1
- package/dist-server/server/turn-context.js +18 -0
- package/dist-server/server/workspace.js +321 -22
- package/dist-server/shared/approval-mode.js +1 -1
- package/dist-server/shared/team-backup.js +14 -2
- package/dist-server/vps-container-mcp.js +6 -2
- package/package.json +1 -1
- package/skills/create-verification-skill/SKILL.md +2 -3
- package/dist/assets/index-COp5lMnv.js +0 -279
- 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
|
|
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 {
|