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.
- package/dist/{batch-TYKULVIK.js → batch-GUQLUOXL.js} +2 -2
- package/dist/{chunk-NRDCQGAN.js → chunk-56IOLQ5E.js} +1 -1
- package/dist/{chunk-ZQFOY5FB.js → chunk-5WUOBFYG.js} +23 -23
- package/dist/{chunk-W4P2KJZN.js → chunk-6SFS6IJH.js} +67 -0
- package/dist/{chunk-JTXHWIBO.js → chunk-7TU4KHMK.js} +1 -1
- package/dist/{chunk-A4KKNBG3.js → chunk-DQBWJG32.js} +422 -236
- package/dist/{chunk-K4AYNZF7.js → chunk-EGRKGP4P.js} +1 -1
- package/dist/{chunk-H4UIWMA5.js → chunk-HXENKVWP.js} +4 -4
- package/dist/{chunk-4TKEA7RE.js → chunk-MAUNCANT.js} +1 -1
- package/dist/{chunk-YIEOU7NG.js → chunk-PWXVC6UZ.js} +5 -5
- package/dist/{chunk-6WQCZTAJ.js → chunk-RVB4GCSQ.js} +4 -4
- package/dist/{chunk-5MUAJ742.js → chunk-SSQARJMS.js} +2 -2
- package/dist/{chunk-3JGBKYCN.js → chunk-VHLZ2ZUQ.js} +7 -7
- package/dist/{chunk-YUQYVOY2.js → chunk-WL4NXDYT.js} +1 -1
- package/dist/{chunk-4RVLGIK3.js → chunk-XF2F2V5Y.js} +1 -1
- package/dist/{chunk-ZQ5HR7XN.js → chunk-YYO6TMOA.js} +1 -1
- package/dist/{ci-MYKIATCI.js → ci-FSMBWLMM.js} +6 -6
- package/dist/{ci-format-QOXT6E3A.js → ci-format-BONCHM6H.js} +2 -2
- package/dist/{constants-C55HE2E5.js → constants-ARJE6QPN.js} +1 -1
- package/dist/{constants-WK2I2UCZ.js → constants-RMAQCGVM.js} +1 -1
- package/dist/{doctor-cli-63GBR2D3.js → doctor-cli-T7D4DPWU.js} +6 -6
- package/dist/electron-server.js +519 -266
- package/dist/{hub-J4FZ4GXX.js → hub-KCYDY3U6.js} +1 -1
- package/dist/index.js +19 -19
- package/dist/{persistent-memory-AEIRTKWX.js → persistent-memory-3RG5Z6WK.js} +2 -2
- package/dist/{persistent-memory-6JN7AWD6.js → persistent-memory-ZO6NGZIV.js} +2 -2
- package/dist/{pr-C53TJRXV.js → pr-W2PPGBYA.js} +6 -6
- package/dist/{run-tests-BKFV3HUZ.js → run-tests-72HGJXD4.js} +2 -2
- package/dist/{run-tests-CZLZW5RL.js → run-tests-BWJEL7PA.js} +2 -2
- package/dist/{server-2GYX4CJC.js → server-WTLNIO23.js} +6 -6
- package/dist/{server-2UB74CEK.js → server-Y3N6RMIN.js} +15 -15
- package/dist/{task-orchestrator-EVONAO6V.js → task-orchestrator-QYCG7NVG.js} +6 -6
- package/dist/{usage-6Y7K2GNN.js → usage-5L4ZQENO.js} +2 -2
- package/dist/web/client/app.js +8 -4
- package/dist/web/client/app.js.br +0 -0
- package/dist/web/client/app.js.gz +0 -0
- package/dist/web/client/vendor/purify.es.mjs +272 -196
- package/dist/web/client/vendor/purify.es.mjs.br +0 -0
- package/dist/web/client/vendor/purify.es.mjs.gz +0 -0
- package/package.json +6 -3
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
searchPersistentMemories,
|
|
12
12
|
touchMemoryReferences,
|
|
13
13
|
updateMemoryEntry
|
|
14
|
-
} from "./chunk-
|
|
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-
|
|
33
|
+
} from "./chunk-PWXVC6UZ.js";
|
|
34
34
|
import {
|
|
35
35
|
getDangerLevel,
|
|
36
36
|
isFileWriteTool
|
|
37
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4531
|
-
if (
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
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
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
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
|
|
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: '
|
|
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"] ??
|
|
4621
|
-
|
|
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
|
|
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
|
|
4628
|
-
const
|
|
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
|
|
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
|
|
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(),
|
|
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 ${
|
|
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(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(///g, "/").replace(/ /g, " ");
|
|
4890
5180
|
}
|
|
4891
|
-
|
|
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,
|