pi-web-providers 0.1.0
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/LICENSE +21 -0
- package/README.md +248 -0
- package/dist/index.js +2802 -0
- package/package.json +66 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2802 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_MAX_BYTES,
|
|
4
|
+
DEFAULT_MAX_LINES,
|
|
5
|
+
formatSize,
|
|
6
|
+
keyHint,
|
|
7
|
+
truncateHead
|
|
8
|
+
} from "@mariozechner/pi-coding-agent";
|
|
9
|
+
import { StringEnum } from "@mariozechner/pi-ai";
|
|
10
|
+
import {
|
|
11
|
+
Editor,
|
|
12
|
+
Key,
|
|
13
|
+
Text,
|
|
14
|
+
getEditorKeybindings,
|
|
15
|
+
matchesKey,
|
|
16
|
+
truncateToWidth,
|
|
17
|
+
visibleWidth,
|
|
18
|
+
wrapTextWithAnsi
|
|
19
|
+
} from "@mariozechner/pi-tui";
|
|
20
|
+
import { Type } from "@sinclair/typebox";
|
|
21
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
|
|
22
|
+
import { tmpdir } from "node:os";
|
|
23
|
+
import { join as join2 } from "node:path";
|
|
24
|
+
|
|
25
|
+
// src/config.ts
|
|
26
|
+
import { getAgentDir } from "@mariozechner/pi-coding-agent";
|
|
27
|
+
import { execSync } from "node:child_process";
|
|
28
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
29
|
+
import { dirname, join } from "node:path";
|
|
30
|
+
|
|
31
|
+
// src/provider-tools.ts
|
|
32
|
+
var PROVIDER_TOOLS = {
|
|
33
|
+
codex: ["search"],
|
|
34
|
+
exa: ["search", "contents", "answer", "research"],
|
|
35
|
+
gemini: ["search", "answer", "research"],
|
|
36
|
+
parallel: ["search", "contents"],
|
|
37
|
+
valyu: ["search", "contents", "answer", "research"]
|
|
38
|
+
};
|
|
39
|
+
var PROVIDER_TOOL_META = {
|
|
40
|
+
search: {
|
|
41
|
+
label: "Search",
|
|
42
|
+
help: "Enable the provider's search tool."
|
|
43
|
+
},
|
|
44
|
+
contents: {
|
|
45
|
+
label: "Contents",
|
|
46
|
+
help: "Enable the provider's content extraction tool."
|
|
47
|
+
},
|
|
48
|
+
answer: {
|
|
49
|
+
label: "Answer",
|
|
50
|
+
help: "Enable the provider's answer generation tool."
|
|
51
|
+
},
|
|
52
|
+
research: {
|
|
53
|
+
label: "Research",
|
|
54
|
+
help: "Enable the provider's long-form research tool."
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
function supportsProviderTool(providerId, toolId) {
|
|
58
|
+
return PROVIDER_TOOLS[providerId].includes(toolId);
|
|
59
|
+
}
|
|
60
|
+
function isProviderToolEnabled(providerId, config, toolId) {
|
|
61
|
+
if (!supportsProviderTool(providerId, toolId)) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
const tools = config?.tools;
|
|
65
|
+
return tools?.[toolId] ?? true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/config.ts
|
|
69
|
+
var LEGACY_TOOL_ALIASES = {
|
|
70
|
+
exa: {
|
|
71
|
+
websetsPreview: null
|
|
72
|
+
},
|
|
73
|
+
valyu: {
|
|
74
|
+
deepResearch: "research"
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
var CONFIG_FILE_NAME = "web-providers.json";
|
|
78
|
+
var VERSION = 1;
|
|
79
|
+
function getConfigPath() {
|
|
80
|
+
return join(getAgentDir(), CONFIG_FILE_NAME);
|
|
81
|
+
}
|
|
82
|
+
async function loadConfig() {
|
|
83
|
+
return readConfigFile(getConfigPath());
|
|
84
|
+
}
|
|
85
|
+
async function readConfigFile(path) {
|
|
86
|
+
try {
|
|
87
|
+
const content = await readFile(path, "utf-8");
|
|
88
|
+
return parseConfig(content, path);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (error.code === "ENOENT") {
|
|
91
|
+
return emptyConfig();
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async function writeConfigFile(config) {
|
|
97
|
+
const path = getConfigPath();
|
|
98
|
+
await mkdir(dirname(path), { recursive: true });
|
|
99
|
+
await writeFile(path, serializeConfig(config), "utf-8");
|
|
100
|
+
return path;
|
|
101
|
+
}
|
|
102
|
+
function parseConfig(text, source = CONFIG_FILE_NAME) {
|
|
103
|
+
let raw;
|
|
104
|
+
try {
|
|
105
|
+
raw = JSON.parse(text);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
throw new Error(`Invalid JSON in ${source}: ${error.message}`);
|
|
108
|
+
}
|
|
109
|
+
return normalizeConfig(raw, source);
|
|
110
|
+
}
|
|
111
|
+
function serializeConfig(config) {
|
|
112
|
+
return `${JSON.stringify(config, null, 2)}
|
|
113
|
+
`;
|
|
114
|
+
}
|
|
115
|
+
function resolveConfigValue(reference) {
|
|
116
|
+
if (!reference) return void 0;
|
|
117
|
+
if (reference.startsWith("!")) {
|
|
118
|
+
const output = execSync(reference.slice(1), {
|
|
119
|
+
encoding: "utf-8",
|
|
120
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
121
|
+
}).trim();
|
|
122
|
+
return output.length > 0 ? output : void 0;
|
|
123
|
+
}
|
|
124
|
+
const envValue = process.env[reference];
|
|
125
|
+
if (envValue !== void 0) {
|
|
126
|
+
return envValue;
|
|
127
|
+
}
|
|
128
|
+
if (/^[A-Z][A-Z0-9_]*$/.test(reference)) {
|
|
129
|
+
return void 0;
|
|
130
|
+
}
|
|
131
|
+
return reference;
|
|
132
|
+
}
|
|
133
|
+
function resolveEnvMap(envMap) {
|
|
134
|
+
if (!envMap) return void 0;
|
|
135
|
+
const resolved = Object.fromEntries(
|
|
136
|
+
Object.entries(envMap).map(([key, value]) => [key, resolveConfigValue(value)]).filter(
|
|
137
|
+
(entry) => typeof entry[1] === "string"
|
|
138
|
+
)
|
|
139
|
+
);
|
|
140
|
+
return Object.keys(resolved).length > 0 ? resolved : void 0;
|
|
141
|
+
}
|
|
142
|
+
function emptyConfig() {
|
|
143
|
+
return { version: VERSION };
|
|
144
|
+
}
|
|
145
|
+
function normalizeConfig(raw, source) {
|
|
146
|
+
if (!isPlainObject(raw)) {
|
|
147
|
+
throw new Error(`Config in ${source} must be a JSON object.`);
|
|
148
|
+
}
|
|
149
|
+
const version = raw.version ?? VERSION;
|
|
150
|
+
if (version !== VERSION) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`Unsupported config version '${String(version)}' in ${source}. Expected ${VERSION}.`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
const config = { version: VERSION };
|
|
156
|
+
if (raw.providers !== void 0) {
|
|
157
|
+
if (!isPlainObject(raw.providers)) {
|
|
158
|
+
throw new Error(`'providers' in ${source} must be a JSON object.`);
|
|
159
|
+
}
|
|
160
|
+
config.providers = {};
|
|
161
|
+
if (raw.providers.codex !== void 0) {
|
|
162
|
+
config.providers.codex = normalizeCodexProvider(
|
|
163
|
+
raw.providers.codex,
|
|
164
|
+
source
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
if (raw.providers.exa !== void 0) {
|
|
168
|
+
config.providers.exa = normalizeExaProvider(raw.providers.exa, source);
|
|
169
|
+
}
|
|
170
|
+
if (raw.providers.gemini !== void 0) {
|
|
171
|
+
config.providers.gemini = normalizeGeminiProvider(
|
|
172
|
+
raw.providers.gemini,
|
|
173
|
+
source
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
if (raw.providers.parallel !== void 0) {
|
|
177
|
+
config.providers.parallel = normalizeParallelProvider(
|
|
178
|
+
raw.providers.parallel,
|
|
179
|
+
source
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
if (raw.providers.valyu !== void 0) {
|
|
183
|
+
config.providers.valyu = normalizeValyuProvider(
|
|
184
|
+
raw.providers.valyu,
|
|
185
|
+
source
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
const unknownProviders = Object.keys(raw.providers).filter(
|
|
189
|
+
(key) => key !== "codex" && key !== "exa" && key !== "gemini" && key !== "parallel" && key !== "valyu"
|
|
190
|
+
);
|
|
191
|
+
if (unknownProviders.length > 0) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
`Unknown providers in ${source}: ${unknownProviders.join(", ")}.`
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return config;
|
|
198
|
+
}
|
|
199
|
+
function normalizeCodexProvider(raw, source) {
|
|
200
|
+
const provider = parseProviderObject(raw, source, "codex");
|
|
201
|
+
const defaults = parseOptionalJsonObject(
|
|
202
|
+
provider.defaults,
|
|
203
|
+
source,
|
|
204
|
+
"providers.codex.defaults"
|
|
205
|
+
);
|
|
206
|
+
return {
|
|
207
|
+
enabled: parseOptionalBoolean(
|
|
208
|
+
provider.enabled,
|
|
209
|
+
source,
|
|
210
|
+
"providers.codex.enabled"
|
|
211
|
+
),
|
|
212
|
+
tools: parseOptionalProviderTools(
|
|
213
|
+
"codex",
|
|
214
|
+
provider.tools,
|
|
215
|
+
source,
|
|
216
|
+
"providers.codex.tools"
|
|
217
|
+
),
|
|
218
|
+
codexPath: parseOptionalString(
|
|
219
|
+
provider.codexPath,
|
|
220
|
+
source,
|
|
221
|
+
"providers.codex.codexPath"
|
|
222
|
+
),
|
|
223
|
+
baseUrl: parseOptionalString(
|
|
224
|
+
provider.baseUrl,
|
|
225
|
+
source,
|
|
226
|
+
"providers.codex.baseUrl"
|
|
227
|
+
),
|
|
228
|
+
apiKey: parseOptionalString(
|
|
229
|
+
provider.apiKey,
|
|
230
|
+
source,
|
|
231
|
+
"providers.codex.apiKey"
|
|
232
|
+
),
|
|
233
|
+
env: parseOptionalStringMap(provider.env, source, "providers.codex.env"),
|
|
234
|
+
config: parseOptionalJsonObject(
|
|
235
|
+
provider.config,
|
|
236
|
+
source,
|
|
237
|
+
"providers.codex.config"
|
|
238
|
+
),
|
|
239
|
+
defaults: defaults === void 0 ? void 0 : {
|
|
240
|
+
model: parseOptionalString(
|
|
241
|
+
defaults.model,
|
|
242
|
+
source,
|
|
243
|
+
"providers.codex.defaults.model"
|
|
244
|
+
),
|
|
245
|
+
modelReasoningEffort: parseOptionalLiteral(
|
|
246
|
+
defaults.modelReasoningEffort,
|
|
247
|
+
source,
|
|
248
|
+
"providers.codex.defaults.modelReasoningEffort",
|
|
249
|
+
["minimal", "low", "medium", "high", "xhigh"]
|
|
250
|
+
),
|
|
251
|
+
networkAccessEnabled: parseOptionalBoolean(
|
|
252
|
+
defaults.networkAccessEnabled,
|
|
253
|
+
source,
|
|
254
|
+
"providers.codex.defaults.networkAccessEnabled"
|
|
255
|
+
),
|
|
256
|
+
webSearchMode: parseOptionalLiteral(
|
|
257
|
+
defaults.webSearchMode,
|
|
258
|
+
source,
|
|
259
|
+
"providers.codex.defaults.webSearchMode",
|
|
260
|
+
["disabled", "cached", "live"]
|
|
261
|
+
),
|
|
262
|
+
webSearchEnabled: parseOptionalBoolean(
|
|
263
|
+
defaults.webSearchEnabled,
|
|
264
|
+
source,
|
|
265
|
+
"providers.codex.defaults.webSearchEnabled"
|
|
266
|
+
),
|
|
267
|
+
additionalDirectories: parseOptionalStringArray(
|
|
268
|
+
defaults.additionalDirectories,
|
|
269
|
+
source,
|
|
270
|
+
"providers.codex.defaults.additionalDirectories"
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
function normalizeExaProvider(raw, source) {
|
|
276
|
+
const provider = parseProviderObject(raw, source, "exa");
|
|
277
|
+
return {
|
|
278
|
+
enabled: parseOptionalBoolean(
|
|
279
|
+
provider.enabled,
|
|
280
|
+
source,
|
|
281
|
+
"providers.exa.enabled"
|
|
282
|
+
),
|
|
283
|
+
tools: parseOptionalProviderTools(
|
|
284
|
+
"exa",
|
|
285
|
+
provider.tools,
|
|
286
|
+
source,
|
|
287
|
+
"providers.exa.tools"
|
|
288
|
+
),
|
|
289
|
+
apiKey: parseOptionalString(
|
|
290
|
+
provider.apiKey,
|
|
291
|
+
source,
|
|
292
|
+
"providers.exa.apiKey"
|
|
293
|
+
),
|
|
294
|
+
baseUrl: parseOptionalString(
|
|
295
|
+
provider.baseUrl,
|
|
296
|
+
source,
|
|
297
|
+
"providers.exa.baseUrl"
|
|
298
|
+
),
|
|
299
|
+
defaults: parseOptionalJsonObject(
|
|
300
|
+
provider.defaults,
|
|
301
|
+
source,
|
|
302
|
+
"providers.exa.defaults"
|
|
303
|
+
)
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
function normalizeValyuProvider(raw, source) {
|
|
307
|
+
const provider = parseProviderObject(raw, source, "valyu");
|
|
308
|
+
return {
|
|
309
|
+
enabled: parseOptionalBoolean(
|
|
310
|
+
provider.enabled,
|
|
311
|
+
source,
|
|
312
|
+
"providers.valyu.enabled"
|
|
313
|
+
),
|
|
314
|
+
tools: parseOptionalProviderTools(
|
|
315
|
+
"valyu",
|
|
316
|
+
provider.tools,
|
|
317
|
+
source,
|
|
318
|
+
"providers.valyu.tools"
|
|
319
|
+
),
|
|
320
|
+
apiKey: parseOptionalString(
|
|
321
|
+
provider.apiKey,
|
|
322
|
+
source,
|
|
323
|
+
"providers.valyu.apiKey"
|
|
324
|
+
),
|
|
325
|
+
baseUrl: parseOptionalString(
|
|
326
|
+
provider.baseUrl,
|
|
327
|
+
source,
|
|
328
|
+
"providers.valyu.baseUrl"
|
|
329
|
+
),
|
|
330
|
+
defaults: parseOptionalJsonObject(
|
|
331
|
+
provider.defaults,
|
|
332
|
+
source,
|
|
333
|
+
"providers.valyu.defaults"
|
|
334
|
+
)
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
function normalizeGeminiProvider(raw, source) {
|
|
338
|
+
const provider = parseProviderObject(raw, source, "gemini");
|
|
339
|
+
const defaults = parseOptionalJsonObject(
|
|
340
|
+
provider.defaults,
|
|
341
|
+
source,
|
|
342
|
+
"providers.gemini.defaults"
|
|
343
|
+
);
|
|
344
|
+
return {
|
|
345
|
+
enabled: parseOptionalBoolean(
|
|
346
|
+
provider.enabled,
|
|
347
|
+
source,
|
|
348
|
+
"providers.gemini.enabled"
|
|
349
|
+
),
|
|
350
|
+
tools: parseOptionalProviderTools(
|
|
351
|
+
"gemini",
|
|
352
|
+
provider.tools,
|
|
353
|
+
source,
|
|
354
|
+
"providers.gemini.tools"
|
|
355
|
+
),
|
|
356
|
+
apiKey: parseOptionalString(
|
|
357
|
+
provider.apiKey,
|
|
358
|
+
source,
|
|
359
|
+
"providers.gemini.apiKey"
|
|
360
|
+
),
|
|
361
|
+
defaults: defaults === void 0 ? void 0 : {
|
|
362
|
+
apiVersion: parseOptionalString(
|
|
363
|
+
defaults.apiVersion,
|
|
364
|
+
source,
|
|
365
|
+
"providers.gemini.defaults.apiVersion"
|
|
366
|
+
),
|
|
367
|
+
searchModel: parseOptionalString(
|
|
368
|
+
defaults.searchModel,
|
|
369
|
+
source,
|
|
370
|
+
"providers.gemini.defaults.searchModel"
|
|
371
|
+
),
|
|
372
|
+
answerModel: parseOptionalString(
|
|
373
|
+
defaults.answerModel,
|
|
374
|
+
source,
|
|
375
|
+
"providers.gemini.defaults.answerModel"
|
|
376
|
+
),
|
|
377
|
+
researchAgent: parseOptionalString(
|
|
378
|
+
defaults.researchAgent,
|
|
379
|
+
source,
|
|
380
|
+
"providers.gemini.defaults.researchAgent"
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
function normalizeParallelProvider(raw, source) {
|
|
386
|
+
const provider = parseProviderObject(raw, source, "parallel");
|
|
387
|
+
const defaults = parseOptionalJsonObject(
|
|
388
|
+
provider.defaults,
|
|
389
|
+
source,
|
|
390
|
+
"providers.parallel.defaults"
|
|
391
|
+
);
|
|
392
|
+
return {
|
|
393
|
+
enabled: parseOptionalBoolean(
|
|
394
|
+
provider.enabled,
|
|
395
|
+
source,
|
|
396
|
+
"providers.parallel.enabled"
|
|
397
|
+
),
|
|
398
|
+
tools: parseOptionalProviderTools(
|
|
399
|
+
"parallel",
|
|
400
|
+
provider.tools,
|
|
401
|
+
source,
|
|
402
|
+
"providers.parallel.tools"
|
|
403
|
+
),
|
|
404
|
+
apiKey: parseOptionalString(
|
|
405
|
+
provider.apiKey,
|
|
406
|
+
source,
|
|
407
|
+
"providers.parallel.apiKey"
|
|
408
|
+
),
|
|
409
|
+
baseUrl: parseOptionalString(
|
|
410
|
+
provider.baseUrl,
|
|
411
|
+
source,
|
|
412
|
+
"providers.parallel.baseUrl"
|
|
413
|
+
),
|
|
414
|
+
defaults: defaults === void 0 ? void 0 : {
|
|
415
|
+
search: parseOptionalJsonObject(
|
|
416
|
+
defaults.search,
|
|
417
|
+
source,
|
|
418
|
+
"providers.parallel.defaults.search"
|
|
419
|
+
),
|
|
420
|
+
extract: parseOptionalJsonObject(
|
|
421
|
+
defaults.extract,
|
|
422
|
+
source,
|
|
423
|
+
"providers.parallel.defaults.extract"
|
|
424
|
+
)
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
function parseProviderObject(raw, source, field) {
|
|
429
|
+
if (!isPlainObject(raw)) {
|
|
430
|
+
throw new Error(`'providers.${field}' in ${source} must be a JSON object.`);
|
|
431
|
+
}
|
|
432
|
+
return raw;
|
|
433
|
+
}
|
|
434
|
+
function parseOptionalJsonObject(value, source, field) {
|
|
435
|
+
if (value === void 0) return void 0;
|
|
436
|
+
if (!isPlainObject(value)) {
|
|
437
|
+
throw new Error(`'${field}' in ${source} must be a JSON object.`);
|
|
438
|
+
}
|
|
439
|
+
return value;
|
|
440
|
+
}
|
|
441
|
+
function parseOptionalProviderTools(providerId, value, source, field) {
|
|
442
|
+
if (value === void 0) return void 0;
|
|
443
|
+
if (!isPlainObject(value)) {
|
|
444
|
+
throw new Error(`'${field}' in ${source} must be a JSON object.`);
|
|
445
|
+
}
|
|
446
|
+
const parsed = {};
|
|
447
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
448
|
+
const normalizedKey = normalizeProviderToolKey(providerId, key);
|
|
449
|
+
if (normalizedKey === null) {
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
if (!supportsProviderTool(providerId, normalizedKey)) {
|
|
453
|
+
throw new Error(
|
|
454
|
+
`Unknown tools for ${providerId} in ${source}: ${key}.`
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
parsed[normalizedKey] = parseBoolean(entry, source, `${field}.${key}`);
|
|
458
|
+
}
|
|
459
|
+
const unknownTools = Object.keys(value).filter(
|
|
460
|
+
(toolId) => {
|
|
461
|
+
const normalizedKey = normalizeProviderToolKey(providerId, toolId);
|
|
462
|
+
return normalizedKey !== null && !PROVIDER_TOOLS[providerId].includes(normalizedKey);
|
|
463
|
+
}
|
|
464
|
+
);
|
|
465
|
+
if (unknownTools.length > 0) {
|
|
466
|
+
throw new Error(
|
|
467
|
+
`Unknown tools for ${providerId} in ${source}: ${unknownTools.join(", ")}.`
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
return parsed;
|
|
471
|
+
}
|
|
472
|
+
function normalizeProviderToolKey(providerId, key) {
|
|
473
|
+
const alias = LEGACY_TOOL_ALIASES[providerId]?.[key];
|
|
474
|
+
if (alias !== void 0) {
|
|
475
|
+
return alias;
|
|
476
|
+
}
|
|
477
|
+
return key;
|
|
478
|
+
}
|
|
479
|
+
function parseOptionalStringMap(value, source, field) {
|
|
480
|
+
if (value === void 0) return void 0;
|
|
481
|
+
if (!isPlainObject(value)) {
|
|
482
|
+
throw new Error(
|
|
483
|
+
`'${field}' in ${source} must be a JSON object of strings.`
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
return Object.fromEntries(
|
|
487
|
+
Object.entries(value).map(([key, entry]) => [
|
|
488
|
+
key,
|
|
489
|
+
parseString(entry, source, `${field}.${key}`)
|
|
490
|
+
])
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
function parseOptionalStringArray(value, source, field) {
|
|
494
|
+
if (value === void 0) return void 0;
|
|
495
|
+
if (!Array.isArray(value)) {
|
|
496
|
+
throw new Error(`'${field}' in ${source} must be an array of strings.`);
|
|
497
|
+
}
|
|
498
|
+
return value.map(
|
|
499
|
+
(entry, index) => parseString(entry, source, `${field}[${index}]`)
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
function parseOptionalBoolean(value, source, field) {
|
|
503
|
+
if (value === void 0) return void 0;
|
|
504
|
+
if (typeof value !== "boolean") {
|
|
505
|
+
throw new Error(`'${field}' in ${source} must be a boolean.`);
|
|
506
|
+
}
|
|
507
|
+
return value;
|
|
508
|
+
}
|
|
509
|
+
function parseBoolean(value, source, field) {
|
|
510
|
+
if (typeof value !== "boolean") {
|
|
511
|
+
throw new Error(`'${field}' in ${source} must be a boolean.`);
|
|
512
|
+
}
|
|
513
|
+
return value;
|
|
514
|
+
}
|
|
515
|
+
function parseOptionalString(value, source, field) {
|
|
516
|
+
if (value === void 0) return void 0;
|
|
517
|
+
return parseString(value, source, field);
|
|
518
|
+
}
|
|
519
|
+
function parseString(value, source, field) {
|
|
520
|
+
if (typeof value !== "string") {
|
|
521
|
+
throw new Error(`'${field}' in ${source} must be a string.`);
|
|
522
|
+
}
|
|
523
|
+
return value;
|
|
524
|
+
}
|
|
525
|
+
function parseOptionalLiteral(value, source, field, allowed) {
|
|
526
|
+
if (value === void 0) return void 0;
|
|
527
|
+
if (typeof value !== "string" || !allowed.includes(value)) {
|
|
528
|
+
throw new Error(
|
|
529
|
+
`'${field}' in ${source} must be one of: ${allowed.join(", ")}.`
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
return value;
|
|
533
|
+
}
|
|
534
|
+
function isPlainObject(value) {
|
|
535
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// src/providers/codex.ts
|
|
539
|
+
import { Codex } from "@openai/codex-sdk";
|
|
540
|
+
|
|
541
|
+
// src/providers/shared.ts
|
|
542
|
+
function trimSnippet(input, maxLength = 300) {
|
|
543
|
+
const text = (input ?? "").replace(/\s+/g, " ").trim();
|
|
544
|
+
if (text.length <= maxLength) return text;
|
|
545
|
+
return `${text.slice(0, maxLength - 1)}\u2026`;
|
|
546
|
+
}
|
|
547
|
+
function asJsonObject(value) {
|
|
548
|
+
return value ? { ...value } : {};
|
|
549
|
+
}
|
|
550
|
+
function formatJson(value) {
|
|
551
|
+
return JSON.stringify(value, null, 2);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// src/providers/codex.ts
|
|
555
|
+
var OUTPUT_SCHEMA = {
|
|
556
|
+
type: "object",
|
|
557
|
+
additionalProperties: false,
|
|
558
|
+
properties: {
|
|
559
|
+
sources: {
|
|
560
|
+
type: "array",
|
|
561
|
+
items: {
|
|
562
|
+
type: "object",
|
|
563
|
+
additionalProperties: false,
|
|
564
|
+
properties: {
|
|
565
|
+
title: { type: "string" },
|
|
566
|
+
url: { type: "string" },
|
|
567
|
+
snippet: { type: "string" }
|
|
568
|
+
},
|
|
569
|
+
required: ["title", "url", "snippet"]
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
},
|
|
573
|
+
required: ["sources"]
|
|
574
|
+
};
|
|
575
|
+
var CodexProvider = class {
|
|
576
|
+
id = "codex";
|
|
577
|
+
label = "Codex";
|
|
578
|
+
docsUrl = "https://github.com/openai/codex/tree/main/sdk/typescript";
|
|
579
|
+
createTemplate() {
|
|
580
|
+
return {
|
|
581
|
+
enabled: true,
|
|
582
|
+
tools: {
|
|
583
|
+
search: true
|
|
584
|
+
},
|
|
585
|
+
defaults: {
|
|
586
|
+
networkAccessEnabled: true,
|
|
587
|
+
webSearchEnabled: true,
|
|
588
|
+
webSearchMode: "live"
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
getStatus(config, _cwd) {
|
|
593
|
+
if (!config) {
|
|
594
|
+
return { available: false, summary: "not configured" };
|
|
595
|
+
}
|
|
596
|
+
if (config.enabled === false) {
|
|
597
|
+
return { available: false, summary: "disabled" };
|
|
598
|
+
}
|
|
599
|
+
return { available: true, summary: "enabled" };
|
|
600
|
+
}
|
|
601
|
+
async search(query, maxResults, config, context) {
|
|
602
|
+
const codex = new Codex({
|
|
603
|
+
codexPathOverride: config.codexPath,
|
|
604
|
+
baseUrl: config.baseUrl,
|
|
605
|
+
apiKey: resolveConfigValue(config.apiKey),
|
|
606
|
+
config: config.config,
|
|
607
|
+
env: resolveEnvMap(config.env)
|
|
608
|
+
});
|
|
609
|
+
const thread = codex.startThread({
|
|
610
|
+
additionalDirectories: config.defaults?.additionalDirectories,
|
|
611
|
+
approvalPolicy: "never",
|
|
612
|
+
model: config.defaults?.model,
|
|
613
|
+
modelReasoningEffort: config.defaults?.modelReasoningEffort,
|
|
614
|
+
networkAccessEnabled: config.defaults?.networkAccessEnabled ?? true,
|
|
615
|
+
sandboxMode: "read-only",
|
|
616
|
+
skipGitRepoCheck: true,
|
|
617
|
+
webSearchEnabled: config.defaults?.webSearchEnabled ?? true,
|
|
618
|
+
webSearchMode: config.defaults?.webSearchMode ?? "live",
|
|
619
|
+
workingDirectory: context.cwd
|
|
620
|
+
});
|
|
621
|
+
const prompt = [
|
|
622
|
+
"You are performing web research for another coding agent.",
|
|
623
|
+
"Search the public web and return only a JSON object matching the provided schema.",
|
|
624
|
+
"Do not include markdown fences or extra commentary.",
|
|
625
|
+
`Return at most ${maxResults} sources.`,
|
|
626
|
+
"Prefer primary or official sources when they are available.",
|
|
627
|
+
"Each snippet should be short and specific.",
|
|
628
|
+
"",
|
|
629
|
+
`User query: ${query}`
|
|
630
|
+
].join("\n");
|
|
631
|
+
const streamed = await thread.runStreamed(prompt, {
|
|
632
|
+
outputSchema: OUTPUT_SCHEMA,
|
|
633
|
+
signal: context.signal
|
|
634
|
+
});
|
|
635
|
+
let finalResponse = "";
|
|
636
|
+
const seenQueries = /* @__PURE__ */ new Set();
|
|
637
|
+
for await (const event of streamed.events) {
|
|
638
|
+
handleProgressEvent(event, seenQueries, context.onProgress);
|
|
639
|
+
if (event.type === "item.completed" && event.item.type === "agent_message") {
|
|
640
|
+
finalResponse = event.item.text;
|
|
641
|
+
}
|
|
642
|
+
if (event.type === "turn.failed") {
|
|
643
|
+
throw new Error(event.error.message);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
const parsed = parseOutput(finalResponse);
|
|
647
|
+
return {
|
|
648
|
+
provider: this.id,
|
|
649
|
+
results: parsed.sources.slice(0, maxResults).map((source) => ({
|
|
650
|
+
title: source.title.trim(),
|
|
651
|
+
url: source.url.trim(),
|
|
652
|
+
snippet: trimSnippet(source.snippet)
|
|
653
|
+
}))
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
function handleProgressEvent(event, seenQueries, onProgress) {
|
|
658
|
+
if (!onProgress) return;
|
|
659
|
+
if (event.type === "item.completed" && event.item.type === "web_search" && !seenQueries.has(event.item.query)) {
|
|
660
|
+
seenQueries.add(event.item.query);
|
|
661
|
+
onProgress(`Codex web search ${seenQueries.size}: ${event.item.query}`);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
function parseOutput(raw) {
|
|
665
|
+
if (!raw.trim()) {
|
|
666
|
+
throw new Error("Codex returned an empty response.");
|
|
667
|
+
}
|
|
668
|
+
try {
|
|
669
|
+
return JSON.parse(raw);
|
|
670
|
+
} catch {
|
|
671
|
+
const match = raw.match(/\{[\s\S]*\}/);
|
|
672
|
+
if (!match) {
|
|
673
|
+
throw new Error("Codex returned invalid JSON output.");
|
|
674
|
+
}
|
|
675
|
+
return JSON.parse(match[0]);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// src/providers/exa.ts
|
|
680
|
+
import { Exa } from "exa-js";
|
|
681
|
+
var ExaProvider = class {
|
|
682
|
+
id = "exa";
|
|
683
|
+
label = "Exa";
|
|
684
|
+
docsUrl = "https://exa.ai/docs/sdks/typescript-sdk-specification";
|
|
685
|
+
createTemplate() {
|
|
686
|
+
return {
|
|
687
|
+
enabled: false,
|
|
688
|
+
tools: {
|
|
689
|
+
search: true,
|
|
690
|
+
contents: true,
|
|
691
|
+
answer: true,
|
|
692
|
+
research: true
|
|
693
|
+
},
|
|
694
|
+
apiKey: "EXA_API_KEY",
|
|
695
|
+
defaults: {
|
|
696
|
+
type: "auto",
|
|
697
|
+
contents: {
|
|
698
|
+
text: true
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
getStatus(config) {
|
|
704
|
+
if (!config) {
|
|
705
|
+
return { available: false, summary: "not configured" };
|
|
706
|
+
}
|
|
707
|
+
if (config.enabled === false) {
|
|
708
|
+
return { available: false, summary: "disabled" };
|
|
709
|
+
}
|
|
710
|
+
const apiKey = resolveConfigValue(config.apiKey);
|
|
711
|
+
if (!apiKey) {
|
|
712
|
+
return { available: false, summary: "missing apiKey" };
|
|
713
|
+
}
|
|
714
|
+
return { available: true, summary: "enabled" };
|
|
715
|
+
}
|
|
716
|
+
async search(query, maxResults, config, context) {
|
|
717
|
+
const apiKey = resolveConfigValue(config.apiKey);
|
|
718
|
+
if (!apiKey) {
|
|
719
|
+
throw new Error("Exa is missing an API key.");
|
|
720
|
+
}
|
|
721
|
+
const client = new Exa(apiKey, config.baseUrl);
|
|
722
|
+
const options = {
|
|
723
|
+
...asJsonObject(config.defaults),
|
|
724
|
+
numResults: maxResults
|
|
725
|
+
};
|
|
726
|
+
context.onProgress?.(`Searching Exa for: ${query}`);
|
|
727
|
+
const response = await client.search(query, options);
|
|
728
|
+
return {
|
|
729
|
+
provider: this.id,
|
|
730
|
+
results: (response.results ?? []).slice(0, maxResults).map((result) => ({
|
|
731
|
+
title: String(result.title ?? result.url ?? "Untitled"),
|
|
732
|
+
url: String(result.url ?? ""),
|
|
733
|
+
snippet: trimSnippet(
|
|
734
|
+
typeof result.text === "string" ? result.text : Array.isArray(result.highlights) ? result.highlights.join(" ") : typeof result.summary === "string" ? result.summary : ""
|
|
735
|
+
),
|
|
736
|
+
score: typeof result.score === "number" ? result.score : void 0
|
|
737
|
+
}))
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
async contents(urls, options, config, context) {
|
|
741
|
+
const apiKey = resolveConfigValue(config.apiKey);
|
|
742
|
+
if (!apiKey) {
|
|
743
|
+
throw new Error("Exa is missing an API key.");
|
|
744
|
+
}
|
|
745
|
+
const client = new Exa(apiKey, config.baseUrl);
|
|
746
|
+
context.onProgress?.(`Fetching contents from Exa for ${urls.length} URL(s)`);
|
|
747
|
+
const response = await client.getContents(urls, options);
|
|
748
|
+
const lines = [];
|
|
749
|
+
for (const [index, result] of (response.results ?? []).entries()) {
|
|
750
|
+
lines.push(`${index + 1}. ${String(result.title ?? result.url ?? "Untitled")}`);
|
|
751
|
+
lines.push(` ${String(result.url ?? "")}`);
|
|
752
|
+
const summary = typeof result.summary === "string" ? result.summary : result.summary ? formatJson(result.summary) : void 0;
|
|
753
|
+
const text = typeof result.text === "string" ? result.text : Array.isArray(result.highlights) ? result.highlights.join(" ") : "";
|
|
754
|
+
const body = trimSnippet(summary ?? text);
|
|
755
|
+
if (body) {
|
|
756
|
+
lines.push(` ${body}`);
|
|
757
|
+
}
|
|
758
|
+
lines.push("");
|
|
759
|
+
}
|
|
760
|
+
return {
|
|
761
|
+
provider: this.id,
|
|
762
|
+
text: lines.join("\n").trimEnd() || "No contents found.",
|
|
763
|
+
summary: `${response.results?.length ?? 0} content result(s) via Exa`,
|
|
764
|
+
itemCount: response.results?.length ?? 0
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
async answer(query, options, config, context) {
|
|
768
|
+
const apiKey = resolveConfigValue(config.apiKey);
|
|
769
|
+
if (!apiKey) {
|
|
770
|
+
throw new Error("Exa is missing an API key.");
|
|
771
|
+
}
|
|
772
|
+
const client = new Exa(apiKey, config.baseUrl);
|
|
773
|
+
context.onProgress?.(`Getting Exa answer for: ${query}`);
|
|
774
|
+
const response = await client.answer(query, options);
|
|
775
|
+
const lines = [];
|
|
776
|
+
lines.push(
|
|
777
|
+
typeof response.answer === "string" ? response.answer : formatJson(response.answer)
|
|
778
|
+
);
|
|
779
|
+
const citations = response.citations ?? [];
|
|
780
|
+
if (citations.length > 0) {
|
|
781
|
+
lines.push("");
|
|
782
|
+
lines.push("Sources:");
|
|
783
|
+
for (const [index, citation] of citations.entries()) {
|
|
784
|
+
lines.push(`${index + 1}. ${String(citation.title ?? citation.url ?? "Untitled")}`);
|
|
785
|
+
lines.push(` ${String(citation.url ?? "")}`);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
return {
|
|
789
|
+
provider: this.id,
|
|
790
|
+
text: lines.join("\n").trimEnd(),
|
|
791
|
+
summary: `Answer via Exa with ${citations.length} source(s)`,
|
|
792
|
+
itemCount: citations.length
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
async research(input, options, config, context) {
|
|
796
|
+
const apiKey = resolveConfigValue(config.apiKey);
|
|
797
|
+
if (!apiKey) {
|
|
798
|
+
throw new Error("Exa is missing an API key.");
|
|
799
|
+
}
|
|
800
|
+
const client = new Exa(apiKey, config.baseUrl);
|
|
801
|
+
context.onProgress?.("Creating Exa research task");
|
|
802
|
+
const task = await client.research.create({
|
|
803
|
+
instructions: input,
|
|
804
|
+
...options ?? {}
|
|
805
|
+
});
|
|
806
|
+
const result = await client.research.pollUntilFinished(task.researchId, {
|
|
807
|
+
pollInterval: 3e3
|
|
808
|
+
});
|
|
809
|
+
if (result.status === "failed") {
|
|
810
|
+
throw new Error(result.error ?? "Exa research failed.");
|
|
811
|
+
}
|
|
812
|
+
if (result.status === "canceled") {
|
|
813
|
+
throw new Error("Exa research was canceled.");
|
|
814
|
+
}
|
|
815
|
+
return {
|
|
816
|
+
provider: this.id,
|
|
817
|
+
text: typeof result.output.content === "string" ? result.output.content : formatJson(result.output.content),
|
|
818
|
+
summary: "Research via Exa"
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
// src/providers/gemini.ts
|
|
824
|
+
import { GoogleGenAI } from "@google/genai";
|
|
825
|
+
var DEFAULT_SEARCH_MODEL = "gemini-2.5-flash";
|
|
826
|
+
var DEFAULT_ANSWER_MODEL = "gemini-2.5-flash";
|
|
827
|
+
var DEFAULT_RESEARCH_AGENT = "deep-research-pro-preview-12-2025";
|
|
828
|
+
var DEFAULT_POLL_INTERVAL_MS = 3e3;
|
|
829
|
+
var GeminiProvider = class {
|
|
830
|
+
id = "gemini";
|
|
831
|
+
label = "Gemini";
|
|
832
|
+
docsUrl = "https://github.com/googleapis/js-genai";
|
|
833
|
+
createTemplate() {
|
|
834
|
+
return {
|
|
835
|
+
enabled: false,
|
|
836
|
+
tools: {
|
|
837
|
+
search: true,
|
|
838
|
+
answer: true,
|
|
839
|
+
research: true
|
|
840
|
+
},
|
|
841
|
+
apiKey: "GOOGLE_API_KEY",
|
|
842
|
+
defaults: {
|
|
843
|
+
searchModel: DEFAULT_SEARCH_MODEL,
|
|
844
|
+
answerModel: DEFAULT_ANSWER_MODEL,
|
|
845
|
+
researchAgent: DEFAULT_RESEARCH_AGENT
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
getStatus(config) {
|
|
850
|
+
if (!config) {
|
|
851
|
+
return { available: false, summary: "not configured" };
|
|
852
|
+
}
|
|
853
|
+
if (config.enabled === false) {
|
|
854
|
+
return { available: false, summary: "disabled" };
|
|
855
|
+
}
|
|
856
|
+
const apiKey = resolveConfigValue(config.apiKey);
|
|
857
|
+
if (!apiKey) {
|
|
858
|
+
return { available: false, summary: "missing apiKey" };
|
|
859
|
+
}
|
|
860
|
+
return { available: true, summary: "enabled" };
|
|
861
|
+
}
|
|
862
|
+
async search(query, maxResults, config, context) {
|
|
863
|
+
const ai = this.createClient(config);
|
|
864
|
+
const model = config.defaults?.searchModel ?? DEFAULT_SEARCH_MODEL;
|
|
865
|
+
context.onProgress?.(`Searching Gemini for: ${query}`);
|
|
866
|
+
const interaction = await ai.interactions.create({
|
|
867
|
+
model,
|
|
868
|
+
input: query,
|
|
869
|
+
tools: [{ type: "google_search" }],
|
|
870
|
+
generation_config: {
|
|
871
|
+
tool_choice: "any"
|
|
872
|
+
}
|
|
873
|
+
});
|
|
874
|
+
const results = extractGoogleSearchResults(interaction.outputs).slice(0, maxResults).map((result) => ({
|
|
875
|
+
title: result.title ?? result.url ?? "Untitled",
|
|
876
|
+
url: result.url ?? "",
|
|
877
|
+
snippet: trimSnippet(result.rendered_content ?? "")
|
|
878
|
+
}));
|
|
879
|
+
return {
|
|
880
|
+
provider: this.id,
|
|
881
|
+
results
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
async answer(query, options, config, context) {
|
|
885
|
+
const ai = this.createClient(config);
|
|
886
|
+
const model = config.defaults?.answerModel ?? DEFAULT_ANSWER_MODEL;
|
|
887
|
+
context.onProgress?.(`Getting Gemini answer for: ${query}`);
|
|
888
|
+
const response = await ai.models.generateContent({
|
|
889
|
+
model,
|
|
890
|
+
contents: query,
|
|
891
|
+
config: {
|
|
892
|
+
...options ?? {},
|
|
893
|
+
tools: [{ googleSearch: {} }]
|
|
894
|
+
}
|
|
895
|
+
});
|
|
896
|
+
const lines = [];
|
|
897
|
+
lines.push(response.text?.trim() || "No answer returned.");
|
|
898
|
+
const sources = extractGroundingSources(
|
|
899
|
+
response.candidates?.[0]?.groundingMetadata?.groundingChunks
|
|
900
|
+
);
|
|
901
|
+
if (sources.length > 0) {
|
|
902
|
+
lines.push("");
|
|
903
|
+
lines.push("Sources:");
|
|
904
|
+
for (const [index, source] of sources.entries()) {
|
|
905
|
+
lines.push(`${index + 1}. ${source.title}`);
|
|
906
|
+
lines.push(` ${source.url}`);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
return {
|
|
910
|
+
provider: this.id,
|
|
911
|
+
text: lines.join("\n").trimEnd(),
|
|
912
|
+
summary: `Answer via Gemini with ${sources.length} source(s)`,
|
|
913
|
+
itemCount: sources.length
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
async research(input, options, config, context) {
|
|
917
|
+
const ai = this.createClient(config);
|
|
918
|
+
const agent = config.defaults?.researchAgent ?? DEFAULT_RESEARCH_AGENT;
|
|
919
|
+
const pollIntervalMs = getPollInterval(options);
|
|
920
|
+
const requestOptions = stripPollIntervalOption(options);
|
|
921
|
+
context.onProgress?.("Starting Gemini deep research");
|
|
922
|
+
const initialInteraction = await ai.interactions.create({
|
|
923
|
+
...requestOptions,
|
|
924
|
+
input,
|
|
925
|
+
agent,
|
|
926
|
+
background: true
|
|
927
|
+
});
|
|
928
|
+
context.onProgress?.(`Gemini research started: ${initialInteraction.id}`);
|
|
929
|
+
while (true) {
|
|
930
|
+
if (context.signal?.aborted) {
|
|
931
|
+
throw new Error("Gemini research aborted.");
|
|
932
|
+
}
|
|
933
|
+
const interaction = await ai.interactions.get(initialInteraction.id);
|
|
934
|
+
context.onProgress?.(`Gemini research status: ${interaction.status}`);
|
|
935
|
+
if (interaction.status === "completed") {
|
|
936
|
+
const text = formatInteractionOutputs(interaction.outputs);
|
|
937
|
+
return {
|
|
938
|
+
provider: this.id,
|
|
939
|
+
text: text || "Gemini research completed without textual output.",
|
|
940
|
+
summary: "Research via Gemini"
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
if (interaction.status === "failed" || interaction.status === "cancelled") {
|
|
944
|
+
throw new Error(`Gemini research ${interaction.status}.`);
|
|
945
|
+
}
|
|
946
|
+
await sleep(pollIntervalMs, context.signal);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
createClient(config) {
|
|
950
|
+
const apiKey = resolveConfigValue(config.apiKey);
|
|
951
|
+
if (!apiKey) {
|
|
952
|
+
throw new Error("Gemini is missing an API key.");
|
|
953
|
+
}
|
|
954
|
+
return new GoogleGenAI({
|
|
955
|
+
apiKey,
|
|
956
|
+
apiVersion: config.defaults?.apiVersion
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
};
|
|
960
|
+
function extractGoogleSearchResults(outputs) {
|
|
961
|
+
const results = [];
|
|
962
|
+
if (!Array.isArray(outputs)) {
|
|
963
|
+
return results;
|
|
964
|
+
}
|
|
965
|
+
for (const output of outputs) {
|
|
966
|
+
if (typeof output !== "object" || output === null) {
|
|
967
|
+
continue;
|
|
968
|
+
}
|
|
969
|
+
const content = output;
|
|
970
|
+
if (content.type !== "google_search_result") {
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
const items = Array.isArray(content.result) ? content.result : [];
|
|
974
|
+
for (const item of items) {
|
|
975
|
+
if (typeof item !== "object" || item === null) {
|
|
976
|
+
continue;
|
|
977
|
+
}
|
|
978
|
+
const record = item;
|
|
979
|
+
results.push({
|
|
980
|
+
title: typeof record.title === "string" ? record.title : void 0,
|
|
981
|
+
url: typeof record.url === "string" ? record.url : void 0,
|
|
982
|
+
rendered_content: typeof record.rendered_content === "string" ? record.rendered_content : void 0
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
return results;
|
|
987
|
+
}
|
|
988
|
+
function extractGroundingSources(chunks) {
|
|
989
|
+
const seen = /* @__PURE__ */ new Set();
|
|
990
|
+
const sources = [];
|
|
991
|
+
if (!Array.isArray(chunks)) {
|
|
992
|
+
return sources;
|
|
993
|
+
}
|
|
994
|
+
for (const chunk of chunks) {
|
|
995
|
+
const web = typeof chunk === "object" && chunk !== null && "web" in chunk && typeof chunk.web === "object" && chunk.web !== null ? chunk.web : void 0;
|
|
996
|
+
if (!web) continue;
|
|
997
|
+
const url = typeof web.uri === "string" ? web.uri : void 0;
|
|
998
|
+
if (!url || seen.has(url)) continue;
|
|
999
|
+
seen.add(url);
|
|
1000
|
+
sources.push({
|
|
1001
|
+
title: typeof web.title === "string" ? web.title : url,
|
|
1002
|
+
url
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
return sources;
|
|
1006
|
+
}
|
|
1007
|
+
function formatInteractionOutputs(outputs) {
|
|
1008
|
+
const lines = [];
|
|
1009
|
+
if (!Array.isArray(outputs)) {
|
|
1010
|
+
return "";
|
|
1011
|
+
}
|
|
1012
|
+
for (const output of outputs) {
|
|
1013
|
+
if (typeof output === "object" && output !== null && "type" in output && output.type === "text" && "text" in output && typeof output.text === "string") {
|
|
1014
|
+
const text = output.text.trim();
|
|
1015
|
+
if (text) {
|
|
1016
|
+
lines.push(text);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
return lines.join("\n\n").trim();
|
|
1021
|
+
}
|
|
1022
|
+
async function sleep(ms, signal) {
|
|
1023
|
+
if (signal?.aborted) {
|
|
1024
|
+
throw new Error("Operation aborted.");
|
|
1025
|
+
}
|
|
1026
|
+
await new Promise((resolve, reject) => {
|
|
1027
|
+
const timer = setTimeout(() => {
|
|
1028
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1029
|
+
resolve();
|
|
1030
|
+
}, ms);
|
|
1031
|
+
const onAbort = () => {
|
|
1032
|
+
clearTimeout(timer);
|
|
1033
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1034
|
+
reject(new Error("Operation aborted."));
|
|
1035
|
+
};
|
|
1036
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
function getPollInterval(options) {
|
|
1040
|
+
const raw = options?.pollIntervalMs;
|
|
1041
|
+
if (typeof raw === "number" && Number.isFinite(raw) && raw >= 1e3) {
|
|
1042
|
+
return Math.trunc(raw);
|
|
1043
|
+
}
|
|
1044
|
+
return DEFAULT_POLL_INTERVAL_MS;
|
|
1045
|
+
}
|
|
1046
|
+
function stripPollIntervalOption(options) {
|
|
1047
|
+
if (!options || !Object.hasOwn(options, "pollIntervalMs")) {
|
|
1048
|
+
return options;
|
|
1049
|
+
}
|
|
1050
|
+
const { pollIntervalMs: _ignored, ...rest } = options;
|
|
1051
|
+
return rest;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// src/providers/parallel.ts
|
|
1055
|
+
import Parallel from "parallel-web";
|
|
1056
|
+
var ParallelProvider = class {
|
|
1057
|
+
id = "parallel";
|
|
1058
|
+
label = "Parallel";
|
|
1059
|
+
docsUrl = "https://github.com/parallel-web/parallel-sdk-typescript";
|
|
1060
|
+
createTemplate() {
|
|
1061
|
+
return {
|
|
1062
|
+
enabled: false,
|
|
1063
|
+
tools: {
|
|
1064
|
+
search: true,
|
|
1065
|
+
contents: true
|
|
1066
|
+
},
|
|
1067
|
+
apiKey: "PARALLEL_API_KEY",
|
|
1068
|
+
defaults: {
|
|
1069
|
+
search: {
|
|
1070
|
+
mode: "agentic"
|
|
1071
|
+
},
|
|
1072
|
+
extract: {
|
|
1073
|
+
excerpts: true,
|
|
1074
|
+
full_content: false
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
getStatus(config) {
|
|
1080
|
+
if (!config) {
|
|
1081
|
+
return { available: false, summary: "not configured" };
|
|
1082
|
+
}
|
|
1083
|
+
if (config.enabled === false) {
|
|
1084
|
+
return { available: false, summary: "disabled" };
|
|
1085
|
+
}
|
|
1086
|
+
const apiKey = resolveConfigValue(config.apiKey);
|
|
1087
|
+
if (!apiKey) {
|
|
1088
|
+
return { available: false, summary: "missing apiKey" };
|
|
1089
|
+
}
|
|
1090
|
+
return { available: true, summary: "enabled" };
|
|
1091
|
+
}
|
|
1092
|
+
async search(query, maxResults, config, context) {
|
|
1093
|
+
const client = this.createClient(config);
|
|
1094
|
+
const defaults = asJsonObject(config.defaults?.search);
|
|
1095
|
+
context.onProgress?.(`Searching Parallel for: ${query}`);
|
|
1096
|
+
const response = await client.beta.search({
|
|
1097
|
+
...defaults,
|
|
1098
|
+
objective: query,
|
|
1099
|
+
max_results: maxResults
|
|
1100
|
+
});
|
|
1101
|
+
return {
|
|
1102
|
+
provider: this.id,
|
|
1103
|
+
results: response.results.slice(0, maxResults).map((result) => ({
|
|
1104
|
+
title: result.title ?? result.url,
|
|
1105
|
+
url: result.url,
|
|
1106
|
+
snippet: trimSnippet(result.excerpts?.join(" ") ?? "")
|
|
1107
|
+
}))
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
async contents(urls, options, config, context) {
|
|
1111
|
+
const client = this.createClient(config);
|
|
1112
|
+
const defaults = asJsonObject(config.defaults?.extract);
|
|
1113
|
+
context.onProgress?.(
|
|
1114
|
+
`Fetching contents from Parallel for ${urls.length} URL(s)`
|
|
1115
|
+
);
|
|
1116
|
+
const response = await client.beta.extract({
|
|
1117
|
+
...defaults,
|
|
1118
|
+
...options ?? {},
|
|
1119
|
+
urls
|
|
1120
|
+
});
|
|
1121
|
+
const lines = [];
|
|
1122
|
+
for (const [index, result] of response.results.entries()) {
|
|
1123
|
+
lines.push(`${index + 1}. ${result.title ?? result.url}`);
|
|
1124
|
+
lines.push(` ${result.url}`);
|
|
1125
|
+
const text = result.excerpts?.join(" ") ?? result.full_content ?? "";
|
|
1126
|
+
const snippet = trimSnippet(text);
|
|
1127
|
+
if (snippet) {
|
|
1128
|
+
lines.push(` ${snippet}`);
|
|
1129
|
+
}
|
|
1130
|
+
lines.push("");
|
|
1131
|
+
}
|
|
1132
|
+
for (const error of response.errors) {
|
|
1133
|
+
lines.push(`Error: ${error.url}`);
|
|
1134
|
+
lines.push(` ${error.error_type}`);
|
|
1135
|
+
if (error.content) {
|
|
1136
|
+
lines.push(` ${trimSnippet(error.content)}`);
|
|
1137
|
+
}
|
|
1138
|
+
lines.push("");
|
|
1139
|
+
}
|
|
1140
|
+
const itemCount = response.results.length;
|
|
1141
|
+
return {
|
|
1142
|
+
provider: this.id,
|
|
1143
|
+
text: lines.join("\n").trimEnd() || "No contents found.",
|
|
1144
|
+
summary: `${itemCount} content result(s) via Parallel`,
|
|
1145
|
+
itemCount
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
createClient(config) {
|
|
1149
|
+
const apiKey = resolveConfigValue(config.apiKey);
|
|
1150
|
+
if (!apiKey) {
|
|
1151
|
+
throw new Error("Parallel is missing an API key.");
|
|
1152
|
+
}
|
|
1153
|
+
return new Parallel({
|
|
1154
|
+
apiKey,
|
|
1155
|
+
baseURL: resolveConfigValue(config.baseUrl)
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
};
|
|
1159
|
+
|
|
1160
|
+
// src/providers/valyu.ts
|
|
1161
|
+
import { Valyu } from "valyu-js";
|
|
1162
|
+
var ValyuProvider = class {
|
|
1163
|
+
id = "valyu";
|
|
1164
|
+
label = "Valyu";
|
|
1165
|
+
docsUrl = "https://docs.valyu.ai/sdk/typescript-sdk";
|
|
1166
|
+
createTemplate() {
|
|
1167
|
+
return {
|
|
1168
|
+
enabled: false,
|
|
1169
|
+
tools: {
|
|
1170
|
+
search: true,
|
|
1171
|
+
contents: true,
|
|
1172
|
+
answer: true,
|
|
1173
|
+
research: true
|
|
1174
|
+
},
|
|
1175
|
+
apiKey: "VALYU_API_KEY",
|
|
1176
|
+
defaults: {
|
|
1177
|
+
searchType: "all",
|
|
1178
|
+
responseLength: "short"
|
|
1179
|
+
}
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
getStatus(config) {
|
|
1183
|
+
if (!config) {
|
|
1184
|
+
return { available: false, summary: "not configured" };
|
|
1185
|
+
}
|
|
1186
|
+
if (config.enabled === false) {
|
|
1187
|
+
return { available: false, summary: "disabled" };
|
|
1188
|
+
}
|
|
1189
|
+
const apiKey = resolveConfigValue(config.apiKey);
|
|
1190
|
+
if (!apiKey) {
|
|
1191
|
+
return { available: false, summary: "missing apiKey" };
|
|
1192
|
+
}
|
|
1193
|
+
return { available: true, summary: "enabled" };
|
|
1194
|
+
}
|
|
1195
|
+
async search(query, maxResults, config, context) {
|
|
1196
|
+
const apiKey = resolveConfigValue(config.apiKey);
|
|
1197
|
+
if (!apiKey) {
|
|
1198
|
+
throw new Error("Valyu is missing an API key.");
|
|
1199
|
+
}
|
|
1200
|
+
const client = new Valyu(apiKey, config.baseUrl);
|
|
1201
|
+
const options = {
|
|
1202
|
+
...asJsonObject(config.defaults),
|
|
1203
|
+
maxNumResults: maxResults
|
|
1204
|
+
};
|
|
1205
|
+
context.onProgress?.(`Searching Valyu for: ${query}`);
|
|
1206
|
+
const response = await client.search(query, options);
|
|
1207
|
+
if (!response.success) {
|
|
1208
|
+
throw new Error(response.error || "Valyu search failed.");
|
|
1209
|
+
}
|
|
1210
|
+
return {
|
|
1211
|
+
provider: this.id,
|
|
1212
|
+
results: (response.results ?? []).slice(0, maxResults).map((result) => ({
|
|
1213
|
+
title: result.title,
|
|
1214
|
+
url: result.url,
|
|
1215
|
+
snippet: trimSnippet(
|
|
1216
|
+
result.description ?? (typeof result.content === "string" ? result.content : "")
|
|
1217
|
+
),
|
|
1218
|
+
score: result.relevance_score
|
|
1219
|
+
}))
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
async contents(urls, options, config, context) {
|
|
1223
|
+
const apiKey = resolveConfigValue(config.apiKey);
|
|
1224
|
+
if (!apiKey) {
|
|
1225
|
+
throw new Error("Valyu is missing an API key.");
|
|
1226
|
+
}
|
|
1227
|
+
const client = new Valyu(apiKey, config.baseUrl);
|
|
1228
|
+
context.onProgress?.(`Fetching contents from Valyu for ${urls.length} URL(s)`);
|
|
1229
|
+
const response = await client.contents(urls, options);
|
|
1230
|
+
const finalResponse = "jobId" in response ? await client.waitForJob(response.jobId, {
|
|
1231
|
+
onProgress: (status) => context.onProgress?.(
|
|
1232
|
+
`Valyu contents: ${status.urlsProcessed}/${status.urlsTotal} processed`
|
|
1233
|
+
)
|
|
1234
|
+
}) : response;
|
|
1235
|
+
if (!finalResponse.success) {
|
|
1236
|
+
throw new Error(finalResponse.error || "Valyu contents failed.");
|
|
1237
|
+
}
|
|
1238
|
+
const results = finalResponse.results ?? [];
|
|
1239
|
+
const lines = [];
|
|
1240
|
+
for (const [index, result] of results.entries()) {
|
|
1241
|
+
lines.push(`${index + 1}. ${result.url}`);
|
|
1242
|
+
if (result.status === "failed") {
|
|
1243
|
+
lines.push(` Failed: ${result.error}`);
|
|
1244
|
+
} else {
|
|
1245
|
+
const snippet = typeof result.summary === "string" ? result.summary : result.summary ? formatJson(result.summary) : typeof result.content === "string" || typeof result.content === "number" ? String(result.content) : formatJson(result.content);
|
|
1246
|
+
if (result.title) {
|
|
1247
|
+
lines.push(` ${result.title}`);
|
|
1248
|
+
}
|
|
1249
|
+
lines.push(` ${trimSnippet(snippet)}`);
|
|
1250
|
+
}
|
|
1251
|
+
lines.push("");
|
|
1252
|
+
}
|
|
1253
|
+
return {
|
|
1254
|
+
provider: this.id,
|
|
1255
|
+
text: lines.join("\n").trimEnd() || "No contents found.",
|
|
1256
|
+
summary: `${results.length} content result(s) via Valyu`,
|
|
1257
|
+
itemCount: results.length
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
async answer(query, options, config, context) {
|
|
1261
|
+
const apiKey = resolveConfigValue(config.apiKey);
|
|
1262
|
+
if (!apiKey) {
|
|
1263
|
+
throw new Error("Valyu is missing an API key.");
|
|
1264
|
+
}
|
|
1265
|
+
const client = new Valyu(apiKey, config.baseUrl);
|
|
1266
|
+
context.onProgress?.(`Getting Valyu answer for: ${query}`);
|
|
1267
|
+
const response = await client.answer(query, {
|
|
1268
|
+
...options ?? {},
|
|
1269
|
+
streaming: false
|
|
1270
|
+
});
|
|
1271
|
+
if (!("success" in response) || !response.success) {
|
|
1272
|
+
throw new Error(
|
|
1273
|
+
"error" in response && typeof response.error === "string" ? response.error : "Valyu answer failed."
|
|
1274
|
+
);
|
|
1275
|
+
}
|
|
1276
|
+
const lines = [];
|
|
1277
|
+
const contents = typeof response.contents === "string" ? response.contents : formatJson(response.contents);
|
|
1278
|
+
lines.push(contents);
|
|
1279
|
+
const sources = response.search_results ?? [];
|
|
1280
|
+
if (sources.length > 0) {
|
|
1281
|
+
lines.push("");
|
|
1282
|
+
lines.push("Sources:");
|
|
1283
|
+
for (const [index, result] of sources.entries()) {
|
|
1284
|
+
lines.push(`${index + 1}. ${result.title}`);
|
|
1285
|
+
lines.push(` ${result.url}`);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
return {
|
|
1289
|
+
provider: this.id,
|
|
1290
|
+
text: lines.join("\n").trimEnd(),
|
|
1291
|
+
summary: `Answer via Valyu with ${sources.length} source(s)`,
|
|
1292
|
+
itemCount: sources.length
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
async research(input, options, config, context) {
|
|
1296
|
+
const apiKey = resolveConfigValue(config.apiKey);
|
|
1297
|
+
if (!apiKey) {
|
|
1298
|
+
throw new Error("Valyu is missing an API key.");
|
|
1299
|
+
}
|
|
1300
|
+
const client = new Valyu(apiKey, config.baseUrl);
|
|
1301
|
+
context.onProgress?.("Creating Valyu deep research task");
|
|
1302
|
+
const task = await client.deepresearch.create({
|
|
1303
|
+
input,
|
|
1304
|
+
...options ?? {}
|
|
1305
|
+
});
|
|
1306
|
+
if (!task.success || !task.deepresearch_id) {
|
|
1307
|
+
throw new Error(task.error || "Valyu deep research creation failed.");
|
|
1308
|
+
}
|
|
1309
|
+
const result = await client.deepresearch.wait(task.deepresearch_id, {
|
|
1310
|
+
onProgress: (status) => {
|
|
1311
|
+
const progress = status.progress;
|
|
1312
|
+
if (progress) {
|
|
1313
|
+
context.onProgress?.(
|
|
1314
|
+
`Valyu deep research: ${progress.current_step}/${progress.total_steps}`
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
});
|
|
1319
|
+
if (!result.success) {
|
|
1320
|
+
throw new Error(result.error || "Valyu deep research failed.");
|
|
1321
|
+
}
|
|
1322
|
+
const lines = [];
|
|
1323
|
+
lines.push(
|
|
1324
|
+
typeof result.output === "string" ? result.output : formatJson(result.output)
|
|
1325
|
+
);
|
|
1326
|
+
const sources = result.sources ?? [];
|
|
1327
|
+
if (sources.length > 0) {
|
|
1328
|
+
lines.push("");
|
|
1329
|
+
lines.push("Sources:");
|
|
1330
|
+
for (const [index, source] of sources.entries()) {
|
|
1331
|
+
lines.push(`${index + 1}. ${source.title}`);
|
|
1332
|
+
lines.push(` ${source.url}`);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
return {
|
|
1336
|
+
provider: this.id,
|
|
1337
|
+
text: lines.join("\n").trimEnd(),
|
|
1338
|
+
summary: `Research via Valyu with ${sources.length} source(s)`,
|
|
1339
|
+
itemCount: sources.length
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
};
|
|
1343
|
+
|
|
1344
|
+
// src/providers/index.ts
|
|
1345
|
+
var PROVIDERS = [
|
|
1346
|
+
new CodexProvider(),
|
|
1347
|
+
new ExaProvider(),
|
|
1348
|
+
new GeminiProvider(),
|
|
1349
|
+
new ParallelProvider(),
|
|
1350
|
+
new ValyuProvider()
|
|
1351
|
+
];
|
|
1352
|
+
var PROVIDER_MAP = {
|
|
1353
|
+
codex: PROVIDERS[0],
|
|
1354
|
+
exa: PROVIDERS[1],
|
|
1355
|
+
gemini: PROVIDERS[2],
|
|
1356
|
+
parallel: PROVIDERS[3],
|
|
1357
|
+
valyu: PROVIDERS[4]
|
|
1358
|
+
};
|
|
1359
|
+
|
|
1360
|
+
// src/provider-resolution.ts
|
|
1361
|
+
function resolveProviderChoice(config, explicit, cwd) {
|
|
1362
|
+
return resolveProviderForCapability(config, explicit, cwd, "search");
|
|
1363
|
+
}
|
|
1364
|
+
function resolveProviderForCapability(config, explicit, cwd, capability) {
|
|
1365
|
+
if (explicit) {
|
|
1366
|
+
const provider = PROVIDER_MAP[explicit];
|
|
1367
|
+
if (typeof provider[capability] !== "function") {
|
|
1368
|
+
throw new Error(
|
|
1369
|
+
`Provider '${explicit}' does not support '${capability}'.`
|
|
1370
|
+
);
|
|
1371
|
+
}
|
|
1372
|
+
if (!isProviderToolEnabled(
|
|
1373
|
+
explicit,
|
|
1374
|
+
config.providers?.[explicit],
|
|
1375
|
+
capability
|
|
1376
|
+
)) {
|
|
1377
|
+
throw new Error(
|
|
1378
|
+
`Provider '${explicit}' has '${capability}' disabled in config.`
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
const status = provider.getStatus(
|
|
1382
|
+
config.providers?.[explicit],
|
|
1383
|
+
cwd
|
|
1384
|
+
);
|
|
1385
|
+
if (!status.available) {
|
|
1386
|
+
throw new Error(
|
|
1387
|
+
`Provider '${explicit}' is not available: ${status.summary}.`
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
return provider;
|
|
1391
|
+
}
|
|
1392
|
+
for (const provider of PROVIDERS) {
|
|
1393
|
+
if (typeof provider[capability] !== "function") continue;
|
|
1394
|
+
const providerConfig = config.providers?.[provider.id];
|
|
1395
|
+
if (providerConfig?.enabled !== true) continue;
|
|
1396
|
+
if (!isProviderToolEnabled(
|
|
1397
|
+
provider.id,
|
|
1398
|
+
providerConfig,
|
|
1399
|
+
capability
|
|
1400
|
+
)) {
|
|
1401
|
+
continue;
|
|
1402
|
+
}
|
|
1403
|
+
const status = provider.getStatus(providerConfig, cwd);
|
|
1404
|
+
if (status.available) return provider;
|
|
1405
|
+
}
|
|
1406
|
+
for (const provider of PROVIDERS) {
|
|
1407
|
+
if (typeof provider[capability] !== "function") continue;
|
|
1408
|
+
if (!isProviderToolEnabled(
|
|
1409
|
+
provider.id,
|
|
1410
|
+
config.providers?.[provider.id],
|
|
1411
|
+
capability
|
|
1412
|
+
)) {
|
|
1413
|
+
continue;
|
|
1414
|
+
}
|
|
1415
|
+
const status = provider.getStatus(
|
|
1416
|
+
config.providers?.[provider.id],
|
|
1417
|
+
cwd
|
|
1418
|
+
);
|
|
1419
|
+
if (status.available) return provider;
|
|
1420
|
+
}
|
|
1421
|
+
throw new Error(
|
|
1422
|
+
`No provider is configured for '${capability}'. Run /web-providers to create ~/.pi/agent/web-providers.json.`
|
|
1423
|
+
);
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
// src/types.ts
|
|
1427
|
+
var PROVIDER_IDS = ["codex", "exa", "gemini", "parallel", "valyu"];
|
|
1428
|
+
|
|
1429
|
+
// src/index.ts
|
|
1430
|
+
var DEFAULT_MAX_RESULTS = 5;
|
|
1431
|
+
var MAX_ALLOWED_RESULTS = 20;
|
|
1432
|
+
function webProvidersExtension(pi) {
|
|
1433
|
+
registerWebSearchTool(pi);
|
|
1434
|
+
registerWebContentsTool(pi);
|
|
1435
|
+
registerWebAnswerTool(pi);
|
|
1436
|
+
registerWebResearchTool(pi);
|
|
1437
|
+
pi.registerCommand("web-providers", {
|
|
1438
|
+
description: "Configure web search providers",
|
|
1439
|
+
handler: async (_args, ctx) => {
|
|
1440
|
+
if (!ctx.hasUI) {
|
|
1441
|
+
ctx.ui.notify("web-providers requires interactive mode", "error");
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
await runWebProvidersConfig(ctx);
|
|
1445
|
+
}
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
function registerWebSearchTool(pi) {
|
|
1449
|
+
pi.registerTool({
|
|
1450
|
+
name: "web_search",
|
|
1451
|
+
label: "Web Search",
|
|
1452
|
+
description: `Search the public web and return results with titles, URLs, and snippets. Output is truncated to ${DEFAULT_MAX_LINES} lines or ${formatSize(DEFAULT_MAX_BYTES)} when needed.`,
|
|
1453
|
+
parameters: Type.Object({
|
|
1454
|
+
query: Type.String({ description: "What to search for on the web" }),
|
|
1455
|
+
maxResults: Type.Optional(
|
|
1456
|
+
Type.Integer({
|
|
1457
|
+
minimum: 1,
|
|
1458
|
+
maximum: MAX_ALLOWED_RESULTS,
|
|
1459
|
+
description: `Maximum number of results to return (default: ${DEFAULT_MAX_RESULTS})`
|
|
1460
|
+
})
|
|
1461
|
+
),
|
|
1462
|
+
provider: Type.Optional(
|
|
1463
|
+
StringEnum(PROVIDER_IDS, {
|
|
1464
|
+
description: "Provider override. If omitted, uses the active configured provider or falls back to the first available provider alphabetically."
|
|
1465
|
+
})
|
|
1466
|
+
)
|
|
1467
|
+
}),
|
|
1468
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
1469
|
+
const config = await loadConfig();
|
|
1470
|
+
const provider = resolveProviderChoice(config, params.provider, ctx.cwd);
|
|
1471
|
+
const maxResults = clampResults(params.maxResults);
|
|
1472
|
+
const providerConfig = config.providers?.[provider.id];
|
|
1473
|
+
if (!providerConfig) {
|
|
1474
|
+
throw new Error(`Provider '${provider.id}' is not configured.`);
|
|
1475
|
+
}
|
|
1476
|
+
const response = await provider.search(
|
|
1477
|
+
params.query,
|
|
1478
|
+
maxResults,
|
|
1479
|
+
providerConfig,
|
|
1480
|
+
{
|
|
1481
|
+
cwd: ctx.cwd,
|
|
1482
|
+
signal: signal ?? void 0,
|
|
1483
|
+
onProgress: (message) => onUpdate?.({
|
|
1484
|
+
content: [{ type: "text", text: message }],
|
|
1485
|
+
details: {}
|
|
1486
|
+
})
|
|
1487
|
+
}
|
|
1488
|
+
);
|
|
1489
|
+
const rendered = await truncateAndSave(
|
|
1490
|
+
formatSearchResponse(response),
|
|
1491
|
+
"web-search"
|
|
1492
|
+
);
|
|
1493
|
+
const details = {
|
|
1494
|
+
tool: "web_search",
|
|
1495
|
+
query: params.query,
|
|
1496
|
+
provider: response.provider,
|
|
1497
|
+
resultCount: response.results.length
|
|
1498
|
+
};
|
|
1499
|
+
return { content: [{ type: "text", text: rendered }], details };
|
|
1500
|
+
},
|
|
1501
|
+
renderCall(args, theme) {
|
|
1502
|
+
return renderCallHeader(
|
|
1503
|
+
args,
|
|
1504
|
+
theme
|
|
1505
|
+
);
|
|
1506
|
+
},
|
|
1507
|
+
renderResult(result, { expanded, isPartial }, theme) {
|
|
1508
|
+
const text = extractTextContent(result.content);
|
|
1509
|
+
const isError = Boolean(result.isError);
|
|
1510
|
+
if (isPartial) {
|
|
1511
|
+
return renderSimpleText(text ?? "Searching\u2026", theme, "warning");
|
|
1512
|
+
}
|
|
1513
|
+
if (isError) {
|
|
1514
|
+
return renderBlockText(text ?? "web_search failed", theme, "error");
|
|
1515
|
+
}
|
|
1516
|
+
const details = result.details;
|
|
1517
|
+
if (!details) {
|
|
1518
|
+
return renderBlockText(text ?? "", theme, "toolOutput");
|
|
1519
|
+
}
|
|
1520
|
+
if (expanded) {
|
|
1521
|
+
return renderBlockText(text ?? "", theme, "toolOutput");
|
|
1522
|
+
}
|
|
1523
|
+
return renderCollapsedSearchSummary(details, text, theme);
|
|
1524
|
+
}
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
function registerWebContentsTool(pi) {
|
|
1528
|
+
const providerIds = getProviderIdsForCapability("contents");
|
|
1529
|
+
if (providerIds.length === 0) return;
|
|
1530
|
+
pi.registerTool({
|
|
1531
|
+
name: "web_contents",
|
|
1532
|
+
label: "Web Contents",
|
|
1533
|
+
description: "Fetch extracted contents for one or more URLs using a configured provider.",
|
|
1534
|
+
parameters: Type.Object({
|
|
1535
|
+
urls: Type.Array(Type.String({ minLength: 1 }), {
|
|
1536
|
+
minItems: 1,
|
|
1537
|
+
description: "One or more URLs to extract"
|
|
1538
|
+
}),
|
|
1539
|
+
options: jsonOptionsSchema("Provider-specific extraction options."),
|
|
1540
|
+
provider: providerEnum(
|
|
1541
|
+
providerIds,
|
|
1542
|
+
"Provider override. If omitted, uses the active configured provider that supports web contents."
|
|
1543
|
+
)
|
|
1544
|
+
}),
|
|
1545
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
1546
|
+
return executeProviderTool({
|
|
1547
|
+
capability: "contents",
|
|
1548
|
+
config: await loadConfig(),
|
|
1549
|
+
explicitProvider: params.provider,
|
|
1550
|
+
ctx,
|
|
1551
|
+
signal,
|
|
1552
|
+
onUpdate,
|
|
1553
|
+
invoke: (provider, providerConfig, context) => provider.contents(
|
|
1554
|
+
params.urls,
|
|
1555
|
+
normalizeOptions(params.options),
|
|
1556
|
+
providerConfig,
|
|
1557
|
+
context
|
|
1558
|
+
)
|
|
1559
|
+
});
|
|
1560
|
+
},
|
|
1561
|
+
renderCall(args, theme) {
|
|
1562
|
+
return renderToolCallHeader(
|
|
1563
|
+
"web_contents",
|
|
1564
|
+
`${Array.isArray(args.urls) ? args.urls?.length ?? 0 : 0} url(s)`,
|
|
1565
|
+
[
|
|
1566
|
+
`provider=${String(args.provider ?? "auto")}`
|
|
1567
|
+
],
|
|
1568
|
+
theme
|
|
1569
|
+
);
|
|
1570
|
+
},
|
|
1571
|
+
renderResult(result, state, theme) {
|
|
1572
|
+
return renderProviderToolResult(
|
|
1573
|
+
result,
|
|
1574
|
+
state.expanded,
|
|
1575
|
+
state.isPartial,
|
|
1576
|
+
"web_contents failed",
|
|
1577
|
+
theme
|
|
1578
|
+
);
|
|
1579
|
+
}
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
function registerWebAnswerTool(pi) {
|
|
1583
|
+
const providerIds = getProviderIdsForCapability("answer");
|
|
1584
|
+
if (providerIds.length === 0) return;
|
|
1585
|
+
pi.registerTool({
|
|
1586
|
+
name: "web_answer",
|
|
1587
|
+
label: "Web Answer",
|
|
1588
|
+
description: "Get a provider-generated answer grounded in web results.",
|
|
1589
|
+
parameters: Type.Object({
|
|
1590
|
+
query: Type.String({ description: "Question to answer" }),
|
|
1591
|
+
options: jsonOptionsSchema("Provider-specific answer options."),
|
|
1592
|
+
provider: providerEnum(
|
|
1593
|
+
providerIds,
|
|
1594
|
+
"Provider override. If omitted, uses the active configured provider that supports web answers."
|
|
1595
|
+
)
|
|
1596
|
+
}),
|
|
1597
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
1598
|
+
return executeProviderTool({
|
|
1599
|
+
capability: "answer",
|
|
1600
|
+
config: await loadConfig(),
|
|
1601
|
+
explicitProvider: params.provider,
|
|
1602
|
+
ctx,
|
|
1603
|
+
signal,
|
|
1604
|
+
onUpdate,
|
|
1605
|
+
invoke: (provider, providerConfig, context) => provider.answer(
|
|
1606
|
+
params.query,
|
|
1607
|
+
normalizeOptions(params.options),
|
|
1608
|
+
providerConfig,
|
|
1609
|
+
context
|
|
1610
|
+
)
|
|
1611
|
+
});
|
|
1612
|
+
},
|
|
1613
|
+
renderCall(args, theme) {
|
|
1614
|
+
return renderToolCallHeader(
|
|
1615
|
+
"web_answer",
|
|
1616
|
+
`"${cleanSingleLine(String(args.query ?? "")).slice(0, 80)}"`,
|
|
1617
|
+
[`provider=${String(args.provider ?? "auto")}`],
|
|
1618
|
+
theme
|
|
1619
|
+
);
|
|
1620
|
+
},
|
|
1621
|
+
renderResult(result, state, theme) {
|
|
1622
|
+
return renderProviderToolResult(
|
|
1623
|
+
result,
|
|
1624
|
+
state.expanded,
|
|
1625
|
+
state.isPartial,
|
|
1626
|
+
"web_answer failed",
|
|
1627
|
+
theme
|
|
1628
|
+
);
|
|
1629
|
+
}
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
function registerWebResearchTool(pi) {
|
|
1633
|
+
const providerIds = getProviderIdsForCapability("research");
|
|
1634
|
+
if (providerIds.length === 0) return;
|
|
1635
|
+
pi.registerTool({
|
|
1636
|
+
name: "web_research",
|
|
1637
|
+
label: "Web Research",
|
|
1638
|
+
description: "Run a longer-form research task using a provider that supports research.",
|
|
1639
|
+
parameters: Type.Object({
|
|
1640
|
+
input: Type.String({ description: "Research brief or question" }),
|
|
1641
|
+
options: jsonOptionsSchema("Provider-specific research options."),
|
|
1642
|
+
provider: providerEnum(
|
|
1643
|
+
providerIds,
|
|
1644
|
+
"Provider override. If omitted, uses the active configured provider that supports research."
|
|
1645
|
+
)
|
|
1646
|
+
}),
|
|
1647
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
1648
|
+
return executeProviderTool({
|
|
1649
|
+
capability: "research",
|
|
1650
|
+
config: await loadConfig(),
|
|
1651
|
+
explicitProvider: params.provider,
|
|
1652
|
+
ctx,
|
|
1653
|
+
signal,
|
|
1654
|
+
onUpdate,
|
|
1655
|
+
invoke: (provider, providerConfig, context) => provider.research(
|
|
1656
|
+
params.input,
|
|
1657
|
+
normalizeOptions(params.options),
|
|
1658
|
+
providerConfig,
|
|
1659
|
+
context
|
|
1660
|
+
)
|
|
1661
|
+
});
|
|
1662
|
+
},
|
|
1663
|
+
renderCall(args, theme) {
|
|
1664
|
+
return renderToolCallHeader(
|
|
1665
|
+
"web_research",
|
|
1666
|
+
`"${cleanSingleLine(String(args.input ?? "")).slice(0, 80)}"`,
|
|
1667
|
+
[`provider=${String(args.provider ?? "auto")}`],
|
|
1668
|
+
theme
|
|
1669
|
+
);
|
|
1670
|
+
},
|
|
1671
|
+
renderResult(result, state, theme) {
|
|
1672
|
+
return renderProviderToolResult(
|
|
1673
|
+
result,
|
|
1674
|
+
state.expanded,
|
|
1675
|
+
state.isPartial,
|
|
1676
|
+
"web_research failed",
|
|
1677
|
+
theme
|
|
1678
|
+
);
|
|
1679
|
+
}
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
async function runWebProvidersConfig(ctx) {
|
|
1683
|
+
const config = await loadConfig();
|
|
1684
|
+
const activeProvider = await getPreferredProvider(ctx.cwd);
|
|
1685
|
+
await ctx.ui.custom(
|
|
1686
|
+
(tui, theme, _keybindings, done) => new WebProvidersSettingsView(
|
|
1687
|
+
tui,
|
|
1688
|
+
theme,
|
|
1689
|
+
done,
|
|
1690
|
+
ctx,
|
|
1691
|
+
config,
|
|
1692
|
+
activeProvider
|
|
1693
|
+
)
|
|
1694
|
+
);
|
|
1695
|
+
}
|
|
1696
|
+
function getProviderIdsForCapability(capability) {
|
|
1697
|
+
return PROVIDERS.filter(
|
|
1698
|
+
(provider) => typeof provider[capability] === "function"
|
|
1699
|
+
).map((provider) => provider.id);
|
|
1700
|
+
}
|
|
1701
|
+
function providerEnum(providerIds, description) {
|
|
1702
|
+
if (providerIds.length === 1) {
|
|
1703
|
+
return Type.Optional(Type.Literal(providerIds[0], { description }));
|
|
1704
|
+
}
|
|
1705
|
+
return Type.Optional(
|
|
1706
|
+
Type.Union(providerIds.map((id) => Type.Literal(id)), { description })
|
|
1707
|
+
);
|
|
1708
|
+
}
|
|
1709
|
+
function jsonOptionsSchema(description) {
|
|
1710
|
+
return Type.Optional(
|
|
1711
|
+
Type.Object({}, {
|
|
1712
|
+
additionalProperties: true,
|
|
1713
|
+
description
|
|
1714
|
+
})
|
|
1715
|
+
);
|
|
1716
|
+
}
|
|
1717
|
+
async function executeProviderTool({
|
|
1718
|
+
capability,
|
|
1719
|
+
config,
|
|
1720
|
+
explicitProvider,
|
|
1721
|
+
ctx,
|
|
1722
|
+
signal,
|
|
1723
|
+
onUpdate,
|
|
1724
|
+
invoke
|
|
1725
|
+
}) {
|
|
1726
|
+
const provider = resolveProviderForCapability(
|
|
1727
|
+
config,
|
|
1728
|
+
explicitProvider,
|
|
1729
|
+
ctx.cwd,
|
|
1730
|
+
capability
|
|
1731
|
+
);
|
|
1732
|
+
const providerConfig = config.providers?.[provider.id];
|
|
1733
|
+
if (!providerConfig) {
|
|
1734
|
+
throw new Error(`Provider '${provider.id}' is not configured.`);
|
|
1735
|
+
}
|
|
1736
|
+
const response = await invoke(
|
|
1737
|
+
provider,
|
|
1738
|
+
providerConfig,
|
|
1739
|
+
{
|
|
1740
|
+
cwd: ctx.cwd,
|
|
1741
|
+
signal: signal ?? void 0,
|
|
1742
|
+
onProgress: (message) => onUpdate?.({
|
|
1743
|
+
content: [{ type: "text", text: message }],
|
|
1744
|
+
details: {}
|
|
1745
|
+
})
|
|
1746
|
+
}
|
|
1747
|
+
);
|
|
1748
|
+
const details = {
|
|
1749
|
+
tool: `web_${capability}`,
|
|
1750
|
+
provider: response.provider,
|
|
1751
|
+
summary: response.summary,
|
|
1752
|
+
itemCount: response.itemCount
|
|
1753
|
+
};
|
|
1754
|
+
return {
|
|
1755
|
+
content: [{ type: "text", text: response.text }],
|
|
1756
|
+
details
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
function normalizeOptions(value) {
|
|
1760
|
+
return isJsonObject(value) ? value : void 0;
|
|
1761
|
+
}
|
|
1762
|
+
function renderToolCallHeader(toolName, primary, details, theme) {
|
|
1763
|
+
return {
|
|
1764
|
+
invalidate() {
|
|
1765
|
+
},
|
|
1766
|
+
render(width) {
|
|
1767
|
+
let header = theme.fg("toolTitle", theme.bold(toolName));
|
|
1768
|
+
if (primary.trim().length > 0) {
|
|
1769
|
+
header += ` ${theme.fg("accent", primary)}`;
|
|
1770
|
+
}
|
|
1771
|
+
const lines = [];
|
|
1772
|
+
const headerLine = truncateToWidth(header.trimEnd(), width);
|
|
1773
|
+
lines.push(
|
|
1774
|
+
headerLine + " ".repeat(Math.max(0, width - visibleWidth(headerLine)))
|
|
1775
|
+
);
|
|
1776
|
+
if (details.length > 0) {
|
|
1777
|
+
const detailLine = truncateToWidth(
|
|
1778
|
+
` ${theme.fg("muted", details.join(" "))}`,
|
|
1779
|
+
width
|
|
1780
|
+
);
|
|
1781
|
+
lines.push(
|
|
1782
|
+
detailLine + " ".repeat(Math.max(0, width - visibleWidth(detailLine)))
|
|
1783
|
+
);
|
|
1784
|
+
}
|
|
1785
|
+
return lines;
|
|
1786
|
+
}
|
|
1787
|
+
};
|
|
1788
|
+
}
|
|
1789
|
+
function renderProviderToolResult(result, expanded, isPartial, failureText, theme) {
|
|
1790
|
+
const text = extractTextContent(result.content);
|
|
1791
|
+
if (isPartial) {
|
|
1792
|
+
return renderSimpleText(text ?? "Working\u2026", theme, "warning");
|
|
1793
|
+
}
|
|
1794
|
+
if (result.isError) {
|
|
1795
|
+
return renderBlockText(text ?? failureText, theme, "error");
|
|
1796
|
+
}
|
|
1797
|
+
if (expanded) {
|
|
1798
|
+
return renderBlockText(text ?? "", theme, "toolOutput");
|
|
1799
|
+
}
|
|
1800
|
+
const details = result.details;
|
|
1801
|
+
const summary = details?.summary ?? getFirstLine(text) ?? `${details?.tool ?? "tool"} output available`;
|
|
1802
|
+
let summaryText = theme.fg("success", summary);
|
|
1803
|
+
summaryText += theme.fg("muted", ` (${getExpandHint()})`);
|
|
1804
|
+
return new Text(summaryText, 0, 0);
|
|
1805
|
+
}
|
|
1806
|
+
function buildProviderToolMenuOptions(providerId) {
|
|
1807
|
+
return PROVIDER_TOOLS[providerId].map((toolId) => ({
|
|
1808
|
+
key: toolId,
|
|
1809
|
+
label: PROVIDER_TOOL_META[toolId].label,
|
|
1810
|
+
help: PROVIDER_TOOL_META[toolId].help
|
|
1811
|
+
}));
|
|
1812
|
+
}
|
|
1813
|
+
function buildProviderMenuOptions(providerId) {
|
|
1814
|
+
const options = [];
|
|
1815
|
+
const pushText = (key, label, help) => {
|
|
1816
|
+
options.push({
|
|
1817
|
+
key,
|
|
1818
|
+
label,
|
|
1819
|
+
help,
|
|
1820
|
+
kind: "text"
|
|
1821
|
+
});
|
|
1822
|
+
};
|
|
1823
|
+
const pushValues = (key, label, help, values) => {
|
|
1824
|
+
options.push({
|
|
1825
|
+
key,
|
|
1826
|
+
label,
|
|
1827
|
+
help,
|
|
1828
|
+
kind: "values",
|
|
1829
|
+
values
|
|
1830
|
+
});
|
|
1831
|
+
};
|
|
1832
|
+
if (providerId === "codex") {
|
|
1833
|
+
pushText(
|
|
1834
|
+
"model",
|
|
1835
|
+
"Model",
|
|
1836
|
+
"Optional Codex model override. Leave empty to use the local default."
|
|
1837
|
+
);
|
|
1838
|
+
pushValues(
|
|
1839
|
+
"modelReasoningEffort",
|
|
1840
|
+
"Reasoning effort",
|
|
1841
|
+
"Reasoning depth for Codex. 'default' uses the SDK default.",
|
|
1842
|
+
["default", "minimal", "low", "medium", "high", "xhigh"]
|
|
1843
|
+
);
|
|
1844
|
+
pushValues(
|
|
1845
|
+
"webSearchMode",
|
|
1846
|
+
"Web search mode",
|
|
1847
|
+
"How Codex should source web results. 'default' currently behaves like 'live'.",
|
|
1848
|
+
["default", "disabled", "cached", "live"]
|
|
1849
|
+
);
|
|
1850
|
+
pushValues(
|
|
1851
|
+
"networkAccessEnabled",
|
|
1852
|
+
"Network access",
|
|
1853
|
+
"Allow Codex network access during search runs. 'default' currently behaves like 'true'.",
|
|
1854
|
+
["default", "true", "false"]
|
|
1855
|
+
);
|
|
1856
|
+
pushValues(
|
|
1857
|
+
"webSearchEnabled",
|
|
1858
|
+
"Web search",
|
|
1859
|
+
"Enable Codex web search. 'default' currently behaves like 'true'.",
|
|
1860
|
+
["default", "true", "false"]
|
|
1861
|
+
);
|
|
1862
|
+
pushText(
|
|
1863
|
+
"additionalDirectories",
|
|
1864
|
+
"Additional dirs",
|
|
1865
|
+
"Optional comma-separated directories that Codex may read in addition to the current working directory."
|
|
1866
|
+
);
|
|
1867
|
+
return options;
|
|
1868
|
+
}
|
|
1869
|
+
pushText(
|
|
1870
|
+
"apiKey",
|
|
1871
|
+
"API key",
|
|
1872
|
+
"Provider API key. You can use a literal value, an env var name like EXA_API_KEY, or !command."
|
|
1873
|
+
);
|
|
1874
|
+
if (providerId !== "gemini") {
|
|
1875
|
+
pushText("baseUrl", "Base URL", "Optional API base URL override.");
|
|
1876
|
+
}
|
|
1877
|
+
if (providerId === "exa") {
|
|
1878
|
+
pushValues(
|
|
1879
|
+
"exaSearchType",
|
|
1880
|
+
"Search type",
|
|
1881
|
+
"Exa search mode. 'default' uses the SDK default.",
|
|
1882
|
+
[
|
|
1883
|
+
"default",
|
|
1884
|
+
"keyword",
|
|
1885
|
+
"neural",
|
|
1886
|
+
"auto",
|
|
1887
|
+
"hybrid",
|
|
1888
|
+
"fast",
|
|
1889
|
+
"instant",
|
|
1890
|
+
"deep",
|
|
1891
|
+
"deep-reasoning",
|
|
1892
|
+
"deep-max"
|
|
1893
|
+
]
|
|
1894
|
+
);
|
|
1895
|
+
pushValues(
|
|
1896
|
+
"exaTextContents",
|
|
1897
|
+
"Text contents",
|
|
1898
|
+
"Whether Exa should include text contents in search results. 'default' uses the SDK default.",
|
|
1899
|
+
["default", "true", "false"]
|
|
1900
|
+
);
|
|
1901
|
+
return options;
|
|
1902
|
+
}
|
|
1903
|
+
if (providerId === "gemini") {
|
|
1904
|
+
pushValues(
|
|
1905
|
+
"geminiApiVersion",
|
|
1906
|
+
"API version",
|
|
1907
|
+
"Gemini API version. 'default' uses the SDK default beta endpoints.",
|
|
1908
|
+
["default", "v1alpha", "v1beta", "v1"]
|
|
1909
|
+
);
|
|
1910
|
+
pushText(
|
|
1911
|
+
"geminiSearchModel",
|
|
1912
|
+
"Search model",
|
|
1913
|
+
"Model used for Gemini search interactions."
|
|
1914
|
+
);
|
|
1915
|
+
pushText(
|
|
1916
|
+
"geminiAnswerModel",
|
|
1917
|
+
"Answer model",
|
|
1918
|
+
"Model used for grounded Gemini answers."
|
|
1919
|
+
);
|
|
1920
|
+
pushText(
|
|
1921
|
+
"geminiResearchAgent",
|
|
1922
|
+
"Research agent",
|
|
1923
|
+
"Agent used for Gemini deep research runs."
|
|
1924
|
+
);
|
|
1925
|
+
return options;
|
|
1926
|
+
}
|
|
1927
|
+
if (providerId === "parallel") {
|
|
1928
|
+
pushValues(
|
|
1929
|
+
"parallelSearchMode",
|
|
1930
|
+
"Search mode",
|
|
1931
|
+
"Parallel search mode. 'default' uses the SDK default.",
|
|
1932
|
+
["default", "agentic", "one-shot"]
|
|
1933
|
+
);
|
|
1934
|
+
pushValues(
|
|
1935
|
+
"parallelExtractExcerpts",
|
|
1936
|
+
"Extract excerpts",
|
|
1937
|
+
"Include excerpts in Parallel extraction results. 'default' uses the SDK default.",
|
|
1938
|
+
["default", "on", "off"]
|
|
1939
|
+
);
|
|
1940
|
+
pushValues(
|
|
1941
|
+
"parallelExtractFullContent",
|
|
1942
|
+
"Extract full content",
|
|
1943
|
+
"Include full page content in Parallel extraction results. 'default' uses the SDK default.",
|
|
1944
|
+
["default", "on", "off"]
|
|
1945
|
+
);
|
|
1946
|
+
return options;
|
|
1947
|
+
}
|
|
1948
|
+
pushValues(
|
|
1949
|
+
"valyuSearchType",
|
|
1950
|
+
"Search type",
|
|
1951
|
+
"Valyu search type. 'default' uses the SDK default.",
|
|
1952
|
+
["default", "all", "web", "proprietary", "news"]
|
|
1953
|
+
);
|
|
1954
|
+
pushValues(
|
|
1955
|
+
"valyuResponseLength",
|
|
1956
|
+
"Response length",
|
|
1957
|
+
"Valyu response length. 'default' uses the SDK default.",
|
|
1958
|
+
["default", "short", "medium", "large", "max"]
|
|
1959
|
+
);
|
|
1960
|
+
return options;
|
|
1961
|
+
}
|
|
1962
|
+
var WebProvidersSettingsView = class {
|
|
1963
|
+
constructor(tui, theme, done, ctx, initialConfig, initialProvider) {
|
|
1964
|
+
this.tui = tui;
|
|
1965
|
+
this.theme = theme;
|
|
1966
|
+
this.done = done;
|
|
1967
|
+
this.ctx = ctx;
|
|
1968
|
+
this.config = structuredClone(initialConfig);
|
|
1969
|
+
this.activeProvider = initialProvider;
|
|
1970
|
+
}
|
|
1971
|
+
config;
|
|
1972
|
+
activeProvider;
|
|
1973
|
+
activeSection = "provider";
|
|
1974
|
+
selection = {
|
|
1975
|
+
provider: 0,
|
|
1976
|
+
tools: 0,
|
|
1977
|
+
config: 0
|
|
1978
|
+
};
|
|
1979
|
+
submenu;
|
|
1980
|
+
render(width) {
|
|
1981
|
+
if (this.submenu) {
|
|
1982
|
+
return this.submenu.render(width);
|
|
1983
|
+
}
|
|
1984
|
+
const lines = [];
|
|
1985
|
+
const providerItems = this.buildProviderSectionItems();
|
|
1986
|
+
lines.push(...this.renderSection(width, "Provider", "provider", providerItems));
|
|
1987
|
+
lines.push("");
|
|
1988
|
+
const toolItems = this.buildToolSectionItems();
|
|
1989
|
+
lines.push(...this.renderSection(width, "Tools", "tools", toolItems));
|
|
1990
|
+
lines.push("");
|
|
1991
|
+
const configItems = this.buildConfigSectionItems();
|
|
1992
|
+
lines.push(...this.renderSection(width, "Provider config", "config", configItems));
|
|
1993
|
+
const selected = this.getSelectedEntry();
|
|
1994
|
+
if (selected) {
|
|
1995
|
+
lines.push("");
|
|
1996
|
+
for (const line of wrapTextWithAnsi(selected.description, Math.max(10, width - 2))) {
|
|
1997
|
+
lines.push(truncateToWidth(this.theme.fg("dim", line), width));
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
lines.push("");
|
|
2001
|
+
lines.push(
|
|
2002
|
+
truncateToWidth(
|
|
2003
|
+
this.theme.fg(
|
|
2004
|
+
"dim",
|
|
2005
|
+
"\u2191\u2193 move \xB7 Tab/Shift+Tab switch section \xB7 Enter edit/toggle \xB7 Esc close"
|
|
2006
|
+
),
|
|
2007
|
+
width
|
|
2008
|
+
)
|
|
2009
|
+
);
|
|
2010
|
+
return lines;
|
|
2011
|
+
}
|
|
2012
|
+
invalidate() {
|
|
2013
|
+
this.submenu?.invalidate();
|
|
2014
|
+
}
|
|
2015
|
+
handleInput(data) {
|
|
2016
|
+
if (this.submenu) {
|
|
2017
|
+
this.submenu.handleInput?.(data);
|
|
2018
|
+
this.tui.requestRender();
|
|
2019
|
+
return;
|
|
2020
|
+
}
|
|
2021
|
+
const kb = getEditorKeybindings();
|
|
2022
|
+
const entries = this.getActiveSectionEntries();
|
|
2023
|
+
if (kb.matches(data, "selectUp")) {
|
|
2024
|
+
if (entries.length > 0) {
|
|
2025
|
+
this.moveSelection(-1);
|
|
2026
|
+
}
|
|
2027
|
+
} else if (kb.matches(data, "selectDown")) {
|
|
2028
|
+
if (entries.length > 0) {
|
|
2029
|
+
this.moveSelection(1);
|
|
2030
|
+
}
|
|
2031
|
+
} else if (matchesKey(data, Key.tab)) {
|
|
2032
|
+
this.moveSection(1);
|
|
2033
|
+
} else if (matchesKey(data, Key.shift("tab"))) {
|
|
2034
|
+
this.moveSection(-1);
|
|
2035
|
+
} else if (kb.matches(data, "selectConfirm") || data === " ") {
|
|
2036
|
+
void this.activateCurrentEntry();
|
|
2037
|
+
} else if (kb.matches(data, "selectCancel")) {
|
|
2038
|
+
this.done(void 0);
|
|
2039
|
+
return;
|
|
2040
|
+
}
|
|
2041
|
+
this.tui.requestRender();
|
|
2042
|
+
}
|
|
2043
|
+
buildProviderSectionItems() {
|
|
2044
|
+
return [
|
|
2045
|
+
{
|
|
2046
|
+
id: "provider",
|
|
2047
|
+
label: "Engine",
|
|
2048
|
+
currentValue: PROVIDER_MAP[this.activeProvider].label,
|
|
2049
|
+
description: "Active web provider. Enter cycles through providers.",
|
|
2050
|
+
kind: "cycle",
|
|
2051
|
+
values: PROVIDERS.map((provider) => provider.label)
|
|
2052
|
+
}
|
|
2053
|
+
];
|
|
2054
|
+
}
|
|
2055
|
+
buildToolSectionItems() {
|
|
2056
|
+
const providerConfig = this.currentProviderConfig();
|
|
2057
|
+
return buildProviderToolMenuOptions(this.activeProvider).map((option) => ({
|
|
2058
|
+
id: `tool:${option.key}`,
|
|
2059
|
+
label: option.label,
|
|
2060
|
+
currentValue: isProviderToolEnabled(
|
|
2061
|
+
this.activeProvider,
|
|
2062
|
+
providerConfig,
|
|
2063
|
+
option.key
|
|
2064
|
+
) ? "on" : "off",
|
|
2065
|
+
description: option.help,
|
|
2066
|
+
kind: "cycle",
|
|
2067
|
+
values: ["on", "off"]
|
|
2068
|
+
}));
|
|
2069
|
+
}
|
|
2070
|
+
buildConfigSectionItems() {
|
|
2071
|
+
const providerConfig = this.currentProviderConfig();
|
|
2072
|
+
return buildProviderMenuOptions(this.activeProvider).map(
|
|
2073
|
+
(option) => this.buildProviderItem(option, providerConfig)
|
|
2074
|
+
);
|
|
2075
|
+
}
|
|
2076
|
+
buildProviderItem(option, providerConfig) {
|
|
2077
|
+
if (option.kind === "values") {
|
|
2078
|
+
return {
|
|
2079
|
+
id: option.key,
|
|
2080
|
+
label: option.label,
|
|
2081
|
+
currentValue: getProviderChoiceValue(
|
|
2082
|
+
this.activeProvider,
|
|
2083
|
+
providerConfig,
|
|
2084
|
+
option.key
|
|
2085
|
+
),
|
|
2086
|
+
values: option.values,
|
|
2087
|
+
description: option.help,
|
|
2088
|
+
kind: "cycle"
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2091
|
+
if (option.kind === "text") {
|
|
2092
|
+
const key = option.key;
|
|
2093
|
+
const currentValue = key === "model" || key === "additionalDirectories" ? getCodexTextSettingValue(
|
|
2094
|
+
providerConfig,
|
|
2095
|
+
key
|
|
2096
|
+
) : key === "geminiSearchModel" || key === "geminiAnswerModel" || key === "geminiResearchAgent" ? getGeminiTextSettingValue(
|
|
2097
|
+
providerConfig,
|
|
2098
|
+
key
|
|
2099
|
+
) : getProviderStringValue(
|
|
2100
|
+
providerConfig,
|
|
2101
|
+
key
|
|
2102
|
+
);
|
|
2103
|
+
const secret = key === "apiKey";
|
|
2104
|
+
return {
|
|
2105
|
+
id: key,
|
|
2106
|
+
label: option.label,
|
|
2107
|
+
currentValue: summarizeStringValue(currentValue, secret),
|
|
2108
|
+
description: option.help,
|
|
2109
|
+
kind: "text"
|
|
2110
|
+
};
|
|
2111
|
+
}
|
|
2112
|
+
throw new Error(`Unsupported provider menu option: ${option.key}`);
|
|
2113
|
+
}
|
|
2114
|
+
currentProviderConfig() {
|
|
2115
|
+
return this.config.providers?.[this.activeProvider];
|
|
2116
|
+
}
|
|
2117
|
+
getSectionEntries(section) {
|
|
2118
|
+
if (section === "provider") return this.buildProviderSectionItems();
|
|
2119
|
+
if (section === "tools") return this.buildToolSectionItems();
|
|
2120
|
+
return this.buildConfigSectionItems();
|
|
2121
|
+
}
|
|
2122
|
+
getActiveSectionEntries() {
|
|
2123
|
+
return this.getSectionEntries(this.activeSection);
|
|
2124
|
+
}
|
|
2125
|
+
getSelectedEntry() {
|
|
2126
|
+
const entries = this.getActiveSectionEntries();
|
|
2127
|
+
return entries[this.selection[this.activeSection]];
|
|
2128
|
+
}
|
|
2129
|
+
moveSection(direction) {
|
|
2130
|
+
const sections = [
|
|
2131
|
+
"provider",
|
|
2132
|
+
"tools",
|
|
2133
|
+
"config"
|
|
2134
|
+
];
|
|
2135
|
+
let index = sections.indexOf(this.activeSection);
|
|
2136
|
+
for (let offset = 1; offset <= sections.length; offset++) {
|
|
2137
|
+
const next = sections[(index + offset * direction + sections.length) % sections.length];
|
|
2138
|
+
if (this.getSectionEntries(next).length > 0) {
|
|
2139
|
+
this.activeSection = next;
|
|
2140
|
+
return;
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
moveSelection(direction) {
|
|
2145
|
+
const sections = [
|
|
2146
|
+
"provider",
|
|
2147
|
+
"tools",
|
|
2148
|
+
"config"
|
|
2149
|
+
];
|
|
2150
|
+
const currentEntries = this.getActiveSectionEntries();
|
|
2151
|
+
const currentIndex = this.selection[this.activeSection];
|
|
2152
|
+
if (direction === -1 && currentIndex > 0) {
|
|
2153
|
+
this.selection[this.activeSection] = currentIndex - 1;
|
|
2154
|
+
return;
|
|
2155
|
+
}
|
|
2156
|
+
if (direction === 1 && currentIndex < currentEntries.length - 1) {
|
|
2157
|
+
this.selection[this.activeSection] = currentIndex + 1;
|
|
2158
|
+
return;
|
|
2159
|
+
}
|
|
2160
|
+
const startSectionIndex = sections.indexOf(this.activeSection);
|
|
2161
|
+
for (let offset = 1; offset <= sections.length; offset++) {
|
|
2162
|
+
const nextSection = sections[(startSectionIndex + offset * direction + sections.length) % sections.length];
|
|
2163
|
+
const nextEntries = this.getSectionEntries(nextSection);
|
|
2164
|
+
if (nextEntries.length === 0) continue;
|
|
2165
|
+
this.activeSection = nextSection;
|
|
2166
|
+
this.selection[nextSection] = direction === 1 ? 0 : nextEntries.length - 1;
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
renderSection(width, title, section, entries) {
|
|
2171
|
+
const lines = [
|
|
2172
|
+
truncateToWidth(
|
|
2173
|
+
this.activeSection === section ? this.theme.fg("accent", this.theme.bold(title)) : this.theme.bold(title),
|
|
2174
|
+
width
|
|
2175
|
+
)
|
|
2176
|
+
];
|
|
2177
|
+
const labelWidth = Math.min(
|
|
2178
|
+
20,
|
|
2179
|
+
Math.max(...entries.map((entry) => entry.label.length), 0)
|
|
2180
|
+
);
|
|
2181
|
+
for (const [index, entry] of entries.entries()) {
|
|
2182
|
+
const selected = this.activeSection === section && this.selection[section] === index;
|
|
2183
|
+
const prefix = selected ? this.theme.fg("accent", "\u2192 ") : " ";
|
|
2184
|
+
const paddedLabel = entry.label.padEnd(labelWidth, " ");
|
|
2185
|
+
const label = selected ? this.theme.fg("accent", paddedLabel) : paddedLabel;
|
|
2186
|
+
const value = selected ? this.theme.fg("accent", entry.currentValue) : this.theme.fg("muted", entry.currentValue);
|
|
2187
|
+
lines.push(truncateToWidth(`${prefix}${label} ${value}`, width));
|
|
2188
|
+
}
|
|
2189
|
+
return lines;
|
|
2190
|
+
}
|
|
2191
|
+
async activateCurrentEntry() {
|
|
2192
|
+
const entry = this.getSelectedEntry();
|
|
2193
|
+
if (!entry) return;
|
|
2194
|
+
if (entry.kind === "cycle" && entry.values && entry.values.length > 0) {
|
|
2195
|
+
const currentIndex = entry.values.indexOf(entry.currentValue);
|
|
2196
|
+
const nextValue = entry.values[(currentIndex + 1) % entry.values.length];
|
|
2197
|
+
await this.handleChange(entry.id, nextValue);
|
|
2198
|
+
return;
|
|
2199
|
+
}
|
|
2200
|
+
if (entry.kind === "text") {
|
|
2201
|
+
const currentValue = this.getEntryRawValue(entry.id) ?? "";
|
|
2202
|
+
this.submenu = new TextValueSubmenu(
|
|
2203
|
+
this.tui,
|
|
2204
|
+
this.theme,
|
|
2205
|
+
entry.label,
|
|
2206
|
+
currentValue,
|
|
2207
|
+
entry.description,
|
|
2208
|
+
(selectedValue) => {
|
|
2209
|
+
this.submenu = void 0;
|
|
2210
|
+
if (selectedValue !== void 0) {
|
|
2211
|
+
void this.handleChange(entry.id, selectedValue);
|
|
2212
|
+
}
|
|
2213
|
+
this.tui.requestRender();
|
|
2214
|
+
}
|
|
2215
|
+
);
|
|
2216
|
+
return;
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
getEntryRawValue(id) {
|
|
2220
|
+
const providerConfig = this.currentProviderConfig();
|
|
2221
|
+
if (id === "apiKey" || id === "baseUrl") {
|
|
2222
|
+
return getProviderStringValue(providerConfig, id);
|
|
2223
|
+
}
|
|
2224
|
+
if (id === "model" || id === "additionalDirectories") {
|
|
2225
|
+
return getCodexTextSettingValue(
|
|
2226
|
+
providerConfig,
|
|
2227
|
+
id
|
|
2228
|
+
);
|
|
2229
|
+
}
|
|
2230
|
+
if (id === "geminiSearchModel" || id === "geminiAnswerModel" || id === "geminiResearchAgent") {
|
|
2231
|
+
return getGeminiTextSettingValue(
|
|
2232
|
+
providerConfig,
|
|
2233
|
+
id
|
|
2234
|
+
);
|
|
2235
|
+
}
|
|
2236
|
+
return void 0;
|
|
2237
|
+
}
|
|
2238
|
+
async handleChange(id, value) {
|
|
2239
|
+
if (id === "provider") {
|
|
2240
|
+
const nextProvider = PROVIDERS.find(
|
|
2241
|
+
(provider) => provider.label === value
|
|
2242
|
+
)?.id;
|
|
2243
|
+
if (!nextProvider || nextProvider === this.activeProvider) {
|
|
2244
|
+
return;
|
|
2245
|
+
}
|
|
2246
|
+
this.activeProvider = nextProvider;
|
|
2247
|
+
await this.persist((config) => {
|
|
2248
|
+
setActiveProvider(config, nextProvider);
|
|
2249
|
+
});
|
|
2250
|
+
this.selection.tools = 0;
|
|
2251
|
+
this.selection.config = 0;
|
|
2252
|
+
return;
|
|
2253
|
+
}
|
|
2254
|
+
await this.persist((config) => {
|
|
2255
|
+
config.providers ??= {};
|
|
2256
|
+
const providerConfig = getEditableProviderConfig(
|
|
2257
|
+
this.activeProvider,
|
|
2258
|
+
config.providers?.[this.activeProvider]
|
|
2259
|
+
);
|
|
2260
|
+
if (id.startsWith("tool:")) {
|
|
2261
|
+
const toolId = id.slice("tool:".length);
|
|
2262
|
+
const typedProviderConfig = providerConfig;
|
|
2263
|
+
const tools = typedProviderConfig.tools ?? {};
|
|
2264
|
+
tools[toolId] = value === "on";
|
|
2265
|
+
typedProviderConfig.tools = tools;
|
|
2266
|
+
config.providers[this.activeProvider] = typedProviderConfig;
|
|
2267
|
+
return;
|
|
2268
|
+
}
|
|
2269
|
+
if (id === "apiKey" || id === "baseUrl") {
|
|
2270
|
+
assignOptionalString(providerConfig, id, value);
|
|
2271
|
+
} else if (this.activeProvider === "codex" && applyCodexSettingChange(
|
|
2272
|
+
providerConfig,
|
|
2273
|
+
id,
|
|
2274
|
+
value
|
|
2275
|
+
)) {
|
|
2276
|
+
} else if (this.activeProvider === "exa" && applyExaSettingChange(
|
|
2277
|
+
providerConfig,
|
|
2278
|
+
id,
|
|
2279
|
+
value
|
|
2280
|
+
)) {
|
|
2281
|
+
} else if (this.activeProvider === "gemini" && applyGeminiSettingChange(
|
|
2282
|
+
providerConfig,
|
|
2283
|
+
id,
|
|
2284
|
+
value
|
|
2285
|
+
)) {
|
|
2286
|
+
} else if (this.activeProvider === "parallel" && applyParallelSettingChange(
|
|
2287
|
+
providerConfig,
|
|
2288
|
+
id,
|
|
2289
|
+
value
|
|
2290
|
+
)) {
|
|
2291
|
+
} else if (this.activeProvider === "valyu" && applyValyuSettingChange(
|
|
2292
|
+
providerConfig,
|
|
2293
|
+
id,
|
|
2294
|
+
value
|
|
2295
|
+
)) {
|
|
2296
|
+
} else {
|
|
2297
|
+
throw new Error(`Unknown setting '${id}'.`);
|
|
2298
|
+
}
|
|
2299
|
+
config.providers[this.activeProvider] = providerConfig;
|
|
2300
|
+
});
|
|
2301
|
+
}
|
|
2302
|
+
async persist(mutate) {
|
|
2303
|
+
const nextConfig = structuredClone(this.config);
|
|
2304
|
+
try {
|
|
2305
|
+
mutate(nextConfig);
|
|
2306
|
+
await writeConfigFile(nextConfig);
|
|
2307
|
+
this.config = nextConfig;
|
|
2308
|
+
this.tui.requestRender();
|
|
2309
|
+
} catch (error) {
|
|
2310
|
+
this.ctx.ui.notify(error.message, "error");
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
};
|
|
2314
|
+
var TextValueSubmenu = class {
|
|
2315
|
+
constructor(tui, theme, title, initialValue, help, done) {
|
|
2316
|
+
this.theme = theme;
|
|
2317
|
+
this.title = title;
|
|
2318
|
+
this.help = help;
|
|
2319
|
+
this.done = done;
|
|
2320
|
+
const editorTheme = {
|
|
2321
|
+
borderColor: (text) => this.theme.fg("accent", text),
|
|
2322
|
+
selectList: {
|
|
2323
|
+
selectedPrefix: (text) => this.theme.fg("accent", text),
|
|
2324
|
+
selectedText: (text) => this.theme.fg("accent", text),
|
|
2325
|
+
description: (text) => this.theme.fg("muted", text),
|
|
2326
|
+
scrollInfo: (text) => this.theme.fg("dim", text),
|
|
2327
|
+
noMatch: (text) => this.theme.fg("warning", text)
|
|
2328
|
+
}
|
|
2329
|
+
};
|
|
2330
|
+
this.editor = new Editor(tui, editorTheme);
|
|
2331
|
+
this.editor.setText(initialValue);
|
|
2332
|
+
this.editor.onSubmit = (text) => {
|
|
2333
|
+
this.done(text.trim());
|
|
2334
|
+
};
|
|
2335
|
+
}
|
|
2336
|
+
editor;
|
|
2337
|
+
render(width) {
|
|
2338
|
+
return [
|
|
2339
|
+
truncateToWidth(this.theme.fg("accent", this.title), width),
|
|
2340
|
+
"",
|
|
2341
|
+
...this.editor.render(width),
|
|
2342
|
+
"",
|
|
2343
|
+
truncateToWidth(this.theme.fg("dim", this.help), width),
|
|
2344
|
+
truncateToWidth(
|
|
2345
|
+
this.theme.fg(
|
|
2346
|
+
"dim",
|
|
2347
|
+
"Enter to save \xB7 Shift+Enter for newline \xB7 Esc to cancel"
|
|
2348
|
+
),
|
|
2349
|
+
width
|
|
2350
|
+
)
|
|
2351
|
+
];
|
|
2352
|
+
}
|
|
2353
|
+
invalidate() {
|
|
2354
|
+
this.editor.invalidate();
|
|
2355
|
+
}
|
|
2356
|
+
handleInput(data) {
|
|
2357
|
+
if (matchesKey(data, Key.escape)) {
|
|
2358
|
+
this.done(void 0);
|
|
2359
|
+
return;
|
|
2360
|
+
}
|
|
2361
|
+
this.editor.handleInput(data);
|
|
2362
|
+
}
|
|
2363
|
+
};
|
|
2364
|
+
function getEditableProviderConfig(providerId, current) {
|
|
2365
|
+
return structuredClone(
|
|
2366
|
+
current ?? PROVIDER_MAP[providerId].createTemplate()
|
|
2367
|
+
);
|
|
2368
|
+
}
|
|
2369
|
+
function setActiveProvider(config, providerId) {
|
|
2370
|
+
const currentProviders = config.providers ?? {};
|
|
2371
|
+
const candidateIds = /* @__PURE__ */ new Set([providerId]);
|
|
2372
|
+
for (const id of Object.keys(currentProviders)) {
|
|
2373
|
+
candidateIds.add(id);
|
|
2374
|
+
}
|
|
2375
|
+
config.providers ??= {};
|
|
2376
|
+
for (const id of candidateIds) {
|
|
2377
|
+
const providerConfig = getEditableProviderConfig(
|
|
2378
|
+
id,
|
|
2379
|
+
config.providers?.[id]
|
|
2380
|
+
);
|
|
2381
|
+
providerConfig.enabled = id === providerId;
|
|
2382
|
+
config.providers[id] = providerConfig;
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
function getResolvedProviderChoice(effective, cwd) {
|
|
2386
|
+
try {
|
|
2387
|
+
return resolveProviderChoice(effective, void 0, cwd).id;
|
|
2388
|
+
} catch {
|
|
2389
|
+
return void 0;
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
async function getPreferredProvider(cwd) {
|
|
2393
|
+
const current = await loadConfig();
|
|
2394
|
+
return getResolvedProviderChoice(current, cwd) ?? "codex";
|
|
2395
|
+
}
|
|
2396
|
+
function summarizeStringValue(value, secret) {
|
|
2397
|
+
if (!value) return "unset";
|
|
2398
|
+
if (secret) {
|
|
2399
|
+
if (value.startsWith("!")) return "!command";
|
|
2400
|
+
if (/^[A-Z][A-Z0-9_]*$/.test(value)) return `env:${value}`;
|
|
2401
|
+
return "literal";
|
|
2402
|
+
}
|
|
2403
|
+
return truncateInline(value, 40);
|
|
2404
|
+
}
|
|
2405
|
+
function getProviderStringValue(config, key) {
|
|
2406
|
+
if (!config) return void 0;
|
|
2407
|
+
const value = config[key];
|
|
2408
|
+
return typeof value === "string" ? value : void 0;
|
|
2409
|
+
}
|
|
2410
|
+
function getProviderChoiceValue(providerId, config, key) {
|
|
2411
|
+
if (providerId === "codex") {
|
|
2412
|
+
const defaults = config?.defaults;
|
|
2413
|
+
if (key === "networkAccessEnabled" || key === "webSearchEnabled") {
|
|
2414
|
+
const value = defaults?.[key];
|
|
2415
|
+
return typeof value === "boolean" ? String(value) : "default";
|
|
2416
|
+
}
|
|
2417
|
+
if (key === "modelReasoningEffort" || key === "webSearchMode") {
|
|
2418
|
+
const value = defaults?.[key];
|
|
2419
|
+
return typeof value === "string" ? value : "default";
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
if (providerId === "exa") {
|
|
2423
|
+
const defaults = config?.defaults;
|
|
2424
|
+
if (key === "exaSearchType") {
|
|
2425
|
+
return typeof defaults?.type === "string" ? defaults.type : "default";
|
|
2426
|
+
}
|
|
2427
|
+
if (key === "exaTextContents") {
|
|
2428
|
+
const contents = isJsonObject(defaults?.contents) ? defaults.contents : void 0;
|
|
2429
|
+
return typeof contents?.text === "boolean" ? String(contents.text) : "default";
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
if (providerId === "valyu") {
|
|
2433
|
+
const defaults = config?.defaults;
|
|
2434
|
+
if (key === "valyuSearchType") {
|
|
2435
|
+
return typeof defaults?.searchType === "string" ? defaults.searchType : "default";
|
|
2436
|
+
}
|
|
2437
|
+
if (key === "valyuResponseLength") {
|
|
2438
|
+
return typeof defaults?.responseLength === "string" ? defaults.responseLength : "default";
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
if (providerId === "gemini") {
|
|
2442
|
+
const defaults = config?.defaults;
|
|
2443
|
+
if (key === "geminiApiVersion") {
|
|
2444
|
+
return typeof defaults?.apiVersion === "string" ? defaults.apiVersion : "default";
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
if (providerId === "parallel") {
|
|
2448
|
+
const defaults = config?.defaults;
|
|
2449
|
+
const search = isJsonObject(defaults?.search) ? defaults.search : void 0;
|
|
2450
|
+
const extract = isJsonObject(defaults?.extract) ? defaults.extract : void 0;
|
|
2451
|
+
if (key === "parallelSearchMode") {
|
|
2452
|
+
return typeof search?.mode === "string" ? search.mode : "default";
|
|
2453
|
+
}
|
|
2454
|
+
if (key === "parallelExtractExcerpts") {
|
|
2455
|
+
if (extract?.excerpts === void 0) return "default";
|
|
2456
|
+
return extract.excerpts ? "on" : "off";
|
|
2457
|
+
}
|
|
2458
|
+
if (key === "parallelExtractFullContent") {
|
|
2459
|
+
if (extract?.full_content === void 0) return "default";
|
|
2460
|
+
return extract.full_content ? "on" : "off";
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
throw new Error(`Unsupported choice setting '${key}' for '${providerId}'.`);
|
|
2464
|
+
}
|
|
2465
|
+
function getCodexTextSettingValue(config, key) {
|
|
2466
|
+
const defaults = config?.defaults;
|
|
2467
|
+
if (!defaults) return void 0;
|
|
2468
|
+
if (key === "additionalDirectories") {
|
|
2469
|
+
return defaults.additionalDirectories?.join(", ");
|
|
2470
|
+
}
|
|
2471
|
+
return defaults.model;
|
|
2472
|
+
}
|
|
2473
|
+
function getGeminiTextSettingValue(config, key) {
|
|
2474
|
+
const defaults = config?.defaults;
|
|
2475
|
+
if (!defaults) return void 0;
|
|
2476
|
+
if (key === "geminiSearchModel") return defaults.searchModel;
|
|
2477
|
+
if (key === "geminiAnswerModel") return defaults.answerModel;
|
|
2478
|
+
return defaults.researchAgent;
|
|
2479
|
+
}
|
|
2480
|
+
function assignOptionalString(target, key, value) {
|
|
2481
|
+
const trimmed = value.trim();
|
|
2482
|
+
if (!trimmed) {
|
|
2483
|
+
delete target[key];
|
|
2484
|
+
} else {
|
|
2485
|
+
target[key] = trimmed;
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
function applyCodexSettingChange(target, key, value) {
|
|
2489
|
+
target.defaults ??= {};
|
|
2490
|
+
switch (key) {
|
|
2491
|
+
case "model":
|
|
2492
|
+
assignOptionalString(
|
|
2493
|
+
target.defaults,
|
|
2494
|
+
"model",
|
|
2495
|
+
value
|
|
2496
|
+
);
|
|
2497
|
+
cleanupCodexDefaults(target);
|
|
2498
|
+
return true;
|
|
2499
|
+
case "additionalDirectories": {
|
|
2500
|
+
const trimmed = value.trim();
|
|
2501
|
+
if (!trimmed) {
|
|
2502
|
+
delete target.defaults.additionalDirectories;
|
|
2503
|
+
} else {
|
|
2504
|
+
target.defaults.additionalDirectories = trimmed.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
2505
|
+
}
|
|
2506
|
+
cleanupCodexDefaults(target);
|
|
2507
|
+
return true;
|
|
2508
|
+
}
|
|
2509
|
+
case "modelReasoningEffort":
|
|
2510
|
+
case "webSearchMode":
|
|
2511
|
+
if (value === "default") {
|
|
2512
|
+
delete target.defaults[key];
|
|
2513
|
+
} else {
|
|
2514
|
+
target.defaults[key] = value;
|
|
2515
|
+
}
|
|
2516
|
+
cleanupCodexDefaults(target);
|
|
2517
|
+
return true;
|
|
2518
|
+
case "networkAccessEnabled":
|
|
2519
|
+
case "webSearchEnabled":
|
|
2520
|
+
if (value === "default") {
|
|
2521
|
+
delete target.defaults[key];
|
|
2522
|
+
} else {
|
|
2523
|
+
target.defaults[key] = value === "true";
|
|
2524
|
+
}
|
|
2525
|
+
cleanupCodexDefaults(target);
|
|
2526
|
+
return true;
|
|
2527
|
+
default:
|
|
2528
|
+
return false;
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
function applyExaSettingChange(target, key, value) {
|
|
2532
|
+
target.defaults = isJsonObject(target.defaults) ? { ...target.defaults } : {};
|
|
2533
|
+
switch (key) {
|
|
2534
|
+
case "exaSearchType":
|
|
2535
|
+
if (value === "default") {
|
|
2536
|
+
delete target.defaults.type;
|
|
2537
|
+
} else {
|
|
2538
|
+
target.defaults.type = value;
|
|
2539
|
+
}
|
|
2540
|
+
cleanupGenericDefaults(target);
|
|
2541
|
+
return true;
|
|
2542
|
+
case "exaTextContents": {
|
|
2543
|
+
const contents = isJsonObject(target.defaults.contents) ? { ...target.defaults.contents } : {};
|
|
2544
|
+
if (value === "default") {
|
|
2545
|
+
delete contents.text;
|
|
2546
|
+
} else {
|
|
2547
|
+
contents.text = value === "true";
|
|
2548
|
+
}
|
|
2549
|
+
if (Object.keys(contents).length === 0) {
|
|
2550
|
+
delete target.defaults.contents;
|
|
2551
|
+
} else {
|
|
2552
|
+
target.defaults.contents = contents;
|
|
2553
|
+
}
|
|
2554
|
+
cleanupGenericDefaults(target);
|
|
2555
|
+
return true;
|
|
2556
|
+
}
|
|
2557
|
+
default:
|
|
2558
|
+
return false;
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
function applyValyuSettingChange(target, key, value) {
|
|
2562
|
+
target.defaults = isJsonObject(target.defaults) ? { ...target.defaults } : {};
|
|
2563
|
+
switch (key) {
|
|
2564
|
+
case "valyuSearchType":
|
|
2565
|
+
if (value === "default") {
|
|
2566
|
+
delete target.defaults.searchType;
|
|
2567
|
+
} else {
|
|
2568
|
+
target.defaults.searchType = value;
|
|
2569
|
+
}
|
|
2570
|
+
cleanupGenericDefaults(target);
|
|
2571
|
+
return true;
|
|
2572
|
+
case "valyuResponseLength":
|
|
2573
|
+
if (value === "default") {
|
|
2574
|
+
delete target.defaults.responseLength;
|
|
2575
|
+
} else {
|
|
2576
|
+
target.defaults.responseLength = value;
|
|
2577
|
+
}
|
|
2578
|
+
cleanupGenericDefaults(target);
|
|
2579
|
+
return true;
|
|
2580
|
+
default:
|
|
2581
|
+
return false;
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
function applyGeminiSettingChange(target, key, value) {
|
|
2585
|
+
target.defaults ??= {};
|
|
2586
|
+
switch (key) {
|
|
2587
|
+
case "geminiApiVersion":
|
|
2588
|
+
if (value === "default") {
|
|
2589
|
+
delete target.defaults.apiVersion;
|
|
2590
|
+
} else {
|
|
2591
|
+
target.defaults.apiVersion = value;
|
|
2592
|
+
}
|
|
2593
|
+
cleanupGeminiDefaults(target);
|
|
2594
|
+
return true;
|
|
2595
|
+
case "geminiSearchModel":
|
|
2596
|
+
assignOptionalString(
|
|
2597
|
+
target.defaults,
|
|
2598
|
+
"searchModel",
|
|
2599
|
+
value
|
|
2600
|
+
);
|
|
2601
|
+
cleanupGeminiDefaults(target);
|
|
2602
|
+
return true;
|
|
2603
|
+
case "geminiAnswerModel":
|
|
2604
|
+
assignOptionalString(
|
|
2605
|
+
target.defaults,
|
|
2606
|
+
"answerModel",
|
|
2607
|
+
value
|
|
2608
|
+
);
|
|
2609
|
+
cleanupGeminiDefaults(target);
|
|
2610
|
+
return true;
|
|
2611
|
+
case "geminiResearchAgent":
|
|
2612
|
+
assignOptionalString(
|
|
2613
|
+
target.defaults,
|
|
2614
|
+
"researchAgent",
|
|
2615
|
+
value
|
|
2616
|
+
);
|
|
2617
|
+
cleanupGeminiDefaults(target);
|
|
2618
|
+
return true;
|
|
2619
|
+
default:
|
|
2620
|
+
return false;
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
function applyParallelSettingChange(target, key, value) {
|
|
2624
|
+
target.defaults ??= {};
|
|
2625
|
+
target.defaults.search = isJsonObject(target.defaults.search) ? { ...target.defaults.search } : {};
|
|
2626
|
+
target.defaults.extract = isJsonObject(target.defaults.extract) ? { ...target.defaults.extract } : {};
|
|
2627
|
+
switch (key) {
|
|
2628
|
+
case "parallelSearchMode":
|
|
2629
|
+
if (value === "default") {
|
|
2630
|
+
delete target.defaults.search.mode;
|
|
2631
|
+
} else {
|
|
2632
|
+
target.defaults.search.mode = value;
|
|
2633
|
+
}
|
|
2634
|
+
cleanupParallelDefaults(target);
|
|
2635
|
+
return true;
|
|
2636
|
+
case "parallelExtractExcerpts":
|
|
2637
|
+
if (value === "default") {
|
|
2638
|
+
delete target.defaults.extract.excerpts;
|
|
2639
|
+
} else {
|
|
2640
|
+
target.defaults.extract.excerpts = value === "on";
|
|
2641
|
+
}
|
|
2642
|
+
cleanupParallelDefaults(target);
|
|
2643
|
+
return true;
|
|
2644
|
+
case "parallelExtractFullContent":
|
|
2645
|
+
if (value === "default") {
|
|
2646
|
+
delete target.defaults.extract.full_content;
|
|
2647
|
+
} else {
|
|
2648
|
+
target.defaults.extract.full_content = value === "on";
|
|
2649
|
+
}
|
|
2650
|
+
cleanupParallelDefaults(target);
|
|
2651
|
+
return true;
|
|
2652
|
+
default:
|
|
2653
|
+
return false;
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
function cleanupCodexDefaults(target) {
|
|
2657
|
+
if (target.defaults && Object.keys(target.defaults).length === 0) {
|
|
2658
|
+
delete target.defaults;
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
function cleanupGenericDefaults(target) {
|
|
2662
|
+
if (target.defaults && Object.keys(target.defaults).length === 0) {
|
|
2663
|
+
delete target.defaults;
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
function cleanupGeminiDefaults(target) {
|
|
2667
|
+
if (target.defaults && Object.keys(target.defaults).length === 0) {
|
|
2668
|
+
delete target.defaults;
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
function cleanupParallelDefaults(target) {
|
|
2672
|
+
if (target.defaults?.search && Object.keys(target.defaults.search).length === 0) {
|
|
2673
|
+
delete target.defaults.search;
|
|
2674
|
+
}
|
|
2675
|
+
if (target.defaults?.extract && Object.keys(target.defaults.extract).length === 0) {
|
|
2676
|
+
delete target.defaults.extract;
|
|
2677
|
+
}
|
|
2678
|
+
if (target.defaults && Object.keys(target.defaults).length === 0) {
|
|
2679
|
+
delete target.defaults;
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
function isJsonObject(value) {
|
|
2683
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2684
|
+
}
|
|
2685
|
+
function clampResults(value) {
|
|
2686
|
+
if (value === void 0) return DEFAULT_MAX_RESULTS;
|
|
2687
|
+
return Math.min(Math.max(Math.trunc(value), 1), MAX_ALLOWED_RESULTS);
|
|
2688
|
+
}
|
|
2689
|
+
function extractTextContent(content) {
|
|
2690
|
+
if (!content || content.length === 0) {
|
|
2691
|
+
return void 0;
|
|
2692
|
+
}
|
|
2693
|
+
const text = content.filter((item) => item.type === "text" && typeof item.text === "string").map((item) => item.text?.trimEnd() ?? "").join("\n").trim();
|
|
2694
|
+
return text.length > 0 ? text : void 0;
|
|
2695
|
+
}
|
|
2696
|
+
function renderCallHeader(params, theme) {
|
|
2697
|
+
return {
|
|
2698
|
+
invalidate() {
|
|
2699
|
+
},
|
|
2700
|
+
render(width) {
|
|
2701
|
+
let header = theme.fg("toolTitle", theme.bold("web_search"));
|
|
2702
|
+
const query = cleanSingleLine(String(params.query ?? "")).trim();
|
|
2703
|
+
if (query.length > 0) {
|
|
2704
|
+
header += ` ${theme.fg("accent", `"${query.slice(0, 80)}"`)} `;
|
|
2705
|
+
}
|
|
2706
|
+
const lines = [];
|
|
2707
|
+
const headerLine = truncateToWidth(header.trimEnd(), width);
|
|
2708
|
+
lines.push(
|
|
2709
|
+
headerLine + " ".repeat(Math.max(0, width - visibleWidth(headerLine)))
|
|
2710
|
+
);
|
|
2711
|
+
const detailParts = [
|
|
2712
|
+
`provider=${params.provider ?? "auto"}`,
|
|
2713
|
+
`maxResults=${params.maxResults ?? DEFAULT_MAX_RESULTS}`
|
|
2714
|
+
];
|
|
2715
|
+
const details = truncateToWidth(
|
|
2716
|
+
` ${theme.fg("muted", detailParts.join(" "))}`,
|
|
2717
|
+
width
|
|
2718
|
+
);
|
|
2719
|
+
lines.push(
|
|
2720
|
+
details + " ".repeat(Math.max(0, width - visibleWidth(details)))
|
|
2721
|
+
);
|
|
2722
|
+
return lines;
|
|
2723
|
+
}
|
|
2724
|
+
};
|
|
2725
|
+
}
|
|
2726
|
+
function renderBlockText(text, theme, color) {
|
|
2727
|
+
if (!text) {
|
|
2728
|
+
return new Text("", 0, 0);
|
|
2729
|
+
}
|
|
2730
|
+
const rendered = text.split("\n").map((line) => theme.fg(color, line)).join("\n");
|
|
2731
|
+
return new Text(`
|
|
2732
|
+
${rendered}`, 0, 0);
|
|
2733
|
+
}
|
|
2734
|
+
function renderSimpleText(text, theme, color) {
|
|
2735
|
+
return new Text(theme.fg(color, text), 0, 0);
|
|
2736
|
+
}
|
|
2737
|
+
function renderCollapsedSearchSummary(details, text, theme) {
|
|
2738
|
+
const count = `${details.resultCount} result${details.resultCount === 1 ? "" : "s"}`;
|
|
2739
|
+
const base = getFirstLine(text) ?? `${count} via ${details.provider}`;
|
|
2740
|
+
let summary = theme.fg("success", base);
|
|
2741
|
+
summary += theme.fg("muted", ` (${getExpandHint()})`);
|
|
2742
|
+
return new Text(summary, 0, 0);
|
|
2743
|
+
}
|
|
2744
|
+
function getFirstLine(text) {
|
|
2745
|
+
if (!text) {
|
|
2746
|
+
return void 0;
|
|
2747
|
+
}
|
|
2748
|
+
const firstLine = text.split("\n", 1)[0]?.trim();
|
|
2749
|
+
return firstLine && firstLine.length > 0 ? firstLine : void 0;
|
|
2750
|
+
}
|
|
2751
|
+
function getExpandHint() {
|
|
2752
|
+
try {
|
|
2753
|
+
return keyHint("expandTools", "to expand");
|
|
2754
|
+
} catch {
|
|
2755
|
+
return "to expand";
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
function cleanSingleLine(text) {
|
|
2759
|
+
return text.replace(/\s+/g, " ").trim();
|
|
2760
|
+
}
|
|
2761
|
+
function formatSearchResponse(response) {
|
|
2762
|
+
if (response.results.length === 0) {
|
|
2763
|
+
return "No results found.";
|
|
2764
|
+
}
|
|
2765
|
+
const lines = [];
|
|
2766
|
+
for (const [index, result] of response.results.entries()) {
|
|
2767
|
+
lines.push(`${index + 1}. ${result.title}`);
|
|
2768
|
+
lines.push(` ${result.url}`);
|
|
2769
|
+
if (result.snippet) {
|
|
2770
|
+
lines.push(` ${result.snippet}`);
|
|
2771
|
+
}
|
|
2772
|
+
lines.push("");
|
|
2773
|
+
}
|
|
2774
|
+
return lines.join("\n").trimEnd();
|
|
2775
|
+
}
|
|
2776
|
+
async function truncateAndSave(text, prefix) {
|
|
2777
|
+
const truncation = truncateHead(text, {
|
|
2778
|
+
maxLines: DEFAULT_MAX_LINES,
|
|
2779
|
+
maxBytes: DEFAULT_MAX_BYTES
|
|
2780
|
+
});
|
|
2781
|
+
if (!truncation.truncated) return truncation.content;
|
|
2782
|
+
const dir = join2(tmpdir(), `pi-web-providers-${prefix}-${Date.now()}`);
|
|
2783
|
+
await mkdir2(dir, { recursive: true });
|
|
2784
|
+
const fullPath = join2(dir, "output.txt");
|
|
2785
|
+
await writeFile2(fullPath, text, "utf-8");
|
|
2786
|
+
return truncation.content + `
|
|
2787
|
+
|
|
2788
|
+
[Output truncated: ${truncation.outputLines} of ${truncation.totalLines} lines (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}). Full output saved to: ${fullPath}]`;
|
|
2789
|
+
}
|
|
2790
|
+
function truncateInline(text, maxLength) {
|
|
2791
|
+
if (text.length <= maxLength) return text;
|
|
2792
|
+
return `${text.slice(0, maxLength - 1)}\u2026`;
|
|
2793
|
+
}
|
|
2794
|
+
var __test__ = {
|
|
2795
|
+
extractTextContent,
|
|
2796
|
+
renderCallHeader,
|
|
2797
|
+
renderCollapsedSearchSummary
|
|
2798
|
+
};
|
|
2799
|
+
export {
|
|
2800
|
+
__test__,
|
|
2801
|
+
webProvidersExtension as default
|
|
2802
|
+
};
|