jinzd-ai-cli 0.4.334 → 0.4.338

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/{batch-6UJGEEVB.js → batch-N4DOEUEW.js} +2 -2
  2. package/dist/{chunk-W4P2KJZN.js → chunk-7BOHS5IX.js} +72 -0
  3. package/dist/{chunk-SZQ2C3HH.js → chunk-ATOF545W.js} +1 -1
  4. package/dist/{chunk-7GGWVBHG.js → chunk-DCWBDSMO.js} +2 -2
  5. package/dist/{chunk-RQOOEXDU.js → chunk-DOO4QAGJ.js} +2 -2
  6. package/dist/{chunk-XMHVHS2X.js → chunk-DRCN44SZ.js} +4 -4
  7. package/dist/{chunk-YTAYWGS4.js → chunk-JUYK4OML.js} +1 -1
  8. package/dist/{chunk-DPUZ22D7.js → chunk-KGHVSYGI.js} +27 -23
  9. package/dist/{chunk-EOYIIGBJ.js → chunk-NJREBH6A.js} +5 -5
  10. package/dist/{chunk-FHHBPCCZ.js → chunk-NRFAGZVJ.js} +1 -1
  11. package/dist/{chunk-YVEBRHSV.js → chunk-OXVJZLJM.js} +6 -5
  12. package/dist/{chunk-YY4QFBBZ.js → chunk-PKYNCSQC.js} +475 -240
  13. package/dist/{chunk-KSZEQADY.js → chunk-RIDNF4G2.js} +1 -1
  14. package/dist/{chunk-27RTBMY7.js → chunk-TTLLXFJC.js} +40 -3
  15. package/dist/{chunk-DX4Y4CRH.js → chunk-UWEBIOHD.js} +8 -20
  16. package/dist/{chunk-ZK5VCQD2.js → chunk-WQAUMAEV.js} +1 -1
  17. package/dist/{ci-S7WXRTZ6.js → ci-HFAPFMZY.js} +6 -6
  18. package/dist/{ci-format-TPCL2KWQ.js → ci-format-QTRMHY3N.js} +2 -2
  19. package/dist/{constants-WJAUJAQQ.js → constants-JZH2QKMQ.js} +1 -1
  20. package/dist/{constants-5VQMHBFU.js → constants-QJNJFMC6.js} +1 -1
  21. package/dist/{doctor-cli-DYQLNMPT.js → doctor-cli-FZ4IVZ6H.js} +6 -6
  22. package/dist/electron-server.js +622 -285
  23. package/dist/{hub-A7P2ZBYB.js → hub-IRILXTL3.js} +1 -1
  24. package/dist/index.js +25 -23
  25. package/dist/{persistent-memory-WFGBTGWT.js → persistent-memory-UZIZVHRT.js} +2 -2
  26. package/dist/{persistent-memory-SKGOCEGT.js → persistent-memory-VRBNWOWN.js} +2 -2
  27. package/dist/{pr-IPXLVP2K.js → pr-4KNQWYC6.js} +7 -7
  28. package/dist/{run-tests-BG6SGRWQ.js → run-tests-N2VKD7BU.js} +2 -2
  29. package/dist/{run-tests-TUTI7HIN.js → run-tests-UXEOFPAW.js} +2 -2
  30. package/dist/{server-4QJPQBEW.js → server-2VSO2ZX4.js} +6 -6
  31. package/dist/{server-S6OROC7S.js → server-HWKYWQEP.js} +21 -20
  32. package/dist/{task-orchestrator-6JG5GAXU.js → task-orchestrator-SC42LAFG.js} +6 -6
  33. package/dist/{usage-LPQQ6NGG.js → usage-ZVV27BHD.js} +2 -2
  34. package/dist/web/client/vendor/purify.es.mjs +272 -196
  35. package/dist/web/client/vendor/purify.es.mjs.br +0 -0
  36. package/dist/web/client/vendor/purify.es.mjs.gz +0 -0
  37. package/package.json +6 -3
@@ -55,7 +55,7 @@ import {
55
55
  touchMemoryReferences,
56
56
  updateMemoryApproval,
57
57
  updateMemoryEntry
58
- } from "./chunk-YTAYWGS4.js";
58
+ } from "./chunk-JUYK4OML.js";
59
59
  import {
60
60
  DEFAULT_PATTERNS,
61
61
  redactJson,
@@ -81,7 +81,7 @@ import {
81
81
  recoverGbkMojibake,
82
82
  runTestsTool,
83
83
  shellInvocation
84
- } from "./chunk-7GGWVBHG.js";
84
+ } from "./chunk-DCWBDSMO.js";
85
85
  import {
86
86
  getCurrentSessionKey,
87
87
  getExecutionConfigDir,
@@ -123,7 +123,7 @@ import {
123
123
  SUBAGENT_MAX_ROUNDS_LIMIT,
124
124
  VERSION,
125
125
  buildUserIdentityPrompt
126
- } from "./chunk-SZQ2C3HH.js";
126
+ } from "./chunk-ATOF545W.js";
127
127
 
128
128
  // src/web/access.ts
129
129
  import { randomBytes, timingSafeEqual } from "crypto";
@@ -262,32 +262,36 @@ var UserProfileSchema = z.object({
262
262
  locale: z.string().max(20).optional(),
263
263
  /** 自定义人设补充(自由格式,直接注入 system prompt) */
264
264
  extra: z.string().max(5e3).optional()
265
- }).default({});
265
+ }).prefault({});
266
266
  var ConfigSchema = z.object({
267
267
  version: z.string().default("1.0.0"),
268
268
  // 用户身份档案 — 跨 Provider 的 "灵魂"
269
269
  userProfile: UserProfileSchema,
270
270
  defaultProvider: z.string().default("claude"),
271
271
  // 每个 provider 的默认模型(key 为 provider ID)
272
- defaultModels: z.record(z.string()).default({}),
272
+ defaultModels: z.record(z.string(), z.string()).default({}),
273
273
  // API Keys:放宽为任意 record,支持自定义 provider ID
274
- apiKeys: z.record(z.string()).default({}),
275
- customBaseUrls: z.record(z.string()).default({}),
274
+ apiKeys: z.record(z.string(), z.string()).default({}),
275
+ customBaseUrls: z.record(z.string(), z.string()).default({}),
276
276
  // Per-provider timeout in ms (e.g. { deepseek: 60000 })
277
277
  // ⚠️ Timeout 优先级说明(L5):
278
278
  // 1. modelParams[modelId].timeout — 最高优先级,精确到具体模型(如 deepseek-reasoner 需要更长超时)
279
279
  // 2. timeouts[providerId] — Provider 级默认,覆盖内置默认值
280
280
  // 3. 内置 Provider 的硬编码默认值 — 最低兜底(如 OpenAI 兼容系列默认 60000ms)
281
281
  // 实现位置:src/providers/registry.ts initialize(),将 timeouts[id] 注入 provider
282
- timeouts: z.record(z.number()).default({}),
282
+ timeouts: z.record(z.string(), z.number()).default({}),
283
283
  // HTTP/HTTPS 代理地址(Node.js 不自动使用系统代理,需显式配置)
284
284
  // 例:http://127.0.0.1:10809
285
285
  // 也可通过环境变量 HTTPS_PROXY / HTTP_PROXY 覆盖
286
286
  proxy: z.string().max(500).optional(),
287
+ // 不走代理的 API 主机(后缀匹配,如 "deepseek.com" 也覆盖 api.deepseek.com;"*" = 全部直连)。
288
+ // 不填 = 内置国内模型清单(DeepSeek/智谱/Kimi/Qwen/MiniMax 直连,见 core/proxy.ts);
289
+ // 填 [] = 所有 Provider 都走代理(公司网络只能经代理出网时用)。
290
+ noProxy: z.array(z.string().max(200)).max(100).optional(),
287
291
  // 自定义 Provider 列表(OpenAI 兼容接口,无需改代码)
288
292
  customProviders: z.array(CustomProviderConfigSchema).default([]),
289
293
  // 按模型 ID 存储的推理参数(key 为模型 ID,如 "deepseek-chat")
290
- modelParams: z.record(ModelParamsSchema).default({}),
294
+ modelParams: z.record(z.string(), ModelParamsSchema).default({}),
291
295
  ui: z.object({
292
296
  streaming: z.boolean().default(true),
293
297
  markdownRendering: z.boolean().default(true),
@@ -321,12 +325,12 @@ var ConfigSchema = z.object({
321
325
  toolResult: z.string().optional(),
322
326
  heading: z.string().optional()
323
327
  }).optional()
324
- }).default({}),
328
+ }).prefault({}),
325
329
  session: z.object({
326
330
  autoSave: z.boolean().default(true),
327
331
  maxHistoryDays: z.number().default(30),
328
332
  systemPrompt: z.string().max(1e5).optional()
329
- }).default({}),
333
+ }).prefault({}),
330
334
  // 项目上下文文件配置
331
335
  // 启动时自动读取并注入 system prompt,类似 Claude Code 的 CLAUDE.md / Codex AGENTS.md 机制
332
336
  // 默认按顺序查找:AICLI.override.md → AGENTS.override.md → AICLI.md → CLAUDE.md → AGENTS.md
@@ -336,13 +340,13 @@ var ConfigSchema = z.object({
336
340
  projectDocFallbackFilenames: z.array(z.string().max(500)).max(20).default([]),
337
341
  projectDocMaxBytes: z.number().int().positive().default(32 * 1024),
338
342
  showLoadedContextSources: z.boolean().default(false)
339
- }).default({}),
343
+ }).prefault({}),
340
344
  // 持久记忆注入配置(MEM-005)
341
345
  // maxChars: 注入 system prompt 的字符预算,超出丢最老条目并显式标注截断。
342
346
  // 注意 CJK ≈ 1 字符/token——中文记忆的 token 开销约等于字符数。
