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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +248 -0
  3. package/dist/index.js +2802 -0
  4. 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
+ };