@moikapy/lich 0.6.0 → 0.7.1
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/CHANGELOG.md +75 -1
- package/README.md +59 -7
- package/dist/{chunk-JC2G3XH2.js → chunk-CVX7LZWC.js} +2 -2
- package/dist/chunk-PZNYVGD4.js +58 -0
- package/dist/chunk-PZNYVGD4.js.map +1 -0
- package/dist/{chunk-7HLVKVIG.js → chunk-WNFBIX4E.js} +1344 -162
- package/dist/chunk-WNFBIX4E.js.map +1 -0
- package/dist/cli.js +246 -18
- package/dist/cli.js.map +1 -1
- package/dist/{gateway-RJFZJEUZ.js → gateway-44QTIJTJ.js} +108 -49
- package/dist/gateway-44QTIJTJ.js.map +1 -0
- package/dist/index.d.ts +288 -154
- package/dist/index.js +5 -3
- package/dist/{tui-LOUJVZ6A.js → tui-MEJGEILU.js} +25 -10
- package/dist/tui-MEJGEILU.js.map +1 -0
- package/docs/.vitepress/config.mts +4 -0
- package/docs/architecture/overview.md +30 -18
- package/docs/architecture/plugins.md +1 -1
- package/docs/architecture/tools.md +36 -4
- package/docs/getting-started.md +7 -6
- package/docs/index.md +5 -4
- package/docs/user-guide/cli.md +21 -8
- package/docs/user-guide/games.md +1 -1
- package/docs/user-guide/gateway.md +70 -16
- package/docs/user-guide/godot.md +3 -1
- package/docs/user-guide/library.md +4 -2
- package/docs/user-guide/plugins.md +2 -2
- package/docs/user-guide/redot.md +93 -0
- package/docs/user-guide/tui.md +2 -2
- package/examples/game_bridge/README.md +2 -0
- package/optional-mcps/godot/manifest.json +6 -0
- package/optional-mcps/redot/manifest.json +18 -0
- package/package.json +2 -1
- package/dist/chunk-6M6OAQGN.js +0 -17
- package/dist/chunk-6M6OAQGN.js.map +0 -1
- package/dist/chunk-7HLVKVIG.js.map +0 -1
- package/dist/gateway-RJFZJEUZ.js.map +0 -1
- package/dist/tui-LOUJVZ6A.js.map +0 -1
- /package/dist/{chunk-JC2G3XH2.js.map → chunk-CVX7LZWC.js.map} +0 -0
|
@@ -4,6 +4,7 @@ import { readdir } from "fs/promises";
|
|
|
4
4
|
import path2 from "path";
|
|
5
5
|
|
|
6
6
|
// src/tools/guard.ts
|
|
7
|
+
import fs from "fs";
|
|
7
8
|
import path from "path";
|
|
8
9
|
|
|
9
10
|
// src/util/json.ts
|
|
@@ -33,14 +34,73 @@ function truncate_text(text, max_chars) {
|
|
|
33
34
|
// src/tools/guard.ts
|
|
34
35
|
var DEFAULT_MAX_OUTPUT_CHARS = 2e4;
|
|
35
36
|
var DEFAULT_TOOL_TIMEOUT_MS = 3e4;
|
|
36
|
-
function
|
|
37
|
+
function is_inside(base, candidate) {
|
|
38
|
+
const relative = path.relative(base, candidate);
|
|
39
|
+
return relative.startsWith("..") === false && path.isAbsolute(relative) === false;
|
|
40
|
+
}
|
|
41
|
+
function deepest_existing(target) {
|
|
42
|
+
let current = target;
|
|
43
|
+
while (fs.existsSync(current) === false) {
|
|
44
|
+
const parent = path.dirname(current);
|
|
45
|
+
if (parent === current) {
|
|
46
|
+
return current;
|
|
47
|
+
}
|
|
48
|
+
current = parent;
|
|
49
|
+
}
|
|
50
|
+
return current;
|
|
51
|
+
}
|
|
52
|
+
function resolve_safe_path(base_dir, target, for_write = false) {
|
|
37
53
|
const base = path.resolve(base_dir);
|
|
38
54
|
const resolved = path.resolve(base, target);
|
|
39
|
-
|
|
40
|
-
|
|
55
|
+
if (is_inside(base, resolved) === false) {
|
|
56
|
+
throw new Error(`path_escape: ${target} escapes ${base_dir}`);
|
|
57
|
+
}
|
|
58
|
+
const real_base = fs.realpathSync(base);
|
|
59
|
+
const existing = deepest_existing(resolved);
|
|
60
|
+
const real_existing = fs.realpathSync(existing);
|
|
61
|
+
const suffix = path.relative(existing, resolved);
|
|
62
|
+
const real_resolved = suffix.length === 0 ? real_existing : path.resolve(real_existing, suffix);
|
|
63
|
+
if (is_inside(real_base, real_resolved) === false) {
|
|
64
|
+
throw new Error(`path_escape: ${target} escapes ${base_dir}`);
|
|
65
|
+
}
|
|
66
|
+
if (for_write === true) {
|
|
67
|
+
reject_symlink_leaf(resolved, target, base_dir);
|
|
68
|
+
}
|
|
69
|
+
return real_resolved;
|
|
70
|
+
}
|
|
71
|
+
function reject_symlink_leaf(resolved, target, base_dir) {
|
|
72
|
+
let info;
|
|
73
|
+
try {
|
|
74
|
+
info = fs.lstatSync(resolved);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
if (is_enoent(err) === true) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
throw err;
|
|
80
|
+
}
|
|
81
|
+
if (info.isSymbolicLink() === true) {
|
|
41
82
|
throw new Error(`path_escape: ${target} escapes ${base_dir}`);
|
|
42
83
|
}
|
|
43
|
-
|
|
84
|
+
}
|
|
85
|
+
function assert_file_tool_access(work_dir, resolved, mode) {
|
|
86
|
+
const base = fs.realpathSync(path.resolve(work_dir));
|
|
87
|
+
const rel = path.relative(base, resolved);
|
|
88
|
+
const parts = rel.split(path.sep).filter((part) => part.length > 0);
|
|
89
|
+
if (parts[0] === ".lich" && parts[1] === "config.json" && parts.length === 2) {
|
|
90
|
+
throw new Error("forbidden_path: .lich/config.json");
|
|
91
|
+
}
|
|
92
|
+
if (mode === "write" && parts[0] === ".lich") {
|
|
93
|
+
const allowed = parts[1] === "skills" || parts[1] === "plugins";
|
|
94
|
+
if (allowed === false) {
|
|
95
|
+
throw new Error("forbidden_path: .lich writes limited to skills/ and plugins/");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (mode === "write") {
|
|
99
|
+
const leaf = path.basename(resolved);
|
|
100
|
+
if (leaf === ".env" || leaf.startsWith(".env.")) {
|
|
101
|
+
throw new Error("forbidden_path: .env*");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
44
104
|
}
|
|
45
105
|
function require_string_arg(args, key) {
|
|
46
106
|
const value = args[key];
|
|
@@ -116,6 +176,233 @@ async function capture_errors(run) {
|
|
|
116
176
|
}
|
|
117
177
|
}
|
|
118
178
|
|
|
179
|
+
// src/tools/url_guard.ts
|
|
180
|
+
import dns from "dns/promises";
|
|
181
|
+
import http from "http";
|
|
182
|
+
import https from "https";
|
|
183
|
+
import net from "net";
|
|
184
|
+
var MAX_REDIRECTS = 5;
|
|
185
|
+
var REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
186
|
+
var fetch_override;
|
|
187
|
+
function private_urls_allowed() {
|
|
188
|
+
return process.env["LICH_ALLOW_PRIVATE_URLS"] === "1";
|
|
189
|
+
}
|
|
190
|
+
function is_blocked_ipv4(address) {
|
|
191
|
+
const parts = address.split(".").map((part) => Number(part));
|
|
192
|
+
if (parts.length !== 4 || parts.some((n) => Number.isFinite(n) === false)) {
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
const [a, b] = parts;
|
|
196
|
+
if (a === 0 || a === 10 || a === 127) {
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
if (a === 169 && b === 254) {
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
if (a === 172 && b >= 16 && b <= 31) {
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
if (a === 192 && b === 168) {
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
if (a === 100 && b >= 64 && b <= 127) {
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
return a >= 224;
|
|
212
|
+
}
|
|
213
|
+
function is_blocked_ipv6(address) {
|
|
214
|
+
const normalized = address.toLowerCase();
|
|
215
|
+
if (normalized === "::" || normalized === "::1") {
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
if (normalized.startsWith("::ffff:")) {
|
|
219
|
+
const mapped = normalized.slice("::ffff:".length);
|
|
220
|
+
return net.isIPv4(mapped) === true ? is_blocked_ipv4(mapped) : true;
|
|
221
|
+
}
|
|
222
|
+
const head = Number.parseInt(normalized.split(":")[0] ?? "", 16);
|
|
223
|
+
if (Number.isFinite(head) === false) {
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
if ((head & 65472) === 65152) {
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
if ((head & 65024) === 64512) {
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
function is_blocked_ip(address) {
|
|
235
|
+
if (net.isIPv4(address) === true) {
|
|
236
|
+
return is_blocked_ipv4(address);
|
|
237
|
+
}
|
|
238
|
+
if (net.isIPv6(address) === true) {
|
|
239
|
+
return is_blocked_ipv6(address);
|
|
240
|
+
}
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
function parse_http_url(raw) {
|
|
244
|
+
let parsed;
|
|
245
|
+
try {
|
|
246
|
+
parsed = new URL(raw);
|
|
247
|
+
} catch {
|
|
248
|
+
throw new Error(`invalid_url: ${raw}`);
|
|
249
|
+
}
|
|
250
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
251
|
+
throw new Error(`invalid_url: unsupported protocol ${parsed.protocol}`);
|
|
252
|
+
}
|
|
253
|
+
return parsed;
|
|
254
|
+
}
|
|
255
|
+
function normalize_hostname(hostname) {
|
|
256
|
+
if (hostname.startsWith("[") === true && hostname.endsWith("]") === true) {
|
|
257
|
+
return hostname.slice(1, -1);
|
|
258
|
+
}
|
|
259
|
+
return hostname;
|
|
260
|
+
}
|
|
261
|
+
async function resolve_public_ip(raw_hostname) {
|
|
262
|
+
const hostname = normalize_hostname(raw_hostname);
|
|
263
|
+
if (private_urls_allowed() === true) {
|
|
264
|
+
if (net.isIP(hostname) !== 0) {
|
|
265
|
+
return hostname;
|
|
266
|
+
}
|
|
267
|
+
const hit = await dns.lookup(hostname);
|
|
268
|
+
return hit.address;
|
|
269
|
+
}
|
|
270
|
+
if (hostname === "localhost" || hostname.endsWith(".localhost") || hostname.endsWith(".local")) {
|
|
271
|
+
throw new Error(`blocked_url: ${hostname}`);
|
|
272
|
+
}
|
|
273
|
+
if (net.isIP(hostname) !== 0) {
|
|
274
|
+
if (is_blocked_ip(hostname) === true) {
|
|
275
|
+
throw new Error(`blocked_url: ${hostname}`);
|
|
276
|
+
}
|
|
277
|
+
return hostname;
|
|
278
|
+
}
|
|
279
|
+
const records = await dns.lookup(hostname, { all: true, verbatim: true });
|
|
280
|
+
if (records.length === 0) {
|
|
281
|
+
throw new Error(`blocked_url: ${hostname}`);
|
|
282
|
+
}
|
|
283
|
+
for (const record of records) {
|
|
284
|
+
if (is_blocked_ip(record.address) === true) {
|
|
285
|
+
throw new Error(`blocked_url: ${hostname}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return records[0]?.address ?? hostname;
|
|
289
|
+
}
|
|
290
|
+
function pinned_lookup(ip) {
|
|
291
|
+
const family = net.isIPv6(ip) === true ? 6 : 4;
|
|
292
|
+
return ((_hostname, options, callback) => {
|
|
293
|
+
const cb = typeof options === "function" ? options : callback;
|
|
294
|
+
const opts = typeof options === "function" ? void 0 : options;
|
|
295
|
+
if (typeof cb !== "function") {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (opts !== void 0 && opts.all === true) {
|
|
299
|
+
cb(null, [{ address: ip, family }]);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
cb(null, ip, family);
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
function request_headers(init) {
|
|
306
|
+
const headers = {};
|
|
307
|
+
new Headers(init?.headers).forEach((value, key) => {
|
|
308
|
+
headers[key] = value;
|
|
309
|
+
});
|
|
310
|
+
return headers;
|
|
311
|
+
}
|
|
312
|
+
function request_body(init) {
|
|
313
|
+
const body = init?.body;
|
|
314
|
+
if (body === void 0 || body === null) {
|
|
315
|
+
return void 0;
|
|
316
|
+
}
|
|
317
|
+
if (typeof body === "string" || body instanceof Uint8Array) {
|
|
318
|
+
return body;
|
|
319
|
+
}
|
|
320
|
+
throw new Error("blocked_url: unsupported_body");
|
|
321
|
+
}
|
|
322
|
+
function pinned_http_fetch(url, ip, init) {
|
|
323
|
+
const lib = url.protocol === "https:" ? https : http;
|
|
324
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
325
|
+
const headers = request_headers(init);
|
|
326
|
+
const body = request_body(init);
|
|
327
|
+
return new Promise((resolve, reject) => {
|
|
328
|
+
const req = lib.request(
|
|
329
|
+
{
|
|
330
|
+
protocol: url.protocol,
|
|
331
|
+
hostname: url.hostname,
|
|
332
|
+
port: url.port.length > 0 ? Number(url.port) : void 0,
|
|
333
|
+
path: `${url.pathname}${url.search}`,
|
|
334
|
+
method,
|
|
335
|
+
headers,
|
|
336
|
+
lookup: pinned_lookup(ip)
|
|
337
|
+
},
|
|
338
|
+
(incoming) => {
|
|
339
|
+
const chunks = [];
|
|
340
|
+
incoming.on("data", (chunk) => {
|
|
341
|
+
chunks.push(chunk);
|
|
342
|
+
});
|
|
343
|
+
incoming.on("end", () => {
|
|
344
|
+
const status = incoming.statusCode ?? 0;
|
|
345
|
+
const response_headers = new Headers();
|
|
346
|
+
for (const [key, value] of Object.entries(incoming.headers)) {
|
|
347
|
+
if (typeof value === "string") {
|
|
348
|
+
response_headers.set(key, value);
|
|
349
|
+
} else if (Array.isArray(value) === true) {
|
|
350
|
+
for (const part of value) {
|
|
351
|
+
response_headers.append(key, part);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
resolve(new Response(Buffer.concat(chunks), { status, headers: response_headers }));
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
);
|
|
359
|
+
req.on("error", reject);
|
|
360
|
+
const signal = init.signal;
|
|
361
|
+
if (signal !== void 0 && signal !== null) {
|
|
362
|
+
if (signal.aborted === true) {
|
|
363
|
+
req.destroy(new Error("aborted"));
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
signal.addEventListener(
|
|
367
|
+
"abort",
|
|
368
|
+
() => {
|
|
369
|
+
req.destroy(new Error("aborted"));
|
|
370
|
+
},
|
|
371
|
+
{ once: true }
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
if (body !== void 0) {
|
|
375
|
+
req.write(body);
|
|
376
|
+
}
|
|
377
|
+
req.end();
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
function redirect_target(current, response) {
|
|
381
|
+
if (REDIRECT_STATUSES.has(response.status) === false) {
|
|
382
|
+
return void 0;
|
|
383
|
+
}
|
|
384
|
+
const location = response.headers.get("location");
|
|
385
|
+
if (location === null || location.length === 0) {
|
|
386
|
+
return void 0;
|
|
387
|
+
}
|
|
388
|
+
return new URL(location, current);
|
|
389
|
+
}
|
|
390
|
+
async function safe_fetch(raw_url, init) {
|
|
391
|
+
let current = parse_http_url(raw_url);
|
|
392
|
+
let request_init = { ...init ?? {}, redirect: "manual" };
|
|
393
|
+
for (let hop = 0; hop < MAX_REDIRECTS; hop += 1) {
|
|
394
|
+
const ip = await resolve_public_ip(current.hostname);
|
|
395
|
+
const response = fetch_override !== void 0 ? await fetch_override(current.href, { ...request_init, redirect: "manual" }) : await pinned_http_fetch(current, ip, request_init);
|
|
396
|
+
const next = redirect_target(current, response);
|
|
397
|
+
if (next === void 0) {
|
|
398
|
+
return response;
|
|
399
|
+
}
|
|
400
|
+
current = next;
|
|
401
|
+
request_init = { ...request_init, method: "GET", body: void 0 };
|
|
402
|
+
}
|
|
403
|
+
throw new Error("blocked_url: too_many_redirects");
|
|
404
|
+
}
|
|
405
|
+
|
|
119
406
|
// src/tools/builtin/fetch_url.ts
|
|
120
407
|
var DEFAULT_MAX_CHARS = 2e4;
|
|
121
408
|
var MAX_MAX_CHARS = 1e5;
|
|
@@ -136,15 +423,7 @@ function clamp_int_arg(args, key, fallback, max) {
|
|
|
136
423
|
return Math.min(max, Math.max(1, Math.floor(optional_number_arg(args, key, fallback))));
|
|
137
424
|
}
|
|
138
425
|
function valid_http_url(raw) {
|
|
139
|
-
|
|
140
|
-
try {
|
|
141
|
-
parsed = new URL(raw);
|
|
142
|
-
} catch {
|
|
143
|
-
throw new Error(`invalid_url: ${raw}`);
|
|
144
|
-
}
|
|
145
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
146
|
-
throw new Error(`invalid_url: unsupported protocol ${parsed.protocol}`);
|
|
147
|
-
}
|
|
426
|
+
parse_http_url(raw);
|
|
148
427
|
}
|
|
149
428
|
function compose_abort_signal(timeout_ms, external) {
|
|
150
429
|
const timeout_signal = AbortSignal.timeout(timeout_ms);
|
|
@@ -155,8 +434,7 @@ async function run_fetch_url(args, external) {
|
|
|
155
434
|
valid_http_url(url);
|
|
156
435
|
const max_chars = clamp_int_arg(args, "max_chars", DEFAULT_MAX_CHARS, MAX_MAX_CHARS);
|
|
157
436
|
const timeout_ms = clamp_int_arg(args, "timeout_ms", DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS);
|
|
158
|
-
const response = await
|
|
159
|
-
redirect: "follow",
|
|
437
|
+
const response = await safe_fetch(url, {
|
|
160
438
|
headers: { "user-agent": USER_AGENT },
|
|
161
439
|
signal: compose_abort_signal(timeout_ms, external)
|
|
162
440
|
});
|
|
@@ -618,10 +896,11 @@ function replacement_for(content, old_string, new_string, replace_all) {
|
|
|
618
896
|
if (replace_all === true) {
|
|
619
897
|
return content.split(old_string).join(new_string);
|
|
620
898
|
}
|
|
621
|
-
return content.replace(old_string, new_string);
|
|
899
|
+
return content.replace(old_string, () => new_string);
|
|
622
900
|
}
|
|
623
901
|
async function apply_edit(work_dir, target, old_string, new_string, replace_all) {
|
|
624
|
-
const file_path = resolve_safe_path(work_dir, target);
|
|
902
|
+
const file_path = resolve_safe_path(work_dir, target, true);
|
|
903
|
+
assert_file_tool_access(work_dir, file_path, "write");
|
|
625
904
|
let content;
|
|
626
905
|
try {
|
|
627
906
|
content = await readFile(file_path, "utf8");
|
|
@@ -650,9 +929,12 @@ var edit_file_tool = {
|
|
|
650
929
|
execute: async (args, context) => capture_errors(async () => {
|
|
651
930
|
const target = require_string_arg(args, "path");
|
|
652
931
|
const old_string = require_string_arg(args, "old_string");
|
|
653
|
-
const
|
|
932
|
+
const new_raw = args["new_string"];
|
|
933
|
+
if (typeof new_raw !== "string") {
|
|
934
|
+
throw new Error("missing_arg: new_string");
|
|
935
|
+
}
|
|
654
936
|
const replace_all = optional_boolean_arg(args, "replace_all", false);
|
|
655
|
-
const result = await apply_edit(context.work_dir, target, old_string,
|
|
937
|
+
const result = await apply_edit(context.work_dir, target, old_string, new_raw, replace_all);
|
|
656
938
|
return { ok: true, output: result.output };
|
|
657
939
|
})
|
|
658
940
|
};
|
|
@@ -812,7 +1094,7 @@ async function scan_dir(frame, matcher) {
|
|
|
812
1094
|
return { files, dirs };
|
|
813
1095
|
}
|
|
814
1096
|
for (const entry of entries) {
|
|
815
|
-
if (SKIP_DIRS.has(entry.name) === true) {
|
|
1097
|
+
if (SKIP_DIRS.has(entry.name) === true || entry.isSymbolicLink() === true) {
|
|
816
1098
|
continue;
|
|
817
1099
|
}
|
|
818
1100
|
const full = path5.join(frame.dir, entry.name);
|
|
@@ -824,13 +1106,25 @@ async function scan_dir(frame, matcher) {
|
|
|
824
1106
|
}
|
|
825
1107
|
return { files, dirs };
|
|
826
1108
|
}
|
|
827
|
-
|
|
828
|
-
const
|
|
829
|
-
const
|
|
1109
|
+
function guard_grep_target(work_dir, absolute) {
|
|
1110
|
+
const relative = path5.relative(work_dir, absolute);
|
|
1111
|
+
const safe = resolve_safe_path(work_dir, relative);
|
|
1112
|
+
assert_file_tool_access(work_dir, safe, "read");
|
|
1113
|
+
return safe;
|
|
1114
|
+
}
|
|
1115
|
+
async function search_file(frame, work_dir, relative_root, regex, collected, max_results) {
|
|
1116
|
+
let safe;
|
|
1117
|
+
try {
|
|
1118
|
+
safe = guard_grep_target(work_dir, frame.dir);
|
|
1119
|
+
} catch {
|
|
1120
|
+
return false;
|
|
1121
|
+
}
|
|
1122
|
+
const size = await file_size(safe);
|
|
1123
|
+
const lines = await read_if_text(safe, size);
|
|
830
1124
|
if (lines === void 0) {
|
|
831
1125
|
return false;
|
|
832
1126
|
}
|
|
833
|
-
const relative = path5.relative(relative_root,
|
|
1127
|
+
const relative = path5.relative(relative_root, safe);
|
|
834
1128
|
for (const hit of match_lines(lines, regex)) {
|
|
835
1129
|
collected.push(`${relative}:${hit.line_no}: ${hit.text}`);
|
|
836
1130
|
if (collected.length >= max_results) {
|
|
@@ -839,7 +1133,7 @@ async function search_file(frame, relative_root, regex, collected, max_results)
|
|
|
839
1133
|
}
|
|
840
1134
|
return false;
|
|
841
1135
|
}
|
|
842
|
-
async function search_tree(root, regex, matcher, max_results) {
|
|
1136
|
+
async function search_tree(root, work_dir, regex, matcher, max_results) {
|
|
843
1137
|
const collected = [];
|
|
844
1138
|
const stack = [{ dir: root, name: root }];
|
|
845
1139
|
while (stack.length > 0 && collected.length < max_results) {
|
|
@@ -849,7 +1143,7 @@ async function search_tree(root, regex, matcher, max_results) {
|
|
|
849
1143
|
}
|
|
850
1144
|
const found = await scan_dir(frame, matcher);
|
|
851
1145
|
for (const file of found.files) {
|
|
852
|
-
const hit_cap = await search_file(file, root, regex, collected, max_results);
|
|
1146
|
+
const hit_cap = await search_file(file, work_dir, root, regex, collected, max_results);
|
|
853
1147
|
if (hit_cap === true) {
|
|
854
1148
|
break;
|
|
855
1149
|
}
|
|
@@ -869,13 +1163,20 @@ function finalize_output(matches, max_results) {
|
|
|
869
1163
|
}
|
|
870
1164
|
return matches.join("\n");
|
|
871
1165
|
}
|
|
872
|
-
function search_file_direct(file_path, regex, collected, max_results) {
|
|
873
|
-
return search_file(
|
|
1166
|
+
function search_file_direct(file_path, work_dir, regex, collected, max_results) {
|
|
1167
|
+
return search_file(
|
|
1168
|
+
{ dir: file_path, name: path5.basename(file_path) },
|
|
1169
|
+
work_dir,
|
|
1170
|
+
path5.dirname(file_path),
|
|
1171
|
+
regex,
|
|
1172
|
+
collected,
|
|
1173
|
+
max_results
|
|
1174
|
+
);
|
|
874
1175
|
}
|
|
875
|
-
async function collect_file_matches(root, regex, matcher, max_results) {
|
|
1176
|
+
async function collect_file_matches(root, work_dir, regex, matcher, max_results) {
|
|
876
1177
|
const collected = [];
|
|
877
1178
|
if (matcher(path5.basename(root)) === true) {
|
|
878
|
-
await search_file_direct(root, regex, collected, max_results);
|
|
1179
|
+
await search_file_direct(root, work_dir, regex, collected, max_results);
|
|
879
1180
|
}
|
|
880
1181
|
return collected;
|
|
881
1182
|
}
|
|
@@ -892,9 +1193,10 @@ async function run_grep(args, work_dir) {
|
|
|
892
1193
|
}
|
|
893
1194
|
const matcher = glob.length > 0 ? glob_matcher(glob) : () => true;
|
|
894
1195
|
const root = resolve_safe_path(work_dir, target);
|
|
1196
|
+
assert_file_tool_access(work_dir, root, "read");
|
|
895
1197
|
const root_stat = await stat(root);
|
|
896
1198
|
const cap = max_results + 1;
|
|
897
|
-
const matches = root_stat.isDirectory() === true ? await search_tree(root, regex, matcher, cap) : await collect_file_matches(root, regex, matcher, cap);
|
|
1199
|
+
const matches = root_stat.isDirectory() === true ? await search_tree(root, work_dir, regex, matcher, cap) : await collect_file_matches(root, work_dir, regex, matcher, cap);
|
|
898
1200
|
return finalize_output(matches, max_results);
|
|
899
1201
|
}
|
|
900
1202
|
var grep_files_tool = {
|
|
@@ -966,7 +1268,6 @@ async function run_http(args, external) {
|
|
|
966
1268
|
const init = {
|
|
967
1269
|
method,
|
|
968
1270
|
headers: read_headers(args),
|
|
969
|
-
redirect: "follow",
|
|
970
1271
|
signal: compose_abort_signal(timeout_ms, external)
|
|
971
1272
|
};
|
|
972
1273
|
if (method !== "GET" && method !== "HEAD") {
|
|
@@ -975,7 +1276,7 @@ async function run_http(args, external) {
|
|
|
975
1276
|
init.body = body;
|
|
976
1277
|
}
|
|
977
1278
|
}
|
|
978
|
-
const response = await
|
|
1279
|
+
const response = await safe_fetch(url, init);
|
|
979
1280
|
const content_type = response.headers.get("content-type") ?? "unknown";
|
|
980
1281
|
const text = await response.text();
|
|
981
1282
|
const sections = [
|
|
@@ -1187,6 +1488,7 @@ function slice_lines2(content, offset, limit) {
|
|
|
1187
1488
|
}
|
|
1188
1489
|
async function read_target(args, work_dir, target) {
|
|
1189
1490
|
const file_path = resolve_safe_path(work_dir, target);
|
|
1491
|
+
assert_file_tool_access(work_dir, file_path, "read");
|
|
1190
1492
|
const content = await readFile3(file_path, "utf8");
|
|
1191
1493
|
const offset = optional_number_arg(args, "offset", 1);
|
|
1192
1494
|
const limit = optional_number_arg(args, "limit", Number.MAX_SAFE_INTEGER);
|
|
@@ -1211,78 +1513,47 @@ var read_file_tool = {
|
|
|
1211
1513
|
};
|
|
1212
1514
|
|
|
1213
1515
|
// src/tools/builtin/run_tests.ts
|
|
1516
|
+
import { spawn as spawn2 } from "child_process";
|
|
1517
|
+
|
|
1518
|
+
// src/tools/builtin/terminal.ts
|
|
1214
1519
|
import { spawn } from "child_process";
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
var
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
description: "Optional test-file filter appended to the test command (e.g. a vitest file filter)"
|
|
1223
|
-
}
|
|
1224
|
-
},
|
|
1225
|
-
additionalProperties: false
|
|
1520
|
+
|
|
1521
|
+
// src/gateway/token_env.ts
|
|
1522
|
+
var DEFAULT_GATEWAY_TOKEN_ENVS = {
|
|
1523
|
+
webhook: "LICH_GATEWAY_TOKEN",
|
|
1524
|
+
telegram: "LICH_TELEGRAM_BOT_TOKEN",
|
|
1525
|
+
discord: "LICH_DISCORD_BOT_TOKEN",
|
|
1526
|
+
twitch: "LICH_TWITCH_OAUTH_TOKEN"
|
|
1226
1527
|
};
|
|
1227
|
-
var
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
const child = spawn("bash", ["-lc", command], {
|
|
1231
|
-
cwd,
|
|
1232
|
-
env: process.env,
|
|
1233
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
1234
|
-
});
|
|
1235
|
-
child.stdout?.on("data", (chunk) => on_chunk("stdout", chunk));
|
|
1236
|
-
child.stderr?.on("data", (chunk) => on_chunk("stderr", chunk));
|
|
1237
|
-
return new Promise((resolve) => {
|
|
1238
|
-
child.on("close", (code) => resolve({ exit_code: code ?? -1 }));
|
|
1239
|
-
child.on("error", () => resolve({ exit_code: -1 }));
|
|
1240
|
-
});
|
|
1528
|
+
var ENV_VAR_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
1529
|
+
function is_env_var_name(value) {
|
|
1530
|
+
return ENV_VAR_NAME.test(value) === true;
|
|
1241
1531
|
}
|
|
1242
|
-
function
|
|
1243
|
-
|
|
1532
|
+
function platform_token_env(config, platform) {
|
|
1533
|
+
const named = config.gateway?.token_envs[platform];
|
|
1534
|
+
if (named !== void 0 && named.length > 0) {
|
|
1535
|
+
return is_env_var_name(named) === true ? named : "";
|
|
1536
|
+
}
|
|
1537
|
+
return DEFAULT_GATEWAY_TOKEN_ENVS[platform] ?? "";
|
|
1244
1538
|
}
|
|
1245
|
-
function
|
|
1246
|
-
const
|
|
1247
|
-
|
|
1539
|
+
function read_platform_token(config, platform) {
|
|
1540
|
+
const key = platform_token_env(config, platform);
|
|
1541
|
+
if (is_env_var_name(key) === false) {
|
|
1542
|
+
return void 0;
|
|
1543
|
+
}
|
|
1544
|
+
const value = process.env[key];
|
|
1545
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
1546
|
+
return void 0;
|
|
1547
|
+
}
|
|
1548
|
+
return value;
|
|
1248
1549
|
}
|
|
1249
|
-
var run_tests_tool = {
|
|
1250
|
-
name: "run_tests",
|
|
1251
|
-
description: "Run the project's test suite via LICH_TEST_COMMAND (default: vitest) in work_dir and report a structured pass/fail result with clamped output.",
|
|
1252
|
-
parameters: parameters12,
|
|
1253
|
-
timeout_ms: 6e5,
|
|
1254
|
-
execute: async (args, context) => capture_errors(async () => {
|
|
1255
|
-
if (busy === true) {
|
|
1256
|
-
return { ok: false, output: "", error: "run_tests_busy" };
|
|
1257
|
-
}
|
|
1258
|
-
busy = true;
|
|
1259
|
-
try {
|
|
1260
|
-
const filter = optional_string_arg(args, "filter", "");
|
|
1261
|
-
const command = build_command(filter === "" ? void 0 : filter, context.env);
|
|
1262
|
-
const streams = { stdout: "", stderr: "" };
|
|
1263
|
-
const on_chunk = (stream, chunk) => {
|
|
1264
|
-
streams[stream] = streams[stream] + chunk.toString("utf8");
|
|
1265
|
-
};
|
|
1266
|
-
const outcome = await run_test_command(command, context.work_dir, on_chunk);
|
|
1267
|
-
const ok = outcome.exit_code === 0;
|
|
1268
|
-
return {
|
|
1269
|
-
ok,
|
|
1270
|
-
output: clamp_output(`${streams.stdout}${streams.stderr}
|
|
1271
|
-
[exit ${outcome.exit_code}]`, MAX_OUTPUT_CHARS),
|
|
1272
|
-
...ok ? {} : { error: "tests_failed" }
|
|
1273
|
-
};
|
|
1274
|
-
} finally {
|
|
1275
|
-
busy = false;
|
|
1276
|
-
}
|
|
1277
|
-
})
|
|
1278
|
-
};
|
|
1279
1550
|
|
|
1280
1551
|
// src/tools/builtin/terminal.ts
|
|
1281
|
-
import { spawn as spawn2 } from "child_process";
|
|
1282
1552
|
var MAX_STREAM_CHARS = 5e4;
|
|
1283
1553
|
var DEFAULT_TIMEOUT_MS3 = 6e4;
|
|
1284
1554
|
var MAX_TIMEOUT_MS3 = 3e5;
|
|
1285
|
-
var
|
|
1555
|
+
var PROVIDER_KEY_ENVS = ["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "LICH_API_KEY"];
|
|
1556
|
+
var parameters12 = {
|
|
1286
1557
|
type: "object",
|
|
1287
1558
|
properties: {
|
|
1288
1559
|
command: { type: "string", description: "Shell command to run via bash -lc" },
|
|
@@ -1303,6 +1574,21 @@ function stream_chunk(current, chunk) {
|
|
|
1303
1574
|
function clamp_timeout(raw) {
|
|
1304
1575
|
return Math.min(MAX_TIMEOUT_MS3, Math.max(1, Math.floor(raw)));
|
|
1305
1576
|
}
|
|
1577
|
+
function scrub_spawn_env(process_env, context_env) {
|
|
1578
|
+
const drop = /* @__PURE__ */ new Set([...Object.values(DEFAULT_GATEWAY_TOKEN_ENVS), ...PROVIDER_KEY_ENVS]);
|
|
1579
|
+
const merged = { ...process_env, ...context_env };
|
|
1580
|
+
const scrubbed = {};
|
|
1581
|
+
for (const [name, value] of Object.entries(merged)) {
|
|
1582
|
+
if (value === void 0) {
|
|
1583
|
+
continue;
|
|
1584
|
+
}
|
|
1585
|
+
if (drop.has(name) === true || SECRET_PATTERN.test(name) === true) {
|
|
1586
|
+
continue;
|
|
1587
|
+
}
|
|
1588
|
+
scrubbed[name] = value;
|
|
1589
|
+
}
|
|
1590
|
+
return scrubbed;
|
|
1591
|
+
}
|
|
1306
1592
|
function wire_kill(child, timeout_signal, external) {
|
|
1307
1593
|
timeout_signal.addEventListener("abort", () => child.kill("SIGKILL"), { once: true });
|
|
1308
1594
|
external?.addEventListener("abort", () => child.kill("SIGKILL"), { once: true });
|
|
@@ -1316,7 +1602,10 @@ function wait_close(child) {
|
|
|
1316
1602
|
async function run_command(command, work_dir, env, timeout_ms, external) {
|
|
1317
1603
|
const stdout = { text: "" };
|
|
1318
1604
|
const stderr = { text: "" };
|
|
1319
|
-
const child =
|
|
1605
|
+
const child = spawn("bash", ["-lc", command], {
|
|
1606
|
+
cwd: work_dir,
|
|
1607
|
+
env: scrub_spawn_env(process.env, env)
|
|
1608
|
+
});
|
|
1320
1609
|
child.stdout.on("data", (chunk) => stream_chunk(stdout, chunk));
|
|
1321
1610
|
child.stderr.on("data", (chunk) => stream_chunk(stderr, chunk));
|
|
1322
1611
|
const close_promise = wait_close(child);
|
|
@@ -1354,7 +1643,7 @@ function terminal_result(outcome) {
|
|
|
1354
1643
|
var terminal_tool = {
|
|
1355
1644
|
name: "terminal",
|
|
1356
1645
|
description: "Run a shell command with bash -lc and capture combined stdout/stderr plus the exit code.",
|
|
1357
|
-
parameters:
|
|
1646
|
+
parameters: parameters12,
|
|
1358
1647
|
timeout_ms: MAX_TIMEOUT_MS3,
|
|
1359
1648
|
execute: async (args, context) => capture_errors(async () => {
|
|
1360
1649
|
const command = require_string_arg(args, "command");
|
|
@@ -1364,6 +1653,75 @@ var terminal_tool = {
|
|
|
1364
1653
|
})
|
|
1365
1654
|
};
|
|
1366
1655
|
|
|
1656
|
+
// src/tools/builtin/run_tests.ts
|
|
1657
|
+
var MAX_OUTPUT_CHARS = 2e3;
|
|
1658
|
+
var DEFAULT_TEST_COMMAND = "node node_modules/vitest/vitest.mjs run";
|
|
1659
|
+
var parameters13 = {
|
|
1660
|
+
type: "object",
|
|
1661
|
+
properties: {
|
|
1662
|
+
filter: {
|
|
1663
|
+
type: "string",
|
|
1664
|
+
description: "Optional test-file filter appended to the test command (e.g. a vitest file filter)"
|
|
1665
|
+
}
|
|
1666
|
+
},
|
|
1667
|
+
additionalProperties: false
|
|
1668
|
+
};
|
|
1669
|
+
var busy = false;
|
|
1670
|
+
var run_test_command = default_runner;
|
|
1671
|
+
function default_runner(command, cwd, on_chunk) {
|
|
1672
|
+
const child = spawn2("bash", ["-lc", command], {
|
|
1673
|
+
cwd,
|
|
1674
|
+
env: scrub_spawn_env(process.env, {}),
|
|
1675
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1676
|
+
});
|
|
1677
|
+
child.stdout?.on("data", (chunk) => on_chunk("stdout", chunk));
|
|
1678
|
+
child.stderr?.on("data", (chunk) => on_chunk("stderr", chunk));
|
|
1679
|
+
return new Promise((resolve) => {
|
|
1680
|
+
child.on("close", (code) => resolve({ exit_code: code ?? -1 }));
|
|
1681
|
+
child.on("error", () => resolve({ exit_code: -1 }));
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1684
|
+
function shell_quote(token) {
|
|
1685
|
+
return `'${token.replaceAll("'", "'\\''")}'`;
|
|
1686
|
+
}
|
|
1687
|
+
function build_command(filter, env) {
|
|
1688
|
+
const base = optional_string_arg(env, "LICH_TEST_COMMAND", DEFAULT_TEST_COMMAND);
|
|
1689
|
+
return filter === void 0 ? base : `${base} ${shell_quote(filter)}`;
|
|
1690
|
+
}
|
|
1691
|
+
var run_tests_tool = {
|
|
1692
|
+
name: "run_tests",
|
|
1693
|
+
description: "Run the project's test suite via LICH_TEST_COMMAND (default: vitest) in work_dir and report a structured pass/fail result with clamped output.",
|
|
1694
|
+
parameters: parameters13,
|
|
1695
|
+
timeout_ms: 6e5,
|
|
1696
|
+
execute: async (args, context) => capture_errors(async () => {
|
|
1697
|
+
if (busy === true) {
|
|
1698
|
+
return { ok: false, output: "", error: "run_tests_busy" };
|
|
1699
|
+
}
|
|
1700
|
+
busy = true;
|
|
1701
|
+
try {
|
|
1702
|
+
const filter = optional_string_arg(args, "filter", "");
|
|
1703
|
+
if (filter.startsWith("-") === true) {
|
|
1704
|
+
return { ok: false, output: "", error: "invalid_filter: must not start with -" };
|
|
1705
|
+
}
|
|
1706
|
+
const command = build_command(filter === "" ? void 0 : filter, context.env);
|
|
1707
|
+
const streams = { stdout: "", stderr: "" };
|
|
1708
|
+
const on_chunk = (stream, chunk) => {
|
|
1709
|
+
streams[stream] = streams[stream] + chunk.toString("utf8");
|
|
1710
|
+
};
|
|
1711
|
+
const outcome = await run_test_command(command, context.work_dir, on_chunk);
|
|
1712
|
+
const ok = outcome.exit_code === 0;
|
|
1713
|
+
return {
|
|
1714
|
+
ok,
|
|
1715
|
+
output: clamp_output(`${streams.stdout}${streams.stderr}
|
|
1716
|
+
[exit ${outcome.exit_code}]`, MAX_OUTPUT_CHARS),
|
|
1717
|
+
...ok ? {} : { error: "tests_failed" }
|
|
1718
|
+
};
|
|
1719
|
+
} finally {
|
|
1720
|
+
busy = false;
|
|
1721
|
+
}
|
|
1722
|
+
})
|
|
1723
|
+
};
|
|
1724
|
+
|
|
1367
1725
|
// src/tools/builtin/web_search.ts
|
|
1368
1726
|
var DEFAULT_MAX_RESULTS4 = 8;
|
|
1369
1727
|
var MAX_RESULTS = 20;
|
|
@@ -1490,7 +1848,8 @@ function read_content_arg(args) {
|
|
|
1490
1848
|
return content;
|
|
1491
1849
|
}
|
|
1492
1850
|
async function write_target(work_dir, target, content) {
|
|
1493
|
-
const file_path = resolve_safe_path(work_dir, target);
|
|
1851
|
+
const file_path = resolve_safe_path(work_dir, target, true);
|
|
1852
|
+
assert_file_tool_access(work_dir, file_path, "write");
|
|
1494
1853
|
await mkdir(path7.dirname(file_path), { recursive: true });
|
|
1495
1854
|
await writeFile2(file_path, content, "utf8");
|
|
1496
1855
|
return `wrote ${content.length} chars to ${target}`;
|
|
@@ -1882,41 +2241,219 @@ var ProviderError = class extends Error {
|
|
|
1882
2241
|
// src/agent/config.ts
|
|
1883
2242
|
import { z } from "zod";
|
|
1884
2243
|
|
|
1885
|
-
// src/gateway/
|
|
1886
|
-
var
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
2244
|
+
// src/gateway/access.ts
|
|
2245
|
+
var PUBLIC_PLATFORMS = /* @__PURE__ */ new Set(["telegram", "discord", "twitch"]);
|
|
2246
|
+
var DEFAULT_GATEWAY_TOOLS_ENABLED = [
|
|
2247
|
+
"read_file",
|
|
2248
|
+
"list_dir",
|
|
2249
|
+
"grep_files",
|
|
2250
|
+
"fetch_url",
|
|
2251
|
+
"web_search",
|
|
2252
|
+
"docs_read",
|
|
2253
|
+
"docs_search"
|
|
2254
|
+
];
|
|
2255
|
+
function gateway_tools_enabled(config) {
|
|
2256
|
+
return config.gateway?.tools_enabled ?? DEFAULT_GATEWAY_TOOLS_ENABLED;
|
|
1895
2257
|
}
|
|
1896
|
-
function
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
return is_env_var_name(named) === true ? named : "";
|
|
2258
|
+
function is_gateway_sender_allowed(config, platform, chat_id, user_id) {
|
|
2259
|
+
if (PUBLIC_PLATFORMS.has(platform) === false) {
|
|
2260
|
+
return true;
|
|
1900
2261
|
}
|
|
1901
|
-
|
|
2262
|
+
const users = config.gateway?.allowed_users?.[platform] ?? [];
|
|
2263
|
+
const chats = config.gateway?.allowed_chats?.[platform] ?? [];
|
|
2264
|
+
if (users.length === 0 && chats.length === 0) {
|
|
2265
|
+
return false;
|
|
2266
|
+
}
|
|
2267
|
+
const user_ok = users.length === 0 || users.includes(user_id);
|
|
2268
|
+
const chat_ok = chats.length === 0 || chats.includes(chat_id);
|
|
2269
|
+
return user_ok && chat_ok;
|
|
1902
2270
|
}
|
|
1903
|
-
function
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
return void 0;
|
|
2271
|
+
function check_gateway_sender(config, platform, chat_id, user_id) {
|
|
2272
|
+
if (is_gateway_sender_allowed(config, platform, chat_id, user_id) === true) {
|
|
2273
|
+
return true;
|
|
1907
2274
|
}
|
|
1908
|
-
|
|
1909
|
-
|
|
2275
|
+
logger.warn(`gateway denied ${platform} chat=${chat_id} user=${user_id}`);
|
|
2276
|
+
return false;
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
// src/mcp/mcp_pin.ts
|
|
2280
|
+
import path11 from "path";
|
|
2281
|
+
|
|
2282
|
+
// src/mcp/mcp_catalog.ts
|
|
2283
|
+
import { existsSync as existsSync2, readFileSync as readFileSync4, readdirSync as readdirSync3 } from "fs";
|
|
2284
|
+
import path9 from "path";
|
|
2285
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2286
|
+
var cached;
|
|
2287
|
+
function catalog_dir() {
|
|
2288
|
+
let dir = path9.dirname(fileURLToPath2(import.meta.url));
|
|
2289
|
+
for (let hop = 0; hop < 6; hop += 1) {
|
|
2290
|
+
const candidate = path9.join(dir, "optional-mcps");
|
|
2291
|
+
if (existsSync2(path9.join(candidate, "redot", "manifest.json")) === true) {
|
|
2292
|
+
return candidate;
|
|
2293
|
+
}
|
|
2294
|
+
dir = path9.dirname(dir);
|
|
2295
|
+
}
|
|
2296
|
+
throw new Error("mcp catalog not found");
|
|
2297
|
+
}
|
|
2298
|
+
function read_manifest(file) {
|
|
2299
|
+
const parsed = safe_json_parse(readFileSync4(file, "utf8"));
|
|
2300
|
+
if (parsed === void 0 || typeof parsed.name !== "string") {
|
|
2301
|
+
throw new Error("mcp catalog manifest rejected");
|
|
2302
|
+
}
|
|
2303
|
+
return parsed;
|
|
2304
|
+
}
|
|
2305
|
+
function load_catalog() {
|
|
2306
|
+
if (cached !== void 0) {
|
|
2307
|
+
return cached;
|
|
2308
|
+
}
|
|
2309
|
+
const manifests = [];
|
|
2310
|
+
for (const name of readdirSync3(catalog_dir())) {
|
|
2311
|
+
const file = path9.join(catalog_dir(), name, "manifest.json");
|
|
2312
|
+
if (existsSync2(file) === true) {
|
|
2313
|
+
manifests.push(read_manifest(file));
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
cached = manifests;
|
|
2317
|
+
return cached;
|
|
2318
|
+
}
|
|
2319
|
+
function catalog_by_name(name) {
|
|
2320
|
+
return load_catalog().find((entry) => entry.name === name);
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
// src/mcp/mcp_refuse.ts
|
|
2324
|
+
import path10 from "path";
|
|
2325
|
+
var SHELL = /[;&|`$<>]/;
|
|
2326
|
+
var DOWNLOADERS = /* @__PURE__ */ new Set(["npx", "npm", "bunx", "uvx", "curl", "wget"]);
|
|
2327
|
+
function refuse_stdio_command(command) {
|
|
2328
|
+
if (command.includes("://") === true) {
|
|
2329
|
+
return "refused url; only a local binary is allowed";
|
|
2330
|
+
}
|
|
2331
|
+
if (SHELL.test(command) === true) {
|
|
2332
|
+
return "refused shell metacharacters in mcp command";
|
|
2333
|
+
}
|
|
2334
|
+
if (command.length === 0 || command.trim() !== command || /\s/.test(command) === true) {
|
|
2335
|
+
return "refused mcp command";
|
|
2336
|
+
}
|
|
2337
|
+
const base = path10.basename(command);
|
|
2338
|
+
if (DOWNLOADERS.has(base) === true) {
|
|
2339
|
+
return `refused download command '${base}'`;
|
|
2340
|
+
}
|
|
2341
|
+
return void 0;
|
|
2342
|
+
}
|
|
2343
|
+
function refuse_stdio_arg(arg) {
|
|
2344
|
+
if (arg.includes("://") === true) {
|
|
2345
|
+
return "refused url in mcp args";
|
|
2346
|
+
}
|
|
2347
|
+
if (SHELL.test(arg) === true) {
|
|
2348
|
+
return "refused shell metacharacters in mcp args";
|
|
2349
|
+
}
|
|
2350
|
+
return void 0;
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
// src/mcp/mcp_url.ts
|
|
2354
|
+
function refuse_http_url(url) {
|
|
2355
|
+
let parsed;
|
|
2356
|
+
try {
|
|
2357
|
+
parsed = new URL(url);
|
|
2358
|
+
} catch {
|
|
2359
|
+
return "refused mcp url";
|
|
2360
|
+
}
|
|
2361
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
2362
|
+
return "refused mcp url";
|
|
2363
|
+
}
|
|
2364
|
+
if (parsed.username.length > 0 || parsed.password.length > 0) {
|
|
2365
|
+
return "refused mcp url credentials";
|
|
2366
|
+
}
|
|
2367
|
+
const host = parsed.hostname.toLowerCase();
|
|
2368
|
+
if (host !== "127.0.0.1" && host !== "localhost") {
|
|
2369
|
+
return "refused mcp url; loopback only";
|
|
2370
|
+
}
|
|
2371
|
+
return void 0;
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
// src/mcp/mcp_pin.ts
|
|
2375
|
+
function refuse_catalog_stdio(name, command, args) {
|
|
2376
|
+
const pin = catalog_by_name(name);
|
|
2377
|
+
if (pin?.command_basename === void 0) {
|
|
1910
2378
|
return void 0;
|
|
1911
2379
|
}
|
|
1912
|
-
|
|
2380
|
+
if (path11.basename(command) !== pin.command_basename) {
|
|
2381
|
+
return `refused command basename '${path11.basename(command)}'; only '${pin.command_basename}' is allowed`;
|
|
2382
|
+
}
|
|
2383
|
+
const prefix = pin.args_prefix ?? [];
|
|
2384
|
+
if (args.length !== prefix.length + 1) {
|
|
2385
|
+
return `refused ${name} args; expected ${prefix.join(" ")} <project>`;
|
|
2386
|
+
}
|
|
2387
|
+
for (let index = 0; index < prefix.length; index += 1) {
|
|
2388
|
+
if (args[index] !== prefix[index]) {
|
|
2389
|
+
return `refused ${name} args; expected ${prefix.join(" ")} <project>`;
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
const project = args[prefix.length];
|
|
2393
|
+
if (project === void 0 || project.length === 0 || project.includes("://") === true) {
|
|
2394
|
+
return "refused project path; pass a local project directory";
|
|
2395
|
+
}
|
|
2396
|
+
return void 0;
|
|
2397
|
+
}
|
|
2398
|
+
function refuse_arg_list(args) {
|
|
2399
|
+
for (const arg of args) {
|
|
2400
|
+
const refused = refuse_stdio_arg(arg);
|
|
2401
|
+
if (refused !== void 0) {
|
|
2402
|
+
return refused;
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
return void 0;
|
|
2406
|
+
}
|
|
2407
|
+
function refuse_mcp_entry(name, entry) {
|
|
2408
|
+
if (typeof entry.url === "string") {
|
|
2409
|
+
return refuse_http_url(entry.url);
|
|
2410
|
+
}
|
|
2411
|
+
if (typeof entry.command !== "string") {
|
|
2412
|
+
return "refused mcp entry";
|
|
2413
|
+
}
|
|
2414
|
+
const args = entry.args ?? [];
|
|
2415
|
+
return refuse_stdio_command(entry.command) ?? refuse_catalog_stdio(name, entry.command, args) ?? refuse_arg_list(args);
|
|
1913
2416
|
}
|
|
1914
2417
|
|
|
1915
2418
|
// src/agent/config.ts
|
|
2419
|
+
var gateway_allowlist = z.record(z.string(), z.array(z.string())).default({});
|
|
1916
2420
|
var gateway_schema = z.object({
|
|
1917
2421
|
platforms: z.array(z.enum(["webhook", "telegram", "discord", "twitch"])).default([]),
|
|
1918
2422
|
/** Env-var names that hold tokens. Never store the secrets themselves. */
|
|
1919
|
-
token_envs: z.record(z.string(), z.string().regex(ENV_VAR_NAME, "invalid env var name")).default({})
|
|
2423
|
+
token_envs: z.record(z.string(), z.string().regex(ENV_VAR_NAME, "invalid env var name")).default({}),
|
|
2424
|
+
/** Per-platform user ids allowed to talk to the bot (default-deny on public platforms). */
|
|
2425
|
+
allowed_users: gateway_allowlist,
|
|
2426
|
+
/** Per-platform chat/channel ids allowed (default-deny on public platforms). */
|
|
2427
|
+
allowed_chats: gateway_allowlist,
|
|
2428
|
+
/** Tool allowlist for the gateway agent; defaults to a read-only safe subset. */
|
|
2429
|
+
tools_enabled: z.union([z.literal("all"), z.array(z.string())]).default([...DEFAULT_GATEWAY_TOOLS_ENABLED])
|
|
2430
|
+
}).optional();
|
|
2431
|
+
var stdio_mcp_schema = z.object({
|
|
2432
|
+
enabled: z.boolean().default(false),
|
|
2433
|
+
command: z.string().min(1),
|
|
2434
|
+
args: z.array(z.string()).default([]),
|
|
2435
|
+
env: z.record(z.string().regex(ENV_VAR_NAME, "invalid env var name"), z.string()).optional()
|
|
2436
|
+
}).strict();
|
|
2437
|
+
var http_mcp_schema = z.object({
|
|
2438
|
+
enabled: z.boolean().default(false),
|
|
2439
|
+
url: z.string().min(1)
|
|
2440
|
+
}).strict();
|
|
2441
|
+
var mcp_server_schema = z.union([stdio_mcp_schema, http_mcp_schema]);
|
|
2442
|
+
var SERVER_NAME = /^[a-z][a-z0-9_]*$/;
|
|
2443
|
+
var mcp_servers_schema = z.record(z.string(), mcp_server_schema).superRefine((servers, ctx) => {
|
|
2444
|
+
for (const [name, entry] of Object.entries(servers)) {
|
|
2445
|
+
if (SERVER_NAME.test(name) === false) {
|
|
2446
|
+
ctx.addIssue({
|
|
2447
|
+
code: z.ZodIssueCode.custom,
|
|
2448
|
+
path: [name],
|
|
2449
|
+
message: "mcp server name must be snake_case"
|
|
2450
|
+
});
|
|
2451
|
+
}
|
|
2452
|
+
const refused = refuse_mcp_entry(name, entry);
|
|
2453
|
+
if (refused !== void 0) {
|
|
2454
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: [name], message: refused });
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
1920
2457
|
}).optional();
|
|
1921
2458
|
var provider_schema = z.object({
|
|
1922
2459
|
kind: z.enum(["openai_compat", "anthropic", "ollama"]),
|
|
@@ -1947,7 +2484,9 @@ var agent_config_schema = z.object({
|
|
|
1947
2484
|
plugins: z.array(z.string()).default([]),
|
|
1948
2485
|
gateway: gateway_schema,
|
|
1949
2486
|
log_level: z.enum(["debug", "info", "warn", "error"]).default("info"),
|
|
1950
|
-
theme: z.string().min(1).default("lich")
|
|
2487
|
+
theme: z.string().min(1).default("lich"),
|
|
2488
|
+
/** Named MCP servers. Each entry is stdio or loopback http. Default off. */
|
|
2489
|
+
mcp_servers: mcp_servers_schema
|
|
1951
2490
|
}).transform((config) => {
|
|
1952
2491
|
const work_dir = config.work_dir ?? process.cwd();
|
|
1953
2492
|
return {
|
|
@@ -1966,8 +2505,25 @@ function freeze_config(config) {
|
|
|
1966
2505
|
if (config.gateway !== void 0) {
|
|
1967
2506
|
Object.freeze(config.gateway.platforms);
|
|
1968
2507
|
Object.freeze(config.gateway.token_envs);
|
|
2508
|
+
Object.freeze(config.gateway.allowed_users);
|
|
2509
|
+
Object.freeze(config.gateway.allowed_chats);
|
|
2510
|
+
if (Array.isArray(config.gateway.tools_enabled) === true) {
|
|
2511
|
+
Object.freeze(config.gateway.tools_enabled);
|
|
2512
|
+
}
|
|
1969
2513
|
Object.freeze(config.gateway);
|
|
1970
2514
|
}
|
|
2515
|
+
if (config.mcp_servers !== void 0) {
|
|
2516
|
+
for (const entry of Object.values(config.mcp_servers)) {
|
|
2517
|
+
if ("args" in entry) {
|
|
2518
|
+
Object.freeze(entry.args);
|
|
2519
|
+
}
|
|
2520
|
+
if ("env" in entry && entry.env !== void 0) {
|
|
2521
|
+
Object.freeze(entry.env);
|
|
2522
|
+
}
|
|
2523
|
+
Object.freeze(entry);
|
|
2524
|
+
}
|
|
2525
|
+
Object.freeze(config.mcp_servers);
|
|
2526
|
+
}
|
|
1971
2527
|
return config;
|
|
1972
2528
|
}
|
|
1973
2529
|
function parse_agent_config(raw) {
|
|
@@ -2000,15 +2556,547 @@ var AgentEmitter = class {
|
|
|
2000
2556
|
}
|
|
2001
2557
|
};
|
|
2002
2558
|
|
|
2003
|
-
// src/
|
|
2559
|
+
// src/mcp/mcp_http.ts
|
|
2560
|
+
async function post_rpc(url, fetch_fn, body) {
|
|
2561
|
+
const response = await fetch_fn(url, {
|
|
2562
|
+
method: "POST",
|
|
2563
|
+
redirect: "error",
|
|
2564
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
2565
|
+
body: JSON.stringify(body)
|
|
2566
|
+
});
|
|
2567
|
+
const parsed = await response.json();
|
|
2568
|
+
if (parsed.error !== void 0) {
|
|
2569
|
+
const detail = parsed.error.message;
|
|
2570
|
+
throw new Error(typeof detail === "string" && detail.length > 0 ? detail : "mcp error");
|
|
2571
|
+
}
|
|
2572
|
+
return parsed.result;
|
|
2573
|
+
}
|
|
2574
|
+
function http_pipe(url, fetch_fn) {
|
|
2575
|
+
let next_id = 1;
|
|
2576
|
+
return {
|
|
2577
|
+
request(method, params) {
|
|
2578
|
+
const id = next_id;
|
|
2579
|
+
next_id += 1;
|
|
2580
|
+
return post_rpc(url, fetch_fn, { jsonrpc: "2.0", id, method, params });
|
|
2581
|
+
},
|
|
2582
|
+
notify(method) {
|
|
2583
|
+
void fetch_fn(url, {
|
|
2584
|
+
method: "POST",
|
|
2585
|
+
redirect: "error",
|
|
2586
|
+
headers: { "content-type": "application/json" },
|
|
2587
|
+
body: JSON.stringify({ jsonrpc: "2.0", method })
|
|
2588
|
+
}).catch(() => void 0);
|
|
2589
|
+
},
|
|
2590
|
+
close() {
|
|
2591
|
+
return void 0;
|
|
2592
|
+
}
|
|
2593
|
+
};
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
// src/mcp/mcp_plan.ts
|
|
2597
|
+
import { existsSync as existsSync3 } from "fs";
|
|
2598
|
+
import path12 from "path";
|
|
2599
|
+
function find_on_path(command, env_path) {
|
|
2600
|
+
if (env_path === void 0 || env_path.length === 0) {
|
|
2601
|
+
return void 0;
|
|
2602
|
+
}
|
|
2603
|
+
for (const dir of env_path.split(path12.delimiter)) {
|
|
2604
|
+
if (dir.length === 0) {
|
|
2605
|
+
continue;
|
|
2606
|
+
}
|
|
2607
|
+
const candidate = path12.join(dir, command);
|
|
2608
|
+
if (existsSync3(candidate) === true) {
|
|
2609
|
+
return candidate;
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
return void 0;
|
|
2613
|
+
}
|
|
2614
|
+
function locate(command, env_path) {
|
|
2615
|
+
if (command.includes("/") === true || command.includes("\\") === true) {
|
|
2616
|
+
return existsSync3(command) === true ? command : void 0;
|
|
2617
|
+
}
|
|
2618
|
+
return find_on_path(command, env_path);
|
|
2619
|
+
}
|
|
2620
|
+
function plan_stdio(name, command, args, env_path) {
|
|
2621
|
+
const refused = refuse_mcp_entry(name, { command, args });
|
|
2622
|
+
if (refused !== void 0) {
|
|
2623
|
+
return refused;
|
|
2624
|
+
}
|
|
2625
|
+
const binary = locate(command, env_path);
|
|
2626
|
+
if (binary === void 0) {
|
|
2627
|
+
return catalog_by_name(name)?.missing_hint ?? "mcp command not found";
|
|
2628
|
+
}
|
|
2629
|
+
return { command: binary, args };
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
// src/mcp/mcp_pipe.ts
|
|
2633
|
+
var SKIP_LIMIT = 32;
|
|
2634
|
+
function parse_rpc_line(line) {
|
|
2635
|
+
const parsed = safe_json_parse(line);
|
|
2636
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
2637
|
+
return void 0;
|
|
2638
|
+
}
|
|
2639
|
+
return parsed;
|
|
2640
|
+
}
|
|
2641
|
+
async function read_id(child, id) {
|
|
2642
|
+
for (let skipped = 0; skipped < SKIP_LIMIT; skipped += 1) {
|
|
2643
|
+
const line = await child.read_line();
|
|
2644
|
+
const failure = child.failed();
|
|
2645
|
+
if (failure !== void 0) {
|
|
2646
|
+
throw new Error(failure);
|
|
2647
|
+
}
|
|
2648
|
+
if (line === void 0) {
|
|
2649
|
+
throw new Error("mcp closed the pipe");
|
|
2650
|
+
}
|
|
2651
|
+
const parsed = parse_rpc_line(line);
|
|
2652
|
+
if (parsed === void 0 || parsed.id !== id) {
|
|
2653
|
+
continue;
|
|
2654
|
+
}
|
|
2655
|
+
if (parsed.error !== void 0) {
|
|
2656
|
+
const detail = parsed.error.message;
|
|
2657
|
+
throw new Error(typeof detail === "string" && detail.length > 0 ? detail : "mcp error");
|
|
2658
|
+
}
|
|
2659
|
+
return parsed.result;
|
|
2660
|
+
}
|
|
2661
|
+
throw new Error("mcp sent no matching response");
|
|
2662
|
+
}
|
|
2663
|
+
function stdio_pipe(child) {
|
|
2664
|
+
let next_id = 1;
|
|
2665
|
+
return {
|
|
2666
|
+
request(method, params) {
|
|
2667
|
+
const id = next_id;
|
|
2668
|
+
next_id += 1;
|
|
2669
|
+
child.write_line(JSON.stringify({ jsonrpc: "2.0", id, method, params }));
|
|
2670
|
+
return read_id(child, id);
|
|
2671
|
+
},
|
|
2672
|
+
notify(method) {
|
|
2673
|
+
child.write_line(JSON.stringify({ jsonrpc: "2.0", method }));
|
|
2674
|
+
},
|
|
2675
|
+
close() {
|
|
2676
|
+
child.stop();
|
|
2677
|
+
}
|
|
2678
|
+
};
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
// src/mcp/mcp_names.ts
|
|
2682
|
+
function mcp_tool_name(server, tool) {
|
|
2683
|
+
return `mcp_${sanitize(server)}_${sanitize(tool)}`;
|
|
2684
|
+
}
|
|
2685
|
+
function sanitize(value) {
|
|
2686
|
+
return value.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
// src/mcp/mcp_register.ts
|
|
2690
|
+
function name_allowed(enabled, name) {
|
|
2691
|
+
if (enabled === "all") {
|
|
2692
|
+
return true;
|
|
2693
|
+
}
|
|
2694
|
+
return enabled.includes(name);
|
|
2695
|
+
}
|
|
2696
|
+
function run_call(session, wire_name, args, context) {
|
|
2697
|
+
return capture_errors(async () => {
|
|
2698
|
+
if (context.signal?.aborted === true) {
|
|
2699
|
+
throw new Error("cancelled");
|
|
2700
|
+
}
|
|
2701
|
+
return { ok: true, output: await session.call_tool(wire_name, args) };
|
|
2702
|
+
});
|
|
2703
|
+
}
|
|
2704
|
+
function tool_for(registered, wire_name, spec, session) {
|
|
2705
|
+
return {
|
|
2706
|
+
name: registered,
|
|
2707
|
+
description: spec.description,
|
|
2708
|
+
parameters: spec.parameters,
|
|
2709
|
+
timeout_ms: 12e4,
|
|
2710
|
+
execute: (args, context) => run_call(session, wire_name, args, context)
|
|
2711
|
+
};
|
|
2712
|
+
}
|
|
2713
|
+
function register_listed(registry, server, listed, enabled, session) {
|
|
2714
|
+
const excluded = new Set(catalog_by_name(server)?.exclude_tools ?? []);
|
|
2715
|
+
for (const spec of listed) {
|
|
2716
|
+
if (excluded.has(spec.name) === true) {
|
|
2717
|
+
continue;
|
|
2718
|
+
}
|
|
2719
|
+
const registered = mcp_tool_name(server, spec.name);
|
|
2720
|
+
if (name_allowed(enabled, registered) === false || registry.has(registered) === true) {
|
|
2721
|
+
continue;
|
|
2722
|
+
}
|
|
2723
|
+
registry.register(tool_for(registered, spec.name, spec, session));
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2727
|
+
// src/mcp/mcp_handshake.ts
|
|
2728
|
+
var PROTOCOL_VERSION = "2024-11-05";
|
|
2729
|
+
function init_params() {
|
|
2730
|
+
return {
|
|
2731
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
2732
|
+
capabilities: {},
|
|
2733
|
+
clientInfo: { name: "lich", version: "1" }
|
|
2734
|
+
};
|
|
2735
|
+
}
|
|
2736
|
+
function assert_handshake(result) {
|
|
2737
|
+
if (typeof result !== "object" || result === null) {
|
|
2738
|
+
throw new Error("mcp handshake rejected");
|
|
2739
|
+
}
|
|
2740
|
+
const body = result;
|
|
2741
|
+
if (typeof body.protocolVersion !== "string" || body.protocolVersion.length === 0) {
|
|
2742
|
+
throw new Error("mcp handshake rejected");
|
|
2743
|
+
}
|
|
2744
|
+
if (typeof body.serverInfo?.name !== "string" || body.serverInfo.name.length === 0) {
|
|
2745
|
+
throw new Error("mcp handshake rejected");
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2749
|
+
// src/mcp/mcp_content.ts
|
|
2750
|
+
function content_text(result) {
|
|
2751
|
+
if (typeof result !== "object" || result === null) {
|
|
2752
|
+
return "";
|
|
2753
|
+
}
|
|
2754
|
+
const body = result;
|
|
2755
|
+
const parts = [];
|
|
2756
|
+
if (Array.isArray(body.content) === true) {
|
|
2757
|
+
for (const item of body.content) {
|
|
2758
|
+
if (typeof item !== "object" || item === null) {
|
|
2759
|
+
continue;
|
|
2760
|
+
}
|
|
2761
|
+
const chunk = item;
|
|
2762
|
+
if (chunk.type === "text" && typeof chunk.text === "string") {
|
|
2763
|
+
parts.push(chunk.text);
|
|
2764
|
+
}
|
|
2765
|
+
if (chunk.type === "image") {
|
|
2766
|
+
parts.push("[image omitted]");
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
const text = parts.join("\n");
|
|
2771
|
+
if (body.isError === true) {
|
|
2772
|
+
throw new Error(text.length > 0 ? text : "mcp tool failed");
|
|
2773
|
+
}
|
|
2774
|
+
return text;
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2777
|
+
// src/mcp/mcp_result.ts
|
|
2778
|
+
function tool_schema(raw) {
|
|
2779
|
+
if (typeof raw !== "object" || raw === null) {
|
|
2780
|
+
return { type: "object" };
|
|
2781
|
+
}
|
|
2782
|
+
const body = raw;
|
|
2783
|
+
const schema = { type: "object" };
|
|
2784
|
+
if (typeof body.properties === "object" && body.properties !== null) {
|
|
2785
|
+
schema.properties = body.properties;
|
|
2786
|
+
}
|
|
2787
|
+
if (Array.isArray(body.required) === true) {
|
|
2788
|
+
schema.required = body.required.filter((item) => typeof item === "string");
|
|
2789
|
+
}
|
|
2790
|
+
if (typeof body.additionalProperties === "boolean") {
|
|
2791
|
+
schema.additionalProperties = body.additionalProperties;
|
|
2792
|
+
}
|
|
2793
|
+
return schema;
|
|
2794
|
+
}
|
|
2795
|
+
function parse_tools(result) {
|
|
2796
|
+
if (typeof result !== "object" || result === null || Array.isArray(result.tools) === false) {
|
|
2797
|
+
throw new Error("mcp tools/list rejected");
|
|
2798
|
+
}
|
|
2799
|
+
const tools = [];
|
|
2800
|
+
for (const item of result.tools) {
|
|
2801
|
+
if (typeof item !== "object" || item === null) {
|
|
2802
|
+
continue;
|
|
2803
|
+
}
|
|
2804
|
+
const tool = item;
|
|
2805
|
+
if (typeof tool.name !== "string" || tool.name.length === 0) {
|
|
2806
|
+
continue;
|
|
2807
|
+
}
|
|
2808
|
+
tools.push({
|
|
2809
|
+
name: tool.name,
|
|
2810
|
+
description: typeof tool.description === "string" ? tool.description : tool.name,
|
|
2811
|
+
parameters: tool_schema(tool.inputSchema)
|
|
2812
|
+
});
|
|
2813
|
+
}
|
|
2814
|
+
return tools;
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
// src/mcp/mcp_session.ts
|
|
2818
|
+
var McpSession = class {
|
|
2819
|
+
constructor(pipe) {
|
|
2820
|
+
this.pipe = pipe;
|
|
2821
|
+
}
|
|
2822
|
+
pipe;
|
|
2823
|
+
ready_done = false;
|
|
2824
|
+
closed = false;
|
|
2825
|
+
close() {
|
|
2826
|
+
if (this.closed === true) {
|
|
2827
|
+
return;
|
|
2828
|
+
}
|
|
2829
|
+
this.closed = true;
|
|
2830
|
+
this.pipe.close();
|
|
2831
|
+
}
|
|
2832
|
+
async list_tools() {
|
|
2833
|
+
await this.ensure_ready();
|
|
2834
|
+
return parse_tools(await this.pipe.request("tools/list", {}));
|
|
2835
|
+
}
|
|
2836
|
+
async call_tool(name, args) {
|
|
2837
|
+
await this.ensure_ready();
|
|
2838
|
+
return content_text(await this.pipe.request("tools/call", { name, arguments: args }));
|
|
2839
|
+
}
|
|
2840
|
+
async ensure_ready() {
|
|
2841
|
+
if (this.ready_done === true) {
|
|
2842
|
+
return;
|
|
2843
|
+
}
|
|
2844
|
+
try {
|
|
2845
|
+
assert_handshake(await this.pipe.request("initialize", init_params()));
|
|
2846
|
+
this.pipe.notify("notifications/initialized");
|
|
2847
|
+
this.ready_done = true;
|
|
2848
|
+
} catch (error) {
|
|
2849
|
+
this.close();
|
|
2850
|
+
throw error;
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
};
|
|
2854
|
+
|
|
2855
|
+
// src/mcp/mcp_lines.ts
|
|
2856
|
+
function create_line_queue() {
|
|
2857
|
+
const pending = [];
|
|
2858
|
+
const waiters = [];
|
|
2859
|
+
let closed = false;
|
|
2860
|
+
return {
|
|
2861
|
+
push(line) {
|
|
2862
|
+
const waiter = waiters.shift();
|
|
2863
|
+
if (waiter !== void 0) {
|
|
2864
|
+
waiter(line);
|
|
2865
|
+
return;
|
|
2866
|
+
}
|
|
2867
|
+
pending.push(line);
|
|
2868
|
+
},
|
|
2869
|
+
close() {
|
|
2870
|
+
closed = true;
|
|
2871
|
+
for (const waiter of waiters.splice(0)) {
|
|
2872
|
+
waiter(void 0);
|
|
2873
|
+
}
|
|
2874
|
+
},
|
|
2875
|
+
read() {
|
|
2876
|
+
const next = pending.shift();
|
|
2877
|
+
if (next !== void 0) {
|
|
2878
|
+
return Promise.resolve(next);
|
|
2879
|
+
}
|
|
2880
|
+
if (closed === true) {
|
|
2881
|
+
return Promise.resolve(void 0);
|
|
2882
|
+
}
|
|
2883
|
+
return new Promise((resolve) => {
|
|
2884
|
+
waiters.push(resolve);
|
|
2885
|
+
});
|
|
2886
|
+
}
|
|
2887
|
+
};
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
// src/mcp/mcp_child.ts
|
|
2891
|
+
function spawn_failure(code) {
|
|
2892
|
+
if (code === "ENOENT") {
|
|
2893
|
+
return "mcp command not found";
|
|
2894
|
+
}
|
|
2895
|
+
return "mcp spawn failed";
|
|
2896
|
+
}
|
|
2897
|
+
function failed_child(message) {
|
|
2898
|
+
return {
|
|
2899
|
+
write_line() {
|
|
2900
|
+
return void 0;
|
|
2901
|
+
},
|
|
2902
|
+
read_line() {
|
|
2903
|
+
return Promise.resolve(void 0);
|
|
2904
|
+
},
|
|
2905
|
+
stop() {
|
|
2906
|
+
return void 0;
|
|
2907
|
+
},
|
|
2908
|
+
failed() {
|
|
2909
|
+
return message;
|
|
2910
|
+
}
|
|
2911
|
+
};
|
|
2912
|
+
}
|
|
2913
|
+
|
|
2914
|
+
// src/mcp/mcp_stdio_bun.ts
|
|
2915
|
+
async function pump_stdout(stream, queue) {
|
|
2916
|
+
const reader = stream.getReader();
|
|
2917
|
+
const decoder = new TextDecoder();
|
|
2918
|
+
let buffer = "";
|
|
2919
|
+
for (; ; ) {
|
|
2920
|
+
const next = await reader.read();
|
|
2921
|
+
if (next.done === true) {
|
|
2922
|
+
if (buffer.length > 0) {
|
|
2923
|
+
queue.push(buffer);
|
|
2924
|
+
}
|
|
2925
|
+
queue.close();
|
|
2926
|
+
return;
|
|
2927
|
+
}
|
|
2928
|
+
buffer += decoder.decode(next.value, { stream: true });
|
|
2929
|
+
const parts = buffer.split("\n");
|
|
2930
|
+
buffer = parts.pop() ?? "";
|
|
2931
|
+
for (const part of parts) {
|
|
2932
|
+
queue.push(part);
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
async function drain_stderr(stream) {
|
|
2937
|
+
const reader = stream.getReader();
|
|
2938
|
+
while ((await reader.read()).done === false) {
|
|
2939
|
+
continue;
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
function bun_line_child(command, args, env) {
|
|
2943
|
+
const queue = create_line_queue();
|
|
2944
|
+
try {
|
|
2945
|
+
const options = { stdin: "pipe", stdout: "pipe", stderr: "pipe" };
|
|
2946
|
+
const child = env === void 0 ? Bun.spawn([command, ...args], options) : Bun.spawn([command, ...args], { ...options, env });
|
|
2947
|
+
void pump_stdout(child.stdout, queue);
|
|
2948
|
+
void drain_stderr(child.stderr);
|
|
2949
|
+
return {
|
|
2950
|
+
write_line(line) {
|
|
2951
|
+
child.stdin.write(`${line}
|
|
2952
|
+
`);
|
|
2953
|
+
void child.stdin.flush();
|
|
2954
|
+
},
|
|
2955
|
+
read_line() {
|
|
2956
|
+
return queue.read();
|
|
2957
|
+
},
|
|
2958
|
+
stop() {
|
|
2959
|
+
child.kill();
|
|
2960
|
+
},
|
|
2961
|
+
failed() {
|
|
2962
|
+
return void 0;
|
|
2963
|
+
}
|
|
2964
|
+
};
|
|
2965
|
+
} catch (error) {
|
|
2966
|
+
const coded = error;
|
|
2967
|
+
return failed_child(spawn_failure(coded.code));
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
// src/mcp/mcp_stdio_node.ts
|
|
2004
2972
|
import { spawn as spawn3 } from "child_process";
|
|
2973
|
+
import { createInterface } from "readline";
|
|
2974
|
+
function child_env(extra) {
|
|
2975
|
+
if (extra === void 0) {
|
|
2976
|
+
return void 0;
|
|
2977
|
+
}
|
|
2978
|
+
const merged = {};
|
|
2979
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
2980
|
+
if (typeof value === "string") {
|
|
2981
|
+
merged[key] = value;
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
for (const [key, value] of Object.entries(extra)) {
|
|
2985
|
+
merged[key] = value;
|
|
2986
|
+
}
|
|
2987
|
+
return merged;
|
|
2988
|
+
}
|
|
2989
|
+
function node_line_child(command, args, env) {
|
|
2990
|
+
const queue = create_line_queue();
|
|
2991
|
+
let failure;
|
|
2992
|
+
const child = spawn3(command, [...args], { stdio: ["pipe", "pipe", "pipe"], env: child_env(env) });
|
|
2993
|
+
child.unref();
|
|
2994
|
+
const reader = createInterface({ input: child.stdout });
|
|
2995
|
+
reader.on("line", (line) => {
|
|
2996
|
+
queue.push(line);
|
|
2997
|
+
});
|
|
2998
|
+
reader.on("close", () => {
|
|
2999
|
+
queue.close();
|
|
3000
|
+
});
|
|
3001
|
+
child.stderr?.resume();
|
|
3002
|
+
child.on("error", (error) => {
|
|
3003
|
+
failure = spawn_failure(error.code);
|
|
3004
|
+
queue.close();
|
|
3005
|
+
});
|
|
3006
|
+
return {
|
|
3007
|
+
write_line(line) {
|
|
3008
|
+
child.stdin?.write(`${line}
|
|
3009
|
+
`);
|
|
3010
|
+
},
|
|
3011
|
+
read_line() {
|
|
3012
|
+
return queue.read();
|
|
3013
|
+
},
|
|
3014
|
+
stop() {
|
|
3015
|
+
child.kill();
|
|
3016
|
+
},
|
|
3017
|
+
failed() {
|
|
3018
|
+
return failure;
|
|
3019
|
+
}
|
|
3020
|
+
};
|
|
3021
|
+
}
|
|
3022
|
+
|
|
3023
|
+
// src/mcp/mcp_stdio.ts
|
|
3024
|
+
function running_under_bun() {
|
|
3025
|
+
return process.versions.bun !== void 0;
|
|
3026
|
+
}
|
|
3027
|
+
function default_line_spawner(command, args, env) {
|
|
3028
|
+
if (running_under_bun() === true) {
|
|
3029
|
+
return bun_line_child(command, args, env);
|
|
3030
|
+
}
|
|
3031
|
+
return node_line_child(command, args, env);
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3034
|
+
// src/mcp/mcp_attach.ts
|
|
3035
|
+
async function open_and_register(registry, name, enabled, session) {
|
|
3036
|
+
try {
|
|
3037
|
+
register_listed(registry, name, await session.list_tools(), enabled, session);
|
|
3038
|
+
return session;
|
|
3039
|
+
} catch (error) {
|
|
3040
|
+
session.close();
|
|
3041
|
+
const message = error instanceof Error ? error.message : "mcp skipped";
|
|
3042
|
+
logger.warn(`mcp ${name} skipped: ${message}`);
|
|
3043
|
+
return void 0;
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
async function attach_stdio(registry, name, entry, config, runtime) {
|
|
3047
|
+
const planned = plan_stdio(name, entry.command, entry.args, runtime?.env_path ?? process.env.PATH);
|
|
3048
|
+
if (typeof planned === "string") {
|
|
3049
|
+
logger.warn(`mcp ${name} skipped: ${planned}`);
|
|
3050
|
+
return void 0;
|
|
3051
|
+
}
|
|
3052
|
+
const spawn5 = runtime?.spawn ?? default_line_spawner;
|
|
3053
|
+
const session = new McpSession(stdio_pipe(spawn5(planned.command, planned.args, entry.env)));
|
|
3054
|
+
return open_and_register(registry, name, config.tools_enabled, session);
|
|
3055
|
+
}
|
|
3056
|
+
async function attach_http(registry, name, url, config, runtime) {
|
|
3057
|
+
const session = new McpSession(http_pipe(url, runtime?.fetch_fn ?? fetch));
|
|
3058
|
+
return open_and_register(registry, name, config.tools_enabled, session);
|
|
3059
|
+
}
|
|
3060
|
+
|
|
3061
|
+
// src/mcp/mcp_tools.ts
|
|
3062
|
+
function allowlist_wants_mcp(enabled) {
|
|
3063
|
+
if (enabled === "all") {
|
|
3064
|
+
return true;
|
|
3065
|
+
}
|
|
3066
|
+
return enabled.some((name) => name.startsWith("mcp_") === true);
|
|
3067
|
+
}
|
|
3068
|
+
async function attach_enabled_mcp_tools(registry, config, runtime) {
|
|
3069
|
+
const sessions = [];
|
|
3070
|
+
const servers = config.mcp_servers;
|
|
3071
|
+
if (servers === void 0 || allowlist_wants_mcp(config.tools_enabled) === false) {
|
|
3072
|
+
return sessions;
|
|
3073
|
+
}
|
|
3074
|
+
for (const [name, entry] of Object.entries(servers)) {
|
|
3075
|
+
if (entry.enabled !== true) {
|
|
3076
|
+
continue;
|
|
3077
|
+
}
|
|
3078
|
+
try {
|
|
3079
|
+
const session = "url" in entry ? await attach_http(registry, name, entry.url, config, runtime) : await attach_stdio(registry, name, entry, config, runtime);
|
|
3080
|
+
if (session !== void 0) {
|
|
3081
|
+
sessions.push(session);
|
|
3082
|
+
}
|
|
3083
|
+
} catch (error) {
|
|
3084
|
+
const message = error instanceof Error ? error.message : "mcp skipped";
|
|
3085
|
+
logger.warn(`mcp ${name} skipped: ${message}`);
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
return sessions;
|
|
3089
|
+
}
|
|
3090
|
+
|
|
3091
|
+
// src/plugins/builtin/gatekeeper.plugin.ts
|
|
3092
|
+
import { spawn as spawn4 } from "child_process";
|
|
2005
3093
|
import { statSync as statSync2 } from "fs";
|
|
2006
|
-
import
|
|
3094
|
+
import path13 from "path";
|
|
2007
3095
|
var HOOKS_OFF = ["-c", "core.hooksPath=/dev/null"];
|
|
2008
3096
|
var PATHSPEC_MAGIC = /[:*?[]/;
|
|
2009
3097
|
var SECRET_BASENAMES = [".env", ".env.local", "id_rsa"];
|
|
2010
3098
|
function is_secret_path(file_path) {
|
|
2011
|
-
const base =
|
|
3099
|
+
const base = path13.basename(file_path);
|
|
2012
3100
|
if (SECRET_BASENAMES.includes(base) === true) {
|
|
2013
3101
|
return true;
|
|
2014
3102
|
}
|
|
@@ -2035,7 +3123,7 @@ function matches_git_denylist(command) {
|
|
|
2035
3123
|
}
|
|
2036
3124
|
function run_git(args, work_dir, signal) {
|
|
2037
3125
|
return new Promise((resolve) => {
|
|
2038
|
-
const child =
|
|
3126
|
+
const child = spawn4("git", args, { cwd: work_dir, env: process.env });
|
|
2039
3127
|
let settled = false;
|
|
2040
3128
|
let out = "";
|
|
2041
3129
|
const on_abort = () => {
|
|
@@ -2086,7 +3174,7 @@ function invalid_commit_path(raw, work_dir) {
|
|
|
2086
3174
|
return `invalid_path: ${raw}`;
|
|
2087
3175
|
}
|
|
2088
3176
|
const resolved = resolve_safe_path(work_dir, raw);
|
|
2089
|
-
if (resolved ===
|
|
3177
|
+
if (resolved === path13.resolve(work_dir)) {
|
|
2090
3178
|
return `invalid_path: ${raw} resolves to work_dir`;
|
|
2091
3179
|
}
|
|
2092
3180
|
if (is_secret_path(raw) === true) {
|
|
@@ -2204,8 +3292,12 @@ function gatekeeper_hooks(allow_self_commit) {
|
|
|
2204
3292
|
if (info.tool_name === "write_file" || info.tool_name === "edit_file") {
|
|
2205
3293
|
ctx.state?.set("dirty", true);
|
|
2206
3294
|
} else if (info.tool_name === "run_tests") {
|
|
2207
|
-
|
|
2208
|
-
|
|
3295
|
+
const filter = info.args["filter"];
|
|
3296
|
+
const filtered = typeof filter === "string" && filter.length > 0;
|
|
3297
|
+
if (filtered === false) {
|
|
3298
|
+
ctx.state?.set("tests_ok", true);
|
|
3299
|
+
ctx.state?.set("dirty", false);
|
|
3300
|
+
}
|
|
2209
3301
|
} else if (info.tool_name === "git_commit") {
|
|
2210
3302
|
ctx.state?.set("commits", state_count(ctx, "commits") + 1);
|
|
2211
3303
|
}
|
|
@@ -2347,6 +3439,7 @@ var MAX_ERROR_BODY_CHARS = 500;
|
|
|
2347
3439
|
var OVERFLOW_BODY_PATTERN = /context|token|maximum/i;
|
|
2348
3440
|
var OVERLOADED_STATUS = 529;
|
|
2349
3441
|
var UNPARSEABLE_ARGS_NOTE = "[unparseable tool arguments]";
|
|
3442
|
+
var EMPTY_TEXT_PLACEHOLDER = "(empty)";
|
|
2350
3443
|
var DEFAULT_MAX_TOKENS = 4096;
|
|
2351
3444
|
var AnthropicProvider = class {
|
|
2352
3445
|
name;
|
|
@@ -2460,7 +3553,7 @@ function to_anthropic_turns(messages) {
|
|
|
2460
3553
|
}
|
|
2461
3554
|
flush_tool_results(turns, pending_tool_results);
|
|
2462
3555
|
if (message.role === "user") {
|
|
2463
|
-
turns.push({ role: "user", content: [
|
|
3556
|
+
turns.push({ role: "user", content: [text_block(message.content)] });
|
|
2464
3557
|
} else {
|
|
2465
3558
|
turns.push({ role: "assistant", content: assistant_to_blocks(message) });
|
|
2466
3559
|
}
|
|
@@ -2474,11 +3567,14 @@ function flush_tool_results(turns, pending_tool_results) {
|
|
|
2474
3567
|
}
|
|
2475
3568
|
turns.push({ role: "user", content: pending_tool_results.splice(0, pending_tool_results.length) });
|
|
2476
3569
|
}
|
|
3570
|
+
function text_block(text) {
|
|
3571
|
+
return { type: "text", text: text.length > 0 ? text : EMPTY_TEXT_PLACEHOLDER };
|
|
3572
|
+
}
|
|
2477
3573
|
function tool_message_to_block(message) {
|
|
2478
3574
|
const block = {
|
|
2479
3575
|
type: "tool_result",
|
|
2480
3576
|
tool_use_id: message.tool_call_id,
|
|
2481
|
-
content: [
|
|
3577
|
+
content: [text_block(message.content)]
|
|
2482
3578
|
};
|
|
2483
3579
|
if (message.is_error === true) {
|
|
2484
3580
|
return { ...block, is_error: true };
|
|
@@ -2494,7 +3590,7 @@ function assistant_to_blocks(message) {
|
|
|
2494
3590
|
blocks.push({ type: "tool_use", id: tool_call.id, name: tool_call.name, input: tool_call.args });
|
|
2495
3591
|
}
|
|
2496
3592
|
if (blocks.length === 0) {
|
|
2497
|
-
blocks.push(
|
|
3593
|
+
blocks.push(text_block(""));
|
|
2498
3594
|
}
|
|
2499
3595
|
return blocks;
|
|
2500
3596
|
}
|
|
@@ -3338,6 +4434,9 @@ async function chat_with_failover(router, messages, tools, options) {
|
|
|
3338
4434
|
if (result.ok === true) {
|
|
3339
4435
|
return result.value;
|
|
3340
4436
|
}
|
|
4437
|
+
if (is_abort_failure(result.error, options?.signal) === true) {
|
|
4438
|
+
throw result.error;
|
|
4439
|
+
}
|
|
3341
4440
|
last_error = result.error;
|
|
3342
4441
|
log_fail_over(result.error);
|
|
3343
4442
|
}
|
|
@@ -3371,9 +4470,18 @@ async function attempt_provider(provider, messages, tools, options) {
|
|
|
3371
4470
|
})
|
|
3372
4471
|
};
|
|
3373
4472
|
} catch (error) {
|
|
4473
|
+
if (is_abort_failure(error, options?.signal) === true) {
|
|
4474
|
+
throw error;
|
|
4475
|
+
}
|
|
3374
4476
|
return { ok: false, error: to_provider_error(error, provider.name) };
|
|
3375
4477
|
}
|
|
3376
4478
|
}
|
|
4479
|
+
function is_abort_failure(error, signal) {
|
|
4480
|
+
if (signal?.aborted === true) {
|
|
4481
|
+
return true;
|
|
4482
|
+
}
|
|
4483
|
+
return error instanceof Error && error.name === "AbortError";
|
|
4484
|
+
}
|
|
3377
4485
|
function to_provider_error(error, fallback_name) {
|
|
3378
4486
|
if (error instanceof ProviderError) {
|
|
3379
4487
|
return error;
|
|
@@ -3388,7 +4496,7 @@ function to_provider_error(error, fallback_name) {
|
|
|
3388
4496
|
|
|
3389
4497
|
// src/session/store.ts
|
|
3390
4498
|
import { appendFile, mkdir as mkdir2, readFile as readFile4 } from "fs/promises";
|
|
3391
|
-
import
|
|
4499
|
+
import path14 from "path";
|
|
3392
4500
|
var counter_state = { value: 0 };
|
|
3393
4501
|
function slugify_label(label) {
|
|
3394
4502
|
const slug = label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
@@ -3399,7 +4507,7 @@ async function open_session(dir, label) {
|
|
|
3399
4507
|
counter_state.value += 1;
|
|
3400
4508
|
const label_part = label === void 0 ? "" : slugify_label(label);
|
|
3401
4509
|
const id = `${Date.now().toString(36)}-${counter_state.value}${label_part}`;
|
|
3402
|
-
const file_path =
|
|
4510
|
+
const file_path = path14.join(dir, `${id}.jsonl`);
|
|
3403
4511
|
return {
|
|
3404
4512
|
id,
|
|
3405
4513
|
path: file_path,
|
|
@@ -3455,12 +4563,18 @@ function log_compression_failure(error) {
|
|
|
3455
4563
|
}
|
|
3456
4564
|
logger.warn("context compression failed", error);
|
|
3457
4565
|
}
|
|
4566
|
+
function split_keep_recent(non_system, keep_recent) {
|
|
4567
|
+
let cut = Math.max(0, non_system.length - keep_recent);
|
|
4568
|
+
while (cut > 0 && non_system[cut]?.role === "tool") {
|
|
4569
|
+
cut -= 1;
|
|
4570
|
+
}
|
|
4571
|
+
return { recent: non_system.slice(cut), older: non_system.slice(0, cut) };
|
|
4572
|
+
}
|
|
3458
4573
|
async function compress_messages(deps, messages, params) {
|
|
3459
4574
|
const system_messages = messages.filter((message) => message.role === "system");
|
|
3460
4575
|
const non_system = messages.filter((message) => message.role !== "system");
|
|
3461
4576
|
const keep_recent = Math.max(0, params.keep_recent);
|
|
3462
|
-
const recent = non_system
|
|
3463
|
-
const older = non_system.slice(0, Math.max(0, non_system.length - recent.length));
|
|
4577
|
+
const { recent, older } = split_keep_recent(non_system, keep_recent);
|
|
3464
4578
|
if (older.length === 0) {
|
|
3465
4579
|
return { messages: [...messages], summary_chars: 0 };
|
|
3466
4580
|
}
|
|
@@ -3513,8 +4627,12 @@ function format_tool_result_content(result) {
|
|
|
3513
4627
|
}
|
|
3514
4628
|
return result.output;
|
|
3515
4629
|
}
|
|
3516
|
-
async function run_tool_calls(deps, history, turn, calls, emitter) {
|
|
4630
|
+
async function run_tool_calls(deps, history, turn, calls, emitter, signal) {
|
|
3517
4631
|
for (const call of calls) {
|
|
4632
|
+
if (signal_aborted(signal) === true) {
|
|
4633
|
+
history.push(cancelled_tool_message(call));
|
|
4634
|
+
continue;
|
|
4635
|
+
}
|
|
3518
4636
|
emitter?.emit({ type: "tool_call_start", turn, call });
|
|
3519
4637
|
const result = await deps.tools.execute(call.name, call.args, deps.tool_context);
|
|
3520
4638
|
const tool_message = {
|
|
@@ -3529,6 +4647,16 @@ async function run_tool_calls(deps, history, turn, calls, emitter) {
|
|
|
3529
4647
|
history.push(tool_message);
|
|
3530
4648
|
emitter?.emit({ type: "tool_call_end", turn, call, result });
|
|
3531
4649
|
}
|
|
4650
|
+
return signal_aborted(signal) === true ? "aborted" : "continued";
|
|
4651
|
+
}
|
|
4652
|
+
function cancelled_tool_message(call) {
|
|
4653
|
+
return {
|
|
4654
|
+
role: "tool",
|
|
4655
|
+
tool_call_id: call.id,
|
|
4656
|
+
name: call.name,
|
|
4657
|
+
content: format_tool_result_content({ ok: false, output: "", error: "cancelled" }),
|
|
4658
|
+
is_error: true
|
|
4659
|
+
};
|
|
3532
4660
|
}
|
|
3533
4661
|
async function call_chat(deps, history, params, emitter) {
|
|
3534
4662
|
try {
|
|
@@ -3538,6 +4666,9 @@ async function call_chat(deps, history, params, emitter) {
|
|
|
3538
4666
|
signal: params.signal
|
|
3539
4667
|
});
|
|
3540
4668
|
} catch (error) {
|
|
4669
|
+
if (signal_aborted(params.signal) === true) {
|
|
4670
|
+
throw error;
|
|
4671
|
+
}
|
|
3541
4672
|
if (error instanceof ProviderError) {
|
|
3542
4673
|
logger.error(`provider error kind=${error.kind} provider=${error.provider_name}`, error);
|
|
3543
4674
|
} else {
|
|
@@ -3575,24 +4706,38 @@ async function compress_if_needed(deps, history, params, emitter) {
|
|
|
3575
4706
|
function find_last_assistant(messages) {
|
|
3576
4707
|
return [...messages].reverse().find((message) => message.role === "assistant");
|
|
3577
4708
|
}
|
|
4709
|
+
function signal_aborted(signal) {
|
|
4710
|
+
return signal?.aborted === true;
|
|
4711
|
+
}
|
|
4712
|
+
function aborted_outcome(history, turns_used, emitter) {
|
|
4713
|
+
emitter?.emit({ type: "error", error: new DOMException("agent loop aborted", "AbortError") });
|
|
4714
|
+
return {
|
|
4715
|
+
messages: history,
|
|
4716
|
+
final: find_last_assistant(history),
|
|
4717
|
+
result: void 0,
|
|
4718
|
+
turns_used,
|
|
4719
|
+
stopped_reason: "aborted"
|
|
4720
|
+
};
|
|
4721
|
+
}
|
|
3578
4722
|
async function run_conversation(deps, messages, params) {
|
|
3579
4723
|
const history = seed_system_prompt(messages, params.system_prompt);
|
|
3580
4724
|
const emitter = deps.emitter;
|
|
3581
4725
|
for (const turn of turn_range(params.max_turns)) {
|
|
3582
|
-
if (params.signal
|
|
3583
|
-
|
|
3584
|
-
return {
|
|
3585
|
-
messages: history,
|
|
3586
|
-
final: find_last_assistant(history),
|
|
3587
|
-
result: void 0,
|
|
3588
|
-
turns_used: turn - 1,
|
|
3589
|
-
stopped_reason: "aborted"
|
|
3590
|
-
};
|
|
4726
|
+
if (signal_aborted(params.signal) === true) {
|
|
4727
|
+
return aborted_outcome(history, turn - 1, emitter);
|
|
3591
4728
|
}
|
|
3592
4729
|
emitter?.emit({ type: "turn_start", turn });
|
|
3593
4730
|
await compress_if_needed(deps, history, params, emitter);
|
|
3594
4731
|
emitter?.emit({ type: "llm_start", turn });
|
|
3595
|
-
|
|
4732
|
+
let result;
|
|
4733
|
+
try {
|
|
4734
|
+
result = await call_chat(deps, history, params, emitter);
|
|
4735
|
+
} catch (error) {
|
|
4736
|
+
if (signal_aborted(params.signal) === true) {
|
|
4737
|
+
return aborted_outcome(history, turn - 1, emitter);
|
|
4738
|
+
}
|
|
4739
|
+
throw error;
|
|
4740
|
+
}
|
|
3596
4741
|
emitter?.emit({ type: "llm_end", turn, result });
|
|
3597
4742
|
history.push(result.message);
|
|
3598
4743
|
const calls = result.message.tool_calls ?? [];
|
|
@@ -3601,7 +4746,10 @@ async function run_conversation(deps, messages, params) {
|
|
|
3601
4746
|
emitter?.emit({ type: "turn_end", turn });
|
|
3602
4747
|
return { messages: history, final: result.message, result, turns_used: turn, stopped_reason: "final" };
|
|
3603
4748
|
}
|
|
3604
|
-
await run_tool_calls(deps, history, turn, calls, emitter);
|
|
4749
|
+
const tool_status = await run_tool_calls(deps, history, turn, calls, emitter, params.signal);
|
|
4750
|
+
if (tool_status === "aborted") {
|
|
4751
|
+
return aborted_outcome(history, turn, emitter);
|
|
4752
|
+
}
|
|
3605
4753
|
}
|
|
3606
4754
|
emitter?.emit({ type: "budget_exhausted", turns_used: params.max_turns });
|
|
3607
4755
|
emitter?.emit({ type: "turn_end", turn: params.max_turns });
|
|
@@ -3680,8 +4828,12 @@ var Agent = class {
|
|
|
3680
4828
|
registry;
|
|
3681
4829
|
executor;
|
|
3682
4830
|
hook_runner;
|
|
3683
|
-
|
|
4831
|
+
mcp_runtime;
|
|
4832
|
+
mcp_sessions = [];
|
|
4833
|
+
mcp_attach;
|
|
4834
|
+
constructor(config, plugins = [], runtime) {
|
|
3684
4835
|
this.config = config;
|
|
4836
|
+
this.mcp_runtime = runtime?.mcp;
|
|
3685
4837
|
this.events = new AgentEmitter();
|
|
3686
4838
|
this.router = new ProviderRouter(config.providers);
|
|
3687
4839
|
const base_registry = new ToolRegistry();
|
|
@@ -3705,15 +4857,22 @@ var Agent = class {
|
|
|
3705
4857
|
}
|
|
3706
4858
|
}
|
|
3707
4859
|
async run(options) {
|
|
4860
|
+
await this.attach_mcp_once();
|
|
3708
4861
|
const usage_total = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
|
|
3709
|
-
const
|
|
4862
|
+
const run_events = new AgentEmitter();
|
|
4863
|
+
const stop_forwarding = run_events.on((event) => this.events.emit(event));
|
|
4864
|
+
const stop_collecting = run_events.on(collect_usage(usage_total));
|
|
3710
4865
|
await this.call_plugin_run_start(options.input);
|
|
3711
4866
|
let outcome;
|
|
3712
4867
|
try {
|
|
3713
4868
|
const seed_messages = [...options.history ?? []];
|
|
3714
4869
|
seed_messages.push({ role: "user", content: options.input });
|
|
3715
|
-
const tool_context = {
|
|
3716
|
-
|
|
4870
|
+
const tool_context = {
|
|
4871
|
+
work_dir: this.config.work_dir,
|
|
4872
|
+
env: tool_env(this.config),
|
|
4873
|
+
signal: options.signal
|
|
4874
|
+
};
|
|
4875
|
+
outcome = await run_conversation(this.loop_deps(tool_context, run_events), seed_messages, {
|
|
3717
4876
|
system_prompt: this.config.system_prompt ?? DEFAULT_AGENT_SYSTEM_PROMPT,
|
|
3718
4877
|
max_turns: this.config.max_turns,
|
|
3719
4878
|
temperature: this.config.temperature,
|
|
@@ -3724,21 +4883,36 @@ var Agent = class {
|
|
|
3724
4883
|
});
|
|
3725
4884
|
} finally {
|
|
3726
4885
|
stop_collecting();
|
|
4886
|
+
stop_forwarding();
|
|
3727
4887
|
if (outcome !== void 0) {
|
|
3728
4888
|
await this.call_plugin_run_end(outcome);
|
|
3729
4889
|
}
|
|
3730
4890
|
}
|
|
3731
4891
|
const session_path = await this.persist_session(outcome, options, usage_total);
|
|
3732
|
-
|
|
3733
|
-
|
|
4892
|
+
return { outcome, messages: outcome.messages, usage_total, session_path };
|
|
4893
|
+
}
|
|
4894
|
+
/** Close MCP sessions so stdio children do not keep the event loop alive. */
|
|
4895
|
+
close() {
|
|
4896
|
+
for (const session of this.mcp_sessions) {
|
|
4897
|
+
session.close();
|
|
4898
|
+
}
|
|
4899
|
+
this.mcp_sessions = [];
|
|
4900
|
+
}
|
|
4901
|
+
/** tools/list once, before the model sees definitions. Empty allowlists never connect. */
|
|
4902
|
+
async attach_mcp_once() {
|
|
4903
|
+
this.mcp_attach ??= this.do_attach_mcp();
|
|
4904
|
+
await this.mcp_attach;
|
|
4905
|
+
}
|
|
4906
|
+
async do_attach_mcp() {
|
|
4907
|
+
this.mcp_sessions = await attach_enabled_mcp_tools(this.registry, this.config, this.mcp_runtime);
|
|
3734
4908
|
}
|
|
3735
4909
|
/** Per-run deps: the built-once ToolContext threads through every tool execution. */
|
|
3736
|
-
loop_deps(tool_context) {
|
|
4910
|
+
loop_deps(tool_context, emitter = this.events) {
|
|
3737
4911
|
return {
|
|
3738
4912
|
chat: (messages, tools, chat_options) => this.router.chat_with_failover(messages, tools, chat_options),
|
|
3739
4913
|
tools: this.executor,
|
|
3740
4914
|
definitions: () => this.registry.definitions(),
|
|
3741
|
-
emitter
|
|
4915
|
+
emitter,
|
|
3742
4916
|
tool_context
|
|
3743
4917
|
};
|
|
3744
4918
|
}
|
|
@@ -3793,14 +4967,24 @@ async function create_agent_with_plugins(raw_config) {
|
|
|
3793
4967
|
}
|
|
3794
4968
|
async function run_agent(raw_config, input, options) {
|
|
3795
4969
|
const agent = await create_agent_with_plugins(raw_config);
|
|
3796
|
-
|
|
4970
|
+
try {
|
|
4971
|
+
return await agent.run({ input, signal: options?.signal, label: options?.label });
|
|
4972
|
+
} finally {
|
|
4973
|
+
agent.close();
|
|
4974
|
+
}
|
|
3797
4975
|
}
|
|
3798
4976
|
|
|
3799
4977
|
export {
|
|
3800
4978
|
safe_json_parse,
|
|
3801
4979
|
truncate_text,
|
|
4980
|
+
DEFAULT_GATEWAY_TOKEN_ENVS,
|
|
4981
|
+
is_env_var_name,
|
|
4982
|
+
platform_token_env,
|
|
4983
|
+
read_platform_token,
|
|
3802
4984
|
logger,
|
|
3803
4985
|
register_builtin_tools,
|
|
4986
|
+
catalog_by_name,
|
|
4987
|
+
refuse_mcp_entry,
|
|
3804
4988
|
ToolRegistry,
|
|
3805
4989
|
ToolExecutor,
|
|
3806
4990
|
HookedToolRunner,
|
|
@@ -3808,10 +4992,8 @@ export {
|
|
|
3808
4992
|
plugin_errors_summary,
|
|
3809
4993
|
sleep,
|
|
3810
4994
|
ProviderError,
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
platform_token_env,
|
|
3814
|
-
read_platform_token,
|
|
4995
|
+
gateway_tools_enabled,
|
|
4996
|
+
check_gateway_sender,
|
|
3815
4997
|
parse_agent_config,
|
|
3816
4998
|
AgentEmitter,
|
|
3817
4999
|
Agent,
|
|
@@ -3819,4 +5001,4 @@ export {
|
|
|
3819
5001
|
create_agent_with_plugins,
|
|
3820
5002
|
run_agent
|
|
3821
5003
|
};
|
|
3822
|
-
//# sourceMappingURL=chunk-
|
|
5004
|
+
//# sourceMappingURL=chunk-WNFBIX4E.js.map
|