jinzd-ai-cli 0.3.6 → 0.4.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.
@@ -1,4 +1,8 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ EnvLoader,
4
+ schemaToJsonSchema
5
+ } from "./chunk-MXMU4PSC.js";
2
6
  import {
3
7
  APP_NAME,
4
8
  CONFIG_DIR_NAME,
@@ -9,14 +13,9 @@ import {
9
13
  MCP_CONNECT_TIMEOUT,
10
14
  MCP_PROTOCOL_VERSION,
11
15
  MCP_TOOL_PREFIX,
12
- MEMORY_FILE_NAME,
13
16
  PLUGINS_DIR_NAME,
14
- SUBAGENT_ALLOWED_TOOLS,
15
- SUBAGENT_DEFAULT_MAX_ROUNDS,
16
- SUBAGENT_MAX_ROUNDS_LIMIT,
17
- VERSION,
18
- runTestsTool
19
- } from "./chunk-OQK3WSFD.js";
17
+ VERSION
18
+ } from "./chunk-SX52VL4D.js";
20
19
 
21
20
  // src/config/config-manager.ts
22
21
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
@@ -153,44 +152,6 @@ var ConfigSchema = z.object({
153
152
  allowPlugins: z.boolean().default(false)
154
153
  });
155
154
 
156
- // src/config/env-loader.ts
157
- var ENV_KEY_MAP = {
158
- claude: "AICLI_API_KEY_CLAUDE",
159
- gemini: "AICLI_API_KEY_GEMINI",
160
- deepseek: "AICLI_API_KEY_DEEPSEEK",
161
- zhipu: "AICLI_API_KEY_ZHIPU",
162
- kimi: "AICLI_API_KEY_KIMI",
163
- openai: "AICLI_API_KEY_OPENAI",
164
- openrouter: "AICLI_API_KEY_OPENROUTER",
165
- "google-search": "AICLI_API_KEY_GOOGLESEARCH"
166
- };
167
- var EnvLoader = class {
168
- /**
169
- * 读取指定 provider 的 API Key 环境变量。
170
- * 优先级:固定映射(如 AICLI_API_KEY_CLAUDE)> 动态格式(AICLI_API_KEY_<ID大写>)
171
- * 自定义 provider 示例:id="siliconflow" → 读取 AICLI_API_KEY_SILICONFLOW
172
- */
173
- static getApiKey(providerId) {
174
- const fixedEnvVar = ENV_KEY_MAP[providerId];
175
- if (fixedEnvVar) {
176
- const val = process.env[fixedEnvVar];
177
- if (val) return val;
178
- }
179
- const dynamicEnvVar = `AICLI_API_KEY_${providerId.toUpperCase().replace(/-/g, "_")}`;
180
- return process.env[dynamicEnvVar] || void 0;
181
- }
182
- static getDefaultProvider() {
183
- return process.env["AICLI_PROVIDER"] || void 0;
184
- }
185
- static isStreamingDisabled() {
186
- return process.env["AICLI_NO_STREAM"] === "1";
187
- }
188
- /** Google Custom Search Engine ID (cx) 环境变量 */
189
- static getGoogleSearchEngineId() {
190
- return process.env["AICLI_GOOGLE_CX"] || void 0;
191
- }
192
- };
193
-
194
155
  // src/core/errors.ts
