jinzd-ai-cli 0.4.334 → 0.4.338
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{batch-6UJGEEVB.js → batch-N4DOEUEW.js} +2 -2
- package/dist/{chunk-W4P2KJZN.js → chunk-7BOHS5IX.js} +72 -0
- package/dist/{chunk-SZQ2C3HH.js → chunk-ATOF545W.js} +1 -1
- package/dist/{chunk-7GGWVBHG.js → chunk-DCWBDSMO.js} +2 -2
- package/dist/{chunk-RQOOEXDU.js → chunk-DOO4QAGJ.js} +2 -2
- package/dist/{chunk-XMHVHS2X.js → chunk-DRCN44SZ.js} +4 -4
- package/dist/{chunk-YTAYWGS4.js → chunk-JUYK4OML.js} +1 -1
- package/dist/{chunk-DPUZ22D7.js → chunk-KGHVSYGI.js} +27 -23
- package/dist/{chunk-EOYIIGBJ.js → chunk-NJREBH6A.js} +5 -5
- package/dist/{chunk-FHHBPCCZ.js → chunk-NRFAGZVJ.js} +1 -1
- package/dist/{chunk-YVEBRHSV.js → chunk-OXVJZLJM.js} +6 -5
- package/dist/{chunk-YY4QFBBZ.js → chunk-PKYNCSQC.js} +475 -240
- package/dist/{chunk-KSZEQADY.js → chunk-RIDNF4G2.js} +1 -1
- package/dist/{chunk-27RTBMY7.js → chunk-TTLLXFJC.js} +40 -3
- package/dist/{chunk-DX4Y4CRH.js → chunk-UWEBIOHD.js} +8 -20
- package/dist/{chunk-ZK5VCQD2.js → chunk-WQAUMAEV.js} +1 -1
- package/dist/{ci-S7WXRTZ6.js → ci-HFAPFMZY.js} +6 -6
- package/dist/{ci-format-TPCL2KWQ.js → ci-format-QTRMHY3N.js} +2 -2
- package/dist/{constants-WJAUJAQQ.js → constants-JZH2QKMQ.js} +1 -1
- package/dist/{constants-5VQMHBFU.js → constants-QJNJFMC6.js} +1 -1
- package/dist/{doctor-cli-DYQLNMPT.js → doctor-cli-FZ4IVZ6H.js} +6 -6
- package/dist/electron-server.js +622 -285
- package/dist/{hub-A7P2ZBYB.js → hub-IRILXTL3.js} +1 -1
- package/dist/index.js +25 -23
- package/dist/{persistent-memory-WFGBTGWT.js → persistent-memory-UZIZVHRT.js} +2 -2
- package/dist/{persistent-memory-SKGOCEGT.js → persistent-memory-VRBNWOWN.js} +2 -2
- package/dist/{pr-IPXLVP2K.js → pr-4KNQWYC6.js} +7 -7
- package/dist/{run-tests-BG6SGRWQ.js → run-tests-N2VKD7BU.js} +2 -2
- package/dist/{run-tests-TUTI7HIN.js → run-tests-UXEOFPAW.js} +2 -2
- package/dist/{server-4QJPQBEW.js → server-2VSO2ZX4.js} +6 -6
- package/dist/{server-S6OROC7S.js → server-HWKYWQEP.js} +21 -20
- package/dist/{task-orchestrator-6JG5GAXU.js → task-orchestrator-SC42LAFG.js} +6 -6
- package/dist/{usage-LPQQ6NGG.js → usage-ZVV27BHD.js} +2 -2
- 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-WQAUMAEV.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-NJREBH6A.js";
|
|
34
34
|
import {
|
|
35
35
|
getDangerLevel,
|
|
36
36
|
isFileWriteTool
|
|
37
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-7BOHS5IX.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-DOO4QAGJ.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-NRFAGZVJ.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";
|
|
@@ -6059,26 +6245,75 @@ var taskCreateTool = {
|
|
|
6059
6245
|
ID: ${task.id}
|
|
6060
6246
|
Command: ${command}
|
|
6061
6247
|
Description: ${description}
|
|
6062
|
-
Use task_list to check status, task_stop to terminate.`;
|
|
6248
|
+
Use task_list to check status (task_list with id + wait_seconds waits for it to finish \u2014 no bash sleep needed), task_stop to terminate.`;
|
|
6063
6249
|
}
|
|
6064
6250
|
};
|
|
6065
6251
|
|
|
6066
6252
|
// src/tools/builtin/task-list.ts
|
|
6253
|
+
var MAX_TASK_WAIT_SECONDS = 300;
|
|
6254
|
+
var POLL_MS = 200;
|
|
6255
|
+
async function waitForTasks(id, seconds) {
|
|
6256
|
+
const isRunning = (taskId) => getTaskOutput(taskId)?.status === "running";
|
|
6257
|
+
const watched = id ? [id] : listTasks().filter((t) => t.status === "running").map((t) => t.id);
|
|
6258
|
+
if (!watched.some(isRunning)) return "nothing-running";
|
|
6259
|
+
const signal = currentAbortSignal();
|
|
6260
|
+
const deadline = Date.now() + seconds * 1e3;
|
|
6261
|
+
for (; ; ) {
|
|
6262
|
+
if (signal.aborted) return "interrupted";
|
|
6263
|
+
if (!watched.every(isRunning)) return "finished";
|
|
6264
|
+
const left = deadline - Date.now();
|
|
6265
|
+
if (left <= 0) return "timeout";
|
|
6266
|
+
await new Promise((resolve7) => {
|
|
6267
|
+
const timer = setTimeout(done, Math.min(POLL_MS, left));
|
|
6268
|
+
function done() {
|
|
6269
|
+
clearTimeout(timer);
|
|
6270
|
+
signal.removeEventListener("abort", done);
|
|
6271
|
+
resolve7();
|
|
6272
|
+
}
|
|
6273
|
+
signal.addEventListener("abort", done, { once: true });
|
|
6274
|
+
});
|
|
6275
|
+
}
|
|
6276
|
+
}
|
|
6277
|
+
function parseWaitSeconds(raw) {
|
|
6278
|
+
const n = Number(raw);
|
|
6279
|
+
if (raw === void 0 || raw === null || raw === "" || !Number.isFinite(n) || n <= 0) {
|
|
6280
|
+
return { seconds: 0, clamped: false };
|
|
6281
|
+
}
|
|
6282
|
+
return n > MAX_TASK_WAIT_SECONDS ? { seconds: MAX_TASK_WAIT_SECONDS, clamped: true } : { seconds: n, clamped: false };
|
|
6283
|
+
}
|
|
6067
6284
|
var taskListTool = {
|
|
6068
6285
|
definition: {
|
|
6069
6286
|
name: "task_list",
|
|
6070
|
-
description: `List all background tasks with their status. Provide an ID to get detailed output for a specific task.`,
|
|
6287
|
+
description: `List all background tasks with their status. Provide an ID to get detailed output for a specific task. To wait for a background task, pass wait_seconds (max ${MAX_TASK_WAIT_SECONDS}) instead of polling with bash sleep: the call returns as soon as the task (or, without an id, any running task) finishes.`,
|
|
6071
6288
|
parameters: {
|
|
6072
6289
|
id: {
|
|
6073
6290
|
type: "string",
|
|
6074
6291
|
description: "Optional: specific task ID to get detailed stdout/stderr output",
|
|
6075
6292
|
required: false
|
|
6293
|
+
},
|
|
6294
|
+
wait_seconds: {
|
|
6295
|
+
type: "number",
|
|
6296
|
+
description: `Optional: wait up to this many seconds (max ${MAX_TASK_WAIT_SECONDS}) for the task with this id \u2014 or, without id, for any running task \u2014 to finish, then report. Returns early on completion.`,
|
|
6297
|
+
required: false
|
|
6076
6298
|
}
|
|
6077
6299
|
},
|
|
6078
6300
|
dangerous: false
|
|
6079
6301
|
},
|
|
6080
6302
|
async execute(args) {
|
|
6081
6303
|
const id = args["id"] ? String(args["id"]) : void 0;
|
|
6304
|
+
const { seconds, clamped } = parseWaitSeconds(args["wait_seconds"]);
|
|
6305
|
+
let note = "";
|
|
6306
|
+
if (seconds > 0) {
|
|
6307
|
+
const started = Date.now();
|
|
6308
|
+
const outcome = await waitForTasks(id, seconds);
|
|
6309
|
+
const waited = ((Date.now() - started) / 1e3).toFixed(1);
|
|
6310
|
+
if (outcome === "timeout") note = `(waited ${waited}s; still running \u2014 call again with wait_seconds to keep waiting)`;
|
|
6311
|
+
else if (outcome === "finished") note = `(waited ${waited}s; task finished)`;
|
|
6312
|
+
else if (outcome === "interrupted") note = `(wait interrupted by user after ${waited}s)`;
|
|
6313
|
+
if (clamped) note += `${note ? " " : ""}(wait_seconds capped at ${MAX_TASK_WAIT_SECONDS})`;
|
|
6314
|
+
}
|
|
6315
|
+
const withNote = (body) => note ? `${note}
|
|
6316
|
+
${body}` : body;
|
|
6082
6317
|
if (id) {
|
|
6083
6318
|
const output = getTaskOutput(id);
|
|
6084
6319
|
if (!output) return `Task not found: ${id}`;
|
|
@@ -6090,7 +6325,7 @@ ${output.stdout}`);
|
|
|
6090
6325
|
\u2500\u2500 stderr \u2500\u2500
|
|
6091
6326
|
${output.stderr}`);
|
|
6092
6327
|
if (!output.stdout && !output.stderr) parts.push("\n(no output yet)");
|
|
6093
|
-
return parts.join("");
|
|
6328
|
+
return withNote(parts.join(""));
|
|
6094
6329
|
}
|
|
6095
6330
|
const all = listTasks();
|
|
6096
6331
|
if (all.length === 0) return "No background tasks.";
|
|
@@ -6102,7 +6337,7 @@ ${output.stderr}`);
|
|
|
6102
6337
|
lines.push(` [${t.id}] ${status} (${elapsed.toFixed(1)}s) \u2014 ${t.description}`);
|
|
6103
6338
|
if (t.exitCode !== null) lines.push(` exit code: ${t.exitCode}`);
|
|
6104
6339
|
}
|
|
6105
|
-
return lines.join("\n");
|
|
6340
|
+
return withNote(lines.join("\n"));
|
|
6106
6341
|
}
|
|
6107
6342
|
};
|
|
6108
6343
|
|
|
@@ -7179,10 +7414,10 @@ export {
|
|
|
7179
7414
|
truncateOutput,
|
|
7180
7415
|
ToolExecutor,
|
|
7181
7416
|
globToRegex,
|
|
7417
|
+
googleSearchContext,
|
|
7182
7418
|
lastResponseStore,
|
|
7183
7419
|
cleanupRejectedTeeFile,
|
|
7184
7420
|
askUserContext,
|
|
7185
|
-
googleSearchContext,
|
|
7186
7421
|
isToolCapableProvider,
|
|
7187
7422
|
spawnAgentContext,
|
|
7188
7423
|
estimateTokens,
|