pi-web-providers 0.1.0 → 0.3.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 (3) hide show
  1. package/README.md +118 -41
  2. package/dist/index.js +1669 -324
  3. package/package.json +9 -4
package/dist/index.js CHANGED
@@ -1,4 +1,7 @@
1
1
  // src/index.ts
2
+ import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join as join4 } from "node:path";
2
5
  import {
3
6
  DEFAULT_MAX_BYTES,
4
7
  DEFAULT_MAX_LINES,
@@ -6,33 +9,31 @@ import {
6
9
  keyHint,
7
10
  truncateHead
8
11
  } from "@mariozechner/pi-coding-agent";
9
- import { StringEnum } from "@mariozechner/pi-ai";
10
12
  import {
11
13
  Editor,
12
- Key,
13
- Text,
14
14
  getEditorKeybindings,
15
+ Key,
15
16
  matchesKey,
17
+ Text,
16
18
  truncateToWidth,
17
19
  visibleWidth,
18
20
  wrapTextWithAnsi
19
21
  } from "@mariozechner/pi-tui";
20
22
  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
23
 
25
24
  // src/config.ts
26
- import { getAgentDir } from "@mariozechner/pi-coding-agent";
27
25
  import { execSync } from "node:child_process";
28
26
  import { mkdir, readFile, writeFile } from "node:fs/promises";
29
27
  import { dirname, join } from "node:path";
28
+ import { getAgentDir } from "@mariozechner/pi-coding-agent";
30
29
 
31
30
  // src/provider-tools.ts
32
31
  var PROVIDER_TOOLS = {
32
+ claude: ["search", "answer"],
33
33
  codex: ["search"],
34
34
  exa: ["search", "contents", "answer", "research"],
35
- gemini: ["search", "answer", "research"],
35
+ gemini: ["search", "contents", "answer", "research"],
36
+ perplexity: ["search", "answer", "research"],
36
37
  parallel: ["search", "contents"],
37
38
  valyu: ["search", "contents", "answer", "research"]
38
39
  };
@@ -76,6 +77,7 @@ var LEGACY_TOOL_ALIASES = {
76
77
  };
77
78
  var CONFIG_FILE_NAME = "web-providers.json";
78
79
  var VERSION = 1;
80
+ var commandValueCache = /* @__PURE__ */ new Map();
79
81
  function getConfigPath() {
80
82
  return join(getAgentDir(), CONFIG_FILE_NAME);
81
83
  }
@@ -115,11 +117,26 @@ function serializeConfig(config) {
115
117
  function resolveConfigValue(reference) {
116
118
  if (!reference) return void 0;
117
119
  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;
120
+ const cached = commandValueCache.get(reference);
121
+ if (cached) {
122
+ if (cached.errorMessage) {
123
+ throw new Error(cached.errorMessage);
124
+ }
125
+ return cached.value;
126
+ }
127
+ try {
128
+ const output = execSync(reference.slice(1), {
129
+ encoding: "utf-8",
130
+ stdio: ["ignore", "pipe", "pipe"]
131
+ }).trim();
132
+ const value = output.length > 0 ? output : void 0;
133
+ commandValueCache.set(reference, { value });
134
+ return value;
135
+ } catch (error) {
136
+ const errorMessage = error.message;
137
+ commandValueCache.set(reference, { errorMessage });
138
+ throw error;
139
+ }
123
140
  }
124
141
  const envValue = process.env[reference];
125
142
  if (envValue !== void 0) {
@@ -158,6 +175,12 @@ function normalizeConfig(raw, source) {
158
175
  throw new Error(`'providers' in ${source} must be a JSON object.`);
159
176
  }
160
177
  config.providers = {};
178
+ if (raw.providers.claude !== void 0) {
179
+ config.providers.claude = normalizeClaudeProvider(
180
+ raw.providers.claude,
181
+ source
182
+ );
183
+ }
161
184
  if (raw.providers.codex !== void 0) {
162
185
  config.providers.codex = normalizeCodexProvider(
163
186
  raw.providers.codex,
@@ -173,6 +196,12 @@ function normalizeConfig(raw, source) {
173
196
  source
174
197
  );
175
198
  }
199
+ if (raw.providers.perplexity !== void 0) {
200
+ config.providers.perplexity = normalizePerplexityProvider(
201
+ raw.providers.perplexity,
202
+ source
203
+ );
204
+ }
176
205
  if (raw.providers.parallel !== void 0) {
177
206
  config.providers.parallel = normalizeParallelProvider(
178
207
  raw.providers.parallel,
@@ -186,7 +215,7 @@ function normalizeConfig(raw, source) {
186
215
  );
187
216
  }
188
217
  const unknownProviders = Object.keys(raw.providers).filter(
189
- (key) => key !== "codex" && key !== "exa" && key !== "gemini" && key !== "parallel" && key !== "valyu"
218
+ (key) => key !== "claude" && key !== "codex" && key !== "exa" && key !== "gemini" && key !== "perplexity" && key !== "parallel" && key !== "valyu"
190
219
  );
191
220
  if (unknownProviders.length > 0) {
192
221
  throw new Error(
@@ -196,6 +225,50 @@ function normalizeConfig(raw, source) {
196
225
  }
197
226
  return config;
198
227
  }
228
+ function normalizeClaudeProvider(raw, source) {
229
+ const provider = parseProviderObject(raw, source, "claude");
230
+ const defaults = parseOptionalJsonObject(
231
+ provider.defaults,
232
+ source,
233
+ "providers.claude.defaults"
234
+ );
235
+ return {
236
+ enabled: parseOptionalBoolean(
237
+ provider.enabled,
238
+ source,
239
+ "providers.claude.enabled"
240
+ ),
241
+ tools: parseOptionalProviderTools(
242
+ "claude",
243
+ provider.tools,
244
+ source,
245
+ "providers.claude.tools"
246
+ ),
247
+ pathToClaudeCodeExecutable: parseOptionalString(
248
+ provider.pathToClaudeCodeExecutable,
249
+ source,
250
+ "providers.claude.pathToClaudeCodeExecutable"
251
+ ),
252
+ defaults: defaults === void 0 ? void 0 : {
253
+ model: parseOptionalString(
254
+ defaults.model,
255
+ source,
256
+ "providers.claude.defaults.model"
257
+ ),
258
+ effort: parseOptionalLiteral(
259
+ defaults.effort,
260
+ source,
261
+ "providers.claude.defaults.effort",
262
+ ["low", "medium", "high", "max"]
263
+ ),
264
+ maxTurns: parseOptionalInteger(
265
+ defaults.maxTurns,
266
+ source,
267
+ "providers.claude.defaults.maxTurns"
268
+ )
269
+ }
270
+ };
271
+ }
199
272
  function normalizeCodexProvider(raw, source) {
200
273
  const provider = parseProviderObject(raw, source, "codex");
201
274
  const defaults = parseOptionalJsonObject(
@@ -369,6 +442,11 @@ function normalizeGeminiProvider(raw, source) {
369
442
  source,
370
443
  "providers.gemini.defaults.searchModel"
371
444
  ),
445
+ contentsModel: parseOptionalString(
446
+ defaults.contentsModel,
447
+ source,
448
+ "providers.gemini.defaults.contentsModel"
449
+ ),
372
450
  answerModel: parseOptionalString(
373
451
  defaults.answerModel,
374
452
  source,
@@ -382,6 +460,54 @@ function normalizeGeminiProvider(raw, source) {
382
460
  }
383
461
  };
384
462
  }
463
+ function normalizePerplexityProvider(raw, source) {
464
+ const provider = parseProviderObject(raw, source, "perplexity");
465
+ const defaults = parseOptionalJsonObject(
466
+ provider.defaults,
467
+ source,
468
+ "providers.perplexity.defaults"
469
+ );
470
+ return {
471
+ enabled: parseOptionalBoolean(
472
+ provider.enabled,
473
+ source,
474
+ "providers.perplexity.enabled"
475
+ ),
476
+ tools: parseOptionalProviderTools(
477
+ "perplexity",
478
+ provider.tools,
479
+ source,
480
+ "providers.perplexity.tools"
481
+ ),
482
+ apiKey: parseOptionalString(
483
+ provider.apiKey,
484
+ source,
485
+ "providers.perplexity.apiKey"
486
+ ),
487
+ baseUrl: parseOptionalString(
488
+ provider.baseUrl,
489
+ source,
490
+ "providers.perplexity.baseUrl"
491
+ ),
492
+ defaults: defaults === void 0 ? void 0 : {
493
+ search: parseOptionalJsonObject(
494
+ defaults.search,
495
+ source,
496
+ "providers.perplexity.defaults.search"
497
+ ),
498
+ answer: parseOptionalJsonObject(
499
+ defaults.answer,
500
+ source,
501
+ "providers.perplexity.defaults.answer"
502
+ ),
503
+ research: parseOptionalJsonObject(
504
+ defaults.research,
505
+ source,
506
+ "providers.perplexity.defaults.research"
507
+ )
508
+ }
509
+ };
510
+ }
385
511
  function normalizeParallelProvider(raw, source) {
386
512
  const provider = parseProviderObject(raw, source, "parallel");
387
513
  const defaults = parseOptionalJsonObject(
@@ -450,18 +576,14 @@ function parseOptionalProviderTools(providerId, value, source, field) {
450
576
  continue;
451
577
  }
452
578
  if (!supportsProviderTool(providerId, normalizedKey)) {
453
- throw new Error(
454
- `Unknown tools for ${providerId} in ${source}: ${key}.`
455
- );
579
+ throw new Error(`Unknown tools for ${providerId} in ${source}: ${key}.`);
456
580
  }
457
581
  parsed[normalizedKey] = parseBoolean(entry, source, `${field}.${key}`);
458
582
  }
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
- );
583
+ const unknownTools = Object.keys(value).filter((toolId) => {
584
+ const normalizedKey = normalizeProviderToolKey(providerId, toolId);
585
+ return normalizedKey !== null && !PROVIDER_TOOLS[providerId].includes(normalizedKey);
586
+ });
465
587
  if (unknownTools.length > 0) {
466
588
  throw new Error(
467
589
  `Unknown tools for ${providerId} in ${source}: ${unknownTools.join(", ")}.`
@@ -522,6 +644,13 @@ function parseString(value, source, field) {
522
644
  }
523
645
  return value;
524
646
  }
647
+ function parseOptionalInteger(value, source, field) {
648
+ if (value === void 0) return void 0;
649
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
650
+ throw new Error(`'${field}' in ${source} must be a positive integer.`);
651
+ }
652
+ return value;
653
+ }
525
654
  function parseOptionalLiteral(value, source, field, allowed) {
526
655
  if (value === void 0) return void 0;
527
656
  if (typeof value !== "string" || !allowed.includes(value)) {
@@ -535,8 +664,14 @@ function isPlainObject(value) {
535
664
  return typeof value === "object" && value !== null && !Array.isArray(value);
536
665
  }
537
666
 
538
- // src/providers/codex.ts
539
- import { Codex } from "@openai/codex-sdk";
667
+ // src/providers/claude.ts
668
+ import { execFileSync } from "node:child_process";
669
+ import { existsSync } from "node:fs";
670
+ import { createRequire } from "node:module";
671
+ import { dirname as dirname2, extname, join as join2 } from "node:path";
672
+ import {
673
+ query
674
+ } from "@anthropic-ai/claude-agent-sdk";
540
675
 
541
676
  // src/providers/shared.ts
542
677
  function trimSnippet(input, maxLength = 300) {
@@ -551,8 +686,9 @@ function formatJson(value) {
551
686
  return JSON.stringify(value, null, 2);
552
687
  }
553
688
 
554
- // src/providers/codex.ts
555
- var OUTPUT_SCHEMA = {
689
+ // src/providers/claude.ts
690
+ var require2 = createRequire(import.meta.url);
691
+ var SEARCH_OUTPUT_SCHEMA = {
556
692
  type: "object",
557
693
  additionalProperties: false,
558
694
  properties: {
@@ -572,20 +708,36 @@ var OUTPUT_SCHEMA = {
572
708
  },
573
709
  required: ["sources"]
574
710
  };
575
- var CodexProvider = class {
576
- id = "codex";
577
- label = "Codex";
578
- docsUrl = "https://github.com/openai/codex/tree/main/sdk/typescript";
711
+ var ANSWER_OUTPUT_SCHEMA = {
712
+ type: "object",
713
+ additionalProperties: false,
714
+ properties: {
715
+ answer: { type: "string" },
716
+ sources: {
717
+ type: "array",
718
+ items: {
719
+ type: "object",
720
+ additionalProperties: false,
721
+ properties: {
722
+ title: { type: "string" },
723
+ url: { type: "string" }
724
+ },
725
+ required: ["title", "url"]
726
+ }
727
+ }
728
+ },
729
+ required: ["answer", "sources"]
730
+ };
731
+ var ClaudeProvider = class {
732
+ id = "claude";
733
+ label = "Claude";
734
+ docsUrl = "https://github.com/anthropics/claude-agent-sdk-typescript";
579
735
  createTemplate() {
580
736
  return {
581
- enabled: true,
737
+ enabled: false,
582
738
  tools: {
583
- search: true
584
- },
585
- defaults: {
586
- networkAccessEnabled: true,
587
- webSearchEnabled: true,
588
- webSearchMode: "live"
739
+ search: true,
740
+ answer: true
589
741
  }
590
742
  };
591
743
  }
@@ -596,103 +748,555 @@ var CodexProvider = class {
596
748
  if (config.enabled === false) {
597
749
  return { available: false, summary: "disabled" };
598
750
  }
751
+ const executablePath = resolveClaudeExecutablePath(config);
752
+ if (executablePath && !existsSync(executablePath)) {
753
+ return { available: false, summary: "missing Claude Code executable" };
754
+ }
755
+ const authStatus = getClaudeAuthStatus(executablePath);
756
+ if (!authStatus.loggedIn) {
757
+ return { available: false, summary: "missing Claude auth" };
758
+ }
599
759
  return { available: true, summary: "enabled" };
600
760
  }
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);
761
+ async search(queryText, maxResults, options, config, context) {
762
+ const output = parseClaudeSearchOutput(
763
+ await this.runStructuredQuery({
764
+ prompt: [
765
+ "You are performing web research for another coding agent.",
766
+ "Use the WebSearch tool to search the public web.",
767
+ "Return only a JSON object matching the provided schema.",
768
+ "Do not include markdown fences or extra commentary.",
769
+ `Return at most ${maxResults} sources.`,
770
+ "Each snippet should be short, factual, and specific to the result.",
771
+ "Prefer primary or official sources when they are available.",
772
+ "",
773
+ `User query: ${queryText}`
774
+ ].join("\n"),
775
+ schema: SEARCH_OUTPUT_SCHEMA,
776
+ tools: ["WebSearch"],
777
+ config,
778
+ context,
779
+ options
780
+ })
781
+ );
647
782
  return {
648
783
  provider: this.id,
649
- results: parsed.sources.slice(0, maxResults).map((source) => ({
784
+ results: output.sources.slice(0, maxResults).map((source) => ({
650
785
  title: source.title.trim(),
651
786
  url: source.url.trim(),
652
787
  snippet: trimSnippet(source.snippet)
653
788
  }))
654
789
  };
655
790
  }
791
+ async answer(queryText, options, config, context) {
792
+ const output = parseClaudeAnswerOutput(
793
+ await this.runStructuredQuery({
794
+ prompt: [
795
+ "Answer the user's question using current public web information.",
796
+ "Use WebSearch to find relevant sources and WebFetch when you need to verify important details.",
797
+ "Return only a JSON object matching the provided schema.",
798
+ "Do not include markdown fences or extra commentary.",
799
+ "Keep the answer concise but informative.",
800
+ "Only cite sources you actually used.",
801
+ "",
802
+ `User query: ${queryText}`
803
+ ].join("\n"),
804
+ schema: ANSWER_OUTPUT_SCHEMA,
805
+ tools: ["WebSearch", "WebFetch"],
806
+ config,
807
+ context,
808
+ options
809
+ })
810
+ );
811
+ const lines = [];
812
+ lines.push(output.answer.trim() || "No answer returned.");
813
+ if (output.sources.length > 0) {
814
+ lines.push("");
815
+ lines.push("Sources:");
816
+ for (const [index, source] of output.sources.entries()) {
817
+ lines.push(`${index + 1}. ${source.title}`);
818
+ lines.push(` ${source.url}`);
819
+ }
820
+ }
821
+ return {
822
+ provider: this.id,
823
+ text: lines.join("\n").trimEnd(),
824
+ summary: `Answer via Claude with ${output.sources.length} source(s)`,
825
+ itemCount: output.sources.length
826
+ };
827
+ }
828
+ async runStructuredQuery({
829
+ prompt,
830
+ schema,
831
+ tools,
832
+ config,
833
+ context,
834
+ options
835
+ }) {
836
+ const abortController = new AbortController();
837
+ if (context.signal?.aborted) {
838
+ abortController.abort(context.signal.reason);
839
+ }
840
+ const onAbort = () => {
841
+ abortController.abort(context.signal?.reason);
842
+ };
843
+ context.signal?.addEventListener("abort", onAbort, { once: true });
844
+ const stream = query({
845
+ prompt,
846
+ options: {
847
+ abortController,
848
+ allowedTools: tools,
849
+ cwd: context.cwd,
850
+ ...getClaudeRuntimeOptions(config, options),
851
+ outputFormat: {
852
+ type: "json_schema",
853
+ schema
854
+ },
855
+ pathToClaudeCodeExecutable: config.pathToClaudeCodeExecutable,
856
+ persistSession: false,
857
+ permissionMode: "dontAsk",
858
+ systemPrompt: {
859
+ type: "preset",
860
+ preset: "claude_code",
861
+ append: "Use only the provided web tools. Always produce output that matches the requested JSON schema exactly."
862
+ },
863
+ tools
864
+ }
865
+ });
866
+ const seenToolUseIds = /* @__PURE__ */ new Set();
867
+ let finalResult;
868
+ try {
869
+ for await (const message of stream) {
870
+ handleProgressMessage(message, seenToolUseIds, context.onProgress);
871
+ if (message.type === "result") {
872
+ finalResult = message;
873
+ }
874
+ }
875
+ } finally {
876
+ context.signal?.removeEventListener("abort", onAbort);
877
+ stream.close();
878
+ }
879
+ if (!finalResult) {
880
+ throw new Error("Claude returned no result.");
881
+ }
882
+ if (finalResult.subtype !== "success") {
883
+ throw new Error(
884
+ finalResult.errors.join("\n") || `Claude query failed (${finalResult.subtype}).`
885
+ );
886
+ }
887
+ return parseStructuredOutput(finalResult);
888
+ }
656
889
  };
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}`);
890
+ var CLAUDE_AUTH_CACHE_TTL_MS = 5e3;
891
+ var defaultClaudeExecutablePath;
892
+ var claudeAuthStatusCache = /* @__PURE__ */ new Map();
893
+ function resolveClaudeExecutablePath(config) {
894
+ if (config.pathToClaudeCodeExecutable) {
895
+ return config.pathToClaudeCodeExecutable;
662
896
  }
897
+ if (defaultClaudeExecutablePath !== void 0) {
898
+ return defaultClaudeExecutablePath;
899
+ }
900
+ try {
901
+ const sdkEntryPath = require2.resolve("@anthropic-ai/claude-agent-sdk");
902
+ defaultClaudeExecutablePath = join2(dirname2(sdkEntryPath), "cli.js");
903
+ } catch {
904
+ defaultClaudeExecutablePath = void 0;
905
+ }
906
+ return defaultClaudeExecutablePath;
663
907
  }
664
- function parseOutput(raw) {
665
- if (!raw.trim()) {
666
- throw new Error("Codex returned an empty response.");
908
+ function getClaudeAuthStatus(executablePath) {
909
+ if (!executablePath) {
910
+ return { loggedIn: false };
911
+ }
912
+ const cachedStatus = claudeAuthStatusCache.get(executablePath);
913
+ if (cachedStatus && Date.now() - cachedStatus.checkedAt < CLAUDE_AUTH_CACHE_TTL_MS) {
914
+ return { loggedIn: cachedStatus.loggedIn };
667
915
  }
916
+ const [command, ...args] = getClaudeAuthCommand(executablePath);
668
917
  try {
669
- return JSON.parse(raw);
918
+ const stdout = execFileSync(command, args, {
919
+ encoding: "utf8",
920
+ stdio: ["ignore", "pipe", "pipe"]
921
+ });
922
+ return cacheClaudeAuthStatus(executablePath, parseClaudeAuthStatus(stdout));
923
+ } catch (error) {
924
+ const stdout = getExecOutput(
925
+ error.stdout
926
+ );
927
+ if (stdout) {
928
+ return cacheClaudeAuthStatus(
929
+ executablePath,
930
+ parseClaudeAuthStatus(stdout)
931
+ );
932
+ }
933
+ return cacheClaudeAuthStatus(executablePath, { loggedIn: false });
934
+ }
935
+ }
936
+ function cacheClaudeAuthStatus(executablePath, status) {
937
+ claudeAuthStatusCache.set(executablePath, {
938
+ ...status,
939
+ checkedAt: Date.now()
940
+ });
941
+ return status;
942
+ }
943
+ function getClaudeAuthCommand(executablePath) {
944
+ const extension = extname(executablePath);
945
+ if (extension === ".js" || extension === ".cjs" || extension === ".mjs") {
946
+ return [process.execPath, executablePath, "auth", "status", "--json"];
947
+ }
948
+ return [executablePath, "auth", "status", "--json"];
949
+ }
950
+ function getExecOutput(output) {
951
+ if (typeof output === "string") {
952
+ return output;
953
+ }
954
+ if (Buffer.isBuffer(output)) {
955
+ return output.toString("utf8");
956
+ }
957
+ return "";
958
+ }
959
+ function parseClaudeAuthStatus(raw) {
960
+ try {
961
+ const parsed = JSON.parse(raw);
962
+ return { loggedIn: parsed.loggedIn === true };
670
963
  } catch {
671
- const match = raw.match(/\{[\s\S]*\}/);
964
+ return { loggedIn: false };
965
+ }
966
+ }
967
+ function handleProgressMessage(message, seenToolUseIds, onProgress) {
968
+ if (!onProgress || message.type !== "tool_progress") {
969
+ return;
970
+ }
971
+ if (seenToolUseIds.has(message.tool_use_id)) {
972
+ return;
973
+ }
974
+ seenToolUseIds.add(message.tool_use_id);
975
+ onProgress(`Claude ${formatToolName(message.tool_name)}`);
976
+ }
977
+ function formatToolName(toolName) {
978
+ if (toolName === "WebSearch") return "web search";
979
+ if (toolName === "WebFetch") return "web fetch";
980
+ return toolName;
981
+ }
982
+ function parseStructuredOutput(result) {
983
+ if (result.subtype !== "success") {
984
+ throw new Error("Claude query did not succeed.");
985
+ }
986
+ if (result.structured_output !== void 0) {
987
+ return result.structured_output;
988
+ }
989
+ if (!result.result.trim()) {
990
+ throw new Error("Claude returned an empty response.");
991
+ }
992
+ try {
993
+ return JSON.parse(result.result);
994
+ } catch {
995
+ const match = result.result.match(/\{[\s\S]*\}/);
672
996
  if (!match) {
673
- throw new Error("Codex returned invalid JSON output.");
997
+ throw new Error("Claude returned invalid JSON output.");
674
998
  }
675
999
  return JSON.parse(match[0]);
676
1000
  }
677
1001
  }
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: {
1002
+ function getClaudeRuntimeOptions(config, options) {
1003
+ const model = readNonEmptyString(options?.model) ?? config.defaults?.model;
1004
+ const effort = readEnum(options?.effort, ["low", "medium", "high", "max"]);
1005
+ const maxTurns = readPositiveInteger(options?.maxTurns);
1006
+ const maxThinkingTokens = readNonNegativeInteger(options?.maxThinkingTokens);
1007
+ const maxBudgetUsd = readPositiveNumber(options?.maxBudgetUsd);
1008
+ const thinking = isPlainObject2(options?.thinking) ? options?.thinking : void 0;
1009
+ return {
1010
+ ...model ? { model } : {},
1011
+ ...effort ?? config.defaults?.effort ? { effort: effort ?? config.defaults?.effort } : {},
1012
+ ...maxTurns ?? config.defaults?.maxTurns ? { maxTurns: maxTurns ?? config.defaults?.maxTurns } : {},
1013
+ ...maxThinkingTokens !== void 0 ? { maxThinkingTokens } : {},
1014
+ ...maxBudgetUsd !== void 0 ? { maxBudgetUsd } : {},
1015
+ ...thinking ? { thinking } : {}
1016
+ };
1017
+ }
1018
+ function readNonEmptyString(value) {
1019
+ return typeof value === "string" && value.trim().length > 0 ? value : void 0;
1020
+ }
1021
+ function readPositiveInteger(value) {
1022
+ return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : void 0;
1023
+ }
1024
+ function readNonNegativeInteger(value) {
1025
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : void 0;
1026
+ }
1027
+ function readPositiveNumber(value) {
1028
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
1029
+ }
1030
+ function readEnum(value, values) {
1031
+ return typeof value === "string" && values.includes(value) ? value : void 0;
1032
+ }
1033
+ function isPlainObject2(value) {
1034
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1035
+ }
1036
+ function parseClaudeSearchOutput(value) {
1037
+ const sources = readArray(value, "sources").map((entry) => ({
1038
+ title: readString(entry, "title"),
1039
+ url: readString(entry, "url"),
1040
+ snippet: readString(entry, "snippet")
1041
+ }));
1042
+ return { sources };
1043
+ }
1044
+ function parseClaudeAnswerOutput(value) {
1045
+ return {
1046
+ answer: readString(value, "answer"),
1047
+ sources: readArray(value, "sources").map((entry) => ({
1048
+ title: readString(entry, "title"),
1049
+ url: readString(entry, "url")
1050
+ }))
1051
+ };
1052
+ }
1053
+ function readArray(value, key) {
1054
+ if (typeof value !== "object" || value === null || !(key in value)) {
1055
+ throw new Error(`Claude output is missing '${key}'.`);
1056
+ }
1057
+ const entry = value[key];
1058
+ if (!Array.isArray(entry)) {
1059
+ throw new Error(`Claude output field '${key}' must be an array.`);
1060
+ }
1061
+ return entry;
1062
+ }
1063
+ function readString(value, key) {
1064
+ if (typeof value !== "object" || value === null || !(key in value)) {
1065
+ throw new Error(`Claude output is missing '${key}'.`);
1066
+ }
1067
+ const entry = value[key];
1068
+ if (typeof entry !== "string") {
1069
+ throw new Error(`Claude output field '${key}' must be a string.`);
1070
+ }
1071
+ return entry;
1072
+ }
1073
+
1074
+ // src/providers/codex.ts
1075
+ import { existsSync as existsSync2 } from "node:fs";
1076
+ import { homedir } from "node:os";
1077
+ import { join as join3 } from "node:path";
1078
+ import { Codex } from "@openai/codex-sdk";
1079
+ var OUTPUT_SCHEMA = {
1080
+ type: "object",
1081
+ additionalProperties: false,
1082
+ properties: {
1083
+ sources: {
1084
+ type: "array",
1085
+ items: {
1086
+ type: "object",
1087
+ additionalProperties: false,
1088
+ properties: {
1089
+ title: { type: "string" },
1090
+ url: { type: "string" },
1091
+ snippet: { type: "string" }
1092
+ },
1093
+ required: ["title", "url", "snippet"]
1094
+ }
1095
+ }
1096
+ },
1097
+ required: ["sources"]
1098
+ };
1099
+ var CodexProvider = class {
1100
+ id = "codex";
1101
+ label = "Codex";
1102
+ docsUrl = "https://github.com/openai/codex/tree/main/sdk/typescript";
1103
+ createTemplate() {
1104
+ return {
1105
+ enabled: true,
1106
+ tools: {
1107
+ search: true
1108
+ },
1109
+ defaults: {
1110
+ networkAccessEnabled: true,
1111
+ webSearchEnabled: true,
1112
+ webSearchMode: "live"
1113
+ }
1114
+ };
1115
+ }
1116
+ getStatus(config, _cwd) {
1117
+ if (!config) {
1118
+ return { available: false, summary: "not configured" };
1119
+ }
1120
+ if (config.enabled === false) {
1121
+ return { available: false, summary: "disabled" };
1122
+ }
1123
+ try {
1124
+ new Codex({
1125
+ codexPathOverride: config.codexPath,
1126
+ config: config.config
1127
+ });
1128
+ } catch (error) {
1129
+ return {
1130
+ available: false,
1131
+ summary: error.message
1132
+ };
1133
+ }
1134
+ if (!hasCodexCredentials(config)) {
1135
+ return { available: false, summary: "missing Codex auth" };
1136
+ }
1137
+ return { available: true, summary: "enabled" };
1138
+ }
1139
+ async search(query2, maxResults, options, config, context) {
1140
+ const codex = new Codex({
1141
+ codexPathOverride: config.codexPath,
1142
+ baseUrl: config.baseUrl,
1143
+ apiKey: resolveConfigValue(config.apiKey),
1144
+ config: config.config,
1145
+ env: resolveEnvMap(config.env)
1146
+ });
1147
+ const thread = codex.startThread(
1148
+ buildCodexSearchThreadOptions(config, context.cwd, options)
1149
+ );
1150
+ const prompt = [
1151
+ "You are performing web research for another coding agent.",
1152
+ "Search the public web and return only a JSON object matching the provided schema.",
1153
+ "Do not include markdown fences or extra commentary.",
1154
+ `Return at most ${maxResults} sources.`,
1155
+ "Prefer primary or official sources when they are available.",
1156
+ "Each snippet should be short and specific.",
1157
+ "",
1158
+ `User query: ${query2}`
1159
+ ].join("\n");
1160
+ const streamed = await thread.runStreamed(prompt, {
1161
+ outputSchema: OUTPUT_SCHEMA,
1162
+ signal: context.signal
1163
+ });
1164
+ let finalResponse = "";
1165
+ const seenQueries = /* @__PURE__ */ new Set();
1166
+ for await (const event of streamed.events) {
1167
+ handleProgressEvent(event, seenQueries, context.onProgress);
1168
+ if (event.type === "item.completed" && event.item.type === "agent_message") {
1169
+ finalResponse = event.item.text;
1170
+ }
1171
+ if (event.type === "turn.failed") {
1172
+ throw new Error(event.error.message);
1173
+ }
1174
+ }
1175
+ const parsed = parseOutput(finalResponse);
1176
+ return {
1177
+ provider: this.id,
1178
+ results: parsed.sources.slice(0, maxResults).map((source) => ({
1179
+ title: source.title.trim(),
1180
+ url: source.url.trim(),
1181
+ snippet: trimSnippet(source.snippet)
1182
+ }))
1183
+ };
1184
+ }
1185
+ };
1186
+ function buildCodexSearchThreadOptions(config, cwd, options) {
1187
+ const runtimeOptions = getCodexSearchRuntimeOptions(options);
1188
+ return {
1189
+ additionalDirectories: config.defaults?.additionalDirectories,
1190
+ approvalPolicy: "never",
1191
+ model: runtimeOptions.model ?? config.defaults?.model,
1192
+ modelReasoningEffort: runtimeOptions.modelReasoningEffort ?? config.defaults?.modelReasoningEffort,
1193
+ networkAccessEnabled: config.defaults?.networkAccessEnabled ?? true,
1194
+ sandboxMode: "read-only",
1195
+ skipGitRepoCheck: true,
1196
+ webSearchEnabled: config.defaults?.webSearchEnabled ?? true,
1197
+ webSearchMode: runtimeOptions.webSearchMode ?? config.defaults?.webSearchMode ?? "live",
1198
+ workingDirectory: cwd
1199
+ };
1200
+ }
1201
+ function getCodexSearchRuntimeOptions(options) {
1202
+ if (!options) {
1203
+ return {};
1204
+ }
1205
+ const model = readNonEmptyString2(options.model);
1206
+ const modelReasoningEffort = readEnum2(options.modelReasoningEffort, [
1207
+ "minimal",
1208
+ "low",
1209
+ "medium",
1210
+ "high",
1211
+ "xhigh"
1212
+ ]);
1213
+ const webSearchMode = readEnum2(options.webSearchMode, [
1214
+ "disabled",
1215
+ "cached",
1216
+ "live"
1217
+ ]);
1218
+ return {
1219
+ ...model ? { model } : {},
1220
+ ...modelReasoningEffort ? { modelReasoningEffort } : {},
1221
+ ...webSearchMode ? { webSearchMode } : {}
1222
+ };
1223
+ }
1224
+ function readNonEmptyString2(value) {
1225
+ return typeof value === "string" && value.trim().length > 0 ? value : void 0;
1226
+ }
1227
+ function readEnum2(value, values) {
1228
+ return typeof value === "string" && values.includes(value) ? value : void 0;
1229
+ }
1230
+ function hasCodexCredentials(config) {
1231
+ if (hasConfiguredReference(config.apiKey)) {
1232
+ return true;
1233
+ }
1234
+ if (hasConfiguredReference(config.env?.CODEX_API_KEY) || hasConfiguredReference(config.env?.OPENAI_API_KEY)) {
1235
+ return true;
1236
+ }
1237
+ if (!config.env) {
1238
+ const inheritedKey = process.env.CODEX_API_KEY ?? process.env.OPENAI_API_KEY;
1239
+ if (typeof inheritedKey === "string" && inheritedKey.trim().length > 0) {
1240
+ return true;
1241
+ }
1242
+ }
1243
+ return existsSync2(join3(homedir(), ".codex", "auth.json"));
1244
+ }
1245
+ function hasConfiguredReference(reference) {
1246
+ if (!reference) {
1247
+ return false;
1248
+ }
1249
+ if (reference.startsWith("!")) {
1250
+ return reference.slice(1).trim().length > 0;
1251
+ }
1252
+ const envValue = process.env[reference];
1253
+ if (typeof envValue === "string") {
1254
+ return envValue.trim().length > 0;
1255
+ }
1256
+ if (/^[A-Z][A-Z0-9_]*$/.test(reference)) {
1257
+ return false;
1258
+ }
1259
+ return reference.trim().length > 0;
1260
+ }
1261
+ function handleProgressEvent(event, seenQueries, onProgress) {
1262
+ if (!onProgress) return;
1263
+ if (event.type === "item.completed" && event.item.type === "web_search" && !seenQueries.has(event.item.query)) {
1264
+ seenQueries.add(event.item.query);
1265
+ onProgress(`Codex web search ${seenQueries.size}: ${event.item.query}`);
1266
+ }
1267
+ }
1268
+ function parseOutput(raw) {
1269
+ if (!raw.trim()) {
1270
+ throw new Error("Codex returned an empty response.");
1271
+ }
1272
+ try {
1273
+ return JSON.parse(raw);
1274
+ } catch {
1275
+ const match = raw.match(/\{[\s\S]*\}/);
1276
+ if (!match) {
1277
+ throw new Error("Codex returned invalid JSON output.");
1278
+ }
1279
+ return JSON.parse(match[0]);
1280
+ }
1281
+ }
1282
+
1283
+ // src/providers/exa.ts
1284
+ import { Exa } from "exa-js";
1285
+ var ExaProvider = class {
1286
+ id = "exa";
1287
+ label = "Exa";
1288
+ docsUrl = "https://exa.ai/docs/sdks/typescript-sdk-specification";
1289
+ createTemplate() {
1290
+ return {
1291
+ enabled: false,
1292
+ tools: {
1293
+ search: true,
1294
+ contents: true,
1295
+ answer: true,
1296
+ research: true
1297
+ },
1298
+ apiKey: "EXA_API_KEY",
1299
+ defaults: {
696
1300
  type: "auto",
697
1301
  contents: {
698
1302
  text: true
@@ -713,7 +1317,7 @@ var ExaProvider = class {
713
1317
  }
714
1318
  return { available: true, summary: "enabled" };
715
1319
  }
716
- async search(query, maxResults, config, context) {
1320
+ async search(query2, maxResults, searchOptions, config, context) {
717
1321
  const apiKey = resolveConfigValue(config.apiKey);
718
1322
  if (!apiKey) {
719
1323
  throw new Error("Exa is missing an API key.");
@@ -721,10 +1325,11 @@ var ExaProvider = class {
721
1325
  const client = new Exa(apiKey, config.baseUrl);
722
1326
  const options = {
723
1327
  ...asJsonObject(config.defaults),
1328
+ ...searchOptions ?? {},
724
1329
  numResults: maxResults
725
1330
  };
726
- context.onProgress?.(`Searching Exa for: ${query}`);
727
- const response = await client.search(query, options);
1331
+ context.onProgress?.(`Searching Exa for: ${query2}`);
1332
+ const response = await client.search(query2, options);
728
1333
  return {
729
1334
  provider: this.id,
730
1335
  results: (response.results ?? []).slice(0, maxResults).map((result) => ({
@@ -743,11 +1348,15 @@ var ExaProvider = class {
743
1348
  throw new Error("Exa is missing an API key.");
744
1349
  }
745
1350
  const client = new Exa(apiKey, config.baseUrl);
746
- context.onProgress?.(`Fetching contents from Exa for ${urls.length} URL(s)`);
1351
+ context.onProgress?.(
1352
+ `Fetching contents from Exa for ${urls.length} URL(s)`
1353
+ );
747
1354
  const response = await client.getContents(urls, options);
748
1355
  const lines = [];
749
1356
  for (const [index, result] of (response.results ?? []).entries()) {
750
- lines.push(`${index + 1}. ${String(result.title ?? result.url ?? "Untitled")}`);
1357
+ lines.push(
1358
+ `${index + 1}. ${String(result.title ?? result.url ?? "Untitled")}`
1359
+ );
751
1360
  lines.push(` ${String(result.url ?? "")}`);
752
1361
  const summary = typeof result.summary === "string" ? result.summary : result.summary ? formatJson(result.summary) : void 0;
753
1362
  const text = typeof result.text === "string" ? result.text : Array.isArray(result.highlights) ? result.highlights.join(" ") : "";
@@ -764,14 +1373,14 @@ var ExaProvider = class {
764
1373
  itemCount: response.results?.length ?? 0
765
1374
  };
766
1375
  }
767
- async answer(query, options, config, context) {
1376
+ async answer(query2, options, config, context) {
768
1377
  const apiKey = resolveConfigValue(config.apiKey);
769
1378
  if (!apiKey) {
770
1379
  throw new Error("Exa is missing an API key.");
771
1380
  }
772
1381
  const client = new Exa(apiKey, config.baseUrl);
773
- context.onProgress?.(`Getting Exa answer for: ${query}`);
774
- const response = await client.answer(query, options);
1382
+ context.onProgress?.(`Getting Exa answer for: ${query2}`);
1383
+ const response = await client.answer(query2, options);
775
1384
  const lines = [];
776
1385
  lines.push(
777
1386
  typeof response.answer === "string" ? response.answer : formatJson(response.answer)
@@ -781,7 +1390,9 @@ var ExaProvider = class {
781
1390
  lines.push("");
782
1391
  lines.push("Sources:");
783
1392
  for (const [index, citation] of citations.entries()) {
784
- lines.push(`${index + 1}. ${String(citation.title ?? citation.url ?? "Untitled")}`);
1393
+ lines.push(
1394
+ `${index + 1}. ${String(citation.title ?? citation.url ?? "Untitled")}`
1395
+ );
785
1396
  lines.push(` ${String(citation.url ?? "")}`);
786
1397
  }
787
1398
  }
@@ -823,6 +1434,7 @@ var ExaProvider = class {
823
1434
  // src/providers/gemini.ts
824
1435
  import { GoogleGenAI } from "@google/genai";
825
1436
  var DEFAULT_SEARCH_MODEL = "gemini-2.5-flash";
1437
+ var DEFAULT_CONTENTS_MODEL = "gemini-2.5-flash";
826
1438
  var DEFAULT_ANSWER_MODEL = "gemini-2.5-flash";
827
1439
  var DEFAULT_RESEARCH_AGENT = "deep-research-pro-preview-12-2025";
828
1440
  var DEFAULT_POLL_INTERVAL_MS = 3e3;
@@ -835,12 +1447,14 @@ var GeminiProvider = class {
835
1447
  enabled: false,
836
1448
  tools: {
837
1449
  search: true,
1450
+ contents: true,
838
1451
  answer: true,
839
1452
  research: true
840
1453
  },
841
1454
  apiKey: "GOOGLE_API_KEY",
842
1455
  defaults: {
843
1456
  searchModel: DEFAULT_SEARCH_MODEL,
1457
+ contentsModel: DEFAULT_CONTENTS_MODEL,
844
1458
  answerModel: DEFAULT_ANSWER_MODEL,
845
1459
  researchAgent: DEFAULT_RESEARCH_AGENT
846
1460
  }
@@ -859,39 +1473,90 @@ var GeminiProvider = class {
859
1473
  }
860
1474
  return { available: true, summary: "enabled" };
861
1475
  }
862
- async search(query, maxResults, config, context) {
1476
+ async search(query2, maxResults, options, config, context) {
863
1477
  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
- }));
1478
+ const request = buildGeminiSearchRequest(
1479
+ query2,
1480
+ config.defaults?.searchModel ?? DEFAULT_SEARCH_MODEL,
1481
+ options
1482
+ );
1483
+ context.onProgress?.(`Searching Gemini for: ${query2}`);
1484
+ const interaction = await createSearchInteraction(ai, request);
1485
+ const results = await Promise.all(
1486
+ extractGoogleSearchResults(interaction.outputs).slice(0, maxResults).map(async (result) => {
1487
+ const resolvedUrl = await resolveGoogleSearchUrl(result.url);
1488
+ return {
1489
+ title: result.title ?? resolvedUrl ?? result.url ?? "Untitled",
1490
+ url: resolvedUrl ?? result.url ?? "",
1491
+ snippet: ""
1492
+ };
1493
+ })
1494
+ );
879
1495
  return {
880
1496
  provider: this.id,
881
1497
  results
882
1498
  };
883
1499
  }
884
- async answer(query, options, config, context) {
1500
+ async contents(urls, options, config, context) {
885
1501
  const ai = this.createClient(config);
886
- const model = config.defaults?.answerModel ?? DEFAULT_ANSWER_MODEL;
887
- context.onProgress?.(`Getting Gemini answer for: ${query}`);
1502
+ context.onProgress?.(
1503
+ `Fetching contents from Gemini for ${urls.length} URL(s)`
1504
+ );
1505
+ const urlList = urls.map((url) => `- ${url}`).join("\n");
1506
+ const request = buildGeminiGenerateContentRequest({
1507
+ defaultModel: config.defaults?.contentsModel ?? DEFAULT_CONTENTS_MODEL,
1508
+ prompt: `Extract the main textual content from each of the following URLs. For each URL, return the page title followed by the cleaned body text. Preserve the original structure (headings, paragraphs, lists) but remove navigation, ads, and boilerplate.
1509
+
1510
+ ${urlList}`,
1511
+ options,
1512
+ toolConfig: { urlContext: {} }
1513
+ });
888
1514
  const response = await ai.models.generateContent({
889
- model,
890
- contents: query,
891
- config: {
892
- ...options ?? {},
893
- tools: [{ googleSearch: {} }]
1515
+ model: request.model,
1516
+ contents: [request.contents],
1517
+ config: request.config
1518
+ });
1519
+ const text = response.text?.trim() || "";
1520
+ const metadata = extractUrlContextMetadata(response.candidates);
1521
+ const lines = [];
1522
+ if (text) {
1523
+ lines.push(text);
1524
+ }
1525
+ if (metadata.length > 0) {
1526
+ const failures = metadata.filter(
1527
+ (entry) => entry.status !== "URL_RETRIEVAL_STATUS_SUCCESS" && entry.status !== void 0
1528
+ );
1529
+ if (failures.length > 0) {
1530
+ lines.push("");
1531
+ lines.push("Retrieval issues:");
1532
+ for (const failure of failures) {
1533
+ lines.push(`- ${failure.url}: ${failure.status}`);
1534
+ }
894
1535
  }
1536
+ }
1537
+ const successCount = metadata.filter(
1538
+ (entry) => entry.status === "URL_RETRIEVAL_STATUS_SUCCESS" || entry.status === void 0
1539
+ ).length;
1540
+ return {
1541
+ provider: this.id,
1542
+ text: lines.join("\n").trimEnd() || "No contents extracted.",
1543
+ summary: `${successCount} of ${urls.length} URL(s) extracted via Gemini`,
1544
+ itemCount: successCount
1545
+ };
1546
+ }
1547
+ async answer(query2, options, config, context) {
1548
+ const ai = this.createClient(config);
1549
+ const request = buildGeminiGenerateContentRequest({
1550
+ defaultModel: config.defaults?.answerModel ?? DEFAULT_ANSWER_MODEL,
1551
+ prompt: query2,
1552
+ options,
1553
+ toolConfig: { googleSearch: {} }
1554
+ });
1555
+ context.onProgress?.(`Getting Gemini answer for: ${query2}`);
1556
+ const response = await ai.models.generateContent({
1557
+ model: request.model,
1558
+ contents: request.contents,
1559
+ config: request.config
895
1560
  });
896
1561
  const lines = [];
897
1562
  lines.push(response.text?.trim() || "No answer returned.");
@@ -903,7 +1568,9 @@ var GeminiProvider = class {
903
1568
  lines.push("Sources:");
904
1569
  for (const [index, source] of sources.entries()) {
905
1570
  lines.push(`${index + 1}. ${source.title}`);
906
- lines.push(` ${source.url}`);
1571
+ if (source.url) {
1572
+ lines.push(` ${source.url}`);
1573
+ }
907
1574
  }
908
1575
  }
909
1576
  return {
@@ -917,7 +1584,11 @@ var GeminiProvider = class {
917
1584
  const ai = this.createClient(config);
918
1585
  const agent = config.defaults?.researchAgent ?? DEFAULT_RESEARCH_AGENT;
919
1586
  const pollIntervalMs = getPollInterval(options);
920
- const requestOptions = stripPollIntervalOption(options);
1587
+ const requestOptions = getGeminiResearchRequestOptions(
1588
+ stripPollIntervalOption(options)
1589
+ );
1590
+ const startedAt = Date.now();
1591
+ let lastStatus;
921
1592
  context.onProgress?.("Starting Gemini deep research");
922
1593
  const initialInteraction = await ai.interactions.create({
923
1594
  ...requestOptions,
@@ -931,7 +1602,13 @@ var GeminiProvider = class {
931
1602
  throw new Error("Gemini research aborted.");
932
1603
  }
933
1604
  const interaction = await ai.interactions.get(initialInteraction.id);
934
- context.onProgress?.(`Gemini research status: ${interaction.status}`);
1605
+ const now = Date.now();
1606
+ if (interaction.status !== lastStatus) {
1607
+ context.onProgress?.(
1608
+ `Gemini research status: ${interaction.status} (${formatElapsed(now - startedAt)} elapsed)`
1609
+ );
1610
+ lastStatus = interaction.status;
1611
+ }
935
1612
  if (interaction.status === "completed") {
936
1613
  const text = formatInteractionOutputs(interaction.outputs);
937
1614
  return {
@@ -943,112 +1620,482 @@ var GeminiProvider = class {
943
1620
  if (interaction.status === "failed" || interaction.status === "cancelled") {
944
1621
  throw new Error(`Gemini research ${interaction.status}.`);
945
1622
  }
946
- await sleep(pollIntervalMs, context.signal);
1623
+ await sleep(pollIntervalMs, context.signal);
1624
+ }
1625
+ }
1626
+ createClient(config) {
1627
+ const apiKey = resolveConfigValue(config.apiKey);
1628
+ if (!apiKey) {
1629
+ throw new Error("Gemini is missing an API key.");
1630
+ }
1631
+ return new GoogleGenAI({
1632
+ apiKey,
1633
+ apiVersion: config.defaults?.apiVersion
1634
+ });
1635
+ }
1636
+ };
1637
+ function extractGoogleSearchResults(outputs) {
1638
+ const results = [];
1639
+ if (!Array.isArray(outputs)) {
1640
+ return results;
1641
+ }
1642
+ for (const output of outputs) {
1643
+ if (typeof output !== "object" || output === null) {
1644
+ continue;
1645
+ }
1646
+ const content = output;
1647
+ if (content.type !== "google_search_result") {
1648
+ continue;
1649
+ }
1650
+ const items = Array.isArray(content.result) ? content.result : [];
1651
+ for (const item of items) {
1652
+ if (typeof item !== "object" || item === null) {
1653
+ continue;
1654
+ }
1655
+ const record = item;
1656
+ results.push({
1657
+ title: typeof record.title === "string" ? record.title : void 0,
1658
+ url: typeof record.url === "string" ? record.url : void 0,
1659
+ rendered_content: typeof record.rendered_content === "string" ? record.rendered_content : void 0
1660
+ });
1661
+ }
1662
+ }
1663
+ return results;
1664
+ }
1665
+ function extractGroundingSources(chunks) {
1666
+ const seen = /* @__PURE__ */ new Set();
1667
+ const sources = [];
1668
+ const maxSources = 5;
1669
+ if (!Array.isArray(chunks)) {
1670
+ return sources;
1671
+ }
1672
+ for (const chunk of chunks) {
1673
+ const web = typeof chunk === "object" && chunk !== null && "web" in chunk && typeof chunk.web === "object" && chunk.web !== null ? chunk.web : void 0;
1674
+ if (!web) continue;
1675
+ const rawUrl = typeof web.uri === "string" ? web.uri : "";
1676
+ const title = formatGroundingSourceTitle(
1677
+ typeof web.title === "string" ? web.title : rawUrl,
1678
+ rawUrl
1679
+ );
1680
+ const url = formatGroundingSourceUrl(rawUrl);
1681
+ const key = [title.toLowerCase(), url.toLowerCase()].join("::");
1682
+ if (seen.has(key)) continue;
1683
+ seen.add(key);
1684
+ sources.push({
1685
+ title,
1686
+ url
1687
+ });
1688
+ if (sources.length >= maxSources) {
1689
+ break;
1690
+ }
1691
+ }
1692
+ return sources;
1693
+ }
1694
+ function extractUrlContextMetadata(candidates) {
1695
+ const results = [];
1696
+ if (!Array.isArray(candidates)) {
1697
+ return results;
1698
+ }
1699
+ for (const candidate of candidates) {
1700
+ if (typeof candidate !== "object" || candidate === null) {
1701
+ continue;
1702
+ }
1703
+ const metadata = candidate.urlContextMetadata;
1704
+ if (!metadata?.urlMetadata || !Array.isArray(metadata.urlMetadata)) {
1705
+ continue;
1706
+ }
1707
+ for (const entry of metadata.urlMetadata) {
1708
+ if (typeof entry !== "object" || entry === null) {
1709
+ continue;
1710
+ }
1711
+ results.push({
1712
+ url: typeof entry.retrievedUrl === "string" ? entry.retrievedUrl : "unknown",
1713
+ status: typeof entry.urlRetrievalStatus === "string" ? entry.urlRetrievalStatus : void 0
1714
+ });
1715
+ }
1716
+ }
1717
+ return results;
1718
+ }
1719
+ function formatInteractionOutputs(outputs) {
1720
+ const lines = [];
1721
+ if (!Array.isArray(outputs)) {
1722
+ return "";
1723
+ }
1724
+ for (const output of outputs) {
1725
+ if (typeof output === "object" && output !== null && "type" in output && output.type === "text" && "text" in output && typeof output.text === "string") {
1726
+ const text = output.text.trim();
1727
+ if (text) {
1728
+ lines.push(text);
1729
+ }
1730
+ }
1731
+ }
1732
+ return lines.join("\n\n").trim();
1733
+ }
1734
+ function formatGroundingSourceTitle(title, url) {
1735
+ const trimmedTitle = title?.trim();
1736
+ if (trimmedTitle) {
1737
+ return trimmedTitle;
1738
+ }
1739
+ if (url) {
1740
+ try {
1741
+ return new URL(url).hostname;
1742
+ } catch {
1743
+ return url;
1744
+ }
1745
+ }
1746
+ return "Untitled";
1747
+ }
1748
+ function formatGroundingSourceUrl(url) {
1749
+ if (!url) {
1750
+ return "";
1751
+ }
1752
+ if (isGoogleGroundingRedirect(url)) {
1753
+ return "";
1754
+ }
1755
+ return url;
1756
+ }
1757
+ function isGoogleGroundingRedirect(url) {
1758
+ try {
1759
+ const parsed = new URL(url);
1760
+ return parsed.hostname === "vertexaisearch.cloud.google.com" && parsed.pathname.startsWith("/grounding-api-redirect/");
1761
+ } catch {
1762
+ return false;
1763
+ }
1764
+ }
1765
+ async function createSearchInteraction(ai, request) {
1766
+ const forcedRequest = {
1767
+ ...request,
1768
+ ...request.generation_config ? {
1769
+ generation_config: {
1770
+ ...request.generation_config,
1771
+ tool_choice: "any"
1772
+ }
1773
+ } : {
1774
+ generation_config: {
1775
+ tool_choice: "any"
1776
+ }
1777
+ }
1778
+ };
1779
+ try {
1780
+ return await ai.interactions.create(forcedRequest);
1781
+ } catch (error) {
1782
+ if (!isBuiltInToolChoiceError(error)) {
1783
+ throw error;
1784
+ }
1785
+ const fallbackGenerationConfig = stripToolChoice(request.generation_config);
1786
+ return ai.interactions.create({
1787
+ ...request,
1788
+ ...fallbackGenerationConfig ? { generation_config: fallbackGenerationConfig } : {}
1789
+ });
1790
+ }
1791
+ }
1792
+ function isBuiltInToolChoiceError(error) {
1793
+ if (error instanceof Error) {
1794
+ return error.message.includes(
1795
+ "Function calling config is set without function_declarations"
1796
+ );
1797
+ }
1798
+ if (typeof error === "object" && error !== null && "message" in error && typeof error.message === "string") {
1799
+ return error.message.includes(
1800
+ "Function calling config is set without function_declarations"
1801
+ );
1802
+ }
1803
+ return false;
1804
+ }
1805
+ async function resolveGoogleSearchUrl(url) {
1806
+ if (!url) {
1807
+ return void 0;
1808
+ }
1809
+ if (!isGoogleGroundingRedirect(url)) {
1810
+ return url;
1811
+ }
1812
+ try {
1813
+ const response = await fetch(url, {
1814
+ method: "HEAD",
1815
+ redirect: "manual"
1816
+ });
1817
+ return response.headers.get("location") || url;
1818
+ } catch {
1819
+ return url;
1820
+ }
1821
+ }
1822
+ async function sleep(ms, signal) {
1823
+ if (signal?.aborted) {
1824
+ throw new Error("Operation aborted.");
1825
+ }
1826
+ await new Promise((resolve, reject) => {
1827
+ const timer = setTimeout(() => {
1828
+ signal?.removeEventListener("abort", onAbort);
1829
+ resolve();
1830
+ }, ms);
1831
+ const onAbort = () => {
1832
+ clearTimeout(timer);
1833
+ signal?.removeEventListener("abort", onAbort);
1834
+ reject(new Error("Operation aborted."));
1835
+ };
1836
+ signal?.addEventListener("abort", onAbort, { once: true });
1837
+ });
1838
+ }
1839
+ function formatElapsed(ms) {
1840
+ const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
1841
+ const minutes = Math.floor(totalSeconds / 60);
1842
+ const seconds = totalSeconds % 60;
1843
+ if (minutes > 0) {
1844
+ return `${minutes}m ${seconds}s`;
1845
+ }
1846
+ return `${totalSeconds}s`;
1847
+ }
1848
+ function getPollInterval(options) {
1849
+ const raw = options?.pollIntervalMs;
1850
+ if (typeof raw === "number" && Number.isFinite(raw) && raw >= 1e3) {
1851
+ return Math.trunc(raw);
1852
+ }
1853
+ return DEFAULT_POLL_INTERVAL_MS;
1854
+ }
1855
+ function stripPollIntervalOption(options) {
1856
+ if (!options || !Object.hasOwn(options, "pollIntervalMs")) {
1857
+ return options;
1858
+ }
1859
+ const { pollIntervalMs: _ignored, ...rest } = options;
1860
+ return rest;
1861
+ }
1862
+ function buildGeminiSearchRequest(query2, defaultModel, options) {
1863
+ return {
1864
+ model: readNonEmptyString3(options?.model) ?? defaultModel,
1865
+ input: query2,
1866
+ tools: [{ type: "google_search" }],
1867
+ ...isPlainObject3(options?.generation_config) ? { generation_config: options.generation_config } : {}
1868
+ };
1869
+ }
1870
+ function buildGeminiGenerateContentRequest({
1871
+ defaultModel,
1872
+ prompt,
1873
+ options,
1874
+ toolConfig
1875
+ }) {
1876
+ const requestOptions = isPlainObject3(options) ? options : {};
1877
+ const explicitConfig = isPlainObject3(requestOptions.config) ? requestOptions.config : {};
1878
+ return {
1879
+ model: readNonEmptyString3(requestOptions.model) ?? defaultModel,
1880
+ contents: prompt,
1881
+ config: {
1882
+ ...explicitConfig,
1883
+ tools: [toolConfig]
1884
+ }
1885
+ };
1886
+ }
1887
+ function getGeminiResearchRequestOptions(options) {
1888
+ if (!isPlainObject3(options)) {
1889
+ return {};
1890
+ }
1891
+ return { ...options };
1892
+ }
1893
+ function stripToolChoice(generationConfig) {
1894
+ if (!generationConfig || !Object.hasOwn(generationConfig, "tool_choice")) {
1895
+ return generationConfig;
1896
+ }
1897
+ const { tool_choice: _ignored, ...rest } = generationConfig;
1898
+ return Object.keys(rest).length > 0 ? rest : void 0;
1899
+ }
1900
+ function isPlainObject3(value) {
1901
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1902
+ }
1903
+ function readNonEmptyString3(value) {
1904
+ return typeof value === "string" && value.trim().length > 0 ? value : void 0;
1905
+ }
1906
+
1907
+ // src/providers/perplexity.ts
1908
+ import Perplexity from "@perplexity-ai/perplexity_ai";
1909
+ var DEFAULT_ANSWER_MODEL2 = "sonar";
1910
+ var DEFAULT_RESEARCH_MODEL = "sonar-deep-research";
1911
+ var PerplexityProvider = class {
1912
+ id = "perplexity";
1913
+ label = "Perplexity";
1914
+ docsUrl = "https://docs.perplexity.ai/docs/sdk/overview.md";
1915
+ createTemplate() {
1916
+ return {
1917
+ enabled: false,
1918
+ tools: {
1919
+ search: true,
1920
+ answer: true,
1921
+ research: true
1922
+ },
1923
+ apiKey: "PERPLEXITY_API_KEY",
1924
+ defaults: {
1925
+ answer: {
1926
+ model: DEFAULT_ANSWER_MODEL2
1927
+ },
1928
+ research: {
1929
+ model: DEFAULT_RESEARCH_MODEL
1930
+ }
1931
+ }
1932
+ };
1933
+ }
1934
+ getStatus(config) {
1935
+ if (!config) {
1936
+ return { available: false, summary: "not configured" };
1937
+ }
1938
+ if (config.enabled === false) {
1939
+ return { available: false, summary: "disabled" };
1940
+ }
1941
+ const apiKey = resolveConfigValue(config.apiKey);
1942
+ if (!apiKey) {
1943
+ return { available: false, summary: "missing apiKey" };
1944
+ }
1945
+ return { available: true, summary: "enabled" };
1946
+ }
1947
+ async search(query2, maxResults, options, config, context) {
1948
+ const client = this.createClient(config);
1949
+ const request = {
1950
+ ...asJsonObject(config.defaults?.search),
1951
+ ...options ?? {},
1952
+ query: query2,
1953
+ max_results: maxResults
1954
+ };
1955
+ context.onProgress?.(`Searching Perplexity for: ${query2}`);
1956
+ const response = await client.search.create(
1957
+ request,
1958
+ buildRequestOptions(context)
1959
+ );
1960
+ return {
1961
+ provider: this.id,
1962
+ results: response.results.slice(0, maxResults).map((result) => ({
1963
+ title: result.title,
1964
+ url: result.url,
1965
+ snippet: trimSnippet(result.snippet),
1966
+ metadata: result.date || result.last_updated ? {
1967
+ ...result.date ? { date: result.date } : {},
1968
+ ...result.last_updated ? { last_updated: result.last_updated } : {}
1969
+ } : void 0
1970
+ }))
1971
+ };
1972
+ }
1973
+ async answer(query2, options, config, context) {
1974
+ context.onProgress?.(`Getting Perplexity answer for: ${query2}`);
1975
+ return this.runChatTool(
1976
+ query2,
1977
+ options,
1978
+ config,
1979
+ context,
1980
+ DEFAULT_ANSWER_MODEL2,
1981
+ "Answer"
1982
+ );
1983
+ }
1984
+ async research(input, options, config, context) {
1985
+ context.onProgress?.("Starting Perplexity research");
1986
+ return this.runChatTool(
1987
+ input,
1988
+ options,
1989
+ config,
1990
+ context,
1991
+ DEFAULT_RESEARCH_MODEL,
1992
+ "Research",
1993
+ true
1994
+ );
1995
+ }
1996
+ async runChatTool(input, options, config, context, fallbackModel, label, isResearch = false) {
1997
+ const client = this.createClient(config);
1998
+ const defaults = isResearch ? config.defaults?.research : config.defaults?.answer;
1999
+ const request = {
2000
+ ...asJsonObject(defaults),
2001
+ ...options ?? {},
2002
+ messages: [{ role: "user", content: input }],
2003
+ model: resolveModel(
2004
+ (options ?? {}).model,
2005
+ asJsonObject(defaults).model,
2006
+ fallbackModel
2007
+ ) ?? fallbackModel,
2008
+ stream: false
2009
+ };
2010
+ const response = await client.chat.completions.create(
2011
+ request,
2012
+ buildRequestOptions(context)
2013
+ );
2014
+ const content = extractMessageText(response.choices[0]?.message?.content);
2015
+ const sources = dedupeSources(extractSources(response));
2016
+ const lines = [];
2017
+ lines.push(content || `No ${label.toLowerCase()} returned.`);
2018
+ if (sources.length > 0) {
2019
+ lines.push("");
2020
+ lines.push("Sources:");
2021
+ for (const [index, source] of sources.entries()) {
2022
+ lines.push(`${index + 1}. ${source.title}`);
2023
+ lines.push(` ${source.url}`);
2024
+ }
947
2025
  }
2026
+ return {
2027
+ provider: this.id,
2028
+ text: lines.join("\n").trimEnd(),
2029
+ summary: `${label} via Perplexity with ${sources.length} source(s)`,
2030
+ itemCount: sources.length
2031
+ };
948
2032
  }
949
2033
  createClient(config) {
950
2034
  const apiKey = resolveConfigValue(config.apiKey);
951
2035
  if (!apiKey) {
952
- throw new Error("Gemini is missing an API key.");
2036
+ throw new Error("Perplexity is missing an API key.");
953
2037
  }
954
- return new GoogleGenAI({
2038
+ return new Perplexity({
955
2039
  apiKey,
956
- apiVersion: config.defaults?.apiVersion
2040
+ baseURL: resolveConfigValue(config.baseUrl)
957
2041
  });
958
2042
  }
959
2043
  };
960
- function extractGoogleSearchResults(outputs) {
961
- const results = [];
962
- if (!Array.isArray(outputs)) {
963
- return results;
2044
+ function resolveModel(optionModel, defaultModel, fallbackModel) {
2045
+ if (typeof optionModel === "string" && optionModel.trim().length > 0) {
2046
+ return optionModel;
964
2047
  }
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
- }
2048
+ if (typeof defaultModel === "string" && defaultModel.trim().length > 0) {
2049
+ return defaultModel;
985
2050
  }
986
- return results;
2051
+ return fallbackModel;
987
2052
  }
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
- });
2053
+ function extractMessageText(content) {
2054
+ if (typeof content === "string") {
2055
+ return content.trim();
1004
2056
  }
1005
- return sources;
1006
- }
1007
- function formatInteractionOutputs(outputs) {
1008
- const lines = [];
1009
- if (!Array.isArray(outputs)) {
2057
+ if (!Array.isArray(content)) {
1010
2058
  return "";
1011
2059
  }
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
- }
2060
+ return content.flatMap((chunk) => {
2061
+ if (typeof chunk === "object" && chunk !== null && "type" in chunk && chunk.type === "text" && "text" in chunk && typeof chunk.text === "string") {
2062
+ return [chunk.text.trim()];
1018
2063
  }
1019
- }
1020
- return lines.join("\n\n").trim();
2064
+ return [];
2065
+ }).filter((text) => text.length > 0).join("\n\n").trim();
1021
2066
  }
1022
- async function sleep(ms, signal) {
1023
- if (signal?.aborted) {
1024
- throw new Error("Operation aborted.");
2067
+ function dedupeSources(sources) {
2068
+ const seen = /* @__PURE__ */ new Set();
2069
+ const unique = [];
2070
+ for (const source of sources) {
2071
+ const title = source.title.trim() || source.url.trim() || "Untitled";
2072
+ const url = source.url.trim();
2073
+ if (!url) continue;
2074
+ const key = `${title.toLowerCase()}::${url.toLowerCase()}`;
2075
+ if (seen.has(key)) continue;
2076
+ seen.add(key);
2077
+ unique.push({ title, url });
1025
2078
  }
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
- });
2079
+ return unique;
1038
2080
  }
1039
- function getPollInterval(options) {
1040
- const raw = options?.pollIntervalMs;
1041
- if (typeof raw === "number" && Number.isFinite(raw) && raw >= 1e3) {
1042
- return Math.trunc(raw);
2081
+ function extractSources(response) {
2082
+ const searchResults = response.search_results?.flatMap((result) => {
2083
+ const url = result.url?.trim() ?? "";
2084
+ if (!url) {
2085
+ return [];
2086
+ }
2087
+ return [{ title: result.title?.trim() ?? url, url }];
2088
+ }) ?? [];
2089
+ if (searchResults.length > 0) {
2090
+ return searchResults;
1043
2091
  }
1044
- return DEFAULT_POLL_INTERVAL_MS;
2092
+ return response.citations?.flatMap((citation) => {
2093
+ const url = citation?.trim() ?? "";
2094
+ return url ? [{ title: url, url }] : [];
2095
+ }) ?? [];
1045
2096
  }
1046
- function stripPollIntervalOption(options) {
1047
- if (!options || !Object.hasOwn(options, "pollIntervalMs")) {
1048
- return options;
1049
- }
1050
- const { pollIntervalMs: _ignored, ...rest } = options;
1051
- return rest;
2097
+ function buildRequestOptions(context) {
2098
+ return context.signal ? { signal: context.signal } : void 0;
1052
2099
  }
1053
2100
 
1054
2101
  // src/providers/parallel.ts
@@ -1089,13 +2136,14 @@ var ParallelProvider = class {
1089
2136
  }
1090
2137
  return { available: true, summary: "enabled" };
1091
2138
  }
1092
- async search(query, maxResults, config, context) {
2139
+ async search(query2, maxResults, options, config, context) {
1093
2140
  const client = this.createClient(config);
1094
2141
  const defaults = asJsonObject(config.defaults?.search);
1095
- context.onProgress?.(`Searching Parallel for: ${query}`);
2142
+ context.onProgress?.(`Searching Parallel for: ${query2}`);
1096
2143
  const response = await client.beta.search({
1097
2144
  ...defaults,
1098
- objective: query,
2145
+ ...options ?? {},
2146
+ objective: query2,
1099
2147
  max_results: maxResults
1100
2148
  });
1101
2149
  return {
@@ -1192,7 +2240,7 @@ var ValyuProvider = class {
1192
2240
  }
1193
2241
  return { available: true, summary: "enabled" };
1194
2242
  }
1195
- async search(query, maxResults, config, context) {
2243
+ async search(query2, maxResults, searchOptions, config, context) {
1196
2244
  const apiKey = resolveConfigValue(config.apiKey);
1197
2245
  if (!apiKey) {
1198
2246
  throw new Error("Valyu is missing an API key.");
@@ -1200,10 +2248,11 @@ var ValyuProvider = class {
1200
2248
  const client = new Valyu(apiKey, config.baseUrl);
1201
2249
  const options = {
1202
2250
  ...asJsonObject(config.defaults),
2251
+ ...searchOptions ?? {},
1203
2252
  maxNumResults: maxResults
1204
2253
  };
1205
- context.onProgress?.(`Searching Valyu for: ${query}`);
1206
- const response = await client.search(query, options);
2254
+ context.onProgress?.(`Searching Valyu for: ${query2}`);
2255
+ const response = await client.search(query2, options);
1207
2256
  if (!response.success) {
1208
2257
  throw new Error(response.error || "Valyu search failed.");
1209
2258
  }
@@ -1225,7 +2274,9 @@ var ValyuProvider = class {
1225
2274
  throw new Error("Valyu is missing an API key.");
1226
2275
  }
1227
2276
  const client = new Valyu(apiKey, config.baseUrl);
1228
- context.onProgress?.(`Fetching contents from Valyu for ${urls.length} URL(s)`);
2277
+ context.onProgress?.(
2278
+ `Fetching contents from Valyu for ${urls.length} URL(s)`
2279
+ );
1229
2280
  const response = await client.contents(urls, options);
1230
2281
  const finalResponse = "jobId" in response ? await client.waitForJob(response.jobId, {
1231
2282
  onProgress: (status) => context.onProgress?.(
@@ -1257,14 +2308,14 @@ var ValyuProvider = class {
1257
2308
  itemCount: results.length
1258
2309
  };
1259
2310
  }
1260
- async answer(query, options, config, context) {
2311
+ async answer(query2, options, config, context) {
1261
2312
  const apiKey = resolveConfigValue(config.apiKey);
1262
2313
  if (!apiKey) {
1263
2314
  throw new Error("Valyu is missing an API key.");
1264
2315
  }
1265
2316
  const client = new Valyu(apiKey, config.baseUrl);
1266
- context.onProgress?.(`Getting Valyu answer for: ${query}`);
1267
- const response = await client.answer(query, {
2317
+ context.onProgress?.(`Getting Valyu answer for: ${query2}`);
2318
+ const response = await client.answer(query2, {
1268
2319
  ...options ?? {},
1269
2320
  streaming: false
1270
2321
  });
@@ -1343,45 +2394,57 @@ var ValyuProvider = class {
1343
2394
 
1344
2395
  // src/providers/index.ts
1345
2396
  var PROVIDERS = [
2397
+ new ClaudeProvider(),
1346
2398
  new CodexProvider(),
1347
2399
  new ExaProvider(),
1348
2400
  new GeminiProvider(),
2401
+ new PerplexityProvider(),
1349
2402
  new ParallelProvider(),
1350
2403
  new ValyuProvider()
1351
2404
  ];
1352
2405
  var PROVIDER_MAP = {
1353
- codex: PROVIDERS[0],
1354
- exa: PROVIDERS[1],
1355
- gemini: PROVIDERS[2],
1356
- parallel: PROVIDERS[3],
1357
- valyu: PROVIDERS[4]
2406
+ claude: PROVIDERS[0],
2407
+ codex: PROVIDERS[1],
2408
+ exa: PROVIDERS[2],
2409
+ gemini: PROVIDERS[3],
2410
+ perplexity: PROVIDERS[4],
2411
+ parallel: PROVIDERS[5],
2412
+ valyu: PROVIDERS[6]
1358
2413
  };
1359
2414
 
1360
2415
  // src/provider-resolution.ts
2416
+ var IMPLICIT_PROVIDER_FALLBACKS = ["codex"];
1361
2417
  function resolveProviderChoice(config, explicit, cwd) {
1362
2418
  return resolveProviderForCapability(config, explicit, cwd, "search");
1363
2419
  }
2420
+ function getEffectiveProviderConfig(config, providerId) {
2421
+ const configured = config.providers?.[providerId];
2422
+ if (configured) {
2423
+ return configured;
2424
+ }
2425
+ if (IMPLICIT_PROVIDER_FALLBACKS.includes(providerId)) {
2426
+ return {
2427
+ ...PROVIDER_MAP[providerId].createTemplate(),
2428
+ enabled: true
2429
+ };
2430
+ }
2431
+ return void 0;
2432
+ }
1364
2433
  function resolveProviderForCapability(config, explicit, cwd, capability) {
1365
2434
  if (explicit) {
1366
2435
  const provider = PROVIDER_MAP[explicit];
2436
+ const providerConfig = getEffectiveProviderConfig(config, explicit);
1367
2437
  if (typeof provider[capability] !== "function") {
1368
2438
  throw new Error(
1369
2439
  `Provider '${explicit}' does not support '${capability}'.`
1370
2440
  );
1371
2441
  }
1372
- if (!isProviderToolEnabled(
1373
- explicit,
1374
- config.providers?.[explicit],
1375
- capability
1376
- )) {
2442
+ if (!isProviderToolEnabled(explicit, providerConfig, capability)) {
1377
2443
  throw new Error(
1378
2444
  `Provider '${explicit}' has '${capability}' disabled in config.`
1379
2445
  );
1380
2446
  }
1381
- const status = provider.getStatus(
1382
- config.providers?.[explicit],
1383
- cwd
1384
- );
2447
+ const status = provider.getStatus(providerConfig, cwd);
1385
2448
  if (!status.available) {
1386
2449
  throw new Error(
1387
2450
  `Provider '${explicit}' is not available: ${status.summary}.`
@@ -1403,19 +2466,14 @@ function resolveProviderForCapability(config, explicit, cwd, capability) {
1403
2466
  const status = provider.getStatus(providerConfig, cwd);
1404
2467
  if (status.available) return provider;
1405
2468
  }
1406
- for (const provider of PROVIDERS) {
2469
+ for (const providerId of IMPLICIT_PROVIDER_FALLBACKS) {
2470
+ const provider = PROVIDER_MAP[providerId];
1407
2471
  if (typeof provider[capability] !== "function") continue;
1408
- if (!isProviderToolEnabled(
1409
- provider.id,
1410
- config.providers?.[provider.id],
1411
- capability
1412
- )) {
2472
+ const providerConfig = getEffectiveProviderConfig(config, provider.id);
2473
+ if (!isProviderToolEnabled(provider.id, providerConfig, capability)) {
1413
2474
  continue;
1414
2475
  }
1415
- const status = provider.getStatus(
1416
- config.providers?.[provider.id],
1417
- cwd
1418
- );
2476
+ const status = provider.getStatus(providerConfig, cwd);
1419
2477
  if (status.available) return provider;
1420
2478
  }
1421
2479
  throw new Error(
@@ -1424,16 +2482,32 @@ function resolveProviderForCapability(config, explicit, cwd, capability) {
1424
2482
  }
1425
2483
 
1426
2484
  // src/types.ts
1427
- var PROVIDER_IDS = ["codex", "exa", "gemini", "parallel", "valyu"];
2485
+ var PROVIDER_IDS = [
2486
+ "claude",
2487
+ "codex",
2488
+ "exa",
2489
+ "gemini",
2490
+ "perplexity",
2491
+ "parallel",
2492
+ "valyu"
2493
+ ];
1428
2494
 
1429
2495
  // src/index.ts
1430
2496
  var DEFAULT_MAX_RESULTS = 5;
1431
2497
  var MAX_ALLOWED_RESULTS = 20;
2498
+ var RESEARCH_HEARTBEAT_MS = 15e3;
2499
+ var CAPABILITY_TOOL_NAMES = {
2500
+ search: "web_search",
2501
+ contents: "web_contents",
2502
+ answer: "web_answer",
2503
+ research: "web_research"
2504
+ };
2505
+ var MANAGED_TOOL_NAMES = Object.values(CAPABILITY_TOOL_NAMES);
2506
+ var PROVIDER_OVERRIDE_GUIDELINES = [
2507
+ "Do not set provider unless the user asks for one."
2508
+ ];
1432
2509
  function webProvidersExtension(pi) {
1433
- registerWebSearchTool(pi);
1434
- registerWebContentsTool(pi);
1435
- registerWebAnswerTool(pi);
1436
- registerWebResearchTool(pi);
2510
+ registerManagedTools(pi);
1437
2511
  pi.registerCommand("web-providers", {
1438
2512
  description: "Configure web search providers",
1439
2513
  handler: async (_args, ctx) => {
@@ -1441,15 +2515,38 @@ function webProvidersExtension(pi) {
1441
2515
  ctx.ui.notify("web-providers requires interactive mode", "error");
1442
2516
  return;
1443
2517
  }
1444
- await runWebProvidersConfig(ctx);
2518
+ await runWebProvidersConfig(pi, ctx);
1445
2519
  }
1446
2520
  });
2521
+ pi.on("session_start", async (_event, ctx) => {
2522
+ await refreshManagedTools(pi, ctx.cwd, { addAvailable: true });
2523
+ });
2524
+ pi.on("before_agent_start", async (_event, ctx) => {
2525
+ await refreshManagedTools(pi, ctx.cwd, { addAvailable: false });
2526
+ });
2527
+ }
2528
+ function registerManagedTools(pi, providerIdsByCapability = {}) {
2529
+ registerWebSearchTool(pi, providerIdsByCapability.search ?? PROVIDER_IDS);
2530
+ registerWebContentsTool(
2531
+ pi,
2532
+ providerIdsByCapability.contents ?? getProviderIdsForCapability("contents")
2533
+ );
2534
+ registerWebAnswerTool(
2535
+ pi,
2536
+ providerIdsByCapability.answer ?? getProviderIdsForCapability("answer")
2537
+ );
2538
+ registerWebResearchTool(
2539
+ pi,
2540
+ providerIdsByCapability.research ?? getProviderIdsForCapability("research")
2541
+ );
1447
2542
  }
1448
- function registerWebSearchTool(pi) {
2543
+ function registerWebSearchTool(pi, providerIds) {
2544
+ const visibleProviderIds = providerIds.length > 0 ? providerIds : PROVIDER_IDS;
1449
2545
  pi.registerTool({
1450
2546
  name: "web_search",
1451
2547
  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.`,
2548
+ description: `Find likely sources on the public web and return titles, URLs, and snippets. Output is truncated to ${DEFAULT_MAX_LINES} lines or ${formatSize(DEFAULT_MAX_BYTES)} when needed.`,
2549
+ promptGuidelines: PROVIDER_OVERRIDE_GUIDELINES,
1453
2550
  parameters: Type.Object({
1454
2551
  query: Type.String({ description: "What to search for on the web" }),
1455
2552
  maxResults: Type.Optional(
@@ -1459,23 +2556,24 @@ function registerWebSearchTool(pi) {
1459
2556
  description: `Maximum number of results to return (default: ${DEFAULT_MAX_RESULTS})`
1460
2557
  })
1461
2558
  ),
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
- })
2559
+ options: jsonOptionsSchema("Provider-specific search options."),
2560
+ provider: providerEnum(
2561
+ visibleProviderIds,
2562
+ "Provider override. If omitted, uses the active configured provider or falls back to Codex for search when it is not explicitly disabled."
1466
2563
  )
1467
2564
  }),
1468
2565
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
1469
2566
  const config = await loadConfig();
1470
2567
  const provider = resolveProviderChoice(config, params.provider, ctx.cwd);
1471
2568
  const maxResults = clampResults(params.maxResults);
1472
- const providerConfig = config.providers?.[provider.id];
2569
+ const providerConfig = getEffectiveProviderConfig(config, provider.id);
1473
2570
  if (!providerConfig) {
1474
2571
  throw new Error(`Provider '${provider.id}' is not configured.`);
1475
2572
  }
1476
2573
  const response = await provider.search(
1477
2574
  params.query,
1478
2575
  maxResults,
2576
+ normalizeOptions(params.options),
1479
2577
  providerConfig,
1480
2578
  {
1481
2579
  cwd: ctx.cwd,
@@ -1524,13 +2622,12 @@ function registerWebSearchTool(pi) {
1524
2622
  }
1525
2623
  });
1526
2624
  }
1527
- function registerWebContentsTool(pi) {
1528
- const providerIds = getProviderIdsForCapability("contents");
2625
+ function registerWebContentsTool(pi, providerIds) {
1529
2626
  if (providerIds.length === 0) return;
1530
2627
  pi.registerTool({
1531
2628
  name: "web_contents",
1532
2629
  label: "Web Contents",
1533
- description: "Fetch extracted contents for one or more URLs using a configured provider.",
2630
+ description: "Read and extract the main contents of one or more web pages.",
1534
2631
  parameters: Type.Object({
1535
2632
  urls: Type.Array(Type.String({ minLength: 1 }), {
1536
2633
  minItems: 1,
@@ -1542,6 +2639,7 @@ function registerWebContentsTool(pi) {
1542
2639
  "Provider override. If omitted, uses the active configured provider that supports web contents."
1543
2640
  )
1544
2641
  }),
2642
+ promptGuidelines: PROVIDER_OVERRIDE_GUIDELINES,
1545
2643
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
1546
2644
  return executeProviderTool({
1547
2645
  capability: "contents",
@@ -1579,13 +2677,12 @@ function registerWebContentsTool(pi) {
1579
2677
  }
1580
2678
  });
1581
2679
  }
1582
- function registerWebAnswerTool(pi) {
1583
- const providerIds = getProviderIdsForCapability("answer");
2680
+ function registerWebAnswerTool(pi, providerIds) {
1584
2681
  if (providerIds.length === 0) return;
1585
2682
  pi.registerTool({
1586
2683
  name: "web_answer",
1587
2684
  label: "Web Answer",
1588
- description: "Get a provider-generated answer grounded in web results.",
2685
+ description: "Answer a question using web-grounded evidence.",
1589
2686
  parameters: Type.Object({
1590
2687
  query: Type.String({ description: "Question to answer" }),
1591
2688
  options: jsonOptionsSchema("Provider-specific answer options."),
@@ -1594,6 +2691,7 @@ function registerWebAnswerTool(pi) {
1594
2691
  "Provider override. If omitted, uses the active configured provider that supports web answers."
1595
2692
  )
1596
2693
  }),
2694
+ promptGuidelines: PROVIDER_OVERRIDE_GUIDELINES,
1597
2695
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
1598
2696
  return executeProviderTool({
1599
2697
  capability: "answer",
@@ -1613,8 +2711,10 @@ function registerWebAnswerTool(pi) {
1613
2711
  renderCall(args, theme) {
1614
2712
  return renderToolCallHeader(
1615
2713
  "web_answer",
1616
- `"${cleanSingleLine(String(args.query ?? "")).slice(0, 80)}"`,
1617
- [`provider=${String(args.provider ?? "auto")}`],
2714
+ formatQuotedPreview(String(args.query ?? "")),
2715
+ [
2716
+ `provider=${String(args.provider ?? "auto")}`
2717
+ ],
1618
2718
  theme
1619
2719
  );
1620
2720
  },
@@ -1629,13 +2729,12 @@ function registerWebAnswerTool(pi) {
1629
2729
  }
1630
2730
  });
1631
2731
  }
1632
- function registerWebResearchTool(pi) {
1633
- const providerIds = getProviderIdsForCapability("research");
2732
+ function registerWebResearchTool(pi, providerIds) {
1634
2733
  if (providerIds.length === 0) return;
1635
2734
  pi.registerTool({
1636
2735
  name: "web_research",
1637
2736
  label: "Web Research",
1638
- description: "Run a longer-form research task using a provider that supports research.",
2737
+ description: "Investigate a topic across web sources and produce a longer report.",
1639
2738
  parameters: Type.Object({
1640
2739
  input: Type.String({ description: "Research brief or question" }),
1641
2740
  options: jsonOptionsSchema("Provider-specific research options."),
@@ -1644,6 +2743,7 @@ function registerWebResearchTool(pi) {
1644
2743
  "Provider override. If omitted, uses the active configured provider that supports research."
1645
2744
  )
1646
2745
  }),
2746
+ promptGuidelines: PROVIDER_OVERRIDE_GUIDELINES,
1647
2747
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
1648
2748
  return executeProviderTool({
1649
2749
  capability: "research",
@@ -1663,8 +2763,10 @@ function registerWebResearchTool(pi) {
1663
2763
  renderCall(args, theme) {
1664
2764
  return renderToolCallHeader(
1665
2765
  "web_research",
1666
- `"${cleanSingleLine(String(args.input ?? "")).slice(0, 80)}"`,
1667
- [`provider=${String(args.provider ?? "auto")}`],
2766
+ formatQuotedPreview(String(args.input ?? "")),
2767
+ [
2768
+ `provider=${String(args.provider ?? "auto")}`
2769
+ ],
1668
2770
  theme
1669
2771
  );
1670
2772
  },
@@ -1679,7 +2781,7 @@ function registerWebResearchTool(pi) {
1679
2781
  }
1680
2782
  });
1681
2783
  }
1682
- async function runWebProvidersConfig(ctx) {
2784
+ async function runWebProvidersConfig(pi, ctx) {
1683
2785
  const config = await loadConfig();
1684
2786
  const activeProvider = await getPreferredProvider(ctx.cwd);
1685
2787
  await ctx.ui.custom(
@@ -1692,6 +2794,60 @@ async function runWebProvidersConfig(ctx) {
1692
2794
  activeProvider
1693
2795
  )
1694
2796
  );
2797
+ await refreshManagedTools(pi, ctx.cwd, { addAvailable: true });
2798
+ }
2799
+ function getAvailableProviderIdsForCapability(config, cwd, capability) {
2800
+ const providerIds = [];
2801
+ for (const providerId of getProviderIdsForCapability(capability)) {
2802
+ try {
2803
+ resolveProviderForCapability(config, providerId, cwd, capability);
2804
+ providerIds.push(providerId);
2805
+ } catch {
2806
+ }
2807
+ }
2808
+ return providerIds;
2809
+ }
2810
+ function getAvailableManagedToolNames(config, cwd) {
2811
+ return Object.keys(CAPABILITY_TOOL_NAMES).filter(
2812
+ (capability) => getAvailableProviderIdsForCapability(config, cwd, capability).length > 0
2813
+ ).map((capability) => CAPABILITY_TOOL_NAMES[capability]);
2814
+ }
2815
+ function getSyncedActiveTools(config, cwd, activeToolNames, options) {
2816
+ const availableToolNames = new Set(getAvailableManagedToolNames(config, cwd));
2817
+ const nextActiveTools = new Set(activeToolNames);
2818
+ for (const toolName of MANAGED_TOOL_NAMES) {
2819
+ if (availableToolNames.has(toolName)) {
2820
+ if (options.addAvailable) {
2821
+ nextActiveTools.add(toolName);
2822
+ }
2823
+ continue;
2824
+ }
2825
+ nextActiveTools.delete(toolName);
2826
+ }
2827
+ return nextActiveTools;
2828
+ }
2829
+ async function refreshManagedTools(pi, cwd, options) {
2830
+ const config = await loadConfig();
2831
+ const nextActiveTools = getSyncedActiveTools(
2832
+ config,
2833
+ cwd,
2834
+ pi.getActiveTools(),
2835
+ options
2836
+ );
2837
+ registerManagedTools(pi, {
2838
+ search: getAvailableProviderIdsForCapability(config, cwd, "search"),
2839
+ contents: getAvailableProviderIdsForCapability(config, cwd, "contents"),
2840
+ answer: getAvailableProviderIdsForCapability(config, cwd, "answer"),
2841
+ research: getAvailableProviderIdsForCapability(config, cwd, "research")
2842
+ });
2843
+ await syncManagedToolAvailability(pi, nextActiveTools);
2844
+ }
2845
+ async function syncManagedToolAvailability(pi, nextActiveTools) {
2846
+ const activeTools = pi.getActiveTools();
2847
+ const changed = activeTools.length !== nextActiveTools.size || activeTools.some((toolName) => !nextActiveTools.has(toolName));
2848
+ if (changed) {
2849
+ pi.setActiveTools(Array.from(nextActiveTools));
2850
+ }
1695
2851
  }
1696
2852
  function getProviderIdsForCapability(capability) {
1697
2853
  return PROVIDERS.filter(
@@ -1703,15 +2859,21 @@ function providerEnum(providerIds, description) {
1703
2859
  return Type.Optional(Type.Literal(providerIds[0], { description }));
1704
2860
  }
1705
2861
  return Type.Optional(
1706
- Type.Union(providerIds.map((id) => Type.Literal(id)), { description })
2862
+ Type.Union(
2863
+ providerIds.map((id) => Type.Literal(id)),
2864
+ { description }
2865
+ )
1707
2866
  );
1708
2867
  }
1709
2868
  function jsonOptionsSchema(description) {
1710
2869
  return Type.Optional(
1711
- Type.Object({}, {
1712
- additionalProperties: true,
1713
- description
1714
- })
2870
+ Type.Object(
2871
+ {},
2872
+ {
2873
+ additionalProperties: true,
2874
+ description
2875
+ }
2876
+ )
1715
2877
  );
1716
2878
  }
1717
2879
  async function executeProviderTool({
@@ -1729,36 +2891,74 @@ async function executeProviderTool({
1729
2891
  ctx.cwd,
1730
2892
  capability
1731
2893
  );
1732
- const providerConfig = config.providers?.[provider.id];
2894
+ const providerConfig = getEffectiveProviderConfig(config, provider.id);
1733
2895
  if (!providerConfig) {
1734
2896
  throw new Error(`Provider '${provider.id}' is not configured.`);
1735
2897
  }
1736
- const response = await invoke(
1737
- provider,
1738
- providerConfig,
1739
- {
2898
+ const progress = createToolProgressReporter(
2899
+ capability,
2900
+ provider.id,
2901
+ onUpdate
2902
+ );
2903
+ let response;
2904
+ try {
2905
+ response = await invoke(provider, providerConfig, {
1740
2906
  cwd: ctx.cwd,
1741
2907
  signal: signal ?? void 0,
1742
- onProgress: (message) => onUpdate?.({
1743
- content: [{ type: "text", text: message }],
1744
- details: {}
1745
- })
1746
- }
1747
- );
2908
+ onProgress: progress.report
2909
+ });
2910
+ } finally {
2911
+ progress.stop();
2912
+ }
1748
2913
  const details = {
1749
2914
  tool: `web_${capability}`,
1750
2915
  provider: response.provider,
1751
2916
  summary: response.summary,
1752
2917
  itemCount: response.itemCount
1753
2918
  };
2919
+ const text = await truncateAndSave(response.text, capability);
1754
2920
  return {
1755
- content: [{ type: "text", text: response.text }],
2921
+ content: [{ type: "text", text }],
1756
2922
  details
1757
2923
  };
1758
2924
  }
1759
2925
  function normalizeOptions(value) {
1760
2926
  return isJsonObject(value) ? value : void 0;
1761
2927
  }
2928
+ function createToolProgressReporter(capability, providerId, onUpdate) {
2929
+ if (!onUpdate) {
2930
+ return { report: void 0, stop: () => {
2931
+ } };
2932
+ }
2933
+ const emit = (message) => onUpdate({
2934
+ content: [{ type: "text", text: message }],
2935
+ details: {}
2936
+ });
2937
+ const startedAt = Date.now();
2938
+ let lastUpdateAt = startedAt;
2939
+ let timer;
2940
+ if (capability === "research") {
2941
+ timer = setInterval(() => {
2942
+ if (Date.now() - lastUpdateAt < RESEARCH_HEARTBEAT_MS) {
2943
+ return;
2944
+ }
2945
+ const elapsed = formatElapsed2(Date.now() - startedAt);
2946
+ emit(`web_research still running via ${providerId} (${elapsed} elapsed)`);
2947
+ lastUpdateAt = Date.now();
2948
+ }, RESEARCH_HEARTBEAT_MS);
2949
+ }
2950
+ return {
2951
+ report: (message) => {
2952
+ lastUpdateAt = Date.now();
2953
+ emit(message);
2954
+ },
2955
+ stop: () => {
2956
+ if (timer) {
2957
+ clearInterval(timer);
2958
+ }
2959
+ }
2960
+ };
2961
+ }
1762
2962
  function renderToolCallHeader(toolName, primary, details, theme) {
1763
2963
  return {
1764
2964
  invalidate() {
@@ -1829,6 +3029,30 @@ function buildProviderMenuOptions(providerId) {
1829
3029
  values
1830
3030
  });
1831
3031
  };
3032
+ if (providerId === "claude") {
3033
+ pushText(
3034
+ "model",
3035
+ "Model",
3036
+ "Optional Claude model override. Leave empty to use the local default."
3037
+ );
3038
+ pushValues(
3039
+ "claudeEffort",
3040
+ "Effort",
3041
+ "How much effort Claude should use. 'default' uses the SDK default.",
3042
+ ["default", "low", "medium", "high", "max"]
3043
+ );
3044
+ pushText(
3045
+ "claudeMaxTurns",
3046
+ "Max turns",
3047
+ "Optional maximum number of Claude turns. Leave empty to use the SDK default."
3048
+ );
3049
+ pushText(
3050
+ "claudePathToExecutable",
3051
+ "Executable path",
3052
+ "Optional path to the Claude Code executable. Leave empty to use the bundled/default executable."
3053
+ );
3054
+ return options;
3055
+ }
1832
3056
  if (providerId === "codex") {
1833
3057
  pushText(
1834
3058
  "model",
@@ -1912,6 +3136,11 @@ function buildProviderMenuOptions(providerId) {
1912
3136
  "Search model",
1913
3137
  "Model used for Gemini search interactions."
1914
3138
  );
3139
+ pushText(
3140
+ "geminiContentsModel",
3141
+ "Contents model",
3142
+ "Model used for Gemini URL content extraction via URL Context."
3143
+ );
1915
3144
  pushText(
1916
3145
  "geminiAnswerModel",
1917
3146
  "Answer model",
@@ -1924,6 +3153,9 @@ function buildProviderMenuOptions(providerId) {
1924
3153
  );
1925
3154
  return options;
1926
3155
  }
3156
+ if (providerId === "perplexity") {
3157
+ return options;
3158
+ }
1927
3159
  if (providerId === "parallel") {
1928
3160
  pushValues(
1929
3161
  "parallelSearchMode",
@@ -1983,17 +3215,24 @@ var WebProvidersSettingsView = class {
1983
3215
  }
1984
3216
  const lines = [];
1985
3217
  const providerItems = this.buildProviderSectionItems();
1986
- lines.push(...this.renderSection(width, "Provider", "provider", providerItems));
3218
+ lines.push(
3219
+ ...this.renderSection(width, "Provider", "provider", providerItems)
3220
+ );
1987
3221
  lines.push("");
1988
3222
  const toolItems = this.buildToolSectionItems();
1989
3223
  lines.push(...this.renderSection(width, "Tools", "tools", toolItems));
1990
3224
  lines.push("");
1991
3225
  const configItems = this.buildConfigSectionItems();
1992
- lines.push(...this.renderSection(width, "Provider config", "config", configItems));
3226
+ lines.push(
3227
+ ...this.renderSection(width, "Provider config", "config", configItems)
3228
+ );
1993
3229
  const selected = this.getSelectedEntry();
1994
3230
  if (selected) {
1995
3231
  lines.push("");
1996
- for (const line of wrapTextWithAnsi(selected.description, Math.max(10, width - 2))) {
3232
+ for (const line of wrapTextWithAnsi(
3233
+ selected.description,
3234
+ Math.max(10, width - 2)
3235
+ )) {
1997
3236
  lines.push(truncateToWidth(this.theme.fg("dim", line), width));
1998
3237
  }
1999
3238
  }
@@ -2090,10 +3329,13 @@ var WebProvidersSettingsView = class {
2090
3329
  }
2091
3330
  if (option.kind === "text") {
2092
3331
  const key = option.key;
2093
- const currentValue = key === "model" || key === "additionalDirectories" ? getCodexTextSettingValue(
3332
+ const currentValue = this.activeProvider === "claude" && (key === "model" || key === "claudePathToExecutable" || key === "claudeMaxTurns") ? getClaudeTextSettingValue(
2094
3333
  providerConfig,
2095
3334
  key
2096
- ) : key === "geminiSearchModel" || key === "geminiAnswerModel" || key === "geminiResearchAgent" ? getGeminiTextSettingValue(
3335
+ ) : key === "model" || key === "additionalDirectories" ? getCodexTextSettingValue(
3336
+ providerConfig,
3337
+ key
3338
+ ) : key === "geminiSearchModel" || key === "geminiContentsModel" || key === "geminiAnswerModel" || key === "geminiResearchAgent" ? getGeminiTextSettingValue(
2097
3339
  providerConfig,
2098
3340
  key
2099
3341
  ) : getProviderStringValue(
@@ -2132,7 +3374,7 @@ var WebProvidersSettingsView = class {
2132
3374
  "tools",
2133
3375
  "config"
2134
3376
  ];
2135
- let index = sections.indexOf(this.activeSection);
3377
+ const index = sections.indexOf(this.activeSection);
2136
3378
  for (let offset = 1; offset <= sections.length; offset++) {
2137
3379
  const next = sections[(index + offset * direction + sections.length) % sections.length];
2138
3380
  if (this.getSectionEntries(next).length > 0) {
@@ -2221,13 +3463,19 @@ var WebProvidersSettingsView = class {
2221
3463
  if (id === "apiKey" || id === "baseUrl") {
2222
3464
  return getProviderStringValue(providerConfig, id);
2223
3465
  }
3466
+ if (this.activeProvider === "claude" && (id === "model" || id === "claudePathToExecutable" || id === "claudeMaxTurns")) {
3467
+ return getClaudeTextSettingValue(
3468
+ providerConfig,
3469
+ id
3470
+ );
3471
+ }
2224
3472
  if (id === "model" || id === "additionalDirectories") {
2225
3473
  return getCodexTextSettingValue(
2226
3474
  providerConfig,
2227
3475
  id
2228
3476
  );
2229
3477
  }
2230
- if (id === "geminiSearchModel" || id === "geminiAnswerModel" || id === "geminiResearchAgent") {
3478
+ if (id === "geminiSearchModel" || id === "geminiContentsModel" || id === "geminiAnswerModel" || id === "geminiResearchAgent") {
2231
3479
  return getGeminiTextSettingValue(
2232
3480
  providerConfig,
2233
3481
  id
@@ -2268,6 +3516,11 @@ var WebProvidersSettingsView = class {
2268
3516
  }
2269
3517
  if (id === "apiKey" || id === "baseUrl") {
2270
3518
  assignOptionalString(providerConfig, id, value);
3519
+ } else if (this.activeProvider === "claude" && applyClaudeSettingChange(
3520
+ providerConfig,
3521
+ id,
3522
+ value
3523
+ )) {
2271
3524
  } else if (this.activeProvider === "codex" && applyCodexSettingChange(
2272
3525
  providerConfig,
2273
3526
  id,
@@ -2408,6 +3661,12 @@ function getProviderStringValue(config, key) {
2408
3661
  return typeof value === "string" ? value : void 0;
2409
3662
  }
2410
3663
  function getProviderChoiceValue(providerId, config, key) {
3664
+ if (providerId === "claude") {
3665
+ const defaults = config?.defaults;
3666
+ if (key === "claudeEffort") {
3667
+ return typeof defaults?.effort === "string" ? defaults.effort : "default";
3668
+ }
3669
+ }
2411
3670
  if (providerId === "codex") {
2412
3671
  const defaults = config?.defaults;
2413
3672
  if (key === "networkAccessEnabled" || key === "webSearchEnabled") {
@@ -2462,6 +3721,17 @@ function getProviderChoiceValue(providerId, config, key) {
2462
3721
  }
2463
3722
  throw new Error(`Unsupported choice setting '${key}' for '${providerId}'.`);
2464
3723
  }
3724
+ function getClaudeTextSettingValue(config, key) {
3725
+ if (key === "claudePathToExecutable") {
3726
+ return config?.pathToClaudeCodeExecutable;
3727
+ }
3728
+ const defaults = config?.defaults;
3729
+ if (!defaults) return void 0;
3730
+ if (key === "claudeMaxTurns") {
3731
+ return typeof defaults.maxTurns === "number" ? String(defaults.maxTurns) : void 0;
3732
+ }
3733
+ return defaults.model;
3734
+ }
2465
3735
  function getCodexTextSettingValue(config, key) {
2466
3736
  const defaults = config?.defaults;
2467
3737
  if (!defaults) return void 0;
@@ -2474,6 +3744,7 @@ function getGeminiTextSettingValue(config, key) {
2474
3744
  const defaults = config?.defaults;
2475
3745
  if (!defaults) return void 0;
2476
3746
  if (key === "geminiSearchModel") return defaults.searchModel;
3747
+ if (key === "geminiContentsModel") return defaults.contentsModel;
2477
3748
  if (key === "geminiAnswerModel") return defaults.answerModel;
2478
3749
  return defaults.researchAgent;
2479
3750
  }
@@ -2485,6 +3756,51 @@ function assignOptionalString(target, key, value) {
2485
3756
  target[key] = trimmed;
2486
3757
  }
2487
3758
  }
3759
+ function applyClaudeSettingChange(target, key, value) {
3760
+ target.defaults ??= {};
3761
+ switch (key) {
3762
+ case "model":
3763
+ assignOptionalString(
3764
+ target.defaults,
3765
+ "model",
3766
+ value
3767
+ );
3768
+ cleanupClaudeDefaults(target);
3769
+ return true;
3770
+ case "claudePathToExecutable":
3771
+ assignOptionalString(
3772
+ target,
3773
+ "pathToClaudeCodeExecutable",
3774
+ value
3775
+ );
3776
+ cleanupClaudeDefaults(target);
3777
+ return true;
3778
+ case "claudeMaxTurns": {
3779
+ const trimmed = value.trim();
3780
+ if (!trimmed) {
3781
+ delete target.defaults.maxTurns;
3782
+ } else {
3783
+ const parsed = Number(trimmed);
3784
+ if (!Number.isInteger(parsed) || parsed < 1) {
3785
+ throw new Error("Claude max turns must be a positive integer.");
3786
+ }
3787
+ target.defaults.maxTurns = parsed;
3788
+ }
3789
+ cleanupClaudeDefaults(target);
3790
+ return true;
3791
+ }
3792
+ case "claudeEffort":
3793
+ if (value === "default") {
3794
+ delete target.defaults.effort;
3795
+ } else {
3796
+ target.defaults.effort = value;
3797
+ }
3798
+ cleanupClaudeDefaults(target);
3799
+ return true;
3800
+ default:
3801
+ return false;
3802
+ }
3803
+ }
2488
3804
  function applyCodexSettingChange(target, key, value) {
2489
3805
  target.defaults ??= {};
2490
3806
  switch (key) {
@@ -2600,6 +3916,14 @@ function applyGeminiSettingChange(target, key, value) {
2600
3916
  );
2601
3917
  cleanupGeminiDefaults(target);
2602
3918
  return true;
3919
+ case "geminiContentsModel":
3920
+ assignOptionalString(
3921
+ target.defaults,
3922
+ "contentsModel",
3923
+ value
3924
+ );
3925
+ cleanupGeminiDefaults(target);
3926
+ return true;
2603
3927
  case "geminiAnswerModel":
2604
3928
  assignOptionalString(
2605
3929
  target.defaults,
@@ -2653,6 +3977,11 @@ function applyParallelSettingChange(target, key, value) {
2653
3977
  return false;
2654
3978
  }
2655
3979
  }
3980
+ function cleanupClaudeDefaults(target) {
3981
+ if (target.defaults && Object.keys(target.defaults).length === 0) {
3982
+ delete target.defaults;
3983
+ }
3984
+ }
2656
3985
  function cleanupCodexDefaults(target) {
2657
3986
  if (target.defaults && Object.keys(target.defaults).length === 0) {
2658
3987
  delete target.defaults;
@@ -2699,9 +4028,9 @@ function renderCallHeader(params, theme) {
2699
4028
  },
2700
4029
  render(width) {
2701
4030
  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)}"`)} `;
4031
+ const query2 = cleanSingleLine(String(params.query ?? "")).trim();
4032
+ if (query2.length > 0) {
4033
+ header += ` ${theme.fg("accent", formatQuotedPreview(query2))} `;
2705
4034
  }
2706
4035
  const lines = [];
2707
4036
  const headerLine = truncateToWidth(header.trimEnd(), width);
@@ -2758,6 +4087,18 @@ function getExpandHint() {
2758
4087
  function cleanSingleLine(text) {
2759
4088
  return text.replace(/\s+/g, " ").trim();
2760
4089
  }
4090
+ function formatQuotedPreview(text, maxLength = 80) {
4091
+ return `"${truncateInline(cleanSingleLine(text), maxLength)}"`;
4092
+ }
4093
+ function formatElapsed2(ms) {
4094
+ const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
4095
+ const minutes = Math.floor(totalSeconds / 60);
4096
+ const seconds = totalSeconds % 60;
4097
+ if (minutes > 0) {
4098
+ return `${minutes}m ${seconds}s`;
4099
+ }
4100
+ return `${totalSeconds}s`;
4101
+ }
2761
4102
  function formatSearchResponse(response) {
2762
4103
  if (response.results.length === 0) {
2763
4104
  return "No results found.";
@@ -2779,9 +4120,9 @@ async function truncateAndSave(text, prefix) {
2779
4120
  maxBytes: DEFAULT_MAX_BYTES
2780
4121
  });
2781
4122
  if (!truncation.truncated) return truncation.content;
2782
- const dir = join2(tmpdir(), `pi-web-providers-${prefix}-${Date.now()}`);
4123
+ const dir = join4(tmpdir(), `pi-web-providers-${prefix}-${Date.now()}`);
2783
4124
  await mkdir2(dir, { recursive: true });
2784
- const fullPath = join2(dir, "output.txt");
4125
+ const fullPath = join4(dir, "output.txt");
2785
4126
  await writeFile2(fullPath, text, "utf-8");
2786
4127
  return truncation.content + `
2787
4128
 
@@ -2792,7 +4133,11 @@ function truncateInline(text, maxLength) {
2792
4133
  return `${text.slice(0, maxLength - 1)}\u2026`;
2793
4134
  }
2794
4135
  var __test__ = {
4136
+ executeProviderTool,
2795
4137
  extractTextContent,
4138
+ getAvailableManagedToolNames,
4139
+ getAvailableProviderIdsForCapability,
4140
+ getSyncedActiveTools,
2796
4141
  renderCallHeader,
2797
4142
  renderCollapsedSearchSummary
2798
4143
  };