jinzd-ai-cli 0.4.334 → 0.4.337

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-GUQLUOXL.js} +2 -2
  2. package/dist/{chunk-KSZEQADY.js → chunk-56IOLQ5E.js} +1 -1
  3. package/dist/{chunk-DPUZ22D7.js → chunk-5WUOBFYG.js} +23 -23
  4. package/dist/{chunk-W4P2KJZN.js → chunk-6SFS6IJH.js} +67 -0
  5. package/dist/{chunk-FHHBPCCZ.js → chunk-7TU4KHMK.js} +1 -1
  6. package/dist/{chunk-YY4QFBBZ.js → chunk-DQBWJG32.js} +422 -236
  7. package/dist/{chunk-RQOOEXDU.js → chunk-EGRKGP4P.js} +1 -1
  8. package/dist/{chunk-XMHVHS2X.js → chunk-HXENKVWP.js} +4 -4
  9. package/dist/{chunk-7GGWVBHG.js → chunk-MAUNCANT.js} +1 -1
  10. package/dist/{chunk-EOYIIGBJ.js → chunk-PWXVC6UZ.js} +5 -5
  11. package/dist/{chunk-YVEBRHSV.js → chunk-RVB4GCSQ.js} +4 -4
  12. package/dist/{chunk-27RTBMY7.js → chunk-SSQARJMS.js} +2 -2
  13. package/dist/{chunk-DX4Y4CRH.js → chunk-VHLZ2ZUQ.js} +7 -7
  14. package/dist/{chunk-SZQ2C3HH.js → chunk-WL4NXDYT.js} +1 -1
  15. package/dist/{chunk-ZK5VCQD2.js → chunk-XF2F2V5Y.js} +1 -1
  16. package/dist/{chunk-YTAYWGS4.js → chunk-YYO6TMOA.js} +1 -1
  17. package/dist/{ci-S7WXRTZ6.js → ci-FSMBWLMM.js} +6 -6
  18. package/dist/{ci-format-TPCL2KWQ.js → ci-format-BONCHM6H.js} +2 -2
  19. package/dist/{constants-WJAUJAQQ.js → constants-ARJE6QPN.js} +1 -1
  20. package/dist/{constants-5VQMHBFU.js → constants-RMAQCGVM.js} +1 -1
  21. package/dist/{doctor-cli-DYQLNMPT.js → doctor-cli-T7D4DPWU.js} +6 -6
  22. package/dist/electron-server.js +519 -266
  23. package/dist/{hub-A7P2ZBYB.js → hub-KCYDY3U6.js} +1 -1
  24. package/dist/index.js +19 -19
  25. package/dist/{persistent-memory-SKGOCEGT.js → persistent-memory-3RG5Z6WK.js} +2 -2
  26. package/dist/{persistent-memory-WFGBTGWT.js → persistent-memory-ZO6NGZIV.js} +2 -2
  27. package/dist/{pr-IPXLVP2K.js → pr-W2PPGBYA.js} +6 -6
  28. package/dist/{run-tests-TUTI7HIN.js → run-tests-72HGJXD4.js} +2 -2
  29. package/dist/{run-tests-BG6SGRWQ.js → run-tests-BWJEL7PA.js} +2 -2
  30. package/dist/{server-4QJPQBEW.js → server-WTLNIO23.js} +6 -6
  31. package/dist/{server-S6OROC7S.js → server-Y3N6RMIN.js} +15 -15
  32. package/dist/{task-orchestrator-6JG5GAXU.js → task-orchestrator-QYCG7NVG.js} +6 -6
  33. package/dist/{usage-LPQQ6NGG.js → usage-5L4ZQENO.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-YYO6TMOA.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-MAUNCANT.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-WL4NXDYT.js";
127
127
 
128
128
  // src/web/access.ts
129
129
  import { randomBytes, timingSafeEqual } from "crypto";
@@ -262,24 +262,24 @@ 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 覆盖
@@ -287,7 +287,7 @@ var ConfigSchema = z.object({
287
287
  // 自定义 Provider 列表(OpenAI 兼容接口,无需改代码)
288
288
  customProviders: z.array(CustomProviderConfigSchema).default([]),
289
289
  // 按模型 ID 存储的推理参数(key 为模型 ID,如 "deepseek-chat")
290
- modelParams: z.record(ModelParamsSchema).default({}),
290
+ modelParams: z.record(z.string(), ModelParamsSchema).default({}),
291
291
  ui: z.object({
292
292
  streaming: z.boolean().default(true),
293
293
  markdownRendering: z.boolean().default(true),
@@ -321,12 +321,12 @@ var ConfigSchema = z.object({
321
321
  toolResult: z.string().optional(),
322
322
  heading: z.string().optional()
323
323
  }).optional()
324
- }).default({}),
324
+ }).prefault({}),
325
325
  session: z.object({
326
326
  autoSave: z.boolean().default(true),
327
327
  maxHistoryDays: z.number().default(30),
328
328
  systemPrompt: z.string().max(1e5).optional()
329
- }).default({}),
329
+ }).prefault({}),
330
330
  // 项目上下文文件配置
331
331
  // 启动时自动读取并注入 system prompt,类似 Claude Code 的 CLAUDE.md / Codex AGENTS.md 机制
332
332
  // 默认按顺序查找:AICLI.override.md → AGENTS.override.md → AICLI.md → CLAUDE.md → AGENTS.md
@@ -336,13 +336,13 @@ var ConfigSchema = z.object({
336
336
  projectDocFallbackFilenames: z.array(z.string().max(500)).max(20).default([]),
337
337
  projectDocMaxBytes: z.number().int().positive().default(32 * 1024),
338
338
  showLoadedContextSources: z.boolean().default(false)
339
- }).default({}),
339
+ }).prefault({}),
340
340
  // 持久记忆注入配置(MEM-005)
341
341
  // maxChars: 注入 system prompt 的字符预算,超出丢最老条目并显式标注截断。
342
342
  // 注意 CJK ≈ 1 字符/token——中文记忆的 token 开销约等于字符数。
343
343
  memory: z.object({
344
344
  maxChars: z.number().int().positive().max(2e5).default(1e4)
345
- }).default({}),
345
+ }).prefault({}),
346
346
  // Google Custom Search API 的 Search Engine ID (cx 参数)
