@duckmind/dm-windows-x64 0.63.0 → 0.63.4
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/dm.exe +0 -0
- package/extensions/.dm-extensions.json +138 -2
- package/extensions/dm-web-access/SECURITY.md +5 -0
- package/extensions/dm-web-access/activity.js +65 -0
- package/extensions/dm-web-access/anysearch.js +158 -0
- package/extensions/dm-web-access/auth-fetch.js +131 -0
- package/extensions/dm-web-access/bocha.js +214 -0
- package/extensions/dm-web-access/brave.js +196 -0
- package/extensions/dm-web-access/brightdata-unlocker.js +202 -0
- package/extensions/dm-web-access/brightdata.js +334 -0
- package/extensions/dm-web-access/chrome-cookies.js +627 -0
- package/extensions/dm-web-access/content-find.js +114 -0
- package/extensions/dm-web-access/credential-source.js +150 -0
- package/extensions/dm-web-access/curator-page.js +3559 -0
- package/extensions/dm-web-access/curator-server.js +691 -0
- package/extensions/dm-web-access/data-uri-sanitize.js +312 -0
- package/extensions/dm-web-access/datalab-pdf-extract.js +346 -0
- package/extensions/dm-web-access/declared-web-links.js +167 -0
- package/extensions/dm-web-access/dm-web-fetch-demo.mp4 +0 -0
- package/extensions/dm-web-access/duckduckgo.js +118 -0
- package/extensions/dm-web-access/exa.js +401 -0
- package/extensions/dm-web-access/extract.js +1220 -0
- package/extensions/dm-web-access/feature-config.js +24 -0
- package/extensions/dm-web-access/fetch-params.js +81 -0
- package/extensions/dm-web-access/firecrawl.js +378 -0
- package/extensions/dm-web-access/gemini-adc.js +241 -0
- package/extensions/dm-web-access/gemini-api.js +258 -0
- package/extensions/dm-web-access/gemini-pdf-extract.js +74 -0
- package/extensions/dm-web-access/gemini-search.js +889 -0
- package/extensions/dm-web-access/gemini-url-context.js +97 -0
- package/extensions/dm-web-access/gemini-web-config.js +84 -0
- package/extensions/dm-web-access/gemini-web.js +351 -0
- package/extensions/dm-web-access/github-api.js +166 -0
- package/extensions/dm-web-access/github-extract.js +991 -0
- package/extensions/dm-web-access/github-issue-pr.js +750 -0
- package/extensions/dm-web-access/index.js +3117 -0
- package/extensions/dm-web-access/jina-search.js +242 -0
- package/extensions/dm-web-access/kagi.js +255 -0
- package/extensions/dm-web-access/kimi-search.js +214 -0
- package/extensions/dm-web-access/ollama.js +209 -0
- package/extensions/dm-web-access/openai-search.js +510 -0
- package/extensions/dm-web-access/package.json +34 -0
- package/extensions/dm-web-access/page-query.js +124 -0
- package/extensions/dm-web-access/parallel-mcp.js +223 -0
- package/extensions/dm-web-access/parallel.js +345 -0
- package/extensions/dm-web-access/pdf-extract.js +257 -0
- package/extensions/dm-web-access/perplexity.js +151 -0
- package/extensions/dm-web-access/querit.js +327 -0
- package/extensions/dm-web-access/query-rewrite.js +40 -0
- package/extensions/dm-web-access/render-search-error.js +80 -0
- package/extensions/dm-web-access/rsc-extract.js +347 -0
- package/extensions/dm-web-access/search1api.js +245 -0
- package/extensions/dm-web-access/searchinfinity.js +221 -0
- package/extensions/dm-web-access/searxng.js +223 -0
- package/extensions/dm-web-access/serpbase.js +205 -0
- package/extensions/dm-web-access/serpdive.js +238 -0
- package/extensions/dm-web-access/serper.js +200 -0
- package/extensions/dm-web-access/source-check.js +198 -0
- package/extensions/dm-web-access/ssrf-protection.js +436 -0
- package/extensions/dm-web-access/storage.js +451 -0
- package/extensions/dm-web-access/summary-model-scope.js +83 -0
- package/extensions/dm-web-access/summary-review.js +364 -0
- package/extensions/dm-web-access/tavily.js +199 -0
- package/extensions/dm-web-access/tinyfish.js +325 -0
- package/extensions/dm-web-access/utils.js +476 -0
- package/extensions/dm-web-access/valyu.js +189 -0
- package/extensions/dm-web-access/video-extract.js +336 -0
- package/extensions/dm-web-access/xai-search.js +285 -0
- package/extensions/dm-web-access/xcrawl.js +221 -0
- package/extensions/dm-web-access/youtube-extract.js +279 -0
- package/package.json +9 -1
|
@@ -0,0 +1,3117 @@
|
|
|
1
|
+
import { Box, Text, truncateToWidth } from "@duckmind/dm-tui";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { StringEnum } from "@duckmind/dm-ai/compat";
|
|
4
|
+
import { normalizeFetchContentParams } from "./fetch-params.js";
|
|
5
|
+
import { resolveAuthFetchProfile } from "./auth-fetch.js";
|
|
6
|
+
import { findContent } from "./content-find.js";
|
|
7
|
+
import { answerFromPage } from "./page-query.js";
|
|
8
|
+
import { rewriteSearchQuery } from "./query-rewrite.js";
|
|
9
|
+
import { clearCloneCache } from "./github-extract.js";
|
|
10
|
+
import { ALL_SEARCH_PROVIDERS, getConfiguredSearchRouting, normalizeSearchProviderSelection, RESOLVED_SEARCH_PROVIDERS, SEARCH_PROVIDERS, search } from "./gemini-search.js";
|
|
11
|
+
import { formatSeconds, getWebSearchConfigDir, getWebSearchConfigPath, installGlobalProxyFetch, resolveCuratorNetworkConfig, runWithProxy } from "./utils.js";
|
|
12
|
+
import {
|
|
13
|
+
clearResults,
|
|
14
|
+
deleteResult,
|
|
15
|
+
generateId,
|
|
16
|
+
getAllResults,
|
|
17
|
+
getResult,
|
|
18
|
+
restoreFromSession,
|
|
19
|
+
storeFetchedContentResult,
|
|
20
|
+
storeResult
|
|
21
|
+
} from "./storage.js";
|
|
22
|
+
import { activityMonitor } from "./activity.js";
|
|
23
|
+
import { startCuratorServer } from "./curator-server.js";
|
|
24
|
+
import {
|
|
25
|
+
buildDeterministicSummary,
|
|
26
|
+
generateSummaryDraft,
|
|
27
|
+
SUMMARY_GENERATION_DEADLINE_MS
|
|
28
|
+
} from "./summary-review.js";
|
|
29
|
+
import { randomUUID } from "node:crypto";
|
|
30
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
31
|
+
import { createRequire } from "node:module";
|
|
32
|
+
import { platform } from "node:os";
|
|
33
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
34
|
+
import { join } from "node:path";
|
|
35
|
+
import { isPerplexityAvailable } from "./perplexity.js";
|
|
36
|
+
import { isExaAvailable } from "./exa.js";
|
|
37
|
+
import { isGeminiApiAvailable } from "./gemini-api.js";
|
|
38
|
+
import { getActiveGoogleEmail, getGeminiWebAvailabilityDiagnostic, getGeminiWebAvailabilityDiagnosticDetails, isGeminiWebAvailable } from "./gemini-web.js";
|
|
39
|
+
import { isBrowserCookieAccessAllowed } from "./gemini-web-config.js";
|
|
40
|
+
import { isBraveAvailable } from "./brave.js";
|
|
41
|
+
import { isCurrentModelHostedSearchEligible, isOpenAISearchAvailable } from "./openai-search.js";
|
|
42
|
+
import { isParallelAvailable } from "./parallel.js";
|
|
43
|
+
import { isParallelMcpAvailable } from "./parallel-mcp.js";
|
|
44
|
+
import { isTinyFishAvailable } from "./tinyfish.js";
|
|
45
|
+
import { isSearch1APIAvailable } from "./search1api.js";
|
|
46
|
+
import { isSearchinfinityAvailable } from "./searchinfinity.js";
|
|
47
|
+
import { isQueritAvailable } from "./querit.js";
|
|
48
|
+
import { isTavilyAvailable } from "./tavily.js";
|
|
49
|
+
import { isFirecrawlAvailable } from "./firecrawl.js";
|
|
50
|
+
import { isJinaSearchAvailable } from "./jina-search.js";
|
|
51
|
+
import { isSerpdiveAvailable } from "./serpdive.js";
|
|
52
|
+
import { isKagiAvailable } from "./kagi.js";
|
|
53
|
+
import { isBochaAvailable } from "./bocha.js";
|
|
54
|
+
import { isOllamaAvailable } from "./ollama.js";
|
|
55
|
+
import { isSearXNGAvailable } from "./searxng.js";
|
|
56
|
+
import { isDuckDuckGoAvailable } from "./duckduckgo.js";
|
|
57
|
+
import { isAnySearchAvailable } from "./anysearch.js";
|
|
58
|
+
import { isXaiSearchAvailable } from "./xai-search.js";
|
|
59
|
+
import { isKimiSearchAvailable } from "./kimi-search.js";
|
|
60
|
+
import { isBrightDataAvailable } from "./brightdata.js";
|
|
61
|
+
import { isSerpBaseAvailable } from "./serpbase.js";
|
|
62
|
+
import { isSerperAvailable } from "./serper.js";
|
|
63
|
+
import { isValyuAvailable } from "./valyu.js";
|
|
64
|
+
import { isXcrawlAvailable } from "./xcrawl.js";
|
|
65
|
+
import { buildSearchErrorPlan } from "./render-search-error.js";
|
|
66
|
+
import { findModelWithProviderRouting, loadEnabledModelPatterns, modelMatchesEnabledPatterns, splitThinkingSuffix } from "./summary-model-scope.js";
|
|
67
|
+
import {
|
|
68
|
+
buildResearchArtifact,
|
|
69
|
+
withClaimAssessment,
|
|
70
|
+
storeResearchArtifact,
|
|
71
|
+
getResearchArtifact
|
|
72
|
+
} from "./source-check.js";
|
|
73
|
+
const WEB_SEARCH_CONFIG_PATH = getWebSearchConfigPath();
|
|
74
|
+
let extractModulePromise;
|
|
75
|
+
async function fetchAllContent(urls, signal, options) {
|
|
76
|
+
const extractModule = await (extractModulePromise ??= import("./extract.js"));
|
|
77
|
+
return extractModule.fetchAllContent(urls, signal, options);
|
|
78
|
+
}
|
|
79
|
+
function withRegisteredFetchOptions(options, toolNames, proxy) {
|
|
80
|
+
return {
|
|
81
|
+
...options ?? {},
|
|
82
|
+
toolNames,
|
|
83
|
+
...proxy !== undefined ? { proxy } : {}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function isAbortError(err) {
|
|
87
|
+
return (err instanceof Error ? err.message : String(err)).toLowerCase().includes("abort");
|
|
88
|
+
}
|
|
89
|
+
function renderSearchErrorPlan(plan, expanded, theme) {
|
|
90
|
+
if (expanded) {
|
|
91
|
+
return new Text(plan.expanded.map((l, i) => i === 0 ? theme.fg("error", l) : theme.fg("toolOutput", l)).join(`
|
|
92
|
+
`), 0, 0);
|
|
93
|
+
}
|
|
94
|
+
const box = new Box(1, 0, (t) => theme.bg("toolErrorBg", t));
|
|
95
|
+
box.addChild(new Text(theme.fg("error", plan.expanded[0]), 0, 0));
|
|
96
|
+
for (const line of plan.collapsed) {
|
|
97
|
+
box.addChild(new Text(theme.fg("dim", line), 0, 0));
|
|
98
|
+
}
|
|
99
|
+
if (plan.expandHint) {
|
|
100
|
+
box.addChild(new Text(theme.fg("muted", plan.expandHint), 0, 0));
|
|
101
|
+
}
|
|
102
|
+
return box;
|
|
103
|
+
}
|
|
104
|
+
function parseConfigRoot(raw) {
|
|
105
|
+
let parsed;
|
|
106
|
+
try {
|
|
107
|
+
parsed = JSON.parse(raw);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
110
|
+
throw new Error(`Failed to parse ${WEB_SEARCH_CONFIG_PATH}: ${message}`);
|
|
111
|
+
}
|
|
112
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
113
|
+
throw new Error(`Invalid config in ${WEB_SEARCH_CONFIG_PATH}: expected a JSON object`);
|
|
114
|
+
}
|
|
115
|
+
return parsed;
|
|
116
|
+
}
|
|
117
|
+
function loadConfig() {
|
|
118
|
+
if (!existsSync(WEB_SEARCH_CONFIG_PATH))
|
|
119
|
+
return {};
|
|
120
|
+
return parseConfigRoot(readFileSync(WEB_SEARCH_CONFIG_PATH, "utf-8"));
|
|
121
|
+
}
|
|
122
|
+
function saveConfig(updates) {
|
|
123
|
+
let config = {};
|
|
124
|
+
if (existsSync(WEB_SEARCH_CONFIG_PATH)) {
|
|
125
|
+
config = parseConfigRoot(readFileSync(WEB_SEARCH_CONFIG_PATH, "utf-8"));
|
|
126
|
+
}
|
|
127
|
+
Object.assign(config, updates);
|
|
128
|
+
const dir = getWebSearchConfigDir();
|
|
129
|
+
if (!existsSync(dir))
|
|
130
|
+
mkdirSync(dir, { recursive: true });
|
|
131
|
+
writeFileSync(WEB_SEARCH_CONFIG_PATH, JSON.stringify(config, null, 2) + `
|
|
132
|
+
`);
|
|
133
|
+
}
|
|
134
|
+
const DEFAULT_TOOL_NAMES = {
|
|
135
|
+
webSearch: "web_search",
|
|
136
|
+
sourceCheck: "source_check",
|
|
137
|
+
fetchContent: "fetch_content",
|
|
138
|
+
getSearchContent: "get_search_content"
|
|
139
|
+
};
|
|
140
|
+
const TOOL_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
|
|
141
|
+
const DEFAULT_SHORTCUTS = { curate: "ctrl+shift+s", activity: "ctrl+shift+w" };
|
|
142
|
+
const DEFAULT_CURATOR_TIMEOUT_SECONDS = 20;
|
|
143
|
+
const DEFAULT_REMOTE_CURATOR_TIMEOUT_SECONDS = 60;
|
|
144
|
+
const MAX_CURATOR_TIMEOUT_SECONDS = 600;
|
|
145
|
+
const MAX_SUMMARY_GENERATION_DEADLINE_MS = 600000;
|
|
146
|
+
function searchProviderSchema(description) {
|
|
147
|
+
return Type.Union([
|
|
148
|
+
StringEnum([...SEARCH_PROVIDERS]),
|
|
149
|
+
Type.Array(StringEnum([...RESOLVED_SEARCH_PROVIDERS]), { minItems: 1 })
|
|
150
|
+
], { description });
|
|
151
|
+
}
|
|
152
|
+
function isToolEnabled(config, key) {
|
|
153
|
+
const override = config.tools?.[key]?.enabled;
|
|
154
|
+
if (typeof override === "boolean")
|
|
155
|
+
return override;
|
|
156
|
+
return key !== "webSearch" && key !== "sourceCheck" || config.webSearch?.enabled !== false;
|
|
157
|
+
}
|
|
158
|
+
function isCommandEnabled(config, name) {
|
|
159
|
+
return config.commands?.[name]?.enabled !== false;
|
|
160
|
+
}
|
|
161
|
+
function joinToolNames(names) {
|
|
162
|
+
if (names.length === 0)
|
|
163
|
+
return "stored content";
|
|
164
|
+
if (names.length === 1)
|
|
165
|
+
return names[0];
|
|
166
|
+
if (names.length === 2)
|
|
167
|
+
return `${names[0]} or ${names[1]}`;
|
|
168
|
+
return `${names.slice(0, -1).join(", ")}, or ${names[names.length - 1]}`;
|
|
169
|
+
}
|
|
170
|
+
function resolveToolNames(config) {
|
|
171
|
+
if (config.toolNames !== undefined && (!config.toolNames || typeof config.toolNames !== "object" || Array.isArray(config.toolNames))) {
|
|
172
|
+
throw new Error(`toolNames in ${WEB_SEARCH_CONFIG_PATH} must be an object`);
|
|
173
|
+
}
|
|
174
|
+
const names = { ...DEFAULT_TOOL_NAMES };
|
|
175
|
+
for (const key of Object.keys(DEFAULT_TOOL_NAMES)) {
|
|
176
|
+
const value = config.toolNames?.[key];
|
|
177
|
+
if (value === undefined)
|
|
178
|
+
continue;
|
|
179
|
+
if (typeof value !== "string")
|
|
180
|
+
throw new Error(`toolNames.${key} in ${WEB_SEARCH_CONFIG_PATH} must be a string`);
|
|
181
|
+
const trimmed = value.trim();
|
|
182
|
+
if (!TOOL_NAME_PATTERN.test(trimmed)) {
|
|
183
|
+
throw new Error(`toolNames.${key} in ${WEB_SEARCH_CONFIG_PATH} must start with a letter and contain only letters, numbers, underscores, or hyphens`);
|
|
184
|
+
}
|
|
185
|
+
names[key] = trimmed;
|
|
186
|
+
}
|
|
187
|
+
const registeredKeys = Object.keys(DEFAULT_TOOL_NAMES).filter((key) => isToolEnabled(config, key));
|
|
188
|
+
const seen = new Map;
|
|
189
|
+
for (const key of registeredKeys) {
|
|
190
|
+
const name = names[key];
|
|
191
|
+
const previous = seen.get(name);
|
|
192
|
+
if (previous)
|
|
193
|
+
throw new Error(`toolNames.${key} duplicates toolNames.${previous} in ${WEB_SEARCH_CONFIG_PATH}`);
|
|
194
|
+
seen.set(name, key);
|
|
195
|
+
}
|
|
196
|
+
return names;
|
|
197
|
+
}
|
|
198
|
+
function loadConfigForExtensionInit() {
|
|
199
|
+
try {
|
|
200
|
+
return loadConfig();
|
|
201
|
+
} catch (err) {
|
|
202
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
203
|
+
console.error(`[dm-web-access] ${message}`);
|
|
204
|
+
return {};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function normalizeProviderInput(value, label = "provider") {
|
|
208
|
+
if (value === undefined)
|
|
209
|
+
return;
|
|
210
|
+
return normalizeSearchProviderSelection(value, label);
|
|
211
|
+
}
|
|
212
|
+
function resolveRequestedProvider(requested) {
|
|
213
|
+
const normalizedRequested = normalizeProviderInput(requested);
|
|
214
|
+
if (normalizedRequested && normalizedRequested !== "auto")
|
|
215
|
+
return normalizedRequested;
|
|
216
|
+
const config = loadConfig();
|
|
217
|
+
return normalizeProviderInput(config.searchProvider ?? config.provider, `provider in ${WEB_SEARCH_CONFIG_PATH}`) ?? "auto";
|
|
218
|
+
}
|
|
219
|
+
function toCuratorProvider(provider) {
|
|
220
|
+
if (Array.isArray(provider))
|
|
221
|
+
return "all";
|
|
222
|
+
return provider === "auto" ? undefined : provider;
|
|
223
|
+
}
|
|
224
|
+
function resolveCuratorSearchProvider(requested, current) {
|
|
225
|
+
const normalized = normalizeProviderInput(requested);
|
|
226
|
+
if (!normalized || normalized === "auto")
|
|
227
|
+
return current;
|
|
228
|
+
if (normalized === "all" && Array.isArray(current))
|
|
229
|
+
return current;
|
|
230
|
+
return normalized;
|
|
231
|
+
}
|
|
232
|
+
function normalizeRecencyFilter(value) {
|
|
233
|
+
return value === "day" || value === "week" || value === "month" || value === "year" ? value : undefined;
|
|
234
|
+
}
|
|
235
|
+
function normalizeCuratorTimeoutSeconds(value) {
|
|
236
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
237
|
+
return;
|
|
238
|
+
const normalized = Math.floor(value);
|
|
239
|
+
if (normalized < 1)
|
|
240
|
+
return;
|
|
241
|
+
return Math.min(normalized, MAX_CURATOR_TIMEOUT_SECONDS);
|
|
242
|
+
}
|
|
243
|
+
function resolveWorkflow(input, hasUI) {
|
|
244
|
+
const normalized = typeof input === "string" ? input.trim().toLowerCase() : "";
|
|
245
|
+
if (normalized === "auto-summary")
|
|
246
|
+
return "auto-summary";
|
|
247
|
+
if (!hasUI)
|
|
248
|
+
return "none";
|
|
249
|
+
if (normalized === "none")
|
|
250
|
+
return "none";
|
|
251
|
+
return "summary-review";
|
|
252
|
+
}
|
|
253
|
+
function normalizeQueryList(queryList) {
|
|
254
|
+
const normalized = [];
|
|
255
|
+
for (const query of queryList) {
|
|
256
|
+
if (typeof query !== "string")
|
|
257
|
+
continue;
|
|
258
|
+
const trimmed = query.trim();
|
|
259
|
+
if (trimmed.length > 0)
|
|
260
|
+
normalized.push(trimmed);
|
|
261
|
+
}
|
|
262
|
+
return normalized;
|
|
263
|
+
}
|
|
264
|
+
function expandQueryString(query) {
|
|
265
|
+
if (typeof query !== "string")
|
|
266
|
+
return [];
|
|
267
|
+
const trimmed = query.trim();
|
|
268
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
269
|
+
try {
|
|
270
|
+
const parsed = JSON.parse(trimmed);
|
|
271
|
+
if (Array.isArray(parsed) && parsed.every((entry) => typeof entry === "string")) {
|
|
272
|
+
return parsed.map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
273
|
+
}
|
|
274
|
+
} catch {}
|
|
275
|
+
}
|
|
276
|
+
return [query];
|
|
277
|
+
}
|
|
278
|
+
function getCuratorTimeoutSeconds() {
|
|
279
|
+
const source = loadConfig();
|
|
280
|
+
const explicit = normalizeCuratorTimeoutSeconds(source.curatorTimeoutSeconds);
|
|
281
|
+
if (explicit !== undefined)
|
|
282
|
+
return explicit;
|
|
283
|
+
return resolveCuratorNetworkConfig().enabled ? DEFAULT_REMOTE_CURATOR_TIMEOUT_SECONDS : DEFAULT_CURATOR_TIMEOUT_SECONDS;
|
|
284
|
+
}
|
|
285
|
+
export function getSummaryGenerationDeadlineMs() {
|
|
286
|
+
const value = loadConfig().summaryGenerationDeadlineMs;
|
|
287
|
+
if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
|
|
288
|
+
return SUMMARY_GENERATION_DEADLINE_MS;
|
|
289
|
+
}
|
|
290
|
+
return Math.min(value, MAX_SUMMARY_GENERATION_DEADLINE_MS);
|
|
291
|
+
}
|
|
292
|
+
function shouldAutoOpenCuratorBrowser(config) {
|
|
293
|
+
if (config.autoOpenBrowser === false)
|
|
294
|
+
return false;
|
|
295
|
+
if (resolveCuratorNetworkConfig().enabled && config.autoOpenBrowser !== true)
|
|
296
|
+
return false;
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
async function getProviderAvailability(ctx) {
|
|
300
|
+
const geminiWebAvail = await getOptionalGeminiWebAvailability();
|
|
301
|
+
const geminiApiAvail = isGeminiApiAvailable();
|
|
302
|
+
const providers = {
|
|
303
|
+
openai: await isOpenAISearchAvailable(ctx),
|
|
304
|
+
brave: isBraveAvailable(),
|
|
305
|
+
parallel: isParallelAvailable(),
|
|
306
|
+
"parallel-mcp": isParallelMcpAvailable(),
|
|
307
|
+
tinyfish: isTinyFishAvailable(),
|
|
308
|
+
search1api: isSearch1APIAvailable(),
|
|
309
|
+
searchinfinity: isSearchinfinityAvailable(),
|
|
310
|
+
querit: isQueritAvailable(),
|
|
311
|
+
tavily: isTavilyAvailable(),
|
|
312
|
+
firecrawl: isFirecrawlAvailable(),
|
|
313
|
+
jina: isJinaSearchAvailable(),
|
|
314
|
+
serpdive: isSerpdiveAvailable(),
|
|
315
|
+
kagi: isKagiAvailable(),
|
|
316
|
+
bocha: isBochaAvailable(),
|
|
317
|
+
ollama: isOllamaAvailable(),
|
|
318
|
+
searxng: isSearXNGAvailable(),
|
|
319
|
+
duckduckgo: isDuckDuckGoAvailable(),
|
|
320
|
+
perplexity: isPerplexityAvailable(),
|
|
321
|
+
exa: isExaAvailable(),
|
|
322
|
+
gemini: geminiApiAvail || !!geminiWebAvail,
|
|
323
|
+
kimi: await isKimiSearchAvailable(ctx),
|
|
324
|
+
anysearch: isAnySearchAvailable(),
|
|
325
|
+
xcrawl: isXcrawlAvailable(),
|
|
326
|
+
xai: await isXaiSearchAvailable(ctx),
|
|
327
|
+
brightdata: isBrightDataAvailable(),
|
|
328
|
+
serpbase: isSerpBaseAvailable(),
|
|
329
|
+
serper: isSerperAvailable(),
|
|
330
|
+
valyu: isValyuAvailable()
|
|
331
|
+
};
|
|
332
|
+
const allSearchProviders = new Set(ALL_SEARCH_PROVIDERS);
|
|
333
|
+
return {
|
|
334
|
+
all: Object.entries(providers).some(([provider, available]) => provider !== "gemini" && allSearchProviders.has(provider) && available) || geminiApiAvail,
|
|
335
|
+
...providers
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
async function getOptionalGeminiWebAvailability() {
|
|
339
|
+
try {
|
|
340
|
+
return await isGeminiWebAvailable();
|
|
341
|
+
} catch {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function shouldUseOpenAICodexDefault(ctx) {
|
|
346
|
+
return ctx?.model?.provider === "openai-codex";
|
|
347
|
+
}
|
|
348
|
+
function shouldPreferOpenAI(options, preferOpenAICodexDefault) {
|
|
349
|
+
if (options?.recencyFilter)
|
|
350
|
+
return false;
|
|
351
|
+
if (typeof options?.numResults === "number" && Number.isFinite(options.numResults) && Math.floor(options.numResults) !== 5) {
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
return preferOpenAICodexDefault;
|
|
355
|
+
}
|
|
356
|
+
async function loadCuratorBootstrap(requestedProvider, ctx, options) {
|
|
357
|
+
const provider = resolveRequestedProvider(requestedProvider);
|
|
358
|
+
const availableProviders = await getProviderAvailability(ctx);
|
|
359
|
+
if (Array.isArray(provider))
|
|
360
|
+
availableProviders.all = true;
|
|
361
|
+
return {
|
|
362
|
+
availableProviders,
|
|
363
|
+
defaultProvider: resolveCuratorDefaultProvider(provider, availableProviders, ctx, options),
|
|
364
|
+
timeoutSeconds: getCuratorTimeoutSeconds()
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
export function resolveCuratorDefaultProvider(provider, available, ctx, options) {
|
|
368
|
+
return resolveProvider(provider, available, options, shouldUseOpenAICodexDefault(ctx), ctx);
|
|
369
|
+
}
|
|
370
|
+
function firstAvailableProvider(available, preferOpenAI, fallback) {
|
|
371
|
+
if (available.searxng)
|
|
372
|
+
return "searxng";
|
|
373
|
+
if (preferOpenAI && available.openai)
|
|
374
|
+
return "openai";
|
|
375
|
+
if (available.exa)
|
|
376
|
+
return "exa";
|
|
377
|
+
if (available.openai)
|
|
378
|
+
return "openai";
|
|
379
|
+
if (available.brave)
|
|
380
|
+
return "brave";
|
|
381
|
+
if (available.parallel)
|
|
382
|
+
return "parallel";
|
|
383
|
+
if (available.tinyfish)
|
|
384
|
+
return "tinyfish";
|
|
385
|
+
if (available.search1api)
|
|
386
|
+
return "search1api";
|
|
387
|
+
if (available.searchinfinity)
|
|
388
|
+
return "searchinfinity";
|
|
389
|
+
if (available.querit)
|
|
390
|
+
return "querit";
|
|
391
|
+
if (available.tavily)
|
|
392
|
+
return "tavily";
|
|
393
|
+
if (available.firecrawl)
|
|
394
|
+
return "firecrawl";
|
|
395
|
+
if (available.jina)
|
|
396
|
+
return "jina";
|
|
397
|
+
if (available.serpdive)
|
|
398
|
+
return "serpdive";
|
|
399
|
+
if (available.kagi)
|
|
400
|
+
return "kagi";
|
|
401
|
+
if (available.bocha)
|
|
402
|
+
return "bocha";
|
|
403
|
+
if (available.ollama)
|
|
404
|
+
return "ollama";
|
|
405
|
+
if (available.perplexity)
|
|
406
|
+
return "perplexity";
|
|
407
|
+
if (available.gemini)
|
|
408
|
+
return "gemini";
|
|
409
|
+
return fallback;
|
|
410
|
+
}
|
|
411
|
+
function resolveProvider(provider, available, options, preferOpenAICodexDefault = false, ctx) {
|
|
412
|
+
if (Array.isArray(provider))
|
|
413
|
+
return "all";
|
|
414
|
+
const preferOpenAI = shouldPreferOpenAI(options, preferOpenAICodexDefault);
|
|
415
|
+
if (provider === "auto") {
|
|
416
|
+
const routing = getConfiguredSearchRouting();
|
|
417
|
+
if (routing) {
|
|
418
|
+
for (const candidate of routing.providers) {
|
|
419
|
+
if (candidate === "openai" && routing.useCurrentModel === true && !isCurrentModelHostedSearchEligible(ctx))
|
|
420
|
+
continue;
|
|
421
|
+
if (available[candidate])
|
|
422
|
+
return candidate;
|
|
423
|
+
}
|
|
424
|
+
return routing.providers.find((candidate) => candidate !== "openai" || routing.useCurrentModel !== true || isCurrentModelHostedSearchEligible(ctx)) ?? routing.providers[0];
|
|
425
|
+
}
|
|
426
|
+
return firstAvailableProvider(available, preferOpenAI, "exa");
|
|
427
|
+
}
|
|
428
|
+
if (provider === "all" && !available.all) {
|
|
429
|
+
return firstAvailableProvider(available, preferOpenAI, "exa");
|
|
430
|
+
}
|
|
431
|
+
if (provider === "openai" && !available.openai) {
|
|
432
|
+
return firstAvailableProvider(available, false, "openai");
|
|
433
|
+
}
|
|
434
|
+
if (provider === "brave" && !available.brave) {
|
|
435
|
+
return firstAvailableProvider(available, preferOpenAI, "brave");
|
|
436
|
+
}
|
|
437
|
+
if (provider === "parallel" && !available.parallel) {
|
|
438
|
+
return firstAvailableProvider(available, preferOpenAI, "parallel");
|
|
439
|
+
}
|
|
440
|
+
if (provider === "tinyfish" && !available.tinyfish) {
|
|
441
|
+
return firstAvailableProvider(available, preferOpenAI, "tinyfish");
|
|
442
|
+
}
|
|
443
|
+
if (provider === "search1api" && !available.search1api) {
|
|
444
|
+
return firstAvailableProvider(available, preferOpenAI, "search1api");
|
|
445
|
+
}
|
|
446
|
+
if (provider === "searchinfinity" && !available.searchinfinity) {
|
|
447
|
+
return firstAvailableProvider(available, preferOpenAI, "searchinfinity");
|
|
448
|
+
}
|
|
449
|
+
if (provider === "querit" && !available.querit) {
|
|
450
|
+
return firstAvailableProvider(available, preferOpenAI, "querit");
|
|
451
|
+
}
|
|
452
|
+
if (provider === "tavily" && !available.tavily) {
|
|
453
|
+
return firstAvailableProvider(available, preferOpenAI, "tavily");
|
|
454
|
+
}
|
|
455
|
+
if (provider === "firecrawl" && !available.firecrawl) {
|
|
456
|
+
return firstAvailableProvider(available, preferOpenAI, "firecrawl");
|
|
457
|
+
}
|
|
458
|
+
if (provider === "jina" && !available.jina) {
|
|
459
|
+
return firstAvailableProvider(available, preferOpenAI, "jina");
|
|
460
|
+
}
|
|
461
|
+
if (provider === "serpdive" && !available.serpdive) {
|
|
462
|
+
return firstAvailableProvider(available, preferOpenAI, "serpdive");
|
|
463
|
+
}
|
|
464
|
+
if (provider === "kagi" && !available.kagi) {
|
|
465
|
+
return firstAvailableProvider(available, preferOpenAI, "kagi");
|
|
466
|
+
}
|
|
467
|
+
if (provider === "bocha" && !available.bocha) {
|
|
468
|
+
return firstAvailableProvider(available, preferOpenAI, "bocha");
|
|
469
|
+
}
|
|
470
|
+
if (provider === "ollama" && !available.ollama) {
|
|
471
|
+
return firstAvailableProvider(available, preferOpenAI, "ollama");
|
|
472
|
+
}
|
|
473
|
+
if (provider === "searxng" && !available.searxng) {
|
|
474
|
+
return firstAvailableProvider(available, preferOpenAI, "searxng");
|
|
475
|
+
}
|
|
476
|
+
if (provider === "exa" && !available.exa) {
|
|
477
|
+
return firstAvailableProvider(available, preferOpenAI, "exa");
|
|
478
|
+
}
|
|
479
|
+
if (provider === "perplexity" && !available.perplexity) {
|
|
480
|
+
return firstAvailableProvider(available, preferOpenAI, "perplexity");
|
|
481
|
+
}
|
|
482
|
+
if (provider === "gemini" && !available.gemini) {
|
|
483
|
+
return firstAvailableProvider(available, preferOpenAI, "gemini");
|
|
484
|
+
}
|
|
485
|
+
return provider;
|
|
486
|
+
}
|
|
487
|
+
const pendingFetches = new Map;
|
|
488
|
+
let sessionActive = false;
|
|
489
|
+
let widgetVisible = false;
|
|
490
|
+
let widgetUnsubscribe = null;
|
|
491
|
+
const pendingCurates = new Map;
|
|
492
|
+
const activeCurators = new Map;
|
|
493
|
+
const glimpseWins = new Map;
|
|
494
|
+
const DEFAULT_MAX_INLINE_CONTENT_CHARS = 30000;
|
|
495
|
+
const MAX_INLINE_CONTENT_CHARS = 200000;
|
|
496
|
+
function getMaxInlineContentChars(config = loadConfig()) {
|
|
497
|
+
const value = config.maxInlineContentChars;
|
|
498
|
+
if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
|
|
499
|
+
return DEFAULT_MAX_INLINE_CONTENT_CHARS;
|
|
500
|
+
}
|
|
501
|
+
return Math.min(value, MAX_INLINE_CONTENT_CHARS);
|
|
502
|
+
}
|
|
503
|
+
function stripThumbnails(results) {
|
|
504
|
+
return results.map(({ thumbnail, frames, ...rest }) => rest);
|
|
505
|
+
}
|
|
506
|
+
function storeFetchResult(pi, responseId, data, authProfile) {
|
|
507
|
+
if (authProfile?.cache === "off")
|
|
508
|
+
return false;
|
|
509
|
+
pi.appendEntry("web-search-results", storeFetchedContentResult(responseId, data));
|
|
510
|
+
return true;
|
|
511
|
+
}
|
|
512
|
+
function initialContentSlice(content, maxChars) {
|
|
513
|
+
let endOffset = Math.min(content.length, maxChars);
|
|
514
|
+
if (endOffset < content.length) {
|
|
515
|
+
const lineBreak = content.lastIndexOf(`
|
|
516
|
+
`, endOffset);
|
|
517
|
+
if (lineBreak >= Math.floor(maxChars * 0.8))
|
|
518
|
+
endOffset = lineBreak + 1;
|
|
519
|
+
}
|
|
520
|
+
const text = content.slice(0, endOffset);
|
|
521
|
+
return {
|
|
522
|
+
text,
|
|
523
|
+
endOffset,
|
|
524
|
+
totalBytes: Buffer.byteLength(content),
|
|
525
|
+
totalLines: content.length === 0 ? 0 : content.split(`
|
|
526
|
+
`).length,
|
|
527
|
+
shownBytes: Buffer.byteLength(text),
|
|
528
|
+
shownLines: text.length === 0 ? 0 : text.split(`
|
|
529
|
+
`).length
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
function normalizeFindQueries(value) {
|
|
533
|
+
const queries = (Array.isArray(value) ? value : [value]).map((query) => query.trim()).filter(Boolean);
|
|
534
|
+
if (queries.length === 0)
|
|
535
|
+
throw new Error("findText must contain at least one non-empty string");
|
|
536
|
+
return queries;
|
|
537
|
+
}
|
|
538
|
+
function normalizeFindMode(value) {
|
|
539
|
+
if (value === undefined)
|
|
540
|
+
return;
|
|
541
|
+
if (value === "exact" || value === "case-insensitive" || value === "fuzzy")
|
|
542
|
+
return value;
|
|
543
|
+
throw new Error('findMode must be "exact", "case-insensitive", or "fuzzy"');
|
|
544
|
+
}
|
|
545
|
+
function normalizeGetSearchContentParams(params) {
|
|
546
|
+
const normalized = { ...params, findMode: normalizeFindMode(params.findMode) };
|
|
547
|
+
if (normalized.query?.trim() === "")
|
|
548
|
+
delete normalized.query;
|
|
549
|
+
if (normalized.url?.trim() === "")
|
|
550
|
+
delete normalized.url;
|
|
551
|
+
if (normalized.findText !== undefined) {
|
|
552
|
+
delete normalized.offset;
|
|
553
|
+
delete normalized.limit;
|
|
554
|
+
}
|
|
555
|
+
return normalized;
|
|
556
|
+
}
|
|
557
|
+
function formatInputValue(value) {
|
|
558
|
+
if (typeof value === "string")
|
|
559
|
+
return JSON.stringify(value);
|
|
560
|
+
if (typeof value === "number")
|
|
561
|
+
return Number.isNaN(value) ? "NaN" : String(value);
|
|
562
|
+
try {
|
|
563
|
+
const serialized = JSON.stringify(value);
|
|
564
|
+
return serialized === undefined ? String(value) : serialized;
|
|
565
|
+
} catch {
|
|
566
|
+
return String(value);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
function formatSearchSummary(results, answer) {
|
|
570
|
+
if (results.length === 0) {
|
|
571
|
+
return answer ? `${answer}
|
|
572
|
+
|
|
573
|
+
---
|
|
574
|
+
|
|
575
|
+
**Sources:**
|
|
576
|
+
No sources returned.` : "No results found.";
|
|
577
|
+
}
|
|
578
|
+
let output = answer ? `${answer}
|
|
579
|
+
|
|
580
|
+
---
|
|
581
|
+
|
|
582
|
+
**Sources:**
|
|
583
|
+
` : "";
|
|
584
|
+
output += results.map((r, i) => `${i + 1}. ${r.title}
|
|
585
|
+
${r.url}`).join(`
|
|
586
|
+
|
|
587
|
+
`);
|
|
588
|
+
return output;
|
|
589
|
+
}
|
|
590
|
+
function formatSourceCheckResult(artifact, getSearchContentTool = DEFAULT_TOOL_NAMES.getSearchContent) {
|
|
591
|
+
const assessment = artifact.claims?.[0];
|
|
592
|
+
const lines = [`# Source check: ${artifact.query}`, ""];
|
|
593
|
+
if (assessment) {
|
|
594
|
+
lines.push(`**Status:** ${assessment.status} (confidence ${assessment.confidence.toFixed(2)})`);
|
|
595
|
+
lines.push(`**Rationale:** ${assessment.rationale}`);
|
|
596
|
+
if (assessment.supporting_passages.length > 0)
|
|
597
|
+
lines.push(`**Supporting passages:** ${assessment.supporting_passages.join(", ")}`);
|
|
598
|
+
if (assessment.contradicting_passages.length > 0)
|
|
599
|
+
lines.push(`**Contradicting passages:** ${assessment.contradicting_passages.join(", ")}`);
|
|
600
|
+
lines.push("");
|
|
601
|
+
}
|
|
602
|
+
if (artifact.sources.length > 0) {
|
|
603
|
+
lines.push("## Sources");
|
|
604
|
+
for (const source of artifact.sources)
|
|
605
|
+
lines.push(`${source.rank}. [${source.quality}] ${source.title}
|
|
606
|
+
${source.url}`);
|
|
607
|
+
lines.push("");
|
|
608
|
+
}
|
|
609
|
+
if (artifact.errors?.length)
|
|
610
|
+
lines.push(`Search errors: ${artifact.errors.map((entry) => `${entry.query}: ${entry.error}`).join("; ")}`);
|
|
611
|
+
lines.push(getSearchContentTool ? `Artifact responseId: ${artifact.id} (retrievable via ${getSearchContentTool}).` : `Artifact responseId: ${artifact.id}. Content retrieval is not registered.`);
|
|
612
|
+
return lines.join(`
|
|
613
|
+
`);
|
|
614
|
+
}
|
|
615
|
+
function duplicateQuerySet(results) {
|
|
616
|
+
const counts = new Map;
|
|
617
|
+
for (const result of results) {
|
|
618
|
+
counts.set(result.query, (counts.get(result.query) ?? 0) + 1);
|
|
619
|
+
}
|
|
620
|
+
const duplicates = new Set;
|
|
621
|
+
for (const [query, count] of counts) {
|
|
622
|
+
if (count > 1)
|
|
623
|
+
duplicates.add(query);
|
|
624
|
+
}
|
|
625
|
+
return duplicates;
|
|
626
|
+
}
|
|
627
|
+
function formatQueryHeader(query, provider, duplicateQueries) {
|
|
628
|
+
const suffix = duplicateQueries.has(query) && provider ? ` (${provider})` : "";
|
|
629
|
+
return `## Query: "${query}"${suffix}
|
|
630
|
+
|
|
631
|
+
`;
|
|
632
|
+
}
|
|
633
|
+
function hasFullInlineCoverage(urls, inlineContent) {
|
|
634
|
+
if (!inlineContent || inlineContent.length === 0)
|
|
635
|
+
return false;
|
|
636
|
+
const coveredUrls = new Set(inlineContent.map((c) => c.url));
|
|
637
|
+
return urls.every((url) => coveredUrls.has(url));
|
|
638
|
+
}
|
|
639
|
+
function formatFullResults(queryData) {
|
|
640
|
+
let output = `## Results for: "${queryData.query}"
|
|
641
|
+
|
|
642
|
+
`;
|
|
643
|
+
if (queryData.answer) {
|
|
644
|
+
output += `${queryData.answer}
|
|
645
|
+
|
|
646
|
+
---
|
|
647
|
+
|
|
648
|
+
`;
|
|
649
|
+
}
|
|
650
|
+
for (const r of queryData.results) {
|
|
651
|
+
output += `### ${r.title}
|
|
652
|
+
${r.url}
|
|
653
|
+
|
|
654
|
+
`;
|
|
655
|
+
}
|
|
656
|
+
return output;
|
|
657
|
+
}
|
|
658
|
+
function abortPendingFetches() {
|
|
659
|
+
for (const controller of pendingFetches.values()) {
|
|
660
|
+
controller.abort();
|
|
661
|
+
}
|
|
662
|
+
pendingFetches.clear();
|
|
663
|
+
}
|
|
664
|
+
function closeCurator(callId) {
|
|
665
|
+
if (callId !== undefined) {
|
|
666
|
+
const win = glimpseWins.get(callId);
|
|
667
|
+
glimpseWins.delete(callId);
|
|
668
|
+
try {
|
|
669
|
+
win?.close();
|
|
670
|
+
} catch {}
|
|
671
|
+
pendingCurates.get(callId)?.cancel("stale");
|
|
672
|
+
pendingCurates.delete(callId);
|
|
673
|
+
const curator = activeCurators.get(callId);
|
|
674
|
+
activeCurators.delete(callId);
|
|
675
|
+
try {
|
|
676
|
+
curator?.close();
|
|
677
|
+
} catch {}
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
for (const win of glimpseWins.values()) {
|
|
681
|
+
try {
|
|
682
|
+
win.close();
|
|
683
|
+
} catch {}
|
|
684
|
+
}
|
|
685
|
+
glimpseWins.clear();
|
|
686
|
+
for (const pc of pendingCurates.values()) {
|
|
687
|
+
try {
|
|
688
|
+
pc.cancel("stale");
|
|
689
|
+
} catch {}
|
|
690
|
+
}
|
|
691
|
+
pendingCurates.clear();
|
|
692
|
+
for (const curator of activeCurators.values()) {
|
|
693
|
+
try {
|
|
694
|
+
curator.close();
|
|
695
|
+
} catch {}
|
|
696
|
+
}
|
|
697
|
+
activeCurators.clear();
|
|
698
|
+
}
|
|
699
|
+
async function openInBrowser(pi, url) {
|
|
700
|
+
const plat = platform();
|
|
701
|
+
if (plat !== "darwin" && plat !== "win32") {
|
|
702
|
+
await new Promise((resolve, reject) => {
|
|
703
|
+
const child = spawn("xdg-open", [url], { detached: true, stdio: "ignore" });
|
|
704
|
+
const timer = setTimeout(resolve, 100);
|
|
705
|
+
child.once("error", (err) => {
|
|
706
|
+
clearTimeout(timer);
|
|
707
|
+
reject(err);
|
|
708
|
+
});
|
|
709
|
+
child.once("exit", (code) => {
|
|
710
|
+
clearTimeout(timer);
|
|
711
|
+
if (code === 0)
|
|
712
|
+
resolve();
|
|
713
|
+
else
|
|
714
|
+
reject(new Error(`Failed to open browser (exit code ${code ?? "unknown"})`));
|
|
715
|
+
});
|
|
716
|
+
child.unref();
|
|
717
|
+
});
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
const result = plat === "darwin" ? await pi.exec("open", [url]) : await pi.exec("cmd", ["/c", "start", "", url]);
|
|
721
|
+
if (result.code !== 0) {
|
|
722
|
+
throw new Error(result.stderr || `Failed to open browser (exit code ${result.code})`);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
let glimpseOpen;
|
|
726
|
+
function findGlimpseMjs() {
|
|
727
|
+
try {
|
|
728
|
+
const req = createRequire(import.meta.url);
|
|
729
|
+
return req.resolve("glimpseui");
|
|
730
|
+
} catch {}
|
|
731
|
+
try {
|
|
732
|
+
const globalRoot = execFileSync("npm", ["root", "-g"], { encoding: "utf-8" }).trim();
|
|
733
|
+
const entry = join(globalRoot, "glimpseui", "src", "glimpse.mjs");
|
|
734
|
+
if (existsSync(entry))
|
|
735
|
+
return entry;
|
|
736
|
+
} catch {}
|
|
737
|
+
return null;
|
|
738
|
+
}
|
|
739
|
+
async function getGlimpseOpen() {
|
|
740
|
+
if (glimpseOpen !== undefined)
|
|
741
|
+
return glimpseOpen;
|
|
742
|
+
const resolved = findGlimpseMjs();
|
|
743
|
+
if (resolved) {
|
|
744
|
+
try {
|
|
745
|
+
glimpseOpen = (await import(resolved)).open;
|
|
746
|
+
return glimpseOpen;
|
|
747
|
+
} catch {}
|
|
748
|
+
}
|
|
749
|
+
glimpseOpen = null;
|
|
750
|
+
return glimpseOpen;
|
|
751
|
+
}
|
|
752
|
+
function openInGlimpse(open, url, title) {
|
|
753
|
+
const shellHTML = `<!DOCTYPE html>
|
|
754
|
+
<html>
|
|
755
|
+
<head><meta charset="UTF-8"><title>${title}</title></head>
|
|
756
|
+
<body style="margin:0; background:#1a1a2e;">
|
|
757
|
+
<script>window.location.replace(${JSON.stringify(url)});</script>
|
|
758
|
+
</body>
|
|
759
|
+
</html>`;
|
|
760
|
+
const win = open(shellHTML, {
|
|
761
|
+
width: 800,
|
|
762
|
+
height: 900,
|
|
763
|
+
title
|
|
764
|
+
});
|
|
765
|
+
let maxHeight = 1200;
|
|
766
|
+
win.on("ready", (info) => {
|
|
767
|
+
const visibleHeight = info?.screen?.visibleHeight;
|
|
768
|
+
if (typeof visibleHeight === "number" && visibleHeight > 0) {
|
|
769
|
+
maxHeight = Math.floor(visibleHeight * 0.85);
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
win.on("message", (data) => {
|
|
773
|
+
if (!data || typeof data !== "object")
|
|
774
|
+
return;
|
|
775
|
+
const msg = data;
|
|
776
|
+
if (msg.type !== "resize" || typeof msg.height !== "number")
|
|
777
|
+
return;
|
|
778
|
+
const clamped = Math.max(400, Math.min(Math.round(msg.height), maxHeight));
|
|
779
|
+
win._write({ type: "resize", width: 800, height: clamped });
|
|
780
|
+
});
|
|
781
|
+
return win;
|
|
782
|
+
}
|
|
783
|
+
function extractDomain(url) {
|
|
784
|
+
try {
|
|
785
|
+
return new URL(url).hostname;
|
|
786
|
+
} catch {
|
|
787
|
+
return url;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
function toCuratorSearchEntries(response) {
|
|
791
|
+
const providerResponses = response.provider === "all" && response.providerResponses?.length ? response.providerResponses : [response];
|
|
792
|
+
const entries = providerResponses.map((result) => ({
|
|
793
|
+
answer: result.answer,
|
|
794
|
+
results: result.results.map((source) => ({ ...source, domain: extractDomain(source.url) })),
|
|
795
|
+
provider: result.provider
|
|
796
|
+
}));
|
|
797
|
+
for (const failure of response.providerErrors ?? []) {
|
|
798
|
+
entries.push({
|
|
799
|
+
answer: "",
|
|
800
|
+
results: [],
|
|
801
|
+
provider: failure.provider,
|
|
802
|
+
error: failure.error
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
return entries;
|
|
806
|
+
}
|
|
807
|
+
function indexedCuratorEntryToQueryResult(entry) {
|
|
808
|
+
return {
|
|
809
|
+
query: entry.query,
|
|
810
|
+
answer: entry.answer,
|
|
811
|
+
results: entry.results.map((source) => ({
|
|
812
|
+
title: source.title,
|
|
813
|
+
url: source.url,
|
|
814
|
+
snippet: source.snippet ?? ""
|
|
815
|
+
})),
|
|
816
|
+
error: entry.error ?? null,
|
|
817
|
+
provider: entry.provider
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
function updateWidget(ctx) {
|
|
821
|
+
const theme = ctx.ui.theme;
|
|
822
|
+
const entries = activityMonitor.getEntries();
|
|
823
|
+
const lines = [];
|
|
824
|
+
lines.push(theme.fg("accent", "─── Web Search Activity " + "─".repeat(36)));
|
|
825
|
+
if (entries.length === 0) {
|
|
826
|
+
lines.push(theme.fg("muted", " No activity yet"));
|
|
827
|
+
} else {
|
|
828
|
+
for (const e of entries) {
|
|
829
|
+
lines.push(" " + formatEntryLine(e, theme));
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
lines.push(theme.fg("accent", "─".repeat(60)));
|
|
833
|
+
const rateInfo = activityMonitor.getRateLimitInfo();
|
|
834
|
+
const resetMs = rateInfo.oldestTimestamp ? Math.max(0, rateInfo.oldestTimestamp + rateInfo.windowMs - Date.now()) : 0;
|
|
835
|
+
const resetSec = Math.ceil(resetMs / 1000);
|
|
836
|
+
lines.push(theme.fg("muted", `Rate: ${rateInfo.used}/${rateInfo.max}`) + (resetMs > 0 ? theme.fg("dim", ` (resets in ${resetSec}s)`) : ""));
|
|
837
|
+
ctx.ui.setWidget("web-activity", lines);
|
|
838
|
+
}
|
|
839
|
+
function formatEntryLine(entry, theme) {
|
|
840
|
+
const typeStr = entry.type === "api" ? "API" : "GET";
|
|
841
|
+
const target = entry.type === "api" ? `"${truncateToWidth(entry.query || "", 28, "")}"` : truncateToWidth(entry.url?.replace(/^https?:\/\//, "") || "", 30, "");
|
|
842
|
+
const duration = entry.endTime ? `${((entry.endTime - entry.startTime) / 1000).toFixed(1)}s` : `${((Date.now() - entry.startTime) / 1000).toFixed(1)}s`;
|
|
843
|
+
let statusStr;
|
|
844
|
+
let indicator;
|
|
845
|
+
if (entry.error) {
|
|
846
|
+
statusStr = "err";
|
|
847
|
+
indicator = theme.fg("error", "✗");
|
|
848
|
+
} else if (entry.status === null) {
|
|
849
|
+
statusStr = "...";
|
|
850
|
+
indicator = theme.fg("warning", "⋯");
|
|
851
|
+
} else if (entry.status === 0) {
|
|
852
|
+
statusStr = "abort";
|
|
853
|
+
indicator = theme.fg("muted", "○");
|
|
854
|
+
} else {
|
|
855
|
+
statusStr = String(entry.status);
|
|
856
|
+
indicator = entry.status >= 200 && entry.status < 300 ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
|
857
|
+
}
|
|
858
|
+
return `${typeStr.padEnd(4)} ${target.padEnd(32)} ${statusStr.padStart(5)} ${duration.padStart(5)} ${indicator}`;
|
|
859
|
+
}
|
|
860
|
+
function handleSessionChange(ctx) {
|
|
861
|
+
abortPendingFetches();
|
|
862
|
+
closeCurator();
|
|
863
|
+
clearCloneCache();
|
|
864
|
+
sessionActive = true;
|
|
865
|
+
restoreFromSession(ctx);
|
|
866
|
+
widgetUnsubscribe?.();
|
|
867
|
+
widgetUnsubscribe = null;
|
|
868
|
+
activityMonitor.clear();
|
|
869
|
+
if (widgetVisible) {
|
|
870
|
+
widgetUnsubscribe = activityMonitor.onUpdate(() => updateWidget(ctx));
|
|
871
|
+
updateWidget(ctx);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
export default function dm_web_access_default(pi) {
|
|
875
|
+
const initConfig = loadConfigForExtensionInit();
|
|
876
|
+
installGlobalProxyFetch();
|
|
877
|
+
const toolNames = resolveToolNames(initConfig);
|
|
878
|
+
const webSearchEnabled = isToolEnabled(initConfig, "webSearch");
|
|
879
|
+
const sourceCheckEnabled = isToolEnabled(initConfig, "sourceCheck");
|
|
880
|
+
const fetchContentEnabled = isToolEnabled(initConfig, "fetchContent");
|
|
881
|
+
const getSearchContentEnabled = isToolEnabled(initConfig, "getSearchContent");
|
|
882
|
+
const registeredToolNames = {
|
|
883
|
+
...webSearchEnabled ? { webSearch: toolNames.webSearch } : {},
|
|
884
|
+
...fetchContentEnabled ? { fetchContent: toolNames.fetchContent } : {}
|
|
885
|
+
};
|
|
886
|
+
const storedContentSources = joinToolNames([
|
|
887
|
+
...webSearchEnabled ? [toolNames.webSearch] : [],
|
|
888
|
+
...sourceCheckEnabled ? [toolNames.sourceCheck] : [],
|
|
889
|
+
...fetchContentEnabled ? [toolNames.fetchContent] : []
|
|
890
|
+
]);
|
|
891
|
+
const searchQueryDescription = webSearchEnabled ? `Get content for this query (${toolNames.webSearch})` : "Get content for a stored search query";
|
|
892
|
+
const fetchContentStorageNote = getSearchContentEnabled ? `Full original content is stored for retrieval with ${toolNames.getSearchContent}.` : "Full original content is stored internally, but the retrieval tool is not registered.";
|
|
893
|
+
const curateKey = initConfig.shortcuts?.curate || DEFAULT_SHORTCUTS.curate;
|
|
894
|
+
const activityKey = initConfig.shortcuts?.activity || DEFAULT_SHORTCUTS.activity;
|
|
895
|
+
function startBackgroundFetch(urls, proxy) {
|
|
896
|
+
if (urls.length === 0)
|
|
897
|
+
return null;
|
|
898
|
+
const fetchId = generateId();
|
|
899
|
+
const controller = new AbortController;
|
|
900
|
+
pendingFetches.set(fetchId, controller);
|
|
901
|
+
runWithProxy(proxy, () => fetchAllContent(urls, controller.signal, withRegisteredFetchOptions(undefined, registeredToolNames, proxy))).then((fetched) => {
|
|
902
|
+
if (!sessionActive || !pendingFetches.has(fetchId))
|
|
903
|
+
return;
|
|
904
|
+
const data = {
|
|
905
|
+
id: fetchId,
|
|
906
|
+
type: "fetch",
|
|
907
|
+
timestamp: Date.now(),
|
|
908
|
+
urls: stripThumbnails(fetched)
|
|
909
|
+
};
|
|
910
|
+
pi.appendEntry("web-search-results", storeFetchedContentResult(fetchId, data));
|
|
911
|
+
const ok = fetched.filter((f) => !f.error).length;
|
|
912
|
+
const availability = ok === fetched.length ? "Full page content now available." : ok > 0 ? "Partial page content now available." : "No page content was fetched. Stored fetch diagnostics are available.";
|
|
913
|
+
pi.sendMessage({
|
|
914
|
+
customType: "web-search-content-ready",
|
|
915
|
+
content: `Content fetched for ${ok}/${fetched.length} URLs [${fetchId}]. ${availability}`,
|
|
916
|
+
display: true
|
|
917
|
+
}, { triggerTurn: true });
|
|
918
|
+
}).catch((err) => {
|
|
919
|
+
if (!sessionActive || !pendingFetches.has(fetchId))
|
|
920
|
+
return;
|
|
921
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
922
|
+
const isAbort = err instanceof Error && err.name === "AbortError" || message.toLowerCase().includes("abort");
|
|
923
|
+
if (!isAbort) {
|
|
924
|
+
pi.sendMessage({
|
|
925
|
+
customType: "web-search-error",
|
|
926
|
+
content: `Content fetch failed [${fetchId}]: ${message}`,
|
|
927
|
+
display: true
|
|
928
|
+
}, { triggerTurn: false });
|
|
929
|
+
}
|
|
930
|
+
}).finally(() => {
|
|
931
|
+
pendingFetches.delete(fetchId);
|
|
932
|
+
});
|
|
933
|
+
return fetchId;
|
|
934
|
+
}
|
|
935
|
+
function storeAndPublishSearch(results) {
|
|
936
|
+
const id = generateId();
|
|
937
|
+
const data = {
|
|
938
|
+
id,
|
|
939
|
+
type: "search",
|
|
940
|
+
timestamp: Date.now(),
|
|
941
|
+
queries: results
|
|
942
|
+
};
|
|
943
|
+
storeResult(id, data);
|
|
944
|
+
pi.appendEntry("web-search-results", data);
|
|
945
|
+
return id;
|
|
946
|
+
}
|
|
947
|
+
function normalizeSummaryMeta(meta, summaryText) {
|
|
948
|
+
const normalizedText = summaryText.trim();
|
|
949
|
+
if (!meta) {
|
|
950
|
+
return {
|
|
951
|
+
model: null,
|
|
952
|
+
durationMs: 0,
|
|
953
|
+
tokenEstimate: normalizedText.length > 0 ? Math.max(1, Math.ceil(normalizedText.length / 4)) : 0,
|
|
954
|
+
fallbackUsed: false,
|
|
955
|
+
edited: false
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
return {
|
|
959
|
+
model: meta.model,
|
|
960
|
+
durationMs: Number.isFinite(meta.durationMs) && meta.durationMs >= 0 ? meta.durationMs : 0,
|
|
961
|
+
tokenEstimate: Number.isFinite(meta.tokenEstimate) && meta.tokenEstimate >= 0 ? meta.tokenEstimate : normalizedText.length > 0 ? Math.max(1, Math.ceil(normalizedText.length / 4)) : 0,
|
|
962
|
+
fallbackUsed: meta.fallbackUsed === true,
|
|
963
|
+
fallbackReason: meta.fallbackReason,
|
|
964
|
+
phase: meta.phase,
|
|
965
|
+
edited: meta.edited === true
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
function buildCurationCancelledReturn(reason, partial) {
|
|
969
|
+
const message = `Search curation cancelled (${reason}).`;
|
|
970
|
+
const cancelledQueries = partial?.queries?.length ? partial.queries.map((q) => ({
|
|
971
|
+
query: q.query,
|
|
972
|
+
provider: q.provider ?? null,
|
|
973
|
+
error: q.error,
|
|
974
|
+
resultCount: q.results?.length ?? 0
|
|
975
|
+
})) : undefined;
|
|
976
|
+
const extraLines = [];
|
|
977
|
+
if (partial?.curatorUrl)
|
|
978
|
+
extraLines.push(`curator: ${partial.curatorUrl}`);
|
|
979
|
+
if (partial?.browserOpenError)
|
|
980
|
+
extraLines.push(`browser open error: ${partial.browserOpenError}`);
|
|
981
|
+
return {
|
|
982
|
+
content: [{ type: "text", text: message }],
|
|
983
|
+
details: {
|
|
984
|
+
error: message,
|
|
985
|
+
cancelled: true,
|
|
986
|
+
cancelReason: reason,
|
|
987
|
+
browserConnected: partial?.browserConnected,
|
|
988
|
+
lastHeartbeatAgeMs: partial?.lastHeartbeatAgeMs,
|
|
989
|
+
queryCount: partial?.queryCount,
|
|
990
|
+
cancelledQueries,
|
|
991
|
+
extraLines: extraLines.length > 0 ? extraLines : undefined
|
|
992
|
+
}
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
async function generateSummaryForSelectedIndices(selectedQueryIndices, resultsByIndex, summaryContext, signal, modelOverride, feedback) {
|
|
996
|
+
const selectedResults = [];
|
|
997
|
+
for (const qi of selectedQueryIndices) {
|
|
998
|
+
const result = resultsByIndex.get(qi);
|
|
999
|
+
if (result)
|
|
1000
|
+
selectedResults.push(result);
|
|
1001
|
+
}
|
|
1002
|
+
if (selectedResults.length === 0) {
|
|
1003
|
+
throw new Error("No selected results available for summary generation");
|
|
1004
|
+
}
|
|
1005
|
+
try {
|
|
1006
|
+
return await generateSummaryDraft(selectedResults, summaryContext, signal, modelOverride, feedback, undefined, getSummaryGenerationDeadlineMs());
|
|
1007
|
+
} catch (err) {
|
|
1008
|
+
const isEmptyResponse = err instanceof Error && err.message.includes("Summary model returned empty response");
|
|
1009
|
+
if (!isEmptyResponse)
|
|
1010
|
+
throw err;
|
|
1011
|
+
const deterministic = buildDeterministicSummary(selectedResults);
|
|
1012
|
+
return {
|
|
1013
|
+
summary: deterministic.summary,
|
|
1014
|
+
meta: {
|
|
1015
|
+
...deterministic.meta,
|
|
1016
|
+
fallbackReason: "summary-model-empty-response"
|
|
1017
|
+
}
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
async function loadSummaryModelChoices(summaryContext) {
|
|
1022
|
+
const summaryModels = [];
|
|
1023
|
+
const seen = new Set;
|
|
1024
|
+
const availableValues = new Set;
|
|
1025
|
+
const addModel = (provider, id) => {
|
|
1026
|
+
const value = `${provider}/${id}`;
|
|
1027
|
+
if (seen.has(value))
|
|
1028
|
+
return;
|
|
1029
|
+
seen.add(value);
|
|
1030
|
+
summaryModels.push({ value, label: value });
|
|
1031
|
+
};
|
|
1032
|
+
let enabledModelPatterns = null;
|
|
1033
|
+
let scopeLoaded = true;
|
|
1034
|
+
try {
|
|
1035
|
+
enabledModelPatterns = loadEnabledModelPatterns(summaryContext);
|
|
1036
|
+
const availableModels = summaryContext.modelRegistry.getAvailable();
|
|
1037
|
+
for (const model of availableModels) {
|
|
1038
|
+
if (!modelMatchesEnabledPatterns(model, enabledModelPatterns))
|
|
1039
|
+
continue;
|
|
1040
|
+
const value = `${model.provider}/${model.id}`;
|
|
1041
|
+
availableValues.add(value);
|
|
1042
|
+
addModel(model.provider, model.id);
|
|
1043
|
+
}
|
|
1044
|
+
} catch (err) {
|
|
1045
|
+
scopeLoaded = false;
|
|
1046
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1047
|
+
console.error(`Failed to load summary models: ${message}`);
|
|
1048
|
+
}
|
|
1049
|
+
const currentModelValue = summaryContext.model ? `${summaryContext.model.provider}/${summaryContext.model.id}` : null;
|
|
1050
|
+
if (scopeLoaded && summaryContext.model && currentModelValue && !seen.has(currentModelValue) && modelMatchesEnabledPatterns(summaryContext.model, enabledModelPatterns)) {
|
|
1051
|
+
addModel(summaryContext.model.provider, summaryContext.model.id);
|
|
1052
|
+
}
|
|
1053
|
+
const config = loadConfig();
|
|
1054
|
+
const configuredSummaryModel = typeof config.summaryModel === "string" ? config.summaryModel.trim() : "";
|
|
1055
|
+
const preferredDefaults = [
|
|
1056
|
+
{ provider: "anthropic", id: "claude-haiku-4-5" },
|
|
1057
|
+
{ provider: "openai-codex", id: "gpt-5.6-luna" },
|
|
1058
|
+
{ provider: "openai-codex", id: "gpt-5.6-terra" },
|
|
1059
|
+
{ provider: "google", id: "gemini-3.6-flash" },
|
|
1060
|
+
{ provider: "openai", id: "gpt-5-mini" },
|
|
1061
|
+
{ provider: "deepseek", id: "deepseek-v4-flash" }
|
|
1062
|
+
];
|
|
1063
|
+
const resolveAvailableModelValue = (selector) => {
|
|
1064
|
+
const parsed = splitThinkingSuffix(selector);
|
|
1065
|
+
const slashIndex = parsed.value.indexOf("/");
|
|
1066
|
+
if (slashIndex <= 0 || slashIndex >= parsed.value.length - 1)
|
|
1067
|
+
return null;
|
|
1068
|
+
const model = findModelWithProviderRouting(summaryContext.modelRegistry, parsed.value.slice(0, slashIndex), parsed.value.slice(slashIndex + 1));
|
|
1069
|
+
if (!model)
|
|
1070
|
+
return null;
|
|
1071
|
+
const value = `${model.provider}/${model.id}`;
|
|
1072
|
+
if (!availableValues.has(value))
|
|
1073
|
+
return null;
|
|
1074
|
+
if (selector !== value && !seen.has(selector)) {
|
|
1075
|
+
seen.add(selector);
|
|
1076
|
+
summaryModels.push({ value: selector, label: selector });
|
|
1077
|
+
}
|
|
1078
|
+
return selector;
|
|
1079
|
+
};
|
|
1080
|
+
let defaultSummaryModel = null;
|
|
1081
|
+
if (scopeLoaded && configuredSummaryModel.length > 0) {
|
|
1082
|
+
defaultSummaryModel = availableValues.has(configuredSummaryModel) ? configuredSummaryModel : resolveAvailableModelValue(configuredSummaryModel);
|
|
1083
|
+
}
|
|
1084
|
+
if (scopeLoaded && !defaultSummaryModel) {
|
|
1085
|
+
for (const preferred of preferredDefaults) {
|
|
1086
|
+
const model = findModelWithProviderRouting(summaryContext.modelRegistry, preferred.provider, preferred.id);
|
|
1087
|
+
const value = model ? `${model.provider}/${model.id}` : null;
|
|
1088
|
+
if (value && availableValues.has(value)) {
|
|
1089
|
+
defaultSummaryModel = value;
|
|
1090
|
+
break;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
return { summaryModels, defaultSummaryModel };
|
|
1095
|
+
}
|
|
1096
|
+
function resolveSummaryForSubmit(payload, resultsByIndex) {
|
|
1097
|
+
const submittedSummary = typeof payload.summary === "string" ? payload.summary.trim() : "";
|
|
1098
|
+
if (submittedSummary.length > 0) {
|
|
1099
|
+
return {
|
|
1100
|
+
approvedSummary: submittedSummary,
|
|
1101
|
+
summaryMeta: normalizeSummaryMeta(payload.summaryMeta, submittedSummary)
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
const selected = filterByQueryIndices(payload.selectedQueryIndices, resultsByIndex).results;
|
|
1105
|
+
const fallbackResults = selected.length > 0 ? selected : [...resultsByIndex.values()];
|
|
1106
|
+
const deterministic = buildDeterministicSummary(fallbackResults);
|
|
1107
|
+
return {
|
|
1108
|
+
approvedSummary: deterministic.summary,
|
|
1109
|
+
summaryMeta: deterministic.meta
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
function buildSearchReturn(opts) {
|
|
1113
|
+
const sc = opts.results.filter((r) => !r.error).length;
|
|
1114
|
+
const tr = opts.results.reduce((sum, r) => sum + r.results.length, 0);
|
|
1115
|
+
const hasApprovedSummary = typeof opts.approvedSummary === "string" && opts.approvedSummary.trim().length > 0;
|
|
1116
|
+
let output = "";
|
|
1117
|
+
if (hasApprovedSummary) {
|
|
1118
|
+
output = opts.approvedSummary.trim();
|
|
1119
|
+
} else {
|
|
1120
|
+
if (opts.curated) {
|
|
1121
|
+
output += `[These results were manually curated by the user in the browser. Use them as-is — do not re-search or discard.]
|
|
1122
|
+
|
|
1123
|
+
`;
|
|
1124
|
+
}
|
|
1125
|
+
const duplicateQueries = opts.curated ? duplicateQuerySet(opts.results) : new Set;
|
|
1126
|
+
for (const { query, answer, results, error, provider } of opts.results) {
|
|
1127
|
+
if (opts.queryList.length > 1) {
|
|
1128
|
+
output += opts.curated ? formatQueryHeader(query, provider, duplicateQueries) : `## Query: "${query}"
|
|
1129
|
+
|
|
1130
|
+
`;
|
|
1131
|
+
}
|
|
1132
|
+
if (error)
|
|
1133
|
+
output += `Error: ${error}
|
|
1134
|
+
|
|
1135
|
+
`;
|
|
1136
|
+
else
|
|
1137
|
+
output += formatSearchSummary(results, answer) + `
|
|
1138
|
+
|
|
1139
|
+
`;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
const hasInlineReady = hasFullInlineCoverage(opts.urls, opts.inlineContent);
|
|
1143
|
+
let fetchId = null;
|
|
1144
|
+
if (hasInlineReady && opts.inlineContent) {
|
|
1145
|
+
fetchId = generateId();
|
|
1146
|
+
const data = {
|
|
1147
|
+
id: fetchId,
|
|
1148
|
+
type: "fetch",
|
|
1149
|
+
timestamp: Date.now(),
|
|
1150
|
+
urls: opts.inlineContent
|
|
1151
|
+
};
|
|
1152
|
+
pi.appendEntry("web-search-results", storeFetchedContentResult(fetchId, data));
|
|
1153
|
+
if (!hasApprovedSummary) {
|
|
1154
|
+
output += `---
|
|
1155
|
+
Full content for ${opts.inlineContent.length} sources available [${fetchId}].`;
|
|
1156
|
+
}
|
|
1157
|
+
} else if (opts.includeContent) {
|
|
1158
|
+
fetchId = startBackgroundFetch(opts.urls, opts.proxy);
|
|
1159
|
+
if (fetchId && !hasApprovedSummary) {
|
|
1160
|
+
output += `---
|
|
1161
|
+
Content fetching in background [${fetchId}]. Will notify when ready.`;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
const searchId = storeAndPublishSearch(opts.results);
|
|
1165
|
+
const isBackgroundFetch = fetchId !== null && !hasInlineReady;
|
|
1166
|
+
return {
|
|
1167
|
+
content: [{ type: "text", text: output.trim() }],
|
|
1168
|
+
details: {
|
|
1169
|
+
queries: opts.queryList,
|
|
1170
|
+
queryCount: opts.queryList.length,
|
|
1171
|
+
successfulQueries: sc,
|
|
1172
|
+
totalResults: tr,
|
|
1173
|
+
includeContent: opts.includeContent,
|
|
1174
|
+
fetchId,
|
|
1175
|
+
fetchUrls: isBackgroundFetch ? opts.urls : undefined,
|
|
1176
|
+
searchId,
|
|
1177
|
+
...opts.curated ? {
|
|
1178
|
+
curated: true,
|
|
1179
|
+
curatedFrom: opts.curatedFrom,
|
|
1180
|
+
curatedQueries: opts.results.map((r) => ({
|
|
1181
|
+
query: r.query,
|
|
1182
|
+
provider: r.provider || null,
|
|
1183
|
+
answer: r.answer || null,
|
|
1184
|
+
sources: r.results.map((s) => ({ title: s.title, url: s.url })),
|
|
1185
|
+
error: r.error
|
|
1186
|
+
}))
|
|
1187
|
+
} : {},
|
|
1188
|
+
...opts.workflow && hasApprovedSummary ? {
|
|
1189
|
+
summary: {
|
|
1190
|
+
text: opts.approvedSummary.trim(),
|
|
1191
|
+
workflow: opts.workflow,
|
|
1192
|
+
model: opts.summaryMeta?.model ?? null,
|
|
1193
|
+
durationMs: opts.summaryMeta?.durationMs ?? 0,
|
|
1194
|
+
tokenEstimate: opts.summaryMeta?.tokenEstimate ?? 0,
|
|
1195
|
+
fallbackUsed: opts.summaryMeta?.fallbackUsed === true,
|
|
1196
|
+
fallbackReason: opts.summaryMeta?.fallbackReason,
|
|
1197
|
+
phase: opts.summaryMeta?.phase,
|
|
1198
|
+
edited: opts.summaryMeta?.edited === true
|
|
1199
|
+
}
|
|
1200
|
+
} : {}
|
|
1201
|
+
}
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
function filterByQueryIndices(selectedQueryIndices, results) {
|
|
1205
|
+
const filteredResults = [];
|
|
1206
|
+
const filteredUrls = [];
|
|
1207
|
+
for (const qi of selectedQueryIndices) {
|
|
1208
|
+
const r = results.get(qi);
|
|
1209
|
+
if (r) {
|
|
1210
|
+
filteredResults.push(r);
|
|
1211
|
+
for (const res of r.results) {
|
|
1212
|
+
if (!filteredUrls.includes(res.url))
|
|
1213
|
+
filteredUrls.push(res.url);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
return { results: filteredResults, urls: filteredUrls };
|
|
1218
|
+
}
|
|
1219
|
+
function collectAllResultsAndUrls(resultsByIndex) {
|
|
1220
|
+
const results = [...resultsByIndex.values()];
|
|
1221
|
+
const urls = [];
|
|
1222
|
+
for (const result of results) {
|
|
1223
|
+
for (const source of result.results) {
|
|
1224
|
+
if (!urls.includes(source.url))
|
|
1225
|
+
urls.push(source.url);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
return { results, urls };
|
|
1229
|
+
}
|
|
1230
|
+
async function openCuratorBrowser(callId, pc, ctx, searchesComplete = true) {
|
|
1231
|
+
if (pendingCurates.get(callId) !== pc)
|
|
1232
|
+
return;
|
|
1233
|
+
let handle = null;
|
|
1234
|
+
const sendCuratorFallbackUpdate = (message) => {
|
|
1235
|
+
if (!handle)
|
|
1236
|
+
return;
|
|
1237
|
+
pc.onUpdate?.({
|
|
1238
|
+
content: [{ type: "text", text: `${message}
|
|
1239
|
+
Open manually: ${handle.url}` }],
|
|
1240
|
+
details: {
|
|
1241
|
+
phase: "curator-fallback",
|
|
1242
|
+
progress: searchesComplete ? 1 : 0.5,
|
|
1243
|
+
curatorUrl: handle.url,
|
|
1244
|
+
timeoutSeconds: pc.timeoutSeconds,
|
|
1245
|
+
shortcut: curateKey,
|
|
1246
|
+
browserOpenError: pc.browserOpenError
|
|
1247
|
+
}
|
|
1248
|
+
});
|
|
1249
|
+
};
|
|
1250
|
+
try {
|
|
1251
|
+
pc.phase = "curating";
|
|
1252
|
+
const searchAbort = new AbortController;
|
|
1253
|
+
const addSearchSignal = pc.signal ? AbortSignal.any([pc.signal, searchAbort.signal]) : searchAbort.signal;
|
|
1254
|
+
const sessionToken = randomUUID();
|
|
1255
|
+
handle = await startCuratorServer({
|
|
1256
|
+
queries: pc.queryList,
|
|
1257
|
+
sessionToken,
|
|
1258
|
+
timeout: pc.timeoutSeconds,
|
|
1259
|
+
availableProviders: pc.availableProviders,
|
|
1260
|
+
defaultProvider: pc.defaultProvider,
|
|
1261
|
+
searchProvider: toCuratorProvider(pc.searchProvider) ?? "auto",
|
|
1262
|
+
summaryModels: pc.summaryModels,
|
|
1263
|
+
defaultSummaryModel: pc.defaultSummaryModel
|
|
1264
|
+
}, {
|
|
1265
|
+
async onSummarize(selectedQueryIndices, summarizeSignal, model, feedback) {
|
|
1266
|
+
return runWithProxy(pc.proxy, async () => {
|
|
1267
|
+
if (pendingCurates.get(callId) !== pc)
|
|
1268
|
+
throw new Error("Curator session is no longer active.");
|
|
1269
|
+
pc.onUpdate?.({
|
|
1270
|
+
content: [{ type: "text", text: "Generating summary draft..." }],
|
|
1271
|
+
details: { phase: "generating-summary", progress: 0.9, curatorUrl: pc.curatorUrl, timeoutSeconds: pc.timeoutSeconds, shortcut: curateKey }
|
|
1272
|
+
});
|
|
1273
|
+
const draft = await generateSummaryForSelectedIndices(selectedQueryIndices, pc.searchResults, pc.summaryContext, summarizeSignal, model, feedback);
|
|
1274
|
+
if (pendingCurates.get(callId) !== pc)
|
|
1275
|
+
throw new Error("Curator session is no longer active.");
|
|
1276
|
+
pc.onUpdate?.({
|
|
1277
|
+
content: [{ type: "text", text: "Summary draft ready — waiting for approval..." }],
|
|
1278
|
+
details: { phase: "waiting-for-approval", progress: 1, curatorUrl: pc.curatorUrl, timeoutSeconds: pc.timeoutSeconds, shortcut: curateKey }
|
|
1279
|
+
});
|
|
1280
|
+
return draft;
|
|
1281
|
+
});
|
|
1282
|
+
},
|
|
1283
|
+
onSubmit(payload) {
|
|
1284
|
+
if (pendingCurates.get(callId) !== pc)
|
|
1285
|
+
return;
|
|
1286
|
+
searchAbort.abort();
|
|
1287
|
+
const filtered = payload.selectedQueryIndices.length > 0 ? filterByQueryIndices(payload.selectedQueryIndices, pc.searchResults) : collectAllResultsAndUrls(pc.searchResults);
|
|
1288
|
+
const filteredInline = pc.allInlineContent.filter((c) => filtered.urls.includes(c.url));
|
|
1289
|
+
const base = {
|
|
1290
|
+
queryList: filtered.results.map((r) => r.query),
|
|
1291
|
+
results: filtered.results,
|
|
1292
|
+
urls: filtered.urls,
|
|
1293
|
+
includeContent: pc.includeContent,
|
|
1294
|
+
inlineContent: filteredInline.length > 0 ? filteredInline : undefined,
|
|
1295
|
+
curated: true,
|
|
1296
|
+
curatedFrom: pc.searchResults.size,
|
|
1297
|
+
proxy: pc.proxy
|
|
1298
|
+
};
|
|
1299
|
+
if (!payload.rawResults) {
|
|
1300
|
+
const resolvedSummary = resolveSummaryForSubmit(payload, pc.searchResults);
|
|
1301
|
+
base.workflow = pc.workflow;
|
|
1302
|
+
base.approvedSummary = resolvedSummary.approvedSummary;
|
|
1303
|
+
base.summaryMeta = resolvedSummary.summaryMeta;
|
|
1304
|
+
}
|
|
1305
|
+
pc.finish(buildSearchReturn(base));
|
|
1306
|
+
closeCurator(callId);
|
|
1307
|
+
},
|
|
1308
|
+
onCancel(reason) {
|
|
1309
|
+
if (pendingCurates.get(callId) !== pc)
|
|
1310
|
+
return;
|
|
1311
|
+
searchAbort.abort();
|
|
1312
|
+
if (reason === "timeout") {
|
|
1313
|
+
const resolvedSummary = resolveSummaryForSubmit({ selectedQueryIndices: [], summary: undefined, summaryMeta: undefined }, pc.searchResults);
|
|
1314
|
+
const all = collectAllResultsAndUrls(pc.searchResults);
|
|
1315
|
+
const filteredInline = pc.allInlineContent.filter((c) => all.urls.includes(c.url));
|
|
1316
|
+
pc.finish(buildSearchReturn({
|
|
1317
|
+
queryList: all.results.map((r) => r.query),
|
|
1318
|
+
results: all.results,
|
|
1319
|
+
urls: all.urls,
|
|
1320
|
+
includeContent: pc.includeContent,
|
|
1321
|
+
inlineContent: filteredInline.length > 0 ? filteredInline : undefined,
|
|
1322
|
+
curated: true,
|
|
1323
|
+
curatedFrom: pc.searchResults.size,
|
|
1324
|
+
workflow: pc.workflow,
|
|
1325
|
+
approvedSummary: resolvedSummary.approvedSummary,
|
|
1326
|
+
summaryMeta: resolvedSummary.summaryMeta,
|
|
1327
|
+
proxy: pc.proxy
|
|
1328
|
+
}));
|
|
1329
|
+
} else {
|
|
1330
|
+
const conn = activeCurators.get(callId)?.getConnectionState();
|
|
1331
|
+
pc.finish(buildCurationCancelledReturn(reason, {
|
|
1332
|
+
queries: Array.from(pc.searchResults.values()),
|
|
1333
|
+
queryCount: pc.queryList.length,
|
|
1334
|
+
browserConnected: conn?.browserConnected,
|
|
1335
|
+
lastHeartbeatAgeMs: conn?.lastHeartbeatAgeMs,
|
|
1336
|
+
curatorUrl: pc.curatorUrl,
|
|
1337
|
+
browserOpenError: pc.browserOpenError
|
|
1338
|
+
}));
|
|
1339
|
+
}
|
|
1340
|
+
closeCurator(callId);
|
|
1341
|
+
},
|
|
1342
|
+
onProviderChange(provider) {
|
|
1343
|
+
if (pendingCurates.get(callId) !== pc)
|
|
1344
|
+
return;
|
|
1345
|
+
const normalized = normalizeProviderInput(provider);
|
|
1346
|
+
if (!normalized || normalized === "auto" || Array.isArray(normalized))
|
|
1347
|
+
return;
|
|
1348
|
+
pc.defaultProvider = normalized;
|
|
1349
|
+
pc.searchProvider = normalized;
|
|
1350
|
+
try {
|
|
1351
|
+
saveConfig({ provider: normalized });
|
|
1352
|
+
} catch (err) {
|
|
1353
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1354
|
+
console.error(`Failed to persist default provider: ${message}`);
|
|
1355
|
+
}
|
|
1356
|
+
},
|
|
1357
|
+
async onAddSearch(query, provider) {
|
|
1358
|
+
return runWithProxy(pc.proxy, async () => {
|
|
1359
|
+
if (pendingCurates.get(callId) !== pc)
|
|
1360
|
+
throw new Error("Curator session is no longer active.");
|
|
1361
|
+
const requestedProvider = resolveCuratorSearchProvider(provider, pc.searchProvider);
|
|
1362
|
+
const response = await search(query, {
|
|
1363
|
+
provider: requestedProvider,
|
|
1364
|
+
numResults: pc.numResults,
|
|
1365
|
+
recencyFilter: pc.recencyFilter,
|
|
1366
|
+
domainFilter: pc.domainFilter,
|
|
1367
|
+
includeContent: pc.includeContent,
|
|
1368
|
+
signal: addSearchSignal,
|
|
1369
|
+
extensionContext: ctx
|
|
1370
|
+
});
|
|
1371
|
+
if (pendingCurates.get(callId) !== pc)
|
|
1372
|
+
throw new Error("Curator session is no longer active.");
|
|
1373
|
+
if (response.inlineContent)
|
|
1374
|
+
pc.allInlineContent.push(...response.inlineContent);
|
|
1375
|
+
return toCuratorSearchEntries(response);
|
|
1376
|
+
});
|
|
1377
|
+
},
|
|
1378
|
+
onAddSearchResults(entries) {
|
|
1379
|
+
if (pendingCurates.get(callId) !== pc)
|
|
1380
|
+
return;
|
|
1381
|
+
for (const entry of entries) {
|
|
1382
|
+
pc.searchResults.set(entry.queryIndex, indexedCuratorEntryToQueryResult(entry));
|
|
1383
|
+
}
|
|
1384
|
+
},
|
|
1385
|
+
async onRewriteQuery(query, rewriteSignal) {
|
|
1386
|
+
return runWithProxy(pc.proxy, async () => {
|
|
1387
|
+
if (pendingCurates.get(callId) !== pc)
|
|
1388
|
+
throw new Error("Curator session is no longer active.");
|
|
1389
|
+
return rewriteSearchQuery(query, pc.summaryContext, rewriteSignal);
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
});
|
|
1393
|
+
if (pendingCurates.get(callId) !== pc) {
|
|
1394
|
+
handle.close();
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
activeCurators.set(callId, handle);
|
|
1398
|
+
pc.curatorUrl = handle.url;
|
|
1399
|
+
for (const [qi, data] of pc.searchResults) {
|
|
1400
|
+
const slotIndex = pc.resultSlots.get(qi);
|
|
1401
|
+
if (data.error) {
|
|
1402
|
+
handle.pushError(qi, data.error, data.provider, { query: data.query, slotIndex });
|
|
1403
|
+
} else {
|
|
1404
|
+
handle.pushResult(qi, {
|
|
1405
|
+
answer: data.answer,
|
|
1406
|
+
results: data.results.map((r) => ({ ...r, domain: extractDomain(r.url) })),
|
|
1407
|
+
provider: data.provider || pc.defaultProvider,
|
|
1408
|
+
query: data.query,
|
|
1409
|
+
slotIndex
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
if (searchesComplete)
|
|
1414
|
+
handle.searchesDone();
|
|
1415
|
+
pc.onUpdate?.({
|
|
1416
|
+
content: [{ type: "text", text: searchesComplete ? "Waiting for summary approval in browser..." : "Searches streaming to browser..." }],
|
|
1417
|
+
details: {
|
|
1418
|
+
phase: "curating",
|
|
1419
|
+
progress: searchesComplete ? 1 : 0.5,
|
|
1420
|
+
curatorUrl: handle.url,
|
|
1421
|
+
timeoutSeconds: pc.timeoutSeconds,
|
|
1422
|
+
shortcut: curateKey
|
|
1423
|
+
}
|
|
1424
|
+
});
|
|
1425
|
+
if (!shouldAutoOpenCuratorBrowser(loadConfig())) {
|
|
1426
|
+
sendCuratorFallbackUpdate("Search curator is running. Open the curator URL manually.");
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
const open = platform() === "darwin" ? await getGlimpseOpen() : null;
|
|
1430
|
+
if (open) {
|
|
1431
|
+
try {
|
|
1432
|
+
const win = openInGlimpse(open, handle.url, "Search Curator");
|
|
1433
|
+
glimpseWins.set(callId, win);
|
|
1434
|
+
win.on("closed", () => {
|
|
1435
|
+
if (glimpseWins.get(callId) === win) {
|
|
1436
|
+
glimpseWins.delete(callId);
|
|
1437
|
+
closeCurator(callId);
|
|
1438
|
+
}
|
|
1439
|
+
});
|
|
1440
|
+
return;
|
|
1441
|
+
} catch (err) {
|
|
1442
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1443
|
+
console.error(`Failed to open Glimpse curator window: ${message}`);
|
|
1444
|
+
glimpseWins.delete(callId);
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
await openInBrowser(pi, handle.url);
|
|
1448
|
+
} catch (err) {
|
|
1449
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1450
|
+
console.error(`Failed to open curator UI: ${message}`);
|
|
1451
|
+
if (handle && activeCurators.get(callId) === handle && pendingCurates.get(callId) === pc) {
|
|
1452
|
+
pc.browserOpenError = message;
|
|
1453
|
+
sendCuratorFallbackUpdate("Search curator is running, but the browser did not open automatically.");
|
|
1454
|
+
} else if (pendingCurates.get(callId) === pc || handle && activeCurators.get(callId) === handle) {
|
|
1455
|
+
closeCurator(callId);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
pi.registerShortcut(curateKey, {
|
|
1460
|
+
description: "Review search results",
|
|
1461
|
+
handler: async (ctx) => {
|
|
1462
|
+
const entries = [...pendingCurates.entries()];
|
|
1463
|
+
if (entries.length === 0)
|
|
1464
|
+
return;
|
|
1465
|
+
const [callId, pc] = entries[entries.length - 1];
|
|
1466
|
+
if (pc.phase === "searching") {
|
|
1467
|
+
pc.browserPromise = openCuratorBrowser(callId, pc, ctx, false);
|
|
1468
|
+
ctx.ui.notify("Opening curator — remaining searches will stream in", "info");
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
});
|
|
1473
|
+
pi.registerShortcut(activityKey, {
|
|
1474
|
+
description: "Toggle web search activity",
|
|
1475
|
+
handler: async (ctx) => {
|
|
1476
|
+
widgetVisible = !widgetVisible;
|
|
1477
|
+
if (widgetVisible) {
|
|
1478
|
+
widgetUnsubscribe = activityMonitor.onUpdate(() => updateWidget(ctx));
|
|
1479
|
+
updateWidget(ctx);
|
|
1480
|
+
} else {
|
|
1481
|
+
widgetUnsubscribe?.();
|
|
1482
|
+
widgetUnsubscribe = null;
|
|
1483
|
+
ctx.ui.setWidget("web-activity", undefined);
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
});
|
|
1487
|
+
pi.on("session_start", async (_event, ctx) => handleSessionChange(ctx));
|
|
1488
|
+
pi.on("session_tree", async (_event, ctx) => handleSessionChange(ctx));
|
|
1489
|
+
pi.on("session_shutdown", () => {
|
|
1490
|
+
sessionActive = false;
|
|
1491
|
+
abortPendingFetches();
|
|
1492
|
+
closeCurator();
|
|
1493
|
+
clearCloneCache();
|
|
1494
|
+
clearResults();
|
|
1495
|
+
widgetUnsubscribe?.();
|
|
1496
|
+
widgetUnsubscribe = null;
|
|
1497
|
+
activityMonitor.clear();
|
|
1498
|
+
widgetVisible = false;
|
|
1499
|
+
});
|
|
1500
|
+
if (webSearchEnabled)
|
|
1501
|
+
pi.registerTool({
|
|
1502
|
+
name: toolNames.webSearch,
|
|
1503
|
+
label: "Web Search",
|
|
1504
|
+
description: `Search the web using OpenAI, Brave, Parallel, Parallel MCP, TinyFish, Search1API, Searchinfinity, Querit, Tavily, Firecrawl, Jina, SERPdive, Kagi, Bocha, Ollama, SearXNG, DuckDuckGo, Exa, Perplexity, Gemini, Kimi, AnySearch, XCrawl, Valyu, xAI, Bright Data, SerpBase, or Serper. Pass a provider array to search only those providers simultaneously, or use provider "all" to search every eligible provider except Parallel MCP, DuckDuckGo, Kimi, AnySearch, XCrawl, Valyu, xAI, Bright Data, SerpBase, and Serper. Returns an AI-synthesized answer with source citations. OpenAI search uses a Codex subscription or OpenAI API key; Kimi search uses a Kimi Code Plan authenticated through /login kimi-coding; xAI search uses a SuperGrok/X Premium subscription or xAI API key. Parallel MCP, DuckDuckGo, Kimi, AnySearch, XCrawl, Valyu, xAI, Bright Data, SerpBase, and Serper are available only when explicitly selected. For comprehensive research, prefer queries (plural) with 2-4 varied angles over a single query — each query gets its own synthesized answer, so varying phrasing and scope gives much broader coverage. When includeContent is true, full page content is fetched in the background. Searches auto-open the interactive browser curator and stream results live; set workflow to "none" to skip curation or "auto-summary" for a model-generated summary without the browser curator. The configured provider is used when provider is omitted or set to auto; omit provider unless explicitly overriding it. Without a configured provider, SearXNG is preferred first for local/private search. When the active DM model is openai-codex, Codex-backed OpenAI search is preferred next. Otherwise Exa is preferred before OpenAI, then Brave, Parallel, TinyFish, Search1API, Searchinfinity, Querit, Tavily, Firecrawl, Jina, SERPdive, Kagi, Bocha, Ollama, Perplexity, Gemini API, or Gemini Web.`,
|
|
1505
|
+
promptSnippet: "Use for web research questions. Prefer {queries:[...]} with 2-4 varied angles over a single query for broader coverage. Omit provider unless explicitly overriding the configured default.",
|
|
1506
|
+
parameters: Type.Object({
|
|
1507
|
+
query: Type.Optional(Type.String({ description: "Single search query. For research tasks, prefer 'queries' with multiple varied angles instead." })),
|
|
1508
|
+
queries: Type.Optional(Type.Array(Type.String(), { description: "Multiple queries searched in sequence, each returning its own synthesized answer. Prefer this for research — vary phrasing, scope, and angle across 2-4 queries to maximize coverage. Good: ['React vs Vue performance benchmarks 2026', 'React vs Vue developer experience comparison', 'React ecosystem size vs Vue ecosystem']. Bad: ['React vs Vue', 'React vs Vue comparison', 'React vs Vue review'] (too similar, redundant results)." })),
|
|
1509
|
+
numResults: Type.Optional(Type.Integer({ minimum: 1, maximum: 20, description: "Results per query (default: 5, max: 20)" })),
|
|
1510
|
+
includeContent: Type.Optional(Type.Boolean({ description: "Fetch full page content (async)" })),
|
|
1511
|
+
recencyFilter: Type.Optional(StringEnum(["day", "week", "month", "year"], { description: "Filter by recency" })),
|
|
1512
|
+
domainFilter: Type.Optional(Type.Array(Type.String(), { description: "Limit to domains (prefix with - to exclude)" })),
|
|
1513
|
+
provider: Type.Optional(searchProviderSchema("Search provider or non-empty list of providers to search simultaneously; use all to search every eligible provider except Parallel MCP, DuckDuckGo, Kimi, AnySearch, XCrawl, Valyu, xAI, Bright Data, SerpBase, and Serper, omit this field to use the configured provider, or use auto when none is configured")),
|
|
1514
|
+
workflow: Type.Optional(StringEnum(["none", "summary-review", "auto-summary"], {
|
|
1515
|
+
description: "Search workflow mode: none = no curator, summary-review = open curator with auto summary draft (default), auto-summary = generate summary without opening curator"
|
|
1516
|
+
})),
|
|
1517
|
+
proxy: Type.Optional(Type.String({
|
|
1518
|
+
description: "http(s) proxy URL (e.g. http://host:port) used for every outbound request in this call (search APIs and content fetches). Node fetch ignores HTTP(S)_PROXY env vars, so set this (or `proxy` in web-search.json) when direct access is blocked; empty string forces direct access."
|
|
1519
|
+
}))
|
|
1520
|
+
}),
|
|
1521
|
+
async execute(callId, params, signal, onUpdate, ctx) {
|
|
1522
|
+
return runWithProxy(typeof params.proxy === "string" ? params.proxy : undefined, async () => {
|
|
1523
|
+
const rawQueryList = Array.isArray(params.queries) ? params.queries : params.query !== undefined ? expandQueryString(params.query) : [];
|
|
1524
|
+
const queryList = normalizeQueryList(rawQueryList);
|
|
1525
|
+
const configWorkflow = loadConfigForExtensionInit().workflow;
|
|
1526
|
+
const workflow = resolveWorkflow(params.workflow ?? configWorkflow, ctx?.hasUI !== false);
|
|
1527
|
+
const shouldCurate = workflow === "summary-review";
|
|
1528
|
+
const recencyFilter = normalizeRecencyFilter(params.recencyFilter);
|
|
1529
|
+
if (queryList.length === 0) {
|
|
1530
|
+
return {
|
|
1531
|
+
content: [{ type: "text", text: "Error: No query provided. Use 'query' or 'queries' parameter." }],
|
|
1532
|
+
details: { error: "No query provided" }
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
if (shouldCurate && !ctx) {
|
|
1536
|
+
return {
|
|
1537
|
+
content: [{ type: "text", text: "Error: Curation requires an active extension context." }],
|
|
1538
|
+
details: { error: "Missing extension context" }
|
|
1539
|
+
};
|
|
1540
|
+
}
|
|
1541
|
+
if (shouldCurate) {
|
|
1542
|
+
closeCurator(callId);
|
|
1543
|
+
let resolvePromise = () => {};
|
|
1544
|
+
const promise = new Promise((resolve) => {
|
|
1545
|
+
resolvePromise = resolve;
|
|
1546
|
+
});
|
|
1547
|
+
const includeContent = params.includeContent ?? false;
|
|
1548
|
+
const searchResults = new Map;
|
|
1549
|
+
const resultSlots = new Map;
|
|
1550
|
+
const allInlineContent = [];
|
|
1551
|
+
let nextResultIndex = queryList.length;
|
|
1552
|
+
const searchAbort = new AbortController;
|
|
1553
|
+
const searchSignal = signal ? AbortSignal.any([signal, searchAbort.signal]) : searchAbort.signal;
|
|
1554
|
+
let cancelled = false;
|
|
1555
|
+
const requestedProvider = resolveRequestedProvider(params.provider);
|
|
1556
|
+
const bootstrap = await loadCuratorBootstrap(requestedProvider, ctx, {
|
|
1557
|
+
numResults: params.numResults,
|
|
1558
|
+
recencyFilter
|
|
1559
|
+
});
|
|
1560
|
+
const availableProviders = bootstrap.availableProviders;
|
|
1561
|
+
const defaultProvider = bootstrap.defaultProvider;
|
|
1562
|
+
const searchProvider = requestedProvider;
|
|
1563
|
+
const curatorTimeoutSeconds = bootstrap.timeoutSeconds;
|
|
1564
|
+
const curatorWorkflow = "summary-review";
|
|
1565
|
+
const summaryContext = {
|
|
1566
|
+
model: ctx.model,
|
|
1567
|
+
modelRegistry: ctx.modelRegistry,
|
|
1568
|
+
cwd: ctx.cwd,
|
|
1569
|
+
isProjectTrusted: () => ctx.isProjectTrusted()
|
|
1570
|
+
};
|
|
1571
|
+
const summaryModelChoices = await loadSummaryModelChoices(summaryContext);
|
|
1572
|
+
const pc = {
|
|
1573
|
+
phase: "searching",
|
|
1574
|
+
workflow: curatorWorkflow,
|
|
1575
|
+
summaryContext,
|
|
1576
|
+
searchResults,
|
|
1577
|
+
resultSlots,
|
|
1578
|
+
allInlineContent,
|
|
1579
|
+
queryList,
|
|
1580
|
+
includeContent,
|
|
1581
|
+
numResults: params.numResults,
|
|
1582
|
+
recencyFilter,
|
|
1583
|
+
domainFilter: params.domainFilter,
|
|
1584
|
+
availableProviders,
|
|
1585
|
+
defaultProvider,
|
|
1586
|
+
searchProvider,
|
|
1587
|
+
summaryModels: summaryModelChoices.summaryModels,
|
|
1588
|
+
defaultSummaryModel: summaryModelChoices.defaultSummaryModel,
|
|
1589
|
+
timeoutSeconds: curatorTimeoutSeconds,
|
|
1590
|
+
proxy: typeof params.proxy === "string" ? params.proxy : undefined,
|
|
1591
|
+
onUpdate,
|
|
1592
|
+
signal,
|
|
1593
|
+
abortSearches: () => {
|
|
1594
|
+
if (!searchAbort.signal.aborted)
|
|
1595
|
+
searchAbort.abort();
|
|
1596
|
+
},
|
|
1597
|
+
finish: () => {},
|
|
1598
|
+
cancel: () => {}
|
|
1599
|
+
};
|
|
1600
|
+
const finish = (value) => {
|
|
1601
|
+
if (cancelled)
|
|
1602
|
+
return;
|
|
1603
|
+
cancelled = true;
|
|
1604
|
+
pc.abortSearches();
|
|
1605
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1606
|
+
pendingCurates.delete(callId);
|
|
1607
|
+
resolvePromise(value);
|
|
1608
|
+
};
|
|
1609
|
+
const cancel = (reason = "stale") => {
|
|
1610
|
+
if (cancelled)
|
|
1611
|
+
return;
|
|
1612
|
+
const conn = activeCurators.get(callId)?.getConnectionState();
|
|
1613
|
+
finish(buildCurationCancelledReturn(reason, {
|
|
1614
|
+
queries: Array.from(searchResults.values()),
|
|
1615
|
+
queryCount: queryList.length,
|
|
1616
|
+
browserConnected: conn?.browserConnected,
|
|
1617
|
+
lastHeartbeatAgeMs: conn?.lastHeartbeatAgeMs,
|
|
1618
|
+
curatorUrl: pc.curatorUrl,
|
|
1619
|
+
browserOpenError: pc.browserOpenError
|
|
1620
|
+
}));
|
|
1621
|
+
};
|
|
1622
|
+
pc.finish = finish;
|
|
1623
|
+
pc.cancel = cancel;
|
|
1624
|
+
const onAbort = () => closeCurator(callId);
|
|
1625
|
+
pendingCurates.set(callId, pc);
|
|
1626
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1627
|
+
pc.browserPromise = openCuratorBrowser(callId, pc, ctx, false);
|
|
1628
|
+
for (let qi = 0;qi < queryList.length; qi++) {
|
|
1629
|
+
if (signal?.aborted || cancelled || searchAbort.signal.aborted)
|
|
1630
|
+
break;
|
|
1631
|
+
onUpdate?.({
|
|
1632
|
+
content: [{ type: "text", text: `Searching ${qi + 1}/${queryList.length}: "${queryList[qi]}"...` }],
|
|
1633
|
+
details: { phase: "searching", progress: qi / queryList.length, currentQuery: queryList[qi] }
|
|
1634
|
+
});
|
|
1635
|
+
const requestedProvider = pc.searchProvider;
|
|
1636
|
+
try {
|
|
1637
|
+
const response = await search(queryList[qi], {
|
|
1638
|
+
provider: requestedProvider,
|
|
1639
|
+
numResults: params.numResults,
|
|
1640
|
+
recencyFilter,
|
|
1641
|
+
domainFilter: params.domainFilter,
|
|
1642
|
+
includeContent: params.includeContent,
|
|
1643
|
+
signal: searchSignal,
|
|
1644
|
+
extensionContext: ctx
|
|
1645
|
+
});
|
|
1646
|
+
if (signal?.aborted || cancelled || searchAbort.signal.aborted)
|
|
1647
|
+
break;
|
|
1648
|
+
if (response.inlineContent)
|
|
1649
|
+
allInlineContent.push(...response.inlineContent);
|
|
1650
|
+
const entries = toCuratorSearchEntries(response);
|
|
1651
|
+
const curator = activeCurators.get(callId);
|
|
1652
|
+
for (let entryIndex = 0;entryIndex < entries.length; entryIndex++) {
|
|
1653
|
+
const entry = entries[entryIndex];
|
|
1654
|
+
const resultIndex = entryIndex === 0 ? qi : nextResultIndex++;
|
|
1655
|
+
const indexedEntry = {
|
|
1656
|
+
...entry,
|
|
1657
|
+
queryIndex: resultIndex,
|
|
1658
|
+
query: queryList[qi]
|
|
1659
|
+
};
|
|
1660
|
+
searchResults.set(resultIndex, indexedCuratorEntryToQueryResult(indexedEntry));
|
|
1661
|
+
resultSlots.set(resultIndex, qi);
|
|
1662
|
+
if (curator) {
|
|
1663
|
+
if (entry.error) {
|
|
1664
|
+
curator.pushError(resultIndex, entry.error, entry.provider, { query: queryList[qi], slotIndex: qi });
|
|
1665
|
+
} else {
|
|
1666
|
+
curator.pushResult(resultIndex, { ...entry, query: queryList[qi], slotIndex: qi });
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
} catch (err) {
|
|
1671
|
+
if (signal?.aborted || cancelled || searchAbort.signal.aborted)
|
|
1672
|
+
break;
|
|
1673
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1674
|
+
const failedProvider = toCuratorProvider(requestedProvider);
|
|
1675
|
+
searchResults.set(qi, { query: queryList[qi], answer: "", results: [], error: message, provider: failedProvider });
|
|
1676
|
+
resultSlots.set(qi, qi);
|
|
1677
|
+
const curator = activeCurators.get(callId);
|
|
1678
|
+
if (curator) {
|
|
1679
|
+
curator.pushError(qi, message, failedProvider, { query: queryList[qi], slotIndex: qi });
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
if (signal?.aborted || cancelled || searchAbort.signal.aborted) {
|
|
1684
|
+
cancel();
|
|
1685
|
+
return promise;
|
|
1686
|
+
}
|
|
1687
|
+
await pc.browserPromise;
|
|
1688
|
+
const curator = activeCurators.get(callId);
|
|
1689
|
+
if (curator && !cancelled) {
|
|
1690
|
+
curator.searchesDone();
|
|
1691
|
+
if (pc.browserOpenError) {
|
|
1692
|
+
pc.onUpdate?.({
|
|
1693
|
+
content: [{ type: "text", text: `All searches complete. Open the curator manually: ${pc.curatorUrl}` }],
|
|
1694
|
+
details: {
|
|
1695
|
+
phase: "curator-fallback",
|
|
1696
|
+
progress: 1,
|
|
1697
|
+
curatorUrl: pc.curatorUrl,
|
|
1698
|
+
timeoutSeconds: pc.timeoutSeconds,
|
|
1699
|
+
shortcut: curateKey,
|
|
1700
|
+
browserOpenError: pc.browserOpenError
|
|
1701
|
+
}
|
|
1702
|
+
});
|
|
1703
|
+
} else {
|
|
1704
|
+
pc.onUpdate?.({
|
|
1705
|
+
content: [{ type: "text", text: "All searches complete — waiting for summary approval in browser..." }],
|
|
1706
|
+
details: {
|
|
1707
|
+
phase: "curating",
|
|
1708
|
+
progress: 1,
|
|
1709
|
+
curatorUrl: pc.curatorUrl,
|
|
1710
|
+
timeoutSeconds: pc.timeoutSeconds,
|
|
1711
|
+
shortcut: curateKey
|
|
1712
|
+
}
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
return promise;
|
|
1717
|
+
}
|
|
1718
|
+
const searchResults = [];
|
|
1719
|
+
const allUrls = [];
|
|
1720
|
+
const allInlineContent = [];
|
|
1721
|
+
const resolvedProvider = resolveRequestedProvider(params.provider);
|
|
1722
|
+
for (let i = 0;i < queryList.length; i++) {
|
|
1723
|
+
const query = queryList[i];
|
|
1724
|
+
onUpdate?.({
|
|
1725
|
+
content: [{ type: "text", text: `Searching ${i + 1}/${queryList.length}: "${query}"...` }],
|
|
1726
|
+
details: { phase: "search", progress: i / queryList.length, currentQuery: query }
|
|
1727
|
+
});
|
|
1728
|
+
try {
|
|
1729
|
+
const { answer, results, inlineContent, provider } = await search(query, {
|
|
1730
|
+
provider: resolvedProvider,
|
|
1731
|
+
numResults: params.numResults,
|
|
1732
|
+
recencyFilter,
|
|
1733
|
+
domainFilter: params.domainFilter,
|
|
1734
|
+
includeContent: params.includeContent,
|
|
1735
|
+
signal,
|
|
1736
|
+
extensionContext: ctx
|
|
1737
|
+
});
|
|
1738
|
+
searchResults.push({ query, answer, results, error: null, provider });
|
|
1739
|
+
for (const r of results) {
|
|
1740
|
+
if (!allUrls.includes(r.url)) {
|
|
1741
|
+
allUrls.push(r.url);
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
if (inlineContent)
|
|
1745
|
+
allInlineContent.push(...inlineContent);
|
|
1746
|
+
} catch (err) {
|
|
1747
|
+
if (signal?.aborted || isAbortError(err))
|
|
1748
|
+
throw err;
|
|
1749
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1750
|
+
const requestedProvider = toCuratorProvider(resolvedProvider);
|
|
1751
|
+
searchResults.push({ query, answer: "", results: [], error: message, provider: requestedProvider });
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
let approvedSummary;
|
|
1755
|
+
let summaryMeta;
|
|
1756
|
+
if (workflow === "auto-summary") {
|
|
1757
|
+
if (!ctx) {
|
|
1758
|
+
return {
|
|
1759
|
+
content: [{ type: "text", text: "Error: Auto-summary requires an active extension context." }],
|
|
1760
|
+
details: { error: "Missing extension context" }
|
|
1761
|
+
};
|
|
1762
|
+
}
|
|
1763
|
+
onUpdate?.({
|
|
1764
|
+
content: [{ type: "text", text: "Generating summary..." }],
|
|
1765
|
+
details: { phase: "generating-summary", progress: 1 }
|
|
1766
|
+
});
|
|
1767
|
+
const summaryContext = {
|
|
1768
|
+
model: ctx.model,
|
|
1769
|
+
modelRegistry: ctx.modelRegistry,
|
|
1770
|
+
cwd: ctx.cwd,
|
|
1771
|
+
isProjectTrusted: () => ctx.isProjectTrusted()
|
|
1772
|
+
};
|
|
1773
|
+
const summaryModelChoices = await loadSummaryModelChoices(summaryContext);
|
|
1774
|
+
const generated = await generateSummaryDraft(searchResults, summaryContext, signal, summaryModelChoices.defaultSummaryModel ?? undefined, undefined, undefined, getSummaryGenerationDeadlineMs());
|
|
1775
|
+
approvedSummary = generated.summary;
|
|
1776
|
+
summaryMeta = generated.meta;
|
|
1777
|
+
}
|
|
1778
|
+
return buildSearchReturn({
|
|
1779
|
+
queryList,
|
|
1780
|
+
results: searchResults,
|
|
1781
|
+
urls: allUrls,
|
|
1782
|
+
includeContent: params.includeContent ?? false,
|
|
1783
|
+
inlineContent: allInlineContent.length > 0 ? allInlineContent : undefined,
|
|
1784
|
+
workflow: workflow === "auto-summary" ? "auto-summary" : undefined,
|
|
1785
|
+
approvedSummary,
|
|
1786
|
+
summaryMeta,
|
|
1787
|
+
proxy: typeof params.proxy === "string" ? params.proxy : undefined
|
|
1788
|
+
});
|
|
1789
|
+
});
|
|
1790
|
+
},
|
|
1791
|
+
renderCall(args, theme) {
|
|
1792
|
+
const input = args;
|
|
1793
|
+
const rawQueryList = Array.isArray(input.queries) ? input.queries : input.query !== undefined ? expandQueryString(input.query) : [];
|
|
1794
|
+
const queryList = normalizeQueryList(rawQueryList);
|
|
1795
|
+
if (queryList.length === 0) {
|
|
1796
|
+
return new Text(theme.fg("toolTitle", theme.bold("search ")) + theme.fg("error", "(no query)"), 0, 0);
|
|
1797
|
+
}
|
|
1798
|
+
if (queryList.length === 1) {
|
|
1799
|
+
const q = queryList[0];
|
|
1800
|
+
const display = q.length > 60 ? q.slice(0, 57) + "..." : q;
|
|
1801
|
+
return new Text(theme.fg("toolTitle", theme.bold("search ")) + theme.fg("accent", `"${display}"`), 0, 0);
|
|
1802
|
+
}
|
|
1803
|
+
const lines = [theme.fg("toolTitle", theme.bold("search ")) + theme.fg("accent", `${queryList.length} queries`)];
|
|
1804
|
+
for (const q of queryList.slice(0, 5)) {
|
|
1805
|
+
const display = q.length > 50 ? q.slice(0, 47) + "..." : q;
|
|
1806
|
+
lines.push(theme.fg("muted", ` "${display}"`));
|
|
1807
|
+
}
|
|
1808
|
+
if (queryList.length > 5) {
|
|
1809
|
+
lines.push(theme.fg("muted", ` ... and ${queryList.length - 5} more`));
|
|
1810
|
+
}
|
|
1811
|
+
return new Text(lines.join(`
|
|
1812
|
+
`), 0, 0);
|
|
1813
|
+
},
|
|
1814
|
+
renderResult(result, { expanded, isPartial }, theme) {
|
|
1815
|
+
const details = result.details;
|
|
1816
|
+
if (isPartial) {
|
|
1817
|
+
if (details?.phase === "curator-fallback") {
|
|
1818
|
+
const lines = [theme.fg("warning", "Open the search curator manually:")];
|
|
1819
|
+
if (details?.curatorUrl)
|
|
1820
|
+
lines.push(theme.fg("muted", ` ${details.curatorUrl}`));
|
|
1821
|
+
if (details?.browserOpenError)
|
|
1822
|
+
lines.push(theme.fg("dim", ` auto-open failed: ${details.browserOpenError}`));
|
|
1823
|
+
const timeout = typeof details?.timeoutSeconds === "number" ? details.timeoutSeconds : undefined;
|
|
1824
|
+
const shortcut = typeof details?.shortcut === "string" ? details.shortcut : curateKey;
|
|
1825
|
+
lines.push(theme.fg("dim", timeout ? ` auto-submits after ${timeout}s idle; ${shortcut} reopens` : ` ${shortcut} reopens`));
|
|
1826
|
+
return new Text(lines.join(`
|
|
1827
|
+
`), 0, 0);
|
|
1828
|
+
}
|
|
1829
|
+
if (details?.phase === "curating" || details?.phase === "waiting-for-approval" || details?.phase === "generating-summary") {
|
|
1830
|
+
const phaseText = details?.phase === "generating-summary" ? "generating summary draft..." : details?.phase === "waiting-for-approval" ? "summary draft ready; approve in browser..." : "waiting for summary approval in browser...";
|
|
1831
|
+
const lines = [theme.fg("accent", phaseText)];
|
|
1832
|
+
if (details?.curatorUrl) {
|
|
1833
|
+
lines.push(theme.fg("muted", ` ${details.curatorUrl}`));
|
|
1834
|
+
}
|
|
1835
|
+
const timeout = typeof details?.timeoutSeconds === "number" ? details.timeoutSeconds : undefined;
|
|
1836
|
+
const shortcut = typeof details?.shortcut === "string" ? details.shortcut : curateKey;
|
|
1837
|
+
if (timeout) {
|
|
1838
|
+
lines.push(theme.fg("dim", ` auto-submits after ${timeout}s idle; ${shortcut} reopens`));
|
|
1839
|
+
} else {
|
|
1840
|
+
lines.push(theme.fg("dim", ` ${shortcut} reopens`));
|
|
1841
|
+
}
|
|
1842
|
+
return new Text(lines.join(`
|
|
1843
|
+
`), 0, 0);
|
|
1844
|
+
}
|
|
1845
|
+
if (details?.phase === "searching") {
|
|
1846
|
+
const progress = details?.progress ?? 0;
|
|
1847
|
+
const bar = "█".repeat(Math.floor(progress * 10)) + "░".repeat(10 - Math.floor(progress * 10));
|
|
1848
|
+
const query = details?.currentQuery || "";
|
|
1849
|
+
const display = query.length > 40 ? query.slice(0, 37) + "..." : query;
|
|
1850
|
+
return new Text(theme.fg("accent", `[${bar}] ${display}`), 0, 0);
|
|
1851
|
+
}
|
|
1852
|
+
const progress = details?.progress ?? 0;
|
|
1853
|
+
const bar = "█".repeat(Math.floor(progress * 10)) + "░".repeat(10 - Math.floor(progress * 10));
|
|
1854
|
+
return new Text(theme.fg("accent", `[${bar}] ${details?.phase || "searching"}`), 0, 0);
|
|
1855
|
+
}
|
|
1856
|
+
if (details?.error) {
|
|
1857
|
+
const plan = buildSearchErrorPlan(details);
|
|
1858
|
+
if (plan)
|
|
1859
|
+
return renderSearchErrorPlan(plan, expanded, theme);
|
|
1860
|
+
return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
|
|
1861
|
+
}
|
|
1862
|
+
let statusLine;
|
|
1863
|
+
const queryInfo = details?.queryCount === 1 ? "" : `${details?.successfulQueries}/${details?.queryCount} queries, `;
|
|
1864
|
+
statusLine = theme.fg("success", `${queryInfo}${details?.totalResults ?? 0} sources`);
|
|
1865
|
+
if (details?.curated && details?.curatedFrom) {
|
|
1866
|
+
statusLine += theme.fg("muted", ` (${details.queryCount}/${details.curatedFrom} queries curated)`);
|
|
1867
|
+
}
|
|
1868
|
+
if (details?.fetchId && details?.fetchUrls) {
|
|
1869
|
+
statusLine += theme.fg("muted", ` (fetching ${details.fetchUrls.length} URLs)`);
|
|
1870
|
+
} else if (details?.fetchId) {
|
|
1871
|
+
statusLine += theme.fg("muted", " (content ready)");
|
|
1872
|
+
}
|
|
1873
|
+
const lines = [statusLine];
|
|
1874
|
+
if (details?.summary?.text) {
|
|
1875
|
+
lines.push("");
|
|
1876
|
+
lines.push(theme.fg("accent", `── Summary (${details.summary.workflow}) ` + "─".repeat(32)));
|
|
1877
|
+
lines.push("");
|
|
1878
|
+
for (const line of details.summary.text.split(`
|
|
1879
|
+
`)) {
|
|
1880
|
+
lines.push(` ${line}`);
|
|
1881
|
+
}
|
|
1882
|
+
lines.push("");
|
|
1883
|
+
const metaParts = [
|
|
1884
|
+
details.summary.model ? `model=${details.summary.model}` : "model=deterministic",
|
|
1885
|
+
`duration=${details.summary.durationMs}ms`,
|
|
1886
|
+
`tokens~${details.summary.tokenEstimate}`,
|
|
1887
|
+
details.summary.fallbackUsed ? "fallback=true" : "fallback=false",
|
|
1888
|
+
details.summary.phase ? `phase=${details.summary.phase}` : "",
|
|
1889
|
+
details.summary.edited ? "edited=true" : "edited=false"
|
|
1890
|
+
];
|
|
1891
|
+
if (details.summary.fallbackReason) {
|
|
1892
|
+
metaParts.push(`reason=${details.summary.fallbackReason}`);
|
|
1893
|
+
}
|
|
1894
|
+
lines.push(theme.fg("dim", " " + metaParts.filter(Boolean).join(" · ")));
|
|
1895
|
+
}
|
|
1896
|
+
const queryDetails = details?.curatedQueries;
|
|
1897
|
+
if (queryDetails?.length) {
|
|
1898
|
+
const kept = queryDetails.length;
|
|
1899
|
+
const from = details?.curatedFrom ?? kept;
|
|
1900
|
+
lines.push("");
|
|
1901
|
+
lines.push(theme.fg("accent", `── Curated Results (${kept} of ${from} queries kept) ` + "─".repeat(24)));
|
|
1902
|
+
for (const cq of queryDetails) {
|
|
1903
|
+
lines.push("");
|
|
1904
|
+
const dq = cq.query.length > 65 ? cq.query.slice(0, 62) + "..." : cq.query;
|
|
1905
|
+
const providerLabel = cq.provider ? ` (${cq.provider})` : "";
|
|
1906
|
+
lines.push(theme.fg("accent", ` "${dq}"${providerLabel}`));
|
|
1907
|
+
if (cq.error) {
|
|
1908
|
+
lines.push(theme.fg("error", ` ${cq.error}`));
|
|
1909
|
+
} else if (cq.answer) {
|
|
1910
|
+
lines.push("");
|
|
1911
|
+
for (const line of cq.answer.split(`
|
|
1912
|
+
`)) {
|
|
1913
|
+
lines.push(` ${line}`);
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
if (cq.sources.length > 0) {
|
|
1917
|
+
lines.push("");
|
|
1918
|
+
for (const s of cq.sources) {
|
|
1919
|
+
const domain = s.url.replace(/^https?:\/\//, "").replace(/\/.*$/, "");
|
|
1920
|
+
const title = s.title.length > 50 ? s.title.slice(0, 47) + "..." : s.title;
|
|
1921
|
+
lines.push(theme.fg("muted", ` ▸ ${title}`) + theme.fg("dim", ` · ${domain}`));
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
lines.push("");
|
|
1926
|
+
} else {
|
|
1927
|
+
const textContent = result.content.find((c) => c.type === "text")?.text || "";
|
|
1928
|
+
const preview = textContent.length > 500 ? textContent.slice(0, 500) + "..." : textContent;
|
|
1929
|
+
for (const line of preview.split(`
|
|
1930
|
+
`)) {
|
|
1931
|
+
lines.push(theme.fg("dim", line));
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
if (details?.fetchUrls && details.fetchUrls.length > 0) {
|
|
1935
|
+
if (details.curated) {
|
|
1936
|
+
lines.push(theme.fg("muted", `Fetching ${details.fetchUrls.length} URLs in background`));
|
|
1937
|
+
} else {
|
|
1938
|
+
lines.push(theme.fg("muted", "Fetching:"));
|
|
1939
|
+
for (const u of details.fetchUrls.slice(0, 5)) {
|
|
1940
|
+
const display = u.length > 60 ? u.slice(0, 57) + "..." : u;
|
|
1941
|
+
lines.push(theme.fg("dim", " " + display));
|
|
1942
|
+
}
|
|
1943
|
+
if (details.fetchUrls.length > 5) {
|
|
1944
|
+
lines.push(theme.fg("dim", ` ... and ${details.fetchUrls.length - 5} more`));
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
const totalLines = lines.length;
|
|
1949
|
+
if (!expanded) {
|
|
1950
|
+
const box = new Box(1, 0);
|
|
1951
|
+
box.addChild(new Text(statusLine, 0, 0));
|
|
1952
|
+
let collapsedLines = 1;
|
|
1953
|
+
const summaryPreview = details?.summary?.text?.trim() || "";
|
|
1954
|
+
if (summaryPreview) {
|
|
1955
|
+
const preview = summaryPreview.length > 120 ? summaryPreview.slice(0, 117) + "..." : summaryPreview;
|
|
1956
|
+
box.addChild(new Text(theme.fg("dim", preview), 0, 0));
|
|
1957
|
+
collapsedLines++;
|
|
1958
|
+
} else if (details?.curatedQueries?.length) {
|
|
1959
|
+
for (const cq of details.curatedQueries.slice(0, 3)) {
|
|
1960
|
+
const dq = cq.query.length > 55 ? cq.query.slice(0, 52) + "..." : cq.query;
|
|
1961
|
+
const srcCount = cq.sources?.length ?? 0;
|
|
1962
|
+
const suffix = cq.error ? theme.fg("error", " (error)") : theme.fg("dim", ` · ${srcCount} sources`);
|
|
1963
|
+
box.addChild(new Text(theme.fg("accent", ` "${dq}"`) + suffix, 0, 0));
|
|
1964
|
+
collapsedLines++;
|
|
1965
|
+
}
|
|
1966
|
+
if (details.curatedQueries.length > 3) {
|
|
1967
|
+
box.addChild(new Text(theme.fg("dim", ` ... and ${details.curatedQueries.length - 3} more`), 0, 0));
|
|
1968
|
+
collapsedLines++;
|
|
1969
|
+
}
|
|
1970
|
+
} else {
|
|
1971
|
+
const textContent = result.content.find((c) => c.type === "text")?.text || "";
|
|
1972
|
+
const firstContentLine = textContent.split(`
|
|
1973
|
+
`).find((l) => {
|
|
1974
|
+
const t = l.trim();
|
|
1975
|
+
return t && !t.startsWith("[") && !t.startsWith("#") && !t.startsWith("---");
|
|
1976
|
+
});
|
|
1977
|
+
const fallbackLine = (firstContentLine?.trim() || "").replace(/\*\*/g, "");
|
|
1978
|
+
if (fallbackLine) {
|
|
1979
|
+
const preview = fallbackLine.length > 120 ? fallbackLine.slice(0, 117) + "..." : fallbackLine;
|
|
1980
|
+
box.addChild(new Text(theme.fg("dim", preview), 0, 0));
|
|
1981
|
+
collapsedLines++;
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
const moreLines = Math.max(0, totalLines - collapsedLines);
|
|
1985
|
+
if (moreLines > 0) {
|
|
1986
|
+
box.addChild(new Text(theme.fg("muted", `
|
|
1987
|
+
... (${moreLines} more lines, ${totalLines} total, ctrl+o to expand)`), 0, 0));
|
|
1988
|
+
}
|
|
1989
|
+
return box;
|
|
1990
|
+
}
|
|
1991
|
+
return new Text(lines.join(`
|
|
1992
|
+
`), 0, 0);
|
|
1993
|
+
}
|
|
1994
|
+
});
|
|
1995
|
+
if (sourceCheckEnabled)
|
|
1996
|
+
pi.registerTool({
|
|
1997
|
+
name: toolNames.sourceCheck,
|
|
1998
|
+
label: "Source Check",
|
|
1999
|
+
description: "Check a claim against web sources and return a bounded machine-readable research artifact with exact passage citations.",
|
|
2000
|
+
promptSnippet: "Verify a claim with structured source evidence and passage-level citations.",
|
|
2001
|
+
parameters: Type.Object({
|
|
2002
|
+
claim: Type.String({ description: "The assertion to check against web sources." }),
|
|
2003
|
+
queries: Type.Optional(Type.Array(Type.String(), { description: "Search queries (default: the claim)." })),
|
|
2004
|
+
numResults: Type.Optional(Type.Integer({ minimum: 1, maximum: 20, description: "Results per query (default: 5, max: 20)." })),
|
|
2005
|
+
fetchContent: Type.Optional(Type.Boolean({ description: "Fetch up to 5 result pages for exact passage extraction." })),
|
|
2006
|
+
recencyFilter: Type.Optional(StringEnum(["day", "week", "month", "year"], { description: "Filter by recency." })),
|
|
2007
|
+
domainFilter: Type.Optional(Type.Array(Type.String(), { description: "Limit to domains; prefix with - to exclude." })),
|
|
2008
|
+
provider: Type.Optional(searchProviderSchema("Search provider or non-empty list of providers to search simultaneously; all searches every eligible provider except Parallel MCP, DuckDuckGo, Kimi, AnySearch, XCrawl, Valyu, xAI, Bright Data, SerpBase, and Serper")),
|
|
2009
|
+
proxy: Type.Optional(Type.String({
|
|
2010
|
+
description: "http(s) proxy URL (e.g. http://host:port) used for every outbound request in this call (search APIs and result-page fetches). Empty string forces direct access."
|
|
2011
|
+
}))
|
|
2012
|
+
}),
|
|
2013
|
+
async execute(_callId, params, signal, _onUpdate, ctx) {
|
|
2014
|
+
return runWithProxy(typeof params.proxy === "string" ? params.proxy : undefined, async () => {
|
|
2015
|
+
const claim = typeof params.claim === "string" ? params.claim.trim() : "";
|
|
2016
|
+
if (!claim) {
|
|
2017
|
+
return { content: [{ type: "text", text: "Error: 'claim' is required." }], details: { error: "Missing claim" } };
|
|
2018
|
+
}
|
|
2019
|
+
const requestedQueries = Array.isArray(params.queries) ? params.queries.filter((query) => typeof query === "string").map((query) => query.trim()).filter(Boolean) : [];
|
|
2020
|
+
const queries = (requestedQueries.length > 0 ? requestedQueries : [claim]).slice(0, 8);
|
|
2021
|
+
const numResults = typeof params.numResults === "number" && Number.isFinite(params.numResults) ? Math.min(20, Math.max(1, Math.floor(params.numResults))) : 5;
|
|
2022
|
+
const domainFilter = Array.isArray(params.domainFilter) ? params.domainFilter.filter((domain) => typeof domain === "string") : undefined;
|
|
2023
|
+
const recencyFilter = normalizeRecencyFilter(params.recencyFilter);
|
|
2024
|
+
const resultsByUrl = new Map;
|
|
2025
|
+
const summaries = [];
|
|
2026
|
+
const errors = [];
|
|
2027
|
+
let provider;
|
|
2028
|
+
for (const query of queries) {
|
|
2029
|
+
if (signal?.aborted)
|
|
2030
|
+
break;
|
|
2031
|
+
try {
|
|
2032
|
+
const response = await search(query, {
|
|
2033
|
+
provider: resolveRequestedProvider(params.provider),
|
|
2034
|
+
numResults,
|
|
2035
|
+
recencyFilter,
|
|
2036
|
+
domainFilter,
|
|
2037
|
+
signal,
|
|
2038
|
+
extensionContext: ctx
|
|
2039
|
+
});
|
|
2040
|
+
if (signal?.aborted)
|
|
2041
|
+
break;
|
|
2042
|
+
provider ??= response.provider;
|
|
2043
|
+
if (response.answer)
|
|
2044
|
+
summaries.push(`${query}: ${response.answer}`);
|
|
2045
|
+
for (const result of response.results) {
|
|
2046
|
+
if (!resultsByUrl.has(result.url))
|
|
2047
|
+
resultsByUrl.set(result.url, result);
|
|
2048
|
+
}
|
|
2049
|
+
} catch (err) {
|
|
2050
|
+
if (signal?.aborted || isAbortError(err))
|
|
2051
|
+
break;
|
|
2052
|
+
errors.push({ query, error: err instanceof Error ? err.message : String(err) });
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
const results = [...resultsByUrl.values()].slice(0, 20).map((result, index) => ({ ...result, rank: index + 1 }));
|
|
2056
|
+
let fetched = [];
|
|
2057
|
+
if (params.fetchContent && results.length > 0) {
|
|
2058
|
+
const urls = results.slice(0, 5).map((result) => result.url);
|
|
2059
|
+
try {
|
|
2060
|
+
fetched = await fetchAllContent(urls, signal, withRegisteredFetchOptions(undefined, registeredToolNames, typeof params.proxy === "string" ? params.proxy : undefined));
|
|
2061
|
+
} catch (err) {
|
|
2062
|
+
if (signal?.aborted || isAbortError(err))
|
|
2063
|
+
throw err;
|
|
2064
|
+
fetched = urls.map((url) => ({ url, title: "", content: "", error: err instanceof Error ? err.message : String(err) }));
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
const artifact = withClaimAssessment(buildResearchArtifact({
|
|
2068
|
+
query: claim,
|
|
2069
|
+
provider,
|
|
2070
|
+
summary: summaries.length > 0 ? summaries.join(`
|
|
2071
|
+
|
|
2072
|
+
`) : undefined,
|
|
2073
|
+
results,
|
|
2074
|
+
fetched,
|
|
2075
|
+
recency: recencyFilter,
|
|
2076
|
+
domainFilter
|
|
2077
|
+
}), [claim]);
|
|
2078
|
+
if (errors.length > 0)
|
|
2079
|
+
artifact.errors = errors;
|
|
2080
|
+
storeResearchArtifact(artifact);
|
|
2081
|
+
pi.appendEntry("web-search-results", {
|
|
2082
|
+
id: artifact.id,
|
|
2083
|
+
type: "research",
|
|
2084
|
+
timestamp: artifact.timestamp,
|
|
2085
|
+
artifact
|
|
2086
|
+
});
|
|
2087
|
+
return {
|
|
2088
|
+
content: [{ type: "text", text: formatSourceCheckResult(artifact, getSearchContentEnabled ? toolNames.getSearchContent : null) }],
|
|
2089
|
+
details: { responseId: artifact.id, artifact, sourceCount: artifact.sources.length, passageCount: artifact.passages.length }
|
|
2090
|
+
};
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
2093
|
+
});
|
|
2094
|
+
if (fetchContentEnabled)
|
|
2095
|
+
pi.registerTool({
|
|
2096
|
+
name: toolNames.fetchContent,
|
|
2097
|
+
label: "Fetch Content",
|
|
2098
|
+
description: `Fetch URL(s) and extract readable content as markdown. Use mode "raw" for exact textual HTTP response bodies or mode "answer" with prompt to answer using only fetched content. Direct image URLs return resized image content. Supports YouTube transcripts, GitHub repositories, PDFs, and local videos. ${fetchContentStorageNote}`,
|
|
2099
|
+
promptSnippet: "Use to fetch readable or raw URL content, direct images, GitHub repos, and videos. Mode answer answers a prompt using only the fetched source.",
|
|
2100
|
+
parameters: Type.Object({
|
|
2101
|
+
url: Type.Optional(Type.String({ description: "Single URL to fetch" })),
|
|
2102
|
+
urls: Type.Optional(Type.Array(Type.String(), { description: "Multiple URLs (parallel)" })),
|
|
2103
|
+
forceClone: Type.Optional(Type.Boolean({
|
|
2104
|
+
description: "Force cloning large GitHub repositories that exceed the size threshold"
|
|
2105
|
+
})),
|
|
2106
|
+
prompt: Type.Optional(Type.String({
|
|
2107
|
+
description: "Question or instruction for video analysis, or the page-local question required by mode answer."
|
|
2108
|
+
})),
|
|
2109
|
+
mode: Type.Optional(StringEnum(["readable", "raw", "answer"], {
|
|
2110
|
+
description: "Fetch mode: readable (default extraction), raw (exact textual HTTP body), or answer (answer prompt using only fetched content)."
|
|
2111
|
+
})),
|
|
2112
|
+
answerModel: Type.Optional(Type.String({
|
|
2113
|
+
description: "Optional provider/model-id override for mode answer. Defaults to fetch.answerProvider + fetch.answerModel when configured, otherwise the current DM model."
|
|
2114
|
+
})),
|
|
2115
|
+
timestamp: Type.Optional(Type.String({
|
|
2116
|
+
description: "Extract video frame(s) at a timestamp or time range. Single: '1:23:45', '23:45', or '85' (seconds). Range: '23:41-25:00' extracts evenly-spaced frames across that span (default 6). Use frames with ranges to control density; single+frames uses a fixed 5s interval. YouTube requires yt-dlp + ffmpeg; local videos require ffmpeg. Use a range when you know the approximate area but not the exact moment — you'll get a contact sheet to visually identify the right frame."
|
|
2117
|
+
})),
|
|
2118
|
+
frames: Type.Optional(Type.Integer({
|
|
2119
|
+
minimum: 1,
|
|
2120
|
+
maximum: 12,
|
|
2121
|
+
description: "Number of frames to extract. Use with timestamp range for custom density, with single timestamp to get N frames at 5s intervals, or alone to sample across the entire video. Requires yt-dlp + ffmpeg for YouTube, ffmpeg for local video."
|
|
2122
|
+
})),
|
|
2123
|
+
model: Type.Optional(Type.String({
|
|
2124
|
+
description: "Override the Gemini model for video/YouTube analysis (e.g. 'gemini-3.6-flash'). Defaults to config or gemini-3.6-flash."
|
|
2125
|
+
})),
|
|
2126
|
+
auth: Type.Optional(Type.Union([Type.String(), Type.Boolean()], {
|
|
2127
|
+
description: "Opt into an authFetch profile for local browser-cookie fetching. Use a profile name, or true only when exactly one profile exists."
|
|
2128
|
+
})),
|
|
2129
|
+
proxy: Type.Optional(Type.String({
|
|
2130
|
+
description: "http(s) proxy URL (e.g. http://host:port) used for this fetch. Needed when the target is unreachable directly; localhost and NO_PROXY hosts always bypass the proxy. Empty string forces direct access."
|
|
2131
|
+
}))
|
|
2132
|
+
}),
|
|
2133
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
2134
|
+
let normalized;
|
|
2135
|
+
try {
|
|
2136
|
+
normalized = normalizeFetchContentParams(params);
|
|
2137
|
+
} catch (err) {
|
|
2138
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
2139
|
+
return { content: [{ type: "text", text: `Error: ${error}` }], details: { error } };
|
|
2140
|
+
}
|
|
2141
|
+
const { urlList, options } = normalized;
|
|
2142
|
+
return runWithProxy(options.proxy, async () => {
|
|
2143
|
+
const mode = options.mode ?? "readable";
|
|
2144
|
+
if (mode === "answer" && !options.prompt) {
|
|
2145
|
+
return { content: [{ type: "text", text: "Error: mode answer requires prompt." }], details: { error: "mode answer requires prompt" } };
|
|
2146
|
+
}
|
|
2147
|
+
if (mode === "raw" && (options.forceClone === true || options.timestamp || options.frames || options.prompt || options.model || options.answerModel)) {
|
|
2148
|
+
return { content: [{ type: "text", text: "Error: mode raw cannot be combined with forceClone, prompt, timestamp, frames, model, or answerModel." }], details: { error: "Incompatible raw mode options" } };
|
|
2149
|
+
}
|
|
2150
|
+
if (mode !== "answer" && options.answerModel) {
|
|
2151
|
+
return { content: [{ type: "text", text: "Error: answerModel requires mode answer." }], details: { error: "answerModel requires mode answer" } };
|
|
2152
|
+
}
|
|
2153
|
+
if (mode === "answer" && options.model) {
|
|
2154
|
+
return { content: [{ type: "text", text: "Error: use answerModel, not model, with mode answer." }], details: { error: "model is incompatible with mode answer" } };
|
|
2155
|
+
}
|
|
2156
|
+
if (mode === "answer" && options.auth !== undefined) {
|
|
2157
|
+
return { content: [{ type: "text", text: "Error: auth cannot be combined with mode answer." }], details: { error: "auth cannot be combined with mode answer" } };
|
|
2158
|
+
}
|
|
2159
|
+
let authFetchProfile;
|
|
2160
|
+
if (options.auth !== undefined) {
|
|
2161
|
+
try {
|
|
2162
|
+
authFetchProfile = resolveAuthFetchProfile(options.auth);
|
|
2163
|
+
} catch (err) {
|
|
2164
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
2165
|
+
return { content: [{ type: "text", text: `Error: ${error}` }], details: { error } };
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
if (urlList.length === 0) {
|
|
2169
|
+
return {
|
|
2170
|
+
content: [{ type: "text", text: "Error: No URL provided." }],
|
|
2171
|
+
details: { error: "No URL provided" }
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
onUpdate?.({
|
|
2175
|
+
content: [{ type: "text", text: `Fetching ${urlList.length} URL(s)...` }],
|
|
2176
|
+
details: { phase: "fetch", progress: 0 }
|
|
2177
|
+
});
|
|
2178
|
+
const { answerModel: _answerModel, auth: _auth, ...extractionOptions } = options;
|
|
2179
|
+
const fetchOptions = mode === "answer" ? (() => {
|
|
2180
|
+
const { prompt: _prompt, ...rest } = extractionOptions;
|
|
2181
|
+
return { ...rest, ...authFetchProfile ? { authFetchProfile } : {} };
|
|
2182
|
+
})() : { ...extractionOptions, ...authFetchProfile ? { authFetchProfile } : {} };
|
|
2183
|
+
const fetchResults = await fetchAllContent(urlList, signal, withRegisteredFetchOptions(fetchOptions, registeredToolNames, options.proxy));
|
|
2184
|
+
const presentedResults = mode === "answer" ? await Promise.all(fetchResults.map(async (result) => {
|
|
2185
|
+
if (result.error)
|
|
2186
|
+
return result;
|
|
2187
|
+
if (result.thumbnail || result.mimeType?.startsWith("image/")) {
|
|
2188
|
+
return { ...result, error: "Page answer requires textual fetched content" };
|
|
2189
|
+
}
|
|
2190
|
+
try {
|
|
2191
|
+
const answer = await answerFromPage({
|
|
2192
|
+
question: options.prompt,
|
|
2193
|
+
pageText: result.content,
|
|
2194
|
+
sourceUrl: result.url,
|
|
2195
|
+
...options.answerModel ? { model: options.answerModel } : {}
|
|
2196
|
+
}, ctx, signal);
|
|
2197
|
+
return { ...result, content: answer.text };
|
|
2198
|
+
} catch (err) {
|
|
2199
|
+
return { ...result, error: `Page answer failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
2200
|
+
}
|
|
2201
|
+
})) : fetchResults;
|
|
2202
|
+
const successful = presentedResults.filter((r) => !r.error).length;
|
|
2203
|
+
const totalChars = presentedResults.reduce((sum, r) => sum + r.content.length, 0);
|
|
2204
|
+
const responseId = generateId();
|
|
2205
|
+
const data = {
|
|
2206
|
+
id: responseId,
|
|
2207
|
+
type: "fetch",
|
|
2208
|
+
timestamp: Date.now(),
|
|
2209
|
+
urls: stripThumbnails(fetchResults)
|
|
2210
|
+
};
|
|
2211
|
+
const storedContent = storeFetchResult(pi, responseId, data, authFetchProfile);
|
|
2212
|
+
if (urlList.length === 1) {
|
|
2213
|
+
const result = presentedResults[0];
|
|
2214
|
+
if (result.error) {
|
|
2215
|
+
return {
|
|
2216
|
+
content: [{ type: "text", text: `Error: ${result.error}` }],
|
|
2217
|
+
details: { urls: urlList, urlCount: 1, successful: 0, error: result.error, ...storedContent ? { responseId } : {}, prompt: params.prompt, timestamp: params.timestamp, frames: params.frames }
|
|
2218
|
+
};
|
|
2219
|
+
}
|
|
2220
|
+
const fullLength = result.content.length;
|
|
2221
|
+
const slice = initialContentSlice(result.content, getMaxInlineContentChars());
|
|
2222
|
+
const truncated = slice.endOffset < fullLength;
|
|
2223
|
+
let output = slice.text;
|
|
2224
|
+
if (truncated) {
|
|
2225
|
+
output += `
|
|
2226
|
+
|
|
2227
|
+
---
|
|
2228
|
+
Showing ${slice.endOffset} of ${fullLength} chars, ${slice.shownBytes} of ${slice.totalBytes} bytes, and ${slice.shownLines} of ${slice.totalLines} lines. `;
|
|
2229
|
+
output += storedContent ? getSearchContentEnabled ? `Use ${toolNames.getSearchContent}({ responseId: "${responseId}", urlIndex: 0, offset: ${slice.endOffset} }) for the next slice.` : "Content retrieval is not registered." : "Authenticated fetch cache is off; repeat the fetch to read more.";
|
|
2230
|
+
}
|
|
2231
|
+
const content = [];
|
|
2232
|
+
if (result.frames?.length) {
|
|
2233
|
+
for (const frame of result.frames) {
|
|
2234
|
+
content.push({ type: "image", data: frame.data, mimeType: frame.mimeType });
|
|
2235
|
+
content.push({ type: "text", text: `Frame at ${frame.timestamp}` });
|
|
2236
|
+
}
|
|
2237
|
+
} else if (result.thumbnail) {
|
|
2238
|
+
content.push({ type: "image", data: result.thumbnail.data, mimeType: result.thumbnail.mimeType });
|
|
2239
|
+
}
|
|
2240
|
+
content.push({ type: "text", text: output });
|
|
2241
|
+
const imageCount = (result.frames?.length ?? 0) + (result.thumbnail ? 1 : 0);
|
|
2242
|
+
return {
|
|
2243
|
+
content,
|
|
2244
|
+
details: {
|
|
2245
|
+
urls: urlList,
|
|
2246
|
+
urlCount: 1,
|
|
2247
|
+
successful: 1,
|
|
2248
|
+
totalChars: fullLength,
|
|
2249
|
+
title: result.title,
|
|
2250
|
+
...storedContent ? { responseId } : {},
|
|
2251
|
+
truncated,
|
|
2252
|
+
hasImage: imageCount > 0,
|
|
2253
|
+
imageCount,
|
|
2254
|
+
prompt: params.prompt,
|
|
2255
|
+
timestamp: params.timestamp,
|
|
2256
|
+
frames: params.frames,
|
|
2257
|
+
duration: result.duration,
|
|
2258
|
+
mode,
|
|
2259
|
+
mimeType: result.mimeType,
|
|
2260
|
+
status: result.status,
|
|
2261
|
+
totalBytes: slice.totalBytes,
|
|
2262
|
+
totalLines: slice.totalLines,
|
|
2263
|
+
shownBytes: slice.shownBytes,
|
|
2264
|
+
shownLines: slice.shownLines
|
|
2265
|
+
}
|
|
2266
|
+
};
|
|
2267
|
+
}
|
|
2268
|
+
let output = `## Fetched URLs
|
|
2269
|
+
|
|
2270
|
+
`;
|
|
2271
|
+
for (const { url, title, content, error } of presentedResults) {
|
|
2272
|
+
if (error) {
|
|
2273
|
+
output += `- ${url}: Error - ${error}
|
|
2274
|
+
`;
|
|
2275
|
+
} else {
|
|
2276
|
+
output += `- ${title || url} (${content.length} chars)
|
|
2277
|
+
`;
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
output += storedContent ? getSearchContentEnabled ? `
|
|
2281
|
+
---
|
|
2282
|
+
Use ${toolNames.getSearchContent}({ responseId: "${responseId}", urlIndex: 0 }) to retrieve bounded content slices.` : `
|
|
2283
|
+
---
|
|
2284
|
+
Content retrieval is not registered.` : `
|
|
2285
|
+
---
|
|
2286
|
+
Authenticated fetch cache is off; repeat the fetch to read content.`;
|
|
2287
|
+
return {
|
|
2288
|
+
content: [{ type: "text", text: output }],
|
|
2289
|
+
details: { urls: urlList, urlCount: urlList.length, successful, totalChars, ...storedContent ? { responseId } : {} }
|
|
2290
|
+
};
|
|
2291
|
+
});
|
|
2292
|
+
},
|
|
2293
|
+
renderCall(args, theme) {
|
|
2294
|
+
const { urlList, options } = normalizeFetchContentParams(args);
|
|
2295
|
+
const { prompt, timestamp, frames, model, mode, answerModel, auth } = options;
|
|
2296
|
+
if (urlList.length === 0) {
|
|
2297
|
+
return new Text(theme.fg("toolTitle", theme.bold("fetch ")) + theme.fg("error", "(no URL)"), 0, 0);
|
|
2298
|
+
}
|
|
2299
|
+
const lines = [];
|
|
2300
|
+
if (urlList.length === 1) {
|
|
2301
|
+
const display = urlList[0].length > 60 ? urlList[0].slice(0, 57) + "..." : urlList[0];
|
|
2302
|
+
lines.push(theme.fg("toolTitle", theme.bold("fetch ")) + theme.fg("accent", display));
|
|
2303
|
+
} else {
|
|
2304
|
+
lines.push(theme.fg("toolTitle", theme.bold("fetch ")) + theme.fg("accent", `${urlList.length} URLs`));
|
|
2305
|
+
for (const u of urlList.slice(0, 5)) {
|
|
2306
|
+
const display = u.length > 60 ? u.slice(0, 57) + "..." : u;
|
|
2307
|
+
lines.push(theme.fg("muted", " " + display));
|
|
2308
|
+
}
|
|
2309
|
+
if (urlList.length > 5) {
|
|
2310
|
+
lines.push(theme.fg("muted", ` ... and ${urlList.length - 5} more`));
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
if (mode && mode !== "readable") {
|
|
2314
|
+
lines.push(theme.fg("dim", " mode: ") + theme.fg("warning", mode));
|
|
2315
|
+
}
|
|
2316
|
+
if (timestamp) {
|
|
2317
|
+
lines.push(theme.fg("dim", " timestamp: ") + theme.fg("warning", timestamp));
|
|
2318
|
+
}
|
|
2319
|
+
if (typeof frames === "number") {
|
|
2320
|
+
lines.push(theme.fg("dim", " frames: ") + theme.fg("warning", String(frames)));
|
|
2321
|
+
}
|
|
2322
|
+
if (prompt) {
|
|
2323
|
+
const display = prompt.length > 250 ? prompt.slice(0, 247) + "..." : prompt;
|
|
2324
|
+
lines.push(theme.fg("dim", " prompt: ") + theme.fg("muted", `"${display}"`));
|
|
2325
|
+
}
|
|
2326
|
+
if (model) {
|
|
2327
|
+
lines.push(theme.fg("dim", " model: ") + theme.fg("warning", model));
|
|
2328
|
+
}
|
|
2329
|
+
if (answerModel) {
|
|
2330
|
+
lines.push(theme.fg("dim", " answer model: ") + theme.fg("warning", answerModel));
|
|
2331
|
+
}
|
|
2332
|
+
if (auth !== undefined) {
|
|
2333
|
+
lines.push(theme.fg("dim", " auth: ") + theme.fg("warning", auth === true ? "true" : auth));
|
|
2334
|
+
}
|
|
2335
|
+
return new Text(lines.join(`
|
|
2336
|
+
`), 0, 0);
|
|
2337
|
+
},
|
|
2338
|
+
renderResult(result, { expanded, isPartial }, theme) {
|
|
2339
|
+
const details = result.details;
|
|
2340
|
+
if (isPartial) {
|
|
2341
|
+
const progress = details?.progress ?? 0;
|
|
2342
|
+
const bar = "█".repeat(Math.floor(progress * 10)) + "░".repeat(10 - Math.floor(progress * 10));
|
|
2343
|
+
return new Text(theme.fg("accent", `[${bar}] ${details?.phase || "fetching"}`), 0, 0);
|
|
2344
|
+
}
|
|
2345
|
+
if (details?.error) {
|
|
2346
|
+
const fd = details;
|
|
2347
|
+
const extras = [];
|
|
2348
|
+
if (typeof fd.urlCount === "number" || typeof fd.successful === "number") {
|
|
2349
|
+
extras.push(`urls: ${fd.successful ?? 0}/${fd.urlCount ?? 0} succeeded`);
|
|
2350
|
+
}
|
|
2351
|
+
if (fd.responseId)
|
|
2352
|
+
extras.push(`response id: ${fd.responseId}`);
|
|
2353
|
+
if (fd.urls && fd.urls.length > 0) {
|
|
2354
|
+
for (const u of fd.urls.slice(0, 8))
|
|
2355
|
+
extras.push(` ▸ ${u}`);
|
|
2356
|
+
if (fd.urls.length > 8)
|
|
2357
|
+
extras.push(` ... and ${fd.urls.length - 8} more`);
|
|
2358
|
+
}
|
|
2359
|
+
const plan = buildSearchErrorPlan({ error: details.error, extraLines: extras });
|
|
2360
|
+
if (plan)
|
|
2361
|
+
return renderSearchErrorPlan(plan, expanded, theme);
|
|
2362
|
+
return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
|
|
2363
|
+
}
|
|
2364
|
+
if (details?.urlCount === 1) {
|
|
2365
|
+
const title = details?.title || "Untitled";
|
|
2366
|
+
const imgCount = details?.imageCount ?? (details?.hasImage ? 1 : 0);
|
|
2367
|
+
const imageBadge = imgCount > 1 ? theme.fg("accent", ` [${imgCount} images]`) : imgCount === 1 ? theme.fg("accent", " [image]") : "";
|
|
2368
|
+
let statusLine = theme.fg("success", title) + theme.fg("muted", ` (${details?.totalChars ?? 0} chars)`) + imageBadge;
|
|
2369
|
+
if (details?.truncated) {
|
|
2370
|
+
statusLine += theme.fg("warning", " [truncated]");
|
|
2371
|
+
}
|
|
2372
|
+
if (typeof details?.duration === "number") {
|
|
2373
|
+
statusLine += theme.fg("muted", ` | ${formatSeconds(Math.floor(details.duration))} total`);
|
|
2374
|
+
}
|
|
2375
|
+
const textContent = result.content.find((c) => c.type === "text")?.text || "";
|
|
2376
|
+
if (!expanded) {
|
|
2377
|
+
const brief = textContent.length > 200 ? textContent.slice(0, 200) + "..." : textContent;
|
|
2378
|
+
return new Text(statusLine + `
|
|
2379
|
+
` + theme.fg("dim", brief), 0, 0);
|
|
2380
|
+
}
|
|
2381
|
+
const lines = [statusLine];
|
|
2382
|
+
if (details?.prompt) {
|
|
2383
|
+
const display = details.prompt.length > 250 ? details.prompt.slice(0, 247) + "..." : details.prompt;
|
|
2384
|
+
lines.push(theme.fg("dim", ` prompt: "${display}"`));
|
|
2385
|
+
}
|
|
2386
|
+
if (details?.timestamp) {
|
|
2387
|
+
lines.push(theme.fg("dim", ` timestamp: ${details.timestamp}`));
|
|
2388
|
+
}
|
|
2389
|
+
if (typeof details?.frames === "number") {
|
|
2390
|
+
lines.push(theme.fg("dim", ` frames: ${details.frames}`));
|
|
2391
|
+
}
|
|
2392
|
+
const preview = textContent.length > 500 ? textContent.slice(0, 500) + "..." : textContent;
|
|
2393
|
+
lines.push(theme.fg("dim", preview));
|
|
2394
|
+
return new Text(lines.join(`
|
|
2395
|
+
`), 0, 0);
|
|
2396
|
+
}
|
|
2397
|
+
const countColor = (details?.successful ?? 0) > 0 ? "success" : "error";
|
|
2398
|
+
const statusLine = theme.fg(countColor, `${details?.successful}/${details?.urlCount} URLs`) + theme.fg("muted", getSearchContentEnabled ? " (content stored)" : " (content fetched)");
|
|
2399
|
+
if (!expanded) {
|
|
2400
|
+
return new Text(statusLine, 0, 0);
|
|
2401
|
+
}
|
|
2402
|
+
const textContent = result.content.find((c) => c.type === "text")?.text || "";
|
|
2403
|
+
const preview = textContent.length > 500 ? textContent.slice(0, 500) + "..." : textContent;
|
|
2404
|
+
return new Text(statusLine + `
|
|
2405
|
+
` + theme.fg("dim", preview), 0, 0);
|
|
2406
|
+
}
|
|
2407
|
+
});
|
|
2408
|
+
if (getSearchContentEnabled) {
|
|
2409
|
+
const maxInlineContentChars = getMaxInlineContentChars(initConfig);
|
|
2410
|
+
pi.registerTool({
|
|
2411
|
+
name: toolNames.getSearchContent,
|
|
2412
|
+
label: "Get Search Content",
|
|
2413
|
+
description: `Retrieve bounded content slices or find matching passages in a previous ${storedContentSources} call.`,
|
|
2414
|
+
promptSnippet: `Use after ${storedContentSources} to retrieve stored content via responseId. Use findText to locate passages without paging through the full content.`,
|
|
2415
|
+
parameters: Type.Object({
|
|
2416
|
+
responseId: Type.String({ description: `The responseId from ${storedContentSources}` }),
|
|
2417
|
+
query: Type.Optional(Type.String({ description: searchQueryDescription })),
|
|
2418
|
+
queryIndex: Type.Optional(Type.Integer({ minimum: 0, description: "Get content for query at index" })),
|
|
2419
|
+
url: Type.Optional(Type.String({ description: "Get content for this URL" })),
|
|
2420
|
+
urlIndex: Type.Optional(Type.Integer({ minimum: 0, description: "Get content for URL at index" })),
|
|
2421
|
+
offset: Type.Optional(Type.Integer({ minimum: 0, description: "Character offset for fetched URL content slices (default 0). Ignored when findText is supplied." })),
|
|
2422
|
+
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: maxInlineContentChars, description: "Maximum characters to return for fetched URL content slices (default and max are set by maxInlineContentChars). Ignored when findText is supplied." })),
|
|
2423
|
+
findText: Type.Optional(Type.Union([
|
|
2424
|
+
Type.String({ minLength: 1, maxLength: 500 }),
|
|
2425
|
+
Type.Array(Type.String({ minLength: 1, maxLength: 500 }), { minItems: 1, maxItems: 10 })
|
|
2426
|
+
], { description: "Text or texts to find in the selected stored content. When supplied, offset and limit are ignored." })),
|
|
2427
|
+
findMode: Type.Optional(StringEnum(["exact", "case-insensitive", "fuzzy"], { description: "Matching mode for findText (default: case-insensitive). Requires findText." }))
|
|
2428
|
+
}),
|
|
2429
|
+
async execute(_toolCallId, rawParams) {
|
|
2430
|
+
const params = normalizeGetSearchContentParams(rawParams);
|
|
2431
|
+
if (params.findMode !== undefined && params.findText === undefined) {
|
|
2432
|
+
return {
|
|
2433
|
+
content: [{ type: "text", text: `findMode ${formatInputValue(params.findMode)} requires findText; provide findText or omit findMode.` }],
|
|
2434
|
+
details: { error: "findMode requires findText" }
|
|
2435
|
+
};
|
|
2436
|
+
}
|
|
2437
|
+
const data = getResult(params.responseId);
|
|
2438
|
+
if (!data) {
|
|
2439
|
+
return {
|
|
2440
|
+
content: [{ type: "text", text: `Error: No stored results for responseId ${formatInputValue(params.responseId)}. Use a responseId returned by ${storedContentSources}.` }],
|
|
2441
|
+
details: { error: "Not found", responseId: params.responseId }
|
|
2442
|
+
};
|
|
2443
|
+
}
|
|
2444
|
+
if (data.type === "research") {
|
|
2445
|
+
const artifact = getResearchArtifact(params.responseId);
|
|
2446
|
+
if (!artifact) {
|
|
2447
|
+
return {
|
|
2448
|
+
content: [{ type: "text", text: `Error: stored research artifact for responseId ${formatInputValue(params.responseId)} was not found. Use a responseId returned by ${storedContentSources}.` }],
|
|
2449
|
+
details: { error: "Artifact not found", responseId: params.responseId }
|
|
2450
|
+
};
|
|
2451
|
+
}
|
|
2452
|
+
const serialized = JSON.stringify(artifact, null, 2);
|
|
2453
|
+
if (params.findText !== undefined) {
|
|
2454
|
+
try {
|
|
2455
|
+
const found = findContent(serialized, normalizeFindQueries(params.findText), params.findMode ?? "case-insensitive");
|
|
2456
|
+
const { text, ...findDetails } = found;
|
|
2457
|
+
return {
|
|
2458
|
+
content: [{ type: "text", text }],
|
|
2459
|
+
details: { responseId: artifact.id, type: "research", contentLength: serialized.length, findMode: params.findMode ?? "case-insensitive", ...findDetails }
|
|
2460
|
+
};
|
|
2461
|
+
} catch (err) {
|
|
2462
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
2463
|
+
return {
|
|
2464
|
+
content: [{ type: "text", text: `Unable to find ${formatInputValue(params.findText)} in research artifact for responseId ${formatInputValue(params.responseId)}: ${error}. Check findText and use a supported findMode.` }],
|
|
2465
|
+
details: { error, responseId: params.responseId, type: "research" }
|
|
2466
|
+
};
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
const offset = params.offset ?? 0;
|
|
2470
|
+
const limit = params.limit ?? maxInlineContentChars;
|
|
2471
|
+
if (!Number.isInteger(offset) || offset < 0) {
|
|
2472
|
+
return {
|
|
2473
|
+
content: [{ type: "text", text: `Invalid offset: received ${formatInputValue(offset)} for responseId ${formatInputValue(params.responseId)}; offset must be a non-negative integer. Use 0 or a larger integer.` }],
|
|
2474
|
+
details: { error: "Invalid offset", offset }
|
|
2475
|
+
};
|
|
2476
|
+
}
|
|
2477
|
+
if (!Number.isInteger(limit) || limit <= 0 || limit > maxInlineContentChars) {
|
|
2478
|
+
return {
|
|
2479
|
+
content: [{ type: "text", text: `Invalid limit: received ${formatInputValue(limit)} for responseId ${formatInputValue(params.responseId)}; limit must be an integer from 1 to ${maxInlineContentChars}. Use a value in that range.` }],
|
|
2480
|
+
details: { error: "Invalid limit", limit, maxLimit: maxInlineContentChars }
|
|
2481
|
+
};
|
|
2482
|
+
}
|
|
2483
|
+
if (offset > serialized.length) {
|
|
2484
|
+
return {
|
|
2485
|
+
content: [{ type: "text", text: `Offset ${offset} is out of range for responseId ${formatInputValue(params.responseId)}. Received offset ${offset}; valid range is 0-${serialized.length}. Use an offset within that range.` }],
|
|
2486
|
+
details: { error: "Offset out of range", offset, contentLength: serialized.length }
|
|
2487
|
+
};
|
|
2488
|
+
}
|
|
2489
|
+
const endOffset = Math.min(offset + limit, serialized.length);
|
|
2490
|
+
const artifactSlice = serialized.slice(offset, endOffset);
|
|
2491
|
+
const hasMore = endOffset < serialized.length;
|
|
2492
|
+
return {
|
|
2493
|
+
content: [{ type: "text", text: artifactSlice }],
|
|
2494
|
+
details: { responseId: artifact.id, type: "research", contentLength: serialized.length, offset, limit, returnedChars: artifactSlice.length, nextOffset: hasMore ? endOffset : null, truncated: hasMore }
|
|
2495
|
+
};
|
|
2496
|
+
}
|
|
2497
|
+
if (data.type === "search" && data.queries) {
|
|
2498
|
+
let queryData;
|
|
2499
|
+
if (params.query !== undefined) {
|
|
2500
|
+
queryData = data.queries.find((q) => q.query === params.query);
|
|
2501
|
+
if (!queryData) {
|
|
2502
|
+
const available = data.queries.map((q) => `"${q.query}"`).join(", ");
|
|
2503
|
+
return {
|
|
2504
|
+
content: [{ type: "text", text: `Query ${formatInputValue(params.query)} was not found for responseId ${formatInputValue(params.responseId)}. Received query=${formatInputValue(params.query)}. Available queries: ${available || "none"}. Use one of the available queries or queryIndex.` }],
|
|
2505
|
+
details: { error: "Query not found" }
|
|
2506
|
+
};
|
|
2507
|
+
}
|
|
2508
|
+
} else if (params.queryIndex !== undefined) {
|
|
2509
|
+
queryData = data.queries[params.queryIndex];
|
|
2510
|
+
if (!queryData) {
|
|
2511
|
+
const available = data.queries.map((q, i) => `${i}: "${q.query}"`).join(", ");
|
|
2512
|
+
return {
|
|
2513
|
+
content: [{ type: "text", text: `Query index ${formatInputValue(params.queryIndex)} is out of range for responseId ${formatInputValue(params.responseId)}. Received queryIndex=${formatInputValue(params.queryIndex)}; valid indexes are 0-${data.queries.length - 1}. Available queries: ${available || "none"}. Use one of the available indexes.` }],
|
|
2514
|
+
details: { error: "Index out of range" }
|
|
2515
|
+
};
|
|
2516
|
+
}
|
|
2517
|
+
} else {
|
|
2518
|
+
const available = data.queries.map((q, i) => `${i}: "${q.query}"`).join(", ");
|
|
2519
|
+
return {
|
|
2520
|
+
content: [{ type: "text", text: `Specify query or queryIndex for responseId ${formatInputValue(params.responseId)}. Available queries: ${available || "none"}.` }],
|
|
2521
|
+
details: { error: "No query specified" }
|
|
2522
|
+
};
|
|
2523
|
+
}
|
|
2524
|
+
if (queryData.error) {
|
|
2525
|
+
return {
|
|
2526
|
+
content: [{ type: "text", text: `Error retrieving query ${formatInputValue(queryData.query)} from responseId ${formatInputValue(params.responseId)}: ${queryData.error}. Check the stored search result and retry with another query or queryIndex if needed.` }],
|
|
2527
|
+
details: { error: queryData.error, query: queryData.query }
|
|
2528
|
+
};
|
|
2529
|
+
}
|
|
2530
|
+
const fullResults = formatFullResults(queryData);
|
|
2531
|
+
if (params.findText !== undefined) {
|
|
2532
|
+
try {
|
|
2533
|
+
const found = findContent(fullResults, normalizeFindQueries(params.findText), params.findMode ?? "case-insensitive");
|
|
2534
|
+
const { text, ...findDetails } = found;
|
|
2535
|
+
return {
|
|
2536
|
+
content: [{ type: "text", text }],
|
|
2537
|
+
details: { query: queryData.query, resultCount: queryData.results.length, findMode: params.findMode ?? "case-insensitive", ...findDetails }
|
|
2538
|
+
};
|
|
2539
|
+
} catch (err) {
|
|
2540
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
2541
|
+
return {
|
|
2542
|
+
content: [{ type: "text", text: `Unable to find ${formatInputValue(params.findText)} in query ${formatInputValue(queryData.query)} for responseId ${formatInputValue(params.responseId)}: ${error}. Check findText and use a supported findMode.` }],
|
|
2543
|
+
details: { error, query: queryData.query }
|
|
2544
|
+
};
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
return {
|
|
2548
|
+
content: [{ type: "text", text: fullResults }],
|
|
2549
|
+
details: { query: queryData.query, resultCount: queryData.results.length }
|
|
2550
|
+
};
|
|
2551
|
+
}
|
|
2552
|
+
if (data.type === "fetch" && data.urls) {
|
|
2553
|
+
let urlData;
|
|
2554
|
+
let selectedUrlIndex = -1;
|
|
2555
|
+
if (params.url !== undefined) {
|
|
2556
|
+
selectedUrlIndex = data.urls.findIndex((u) => u.url === params.url);
|
|
2557
|
+
urlData = data.urls[selectedUrlIndex];
|
|
2558
|
+
if (!urlData) {
|
|
2559
|
+
const available = data.urls.map((u) => u.url).join(`
|
|
2560
|
+
`);
|
|
2561
|
+
return {
|
|
2562
|
+
content: [{ type: "text", text: `URL ${formatInputValue(params.url)} was not found for responseId ${formatInputValue(params.responseId)}. Received url=${formatInputValue(params.url)}. Available URLs:
|
|
2563
|
+
${available || " none"}
|
|
2564
|
+
Use one of the available URLs or urlIndex.` }],
|
|
2565
|
+
details: { error: "URL not found" }
|
|
2566
|
+
};
|
|
2567
|
+
}
|
|
2568
|
+
} else if (params.urlIndex !== undefined) {
|
|
2569
|
+
selectedUrlIndex = params.urlIndex;
|
|
2570
|
+
urlData = data.urls[selectedUrlIndex];
|
|
2571
|
+
if (!urlData) {
|
|
2572
|
+
const available = data.urls.map((u, i) => `${i}: ${u.url}`).join(`
|
|
2573
|
+
`);
|
|
2574
|
+
return {
|
|
2575
|
+
content: [{ type: "text", text: `URL index ${formatInputValue(params.urlIndex)} is out of range for responseId ${formatInputValue(params.responseId)}. Received urlIndex=${formatInputValue(params.urlIndex)}; valid indexes are 0-${data.urls.length - 1}. Available URLs:
|
|
2576
|
+
${available || " none"}
|
|
2577
|
+
Use one of the available indexes.` }],
|
|
2578
|
+
details: { error: "Index out of range" }
|
|
2579
|
+
};
|
|
2580
|
+
}
|
|
2581
|
+
} else {
|
|
2582
|
+
const available = data.urls.map((u, i) => `${i}: ${u.url}`).join(`
|
|
2583
|
+
`);
|
|
2584
|
+
return {
|
|
2585
|
+
content: [{ type: "text", text: `Specify url or urlIndex for responseId ${formatInputValue(params.responseId)}. Available URLs:
|
|
2586
|
+
${available || " none"}` }],
|
|
2587
|
+
details: { error: "No URL specified" }
|
|
2588
|
+
};
|
|
2589
|
+
}
|
|
2590
|
+
if (urlData.error) {
|
|
2591
|
+
return {
|
|
2592
|
+
content: [{ type: "text", text: `Error retrieving URL ${formatInputValue(urlData.url)} from responseId ${formatInputValue(params.responseId)}: ${urlData.error}. Check the stored fetch result and retry with another URL or urlIndex if needed.` }],
|
|
2593
|
+
details: { error: urlData.error, url: urlData.url }
|
|
2594
|
+
};
|
|
2595
|
+
}
|
|
2596
|
+
if (params.findText !== undefined) {
|
|
2597
|
+
try {
|
|
2598
|
+
const found = findContent(urlData.content, normalizeFindQueries(params.findText), params.findMode ?? "case-insensitive");
|
|
2599
|
+
const { text, ...findDetails } = found;
|
|
2600
|
+
return {
|
|
2601
|
+
content: [{ type: "text", text: `# ${urlData.title || urlData.url}
|
|
2602
|
+
|
|
2603
|
+
${text}` }],
|
|
2604
|
+
details: { url: urlData.url, title: urlData.title, contentLength: urlData.content.length, findMode: params.findMode ?? "case-insensitive", ...findDetails }
|
|
2605
|
+
};
|
|
2606
|
+
} catch (err) {
|
|
2607
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
2608
|
+
return {
|
|
2609
|
+
content: [{ type: "text", text: `Unable to find ${formatInputValue(params.findText)} in URL ${formatInputValue(urlData.url)} for responseId ${formatInputValue(params.responseId)}: ${error}. Check findText and use a supported findMode.` }],
|
|
2610
|
+
details: { error, url: urlData.url }
|
|
2611
|
+
};
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
const offset = params.offset ?? 0;
|
|
2615
|
+
const limit = params.limit ?? maxInlineContentChars;
|
|
2616
|
+
if (!Number.isInteger(offset) || offset < 0) {
|
|
2617
|
+
return {
|
|
2618
|
+
content: [{ type: "text", text: `Invalid offset: received ${formatInputValue(offset)} for URL ${formatInputValue(urlData.url)}; offset must be a non-negative integer. Use 0 or a larger integer.` }],
|
|
2619
|
+
details: { error: "Invalid offset", offset }
|
|
2620
|
+
};
|
|
2621
|
+
}
|
|
2622
|
+
if (!Number.isInteger(limit) || limit <= 0 || limit > maxInlineContentChars) {
|
|
2623
|
+
return {
|
|
2624
|
+
content: [{ type: "text", text: `Invalid limit: received ${formatInputValue(limit)} for URL ${formatInputValue(urlData.url)}; limit must be an integer from 1 to ${maxInlineContentChars}. Use a value in that range.` }],
|
|
2625
|
+
details: { error: "Invalid limit", limit, maxLimit: maxInlineContentChars }
|
|
2626
|
+
};
|
|
2627
|
+
}
|
|
2628
|
+
if (offset > urlData.content.length) {
|
|
2629
|
+
return {
|
|
2630
|
+
content: [{ type: "text", text: `Offset ${offset} is out of range for URL ${formatInputValue(urlData.url)} in responseId ${formatInputValue(params.responseId)}. Received offset ${offset}; valid range is 0-${urlData.content.length}. Use an offset within that range.` }],
|
|
2631
|
+
details: { error: "Offset out of range", offset, contentLength: urlData.content.length }
|
|
2632
|
+
};
|
|
2633
|
+
}
|
|
2634
|
+
const endOffset = Math.min(offset + limit, urlData.content.length);
|
|
2635
|
+
const contentSlice = urlData.content.slice(offset, endOffset);
|
|
2636
|
+
const hasMore = endOffset < urlData.content.length;
|
|
2637
|
+
let text = `# ${urlData.title || urlData.url}
|
|
2638
|
+
|
|
2639
|
+
${contentSlice}`;
|
|
2640
|
+
if (hasMore || offset > 0) {
|
|
2641
|
+
text += `
|
|
2642
|
+
|
|
2643
|
+
---
|
|
2644
|
+
Showing chars ${offset}-${endOffset} of ${urlData.content.length}.`;
|
|
2645
|
+
if (hasMore) {
|
|
2646
|
+
text += ` Use ${toolNames.getSearchContent}({ responseId: "${params.responseId}", urlIndex: ${selectedUrlIndex}, offset: ${endOffset}, limit: ${limit} }) for the next slice.`;
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
return {
|
|
2650
|
+
content: [{ type: "text", text }],
|
|
2651
|
+
details: {
|
|
2652
|
+
url: urlData.url,
|
|
2653
|
+
title: urlData.title,
|
|
2654
|
+
contentLength: urlData.content.length,
|
|
2655
|
+
offset,
|
|
2656
|
+
limit,
|
|
2657
|
+
returnedChars: contentSlice.length,
|
|
2658
|
+
nextOffset: hasMore ? endOffset : null,
|
|
2659
|
+
truncated: hasMore
|
|
2660
|
+
}
|
|
2661
|
+
};
|
|
2662
|
+
}
|
|
2663
|
+
return {
|
|
2664
|
+
content: [{ type: "text", text: `Invalid stored data for responseId ${formatInputValue(params.responseId)}: received type ${formatInputValue(data.type)}. Use a responseId returned by ${storedContentSources}.` }],
|
|
2665
|
+
details: { error: "Invalid data" }
|
|
2666
|
+
};
|
|
2667
|
+
},
|
|
2668
|
+
renderCall(args, theme) {
|
|
2669
|
+
const { responseId, query, queryIndex, url, urlIndex, offset, findText } = args;
|
|
2670
|
+
let target = "";
|
|
2671
|
+
if (query)
|
|
2672
|
+
target = `query="${query}"`;
|
|
2673
|
+
else if (queryIndex !== undefined)
|
|
2674
|
+
target = `queryIndex=${queryIndex}`;
|
|
2675
|
+
else if (url)
|
|
2676
|
+
target = url.length > 30 ? url.slice(0, 27) + "..." : url;
|
|
2677
|
+
else if (urlIndex !== undefined)
|
|
2678
|
+
target = `urlIndex=${urlIndex}`;
|
|
2679
|
+
if (offset !== undefined)
|
|
2680
|
+
target += target ? ` @ ${offset}` : `offset=${offset}`;
|
|
2681
|
+
if (findText !== undefined) {
|
|
2682
|
+
const queries = Array.isArray(findText) ? findText : [findText];
|
|
2683
|
+
target += `${target ? " · " : ""}find ${queries.length}`;
|
|
2684
|
+
}
|
|
2685
|
+
return new Text(theme.fg("toolTitle", theme.bold("get_content ")) + theme.fg("accent", target || responseId.slice(0, 8)), 0, 0);
|
|
2686
|
+
},
|
|
2687
|
+
renderResult(result, { expanded }, theme) {
|
|
2688
|
+
const details = result.details;
|
|
2689
|
+
if (details?.error) {
|
|
2690
|
+
const extras = [];
|
|
2691
|
+
if (details.query)
|
|
2692
|
+
extras.push(`query: ${details.query}`);
|
|
2693
|
+
if (details.url)
|
|
2694
|
+
extras.push(`url: ${details.url}`);
|
|
2695
|
+
else if (details.title)
|
|
2696
|
+
extras.push(`resource: ${details.title}`);
|
|
2697
|
+
const plan = buildSearchErrorPlan({ error: details.error, extraLines: extras });
|
|
2698
|
+
if (plan)
|
|
2699
|
+
return renderSearchErrorPlan(plan, expanded, theme);
|
|
2700
|
+
return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
|
|
2701
|
+
}
|
|
2702
|
+
let statusLine;
|
|
2703
|
+
if (typeof details?.matchCount === "number") {
|
|
2704
|
+
statusLine = theme.fg("success", details?.title || details?.query || "Content") + theme.fg("muted", ` (${details.matchCount} matches, ${details.returnedMatches ?? 0} shown)`);
|
|
2705
|
+
} else if (details?.query) {
|
|
2706
|
+
statusLine = theme.fg("success", `"${details.query}"`) + theme.fg("muted", ` (${details.resultCount} results)`);
|
|
2707
|
+
} else {
|
|
2708
|
+
const start = details?.offset ?? 0;
|
|
2709
|
+
const returned = details?.returnedChars ?? details?.contentLength ?? 0;
|
|
2710
|
+
const end = start + returned;
|
|
2711
|
+
const slice = details?.nextOffset !== undefined || start > 0 ? `, showing ${start}-${end}` : "";
|
|
2712
|
+
statusLine = theme.fg("success", details?.title || "Content") + theme.fg("muted", ` (${details?.contentLength ?? 0} chars${slice})`);
|
|
2713
|
+
}
|
|
2714
|
+
if (!expanded) {
|
|
2715
|
+
return new Text(statusLine, 0, 0);
|
|
2716
|
+
}
|
|
2717
|
+
const textContent = result.content.find((c) => c.type === "text")?.text || "";
|
|
2718
|
+
const preview = textContent.length > 500 ? textContent.slice(0, 500) + "..." : textContent;
|
|
2719
|
+
return new Text(statusLine + `
|
|
2720
|
+
` + theme.fg("dim", preview), 0, 0);
|
|
2721
|
+
}
|
|
2722
|
+
});
|
|
2723
|
+
}
|
|
2724
|
+
if (isCommandEnabled(initConfig, "websearch"))
|
|
2725
|
+
pi.registerCommand("websearch", {
|
|
2726
|
+
description: "Open web search curator",
|
|
2727
|
+
handler: async (args, ctx) => {
|
|
2728
|
+
const sessionToken = randomUUID();
|
|
2729
|
+
const commandCallId = `cmd:${sessionToken}`;
|
|
2730
|
+
closeCurator(commandCallId);
|
|
2731
|
+
const raw = args.trim();
|
|
2732
|
+
const queries = raw.length > 0 ? normalizeQueryList(raw.split(",")) : [];
|
|
2733
|
+
let bootstrap;
|
|
2734
|
+
try {
|
|
2735
|
+
bootstrap = await loadCuratorBootstrap(undefined, ctx);
|
|
2736
|
+
} catch (err) {
|
|
2737
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2738
|
+
ctx.ui.notify(`Failed to load web search config: ${message}`, "error");
|
|
2739
|
+
return;
|
|
2740
|
+
}
|
|
2741
|
+
const availableProviders = bootstrap.availableProviders;
|
|
2742
|
+
const initialProvider = bootstrap.defaultProvider;
|
|
2743
|
+
const curatorTimeoutSeconds = bootstrap.timeoutSeconds;
|
|
2744
|
+
let currentProvider = initialProvider;
|
|
2745
|
+
const commandConfig = loadConfig();
|
|
2746
|
+
const rawSearchProvider = normalizeProviderInput(commandConfig.searchProvider ?? commandConfig.provider ?? "auto", `provider in ${WEB_SEARCH_CONFIG_PATH}`) ?? "auto";
|
|
2747
|
+
let currentSearchProvider = Array.isArray(rawSearchProvider) ? rawSearchProvider : rawSearchProvider === "auto" ? "auto" : initialProvider;
|
|
2748
|
+
const summaryContext = {
|
|
2749
|
+
model: ctx.model,
|
|
2750
|
+
modelRegistry: ctx.modelRegistry,
|
|
2751
|
+
cwd: ctx.cwd,
|
|
2752
|
+
isProjectTrusted: () => ctx.isProjectTrusted()
|
|
2753
|
+
};
|
|
2754
|
+
const summaryModelChoices = await loadSummaryModelChoices(summaryContext);
|
|
2755
|
+
ctx.ui.notify("Opening web search curator...", "info");
|
|
2756
|
+
const collected = new Map;
|
|
2757
|
+
const searchAbort = new AbortController;
|
|
2758
|
+
let aborted = false;
|
|
2759
|
+
let commandHandle = null;
|
|
2760
|
+
const isCommandActive = () => commandHandle !== null && activeCurators.get(commandCallId) === commandHandle;
|
|
2761
|
+
function sendFollowUpFromReturn(payload) {
|
|
2762
|
+
pi.sendMessage({
|
|
2763
|
+
customType: "web-search-results",
|
|
2764
|
+
content: payload.content,
|
|
2765
|
+
display: true,
|
|
2766
|
+
details: payload.details
|
|
2767
|
+
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
2768
|
+
}
|
|
2769
|
+
try {
|
|
2770
|
+
const handle = await startCuratorServer({
|
|
2771
|
+
queries,
|
|
2772
|
+
sessionToken,
|
|
2773
|
+
timeout: curatorTimeoutSeconds,
|
|
2774
|
+
availableProviders,
|
|
2775
|
+
defaultProvider: initialProvider,
|
|
2776
|
+
searchProvider: toCuratorProvider(currentSearchProvider) ?? "auto",
|
|
2777
|
+
summaryModels: summaryModelChoices.summaryModels,
|
|
2778
|
+
defaultSummaryModel: summaryModelChoices.defaultSummaryModel
|
|
2779
|
+
}, {
|
|
2780
|
+
async onSummarize(selectedQueryIndices, summarizeSignal, model, feedback) {
|
|
2781
|
+
if (commandHandle && !isCommandActive()) {
|
|
2782
|
+
throw new Error("Curator session is no longer active.");
|
|
2783
|
+
}
|
|
2784
|
+
return generateSummaryForSelectedIndices(selectedQueryIndices, collected, summaryContext, summarizeSignal, model, feedback);
|
|
2785
|
+
},
|
|
2786
|
+
onSubmit(payload) {
|
|
2787
|
+
if (commandHandle && !isCommandActive())
|
|
2788
|
+
return;
|
|
2789
|
+
aborted = true;
|
|
2790
|
+
searchAbort.abort();
|
|
2791
|
+
const filtered = payload.selectedQueryIndices.length > 0 ? filterByQueryIndices(payload.selectedQueryIndices, collected) : collectAllResultsAndUrls(collected);
|
|
2792
|
+
const base = {
|
|
2793
|
+
queryList: filtered.results.map((r) => r.query),
|
|
2794
|
+
results: filtered.results,
|
|
2795
|
+
urls: filtered.urls,
|
|
2796
|
+
includeContent: false,
|
|
2797
|
+
curated: true,
|
|
2798
|
+
curatedFrom: collected.size
|
|
2799
|
+
};
|
|
2800
|
+
if (!payload.rawResults) {
|
|
2801
|
+
const resolvedSummary = resolveSummaryForSubmit(payload, collected);
|
|
2802
|
+
base.workflow = "summary-review";
|
|
2803
|
+
base.approvedSummary = resolvedSummary.approvedSummary;
|
|
2804
|
+
base.summaryMeta = resolvedSummary.summaryMeta;
|
|
2805
|
+
}
|
|
2806
|
+
sendFollowUpFromReturn(buildSearchReturn(base));
|
|
2807
|
+
closeCurator(commandCallId);
|
|
2808
|
+
},
|
|
2809
|
+
onCancel(reason) {
|
|
2810
|
+
if (commandHandle && !isCommandActive())
|
|
2811
|
+
return;
|
|
2812
|
+
aborted = true;
|
|
2813
|
+
searchAbort.abort();
|
|
2814
|
+
if (reason === "timeout") {
|
|
2815
|
+
const all = collectAllResultsAndUrls(collected);
|
|
2816
|
+
const resolvedSummary = resolveSummaryForSubmit({ selectedQueryIndices: [], summary: undefined, summaryMeta: undefined }, collected);
|
|
2817
|
+
sendFollowUpFromReturn(buildSearchReturn({
|
|
2818
|
+
queryList: all.results.map((r) => r.query),
|
|
2819
|
+
results: all.results,
|
|
2820
|
+
urls: all.urls,
|
|
2821
|
+
includeContent: false,
|
|
2822
|
+
curated: true,
|
|
2823
|
+
curatedFrom: collected.size,
|
|
2824
|
+
workflow: "summary-review",
|
|
2825
|
+
approvedSummary: resolvedSummary.approvedSummary,
|
|
2826
|
+
summaryMeta: resolvedSummary.summaryMeta
|
|
2827
|
+
}));
|
|
2828
|
+
}
|
|
2829
|
+
closeCurator(commandCallId);
|
|
2830
|
+
},
|
|
2831
|
+
onProviderChange(provider) {
|
|
2832
|
+
if (commandHandle && !isCommandActive())
|
|
2833
|
+
return;
|
|
2834
|
+
const normalized = normalizeProviderInput(provider);
|
|
2835
|
+
if (!normalized || normalized === "auto" || Array.isArray(normalized))
|
|
2836
|
+
return;
|
|
2837
|
+
currentProvider = normalized;
|
|
2838
|
+
currentSearchProvider = normalized;
|
|
2839
|
+
try {
|
|
2840
|
+
saveConfig({ provider: normalized });
|
|
2841
|
+
} catch (err) {
|
|
2842
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2843
|
+
console.error(`Failed to persist default provider: ${message}`);
|
|
2844
|
+
}
|
|
2845
|
+
},
|
|
2846
|
+
async onAddSearch(query, provider) {
|
|
2847
|
+
if (commandHandle && !isCommandActive()) {
|
|
2848
|
+
throw new Error("Curator session is no longer active.");
|
|
2849
|
+
}
|
|
2850
|
+
const requestedProvider = resolveCuratorSearchProvider(provider, currentSearchProvider);
|
|
2851
|
+
const response = await search(query, {
|
|
2852
|
+
provider: requestedProvider,
|
|
2853
|
+
signal: searchAbort.signal,
|
|
2854
|
+
extensionContext: ctx
|
|
2855
|
+
});
|
|
2856
|
+
if (commandHandle && !isCommandActive()) {
|
|
2857
|
+
throw new Error("Curator session is no longer active.");
|
|
2858
|
+
}
|
|
2859
|
+
return toCuratorSearchEntries(response);
|
|
2860
|
+
},
|
|
2861
|
+
onAddSearchResults(entries) {
|
|
2862
|
+
if (commandHandle && !isCommandActive())
|
|
2863
|
+
return;
|
|
2864
|
+
for (const entry of entries) {
|
|
2865
|
+
collected.set(entry.queryIndex, indexedCuratorEntryToQueryResult(entry));
|
|
2866
|
+
}
|
|
2867
|
+
},
|
|
2868
|
+
async onRewriteQuery(query, rewriteSignal) {
|
|
2869
|
+
if (commandHandle && !isCommandActive()) {
|
|
2870
|
+
throw new Error("Curator session is no longer active.");
|
|
2871
|
+
}
|
|
2872
|
+
return rewriteSearchQuery(query, summaryContext, rewriteSignal);
|
|
2873
|
+
}
|
|
2874
|
+
});
|
|
2875
|
+
commandHandle = handle;
|
|
2876
|
+
activeCurators.set(commandCallId, handle);
|
|
2877
|
+
let browserOpenError = null;
|
|
2878
|
+
if (!shouldAutoOpenCuratorBrowser(loadConfig())) {
|
|
2879
|
+
ctx.ui.notify(`Search curator is running. Open manually: ${handle.url}`, "info");
|
|
2880
|
+
} else {
|
|
2881
|
+
const open = platform() === "darwin" ? await getGlimpseOpen() : null;
|
|
2882
|
+
if (open) {
|
|
2883
|
+
try {
|
|
2884
|
+
const win = openInGlimpse(open, handle.url, "Search Curator");
|
|
2885
|
+
glimpseWins.set(commandCallId, win);
|
|
2886
|
+
win.on("closed", () => {
|
|
2887
|
+
if (glimpseWins.get(commandCallId) === win) {
|
|
2888
|
+
glimpseWins.delete(commandCallId);
|
|
2889
|
+
closeCurator(commandCallId);
|
|
2890
|
+
}
|
|
2891
|
+
});
|
|
2892
|
+
} catch (err) {
|
|
2893
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2894
|
+
console.error(`Failed to open Glimpse curator window: ${message}`);
|
|
2895
|
+
glimpseWins.delete(commandCallId);
|
|
2896
|
+
try {
|
|
2897
|
+
await openInBrowser(pi, handle.url);
|
|
2898
|
+
} catch (browserErr) {
|
|
2899
|
+
browserOpenError = browserErr instanceof Error ? browserErr.message : String(browserErr);
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
} else {
|
|
2903
|
+
try {
|
|
2904
|
+
await openInBrowser(pi, handle.url);
|
|
2905
|
+
} catch (browserErr) {
|
|
2906
|
+
browserOpenError = browserErr instanceof Error ? browserErr.message : String(browserErr);
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2909
|
+
if (browserOpenError) {
|
|
2910
|
+
console.error(`Failed to open curator UI: ${browserOpenError}`);
|
|
2911
|
+
ctx.ui.notify(`Search curator is running, but the browser did not open automatically. Open manually: ${handle.url}`, "info");
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
if (queries.length > 0) {
|
|
2915
|
+
(async () => {
|
|
2916
|
+
let nextResultIndex = queries.length;
|
|
2917
|
+
for (let qi = 0;qi < queries.length; qi++) {
|
|
2918
|
+
if (aborted || !isCommandActive())
|
|
2919
|
+
break;
|
|
2920
|
+
const requestedProvider = currentSearchProvider;
|
|
2921
|
+
try {
|
|
2922
|
+
const response = await search(queries[qi], {
|
|
2923
|
+
provider: requestedProvider,
|
|
2924
|
+
signal: searchAbort.signal,
|
|
2925
|
+
extensionContext: ctx
|
|
2926
|
+
});
|
|
2927
|
+
if (aborted || !isCommandActive())
|
|
2928
|
+
break;
|
|
2929
|
+
const entries = toCuratorSearchEntries(response);
|
|
2930
|
+
for (let entryIndex = 0;entryIndex < entries.length; entryIndex++) {
|
|
2931
|
+
const entry = entries[entryIndex];
|
|
2932
|
+
const resultIndex = entryIndex === 0 ? qi : nextResultIndex++;
|
|
2933
|
+
const indexedEntry = {
|
|
2934
|
+
...entry,
|
|
2935
|
+
queryIndex: resultIndex,
|
|
2936
|
+
query: queries[qi]
|
|
2937
|
+
};
|
|
2938
|
+
collected.set(resultIndex, indexedCuratorEntryToQueryResult(indexedEntry));
|
|
2939
|
+
if (entry.error) {
|
|
2940
|
+
handle.pushError(resultIndex, entry.error, entry.provider, { query: queries[qi], slotIndex: qi });
|
|
2941
|
+
} else {
|
|
2942
|
+
handle.pushResult(resultIndex, { ...entry, query: queries[qi], slotIndex: qi });
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
} catch (err) {
|
|
2946
|
+
if (aborted || !isCommandActive())
|
|
2947
|
+
break;
|
|
2948
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2949
|
+
const failedProvider = toCuratorProvider(requestedProvider);
|
|
2950
|
+
handle.pushError(qi, message, failedProvider, { query: queries[qi], slotIndex: qi });
|
|
2951
|
+
collected.set(qi, { query: queries[qi], answer: "", results: [], error: message, provider: failedProvider });
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
if (!aborted && isCommandActive())
|
|
2955
|
+
handle.searchesDone();
|
|
2956
|
+
})();
|
|
2957
|
+
} else {
|
|
2958
|
+
if (isCommandActive())
|
|
2959
|
+
handle.searchesDone();
|
|
2960
|
+
}
|
|
2961
|
+
} catch (err) {
|
|
2962
|
+
closeCurator(commandCallId);
|
|
2963
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2964
|
+
ctx.ui.notify(`Failed to open curator: ${message}`, "error");
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
});
|
|
2968
|
+
if (isCommandEnabled(initConfig, "curator"))
|
|
2969
|
+
pi.registerCommand("curator", {
|
|
2970
|
+
description: "Toggle or configure the search curator workflow",
|
|
2971
|
+
handler: async (args, ctx) => {
|
|
2972
|
+
const arg = args.trim().toLowerCase();
|
|
2973
|
+
let newWorkflow;
|
|
2974
|
+
if (arg.length === 0) {
|
|
2975
|
+
const current = resolveWorkflow(loadConfigForExtensionInit().workflow, true);
|
|
2976
|
+
newWorkflow = current === "none" ? "summary-review" : "none";
|
|
2977
|
+
} else if (arg === "on") {
|
|
2978
|
+
newWorkflow = "summary-review";
|
|
2979
|
+
} else if (arg === "off") {
|
|
2980
|
+
newWorkflow = "none";
|
|
2981
|
+
} else if (arg === "none" || arg === "summary-review" || arg === "auto-summary") {
|
|
2982
|
+
newWorkflow = arg;
|
|
2983
|
+
} else {
|
|
2984
|
+
ctx.ui.notify(`Unknown option: ${arg}. Use on, off, summary-review, or auto-summary.`, "error");
|
|
2985
|
+
return;
|
|
2986
|
+
}
|
|
2987
|
+
try {
|
|
2988
|
+
saveConfig({ workflow: newWorkflow });
|
|
2989
|
+
} catch (err) {
|
|
2990
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2991
|
+
ctx.ui.notify(`Failed to save config: ${message}`, "error");
|
|
2992
|
+
return;
|
|
2993
|
+
}
|
|
2994
|
+
const label = newWorkflow === "none" ? `Curator disabled — ${toolNames.webSearch} will return raw results` : newWorkflow === "auto-summary" ? `Auto-summary enabled — ${toolNames.webSearch} will generate a summary without opening the curator` : `Curator enabled — ${toolNames.webSearch} will open curator and auto-generate a summary draft`;
|
|
2995
|
+
pi.sendMessage({
|
|
2996
|
+
customType: "curator-config",
|
|
2997
|
+
content: [{ type: "text", text: label }],
|
|
2998
|
+
display: true,
|
|
2999
|
+
details: { workflow: newWorkflow }
|
|
3000
|
+
}, { triggerTurn: false, deliverAs: "followUp" });
|
|
3001
|
+
}
|
|
3002
|
+
});
|
|
3003
|
+
if (isCommandEnabled(initConfig, "google-account"))
|
|
3004
|
+
pi.registerCommand("google-account", {
|
|
3005
|
+
description: "Show the active Google account for Gemini Web",
|
|
3006
|
+
handler: async () => {
|
|
3007
|
+
if (!isBrowserCookieAccessAllowed()) {
|
|
3008
|
+
pi.sendMessage({
|
|
3009
|
+
customType: "google-account",
|
|
3010
|
+
content: [{ type: "text", text: `Gemini Web browser cookie access is disabled. Set allowBrowserCookies: true in ${WEB_SEARCH_CONFIG_PATH} to enable it.` }],
|
|
3011
|
+
display: true,
|
|
3012
|
+
details: { available: false, cookieAccessAllowed: false }
|
|
3013
|
+
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
3014
|
+
return;
|
|
3015
|
+
}
|
|
3016
|
+
const cookies = await isGeminiWebAvailable();
|
|
3017
|
+
if (!cookies) {
|
|
3018
|
+
const diagnostic = getGeminiWebAvailabilityDiagnostic();
|
|
3019
|
+
const diagnosticDetails = getGeminiWebAvailabilityDiagnosticDetails();
|
|
3020
|
+
const attempted = formatCookieAttempts(diagnosticDetails?.attempts ?? []);
|
|
3021
|
+
const text = diagnostic ? `Gemini Web is unavailable: ${diagnostic}${attempted ? ` Attempted browser profiles: ${attempted}.` : ""}` : "Gemini Web is unavailable. Sign into gemini.google.com in a supported Chromium-based browser.";
|
|
3022
|
+
pi.sendMessage({
|
|
3023
|
+
customType: "google-account",
|
|
3024
|
+
content: [{ type: "text", text }],
|
|
3025
|
+
display: true,
|
|
3026
|
+
details: { available: false, cookieAccessAllowed: true, diagnostic, cookieDiagnostic: diagnosticDetails }
|
|
3027
|
+
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
3028
|
+
return;
|
|
3029
|
+
}
|
|
3030
|
+
const email = await getActiveGoogleEmail(cookies);
|
|
3031
|
+
const text = email ? `Active Google account: ${email}` : "Gemini Web is available, but the active Google account could not be determined.";
|
|
3032
|
+
pi.sendMessage({
|
|
3033
|
+
customType: "google-account",
|
|
3034
|
+
content: [{ type: "text", text }],
|
|
3035
|
+
display: true,
|
|
3036
|
+
details: { available: true, email: email ?? null }
|
|
3037
|
+
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
3038
|
+
}
|
|
3039
|
+
});
|
|
3040
|
+
function formatCookieAttempts(attempts) {
|
|
3041
|
+
return attempts.map(({ browser, profile, status }) => `${browser}/${profile} (${status})`).join(", ");
|
|
3042
|
+
}
|
|
3043
|
+
if (isCommandEnabled(initConfig, "search"))
|
|
3044
|
+
pi.registerCommand("search", {
|
|
3045
|
+
description: "Browse stored web search results",
|
|
3046
|
+
handler: async (_args, ctx) => {
|
|
3047
|
+
const results = getAllResults();
|
|
3048
|
+
if (results.length === 0) {
|
|
3049
|
+
ctx.ui.notify("No stored search results", "info");
|
|
3050
|
+
return;
|
|
3051
|
+
}
|
|
3052
|
+
const options = results.map((r) => {
|
|
3053
|
+
const age = Math.floor((Date.now() - r.timestamp) / 60000);
|
|
3054
|
+
const ageStr = age < 60 ? `${age}m ago` : `${Math.floor(age / 60)}h ago`;
|
|
3055
|
+
if (r.type === "search" && r.queries) {
|
|
3056
|
+
const query = r.queries[0]?.query || "unknown";
|
|
3057
|
+
return `[${r.id.slice(0, 6)}] "${query}" (${r.queries.length} queries) - ${ageStr}`;
|
|
3058
|
+
}
|
|
3059
|
+
if (r.type === "fetch" && (r.urls || r.urlMetadata)) {
|
|
3060
|
+
return `[${r.id.slice(0, 6)}] ${(r.urls ?? r.urlMetadata ?? []).length} URLs fetched - ${ageStr}`;
|
|
3061
|
+
}
|
|
3062
|
+
return `[${r.id.slice(0, 6)}] ${r.type} - ${ageStr}`;
|
|
3063
|
+
});
|
|
3064
|
+
const choice = await ctx.ui.select("Stored Search Results", options);
|
|
3065
|
+
if (!choice)
|
|
3066
|
+
return;
|
|
3067
|
+
const match = choice.match(/^\[([a-z0-9]+)\]/);
|
|
3068
|
+
if (!match)
|
|
3069
|
+
return;
|
|
3070
|
+
const selected = results.find((r) => r.id.startsWith(match[1]));
|
|
3071
|
+
if (!selected)
|
|
3072
|
+
return;
|
|
3073
|
+
const actions = ["View details", "Delete"];
|
|
3074
|
+
const action = await ctx.ui.select(`Result ${selected.id.slice(0, 6)}`, actions);
|
|
3075
|
+
if (action === "Delete") {
|
|
3076
|
+
deleteResult(selected.id);
|
|
3077
|
+
ctx.ui.notify(`Deleted ${selected.id.slice(0, 6)}`, "info");
|
|
3078
|
+
} else if (action === "View details") {
|
|
3079
|
+
let info = `ID: ${selected.id}
|
|
3080
|
+
Type: ${selected.type}
|
|
3081
|
+
Age: ${Math.floor((Date.now() - selected.timestamp) / 60000)}m
|
|
3082
|
+
|
|
3083
|
+
`;
|
|
3084
|
+
if (selected.type === "search" && selected.queries) {
|
|
3085
|
+
info += `Queries:
|
|
3086
|
+
`;
|
|
3087
|
+
const queries = selected.queries.slice(0, 10);
|
|
3088
|
+
for (const q of queries) {
|
|
3089
|
+
info += `- "${q.query}" (${q.results.length} results)
|
|
3090
|
+
`;
|
|
3091
|
+
}
|
|
3092
|
+
if (selected.queries.length > 10) {
|
|
3093
|
+
info += `... and ${selected.queries.length - 10} more
|
|
3094
|
+
`;
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
if (selected.type === "fetch" && (selected.urls || selected.urlMetadata)) {
|
|
3098
|
+
info += `URLs:
|
|
3099
|
+
`;
|
|
3100
|
+
const urlItems = selected.urls ?? selected.urlMetadata ?? [];
|
|
3101
|
+
const urls = urlItems.slice(0, 10);
|
|
3102
|
+
for (const u of urls) {
|
|
3103
|
+
const urlDisplay = u.url.length > 50 ? u.url.slice(0, 47) + "..." : u.url;
|
|
3104
|
+
const contentLength = "content" in u ? u.content.length : u.contentLength;
|
|
3105
|
+
info += `- ${urlDisplay} (${u.error || `${contentLength} chars`})
|
|
3106
|
+
`;
|
|
3107
|
+
}
|
|
3108
|
+
if (urlItems.length > 10) {
|
|
3109
|
+
info += `... and ${urlItems.length - 10} more
|
|
3110
|
+
`;
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
ctx.ui.notify(info, "info");
|
|
3114
|
+
}
|
|
3115
|
+
}
|
|
3116
|
+
});
|
|
3117
|
+
}
|