343
347
  memory: z.object({
344
348
  maxChars: z.number().int().positive().max(2e5).default(1e4)
345
- }).default({}),
349
+ }).prefault({}),
346
350
  // Google Custom Search API 的 Search Engine ID (cx 参数)
347
351
  // API Key 通过 apiKeys['google-search'] 或 AICLI_API_KEY_GOOGLESEARCH 环境变量配置
348
352
  // CX 也可通过 AICLI_GOOGLE_CX 环境变量覆盖
@@ -354,7 +358,7 @@ var ConfigSchema = z.object({
354
358
  // 仍想用 API 版?把 enabled 设为 true,并务必到 GCP Console 把 Custom Search Quota 设硬上限。
355
359
  googleSearch: z.object({
356
360
  enabled: z.boolean().default(false)
357
- }).default({ enabled: false }),
361
+ }).prefault({ enabled: false }),
358
362
  // MCP (Model Context Protocol) 全局开关(v0.4.135+)
359
363
  // false 时启动跳过 connectAll(),所有 MCP 服务器都不连,节省冷启动时间 + 工具定义 token。
360
364
  // 对应你的 skills(whitelist 不依赖 MCP 工具时)完全不受影响。
@@ -363,10 +367,10 @@ var ConfigSchema = z.object({
363
367
  // 声明外部 MCP 服务器,启动时自动连接、发现工具并注册
364
368
  // 配置格式兼容 Claude Desktop(command + args + env)
365
369
  // 示例:{ "filesystem": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path"] } }
366
- mcpServers: z.record(z.object({
370
+ mcpServers: z.record(z.string(), z.object({
367
371
  command: z.string().max(500),
368
372
  args: z.array(z.string()).max(100).default([]),
369
- env: z.record(z.string()).optional(),
373
+ env: z.record(z.string(), z.string()).optional(),
370
374
  timeout: z.number().int().min(100).max(3e5).default(3e4)
371
375
  })).default({}),
372
376
  // Hooks 生命周期(v0.4.212+):兼容旧 pre/post 字符串,同时支持 events.<EventName> 结构化 JSON hook。
@@ -374,7 +378,7 @@ var ConfigSchema = z.object({
374
378
  enabled: z.boolean().default(true),
375
379
  preToolExecution: z.string().optional(),
376
380
  postToolExecution: z.string().optional(),
377
- events: z.record(z.union([
381
+ events: z.record(z.string(), z.union([
378
382
  z.string(),
379
383
  z.object({
380
384
  command: z.string(),
@@ -412,12 +416,12 @@ var ConfigSchema = z.object({
412
416
  google_search: z.enum(["allow", "confirm", "deny"]).optional(),
413
417
  shell: z.enum(["allow", "confirm", "deny"]).optional(),
414
418
  mcp: z.enum(["allow", "confirm", "deny"]).optional()
415
- }).default({})
416
- }).default({ enabled: false, defaultAction: "confirm", allowDomains: [], denyDomains: [], allowPorts: [], denyPorts: [], allowPrivateNetwork: false, tools: {} }),
419
+ }).prefault({})
420
+ }).prefault({ enabled: false, defaultAction: "confirm", allowDomains: [], denyDomains: [], allowPorts: [], denyPorts: [], allowPrivateNetwork: false, tools: {} }),
417
421
  // 权限 Profile(v0.4.211+):legacy 保持旧行为;其余 profile 提供更清晰的安全边界。
418
422
  defaultPermissionProfile: z.enum(["legacy", "read-only", "workspace-write", "danger-full-access"]).default("legacy"),
419
423
  allowedPermissionProfiles: z.array(z.string().max(50)).max(20).default(["legacy", "read-only", "workspace-write", "danger-full-access"]),
420
- permissionProfiles: z.record(z.object({
424
+ permissionProfiles: z.record(z.string(), z.object({
421
425
  extends: z.enum(["legacy", "read-only", "workspace-write", "danger-full-access"]).optional(),
422
426
  defaultAction: z.enum(["auto-approve", "deny", "confirm"]).optional(),
423
427
  workspaceRoots: z.array(z.string()).optional(),
@@ -492,7 +496,7 @@ var ConfigSchema = z.object({
492
496
  redactOnSend: z.boolean().default(false),
493
497
  mode: z.enum(["default", "strict", "off"]).default("default"),
494
498
  customPatterns: z.array(z.string().max(200)).max(50).default([])
495
- }).default({}),
499
+ }).prefault({}),
496
500
  // 智能模型路由(v0.4.68+)
497
501
  // 按用户每轮输入的内容/标签/长度动态选择模型,在同一 provider 内切换,
498
502
  // 例:短问题走 haiku(省钱),planning 走 opus(质量)。
@@ -512,7 +516,7 @@ var ConfigSchema = z.object({
512
516
  name: z.string().max(100).optional()
513
517
  })).default([]),
514
518
  fallback: z.string().optional()
515
- }).default({ enabled: false, rules: [] }),
519
+ }).prefault({ enabled: false, rules: [] }),
516
520
  // Provider 容错(v0.4.144+):网络抖动 / 5xx / 429 时,先在当前 provider 上做指数退避重试,
517
521
  // 仍失败则按顺序尝试 chain 里的 fallback provider。auth / 400 / 422 等"硬失败"直接跳过同
518
522
  // provider 重试,但仍会尝试 chain(不同 key/schema 可能能成功)。
@@ -532,7 +536,7 @@ var ConfigSchema = z.object({
532
536
  provider: z.string().max(100),
533
537
  model: z.string().optional()
534
538
  })).default([])
535
- }).default({ enabled: false, retries: 2, initialBackoffMs: 500, maxBackoffMs: 8e3, chain: [] })
539
+ }).prefault({ enabled: false, retries: 2, initialBackoffMs: 500, maxBackoffMs: 8e3, chain: [] })
536
540
  });
537
541
 
538
542
  // src/config/env-loader.ts
@@ -806,7 +810,7 @@ ${err}`
806
810
  const draft = { ...this.config, [key]: value };
807
811
  const result = ConfigSchema.safeParse(draft);
808
812
  if (!result.success) {
809
- const firstErr = result.error.errors[0];
813
+ const firstErr = result.error.issues[0];
810
814
  throw new ConfigError(
811
815
  `Invalid transient config value for "${String(key)}": ${firstErr?.message ?? "validation failed"}`
812
816
  );
@@ -858,7 +862,7 @@ ${err}`
858
862
  current[keys[keys.length - 1]] = value;
859
863
  const result = ConfigSchema.safeParse(draft);
860
864
  if (!result.success) {
861
- const firstErr = result.error.errors[0];
865
+ const firstErr = result.error.issues[0];
862
866
  throw new ConfigError(`Invalid config value for "${path3}": ${firstErr?.message ?? "validation failed"}`);
863
867
  }
864
868
  this.config = result.data;
@@ -1479,6 +1483,77 @@ var BaseProvider = class {
1479
1483
  function isFileWriteTool(name) {
1480
1484
  return name === "write_file" || name === "edit_file" || name === "notebook_edit";
1481
1485
  }
1486
+ var READ_ONLY_CMDLETS = new Set([
1487
+ "Get-ChildItem",
1488
+ "gci",
1489
+ "Get-Content",
1490
+ "gc",
1491
+ "Get-Item",
1492
+ "gi",
1493
+ "Get-ItemProperty",
1494
+ "gp",
1495
+ "Get-ItemPropertyValue",
1496
+ "Get-Location",
1497
+ "gl",
1498
+ "Test-Path",
1499
+ "Resolve-Path",
1500
+ "Split-Path",
1501
+ "Join-Path",
1502
+ "Get-Command",
1503
+ "gcm",
1504
+ "Get-Service",
1505
+ "gsv",
1506
+ "Get-Process",
1507
+ "gps",
1508
+ "Get-Date",
1509
+ "Get-Alias",
1510
+ "Get-Module",
1511
+ "Get-PSDrive",
1512
+ "Get-Acl",
1513
+ "Get-FileHash",
1514
+ "Get-CimInstance",
1515
+ "Get-NetTCPConnection",
1516
+ "Get-NetIPAddress",
1517
+ "Get-WinEvent",
1518
+ "Get-ComputerInfo",
1519
+ "Get-Culture",
1520
+ "Get-ExecutionPolicy",
1521
+ "Get-Unique",
1522
+ "Select-Object",
1523
+ "select",
1524
+ "Where-Object",
1525
+ "Sort-Object",
1526
+ "Measure-Object",
1527
+ "Group-Object",
1528
+ "Compare-Object",
1529
+ "Select-String",
1530
+ "sls",
1531
+ "Format-Table",
1532
+ "ft",
1533
+ "Format-List",
1534
+ "fl",
1535
+ "Format-Wide",
1536
+ "Out-String",
1537
+ "Write-Output",
1538
+ "Write-Host",
1539
+ // Waiting is harmless on its own; chained segments still have to pass this list (v0.4.338).
1540
+ "Start-Sleep",
1541
+ // ls is read-only both as the PowerShell alias and as the Unix binary; models pair it with
1542
+ // Start-Sleep to poll an output folder (2026-11-05 lecture-video run).
1543
+ "ls",
1544
+ "ConvertTo-Json",
1545
+ "ConvertFrom-Json",
1546
+ "ConvertTo-Csv",
1547
+ "ConvertFrom-Csv"
1548
+ ].map((name) => name.toLowerCase()));
1549
+ function isReadOnlyCmdletChain(cmd) {
1550
+ const normalized = cmd.trim().replace(/\s2>\s*\$null\b/gi, " ").replace(/\s2>&1\b/g, " ").replace(/\$(?:env:\w+|null|true|false)\b/gi, "X");
1551
+ if (!normalized || /[&`$<>\n\r(){}]|::/.test(normalized)) return false;
1552
+ return normalized.split(/[;|]/).every((segment) => {
1553
+ const name = segment.trim().split(/\s+/)[0].toLowerCase();
1554
+ return READ_ONLY_CMDLETS.has(name);
1555
+ });
1556
+ }
1482
1557
  function getDangerLevel(toolName, args) {
1483
1558
  if (toolName.startsWith("mcp__")) {
1484
1559
  const rawName = toolName.split("__").pop() ?? "";
@@ -1508,6 +1583,7 @@ function getDangerLevel(toolName, args) {
1508
1583
  if (/\b(python3?|node|deno|bun|perl|ruby|php|bash|sh|zsh|pwsh|powershell)(\.exe)?\b[^\n]*?\s-(?:Command|EncodedCommand|c|e|p|r)\b/i.test(cmd)) return "write";
1509
1584
  if (/\bcmd(\.exe)?\s+\/[ck]\b/i.test(cmd)) return "write";
1510
1585
  if (!/[;&|`$<>\n\r(){}]/.test(cmd) && (/^(?:pwd|ls|cat|head|tail|wc|Get-ChildItem|Get-Location|Get-Content)(?:\s+[\w ./\\:*?=,@'"-]+)?\s*$/i.test(cmd.trim()) || /^echo(?:\s+[\w .,'"-]+)?\s*$/.test(cmd.trim()) || /^(?:node|python3?)\s+(?:-v|--version)\s*$/.test(cmd.trim()) || /^git\s+(?:status|log)(?:\s+(?:--short|--porcelain|--oneline|-\d+))*\s*$/.test(cmd.trim()))) return "safe";
1586
+ if (isReadOnlyCmdletChain(cmd)) return "safe";
1511
1587
  return "write";
1512
1588
  }
1513
1589
  if (toolName === "run_tests") return "write";
@@ -2568,6 +2644,41 @@ function readReasoningContent(message) {
2568
2644
  return typeof value === "string" ? value : void 0;
2569
2645
  }
2570
2646
 
2647
+ // src/core/proxy.ts
2648
+ var DEFAULT_NO_PROXY_HOSTS = [
2649
+ "deepseek.com",
2650
+ "bigmodel.cn",
2651
+ "moonshot.cn",
2652
+ "moonshot.ai",
2653
+ "aliyuncs.com",
2654
+ "minimaxi.com"
2655
+ ];
2656
+ function shouldBypassProxy(baseUrl, noProxy = DEFAULT_NO_PROXY_HOSTS) {
2657
+ if (!baseUrl) return false;
2658
+ let host;
2659
+ try {
2660
+ host = new URL(baseUrl).hostname.toLowerCase();
2661
+ } catch {
2662
+ return false;
2663
+ }
2664
+ if (!host) return false;
2665
+ return noProxy.some((raw) => {
2666
+ const entry = raw.trim().toLowerCase().replace(/^\*?\./, "");
2667
+ if (!entry) return false;
2668
+ if (entry === "*") return true;
2669
+ return host === entry || host.endsWith("." + entry);
2670
+ });
2671
+ }
2672
+ async function setupProxy(configProxy) {
2673
+ const proxyUrl = process.env.HTTPS_PROXY ?? process.env.HTTP_PROXY ?? process.env.https_proxy ?? process.env.http_proxy ?? configProxy;
2674
+ if (!proxyUrl) return;
2675
+ try {
2676
+ const { ProxyAgent, setGlobalDispatcher } = await import("undici");
2677
+ setGlobalDispatcher(new ProxyAgent({ uri: proxyUrl }));
2678
+ } catch {
2679
+ }
2680
+ }
2681
+
2571
2682
  // src/tools/hallucination.ts
2572
2683
  var HALLUCINATION_PATTERNS = [
2573
2684
  /文件路径[::]\s*`?[^\s`]+\.\w{1,5}/,
@@ -3901,7 +4012,8 @@ var OpenAICompatibleProvider = class extends BaseProvider {
3901
4012
  timeout: this.defaultTimeout,
3902
4013
  maxRetries
3903
4014
  };
3904
- const proxyUrl = isLoopbackBaseUrl(clientOptions.baseURL) ? void 0 : options?.proxy;
4015
+ const noProxy = Array.isArray(options?.noProxy) ? options.noProxy : DEFAULT_NO_PROXY_HOSTS;
4016
+ const proxyUrl = isLoopbackBaseUrl(clientOptions.baseURL) || shouldBypassProxy(clientOptions.baseURL, noProxy) ? void 0 : options?.proxy;
3905
4017
  try {
3906
4018
  const { Agent: Agent2, ProxyAgent, fetch: undiciFetch } = await import("undici");
3907
4019
  const STREAM_BODY_TIMEOUT = 30 * 60 * 1e3;
@@ -8361,6 +8473,14 @@ function domainMatches(pattern, host) {
8361
8473
  const h = host.trim().toLowerCase().replace(/:\d+$/, "");
8362
8474
  return h === p || h.endsWith(`.${p}`);
8363
8475
  }
8476
+ function isHostAllowedByNetworkPolicy(host, policy) {
8477
+ if (!policy?.enabled) return true;
8478
+ if (policy.denyDomains?.some((pattern) => domainMatches(pattern, host))) return false;
8479
+ if (policy.allowDomains && policy.allowDomains.length > 0) {
8480
+ return policy.allowDomains.some((pattern) => domainMatches(pattern, host));
8481
+ }
8482
+ return true;
8483
+ }
8364
8484
  function isPrivateNetworkHost(host) {
8365
8485
  const h = host.toLowerCase().replace(/^\[|\]$/g, "");
8366
8486
  if (h === "" || h === "localhost") return true;
@@ -10720,6 +10840,231 @@ ${stderr}`);
10720
10840
  import { promises as dnsPromises } from "dns";
10721
10841
  import net from "net";
10722
10842
  import { Agent } from "undici";
10843
+
10844
+ // src/tools/builtin/google-search.ts
10845
+ var GOOGLE_SEARCH_API = "https://www.googleapis.com/customsearch/v1";
10846
+ var REQUEST_TIMEOUT_MS = 15e3;
10847
+ var MAX_RESULTS = 10;
10848
+ var DEFAULT_RESULTS = 5;
10849
+ var googleSearchContext = {};
10850
+ var googleSearchTool = {
10851
+ definition: {
10852
+ name: "google_search",
10853
+ description: "\u26A0\uFE0F NOT RECOMMENDED \u2014 uses Google Custom Search API which requires a paid API key. API keys have leaked in the wild causing tens of thousands of USD in unauthorized charges. PREFER `web_search` (keyless Bing/Google scraping, zero billing risk). This tool is disabled by default; user must set config.googleSearch.enabled=true to use it.",
10854
+ parameters: {
10855
+ query: {
10856
+ type: "string",
10857
+ description: "The search query string.",
10858
+ required: true
10859
+ },
10860
+ num_results: {
10861
+ type: "number",
10862
+ description: "Number of results to return (1-10, default 5).",
10863
+ required: false
10864
+ }
10865
+ },
10866
+ dangerous: false
10867
+ },
10868
+ async execute(args) {
10869
+ const query = String(args["query"] ?? "").trim();
10870
+ if (!query) throw new ToolError("google_search", "query parameter is required");
10871
+ const enabledFlag = googleSearchContext.configManager?.get("googleSearch")?.enabled ?? false;
10872
+ if (!enabledFlag) {
10873
+ throw new ToolError(
10874
+ "google_search",
10875
+ '\u{1F6AB} google_search is disabled by default (v0.4.135+).\n\nWHY: Google Custom Search has no native daily-spend cap. Keys leak (search GitHub for "AIza" \u2014 thousands of public exposures), bots scrape them within hours, and the GCP bill lands days later. Multiple engineers have lost $10k+ USD this way.\n\nRECOMMENDED: use `web_search` instead \u2014 same use case, scrapes Bing/Google public pages, zero API key, zero billing risk.\n\nIF YOU INSIST: edit ~/.aicli/config.json and set `googleSearch.enabled = true`. ALSO go to GCP Console \u2192 Custom Search API \u2192 Quotas and set a hard daily request cap (e.g. 100/day) \u2014 that is the only real damage cap.'
10876
+ );
10877
+ }
10878
+ const numResults = Math.min(
10879
+ Math.max(Math.floor(Number(args["num_results"] ?? DEFAULT_RESULTS)), 1),
10880
+ MAX_RESULTS
10881
+ );
10882
+ const { apiKey, cx } = resolveConfig();
10883
+ const url = new URL(GOOGLE_SEARCH_API);
10884
+ url.searchParams.set("key", apiKey);
10885
+ url.searchParams.set("cx", cx);
10886
+ url.searchParams.set("q", query);
10887
+ url.searchParams.set("num", String(numResults));
10888
+ const controller = new AbortController();
10889
+ const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
10890
+ try {
10891
+ const response = await fetch(url.toString(), {
10892
+ method: "GET",
10893
+ headers: {
10894
+ "Accept": "application/json"
10895
+ },
10896
+ signal: controller.signal
10897
+ });
10898
+ if (!response.ok) {
10899
+ const errorBody = await response.text().catch(() => "");
10900
+ if (response.status === 403) {
10901
+ throw new NetworkError(
10902
+ `Google Search API 403 Forbidden \u2014 Invalid API Key or daily free quota exceeded (100/day).
10903
+ Please check your API Key and Search Engine ID configuration.`,
10904
+ 403
10905
+ );
10906
+ }
10907
+ if (response.status === 429) {
10908
+ throw new NetworkError("Google Search API 429 \u2014 Too many requests, please try again later.", 429);
10909
+ }
10910
+ throw new NetworkError(
10911
+ `Google Search API error: HTTP ${response.status} ${response.statusText}
10912
+ ${errorBody.slice(0, 500)}`,
10913
+ response.status
10914
+ );
10915
+ }
10916
+ const data = await response.json();
10917
+ return formatResults(query, data, numResults);
10918
+ } catch (err) {
10919
+ if (err instanceof Error && err.name === "AbortError") {
10920
+ throw new NetworkError(`Google Search request timed out (${REQUEST_TIMEOUT_MS / 1e3}s). Please check your network or proxy configuration.`, void 0, err);
10921
+ }
10922
+ throw err;
10923
+ } finally {
10924
+ clearTimeout(timeout);
10925
+ }
10926
+ }
10927
+ };
10928
+ function resolveConfig() {
10929
+ let apiKey;
10930
+ let cx;
10931
+ if (googleSearchContext.configManager) {
10932
+ apiKey = googleSearchContext.configManager.getApiKey("google-search");
10933
+ cx = EnvLoader.getGoogleSearchEngineId() ?? googleSearchContext.configManager.get("googleSearchEngineId");
10934
+ } else {
10935
+ apiKey = EnvLoader.getApiKey("google-search");
10936
+ cx = EnvLoader.getGoogleSearchEngineId();
10937
+ }
10938
+ if (!apiKey) {
10939
+ throw new ToolError(
10940
+ "google_search",
10941
+ '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'
10942
+ );
10943
+ }
10944
+ if (!cx) {
10945
+ throw new ToolError(
10946
+ "google_search",
10947
+ "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"
10948
+ );
10949
+ }
10950
+ return { apiKey, cx };
10951
+ }
10952
+ function formatResults(query, data, _requested) {
10953
+ const items = data.items ?? [];
10954
+ if (items.length === 0) {
10955
+ const info2 = data.searchInformation;
10956
+ return `No results found for: "${query}"` + (info2 ? ` (searched ${info2.formattedTotalResults ?? "0"} pages in ${info2.formattedSearchTime ?? "?"}s)` : "");
10957
+ }
10958
+ const info = data.searchInformation;
10959
+ const header = `Search results for "${query}" (${items.length} of ~${info?.formattedTotalResults ?? "?"} results):
10960
+ `;
10961
+ const results = items.map((item, i) => {
10962
+ const title = item.title ?? "Untitled";
10963
+ const link = item.link ?? "";
10964
+ const snippet = item.snippet ?? "";
10965
+ return `${i + 1}. **${title}**
10966
+ URL: ${link}
10967
+ ${snippet}`;
10968
+ });
10969
+ return header + "\n" + results.join("\n\n");
10970
+ }
10971
+
10972
+ // src/tools/builtin/deepseek-web-api.ts
10973
+ var DEEPSEEK_TOOLS_HOST = "api.deepseek.com";
10974
+ var BASE_URL = `https://${DEEPSEEK_TOOLS_HOST}/v1/tools`;
10975
+ var SEARCH_TIMEOUT_MS = 2e4;
10976
+ var FETCH_TIMEOUT_MS = 3e4;
10977
+ var DEEPSEEK_MAX_QUERY_CHARS = 128;
10978
+ function resolveDeepSeekWebKey(configManager = googleSearchContext.configManager) {
10979
+ const key = configManager ? configManager.getApiKey("deepseek") : EnvLoader.getApiKey("deepseek");
10980
+ if (!key || !key.trim()) return void 0;
10981
+ const policy = configManager?.get("networkPolicy");
10982
+ if (!isHostAllowedByNetworkPolicy(DEEPSEEK_TOOLS_HOST, policy)) return void 0;
10983
+ return key.trim();
10984
+ }
10985
+ async function deepseekSearch(apiKey, query, count) {
10986
+ const data = await postJson(apiKey, "search", { query, count }, SEARCH_TIMEOUT_MS);
10987
+ return parseDeepSeekSearchResponse(data);
10988
+ }
10989
+ async function deepseekFetch(apiKey, url) {
10990
+ const data = await postJson(apiKey, "fetch", { url }, FETCH_TIMEOUT_MS);
10991
+ const content = typeof data?.content === "string" ? data.content : "";
10992
+ if (!content.trim()) throw new NetworkError("DeepSeek Fetch returned no content.");
10993
+ return { title: typeof data.title === "string" ? data.title : "", content };
10994
+ }
10995
+ function parseDeepSeekSearchResponse(data) {
10996
+ const raw = data?.results;
10997
+ if (!Array.isArray(raw)) {
10998
+ throw new NetworkError("DeepSeek Search returned an unexpected response (no results array).");
10999
+ }
11000
+ const out = [];
11001
+ for (const item of raw) {
11002
+ if (!item || typeof item !== "object") continue;
11003
+ const r = item;
11004
+ const url = typeof r["url"] === "string" ? r["url"] : "";
11005
+ const title = typeof r["title"] === "string" ? r["title"].trim() : "";
11006
+ if (!url || !title) continue;
11007
+ out.push({
11008
+ title,
11009
+ url,
11010
+ snippet: typeof r["snippet"] === "string" ? r["snippet"].trim() : "",
11011
+ summary: typeof r["summary"] === "string" ? r["summary"].trim() : "",
11012
+ siteName: typeof r["site_name"] === "string" && r["site_name"] ? r["site_name"] : null,
11013
+ date: typeof r["date"] === "string" && r["date"] ? r["date"] : null,
11014
+ rerankScore: typeof r["rerank_score"] === "number" ? r["rerank_score"] : null,
11015
+ authority: typeof r["authority"] === "number" ? r["authority"] : null
11016
+ });
11017
+ }
11018
+ return out;
11019
+ }
11020
+ async function postJson(apiKey, endpoint, body, timeoutMs) {
11021
+ const controller = new AbortController();
11022
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
11023
+ try {
11024
+ const res = await fetch(`${BASE_URL}/${endpoint}`, {
11025
+ method: "POST",
11026
+ headers: {
11027
+ Authorization: `Bearer ${apiKey}`,
11028
+ "Content-Type": "application/json",
11029
+ Accept: "application/json"
11030
+ },
11031
+ body: JSON.stringify(body),
11032
+ signal: controller.signal
11033
+ });
11034
+ const text = await res.text();
11035
+ if (!res.ok) {
11036
+ throw new NetworkError(
11037
+ `DeepSeek ${endpoint} HTTP ${res.status}: ${describeDeepSeekError(res.status, text)}`,
11038
+ res.status
11039
+ );
11040
+ }
11041
+ try {
11042
+ return JSON.parse(text);
11043
+ } catch {
11044
+ throw new NetworkError(`DeepSeek ${endpoint} returned non-JSON response.`);
11045
+ }
11046
+ } catch (err) {
11047
+ if (err instanceof Error && err.name === "AbortError") {
11048
+ throw new NetworkError(`DeepSeek ${endpoint} timed out after ${timeoutMs / 1e3}s.`, void 0, err);
11049
+ }
11050
+ throw err;
11051
+ } finally {
11052
+ clearTimeout(timer);
11053
+ }
11054
+ }
11055
+ function describeDeepSeekError(status, bodyText) {
11056
+ let message = "";
11057
+ try {
11058
+ const parsed = JSON.parse(bodyText);
11059
+ if (typeof parsed?.error?.message === "string") message = parsed.error.message;
11060
+ } catch {
11061
+ }
11062
+ if (!message) message = bodyText.trim().slice(0, 200) || "(empty body)";
11063
+ const hint = status === 401 ? " \u2014 DeepSeek API key missing or invalid" : status === 402 ? " \u2014 DeepSeek account balance is insufficient" : status === 403 ? " \u2014 rejected by DeepSeek security policy" : status === 404 ? " \u2014 no usable content at this URL" : status === 429 ? " \u2014 DeepSeek QPS limit exceeded" : "";
11064
+ return message + hint;
11065
+ }
11066
+
11067
+ // src/tools/builtin/web-fetch.ts
10723
11068
  function htmlToText(html) {
10724
11069
  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, "");
10725
11070
  const HTML_REGEX_LIMIT = 2e5;
@@ -10892,7 +11237,7 @@ function ssrfSafeDispatcher() {
10892
11237
  var webFetchTool = {
10893
11238
  definition: {
10894
11239
  name: "web_fetch",
10895
- 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.",
11240
+ 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. If the direct request fails and a DeepSeek API key is configured, retries once via the DeepSeek Fetch API.",
10896
11241
  parameters: {
10897
11242
  url: {
10898
11243
  type: "string",
@@ -10922,94 +11267,26 @@ var webFetchTool = {
10922
11267
  if (e.message.startsWith("Blocked:")) throw e;
10923
11268
  throw new NetworkError(`Invalid URL: "${url}"`);
10924
11269
  }
10925
- const controller = new AbortController();
10926
- const timeoutId = setTimeout(() => controller.abort(), 2e4);
10927
- let rawHtml;
10928
- let finalUrl;
10929
- let contentType;
10930
- const MAX_REDIRECTS = 10;
10931
- const FETCH_HEADERS = {
10932
- "User-Agent": "Mozilla/5.0 (compatible; ai-cli/1.0; +https://github.com/ai-cli)",
10933
- Accept: "text/html,application/xhtml+xml,text/plain,*/*",
10934
- "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8"
10935
- };
10936
- const dispatcher = ssrfSafeDispatcher();
11270
+ let text;
10937
11271
  try {
10938
- let currentUrl = url;
10939
- let resp = null;
10940
- for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
10941
- const parsedHop = new URL(currentUrl);
10942
- if (isPrivateHost(parsedHop.hostname)) {
10943
- throw new NetworkError(`Blocked: redirect to private/internal address "${currentUrl}".`);
10944
- }
10945
- await resolveAndCheck(parsedHop.hostname);
10946
- const r = await fetch(currentUrl, {
10947
- signal: controller.signal,
10948
- headers: FETCH_HEADERS,
10949
- redirect: "manual",
10950
- // 手动控制重定向
10951
- dispatcher
10952
- // dual undici-types:`dispatcher` 字段在 undici 与 @types/node 内置的
10953
- // undici-types 两份 RequestInit 间判不兼容,localized 转换。
10954
- });
10955
- if (r.status >= 300 && r.status < 400) {
10956
- if (hop >= MAX_REDIRECTS) {
10957
- throw new NetworkError(`Too many redirects (>${MAX_REDIRECTS}): ${url}`);
10958
- }
10959
- const location = r.headers.get("Location");
10960
- if (!location) {
10961
- resp = r;
10962
- break;
10963
- }
10964
- currentUrl = new URL(location, currentUrl).href;
10965
- continue;
10966
- }
10967
- resp = r;
10968
- break;
10969
- }
10970
- if (!resp) throw new NetworkError(`Too many redirects (>${MAX_REDIRECTS}): ${url}`);
10971
- finalUrl = currentUrl;
10972
- contentType = resp.headers.get("content-type") ?? "";
10973
- if (!resp.ok) {
10974
- throw new NetworkError(`HTTP ${resp.status} ${resp.statusText}`, resp.status);
10975
- }
10976
- const bodyBytes = await readBodyCapped(resp, MAX_BODY_BYTES);
10977
- clearTimeout(timeoutId);
10978
- rawHtml = new TextDecoder("utf-8", { fatal: false }).decode(bodyBytes);
11272
+ text = await fetchDirect(url);
10979
11273
  } catch (err) {
10980
- clearTimeout(timeoutId);
10981
- if (err.name === "AbortError") {
10982
- throw new NetworkError(`Request timed out after 20s: ${url}`, void 0, err);
10983
- }
10984
- const cause = err.cause;
10985
- if (cause?.message?.startsWith("Blocked:")) throw new NetworkError(cause.message);
10986
- if (err instanceof Error && err.message === "fetch failed") {
10987
- const detail = cause?.message ? ` (${cause.message})` : "";
10988
- throw new NetworkError(
10989
- `Connection failed for ${url}${detail}. The domain may be unreachable from the current network (some sites require a proxy \u2014 see config.proxy) or temporarily down. Do not retry the same URL; try an alternative source.`,
10990
- void 0,
10991
- err
10992
- );
11274
+ const msg = err instanceof Error ? err.message : String(err);
11275
+ if (msg.startsWith("Blocked:")) throw err;
11276
+ const key = resolveDeepSeekWebKey();
11277
+ if (!key) throw err;
11278
+ try {
11279
+ const ds = await deepseekFetch(key, url);
11280
+ text = [
11281
+ ds.title ? `# ${ds.title}` : "",
11282
+ `Source: ${url} (via DeepSeek Fetch \u2014 direct request failed: ${msg.split("\n")[0]})`,
11283
+ ""
11284
+ ].filter(Boolean).join("\n") + "\n\n" + ds.content;
11285
+ } catch (dsErr) {
11286
+ const dsMsg = dsErr instanceof Error ? dsErr.message : String(dsErr);
11287
+ throw new NetworkError(`${msg}
11288
+ DeepSeek Fetch fallback also failed: ${dsMsg}`, void 0, err);
10993
11289
  }
10994
- throw err;
10995
- } finally {
10996
- void dispatcher.close().catch(() => {
10997
- });
10998
- }
10999
- let text;
11000
- if (contentType.includes("text/plain") || contentType.includes("application/json")) {
11001
- text = rawHtml;
11002
- } else {
11003
- const title = extractTitle(rawHtml);
11004
- const desc = extractDescription(rawHtml);
11005
- const body = htmlToText(rawHtml);
11006
- const header = [
11007
- title ? `# ${title}` : "",
11008
- desc ? `> ${desc}` : "",
11009
- `Source: ${finalUrl}`,
11010
- ""
11011
- ].filter(Boolean).join("\n");
11012
- text = header + "\n\n" + body;
11013
11290
  }
11014
11291
  if (selector) {
11015
11292
  const lower = selector.toLowerCase();
@@ -11034,16 +11311,132 @@ var webFetchTool = {
11034
11311
  return text;
11035
11312
  }
11036
11313
  };
11314
+ async function fetchDirect(url) {
11315
+ const controller = new AbortController();
11316
+ const timeoutId = setTimeout(() => controller.abort(), 2e4);
11317
+ let rawHtml;
11318
+ let finalUrl;
11319
+ let contentType;
11320
+ const MAX_REDIRECTS = 10;
11321
+ const FETCH_HEADERS = {
11322
+ "User-Agent": "Mozilla/5.0 (compatible; ai-cli/1.0; +https://github.com/ai-cli)",
11323
+ Accept: "text/html,application/xhtml+xml,text/plain,*/*",
11324
+ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8"
11325
+ };
11326
+ const dispatcher = ssrfSafeDispatcher();
11327
+ try {
11328
+ let currentUrl = url;
11329
+ let resp = null;
11330
+ for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
11331
+ const parsedHop = new URL(currentUrl);
11332
+ if (isPrivateHost(parsedHop.hostname)) {
11333
+ throw new NetworkError(`Blocked: redirect to private/internal address "${currentUrl}".`);
11334
+ }
11335
+ await resolveAndCheck(parsedHop.hostname);
11336
+ const r = await fetch(currentUrl, {
11337
+ signal: controller.signal,
11338
+ headers: FETCH_HEADERS,
11339
+ redirect: "manual",
11340
+ // 手动控制重定向
11341
+ dispatcher
11342
+ // dual undici-types:`dispatcher` 字段在 undici 与 @types/node 内置的
11343
+ // undici-types 两份 RequestInit 间判不兼容,localized 转换。
11344
+ });
11345
+ if (r.status >= 300 && r.status < 400) {
11346
+ if (hop >= MAX_REDIRECTS) {
11347
+ throw new NetworkError(`Too many redirects (>${MAX_REDIRECTS}): ${url}`);
11348
+ }
11349
+ const location = r.headers.get("Location");
11350
+ if (!location) {
11351
+ resp = r;
11352
+ break;
11353
+ }
11354
+ currentUrl = new URL(location, currentUrl).href;
11355
+ continue;
11356
+ }
11357
+ resp = r;
11358
+ break;
11359
+ }
11360
+ if (!resp) throw new NetworkError(`Too many redirects (>${MAX_REDIRECTS}): ${url}`);
11361
+ finalUrl = currentUrl;
11362
+ contentType = resp.headers.get("content-type") ?? "";
11363
+ if (!resp.ok) {
11364
+ throw new NetworkError(`HTTP ${resp.status} ${resp.statusText}`, resp.status);
11365
+ }
11366
+ const bodyBytes = await readBodyCapped(resp, MAX_BODY_BYTES);
11367
+ clearTimeout(timeoutId);
11368
+ rawHtml = new TextDecoder("utf-8", { fatal: false }).decode(bodyBytes);
11369
+ } catch (err) {
11370
+ clearTimeout(timeoutId);
11371
+ if (err.name === "AbortError") {
11372
+ throw new NetworkError(`Request timed out after 20s: ${url}`, void 0, err);
11373
+ }
11374
+ const cause = err.cause;
11375
+ if (cause?.message?.startsWith("Blocked:")) throw new NetworkError(cause.message);
11376
+ if (err instanceof Error && err.message === "fetch failed") {
11377
+ const detail = cause?.message ? ` (${cause.message})` : "";
11378
+ throw new NetworkError(
11379
+ `Connection failed for ${url}${detail}. The domain may be unreachable from the current network (some sites require a proxy \u2014 see config.proxy) or temporarily down. Do not retry the same URL; try an alternative source.`,
11380
+ void 0,
11381
+ err
11382
+ );
11383
+ }
11384
+ throw err;
11385
+ } finally {
11386
+ void dispatcher.close().catch(() => {
11387
+ });
11388
+ }
11389
+ let text;
11390
+ if (contentType.includes("text/plain") || contentType.includes("application/json")) {
11391
+ text = rawHtml;
11392
+ } else {
11393
+ const title = extractTitle(rawHtml);
11394
+ const desc = extractDescription(rawHtml);
11395
+ const body = htmlToText(rawHtml);
11396
+ const header = [
11397
+ title ? `# ${title}` : "",
11398
+ desc ? `> ${desc}` : "",
11399
+ `Source: ${finalUrl}`,
11400
+ ""
11401
+ ].filter(Boolean).join("\n");
11402
+ text = header + "\n\n" + body;
11403
+ }
11404
+ return text;
11405
+ }
11037
11406
 
11038
11407
  // src/tools/builtin/web-search.ts
11039
- var REQUEST_TIMEOUT_MS = 15e3;
11040
- var MAX_RESULTS = 10;
11041
- var DEFAULT_RESULTS = 5;
11408
+ function planEngineOrder(engine, query, hasDeepSeekKey) {
11409
+ const skipped = [];
11410
+ if (engine === "bing") return { order: ["bing"], skipped };
11411
+ if (engine === "google") return { order: ["google"], skipped };
11412
+ const tooLong = [...query].length > DEEPSEEK_MAX_QUERY_CHARS;
11413
+ if (engine === "deepseek") {
11414
+ if (!hasDeepSeekKey) {
11415
+ throw new ToolError(
11416
+ "web_search",
11417
+ "engine 'deepseek' needs a DeepSeek API key (config apiKeys.deepseek or AICLI_API_KEY_DEEPSEEK), and networkPolicy must allow api.deepseek.com. Use engine 'auto' or 'bing' instead."
11418
+ );
11419
+ }
11420
+ if (tooLong) {
11421
+ throw new ToolError(
11422
+ "web_search",
11423
+ `engine 'deepseek' accepts queries up to ${DEEPSEEK_MAX_QUERY_CHARS} characters \u2014 shorten the query.`
11424
+ );
11425
+ }
11426
+ return { order: ["deepseek"], skipped };
11427
+ }
11428
+ if (hasDeepSeekKey && !tooLong) return { order: ["deepseek", "bing", "google"], skipped };
11429
+ if (hasDeepSeekKey && tooLong) skipped.push(`deepseek: query longer than ${DEEPSEEK_MAX_QUERY_CHARS} chars, skipped`);
11430
+ return { order: ["bing", "google"], skipped };
11431
+ }
11432
+ var REQUEST_TIMEOUT_MS2 = 15e3;
11433
+ var MAX_RESULTS2 = 10;
11434
+ var DEFAULT_RESULTS2 = 5;
11042
11435
  var UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36";
11043
11436
  var webSearchTool = {
11044
11437
  definition: {
11045
11438
  name: "web_search",
11046
- description: 'Search the web using Bing or Google by scraping their public result pages. No API key required, no billing risk. Returns structured {title, url, snippet} for each result. Use this for fresh facts, news, or any "what does X say about Y" lookup. Prefer this over google_search (which needs a paid API key).',
11439
+ description: 'Search the web. Uses the DeepSeek Search API when a DeepSeek API key is configured (results carry a relevance score and a longer summary), otherwise scrapes Bing / Google public result pages. Returns title, url and summary/snippet for each result. Use this for fresh facts, news, or any "what does X say about Y" lookup. With the deepseek engine the query (max 128 chars) supports operators: site:example.com, -site:, "exact phrase", -term, lang:en, after:2025, before:2026-06-01. Prefer this over google_search (which needs a paid API key).',
11047
11440
  parameters: {
11048
11441
  query: {
11049
11442
  type: "string",
@@ -11057,7 +11450,7 @@ var webSearchTool = {
11057
11450
  },
11058
11451
  engine: {
11059
11452
  type: "string",
11060
- description: "Search engine: 'bing' (default for CN reachability), 'google', or 'auto' (try bing first, fall back to google on empty/error). Default 'auto'.",
11453
+ description: "Search engine: 'auto' (default \u2014 DeepSeek Search API if a DeepSeek key is configured, then Bing, then Google on empty/error), 'deepseek', 'bing', or 'google'.",
11061
11454
  required: false
11062
11455
  }
11063
11456
  },
@@ -11067,21 +11460,30 @@ var webSearchTool = {
11067
11460
  const query = String(args["query"] ?? "").trim();
11068
11461
  if (!query) throw new ToolError("web_search", "query parameter is required");
11069
11462
  const numResults = Math.min(
11070
- Math.max(Math.floor(Number(args["num_results"] ?? DEFAULT_RESULTS)), 1),
11071
- MAX_RESULTS
11463
+ Math.max(Math.floor(Number(args["num_results"] ?? DEFAULT_RESULTS2)), 1),
11464
+ MAX_RESULTS2
11072
11465
  );
11073
11466
  const engine = String(args["engine"] ?? "auto").toLowerCase();
11074
- if (!["bing", "google", "auto"].includes(engine)) {
11075
- throw new ToolError("web_search", `engine must be one of: bing, google, auto (got: ${engine})`);
11467
+ if (!["deepseek", "bing", "google", "auto"].includes(engine)) {
11468
+ throw new ToolError("web_search", `engine must be one of: auto, deepseek, bing, google (got: ${engine})`);
11076
11469
  }
11077
- const order = engine === "auto" ? ["bing", "google"] : engine === "bing" ? ["bing"] : ["google"];
11078
- const errors = [];
11470
+ const deepseekKey = engine === "bing" || engine === "google" ? void 0 : resolveDeepSeekWebKey();
11471
+ const { order, skipped } = planEngineOrder(engine, query, deepseekKey !== void 0);
11472
+ const errors = [...skipped];
11079
11473
  let weakBingFallback = null;
11080
11474
  let weakBingReason = null;
11081
11475
  for (let i = 0; i < order.length; i++) {
11082
11476
  const eng = order[i];
11083
11477
  const isLastEngine = i === order.length - 1;
11084
11478
  try {
11479
+ if (eng === "deepseek") {
11480
+ const dsResults = await deepseekSearch(deepseekKey, query, numResults);
11481
+ if (dsResults.length === 0) {
11482
+ errors.push("deepseek: 0 results");
11483
+ continue;
11484
+ }
11485
+ return formatDeepSeekResults(query, dsResults);
11486
+ }
11085
11487
  const results = eng === "bing" ? await searchBing(query, numResults) : await searchGoogle(query, numResults);
11086
11488
  if (results.length === 0) {
11087
11489
  errors.push(`${eng}: 0 results`);
@@ -11096,13 +11498,13 @@ var webSearchTool = {
11096
11498
  continue;
11097
11499
  }
11098
11500
  }
11099
- return formatResults(query, eng, results);
11501
+ return formatResults2(query, eng, results);
11100
11502
  } catch (err) {
11101
11503
  errors.push(`${eng}: ${err instanceof Error ? err.message : String(err)}`);
11102
11504
  }
11103
11505
  }
11104
11506
  if (weakBingFallback) {
11105
- return formatResults(query, "bing", weakBingFallback, {
11507
+ return formatResults2(query, "bing", weakBingFallback, {
11106
11508
  warning: `engine signaled weak relevance (${weakBingReason}). These results may be Bing's "popular sites" fallback rather than real matches. Consider rephrasing the query (shorter, fewer constraints), or use web_fetch against a known authoritative URL (e.g. baike.baidu.com, mfa.gov.cn, news.cctv.com).`
11107
11509
  });
11108
11510
  }
@@ -11308,7 +11710,7 @@ function parseGoogleResults(html, maxResults) {
11308
11710
  }
11309
11711
  async function fetchHtml(url) {
11310
11712
  const controller = new AbortController();
11311
- const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
11713
+ const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS2);
11312
11714
  try {
11313
11715
  const res = await fetch(url, {
11314
11716
  method: "GET",
@@ -11325,7 +11727,7 @@ async function fetchHtml(url) {
11325
11727
  return await res.text();
11326
11728
  } catch (err) {
11327
11729
  if (err instanceof Error && err.name === "AbortError") {
11328
- throw new NetworkError(`Request timed out after ${REQUEST_TIMEOUT_MS / 1e3}s.`, void 0, err);
11730
+ throw new NetworkError(`Request timed out after ${REQUEST_TIMEOUT_MS2 / 1e3}s.`, void 0, err);
11329
11731
  }
11330
11732
  throw err;
11331
11733
  } finally {
@@ -11338,7 +11740,31 @@ function stripTags2(html) {
11338
11740
  function decodeHtmlEntities(s) {
11339
11741
  return s.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&#x2F;/g, "/").replace(/&nbsp;/g, " ");
11340
11742
  }
11341
- function formatResults(query, engine, results, opts) {
11743
+ var DEEPSEEK_SUMMARY_MAX_CHARS = 600;
11744
+ function clipSummary(s) {
11745
+ const flat = s.replace(/\s+/g, " ").trim();
11746
+ const chars = [...flat];
11747
+ if (chars.length <= DEEPSEEK_SUMMARY_MAX_CHARS) return flat;
11748
+ return chars.slice(0, DEEPSEEK_SUMMARY_MAX_CHARS).join("") + "\u2026";
11749
+ }
11750
+ function formatDeepSeekResults(query, results) {
11751
+ const preamble = `Search results for "${query}" via deepseek (${results.length} results; relevance 0\u20133, \u22651.5 is high quality):
11752
+ `;
11753
+ const body = results.map((r, i) => {
11754
+ const meta = [
11755
+ r.siteName,
11756
+ r.date ? r.date.slice(0, 10) : null,
11757
+ r.rerankScore !== null ? `relevance ${r.rerankScore.toFixed(2)}` : null
11758
+ ].filter(Boolean).join(" \xB7 ");
11759
+ const text = clipSummary(r.summary || r.snippet) || "(no summary)";
11760
+ return `${i + 1}. **${r.title}**
11761
+ URL: ${r.url}${meta ? `
11762
+ ${meta}` : ""}
11763
+ ${text}`;
11764
+ }).join("\n\n");
11765
+ return preamble + "\n" + body;
11766
+ }
11767
+ function formatResults2(query, engine, results, opts) {
11342
11768
  let preamble = `Search results for "${query}" via ${engine} (${results.length} results):
11343
11769
  `;
11344
11770
  if (opts?.warning) {
@@ -11632,134 +12058,6 @@ function renderTodoList(todos) {
11632
12058
  console.log();
11633
12059
  }
11634
12060
 
11635
- // src/tools/builtin/google-search.ts
11636
- var GOOGLE_SEARCH_API = "https://www.googleapis.com/customsearch/v1";
11637
- var REQUEST_TIMEOUT_MS2 = 15e3;
11638
- var MAX_RESULTS2 = 10;
11639
- var DEFAULT_RESULTS2 = 5;
11640
- var googleSearchContext = {};
11641
- var googleSearchTool = {
11642
- definition: {
11643
- name: "google_search",
11644
- description: "\u26A0\uFE0F NOT RECOMMENDED \u2014 uses Google Custom Search API which requires a paid API key. API keys have leaked in the wild causing tens of thousands of USD in unauthorized charges. PREFER `web_search` (keyless Bing/Google scraping, zero billing risk). This tool is disabled by default; user must set config.googleSearch.enabled=true to use it.",
11645
- parameters: {
11646
- query: {
11647
- type: "string",
11648
- description: "The search query string.",
11649
- required: true
11650
- },
11651
- num_results: {
11652
- type: "number",
11653
- description: "Number of results to return (1-10, default 5).",
11654
- required: false
11655
- }
11656
- },
11657
- dangerous: false
11658
- },
11659
- async execute(args) {
11660
- const query = String(args["query"] ?? "").trim();
11661
- if (!query) throw new ToolError("google_search", "query parameter is required");
11662
- const enabledFlag = googleSearchContext.configManager?.get("googleSearch")?.enabled ?? false;
11663
- if (!enabledFlag) {
11664
- throw new ToolError(
11665
- "google_search",
11666
- '\u{1F6AB} google_search is disabled by default (v0.4.135+).\n\nWHY: Google Custom Search has no native daily-spend cap. Keys leak (search GitHub for "AIza" \u2014 thousands of public exposures), bots scrape them within hours, and the GCP bill lands days later. Multiple engineers have lost $10k+ USD this way.\n\nRECOMMENDED: use `web_search` instead \u2014 same use case, scrapes Bing/Google public pages, zero API key, zero billing risk.\n\nIF YOU INSIST: edit ~/.aicli/config.json and set `googleSearch.enabled = true`. ALSO go to GCP Console \u2192 Custom Search API \u2192 Quotas and set a hard daily request cap (e.g. 100/day) \u2014 that is the only real damage cap.'
11667
- );
11668
- }
11669
- const numResults = Math.min(
11670
- Math.max(Math.floor(Number(args["num_results"] ?? DEFAULT_RESULTS2)), 1),
11671
- MAX_RESULTS2
11672
- );
11673
- const { apiKey, cx } = resolveConfig();
11674
- const url = new URL(GOOGLE_SEARCH_API);
11675
- url.searchParams.set("key", apiKey);
11676
- url.searchParams.set("cx", cx);
11677
- url.searchParams.set("q", query);
11678
- url.searchParams.set("num", String(numResults));
11679
- const controller = new AbortController();
11680
- const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS2);
11681
- try {
11682
- const response = await fetch(url.toString(), {
11683
- method: "GET",
11684
- headers: {
11685
- "Accept": "application/json"
11686
- },
11687
- signal: controller.signal
11688
- });
11689
- if (!response.ok) {
11690
- const errorBody = await response.text().catch(() => "");
11691
- if (response.status === 403) {
11692
- throw new NetworkError(
11693
- `Google Search API 403 Forbidden \u2014 Invalid API Key or daily free quota exceeded (100/day).
11694
- Please check your API Key and Search Engine ID configuration.`,
11695
- 403
11696
- );
11697
- }
11698
- if (response.status === 429) {
11699
- throw new NetworkError("Google Search API 429 \u2014 Too many requests, please try again later.", 429);
11700
- }
11701
- throw new NetworkError(
11702
- `Google Search API error: HTTP ${response.status} ${response.statusText}
11703
- ${errorBody.slice(0, 500)}`,
11704
- response.status
11705
- );
11706
- }
11707
- const data = await response.json();
11708
- return formatResults2(query, data, numResults);
11709
- } catch (err) {
11710
- if (err instanceof Error && err.name === "AbortError") {
11711
- throw new NetworkError(`Google Search request timed out (${REQUEST_TIMEOUT_MS2 / 1e3}s). Please check your network or proxy configuration.`, void 0, err);
11712
- }
11713
- throw err;
11714
- } finally {
11715
- clearTimeout(timeout);
11716
- }
11717
- }
11718
- };
11719
- function resolveConfig() {
11720
- let apiKey;
11721
- let cx;
11722
- if (googleSearchContext.configManager) {
11723
- apiKey = googleSearchContext.configManager.getApiKey("google-search");
11724
- cx = EnvLoader.getGoogleSearchEngineId() ?? googleSearchContext.configManager.get("googleSearchEngineId");
11725
- } else {
11726
- apiKey = EnvLoader.getApiKey("google-search");
11727
- cx = EnvLoader.getGoogleSearchEngineId();
11728
- }
11729
- if (!apiKey) {
11730
- throw new ToolError(
11731
- "google_search",
11732
- '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'
11733
- );
11734
- }
11735
- if (!cx) {
11736
- throw new ToolError(
11737
- "google_search",
11738
- "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"
11739
- );
11740
- }
11741
- return { apiKey, cx };
11742
- }
11743
- function formatResults2(query, data, _requested) {
11744
- const items = data.items ?? [];
11745
- if (items.length === 0) {
11746
- const info2 = data.searchInformation;
11747
- return `No results found for: "${query}"` + (info2 ? ` (searched ${info2.formattedTotalResults ?? "0"} pages in ${info2.formattedSearchTime ?? "?"}s)` : "");
11748
- }
11749
- const info = data.searchInformation;
11750
- const header = `Search results for "${query}" (${items.length} of ~${info?.formattedTotalResults ?? "?"} results):
11751
- `;
11752
- const results = items.map((item, i) => {
11753
- const title = item.title ?? "Untitled";
11754
- const link = item.link ?? "";
11755
- const snippet = item.snippet ?? "";
11756
- return `${i + 1}. **${title}**
11757
- URL: ${link}
11758
- ${snippet}`;
11759
- });
11760
- return header + "\n" + results.join("\n\n");
11761
- }
11762
-
11763
12061
  // src/core/tool-capable-provider.ts
11764
12062
  function isToolCapableProvider(provider) {
11765
12063
  if (!provider) return false;
@@ -11816,7 +12114,7 @@ var HookEventNameSchema = z2.enum([
11816
12114
  var McpServerSchema = z2.object({
11817
12115
  command: z2.string(),
11818
12116
  args: z2.array(z2.string()).default([]),
11819
- env: z2.record(z2.string()).optional(),
12117
+ env: z2.record(z2.string(), z2.string()).optional(),
11820
12118
  timeout: z2.number().default(3e4)
11821
12119
  });
11822
12120
  var PluginManifestSchema = z2.object({
@@ -11826,10 +12124,10 @@ var PluginManifestSchema = z2.object({
11826
12124
  skills: z2.array(z2.string()).default([]),
11827
12125
  hooks: z2.object({
11828
12126
  enabled: z2.boolean().optional(),
11829
- events: z2.record(HookEventNameSchema, HookCommandSchema).default({})
11830
- }).default({ events: {} }),
12127
+ events: z2.partialRecord(HookEventNameSchema, HookCommandSchema).default({})
12128
+ }).prefault({ events: {} }),
11831
12129
  commands: z2.array(z2.string()).default([]),
11832
- mcpServers: z2.record(McpServerSchema).default({}),
12130
+ mcpServers: z2.record(z2.string(), McpServerSchema).default({}),
11833
12131
  agents: z2.array(z2.string()).default([]),
11834
12132
  permissionHints: z2.array(z2.string()).default([])
11835
12133
  }).strict();
@@ -12904,26 +13202,75 @@ var taskCreateTool = {
12904
13202
  ID: ${task.id}
12905
13203
  Command: ${command}
12906
13204
  Description: ${description}
12907
- Use task_list to check status, task_stop to terminate.`;
13205
+ Use task_list to check status (task_list with id + wait_seconds waits for it to finish \u2014 no bash sleep needed), task_stop to terminate.`;
12908
13206
  }
12909
13207
  };
12910
13208
 
12911
13209
  // src/tools/builtin/task-list.ts
13210
+ var MAX_TASK_WAIT_SECONDS = 300;
13211
+ var POLL_MS = 200;
13212
+ async function waitForTasks(id, seconds) {
13213
+ const isRunning = (taskId) => getTaskOutput(taskId)?.status === "running";
13214
+ const watched = id ? [id] : listTasks().filter((t) => t.status === "running").map((t) => t.id);
13215
+ if (!watched.some(isRunning)) return "nothing-running";
13216
+ const signal = currentAbortSignal();
13217
+ const deadline = Date.now() + seconds * 1e3;
13218
+ for (; ; ) {
13219
+ if (signal.aborted) return "interrupted";
13220
+ if (!watched.every(isRunning)) return "finished";
13221
+ const left = deadline - Date.now();
13222
+ if (left <= 0) return "timeout";
13223
+ await new Promise((resolve13) => {
13224
+ const timer = setTimeout(done, Math.min(POLL_MS, left));
13225
+ function done() {
13226
+ clearTimeout(timer);
13227
+ signal.removeEventListener("abort", done);
13228
+ resolve13();
13229
+ }
13230
+ signal.addEventListener("abort", done, { once: true });
13231
+ });
13232
+ }
13233
+ }
13234
+ function parseWaitSeconds(raw) {
13235
+ const n = Number(raw);
13236
+ if (raw === void 0 || raw === null || raw === "" || !Number.isFinite(n) || n <= 0) {
13237
+ return { seconds: 0, clamped: false };
13238
+ }
13239
+ return n > MAX_TASK_WAIT_SECONDS ? { seconds: MAX_TASK_WAIT_SECONDS, clamped: true } : { seconds: n, clamped: false };
13240
+ }
12912
13241
  var taskListTool = {
12913
13242
  definition: {
12914
13243
  name: "task_list",
12915
- description: `List all background tasks with their status. Provide an ID to get detailed output for a specific task.`,
13244
+ description: `List all background tasks with their status. Provide an ID to get detailed output for a specific task. To wait for a background task, pass wait_seconds (max ${MAX_TASK_WAIT_SECONDS}) instead of polling with bash sleep: the call returns as soon as the task (or, without an id, any running task) finishes.`,
12916
13245
  parameters: {
12917
13246
  id: {
12918
13247
  type: "string",
12919
13248
  description: "Optional: specific task ID to get detailed stdout/stderr output",
12920
13249
  required: false
13250
+ },
13251
+ wait_seconds: {
13252
+ type: "number",
13253
+ description: `Optional: wait up to this many seconds (max ${MAX_TASK_WAIT_SECONDS}) for the task with this id \u2014 or, without id, for any running task \u2014 to finish, then report. Returns early on completion.`,
13254
+ required: false
12921
13255
  }
12922
13256
  },
12923
13257
  dangerous: false
12924
13258
  },
12925
13259
  async execute(args) {
12926
13260
  const id = args["id"] ? String(args["id"]) : void 0;
13261
+ const { seconds, clamped } = parseWaitSeconds(args["wait_seconds"]);
13262
+ let note = "";
13263
+ if (seconds > 0) {
13264
+ const started = Date.now();
13265
+ const outcome = await waitForTasks(id, seconds);
13266
+ const waited = ((Date.now() - started) / 1e3).toFixed(1);
13267
+ if (outcome === "timeout") note = `(waited ${waited}s; still running \u2014 call again with wait_seconds to keep waiting)`;
13268
+ else if (outcome === "finished") note = `(waited ${waited}s; task finished)`;
13269
+ else if (outcome === "interrupted") note = `(wait interrupted by user after ${waited}s)`;
13270
+ if (clamped) note += `${note ? " " : ""}(wait_seconds capped at ${MAX_TASK_WAIT_SECONDS})`;
13271
+ }
13272
+ const withNote = (body) => note ? `${note}
13273
+ ${body}` : body;
12927
13274
  if (id) {
12928
13275
  const output = getTaskOutput(id);
12929
13276
  if (!output) return `Task not found: ${id}`;
@@ -12935,7 +13282,7 @@ ${output.stdout}`);
12935
13282
  \u2500\u2500 stderr \u2500\u2500
12936
13283
  ${output.stderr}`);
12937
13284
  if (!output.stdout && !output.stderr) parts.push("\n(no output yet)");
12938
- return parts.join("");
13285
+ return withNote(parts.join(""));
12939
13286
  }
12940
13287
  const all = listTasks();
12941
13288
  if (all.length === 0) return "No background tasks.";
@@ -12947,7 +13294,7 @@ ${output.stderr}`);
12947
13294
  lines.push(` [${t.id}] ${status} (${elapsed.toFixed(1)}s) \u2014 ${t.description}`);
12948
13295
  if (t.exitCode !== null) lines.push(` exit code: ${t.exitCode}`);
12949
13296
  }
12950
- return lines.join("\n");
13297
+ return withNote(lines.join("\n"));
12951
13298
  }
12952
13299
  };
12953
13300
 
@@ -17944,7 +18291,7 @@ Cache: write=${cacheCreate} read=${cacheRead}` : "";
17944
18291
  Cost: ${formatCost(cost)}` : "";
17945
18292
  let memoryLine = "";
17946
18293
  try {
17947
- const { countPendingMemories: countPendingMemories2 } = await import("./persistent-memory-WFGBTGWT.js");
18294
+ const { countPendingMemories: countPendingMemories2 } = await import("./persistent-memory-UZIZVHRT.js");
17948
18295
  const pending = countPendingMemories2(ctx.config.getConfigDir());
17949
18296
  if (pending > 0) memoryLine = `
17950
18297
  Memory: \u26A0 ${pending} pending approval \u2014 see the Memory panel or /memory`;
@@ -18932,7 +19279,7 @@ async function handleTest(args, ctx) {
18932
19279
  const isCommand = argStr.includes(" ") || /^(mvn|gradle|npm|pytest|cargo|go)\b/.test(argStr);
18933
19280
  testArgs = isCommand ? { command: argStr } : { filter: argStr };
18934
19281
  }
18935
- const runTests = ctx.runTests ?? (await import("./run-tests-TUTI7HIN.js")).executeTests;
19282
+ const runTests = ctx.runTests ?? (await import("./run-tests-UXEOFPAW.js")).executeTests;
18936
19283
  const report = await runTests(testArgs);
18937
19284
  ctx.send({ type: "info", message: report });
18938
19285
  } catch (err) {
@@ -19323,7 +19670,7 @@ async function handleMemory(args, ctx) {
19323
19670
  }
19324
19671
  if (intent.kind === "status") {
19325
19672
  const { getChatIndexStatus: getChatIndexStatus2 } = await import("./chat-index-RLVLQRRC.js");
19326
- const { listMemoryEntries: listMemoryEntries2, isMemoryExpired: isMemoryExpired2, formatMemoryForPrompt: formatMemoryForPrompt2 } = await import("./persistent-memory-WFGBTGWT.js");
19673
+ const { listMemoryEntries: listMemoryEntries2, isMemoryExpired: isMemoryExpired2, formatMemoryForPrompt: formatMemoryForPrompt2 } = await import("./persistent-memory-UZIZVHRT.js");
19327
19674
  const memories = listMemoryEntries2(configDir, { includeExpired: true, includeRejected: true });
19328
19675
  const injected = formatMemoryForPrompt2(configDir, process.cwd(), ctx.config.get("memory")?.maxChars);
19329
19676
  const view = buildMemoryStatusView({
@@ -19346,7 +19693,7 @@ async function handleMemory(args, ctx) {
19346
19693
  return;
19347
19694
  }
19348
19695
  if (intent.kind === "recall") {
19349
- const { searchPersistentMemories: searchPersistentMemories2, touchMemoryReferences: touchMemoryReferences2 } = await import("./persistent-memory-WFGBTGWT.js");
19696
+ const { searchPersistentMemories: searchPersistentMemories2, touchMemoryReferences: touchMemoryReferences2 } = await import("./persistent-memory-UZIZVHRT.js");
19350
19697
  const { searchChatMemory: searchChatMemory2 } = await import("./chat-index-RLVLQRRC.js");
19351
19698
  const persistentHits = searchPersistentMemories2(configDir, intent.query, process.cwd(), 3);
19352
19699
  if (persistentHits.length > 0) {
@@ -19467,7 +19814,7 @@ async function handleMemory(args, ctx) {
19467
19814
  ctx.handleMemoryManage(intent.action, args[1], intent.action === "expire" ? args[2] : void 0);
19468
19815
  } else if (intent.kind === "export") {
19469
19816
  if (args[1] === "md") {
19470
- const { exportMemoryEntries: exportMemoryEntries2 } = await import("./persistent-memory-WFGBTGWT.js");
19817
+ const { exportMemoryEntries: exportMemoryEntries2 } = await import("./persistent-memory-UZIZVHRT.js");
19471
19818
  ctx.send({
19472
19819
  type: "export_data",
19473
19820
  format: "md",
@@ -20473,7 +20820,7 @@ No tools match "${filter}".
20473
20820
  const { join: join27 } = await import("path");
20474
20821
  const { existsSync: existsSync33 } = await import("fs");
20475
20822
  const { getGitRoot: getGitRoot2 } = await import("./git-context-VNCA2GU7.js");
20476
- const { MCP_PROJECT_CONFIG_NAME: MCP_PROJECT_CONFIG_NAME2 } = await import("./constants-WJAUJAQQ.js");
20823
+ const { MCP_PROJECT_CONFIG_NAME: MCP_PROJECT_CONFIG_NAME2 } = await import("./constants-JZH2QKMQ.js");
20477
20824
  const { approveProject, hashMcpFile } = await import("./project-trust-6CJB5ZMX.js");
20478
20825
  const cwd = process.cwd();
20479
20826
  const projectRoot = getGitRoot2(cwd) ?? cwd;
@@ -21392,7 +21739,7 @@ function createReviewCommands() {
21392
21739
  usage: "/test [command|filter]",
21393
21740
  async execute(args, ctx) {
21394
21741
  try {
21395
- const { executeTests } = await import("./run-tests-TUTI7HIN.js");
21742
+ const { executeTests } = await import("./run-tests-UXEOFPAW.js");
21396
21743
  const argStr = args.join(" ").trim();
21397
21744
  let testArgs = {};
21398
21745
  if (argStr) {
@@ -23616,7 +23963,8 @@ var SessionHandler = class _SessionHandler {
23616
23963
  return;
23617
23964
  }
23618
23965
  case "interjection":
23619
- this.userInterjection = msg.content;
23966
+ this.userInterjection = this.userInterjection ? `${this.userInterjection}
23967
+ ${msg.content}` : msg.content;
23620
23968
  return;
23621
23969
  }
23622
23970
  }
@@ -24084,7 +24432,6 @@ ${systemPromptVolatile}` : systemPrompt;
24084
24432
  if (!this.userInterjection) return null;
24085
24433
  const msg = this.userInterjection;
24086
24434
  this.userInterjection = null;
24087
- this.send({ type: "info", message: `\u26A1 Interjection: "${msg}"` });
24088
24435
  return msg;
24089
24436
  },
24090
24437
  onRoundStart: (round, total) => {
@@ -24943,17 +25290,6 @@ Backup saved: ${result.backupPath}` : "";
24943
25290
  }
24944
25291
  };
24945
25292
 
24946
- // src/core/proxy.ts
24947
- async function setupProxy(configProxy) {
24948
- const proxyUrl = process.env.HTTPS_PROXY ?? process.env.HTTP_PROXY ?? process.env.https_proxy ?? process.env.http_proxy ?? configProxy;
24949
- if (!proxyUrl) return;
24950
- try {
24951
- const { ProxyAgent, setGlobalDispatcher } = await import("undici");
24952
- setGlobalDispatcher(new ProxyAgent({ uri: proxyUrl }));
24953
- } catch {
24954
- }
24955
- }
24956
-
24957
25293
  // src/web/auth.ts
24958
25294
  import { existsSync as existsSync31, readFileSync as readFileSync22, mkdirSync as mkdirSync16, readdirSync as readdirSync15, openSync, closeSync, unlinkSync as unlinkSync8 } from "fs";
24959
25295
  import { isAbsolute as isAbsolute3, join as join25, relative as relative5 } from "path";
@@ -25394,7 +25730,8 @@ async function startWebServer(options = {}) {
25394
25730
  (id) => ({
25395
25731
  baseUrl: config.get("customBaseUrls")[id],
25396
25732
  timeout: config.get("timeouts")[id],
25397
- proxy: config.get("proxy")
25733
+ proxy: config.get("proxy"),
25734
+ noProxy: config.get("noProxy")
25398
25735
  }),
25399
25736
  config.get("customProviders"),
25400
25737
  config.getConfigDir()