jinzd-ai-cli 0.4.333 → 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 (40) hide show
  1. package/dist/{batch-TYKULVIK.js → batch-GUQLUOXL.js} +2 -2
  2. package/dist/{chunk-NRDCQGAN.js → chunk-56IOLQ5E.js} +1 -1
  3. package/dist/{chunk-ZQFOY5FB.js → chunk-5WUOBFYG.js} +23 -23
  4. package/dist/{chunk-W4P2KJZN.js → chunk-6SFS6IJH.js} +67 -0
  5. package/dist/{chunk-JTXHWIBO.js → chunk-7TU4KHMK.js} +1 -1
  6. package/dist/{chunk-A4KKNBG3.js → chunk-DQBWJG32.js} +422 -236
  7. package/dist/{chunk-K4AYNZF7.js → chunk-EGRKGP4P.js} +1 -1
  8. package/dist/{chunk-H4UIWMA5.js → chunk-HXENKVWP.js} +4 -4
  9. package/dist/{chunk-4TKEA7RE.js → chunk-MAUNCANT.js} +1 -1
  10. package/dist/{chunk-YIEOU7NG.js → chunk-PWXVC6UZ.js} +5 -5
  11. package/dist/{chunk-6WQCZTAJ.js → chunk-RVB4GCSQ.js} +4 -4
  12. package/dist/{chunk-5MUAJ742.js → chunk-SSQARJMS.js} +2 -2
  13. package/dist/{chunk-3JGBKYCN.js → chunk-VHLZ2ZUQ.js} +7 -7
  14. package/dist/{chunk-YUQYVOY2.js → chunk-WL4NXDYT.js} +1 -1
  15. package/dist/{chunk-4RVLGIK3.js → chunk-XF2F2V5Y.js} +1 -1
  16. package/dist/{chunk-ZQ5HR7XN.js → chunk-YYO6TMOA.js} +1 -1
  17. package/dist/{ci-MYKIATCI.js → ci-FSMBWLMM.js} +6 -6
  18. package/dist/{ci-format-QOXT6E3A.js → ci-format-BONCHM6H.js} +2 -2
  19. package/dist/{constants-C55HE2E5.js → constants-ARJE6QPN.js} +1 -1
  20. package/dist/{constants-WK2I2UCZ.js → constants-RMAQCGVM.js} +1 -1
  21. package/dist/{doctor-cli-63GBR2D3.js → doctor-cli-T7D4DPWU.js} +6 -6
  22. package/dist/electron-server.js +519 -266
  23. package/dist/{hub-J4FZ4GXX.js → hub-KCYDY3U6.js} +1 -1
  24. package/dist/index.js +19 -19
  25. package/dist/{persistent-memory-AEIRTKWX.js → persistent-memory-3RG5Z6WK.js} +2 -2
  26. package/dist/{persistent-memory-6JN7AWD6.js → persistent-memory-ZO6NGZIV.js} +2 -2
  27. package/dist/{pr-C53TJRXV.js → pr-W2PPGBYA.js} +6 -6
  28. package/dist/{run-tests-BKFV3HUZ.js → run-tests-72HGJXD4.js} +2 -2
  29. package/dist/{run-tests-CZLZW5RL.js → run-tests-BWJEL7PA.js} +2 -2
  30. package/dist/{server-2GYX4CJC.js → server-WTLNIO23.js} +6 -6
  31. package/dist/{server-2UB74CEK.js → server-Y3N6RMIN.js} +15 -15
  32. package/dist/{task-orchestrator-EVONAO6V.js → task-orchestrator-QYCG7NVG.js} +6 -6
  33. package/dist/{usage-6Y7K2GNN.js → usage-5L4ZQENO.js} +2 -2
  34. package/dist/web/client/app.js +8 -4
  35. package/dist/web/client/app.js.br +0 -0
  36. package/dist/web/client/app.js.gz +0 -0
  37. package/dist/web/client/vendor/purify.es.mjs +272 -196
  38. package/dist/web/client/vendor/purify.es.mjs.br +0 -0
  39. package/dist/web/client/vendor/purify.es.mjs.gz +0 -0
  40. package/package.json +6 -3
@@ -11,7 +11,7 @@ import {
11
11
  searchPersistentMemories,
12
12
  touchMemoryReferences,
13
13
  updateMemoryEntry
14
- } from "./chunk-4RVLGIK3.js";
14
+ } from "./chunk-XF2F2V5Y.js";
15
15
  import {
16
16
  indexProject
17
17
  } from "./chunk-RWRZVQCB.js";
