@24klynx/tools 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,5007 @@
1
+ import { ToolError } from "@lynx/core";
2
+ import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
3
+ import { dirname, join, relative, resolve } from "node:path";
4
+ import { exec, execFile, spawn } from "node:child_process";
5
+ import { promisify } from "node:util";
6
+ import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from "node:fs";
7
+ import { randomUUID } from "node:crypto";
8
+ import { homedir } from "node:os";
9
+ //#region src/availability.ts
10
+ function evalAlways() {
11
+ return { available: true };
12
+ }
13
+ function evalNever(expr) {
14
+ return {
15
+ available: false,
16
+ reason: expr.reason
17
+ };
18
+ }
19
+ function evalEnv(expr, ctx) {
20
+ const e = expr;
21
+ return { available: ctx.env[e.key] === e.equals };
22
+ }
23
+ function evalSetting(expr, ctx) {
24
+ const e = expr;
25
+ return { available: getSetting(ctx.settings, e.path) === e.equals };
26
+ }
27
+ function evalFlag(expr, ctx) {
28
+ return { available: ctx.flags.has(expr.flag) };
29
+ }
30
+ function evalPlatform(expr, ctx) {
31
+ return { available: ctx.platform === expr.platform };
32
+ }
33
+ function evalMcpConnected(expr, ctx) {
34
+ return { available: ctx.connectedMcpServers.has(expr.serverName) };
35
+ }
36
+ function evalHasPlugin(expr, ctx) {
37
+ return { available: ctx.loadedPlugins.has(expr.pluginId) };
38
+ }
39
+ function evalSessionMode(expr, ctx) {
40
+ return { available: ctx.sessionMode === expr.mode };
41
+ }
42
+ function evalAll(expr, ctx) {
43
+ const e = expr;
44
+ for (const sub of e.exprs) {
45
+ const result = evaluateAvailability(sub, ctx);
46
+ if (!result.available) return result;
47
+ }
48
+ return { available: true };
49
+ }
50
+ function evalAny(expr, ctx) {
51
+ const e = expr;
52
+ const reasons = [];
53
+ for (const sub of e.exprs) {
54
+ const result = evaluateAvailability(sub, ctx);
55
+ if (result.available) return { available: true };
56
+ if (result.reason) reasons.push(result.reason);
57
+ }
58
+ return {
59
+ available: false,
60
+ reason: reasons.length > 0 ? reasons.join("; ") : "no condition matched"
61
+ };
62
+ }
63
+ function evalNot(expr, ctx) {
64
+ const inner = evaluateAvailability(expr.expr, ctx);
65
+ return {
66
+ available: !inner.available,
67
+ reason: inner.available ? void 0 : inner.reason
68
+ };
69
+ }
70
+ const LEAF_EVALUATORS = {
71
+ always: evalAlways,
72
+ never: evalNever,
73
+ env: evalEnv,
74
+ setting: evalSetting,
75
+ flag: evalFlag,
76
+ platform: evalPlatform,
77
+ mcp_connected: evalMcpConnected,
78
+ has_plugin: evalHasPlugin,
79
+ session_mode: evalSessionMode,
80
+ all: evalAll,
81
+ any: evalAny,
82
+ not: evalNot
83
+ };
84
+ /**
85
+ * Evaluate an availability expression against the current context.
86
+ *
87
+ * Returns `{ available: true }` or `{ available: false, reason: string }`.
88
+ *
89
+ * ```ts
90
+ * const result = evaluateAvailability(expr, ctx);
91
+ * if (!result.available) {
92
+ * console.log(`Tool hidden because: ${result.reason}`);
93
+ * }
94
+ * ```
95
+ */
96
+ function evaluateAvailability(expr, ctx) {
97
+ const evaluator = LEAF_EVALUATORS[expr.type];
98
+ if (evaluator) return evaluator(expr, ctx);
99
+ return {
100
+ available: false,
101
+ reason: `unknown expression type: ${expr.type}`
102
+ };
103
+ }
104
+ /** Navigate a dot‑path into a settings object. */
105
+ function getSetting(settings, path) {
106
+ const parts = path.split(".");
107
+ let current = settings;
108
+ for (const part of parts) {
109
+ if (current === null || current === void 0) return void 0;
110
+ if (typeof current !== "object") return void 0;
111
+ current = current[part];
112
+ }
113
+ return current;
114
+ }
115
+ //#endregion
116
+ //#region src/registry.ts
117
+ /**
118
+ * ToolRegistry — central registration and lookup for all tools.
119
+ *
120
+ * Tools are registered by plugins during startup and the registry stays
121
+ * immutable for the session lifetime. Lookups are O(1) by name and O(1)
122
+ * by executor key.
123
+ */
124
+ /**
125
+ * Create a new empty ToolRegistry.
126
+ */
127
+ function createToolRegistry() {
128
+ const descriptors = /* @__PURE__ */ new Map();
129
+ const handlers = /* @__PURE__ */ new Map();
130
+ return {
131
+ register(descriptor, handler) {
132
+ if (descriptors.has(descriptor.name)) throw new ToolError(`Tool "${descriptor.name}" is already registered`, {
133
+ recoverable: false,
134
+ retryable: false,
135
+ userVisible: false,
136
+ diagnosticHint: "tool_duplicate_name"
137
+ });
138
+ descriptors.set(descriptor.name, descriptor);
139
+ if (!handlers.has(descriptor.executor)) handlers.set(descriptor.executor, handler);
140
+ },
141
+ resolve(name) {
142
+ return descriptors.get(name);
143
+ },
144
+ resolveExecutor(descriptor) {
145
+ const handler = handlers.get(descriptor.executor);
146
+ if (!handler) throw new ToolError(`No handler registered for executor "${descriptor.executor}" (tool: "${descriptor.name}")`, {
147
+ recoverable: false,
148
+ retryable: false,
149
+ userVisible: false,
150
+ diagnosticHint: "tool_missing_handler"
151
+ });
152
+ return handler;
153
+ },
154
+ listAll() {
155
+ return [...descriptors.values()];
156
+ },
157
+ count() {
158
+ return descriptors.size;
159
+ }
160
+ };
161
+ }
162
+ //#endregion
163
+ //#region src/builtin/files/read-file.ts
164
+ /**
165
+ * read_file — Read file contents from the filesystem.
166
+ *
167
+ * Limits: 2000 lines per call, binary files return MIME + base64.
168
+ */
169
+ const descriptor$33 = {
170
+ name: "read_file",
171
+ description: "读取文件内容。支持 offset/limit 分页。二进制和图片文件以 base64 编码数据加 MIME 类型返回。",
172
+ inputSchema: {
173
+ type: "object",
174
+ properties: {
175
+ file_path: {
176
+ type: "string",
177
+ description: "要读取的文件绝对路径"
178
+ },
179
+ offset: {
180
+ type: "integer",
181
+ description: "起始行号(从 1 开始)"
182
+ },
183
+ limit: {
184
+ type: "integer",
185
+ description: "最多读取行数"
186
+ }
187
+ },
188
+ required: ["file_path"]
189
+ },
190
+ kind: "ReadOnly",
191
+ safety: "Safe",
192
+ availability: { type: "always" },
193
+ executor: "core:read_file",
194
+ owner: "core"
195
+ };
196
+ const MAX_LINES = 2e3;
197
+ const TEXT_EXTENSIONS = new Set([
198
+ ".ts",
199
+ ".tsx",
200
+ ".js",
201
+ ".jsx",
202
+ ".json",
203
+ ".md",
204
+ ".txt",
205
+ ".yml",
206
+ ".yaml",
207
+ ".css",
208
+ ".html",
209
+ ".xml",
210
+ ".svg",
211
+ ".py",
212
+ ".rs",
213
+ ".go",
214
+ ".java",
215
+ ".sh",
216
+ ".bash",
217
+ ".zsh",
218
+ ".fish",
219
+ ".toml",
220
+ ".ini",
221
+ ".cfg",
222
+ ".env",
223
+ ".gitignore",
224
+ ".editorconfig",
225
+ ".c",
226
+ ".h",
227
+ ".cpp",
228
+ ".hpp",
229
+ ".rb",
230
+ ".php",
231
+ ".sql",
232
+ ".graphql",
233
+ ".proto",
234
+ ".vue",
235
+ ".svelte",
236
+ ".astro"
237
+ ]);
238
+ function isTextExtension(filePath) {
239
+ const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
240
+ return TEXT_EXTENSIONS.has(ext);
241
+ }
242
+ const handler$30 = { async handle(invocation, signal) {
243
+ const { file_path, offset, limit } = invocation.payload;
244
+ const startTime = Date.now();
245
+ if (signal.aborted) return {
246
+ success: false,
247
+ content: "操作已取消",
248
+ metadata: { durationMs: Date.now() - startTime }
249
+ };
250
+ try {
251
+ const fileStat = await stat(file_path);
252
+ if (!fileStat.isFile()) return {
253
+ success: false,
254
+ content: `不是文件: ${file_path}`,
255
+ metadata: { durationMs: Date.now() - startTime }
256
+ };
257
+ if (fileStat.size > 10 * 1024 * 1024) return {
258
+ success: false,
259
+ content: `文件过大 (${(fileStat.size / 1024 / 1024).toFixed(1)}MB)。请使用 offset/limit 分段读取。`,
260
+ metadata: { durationMs: Date.now() - startTime }
261
+ };
262
+ if (!isTextExtension(file_path)) {
263
+ const buf = await readFile(file_path);
264
+ const mime = guessMime(file_path);
265
+ const b64 = buf.toString("base64");
266
+ const truncated = b64.length > 5e4;
267
+ return {
268
+ success: true,
269
+ content: `[${mime}] base64 数据 (${buf.length} 字节):\n${truncated ? b64.slice(0, 49997) + "..." : b64}`,
270
+ metadata: {
271
+ durationMs: Date.now() - startTime,
272
+ truncated
273
+ }
274
+ };
275
+ }
276
+ const lines = (await readFile(file_path, "utf-8")).split("\n");
277
+ const startLine = (offset ?? 1) - 1;
278
+ const endLine = limit ? startLine + limit : startLine + MAX_LINES;
279
+ const slice = lines.slice(startLine, endLine);
280
+ const truncated = endLine < lines.length;
281
+ let content = slice.join("\n");
282
+ if (truncated) content += `\n\n[已截断 — 剩余 ${lines.length - endLine} 行。使用 offset=${endLine + 1} 继续读取。]`;
283
+ return {
284
+ success: true,
285
+ content,
286
+ metadata: {
287
+ durationMs: Date.now() - startTime,
288
+ truncated,
289
+ truncatedAt: truncated ? endLine : void 0
290
+ }
291
+ };
292
+ } catch (err) {
293
+ const message = err instanceof Error ? err.message : String(err);
294
+ if (err.code === "ENOENT") return {
295
+ success: false,
296
+ content: `文件未找到: ${file_path}`,
297
+ metadata: { durationMs: Date.now() - startTime }
298
+ };
299
+ if (err.code === "EACCES") return {
300
+ success: false,
301
+ content: `权限不足: ${file_path}`,
302
+ metadata: { durationMs: Date.now() - startTime }
303
+ };
304
+ return {
305
+ success: false,
306
+ content: `读取文件失败: ${message}`,
307
+ metadata: { durationMs: Date.now() - startTime }
308
+ };
309
+ }
310
+ } };
311
+ function guessMime(filePath) {
312
+ return {
313
+ ".png": "image/png",
314
+ ".jpg": "image/jpeg",
315
+ ".jpeg": "image/jpeg",
316
+ ".gif": "image/gif",
317
+ ".svg": "image/svg+xml",
318
+ ".webp": "image/webp",
319
+ ".pdf": "application/pdf",
320
+ ".zip": "application/zip",
321
+ ".gz": "application/gzip",
322
+ ".mp3": "audio/mpeg",
323
+ ".wav": "audio/wav",
324
+ ".mp4": "video/mp4",
325
+ ".woff": "font/woff",
326
+ ".woff2": "font/woff2",
327
+ ".ttf": "font/ttf",
328
+ ".ico": "image/x-icon",
329
+ ".bin": "application/octet-stream"
330
+ }[filePath.slice(filePath.lastIndexOf(".")).toLowerCase()] ?? "application/octet-stream";
331
+ }
332
+ //#endregion
333
+ //#region src/builtin/files/write-file.ts
334
+ /**
335
+ * write_file — Create or overwrite a file with content.
336
+ *
337
+ * Creates parent directories automatically. Overwrites existing files
338
+ * without asking (permission pipeline handles safety).
339
+ */
340
+ const descriptor$32 = {
341
+ name: "write_file",
342
+ description: "创建新文件或完全覆盖已有文件。父目录不存在时会自动创建。",
343
+ inputSchema: {
344
+ type: "object",
345
+ properties: {
346
+ file_path: {
347
+ type: "string",
348
+ description: "要写入的文件绝对路径"
349
+ },
350
+ content: {
351
+ type: "string",
352
+ description: "要写入文件的内容"
353
+ }
354
+ },
355
+ required: ["file_path", "content"]
356
+ },
357
+ kind: "WritesFiles",
358
+ safety: "WorkspaceSafe",
359
+ availability: { type: "always" },
360
+ executor: "core:write_file",
361
+ owner: "core"
362
+ };
363
+ const handler$29 = { async handle(invocation, signal) {
364
+ const { file_path, content } = invocation.payload;
365
+ const startTime = Date.now();
366
+ if (signal.aborted) return {
367
+ success: false,
368
+ content: "操作已取消",
369
+ metadata: { durationMs: Date.now() - startTime }
370
+ };
371
+ try {
372
+ await mkdir(dirname(file_path), { recursive: true });
373
+ await writeFile(file_path, content, "utf-8");
374
+ return {
375
+ success: true,
376
+ content: `已写入 ${content.length} 字节到 ${file_path}`,
377
+ metadata: { durationMs: Date.now() - startTime }
378
+ };
379
+ } catch (err) {
380
+ return {
381
+ success: false,
382
+ content: `写入文件失败: ${err instanceof Error ? err.message : String(err)}`,
383
+ metadata: { durationMs: Date.now() - startTime }
384
+ };
385
+ }
386
+ } };
387
+ //#endregion
388
+ //#region src/builtin/files/edit-file.ts
389
+ /**
390
+ * edit_file — Exact string replacement in an existing file.
391
+ *
392
+ * `old_string` must match EXACTLY once in the file (or use replace_all).
393
+ * This is the primary editing tool — LLMs should use it instead of write_file
394
+ * for targeted changes.
395
+ */
396
+ const descriptor$31 = {
397
+ name: "edit_file",
398
+ description: "在文件中执行精确字符串替换。old_string 必须在文件中精确匹配(包括空格和缩进),除非设置 replace_all 为 true。",
399
+ inputSchema: {
400
+ type: "object",
401
+ properties: {
402
+ file_path: {
403
+ type: "string",
404
+ description: "要编辑的文件绝对路径"
405
+ },
406
+ old_string: {
407
+ type: "string",
408
+ description: "要替换的精确文本 — 必须唯一匹配"
409
+ },
410
+ new_string: {
411
+ type: "string",
412
+ description: "替换后的新文本"
413
+ },
414
+ replace_all: {
415
+ type: "boolean",
416
+ description: "替换所有匹配项(而非仅第一个)"
417
+ }
418
+ },
419
+ required: [
420
+ "file_path",
421
+ "old_string",
422
+ "new_string"
423
+ ]
424
+ },
425
+ kind: "WritesFiles",
426
+ safety: "WorkspaceSafe",
427
+ availability: { type: "always" },
428
+ executor: "core:edit_file",
429
+ owner: "core"
430
+ };
431
+ const handler$28 = { async handle(invocation, signal) {
432
+ const { file_path, old_string, new_string, replace_all } = invocation.payload;
433
+ const startTime = Date.now();
434
+ if (signal.aborted) return {
435
+ success: false,
436
+ content: "操作已取消",
437
+ metadata: { durationMs: Date.now() - startTime }
438
+ };
439
+ if (old_string === new_string) return {
440
+ success: false,
441
+ content: "old_string 与 new_string 相同 — 未做任何更改",
442
+ metadata: { durationMs: Date.now() - startTime }
443
+ };
444
+ try {
445
+ const original = await readFile(file_path, "utf-8");
446
+ const count = countOccurrences$1(original, old_string);
447
+ if (count === 0) return {
448
+ success: false,
449
+ content: `在 ${file_path} 中未找到 old_string。请确认文本精确匹配(包括缩进和空格)。`,
450
+ metadata: { durationMs: Date.now() - startTime }
451
+ };
452
+ if (count > 1 && !replace_all) return {
453
+ success: false,
454
+ content: `old_string 在 ${file_path} 中出现了 ${count} 次。请设置 replace_all: true 替换所有匹配项,或提供更多上下文使匹配唯一。`,
455
+ metadata: { durationMs: Date.now() - startTime }
456
+ };
457
+ await writeFile(file_path, replace_all ? original.replaceAll(old_string, new_string) : original.replace(old_string, new_string), "utf-8");
458
+ return {
459
+ success: true,
460
+ content: `已在 ${file_path} 中替换 ${replace_all ? count : 1} 处`,
461
+ metadata: { durationMs: Date.now() - startTime }
462
+ };
463
+ } catch (err) {
464
+ const message = err instanceof Error ? err.message : String(err);
465
+ if (err.code === "ENOENT") return {
466
+ success: false,
467
+ content: `文件未找到: ${file_path}`,
468
+ metadata: { durationMs: Date.now() - startTime }
469
+ };
470
+ return {
471
+ success: false,
472
+ content: `编辑文件失败: ${message}`,
473
+ metadata: { durationMs: Date.now() - startTime }
474
+ };
475
+ }
476
+ } };
477
+ function countOccurrences$1(haystack, needle) {
478
+ if (needle.length === 0) return 0;
479
+ let count = 0;
480
+ let pos = 0;
481
+ while ((pos = haystack.indexOf(needle, pos)) !== -1) {
482
+ count++;
483
+ pos += needle.length;
484
+ }
485
+ return count;
486
+ }
487
+ //#endregion
488
+ //#region src/builtin/files/glob.ts
489
+ /**
490
+ * glob — Fast file pattern matching.
491
+ *
492
+ * Returns sorted absolute paths. Default excludes: node_modules, .git, dist, build.
493
+ */
494
+ const descriptor$30 = {
495
+ name: "glob",
496
+ description: "用 glob 模式匹配文件。返回排序后的绝对路径。支持 ** 递归匹配、* 通配符。",
497
+ inputSchema: {
498
+ type: "object",
499
+ properties: {
500
+ pattern: {
501
+ type: "string",
502
+ description: "要匹配的 glob 模式(如 \"**/*.ts\" 或 \"src/**/*.test.ts\")"
503
+ },
504
+ path: {
505
+ type: "string",
506
+ description: "搜索的根目录(默认为当前工作目录)"
507
+ }
508
+ },
509
+ required: ["pattern"]
510
+ },
511
+ kind: "ReadOnly",
512
+ safety: "Safe",
513
+ availability: { type: "always" },
514
+ executor: "core:glob",
515
+ owner: "core"
516
+ };
517
+ const DEFAULT_EXCLUDE = new Set([
518
+ "node_modules",
519
+ ".git",
520
+ "dist",
521
+ "build",
522
+ ".next",
523
+ "coverage",
524
+ "__pycache__",
525
+ ".venv",
526
+ "venv"
527
+ ]);
528
+ const MAX_RESULTS = 1e4;
529
+ const handler$27 = { async handle(invocation, signal) {
530
+ const { pattern, path: rootPath } = invocation.payload;
531
+ const startTime = Date.now();
532
+ const base = rootPath ? resolve(rootPath) : process.cwd();
533
+ if (signal.aborted) return {
534
+ success: false,
535
+ content: "操作已取消",
536
+ metadata: { durationMs: Date.now() - startTime }
537
+ };
538
+ try {
539
+ const results = [];
540
+ await walk({
541
+ dir: base,
542
+ pattern,
543
+ base,
544
+ results,
545
+ signal,
546
+ maxResults: MAX_RESULTS
547
+ });
548
+ const truncated = results.length >= MAX_RESULTS;
549
+ let content = results.sort().join("\n");
550
+ if (truncated) content += `\n\n[已截断 — ${results.length}+ 条结果。请缩小匹配范围。]`;
551
+ return {
552
+ success: true,
553
+ content: content || "(无匹配)",
554
+ metadata: {
555
+ durationMs: Date.now() - startTime,
556
+ truncated
557
+ }
558
+ };
559
+ } catch (err) {
560
+ return {
561
+ success: false,
562
+ content: `文件匹配失败: ${err instanceof Error ? err.message : String(err)}`,
563
+ metadata: { durationMs: Date.now() - startTime }
564
+ };
565
+ }
566
+ } };
567
+ async function walk(opts) {
568
+ if (opts.results.length >= opts.maxResults || opts.signal.aborted) return;
569
+ let entries;
570
+ try {
571
+ entries = await readdir(opts.dir, { withFileTypes: true });
572
+ } catch {
573
+ return;
574
+ }
575
+ for (const entry of entries) {
576
+ if (opts.results.length >= opts.maxResults || opts.signal.aborted) break;
577
+ if (DEFAULT_EXCLUDE.has(entry.name) || entry.name.startsWith(".")) continue;
578
+ const full = join(opts.dir, entry.name);
579
+ if (entry.isDirectory()) await walk({
580
+ ...opts,
581
+ dir: full
582
+ });
583
+ else if (entry.isFile()) {
584
+ if (matchGlob(relative(opts.base, full).replace(/\\/g, "/"), opts.pattern)) opts.results.push(full);
585
+ }
586
+ }
587
+ }
588
+ function matchGlob(filepath, pattern) {
589
+ const patParts = pattern.replace(/\\/g, "/").split("/");
590
+ const fpParts = filepath.split("/");
591
+ let pi = 0;
592
+ let fi = 0;
593
+ while (pi < patParts.length && fi < fpParts.length) {
594
+ const pp = patParts[pi];
595
+ const fp = fpParts[fi];
596
+ if (pp === "**") {
597
+ if (pi === patParts.length - 1) return true;
598
+ pi++;
599
+ const restPat = patParts.slice(pi);
600
+ for (let k = fi; k < fpParts.length; k++) if (matchRest(fpParts.slice(k), restPat)) return true;
601
+ return false;
602
+ }
603
+ if (!matchSegment(fp, pp)) return false;
604
+ pi++;
605
+ fi++;
606
+ }
607
+ return pi === patParts.length && fi === fpParts.length;
608
+ }
609
+ function matchRest(fpParts, patParts) {
610
+ if (patParts.length === 0) return fpParts.length === 0;
611
+ if (fpParts.length === 0) return patParts.every((p) => p === "**");
612
+ if (patParts[0] === "**") return matchGlob(fpParts.join("/"), patParts.join("/"));
613
+ if (!matchSegment(fpParts[0], patParts[0])) return false;
614
+ return matchRest(fpParts.slice(1), patParts.slice(1));
615
+ }
616
+ function matchSegment(name, seg) {
617
+ if (seg === "*") return !name.includes("/");
618
+ return new RegExp("^" + seg.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]") + "$").test(name);
619
+ }
620
+ //#endregion
621
+ //#region src/builtin/files/grep.ts
622
+ /**
623
+ * grep — Regular expression content search (ripgrep-backed).
624
+ *
625
+ * Falls back to a pure-JS implementation when rg is not available.
626
+ * Default head_limit=250 to prevent overwhelming output.
627
+ */
628
+ promisify(execFile);
629
+ const descriptor$29 = {
630
+ name: "grep",
631
+ description: "使用正则表达式搜索文件内容。返回匹配行及可选的上下文。优先使用 ripgrep。",
632
+ inputSchema: {
633
+ type: "object",
634
+ properties: {
635
+ pattern: {
636
+ type: "string",
637
+ description: "要搜索的正则表达式"
638
+ },
639
+ path: {
640
+ type: "string",
641
+ description: "搜索的文件或目录(默认为当前工作目录)"
642
+ },
643
+ glob: {
644
+ type: "string",
645
+ description: "用于过滤文件的 glob 模式(如 \"*.ts\")"
646
+ },
647
+ output_mode: {
648
+ type: "string",
649
+ enum: [
650
+ "content",
651
+ "files_with_matches",
652
+ "count"
653
+ ],
654
+ description: "输出模式: content 显示匹配行, files_with_matches 显示文件路径, count 显示匹配计数"
655
+ },
656
+ "-n": {
657
+ type: "boolean",
658
+ description: "显示行号"
659
+ },
660
+ "-i": {
661
+ type: "boolean",
662
+ description: "忽略大小写"
663
+ },
664
+ "-C": {
665
+ type: "integer",
666
+ description: "匹配前后各显示的行数(上下文行数)"
667
+ },
668
+ head_limit: {
669
+ type: "integer",
670
+ description: "最大输出行数(默认: 250)"
671
+ }
672
+ },
673
+ required: ["pattern"]
674
+ },
675
+ kind: "ReadOnly",
676
+ safety: "Safe",
677
+ availability: { type: "always" },
678
+ executor: "core:grep",
679
+ owner: "core"
680
+ };
681
+ const DEFAULT_HEAD_LIMIT = 250;
682
+ const MAX_LINE_LENGTH = 1e3;
683
+ const MAX_TOTAL_BYTES = 5e4;
684
+ /**
685
+ * Stream stdout from a ripgrep child process, collecting lines with
686
+ * per-line length and total-byte limits enforced inline.
687
+ */
688
+ function collectRgLines(child, limit) {
689
+ return new Promise((resolve, reject) => {
690
+ const collected = [];
691
+ let lineCount = 0;
692
+ let totalBytes = 0;
693
+ let byteTruncated = false;
694
+ const killIfAlive = () => {
695
+ if (!child.killed) child.kill();
696
+ };
697
+ if (child.stdout) {
698
+ let buffer = "";
699
+ child.stdout.on("data", (chunk) => {
700
+ buffer += chunk.toString("utf-8");
701
+ const rawLines = buffer.split("\n");
702
+ buffer = rawLines.pop() ?? "";
703
+ for (const raw of rawLines) {
704
+ if (lineCount >= limit) {
705
+ killIfAlive();
706
+ break;
707
+ }
708
+ let l = raw;
709
+ if (l.length > MAX_LINE_LENGTH) l = l.slice(0, MAX_LINE_LENGTH) + " [已截断]";
710
+ const lineBytes = Buffer.byteLength(l, "utf-8") + 1;
711
+ if (totalBytes + lineBytes > MAX_TOTAL_BYTES) {
712
+ byteTruncated = true;
713
+ killIfAlive();
714
+ break;
715
+ }
716
+ collected.push(l);
717
+ lineCount++;
718
+ totalBytes += lineBytes;
719
+ }
720
+ });
721
+ }
722
+ child.on("close", () => resolve({
723
+ lines: collected,
724
+ byteTruncated
725
+ }));
726
+ child.on("error", reject);
727
+ });
728
+ }
729
+ /**
730
+ * Build the final {@link ToolResult} from collected grep output.
731
+ */
732
+ function buildGrepResult(collected, limit, byteTruncated, startTime) {
733
+ const truncated = collected.length >= limit || byteTruncated;
734
+ const output = collected.join("\n");
735
+ if (output.length === 0 && !truncated) return {
736
+ success: true,
737
+ content: "(无匹配)",
738
+ metadata: { durationMs: Date.now() - startTime }
739
+ };
740
+ return {
741
+ success: true,
742
+ content: output + (truncated ? byteTruncated ? `\n\n[已截断 — 输出超过 ${MAX_TOTAL_BYTES} 字节。请缩小搜索范围。]` : `\n\n[已截断 — 达到 ${limit} 行限制。请缩小搜索范围。]` : ""),
743
+ metadata: {
744
+ durationMs: Date.now() - startTime,
745
+ truncated,
746
+ truncatedAt: truncated ? limit : void 0
747
+ }
748
+ };
749
+ }
750
+ /**
751
+ * Classify an error thrown during ripgrep execution and return the
752
+ * appropriate {@link ToolResult}, or `null` if the caller should
753
+ * fall through to the JS fallback.
754
+ */
755
+ function classifyRgError(err, signal, startTime) {
756
+ if (err.code === "ENOENT") return null;
757
+ if (err.killed) return {
758
+ success: false,
759
+ content: "搜索超时(30 秒)",
760
+ metadata: { durationMs: Date.now() - startTime }
761
+ };
762
+ if (signal?.aborted) return {
763
+ success: false,
764
+ content: "Operation cancelled",
765
+ metadata: { durationMs: Date.now() - startTime }
766
+ };
767
+ const stderr = err.stderr;
768
+ if (stderr) return {
769
+ success: false,
770
+ content: `grep 错误: ${stderr.slice(0, 500)}`,
771
+ metadata: { durationMs: Date.now() - startTime }
772
+ };
773
+ return {
774
+ success: true,
775
+ content: "(no matches)",
776
+ metadata: { durationMs: Date.now() - startTime }
777
+ };
778
+ }
779
+ const handler$26 = { async handle(invocation, signal) {
780
+ const { pattern, path, glob, output_mode, "-n": showLineNum, "-i": caseInsensitive, "-C": context, head_limit } = invocation.payload;
781
+ const startTime = Date.now();
782
+ if (signal.aborted) return {
783
+ success: false,
784
+ content: "操作已取消",
785
+ metadata: { durationMs: Date.now() - startTime }
786
+ };
787
+ const limit = head_limit ?? DEFAULT_HEAD_LIMIT;
788
+ try {
789
+ const { lines, byteTruncated } = await collectRgLines(execFile("rg", buildRgArgs({
790
+ pattern,
791
+ path,
792
+ glob,
793
+ outputMode: output_mode,
794
+ showLineNum,
795
+ caseInsensitive,
796
+ context
797
+ }), {
798
+ cwd: process.cwd(),
799
+ timeout: 3e4,
800
+ maxBuffer: 10 * 1024 * 1024,
801
+ signal
802
+ }), limit);
803
+ return buildGrepResult(lines, limit, byteTruncated, startTime);
804
+ } catch (err) {
805
+ const classified = classifyRgError(err, signal, startTime);
806
+ if (classified !== null) return classified;
807
+ }
808
+ return {
809
+ success: false,
810
+ content: "ripgrep (rg) 未安装。请安装 ripgrep 以支持 grep 功能: https://github.com/BurntSushi/ripgrep",
811
+ metadata: { durationMs: Date.now() - startTime }
812
+ };
813
+ } };
814
+ function buildRgArgs(opts) {
815
+ const args = ["--no-heading", "--color=never"];
816
+ if (opts.showLineNum !== false) args.push("--line-number");
817
+ if (opts.caseInsensitive) args.push("--ignore-case");
818
+ if (opts.context !== void 0) args.push("-C", String(opts.context));
819
+ if (opts.glob) args.push("--glob", opts.glob);
820
+ switch (opts.outputMode) {
821
+ case "files_with_matches":
822
+ args.push("-l");
823
+ break;
824
+ case "count":
825
+ args.push("--count");
826
+ break;
827
+ default: break;
828
+ }
829
+ args.push("--", opts.pattern);
830
+ if (opts.path) args.push(opts.path);
831
+ return args;
832
+ }
833
+ //#endregion
834
+ //#region src/builtin/files/web-fetch.ts
835
+ /** 约 50 个常用安全域名 */
836
+ const ALLOWED_DOMAINS = new Set([
837
+ "github.com",
838
+ "gitlab.com",
839
+ "npmjs.com",
840
+ "pypi.org",
841
+ "crates.io",
842
+ "docs.rs",
843
+ "stackoverflow.com",
844
+ "wikipedia.org",
845
+ "nodejs.org",
846
+ "typescriptlang.org",
847
+ "developer.mozilla.org",
848
+ "anthropic.com",
849
+ "openai.com",
850
+ "googleapis.com",
851
+ "eslint.org",
852
+ "prettier.io",
853
+ "jestjs.io",
854
+ "vitest.dev",
855
+ "playwright.dev",
856
+ "docker.com",
857
+ "kubernetes.io",
858
+ "terraform.io",
859
+ "redis.io",
860
+ "postgresql.org",
861
+ "mysql.com",
862
+ "mongodb.com",
863
+ "sqlite.org",
864
+ "rust-lang.org",
865
+ "golang.org",
866
+ "python.org",
867
+ "ruby-lang.org",
868
+ "php.net",
869
+ "java.com",
870
+ "kernel.org",
871
+ "archlinux.org",
872
+ "ubuntu.com",
873
+ "debian.org",
874
+ "redhat.com",
875
+ "aws.amazon.com",
876
+ "cloud.google.com",
877
+ "azure.microsoft.com",
878
+ "reactjs.org",
879
+ "vuejs.org",
880
+ "angular.io",
881
+ "svelte.dev",
882
+ "nextjs.org",
883
+ "nuxt.com",
884
+ "tailwindcss.com",
885
+ "vitejs.dev",
886
+ "webpack.js.org",
887
+ "babeljs.io"
888
+ ]);
889
+ /** 禁止访问的域名(本地、云元数据端点等) */
890
+ const BLOCKED_DOMAINS = new Set([
891
+ "localhost",
892
+ "127.0.0.1",
893
+ "0.0.0.0",
894
+ "::1",
895
+ "metadata.google.internal",
896
+ "169.254.169.254"
897
+ ]);
898
+ /** IPv4 地址正则 */
899
+ const IPV4_RE = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
900
+ /**
901
+ * 检查 URL 是否在允许访问的范围内。
902
+ *
903
+ * 规则(按优先级):
904
+ * 1. URL 解析失败 → 拒绝
905
+ * 2. IP 地址 → 拒绝
906
+ * 3. 被阻止的域名 → 拒绝
907
+ * 4. .local / .internal TLD → 拒绝
908
+ * 5. URL 中包含 userinfo(username:password@host)→ 拒绝
909
+ * 6. 域名在允许列表中(去除 www. 前缀后匹配,支持子域名通配)→ 允许
910
+ * 7. 默认 → 拒绝
911
+ */
912
+ function isUrlAllowed(url) {
913
+ let parsed;
914
+ try {
915
+ parsed = new URL(url);
916
+ } catch {
917
+ return {
918
+ allowed: false,
919
+ reason: `URL 解析失败: ${url}`
920
+ };
921
+ }
922
+ const host = parsed.hostname.toLowerCase();
923
+ if (IPV4_RE.test(host) || host === "::1") return {
924
+ allowed: false,
925
+ reason: `不允许访问 IP 地址: ${host}`
926
+ };
927
+ if (BLOCKED_DOMAINS.has(host)) return {
928
+ allowed: false,
929
+ reason: `不允许访问该域名: ${host}`
930
+ };
931
+ if (host.endsWith(".local") || host.endsWith(".internal")) return {
932
+ allowed: false,
933
+ reason: `不允许访问本地/内部域名: ${host}`
934
+ };
935
+ if (parsed.username || parsed.password) return {
936
+ allowed: false,
937
+ reason: "URL 中包含认证信息(username:password),已拒绝"
938
+ };
939
+ const strippedHost = host.startsWith("www.") ? host.slice(4) : host;
940
+ if (ALLOWED_DOMAINS.has(strippedHost)) return {
941
+ allowed: true,
942
+ reason: ""
943
+ };
944
+ const parts = strippedHost.split(".");
945
+ for (let i = 1; i < parts.length; i++) {
946
+ const parentDomain = parts.slice(i).join(".");
947
+ if (ALLOWED_DOMAINS.has(parentDomain)) return {
948
+ allowed: true,
949
+ reason: ""
950
+ };
951
+ }
952
+ return {
953
+ allowed: false,
954
+ reason: `域名 "${host}" 不在允许列表中。如需访问请联系用户确认。`
955
+ };
956
+ }
957
+ const descriptor$28 = {
958
+ name: "web_fetch",
959
+ description: "从 URL 获取内容并以文本形式返回。HTTP 自动升级为 HTTPS。跨域重定向会返回给调用方而非自动跟随。每个 URL 的结果缓存 15 分钟。HTML 内容会转换为纯文本。",
960
+ inputSchema: {
961
+ type: "object",
962
+ properties: {
963
+ url: {
964
+ type: "string",
965
+ format: "uri",
966
+ description: "要获取内容的 URL"
967
+ },
968
+ maxChars: {
969
+ type: "integer",
970
+ description: "返回的最大字符数(默认: 50000)"
971
+ }
972
+ },
973
+ required: ["url"]
974
+ },
975
+ kind: "Network",
976
+ safety: "RequiresApproval",
977
+ availability: { type: "always" },
978
+ executor: "core:web_fetch",
979
+ owner: "core"
980
+ };
981
+ const cache$1 = /* @__PURE__ */ new Map();
982
+ const CACHE_TTL_MS$1 = 900 * 1e3;
983
+ /**
984
+ * Strip HTML tags and decode entities to produce readable plain text.
985
+ *
986
+ * Handles the most common HTML entities and normalizes whitespace.
987
+ * Not a full HTML parser — just enough to extract readable content from
988
+ * typical web pages.
989
+ */
990
+ function htmlToText(html) {
991
+ return html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "").replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, "").replace(/<\/(?:div|p|h[1-6]|li|tr|article|section|header|footer|nav|aside|main|table|thead|tbody|tfoot|form|fieldset|pre|blockquote|dl|dt|dd|hr|br)[^>]*>/gi, "\n").replace(/<(?:br|hr)[^>]*\/?>/gi, "\n").replace(/<[^>]*>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, "\"").replace(/&#39;/g, "'").replace(/&nbsp;/g, " ").replace(/&#x27;/g, "'").replace(/&#x2F;/g, "/").replace(/&#(\d+);/g, (_m, code) => String.fromCodePoint(Number.parseInt(code, 10))).replace(/&#[xX]([0-9a-fA-F]+);/g, (_m, hex) => String.fromCodePoint(Number.parseInt(hex, 16))).replace(/[ \t]+/g, " ").replace(/\n{3,}/g, "\n\n").trim();
992
+ }
993
+ /**
994
+ * Extract readable text from raw response body based on Content-Type.
995
+ *
996
+ * HTML responses are converted to plain text. Non‑HTML responses are
997
+ * returned as‑is.
998
+ */
999
+ function extractContent(raw, contentType) {
1000
+ return (contentType ? contentType.includes("text/html") || contentType.includes("application/xhtml") : raw.trim().startsWith("<") || raw.includes("<html")) ? htmlToText(raw) : raw;
1001
+ }
1002
+ /**
1003
+ * Trim text to the specified character limit, preserving the beginning
1004
+ * (most important content is usually at the top of a page).
1005
+ */
1006
+ function trimToLimit(text, limit) {
1007
+ if (text.length <= limit) return text;
1008
+ return text.slice(0, limit);
1009
+ }
1010
+ /** Set of HTTP redirect status codes. */
1011
+ const REDIRECT_CODES = new Set([
1012
+ 301,
1013
+ 302,
1014
+ 307,
1015
+ 308
1016
+ ]);
1017
+ /**
1018
+ * Validate and normalize a URL for fetching.
1019
+ *
1020
+ * Upgrades HTTP to HTTPS. Returns the normalized URL or an error result.
1021
+ */
1022
+ function validateUrl(rawUrl, startTime) {
1023
+ let normalized = rawUrl;
1024
+ if (normalized.startsWith("http://")) normalized = normalized.replace("http://", "https://");
1025
+ if (!normalized.startsWith("https://")) return {
1026
+ ok: false,
1027
+ error: {
1028
+ success: false,
1029
+ content: `无效的 URL: ${rawUrl}。必须使用 http:// 或 https://。`,
1030
+ metadata: { durationMs: Date.now() - startTime }
1031
+ }
1032
+ };
1033
+ return {
1034
+ ok: true,
1035
+ url: normalized
1036
+ };
1037
+ }
1038
+ /**
1039
+ * Handle an HTTP redirect response.
1040
+ *
1041
+ * For cross-host redirects, returns a message instructing the caller to
1042
+ * re-issue the request. For same-host redirects, follows the redirect
1043
+ * and returns the fetched content.
1044
+ */
1045
+ async function handleFetchRedirect(opts) {
1046
+ const location = opts.resp.headers.get("location") || "";
1047
+ try {
1048
+ if (new URL(location).host !== new URL(opts.fetchUrl).host) return {
1049
+ success: true,
1050
+ content: `重定向至: ${location}\n\n检测到跨域重定向。请使用重定向后的 URL 重新调用 web_fetch。`,
1051
+ metadata: { durationMs: Date.now() - opts.startTime }
1052
+ };
1053
+ const extracted = extractContent(await (await fetch(location, {
1054
+ signal: opts.controller.signal,
1055
+ headers: { "User-Agent": "Lynx/1.0" }
1056
+ })).text(), opts.resp.headers.get("content-type") ?? void 0);
1057
+ cache$1.set(opts.originalUrl, {
1058
+ content: extracted,
1059
+ ts: Date.now()
1060
+ });
1061
+ return {
1062
+ success: true,
1063
+ content: trimToLimit(extracted, opts.charLimit) + (extracted.length > opts.charLimit ? "\n\n[内容已截断]" : ""),
1064
+ metadata: {
1065
+ durationMs: Date.now() - opts.startTime,
1066
+ truncated: extracted.length > opts.charLimit
1067
+ }
1068
+ };
1069
+ } catch {
1070
+ return {
1071
+ success: true,
1072
+ content: `重定向至: ${location}`,
1073
+ metadata: { durationMs: Date.now() - opts.startTime }
1074
+ };
1075
+ }
1076
+ }
1077
+ /**
1078
+ * Build an error ToolResult from a fetch exception.
1079
+ */
1080
+ function buildFetchError(err, startTime) {
1081
+ if (err.name === "AbortError") return {
1082
+ success: false,
1083
+ content: "获取已中止",
1084
+ metadata: { durationMs: Date.now() - startTime }
1085
+ };
1086
+ return {
1087
+ success: false,
1088
+ content: `获取失败: ${err instanceof Error ? err.message : String(err)}`,
1089
+ metadata: { durationMs: Date.now() - startTime }
1090
+ };
1091
+ }
1092
+ const handler$25 = { async handle(invocation, signal) {
1093
+ const { url, maxChars } = invocation.payload;
1094
+ const charLimit = maxChars ?? 5e4;
1095
+ const startTime = Date.now();
1096
+ if (signal.aborted) return {
1097
+ success: false,
1098
+ content: "操作已取消",
1099
+ metadata: { durationMs: Date.now() - startTime }
1100
+ };
1101
+ const validated = validateUrl(url, startTime);
1102
+ if (!validated.ok) return validated.error;
1103
+ const fetchUrl = validated.url;
1104
+ const urlCheck = isUrlAllowed(fetchUrl);
1105
+ if (!urlCheck.allowed) return {
1106
+ success: false,
1107
+ content: urlCheck.reason,
1108
+ metadata: { durationMs: Date.now() - startTime }
1109
+ };
1110
+ const cached = cache$1.get(fetchUrl);
1111
+ if (cached && Date.now() - cached.ts < CACHE_TTL_MS$1) {
1112
+ const content = trimToLimit(cached.content, charLimit);
1113
+ return {
1114
+ success: true,
1115
+ content: `[缓存于 ${Math.round((Date.now() - cached.ts) / 1e3)} 秒前]\n\n${content}`,
1116
+ metadata: {
1117
+ durationMs: Date.now() - startTime,
1118
+ truncated: cached.content.length > charLimit
1119
+ }
1120
+ };
1121
+ }
1122
+ try {
1123
+ const controller = new AbortController();
1124
+ signal.addEventListener("abort", () => controller.abort(), { once: true });
1125
+ const resp = await fetch(fetchUrl, {
1126
+ signal: controller.signal,
1127
+ redirect: "manual",
1128
+ headers: { "User-Agent": "Lynx/1.0" }
1129
+ });
1130
+ if (REDIRECT_CODES.has(resp.status)) return await handleFetchRedirect({
1131
+ fetchUrl,
1132
+ originalUrl: url,
1133
+ resp,
1134
+ controller,
1135
+ charLimit,
1136
+ startTime
1137
+ });
1138
+ if (!resp.ok) return {
1139
+ success: false,
1140
+ content: `HTTP 错误 ${resp.status}: ${resp.statusText}`,
1141
+ metadata: { durationMs: Date.now() - startTime }
1142
+ };
1143
+ const raw = await resp.text();
1144
+ const contentType = resp.headers.get("content-type") ?? void 0;
1145
+ const extracted = extractContent(raw, contentType);
1146
+ cache$1.set(url, {
1147
+ content: extracted,
1148
+ ts: Date.now()
1149
+ });
1150
+ const output = trimToLimit(extracted, charLimit);
1151
+ return {
1152
+ success: true,
1153
+ content: `已获取 ${fetchUrl} (${contentType || "未知类型"}):\n\n${output}${extracted.length > charLimit ? "\n\n[内容已截断]" : ""}`,
1154
+ metadata: {
1155
+ durationMs: Date.now() - startTime,
1156
+ truncated: extracted.length > charLimit
1157
+ }
1158
+ };
1159
+ } catch (err) {
1160
+ return buildFetchError(err, startTime);
1161
+ }
1162
+ } };
1163
+ //#endregion
1164
+ //#region src/builtin/files/web-search.ts
1165
+ const descriptor$27 = {
1166
+ name: "web_search",
1167
+ description: "搜索网页并返回包含标题、URL 和摘要的结果。支持域名白名单和黑名单过滤。",
1168
+ inputSchema: {
1169
+ type: "object",
1170
+ properties: {
1171
+ query: {
1172
+ type: "string",
1173
+ description: "搜索关键词"
1174
+ },
1175
+ allowed_domains: {
1176
+ type: "array",
1177
+ items: { type: "string" },
1178
+ description: "只包含来自这些域名的结果"
1179
+ },
1180
+ blocked_domains: {
1181
+ type: "array",
1182
+ items: { type: "string" },
1183
+ description: "排除来自这些域名的结果"
1184
+ }
1185
+ },
1186
+ required: ["query"]
1187
+ },
1188
+ kind: "Network",
1189
+ safety: "RequiresApproval",
1190
+ availability: { type: "always" },
1191
+ executor: "core:web_search",
1192
+ owner: "core"
1193
+ };
1194
+ /** In-memory cache: query → { results, timestamp } */
1195
+ const cache = /* @__PURE__ */ new Map();
1196
+ const CACHE_TTL_MS = 900 * 1e3;
1197
+ /**
1198
+ * Build an error ToolResult from a fetch exception.
1199
+ */
1200
+ function buildSearchError(err, startTime) {
1201
+ if (err.name === "AbortError") return {
1202
+ success: false,
1203
+ content: "搜索已中止",
1204
+ metadata: { durationMs: Date.now() - startTime }
1205
+ };
1206
+ return {
1207
+ success: false,
1208
+ content: `搜索失败: ${err instanceof Error ? err.message : String(err)}`,
1209
+ metadata: { durationMs: Date.now() - startTime }
1210
+ };
1211
+ }
1212
+ /**
1213
+ * Parse DuckDuckGo HTML search results page into structured results.
1214
+ *
1215
+ * Extracts title, URL, and snippet from the HTML using regex patterns
1216
+ * matching DuckDuckGo's HTML-only result page markup.
1217
+ */
1218
+ function parseDdgHtml(html) {
1219
+ const results = [];
1220
+ const linkRegex = /<a[^>]*class="result__a"[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi;
1221
+ const snippetRegex = /<a[^>]*class="result__snippet"[^>]*>([\s\S]*?)<\/a>/gi;
1222
+ const links = [];
1223
+ let linkMatch;
1224
+ while ((linkMatch = linkRegex.exec(html)) !== null) {
1225
+ const url = linkMatch[1];
1226
+ const title = linkMatch[2].replace(/<[^>]*>/g, "").trim();
1227
+ if (url && title && !url.startsWith("//duckduckgo.com")) links.push({
1228
+ title,
1229
+ url
1230
+ });
1231
+ }
1232
+ const snippets = [];
1233
+ let snippetMatch;
1234
+ while ((snippetMatch = snippetRegex.exec(html)) !== null) {
1235
+ const text = snippetMatch[1].replace(/<[^>]*>/g, "").trim();
1236
+ if (text) snippets.push(text);
1237
+ }
1238
+ const count = Math.min(links.length, snippets.length);
1239
+ for (let i = 0; i < count; i++) results.push({
1240
+ title: links[i].title,
1241
+ url: links[i].url,
1242
+ snippet: snippets[i]
1243
+ });
1244
+ return results;
1245
+ }
1246
+ /**
1247
+ * Filter search results by allowed/blocked domains.
1248
+ */
1249
+ function filterByDomain(results, allowed, blocked) {
1250
+ let filtered = results;
1251
+ if (allowed && allowed.length > 0) {
1252
+ const allowedLower = allowed.map((d) => d.toLowerCase());
1253
+ filtered = filtered.filter((r) => {
1254
+ try {
1255
+ const host = new URL(r.url).host.toLowerCase();
1256
+ return allowedLower.some((d) => host.includes(d));
1257
+ } catch {
1258
+ return false;
1259
+ }
1260
+ });
1261
+ }
1262
+ if (blocked && blocked.length > 0) {
1263
+ const blockedLower = blocked.map((d) => d.toLowerCase());
1264
+ filtered = filtered.filter((r) => {
1265
+ try {
1266
+ const host = new URL(r.url).host.toLowerCase();
1267
+ return !blockedLower.some((d) => host.includes(d));
1268
+ } catch {
1269
+ return true;
1270
+ }
1271
+ });
1272
+ }
1273
+ return filtered;
1274
+ }
1275
+ /**
1276
+ * Format search results into a readable text block.
1277
+ */
1278
+ function formatResults(results, query) {
1279
+ if (results.length === 0) return `未找到与 "${query}" 相关的结果。`;
1280
+ return results.map((r, i) => `${i + 1}. **${r.title}**\n ${r.url}\n ${r.snippet}`).join("\n\n");
1281
+ }
1282
+ /** Generate a cache key from the query and domain filters. */
1283
+ function cacheKey(query, allowed, blocked) {
1284
+ const parts = [query];
1285
+ if (allowed?.length) parts.push("allow:" + allowed.sort().join(","));
1286
+ if (blocked?.length) parts.push("block:" + blocked.sort().join(","));
1287
+ return parts.join("|");
1288
+ }
1289
+ const handler$24 = { async handle(invocation, signal) {
1290
+ const { query, allowed_domains, blocked_domains } = invocation.payload;
1291
+ const startTime = Date.now();
1292
+ if (signal.aborted) return {
1293
+ success: false,
1294
+ content: "操作已取消",
1295
+ metadata: { durationMs: Date.now() - startTime }
1296
+ };
1297
+ if (!query || query.trim().length === 0) return {
1298
+ success: false,
1299
+ content: "请输入搜索关键词.",
1300
+ metadata: { durationMs: Date.now() - startTime }
1301
+ };
1302
+ const key = cacheKey(query.trim(), allowed_domains, blocked_domains);
1303
+ const cached = cache.get(key);
1304
+ if (cached && Date.now() - cached.ts < CACHE_TTL_MS) return {
1305
+ success: true,
1306
+ content: `[缓存于 ${Math.round((Date.now() - cached.ts) / 1e3)} 秒前]\n\n${formatResults(cached.results, query)}`,
1307
+ metadata: { durationMs: Date.now() - startTime }
1308
+ };
1309
+ try {
1310
+ const controller = new AbortController();
1311
+ signal.addEventListener("abort", () => controller.abort(), { once: true });
1312
+ const searchUrl = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query.trim())}`;
1313
+ const urlCheck = isUrlAllowed(searchUrl);
1314
+ if (!urlCheck.allowed) return {
1315
+ success: false,
1316
+ content: urlCheck.reason,
1317
+ metadata: { durationMs: Date.now() - startTime }
1318
+ };
1319
+ const resp = await fetch(searchUrl, {
1320
+ signal: controller.signal,
1321
+ headers: {
1322
+ "User-Agent": "Lynx/1.0",
1323
+ Accept: "text/html"
1324
+ }
1325
+ });
1326
+ if (!resp.ok) return {
1327
+ success: false,
1328
+ content: `搜索请求失败: HTTP ${resp.status} ${resp.statusText}`,
1329
+ metadata: { durationMs: Date.now() - startTime }
1330
+ };
1331
+ const allResults = parseDdgHtml(await resp.text());
1332
+ const filtered = filterByDomain(allResults, allowed_domains, blocked_domains);
1333
+ cache.set(key, {
1334
+ results: filtered,
1335
+ ts: Date.now()
1336
+ });
1337
+ return {
1338
+ success: true,
1339
+ content: formatResults(filtered, query),
1340
+ metadata: {
1341
+ durationMs: Date.now() - startTime,
1342
+ truncated: filtered.length < allResults.length
1343
+ }
1344
+ };
1345
+ } catch (err) {
1346
+ return buildSearchError(err, startTime);
1347
+ }
1348
+ } };
1349
+ //#endregion
1350
+ //#region src/builtin/files/todo-write.ts
1351
+ const descriptor$26 = {
1352
+ name: "todo_write",
1353
+ description: "创建和更新任务列表以跟踪进度。每次调用会替换整个列表。每项任务必须包含 content、status 和 activeForm。",
1354
+ inputSchema: {
1355
+ type: "object",
1356
+ properties: { todos: {
1357
+ type: "array",
1358
+ items: {
1359
+ type: "object",
1360
+ properties: {
1361
+ content: {
1362
+ type: "string",
1363
+ description: "任务描述"
1364
+ },
1365
+ status: {
1366
+ type: "string",
1367
+ enum: [
1368
+ "pending",
1369
+ "in_progress",
1370
+ "completed"
1371
+ ],
1372
+ description: "任务当前状态"
1373
+ },
1374
+ activeForm: {
1375
+ type: "string",
1376
+ description: "任务进行中时显示的现在时标签"
1377
+ }
1378
+ },
1379
+ required: [
1380
+ "content",
1381
+ "status",
1382
+ "activeForm"
1383
+ ]
1384
+ },
1385
+ description: "完整的任务列表(替换已有列表)"
1386
+ } },
1387
+ required: ["todos"]
1388
+ },
1389
+ kind: "WritesFiles",
1390
+ safety: "Safe",
1391
+ availability: { type: "always" },
1392
+ executor: "core:todo_write",
1393
+ owner: "core"
1394
+ };
1395
+ const handler$23 = { async handle(invocation, _signal) {
1396
+ const { todos } = invocation.payload;
1397
+ const startTime = Date.now();
1398
+ if (!Array.isArray(todos)) return {
1399
+ success: false,
1400
+ content: "todos 必须是一个数组",
1401
+ metadata: { durationMs: Date.now() - startTime }
1402
+ };
1403
+ const statusCounts = {
1404
+ pending: 0,
1405
+ in_progress: 0,
1406
+ completed: 0
1407
+ };
1408
+ for (const todo of todos) {
1409
+ if (!todo.content || !todo.status || !todo.activeForm) return {
1410
+ success: false,
1411
+ content: `无效的任务项: 每项必须包含 content、status 和 activeForm。收到: ${JSON.stringify(todo)}`,
1412
+ metadata: { durationMs: Date.now() - startTime }
1413
+ };
1414
+ if (![
1415
+ "pending",
1416
+ "in_progress",
1417
+ "completed"
1418
+ ].includes(todo.status)) return {
1419
+ success: false,
1420
+ content: `"${todo.content}" 的状态 "${todo.status}" 无效。必须为: pending, in_progress 或 completed。`,
1421
+ metadata: { durationMs: Date.now() - startTime }
1422
+ };
1423
+ statusCounts[todo.status]++;
1424
+ }
1425
+ const lines = todos.map((t, _i) => {
1426
+ return `${t.status === "completed" ? "✓" : t.status === "in_progress" ? "●" : "○"} [${t.status}] ${t.activeForm}: ${t.content}`;
1427
+ });
1428
+ return {
1429
+ success: true,
1430
+ content: `任务列表已更新(共 ${todos.length} 项: ${statusCounts.in_progress} 项进行中, ${statusCounts.pending} 项待处理, ${statusCounts.completed} 项已完成):\n\n` + lines.join("\n"),
1431
+ metadata: { durationMs: Date.now() - startTime }
1432
+ };
1433
+ } };
1434
+ //#endregion
1435
+ //#region src/builtin/files/powershell.ts
1436
+ /**
1437
+ * powershell — 在 Windows 上执行 PowerShell 命令。
1438
+ *
1439
+ * 仅在 Windows 平台可用。使用 child_process.exec 执行,
1440
+ * 默认 30 秒超时。返回 stdout 和 stderr。
1441
+ */
1442
+ const descriptor$25 = {
1443
+ name: "powershell",
1444
+ description: "在 Windows 上执行 PowerShell 命令。返回 stdout 和 stderr。仅 Windows 平台可用。命令以 -NoProfile 模式运行。",
1445
+ inputSchema: {
1446
+ type: "object",
1447
+ properties: {
1448
+ command: {
1449
+ type: "string",
1450
+ description: "要执行的 PowerShell 命令"
1451
+ },
1452
+ timeout: {
1453
+ type: "number",
1454
+ description: "超时时间(毫秒),默认 30000",
1455
+ default: 3e4
1456
+ }
1457
+ },
1458
+ required: ["command"]
1459
+ },
1460
+ kind: "ExecutesCode",
1461
+ safety: "RequiresApproval",
1462
+ availability: {
1463
+ type: "platform",
1464
+ platform: "windows"
1465
+ },
1466
+ executor: "core:powershell",
1467
+ owner: "core"
1468
+ };
1469
+ const handler$22 = { async handle(invocation, signal) {
1470
+ const { command, timeout } = invocation.payload;
1471
+ const startTime = Date.now();
1472
+ if (signal.aborted) return {
1473
+ success: false,
1474
+ content: "操作已取消",
1475
+ metadata: { durationMs: Date.now() - startTime }
1476
+ };
1477
+ if (process.platform !== "win32") return {
1478
+ success: false,
1479
+ content: "PowerShell 工具仅支持 Windows 平台。",
1480
+ metadata: { durationMs: Date.now() - startTime }
1481
+ };
1482
+ if (!command || command.trim().length === 0) return {
1483
+ success: false,
1484
+ content: "请输入要执行的 PowerShell 命令。",
1485
+ metadata: { durationMs: Date.now() - startTime }
1486
+ };
1487
+ const timeoutMs = timeout ?? 3e4;
1488
+ const escapedCommand = command.replace(/"/g, "\\\"");
1489
+ return new Promise((resolve) => {
1490
+ let aborted = false;
1491
+ const onAbort = () => {
1492
+ aborted = true;
1493
+ };
1494
+ signal.addEventListener("abort", onAbort, { once: true });
1495
+ exec(`powershell -NoProfile -Command "${escapedCommand}"`, {
1496
+ timeout: timeoutMs,
1497
+ windowsHide: true
1498
+ }, (err, stdout, stderr) => {
1499
+ signal.removeEventListener("abort", onAbort);
1500
+ if (aborted) {
1501
+ resolve({
1502
+ success: false,
1503
+ content: "操作已取消",
1504
+ metadata: { durationMs: Date.now() - startTime }
1505
+ });
1506
+ return;
1507
+ }
1508
+ if (err) {
1509
+ resolve({
1510
+ success: false,
1511
+ content: err.code === "ETIMEDOUT" ? `命令执行超时(${timeoutMs}ms)` : `命令执行失败: ${err.message}\n\nstderr:\n${stderr}`,
1512
+ metadata: { durationMs: Date.now() - startTime }
1513
+ });
1514
+ return;
1515
+ }
1516
+ resolve({
1517
+ success: true,
1518
+ content: [stdout ? `stdout:\n${stdout.trimEnd()}` : null, stderr ? `stderr:\n${stderr.trimEnd()}` : null].filter(Boolean).join("\n\n") || "(无输出)",
1519
+ metadata: { durationMs: Date.now() - startTime }
1520
+ });
1521
+ });
1522
+ });
1523
+ } };
1524
+ //#endregion
1525
+ //#region src/builtin/files/notebook-edit.ts
1526
+ /**
1527
+ * NotebookEditTool — 编辑 Jupyter notebook (.ipynb) 文件中的单元格。
1528
+ *
1529
+ * 支持四种操作:
1530
+ * - list: 列出所有单元格(索引、类型、源码摘要)
1531
+ * - insert: 在指定位置插入新单元格
1532
+ * - replace: 替换指定位置的单元格内容
1533
+ * - delete: 删除指定位置的单元格
1534
+ *
1535
+ * .ipynb 文件结构为 JSON: { cells: [{ cell_type, source, ... }], ... }
1536
+ * source 字段可以是字符串或字符串数组,统一按字符串数组处理。
1537
+ */
1538
+ const descriptor$24 = {
1539
+ name: "NotebookEditTool",
1540
+ description: "编辑 Jupyter notebook (.ipynb) 文件中的单元格。支持插入、替换、删除和列出代码与 Markdown 单元格。",
1541
+ inputSchema: {
1542
+ type: "object",
1543
+ properties: {
1544
+ filePath: {
1545
+ type: "string",
1546
+ description: ".ipynb 文件的绝对路径"
1547
+ },
1548
+ action: {
1549
+ type: "string",
1550
+ enum: [
1551
+ "insert",
1552
+ "replace",
1553
+ "delete",
1554
+ "list"
1555
+ ],
1556
+ description: "操作类型:list(列出)、insert(插入)、replace(替换)、delete(删除)"
1557
+ },
1558
+ cellIndex: {
1559
+ type: "integer",
1560
+ description: "单元格索引(从 0 开始)。insert 表示插入位置,replace/delete 表示目标单元格。"
1561
+ },
1562
+ cellType: {
1563
+ type: "string",
1564
+ enum: ["code", "markdown"],
1565
+ description: "单元格类型(insert 操作必填)"
1566
+ },
1567
+ content: {
1568
+ type: "string",
1569
+ description: "单元格源码内容(insert/replace 操作需要)"
1570
+ }
1571
+ },
1572
+ required: ["filePath", "action"]
1573
+ },
1574
+ kind: "WritesFiles",
1575
+ safety: "WorkspaceSafe",
1576
+ availability: { type: "always" },
1577
+ executor: "core:notebook_edit",
1578
+ owner: "core"
1579
+ };
1580
+ /** 将 source 字段统一转为字符串数组 */
1581
+ function normalizeSource(source) {
1582
+ if (Array.isArray(source)) return source;
1583
+ const lines = source.split("\n");
1584
+ return lines.map((line, i) => i < lines.length - 1 ? line + "\n" : line);
1585
+ }
1586
+ /** 将 source 字段合并为单个字符串 */
1587
+ function sourceToString(source) {
1588
+ if (typeof source === "string") return source;
1589
+ return source.join("");
1590
+ }
1591
+ /** 生成单元格源码摘要(前 80 个字符) */
1592
+ function sourceSummary(source) {
1593
+ const text = sourceToString(source).replace(/\s+/g, " ").trim();
1594
+ if (text.length <= 80) return text;
1595
+ return text.slice(0, 77) + "...";
1596
+ }
1597
+ /** 读取 .ipynb 文件并解析 JSON */
1598
+ function readNotebook(filePath, startTime) {
1599
+ let raw;
1600
+ try {
1601
+ raw = readFileSync(filePath, "utf-8");
1602
+ } catch (err) {
1603
+ const message = err instanceof Error ? err.message : String(err);
1604
+ if (err.code === "ENOENT") return {
1605
+ success: false,
1606
+ content: `文件未找到: ${filePath}`,
1607
+ metadata: { durationMs: Date.now() - startTime }
1608
+ };
1609
+ return {
1610
+ success: false,
1611
+ content: `读取文件失败: ${message}`,
1612
+ metadata: { durationMs: Date.now() - startTime }
1613
+ };
1614
+ }
1615
+ try {
1616
+ return JSON.parse(raw);
1617
+ } catch (err) {
1618
+ return {
1619
+ success: false,
1620
+ content: `解析 .ipynb JSON 失败: ${err instanceof Error ? err.message : String(err)}`,
1621
+ metadata: { durationMs: Date.now() - startTime }
1622
+ };
1623
+ }
1624
+ }
1625
+ /** 原子写入 notebook JSON */
1626
+ function writeNotebookAtomically(filePath, notebook, startTime) {
1627
+ try {
1628
+ const tmpPath = filePath + ".tmp";
1629
+ writeFileSync(tmpPath, JSON.stringify(notebook, null, 1) + "\n", "utf-8");
1630
+ renameSync(tmpPath, filePath);
1631
+ return {
1632
+ success: true,
1633
+ content: `已保存 notebook: ${filePath}`,
1634
+ metadata: { durationMs: Date.now() - startTime }
1635
+ };
1636
+ } catch (err) {
1637
+ return {
1638
+ success: false,
1639
+ content: `写入文件失败: ${err instanceof Error ? err.message : String(err)}`,
1640
+ metadata: { durationMs: Date.now() - startTime }
1641
+ };
1642
+ }
1643
+ }
1644
+ const handler$21 = { async handle(invocation, signal) {
1645
+ const { filePath, action, cellIndex, cellType, content } = invocation.payload;
1646
+ const startTime = Date.now();
1647
+ if (signal.aborted) return {
1648
+ success: false,
1649
+ content: "操作已取消",
1650
+ metadata: { durationMs: Date.now() - startTime }
1651
+ };
1652
+ if (!filePath.endsWith(".ipynb")) return {
1653
+ success: false,
1654
+ content: `不是 .ipynb 文件: ${filePath}`,
1655
+ metadata: { durationMs: Date.now() - startTime }
1656
+ };
1657
+ const notebookOrError = readNotebook(filePath, startTime);
1658
+ if ("success" in notebookOrError) return notebookOrError;
1659
+ const notebook = notebookOrError;
1660
+ if (!Array.isArray(notebook.cells)) notebook.cells = [];
1661
+ switch (action) {
1662
+ case "list": {
1663
+ if (notebook.cells.length === 0) return {
1664
+ success: true,
1665
+ content: "该 notebook 没有单元格。",
1666
+ metadata: { durationMs: Date.now() - startTime }
1667
+ };
1668
+ const lines = notebook.cells.map((cell, i) => {
1669
+ const summary = sourceSummary(cell.source);
1670
+ const execCount = cell.cell_type === "code" && cell.execution_count != null ? ` [执行次数: ${cell.execution_count}]` : "";
1671
+ return `${i}: [${cell.cell_type}]${execCount} ${summary}`;
1672
+ });
1673
+ return {
1674
+ success: true,
1675
+ content: `共 ${notebook.cells.length} 个单元格:\n${lines.join("\n")}`,
1676
+ metadata: { durationMs: Date.now() - startTime }
1677
+ };
1678
+ }
1679
+ case "insert": {
1680
+ if (cellIndex == null) return {
1681
+ success: false,
1682
+ content: "insert 操作需要提供 cellIndex 参数。",
1683
+ metadata: { durationMs: Date.now() - startTime }
1684
+ };
1685
+ if (!cellType || !["code", "markdown"].includes(cellType)) return {
1686
+ success: false,
1687
+ content: "insert 操作需要提供有效的 cellType(code 或 markdown)。",
1688
+ metadata: { durationMs: Date.now() - startTime }
1689
+ };
1690
+ const newCell = {
1691
+ cell_type: cellType,
1692
+ source: content ? normalizeSource(content) : [],
1693
+ metadata: {},
1694
+ outputs: cellType === "code" ? [] : void 0,
1695
+ execution_count: null
1696
+ };
1697
+ const insertIndex = Math.min(cellIndex, notebook.cells.length);
1698
+ notebook.cells.splice(insertIndex, 0, newCell);
1699
+ return writeNotebookAtomically(filePath, notebook, startTime);
1700
+ }
1701
+ case "replace": {
1702
+ if (cellIndex == null) return {
1703
+ success: false,
1704
+ content: "replace 操作需要提供 cellIndex 参数。",
1705
+ metadata: { durationMs: Date.now() - startTime }
1706
+ };
1707
+ if (cellIndex < 0 || cellIndex >= notebook.cells.length) return {
1708
+ success: false,
1709
+ content: `单元格索引 ${cellIndex} 超出范围(0-${notebook.cells.length - 1})。`,
1710
+ metadata: { durationMs: Date.now() - startTime }
1711
+ };
1712
+ const target = notebook.cells[cellIndex];
1713
+ if (content != null) target.source = normalizeSource(content);
1714
+ if (cellType && ["code", "markdown"].includes(cellType)) target.cell_type = cellType;
1715
+ return writeNotebookAtomically(filePath, notebook, startTime);
1716
+ }
1717
+ case "delete": {
1718
+ if (cellIndex == null) return {
1719
+ success: false,
1720
+ content: "delete 操作需要提供 cellIndex 参数。",
1721
+ metadata: { durationMs: Date.now() - startTime }
1722
+ };
1723
+ if (cellIndex < 0 || cellIndex >= notebook.cells.length) return {
1724
+ success: false,
1725
+ content: `单元格索引 ${cellIndex} 超出范围(0-${notebook.cells.length - 1})。`,
1726
+ metadata: { durationMs: Date.now() - startTime }
1727
+ };
1728
+ const removed = notebook.cells[cellIndex];
1729
+ notebook.cells.splice(cellIndex, 1);
1730
+ const result = writeNotebookAtomically(filePath, notebook, startTime);
1731
+ result.content = `已删除单元格 ${cellIndex} [${removed.cell_type}]: ${sourceSummary(removed.source)}\n${result.content}`;
1732
+ return result;
1733
+ }
1734
+ default: return {
1735
+ success: false,
1736
+ content: `不支持的操作: ${action}。支持的操作: list, insert, replace, delete。`,
1737
+ metadata: { durationMs: Date.now() - startTime }
1738
+ };
1739
+ }
1740
+ } };
1741
+ //#endregion
1742
+ //#region src/builtin/files/secret-scan.ts
1743
+ const descriptor$23 = {
1744
+ name: "SecretScanner",
1745
+ description: "扫描文件内容以查找潜在的密钥、令牌和凭证。在写入操作前使用此工具检查敏感信息。检测模式包括:私钥、GitHub token、OpenAI key、Google API key、JWT、通用 API key、密码赋值。",
1746
+ inputSchema: {
1747
+ type: "object",
1748
+ properties: {
1749
+ content: {
1750
+ type: "string",
1751
+ description: "要扫描的文件内容"
1752
+ },
1753
+ filePath: {
1754
+ type: "string",
1755
+ description: "可选的文件路径,用于在结果中标识来源"
1756
+ }
1757
+ },
1758
+ required: ["content"]
1759
+ },
1760
+ kind: "ReadOnly",
1761
+ safety: "Safe",
1762
+ availability: { type: "always" },
1763
+ executor: "core:secret_scan",
1764
+ owner: "core"
1765
+ };
1766
+ /** 预编译的密钥检测正则模式 */
1767
+ const SECRET_PATTERNS$1 = [
1768
+ {
1769
+ name: "私钥块",
1770
+ regex: /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/
1771
+ },
1772
+ {
1773
+ name: "GitHub Token",
1774
+ regex: /gh[pousr]_[A-Za-z0-9_]{36,}/
1775
+ },
1776
+ {
1777
+ name: "OpenAI Key",
1778
+ regex: /sk-[A-Za-z0-9]{32,}/
1779
+ },
1780
+ {
1781
+ name: "Google API Key",
1782
+ regex: /AIza[0-9A-Za-z\-_]{35}/
1783
+ },
1784
+ {
1785
+ name: "JWT Token",
1786
+ regex: /eyJ[A-Za-z0-9\-_]+\.eyJ[A-Za-z0-9\-_]+.[A-Za-z0-9\-_]+/
1787
+ },
1788
+ {
1789
+ name: "通用 API Key",
1790
+ regex: /(?:api[_-]?key|apikey|api[_-]?secret)\s*[:=]\s*['"][A-Za-z0-9_\-]{20,}['"]/gi
1791
+ },
1792
+ {
1793
+ name: "密码赋值",
1794
+ regex: /(?:password|passwd|pwd)\s*[:=]\s*['"][^'"]+['"]/gi
1795
+ }
1796
+ ];
1797
+ /** 对匹配内容进行脱敏:只显示前 4 个字符 + "..." */
1798
+ function redactExcerpt(fullMatch) {
1799
+ if (fullMatch.length <= 8) return fullMatch.slice(0, 4) + "...";
1800
+ return fullMatch.slice(0, 4) + "..." + fullMatch.slice(-4);
1801
+ }
1802
+ const handler$20 = { async handle(invocation, signal) {
1803
+ const { content, filePath } = invocation.payload;
1804
+ const startTime = Date.now();
1805
+ if (signal.aborted) return {
1806
+ success: false,
1807
+ content: "操作已取消",
1808
+ metadata: { durationMs: Date.now() - startTime }
1809
+ };
1810
+ if (!content || content.length === 0) return {
1811
+ success: true,
1812
+ content: "内容为空,无需扫描。",
1813
+ metadata: { durationMs: Date.now() - startTime }
1814
+ };
1815
+ const findings = [];
1816
+ for (const { name, regex } of SECRET_PATTERNS$1) {
1817
+ const re = new RegExp(regex.source, regex.flags);
1818
+ let match;
1819
+ while ((match = re.exec(content)) !== null) {
1820
+ const excerpt = redactExcerpt(match[0]);
1821
+ findings.push({
1822
+ pattern: name,
1823
+ excerpt
1824
+ });
1825
+ }
1826
+ }
1827
+ if (findings.length === 0) return {
1828
+ success: true,
1829
+ content: `未检测到密钥或凭证${filePath ? ` (${filePath})` : ""}。`,
1830
+ metadata: { durationMs: Date.now() - startTime }
1831
+ };
1832
+ const grouped = /* @__PURE__ */ new Map();
1833
+ for (const f of findings) {
1834
+ const excerpts = grouped.get(f.pattern) ?? [];
1835
+ excerpts.push(f.excerpt);
1836
+ grouped.set(f.pattern, excerpts);
1837
+ }
1838
+ const lines = [
1839
+ `检测到 ${findings.length} 个潜在密钥/凭证!`,
1840
+ filePath ? `文件: ${filePath}` : "",
1841
+ ""
1842
+ ];
1843
+ for (const [pattern, excerpts] of grouped) {
1844
+ lines.push(`**${pattern}** (${excerpts.length} 处):`);
1845
+ const unique = [...new Set(excerpts)];
1846
+ for (const excerpt of unique.slice(0, 5)) lines.push(` - ${excerpt}`);
1847
+ if (unique.length > 5) lines.push(` ... 还有 ${unique.length - 5} 处`);
1848
+ lines.push("");
1849
+ }
1850
+ return {
1851
+ success: true,
1852
+ content: lines.join("\n").trimEnd(),
1853
+ metadata: { durationMs: Date.now() - startTime }
1854
+ };
1855
+ } };
1856
+ //#endregion
1857
+ //#region src/builtin/files/lsp.ts
1858
+ /**
1859
+ * LSPTool — 语言服务器协议桥接工具。
1860
+ *
1861
+ * 提供 LSP 诊断、类型信息、跳转定义等功能的最小化接口。
1862
+ * 此工具默认隐藏(需设置 flag "lsp_enabled" 才会暴露)。
1863
+ * 当 IDE 连接后将提供完整的 LSP 集成,目前提供接口契约层。
1864
+ */
1865
+ const descriptor$22 = {
1866
+ name: "LSPTool",
1867
+ description: "查询语言服务器协议(LSP)以获取诊断、类型信息、跳转定义等。支持 diagnostics、hover、definition、references、format 等常见 LSP 操作。",
1868
+ inputSchema: {
1869
+ type: "object",
1870
+ properties: {
1871
+ action: {
1872
+ type: "string",
1873
+ enum: [
1874
+ "diagnostics",
1875
+ "hover",
1876
+ "definition",
1877
+ "references",
1878
+ "format"
1879
+ ],
1880
+ description: "LSP 操作类型:诊断、悬停类型信息、跳转定义、查找引用、格式化"
1881
+ },
1882
+ filePath: {
1883
+ type: "string",
1884
+ description: "要查询的文件绝对路径"
1885
+ },
1886
+ line: {
1887
+ type: "integer",
1888
+ description: "行号(从 1 开始),hover/definition 操作使用"
1889
+ },
1890
+ character: {
1891
+ type: "integer",
1892
+ description: "列号(从 1 开始),hover/definition 操作使用"
1893
+ }
1894
+ },
1895
+ required: ["action", "filePath"]
1896
+ },
1897
+ kind: "ReadOnly",
1898
+ safety: "Safe",
1899
+ availability: {
1900
+ type: "flag",
1901
+ flag: "lsp_enabled"
1902
+ },
1903
+ executor: "core:lsp",
1904
+ owner: "core"
1905
+ };
1906
+ const handler$19 = { async handle(invocation, signal) {
1907
+ const { action, filePath, line, character } = invocation.payload;
1908
+ const startTime = Date.now();
1909
+ if (signal.aborted) return {
1910
+ success: false,
1911
+ content: "LSP 操作已取消",
1912
+ metadata: { durationMs: Date.now() - startTime }
1913
+ };
1914
+ try {
1915
+ switch (action) {
1916
+ case "diagnostics": return await handleDiagnostics(filePath, startTime);
1917
+ case "hover": return handleHover(filePath, line, character, startTime);
1918
+ case "definition": return handleDefinition(filePath, line, character, startTime);
1919
+ case "references": return handleReferences(filePath, line, character, startTime);
1920
+ case "format": return handleFormat(filePath, startTime);
1921
+ default: return {
1922
+ success: false,
1923
+ content: `未知的 LSP 操作:${action}`,
1924
+ metadata: { durationMs: Date.now() - startTime }
1925
+ };
1926
+ }
1927
+ } catch (err) {
1928
+ return {
1929
+ success: false,
1930
+ content: `LSP 操作失败:${err instanceof Error ? err.message : String(err)}`,
1931
+ metadata: { durationMs: Date.now() - startTime }
1932
+ };
1933
+ }
1934
+ } };
1935
+ /**
1936
+ * 解析文件内容并用正则进行基础语法检查。
1937
+ * 真实 LSP 集成在 IDE 连接后提供完整诊断。
1938
+ */
1939
+ async function handleDiagnostics(filePath, startTime) {
1940
+ let content;
1941
+ try {
1942
+ content = await readFile(filePath, "utf-8");
1943
+ } catch {
1944
+ return {
1945
+ success: false,
1946
+ content: `无法读取文件:${filePath}`,
1947
+ metadata: { durationMs: Date.now() - startTime }
1948
+ };
1949
+ }
1950
+ const lines = content.split("\n");
1951
+ const issues = [];
1952
+ for (let i = 0; i < lines.length; i++) {
1953
+ const ln = lines[i];
1954
+ if (countOccurrences(ln, "{") !== countOccurrences(ln, "}")) issues.push(`第 ${i + 1} 行:花括号不匹配`);
1955
+ if (countOccurrences(ln, "(") !== countOccurrences(ln, ")")) issues.push(`第 ${i + 1} 行:圆括号不匹配`);
1956
+ if (countOccurrences(ln, "[") !== countOccurrences(ln, "]")) issues.push(`第 ${i + 1} 行:方括号不匹配`);
1957
+ }
1958
+ return {
1959
+ success: true,
1960
+ content: issues.length > 0 ? `诊断结果(${issues.length} 个问题):\n${issues.join("\n")}` : "未发现基础语法问题。完整 LSP 诊断在 IDE 连接后可用。",
1961
+ metadata: { durationMs: Date.now() - startTime }
1962
+ };
1963
+ }
1964
+ /**
1965
+ * 悬停信息:提示 IDE 连接后可用。
1966
+ */
1967
+ function handleHover(filePath, line, character, startTime) {
1968
+ const pos = line != null ? `第 ${line} 行` : "光标位置";
1969
+ const col = character != null ? `第 ${character} 列` : "";
1970
+ return {
1971
+ success: true,
1972
+ content: `LSP 悬停信息在 IDE 连接后可用。查询位置:${filePath} ${col ? `${pos},${col}` : pos}。当前可用的类型信息需要 IDE 提供 language server 支持。`,
1973
+ metadata: { durationMs: Date.now() - (startTime ?? 0) }
1974
+ };
1975
+ }
1976
+ /**
1977
+ * 跳转定义:返回文件路径和行号提示。
1978
+ */
1979
+ function handleDefinition(filePath, line, character, startTime) {
1980
+ return {
1981
+ success: true,
1982
+ content: `跳转定义在 IDE 连接后可用。源文件:${filePath}${line != null ? `(接近第 ${line} 行)` : ""}。连接 LSP 后将返回精确的定义位置。`,
1983
+ metadata: { durationMs: Date.now() - (startTime ?? 0) }
1984
+ };
1985
+ }
1986
+ /**
1987
+ * 查找引用:返回空列表并说明 IDE 连接后可用。
1988
+ */
1989
+ function handleReferences(filePath, line, character, startTime) {
1990
+ return {
1991
+ success: true,
1992
+ content: `引用查找在 IDE 连接后可用。查询:${filePath}${line != null ? `(第 ${line} 行)` : ""}。当前未找到引用(完整引用分析需要 IDE 的 language server 支持)。`,
1993
+ metadata: { durationMs: Date.now() - (startTime ?? 0) }
1994
+ };
1995
+ }
1996
+ /**
1997
+ * 格式化:提示 IDE 连接后可用。
1998
+ */
1999
+ function handleFormat(filePath, startTime) {
2000
+ return {
2001
+ success: true,
2002
+ content: `格式化功能在 IDE 连接后可用。目标文件:${filePath}。LSP 连接后将提供基于 language server 的代码格式化。`,
2003
+ metadata: { durationMs: Date.now() - startTime }
2004
+ };
2005
+ }
2006
+ /** 统计字符串中某字符的出现次数。 */
2007
+ function countOccurrences(str, char) {
2008
+ let count = 0;
2009
+ for (let i = 0; i < str.length; i++) if (str[i] === char) count++;
2010
+ return count;
2011
+ }
2012
+ //#endregion
2013
+ //#region src/builtin/files/repl.ts
2014
+ /**
2015
+ * REPLTool — 交互式 REPL 代码执行工具。
2016
+ *
2017
+ * 管理持久化 REPL 子进程(Node.js / Python / Bash),
2018
+ * 支持在多次调用之间保持运行状态。
2019
+ * 每个会话分配唯一 UUID,通过模块级 Map 跟踪。
2020
+ */
2021
+ /** 活跃的 REPL 会话映射,key 为 session UUID。 */
2022
+ const activeSessions = /* @__PURE__ */ new Map();
2023
+ const descriptor$21 = {
2024
+ name: "REPLTool",
2025
+ description: "在持久化 REPL 会话中执行代码。启动运行时(Node.js/Python/Bash)并保持状态在多次调用之间。支持 start(启动)、eval(执行)、stop(停止)、status(状态)四种操作。",
2026
+ inputSchema: {
2027
+ type: "object",
2028
+ properties: {
2029
+ action: {
2030
+ type: "string",
2031
+ enum: [
2032
+ "start",
2033
+ "eval",
2034
+ "stop",
2035
+ "status"
2036
+ ],
2037
+ description: "操作类型:启动 REPL、执行代码、停止会话、查看状态"
2038
+ },
2039
+ language: {
2040
+ type: "string",
2041
+ enum: [
2042
+ "node",
2043
+ "python",
2044
+ "bash"
2045
+ ],
2046
+ description: "运行时语言(start 操作时使用)"
2047
+ },
2048
+ code: {
2049
+ type: "string",
2050
+ description: "要执行的代码(eval 操作时使用)"
2051
+ },
2052
+ sessionId: {
2053
+ type: "string",
2054
+ description: "目标会话 ID(eval/stop 操作时使用)"
2055
+ }
2056
+ },
2057
+ required: ["action"]
2058
+ },
2059
+ kind: "ExecutesCode",
2060
+ safety: "RequiresApproval",
2061
+ availability: { type: "always" },
2062
+ executor: "core:repl",
2063
+ owner: "core"
2064
+ };
2065
+ const handler$18 = { async handle(invocation, signal) {
2066
+ const { action, language, code, sessionId } = invocation.payload;
2067
+ const startTime = Date.now();
2068
+ if (signal) signal.addEventListener("abort", () => {
2069
+ for (const [id, session] of activeSessions) {
2070
+ session.process.kill();
2071
+ activeSessions.delete(id);
2072
+ }
2073
+ });
2074
+ try {
2075
+ switch (action) {
2076
+ case "start": return handleStart(language);
2077
+ case "eval": return await handleEval(code, sessionId, signal);
2078
+ case "stop": return handleStop(sessionId);
2079
+ case "status": return handleStatus$1();
2080
+ default: return {
2081
+ success: false,
2082
+ content: `未知操作:${action}`,
2083
+ metadata: { durationMs: Date.now() - startTime }
2084
+ };
2085
+ }
2086
+ } catch (err) {
2087
+ return {
2088
+ success: false,
2089
+ content: `REPL 操作失败:${err instanceof Error ? err.message : String(err)}`,
2090
+ metadata: { durationMs: Date.now() - startTime }
2091
+ };
2092
+ }
2093
+ } };
2094
+ /** 启动一个新的 REPL 会话并返回 session UUID。 */
2095
+ function handleStart(language) {
2096
+ if (!language || ![
2097
+ "node",
2098
+ "python",
2099
+ "bash"
2100
+ ].includes(language)) return {
2101
+ success: false,
2102
+ content: `不支持的语言:${language ?? "未指定"}。支持 node、python、bash。`
2103
+ };
2104
+ const id = randomUUID();
2105
+ let childProcess;
2106
+ switch (language) {
2107
+ case "node":
2108
+ childProcess = spawn("node", ["-i"], {
2109
+ stdio: [
2110
+ "pipe",
2111
+ "pipe",
2112
+ "pipe"
2113
+ ],
2114
+ shell: true
2115
+ });
2116
+ break;
2117
+ case "python":
2118
+ childProcess = spawn("python3", ["-i"], {
2119
+ stdio: [
2120
+ "pipe",
2121
+ "pipe",
2122
+ "pipe"
2123
+ ],
2124
+ shell: true
2125
+ });
2126
+ break;
2127
+ case "bash":
2128
+ childProcess = spawn("bash", [], {
2129
+ stdio: [
2130
+ "pipe",
2131
+ "pipe",
2132
+ "pipe"
2133
+ ],
2134
+ shell: true
2135
+ });
2136
+ break;
2137
+ default: throw new Error(`不支持的语言:${language}`);
2138
+ }
2139
+ const session = {
2140
+ id,
2141
+ language,
2142
+ process: childProcess,
2143
+ createdAt: Date.now()
2144
+ };
2145
+ activeSessions.set(id, session);
2146
+ childProcess.on("exit", () => {
2147
+ activeSessions.delete(id);
2148
+ });
2149
+ return {
2150
+ success: true,
2151
+ content: `REPL 会话已启动(${language})。会话 ID:${id}`
2152
+ };
2153
+ }
2154
+ /** 在现有 REPL 会话中执行代码。 */
2155
+ async function handleEval(code, sessionId, signal) {
2156
+ if (!sessionId) return {
2157
+ success: false,
2158
+ content: "缺少 sessionId 参数"
2159
+ };
2160
+ if (!code) return {
2161
+ success: false,
2162
+ content: "缺少 code 参数"
2163
+ };
2164
+ const session = activeSessions.get(sessionId);
2165
+ if (!session) return {
2166
+ success: false,
2167
+ content: `未找到 REPL 会话:${sessionId}`
2168
+ };
2169
+ return new Promise((resolve) => {
2170
+ const chunks = [];
2171
+ const onData = (data) => chunks.push(data.toString());
2172
+ session.process.stdout?.on("data", onData);
2173
+ const timeout = setTimeout(() => {
2174
+ session.process.stdout?.removeListener("data", onData);
2175
+ resolve({
2176
+ success: true,
2177
+ content: `执行超时(5s),已发送的代码:\n\`\`\`\n${code}\n\`\`\``
2178
+ });
2179
+ }, 5e3);
2180
+ if (signal) {
2181
+ const onAbort = () => {
2182
+ clearTimeout(timeout);
2183
+ session.process.stdout?.removeListener("data", onData);
2184
+ resolve({
2185
+ success: false,
2186
+ content: "REPL 执行已取消"
2187
+ });
2188
+ };
2189
+ signal.addEventListener("abort", onAbort, { once: true });
2190
+ }
2191
+ session.process.stdin?.write(code + "\n");
2192
+ setTimeout(() => {
2193
+ clearTimeout(timeout);
2194
+ session.process.stdout?.removeListener("data", onData);
2195
+ resolve({
2196
+ success: true,
2197
+ content: chunks.join("").trim() || "(无输出)"
2198
+ });
2199
+ }, 1e3);
2200
+ });
2201
+ }
2202
+ /** 停止并移除 REPL 会话。 */
2203
+ function handleStop(sessionId) {
2204
+ if (!sessionId) {
2205
+ let count = 0;
2206
+ for (const [id, session] of activeSessions) {
2207
+ session.process.kill();
2208
+ activeSessions.delete(id);
2209
+ count++;
2210
+ }
2211
+ return {
2212
+ success: true,
2213
+ content: `已停止 ${count} 个 REPL 会话`
2214
+ };
2215
+ }
2216
+ const session = activeSessions.get(sessionId);
2217
+ if (!session) return {
2218
+ success: false,
2219
+ content: `未找到 REPL 会话:${sessionId}`
2220
+ };
2221
+ session.process.kill();
2222
+ activeSessions.delete(sessionId);
2223
+ return {
2224
+ success: true,
2225
+ content: `REPL 会话 ${sessionId} 已停止`
2226
+ };
2227
+ }
2228
+ /** 列出所有活跃会话。 */
2229
+ function handleStatus$1() {
2230
+ if (activeSessions.size === 0) return {
2231
+ success: true,
2232
+ content: "当前无活跃的 REPL 会话"
2233
+ };
2234
+ const lines = [];
2235
+ for (const session of activeSessions.values()) {
2236
+ const age = Math.round((Date.now() - session.createdAt) / 1e3);
2237
+ lines.push(`${session.id} — ${session.language}(运行中,${age} 秒)`);
2238
+ }
2239
+ return {
2240
+ success: true,
2241
+ content: `活跃会话(${activeSessions.size}):\n${lines.join("\n")}`
2242
+ };
2243
+ }
2244
+ //#endregion
2245
+ //#region src/builtin/agent/agent.ts
2246
+ const descriptor$20 = {
2247
+ name: "agent",
2248
+ description: "启动子 Agent 自主处理复杂多步骤任务。每种子 Agent 类型拥有不同能力和可用工具。子 Agent 将最终结果返回给父 Agent。",
2249
+ inputSchema: {
2250
+ type: "object",
2251
+ properties: {
2252
+ description: {
2253
+ type: "string",
2254
+ description: "任务的简短描述(3-5 个词)"
2255
+ },
2256
+ prompt: {
2257
+ type: "string",
2258
+ description: "子 Agent 要执行的任务"
2259
+ },
2260
+ subagent_type: {
2261
+ type: "string",
2262
+ description: "专用 Agent 类型:claude、explore、plan、general-purpose、statusline-setup"
2263
+ },
2264
+ model: {
2265
+ type: "string",
2266
+ enum: [
2267
+ "sonnet",
2268
+ "opus",
2269
+ "haiku"
2270
+ ],
2271
+ description: "可选:为此子 Agent 指定模型覆盖"
2272
+ },
2273
+ isolation: {
2274
+ type: "string",
2275
+ enum: ["worktree"],
2276
+ description: "在隔离的 git worktree 中运行(开销大——仅用于并行文件修改场景)"
2277
+ },
2278
+ allowed_tools: {
2279
+ type: "array",
2280
+ items: { type: "string" },
2281
+ description: "限制子 Agent 可用的工具。留空则继承父级工具。"
2282
+ }
2283
+ },
2284
+ required: ["description", "prompt"]
2285
+ },
2286
+ kind: "ExecutesCode",
2287
+ safety: "RequiresApproval",
2288
+ availability: { type: "always" },
2289
+ executor: "core:agent",
2290
+ owner: "core"
2291
+ };
2292
+ const handler$17 = { async handle(invocation, signal) {
2293
+ const { description, prompt, subagent_type, model, isolation, allowed_tools } = invocation.payload;
2294
+ const startTime = Date.now();
2295
+ if (signal.aborted) return {
2296
+ success: false,
2297
+ content: "子 Agent 已取消",
2298
+ metadata: { durationMs: Date.now() - startTime }
2299
+ };
2300
+ return {
2301
+ success: true,
2302
+ content: [
2303
+ `子 Agent 已调度 [${subagent_type ?? "general-purpose"}]:${description}`,
2304
+ `模型:${model ?? "inherit"}`,
2305
+ `隔离模式:${isolation ?? "none"}`,
2306
+ `工具:${allowed_tools ? allowed_tools.join(", ") : "inherit"}`,
2307
+ "子 Agent 获得父级 token 预算的 1/4。",
2308
+ "",
2309
+ "--- 子 Agent 输出 ---",
2310
+ `[Phase 3 集成:此处将调用真实 spawnSubAgent()。任务:${prompt.slice(0, 200)}]`
2311
+ ].join("\n"),
2312
+ metadata: { durationMs: Date.now() - startTime }
2313
+ };
2314
+ } };
2315
+ //#endregion
2316
+ //#region src/builtin/agent/task.ts
2317
+ const descriptor$19 = {
2318
+ name: "task",
2319
+ description: "管理后台任务——创建、列表、获取、更新或停止任务。任务持久化到 SQLite,会话重启后仍存在。适用于跨多个会话的长时间运行操作。",
2320
+ inputSchema: {
2321
+ type: "object",
2322
+ properties: {
2323
+ action: {
2324
+ type: "string",
2325
+ enum: [
2326
+ "create",
2327
+ "list",
2328
+ "get",
2329
+ "update",
2330
+ "stop"
2331
+ ],
2332
+ description: "CRUD 操作:创建新任务、列出所有任务、获取特定任务、更新状态或停止/取消"
2333
+ },
2334
+ task_id: {
2335
+ type: "string",
2336
+ description: "任务 UUID(get/update/stop 操作必需)"
2337
+ },
2338
+ type: {
2339
+ type: "string",
2340
+ description: "任务类型,例如 'mcp_connect'、'plugin_install'(create 时使用)"
2341
+ },
2342
+ payload: {
2343
+ type: "object",
2344
+ description: "任务负载/数据(create/update 时使用)"
2345
+ },
2346
+ status: {
2347
+ type: "string",
2348
+ enum: [
2349
+ "queued",
2350
+ "running",
2351
+ "completed",
2352
+ "failed",
2353
+ "canceled"
2354
+ ],
2355
+ description: "新状态(update 时使用)"
2356
+ }
2357
+ },
2358
+ required: ["action"]
2359
+ },
2360
+ kind: "ExecutesCode",
2361
+ safety: "WorkspaceSafe",
2362
+ availability: { type: "always" },
2363
+ executor: "core:task",
2364
+ owner: "core"
2365
+ };
2366
+ /**
2367
+ * Inject the real TaskManager backend.
2368
+ *
2369
+ * Called during bootstrap after the engine is created.
2370
+ * Before injection, the handler returns mock results for test compatibility.
2371
+ */
2372
+ function injectTaskManager(tm) {}
2373
+ /**
2374
+ * Create a task handler bound to a specific {@link TaskManagerOps}.
2375
+ *
2376
+ * Factory alternative to {@link injectTaskManager} — useful when the
2377
+ * handler needs to be registered independently of the module‑level singleton.
2378
+ */
2379
+ function createTaskHandler(tm) {
2380
+ return buildHandler(tm);
2381
+ }
2382
+ function buildHandler(tm) {
2383
+ return { async handle(invocation, signal) {
2384
+ const { action, task_id, type, payload, status } = invocation.payload;
2385
+ const startTime = Date.now();
2386
+ if (signal.aborted) return {
2387
+ success: false,
2388
+ content: "任务操作已取消",
2389
+ metadata: { durationMs: Date.now() - startTime }
2390
+ };
2391
+ if (tm) return realDispatch(tm, action, task_id, type, payload, status, startTime);
2392
+ return mockDispatch(action, task_id, type, payload, status, startTime);
2393
+ } };
2394
+ }
2395
+ async function realDispatch(tm, action, taskId, taskType, taskPayload, taskStatus, startTime) {
2396
+ switch (action) {
2397
+ case "create": return {
2398
+ success: true,
2399
+ content: "任务创建由 Agent 引擎内部管理,无需手动创建。使用 'list' 查看当前运行中的任务,使用 'get' 获取特定任务详情,使用 'stop' 停止任务。",
2400
+ metadata: { durationMs: Date.now() - startTime }
2401
+ };
2402
+ case "list": {
2403
+ const tasks = tm.list();
2404
+ if (tasks.length === 0) return {
2405
+ success: true,
2406
+ content: "当前没有运行中的后台任务。",
2407
+ metadata: { durationMs: Date.now() - startTime }
2408
+ };
2409
+ const lines = tasks.map((t) => `- **${t.id}**: ${t.label} (${t.status})`);
2410
+ return {
2411
+ success: true,
2412
+ content: `后台任务列表(共 ${tasks.length} 项):\n\n${lines.join("\n")}`,
2413
+ metadata: { durationMs: Date.now() - startTime }
2414
+ };
2415
+ }
2416
+ case "get": {
2417
+ if (!taskId) return {
2418
+ success: false,
2419
+ content: "缺少必需参数 task_id",
2420
+ metadata: { durationMs: Date.now() - startTime }
2421
+ };
2422
+ const entry = tm.get(taskId);
2423
+ if (!entry) return {
2424
+ success: true,
2425
+ content: `任务 "${taskId}" 未找到。`,
2426
+ metadata: { durationMs: Date.now() - startTime }
2427
+ };
2428
+ const output = tm.getOutput(taskId);
2429
+ const parts = [
2430
+ `**ID**: ${entry.id}`,
2431
+ `**标签**: ${entry.label}`,
2432
+ `**状态**: ${entry.status}`,
2433
+ `**开始时间**: ${new Date(entry.startedAt).toISOString()}`
2434
+ ];
2435
+ if (entry.stoppedAt) parts.push(`**停止时间**: ${new Date(entry.stoppedAt).toISOString()}`);
2436
+ if (entry.error) parts.push(`**错误**: ${entry.error}`);
2437
+ if (entry.progress !== void 0) parts.push(`**进度**: ${entry.progress}%`);
2438
+ if (output) parts.push(`**输出**:\n${output}`);
2439
+ return {
2440
+ success: true,
2441
+ content: parts.join("\n"),
2442
+ metadata: { durationMs: Date.now() - startTime }
2443
+ };
2444
+ }
2445
+ case "update": {
2446
+ if (!taskId) return {
2447
+ success: false,
2448
+ content: "缺少必需参数 task_id",
2449
+ metadata: { durationMs: Date.now() - startTime }
2450
+ };
2451
+ if (!tm.get(taskId)) return {
2452
+ success: true,
2453
+ content: `任务 "${taskId}" 未找到。`,
2454
+ metadata: { durationMs: Date.now() - startTime }
2455
+ };
2456
+ const patch = {};
2457
+ if (taskPayload?.label && typeof taskPayload.label === "string") patch.label = taskPayload.label;
2458
+ if (taskStatus) patch.status = taskStatus;
2459
+ if (Object.keys(patch).length === 0) return {
2460
+ success: false,
2461
+ content: "未提供要更新的字段。请指定 label 或 status。",
2462
+ metadata: { durationMs: Date.now() - startTime }
2463
+ };
2464
+ tm.update(taskId, patch);
2465
+ return {
2466
+ success: true,
2467
+ content: `任务 "${taskId}" 已更新:${JSON.stringify(patch)}`,
2468
+ metadata: { durationMs: Date.now() - startTime }
2469
+ };
2470
+ }
2471
+ case "stop": {
2472
+ if (!taskId) return {
2473
+ success: false,
2474
+ content: "缺少必需参数 task_id",
2475
+ metadata: { durationMs: Date.now() - startTime }
2476
+ };
2477
+ const entry = tm.get(taskId);
2478
+ if (!entry) return {
2479
+ success: true,
2480
+ content: `任务 "${taskId}" 未找到。`,
2481
+ metadata: { durationMs: Date.now() - startTime }
2482
+ };
2483
+ await tm.stop(taskId);
2484
+ return {
2485
+ success: true,
2486
+ content: `任务 "${taskId}"(${entry.label})已停止。`,
2487
+ metadata: { durationMs: Date.now() - startTime }
2488
+ };
2489
+ }
2490
+ default: return {
2491
+ success: false,
2492
+ content: `未知操作:${action}`,
2493
+ metadata: { durationMs: Date.now() - startTime }
2494
+ };
2495
+ }
2496
+ }
2497
+ function mockDispatch(action, taskId, taskType, taskPayload, taskStatus, startTime) {
2498
+ switch (action) {
2499
+ case "create": return {
2500
+ success: true,
2501
+ content: `任务已创建 [mock]:${taskType ?? "generic"} — Phase 4 TaskManager 接入后支持持久化。负载:${JSON.stringify(taskPayload ?? {})}`,
2502
+ metadata: { durationMs: Date.now() - startTime }
2503
+ };
2504
+ case "list": return {
2505
+ success: true,
2506
+ content: "任务列表 [mock]:暂无持久化任务。Phase 4 集成待完成。",
2507
+ metadata: { durationMs: Date.now() - startTime }
2508
+ };
2509
+ case "get": return {
2510
+ success: true,
2511
+ content: `任务 ${taskId} [mock]:未找到或 Phase 4 集成待完成。`,
2512
+ metadata: { durationMs: Date.now() - startTime }
2513
+ };
2514
+ case "update": return {
2515
+ success: true,
2516
+ content: `任务 ${taskId} 已更新为 ${taskStatus ?? "unknown"} [mock]。`,
2517
+ metadata: { durationMs: Date.now() - startTime }
2518
+ };
2519
+ case "stop": return {
2520
+ success: true,
2521
+ content: `任务 ${taskId} 已停止 [mock]。`,
2522
+ metadata: { durationMs: Date.now() - startTime }
2523
+ };
2524
+ default: return {
2525
+ success: false,
2526
+ content: `未知操作:${action}`,
2527
+ metadata: { durationMs: Date.now() - startTime }
2528
+ };
2529
+ }
2530
+ }
2531
+ /** Exported handler — uses injectTaskManager() when wired, mock otherwise. */
2532
+ const handler$16 = buildHandler(null);
2533
+ //#endregion
2534
+ //#region src/builtin/agent/team-create.ts
2535
+ const descriptor$18 = {
2536
+ name: "team_create",
2537
+ description: "创建多 Agent 协作团队。每个成员拥有独立的角色、工具集和模型。团队通过可配置的策略协作完成工作(默认:共识模式)。",
2538
+ inputSchema: {
2539
+ type: "object",
2540
+ properties: {
2541
+ name: {
2542
+ type: "string",
2543
+ description: "团队名称(会话内必须唯一)"
2544
+ },
2545
+ members: {
2546
+ type: "array",
2547
+ items: {
2548
+ type: "object",
2549
+ properties: {
2550
+ name: {
2551
+ type: "string",
2552
+ description: "成员显示名称"
2553
+ },
2554
+ role: {
2555
+ type: "string",
2556
+ description: "角色描述(例如 '安全审查员'、'代码架构师')"
2557
+ },
2558
+ tools: {
2559
+ type: "array",
2560
+ items: { type: "string" },
2561
+ description: "该成员可用的工具(留空 = 全部工具)"
2562
+ },
2563
+ model: {
2564
+ type: "string",
2565
+ description: "该成员的模型覆盖"
2566
+ }
2567
+ },
2568
+ required: ["name", "role"]
2569
+ },
2570
+ description: "团队成员及其角色和能力"
2571
+ },
2572
+ strategy: {
2573
+ type: "string",
2574
+ enum: [
2575
+ "consensus",
2576
+ "majority",
2577
+ "delegated"
2578
+ ],
2579
+ description: "决策策略:consensus(全员同意)、majority(投票)、delegated(领导者决策)"
2580
+ }
2581
+ },
2582
+ required: ["name", "members"]
2583
+ },
2584
+ kind: "ExecutesCode",
2585
+ safety: "RequiresApproval",
2586
+ availability: { type: "always" },
2587
+ executor: "core:team_create",
2588
+ owner: "core"
2589
+ };
2590
+ const handler$15 = { async handle(invocation, signal) {
2591
+ const { name, members, strategy } = invocation.payload;
2592
+ const startTime = Date.now();
2593
+ if (signal.aborted) return {
2594
+ success: false,
2595
+ content: "团队创建已取消",
2596
+ metadata: { durationMs: Date.now() - startTime }
2597
+ };
2598
+ const strat = strategy ?? "consensus";
2599
+ const memberList = members.map((m) => ` - ${m.name} (${m.role})${m.model ? ` [${m.model}]` : ""}`).join("\n");
2600
+ return {
2601
+ success: true,
2602
+ content: [
2603
+ `团队已创建:${name}`,
2604
+ `策略:${strat}`,
2605
+ `成员(${members.length} 人):`,
2606
+ memberList,
2607
+ "",
2608
+ "[Phase 4 集成:通过 @lynx/agent 子 Agent 通道实现真实团队编排]"
2609
+ ].join("\n"),
2610
+ metadata: { durationMs: Date.now() - startTime }
2611
+ };
2612
+ } };
2613
+ //#endregion
2614
+ //#region src/builtin/agent/team-delete.ts
2615
+ const descriptor$17 = {
2616
+ name: "team_delete",
2617
+ description: "删除多 Agent 协作团队。移除团队前会终止所有正在执行的成员任务。",
2618
+ inputSchema: {
2619
+ type: "object",
2620
+ properties: { team_id: {
2621
+ type: "string",
2622
+ description: "团队标识(名称或 UUID)"
2623
+ } },
2624
+ required: ["team_id"]
2625
+ },
2626
+ kind: "ExecutesCode",
2627
+ safety: "RequiresApproval",
2628
+ availability: {
2629
+ type: "all",
2630
+ exprs: [{ type: "always" }]
2631
+ },
2632
+ executor: "core:team_delete",
2633
+ owner: "core"
2634
+ };
2635
+ const handler$14 = { async handle(invocation, signal) {
2636
+ const { team_id } = invocation.payload;
2637
+ const startTime = Date.now();
2638
+ if (signal.aborted) return {
2639
+ success: false,
2640
+ content: "团队删除已取消",
2641
+ metadata: { durationMs: Date.now() - startTime }
2642
+ };
2643
+ return {
2644
+ success: true,
2645
+ content: [
2646
+ `团队已删除:${team_id}`,
2647
+ "所有成员任务已终止。",
2648
+ "[Phase 4 集成:通过 @lynx/agent 实现真实团队生命周期管理]"
2649
+ ].join("\n"),
2650
+ metadata: { durationMs: Date.now() - startTime }
2651
+ };
2652
+ } };
2653
+ //#endregion
2654
+ //#region src/builtin/agent/verify-plan.ts
2655
+ const descriptor$16 = {
2656
+ name: "verify_plan_execution",
2657
+ description: "验证计划执行是否匹配其规范。逐一检查每个检查点的预期输出与实际输出是否一致。在实施计划后使用此工具,确保无遗漏。",
2658
+ inputSchema: {
2659
+ type: "object",
2660
+ properties: {
2661
+ plan_id: {
2662
+ type: "string",
2663
+ description: "要验证的计划标识"
2664
+ },
2665
+ checkpoints: {
2666
+ type: "array",
2667
+ items: {
2668
+ type: "object",
2669
+ properties: {
2670
+ description: {
2671
+ type: "string",
2672
+ description: "该检查点的预期描述"
2673
+ },
2674
+ expected: {
2675
+ type: "string",
2676
+ description: "预期输出或状态"
2677
+ },
2678
+ actual: {
2679
+ type: "string",
2680
+ description: "实际观察到的输出或状态"
2681
+ }
2682
+ },
2683
+ required: [
2684
+ "description",
2685
+ "expected",
2686
+ "actual"
2687
+ ]
2688
+ },
2689
+ description: "待验证的检查点列表"
2690
+ }
2691
+ },
2692
+ required: ["plan_id", "checkpoints"]
2693
+ },
2694
+ kind: "ReadOnly",
2695
+ safety: "Safe",
2696
+ availability: { type: "always" },
2697
+ executor: "core:verify_plan_execution",
2698
+ owner: "core"
2699
+ };
2700
+ const handler$13 = { async handle(invocation, signal) {
2701
+ const { plan_id, checkpoints } = invocation.payload;
2702
+ const startTime = Date.now();
2703
+ if (signal.aborted) return {
2704
+ success: false,
2705
+ content: "验证已取消",
2706
+ metadata: { durationMs: Date.now() - startTime }
2707
+ };
2708
+ const results = checkpoints.map((cp) => {
2709
+ const passed = cp.expected.trim() === cp.actual.trim();
2710
+ return {
2711
+ ...cp,
2712
+ passed
2713
+ };
2714
+ });
2715
+ const passed = results.filter((r) => r.passed).length;
2716
+ const failed = results.filter((r) => !r.passed).length;
2717
+ const total = results.length;
2718
+ return {
2719
+ success: true,
2720
+ content: [
2721
+ `计划验证:${plan_id}`,
2722
+ `结果:${passed}/${total} 通过,${failed} 失败`,
2723
+ "",
2724
+ ...results.map((r) => {
2725
+ return `${r.passed ? "✓" : "✗"} ${r.description}\n 预期:${r.expected.slice(0, 100)}\n 实际:${r.actual.slice(0, 100)}`;
2726
+ })
2727
+ ].join("\n"),
2728
+ metadata: { durationMs: Date.now() - startTime }
2729
+ };
2730
+ } };
2731
+ //#endregion
2732
+ //#region src/builtin/agent/remote-trigger.ts
2733
+ const descriptor$15 = {
2734
+ name: "remote_trigger",
2735
+ description: "通过发送 HTTP 请求(webhook / CI 回调)触发远程任务。支持 GET 和 POST 方法,可自定义请求头和请求体。默认超时 30 秒。",
2736
+ inputSchema: {
2737
+ type: "object",
2738
+ properties: {
2739
+ url: {
2740
+ type: "string",
2741
+ format: "uri",
2742
+ description: "要触发的远程 URL"
2743
+ },
2744
+ method: {
2745
+ type: "string",
2746
+ enum: ["GET", "POST"],
2747
+ description: "HTTP 方法(默认:POST)"
2748
+ },
2749
+ headers: {
2750
+ type: "object",
2751
+ description: "自定义 HTTP 请求头(例如 Authorization、Content-Type)"
2752
+ },
2753
+ body: {
2754
+ type: "string",
2755
+ description: "请求体(POST 时使用)"
2756
+ },
2757
+ timeout: {
2758
+ type: "integer",
2759
+ description: "请求超时时间,毫秒(默认:30000,最大:60000)"
2760
+ }
2761
+ },
2762
+ required: ["url"]
2763
+ },
2764
+ kind: "Network",
2765
+ safety: "RequiresApproval",
2766
+ availability: { type: "always" },
2767
+ executor: "core:remote_trigger",
2768
+ owner: "core"
2769
+ };
2770
+ const handler$12 = { async handle(invocation, signal) {
2771
+ const { url, method, headers, body, timeout } = invocation.payload;
2772
+ const startTime = Date.now();
2773
+ const httpMethod = method ?? "POST";
2774
+ const timeoutMs = Math.min(timeout ?? 3e4, 6e4);
2775
+ if (signal.aborted) return {
2776
+ success: false,
2777
+ content: "远程触发已取消",
2778
+ metadata: { durationMs: Date.now() - startTime }
2779
+ };
2780
+ try {
2781
+ const controller = new AbortController();
2782
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
2783
+ signal.addEventListener("abort", () => {
2784
+ clearTimeout(timeoutId);
2785
+ controller.abort();
2786
+ }, { once: true });
2787
+ const resp = await fetch(url, {
2788
+ method: httpMethod,
2789
+ headers: {
2790
+ "Content-Type": "application/json",
2791
+ "User-Agent": "Lynx/1.0",
2792
+ ...headers
2793
+ },
2794
+ body: httpMethod === "POST" ? body ?? void 0 : void 0,
2795
+ signal: controller.signal
2796
+ });
2797
+ clearTimeout(timeoutId);
2798
+ const respBody = await resp.text().catch(() => "(二进制响应)");
2799
+ const truncated = respBody.length > 2e3;
2800
+ const summary = truncated ? respBody.slice(0, 1997) + "..." : respBody;
2801
+ return {
2802
+ success: resp.ok,
2803
+ content: `HTTP ${resp.status} ${resp.statusText}\n\n${summary}`,
2804
+ metadata: {
2805
+ durationMs: Date.now() - startTime,
2806
+ truncated
2807
+ }
2808
+ };
2809
+ } catch (err) {
2810
+ const message = err instanceof Error ? err.message : String(err);
2811
+ if (err.name === "AbortError" || message.includes("abort")) return {
2812
+ success: false,
2813
+ content: signal.aborted ? "操作已取消" : `请求在 ${timeoutMs}ms 后超时`,
2814
+ metadata: { durationMs: Date.now() - startTime }
2815
+ };
2816
+ return {
2817
+ success: false,
2818
+ content: `远程触发失败:${message}`,
2819
+ metadata: { durationMs: Date.now() - startTime }
2820
+ };
2821
+ }
2822
+ } };
2823
+ //#endregion
2824
+ //#region src/builtin/agent/schedule-cron.ts
2825
+ /**
2826
+ * ScheduleCronTool — 定时任务调度工具。
2827
+ *
2828
+ * 使用 cron 语法安排定时任务。支持一次性提醒和循环计划。
2829
+ * 任务持久化到 ~/.lynx/cron-jobs.json,进程重启后自动恢复。
2830
+ */
2831
+ const STORAGE_DIR$1 = join(homedir(), ".lynx");
2832
+ const STORAGE_FILE$1 = join(STORAGE_DIR$1, "cron-jobs.json");
2833
+ /** 活跃的 cron 任务映射,key 为 job UUID。 */
2834
+ const jobs = /* @__PURE__ */ new Map();
2835
+ /** 初始化:加载持久化任务并启动调度。 */
2836
+ function init$1() {
2837
+ try {
2838
+ const raw = readFileSync(STORAGE_FILE$1, "utf-8");
2839
+ const persisted = JSON.parse(raw);
2840
+ for (const j of persisted) scheduleJob(j.id, j.cron, j.prompt);
2841
+ } catch {}
2842
+ }
2843
+ /** 持久化所有任务到磁盘。 */
2844
+ function persist$1() {
2845
+ mkdirSync(STORAGE_DIR$1, { recursive: true });
2846
+ const arr = [];
2847
+ for (const job of jobs.values()) arr.push({
2848
+ id: job.id,
2849
+ cron: job.cron,
2850
+ prompt: job.prompt
2851
+ });
2852
+ writeFileSync(STORAGE_FILE$1, JSON.stringify(arr, null, 2));
2853
+ }
2854
+ /** 根据 cron 表达式计算下次触发时间(简化实现)。 */
2855
+ function calcNextFire(cron) {
2856
+ const now = /* @__PURE__ */ new Date();
2857
+ if (cron === "@hourly") return now.getTime() + 36e5;
2858
+ if (cron === "@daily") return now.getTime() + 864e5;
2859
+ if (cron === "@weekly") return now.getTime() + 6048e5;
2860
+ const parts = cron.split(/\s+/);
2861
+ if (parts.length !== 5) return now.getTime() + 6e4;
2862
+ const minute = parseField(parts[0], 0, 59);
2863
+ const hour = parseField(parts[1], 0, 23);
2864
+ if (minute !== null && hour !== null) {
2865
+ const next = new Date(now);
2866
+ next.setMinutes(next.getMinutes() + 1);
2867
+ next.setSeconds(0);
2868
+ next.setMilliseconds(0);
2869
+ return next.getTime();
2870
+ }
2871
+ return now.getTime() + 6e4;
2872
+ }
2873
+ /** 解析 cron 字段值,支持 * 和步进语法。 */
2874
+ function parseField(field, _min, _max) {
2875
+ if (field === "*") return 0;
2876
+ const step = field.match(/^\*\/(\d+)$/);
2877
+ if (step) return parseInt(step[1], 10);
2878
+ const num = parseInt(field, 10);
2879
+ if (!isNaN(num)) return num;
2880
+ return null;
2881
+ }
2882
+ /** 调度一个 cron 任务。 */
2883
+ function scheduleJob(id, cron, prompt) {
2884
+ const nextFire = calcNextFire(cron);
2885
+ const delay = Math.max(0, nextFire - Date.now());
2886
+ const job = {
2887
+ id,
2888
+ cron,
2889
+ prompt,
2890
+ nextFire
2891
+ };
2892
+ if (cron.includes("*") || cron.startsWith("@")) job.timer = setInterval(() => {
2893
+ if (Date.now() >= job.nextFire) job.nextFire = calcNextFire(cron);
2894
+ }, 6e4);
2895
+ else job.timer = setTimeout(() => {
2896
+ jobs.delete(id);
2897
+ persist$1();
2898
+ }, delay);
2899
+ jobs.set(id, job);
2900
+ return job;
2901
+ }
2902
+ /** 验证 cron 表达式格式。 */
2903
+ function isValidCron(cron) {
2904
+ if (cron.startsWith("@") && [
2905
+ "@hourly",
2906
+ "@daily",
2907
+ "@weekly"
2908
+ ].includes(cron)) return true;
2909
+ const parts = cron.trim().split(/\s+/);
2910
+ if (parts.length !== 5) return false;
2911
+ return parts.every((p) => {
2912
+ if (p === "*") return true;
2913
+ if (/^\*\/\d+$/.test(p)) return true;
2914
+ return !isNaN(parseInt(p, 10));
2915
+ });
2916
+ }
2917
+ init$1();
2918
+ const descriptor$14 = {
2919
+ name: "ScheduleCronTool",
2920
+ description: "使用 cron 语法安排定时任务。任务在指定时间或间隔触发。支持一次性提醒和循环计划。标准 cron 格式:分 时 日 月 周。也支持 @hourly、@daily、@weekly 别名。",
2921
+ inputSchema: {
2922
+ type: "object",
2923
+ properties: {
2924
+ action: {
2925
+ type: "string",
2926
+ enum: [
2927
+ "create",
2928
+ "list",
2929
+ "delete"
2930
+ ],
2931
+ description: "操作类型:创建任务、列出任务、删除任务"
2932
+ },
2933
+ cron: {
2934
+ type: "string",
2935
+ description: "5 字段 cron 表达式(如 '*/5 * * * *')或别名 @hourly/@daily/@weekly"
2936
+ },
2937
+ prompt: {
2938
+ type: "string",
2939
+ description: "任务触发时入队的提示文本"
2940
+ },
2941
+ jobId: {
2942
+ type: "string",
2943
+ description: "任务 ID(delete 操作时使用)"
2944
+ }
2945
+ },
2946
+ required: ["action"]
2947
+ },
2948
+ kind: "ExecutesCode",
2949
+ safety: "WorkspaceSafe",
2950
+ availability: { type: "always" },
2951
+ executor: "core:schedule_cron",
2952
+ owner: "core"
2953
+ };
2954
+ const handler$11 = { async handle(invocation, signal) {
2955
+ const { action, cron, prompt, jobId } = invocation.payload;
2956
+ const startTime = Date.now();
2957
+ if (signal.aborted) return {
2958
+ success: false,
2959
+ content: "定时任务操作已取消",
2960
+ metadata: { durationMs: Date.now() - startTime }
2961
+ };
2962
+ try {
2963
+ switch (action) {
2964
+ case "create": return handleCreate(cron, prompt);
2965
+ case "list": return handleList$1();
2966
+ case "delete": return handleDelete(jobId);
2967
+ default: return {
2968
+ success: false,
2969
+ content: `未知操作:${action}`,
2970
+ metadata: { durationMs: Date.now() - startTime }
2971
+ };
2972
+ }
2973
+ } catch (err) {
2974
+ return {
2975
+ success: false,
2976
+ content: `定时任务操作失败:${err instanceof Error ? err.message : String(err)}`,
2977
+ metadata: { durationMs: Date.now() - startTime }
2978
+ };
2979
+ }
2980
+ } };
2981
+ /** 创建新的 cron 任务。 */
2982
+ function handleCreate(cron, prompt) {
2983
+ if (!cron || !prompt) return {
2984
+ success: false,
2985
+ content: "缺少必要参数:cron 和 prompt 均为必填"
2986
+ };
2987
+ if (!isValidCron(cron)) return {
2988
+ success: false,
2989
+ content: `无效的 cron 表达式:${cron}。格式:分 时 日 月 周(5 字段),或 @hourly/@daily/@weekly`
2990
+ };
2991
+ const id = randomUUID();
2992
+ const job = scheduleJob(id, cron, prompt);
2993
+ persist$1();
2994
+ return {
2995
+ success: true,
2996
+ content: `定时任务已创建。ID:${id}\n计划:${cron}\n下次触发:${new Date(job.nextFire).toISOString()}`
2997
+ };
2998
+ }
2999
+ /** 列出所有活跃任务。 */
3000
+ function handleList$1() {
3001
+ if (jobs.size === 0) return {
3002
+ success: true,
3003
+ content: "当前无定时任务"
3004
+ };
3005
+ const lines = [];
3006
+ for (const job of jobs.values()) {
3007
+ const nextDate = new Date(job.nextFire).toISOString();
3008
+ lines.push(`${job.id} — ${job.cron} — 下次:${nextDate} — "${job.prompt.slice(0, 40)}"`);
3009
+ }
3010
+ return {
3011
+ success: true,
3012
+ content: `定时任务(${jobs.size}):\n${lines.join("\n")}`
3013
+ };
3014
+ }
3015
+ /** 删除指定任务。 */
3016
+ function handleDelete(jobId) {
3017
+ if (!jobId) return {
3018
+ success: false,
3019
+ content: "缺少 jobId 参数"
3020
+ };
3021
+ const job = jobs.get(jobId);
3022
+ if (!job) return {
3023
+ success: false,
3024
+ content: `未找到任务:${jobId}`
3025
+ };
3026
+ if (job.timer) {
3027
+ clearTimeout(job.timer);
3028
+ clearInterval(job.timer);
3029
+ }
3030
+ jobs.delete(jobId);
3031
+ persist$1();
3032
+ return {
3033
+ success: true,
3034
+ content: `任务 ${jobId} 已删除`
3035
+ };
3036
+ }
3037
+ //#endregion
3038
+ //#region src/builtin/agent/workflow.ts
3039
+ /**
3040
+ * WorkflowTool — 多步骤工作流定义和执行工具。
3041
+ *
3042
+ * 支持定义、运行、查看状态和列出工作流。
3043
+ * 工作流持久化到 ~/.lynx/workflows.json,进程重启后保留定义。
3044
+ */
3045
+ const STORAGE_DIR = join(homedir(), ".lynx");
3046
+ const STORAGE_FILE = join(STORAGE_DIR, "workflows.json");
3047
+ /** 活跃的工作流映射,key 为 workflow UUID。 */
3048
+ const workflows = /* @__PURE__ */ new Map();
3049
+ /** 初始化:从磁盘加载持久化工作流。 */
3050
+ function init() {
3051
+ try {
3052
+ const raw = readFileSync(STORAGE_FILE, "utf-8");
3053
+ const persisted = JSON.parse(raw);
3054
+ for (const w of persisted) workflows.set(w.id, w);
3055
+ } catch {}
3056
+ }
3057
+ /** 持久化所有工作流到磁盘。 */
3058
+ function persist() {
3059
+ mkdirSync(STORAGE_DIR, { recursive: true });
3060
+ writeFileSync(STORAGE_FILE, JSON.stringify(Array.from(workflows.values()), null, 2));
3061
+ }
3062
+ init();
3063
+ const descriptor$13 = {
3064
+ name: "WorkflowTool",
3065
+ description: "定义并执行多步骤工作流。每个步骤可以是一个工具调用,步骤之间可以有依赖关系。支持 define(定义)、run(运行)、status(查看状态)、list(列出所有)四种操作。",
3066
+ inputSchema: {
3067
+ type: "object",
3068
+ properties: {
3069
+ action: {
3070
+ type: "string",
3071
+ enum: [
3072
+ "define",
3073
+ "run",
3074
+ "status",
3075
+ "list"
3076
+ ],
3077
+ description: "操作类型:定义工作流、运行工作流、查看状态、列出所有工作流"
3078
+ },
3079
+ workflowId: {
3080
+ type: "string",
3081
+ description: "工作流 ID(run/status 操作时使用)"
3082
+ },
3083
+ name: {
3084
+ type: "string",
3085
+ description: "工作流名称(define 操作时使用)"
3086
+ },
3087
+ steps: {
3088
+ type: "array",
3089
+ items: {
3090
+ type: "object",
3091
+ properties: {
3092
+ name: {
3093
+ type: "string",
3094
+ description: "步骤名称"
3095
+ },
3096
+ toolName: {
3097
+ type: "string",
3098
+ description: "要调用的工具名称"
3099
+ },
3100
+ args: {
3101
+ type: "object",
3102
+ description: "工具参数"
3103
+ }
3104
+ },
3105
+ required: [
3106
+ "name",
3107
+ "toolName",
3108
+ "args"
3109
+ ]
3110
+ },
3111
+ description: "工作流步骤列表(define 操作时使用)"
3112
+ }
3113
+ },
3114
+ required: ["action"]
3115
+ },
3116
+ kind: "ExecutesCode",
3117
+ safety: "WorkspaceSafe",
3118
+ availability: { type: "always" },
3119
+ executor: "core:workflow",
3120
+ owner: "core"
3121
+ };
3122
+ const handler$10 = { async handle(invocation, signal) {
3123
+ const { action, workflowId, name, steps } = invocation.payload;
3124
+ const startTime = Date.now();
3125
+ if (signal.aborted) return {
3126
+ success: false,
3127
+ content: "工作流操作已取消",
3128
+ metadata: { durationMs: Date.now() - startTime }
3129
+ };
3130
+ try {
3131
+ switch (action) {
3132
+ case "define": return handleDefine(name, steps);
3133
+ case "run": return handleRun(workflowId);
3134
+ case "status": return handleStatus(workflowId);
3135
+ case "list": return handleList();
3136
+ default: return {
3137
+ success: false,
3138
+ content: `未知操作:${action}`,
3139
+ metadata: { durationMs: Date.now() - startTime }
3140
+ };
3141
+ }
3142
+ } catch (err) {
3143
+ return {
3144
+ success: false,
3145
+ content: `工作流操作失败:${err instanceof Error ? err.message : String(err)}`,
3146
+ metadata: { durationMs: Date.now() - startTime }
3147
+ };
3148
+ }
3149
+ } };
3150
+ /** 定义新工作流。 */
3151
+ function handleDefine(name, steps) {
3152
+ if (!name || !steps || steps.length === 0) return {
3153
+ success: false,
3154
+ content: "缺少必要参数:name 和 steps 均为必填,且 steps 不能为空"
3155
+ };
3156
+ const id = randomUUID();
3157
+ const workflow = {
3158
+ id,
3159
+ name,
3160
+ steps,
3161
+ createdAt: Date.now()
3162
+ };
3163
+ workflows.set(id, workflow);
3164
+ persist();
3165
+ return {
3166
+ success: true,
3167
+ content: `工作流已定义。ID:${id}\n名称:${name}\n步骤:\n${steps.map((s, i) => ` ${i + 1}. ${s.name} — 调用 ${s.toolName}`).join("\n")}`
3168
+ };
3169
+ }
3170
+ /** 运行工作流:返回步骤列表供 agent 按顺序执行。 */
3171
+ function handleRun(workflowId) {
3172
+ if (!workflowId) return {
3173
+ success: false,
3174
+ content: "缺少 workflowId 参数"
3175
+ };
3176
+ const workflow = workflows.get(workflowId);
3177
+ if (!workflow) return {
3178
+ success: false,
3179
+ content: `未找到工作流:${workflowId}`
3180
+ };
3181
+ const stepInstructions = workflow.steps.map((s, i) => {
3182
+ const argsStr = JSON.stringify(s.args);
3183
+ return `${i + 1}. **${s.name}** — 调用工具 \`${s.toolName}\`,参数:\`${argsStr}\``;
3184
+ }).join("\n");
3185
+ return {
3186
+ success: true,
3187
+ content: [
3188
+ `开始运行工作流:${workflow.name}(ID: ${workflow.id})`,
3189
+ "",
3190
+ "按以下步骤依次执行:",
3191
+ stepInstructions,
3192
+ "",
3193
+ "请按顺序执行每个步骤,等待每步完成后再进行下一步。"
3194
+ ].join("\n")
3195
+ };
3196
+ }
3197
+ /** 查看工作流状态。 */
3198
+ function handleStatus(workflowId) {
3199
+ if (!workflowId) return {
3200
+ success: false,
3201
+ content: "缺少 workflowId 参数"
3202
+ };
3203
+ const workflow = workflows.get(workflowId);
3204
+ if (!workflow) return {
3205
+ success: false,
3206
+ content: `未找到工作流:${workflowId}`
3207
+ };
3208
+ const createdDate = new Date(workflow.createdAt).toISOString();
3209
+ const stepSummary = workflow.steps.map((s, i) => `${i + 1}. ${s.name}(工具:${s.toolName})`).join("\n");
3210
+ return {
3211
+ success: true,
3212
+ content: [
3213
+ `工作流:${workflow.name}(ID: ${workflow.id})`,
3214
+ `创建时间:${createdDate}`,
3215
+ `步骤(${workflow.steps.length}):`,
3216
+ stepSummary
3217
+ ].join("\n")
3218
+ };
3219
+ }
3220
+ /** 列出所有工作流。 */
3221
+ function handleList() {
3222
+ if (workflows.size === 0) return {
3223
+ success: true,
3224
+ content: "暂无定义的工作流"
3225
+ };
3226
+ const lines = [];
3227
+ for (const w of workflows.values()) {
3228
+ const date = new Date(w.createdAt).toISOString().slice(0, 10);
3229
+ lines.push(`${w.id} — ${w.name}(${w.steps.length} 步骤,创建于 ${date})`);
3230
+ }
3231
+ return {
3232
+ success: true,
3233
+ content: `工作流(${workflows.size}):\n${lines.join("\n")}`
3234
+ };
3235
+ }
3236
+ //#endregion
3237
+ //#region src/builtin/agent/tungsten.ts
3238
+ const descriptor$12 = {
3239
+ name: "TungstenTool",
3240
+ description: "重型复合工具:并行调度多个子 Agent 处理复杂任务的各个部分,然后综合结果。支持 parallel(并行)、pipeline(流水线)、debate(辩论)三种策略。适用于代码审查、多模块重构、全面调研等大规模任务。",
3241
+ inputSchema: {
3242
+ type: "object",
3243
+ properties: {
3244
+ task: {
3245
+ type: "string",
3246
+ description: "要执行的复杂任务描述"
3247
+ },
3248
+ maxAgents: {
3249
+ type: "number",
3250
+ description: "最大并行 Agent 数量(默认 3)"
3251
+ },
3252
+ strategy: {
3253
+ type: "string",
3254
+ enum: [
3255
+ "parallel",
3256
+ "pipeline",
3257
+ "debate"
3258
+ ],
3259
+ description: "执行策略:parallel 并行执行、pipeline 流水线处理、debate 多角度辩论"
3260
+ }
3261
+ },
3262
+ required: ["task"]
3263
+ },
3264
+ kind: "ExecutesCode",
3265
+ safety: "WorkspaceSafe",
3266
+ availability: { type: "always" },
3267
+ executor: "core:tungsten",
3268
+ owner: "core"
3269
+ };
3270
+ const handler$9 = { async handle(invocation, signal) {
3271
+ const { task, maxAgents, strategy } = invocation.payload;
3272
+ const startTime = Date.now();
3273
+ if (signal.aborted) return {
3274
+ success: false,
3275
+ content: "Tungsten 操作已取消",
3276
+ metadata: { durationMs: Date.now() - startTime }
3277
+ };
3278
+ if (!task || task.trim().length === 0) return {
3279
+ success: false,
3280
+ content: "缺少必要参数:task 为必填且不能为空",
3281
+ metadata: { durationMs: Date.now() - startTime }
3282
+ };
3283
+ try {
3284
+ const agentCount = Math.min(Math.max(maxAgents ?? 3, 1), 10);
3285
+ const execStrategy = strategy ?? "parallel";
3286
+ return {
3287
+ success: true,
3288
+ content: buildExecutionPlan(task, decomposeTask(task, agentCount), agentCount, execStrategy),
3289
+ metadata: { durationMs: Date.now() - startTime }
3290
+ };
3291
+ } catch (err) {
3292
+ return {
3293
+ success: false,
3294
+ content: `Tungsten 任务规划失败:${err instanceof Error ? err.message : String(err)}`,
3295
+ metadata: { durationMs: Date.now() - startTime }
3296
+ };
3297
+ }
3298
+ } };
3299
+ /**
3300
+ * 将任务拆解为子任务。
3301
+ *
3302
+ * 按语义标记(编号列表、段落、主题关键词)进行简单拆分。
3303
+ * 真实场景中可接入 LLM 辅助拆解以获得更精准的结果。
3304
+ */
3305
+ function decomposeTask(task, maxAgents) {
3306
+ const numberedPattern = /(?:^|\n)\s*(?:\d+[.)]\s*|[-*]\s+)(.+)/g;
3307
+ const matches = [];
3308
+ let match;
3309
+ while ((match = numberedPattern.exec(task)) !== null) matches.push(match[1].trim());
3310
+ if (matches.length >= 2) {
3311
+ const count = Math.min(matches.length, maxAgents);
3312
+ return matches.slice(0, count).map((m, i) => ({
3313
+ id: i + 1,
3314
+ name: m.slice(0, 40),
3315
+ description: m,
3316
+ suggestedAgentRole: `${i + 1} 号子 Agent`
3317
+ }));
3318
+ }
3319
+ const paragraphs = task.split(/\n\s*\n/).map((p) => p.trim()).filter((p) => p.length > 0);
3320
+ if (paragraphs.length >= 2) {
3321
+ const count = Math.min(paragraphs.length, maxAgents);
3322
+ return paragraphs.slice(0, count).map((p) => p.slice(0, 50)).map((n, i) => ({
3323
+ id: i + 1,
3324
+ name: n,
3325
+ description: paragraphs[i],
3326
+ suggestedAgentRole: `子 Agent ${i + 1}`
3327
+ }));
3328
+ }
3329
+ const sentences = task.split(/[..。!!??]+/).filter((s) => s.trim().length > 10);
3330
+ if (sentences.length >= 2) {
3331
+ const count = Math.min(sentences.length, maxAgents);
3332
+ return sentences.slice(0, count).map((s, i) => ({
3333
+ id: i + 1,
3334
+ name: s.trim().slice(0, 40),
3335
+ description: s.trim(),
3336
+ suggestedAgentRole: `子 Agent ${i + 1}`
3337
+ }));
3338
+ }
3339
+ return [{
3340
+ id: 1,
3341
+ name: task.slice(0, 40),
3342
+ description: task,
3343
+ suggestedAgentRole: "主执行 Agent"
3344
+ }];
3345
+ }
3346
+ /** 根据策略生成执行计划文本。 */
3347
+ function buildExecutionPlan(task, subtasks, maxAgents, strategy) {
3348
+ const planLines = [
3349
+ "═══════════════════════════════════════",
3350
+ " Tungsten 并行执行计划",
3351
+ "═══════════════════════════════════════",
3352
+ "",
3353
+ `总任务:${task.slice(0, 120)}${task.length > 120 ? "..." : ""}`,
3354
+ `执行策略:${strategyLabel(strategy)}`,
3355
+ `并行 Agent 数:${maxAgents}`,
3356
+ `子任务数:${subtasks.length}`,
3357
+ "",
3358
+ "── 子任务分配 ──",
3359
+ ""
3360
+ ];
3361
+ for (const st of subtasks) {
3362
+ planLines.push(`[${st.id}] ${st.name}`);
3363
+ planLines.push(` 角色:${st.suggestedAgentRole}`);
3364
+ planLines.push(` 描述:${st.description.slice(0, 100)}`);
3365
+ planLines.push("");
3366
+ }
3367
+ planLines.push("── 执行指令 ──");
3368
+ planLines.push("");
3369
+ switch (strategy) {
3370
+ case "parallel":
3371
+ planLines.push("所有子 Agent 同时并行执行各自的子任务。", `请为每个子任务创建独立的 Agent,共 ${subtasks.length} 个,同时启动。`, "完成后,由协调 Agent 综合所有结果形成最终输出。");
3372
+ break;
3373
+ case "pipeline":
3374
+ planLines.push("子任务按顺序流水线执行,每个子 Agent 的输出作为下一个的输入。", `请依次启动 ${subtasks.length} 个 Agent,每个等待前一个完成后再开始。`, "流水线末端的 Agent 产出最终结果。");
3375
+ break;
3376
+ case "debate":
3377
+ planLines.push(`每个子 Agent 从不同角度处理任务,共 ${subtasks.length} 个视角。`, "各 Agent 独立产出观点/方案后,由协调 Agent 对比、批判、综合。", "最终输出应包括不同观点的对比分析和综合结论。");
3378
+ break;
3379
+ }
3380
+ planLines.push("");
3381
+ planLines.push("── 综合指令 ──");
3382
+ planLines.push("所有子任务完成后,将结果合并为统一的最终输出。");
3383
+ planLines.push("═══════════════════════════════════════");
3384
+ return planLines.join("\n");
3385
+ }
3386
+ /** 策略的中文标签。 */
3387
+ function strategyLabel(strategy) {
3388
+ switch (strategy) {
3389
+ case "parallel": return "并行";
3390
+ case "pipeline": return "流水线";
3391
+ case "debate": return "辩论";
3392
+ default: return strategy;
3393
+ }
3394
+ }
3395
+ //#endregion
3396
+ //#region src/builtin/mode/enter-plan-mode.ts
3397
+ const descriptor$11 = {
3398
+ name: "enter_plan_mode",
3399
+ description: "进入计划模式 — 切换为只读模式,Agent 只能读取文件和输出设计方案。所有写入/编辑/执行工具将自动被拒绝。在进行重大架构变更前使用此模式,以便在实施前获得用户对方案的认可。",
3400
+ inputSchema: {
3401
+ type: "object",
3402
+ properties: {},
3403
+ required: []
3404
+ },
3405
+ kind: "ReadOnly",
3406
+ safety: "Safe",
3407
+ availability: { type: "always" },
3408
+ executor: "core:enter_plan_mode",
3409
+ owner: "core"
3410
+ };
3411
+ const handler$8 = { async handle(_invocation, signal) {
3412
+ const startTime = Date.now();
3413
+ if (signal.aborted) return {
3414
+ success: false,
3415
+ content: "Operation cancelled",
3416
+ metadata: { durationMs: Date.now() - startTime }
3417
+ };
3418
+ return {
3419
+ success: true,
3420
+ content: [
3421
+ "已进入计划模式。",
3422
+ "- 所有写入/编辑/执行工具当前已被拒绝。",
3423
+ "- 您可执行:读取文件、搜索代码、设计架构、编写计划。",
3424
+ "- 使用 exit_plan_mode 返回普通模式。"
3425
+ ].join("\n"),
3426
+ metadata: { durationMs: Date.now() - startTime }
3427
+ };
3428
+ } };
3429
+ //#endregion
3430
+ //#region src/builtin/mode/exit-plan-mode.ts
3431
+ const descriptor$10 = {
3432
+ name: "exit_plan_mode",
3433
+ description: "退出计划模式,返回普通模式(写入工具重新启用)。计划方案将在执行前展示给用户审批。",
3434
+ inputSchema: {
3435
+ type: "object",
3436
+ properties: {},
3437
+ required: []
3438
+ },
3439
+ kind: "ReadOnly",
3440
+ safety: "Safe",
3441
+ availability: { type: "always" },
3442
+ executor: "core:exit_plan_mode",
3443
+ owner: "core"
3444
+ };
3445
+ const handler$7 = { async handle(_invocation, signal) {
3446
+ const startTime = Date.now();
3447
+ if (signal.aborted) return {
3448
+ success: false,
3449
+ content: "Operation cancelled",
3450
+ metadata: { durationMs: Date.now() - startTime }
3451
+ };
3452
+ return {
3453
+ success: true,
3454
+ content: [
3455
+ "已退出计划模式。",
3456
+ "- 写入/编辑/执行工具已重新启用。",
3457
+ "- 计划方案已就绪,等待用户审批并执行。"
3458
+ ].join("\n"),
3459
+ metadata: { durationMs: Date.now() - startTime }
3460
+ };
3461
+ } };
3462
+ //#endregion
3463
+ //#region src/builtin/mode/enter-worktree.ts
3464
+ const descriptor$9 = {
3465
+ name: "enter_worktree",
3466
+ description: "创建新的 git worktree 或进入已有 worktree 进行隔离工作。Worktree 存放在 .claude/worktrees/ 目录下。使用 name 参数创建新 worktree,使用 path 参数进入已有 worktree。",
3467
+ inputSchema: {
3468
+ type: "object",
3469
+ properties: {
3470
+ name: {
3471
+ type: "string",
3472
+ description: "新 worktree 的名称(与 path 互斥)。不填则自动生成随机名称。"
3473
+ },
3474
+ path: {
3475
+ type: "string",
3476
+ description: "要进入的已有 worktree 的路径(与 name 互斥)。"
3477
+ },
3478
+ base_ref: {
3479
+ type: "string",
3480
+ description: "分支的基准引用(默认:'fresh' 模式基于 origin/master,'head' 模式基于 HEAD)"
3481
+ }
3482
+ },
3483
+ required: []
3484
+ },
3485
+ kind: "ExecutesCode",
3486
+ safety: "WorkspaceSafe",
3487
+ availability: {
3488
+ type: "all",
3489
+ exprs: [{ type: "always" }]
3490
+ },
3491
+ executor: "core:enter_worktree",
3492
+ owner: "core"
3493
+ };
3494
+ const handler$6 = { async handle(invocation, signal) {
3495
+ const { name, path, base_ref } = invocation.payload;
3496
+ const startTime = Date.now();
3497
+ if (signal.aborted) return {
3498
+ success: false,
3499
+ content: "Worktree 操作已取消",
3500
+ metadata: { durationMs: Date.now() - startTime }
3501
+ };
3502
+ if (name && path) return {
3503
+ success: false,
3504
+ content: "name 与 path 互斥。使用 name 创建新 worktree,或使用 path 进入已有 worktree。",
3505
+ metadata: { durationMs: Date.now() - startTime }
3506
+ };
3507
+ if (path) return {
3508
+ success: true,
3509
+ content: `已进入已有 worktree:${path}\n[Phase 4: 通过 @lynx/agent 实现真正的 git worktree 切换]`,
3510
+ metadata: { durationMs: Date.now() - startTime }
3511
+ };
3512
+ const worktreeName = name ?? `worktree-${Date.now().toString(36)}`;
3513
+ const ref = base_ref ?? "fresh";
3514
+ return {
3515
+ success: true,
3516
+ content: [
3517
+ `Worktree 已创建:${worktreeName}`,
3518
+ `基准引用:${ref}`,
3519
+ `位置:.claude/worktrees/${worktreeName}`,
3520
+ "",
3521
+ "[Phase 4: 通过 @lynx/agent 实现真正的 git worktree 创建 + 工作目录切换]"
3522
+ ].join("\n"),
3523
+ metadata: { durationMs: Date.now() - startTime }
3524
+ };
3525
+ } };
3526
+ //#endregion
3527
+ //#region src/builtin/mode/exit-worktree.ts
3528
+ const descriptor$8 = {
3529
+ name: "exit_worktree",
3530
+ description: "退出 git worktree 并返回原始目录。使用 'keep' 保留 worktree,或使用 'remove' 删除(如有未提交变更需搭配 discard_changes)。",
3531
+ inputSchema: {
3532
+ type: "object",
3533
+ properties: {
3534
+ action: {
3535
+ type: "string",
3536
+ enum: ["keep", "remove"],
3537
+ description: "keep = 保留 worktree 不动,remove = 删除 worktree 及分支"
3538
+ },
3539
+ discard_changes: {
3540
+ type: "boolean",
3541
+ description: "当 action=remove 且 worktree 有未提交变更时必须设为 true"
3542
+ }
3543
+ },
3544
+ required: ["action"]
3545
+ },
3546
+ kind: "ExecutesCode",
3547
+ safety: "WorkspaceSafe",
3548
+ availability: {
3549
+ type: "all",
3550
+ exprs: [{ type: "always" }]
3551
+ },
3552
+ executor: "core:exit_worktree",
3553
+ owner: "core"
3554
+ };
3555
+ const handler$5 = { async handle(invocation, signal) {
3556
+ const { action, discard_changes } = invocation.payload;
3557
+ const startTime = Date.now();
3558
+ if (signal.aborted) return {
3559
+ success: false,
3560
+ content: "Worktree 操作已取消",
3561
+ metadata: { durationMs: Date.now() - startTime }
3562
+ };
3563
+ if (action === "keep") return {
3564
+ success: true,
3565
+ content: "已退出 worktree(保留在磁盘上)。已返回原始目录。\n[Phase 4: 通过 @lynx/agent 实现真正的 worktree 退出]",
3566
+ metadata: { durationMs: Date.now() - startTime }
3567
+ };
3568
+ if (action === "remove" && !discard_changes) return {
3569
+ success: false,
3570
+ content: "Worktree 可能有未提交的变更。设置 discard_changes: true 强制删除,或使用 action: 'keep' 保留。",
3571
+ metadata: { durationMs: Date.now() - startTime }
3572
+ };
3573
+ return {
3574
+ success: true,
3575
+ content: "Worktree 已删除(目录及分支均已删除)。已返回原始目录。\n[Phase 4: 通过 @lynx/agent 实现真正的 worktree 删除]",
3576
+ metadata: { durationMs: Date.now() - startTime }
3577
+ };
3578
+ } };
3579
+ //#endregion
3580
+ //#region src/builtin/mode/config-tool.ts
3581
+ /**
3582
+ * ConfigTool — 读取或修改 Lynx 配置。
3583
+ *
3584
+ * 配置存储在 ~/.lynx/config.json。支持四种操作:
3585
+ * - get: 获取指定 key 的配置值
3586
+ * - set: 设置指定 key 的配置值(原子写入)
3587
+ * - list: 列出所有配置项
3588
+ * - path: 返回配置文件路径
3589
+ */
3590
+ const descriptor$7 = {
3591
+ name: "ConfigTool",
3592
+ description: "读取或修改 Lynx 配置。可以查看当前配置值或设置新的配置项。支持四种操作:get(获取单个配置项)、set(设置配置项)、list(列出所有配置项)、path(查看配置文件路径)。",
3593
+ inputSchema: {
3594
+ type: "object",
3595
+ properties: {
3596
+ action: {
3597
+ type: "string",
3598
+ enum: [
3599
+ "get",
3600
+ "set",
3601
+ "list",
3602
+ "path"
3603
+ ],
3604
+ description: "操作类型:get(获取)、set(设置)、list(列出全部)、path(查看路径)"
3605
+ },
3606
+ key: {
3607
+ type: "string",
3608
+ description: "配置项的键名(get/set 操作需要)"
3609
+ },
3610
+ value: {
3611
+ type: "string",
3612
+ description: "要设置的值(仅 set 操作需要)"
3613
+ }
3614
+ },
3615
+ required: ["action"]
3616
+ },
3617
+ kind: "WritesFiles",
3618
+ safety: "WorkspaceSafe",
3619
+ availability: { type: "always" },
3620
+ executor: "core:config_tool",
3621
+ owner: "core"
3622
+ };
3623
+ /** 获取配置文件路径(POSIX 风格)。 */
3624
+ function resolveConfigPath() {
3625
+ return join(homedir(), ".lynx", "config.json").replace(/\\/g, "/");
3626
+ }
3627
+ /** 加载当前配置,文件不存在时返回空对象。 */
3628
+ function loadConfig(path) {
3629
+ if (!existsSync(path)) return {};
3630
+ try {
3631
+ return JSON.parse(readFileSync(path, "utf-8"));
3632
+ } catch {
3633
+ renameSync(path, path + ".bak");
3634
+ return {};
3635
+ }
3636
+ }
3637
+ /** 原子写入配置(先写临时文件再重命名)。 */
3638
+ function saveConfig(path, config) {
3639
+ const dir = dirname(path);
3640
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
3641
+ const tmpPath = path + ".tmp";
3642
+ writeFileSync(tmpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
3643
+ renameSync(tmpPath, path);
3644
+ }
3645
+ const handler$4 = { async handle(invocation, signal) {
3646
+ const { action, key, value } = invocation.payload;
3647
+ const startTime = Date.now();
3648
+ if (signal.aborted) return {
3649
+ success: false,
3650
+ content: "操作已取消",
3651
+ metadata: { durationMs: Date.now() - startTime }
3652
+ };
3653
+ const configPath = resolveConfigPath();
3654
+ switch (action) {
3655
+ case "path": return {
3656
+ success: true,
3657
+ content: configPath,
3658
+ metadata: { durationMs: Date.now() - startTime }
3659
+ };
3660
+ case "list": {
3661
+ const config = loadConfig(configPath);
3662
+ const keys = Object.keys(config);
3663
+ if (keys.length === 0) return {
3664
+ success: true,
3665
+ content: "当前没有配置项。",
3666
+ metadata: { durationMs: Date.now() - startTime }
3667
+ };
3668
+ return {
3669
+ success: true,
3670
+ content: keys.map((k) => `${k} = ${JSON.stringify(config[k])}`).join("\n"),
3671
+ metadata: { durationMs: Date.now() - startTime }
3672
+ };
3673
+ }
3674
+ case "get": {
3675
+ if (!key) return {
3676
+ success: false,
3677
+ content: "get 操作需要提供 key 参数。",
3678
+ metadata: { durationMs: Date.now() - startTime }
3679
+ };
3680
+ const config = loadConfig(configPath);
3681
+ if (!(key in config)) return {
3682
+ success: false,
3683
+ content: `配置项 "${key}" 不存在。`,
3684
+ metadata: { durationMs: Date.now() - startTime }
3685
+ };
3686
+ return {
3687
+ success: true,
3688
+ content: JSON.stringify(config[key]),
3689
+ metadata: { durationMs: Date.now() - startTime }
3690
+ };
3691
+ }
3692
+ case "set": {
3693
+ if (!key) return {
3694
+ success: false,
3695
+ content: "set 操作需要提供 key 参数。",
3696
+ metadata: { durationMs: Date.now() - startTime }
3697
+ };
3698
+ const config = loadConfig(configPath);
3699
+ const setValue = value ?? "";
3700
+ config[key] = setValue;
3701
+ try {
3702
+ saveConfig(configPath, config);
3703
+ } catch (err) {
3704
+ return {
3705
+ success: false,
3706
+ content: `写入配置文件失败: ${err instanceof Error ? err.message : String(err)}`,
3707
+ metadata: { durationMs: Date.now() - startTime }
3708
+ };
3709
+ }
3710
+ return {
3711
+ success: true,
3712
+ content: `已设置 ${key} = ${JSON.stringify(setValue)}`,
3713
+ metadata: { durationMs: Date.now() - startTime }
3714
+ };
3715
+ }
3716
+ default: return {
3717
+ success: false,
3718
+ content: `不支持的操作: ${action}。支持的操作: get, set, list, path。`,
3719
+ metadata: { durationMs: Date.now() - startTime }
3720
+ };
3721
+ }
3722
+ } };
3723
+ //#endregion
3724
+ //#region src/builtin/interact/ask-user-question.ts
3725
+ const descriptor$6 = {
3726
+ name: "ask_user_question",
3727
+ description: "向用户提出一个或多个澄清性问题并等待回答。支持单选、多选以及自由文本的「其他」选项。当您被一个只有用户才能做决定的问题卡住时使用。",
3728
+ inputSchema: {
3729
+ type: "object",
3730
+ properties: { questions: {
3731
+ type: "array",
3732
+ minItems: 1,
3733
+ maxItems: 4,
3734
+ items: {
3735
+ type: "object",
3736
+ properties: {
3737
+ question: {
3738
+ type: "string",
3739
+ description: "向用户提出的完整问题"
3740
+ },
3741
+ header: {
3742
+ type: "string",
3743
+ description: "显示为标签的简短文字(最多 12 个字符)"
3744
+ },
3745
+ options: {
3746
+ type: "array",
3747
+ minItems: 2,
3748
+ maxItems: 4,
3749
+ items: {
3750
+ type: "object",
3751
+ properties: {
3752
+ label: {
3753
+ type: "string",
3754
+ description: "显示文本(1-5 个词)"
3755
+ },
3756
+ description: {
3757
+ type: "string",
3758
+ description: "对该选项含义的解释说明"
3759
+ }
3760
+ },
3761
+ required: ["label", "description"]
3762
+ },
3763
+ description: "可选项(2-4 个)。「其他」选项会自动追加。"
3764
+ },
3765
+ multiSelect: {
3766
+ type: "boolean",
3767
+ description: "是否允许多选"
3768
+ }
3769
+ },
3770
+ required: [
3771
+ "question",
3772
+ "header",
3773
+ "options",
3774
+ "multiSelect"
3775
+ ]
3776
+ },
3777
+ description: "要提问的问题列表(1-4 个)"
3778
+ } },
3779
+ required: ["questions"]
3780
+ },
3781
+ kind: "ReadOnly",
3782
+ safety: "Safe",
3783
+ availability: { type: "always" },
3784
+ executor: "core:ask_user_question",
3785
+ owner: "core"
3786
+ };
3787
+ const handler$3 = { async handle(invocation, signal) {
3788
+ const { questions } = invocation.payload;
3789
+ const startTime = Date.now();
3790
+ if (signal.aborted) return {
3791
+ success: false,
3792
+ content: "提问已取消",
3793
+ metadata: { durationMs: Date.now() - startTime }
3794
+ };
3795
+ return {
3796
+ success: true,
3797
+ content: [
3798
+ "向用户提问:",
3799
+ "",
3800
+ questions.map((q, i) => {
3801
+ const opts = q.options.map((o) => ` - ${o.label}: ${o.description}`).join("\n");
3802
+ return `Q${i + 1}. [${q.header}] ${q.question}${q.multiSelect ? " (multi-select)" : ""}\n${opts}`;
3803
+ }).join("\n\n"),
3804
+ "",
3805
+ "[Phase 4 TUI 集成: 通过 lynx-tui 桥接实现真正的 AskUserQuestion 弹窗]"
3806
+ ].join("\n"),
3807
+ metadata: { durationMs: Date.now() - startTime }
3808
+ };
3809
+ } };
3810
+ //#endregion
3811
+ //#region src/builtin/interact/brief.ts
3812
+ const descriptor$5 = {
3813
+ name: "brief",
3814
+ description: "生成当前对话上下文的结构化摘要简报。适用于进度检查点、会话间交接、或向用户提供状态更新。",
3815
+ inputSchema: {
3816
+ type: "object",
3817
+ properties: {
3818
+ format: {
3819
+ type: "string",
3820
+ enum: ["markdown", "json"],
3821
+ description: "输出格式(默认:markdown)"
3822
+ },
3823
+ sections: {
3824
+ type: "array",
3825
+ items: { type: "string" },
3826
+ description: "要包含的章节(默认:context, goal, progress, blockers, next)"
3827
+ }
3828
+ },
3829
+ required: []
3830
+ },
3831
+ kind: "ReadOnly",
3832
+ safety: "Safe",
3833
+ availability: { type: "always" },
3834
+ executor: "core:brief",
3835
+ owner: "core"
3836
+ };
3837
+ const handler$2 = { async handle(invocation, signal) {
3838
+ const { format, sections } = invocation.payload;
3839
+ const startTime = Date.now();
3840
+ if (signal.aborted) return {
3841
+ success: false,
3842
+ content: "简报生成已取消",
3843
+ metadata: { durationMs: Date.now() - startTime }
3844
+ };
3845
+ const fmt = format ?? "markdown";
3846
+ const secs = sections ?? [
3847
+ "context",
3848
+ "goal",
3849
+ "progress",
3850
+ "blockers",
3851
+ "next"
3852
+ ];
3853
+ if (fmt === "json") {
3854
+ const brief = {};
3855
+ for (const s of secs) brief[s] = `[Phase 3: real context extraction via @lynx/agent compaction]`;
3856
+ return {
3857
+ success: true,
3858
+ content: JSON.stringify(brief, null, 2),
3859
+ metadata: { durationMs: Date.now() - startTime }
3860
+ };
3861
+ }
3862
+ const lines = ["# Brief", ""];
3863
+ for (const s of secs) lines.push(`## ${s}`, "", `[Phase 3: real context extraction via @lynx/agent compaction]`, "");
3864
+ return {
3865
+ success: true,
3866
+ content: lines.join("\n"),
3867
+ metadata: { durationMs: Date.now() - startTime }
3868
+ };
3869
+ } };
3870
+ //#endregion
3871
+ //#region src/builtin/interact/synthetic-output.ts
3872
+ const descriptor$4 = {
3873
+ name: "synthetic_output",
3874
+ description: "生成符合 JSON Schema 的结构化输出。用于在子 Agent 和父 Agent 之间传递带类型的数据。如果输出无法通过 schema 校验,LLM 会自动重试。",
3875
+ inputSchema: {
3876
+ type: "object",
3877
+ properties: {
3878
+ schema: {
3879
+ type: "object",
3880
+ description: "输出必须符合的 JSON Schema"
3881
+ },
3882
+ content: {
3883
+ type: "object",
3884
+ description: "结构化内容(必须与 schema 匹配)"
3885
+ }
3886
+ },
3887
+ required: ["schema", "content"]
3888
+ },
3889
+ kind: "ReadOnly",
3890
+ safety: "Safe",
3891
+ availability: { type: "always" },
3892
+ executor: "core:synthetic_output",
3893
+ owner: "core"
3894
+ };
3895
+ const handler$1 = { async handle(invocation, signal) {
3896
+ const { schema, content } = invocation.payload;
3897
+ const startTime = Date.now();
3898
+ if (signal.aborted) return {
3899
+ success: false,
3900
+ content: "合成输出已取消",
3901
+ metadata: { durationMs: Date.now() - startTime }
3902
+ };
3903
+ if (!schema || typeof schema !== "object") return {
3904
+ success: false,
3905
+ content: "无效的 schema:必须为 JSON Schema 对象",
3906
+ metadata: { durationMs: Date.now() - startTime }
3907
+ };
3908
+ return {
3909
+ success: true,
3910
+ content: JSON.stringify(content, null, 2),
3911
+ metadata: { durationMs: Date.now() - startTime }
3912
+ };
3913
+ } };
3914
+ //#endregion
3915
+ //#region src/builtin/interact/sleep.ts
3916
+ const descriptor$3 = {
3917
+ name: "sleep",
3918
+ description: "暂停执行指定秒数。用于速率限制退避、等待外部事件或轮询间隔。最长 300 秒。必须提供原因。",
3919
+ inputSchema: {
3920
+ type: "object",
3921
+ properties: {
3922
+ seconds: {
3923
+ type: "number",
3924
+ description: "等待的秒数(最多 300)",
3925
+ minimum: 1,
3926
+ maximum: 300
3927
+ },
3928
+ reason: {
3929
+ type: "string",
3930
+ description: "为什么需要等待(必填 — 防止无意义的等待)"
3931
+ }
3932
+ },
3933
+ required: ["seconds", "reason"]
3934
+ },
3935
+ kind: "ReadOnly",
3936
+ safety: "Safe",
3937
+ availability: { type: "always" },
3938
+ executor: "core:sleep",
3939
+ owner: "core"
3940
+ };
3941
+ const handler = { async handle(invocation, signal) {
3942
+ const { seconds, reason } = invocation.payload;
3943
+ const startTime = Date.now();
3944
+ if (signal.aborted) return {
3945
+ success: false,
3946
+ content: "等待已取消",
3947
+ metadata: { durationMs: Date.now() - startTime }
3948
+ };
3949
+ const clamped = Math.min(Math.max(1, Math.floor(seconds)), 300);
3950
+ if (clamped !== seconds) {}
3951
+ await new Promise((resolve, reject) => {
3952
+ const timer = setTimeout(resolve, clamped * 1e3);
3953
+ const onAbort = () => {
3954
+ clearTimeout(timer);
3955
+ reject(/* @__PURE__ */ new Error("ABORTED"));
3956
+ };
3957
+ signal.addEventListener("abort", onAbort, { once: true });
3958
+ }).catch((err) => {
3959
+ if (err.message === "ABORTED") {} else throw err;
3960
+ });
3961
+ const actualWait = Date.now() - startTime;
3962
+ if (signal.aborted) return {
3963
+ success: true,
3964
+ content: `等待在 ${Math.round(actualWait / 1e3)} 秒后被中断(请求:${clamped} 秒)。原因:${reason}`,
3965
+ metadata: { durationMs: actualWait }
3966
+ };
3967
+ return {
3968
+ success: true,
3969
+ content: `已等待 ${clamped} 秒。原因:${reason}`,
3970
+ metadata: { durationMs: actualWait }
3971
+ };
3972
+ } };
3973
+ //#endregion
3974
+ //#region src/builtin/memory/memory-write.ts
3975
+ const descriptor$1 = {
3976
+ name: "memory_write",
3977
+ description: "管理持久化记忆。支持以下操作:create/update — 写入或更新一条记忆;delete — 按名称删除;search — 全文搜索记忆(按相关度排序);compact — 去重压缩相似条目(Jaccard 相似度 > 0.8);forget — 按 glob 模式批量删除匹配的记忆;export — 导出全部记忆为 JSON 字符串;import — 从 JSON 字符串批量导入记忆(跳过同名条目);merge — 合并另一个 memory 目录中的记忆文件。当用户要求记住某事、搜索记忆、清理重复记忆或迁移记忆时使用。",
3978
+ inputSchema: {
3979
+ type: "object",
3980
+ properties: {
3981
+ action: {
3982
+ type: "string",
3983
+ enum: [
3984
+ "create",
3985
+ "update",
3986
+ "delete",
3987
+ "search",
3988
+ "compact",
3989
+ "forget",
3990
+ "export",
3991
+ "import",
3992
+ "merge"
3993
+ ],
3994
+ description: "要执行的操作。create = 新建记忆,update = 更新已有记忆,delete = 按名称删除,search = 全文搜索,compact = 去重压缩,forget = 按 glob 批量删除,export = 导出 JSON,import = 导入 JSON,merge = 合并目录。"
3995
+ },
3996
+ name: {
3997
+ type: "string",
3998
+ description: "记忆的简短 kebab-case 标识(例如 'user-prefers-pnpm')。用作文件名。create/update/delete 操作必填。"
3999
+ },
4000
+ content: {
4001
+ type: "string",
4002
+ description: "记忆的完整 markdown 正文。用自然语言书写。对于反馈/项目类记忆,请包含 **Why:** 和 **How to apply:** 段落。使用 [[slug]] 引用关联相关记忆。create/update 操作时提供。"
4003
+ },
4004
+ description: {
4005
+ type: "string",
4006
+ description: "一行摘要,用于在调用记忆时判断相关性。保持简短且具体。"
4007
+ },
4008
+ type: {
4009
+ type: "string",
4010
+ enum: [
4011
+ "user",
4012
+ "feedback",
4013
+ "project",
4014
+ "reference"
4015
+ ],
4016
+ description: "记忆分类。'user' = 用户身份,'feedback' = 纠正/指引,'project' = 进行中的工作/目标,'reference' = 外部资料引用。"
4017
+ },
4018
+ query: {
4019
+ type: "string",
4020
+ description: "搜索关键词,多个词用空格分隔(search 操作使用)。"
4021
+ },
4022
+ pattern: {
4023
+ type: "string",
4024
+ description: "glob 模式,用于匹配要删除的记忆名称(forget 操作使用)。支持 * 和 ? 通配符。"
4025
+ },
4026
+ data: {
4027
+ type: "string",
4028
+ description: "要导入的 JSON 数据字符串,由 export 操作生成(import 操作使用)。"
4029
+ },
4030
+ sourceDir: {
4031
+ type: "string",
4032
+ description: "源 memory 目录的绝对路径(merge 操作使用)。"
4033
+ }
4034
+ },
4035
+ required: ["action"]
4036
+ },
4037
+ kind: "WritesFiles",
4038
+ safety: "WorkspaceSafe",
4039
+ availability: { type: "always" },
4040
+ executor: "core:memory_write",
4041
+ owner: "core"
4042
+ };
4043
+ /**
4044
+ * 创建绑定到指定 {@link MemoryManager} 的 memory_write 处理器。
4045
+ *
4046
+ * 工厂模式 — 管理器在启动时注入,使工具写入真实的 memory 目录。
4047
+ */
4048
+ function createMemoryWriteHandler(memoryManager) {
4049
+ return { async handle(invocation, _signal) {
4050
+ const payload = invocation.payload;
4051
+ const action = payload.action ?? "create";
4052
+ switch (action) {
4053
+ case "create":
4054
+ case "update": {
4055
+ const { name, content } = payload;
4056
+ if (!name) return {
4057
+ success: false,
4058
+ content: `缺少必填参数 "name"。`
4059
+ };
4060
+ if (!content) return {
4061
+ success: false,
4062
+ content: `缺少必填参数 "content"。`
4063
+ };
4064
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) return {
4065
+ success: false,
4066
+ content: `无效的记忆名称 "${name}"。请使用 kebab-case 格式,仅含小写字母、数字和连字符(例如 "user-prefers-pnpm")。`
4067
+ };
4068
+ const effectiveType = payload.type ?? "reference";
4069
+ const updatedAt = Date.now();
4070
+ memoryManager.put({
4071
+ name,
4072
+ description: payload.description ?? "",
4073
+ type: effectiveType,
4074
+ content,
4075
+ updatedAt,
4076
+ filePath: ""
4077
+ });
4078
+ memoryManager.reload();
4079
+ return {
4080
+ success: true,
4081
+ content: `记忆 "${name}" ${action === "create" ? "已创建" : "已更新"}(类型:${effectiveType})。后续会话中将自动加载到上下文中。`
4082
+ };
4083
+ }
4084
+ case "delete": {
4085
+ const { name } = payload;
4086
+ if (!name) return {
4087
+ success: false,
4088
+ content: `缺少必填参数 "name"。`
4089
+ };
4090
+ if (memoryManager.delete(name)) return {
4091
+ success: true,
4092
+ content: `记忆 "${name}" 已删除。`
4093
+ };
4094
+ return {
4095
+ success: false,
4096
+ content: `未找到名为 "${name}" 的记忆。`
4097
+ };
4098
+ }
4099
+ case "search": {
4100
+ const { query } = payload;
4101
+ if (!query || !query.trim()) return {
4102
+ success: false,
4103
+ content: `缺少必填参数 "query"(搜索关键词)。`
4104
+ };
4105
+ const results = memoryManager.search(query);
4106
+ if (results.length === 0) return {
4107
+ success: true,
4108
+ content: `未找到与 "${query}" 匹配的记忆。`
4109
+ };
4110
+ const lines = [`搜索 "${query}" 找到 ${results.length} 条相关记忆:`, ""];
4111
+ for (const entry of results) {
4112
+ const typeLabel = entry.type;
4113
+ lines.push(`- **${entry.name}** [${typeLabel}]: ${entry.description}`);
4114
+ }
4115
+ return {
4116
+ success: true,
4117
+ content: lines.join("\n")
4118
+ };
4119
+ }
4120
+ case "compact": {
4121
+ const before = memoryManager.list().length;
4122
+ const remaining = memoryManager.compact();
4123
+ const removed = before - remaining.length;
4124
+ if (removed === 0) return {
4125
+ success: true,
4126
+ content: `去重压缩完成。未发现相似条目,${remaining.length} 条记忆保持不变。`
4127
+ };
4128
+ return {
4129
+ success: true,
4130
+ content: `去重压缩完成。移除了 ${removed} 条重复记忆,当前共 ${remaining.length} 条。`
4131
+ };
4132
+ }
4133
+ case "forget": {
4134
+ const { pattern } = payload;
4135
+ if (!pattern || !pattern.trim()) return {
4136
+ success: false,
4137
+ content: `缺少必填参数 "pattern"(glob 模式)。`
4138
+ };
4139
+ const count = memoryManager.forget(pattern);
4140
+ if (count === 0) return {
4141
+ success: true,
4142
+ content: `没有记忆匹配模式 "${pattern}"。`
4143
+ };
4144
+ return {
4145
+ success: true,
4146
+ content: `已删除 ${count} 条匹配模式 "${pattern}" 的记忆。`
4147
+ };
4148
+ }
4149
+ case "export": return {
4150
+ success: true,
4151
+ content: memoryManager.exportAll()
4152
+ };
4153
+ case "import": {
4154
+ const { data } = payload;
4155
+ if (!data || !data.trim()) return {
4156
+ success: false,
4157
+ content: `缺少必填参数 "data"(要导入的 JSON 数据)。`
4158
+ };
4159
+ const count = memoryManager.importAll(data);
4160
+ if (count === 0) return {
4161
+ success: true,
4162
+ content: `未导入任何新记忆。JSON 数据可能为空,或所有条目已存在。`
4163
+ };
4164
+ return {
4165
+ success: true,
4166
+ content: `成功导入 ${count} 条新记忆。`
4167
+ };
4168
+ }
4169
+ case "merge": {
4170
+ const { sourceDir } = payload;
4171
+ if (!sourceDir || !sourceDir.trim()) return {
4172
+ success: false,
4173
+ content: `缺少必填参数 "sourceDir"(源目录路径)。`
4174
+ };
4175
+ const count = memoryManager.merge(sourceDir);
4176
+ if (count === 0) return {
4177
+ success: true,
4178
+ content: `未合并任何新记忆。源目录可能不存在、为空,或所有条目已存在。`
4179
+ };
4180
+ return {
4181
+ success: true,
4182
+ content: `成功从 "${sourceDir}" 合并 ${count} 条新记忆。`
4183
+ };
4184
+ }
4185
+ default: return {
4186
+ success: false,
4187
+ content: `不支持的操作 "${action}"。支持的操作:create, update, delete, search, compact, forget, export, import, merge。`
4188
+ };
4189
+ }
4190
+ } };
4191
+ }
4192
+ //#endregion
4193
+ //#region src/builtin/mcp/mcp-auth.ts
4194
+ /** 工具描述符 — 注册到 ToolRegistry 的静态元数据。 */
4195
+ const descriptor = {
4196
+ name: "McpAuthTool",
4197
+ description: "触发 MCP 服务器的重新认证流程(OAuth 或 XAA)。用于在工具调用失败并返回认证错误时。",
4198
+ inputSchema: {
4199
+ type: "object",
4200
+ properties: { serverName: {
4201
+ type: "string",
4202
+ description: "需要重新认证的 MCP 服务器名称"
4203
+ } },
4204
+ required: ["serverName"]
4205
+ },
4206
+ kind: "Network",
4207
+ safety: "RequiresApproval",
4208
+ availability: { type: "always" },
4209
+ executor: "core:mcp_auth",
4210
+ owner: "core"
4211
+ };
4212
+ /**
4213
+ * 创建 McpAuthTool 的 handler,绑定到给定的 McpAuthOps。
4214
+ *
4215
+ * 工厂模式 — 实际 McpManager 在 bootstrap 时注入。
4216
+ */
4217
+ function createMcpAuthHandler(mcpOps) {
4218
+ return { async handle(invocation, signal) {
4219
+ const { serverName } = invocation.payload;
4220
+ if (!serverName || typeof serverName !== "string") return {
4221
+ success: false,
4222
+ content: "缺少必需参数 serverName"
4223
+ };
4224
+ if (signal.aborted) return {
4225
+ success: false,
4226
+ content: "操作已取消"
4227
+ };
4228
+ try {
4229
+ return {
4230
+ success: true,
4231
+ content: `MCP 服务器 "${serverName}" 重新认证完成,当前状态:${(await mcpOps.reconnect(serverName)).status}`
4232
+ };
4233
+ } catch (err) {
4234
+ return {
4235
+ success: false,
4236
+ content: `MCP 认证失败:${err.message}`
4237
+ };
4238
+ }
4239
+ } };
4240
+ }
4241
+ //#endregion
4242
+ //#region src/builtin/interact/send-message.ts
4243
+ const descriptor$2 = {
4244
+ name: "SendMessageTool",
4245
+ description: "通过配置的消息通道发送消息(飞书等)。用于向用户发送通知或进度摘要。需要提前配置消息通道。",
4246
+ inputSchema: {
4247
+ type: "object",
4248
+ properties: {
4249
+ channel: {
4250
+ type: "string",
4251
+ description: "消息通道名称(如 feishu)"
4252
+ },
4253
+ content: {
4254
+ type: "string",
4255
+ description: "要发送的消息内容"
4256
+ },
4257
+ recipients: {
4258
+ type: "array",
4259
+ items: { type: "string" },
4260
+ description: "接收人列表(可选)"
4261
+ }
4262
+ },
4263
+ required: ["channel", "content"]
4264
+ },
4265
+ kind: "Network",
4266
+ safety: "RequiresApproval",
4267
+ availability: { type: "always" },
4268
+ executor: "core:send_message",
4269
+ owner: "core"
4270
+ };
4271
+ /**
4272
+ * 创建 SendMessageTool 的处理器。
4273
+ *
4274
+ * 通过工厂注入 MessageSender 实现,避免直接 import lynx-channels。
4275
+ * 在 bootstrap 阶段将真实的通道发送器注入此处。
4276
+ */
4277
+ function createSendMessageHandler(sender) {
4278
+ return { async handle(invocation, signal) {
4279
+ const { channel, content, recipients } = invocation.payload;
4280
+ const startTime = Date.now();
4281
+ if (signal.aborted) return {
4282
+ success: false,
4283
+ content: "消息发送已取消",
4284
+ metadata: { durationMs: Date.now() - startTime }
4285
+ };
4286
+ if (!channel || !content) return {
4287
+ success: false,
4288
+ content: "缺少必要参数:channel 和 content 均为必填",
4289
+ metadata: { durationMs: Date.now() - startTime }
4290
+ };
4291
+ try {
4292
+ const result = await sender.send(channel, content, recipients);
4293
+ return {
4294
+ success: true,
4295
+ content: `消息已通过 ${channel} 发送${recipients ? ` 给 ${recipients.join(", ")}` : ""}。消息 ID:${result.messageId}`,
4296
+ metadata: { durationMs: Date.now() - startTime }
4297
+ };
4298
+ } catch (err) {
4299
+ return {
4300
+ success: false,
4301
+ content: `消息发送失败(${channel}):${err instanceof Error ? err.message : String(err)}`,
4302
+ metadata: { durationMs: Date.now() - startTime }
4303
+ };
4304
+ }
4305
+ } };
4306
+ }
4307
+ //#endregion
4308
+ //#region src/builtin/index.ts
4309
+ /** All built‑in (descriptor, handler) pairs in registration order. */
4310
+ const BUILTIN_TOOLS = [
4311
+ {
4312
+ descriptor: descriptor$33,
4313
+ handler: handler$30
4314
+ },
4315
+ {
4316
+ descriptor: descriptor$32,
4317
+ handler: handler$29
4318
+ },
4319
+ {
4320
+ descriptor: descriptor$31,
4321
+ handler: handler$28
4322
+ },
4323
+ {
4324
+ descriptor: descriptor$30,
4325
+ handler: handler$27
4326
+ },
4327
+ {
4328
+ descriptor: descriptor$29,
4329
+ handler: handler$26
4330
+ },
4331
+ {
4332
+ descriptor: descriptor$28,
4333
+ handler: handler$25
4334
+ },
4335
+ {
4336
+ descriptor: descriptor$27,
4337
+ handler: handler$24
4338
+ },
4339
+ {
4340
+ descriptor: descriptor$26,
4341
+ handler: handler$23
4342
+ },
4343
+ {
4344
+ descriptor: descriptor$25,
4345
+ handler: handler$22
4346
+ },
4347
+ {
4348
+ descriptor: descriptor$24,
4349
+ handler: handler$21
4350
+ },
4351
+ {
4352
+ descriptor: descriptor$23,
4353
+ handler: handler$20
4354
+ },
4355
+ {
4356
+ descriptor: descriptor$22,
4357
+ handler: handler$19
4358
+ },
4359
+ {
4360
+ descriptor: descriptor$21,
4361
+ handler: handler$18
4362
+ },
4363
+ {
4364
+ descriptor: descriptor$20,
4365
+ handler: handler$17
4366
+ },
4367
+ {
4368
+ descriptor: descriptor$19,
4369
+ handler: handler$16
4370
+ },
4371
+ {
4372
+ descriptor: descriptor$18,
4373
+ handler: handler$15
4374
+ },
4375
+ {
4376
+ descriptor: descriptor$17,
4377
+ handler: handler$14
4378
+ },
4379
+ {
4380
+ descriptor: descriptor$16,
4381
+ handler: handler$13
4382
+ },
4383
+ {
4384
+ descriptor: descriptor$15,
4385
+ handler: handler$12
4386
+ },
4387
+ {
4388
+ descriptor: descriptor$14,
4389
+ handler: handler$11
4390
+ },
4391
+ {
4392
+ descriptor: descriptor$13,
4393
+ handler: handler$10
4394
+ },
4395
+ {
4396
+ descriptor: descriptor$12,
4397
+ handler: handler$9
4398
+ },
4399
+ {
4400
+ descriptor: descriptor$11,
4401
+ handler: handler$8
4402
+ },
4403
+ {
4404
+ descriptor: descriptor$10,
4405
+ handler: handler$7
4406
+ },
4407
+ {
4408
+ descriptor: descriptor$9,
4409
+ handler: handler$6
4410
+ },
4411
+ {
4412
+ descriptor: descriptor$8,
4413
+ handler: handler$5
4414
+ },
4415
+ {
4416
+ descriptor: descriptor$7,
4417
+ handler: handler$4
4418
+ },
4419
+ {
4420
+ descriptor: descriptor$6,
4421
+ handler: handler$3
4422
+ },
4423
+ {
4424
+ descriptor: descriptor$5,
4425
+ handler: handler$2
4426
+ },
4427
+ {
4428
+ descriptor: descriptor$4,
4429
+ handler: handler$1
4430
+ },
4431
+ {
4432
+ descriptor: descriptor$3,
4433
+ handler
4434
+ }
4435
+ ];
4436
+ /**
4437
+ * Register all built‑in tool (descriptor, handler) pairs with the given registry.
4438
+ *
4439
+ * Each pair is registered atomically — if one fails (duplicate name), the error
4440
+ * propagates so the caller can decide whether to abort or skip.
4441
+ */
4442
+ function registerBuiltinTools(registry) {
4443
+ for (const { descriptor, handler } of BUILTIN_TOOLS) registry.register(descriptor, handler);
4444
+ }
4445
+ /** Return the list of built‑in descriptors (without registering). */
4446
+ function listBuiltinDescriptors() {
4447
+ return BUILTIN_TOOLS.map((t) => t.descriptor);
4448
+ }
4449
+ //#endregion
4450
+ //#region src/planner.ts
4451
+ /**
4452
+ * Compute the tool plan for the current turn.
4453
+ *
4454
+ * Every registered tool is evaluated against the availability context.
4455
+ * Visible tools go into the LLM catalog; hidden tools are returned with
4456
+ * a reason so the debug/log layer can report them.
4457
+ *
4458
+ * ```ts
4459
+ * const plan = buildToolPlan(registry, evalCtx);
4460
+ * console.log(`${plan.visible.length} visible, ${plan.hidden.length} hidden`);
4461
+ * ```
4462
+ */
4463
+ function buildToolPlan(registry, ctx) {
4464
+ const visible = [];
4465
+ const hidden = [];
4466
+ for (const tool of registry.listAll()) {
4467
+ const result = evaluateAvailability(tool.availability, ctx);
4468
+ if (result.available) visible.push(tool);
4469
+ else hidden.push({
4470
+ tool,
4471
+ reason: result.reason ?? "unknown"
4472
+ });
4473
+ }
4474
+ return {
4475
+ visible,
4476
+ hidden
4477
+ };
4478
+ }
4479
+ //#endregion
4480
+ //#region src/safety/sensitive.ts
4481
+ const RULES = [
4482
+ {
4483
+ level: "Critical",
4484
+ patterns: [
4485
+ "**/.ssh/**",
4486
+ "**/.gnupg/**",
4487
+ "**/.aws/**",
4488
+ "/etc/**",
4489
+ "/root/**",
4490
+ "/var/log/**",
4491
+ "/proc/**",
4492
+ "/sys/**",
4493
+ "/dev/**",
4494
+ "**/secrets/**",
4495
+ "**/.lynx/auth.json",
4496
+ "**/.claude/auth.json",
4497
+ "**/credentials.json",
4498
+ "C:/Windows/**",
4499
+ "C:/Windows/System32/**",
4500
+ "C:/Program Files/**"
4501
+ ]
4502
+ },
4503
+ {
4504
+ level: "High",
4505
+ patterns: [
4506
+ ".env",
4507
+ ".env.*",
4508
+ "!**/.env.example",
4509
+ "!**/.env.sample",
4510
+ "**/.git/config",
4511
+ "~/.lynx/config.json",
4512
+ "yarn.lock",
4513
+ "pnpm-lock.yaml",
4514
+ "package-lock.json",
4515
+ "**/id_rsa*",
4516
+ "**/id_ed25519*",
4517
+ "**/*.pem",
4518
+ "**/*.key"
4519
+ ]
4520
+ },
4521
+ {
4522
+ level: "Medium",
4523
+ patterns: [
4524
+ "package.json",
4525
+ "tsconfig.json",
4526
+ "tsconfig.*.json",
4527
+ "Cargo.toml",
4528
+ "Makefile",
4529
+ "Dockerfile",
4530
+ ".github/workflows/**",
4531
+ "docker-compose.yml",
4532
+ "docker-compose.yaml",
4533
+ "**/deploy/**",
4534
+ "**/terraform/**"
4535
+ ]
4536
+ }
4537
+ ];
4538
+ /**
4539
+ * Simple glob‑to‑regex for the path patterns we use.
4540
+ * Handles **, *, and ! (negation prefix).
4541
+ */
4542
+ function patternToRegex(pattern) {
4543
+ const negate = pattern.startsWith("!");
4544
+ const body = negate ? pattern.slice(1) : pattern;
4545
+ let escaped = body.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "<<<GLOBSTAR>>>").replace(/\*/g, "[^/]*").replace(/<<<GLOBSTAR>>>/g, ".*");
4546
+ if (body.startsWith("**/") || !body.includes("/")) escaped = "(?:.*/)?" + escaped;
4547
+ return {
4548
+ regex: new RegExp(`^${escaped}$`, "i"),
4549
+ negate
4550
+ };
4551
+ }
4552
+ let _compiled$1 = null;
4553
+ function compiledRules$1() {
4554
+ if (_compiled$1) return _compiled$1;
4555
+ _compiled$1 = [];
4556
+ for (const rule of RULES) for (const pattern of rule.patterns) {
4557
+ const { regex, negate } = patternToRegex(pattern);
4558
+ _compiled$1.push({
4559
+ regex,
4560
+ level: rule.level,
4561
+ negate
4562
+ });
4563
+ }
4564
+ return _compiled$1;
4565
+ }
4566
+ /**
4567
+ * Classify a file path by sensitivity level.
4568
+ *
4569
+ * The path is tested against all rules in order. The first match wins.
4570
+ * Negation patterns (`!`) remove previous matches at the same level.
4571
+ *
4572
+ * @returns The sensitivity level for the given path.
4573
+ */
4574
+ function classifySensitivity(filePath) {
4575
+ const norm = filePath.replace(/\\/g, "/");
4576
+ let result = "Low";
4577
+ for (const { regex, level, negate } of compiledRules$1()) if (regex.test(norm)) {
4578
+ if (negate) {
4579
+ if (result === level) result = "Low";
4580
+ } else if (severityOrder(level) > severityOrder(result)) result = level;
4581
+ }
4582
+ return result;
4583
+ }
4584
+ const SEVERITY_MAP = {
4585
+ Low: 0,
4586
+ Medium: 1,
4587
+ High: 2,
4588
+ Critical: 3
4589
+ };
4590
+ function severityOrder(level) {
4591
+ return SEVERITY_MAP[level];
4592
+ }
4593
+ //#endregion
4594
+ //#region src/safety.ts
4595
+ /**
4596
+ * Map the 4‑level file sensitivity to a minimum command safety level.
4597
+ *
4598
+ * Low → no escalation
4599
+ * Medium → WorkspaceSafe (write needs confirmation)
4600
+ * High → RequiresApproval (read is ok, write is blocked)
4601
+ * Critical → Dangerous (no read, no write)
4602
+ */
4603
+ const SENSITIVITY_ESCALATION = {
4604
+ Low: null,
4605
+ Medium: "WorkspaceSafe",
4606
+ High: "RequiresApproval",
4607
+ Critical: "RequiresApproval"
4608
+ };
4609
+ /**
4610
+ * Escalate the safety level if the command touches a sensitive path.
4611
+ *
4612
+ * Uses the 4‑level sensitivity classification from {@link classifySensitivity}.
4613
+ * Even a "Safe" tool becomes Dangerous if it tries to read ~/.ssh.
4614
+ */
4615
+ function escalateForSensitivePaths(safety, paths) {
4616
+ const SAFETY_ORDER = [
4617
+ "Safe",
4618
+ "WorkspaceSafe",
4619
+ "RequiresApproval",
4620
+ "Dangerous"
4621
+ ];
4622
+ let maxSensitivity = "Low";
4623
+ for (const p of paths) {
4624
+ const level = classifySensitivity(p);
4625
+ if (SENSITIVITY_ORDER[level] > SENSITIVITY_ORDER[maxSensitivity]) maxSensitivity = level;
4626
+ }
4627
+ const minSafety = SENSITIVITY_ESCALATION[maxSensitivity];
4628
+ if (minSafety && SAFETY_ORDER.indexOf(safety) < SAFETY_ORDER.indexOf(minSafety)) return minSafety;
4629
+ return safety;
4630
+ }
4631
+ const SENSITIVITY_ORDER = {
4632
+ Low: 0,
4633
+ Medium: 1,
4634
+ High: 2,
4635
+ Critical: 3
4636
+ };
4637
+ /**
4638
+ * Returns true if the tool requires interactive user confirmation.
4639
+ */
4640
+ function needsApproval(safety) {
4641
+ return safety === "RequiresApproval" || safety === "Dangerous";
4642
+ }
4643
+ /**
4644
+ * Returns true if the tool can run automatically in yolo/headless mode.
4645
+ */
4646
+ function isHeadlessSafe(safety) {
4647
+ return safety === "Safe" || safety === "WorkspaceSafe";
4648
+ }
4649
+ /** Human‑readable labels for each safety level. */
4650
+ const SAFETY_LABELS = {
4651
+ Safe: "安全(只读,无副作用)",
4652
+ WorkspaceSafe: "工作区安全(仅在项目内写入)",
4653
+ RequiresApproval: "需要审批",
4654
+ Dangerous: "危险(始终需要确认)"
4655
+ };
4656
+ //#endregion
4657
+ //#region src/safety/sanitize.ts
4658
+ const STRIP_RULES = [
4659
+ {
4660
+ label: "zero-width spaces",
4661
+ source: "[\\u200B-\\u200F]",
4662
+ flags: "g",
4663
+ replacement: ""
4664
+ },
4665
+ {
4666
+ label: "bidi overrides",
4667
+ source: "[\\u202A-\\u202E]",
4668
+ flags: "g",
4669
+ replacement: ""
4670
+ },
4671
+ {
4672
+ label: "deprecated format chars",
4673
+ source: "[\\u2060-\\u2069]",
4674
+ flags: "g",
4675
+ replacement: ""
4676
+ },
4677
+ {
4678
+ label: "byte order mark",
4679
+ source: "\\uFEFF",
4680
+ flags: "g",
4681
+ replacement: ""
4682
+ },
4683
+ {
4684
+ label: "variation selectors",
4685
+ source: "[\\uFE00-\\uFE0F]",
4686
+ flags: "g",
4687
+ replacement: ""
4688
+ },
4689
+ {
4690
+ label: "soft hyphen",
4691
+ source: "\\u00AD",
4692
+ flags: "g",
4693
+ replacement: ""
4694
+ },
4695
+ {
4696
+ label: "line separator",
4697
+ source: "\\u2028",
4698
+ flags: "g",
4699
+ replacement: "\n"
4700
+ },
4701
+ {
4702
+ label: "paragraph separator",
4703
+ source: "\\u2029",
4704
+ flags: "g",
4705
+ replacement: "\n\n"
4706
+ },
4707
+ {
4708
+ label: "interlinear annotation anchors",
4709
+ source: "[\\uFFF9-\\uFFFB]",
4710
+ flags: "g",
4711
+ replacement: ""
4712
+ },
4713
+ {
4714
+ label: "tag characters",
4715
+ source: "\\uDB40[\\uDC01-\\uDC7F]",
4716
+ flags: "gu",
4717
+ replacement: ""
4718
+ }
4719
+ ];
4720
+ let _compiled = null;
4721
+ function compiledRules() {
4722
+ if (_compiled) return _compiled;
4723
+ _compiled = STRIP_RULES.map((rule) => ({
4724
+ regex: new RegExp(rule.source, rule.flags),
4725
+ replacement: rule.replacement,
4726
+ label: rule.label
4727
+ }));
4728
+ return _compiled;
4729
+ }
4730
+ /**
4731
+ * Strip invisible and potentially dangerous Unicode characters from text.
4732
+ *
4733
+ * Preserves CJK Unified ideographs, emoji, combining diacritical marks,
4734
+ * and other legitimate Unicode.
4735
+ */
4736
+ function sanitizeUnicode(input) {
4737
+ let output = input;
4738
+ for (const { regex, replacement } of compiledRules()) output = output.replace(regex, replacement);
4739
+ return output;
4740
+ }
4741
+ //#endregion
4742
+ //#region src/safety/path-check.ts
4743
+ /**
4744
+ * Path traversal / jailbreak detection.
4745
+ *
4746
+ * Verifies that a resolved file path stays within the workspace root.
4747
+ * Also catches null-byte injection attacks that can truncate paths
4748
+ * in C‑based filesystem APIs.
4749
+ */
4750
+ const NULL_BYTE_PATTERN = /\x00/;
4751
+ /**
4752
+ * Check whether a file path is safely contained within the workspace root.
4753
+ *
4754
+ * Algorithm:
4755
+ * 1. Reject paths containing null bytes (truncation attack).
4756
+ * 2. Resolve to absolute path, eliminating `../` segments.
4757
+ * 3. Resolve symlinks to their real physical location.
4758
+ * 4. Verify the final path starts with the workspace root.
4759
+ *
4760
+ * @returns true if the path is safely inside the workspace.
4761
+ */
4762
+ function isPathSafe(filePath, workspaceRoot) {
4763
+ if (NULL_BYTE_PATTERN.test(filePath)) return false;
4764
+ const absolute = resolve(filePath);
4765
+ const wsRoot = resolve(workspaceRoot);
4766
+ if (!absolute.startsWith(wsRoot)) return false;
4767
+ try {
4768
+ if (existsSync(absolute)) return realpathSync(absolute).startsWith(wsRoot);
4769
+ } catch {
4770
+ return false;
4771
+ }
4772
+ return true;
4773
+ }
4774
+ //#endregion
4775
+ //#region src/safety/trusted-cmd.ts
4776
+ /**
4777
+ * Trusted command whitelist — commands that are safe to execute
4778
+ * without interactive user confirmation.
4779
+ *
4780
+ * These are read‑only or read‑heavy commands that don't modify
4781
+ * the filesystem or network. Even in `--yolo` mode, commands
4782
+ * not on this list still go through safety escalation.
4783
+ */
4784
+ const TRUSTED_COMMANDS = new Set([
4785
+ "bat",
4786
+ "cat",
4787
+ "df",
4788
+ "dir",
4789
+ "du",
4790
+ "echo",
4791
+ "env",
4792
+ "fd",
4793
+ "file",
4794
+ "find",
4795
+ "get-command",
4796
+ "grep",
4797
+ "head",
4798
+ "hostname",
4799
+ "less",
4800
+ "ls",
4801
+ "node",
4802
+ "pwd",
4803
+ "rg",
4804
+ "stat",
4805
+ "tail",
4806
+ "type",
4807
+ "uname",
4808
+ "wc",
4809
+ "where",
4810
+ "whereis",
4811
+ "which",
4812
+ "whoami"
4813
+ ]);
4814
+ const TRUSTED_SUBCOMMANDS = new Map([
4815
+ ["git", new Set([
4816
+ "log",
4817
+ "status",
4818
+ "diff",
4819
+ "show",
4820
+ "branch",
4821
+ "tag",
4822
+ "rev-parse",
4823
+ "rev-list",
4824
+ "ls-files",
4825
+ "ls-tree",
4826
+ "describe",
4827
+ "shortlog",
4828
+ "stash",
4829
+ "blame"
4830
+ ])],
4831
+ ["npm", new Set([
4832
+ "list",
4833
+ "ls",
4834
+ "view",
4835
+ "info",
4836
+ "outdated"
4837
+ ])],
4838
+ ["pnpm", new Set([
4839
+ "list",
4840
+ "ls",
4841
+ "view",
4842
+ "info",
4843
+ "outdated",
4844
+ "why"
4845
+ ])]
4846
+ ]);
4847
+ /**
4848
+ * Check if a command is on the trusted whitelist.
4849
+ *
4850
+ * For commands with sub‑command gating (git, npm, pnpm),
4851
+ * the first argument is checked against allowed sub‑commands.
4852
+ */
4853
+ function isTrustedCommand(command, args = []) {
4854
+ if (TRUSTED_COMMANDS.has(command)) return true;
4855
+ const allowed = TRUSTED_SUBCOMMANDS.get(command);
4856
+ if (allowed) {
4857
+ const sub = args[0];
4858
+ if (sub && allowed.has(sub)) return true;
4859
+ }
4860
+ if (args.includes("--version") || args.includes("-v") || args.includes("-V")) return true;
4861
+ return false;
4862
+ }
4863
+ //#endregion
4864
+ //#region src/safety/blocked-cmd.ts
4865
+ const BLOCKED_PATTERNS = [
4866
+ {
4867
+ reason: "cannot erase root filesystem",
4868
+ pattern: /\brm\s+-rf\s+\/\b/i
4869
+ },
4870
+ {
4871
+ reason: "cannot write raw disk devices",
4872
+ pattern: /\bdd\s+if=/i
4873
+ },
4874
+ {
4875
+ reason: "cannot format filesystems",
4876
+ pattern: /\bmkfs\./i
4877
+ },
4878
+ {
4879
+ reason: "cannot overwrite block devices",
4880
+ pattern: />\s*\/dev\/sd[a-z]/i
4881
+ },
4882
+ {
4883
+ reason: "cannot elevate privileges",
4884
+ pattern: /\bsudo\b/i
4885
+ },
4886
+ {
4887
+ reason: "cannot switch user",
4888
+ pattern: /\bsu\b(?:\s+-)?/i
4889
+ },
4890
+ {
4891
+ reason: "cannot elevate via doas",
4892
+ pattern: /\bdoas\b/i
4893
+ },
4894
+ {
4895
+ reason: "cannot make world‑writable files",
4896
+ pattern: /\bchmod\s+777\b/i
4897
+ },
4898
+ {
4899
+ reason: "cannot recursively chown root directory",
4900
+ pattern: /\bchown\s+-R\b.*\s+\//i
4901
+ },
4902
+ {
4903
+ reason: "cannot pipe curl to shell",
4904
+ pattern: /\bcurl\b.*\|\s*(?:sh|bash)\b/i
4905
+ },
4906
+ {
4907
+ reason: "cannot pipe wget to shell",
4908
+ pattern: /\bwget\b.*\|\s*(?:sh|bash)\b/i
4909
+ },
4910
+ {
4911
+ reason: "cannot force push to main/master",
4912
+ pattern: /\bgit\s+push\s+--force\b.*\b(?:main|master)\b/i
4913
+ },
4914
+ {
4915
+ reason: "cannot delete git repository data",
4916
+ pattern: /\bgit\s+reflog\s+expire\b/i
4917
+ },
4918
+ {
4919
+ reason: "cannot hard reset from remote",
4920
+ pattern: /\bgit\s+reset\s+--hard\s+origin\//i
4921
+ }
4922
+ ];
4923
+ /**
4924
+ * Check if a command string matches any blocked pattern.
4925
+ *
4926
+ * @returns The block reason if blocked, or null if safe.
4927
+ */
4928
+ function isBlockedCommand(command, args = []) {
4929
+ const fullCommand = args.length > 0 ? `${command} ${args.join(" ")}` : command;
4930
+ for (const entry of BLOCKED_PATTERNS) if (entry.pattern.test(fullCommand)) return {
4931
+ blocked: true,
4932
+ reason: entry.reason
4933
+ };
4934
+ return { blocked: false };
4935
+ }
4936
+ //#endregion
4937
+ //#region src/safety/redact.ts
4938
+ const SECRET_PATTERNS = [
4939
+ {
4940
+ label: "OpenAI API key",
4941
+ pattern: /sk-(?:proj-)?[A-Za-z0-9]{20,}/g,
4942
+ mode: "partial"
4943
+ },
4944
+ {
4945
+ label: "DeepSeek API key",
4946
+ pattern: /dsk-[A-Za-z0-9]{20,}/g,
4947
+ mode: "partial"
4948
+ },
4949
+ {
4950
+ label: "Anthropic API key",
4951
+ pattern: /sk-ant-(?:api\d{2}-)?[A-Za-z0-9_-]{20,}/g,
4952
+ mode: "partial"
4953
+ },
4954
+ {
4955
+ label: "GitHub token",
4956
+ pattern: /gh[poiurse]_[A-Za-z0-9_]{36,}/g,
4957
+ mode: "partial"
4958
+ },
4959
+ {
4960
+ label: "GitHub PAT",
4961
+ pattern: /github_pat_[A-Za-z0-9_]{36,}/g,
4962
+ mode: "partial"
4963
+ },
4964
+ {
4965
+ label: "AWS access key",
4966
+ pattern: /AKIA[0-9A-Z]{16}/g,
4967
+ mode: "partial"
4968
+ },
4969
+ {
4970
+ label: "AWS secret key",
4971
+ pattern: /(?<=AWS_SECRET_ACCESS_KEY[=:])\s*\S+/gi,
4972
+ mode: "full"
4973
+ },
4974
+ {
4975
+ label: "JWT token",
4976
+ pattern: /eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g,
4977
+ mode: "partial"
4978
+ },
4979
+ {
4980
+ label: "Private key header",
4981
+ pattern: /-----BEGIN (?:RSA|EC|OPENSSH|DSA|PRIVATE) KEY-----/g,
4982
+ mode: "full"
4983
+ },
4984
+ {
4985
+ label: "Generic base64 token",
4986
+ pattern: /(?:(?:api_?key|auth_?token|access_?token|secret|password|apikey)[=:]\s*)([A-Za-z0-9+/]{32,}={0,2})/gi,
4987
+ mode: "full"
4988
+ }
4989
+ ];
4990
+ function redactPartial(match) {
4991
+ if (match.length < 8) return "***";
4992
+ return match.slice(0, 4) + "..." + match.slice(-4);
4993
+ }
4994
+ /**
4995
+ * Redact all detected secrets from the given text.
4996
+ *
4997
+ * @returns The text with secrets replaced by `[REDACTED]` or `sk-...XyZ1`.
4998
+ */
4999
+ function redactSecrets(text) {
5000
+ let result = text;
5001
+ for (const { pattern, mode } of SECRET_PATTERNS) result = result.replace(pattern, (match) => mode === "partial" ? redactPartial(match) : "[REDACTED]");
5002
+ return result;
5003
+ }
5004
+ //#endregion
5005
+ export { SAFETY_LABELS, buildToolPlan, classifySensitivity as classifyPath, createMcpAuthHandler, createMemoryWriteHandler, createSendMessageHandler, createTaskHandler, createToolRegistry, escalateForSensitivePaths, evaluateAvailability, injectTaskManager, isBlockedCommand, isHeadlessSafe, isTrustedCommand, isPathSafe as isWithinWorkspace, listBuiltinDescriptors, descriptor as mcpAuthDescriptor, descriptor$1 as memoryWriteDescriptor, needsApproval, redactSecrets, registerBuiltinTools, sanitizeUnicode, descriptor$2 as sendMessageDescriptor };
5006
+
5007
+ //# sourceMappingURL=index.mjs.map