347
347
  // API Key 通过 apiKeys['google-search'] 或 AICLI_API_KEY_GOOGLESEARCH 环境变量配置
348
348
  // CX 也可通过 AICLI_GOOGLE_CX 环境变量覆盖
@@ -354,7 +354,7 @@ var ConfigSchema = z.object({
354
354
  // 仍想用 API 版?把 enabled 设为 true,并务必到 GCP Console 把 Custom Search Quota 设硬上限。
355
355
  googleSearch: z.object({
356
356
  enabled: z.boolean().default(false)
357
- }).default({ enabled: false }),
357
+ }).prefault({ enabled: false }),
358
358
  // MCP (Model Context Protocol) 全局开关(v0.4.135+)
359
359
  // false 时启动跳过 connectAll(),所有 MCP 服务器都不连,节省冷启动时间 + 工具定义 token。
360
360
  // 对应你的 skills(whitelist 不依赖 MCP 工具时)完全不受影响。
@@ -363,10 +363,10 @@ var ConfigSchema = z.object({
363
363
  // 声明外部 MCP 服务器,启动时自动连接、发现工具并注册
364
364
  // 配置格式兼容 Claude Desktop(command + args + env)
365
365
  // 示例:{ "filesystem": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path"] } }
366
- mcpServers: z.record(z.object({
366
+ mcpServers: z.record(z.string(), z.object({
367
367
  command: z.string().max(500),
368
368
  args: z.array(z.string()).max(100).default([]),
369
- env: z.record(z.string()).optional(),
369
+ env: z.record(z.string(), z.string()).optional(),
370
370
  timeout: z.number().int().min(100).max(3e5).default(3e4)
371
371
  })).default({}),
372
372
  // Hooks 生命周期(v0.4.212+):兼容旧 pre/post 字符串,同时支持 events.<EventName> 结构化 JSON hook。
@@ -374,7 +374,7 @@ var ConfigSchema = z.object({
374
374
  enabled: z.boolean().default(true),
375
375
  preToolExecution: z.string().optional(),
376
376
  postToolExecution: z.string().optional(),
377
- events: z.record(z.union([
377
+ events: z.record(z.string(), z.union([
378
378
  z.string(),
379
379
  z.object({
380
380
  command: z.string(),
@@ -412,12 +412,12 @@ var ConfigSchema = z.object({
412
412
  google_search: z.enum(["allow", "confirm", "deny"]).optional(),
413
413
  shell: z.enum(["allow", "confirm", "deny"]).optional(),
414
414
  mcp: z.enum(["allow", "confirm", "deny"]).optional()
415
- }).default({})
416
- }).default({ enabled: false, defaultAction: "confirm", allowDomains: [], denyDomains: [], allowPorts: [], denyPorts: [], allowPrivateNetwork: false, tools: {} }),
415
+ }).prefault({})
416
+ }).prefault({ enabled: false, defaultAction: "confirm", allowDomains: [], denyDomains: [], allowPorts: [], denyPorts: [], allowPrivateNetwork: false, tools: {} }),
417
417
  // 权限 Profile(v0.4.211+):legacy 保持旧行为;其余 profile 提供更清晰的安全边界。
418
418
  defaultPermissionProfile: z.enum(["legacy", "read-only", "workspace-write", "danger-full-access"]).default("legacy"),
419
419
  allowedPermissionProfiles: z.array(z.string().max(50)).max(20).default(["legacy", "read-only", "workspace-write", "danger-full-access"]),
420
- permissionProfiles: z.record(z.object({
420
+ permissionProfiles: z.record(z.string(), z.object({
421
421
  extends: z.enum(["legacy", "read-only", "workspace-write", "danger-full-access"]).optional(),
422
422
  defaultAction: z.enum(["auto-approve", "deny", "confirm"]).optional(),
423
423
  workspaceRoots: z.array(z.string()).optional(),
@@ -492,7 +492,7 @@ var ConfigSchema = z.object({
492
492
  redactOnSend: z.boolean().default(false),
493
493
  mode: z.enum(["default", "strict", "off"]).default("default"),
494
494
  customPatterns: z.array(z.string().max(200)).max(50).default([])
495
- }).default({}),
495
+ }).prefault({}),
496
496
  // 智能模型路由(v0.4.68+)
497
497
  // 按用户每轮输入的内容/标签/长度动态选择模型,在同一 provider 内切换,
498
498
  // 例:短问题走 haiku(省钱),planning 走 opus(质量)。
@@ -512,7 +512,7 @@ var ConfigSchema = z.object({
512
512
  name: z.string().max(100).optional()
513
513
  })).default([]),
514
514
  fallback: z.string().optional()
515
- }).default({ enabled: false, rules: [] }),
515
+ }).prefault({ enabled: false, rules: [] }),
516
516
  // Provider 容错(v0.4.144+):网络抖动 / 5xx / 429 时,先在当前 provider 上做指数退避重试,
517
517
  // 仍失败则按顺序尝试 chain 里的 fallback provider。auth / 400 / 422 等"硬失败"直接跳过同
518
518
  // provider 重试,但仍会尝试 chain(不同 key/schema 可能能成功)。
@@ -532,7 +532,7 @@ var ConfigSchema = z.object({
532
532
  provider: z.string().max(100),
533
533
  model: z.string().optional()
534
534
  })).default([])
535
- }).default({ enabled: false, retries: 2, initialBackoffMs: 500, maxBackoffMs: 8e3, chain: [] })
535
+ }).prefault({ enabled: false, retries: 2, initialBackoffMs: 500, maxBackoffMs: 8e3, chain: [] })
536
536
  });
537
537
 
538
538
  // src/config/env-loader.ts
@@ -806,7 +806,7 @@ ${err}`
806
806
  const draft = { ...this.config, [key]: value };
807
807
  const result = ConfigSchema.safeParse(draft);
808
808
  if (!result.success) {
809
- const firstErr = result.error.errors[0];
809
+ const firstErr = result.error.issues[0];
810
810
  throw new ConfigError(
811
811
  `Invalid transient config value for "${String(key)}": ${firstErr?.message ?? "validation failed"}`
812
812
  );
@@ -858,7 +858,7 @@ ${err}`
858
858
  current[keys[keys.length - 1]] = value;
859
859
  const result = ConfigSchema.safeParse(draft);
860
860
  if (!result.success) {
861
- const firstErr = result.error.errors[0];
861
+ const firstErr = result.error.issues[0];
862
862
  throw new ConfigError(`Invalid config value for "${path3}": ${firstErr?.message ?? "validation failed"}`);
863
863
  }
864
864
  this.config = result.data;
@@ -1479,6 +1479,72 @@ var BaseProvider = class {
1479
1479
  function isFileWriteTool(name) {
1480
1480
  return name === "write_file" || name === "edit_file" || name === "notebook_edit";
1481
1481
  }
1482
+ var READ_ONLY_CMDLETS = new Set([
1483
+ "Get-ChildItem",
1484
+ "gci",
1485
+ "Get-Content",
1486
+ "gc",
1487
+ "Get-Item",
1488
+ "gi",
1489
+ "Get-ItemProperty",
1490
+ "gp",
1491
+ "Get-ItemPropertyValue",
1492
+ "Get-Location",
1493
+ "gl",
1494
+ "Test-Path",
1495
+ "Resolve-Path",
1496
+ "Split-Path",
1497
+ "Join-Path",
1498
+ "Get-Command",
1499
+ "gcm",
1500
+ "Get-Service",
1501
+ "gsv",
1502
+ "Get-Process",
1503
+ "gps",
1504
+ "Get-Date",
1505
+ "Get-Alias",
1506
+ "Get-Module",
1507
+ "Get-PSDrive",
1508
+ "Get-Acl",
1509
+ "Get-FileHash",
1510
+ "Get-CimInstance",
1511
+ "Get-NetTCPConnection",
1512
+ "Get-NetIPAddress",
1513
+ "Get-WinEvent",
1514
+ "Get-ComputerInfo",
1515
+ "Get-Culture",
1516
+ "Get-ExecutionPolicy",
1517
+ "Get-Unique",
1518
+ "Select-Object",
1519
+ "select",
1520
+ "Where-Object",
1521
+ "Sort-Object",
1522
+ "Measure-Object",
1523
+ "Group-Object",
1524
+ "Compare-Object",
1525
+ "Select-String",
1526
+ "sls",
1527
+ "Format-Table",
1528
+ "ft",
1529
+ "Format-List",
1530
+ "fl",
1531
+ "Format-Wide",
1532
+ "Out-String",
1533
+ "Write-Output",
1534
+ "Write-Host",
1535
+ "ConvertTo-Json",
1536
+ "ConvertFrom-Json",
1537
+ "ConvertTo-Csv",
1538
+ "ConvertFrom-Csv"
1539
+ ].map((name) => name.toLowerCase()));
1540
+ function isReadOnlyCmdletChain(cmd) {
1541
+ const normalized = cmd.trim().replace(/\s2>\s*\$null\b/gi, " ").replace(/\s2>&1\b/g, " ").replace(/\$(?:env:\w+|null|true|false)\b/gi, "X");
1542
+ if (!normalized || /[&`$<>\n\r(){}]|::/.test(normalized)) return false;
1543
+ return normalized.split(/[;|]/).every((segment) => {
1544
+ const name = segment.trim().split(/\s+/)[0].toLowerCase();
1545
+ return READ_ONLY_CMDLETS.has(name);
1546
+ });
1547
+ }
1482
1548
  function getDangerLevel(toolName, args) {
1483
1549
  if (toolName.startsWith("mcp__")) {
1484
1550
  const rawName = toolName.split("__").pop() ?? "";
@@ -1508,6 +1574,7 @@ function getDangerLevel(toolName, args) {
1508
1574
  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
1575
  if (/\bcmd(\.exe)?\s+\/[ck]\b/i.test(cmd)) return "write";
1510
1576
  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";
1577
+ if (isReadOnlyCmdletChain(cmd)) return "safe";
1511
1578
  return "write";
1512
1579
  }
1513
1580
  if (toolName === "run_tests") return "write";
@@ -8361,6 +8428,14 @@ function domainMatches(pattern, host) {
8361
8428
  const h = host.trim().toLowerCase().replace(/:\d+$/, "");
8362
8429
  return h === p || h.endsWith(`.${p}`);
8363
8430
  }
8431
+ function isHostAllowedByNetworkPolicy(host, policy) {
8432
+ if (!policy?.enabled) return true;
8433
+ if (policy.denyDomains?.some((pattern) => domainMatches(pattern, host))) return false;
8434
+ if (policy.allowDomains && policy.allowDomains.length > 0) {
8435
+ return policy.allowDomains.some((pattern) => domainMatches(pattern, host));
8436
+ }
8437
+ return true;
8438
+ }
8364
8439
  function isPrivateNetworkHost(host) {
8365
8440
  const h = host.toLowerCase().replace(/^\[|\]$/g, "");
8366
8441
  if (h === "" || h === "localhost") return true;
@@ -10720,6 +10795,231 @@ ${stderr}`);
10720
10795
  import { promises as dnsPromises } from "dns";
10721
10796
  import net from "net";
10722
10797
  import { Agent } from "undici";
10798
+
10799
+ // src/tools/builtin/google-search.ts
10800
+ var GOOGLE_SEARCH_API = "https://www.googleapis.com/customsearch/v1";
10801
+ var REQUEST_TIMEOUT_MS = 15e3;
10802
+ var MAX_RESULTS = 10;
10803
+ var DEFAULT_RESULTS = 5;
10804
+ var googleSearchContext = {};
10805
+ var googleSearchTool = {
10806
+ definition: {
10807
+ name: "google_search",
10808
+ 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.",
10809
+ parameters: {
10810
+ query: {
10811
+ type: "string",
10812
+ description: "The search query string.",
10813
+ required: true
10814
+ },
10815
+ num_results: {
10816
+ type: "number",
10817
+ description: "Number of results to return (1-10, default 5).",
10818
+ required: false
10819
+ }
10820
+ },
10821
+ dangerous: false
10822
+ },
10823
+ async execute(args) {
10824
+ const query = String(args["query"] ?? "").trim();
10825
+ if (!query) throw new ToolError("google_search", "query parameter is required");
10826
+ const enabledFlag = googleSearchContext.configManager?.get("googleSearch")?.enabled ?? false;
10827
+ if (!enabledFlag) {
10828
+ throw new ToolError(
10829
+ "google_search",
10830
+ '\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.'
10831
+ );
10832
+ }
10833
+ const numResults = Math.min(
10834
+ Math.max(Math.floor(Number(args["num_results"] ?? DEFAULT_RESULTS)), 1),
10835
+ MAX_RESULTS
10836
+ );
10837
+ const { apiKey, cx } = resolveConfig();
10838
+ const url = new URL(GOOGLE_SEARCH_API);
10839
+ url.searchParams.set("key", apiKey);
10840
+ url.searchParams.set("cx", cx);
10841
+ url.searchParams.set("q", query);
10842
+ url.searchParams.set("num", String(numResults));
10843
+ const controller = new AbortController();
10844
+ const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
10845
+ try {
10846
+ const response = await fetch(url.toString(), {
10847
+ method: "GET",
10848
+ headers: {
10849
+ "Accept": "application/json"
10850
+ },
10851
+ signal: controller.signal
10852
+ });
10853
+ if (!response.ok) {
10854
+ const errorBody = await response.text().catch(() => "");
10855
+ if (response.status === 403) {
10856
+ throw new NetworkError(
10857
+ `Google Search API 403 Forbidden \u2014 Invalid API Key or daily free quota exceeded (100/day).
10858
+ Please check your API Key and Search Engine ID configuration.`,
10859
+ 403
10860
+ );
10861
+ }
10862
+ if (response.status === 429) {
10863
+ throw new NetworkError("Google Search API 429 \u2014 Too many requests, please try again later.", 429);
10864
+ }
10865
+ throw new NetworkError(
10866
+ `Google Search API error: HTTP ${response.status} ${response.statusText}
10867
+ ${errorBody.slice(0, 500)}`,
10868
+ response.status
10869
+ );
10870
+ }
10871
+ const data = await response.json();
10872
+ return formatResults(query, data, numResults);
10873
+ } catch (err) {
10874
+ if (err instanceof Error && err.name === "AbortError") {
10875
+ throw new NetworkError(`Google Search request timed out (${REQUEST_TIMEOUT_MS / 1e3}s). Please check your network or proxy configuration.`, void 0, err);
10876
+ }
10877
+ throw err;
10878
+ } finally {
10879
+ clearTimeout(timeout);
10880
+ }
10881
+ }
10882
+ };
10883
+ function resolveConfig() {
10884
+ let apiKey;
10885
+ let cx;
10886
+ if (googleSearchContext.configManager) {
10887
+ apiKey = googleSearchContext.configManager.getApiKey("google-search");
10888
+ cx = EnvLoader.getGoogleSearchEngineId() ?? googleSearchContext.configManager.get("googleSearchEngineId");
10889
+ } else {
10890
+ apiKey = EnvLoader.getApiKey("google-search");
10891
+ cx = EnvLoader.getGoogleSearchEngineId();
10892
+ }
10893
+ if (!apiKey) {
10894
+ throw new ToolError(
10895
+ "google_search",
10896
+ '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'
10897
+ );
10898
+ }
10899
+ if (!cx) {
10900
+ throw new ToolError(
10901
+ "google_search",
10902
+ "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"
10903
+ );
10904
+ }
10905
+ return { apiKey, cx };
10906
+ }
10907
+ function formatResults(query, data, _requested) {
10908
+ const items = data.items ?? [];
10909
+ if (items.length === 0) {
10910
+ const info2 = data.searchInformation;
10911
+ return `No results found for: "${query}"` + (info2 ? ` (searched ${info2.formattedTotalResults ?? "0"} pages in ${info2.formattedSearchTime ?? "?"}s)` : "");
10912
+ }
10913
+ const info = data.searchInformation;
10914
+ const header = `Search results for "${query}" (${items.length} of ~${info?.formattedTotalResults ?? "?"} results):
10915
+ `;
10916
+ const results = items.map((item, i) => {
10917
+ const title = item.title ?? "Untitled";
10918
+ const link = item.link ?? "";
10919
+ const snippet = item.snippet ?? "";
10920
+ return `${i + 1}. **${title}**
10921
+ URL: ${link}
10922
+ ${snippet}`;
10923
+ });
10924
+ return header + "\n" + results.join("\n\n");
10925
+ }
10926
+
10927
+ // src/tools/builtin/deepseek-web-api.ts
10928
+ var DEEPSEEK_TOOLS_HOST = "api.deepseek.com";
10929
+ var BASE_URL = `https://${DEEPSEEK_TOOLS_HOST}/v1/tools`;
10930
+ var SEARCH_TIMEOUT_MS = 2e4;
10931
+ var FETCH_TIMEOUT_MS = 3e4;
10932
+ var DEEPSEEK_MAX_QUERY_CHARS = 128;
10933
+ function resolveDeepSeekWebKey(configManager = googleSearchContext.configManager) {
10934
+ const key = configManager ? configManager.getApiKey("deepseek") : EnvLoader.getApiKey("deepseek");
10935
+ if (!key || !key.trim()) return void 0;
10936
+ const policy = configManager?.get("networkPolicy");
10937
+ if (!isHostAllowedByNetworkPolicy(DEEPSEEK_TOOLS_HOST, policy)) return void 0;
10938
+ return key.trim();
10939
+ }
10940
+ async function deepseekSearch(apiKey, query, count) {
10941
+ const data = await postJson(apiKey, "search", { query, count }, SEARCH_TIMEOUT_MS);
10942
+ return parseDeepSeekSearchResponse(data);
10943
+ }
10944
+ async function deepseekFetch(apiKey, url) {
10945
+ const data = await postJson(apiKey, "fetch", { url }, FETCH_TIMEOUT_MS);
10946
+ const content = typeof data?.content === "string" ? data.content : "";
10947
+ if (!content.trim()) throw new NetworkError("DeepSeek Fetch returned no content.");
10948
+ return { title: typeof data.title === "string" ? data.title : "", content };
10949
+ }
10950
+ function parseDeepSeekSearchResponse(data) {
10951
+ const raw = data?.results;
10952
+ if (!Array.isArray(raw)) {
10953
+ throw new NetworkError("DeepSeek Search returned an unexpected response (no results array).");
10954
+ }
10955
+ const out = [];
10956
+ for (const item of raw) {
10957
+ if (!item || typeof item !== "object") continue;
10958
+ const r = item;
10959
+ const url = typeof r["url"] === "string" ? r["url"] : "";
10960
+ const title = typeof r["title"] === "string" ? r["title"].trim() : "";
10961
+ if (!url || !title) continue;
10962
+ out.push({
10963
+ title,
10964
+ url,
10965
+ snippet: typeof r["snippet"] === "string" ? r["snippet"].trim() : "",
10966
+ summary: typeof r["summary"] === "string" ? r["summary"].trim() : "",
10967
+ siteName: typeof r["site_name"] === "string" && r["site_name"] ? r["site_name"] : null,
10968
+ date: typeof r["date"] === "string" && r["date"] ? r["date"] : null,
10969
+ rerankScore: typeof r["rerank_score"] === "number" ? r["rerank_score"] : null,
10970
+ authority: typeof r["authority"] === "number" ? r["authority"] : null
10971
+ });
10972
+ }
10973
+ return out;
10974
+ }
10975
+ async function postJson(apiKey, endpoint, body, timeoutMs) {
10976
+ const controller = new AbortController();
10977
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
10978
+ try {
10979
+ const res = await fetch(`${BASE_URL}/${endpoint}`, {
10980
+ method: "POST",
10981
+ headers: {
10982
+ Authorization: `Bearer ${apiKey}`,
10983
+ "Content-Type": "application/json",
10984
+ Accept: "application/json"
10985
+ },
10986
+ body: JSON.stringify(body),
10987
+ signal: controller.signal
10988
+ });
10989
+ const text = await res.text();
10990
+ if (!res.ok) {
10991
+ throw new NetworkError(
10992
+ `DeepSeek ${endpoint} HTTP ${res.status}: ${describeDeepSeekError(res.status, text)}`,
10993
+ res.status
10994
+ );
10995
+ }
10996
+ try {
10997
+ return JSON.parse(text);
10998
+ } catch {
10999
+ throw new NetworkError(`DeepSeek ${endpoint} returned non-JSON response.`);
11000
+ }
11001
+ } catch (err) {
11002
+ if (err instanceof Error && err.name === "AbortError") {
11003
+ throw new NetworkError(`DeepSeek ${endpoint} timed out after ${timeoutMs / 1e3}s.`, void 0, err);
11004
+ }
11005
+ throw err;
11006
+ } finally {
11007
+ clearTimeout(timer);
11008
+ }
11009
+ }
11010
+ function describeDeepSeekError(status, bodyText) {
11011
+ let message = "";
11012
+ try {
11013
+ const parsed = JSON.parse(bodyText);
11014
+ if (typeof parsed?.error?.message === "string") message = parsed.error.message;
11015
+ } catch {
11016
+ }
11017
+ if (!message) message = bodyText.trim().slice(0, 200) || "(empty body)";
11018
+ 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" : "";
11019
+ return message + hint;
11020
+ }
11021
+
11022
+ // src/tools/builtin/web-fetch.ts
10723
11023
  function htmlToText(html) {
10724
11024
  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
11025
  const HTML_REGEX_LIMIT = 2e5;
@@ -10892,7 +11192,7 @@ function ssrfSafeDispatcher() {
10892
11192
  var webFetchTool = {
10893
11193
  definition: {
10894
11194
  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.",
11195
+ 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
11196
  parameters: {
10897
11197
  url: {
10898
11198
  type: "string",
@@ -10922,94 +11222,26 @@ var webFetchTool = {
10922
11222
  if (e.message.startsWith("Blocked:")) throw e;
10923
11223
  throw new NetworkError(`Invalid URL: "${url}"`);
10924
11224
  }
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();
11225
+ let text;
10937
11226
  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);
11227
+ text = await fetchDirect(url);
10979
11228
  } 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
- );
11229
+ const msg = err instanceof Error ? err.message : String(err);
11230
+ if (msg.startsWith("Blocked:")) throw err;
11231
+ const key = resolveDeepSeekWebKey();
11232
+ if (!key) throw err;
11233
+ try {
11234
+ const ds = await deepseekFetch(key, url);
11235
+ text = [
11236
+ ds.title ? `# ${ds.title}` : "",
11237
+ `Source: ${url} (via DeepSeek Fetch \u2014 direct request failed: ${msg.split("\n")[0]})`,
11238
+ ""
11239
+ ].filter(Boolean).join("\n") + "\n\n" + ds.content;
11240
+ } catch (dsErr) {
11241
+ const dsMsg = dsErr instanceof Error ? dsErr.message : String(dsErr);
11242
+ throw new NetworkError(`${msg}
11243
+ DeepSeek Fetch fallback also failed: ${dsMsg}`, void 0, err);
10993
11244
  }
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
11245
  }
11014
11246
  if (selector) {
11015
11247
  const lower = selector.toLowerCase();
@@ -11034,16 +11266,132 @@ var webFetchTool = {
11034
11266
  return text;
11035
11267
  }
11036
11268
  };
11269
+ async function fetchDirect(url) {
11270
+ const controller = new AbortController();
11271
+ const timeoutId = setTimeout(() => controller.abort(), 2e4);
11272
+ let rawHtml;
11273
+ let finalUrl;
11274
+ let contentType;
11275
+ const MAX_REDIRECTS = 10;
11276
+ const FETCH_HEADERS = {
11277
+ "User-Agent": "Mozilla/5.0 (compatible; ai-cli/1.0; +https://github.com/ai-cli)",
11278
+ Accept: "text/html,application/xhtml+xml,text/plain,*/*",
11279
+ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8"
11280
+ };
11281
+ const dispatcher = ssrfSafeDispatcher();
11282
+ try {
11283
+ let currentUrl = url;
11284
+ let resp = null;
11285
+ for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
11286
+ const parsedHop = new URL(currentUrl);
11287
+ if (isPrivateHost(parsedHop.hostname)) {
11288
+ throw new NetworkError(`Blocked: redirect to private/internal address "${currentUrl}".`);
11289
+ }
11290
+ await resolveAndCheck(parsedHop.hostname);
11291
+ const r = await fetch(currentUrl, {
11292
+ signal: controller.signal,
11293
+ headers: FETCH_HEADERS,
11294
+ redirect: "manual",
11295
+ // 手动控制重定向
11296
+ dispatcher
11297
+ // dual undici-types:`dispatcher` 字段在 undici 与 @types/node 内置的
11298
+ // undici-types 两份 RequestInit 间判不兼容,localized 转换。
11299
+ });
11300
+ if (r.status >= 300 && r.status < 400) {
11301
+ if (hop >= MAX_REDIRECTS) {
11302
+ throw new NetworkError(`Too many redirects (>${MAX_REDIRECTS}): ${url}`);
11303
+ }
11304
+ const location = r.headers.get("Location");
11305
+ if (!location) {
11306
+ resp = r;
11307
+ break;
11308
+ }
11309
+ currentUrl = new URL(location, currentUrl).href;
11310
+ continue;
11311
+ }
11312
+ resp = r;
11313
+ break;
11314
+ }
11315
+ if (!resp) throw new NetworkError(`Too many redirects (>${MAX_REDIRECTS}): ${url}`);
11316
+ finalUrl = currentUrl;
11317
+ contentType = resp.headers.get("content-type") ?? "";
11318
+ if (!resp.ok) {
11319
+ throw new NetworkError(`HTTP ${resp.status} ${resp.statusText}`, resp.status);
11320
+ }
11321
+ const bodyBytes = await readBodyCapped(resp, MAX_BODY_BYTES);
11322
+ clearTimeout(timeoutId);
11323
+ rawHtml = new TextDecoder("utf-8", { fatal: false }).decode(bodyBytes);
11324
+ } catch (err) {
11325
+ clearTimeout(timeoutId);
11326
+ if (err.name === "AbortError") {
11327
+ throw new NetworkError(`Request timed out after 20s: ${url}`, void 0, err);
11328
+ }
11329
+ const cause = err.cause;
11330
+ if (cause?.message?.startsWith("Blocked:")) throw new NetworkError(cause.message);
11331
+ if (err instanceof Error && err.message === "fetch failed") {
11332
+ const detail = cause?.message ? ` (${cause.message})` : "";
11333
+ throw new NetworkError(
11334
+ `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.`,
11335
+ void 0,
11336
+ err
11337
+ );
11338
+ }
11339
+ throw err;
11340
+ } finally {
11341
+ void dispatcher.close().catch(() => {
11342
+ });
11343
+ }
11344
+ let text;
11345
+ if (contentType.includes("text/plain") || contentType.includes("application/json")) {
11346
+ text = rawHtml;
11347
+ } else {
11348
+ const title = extractTitle(rawHtml);
11349
+ const desc = extractDescription(rawHtml);
11350
+ const body = htmlToText(rawHtml);
11351
+ const header = [
11352
+ title ? `# ${title}` : "",
11353
+ desc ? `> ${desc}` : "",
11354
+ `Source: ${finalUrl}`,
11355
+ ""
11356
+ ].filter(Boolean).join("\n");
11357
+ text = header + "\n\n" + body;
11358
+ }
11359
+ return text;
11360
+ }
11037
11361
 
11038
11362
  // src/tools/builtin/web-search.ts
11039
- var REQUEST_TIMEOUT_MS = 15e3;
11040
- var MAX_RESULTS = 10;
11041
- var DEFAULT_RESULTS = 5;
11363
+ function planEngineOrder(engine, query, hasDeepSeekKey) {
11364
+ const skipped = [];
11365
+ if (engine === "bing") return { order: ["bing"], skipped };
11366
+ if (engine === "google") return { order: ["google"], skipped };
11367
+ const tooLong = [...query].length > DEEPSEEK_MAX_QUERY_CHARS;
11368
+ if (engine === "deepseek") {
11369
+ if (!hasDeepSeekKey) {
11370
+ throw new ToolError(
11371
+ "web_search",
11372
+ "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."
11373
+ );
11374
+ }
11375
+ if (tooLong) {
11376
+ throw new ToolError(
11377
+ "web_search",
11378
+ `engine 'deepseek' accepts queries up to ${DEEPSEEK_MAX_QUERY_CHARS} characters \u2014 shorten the query.`
11379
+ );
11380
+ }
11381
+ return { order: ["deepseek"], skipped };
11382
+ }
11383
+ if (hasDeepSeekKey && !tooLong) return { order: ["deepseek", "bing", "google"], skipped };
11384
+ if (hasDeepSeekKey && tooLong) skipped.push(`deepseek: query longer than ${DEEPSEEK_MAX_QUERY_CHARS} chars, skipped`);
11385
+ return { order: ["bing", "google"], skipped };
11386
+ }
11387
+ var REQUEST_TIMEOUT_MS2 = 15e3;
11388
+ var MAX_RESULTS2 = 10;
11389
+ var DEFAULT_RESULTS2 = 5;
11042
11390
  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
11391
  var webSearchTool = {
11044
11392
  definition: {
11045
11393
  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).',
11394
+ 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
11395
  parameters: {
11048
11396
  query: {
11049
11397
  type: "string",
@@ -11057,7 +11405,7 @@ var webSearchTool = {
11057
11405
  },
11058
11406
  engine: {
11059
11407
  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'.",
11408
+ 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
11409
  required: false
11062
11410
  }
11063
11411
  },
@@ -11067,21 +11415,30 @@ var webSearchTool = {
11067
11415
  const query = String(args["query"] ?? "").trim();
11068
11416
  if (!query) throw new ToolError("web_search", "query parameter is required");
11069
11417
  const numResults = Math.min(
11070
- Math.max(Math.floor(Number(args["num_results"] ?? DEFAULT_RESULTS)), 1),
11071
- MAX_RESULTS
11418
+ Math.max(Math.floor(Number(args["num_results"] ?? DEFAULT_RESULTS2)), 1),
11419
+ MAX_RESULTS2
11072
11420
  );
11073
11421
  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})`);
11422
+ if (!["deepseek", "bing", "google", "auto"].includes(engine)) {
11423
+ throw new ToolError("web_search", `engine must be one of: auto, deepseek, bing, google (got: ${engine})`);
11076
11424
  }
11077
- const order = engine === "auto" ? ["bing", "google"] : engine === "bing" ? ["bing"] : ["google"];
11078
- const errors = [];
11425
+ const deepseekKey = engine === "bing" || engine === "google" ? void 0 : resolveDeepSeekWebKey();
11426
+ const { order, skipped } = planEngineOrder(engine, query, deepseekKey !== void 0);
11427
+ const errors = [...skipped];
11079
11428
  let weakBingFallback = null;
11080
11429
  let weakBingReason = null;
11081
11430
  for (let i = 0; i < order.length; i++) {
11082
11431
  const eng = order[i];
11083
11432
  const isLastEngine = i === order.length - 1;
11084
11433
  try {
11434
+ if (eng === "deepseek") {
11435
+ const dsResults = await deepseekSearch(deepseekKey, query, numResults);
11436
+ if (dsResults.length === 0) {
11437
+ errors.push("deepseek: 0 results");
11438
+ continue;
11439
+ }
11440
+ return formatDeepSeekResults(query, dsResults);
11441
+ }
11085
11442
  const results = eng === "bing" ? await searchBing(query, numResults) : await searchGoogle(query, numResults);
11086
11443
  if (results.length === 0) {
11087
11444
  errors.push(`${eng}: 0 results`);
@@ -11096,13 +11453,13 @@ var webSearchTool = {
11096
11453
  continue;
11097
11454
  }
11098
11455
  }
11099
- return formatResults(query, eng, results);
11456
+ return formatResults2(query, eng, results);
11100
11457
  } catch (err) {
11101
11458
  errors.push(`${eng}: ${err instanceof Error ? err.message : String(err)}`);
11102
11459
  }
11103
11460
  }
11104
11461
  if (weakBingFallback) {
11105
- return formatResults(query, "bing", weakBingFallback, {
11462
+ return formatResults2(query, "bing", weakBingFallback, {
11106
11463
  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
11464
  });
11108
11465
  }
@@ -11308,7 +11665,7 @@ function parseGoogleResults(html, maxResults) {
11308
11665
  }
11309
11666
  async function fetchHtml(url) {
11310
11667
  const controller = new AbortController();
11311
- const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
11668
+ const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS2);
11312
11669
  try {
11313
11670
  const res = await fetch(url, {
11314
11671
  method: "GET",
@@ -11325,7 +11682,7 @@ async function fetchHtml(url) {
11325
11682
  return await res.text();
11326
11683
  } catch (err) {
11327
11684
  if (err instanceof Error && err.name === "AbortError") {
11328
- throw new NetworkError(`Request timed out after ${REQUEST_TIMEOUT_MS / 1e3}s.`, void 0, err);
11685
+ throw new NetworkError(`Request timed out after ${REQUEST_TIMEOUT_MS2 / 1e3}s.`, void 0, err);
11329
11686
  }
11330
11687
  throw err;
11331
11688
  } finally {
@@ -11338,7 +11695,31 @@ function stripTags2(html) {
11338
11695
  function decodeHtmlEntities(s) {
11339
11696
  return s.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&#x2F;/g, "/").replace(/&nbsp;/g, " ");
11340
11697
  }
11341
- function formatResults(query, engine, results, opts) {
11698
+ var DEEPSEEK_SUMMARY_MAX_CHARS = 600;
11699
+ function clipSummary(s) {
11700
+ const flat = s.replace(/\s+/g, " ").trim();
11701
+ const chars = [...flat];
11702
+ if (chars.length <= DEEPSEEK_SUMMARY_MAX_CHARS) return flat;
11703
+ return chars.slice(0, DEEPSEEK_SUMMARY_MAX_CHARS).join("") + "\u2026";
11704
+ }
11705
+ function formatDeepSeekResults(query, results) {
11706
+ const preamble = `Search results for "${query}" via deepseek (${results.length} results; relevance 0\u20133, \u22651.5 is high quality):
11707
+ `;
11708
+ const body = results.map((r, i) => {
11709
+ const meta = [
11710
+ r.siteName,
11711
+ r.date ? r.date.slice(0, 10) : null,
11712
+ r.rerankScore !== null ? `relevance ${r.rerankScore.toFixed(2)}` : null
11713
+ ].filter(Boolean).join(" \xB7 ");
11714
+ const text = clipSummary(r.summary || r.snippet) || "(no summary)";
11715
+ return `${i + 1}. **${r.title}**
11716
+ URL: ${r.url}${meta ? `
11717
+ ${meta}` : ""}
11718
+ ${text}`;
11719
+ }).join("\n\n");
11720
+ return preamble + "\n" + body;
11721
+ }
11722
+ function formatResults2(query, engine, results, opts) {
11342
11723
  let preamble = `Search results for "${query}" via ${engine} (${results.length} results):
11343
11724
  `;
11344
11725
  if (opts?.warning) {
@@ -11632,134 +12013,6 @@ function renderTodoList(todos) {
11632
12013
  console.log();
11633
12014
  }
11634
12015
 
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
12016
  // src/core/tool-capable-provider.ts
11764
12017
  function isToolCapableProvider(provider) {
11765
12018
  if (!provider) return false;
@@ -11816,7 +12069,7 @@ var HookEventNameSchema = z2.enum([
11816
12069
  var McpServerSchema = z2.object({
11817
12070
  command: z2.string(),
11818
12071
  args: z2.array(z2.string()).default([]),
11819
- env: z2.record(z2.string()).optional(),
12072
+ env: z2.record(z2.string(), z2.string()).optional(),
11820
12073
  timeout: z2.number().default(3e4)
11821
12074
  });
11822
12075
  var PluginManifestSchema = z2.object({
@@ -11826,10 +12079,10 @@ var PluginManifestSchema = z2.object({
11826
12079
  skills: z2.array(z2.string()).default([]),
11827
12080
  hooks: z2.object({
11828
12081
  enabled: z2.boolean().optional(),
11829
- events: z2.record(HookEventNameSchema, HookCommandSchema).default({})
11830
- }).default({ events: {} }),
12082
+ events: z2.partialRecord(HookEventNameSchema, HookCommandSchema).default({})
12083
+ }).prefault({ events: {} }),
11831
12084
  commands: z2.array(z2.string()).default([]),
11832
- mcpServers: z2.record(McpServerSchema).default({}),
12085
+ mcpServers: z2.record(z2.string(), McpServerSchema).default({}),
11833
12086
  agents: z2.array(z2.string()).default([]),
11834
12087
  permissionHints: z2.array(z2.string()).default([])
11835
12088
  }).strict();
@@ -17944,7 +18197,7 @@ Cache: write=${cacheCreate} read=${cacheRead}` : "";
17944
18197
  Cost: ${formatCost(cost)}` : "";
17945
18198
  let memoryLine = "";
17946
18199
  try {
17947
- const { countPendingMemories: countPendingMemories2 } = await import("./persistent-memory-WFGBTGWT.js");
18200
+ const { countPendingMemories: countPendingMemories2 } = await import("./persistent-memory-ZO6NGZIV.js");
17948
18201
  const pending = countPendingMemories2(ctx.config.getConfigDir());
17949
18202
  if (pending > 0) memoryLine = `
17950
18203
  Memory: \u26A0 ${pending} pending approval \u2014 see the Memory panel or /memory`;
@@ -18932,7 +19185,7 @@ async function handleTest(args, ctx) {
18932
19185
  const isCommand = argStr.includes(" ") || /^(mvn|gradle|npm|pytest|cargo|go)\b/.test(argStr);
18933
19186
  testArgs = isCommand ? { command: argStr } : { filter: argStr };
18934
19187
  }
18935
- const runTests = ctx.runTests ?? (await import("./run-tests-TUTI7HIN.js")).executeTests;
19188
+ const runTests = ctx.runTests ?? (await import("./run-tests-72HGJXD4.js")).executeTests;
18936
19189
  const report = await runTests(testArgs);
18937
19190
  ctx.send({ type: "info", message: report });
18938
19191
  } catch (err) {
@@ -19323,7 +19576,7 @@ async function handleMemory(args, ctx) {
19323
19576
  }
19324
19577
  if (intent.kind === "status") {
19325
19578
  const { getChatIndexStatus: getChatIndexStatus2 } = await import("./chat-index-RLVLQRRC.js");
19326
- const { listMemoryEntries: listMemoryEntries2, isMemoryExpired: isMemoryExpired2, formatMemoryForPrompt: formatMemoryForPrompt2 } = await import("./persistent-memory-WFGBTGWT.js");
19579
+ const { listMemoryEntries: listMemoryEntries2, isMemoryExpired: isMemoryExpired2, formatMemoryForPrompt: formatMemoryForPrompt2 } = await import("./persistent-memory-ZO6NGZIV.js");
19327
19580
  const memories = listMemoryEntries2(configDir, { includeExpired: true, includeRejected: true });
19328
19581
  const injected = formatMemoryForPrompt2(configDir, process.cwd(), ctx.config.get("memory")?.maxChars);
19329
19582
  const view = buildMemoryStatusView({
@@ -19346,7 +19599,7 @@ async function handleMemory(args, ctx) {
19346
19599
  return;
19347
19600
  }
19348
19601
  if (intent.kind === "recall") {
19349
- const { searchPersistentMemories: searchPersistentMemories2, touchMemoryReferences: touchMemoryReferences2 } = await import("./persistent-memory-WFGBTGWT.js");
19602
+ const { searchPersistentMemories: searchPersistentMemories2, touchMemoryReferences: touchMemoryReferences2 } = await import("./persistent-memory-ZO6NGZIV.js");
19350
19603
  const { searchChatMemory: searchChatMemory2 } = await import("./chat-index-RLVLQRRC.js");
19351
19604
  const persistentHits = searchPersistentMemories2(configDir, intent.query, process.cwd(), 3);
19352
19605
  if (persistentHits.length > 0) {
@@ -19467,7 +19720,7 @@ async function handleMemory(args, ctx) {
19467
19720
  ctx.handleMemoryManage(intent.action, args[1], intent.action === "expire" ? args[2] : void 0);
19468
19721
  } else if (intent.kind === "export") {
19469
19722
  if (args[1] === "md") {
19470
- const { exportMemoryEntries: exportMemoryEntries2 } = await import("./persistent-memory-WFGBTGWT.js");
19723
+ const { exportMemoryEntries: exportMemoryEntries2 } = await import("./persistent-memory-ZO6NGZIV.js");
19471
19724
  ctx.send({
19472
19725
  type: "export_data",
19473
19726
  format: "md",
@@ -20473,7 +20726,7 @@ No tools match "${filter}".
20473
20726
  const { join: join27 } = await import("path");
20474
20727
  const { existsSync: existsSync33 } = await import("fs");
20475
20728
  const { getGitRoot: getGitRoot2 } = await import("./git-context-VNCA2GU7.js");
20476
- const { MCP_PROJECT_CONFIG_NAME: MCP_PROJECT_CONFIG_NAME2 } = await import("./constants-WJAUJAQQ.js");
20729
+ const { MCP_PROJECT_CONFIG_NAME: MCP_PROJECT_CONFIG_NAME2 } = await import("./constants-ARJE6QPN.js");
20477
20730
  const { approveProject, hashMcpFile } = await import("./project-trust-6CJB5ZMX.js");
20478
20731
  const cwd = process.cwd();
20479
20732
  const projectRoot = getGitRoot2(cwd) ?? cwd;
@@ -21392,7 +21645,7 @@ function createReviewCommands() {
21392
21645
  usage: "/test [command|filter]",
21393
21646
  async execute(args, ctx) {
21394
21647
  try {
21395
- const { executeTests } = await import("./run-tests-TUTI7HIN.js");
21648
+ const { executeTests } = await import("./run-tests-72HGJXD4.js");
21396
21649
  const argStr = args.join(" ").trim();
21397
21650
  let testArgs = {};
21398
21651
  if (argStr) {