@@ -30,11 +30,11 @@ import {
30
30
  runHook,
31
31
  runLifecycleHooks,
32
32
  runTool
33
- } from "./chunk-YIEOU7NG.js";
33
+ } from "./chunk-PWXVC6UZ.js";
34
34
  import {
35
35
  getDangerLevel,
36
36
  isFileWriteTool
37
- } from "./chunk-W4P2KJZN.js";
37
+ } from "./chunk-6SFS6IJH.js";
38
38
  import {
39
39
  buildTruncatedArgumentsError,
40
40
  runLeanAgentLoop
@@ -66,7 +66,7 @@ import {
66
66
  recoverGbkMojibake,
67
67
  runTestsTool,
68
68
  shellInvocation
69
- } from "./chunk-K4AYNZF7.js";
69
+ } from "./chunk-EGRKGP4P.js";
70
70
  import {
71
71
  CONFIG_DIR_NAME,
72
72
  DEFAULT_MAX_TOOL_OUTPUT_CHARS_CAP,
@@ -75,7 +75,7 @@ import {
75
75
  SUBAGENT_ALLOWED_TOOLS,
76
76
  SUBAGENT_DEFAULT_MAX_ROUNDS,
77
77
  SUBAGENT_MAX_ROUNDS_LIMIT
78
- } from "./chunk-JTXHWIBO.js";
78
+ } from "./chunk-7TU4KHMK.js";
79
79
  import {
80
80
  getCurrentSessionKey,
81
81
  getExecutionConfigDir,
@@ -2028,6 +2028,14 @@ function domainMatches(pattern, host) {
2028
2028
  const h = host.trim().toLowerCase().replace(/:\d+$/, "");
2029
2029
  return h === p || h.endsWith(`.${p}`);
2030
2030
  }
2031
+ function isHostAllowedByNetworkPolicy(host, policy) {
2032
+ if (!policy?.enabled) return true;
2033
+ if (policy.denyDomains?.some((pattern) => domainMatches(pattern, host))) return false;
2034
+ if (policy.allowDomains && policy.allowDomains.length > 0) {
2035
+ return policy.allowDomains.some((pattern) => domainMatches(pattern, host));
2036
+ }
2037
+ return true;
2038
+ }
2031
2039
  function isPrivateNetworkHost(host) {
2032
2040
  const h = host.toLowerCase().replace(/^\[|\]$/g, "");
2033
2041
  if (h === "" || h === "localhost") return true;
@@ -4270,6 +4278,231 @@ ${stderr}`);
4270
4278
  import { promises as dnsPromises } from "dns";
4271
4279
  import net from "net";
4272
4280
  import { Agent } from "undici";
4281
+
4282
+ // src/tools/builtin/google-search.ts
4283
+ var GOOGLE_SEARCH_API = "https://www.googleapis.com/customsearch/v1";
4284
+ var REQUEST_TIMEOUT_MS = 15e3;
4285
+ var MAX_RESULTS = 10;
4286
+ var DEFAULT_RESULTS = 5;
4287
+ var googleSearchContext = {};
4288
+ var googleSearchTool = {
4289
+ definition: {
4290
+ name: "google_search",
4291
+ 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.",
4292
+ parameters: {
4293
+ query: {
4294
+ type: "string",
4295
+ description: "The search query string.",
4296
+ required: true
4297
+ },
4298
+ num_results: {
4299
+ type: "number",
4300
+ description: "Number of results to return (1-10, default 5).",
4301
+ required: false
4302
+ }
4303
+ },
4304
+ dangerous: false
4305
+ },
4306
+ async execute(args) {
4307
+ const query = String(args["query"] ?? "").trim();
4308
+ if (!query) throw new ToolError("google_search", "query parameter is required");
4309
+ const enabledFlag = googleSearchContext.configManager?.get("googleSearch")?.enabled ?? false;
4310
+ if (!enabledFlag) {
4311
+ throw new ToolError(
4312
+ "google_search",
4313
+ '\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.'
4314
+ );
4315
+ }
4316
+ const numResults = Math.min(
4317
+ Math.max(Math.floor(Number(args["num_results"] ?? DEFAULT_RESULTS)), 1),
4318
+ MAX_RESULTS
4319
+ );
4320
+ const { apiKey, cx } = resolveConfig();
4321
+ const url = new URL(GOOGLE_SEARCH_API);
4322
+ url.searchParams.set("key", apiKey);
4323
+ url.searchParams.set("cx", cx);
4324
+ url.searchParams.set("q", query);
4325
+ url.searchParams.set("num", String(numResults));
4326
+ const controller = new AbortController();
4327
+ const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
4328
+ try {
4329
+ const response = await fetch(url.toString(), {
4330
+ method: "GET",
4331
+ headers: {
4332
+ "Accept": "application/json"
4333
+ },
4334
+ signal: controller.signal
4335
+ });
4336
+ if (!response.ok) {
4337
+ const errorBody = await response.text().catch(() => "");
4338
+ if (response.status === 403) {
4339
+ throw new NetworkError(
4340
+ `Google Search API 403 Forbidden \u2014 Invalid API Key or daily free quota exceeded (100/day).
4341
+ Please check your API Key and Search Engine ID configuration.`,
4342
+ 403
4343
+ );
4344
+ }
4345
+ if (response.status === 429) {
4346
+ throw new NetworkError("Google Search API 429 \u2014 Too many requests, please try again later.", 429);
4347
+ }
4348
+ throw new NetworkError(
4349
+ `Google Search API error: HTTP ${response.status} ${response.statusText}
4350
+ ${errorBody.slice(0, 500)}`,
4351
+ response.status
4352
+ );
4353
+ }
4354
+ const data = await response.json();
4355
+ return formatResults(query, data, numResults);
4356
+ } catch (err) {
4357
+ if (err instanceof Error && err.name === "AbortError") {
4358
+ throw new NetworkError(`Google Search request timed out (${REQUEST_TIMEOUT_MS / 1e3}s). Please check your network or proxy configuration.`, void 0, err);
4359
+ }
4360
+ throw err;
4361
+ } finally {
4362
+ clearTimeout(timeout);
4363
+ }
4364
+ }
4365
+ };
4366
+ function resolveConfig() {
4367
+ let apiKey;
4368
+ let cx;
4369
+ if (googleSearchContext.configManager) {
4370
+ apiKey = googleSearchContext.configManager.getApiKey("google-search");
4371
+ cx = EnvLoader.getGoogleSearchEngineId() ?? googleSearchContext.configManager.get("googleSearchEngineId");
4372
+ } else {
4373
+ apiKey = EnvLoader.getApiKey("google-search");
4374
+ cx = EnvLoader.getGoogleSearchEngineId();
4375
+ }
4376
+ if (!apiKey) {
4377
+ throw new ToolError(
4378
+ "google_search",
4379
+ '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'
4380
+ );
4381
+ }
4382
+ if (!cx) {
4383
+ throw new ToolError(
4384
+ "google_search",
4385
+ "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"
4386
+ );
4387
+ }
4388
+ return { apiKey, cx };
4389
+ }
4390
+ function formatResults(query, data, _requested) {
4391
+ const items = data.items ?? [];
4392
+ if (items.length === 0) {
4393
+ const info2 = data.searchInformation;
4394
+ return `No results found for: "${query}"` + (info2 ? ` (searched ${info2.formattedTotalResults ?? "0"} pages in ${info2.formattedSearchTime ?? "?"}s)` : "");
4395
+ }
4396
+ const info = data.searchInformation;
4397
+ const header = `Search results for "${query}" (${items.length} of ~${info?.formattedTotalResults ?? "?"} results):
4398
+ `;
4399
+ const results = items.map((item, i) => {
4400
+ const title = item.title ?? "Untitled";
4401
+ const link = item.link ?? "";
4402
+ const snippet = item.snippet ?? "";
4403
+ return `${i + 1}. **${title}**
4404
+ URL: ${link}
4405
+ ${snippet}`;
4406
+ });
4407
+ return header + "\n" + results.join("\n\n");
4408
+ }
4409
+
4410
+ // src/tools/builtin/deepseek-web-api.ts
4411
+ var DEEPSEEK_TOOLS_HOST = "api.deepseek.com";
4412
+ var BASE_URL = `https://${DEEPSEEK_TOOLS_HOST}/v1/tools`;
4413
+ var SEARCH_TIMEOUT_MS = 2e4;
4414
+ var FETCH_TIMEOUT_MS = 3e4;
4415
+ var DEEPSEEK_MAX_QUERY_CHARS = 128;
4416
+ function resolveDeepSeekWebKey(configManager = googleSearchContext.configManager) {
4417
+ const key = configManager ? configManager.getApiKey("deepseek") : EnvLoader.getApiKey("deepseek");
4418
+ if (!key || !key.trim()) return void 0;
4419
+ const policy = configManager?.get("networkPolicy");
4420
+ if (!isHostAllowedByNetworkPolicy(DEEPSEEK_TOOLS_HOST, policy)) return void 0;
4421
+ return key.trim();
4422
+ }
4423
+ async function deepseekSearch(apiKey, query, count) {
4424
+ const data = await postJson(apiKey, "search", { query, count }, SEARCH_TIMEOUT_MS);
4425
+ return parseDeepSeekSearchResponse(data);
4426
+ }
4427
+ async function deepseekFetch(apiKey, url) {
4428
+ const data = await postJson(apiKey, "fetch", { url }, FETCH_TIMEOUT_MS);
4429
+ const content = typeof data?.content === "string" ? data.content : "";
4430
+ if (!content.trim()) throw new NetworkError("DeepSeek Fetch returned no content.");
4431
+ return { title: typeof data.title === "string" ? data.title : "", content };
4432
+ }
4433
+ function parseDeepSeekSearchResponse(data) {
4434
+ const raw = data?.results;
4435
+ if (!Array.isArray(raw)) {
4436
+ throw new NetworkError("DeepSeek Search returned an unexpected response (no results array).");
4437
+ }
4438
+ const out = [];
4439
+ for (const item of raw) {
4440
+ if (!item || typeof item !== "object") continue;
4441
+ const r = item;
4442
+ const url = typeof r["url"] === "string" ? r["url"] : "";
4443
+ const title = typeof r["title"] === "string" ? r["title"].trim() : "";
4444
+ if (!url || !title) continue;
4445
+ out.push({
4446
+ title,
4447
+ url,
4448
+ snippet: typeof r["snippet"] === "string" ? r["snippet"].trim() : "",
4449
+ summary: typeof r["summary"] === "string" ? r["summary"].trim() : "",
4450
+ siteName: typeof r["site_name"] === "string" && r["site_name"] ? r["site_name"] : null,
4451
+ date: typeof r["date"] === "string" && r["date"] ? r["date"] : null,
4452
+ rerankScore: typeof r["rerank_score"] === "number" ? r["rerank_score"] : null,
4453
+ authority: typeof r["authority"] === "number" ? r["authority"] : null
4454
+ });
4455
+ }
4456
+ return out;
4457
+ }
4458
+ async function postJson(apiKey, endpoint, body, timeoutMs) {
4459
+ const controller = new AbortController();
4460
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
4461
+ try {
4462
+ const res = await fetch(`${BASE_URL}/${endpoint}`, {
4463
+ method: "POST",
4464
+ headers: {
4465
+ Authorization: `Bearer ${apiKey}`,
4466
+ "Content-Type": "application/json",
4467
+ Accept: "application/json"
4468
+ },
4469
+ body: JSON.stringify(body),
4470
+ signal: controller.signal
4471
+ });
4472
+ const text = await res.text();
4473
+ if (!res.ok) {
4474
+ throw new NetworkError(
4475
+ `DeepSeek ${endpoint} HTTP ${res.status}: ${describeDeepSeekError(res.status, text)}`,
4476
+ res.status
4477
+ );
4478
+ }
4479
+ try {
4480
+ return JSON.parse(text);
4481
+ } catch {
4482
+ throw new NetworkError(`DeepSeek ${endpoint} returned non-JSON response.`);
4483
+ }
4484
+ } catch (err) {
4485
+ if (err instanceof Error && err.name === "AbortError") {
4486
+ throw new NetworkError(`DeepSeek ${endpoint} timed out after ${timeoutMs / 1e3}s.`, void 0, err);
4487
+ }
4488
+ throw err;
4489
+ } finally {
4490
+ clearTimeout(timer);
4491
+ }
4492
+ }
4493
+ function describeDeepSeekError(status, bodyText) {
4494
+ let message = "";
4495
+ try {
4496
+ const parsed = JSON.parse(bodyText);
4497
+ if (typeof parsed?.error?.message === "string") message = parsed.error.message;
4498
+ } catch {
4499
+ }
4500
+ if (!message) message = bodyText.trim().slice(0, 200) || "(empty body)";
4501
+ 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" : "";
4502
+ return message + hint;
4503
+ }
4504
+
4505
+ // src/tools/builtin/web-fetch.ts
4273
4506
  function htmlToText(html) {
4274
4507
  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, "");
4275
4508
  const HTML_REGEX_LIMIT = 2e5;
@@ -4442,7 +4675,7 @@ function ssrfSafeDispatcher() {
4442
4675
  var webFetchTool = {
4443
4676
  definition: {
4444
4677
  name: "web_fetch",
4445
- 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.",
4678
+ 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.",
4446
4679
  parameters: {
4447
4680
  url: {
4448
4681
  type: "string",
@@ -4472,94 +4705,26 @@ var webFetchTool = {
4472
4705
  if (e.message.startsWith("Blocked:")) throw e;
4473
4706
  throw new NetworkError(`Invalid URL: "${url}"`);
4474
4707
  }
4475
- const controller = new AbortController();
4476
- const timeoutId = setTimeout(() => controller.abort(), 2e4);
4477
- let rawHtml;
4478
- let finalUrl;
4479
- let contentType;
4480
- const MAX_REDIRECTS = 10;
4481
- const FETCH_HEADERS = {
4482
- "User-Agent": "Mozilla/5.0 (compatible; ai-cli/1.0; +https://github.com/ai-cli)",
4483
- Accept: "text/html,application/xhtml+xml,text/plain,*/*",
4484
- "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8"
4485
- };
4486
- const dispatcher = ssrfSafeDispatcher();
4708
+ let text;
4487
4709
  try {
4488
- let currentUrl = url;
4489
- let resp = null;
4490
- for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
4491
- const parsedHop = new URL(currentUrl);
4492
- if (isPrivateHost(parsedHop.hostname)) {
4493
- throw new NetworkError(`Blocked: redirect to private/internal address "${currentUrl}".`);
4494
- }
4495
- await resolveAndCheck(parsedHop.hostname);
4496
- const r = await fetch(currentUrl, {
4497
- signal: controller.signal,
4498
- headers: FETCH_HEADERS,
4499
- redirect: "manual",
4500
- // 手动控制重定向
4501
- dispatcher
4502
- // dual undici-types:`dispatcher` 字段在 undici 与 @types/node 内置的
4503
- // undici-types 两份 RequestInit 间判不兼容,localized 转换。
4504
- });
4505
- if (r.status >= 300 && r.status < 400) {
4506
- if (hop >= MAX_REDIRECTS) {
4507
- throw new NetworkError(`Too many redirects (>${MAX_REDIRECTS}): ${url}`);
4508
- }
4509
- const location = r.headers.get("Location");
4510
- if (!location) {
4511
- resp = r;
4512
- break;
4513
- }
4514
- currentUrl = new URL(location, currentUrl).href;
4515
- continue;
4516
- }
4517
- resp = r;
4518
- break;
4519
- }
4520
- if (!resp) throw new NetworkError(`Too many redirects (>${MAX_REDIRECTS}): ${url}`);
4521
- finalUrl = currentUrl;
4522
- contentType = resp.headers.get("content-type") ?? "";
4523
- if (!resp.ok) {
4524
- throw new NetworkError(`HTTP ${resp.status} ${resp.statusText}`, resp.status);
4525
- }
4526
- const bodyBytes = await readBodyCapped(resp, MAX_BODY_BYTES);
4527
- clearTimeout(timeoutId);
4528
- rawHtml = new TextDecoder("utf-8", { fatal: false }).decode(bodyBytes);
4710
+ text = await fetchDirect(url);
4529
4711
  } catch (err) {
4530
- clearTimeout(timeoutId);
4531
- if (err.name === "AbortError") {
4532
- throw new NetworkError(`Request timed out after 20s: ${url}`, void 0, err);
4533
- }
4534
- const cause = err.cause;
4535
- if (cause?.message?.startsWith("Blocked:")) throw new NetworkError(cause.message);
4536
- if (err instanceof Error && err.message === "fetch failed") {
4537
- const detail = cause?.message ? ` (${cause.message})` : "";
4538
- throw new NetworkError(
4539
- `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.`,
4540
- void 0,
4541
- err
4542
- );
4712
+ const msg = err instanceof Error ? err.message : String(err);
4713
+ if (msg.startsWith("Blocked:")) throw err;
4714
+ const key = resolveDeepSeekWebKey();
4715
+ if (!key) throw err;
4716
+ try {
4717
+ const ds = await deepseekFetch(key, url);
4718
+ text = [
4719
+ ds.title ? `# ${ds.title}` : "",
4720
+ `Source: ${url} (via DeepSeek Fetch \u2014 direct request failed: ${msg.split("\n")[0]})`,
4721
+ ""
4722
+ ].filter(Boolean).join("\n") + "\n\n" + ds.content;
4723
+ } catch (dsErr) {
4724
+ const dsMsg = dsErr instanceof Error ? dsErr.message : String(dsErr);
4725
+ throw new NetworkError(`${msg}
4726
+ DeepSeek Fetch fallback also failed: ${dsMsg}`, void 0, err);
4543
4727
  }
4544
- throw err;
4545
- } finally {
4546
- void dispatcher.close().catch(() => {
4547
- });
4548
- }
4549
- let text;
4550
- if (contentType.includes("text/plain") || contentType.includes("application/json")) {
4551
- text = rawHtml;
4552
- } else {
4553
- const title = extractTitle(rawHtml);
4554
- const desc = extractDescription(rawHtml);
4555
- const body = htmlToText(rawHtml);
4556
- const header = [
4557
- title ? `# ${title}` : "",
4558
- desc ? `> ${desc}` : "",
4559
- `Source: ${finalUrl}`,
4560
- ""
4561
- ].filter(Boolean).join("\n");
4562
- text = header + "\n\n" + body;
4563
4728
  }
4564
4729
  if (selector) {
4565
4730
  const lower = selector.toLowerCase();
@@ -4584,16 +4749,132 @@ var webFetchTool = {
4584
4749
  return text;
4585
4750
  }
4586
4751
  };
4752
+ async function fetchDirect(url) {
4753
+ const controller = new AbortController();
4754
+ const timeoutId = setTimeout(() => controller.abort(), 2e4);
4755
+ let rawHtml;
4756
+ let finalUrl;
4757
+ let contentType;
4758
+ const MAX_REDIRECTS = 10;
4759
+ const FETCH_HEADERS = {
4760
+ "User-Agent": "Mozilla/5.0 (compatible; ai-cli/1.0; +https://github.com/ai-cli)",
4761
+ Accept: "text/html,application/xhtml+xml,text/plain,*/*",
4762
+ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8"
4763
+ };
4764
+ const dispatcher = ssrfSafeDispatcher();
4765
+ try {
4766
+ let currentUrl = url;
4767
+ let resp = null;
4768
+ for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
4769
+ const parsedHop = new URL(currentUrl);
4770
+ if (isPrivateHost(parsedHop.hostname)) {
4771
+ throw new NetworkError(`Blocked: redirect to private/internal address "${currentUrl}".`);
4772
+ }
4773
+ await resolveAndCheck(parsedHop.hostname);
4774
+ const r = await fetch(currentUrl, {
4775
+ signal: controller.signal,
4776
+ headers: FETCH_HEADERS,
4777
+ redirect: "manual",
4778
+ // 手动控制重定向
4779
+ dispatcher
4780
+ // dual undici-types:`dispatcher` 字段在 undici 与 @types/node 内置的
4781
+ // undici-types 两份 RequestInit 间判不兼容,localized 转换。
4782
+ });
4783
+ if (r.status >= 300 && r.status < 400) {
4784
+ if (hop >= MAX_REDIRECTS) {
4785
+ throw new NetworkError(`Too many redirects (>${MAX_REDIRECTS}): ${url}`);
4786
+ }
4787
+ const location = r.headers.get("Location");
4788
+ if (!location) {
4789
+ resp = r;
4790
+ break;
4791
+ }
4792
+ currentUrl = new URL(location, currentUrl).href;
4793
+ continue;
4794
+ }
4795
+ resp = r;
4796
+ break;
4797
+ }
4798
+ if (!resp) throw new NetworkError(`Too many redirects (>${MAX_REDIRECTS}): ${url}`);
4799
+ finalUrl = currentUrl;
4800
+ contentType = resp.headers.get("content-type") ?? "";
4801
+ if (!resp.ok) {
4802
+ throw new NetworkError(`HTTP ${resp.status} ${resp.statusText}`, resp.status);
4803
+ }
4804
+ const bodyBytes = await readBodyCapped(resp, MAX_BODY_BYTES);
4805
+ clearTimeout(timeoutId);
4806
+ rawHtml = new TextDecoder("utf-8", { fatal: false }).decode(bodyBytes);
4807
+ } catch (err) {
4808
+ clearTimeout(timeoutId);
4809
+ if (err.name === "AbortError") {
4810
+ throw new NetworkError(`Request timed out after 20s: ${url}`, void 0, err);
4811
+ }
4812
+ const cause = err.cause;
4813
+ if (cause?.message?.startsWith("Blocked:")) throw new NetworkError(cause.message);
4814
+ if (err instanceof Error && err.message === "fetch failed") {
4815
+ const detail = cause?.message ? ` (${cause.message})` : "";
4816
+ throw new NetworkError(
4817
+ `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.`,
4818
+ void 0,
4819
+ err
4820
+ );
4821
+ }
4822
+ throw err;
4823
+ } finally {
4824
+ void dispatcher.close().catch(() => {
4825
+ });
4826
+ }
4827
+ let text;
4828
+ if (contentType.includes("text/plain") || contentType.includes("application/json")) {
4829
+ text = rawHtml;
4830
+ } else {
4831
+ const title = extractTitle(rawHtml);
4832
+ const desc = extractDescription(rawHtml);
4833
+ const body = htmlToText(rawHtml);
4834
+ const header = [
4835
+ title ? `# ${title}` : "",
4836
+ desc ? `> ${desc}` : "",
4837
+ `Source: ${finalUrl}`,
4838
+ ""
4839
+ ].filter(Boolean).join("\n");
4840
+ text = header + "\n\n" + body;
4841
+ }
4842
+ return text;
4843
+ }
4587
4844
 
4588
4845
  // src/tools/builtin/web-search.ts
4589
- var REQUEST_TIMEOUT_MS = 15e3;
4590
- var MAX_RESULTS = 10;
4591
- var DEFAULT_RESULTS = 5;
4846
+ function planEngineOrder(engine, query, hasDeepSeekKey) {
4847
+ const skipped = [];
4848
+ if (engine === "bing") return { order: ["bing"], skipped };
4849
+ if (engine === "google") return { order: ["google"], skipped };
4850
+ const tooLong = [...query].length > DEEPSEEK_MAX_QUERY_CHARS;
4851
+ if (engine === "deepseek") {
4852
+ if (!hasDeepSeekKey) {
4853
+ throw new ToolError(
4854
+ "web_search",
4855
+ "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."
4856
+ );
4857
+ }
4858
+ if (tooLong) {
4859
+ throw new ToolError(
4860
+ "web_search",
4861
+ `engine 'deepseek' accepts queries up to ${DEEPSEEK_MAX_QUERY_CHARS} characters \u2014 shorten the query.`
4862
+ );
4863
+ }
4864
+ return { order: ["deepseek"], skipped };
4865
+ }
4866
+ if (hasDeepSeekKey && !tooLong) return { order: ["deepseek", "bing", "google"], skipped };
4867
+ if (hasDeepSeekKey && tooLong) skipped.push(`deepseek: query longer than ${DEEPSEEK_MAX_QUERY_CHARS} chars, skipped`);
4868
+ return { order: ["bing", "google"], skipped };
4869
+ }
4870
+ var REQUEST_TIMEOUT_MS2 = 15e3;
4871
+ var MAX_RESULTS2 = 10;
4872
+ var DEFAULT_RESULTS2 = 5;
4592
4873
  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";
4593
4874
  var webSearchTool = {
4594
4875
  definition: {
4595
4876
  name: "web_search",
4596
- 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).',
4877
+ 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).',
4597
4878
  parameters: {
4598
4879
  query: {
4599
4880
  type: "string",
@@ -4607,7 +4888,7 @@ var webSearchTool = {
4607
4888
  },
4608
4889
  engine: {
4609
4890
  type: "string",
4610
- description: "Search engine: 'bing' (default for CN reachability), 'google', or 'auto' (try bing first, fall back to google on empty/error). Default 'auto'.",
4891
+ 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'.",
4611
4892
  required: false
4612
4893
  }
4613
4894
  },
@@ -4617,21 +4898,30 @@ var webSearchTool = {
4617
4898
  const query = String(args["query"] ?? "").trim();
4618
4899
  if (!query) throw new ToolError("web_search", "query parameter is required");
4619
4900
  const numResults = Math.min(
4620
- Math.max(Math.floor(Number(args["num_results"] ?? DEFAULT_RESULTS)), 1),
4621
- MAX_RESULTS
4901
+ Math.max(Math.floor(Number(args["num_results"] ?? DEFAULT_RESULTS2)), 1),
4902
+ MAX_RESULTS2
4622
4903
  );
4623
4904
  const engine = String(args["engine"] ?? "auto").toLowerCase();
4624
- if (!["bing", "google", "auto"].includes(engine)) {
4625
- throw new ToolError("web_search", `engine must be one of: bing, google, auto (got: ${engine})`);
4905
+ if (!["deepseek", "bing", "google", "auto"].includes(engine)) {
4906
+ throw new ToolError("web_search", `engine must be one of: auto, deepseek, bing, google (got: ${engine})`);
4626
4907
  }
4627
- const order = engine === "auto" ? ["bing", "google"] : engine === "bing" ? ["bing"] : ["google"];
4628
- const errors = [];
4908
+ const deepseekKey = engine === "bing" || engine === "google" ? void 0 : resolveDeepSeekWebKey();
4909
+ const { order, skipped } = planEngineOrder(engine, query, deepseekKey !== void 0);
4910
+ const errors = [...skipped];
4629
4911
  let weakBingFallback = null;
4630
4912
  let weakBingReason = null;
4631
4913
  for (let i = 0; i < order.length; i++) {
4632
4914
  const eng = order[i];
4633
4915
  const isLastEngine = i === order.length - 1;
4634
4916
  try {
4917
+ if (eng === "deepseek") {
4918
+ const dsResults = await deepseekSearch(deepseekKey, query, numResults);
4919
+ if (dsResults.length === 0) {
4920
+ errors.push("deepseek: 0 results");
4921
+ continue;
4922
+ }
4923
+ return formatDeepSeekResults(query, dsResults);
4924
+ }
4635
4925
  const results = eng === "bing" ? await searchBing(query, numResults) : await searchGoogle(query, numResults);
4636
4926
  if (results.length === 0) {
4637
4927
  errors.push(`${eng}: 0 results`);
@@ -4646,13 +4936,13 @@ var webSearchTool = {
4646
4936
  continue;
4647
4937
  }
4648
4938
  }
4649
- return formatResults(query, eng, results);
4939
+ return formatResults2(query, eng, results);
4650
4940
  } catch (err) {
4651
4941
  errors.push(`${eng}: ${err instanceof Error ? err.message : String(err)}`);
4652
4942
  }
4653
4943
  }
4654
4944
  if (weakBingFallback) {
4655
- return formatResults(query, "bing", weakBingFallback, {
4945
+ return formatResults2(query, "bing", weakBingFallback, {
4656
4946
  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).`
4657
4947
  });
4658
4948
  }
@@ -4858,7 +5148,7 @@ function parseGoogleResults(html, maxResults) {
4858
5148
  }
4859
5149
  async function fetchHtml(url) {
4860
5150
  const controller = new AbortController();
4861
- const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
5151
+ const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS2);
4862
5152
  try {
4863
5153
  const res = await fetch(url, {
4864
5154
  method: "GET",
@@ -4875,7 +5165,7 @@ async function fetchHtml(url) {
4875
5165
  return await res.text();
4876
5166
  } catch (err) {
4877
5167
  if (err instanceof Error && err.name === "AbortError") {
4878
- throw new NetworkError(`Request timed out after ${REQUEST_TIMEOUT_MS / 1e3}s.`, void 0, err);
5168
+ throw new NetworkError(`Request timed out after ${REQUEST_TIMEOUT_MS2 / 1e3}s.`, void 0, err);
4879
5169
  }
4880
5170
  throw err;
4881
5171
  } finally {
@@ -4888,7 +5178,31 @@ function stripTags2(html) {
4888
5178
  function decodeHtmlEntities(s) {
4889
5179
  return s.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&#x2F;/g, "/").replace(/&nbsp;/g, " ");
4890
5180
  }
4891
- function formatResults(query, engine, results, opts) {
5181
+ var DEEPSEEK_SUMMARY_MAX_CHARS = 600;
5182
+ function clipSummary(s) {
5183
+ const flat = s.replace(/\s+/g, " ").trim();
5184
+ const chars = [...flat];
5185
+ if (chars.length <= DEEPSEEK_SUMMARY_MAX_CHARS) return flat;
5186
+ return chars.slice(0, DEEPSEEK_SUMMARY_MAX_CHARS).join("") + "\u2026";
5187
+ }
5188
+ function formatDeepSeekResults(query, results) {
5189
+ const preamble = `Search results for "${query}" via deepseek (${results.length} results; relevance 0\u20133, \u22651.5 is high quality):
5190
+ `;
5191
+ const body = results.map((r, i) => {
5192
+ const meta = [
5193
+ r.siteName,
5194
+ r.date ? r.date.slice(0, 10) : null,
5195
+ r.rerankScore !== null ? `relevance ${r.rerankScore.toFixed(2)}` : null
5196
+ ].filter(Boolean).join(" \xB7 ");
5197
+ const text = clipSummary(r.summary || r.snippet) || "(no summary)";
5198
+ return `${i + 1}. **${r.title}**
5199
+ URL: ${r.url}${meta ? `
5200
+ ${meta}` : ""}
5201
+ ${text}`;
5202
+ }).join("\n\n");
5203
+ return preamble + "\n" + body;
5204
+ }
5205
+ function formatResults2(query, engine, results, opts) {
4892
5206
  let preamble = `Search results for "${query}" via ${engine} (${results.length} results):
4893
5207
  `;
4894
5208
  if (opts?.warning) {
@@ -5182,134 +5496,6 @@ function renderTodoList(todos) {
5182
5496
  console.log();
5183
5497
  }
5184
5498
 
5185
- // src/tools/builtin/google-search.ts
5186
- var GOOGLE_SEARCH_API = "https://www.googleapis.com/customsearch/v1";
5187
- var REQUEST_TIMEOUT_MS2 = 15e3;
5188
- var MAX_RESULTS2 = 10;
5189
- var DEFAULT_RESULTS2 = 5;
5190
- var googleSearchContext = {};
5191
- var googleSearchTool = {
5192
- definition: {
5193
- name: "google_search",
5194
- 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.",
5195
- parameters: {
5196
- query: {
5197
- type: "string",
5198
- description: "The search query string.",
5199
- required: true
5200
- },
5201
- num_results: {
5202
- type: "number",
5203
- description: "Number of results to return (1-10, default 5).",
5204
- required: false
5205
- }
5206
- },
5207
- dangerous: false
5208
- },
5209
- async execute(args) {
5210
- const query = String(args["query"] ?? "").trim();
5211
- if (!query) throw new ToolError("google_search", "query parameter is required");
5212
- const enabledFlag = googleSearchContext.configManager?.get("googleSearch")?.enabled ?? false;
5213
- if (!enabledFlag) {
5214
- throw new ToolError(
5215
- "google_search",
5216
- '\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.'
5217
- );
5218
- }
5219
- const numResults = Math.min(
5220
- Math.max(Math.floor(Number(args["num_results"] ?? DEFAULT_RESULTS2)), 1),
5221
- MAX_RESULTS2
5222
- );
5223
- const { apiKey, cx } = resolveConfig();
5224
- const url = new URL(GOOGLE_SEARCH_API);
5225
- url.searchParams.set("key", apiKey);
5226
- url.searchParams.set("cx", cx);
5227
- url.searchParams.set("q", query);
5228
- url.searchParams.set("num", String(numResults));
5229
- const controller = new AbortController();
5230
- const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS2);
5231
- try {
5232
- const response = await fetch(url.toString(), {
5233
- method: "GET",
5234
- headers: {
5235
- "Accept": "application/json"
5236
- },
5237
- signal: controller.signal
5238
- });
5239
- if (!response.ok) {
5240
- const errorBody = await response.text().catch(() => "");
5241
- if (response.status === 403) {
5242
- throw new NetworkError(
5243
- `Google Search API 403 Forbidden \u2014 Invalid API Key or daily free quota exceeded (100/day).
5244
- Please check your API Key and Search Engine ID configuration.`,
5245
- 403
5246
- );
5247
- }
5248
- if (response.status === 429) {
5249
- throw new NetworkError("Google Search API 429 \u2014 Too many requests, please try again later.", 429);
5250
- }
5251
- throw new NetworkError(
5252
- `Google Search API error: HTTP ${response.status} ${response.statusText}
5253
- ${errorBody.slice(0, 500)}`,
5254
- response.status
5255
- );
5256
- }
5257
- const data = await response.json();
5258
- return formatResults2(query, data, numResults);
5259
- } catch (err) {
5260
- if (err instanceof Error && err.name === "AbortError") {
5261
- throw new NetworkError(`Google Search request timed out (${REQUEST_TIMEOUT_MS2 / 1e3}s). Please check your network or proxy configuration.`, void 0, err);
5262
- }
5263
- throw err;
5264
- } finally {
5265
- clearTimeout(timeout);
5266
- }
5267
- }
5268
- };
5269
- function resolveConfig() {
5270
- let apiKey;
5271
- let cx;
5272
- if (googleSearchContext.configManager) {
5273
- apiKey = googleSearchContext.configManager.getApiKey("google-search");
5274
- cx = EnvLoader.getGoogleSearchEngineId() ?? googleSearchContext.configManager.get("googleSearchEngineId");
5275
- } else {
5276
- apiKey = EnvLoader.getApiKey("google-search");
5277
- cx = EnvLoader.getGoogleSearchEngineId();
5278
- }
5279
- if (!apiKey) {
5280
- throw new ToolError(
5281
- "google_search",
5282
- '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'
5283
- );
5284
- }
5285
- if (!cx) {
5286
- throw new ToolError(
5287
- "google_search",
5288
- "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"
5289
- );
5290
- }
5291
- return { apiKey, cx };
5292
- }
5293
- function formatResults2(query, data, _requested) {
5294
- const items = data.items ?? [];
5295
- if (items.length === 0) {
5296
- const info2 = data.searchInformation;
5297
- return `No results found for: "${query}"` + (info2 ? ` (searched ${info2.formattedTotalResults ?? "0"} pages in ${info2.formattedSearchTime ?? "?"}s)` : "");
5298
- }
5299
- const info = data.searchInformation;
5300
- const header = `Search results for "${query}" (${items.length} of ~${info?.formattedTotalResults ?? "?"} results):
5301
- `;
5302
- const results = items.map((item, i) => {
5303
- const title = item.title ?? "Untitled";
5304
- const link = item.link ?? "";
5305
- const snippet = item.snippet ?? "";
5306
- return `${i + 1}. **${title}**
5307
- URL: ${link}
5308
- ${snippet}`;
5309
- });
5310
- return header + "\n" + results.join("\n\n");
5311
- }
5312
-
5313
5499
  // src/agents/agent-config.ts
5314
5500
  import { existsSync as existsSync10, readdirSync as readdirSync6, readFileSync as readFileSync5 } from "fs";
5315
5501
  import { join as join4 } from "path";
@@ -7179,10 +7365,10 @@ export {
7179
7365
  truncateOutput,
7180
7366
  ToolExecutor,
7181
7367
  globToRegex,
7368
+ googleSearchContext,
7182
7369
  lastResponseStore,
7183
7370
  cleanupRejectedTeeFile,
7184
7371
  askUserContext,
7185
- googleSearchContext,
7186
7372
  isToolCapableProvider,
7187
7373
  spawnAgentContext,
7188
7374
  estimateTokens,