195
156
  var AiCliError = class extends Error {
196
157
  constructor(message, options) {
@@ -351,50 +312,6 @@ var BaseProvider = class {
351
312
  }
352
313
  };
353
314
 
354
- // src/tools/types.ts
355
- function isFileWriteTool(name) {
356
- return name === "write_file" || name === "edit_file";
357
- }
358
- function getDangerLevel(toolName, args) {
359
- if (toolName.startsWith("mcp__")) return "safe";
360
- if (toolName === "bash") {
361
- const cmd = String(args["command"] ?? "");
362
- if (/\brm\s+[^\n]*(?:-\w*[rRfF]\w*|--recursive|--force)\b/.test(cmd)) return "destructive";
363
- if (/\brm\s+\S/.test(cmd)) return "destructive";
364
- if (/\brmdir\b|\bformat\b|\bmkfs\b/.test(cmd)) return "destructive";
365
- if (/\bRemove-Item\b.*(?:-Recurse|-Force)|\bri\s+.*-(?:Recurse|Force)\b|\brd\s+\/s\b|\brmdir\s+\/s\b/i.test(cmd)) return "destructive";
366
- if (/\bdel\s+\S/.test(cmd)) return "destructive";
367
- if (/\becho\b.*>>?|\btee\b|\bcp\b|\bmv\b/.test(cmd)) return "write";
368
- if (/\bSet-Content\b|\bOut-File\b|\bAdd-Content\b|\bCopy-Item\b|\bMove-Item\b/i.test(cmd)) return "write";
369
- return "safe";
370
- }
371
- if (toolName === "write_file") return "write";
372
- if (toolName === "edit_file") return "write";
373
- if (toolName === "save_last_response") return "write";
374
- if (toolName === "run_interactive") {
375
- const exe = String(args["executable"] ?? "").toLowerCase();
376
- if (/\b(rm|rmdir|del|format|mkfs|Remove-Item)\b/i.test(exe)) return "destructive";
377
- if (/\b(bash|sh|zsh|cmd|powershell|pwsh|python|node|ruby|perl)\b/i.test(exe)) return "write";
378
- return "write";
379
- }
380
- if (toolName === "read_file" || toolName === "list_dir" || toolName === "grep_files" || toolName === "glob_files" || toolName === "web_fetch" || toolName === "save_memory" || toolName === "ask_user" || toolName === "write_todos" || toolName === "google_search" || toolName === "spawn_agent" || toolName === "run_tests") return "safe";
381
- return "write";
382
- }
383
- function schemaToJsonSchema(schema) {
384
- const result = {
385
- type: schema.type,
386
- description: schema.description
387
- };
388
- if (schema.enum) result["enum"] = schema.enum;
389
- if (schema.items) result["items"] = schemaToJsonSchema(schema.items);
390
- if (schema.properties) {
391
- result["properties"] = Object.fromEntries(
392
- Object.entries(schema.properties).map(([k, v]) => [k, schemaToJsonSchema(v)])
393
- );
394
- }
395
- return result;
396
- }
397
-
398
315
  // src/providers/claude.ts
399
316
  var ClaudeProvider = class extends BaseProvider {
400
317
  client;
@@ -2606,2601 +2523,179 @@ function formatGitContextForPrompt(ctx) {
2606
2523
  return lines.join("\n");
2607
2524
  }
2608
2525
 
2609
- // src/tools/builtin/bash.ts
2610
- import { execSync as execSync2 } from "child_process";
2611
- import { existsSync as existsSync5, readdirSync as readdirSync2, statSync } from "fs";
2612
- import { platform } from "os";
2613
- import { resolve } from "path";
2614
-
2615
- // src/tools/undo-stack.ts
2616
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2, rmdirSync, existsSync as existsSync4 } from "fs";
2617
- var MAX_UNDO_DEPTH = 20;
2618
- var UndoStack = class {
2619
- stack = [];
2620
- /**
2621
- * 在执行文件写入操作之前调用,保存当前状态。
2622
- * @param filePath 将要被写入的文件路径
2623
- * @param description 操作描述,如 "write_file: src/index.ts"
2624
- */
2625
- push(filePath, description) {
2626
- let previousContent = null;
2627
- if (existsSync4(filePath)) {
2628
- try {
2629
- previousContent = readFileSync3(filePath, "utf-8");
2630
- } catch {
2631
- return;
2632
- }
2633
- }
2634
- this.pushEntry({
2635
- filePath,
2636
- previousContent,
2637
- description,
2638
- timestamp: /* @__PURE__ */ new Date()
2639
- });
2640
- }
2641
- /**
2642
- * 推入一个新建文件的条目(previousContent=null),undo 时删除该文件。
2643
- * 用于 bash 工具执行后检测到的新建文件。
2644
- */
2645
- pushNewFile(filePath, description) {
2646
- this.pushEntry({
2647
- filePath,
2648
- previousContent: null,
2649
- description,
2650
- timestamp: /* @__PURE__ */ new Date()
2651
- });
2652
- }
2653
- /**
2654
- * 推入一个新建目录的条目(previousContent=null, isDirectory=true),
2655
- * undo 时尝试 rmdir(仅空目录可删)。
2656
- */
2657
- pushNewDir(dirPath, description) {
2658
- this.pushEntry({
2659
- filePath: dirPath,
2660
- previousContent: null,
2661
- description,
2662
- timestamp: /* @__PURE__ */ new Date(),
2663
- isDirectory: true
2664
- });
2665
- }
2666
- /** 内部统一入栈方法,含溢出裁剪 */
2667
- pushEntry(entry) {
2668
- this.stack.push(entry);
2669
- if (this.stack.length > MAX_UNDO_DEPTH) {
2670
- this.stack.shift();
2671
- }
2672
- }
2673
- /**
2674
- * 弹出并执行最近一次撤销操作。
2675
- * @returns 撤销结果描述,或 null(栈为空时)
2676
- */
2677
- undo() {
2678
- const entry = this.stack.pop();
2679
- if (!entry) return null;
2680
- try {
2681
- if (entry.previousContent === null) {
2682
- if (entry.isDirectory) {
2683
- if (existsSync4(entry.filePath)) {
2684
- try {
2685
- rmdirSync(entry.filePath);
2686
- return { entry, result: `Removed newly created directory: ${entry.filePath}` };
2687
- } catch {
2688
- return { entry, result: `Cannot remove directory (not empty): ${entry.filePath}` };
2689
- }
2690
- }
2691
- return { entry, result: `Directory already removed: ${entry.filePath}` };
2692
- } else {
2693
- if (existsSync4(entry.filePath)) {
2694
- unlinkSync2(entry.filePath);
2695
- }
2696
- return { entry, result: `Deleted newly created file: ${entry.filePath}` };
2697
- }
2698
- } else {
2699
- writeFileSync3(entry.filePath, entry.previousContent, "utf-8");
2700
- const lines = entry.previousContent.split("\n").length;
2701
- return {
2702
- entry,
2703
- result: `Restored ${entry.filePath} to previous state (${lines} lines)`
2704
- };
2705
- }
2706
- } catch (err) {
2707
- const msg = err instanceof Error ? err.message : String(err);
2708
- return { entry, result: `Undo failed: ${msg}` };
2709
- }
2710
- }
2711
- /** 查看栈顶条目(不弹出),用于 /undo 显示预览 */
2712
- peek() {
2713
- return this.stack[this.stack.length - 1] ?? null;
2526
+ // src/mcp/client.ts
2527
+ import { spawn } from "child_process";
2528
+ var McpClient = class {
2529
+ serverId;
2530
+ config;
2531
+ process = null;
2532
+ nextId = 1;
2533
+ connected = false;
2534
+ serverInfo = null;
2535
+ /** stderr 收集(最多保留最后 2KB,用于错误报告) */
2536
+ stderrBuffer = "";
2537
+ /** 缓存已发现的工具列表 */
2538
+ cachedTools = [];
2539
+ /** 错误信息(连接失败时设置) */
2540
+ errorMessage = null;
2541
+ // ── JSON-RPC 请求/响应匹配 ──────────────────────────────────────
2542
+ pendingRequests = /* @__PURE__ */ new Map();
2543
+ /** stdout 残余缓冲区(处理不完整的 JSON 行) */
2544
+ stdoutBuffer = "";
2545
+ constructor(serverId, config) {
2546
+ this.serverId = serverId;
2547
+ this.config = config;
2714
2548
  }
2715
- /** 当前栈深度 */
2716
- get depth() {
2717
- return this.stack.length;
2549
+ get isConnected() {
2550
+ return this.connected;
2718
2551
  }
2719
- /** 返回栈的拷贝(所有文件修改记录),供 /diff 命令使用 */
2720
- getHistory() {
2721
- return [...this.stack];
2552
+ get serverName() {
2553
+ return this.serverInfo?.name ?? this.serverId;
2722
2554
  }
2723
- /** 清空撤销栈(新会话时调用) */
2724
- clear() {
2725
- this.stack = [];
2555
+ get tools() {
2556
+ return this.cachedTools;
2726
2557
  }
2727
- };
2728
- var undoStack = new UndoStack();
2729
-
2730
- // src/tools/builtin/bash.ts
2731
- var IS_WINDOWS = platform() === "win32";
2732
- var SHELL = IS_WINDOWS ? "powershell.exe" : process.env["SHELL"] ?? "/bin/bash";
2733
- var persistentCwd = process.cwd();
2734
- var bashTool = {
2735
- definition: {
2736
- name: "bash",
2737
- description: IS_WINDOWS ? `Execute commands in PowerShell. Supports mkdir, ls, cat, python, etc.
2738
- Important rules:
2739
- 1. Each bash call runs in an independent subprocess; cd commands do not persist. To run in a specific directory, use the cwd parameter, or combine commands: e.g. "cd mydir; ls" or "mkdir mydir; cd mydir; New-Item file.txt".
2740
- 2. If a command fails (returns an error or non-zero exit code), stop immediately, report the error to the user, and do not retry the same or similar commands.
2741
- 3. Multiple commands can be combined with semicolons in a single call to reduce rounds.
2742
- 4. To delete directories, use Remove-Item -Recurse (the system will automatically optimize to a more reliable method).
2743
- 5. IMPORTANT: On Windows, "curl" is an alias for Invoke-WebRequest and does NOT support curl flags like -s, -X, -H. Use Invoke-RestMethod instead for HTTP requests. Example: Invoke-RestMethod -Uri "http://localhost:3000/api/health" -Method Get
2744
- 6. For long-running server commands (node server.js, npm run dev, etc.), use Start-Process -NoNewWindow to run in background, otherwise the tool will block until timeout.` : `Execute commands in ${SHELL}.
2745
- Important rules:
2746
- 1. Each bash call runs in an independent subprocess; cd commands do not persist. To run in a specific directory, use the cwd parameter, or combine commands: e.g. "cd mydir && ls" or "mkdir -p mydir && touch mydir/file.txt".
2747
- 2. If a command fails (returns an error or non-zero exit code), stop immediately, report the error to the user, and do not retry the same or similar commands.
2748
- 3. Multiple commands can be combined with && in a single call to reduce rounds.
2749
- 4. For long-running server commands (node server.js, npm start, npm run dev, etc.), run in background with & or nohup, otherwise the tool will block until timeout.`,
2750
- parameters: {
2751
- command: {
2752
- type: "string",
2753
- description: IS_WINDOWS ? `PowerShell command to execute. Combine multiple commands with semicolons, e.g.: "mkdir mydir; Set-Content mydir/file.txt 'content'"` : `${SHELL} command to execute. Combine multiple commands with &&, e.g.: "mkdir -p mydir && echo 'content' > mydir/file.txt"`,
2754
- required: true
2755
- },
2756
- cwd: {
2757
- type: "string",
2758
- description: "Working directory for the command (absolute or relative path). Once set, subsequent commands will also use this directory.",
2759
- required: false
2760
- },
2761
- timeout: {
2762
- type: "number",
2763
- description: "Timeout in milliseconds, defaults to 30000",
2764
- required: false
2765
- }
2766
- },
2767
- dangerous: false
2768
- },
2769
- async execute(args) {
2770
- const command = String(args["command"] ?? "");
2771
- const MAX_TIMEOUT = 3e5;
2772
- const timeout = Math.min(Math.max(Number(args["timeout"] ?? 3e4), 1e3), MAX_TIMEOUT);
2773
- const cwdArg = args["cwd"] ? String(args["cwd"]) : void 0;
2774
- if (!command.trim()) {
2775
- throw new Error("command is required");
2776
- }
2777
- if (!existsSync5(persistentCwd)) {
2778
- const fallback = process.cwd();
2779
- process.stderr.write(
2780
- `[bash] Previous cwd "${persistentCwd}" no longer exists, reset to "${fallback}"
2781
- `
2782
- );
2783
- persistentCwd = fallback;
2784
- }
2785
- let effectiveCwd = persistentCwd;
2786
- if (cwdArg) {
2787
- const resolved = resolve(persistentCwd, cwdArg);
2788
- if (!existsSync5(resolved)) {
2789
- throw new Error(
2790
- `cwd directory does not exist: "${resolved}". Create it first (e.g. mkdir -p "${resolved}") before specifying it as cwd.`
2791
- );
2792
- }
2793
- effectiveCwd = resolved;
2794
- persistentCwd = resolved;
2795
- }
2796
- let actualCommand;
2797
- if (IS_WINDOWS) {
2798
- const fixedCommand = fixWindowsDeleteCommand(command);
2799
- actualCommand = `[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; $OutputEncoding = [System.Text.Encoding]::UTF8; ${fixedCommand}`;
2800
- } else {
2801
- actualCommand = command;
2802
- }
2803
- const beforeSnapshot = snapshotDir(effectiveCwd);
2804
- const parsedTargets = parseCreationTargets(command, effectiveCwd);
2805
- const parsedTargetsBefore = /* @__PURE__ */ new Map();
2806
- for (const t of parsedTargets) {
2807
- parsedTargetsBefore.set(t, existsSync5(t));
2808
- }
2558
+ // ══════════════════════════════════════════════════════════════════
2559
+ // 连接与初始化
2560
+ // ══════════════════════════════════════════════════════════════════
2561
+ async connect() {
2562
+ const timeout = this.config.timeout ?? MCP_CONNECT_TIMEOUT;
2809
2563
  try {
2810
- const output = execSync2(actualCommand, {
2811
- timeout,
2812
- encoding: IS_WINDOWS ? "buffer" : "utf-8",
2564
+ this.process = spawn(this.config.command, this.config.args ?? [], {
2813
2565
  stdio: ["pipe", "pipe", "pipe"],
2814
- cwd: effectiveCwd,
2815
- shell: SHELL,
2816
- env: {
2817
- ...process.env,
2818
- PYTHONUTF8: "1",
2819
- PYTHONIOENCODING: "utf-8"
2566
+ env: { ...process.env, ...this.config.env },
2567
+ // Windows 上 npx 等是 .cmd 脚本,需要 shell 模式
2568
+ shell: process.platform === "win32",
2569
+ // 不让子进程阻止父进程退出
2570
+ detached: false
2571
+ });
2572
+ this.process.on("error", (err) => {
2573
+ this.errorMessage = err.message;
2574
+ this.connected = false;
2575
+ this.rejectAllPending(new Error(`MCP server [${this.serverId}] process error: ${err.message}`));
2576
+ });
2577
+ this.process.on("exit", (code, signal) => {
2578
+ this.connected = false;
2579
+ const reason = signal ? `signal ${signal}` : `code ${code}`;
2580
+ this.rejectAllPending(new Error(`MCP server [${this.serverId}] exited: ${reason}`));
2581
+ });
2582
+ this.process.stdout.setEncoding("utf-8");
2583
+ this.process.stdout.on("data", (chunk) => this.handleStdoutData(chunk));
2584
+ this.process.stderr.setEncoding("utf-8");
2585
+ this.process.stderr.on("data", (chunk) => {
2586
+ this.stderrBuffer += chunk;
2587
+ if (this.stderrBuffer.length > 2048) {
2588
+ this.stderrBuffer = this.stderrBuffer.slice(-2048);
2820
2589
  }
2821
2590
  });
2822
- updateCwdFromCommand(command, effectiveCwd);
2823
- pushBashUndoEntries(beforeSnapshot, parsedTargetsBefore, effectiveCwd);
2824
- const result = IS_WINDOWS && Buffer.isBuffer(output) ? output.toString("utf-8") : output;
2825
- return result || "(command completed with no output)";
2591
+ const initResult = await this.withTimeout(
2592
+ this.sendRequest("initialize", {
2593
+ protocolVersion: MCP_PROTOCOL_VERSION,
2594
+ capabilities: {},
2595
+ clientInfo: { name: APP_NAME, version: VERSION }
2596
+ }),
2597
+ timeout,
2598
+ "initialize handshake"
2599
+ );
2600
+ this.serverInfo = initResult.serverInfo;
2601
+ this.sendNotification("notifications/initialized");
2602
+ this.connected = true;
2603
+ await this.refreshTools();
2826
2604
  } catch (err) {
2827
- pushBashUndoEntries(beforeSnapshot, parsedTargetsBefore, effectiveCwd);
2828
- if (err && typeof err === "object" && "status" in err) {
2829
- const execErr = err;
2830
- const stderr = IS_WINDOWS && Buffer.isBuffer(execErr.stderr) ? execErr.stderr.toString("utf-8").trim() : execErr.stderr?.toString().trim() ?? "";
2831
- const stdout = IS_WINDOWS && Buffer.isBuffer(execErr.stdout) ? execErr.stdout.toString("utf-8").trim() : execErr.stdout?.toString().trim() ?? "";
2832
- const combined = [stdout, stderr].filter(Boolean).join("\n");
2833
- throw new Error(
2834
- `Exit code ${execErr.status}:
2835
- ${combined || (execErr.message ?? "Unknown error")}
2836
-
2837
- [Command failed. Report this error to the user. Do not retry with variant commands.]`
2838
- );
2839
- }
2605
+ this.errorMessage = err instanceof Error ? err.message : String(err);
2606
+ this.connected = false;
2607
+ this.killProcess();
2840
2608
  throw err;
2841
2609
  }
2842
2610
  }
2843
- };
2844
- function fixWindowsDeleteCommand(command) {
2845
- return command.replace(
2846
- /Remove-Item\b([^;\n]*)/gi,
2847
- (match, args) => {
2848
- if (!/recurse/i.test(args)) return match;
2849
- let pathValue = "";
2850
- const pathMatch = args.match(/-Path\s+(['"]?)([^'";\s]+)\1/i) ?? args.match(/(?:^|\s)(['"]?)([^'";\s-][^'";\s]*)\1/);
2851
- if (pathMatch) {
2852
- pathValue = pathMatch[2] ?? "";
2853
- }
2854
- if (!pathValue) return match;
2855
- const safePath = pathValue.replace(/"/g, '\\"');
2856
- return `cmd /c rmdir /s /q "${safePath}"`;
2857
- }
2858
- );
2859
- }
2860
- function snapshotDir(dir) {
2861
- try {
2862
- return new Set(readdirSync2(dir).map((name) => resolve(dir, name)));
2863
- } catch {
2864
- return /* @__PURE__ */ new Set();
2865
- }
2866
- }
2867
- function parseCreationTargets(command, cwd) {
2868
- const targets = [];
2869
- for (const m of command.matchAll(/(?:echo|cat|printf)\s+[^>]*>\s*(['"]?)([^\s;&|'"]+)\1/g)) {
2870
- if (m[2]) targets.push(resolve(cwd, m[2]));
2871
- }
2872
- for (const m of command.matchAll(/\btouch\s+((?:['"]?[^\s;&|'"]+['"]?\s*)+)/g)) {
2873
- for (const f of m[1].trim().split(/\s+/)) {
2874
- const clean = f.replace(/^['"]|['"]$/g, "");
2875
- if (clean && !clean.startsWith("-")) targets.push(resolve(cwd, clean));
2876
- }
2611
+ // ══════════════════════════════════════════════════════════════════
2612
+ // 工具操作
2613
+ // ══════════════════════════════════════════════════════════════════
2614
+ /** 刷新工具列表(tools/list) */
2615
+ async refreshTools() {
2616
+ this.ensureConnected();
2617
+ const result = await this.withTimeout(
2618
+ this.sendRequest("tools/list", {}),
2619
+ MCP_CALL_TIMEOUT,
2620
+ "tools/list"
2621
+ );
2622
+ this.cachedTools = result.tools ?? [];
2623
+ return this.cachedTools;
2877
2624
  }
2878
- for (const m of command.matchAll(/\bmkdir\s+(?:-\w+\s+)*((?:['"]?[^\s;&|'"]+['"]?\s*)+)/g)) {
2879
- for (const d of m[1].trim().split(/\s+/)) {
2880
- const clean = d.replace(/^['"]|['"]$/g, "");
2881
- if (clean && !clean.startsWith("-")) targets.push(resolve(cwd, clean));
2882
- }
2625
+ /** 调用工具(tools/call) */
2626
+ async callTool(name, args) {
2627
+ this.ensureConnected();
2628
+ return this.withTimeout(
2629
+ this.sendRequest("tools/call", { name, arguments: args }),
2630
+ MCP_CALL_TIMEOUT,
2631
+ `tools/call(${name})`
2632
+ );
2883
2633
  }
2884
- for (const m of command.matchAll(/\bcp\s+(?:-\w+\s+)*['"]?[^\s;&|'"]+['"]?\s+(['"]?)([^\s;&|'"]+)\1/g)) {
2885
- if (m[2]) targets.push(resolve(cwd, m[2]));
2634
+ // ══════════════════════════════════════════════════════════════════
2635
+ // 关闭连接
2636
+ // ══════════════════════════════════════════════════════════════════
2637
+ async close() {
2638
+ this.connected = false;
2639
+ this.rejectAllPending(new Error("Client closing"));
2640
+ this.killProcess();
2886
2641
  }
2887
- for (const m of command.matchAll(/\bNew-Item\s+(?:-(?:Path|ItemType)\s+\w+\s+)*['"]?([^\s;&|'"]+)['"]?/gi)) {
2888
- if (m[1] && !m[1].startsWith("-")) targets.push(resolve(cwd, m[1]));
2642
+ /**
2643
+ * 断线重连:清理旧状态后重新执行完整的 connect() 流程。
2644
+ * 用于 MCP 服务器子进程意外退出后的自动或手动恢复。
2645
+ */
2646
+ async reconnect() {
2647
+ this.connected = false;
2648
+ this.rejectAllPending(new Error("Reconnecting"));
2649
+ this.killProcess();
2650
+ this.errorMessage = null;
2651
+ this.stderrBuffer = "";
2652
+ this.stdoutBuffer = "";
2653
+ this.cachedTools = [];
2654
+ await this.connect();
2889
2655
  }
2890
- return [...new Set(targets)];
2891
- }
2892
- function pushBashUndoEntries(beforeSnapshot, parsedTargetsBefore, cwd) {
2893
- const tracked = /* @__PURE__ */ new Set();
2894
- const afterSnapshot = snapshotDir(cwd);
2895
- for (const absPath of afterSnapshot) {
2896
- if (!beforeSnapshot.has(absPath)) {
2897
- try {
2898
- const st = statSync(absPath);
2899
- if (st.isDirectory()) {
2900
- undoStack.pushNewDir(absPath, `bash (new dir): ${absPath}`);
2901
- } else {
2902
- undoStack.pushNewFile(absPath, `bash (new file): ${absPath}`);
2903
- }
2904
- tracked.add(absPath);
2905
- } catch {
2656
+ // ══════════════════════════════════════════════════════════════════
2657
+ // 内部方法:JSON-RPC 通信
2658
+ // ══════════════════════════════════════════════════════════════════
2659
+ sendRequest(method, params) {
2660
+ return new Promise((resolve, reject) => {
2661
+ if (!this.process?.stdin?.writable) {
2662
+ return reject(new Error(`MCP server [${this.serverId}] stdin not writable`));
2906
2663
  }
2907
- }
2908
- }
2909
- for (const [target, existedBefore] of parsedTargetsBefore) {
2910
- if (!existedBefore && !tracked.has(target) && existsSync5(target)) {
2911
- try {
2912
- const st = statSync(target);
2913
- if (st.isDirectory()) {
2914
- undoStack.pushNewDir(target, `bash (new dir): ${target}`);
2915
- } else {
2916
- undoStack.pushNewFile(target, `bash (new file): ${target}`);
2664
+ const id = this.nextId++;
2665
+ const request = {
2666
+ jsonrpc: "2.0",
2667
+ id,
2668
+ method,
2669
+ ...params !== void 0 ? { params } : {}
2670
+ };
2671
+ let timer;
2672
+ const cleanup = () => {
2673
+ this.pendingRequests.delete(id);
2674
+ clearTimeout(timer);
2675
+ };
2676
+ timer = setTimeout(() => {
2677
+ cleanup();
2678
+ reject(new Error(`MCP request [${method}] timed out (internal)`));
2679
+ }, MCP_CALL_TIMEOUT * 2);
2680
+ this.pendingRequests.set(id, {
2681
+ resolve: (result) => {
2682
+ cleanup();
2683
+ resolve(result);
2684
+ },
2685
+ reject: (error) => {
2686
+ cleanup();
2687
+ reject(error);
2688
+ },
2689
+ timer
2690
+ });
2691
+ const json = JSON.stringify(request) + "\n";
2692
+ this.process.stdin.write(json, (err) => {
2693
+ if (err) {
2694
+ cleanup();
2695
+ reject(new Error(`MCP write error: ${err.message}`));
2917
2696
  }
2918
- } catch {
2919
- }
2920
- }
2921
- }
2922
- }
2923
- function updateCwdFromCommand(command, baseCwd) {
2924
- const cdMatches = [...command.matchAll(/(?:^|[;&|])\s*cd\s+(['"]?)([^\s;&|'"]+)\1/g)];
2925
- if (cdMatches.length === 0) return;
2926
- const lastMatch = cdMatches[cdMatches.length - 1];
2927
- const target = lastMatch?.[2];
2928
- if (!target || target.startsWith("$") || target === "~") return;
2929
- try {
2930
- const newDir = resolve(baseCwd, target);
2931
- if (existsSync5(newDir)) {
2932
- persistentCwd = newDir;
2933
- }
2934
- } catch {
2935
- }
2936
- }
2937
-
2938
- // src/tools/builtin/read-file.ts
2939
- import { readFileSync as readFileSync4, existsSync as existsSync6, statSync as statSync2, readdirSync as readdirSync3 } from "fs";
2940
- import { execSync as execSync3 } from "child_process";
2941
- import { extname, resolve as resolve2, basename, sep, dirname } from "path";
2942
- import { homedir as homedir2 } from "os";
2943
- var MAX_FILE_BYTES = 10 * 1024 * 1024;
2944
- function getSensitiveWarning(normalizedPath) {
2945
- const home = homedir2();
2946
- const p = normalizedPath.toLowerCase();
2947
- const base = basename(normalizedPath).toLowerCase();
2948
- if (normalizedPath.startsWith(home) && p.includes(".aicli") && base === "config.json") {
2949
- return "[\u26A0 Security Warning: This file contains API keys. Be careful not to share this content.]\n\n";
2950
- }
2951
- if (base === ".env" || base.startsWith(".env.") || base.endsWith(".env")) {
2952
- return "[\u26A0 Security Warning: .env files may contain secrets and credentials.]\n\n";
2953
- }
2954
- if (normalizedPath.includes(`${sep}.ssh${sep}`) && (base.startsWith("id_") || base === "identity")) {
2955
- return "[\u26A0 Security Warning: This may be an SSH private key.]\n\n";
2956
- }
2957
- if (base === "credentials" && p.includes(".aws")) {
2958
- return "[\u26A0 Security Warning: This file contains AWS credentials.]\n\n";
2959
- }
2960
- return "";
2961
- }
2962
- var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
2963
- ".pdf",
2964
- ".doc",
2965
- ".docx",
2966
- ".xls",
2967
- ".xlsx",
2968
- ".ppt",
2969
- ".pptx",
2970
- ".zip",
2971
- ".tar",
2972
- ".gz",
2973
- ".7z",
2974
- ".rar",
2975
- ".bz2",
2976
- ".png",
2977
- ".jpg",
2978
- ".jpeg",
2979
- ".gif",
2980
- ".bmp",
2981
- ".ico",
2982
- ".webp",
2983
- ".tiff",
2984
- ".mp3",
2985
- ".mp4",
2986
- ".avi",
2987
- ".mov",
2988
- ".mkv",
2989
- ".wav",
2990
- ".flac",
2991
- ".exe",
2992
- ".dll",
2993
- ".so",
2994
- ".dylib",
2995
- ".bin",
2996
- ".dat",
2997
- ".wasm",
2998
- ".class",
2999
- ".pyc"
3000
- ]);
3001
- function isBinaryBuffer(buf) {
3002
- const sample = buf.subarray(0, 512);
3003
- let nullCount = 0;
3004
- for (let i = 0; i < sample.length; i++) {
3005
- if (sample[i] === 0) nullCount++;
3006
- }
3007
- return nullCount / sample.length > 0.1;
3008
- }
3009
- function findSimilarFiles(filePath) {
3010
- const targetName = basename(filePath).toLowerCase();
3011
- if (!targetName) return [];
3012
- const cwd = process.cwd();
3013
- const candidates = [];
3014
- try {
3015
- for (const entry of readdirSync3(cwd, { withFileTypes: true })) {
3016
- if (entry.isFile() && entry.name.toLowerCase() === targetName) {
3017
- candidates.push(entry.name);
3018
- }
3019
- if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
3020
- try {
3021
- const subDir = resolve2(cwd, entry.name);
3022
- for (const sub of readdirSync3(subDir, { withFileTypes: true })) {
3023
- if (sub.isFile() && sub.name.toLowerCase() === targetName) {
3024
- candidates.push(`${entry.name}/${sub.name}`);
3025
- }
3026
- }
3027
- } catch {
3028
- }
3029
- }
3030
- }
3031
- } catch {
3032
- }
3033
- return candidates.slice(0, 5);
3034
- }
3035
- function tryExtractPdfText(absPath) {
3036
- try {
3037
- const output = execSync3(`pdftotext "${absPath}" -`, {
3038
- timeout: 15e3,
3039
- encoding: "utf-8",
3040
- stdio: ["pipe", "pipe", "pipe"]
3041
- });
3042
- if (output.trim().length > 0) return output;
3043
- } catch {
3044
- }
3045
- try {
3046
- const pyScript = `import sys; exec("try:\\n from pdfminer.high_level import extract_text\\n print(extract_text(sys.argv[1]))\\nexcept: pass")`;
3047
- const output = execSync3(`python -c "${pyScript}" "${absPath}"`, {
3048
- timeout: 15e3,
3049
- encoding: "utf-8",
3050
- stdio: ["pipe", "pipe", "pipe"],
3051
- env: { ...process.env, PYTHONUTF8: "1", PYTHONIOENCODING: "utf-8" }
3052
- });
3053
- if (output.trim().length > 0) return output;
3054
- } catch {
3055
- }
3056
- return null;
3057
- }
3058
- var readFileTool = {
3059
- definition: {
3060
- name: "read_file",
3061
- description: "Read text file contents. Automatically detects binary files (PDF/images/etc) and returns a hint instead of garbled output.",
3062
- parameters: {
3063
- path: {
3064
- type: "string",
3065
- description: "File path (absolute or relative to current working directory)",
3066
- required: true
3067
- },
3068
- encoding: {
3069
- type: "string",
3070
- description: "Encoding format, defaults to utf-8",
3071
- enum: ["utf-8", "utf8", "ascii", "base64"],
3072
- required: false
3073
- }
3074
- },
3075
- dangerous: false
3076
- },
3077
- async execute(args) {
3078
- const filePath = String(args["path"] ?? "");
3079
- const encoding = args["encoding"] ?? "utf-8";
3080
- if (!filePath) throw new Error("path is required");
3081
- const normalizedPath = resolve2(filePath);
3082
- if (!existsSync6(normalizedPath)) {
3083
- const suggestions = findSimilarFiles(filePath);
3084
- if (suggestions.length > 0) {
3085
- throw new Error(
3086
- `File not found: ${filePath}
3087
- Current working directory: ${process.cwd()}
3088
- Found similar files, did you mean:
3089
- ` + suggestions.map((s) => ` \u2192 ${s}`).join("\n") + `
3090
- Please retry with the correct relative path.`
3091
- );
3092
- }
3093
- throw new Error(
3094
- `File not found: ${filePath}
3095
- Current working directory: ${process.cwd()}
3096
- Please use list_dir to verify the file path and retry.`
3097
- );
3098
- }
3099
- const { size } = statSync2(normalizedPath);
3100
- if (size > MAX_FILE_BYTES) {
3101
- const mb = (size / 1024 / 1024).toFixed(1);
3102
- return `[File too large: ${filePath} (${mb} MB)]
3103
- Exceeds single read limit of ${MAX_FILE_BYTES / 1024 / 1024} MB.
3104
- Use the bash tool to read in segments, e.g.:
3105
- head -n 100 "${normalizedPath}" # first 100 lines
3106
- tail -n 100 "${normalizedPath}" # last 100 lines
3107
- sed -n '200,300p' "${normalizedPath}" # lines 200-300`;
3108
- }
3109
- const sensitiveWarning = getSensitiveWarning(normalizedPath);
3110
- const ext = extname(normalizedPath).toLowerCase();
3111
- if (ext === ".pdf") {
3112
- const pdfText = tryExtractPdfText(normalizedPath);
3113
- if (pdfText) {
3114
- const lines2 = pdfText.split("\n").length;
3115
- return `[PDF extracted: ${filePath} | ${lines2} lines]
3116
-
3117
- ${pdfText}`;
3118
- }
3119
- const dir = dirname(normalizedPath);
3120
- const nameNoExt = basename(normalizedPath, ext);
3121
- const textAlts = [".md", ".txt", ".html"].map((e) => resolve2(dir, nameNoExt + e)).filter(existsSync6);
3122
- if (textAlts.length > 0) {
3123
- return `[PDF file: ${filePath}]
3124
- Cannot extract text from this PDF, but found alternative text versions:
3125
- ` + textAlts.map((p) => ` \u2192 ${basename(p)}`).join("\n") + `
3126
- Please use read_file to read the above files.`;
3127
- }
3128
- return `[PDF file: ${filePath}]
3129
- Cannot extract text from this PDF (requires pdftotext or pdfminer.six).
3130
- Suggestion: read existing text versions (.md / .txt) in the project, or install extraction tools via bash and retry.`;
3131
- }
3132
- if (BINARY_EXTENSIONS.has(ext)) {
3133
- return `[Binary file: ${filePath} (${ext})]
3134
- This is a binary file and cannot be read as text. If there is a text version (.md / .txt) in the project, please read that instead.`;
3135
- }
3136
- const buf = readFileSync4(normalizedPath);
3137
- if (encoding === "base64") {
3138
- return `[File: ${filePath} | base64]
3139
-
3140
- ${buf.toString("base64")}`;
3141
- }
3142
- if (isBinaryBuffer(buf)) {
3143
- return `[Binary file: ${filePath}]
3144
- This file contains binary data and cannot be read as text.
3145
- If needed, use the bash tool to run an appropriate conversion program.`;
3146
- }
3147
- const content = buf.toString(encoding);
3148
- const lines = content.split("\n").length;
3149
- return `${sensitiveWarning}[File: ${filePath} | ${lines} lines]
3150
-
3151
- ${content}`;
3152
- }
3153
- };
3154
-
3155
- // src/tools/builtin/write-file.ts
3156
- import { writeFileSync as writeFileSync4, appendFileSync, mkdirSync as mkdirSync3 } from "fs";
3157
- import { dirname as dirname2 } from "path";
3158
- var writeFileTool = {
3159
- definition: {
3160
- name: "write_file",
3161
- description: `Write content to a file. Creates the file if it doesn't exist, overwrites (append=false) or appends (append=true). Automatically creates parent directories.
3162
- Important: For long content (over 500 lines or 3000 chars), you MUST split into multiple calls using append=true, each no more than 300 lines, to avoid truncation. First call uses append=false (overwrite), subsequent calls use append=true.`,
3163
- parameters: {
3164
- path: {
3165
- type: "string",
3166
- description: "File path",
3167
- required: true
3168
- },
3169
- content: {
3170
- type: "string",
3171
- description: "Content to write",
3172
- required: true
3173
- },
3174
- append: {
3175
- type: "string",
3176
- description: "Whether to append. true=append to end, false=overwrite (default). Long files must be written in segments with append.",
3177
- required: false
3178
- },
3179
- encoding: {
3180
- type: "string",
3181
- description: "Encoding format, defaults to utf-8",
3182
- required: false
3183
- }
3184
- },
3185
- dangerous: false
3186
- // executor 会将 write_file 标记为 'write' 级别
3187
- },
3188
- async execute(args) {
3189
- const filePath = String(args["path"] ?? "");
3190
- const content = String(args["content"] ?? "");
3191
- const encoding = args["encoding"] ?? "utf-8";
3192
- const appendMode = String(args["append"] ?? "false").toLowerCase() === "true";
3193
- if (!filePath) throw new Error("path is required");
3194
- undoStack.push(filePath, `write_file${appendMode ? " (append)" : ""}: ${filePath}`);
3195
- mkdirSync3(dirname2(filePath), { recursive: true });
3196
- if (appendMode) {
3197
- appendFileSync(filePath, content, encoding);
3198
- } else {
3199
- writeFileSync4(filePath, content, encoding);
3200
- }
3201
- const lines = content.split("\n").length;
3202
- const mode = appendMode ? "appended" : "written";
3203
- return `File ${mode}: ${filePath} (${lines} lines, ${content.length} bytes)`;
3204
- }
3205
- };
3206
-
3207
- // src/tools/builtin/edit-file.ts
3208
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, existsSync as existsSync7 } from "fs";
3209
- function similarityScore(a, b) {
3210
- if (a === b) return 1;
3211
- if (a.length < 2 || b.length < 2) return 0;
3212
- const getBigrams = (s) => {
3213
- const bigrams = /* @__PURE__ */ new Map();
3214
- for (let i = 0; i < s.length - 1; i++) {
3215
- const bigram = s.slice(i, i + 2);
3216
- bigrams.set(bigram, (bigrams.get(bigram) ?? 0) + 1);
3217
- }
3218
- return bigrams;
3219
- };
3220
- const aBigrams = getBigrams(a.toLowerCase());
3221
- const bBigrams = getBigrams(b.toLowerCase());
3222
- let intersection = 0;
3223
- for (const [bigram, count] of aBigrams) {
3224
- intersection += Math.min(count, bBigrams.get(bigram) ?? 0);
3225
- }
3226
- return 2 * intersection / (a.length - 1 + b.length - 1);
3227
- }
3228
- function findSimilarLines(fileContent, searchStr, maxResults = 3) {
3229
- if (fileContent.length > 5e5) return [];
3230
- const fileLines = fileContent.split("\n");
3231
- const searchLines = searchStr.split("\n");
3232
- const firstSearchLine = searchLines[0].trim();
3233
- if (!firstSearchLine || firstSearchLine.length < 3) return [];
3234
- const candidates = [];
3235
- for (let i = 0; i < fileLines.length; i++) {
3236
- const trimmed = fileLines[i].trim();
3237
- if (!trimmed || trimmed.length < 3) continue;
3238
- const score = similarityScore(trimmed, firstSearchLine);
3239
- if (score > 0.4) {
3240
- candidates.push({ line: i + 1, text: fileLines[i], score });
3241
- }
3242
- }
3243
- return candidates.sort((a, b) => b.score - a.score).slice(0, maxResults).map((c) => ` Line ${c.line}: ${c.text.slice(0, 120)}`);
3244
- }
3245
- function findWhitespaceTolerant(fileLines, searchLines) {
3246
- const trimmedSearch = searchLines.map((l) => l.trim());
3247
- let matchStart = -1;
3248
- let matchCount = 0;
3249
- for (let i = 0; i <= fileLines.length - trimmedSearch.length; i++) {
3250
- let allMatch = true;
3251
- for (let j = 0; j < trimmedSearch.length; j++) {
3252
- if (fileLines[i + j].trim() !== trimmedSearch[j]) {
3253
- allMatch = false;
3254
- break;
3255
- }
3256
- }
3257
- if (allMatch) {
3258
- matchCount++;
3259
- if (matchStart === -1) matchStart = i;
3260
- if (matchCount > 1) break;
3261
- }
3262
- }
3263
- return { matchStart, matchCount };
3264
- }
3265
- var editFileTool = {
3266
- definition: {
3267
- name: "edit_file",
3268
- description: `Precisely edit file contents. Supports three modes:
3269
- 1. String replace (most common): Provide old_str and new_str to replace an exact match. old_str must appear exactly once in the file (unless replace_all is true).
3270
- 2. Line insert: Provide insert_after_line (1-based line number) and insert_content to insert after that line.
3271
- 3. Line delete: Provide delete_from_line and delete_to_line (inclusive) to delete that range.
3272
- Optional ignore_whitespace: true to match ignoring indentation differences.
3273
- Optional replace_all: true to replace ALL occurrences of old_str in the file at once (saves tool rounds when renaming variables/functions).
3274
- Note: Path can be absolute or relative to the current working directory.`,
3275
- parameters: {
3276
- path: {
3277
- type: "string",
3278
- description: "File path to edit",
3279
- required: true
3280
- },
3281
- old_str: {
3282
- type: "string",
3283
- description: "[Replace mode] Original string to replace, must appear exactly once (include enough context for uniqueness)",
3284
- required: false
3285
- },
3286
- new_str: {
3287
- type: "string",
3288
- description: "[Replace mode] New replacement string, can be empty to delete old_str",
3289
- required: false
3290
- },
3291
- ignore_whitespace: {
3292
- type: "boolean",
3293
- description: "[Replace mode] Whether to ignore leading/trailing whitespace per line when matching, defaults to false",
3294
- required: false
3295
- },
3296
- replace_all: {
3297
- type: "boolean",
3298
- description: "[Replace mode] Replace ALL occurrences of old_str instead of requiring unique match. Useful for renaming variables/functions across the file in one call.",
3299
- required: false
3300
- },
3301
- insert_after_line: {
3302
- type: "number",
3303
- description: "[Insert mode] Insert after this line number (1-based), 0 means insert at the beginning",
3304
- required: false
3305
- },
3306
- insert_content: {
3307
- type: "string",
3308
- description: "[Insert mode] Content to insert (no need to add newlines manually)",
3309
- required: false
3310
- },
3311
- delete_from_line: {
3312
- type: "number",
3313
- description: "[Delete mode] Start deleting from this line (1-based)",
3314
- required: false
3315
- },
3316
- delete_to_line: {
3317
- type: "number",
3318
- description: "[Delete mode] Delete up to and including this line (1-based)",
3319
- required: false
3320
- },
3321
- encoding: {
3322
- type: "string",
3323
- description: "File encoding, defaults to utf-8",
3324
- required: false
3325
- }
3326
- },
3327
- dangerous: false
3328
- // executor 中 edit_file 按 write 级别处理
3329
- },
3330
- async execute(args) {
3331
- const filePath = String(args["path"] ?? "");
3332
- const encoding = args["encoding"] ?? "utf-8";
3333
- if (!filePath) throw new Error("path is required");
3334
- if (!existsSync7(filePath)) throw new Error(`File not found: ${filePath}`);
3335
- const original = readFileSync5(filePath, encoding);
3336
- if (args["old_str"] !== void 0) {
3337
- const oldStr = String(args["old_str"]);
3338
- const newStr = String(args["new_str"] ?? "");
3339
- const ignoreWs = Boolean(args["ignore_whitespace"]);
3340
- const replaceAll = Boolean(args["replace_all"]);
3341
- if (oldStr === "") throw new Error("old_str cannot be empty");
3342
- if (ignoreWs) {
3343
- const fileLines = original.split("\n");
3344
- const searchLines = oldStr.split("\n");
3345
- const { matchStart, matchCount } = findWhitespaceTolerant(fileLines, searchLines);
3346
- if (matchStart === -1) {
3347
- const similar = findSimilarLines(original, oldStr);
3348
- const hint = similar.length > 0 ? `
3349
- Similar lines found (did you mean?):
3350
- ${similar.join("\n")}` : "";
3351
- return `ERROR: old_str not found in file (even with whitespace ignored).
3352
- File has ${fileLines.length} lines.${hint}
3353
- Please read the file first and use exact text.`;
3354
- }
3355
- if (matchCount > 1) {
3356
- return `ERROR: old_str matches multiple locations with whitespace-tolerant matching. Please include more surrounding context to make it unique.`;
3357
- }
3358
- undoStack.push(filePath, `edit_file (ws-replace): ${filePath}`);
3359
- const before = fileLines.slice(0, matchStart);
3360
- const after = fileLines.slice(matchStart + searchLines.length);
3361
- const updated2 = [...before, newStr, ...after].join("\n");
3362
- writeFileSync5(filePath, updated2, encoding);
3363
- return `Successfully edited ${filePath} (whitespace-tolerant match)
3364
- Location: around line ${matchStart + 1}
3365
- Replaced: ${searchLines.length} line(s) \u2192 ${newStr.split("\n").length} line(s)
3366
- Old: ${truncatePreview(oldStr)}
3367
- New: ${truncatePreview(newStr)}`;
3368
- }
3369
- if (replaceAll) {
3370
- const occurrences = original.split(oldStr).length - 1;
3371
- if (occurrences === 0) {
3372
- const similar = findSimilarLines(original, oldStr);
3373
- const hint = similar.length > 0 ? `
3374
- Similar lines found (did you mean?):
3375
- ${similar.join("\n")}` : "";
3376
- return `ERROR: old_str not found in file.${hint}
3377
- Please read the file first and use exact text.`;
3378
- }
3379
- undoStack.push(filePath, `edit_file (replace_all): ${filePath}`);
3380
- const updated2 = original.split(oldStr).join(newStr);
3381
- writeFileSync5(filePath, updated2, encoding);
3382
- return `Successfully edited ${filePath} (replace_all)
3383
- Replaced: ${occurrences} occurrence(s) of ${truncatePreview(oldStr)}
3384
- With: ${truncatePreview(newStr)}`;
3385
- }
3386
- const firstIndex = original.indexOf(oldStr);
3387
- if (firstIndex === -1) {
3388
- const lines = original.split("\n");
3389
- const similar = findSimilarLines(original, oldStr);
3390
- const hint = similar.length > 0 ? `
3391
- Similar lines found (did you mean?):
3392
- ${similar.join("\n")}` : "";
3393
- return `ERROR: old_str not found in file.
3394
- File has ${lines.length} lines.${hint}
3395
- Please read the file first and use exact text including whitespace/indentation.
3396
- Tip: You can also try ignore_whitespace: true to match ignoring indentation differences.`;
3397
- }
3398
- const secondIndex = original.indexOf(oldStr, firstIndex + 1);
3399
- if (secondIndex !== -1) {
3400
- return `ERROR: old_str appears multiple times in file (at least at positions ${firstIndex} and ${secondIndex}). Please include more surrounding context to make it unique.`;
3401
- }
3402
- undoStack.push(filePath, `edit_file (replace): ${filePath}`);
3403
- const updated = original.slice(0, firstIndex) + newStr + original.slice(firstIndex + oldStr.length);
3404
- writeFileSync5(filePath, updated, encoding);
3405
- const oldLines = oldStr.split("\n").length;
3406
- const newLines = newStr.split("\n").length;
3407
- const linesBefore = original.slice(0, firstIndex).split("\n").length;
3408
- return `Successfully edited ${filePath}
3409
- Location: around line ${linesBefore}
3410
- Replaced: ${oldLines} line(s) \u2192 ${newLines} line(s)
3411
- Old: ${truncatePreview(oldStr)}
3412
- New: ${truncatePreview(newStr)}`;
3413
- }
3414
- if (args["insert_after_line"] !== void 0) {
3415
- const afterLine = Number(args["insert_after_line"]);
3416
- const content = String(args["insert_content"] ?? "");
3417
- const lines = original.split("\n");
3418
- if (afterLine < 0 || afterLine > lines.length) {
3419
- throw new Error(`insert_after_line ${afterLine} is out of range (file has ${lines.length} lines)`);
3420
- }
3421
- undoStack.push(filePath, `edit_file (insert): ${filePath}`);
3422
- lines.splice(afterLine, 0, content);
3423
- writeFileSync5(filePath, lines.join("\n"), encoding);
3424
- return `Successfully inserted ${content.split("\n").length} line(s) after line ${afterLine} in ${filePath}`;
3425
- }
3426
- if (args["delete_from_line"] !== void 0) {
3427
- const fromLine = Number(args["delete_from_line"]);
3428
- const toLine = Number(args["delete_to_line"] ?? args["delete_from_line"]);
3429
- const lines = original.split("\n");
3430
- if (fromLine < 1 || toLine < fromLine || toLine > lines.length) {
3431
- throw new Error(
3432
- `Invalid line range: ${fromLine}-${toLine} (file has ${lines.length} lines, lines are 1-indexed)`
3433
- );
3434
- }
3435
- undoStack.push(filePath, `edit_file (delete): ${filePath}`);
3436
- const deleted = lines.splice(fromLine - 1, toLine - fromLine + 1);
3437
- writeFileSync5(filePath, lines.join("\n"), encoding);
3438
- return `Successfully deleted lines ${fromLine}-${toLine} (${deleted.length} lines) from ${filePath}`;
3439
- }
3440
- throw new Error(
3441
- "No operation specified. Provide either: (old_str + new_str) for replace, (insert_after_line + insert_content) for insert, or (delete_from_line + delete_to_line) for delete."
3442
- );
3443
- }
3444
- };
3445
- function truncatePreview(str, maxLen = 80) {
3446
- const oneLine = str.replace(/\n/g, "\u21B5");
3447
- if (oneLine.length <= maxLen) return JSON.stringify(str);
3448
- return JSON.stringify(oneLine.slice(0, maxLen)) + "...";
3449
- }
3450
-
3451
- // src/tools/builtin/list-dir.ts
3452
- import { readdirSync as readdirSync4, statSync as statSync3, existsSync as existsSync8 } from "fs";
3453
- import { join as join4, basename as basename2 } from "path";
3454
- var listDirTool = {
3455
- definition: {
3456
- name: "list_dir",
3457
- description: "List directory contents showing file names, types and sizes.",
3458
- parameters: {
3459
- path: {
3460
- type: "string",
3461
- description: "Directory path, defaults to current working directory",
3462
- required: false
3463
- },
3464
- recursive: {
3465
- type: "boolean",
3466
- description: "Whether to recursively list subdirectories, defaults to false",
3467
- required: false
3468
- }
3469
- },
3470
- dangerous: false
3471
- },
3472
- async execute(args) {
3473
- const dirPath = String(args["path"] ?? process.cwd());
3474
- const recursive = Boolean(args["recursive"] ?? false);
3475
- if (!existsSync8(dirPath)) {
3476
- const targetName = basename2(dirPath).toLowerCase();
3477
- const cwd = process.cwd();
3478
- const suggestions = [];
3479
- try {
3480
- for (const entry of readdirSync4(cwd, { withFileTypes: true })) {
3481
- if (entry.isDirectory() && entry.name.toLowerCase() === targetName) {
3482
- suggestions.push(entry.name);
3483
- }
3484
- }
3485
- } catch {
3486
- }
3487
- if (suggestions.length > 0) {
3488
- throw new Error(
3489
- `Directory not found: ${dirPath}
3490
- Current working directory: ${cwd}
3491
- Found similar directories:
3492
- ` + suggestions.map((s) => ` \u2192 ${s}`).join("\n") + `
3493
- Please retry with the correct relative path.`
3494
- );
3495
- }
3496
- throw new Error(
3497
- `Directory not found: ${dirPath}
3498
- Current working directory: ${cwd}
3499
- Please use list_dir (without path) to see the current directory structure first.`
3500
- );
3501
- }
3502
- const lines = [`Directory: ${dirPath}
3503
- `];
3504
- listRecursive(dirPath, "", recursive, lines);
3505
- return lines.join("\n");
3506
- }
3507
- };
3508
- function listRecursive(basePath, indent, recursive, lines) {
3509
- let entries;
3510
- try {
3511
- entries = readdirSync4(basePath, { withFileTypes: true });
3512
- } catch {
3513
- lines.push(`${indent}(permission denied)`);
3514
- return;
3515
- }
3516
- const sorted = entries.sort((a, b) => {
3517
- if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1;
3518
- return a.name.localeCompare(b.name);
3519
- });
3520
- for (const entry of sorted) {
3521
- const USEFUL_DOT_ENTRIES = /* @__PURE__ */ new Set([".github", ".vscode", ".idea", ".gitignore", ".gitattributes", ".editorconfig", ".eslintrc", ".prettierrc", ".npmrc"]);
3522
- if (entry.name === "node_modules" || entry.name.startsWith(".") && !USEFUL_DOT_ENTRIES.has(entry.name) && !entry.name.startsWith(".env")) {
3523
- if (entry.isDirectory()) {
3524
- lines.push(`${indent}\u{1F4C1} ${entry.name}/ (skipped)`);
3525
- }
3526
- continue;
3527
- }
3528
- if (entry.isDirectory()) {
3529
- lines.push(`${indent}\u{1F4C1} ${entry.name}/`);
3530
- if (recursive) {
3531
- listRecursive(join4(basePath, entry.name), indent + " ", true, lines);
3532
- }
3533
- } else {
3534
- try {
3535
- const stat = statSync3(join4(basePath, entry.name));
3536
- const size = formatSize(stat.size);
3537
- lines.push(`${indent}\u{1F4C4} ${entry.name} (${size})`);
3538
- } catch {
3539
- lines.push(`${indent}\u{1F4C4} ${entry.name}`);
3540
- }
3541
- }
3542
- }
3543
- }
3544
- function formatSize(bytes) {
3545
- if (bytes < 1024) return `${bytes}B`;
3546
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
3547
- return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
3548
- }
3549
-
3550
- // src/tools/builtin/grep-files.ts
3551
- import { readdirSync as readdirSync5, readFileSync as readFileSync6, statSync as statSync4, existsSync as existsSync9 } from "fs";
3552
- import { join as join5, relative } from "path";
3553
- var grepFilesTool = {
3554
- definition: {
3555
- name: "grep_files",
3556
- description: `Search for matching text or code in files, returning matching lines with line numbers. Use cases:
3557
- - Find function/class definitions (e.g. "function handleChat" or "class ProviderRegistry")
3558
- - Find all usages of a variable or string
3559
- - Check impact scope before modifying code
3560
- Supports regex. Automatically skips node_modules, dist, .git directories.`,
3561
- parameters: {
3562
- pattern: {
3563
- type: "string",
3564
- description: 'Search pattern, supports regex (e.g. "function\\s+\\w+" or literal "import React")',
3565
- required: true
3566
- },
3567
- path: {
3568
- type: "string",
3569
- description: "Search root directory, defaults to current working directory",
3570
- required: false
3571
- },
3572
- file_pattern: {
3573
- type: "string",
3574
- description: 'Filename filter with simple wildcards (e.g. "*.ts", "*.py", "*.json"), defaults to all text files',
3575
- required: false
3576
- },
3577
- ignore_case: {
3578
- type: "boolean",
3579
- description: "Whether to ignore case, defaults to false",
3580
- required: false
3581
- },
3582
- context_lines: {
3583
- type: "number",
3584
- description: "Number of context lines before/after each match, defaults to 0 (match line only)",
3585
- required: false
3586
- },
3587
- max_results: {
3588
- type: "number",
3589
- description: "Maximum number of results, defaults to 50",
3590
- required: false
3591
- }
3592
- },
3593
- dangerous: false
3594
- },
3595
- async execute(args) {
3596
- const pattern = String(args["pattern"] ?? "");
3597
- const rootPath = String(args["path"] ?? process.cwd());
3598
- const filePattern = args["file_pattern"] ? String(args["file_pattern"]) : void 0;
3599
- const ignoreCase = Boolean(args["ignore_case"] ?? false);
3600
- const contextLines = Math.max(0, Number(args["context_lines"] ?? 0));
3601
- const maxResults = Math.max(1, Number(args["max_results"] ?? 50));
3602
- if (!pattern) throw new Error("pattern is required");
3603
- if (!existsSync9(rootPath)) throw new Error(`Path not found: ${rootPath}`);
3604
- let regex;
3605
- try {
3606
- regex = new RegExp(pattern, ignoreCase ? "gi" : "g");
3607
- } catch {
3608
- const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3609
- regex = new RegExp(escaped, ignoreCase ? "gi" : "g");
3610
- }
3611
- const results = [];
3612
- const stat = statSync4(rootPath);
3613
- if (stat.isFile()) {
3614
- searchInFile(rootPath, rootPath, regex, contextLines, maxResults, results);
3615
- } else {
3616
- collectFiles(rootPath, filePattern, results, regex, contextLines, maxResults, rootPath);
3617
- }
3618
- if (results.length === 0) {
3619
- return `No matches found for pattern: ${pattern}
3620
- Searched in: ${rootPath}${filePattern ? `
3621
- File filter: ${filePattern}` : ""}`;
3622
- }
3623
- const lines = [
3624
- `Found ${results.length} match(es) for: ${pattern}`,
3625
- `Searched in: ${rootPath}`,
3626
- ""
3627
- ];
3628
- let currentFile = "";
3629
- for (const r of results) {
3630
- if (r.file !== currentFile) {
3631
- if (currentFile) lines.push("");
3632
- lines.push(`\u2500\u2500 ${r.file} \u2500\u2500`);
3633
- currentFile = r.file;
3634
- }
3635
- if (r.contextBefore) {
3636
- for (const [ln, text] of r.contextBefore) {
3637
- lines.push(` ${String(ln).padStart(4)}\u2502 ${text}`);
3638
- }
3639
- }
3640
- lines.push(`\u25B6 ${String(r.lineNumber).padStart(4)}\u2502 ${r.lineText}`);
3641
- if (r.contextAfter) {
3642
- for (const [ln, text] of r.contextAfter) {
3643
- lines.push(` ${String(ln).padStart(4)}\u2502 ${text}`);
3644
- }
3645
- }
3646
- }
3647
- if (results.length >= maxResults) {
3648
- lines.push("");
3649
- lines.push(`(Results truncated at ${maxResults}. Use max_results or narrow your search.)`);
3650
- }
3651
- return lines.join("\n");
3652
- }
3653
- };
3654
- var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "dist-cjs", "release", ".next", "__pycache__", ".cache", "coverage", ".nyc_output"]);
3655
- var BINARY_EXTS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".ico", ".woff", ".woff2", ".ttf", ".eot", ".otf", ".mp3", ".mp4", ".wav", ".zip", ".tar", ".gz", ".rar", ".7z", ".exe", ".dll", ".so", ".dylib", ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx"]);
3656
- function matchesFilePattern(filename, pattern) {
3657
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
3658
- return new RegExp(`^${escaped}$`, "i").test(filename);
3659
- }
3660
- function isBinary(filename) {
3661
- const ext = filename.slice(filename.lastIndexOf(".")).toLowerCase();
3662
- return BINARY_EXTS.has(ext);
3663
- }
3664
- function collectFiles(dirPath, filePattern, results, regex, contextLines, maxResults, rootPath) {
3665
- if (results.length >= maxResults) return;
3666
- let entries;
3667
- try {
3668
- entries = readdirSync5(dirPath, { withFileTypes: true });
3669
- } catch {
3670
- return;
3671
- }
3672
- for (const entry of entries) {
3673
- if (results.length >= maxResults) return;
3674
- if (entry.isDirectory()) {
3675
- if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
3676
- collectFiles(join5(dirPath, entry.name), filePattern, results, regex, contextLines, maxResults, rootPath);
3677
- } else if (entry.isFile()) {
3678
- if (isBinary(entry.name)) continue;
3679
- if (filePattern && !matchesFilePattern(entry.name, filePattern)) continue;
3680
- const fullPath = join5(dirPath, entry.name);
3681
- const relPath = relative(rootPath, fullPath);
3682
- searchInFile(fullPath, relPath, regex, contextLines, maxResults, results);
3683
- }
3684
- }
3685
- }
3686
- function searchInFile(fullPath, displayPath, regex, contextLines, maxResults, results) {
3687
- try {
3688
- const stat = statSync4(fullPath);
3689
- if (stat.size > 1e6) return;
3690
- } catch {
3691
- return;
3692
- }
3693
- let content;
3694
- try {
3695
- content = readFileSync6(fullPath, "utf-8");
3696
- } catch {
3697
- return;
3698
- }
3699
- const lines = content.split("\n");
3700
- regex.lastIndex = 0;
3701
- for (let i = 0; i < lines.length; i++) {
3702
- if (results.length >= maxResults) return;
3703
- regex.lastIndex = 0;
3704
- if (regex.test(lines[i])) {
3705
- const result = {
3706
- file: displayPath,
3707
- lineNumber: i + 1,
3708
- lineText: lines[i].trimEnd()
3709
- };
3710
- if (contextLines > 0) {
3711
- result.contextBefore = [];
3712
- result.contextAfter = [];
3713
- for (let c = Math.max(0, i - contextLines); c < i; c++) {
3714
- result.contextBefore.push([c + 1, lines[c].trimEnd()]);
3715
- }
3716
- for (let c = i + 1; c <= Math.min(lines.length - 1, i + contextLines); c++) {
3717
- result.contextAfter.push([c + 1, lines[c].trimEnd()]);
3718
- }
3719
- }
3720
- results.push(result);
3721
- }
3722
- }
3723
- }
3724
-
3725
- // src/tools/builtin/glob-files.ts
3726
- import { readdirSync as readdirSync6, statSync as statSync5, existsSync as existsSync10 } from "fs";
3727
- import { join as join6, relative as relative2, basename as basename3 } from "path";
3728
- var globFilesTool = {
3729
- definition: {
3730
- name: "glob_files",
3731
- description: `Find files by name or path pattern, returning a list of matching file paths. Use cases:
3732
- - Find all files of a type (e.g. "**/*.ts" for all TypeScript files)
3733
- - Find files by name (e.g. "**/index.ts" or "package.json")
3734
- - Find files in a directory (e.g. "src/components/**")
3735
- Results sorted by most recent modification time. Automatically skips node_modules, dist, .git directories.`,
3736
- parameters: {
3737
- pattern: {
3738
- type: "string",
3739
- description: 'Glob pattern, e.g. "**/*.ts", "src/**/*.tsx", "**/package.json", "*.md"',
3740
- required: true
3741
- },
3742
- path: {
3743
- type: "string",
3744
- description: "Search root directory, defaults to current working directory",
3745
- required: false
3746
- },
3747
- max_results: {
3748
- type: "number",
3749
- description: "Maximum number of files to return, defaults to 100",
3750
- required: false
3751
- }
3752
- },
3753
- dangerous: false
3754
- },
3755
- async execute(args) {
3756
- const pattern = String(args["pattern"] ?? "");
3757
- const rootPath = String(args["path"] ?? process.cwd());
3758
- const maxResults = Math.max(1, Number(args["max_results"] ?? 100));
3759
- if (!pattern) throw new Error("pattern is required");
3760
- if (!existsSync10(rootPath)) throw new Error(`Path not found: ${rootPath}`);
3761
- const regex = globToRegex(pattern);
3762
- const matches = [];
3763
- collectMatchingFiles(rootPath, rootPath, regex, matches, maxResults);
3764
- if (matches.length === 0) {
3765
- return `No files matched pattern: ${pattern}
3766
- Searched in: ${rootPath}`;
3767
- }
3768
- matches.sort((a, b) => b.mtime - a.mtime);
3769
- const lines = [
3770
- `Found ${matches.length} file(s) matching: ${pattern}`,
3771
- `Searched in: ${rootPath}`,
3772
- "",
3773
- ...matches.map((m) => m.relPath)
3774
- ];
3775
- if (matches.length >= maxResults) {
3776
- lines.push("");
3777
- lines.push(`(Results truncated at ${maxResults}. Use max_results or narrow your pattern.)`);
3778
- }
3779
- return lines.join("\n");
3780
- }
3781
- };
3782
- var SKIP_DIRS2 = /* @__PURE__ */ new Set([
3783
- "node_modules",
3784
- ".git",
3785
- "dist",
3786
- "dist-cjs",
3787
- "release",
3788
- ".next",
3789
- "__pycache__",
3790
- ".cache",
3791
- "coverage",
3792
- ".nyc_output",
3793
- ".turbo",
3794
- ".vite",
3795
- "build",
3796
- "out"
3797
- ]);
3798
- function globToRegex(pattern) {
3799
- const normalized = pattern.replace(/\\/g, "/");
3800
- let regStr = "";
3801
- let i = 0;
3802
- while (i < normalized.length) {
3803
- const ch = normalized[i];
3804
- if (ch === "*" && normalized[i + 1] === "*") {
3805
- regStr += ".*";
3806
- i += 2;
3807
- if (normalized[i] === "/") i++;
3808
- } else if (ch === "*") {
3809
- regStr += "[^/]*";
3810
- i++;
3811
- } else if (ch === "?") {
3812
- regStr += "[^/]";
3813
- i++;
3814
- } else if (".+^${}()|[]\\".includes(ch)) {
3815
- regStr += "\\" + ch;
3816
- i++;
3817
- } else {
3818
- regStr += ch;
3819
- i++;
3820
- }
3821
- }
3822
- if (!normalized.includes("/")) {
3823
- return new RegExp(`(^|/)${regStr}$`, "i");
3824
- }
3825
- return new RegExp(`(^|/)${regStr}$`, "i");
3826
- }
3827
- function collectMatchingFiles(dirPath, rootPath, regex, results, maxResults) {
3828
- if (results.length >= maxResults) return;
3829
- let entries;
3830
- try {
3831
- entries = readdirSync6(dirPath, { withFileTypes: true });
3832
- } catch {
3833
- return;
3834
- }
3835
- for (const entry of entries) {
3836
- if (results.length >= maxResults) break;
3837
- const fullPath = join6(dirPath, entry.name);
3838
- if (entry.isDirectory()) {
3839
- if (SKIP_DIRS2.has(entry.name) || entry.name.startsWith(".")) continue;
3840
- collectMatchingFiles(fullPath, rootPath, regex, results, maxResults);
3841
- } else if (entry.isFile()) {
3842
- const relPath = relative2(rootPath, fullPath).replace(/\\/g, "/");
3843
- if (regex.test(relPath) || regex.test(basename3(relPath))) {
3844
- try {
3845
- const stat = statSync5(fullPath);
3846
- results.push({ relPath, absPath: fullPath, mtime: stat.mtimeMs });
3847
- } catch {
3848
- results.push({ relPath, absPath: fullPath, mtime: 0 });
3849
- }
3850
- }
3851
- }
3852
- }
3853
- }
3854
-
3855
- // src/tools/builtin/run-interactive.ts
3856
- import { spawn } from "child_process";
3857
- import { platform as platform2 } from "os";
3858
- var IS_WINDOWS2 = platform2() === "win32";
3859
- var runInteractiveTool = {
3860
- definition: {
3861
- name: "run_interactive",
3862
- description: `Run a CLI program that requires stdin interaction (e.g. Python games, Q&A scripts, menu programs). Pre-provide all input lines via stdin_lines array; the program consumes them sequentially and returns full output. [Important] args must be a string array like ["workspace/guess.py"], not a string. [Guessing strategy] For 1-100 guessing: start at 50, use binary search \u2014 at most 7 guesses. Example stdin_lines: ["50","25","37","43","40","41","n"] ("n"=don't play again). Windows Python path: C:\\Users\\Jinzd\\anaconda3\\envs\\python312\\python.exe`,
3863
- parameters: {
3864
- executable: {
3865
- type: "string",
3866
- description: 'Full path or command name of the executable, e.g. "python", "C:\\\\Users\\\\Jinzd\\\\anaconda3\\\\envs\\\\python312\\\\python.exe", "node"',
3867
- required: true
3868
- },
3869
- args: {
3870
- type: "array",
3871
- description: 'Arguments array for the executable, e.g. ["workspace/guess.py"] or ["-c", "print(1)"]',
3872
- items: { type: "string" },
3873
- required: true
3874
- },
3875
- stdin_lines: {
3876
- type: "array",
3877
- description: `Input lines to feed to the program's stdin in order. E.g. for a guessing game: ["50", "25", "37"]. Each line gets a newline appended automatically.`,
3878
- items: { type: "string" },
3879
- required: true
3880
- },
3881
- timeout: {
3882
- type: "number",
3883
- description: "Overall timeout in milliseconds, defaults to 20000 (20s)",
3884
- required: false
3885
- }
3886
- },
3887
- dangerous: false
3888
- },
3889
- async execute(args) {
3890
- const executable = String(args["executable"] ?? "").trim();
3891
- const rawArgs = args["args"];
3892
- let argsTypeWarning = "";
3893
- const cmdArgs = Array.isArray(rawArgs) ? rawArgs.map(String) : typeof rawArgs === "string" && rawArgs.trim() ? (() => {
3894
- argsTypeWarning = `[Warning] "args" should be an array but got string: ${JSON.stringify(rawArgs)}. Applied fallback.
3895
- `;
3896
- process.stderr.write(argsTypeWarning);
3897
- return [rawArgs.trim()];
3898
- })() : [];
3899
- const rawStdin = args["stdin_lines"];
3900
- let stdinTypeWarning = "";
3901
- const stdinLines = Array.isArray(rawStdin) ? rawStdin.map(String) : typeof rawStdin === "string" && rawStdin.trim() ? (() => {
3902
- stdinTypeWarning = `[Warning] "stdin_lines" should be an array but got string: ${JSON.stringify(rawStdin.slice(0, 80))}. Applied fallback.
3903
- `;
3904
- process.stderr.write(stdinTypeWarning);
3905
- return rawStdin.split(",").map((s) => s.trim()).filter(Boolean);
3906
- })() : [];
3907
- const timeout = Math.min(Math.max(Number(args["timeout"] ?? 2e4), 1e3), 3e5);
3908
- if (!executable) {
3909
- throw new Error("executable is required");
3910
- }
3911
- const env = {
3912
- ...process.env,
3913
- // 强制 Python UTF-8 模式,修复 Windows 中文乱码
3914
- PYTHONUTF8: "1",
3915
- PYTHONIOENCODING: "utf-8",
3916
- PYTHONDONTWRITEBYTECODE: "1"
3917
- };
3918
- const prefixWarnings = [argsTypeWarning, stdinTypeWarning].filter(Boolean).join("");
3919
- return new Promise((resolve4) => {
3920
- const child = spawn(executable, cmdArgs.map(String), {
3921
- cwd: process.cwd(),
3922
- env,
3923
- stdio: ["pipe", "pipe", "pipe"]
3924
- });
3925
- let stdout = "";
3926
- let stderr = "";
3927
- child.stdout.setEncoding("utf-8");
3928
- child.stderr.setEncoding("utf-8");
3929
- child.stdout.on("data", (chunk) => {
3930
- stdout += chunk;
3931
- });
3932
- child.stderr.on("data", (chunk) => {
3933
- stderr += chunk;
3934
- });
3935
- let lineIdx = 0;
3936
- const writeNextLine = () => {
3937
- if (lineIdx < stdinLines.length && !child.stdin.destroyed) {
3938
- const line = stdinLines[lineIdx++] + (IS_WINDOWS2 ? "\r\n" : "\n");
3939
- const canContinue = child.stdin.write(line);
3940
- if (canContinue) {
3941
- setTimeout(writeNextLine, 150);
3942
- } else {
3943
- child.stdin.once("drain", () => setTimeout(writeNextLine, 150));
3944
- }
3945
- } else if (!child.stdin.destroyed) {
3946
- child.stdin.end();
3947
- }
3948
- };
3949
- setTimeout(writeNextLine, 400);
3950
- const timer = setTimeout(() => {
3951
- child.kill();
3952
- resolve4(`${prefixWarnings}[Timeout after ${timeout}ms]
3953
- ${buildOutput(stdout, stderr)}`);
3954
- }, timeout);
3955
- child.on("close", (code) => {
3956
- clearTimeout(timer);
3957
- const output = buildOutput(stdout, stderr);
3958
- if (code !== 0 && code !== null) {
3959
- resolve4(`${prefixWarnings}Exit code ${code}:
3960
- ${output}`);
3961
- } else {
3962
- resolve4(`${prefixWarnings}${output || "(no output)"}`);
3963
- }
3964
- });
3965
- child.on("error", (err) => {
3966
- clearTimeout(timer);
3967
- resolve4(
3968
- `${prefixWarnings}Failed to start process "${executable}": ${err.message}
3969
- Hint: On Windows, use the full path to the executable, e.g.:
3970
- C:\\Users\\Jinzd\\anaconda3\\envs\\python312\\python.exe`
3971
- );
3972
- });
3973
- });
3974
- }
3975
- };
3976
- function buildOutput(stdout, stderr) {
3977
- const parts = [];
3978
- if (stdout.trim()) parts.push(stdout);
3979
- if (stderr.trim()) parts.push(`[stderr]
3980
- ${stderr}`);
3981
- return parts.join("\n") || "(no output)";
3982
- }
3983
-
3984
- // src/tools/builtin/web-fetch.ts
3985
- import { promises as dnsPromises } from "dns";
3986
- function htmlToText(html) {
3987
- const HTML_REGEX_LIMIT = 2e5;
3988
- if (html.length > HTML_REGEX_LIMIT) {
3989
- html = html.slice(0, HTML_REGEX_LIMIT);
3990
- }
3991
- let text = html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<noscript[\s\S]*?<\/noscript>/gi, "").replace(/<svg[\s\S]*?<\/svg>/gi, "");
3992
- text = text.replace(/<h([1-6])[^>]*>([\s\S]*?)<\/h\1>/gi, (_m, lvl, content) => {
3993
- const prefix = "#".repeat(Number(lvl));
3994
- return `
3995
- ${prefix} ${stripTags(content).trim()}
3996
- `;
3997
- });
3998
- text = text.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gi, (_m, code) => {
3999
- return "\n```\n" + stripTags(code) + "\n```\n";
4000
- });
4001
- text = text.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, (_m, item) => {
4002
- return `
4003
- - ${stripTags(item).trim()}`;
4004
- });
4005
- text = text.replace(/<\/(p|div|section|article|blockquote|tr)>/gi, "\n");
4006
- text = text.replace(/<br\s*\/?>/gi, "\n");
4007
- text = text.replace(/<a[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, (_m, href, label) => {
4008
- const l = stripTags(label).trim();
4009
- if (href.startsWith("http") && l && l !== href) return `[${l}](${href})`;
4010
- return l || href;
4011
- });
4012
- text = stripTags(text);
4013
- text = text.replace(/\n{3,}/g, "\n\n");
4014
- text = text.split("\n").map((l) => l.trimEnd()).join("\n");
4015
- return text.trim();
4016
- }
4017
- function stripTags(html) {
4018
- return html.replace(/<[^>]+>/g, "");
4019
- }
4020
- function extractTitle(html) {
4021
- const m = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
4022
- return m ? stripTags(m[1]).trim() : "";
4023
- }
4024
- function extractDescription(html) {
4025
- const m = html.match(/<meta[^>]+name=["']description["'][^>]+content=["']([^"']+)["']/i) ?? html.match(/<meta[^>]+content=["']([^"']+)["'][^>]+name=["']description["']/i);
4026
- return m ? m[1].trim() : "";
4027
- }
4028
- var MAX_OUTPUT = 16e3;
4029
- function isPrivateHost(hostname) {
4030
- const h = hostname.toLowerCase().replace(/^\[|\]$/g, "");
4031
- if (h === "localhost" || h === "0.0.0.0" || h === "::1") return true;
4032
- if (h.startsWith("fe80:")) return true;
4033
- const m = h.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
4034
- if (m) {
4035
- const [o1, o2] = [Number(m[1]), Number(m[2])];
4036
- if (o1 === 127) return true;
4037
- if (o1 === 10) return true;
4038
- if (o1 === 172 && o2 >= 16 && o2 <= 31) return true;
4039
- if (o1 === 192 && o2 === 168) return true;
4040
- if (o1 === 169 && o2 === 254) return true;
4041
- if (o1 === 0) return true;
4042
- }
4043
- return false;
4044
- }
4045
- async function resolveAndCheck(hostname) {
4046
- const h = hostname.replace(/^\[|\]$/g, "");
4047
- if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h) || h.includes(":")) return;
4048
- try {
4049
- const { address } = await dnsPromises.lookup(h);
4050
- if (isPrivateHost(address)) {
4051
- throw new Error(`Blocked: "${hostname}" resolves to private address ${address}. web_fetch is restricted to public URLs.`);
4052
- }
4053
- } catch (e) {
4054
- if (e.message.startsWith("Blocked:")) throw e;
4055
- }
4056
- }
4057
- var webFetchTool = {
4058
- definition: {
4059
- name: "web_fetch",
4060
- description: "Fetch a URL and return its content as plain text / Markdown. Use this to read documentation, API references, READMEs, articles, or any public web page. Follows redirects automatically. Returns the first ~16000 characters of extracted text.",
4061
- parameters: {
4062
- url: {
4063
- type: "string",
4064
- description: "The full URL to fetch (must start with http:// or https://)",
4065
- required: true
4066
- },
4067
- selector: {
4068
- type: "string",
4069
- description: "Optional: keyword to search in the extracted text. If provided, returns only the paragraphs / sections that contain this keyword (case-insensitive).",
4070
- required: false
4071
- }
4072
- }
4073
- },
4074
- async execute(args) {
4075
- const url = String(args["url"] ?? "").trim();
4076
- const selector = args["selector"] ? String(args["selector"]).trim() : "";
4077
- if (!url.startsWith("http://") && !url.startsWith("https://")) {
4078
- throw new Error(`Invalid URL: "${url}". URL must start with http:// or https://`);
4079
- }
4080
- try {
4081
- const parsedUrl = new URL(url);
4082
- if (isPrivateHost(parsedUrl.hostname)) {
4083
- throw new Error(`Blocked: "${url}" resolves to a private/internal address. web_fetch is restricted to public URLs.`);
4084
- }
4085
- await resolveAndCheck(parsedUrl.hostname);
4086
- } catch (e) {
4087
- if (e.message.startsWith("Blocked:")) throw e;
4088
- throw new Error(`Invalid URL: "${url}"`);
4089
- }
4090
- const controller = new AbortController();
4091
- const timeoutId = setTimeout(() => controller.abort(), 2e4);
4092
- let rawHtml;
4093
- let finalUrl;
4094
- let contentType;
4095
- const MAX_REDIRECTS = 10;
4096
- const FETCH_HEADERS = {
4097
- "User-Agent": "Mozilla/5.0 (compatible; ai-cli/1.0; +https://github.com/ai-cli)",
4098
- Accept: "text/html,application/xhtml+xml,text/plain,*/*",
4099
- "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8"
4100
- };
4101
- try {
4102
- let currentUrl = url;
4103
- let resp = null;
4104
- for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
4105
- const parsedHop = new URL(currentUrl);
4106
- if (isPrivateHost(parsedHop.hostname)) {
4107
- throw new Error(`Blocked: redirect to private/internal address "${currentUrl}".`);
4108
- }
4109
- await resolveAndCheck(parsedHop.hostname);
4110
- const r = await fetch(currentUrl, {
4111
- signal: controller.signal,
4112
- headers: FETCH_HEADERS,
4113
- redirect: "manual"
4114
- // 手动控制重定向
4115
- });
4116
- if (r.status >= 300 && r.status < 400) {
4117
- if (hop >= MAX_REDIRECTS) {
4118
- throw new Error(`Too many redirects (>${MAX_REDIRECTS}): ${url}`);
4119
- }
4120
- const location = r.headers.get("Location");
4121
- if (!location) {
4122
- resp = r;
4123
- break;
4124
- }
4125
- currentUrl = new URL(location, currentUrl).href;
4126
- continue;
4127
- }
4128
- resp = r;
4129
- break;
4130
- }
4131
- clearTimeout(timeoutId);
4132
- if (!resp) throw new Error(`Too many redirects (>${MAX_REDIRECTS}): ${url}`);
4133
- finalUrl = currentUrl;
4134
- contentType = resp.headers.get("content-type") ?? "";
4135
- if (!resp.ok) {
4136
- throw new Error(`HTTP ${resp.status} ${resp.statusText}`);
4137
- }
4138
- const buf = await resp.arrayBuffer();
4139
- rawHtml = new TextDecoder("utf-8", { fatal: false }).decode(buf.slice(0, 2e6));
4140
- } catch (err) {
4141
- clearTimeout(timeoutId);
4142
- if (err.name === "AbortError") {
4143
- throw new Error(`Request timed out after 20s: ${url}`);
4144
- }
4145
- throw err;
4146
- }
4147
- let text;
4148
- if (contentType.includes("text/plain") || contentType.includes("application/json")) {
4149
- text = rawHtml;
4150
- } else {
4151
- const title = extractTitle(rawHtml);
4152
- const desc = extractDescription(rawHtml);
4153
- let body = htmlToText(rawHtml);
4154
- const header = [
4155
- title ? `# ${title}` : "",
4156
- desc ? `> ${desc}` : "",
4157
- `Source: ${finalUrl}`,
4158
- ""
4159
- ].filter(Boolean).join("\n");
4160
- text = header + "\n\n" + body;
4161
- }
4162
- if (selector) {
4163
- const lower = selector.toLowerCase();
4164
- const paragraphs = text.split("\n\n");
4165
- const matched = paragraphs.filter((p) => p.toLowerCase().includes(lower));
4166
- if (matched.length > 0) {
4167
- text = `[Filtered by keyword: "${selector}"]
4168
-
4169
- ` + matched.join("\n\n");
4170
- } else {
4171
- text = `[No paragraphs contain keyword: "${selector}"]
4172
-
4173
- ` + text;
4174
- }
4175
- }
4176
- if (text.length > MAX_OUTPUT) {
4177
- const kept = text.slice(0, MAX_OUTPUT);
4178
- text = kept + `
4179
-
4180
- ... [Content truncated: ${text.length} chars total, returning first ${MAX_OUTPUT} chars. Use the selector parameter to narrow scope for more details] ...`;
4181
- }
4182
- return text;
4183
- }
4184
- };
4185
-
4186
- // src/tools/builtin/save-last-response.ts
4187
- import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync4 } from "fs";
4188
- import { dirname as dirname3 } from "path";
4189
- var lastResponseStore = { content: "" };
4190
- var saveLastResponseTool = {
4191
- definition: {
4192
- name: "save_last_response",
4193
- description: `Tool for generating and saving large documents (exams, reports, long articles) to a file.
4194
-
4195
- [Usage] When the user requests generating and saving a large document, call this tool with the target path:
4196
- - The system will automatically initiate a streaming generation request, outputting content to both terminal and disk (tee mode)
4197
- - No need to output content first then call \u2014 the system handles "generate + save" in one step
4198
- - Only the file path is passed as a parameter, content is NOT passed via arguments, avoiding API argument truncation
4199
-
4200
- [Warning] Do NOT use write_file to save large content \u2014 write_file's content parameter gets truncated by the API for large documents, resulting in incomplete files.
4201
-
4202
- [Use cases] Exam papers (600-700 lines, 15-25KB), technical reports, long articles \u2014 any content over 2KB.`,
4203
- parameters: {
4204
- path: {
4205
- type: "string",
4206
- description: "File path to save to (including filename), e.g. reports/2026-summary.md",
4207
- required: true
4208
- }
4209
- },
4210
- dangerous: false
4211
- // getDangerLevel 中标记为 write
4212
- },
4213
- async execute(args) {
4214
- const filePath = String(args["path"] ?? "");
4215
- if (!filePath) throw new Error("path is required");
4216
- const content = lastResponseStore.content;
4217
- if (!content) {
4218
- throw new Error("No content to save: AI has not produced any response yet, or the last response was empty.");
4219
- }
4220
- undoStack.push(filePath, `save_last_response: ${filePath}`);
4221
- mkdirSync4(dirname3(filePath), { recursive: true });
4222
- writeFileSync6(filePath, content, "utf-8");
4223
- const lines = content.split("\n").length;
4224
- return `File saved: ${filePath} (${lines} lines, ${content.length} bytes)`;
4225
- }
4226
- };
4227
-
4228
- // src/tools/builtin/save-memory.ts
4229
- import { existsSync as existsSync11, statSync as statSync6, appendFileSync as appendFileSync2, mkdirSync as mkdirSync5 } from "fs";
4230
- import { join as join7 } from "path";
4231
- import { homedir as homedir3 } from "os";
4232
- function getMemoryFilePath() {
4233
- return join7(homedir3(), CONFIG_DIR_NAME, MEMORY_FILE_NAME);
4234
- }
4235
- function formatTimestamp() {
4236
- const now = /* @__PURE__ */ new Date();
4237
- const pad = (n) => String(n).padStart(2, "0");
4238
- return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
4239
- }
4240
- var saveMemoryTool = {
4241
- definition: {
4242
- name: "save_memory",
4243
- description: "Save important information to persistent memory that survives across sessions. Use this to remember: user preferences, coding style, project architecture decisions, recurring patterns, key findings, or any knowledge worth preserving. Keep each memory entry concise (1-3 sentences). The content will be automatically timestamped.",
4244
- parameters: {
4245
- content: {
4246
- type: "string",
4247
- description: "The information to save to persistent memory. Keep it concise and actionable.",
4248
- required: true
4249
- }
4250
- },
4251
- dangerous: false
4252
- },
4253
- async execute(args) {
4254
- const content = String(args["content"] ?? "").trim();
4255
- if (!content) throw new Error("content is required");
4256
- const memoryPath = getMemoryFilePath();
4257
- const configDir = join7(homedir3(), CONFIG_DIR_NAME);
4258
- if (!existsSync11(configDir)) {
4259
- mkdirSync5(configDir, { recursive: true });
4260
- }
4261
- const timestamp = formatTimestamp();
4262
- const entry = `
4263
- ## ${timestamp}
4264
- ${content}
4265
- `;
4266
- appendFileSync2(memoryPath, entry, "utf-8");
4267
- const byteSize = statSync6(memoryPath).size;
4268
- return `Memory saved successfully. File size: ${byteSize} bytes in ${MEMORY_FILE_NAME}`;
4269
- }
4270
- };
4271
-
4272
- // src/tools/builtin/ask-user.ts
4273
- import chalk from "chalk";
4274
- var askUserContext = {
4275
- prompting: false
4276
- };
4277
- var askUserTool = {
4278
- definition: {
4279
- name: "ask_user",
4280
- description: "Ask the user a question and wait for their text response. Use this when you need clarification, confirmation, or any information from the user before proceeding with a task. The user will see the question in the terminal and can type a response. Returns the user's answer as text.",
4281
- parameters: {
4282
- question: {
4283
- type: "string",
4284
- description: "The question to ask the user. Be clear and specific.",
4285
- required: true
4286
- }
4287
- },
4288
- dangerous: false
4289
- },
4290
- async execute(args) {
4291
- const question = String(args["question"] ?? "").trim();
4292
- if (!question) throw new Error("question parameter is required");
4293
- if (!askUserContext.rl) {
4294
- throw new Error("ask_user is not available in this context (readline not initialized)");
4295
- }
4296
- const answer = await promptUser(askUserContext.rl, question);
4297
- if (answer === null) {
4298
- return "User did not respond (cancelled).";
4299
- }
4300
- return `User response: ${answer}`;
4301
- }
4302
- };
4303
- function promptUser(rl, question) {
4304
- const rlAny = rl;
4305
- const savedOutput = rlAny.output;
4306
- rlAny.output = process.stdout;
4307
- rl.resume();
4308
- askUserContext.prompting = true;
4309
- console.log();
4310
- console.log(chalk.cyan("\u2753 ") + chalk.bold(question));
4311
- process.stdout.write(chalk.cyan("> "));
4312
- return new Promise((resolve4) => {
4313
- let completed = false;
4314
- const cleanup = (answer) => {
4315
- if (completed) return;
4316
- completed = true;
4317
- rl.removeListener("line", onLine);
4318
- askUserContext.cancelFn = void 0;
4319
- rl.pause();
4320
- rlAny.output = savedOutput;
4321
- askUserContext.prompting = false;
4322
- resolve4(answer);
4323
- };
4324
- const onLine = (line) => {
4325
- cleanup(line);
4326
- };
4327
- askUserContext.cancelFn = () => {
4328
- process.stdout.write(chalk.gray("\n(cancelled)\n"));
4329
- cleanup(null);
4330
- };
4331
- rl.once("line", onLine);
4332
- });
4333
- }
4334
-
4335
- // src/tools/builtin/write-todos.ts
4336
- import chalk2 from "chalk";
4337
- var VALID_STATUSES = /* @__PURE__ */ new Set(["pending", "in_progress", "completed"]);
4338
- var currentTodos = [];
4339
- var writeTodosTool = {
4340
- definition: {
4341
- name: "write_todos",
4342
- description: `Create or update a task list to track progress on complex tasks. Pass the COMPLETE list of todos each time (full replacement, not incremental). The list will be rendered in the terminal so the user can see progress. Use this proactively when working on multi-step tasks to show the user what you're doing.
4343
- Parameter format: JSON array string, e.g. [{"title":"Read config","status":"completed"},{"title":"Parse args","status":"in_progress"},{"title":"Run tests","status":"pending"}]
4344
- Valid statuses: pending, in_progress, completed.`,
4345
- parameters: {
4346
- todos: {
4347
- type: "string",
4348
- description: 'JSON array of todo objects. Each object must have "title" (string) and "status" ("pending"|"in_progress"|"completed"). Example: [{"title":"Step 1","status":"completed"},{"title":"Step 2","status":"in_progress"}]',
4349
- required: true
4350
- }
4351
- },
4352
- dangerous: false
4353
- },
4354
- async execute(args) {
4355
- const raw = args["todos"];
4356
- let parsed;
4357
- if (typeof raw === "string") {
4358
- const trimmed = raw.trim();
4359
- if (!trimmed) throw new Error("todos parameter is required");
4360
- try {
4361
- parsed = JSON.parse(trimmed);
4362
- } catch (err) {
4363
- throw new Error(`Invalid JSON in todos parameter: ${err.message}`);
4364
- }
4365
- } else if (Array.isArray(raw)) {
4366
- parsed = raw;
4367
- } else {
4368
- throw new Error("todos parameter must be a JSON array string");
4369
- }
4370
- if (!Array.isArray(parsed)) {
4371
- throw new Error("todos must be a JSON array");
4372
- }
4373
- const todos = parsed.map((item, i) => {
4374
- if (typeof item !== "object" || item === null) {
4375
- throw new Error(`todos[${i}] must be an object`);
4376
- }
4377
- const obj = item;
4378
- const title = String(obj["title"] ?? "").trim();
4379
- const status = String(obj["status"] ?? "").trim();
4380
- if (!title) throw new Error(`todos[${i}].title is required`);
4381
- if (!VALID_STATUSES.has(status)) {
4382
- throw new Error(`todos[${i}].status must be one of: pending, in_progress, completed (got "${status}")`);
4383
- }
4384
- return { title, status };
4385
- });
4386
- currentTodos = todos;
4387
- renderTodoList(todos);
4388
- const completed = todos.filter((t) => t.status === "completed").length;
4389
- const inProgress = todos.filter((t) => t.status === "in_progress").length;
4390
- const pending = todos.filter((t) => t.status === "pending").length;
4391
- return `Todo list updated: ${todos.length} items (${completed} completed, ${inProgress} in progress, ${pending} pending)`;
4392
- }
4393
- };
4394
- function renderTodoList(todos) {
4395
- const completed = todos.filter((t) => t.status === "completed").length;
4396
- const total = todos.length;
4397
- console.log();
4398
- console.log(
4399
- chalk2.bold.cyan("\u{1F4CB} Todo List") + chalk2.dim(` (${completed}/${total} completed)`)
4400
- );
4401
- console.log(chalk2.dim(" " + "\u2500".repeat(40)));
4402
- for (const todo of todos) {
4403
- let icon;
4404
- let text;
4405
- switch (todo.status) {
4406
- case "completed":
4407
- icon = chalk2.green(" \u2713 ");
4408
- text = chalk2.strikethrough.gray(todo.title);
4409
- break;
4410
- case "in_progress":
4411
- icon = chalk2.yellow(" \u2192 ");
4412
- text = chalk2.white(todo.title);
4413
- break;
4414
- case "pending":
4415
- default:
4416
- icon = chalk2.gray(" \u25CB ");
4417
- text = chalk2.gray(todo.title);
4418
- break;
4419
- }
4420
- console.log(icon + text);
4421
- }
4422
- console.log();
4423
- }
4424
-
4425
- // src/tools/builtin/google-search.ts
4426
- var GOOGLE_SEARCH_API = "https://www.googleapis.com/customsearch/v1";
4427
- var REQUEST_TIMEOUT_MS = 15e3;
4428
- var MAX_RESULTS = 10;
4429
- var DEFAULT_RESULTS = 5;
4430
- var googleSearchContext = {};
4431
- var googleSearchTool = {
4432
- definition: {
4433
- name: "google_search",
4434
- description: "Search the web using Google Custom Search API. Returns titles, URLs, and descriptions of search results. Use this to look up current information, verify facts, find documentation, or research topics that require up-to-date web data.",
4435
- parameters: {
4436
- query: {
4437
- type: "string",
4438
- description: "The search query string.",
4439
- required: true
4440
- },
4441
- num_results: {
4442
- type: "number",
4443
- description: "Number of results to return (1-10, default 5).",
4444
- required: false
4445
- }
4446
- },
4447
- dangerous: false
4448
- },
4449
- async execute(args) {
4450
- const query = String(args["query"] ?? "").trim();
4451
- if (!query) throw new Error("query parameter is required");
4452
- const numResults = Math.min(
4453
- Math.max(Math.floor(Number(args["num_results"] ?? DEFAULT_RESULTS)), 1),
4454
- MAX_RESULTS
4455
- );
4456
- const { apiKey, cx } = resolveConfig();
4457
- const url = new URL(GOOGLE_SEARCH_API);
4458
- url.searchParams.set("key", apiKey);
4459
- url.searchParams.set("cx", cx);
4460
- url.searchParams.set("q", query);
4461
- url.searchParams.set("num", String(numResults));
4462
- const controller = new AbortController();
4463
- const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
4464
- try {
4465
- const response = await fetch(url.toString(), {
4466
- method: "GET",
4467
- headers: {
4468
- "Accept": "application/json"
4469
- },
4470
- signal: controller.signal
4471
- });
4472
- if (!response.ok) {
4473
- const errorBody = await response.text().catch(() => "");
4474
- if (response.status === 403) {
4475
- throw new Error(
4476
- `Google Search API 403 Forbidden \u2014 Invalid API Key or daily free quota exceeded (100/day).
4477
- Please check your API Key and Search Engine ID configuration.`
4478
- );
4479
- }
4480
- if (response.status === 429) {
4481
- throw new Error("Google Search API 429 \u2014 Too many requests, please try again later.");
4482
- }
4483
- throw new Error(
4484
- `Google Search API error: HTTP ${response.status} ${response.statusText}
4485
- ${errorBody.slice(0, 500)}`
4486
- );
4487
- }
4488
- const data = await response.json();
4489
- return formatResults(query, data, numResults);
4490
- } catch (err) {
4491
- if (err instanceof Error && err.name === "AbortError") {
4492
- throw new Error(`Google Search request timed out (${REQUEST_TIMEOUT_MS / 1e3}s). Please check your network or proxy configuration.`);
4493
- }
4494
- throw err;
4495
- } finally {
4496
- clearTimeout(timeout);
4497
- }
4498
- }
4499
- };
4500
- function resolveConfig() {
4501
- let apiKey;
4502
- let cx;
4503
- if (googleSearchContext.configManager) {
4504
- apiKey = googleSearchContext.configManager.getApiKey("google-search");
4505
- cx = EnvLoader.getGoogleSearchEngineId() ?? googleSearchContext.configManager.get("googleSearchEngineId");
4506
- } else {
4507
- apiKey = EnvLoader.getApiKey("google-search");
4508
- cx = EnvLoader.getGoogleSearchEngineId();
4509
- }
4510
- if (!apiKey) {
4511
- throw new Error(
4512
- 'Google Search API Key not configured.\nConfigure via one of:\n 1. Run /config \u2192 Configure Google Search\n 2. Set env var AICLI_API_KEY_GOOGLESEARCH\n 3. Add apiKeys["google-search"] to ~/.aicli/config.json'
4513
- );
4514
- }
4515
- if (!cx) {
4516
- throw new Error(
4517
- "Google Search Engine ID (cx) not configured.\nConfigure via one of:\n 1. Run /config \u2192 Configure Google Search\n 2. Set env var AICLI_GOOGLE_CX\n 3. Add googleSearchEngineId to ~/.aicli/config.json\n\nGet one at: https://programmablesearchengine.google.com/ \u2192 Create search engine \u2192 Copy Search Engine ID"
4518
- );
4519
- }
4520
- return { apiKey, cx };
4521
- }
4522
- function formatResults(query, data, requested) {
4523
- const items = data.items ?? [];
4524
- if (items.length === 0) {
4525
- const info2 = data.searchInformation;
4526
- return `No results found for: "${query}"` + (info2 ? ` (searched ${info2.formattedTotalResults ?? "0"} pages in ${info2.formattedSearchTime ?? "?"}s)` : "");
4527
- }
4528
- const info = data.searchInformation;
4529
- const header = `Search results for "${query}" (${items.length} of ~${info?.formattedTotalResults ?? "?"} results):
4530
- `;
4531
- const results = items.map((item, i) => {
4532
- const title = item.title ?? "Untitled";
4533
- const link = item.link ?? "";
4534
- const snippet = item.snippet ?? "";
4535
- return `${i + 1}. **${title}**
4536
- URL: ${link}
4537
- ${snippet}`;
4538
- });
4539
- return header + "\n" + results.join("\n\n");
4540
- }
4541
-
4542
- // src/tools/truncate.ts
4543
- var DEFAULT_MAX_TOOL_OUTPUT_CHARS = 12e3;
4544
- function getMaxOutputChars(contextWindow) {
4545
- if (!contextWindow || contextWindow <= 0) return DEFAULT_MAX_TOOL_OUTPUT_CHARS;
4546
- return Math.max(DEFAULT_MAX_TOOL_OUTPUT_CHARS, Math.min(Math.floor(contextWindow / 4), 12e4));
4547
- }
4548
- var activeMaxChars = DEFAULT_MAX_TOOL_OUTPUT_CHARS;
4549
- function setContextWindow(contextWindow) {
4550
- activeMaxChars = getMaxOutputChars(contextWindow);
4551
- }
4552
- function getActiveMaxChars() {
4553
- return activeMaxChars;
4554
- }
4555
- function truncateOutput(content, toolName, maxChars) {
4556
- const limit = maxChars ?? activeMaxChars;
4557
- if (content.length <= limit) return content;
4558
- const keepHead = Math.floor(limit * 0.7);
4559
- const keepTail = Math.floor(limit * 0.2);
4560
- const omitted = content.length - keepHead - keepTail;
4561
- const lines = content.split("\n").length;
4562
- const head = content.slice(0, keepHead);
4563
- const tail = content.slice(content.length - keepTail);
4564
- return head + `
4565
-
4566
- ... [Output truncated: ${content.length} chars / ${lines} lines total, ${omitted} chars omitted. Use read_file to view full content in segments` + (toolName === "bash" ? ", or narrow the command scope" : "") + `] ...
4567
-
4568
- ` + tail;
4569
- }
4570
-
4571
- // src/repl/theme.ts
4572
- import chalk3 from "chalk";
4573
- var DARK_THEME = {
4574
- prompt: chalk3.green,
4575
- info: chalk3.cyan,
4576
- warning: chalk3.yellow,
4577
- error: chalk3.red,
4578
- success: chalk3.green,
4579
- dim: chalk3.dim,
4580
- accent: chalk3.cyan,
4581
- toolCall: chalk3.yellow,
4582
- toolResult: chalk3.green,
4583
- heading: chalk3.bold.cyan
4584
- };
4585
- var LIGHT_THEME = {
4586
- prompt: chalk3.blue,
4587
- info: chalk3.blueBright,
4588
- warning: chalk3.yellow,
4589
- error: chalk3.red,
4590
- success: chalk3.green,
4591
- dim: chalk3.gray,
4592
- accent: chalk3.blueBright,
4593
- toolCall: chalk3.magenta,
4594
- toolResult: chalk3.green,
4595
- heading: chalk3.bold.blue
4596
- };
4597
- function resolveColor(name) {
4598
- if (name.startsWith("#")) return chalk3.hex(name);
4599
- const parts = name.split(".");
4600
- let result = chalk3;
4601
- for (const part of parts) {
4602
- const obj = result;
4603
- if (obj && typeof obj[part] !== "undefined") {
4604
- result = obj[part];
4605
- }
4606
- }
4607
- if (typeof result !== "function") {
4608
- process.stderr.write(`[theme] Warning: unrecognized color "${name}", using default.
4609
- `);
4610
- return chalk3;
4611
- }
4612
- return result;
4613
- }
4614
- function buildCustomTheme(base, overrides) {
4615
- if (!overrides) return base;
4616
- const result = { ...base };
4617
- for (const [key, colorName] of Object.entries(overrides)) {
4618
- if (key in result && colorName) {
4619
- result[key] = resolveColor(colorName);
4620
- }
4621
- }
4622
- return result;
4623
- }
4624
- var _currentTheme = DARK_THEME;
4625
- function initTheme(themeId = "dark", customColors) {
4626
- switch (themeId) {
4627
- case "light":
4628
- _currentTheme = LIGHT_THEME;
4629
- break;
4630
- case "custom":
4631
- _currentTheme = buildCustomTheme(DARK_THEME, customColors);
4632
- break;
4633
- default:
4634
- _currentTheme = DARK_THEME;
4635
- }
4636
- }
4637
- var theme = new Proxy(DARK_THEME, {
4638
- get(_target, prop) {
4639
- return _currentTheme[prop];
4640
- }
4641
- });
4642
-
4643
- // src/tools/builtin/spawn-agent.ts
4644
- var spawnAgentContext = {
4645
- provider: null,
4646
- model: "",
4647
- systemPrompt: void 0,
4648
- modelParams: {},
4649
- configManager: null
4650
- };
4651
- var PREFIX = theme.dim(" \u2503 ");
4652
- var SubAgentExecutor = class {
4653
- constructor(registry) {
4654
- this.registry = registry;
4655
- }
4656
- round = 0;
4657
- totalRounds = 0;
4658
- setRoundInfo(current, total) {
4659
- this.round = current;
4660
- this.totalRounds = total;
4661
- }
4662
- async execute(call) {
4663
- const tool = this.registry.get(call.name);
4664
- if (!tool) {
4665
- return {
4666
- callId: call.id,
4667
- content: `Unknown tool: ${call.name}`,
4668
- isError: true
4669
- };
4670
- }
4671
- const dangerLevel = getDangerLevel(call.name, call.arguments);
4672
- if (dangerLevel === "destructive") {
4673
- this.printPrefixed(
4674
- theme.error("\u26A0 BLOCKED: ") + `Destructive operation ${call.name} not allowed in sub-agent`
4675
- );
4676
- return {
4677
- callId: call.id,
4678
- content: "Destructive operations are not allowed in sub-agents.",
4679
- isError: true
4680
- };
4681
- }
4682
- this.printToolCall(call, dangerLevel);
4683
- try {
4684
- const rawContent = await tool.execute(call.arguments);
4685
- const content = truncateOutput(rawContent, call.name);
4686
- const wasTruncated = content !== rawContent;
4687
- this.printToolResult(call.name, rawContent, false, wasTruncated);
4688
- return { callId: call.id, content, isError: false };
4689
- } catch (err) {
4690
- const message = err instanceof Error ? err.message : String(err);
4691
- this.printToolResult(call.name, message, true, false);
4692
- return { callId: call.id, content: message, isError: true };
4693
- }
4694
- }
4695
- async executeAll(calls) {
4696
- const results = [];
4697
- for (const call of calls) {
4698
- results.push(await this.execute(call));
4699
- }
4700
- return results;
4701
- }
4702
- // ── 带前缀的终端输出 ──
4703
- printPrefixed(text) {
4704
- for (const line of text.split("\n")) {
4705
- console.log(PREFIX + line);
4706
- }
4707
- }
4708
- printToolCall(call, dangerLevel) {
4709
- console.log(PREFIX);
4710
- const icon = dangerLevel === "write" ? theme.warning("\u270E Tool: ") : theme.toolCall("\u2699 Tool: ");
4711
- const roundBadge = this.totalRounds > 0 ? theme.dim(` [${this.round}/${this.totalRounds}]`) : "";
4712
- console.log(PREFIX + icon + call.name + roundBadge);
4713
- for (const [key, val] of Object.entries(call.arguments)) {
4714
- let valStr;
4715
- if (Array.isArray(val)) {
4716
- const json = JSON.stringify(val);
4717
- valStr = json.length > 160 ? json.slice(0, 160) + "..." : json;
4718
- } else if (typeof val === "string" && val.length > 120) {
4719
- valStr = val.slice(0, 120) + "...";
4720
- } else {
4721
- valStr = String(val);
4722
- }
4723
- console.log(PREFIX + theme.dim(` ${key}: `) + valStr);
4724
- }
4725
- }
4726
- printToolResult(name, content, isError, wasTruncated) {
4727
- if (isError) {
4728
- console.log(
4729
- PREFIX + theme.error(`\u26A0 ${name} error: `) + theme.dim(content.slice(0, 300))
4730
- );
4731
- } else {
4732
- const lines = content.split("\n");
4733
- const maxLines = name === "run_interactive" ? 40 : 8;
4734
- const preview = lines.slice(0, maxLines);
4735
- const prefixedPreview = preview.map((l) => PREFIX + " " + theme.dim(l)).join("\n");
4736
- const moreLines = lines.length > maxLines ? "\n" + PREFIX + theme.dim(` ... (${lines.length - maxLines} more lines)`) : "";
4737
- const truncNote = wasTruncated ? "\n" + PREFIX + theme.warning(` \u26A1 Output truncated`) : "";
4738
- console.log(PREFIX + theme.success("\u2713 Result:"));
4739
- console.log(prefixedPreview + moreLines + truncNote);
4740
- }
4741
- }
4742
- };
4743
- function buildSubAgentSystemPrompt(task, parentSystemPrompt) {
4744
- const parts = [];
4745
- if (parentSystemPrompt) {
4746
- parts.push(parentSystemPrompt);
4747
- }
4748
- parts.push(
4749
- `# Sub-Agent Mode
4750
-
4751
- You are a focused sub-agent delegated by the main agent to complete a specific task.
4752
-
4753
- **Your Task**:
4754
- ${task}
4755
-
4756
- **Rules**:
4757
- 1. Stay focused on your task, do not deviate
4758
- 2. When done, output a concise summary: what was done, which files were modified, and the result
4759
- 3. You cannot interact with the user (no ask_user tool), work independently using the task description
4760
- 4. You cannot create sub-agents (no spawn_agent tool)
4761
- 5. Destructive operations (rm -rf etc.) will be automatically blocked
4762
- 6. Be efficient, minimize unnecessary tool call rounds`
4763
- );
4764
- return parts.join("\n\n---\n\n");
4765
- }
4766
- function printSubAgentHeader(task, maxRounds) {
4767
- console.log();
4768
- console.log(theme.dim(" \u250F\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"));
4769
- console.log(PREFIX + theme.toolCall("\u{1F916} Sub-Agent Spawned"));
4770
- console.log(
4771
- PREFIX + theme.dim("Task: ") + (task.slice(0, 120) + (task.length > 120 ? "..." : ""))
4772
- );
4773
- console.log(PREFIX + theme.dim(`Max rounds: ${maxRounds}`));
4774
- console.log(theme.dim(" \u2503"));
4775
- }
4776
- function printSubAgentFooter(usage) {
4777
- console.log(PREFIX);
4778
- console.log(PREFIX + theme.toolCall("Sub-Agent Complete"));
4779
- if (usage.inputTokens > 0 || usage.outputTokens > 0) {
4780
- console.log(
4781
- PREFIX + theme.dim(
4782
- `Tokens: ${usage.inputTokens} in / ${usage.outputTokens} out`
4783
- )
4784
- );
4785
- }
4786
- console.log(theme.dim(" \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"));
4787
- console.log();
4788
- }
4789
- var spawnAgentTool = {
4790
- definition: {
4791
- name: "spawn_agent",
4792
- description: "Delegate a specific subtask to an independent sub-agent. The sub-agent has its own conversation context and agentic tool-call loop, with access to bash, file read/write, edit, search, etc., but cannot interact with the user or spawn more sub-agents. Suitable for independently completable subtasks like: refactoring a module, writing tests, researching a technical approach, or implementing a specific feature. Returns a summary of the sub-agent execution result.",
4793
- parameters: {
4794
- task: {
4795
- type: "string",
4796
- description: "Task description for the sub-agent. Must include all necessary context: file paths, requirements, constraints, expected output. The sub-agent cannot access the parent conversation history; all info must be in this parameter.",
4797
- required: true
4798
- },
4799
- max_rounds: {
4800
- type: "number",
4801
- description: "Max tool-call rounds for the sub-agent (1-15, default 10). Use fewer for simple tasks, more for complex ones.",
4802
- required: false
4803
- }
4804
- },
4805
- dangerous: false
4806
- },
4807
- async execute(args) {
4808
- const task = String(args["task"] ?? "").trim();
4809
- if (!task) throw new Error("task parameter is required");
4810
- const rawMaxRounds = Number(args["max_rounds"] ?? SUBAGENT_DEFAULT_MAX_ROUNDS);
4811
- const maxRounds = Math.min(
4812
- Math.max(Math.round(rawMaxRounds), 1),
4813
- SUBAGENT_MAX_ROUNDS_LIMIT
4814
- );
4815
- const ctx = spawnAgentContext;
4816
- if (!ctx.provider) {
4817
- throw new Error("spawn_agent: provider not initialized (context not injected)");
4818
- }
4819
- const subRegistry = new ToolRegistry();
4820
- for (const tool of subRegistry.listAll()) {
4821
- if (!SUBAGENT_ALLOWED_TOOLS.has(tool.definition.name)) {
4822
- subRegistry.unregister(tool.definition.name);
4823
- }
4824
- }
4825
- const subExecutor = new SubAgentExecutor(subRegistry);
4826
- const subMessages = [
4827
- { role: "user", content: task, timestamp: /* @__PURE__ */ new Date() }
4828
- ];
4829
- const subSystemPrompt = buildSubAgentSystemPrompt(task, ctx.systemPrompt);
4830
- const toolDefs = subRegistry.getDefinitions();
4831
- const extraMessages = [];
4832
- const totalUsage = { inputTokens: 0, outputTokens: 0 };
4833
- let finalContent = "";
4834
- printSubAgentHeader(task, maxRounds);
4835
- try {
4836
- for (let round = 0; round < maxRounds; round++) {
4837
- subExecutor.setRoundInfo(round + 1, maxRounds);
4838
- const result = await ctx.provider.chatWithTools(
4839
- {
4840
- messages: subMessages,
4841
- model: ctx.model,
4842
- systemPrompt: subSystemPrompt,
4843
- stream: false,
4844
- temperature: ctx.modelParams.temperature,
4845
- maxTokens: ctx.modelParams.maxTokens,
4846
- timeout: ctx.modelParams.timeout,
4847
- thinking: ctx.modelParams.thinking,
4848
- ...extraMessages.length > 0 ? { _extraMessages: extraMessages } : {}
4849
- },
4850
- toolDefs
4851
- );
4852
- if (result.usage) {
4853
- totalUsage.inputTokens += result.usage.inputTokens;
4854
- totalUsage.outputTokens += result.usage.outputTokens;
4855
- }
4856
- if ("content" in result) {
4857
- finalContent = result.content;
4858
- break;
4859
- }
4860
- if (ctx.configManager) {
4861
- googleSearchContext.configManager = ctx.configManager;
4862
- }
4863
- const toolResults = await subExecutor.executeAll(result.toolCalls);
4864
- const reasoningContent = "reasoningContent" in result ? result.reasoningContent : void 0;
4865
- const newMsgs = ctx.provider.buildToolResultMessages(
4866
- result.toolCalls,
4867
- toolResults,
4868
- reasoningContent
4869
- );
4870
- extraMessages.push(...newMsgs);
4871
- }
4872
- if (!finalContent) {
4873
- finalContent = `(Sub-agent reached maximum rounds (${maxRounds}) without producing a final response)`;
4874
- }
4875
- } catch (err) {
4876
- const errMsg = err instanceof Error ? err.message : String(err);
4877
- finalContent = `(Sub-agent error: ${errMsg})`;
4878
- process.stderr.write(
4879
- `
4880
- [spawn_agent] Error in sub-agent loop: ${errMsg}
4881
- `
4882
- );
4883
- }
4884
- printSubAgentFooter(totalUsage);
4885
- const lines = [
4886
- "## Sub-Agent Result",
4887
- "",
4888
- finalContent,
4889
- "",
4890
- "---",
4891
- `Token usage: ${totalUsage.inputTokens} input, ${totalUsage.outputTokens} output`
4892
- ];
4893
- return lines.join("\n");
4894
- }
4895
- };
4896
-
4897
- // src/tools/registry.ts
4898
- import { pathToFileURL } from "url";
4899
- import { existsSync as existsSync12, mkdirSync as mkdirSync6, readdirSync as readdirSync7 } from "fs";
4900
- import { join as join8 } from "path";
4901
- var ToolRegistry = class {
4902
- tools = /* @__PURE__ */ new Map();
4903
- pluginToolNames = /* @__PURE__ */ new Set();
4904
- mcpToolNames = /* @__PURE__ */ new Set();
4905
- constructor() {
4906
- this.register(bashTool);
4907
- this.register(readFileTool);
4908
- this.register(writeFileTool);
4909
- this.register(editFileTool);
4910
- this.register(listDirTool);
4911
- this.register(grepFilesTool);
4912
- this.register(globFilesTool);
4913
- this.register(runInteractiveTool);
4914
- this.register(webFetchTool);
4915
- this.register(saveLastResponseTool);
4916
- this.register(saveMemoryTool);
4917
- this.register(askUserTool);
4918
- this.register(writeTodosTool);
4919
- this.register(googleSearchTool);
4920
- this.register(spawnAgentTool);
4921
- this.register(runTestsTool);
4922
- }
4923
- register(tool) {
4924
- this.tools.set(tool.definition.name, tool);
4925
- }
4926
- get(name) {
4927
- return this.tools.get(name);
4928
- }
4929
- /** 注销指定工具(子代理创建过滤后的工具集时使用) */
4930
- unregister(name) {
4931
- return this.tools.delete(name);
4932
- }
4933
- /** 返回所有工具的 schema,用于发送给 AI */
4934
- getDefinitions() {
4935
- return [...this.tools.values()].map((t) => t.definition);
4936
- }
4937
- listAll() {
4938
- return [...this.tools.values()];
4939
- }
4940
- /** Returns only tools loaded from plugins (not built-in). */
4941
- listPluginTools() {
4942
- return [...this.tools.values()].filter((t) => this.pluginToolNames.has(t.definition.name));
4943
- }
4944
- /** 注册一个 MCP 工具(名称以 mcp__ 开头) */
4945
- registerMcpTool(tool) {
4946
- this.tools.set(tool.definition.name, tool);
4947
- this.mcpToolNames.add(tool.definition.name);
4948
- }
4949
- /** 返回所有 MCP 工具 */
4950
- listMcpTools() {
4951
- return [...this.tools.values()].filter((t) => this.mcpToolNames.has(t.definition.name));
4952
- }
4953
- /** 清除所有已注册的 MCP 工具(重连时先清除再重新注册) */
4954
- unregisterMcpTools() {
4955
- for (const name of this.mcpToolNames) {
4956
- this.tools.delete(name);
4957
- }
4958
- this.mcpToolNames.clear();
4959
- }
4960
- /**
4961
- * Dynamically loads .js plugin files from pluginsDir.
4962
- *
4963
- * Security notes:
4964
- * - Only loads when allowPlugins=true (must be explicitly enabled in config).
4965
- * - Plugins run with FULL Node.js privileges in the main process.
4966
- * - Prints a prominent warning listing every file before loading.
4967
- * - Built-in tool names cannot be overridden by plugins.
4968
- *
4969
- * Creates the dir if missing. Skips invalid plugins with a warning.
4970
- * Returns the number of successfully loaded plugins.
4971
- */
4972
- async loadPlugins(pluginsDir, allowPlugins = false) {
4973
- if (!existsSync12(pluginsDir)) {
4974
- try {
4975
- mkdirSync6(pluginsDir, { recursive: true });
4976
- } catch {
4977
- }
4978
- return 0;
4979
- }
4980
- let files;
4981
- try {
4982
- files = readdirSync7(pluginsDir).filter((f) => f.endsWith(".js"));
4983
- } catch {
4984
- return 0;
4985
- }
4986
- if (files.length === 0) return 0;
4987
- if (!allowPlugins) {
4988
- process.stderr.write(
4989
- `[plugins] Found ${files.length} plugin(s) in ${pluginsDir} but loading is disabled.
4990
- To enable, set "allowPlugins": true in ~/.aicli/config.json (or via /config).
4991
- \u26A0 Plugins run with full system privileges. Only enable for trusted sources.
4992
- `
4993
- );
4994
- return 0;
4995
- }
4996
- process.stderr.write(
4997
- `
4998
- [plugins] \u26A0 Loading ${files.length} plugin(s) with FULL system privileges:
4999
- ` + files.map((f) => ` + ${join8(pluginsDir, f)}`).join("\n") + "\n\n"
5000
- );
5001
- let loaded = 0;
5002
- for (const file of files) {
5003
- try {
5004
- const fileUrl = pathToFileURL(join8(pluginsDir, file)).href;
5005
- const mod = await import(fileUrl);
5006
- const tool = mod.tool ?? mod.default?.tool ?? mod.default;
5007
- if (!tool || typeof tool.execute !== "function" || !tool.definition?.name) {
5008
- process.stderr.write(`[plugins] Skipping ${file}: missing or invalid 'tool' export
5009
- `);
5010
- continue;
5011
- }
5012
- if (this.tools.has(tool.definition.name) && !this.pluginToolNames.has(tool.definition.name)) {
5013
- process.stderr.write(`[plugins] Skipping ${file}: name '${tool.definition.name}' conflicts with built-in tool
5014
- `);
5015
- continue;
5016
- }
5017
- this.register(tool);
5018
- this.pluginToolNames.add(tool.definition.name);
5019
- loaded++;
5020
- process.stderr.write(`[plugins] Loaded: ${tool.definition.name} (${file})
5021
- `);
5022
- } catch (err) {
5023
- process.stderr.write(`[plugins] Failed to load ${file}: ${err.message}
5024
- `);
5025
- }
5026
- }
5027
- return loaded;
5028
- }
5029
- };
5030
-
5031
- // src/mcp/client.ts
5032
- import { spawn as spawn2 } from "child_process";
5033
- var McpClient = class {
5034
- serverId;
5035
- config;
5036
- process = null;
5037
- nextId = 1;
5038
- connected = false;
5039
- serverInfo = null;
5040
- /** stderr 收集(最多保留最后 2KB,用于错误报告) */
5041
- stderrBuffer = "";
5042
- /** 缓存已发现的工具列表 */
5043
- cachedTools = [];
5044
- /** 错误信息(连接失败时设置) */
5045
- errorMessage = null;
5046
- // ── JSON-RPC 请求/响应匹配 ──────────────────────────────────────
5047
- pendingRequests = /* @__PURE__ */ new Map();
5048
- /** stdout 残余缓冲区(处理不完整的 JSON 行) */
5049
- stdoutBuffer = "";
5050
- constructor(serverId, config) {
5051
- this.serverId = serverId;
5052
- this.config = config;
5053
- }
5054
- get isConnected() {
5055
- return this.connected;
5056
- }
5057
- get serverName() {
5058
- return this.serverInfo?.name ?? this.serverId;
5059
- }
5060
- get tools() {
5061
- return this.cachedTools;
5062
- }
5063
- // ══════════════════════════════════════════════════════════════════
5064
- // 连接与初始化
5065
- // ══════════════════════════════════════════════════════════════════
5066
- async connect() {
5067
- const timeout = this.config.timeout ?? MCP_CONNECT_TIMEOUT;
5068
- try {
5069
- this.process = spawn2(this.config.command, this.config.args ?? [], {
5070
- stdio: ["pipe", "pipe", "pipe"],
5071
- env: { ...process.env, ...this.config.env },
5072
- // Windows 上 npx 等是 .cmd 脚本,需要 shell 模式
5073
- shell: process.platform === "win32",
5074
- // 不让子进程阻止父进程退出
5075
- detached: false
5076
- });
5077
- this.process.on("error", (err) => {
5078
- this.errorMessage = err.message;
5079
- this.connected = false;
5080
- this.rejectAllPending(new Error(`MCP server [${this.serverId}] process error: ${err.message}`));
5081
- });
5082
- this.process.on("exit", (code, signal) => {
5083
- this.connected = false;
5084
- const reason = signal ? `signal ${signal}` : `code ${code}`;
5085
- this.rejectAllPending(new Error(`MCP server [${this.serverId}] exited: ${reason}`));
5086
- });
5087
- this.process.stdout.setEncoding("utf-8");
5088
- this.process.stdout.on("data", (chunk) => this.handleStdoutData(chunk));
5089
- this.process.stderr.setEncoding("utf-8");
5090
- this.process.stderr.on("data", (chunk) => {
5091
- this.stderrBuffer += chunk;
5092
- if (this.stderrBuffer.length > 2048) {
5093
- this.stderrBuffer = this.stderrBuffer.slice(-2048);
5094
- }
5095
- });
5096
- const initResult = await this.withTimeout(
5097
- this.sendRequest("initialize", {
5098
- protocolVersion: MCP_PROTOCOL_VERSION,
5099
- capabilities: {},
5100
- clientInfo: { name: APP_NAME, version: VERSION }
5101
- }),
5102
- timeout,
5103
- "initialize handshake"
5104
- );
5105
- this.serverInfo = initResult.serverInfo;
5106
- this.sendNotification("notifications/initialized");
5107
- this.connected = true;
5108
- await this.refreshTools();
5109
- } catch (err) {
5110
- this.errorMessage = err instanceof Error ? err.message : String(err);
5111
- this.connected = false;
5112
- this.killProcess();
5113
- throw err;
5114
- }
5115
- }
5116
- // ══════════════════════════════════════════════════════════════════
5117
- // 工具操作
5118
- // ══════════════════════════════════════════════════════════════════
5119
- /** 刷新工具列表(tools/list) */
5120
- async refreshTools() {
5121
- this.ensureConnected();
5122
- const result = await this.withTimeout(
5123
- this.sendRequest("tools/list", {}),
5124
- MCP_CALL_TIMEOUT,
5125
- "tools/list"
5126
- );
5127
- this.cachedTools = result.tools ?? [];
5128
- return this.cachedTools;
5129
- }
5130
- /** 调用工具(tools/call) */
5131
- async callTool(name, args) {
5132
- this.ensureConnected();
5133
- return this.withTimeout(
5134
- this.sendRequest("tools/call", { name, arguments: args }),
5135
- MCP_CALL_TIMEOUT,
5136
- `tools/call(${name})`
5137
- );
5138
- }
5139
- // ══════════════════════════════════════════════════════════════════
5140
- // 关闭连接
5141
- // ══════════════════════════════════════════════════════════════════
5142
- async close() {
5143
- this.connected = false;
5144
- this.rejectAllPending(new Error("Client closing"));
5145
- this.killProcess();
5146
- }
5147
- /**
5148
- * 断线重连:清理旧状态后重新执行完整的 connect() 流程。
5149
- * 用于 MCP 服务器子进程意外退出后的自动或手动恢复。
5150
- */
5151
- async reconnect() {
5152
- this.connected = false;
5153
- this.rejectAllPending(new Error("Reconnecting"));
5154
- this.killProcess();
5155
- this.errorMessage = null;
5156
- this.stderrBuffer = "";
5157
- this.stdoutBuffer = "";
5158
- this.cachedTools = [];
5159
- await this.connect();
5160
- }
5161
- // ══════════════════════════════════════════════════════════════════
5162
- // 内部方法:JSON-RPC 通信
5163
- // ══════════════════════════════════════════════════════════════════
5164
- sendRequest(method, params) {
5165
- return new Promise((resolve4, reject) => {
5166
- if (!this.process?.stdin?.writable) {
5167
- return reject(new Error(`MCP server [${this.serverId}] stdin not writable`));
5168
- }
5169
- const id = this.nextId++;
5170
- const request = {
5171
- jsonrpc: "2.0",
5172
- id,
5173
- method,
5174
- ...params !== void 0 ? { params } : {}
5175
- };
5176
- let timer;
5177
- const cleanup = () => {
5178
- this.pendingRequests.delete(id);
5179
- clearTimeout(timer);
5180
- };
5181
- timer = setTimeout(() => {
5182
- cleanup();
5183
- reject(new Error(`MCP request [${method}] timed out (internal)`));
5184
- }, MCP_CALL_TIMEOUT * 2);
5185
- this.pendingRequests.set(id, {
5186
- resolve: (result) => {
5187
- cleanup();
5188
- resolve4(result);
5189
- },
5190
- reject: (error) => {
5191
- cleanup();
5192
- reject(error);
5193
- },
5194
- timer
5195
- });
5196
- const json = JSON.stringify(request) + "\n";
5197
- this.process.stdin.write(json, (err) => {
5198
- if (err) {
5199
- cleanup();
5200
- reject(new Error(`MCP write error: ${err.message}`));
5201
- }
5202
- });
5203
- });
2697
+ });
2698
+ });
5204
2699
  }
5205
2700
  sendNotification(method, params) {
5206
2701
  if (!this.process?.stdin?.writable) return;
@@ -5256,13 +2751,13 @@ var McpClient = class {
5256
2751
  }
5257
2752
  /** Promise 超时包装 */
5258
2753
  withTimeout(promise, ms, label) {
5259
- return new Promise((resolve4, reject) => {
2754
+ return new Promise((resolve, reject) => {
5260
2755
  const timer = setTimeout(() => {
5261
2756
  reject(new Error(`MCP [${this.serverId}] ${label} timed out after ${ms}ms`));
5262
2757
  }, ms);
5263
2758
  promise.then((val) => {
5264
2759
  clearTimeout(timer);
5265
- resolve4(val);
2760
+ resolve(val);
5266
2761
  }).catch((err) => {
5267
2762
  clearTimeout(timer);
5268
2763
  reject(err);
@@ -5526,12 +3021,12 @@ var McpManager = class {
5526
3021
  };
5527
3022
 
5528
3023
  // src/skills/manager.ts
5529
- import { existsSync as existsSync13, readdirSync as readdirSync8, mkdirSync as mkdirSync7, statSync as statSync7 } from "fs";
5530
- import { join as join9 } from "path";
3024
+ import { existsSync as existsSync4, readdirSync as readdirSync2, mkdirSync as mkdirSync3, statSync } from "fs";
3025
+ import { join as join4 } from "path";
5531
3026
 
5532
3027
  // src/skills/types.ts
5533
- import { readFileSync as readFileSync7 } from "fs";
5534
- import { basename as basename4 } from "path";
3028
+ import { readFileSync as readFileSync3 } from "fs";
3029
+ import { basename } from "path";
5535
3030
  function parseSimpleYaml(yaml) {
5536
3031
  const result = {};
5537
3032
  for (const line of yaml.split("\n")) {
@@ -5552,7 +3047,7 @@ function parseYamlArray(value) {
5552
3047
  function parseSkillFile(filePath) {
5553
3048
  let raw;
5554
3049
  try {
5555
- raw = readFileSync7(filePath, "utf-8");
3050
+ raw = readFileSync3(filePath, "utf-8");
5556
3051
  } catch {
5557
3052
  return null;
5558
3053
  }
@@ -5560,7 +3055,7 @@ function parseSkillFile(filePath) {
5560
3055
  if (!frontmatterMatch) {
5561
3056
  return {
5562
3057
  meta: {
5563
- name: basename4(filePath, ".md"),
3058
+ name: basename(filePath, ".md"),
5564
3059
  description: ""
5565
3060
  },
5566
3061
  content: raw.trim(),
@@ -5571,7 +3066,7 @@ function parseSkillFile(filePath) {
5571
3066
  const parsed = parseSimpleYaml(yaml);
5572
3067
  return {
5573
3068
  meta: {
5574
- name: parsed["name"] ?? basename4(filePath, ".md"),
3069
+ name: parsed["name"] ?? basename(filePath, ".md"),
5575
3070
  description: parsed["description"] ?? "",
5576
3071
  tools: parsed["tools"] ? parseYamlArray(parsed["tools"]) : void 0
5577
3072
  },
@@ -5592,29 +3087,29 @@ var SkillManager = class {
5592
3087
  /** 发现并加载 skillsDir 下所有 .md 文件,返回加载数量 */
5593
3088
  loadSkills() {
5594
3089
  this.skills.clear();
5595
- if (!existsSync13(this.skillsDir)) {
3090
+ if (!existsSync4(this.skillsDir)) {
5596
3091
  try {
5597
- mkdirSync7(this.skillsDir, { recursive: true });
3092
+ mkdirSync3(this.skillsDir, { recursive: true });
5598
3093
  } catch {
5599
3094
  }
5600
3095
  return 0;
5601
3096
  }
5602
3097
  let entries;
5603
3098
  try {
5604
- entries = readdirSync8(this.skillsDir);
3099
+ entries = readdirSync2(this.skillsDir);
5605
3100
  } catch {
5606
3101
  return 0;
5607
3102
  }
5608
3103
  for (const entry of entries) {
5609
3104
  let filePath;
5610
- const fullPath = join9(this.skillsDir, entry);
3105
+ const fullPath = join4(this.skillsDir, entry);
5611
3106
  if (entry.endsWith(".md")) {
5612
3107
  filePath = fullPath;
5613
3108
  } else {
5614
3109
  try {
5615
- if (statSync7(fullPath).isDirectory()) {
5616
- const skillMd = join9(fullPath, "SKILL.md");
5617
- if (existsSync13(skillMd)) {
3110
+ if (statSync(fullPath).isDirectory()) {
3111
+ const skillMd = join4(fullPath, "SKILL.md");
3112
+ if (existsSync4(skillMd)) {
5618
3113
  filePath = skillMd;
5619
3114
  } else {
5620
3115
  continue;
@@ -5688,7 +3183,7 @@ async function setupProxy(configProxy) {
5688
3183
  }
5689
3184
 
5690
3185
  // src/tools/diff-utils.ts
5691
- import chalk4 from "chalk";
3186
+ import chalk from "chalk";
5692
3187
  function renderDiff(oldText, newText, opts = {}) {
5693
3188
  const contextLines = opts.contextLines ?? 3;
5694
3189
  const maxLines = opts.maxLines ?? 120;
@@ -5697,21 +3192,21 @@ function renderDiff(oldText, newText, opts = {}) {
5697
3192
  const newLines = newText.split("\n");
5698
3193
  const hunks = computeHunks(oldLines, newLines, contextLines);
5699
3194
  if (hunks.length === 0) {
5700
- return chalk4.dim(" (no changes)");
3195
+ return chalk.dim(" (no changes)");
5701
3196
  }
5702
3197
  const output = [];
5703
3198
  if (filePath) {
5704
- output.push(chalk4.bold.white(`--- ${filePath} (before)`));
5705
- output.push(chalk4.bold.white(`+++ ${filePath} (after)`));
3199
+ output.push(chalk.bold.white(`--- ${filePath} (before)`));
3200
+ output.push(chalk.bold.white(`+++ ${filePath} (after)`));
5706
3201
  }
5707
3202
  let totalDisplayed = 0;
5708
3203
  for (const hunk of hunks) {
5709
3204
  if (totalDisplayed >= maxLines) {
5710
- output.push(chalk4.dim(` ... (diff truncated, too many changes)`));
3205
+ output.push(chalk.dim(` ... (diff truncated, too many changes)`));
5711
3206
  break;
5712
3207
  }
5713
3208
  output.push(
5714
- chalk4.cyan(
3209
+ chalk.cyan(
5715
3210
  `@@ -${hunk.oldStart + 1},${hunk.oldCount} +${hunk.newStart + 1},${hunk.newCount} @@`
5716
3211
  )
5717
3212
  );
@@ -5719,11 +3214,11 @@ function renderDiff(oldText, newText, opts = {}) {
5719
3214
  if (totalDisplayed >= maxLines) break;
5720
3215
  totalDisplayed++;
5721
3216
  if (line.type === "context") {
5722
- output.push(chalk4.dim(` ${line.text}`));
3217
+ output.push(chalk.dim(` ${line.text}`));
5723
3218
  } else if (line.type === "remove") {
5724
- output.push(chalk4.red(`- ${line.text}`));
3219
+ output.push(chalk.red(`- ${line.text}`));
5725
3220
  } else {
5726
- output.push(chalk4.green(`+ ${line.text}`));
3221
+ output.push(chalk.green(`+ ${line.text}`));
5727
3222
  }
5728
3223
  }
5729
3224
  }
@@ -5841,9 +3336,9 @@ function simpleDiff(oldLines, newLines) {
5841
3336
  }
5842
3337
 
5843
3338
  // src/repl/dev-state.ts
5844
- import { existsSync as existsSync14, readFileSync as readFileSync8, writeFileSync as writeFileSync7, unlinkSync as unlinkSync3, mkdirSync as mkdirSync8 } from "fs";
5845
- import { join as join10 } from "path";
5846
- import { homedir as homedir4 } from "os";
3339
+ import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2, mkdirSync as mkdirSync4 } from "fs";
3340
+ import { join as join5 } from "path";
3341
+ import { homedir as homedir2 } from "os";
5847
3342
  var DEV_STATE_MAX_CHARS = 6e3;
5848
3343
  var SNAPSHOT_PROMPT = `You are about to be replaced by a different AI model. Please generate a structured development state snapshot so the next model can continue seamlessly.
5849
3344
 
@@ -5896,12 +3391,12 @@ function sessionHasMeaningfulContent(messages) {
5896
3391
  return hasUser && hasAssistant;
5897
3392
  }
5898
3393
  function getDevStatePath() {
5899
- return join10(homedir4(), CONFIG_DIR_NAME, DEV_STATE_FILE_NAME);
3394
+ return join5(homedir2(), CONFIG_DIR_NAME, DEV_STATE_FILE_NAME);
5900
3395
  }
5901
3396
  function saveDevState(content) {
5902
- const configDir = join10(homedir4(), CONFIG_DIR_NAME);
5903
- if (!existsSync14(configDir)) {
5904
- mkdirSync8(configDir, { recursive: true });
3397
+ const configDir = join5(homedir2(), CONFIG_DIR_NAME);
3398
+ if (!existsSync5(configDir)) {
3399
+ mkdirSync4(configDir, { recursive: true });
5905
3400
  }
5906
3401
  let trimmed = content.trim();
5907
3402
  if (trimmed.length > DEV_STATE_MAX_CHARS) {
@@ -5912,26 +3407,26 @@ function saveDevState(content) {
5912
3407
  }
5913
3408
  trimmed += "\n\n[...truncated]";
5914
3409
  }
5915
- writeFileSync7(getDevStatePath(), trimmed, "utf-8");
3410
+ writeFileSync3(getDevStatePath(), trimmed, "utf-8");
5916
3411
  }
5917
3412
  function loadDevState() {
5918
3413
  const path = getDevStatePath();
5919
- if (!existsSync14(path)) return null;
5920
- const content = readFileSync8(path, "utf-8").trim();
3414
+ if (!existsSync5(path)) return null;
3415
+ const content = readFileSync4(path, "utf-8").trim();
5921
3416
  return content || null;
5922
3417
  }
5923
3418
  function clearDevState() {
5924
3419
  const path = getDevStatePath();
5925
- if (existsSync14(path)) {
3420
+ if (existsSync5(path)) {
5926
3421
  try {
5927
- unlinkSync3(path);
3422
+ unlinkSync2(path);
5928
3423
  } catch {
5929
3424
  }
5930
3425
  }
5931
3426
  }
5932
3427
 
5933
3428
  // src/tools/hooks.ts
5934
- import { execSync as execSync4 } from "child_process";
3429
+ import { execSync as execSync2 } from "child_process";
5935
3430
  function shellEscape(value) {
5936
3431
  return "'" + value.replace(/'/g, "'\\''") + "'";
5937
3432
  }
@@ -5943,7 +3438,7 @@ function runHook(template, vars) {
5943
3438
  cmd = cmd.replace(/\{args\}/g, shellEscape(vars.args ?? ""));
5944
3439
  cmd = cmd.replace(/\{status\}/g, shellEscape(vars.status ?? ""));
5945
3440
  try {
5946
- execSync4(cmd, {
3441
+ execSync2(cmd, {
5947
3442
  timeout: 5e3,
5948
3443
  stdio: ["pipe", "pipe", "pipe"],
5949
3444
  encoding: "utf-8"
@@ -5972,8 +3467,6 @@ function checkPermission(toolName, args, dangerLevel, rules, defaultAction = "co
5972
3467
 
5973
3468
  export {
5974
3469
  ConfigManager,
5975
- isFileWriteTool,
5976
- getDangerLevel,
5977
3470
  detectsHallucinatedFileOp,
5978
3471
  hadPreviousWriteToolCalls,
5979
3472
  TOOL_CALL_REMINDER,
@@ -5981,21 +3474,10 @@ export {
5981
3474
  ProviderRegistry,
5982
3475
  getContentText,
5983
3476
  SessionManager,
5984
- initTheme,
5985
- theme,
5986
3477
  getGitRoot,
5987
3478
  getGitContext,
5988
3479
  formatGitContextForPrompt,
5989
- undoStack,
5990
3480
  renderDiff,
5991
- lastResponseStore,
5992
- askUserContext,
5993
- googleSearchContext,
5994
- setContextWindow,
5995
- getActiveMaxChars,
5996
- truncateOutput,
5997
- spawnAgentContext,
5998
- ToolRegistry,
5999
3481
  runHook,
6000
3482
  checkPermission,
6001
3483
  parseSimpleYaml,