pi-web-providers 0.1.0 → 0.2.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 +69 -40
  2. package/dist/index.js +1010 -147
  3. package/package.json +7 -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,30 +9,27 @@ 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
35
  gemini: ["search", "answer", "research"],
@@ -76,6 +76,7 @@ var LEGACY_TOOL_ALIASES = {
76
76
  };
77
77
  var CONFIG_FILE_NAME = "web-providers.json";
78
78
  var VERSION = 1;
79
+ var commandValueCache = /* @__PURE__ */ new Map();
79
80
  function getConfigPath() {
80
81
  return join(getAgentDir(), CONFIG_FILE_NAME);
81
82
  }
@@ -115,11 +116,26 @@ function serializeConfig(config) {
115
116
  function resolveConfigValue(reference) {
116
117
  if (!reference) return void 0;
117
118
  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;
119
+ const cached = commandValueCache.get(reference);
120
+ if (cached) {
121
+ if (cached.errorMessage) {
122
+ throw new Error(cached.errorMessage);
123
+ }
124
+ return cached.value;
125
+ }
126
+ try {
127
+ const output = execSync(reference.slice(1), {
128
+ encoding: "utf-8",
129
+ stdio: ["ignore", "pipe", "pipe"]
130
+ }).trim();
131
+ const value = output.length > 0 ? output : void 0;
132
+ commandValueCache.set(reference, { value });
133
+ return value;
134
+ } catch (error) {
135
+ const errorMessage = error.message;
136
+ commandValueCache.set(reference, { errorMessage });
137
+ throw error;
138
+ }
123
139
  }
124
140
  const envValue = process.env[reference];
125
141
  if (envValue !== void 0) {
@@ -158,6 +174,12 @@ function normalizeConfig(raw, source) {
158
174
  throw new Error(`'providers' in ${source} must be a JSON object.`);
159
175
  }
160
176
  config.providers = {};
177
+ if (raw.providers.claude !== void 0) {
178
+ config.providers.claude = normalizeClaudeProvider(
179
+ raw.providers.claude,
180
+ source
181
+ );
182
+ }
161
183
  if (raw.providers.codex !== void 0) {
162
184
  config.providers.codex = normalizeCodexProvider(
163
185
  raw.providers.codex,
@@ -186,7 +208,7 @@ function normalizeConfig(raw, source) {
186
208
  );
187
209
  }
188
210
  const unknownProviders = Object.keys(raw.providers).filter(
189
- (key) => key !== "codex" && key !== "exa" && key !== "gemini" && key !== "parallel" && key !== "valyu"
211
+ (key) => key !== "claude" && key !== "codex" && key !== "exa" && key !== "gemini" && key !== "parallel" && key !== "valyu"
190
212
  );
191
213
  if (unknownProviders.length > 0) {
192
214
  throw new Error(
@@ -196,6 +218,50 @@ function normalizeConfig(raw, source) {
196
218
  }
197
219
  return config;
198
220
  }
221
+ function normalizeClaudeProvider(raw, source) {
222
+ const provider = parseProviderObject(raw, source, "claude");
223
+ const defaults = parseOptionalJsonObject(
224
+ provider.defaults,
225
+ source,
226
+ "providers.claude.defaults"
227
+ );
228
+ return {
229
+ enabled: parseOptionalBoolean(
230
+ provider.enabled,
231
+ source,
232
+ "providers.claude.enabled"
233
+ ),
234
+ tools: parseOptionalProviderTools(
235
+ "claude",
236
+ provider.tools,
237
+ source,
238
+ "providers.claude.tools"
239
+ ),
240
+ pathToClaudeCodeExecutable: parseOptionalString(
241
+ provider.pathToClaudeCodeExecutable,
242
+ source,
243
+ "providers.claude.pathToClaudeCodeExecutable"
244
+ ),
245
+ defaults: defaults === void 0 ? void 0 : {
246
+ model: parseOptionalString(
247
+ defaults.model,
248
+ source,
249
+ "providers.claude.defaults.model"
250
+ ),
251
+ effort: parseOptionalLiteral(
252
+ defaults.effort,
253
+ source,
254
+ "providers.claude.defaults.effort",
255
+ ["low", "medium", "high", "max"]
256
+ ),
257
+ maxTurns: parseOptionalInteger(
258
+ defaults.maxTurns,
259
+ source,
260
+ "providers.claude.defaults.maxTurns"
261
+ )
262
+ }
263
+ };
264
+ }
199
265
  function normalizeCodexProvider(raw, source) {
200
266
  const provider = parseProviderObject(raw, source, "codex");
201
267
  const defaults = parseOptionalJsonObject(
@@ -450,18 +516,14 @@ function parseOptionalProviderTools(providerId, value, source, field) {
450
516
  continue;
451
517
  }
452
518
  if (!supportsProviderTool(providerId, normalizedKey)) {
453
- throw new Error(
454
- `Unknown tools for ${providerId} in ${source}: ${key}.`
455
- );
519
+ throw new Error(`Unknown tools for ${providerId} in ${source}: ${key}.`);
456
520
  }
457
521
  parsed[normalizedKey] = parseBoolean(entry, source, `${field}.${key}`);
458
522
  }
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
- );
523
+ const unknownTools = Object.keys(value).filter((toolId) => {
524
+ const normalizedKey = normalizeProviderToolKey(providerId, toolId);
525
+ return normalizedKey !== null && !PROVIDER_TOOLS[providerId].includes(normalizedKey);
526
+ });
465
527
  if (unknownTools.length > 0) {
466
528
  throw new Error(
467
529
  `Unknown tools for ${providerId} in ${source}: ${unknownTools.join(", ")}.`
@@ -522,6 +584,13 @@ function parseString(value, source, field) {
522
584
  }
523
585
  return value;
524
586
  }
587
+ function parseOptionalInteger(value, source, field) {
588
+ if (value === void 0) return void 0;
589
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
590
+ throw new Error(`'${field}' in ${source} must be a positive integer.`);
591
+ }
592
+ return value;
593
+ }
525
594
  function parseOptionalLiteral(value, source, field, allowed) {
526
595
  if (value === void 0) return void 0;
527
596
  if (typeof value !== "string" || !allowed.includes(value)) {
@@ -535,8 +604,14 @@ function isPlainObject(value) {
535
604
  return typeof value === "object" && value !== null && !Array.isArray(value);
536
605
  }
537
606
 
538
- // src/providers/codex.ts
539
- import { Codex } from "@openai/codex-sdk";
607
+ // src/providers/claude.ts
608
+ import { execFileSync } from "node:child_process";
609
+ import { existsSync } from "node:fs";
610
+ import { createRequire } from "node:module";
611
+ import { dirname as dirname2, extname, join as join2 } from "node:path";
612
+ import {
613
+ query
614
+ } from "@anthropic-ai/claude-agent-sdk";
540
615
 
541
616
  // src/providers/shared.ts
542
617
  function trimSnippet(input, maxLength = 300) {
@@ -551,7 +626,362 @@ function formatJson(value) {
551
626
  return JSON.stringify(value, null, 2);
552
627
  }
553
628
 
629
+ // src/providers/claude.ts
630
+ var require2 = createRequire(import.meta.url);
631
+ var SEARCH_OUTPUT_SCHEMA = {
632
+ type: "object",
633
+ additionalProperties: false,
634
+ properties: {
635
+ sources: {
636
+ type: "array",
637
+ items: {
638
+ type: "object",
639
+ additionalProperties: false,
640
+ properties: {
641
+ title: { type: "string" },
642
+ url: { type: "string" },
643
+ snippet: { type: "string" }
644
+ },
645
+ required: ["title", "url", "snippet"]
646
+ }
647
+ }
648
+ },
649
+ required: ["sources"]
650
+ };
651
+ var ANSWER_OUTPUT_SCHEMA = {
652
+ type: "object",
653
+ additionalProperties: false,
654
+ properties: {
655
+ answer: { type: "string" },
656
+ sources: {
657
+ type: "array",
658
+ items: {
659
+ type: "object",
660
+ additionalProperties: false,
661
+ properties: {
662
+ title: { type: "string" },
663
+ url: { type: "string" }
664
+ },
665
+ required: ["title", "url"]
666
+ }
667
+ }
668
+ },
669
+ required: ["answer", "sources"]
670
+ };
671
+ var ClaudeProvider = class {
672
+ id = "claude";
673
+ label = "Claude";
674
+ docsUrl = "https://github.com/anthropics/claude-agent-sdk-typescript";
675
+ createTemplate() {
676
+ return {
677
+ enabled: false,
678
+ tools: {
679
+ search: true,
680
+ answer: true
681
+ }
682
+ };
683
+ }
684
+ getStatus(config, _cwd) {
685
+ if (!config) {
686
+ return { available: false, summary: "not configured" };
687
+ }
688
+ if (config.enabled === false) {
689
+ return { available: false, summary: "disabled" };
690
+ }
691
+ const executablePath = resolveClaudeExecutablePath(config);
692
+ if (executablePath && !existsSync(executablePath)) {
693
+ return { available: false, summary: "missing Claude Code executable" };
694
+ }
695
+ const authStatus = getClaudeAuthStatus(executablePath);
696
+ if (!authStatus.loggedIn) {
697
+ return { available: false, summary: "missing Claude auth" };
698
+ }
699
+ return { available: true, summary: "enabled" };
700
+ }
701
+ async search(queryText, maxResults, config, context) {
702
+ const output = parseClaudeSearchOutput(
703
+ await this.runStructuredQuery({
704
+ prompt: [
705
+ "You are performing web research for another coding agent.",
706
+ "Use the WebSearch tool to search the public web.",
707
+ "Return only a JSON object matching the provided schema.",
708
+ "Do not include markdown fences or extra commentary.",
709
+ `Return at most ${maxResults} sources.`,
710
+ "Each snippet should be short, factual, and specific to the result.",
711
+ "Prefer primary or official sources when they are available.",
712
+ "",
713
+ `User query: ${queryText}`
714
+ ].join("\n"),
715
+ schema: SEARCH_OUTPUT_SCHEMA,
716
+ tools: ["WebSearch"],
717
+ config,
718
+ context
719
+ })
720
+ );
721
+ return {
722
+ provider: this.id,
723
+ results: output.sources.slice(0, maxResults).map((source) => ({
724
+ title: source.title.trim(),
725
+ url: source.url.trim(),
726
+ snippet: trimSnippet(source.snippet)
727
+ }))
728
+ };
729
+ }
730
+ async answer(queryText, options, config, context) {
731
+ const output = parseClaudeAnswerOutput(
732
+ await this.runStructuredQuery({
733
+ prompt: [
734
+ "Answer the user's question using current public web information.",
735
+ "Use WebSearch to find relevant sources and WebFetch when you need to verify important details.",
736
+ "Return only a JSON object matching the provided schema.",
737
+ "Do not include markdown fences or extra commentary.",
738
+ "Keep the answer concise but informative.",
739
+ "Only cite sources you actually used.",
740
+ "",
741
+ `User query: ${queryText}`,
742
+ options ? `Additional options: ${JSON.stringify(options)}` : ""
743
+ ].filter(Boolean).join("\n"),
744
+ schema: ANSWER_OUTPUT_SCHEMA,
745
+ tools: ["WebSearch", "WebFetch"],
746
+ config,
747
+ context
748
+ })
749
+ );
750
+ const lines = [];
751
+ lines.push(output.answer.trim() || "No answer returned.");
752
+ if (output.sources.length > 0) {
753
+ lines.push("");
754
+ lines.push("Sources:");
755
+ for (const [index, source] of output.sources.entries()) {
756
+ lines.push(`${index + 1}. ${source.title}`);
757
+ lines.push(` ${source.url}`);
758
+ }
759
+ }
760
+ return {
761
+ provider: this.id,
762
+ text: lines.join("\n").trimEnd(),
763
+ summary: `Answer via Claude with ${output.sources.length} source(s)`,
764
+ itemCount: output.sources.length
765
+ };
766
+ }
767
+ async runStructuredQuery({
768
+ prompt,
769
+ schema,
770
+ tools,
771
+ config,
772
+ context
773
+ }) {
774
+ const abortController = new AbortController();
775
+ if (context.signal?.aborted) {
776
+ abortController.abort(context.signal.reason);
777
+ }
778
+ const onAbort = () => {
779
+ abortController.abort(context.signal?.reason);
780
+ };
781
+ context.signal?.addEventListener("abort", onAbort, { once: true });
782
+ const stream = query({
783
+ prompt,
784
+ options: {
785
+ abortController,
786
+ allowedTools: tools,
787
+ cwd: context.cwd,
788
+ effort: config.defaults?.effort,
789
+ maxTurns: config.defaults?.maxTurns,
790
+ model: config.defaults?.model,
791
+ outputFormat: {
792
+ type: "json_schema",
793
+ schema
794
+ },
795
+ pathToClaudeCodeExecutable: config.pathToClaudeCodeExecutable,
796
+ persistSession: false,
797
+ permissionMode: "dontAsk",
798
+ systemPrompt: {
799
+ type: "preset",
800
+ preset: "claude_code",
801
+ append: "Use only the provided web tools. Always produce output that matches the requested JSON schema exactly."
802
+ },
803
+ tools
804
+ }
805
+ });
806
+ const seenToolUseIds = /* @__PURE__ */ new Set();
807
+ let finalResult;
808
+ try {
809
+ for await (const message of stream) {
810
+ handleProgressMessage(message, seenToolUseIds, context.onProgress);
811
+ if (message.type === "result") {
812
+ finalResult = message;
813
+ }
814
+ }
815
+ } finally {
816
+ context.signal?.removeEventListener("abort", onAbort);
817
+ stream.close();
818
+ }
819
+ if (!finalResult) {
820
+ throw new Error("Claude returned no result.");
821
+ }
822
+ if (finalResult.subtype !== "success") {
823
+ throw new Error(
824
+ finalResult.errors.join("\n") || `Claude query failed (${finalResult.subtype}).`
825
+ );
826
+ }
827
+ return parseStructuredOutput(finalResult);
828
+ }
829
+ };
830
+ var CLAUDE_AUTH_CACHE_TTL_MS = 5e3;
831
+ var defaultClaudeExecutablePath;
832
+ var claudeAuthStatusCache = /* @__PURE__ */ new Map();
833
+ function resolveClaudeExecutablePath(config) {
834
+ if (config.pathToClaudeCodeExecutable) {
835
+ return config.pathToClaudeCodeExecutable;
836
+ }
837
+ if (defaultClaudeExecutablePath !== void 0) {
838
+ return defaultClaudeExecutablePath;
839
+ }
840
+ try {
841
+ const sdkEntryPath = require2.resolve("@anthropic-ai/claude-agent-sdk");
842
+ defaultClaudeExecutablePath = join2(dirname2(sdkEntryPath), "cli.js");
843
+ } catch {
844
+ defaultClaudeExecutablePath = void 0;
845
+ }
846
+ return defaultClaudeExecutablePath;
847
+ }
848
+ function getClaudeAuthStatus(executablePath) {
849
+ if (!executablePath) {
850
+ return { loggedIn: false };
851
+ }
852
+ const cachedStatus = claudeAuthStatusCache.get(executablePath);
853
+ if (cachedStatus && Date.now() - cachedStatus.checkedAt < CLAUDE_AUTH_CACHE_TTL_MS) {
854
+ return { loggedIn: cachedStatus.loggedIn };
855
+ }
856
+ const [command, ...args] = getClaudeAuthCommand(executablePath);
857
+ try {
858
+ const stdout = execFileSync(command, args, {
859
+ encoding: "utf8",
860
+ stdio: ["ignore", "pipe", "pipe"]
861
+ });
862
+ return cacheClaudeAuthStatus(executablePath, parseClaudeAuthStatus(stdout));
863
+ } catch (error) {
864
+ const stdout = getExecOutput(
865
+ error.stdout
866
+ );
867
+ if (stdout) {
868
+ return cacheClaudeAuthStatus(
869
+ executablePath,
870
+ parseClaudeAuthStatus(stdout)
871
+ );
872
+ }
873
+ return cacheClaudeAuthStatus(executablePath, { loggedIn: false });
874
+ }
875
+ }
876
+ function cacheClaudeAuthStatus(executablePath, status) {
877
+ claudeAuthStatusCache.set(executablePath, {
878
+ ...status,
879
+ checkedAt: Date.now()
880
+ });
881
+ return status;
882
+ }
883
+ function getClaudeAuthCommand(executablePath) {
884
+ const extension = extname(executablePath);
885
+ if (extension === ".js" || extension === ".cjs" || extension === ".mjs") {
886
+ return [process.execPath, executablePath, "auth", "status", "--json"];
887
+ }
888
+ return [executablePath, "auth", "status", "--json"];
889
+ }
890
+ function getExecOutput(output) {
891
+ if (typeof output === "string") {
892
+ return output;
893
+ }
894
+ if (Buffer.isBuffer(output)) {
895
+ return output.toString("utf8");
896
+ }
897
+ return "";
898
+ }
899
+ function parseClaudeAuthStatus(raw) {
900
+ try {
901
+ const parsed = JSON.parse(raw);
902
+ return { loggedIn: parsed.loggedIn === true };
903
+ } catch {
904
+ return { loggedIn: false };
905
+ }
906
+ }
907
+ function handleProgressMessage(message, seenToolUseIds, onProgress) {
908
+ if (!onProgress || message.type !== "tool_progress") {
909
+ return;
910
+ }
911
+ if (seenToolUseIds.has(message.tool_use_id)) {
912
+ return;
913
+ }
914
+ seenToolUseIds.add(message.tool_use_id);
915
+ onProgress(`Claude ${formatToolName(message.tool_name)}`);
916
+ }
917
+ function formatToolName(toolName) {
918
+ if (toolName === "WebSearch") return "web search";
919
+ if (toolName === "WebFetch") return "web fetch";
920
+ return toolName;
921
+ }
922
+ function parseStructuredOutput(result) {
923
+ if (result.subtype !== "success") {
924
+ throw new Error("Claude query did not succeed.");
925
+ }
926
+ if (result.structured_output !== void 0) {
927
+ return result.structured_output;
928
+ }
929
+ if (!result.result.trim()) {
930
+ throw new Error("Claude returned an empty response.");
931
+ }
932
+ try {
933
+ return JSON.parse(result.result);
934
+ } catch {
935
+ const match = result.result.match(/\{[\s\S]*\}/);
936
+ if (!match) {
937
+ throw new Error("Claude returned invalid JSON output.");
938
+ }
939
+ return JSON.parse(match[0]);
940
+ }
941
+ }
942
+ function parseClaudeSearchOutput(value) {
943
+ const sources = readArray(value, "sources").map((entry) => ({
944
+ title: readString(entry, "title"),
945
+ url: readString(entry, "url"),
946
+ snippet: readString(entry, "snippet")
947
+ }));
948
+ return { sources };
949
+ }
950
+ function parseClaudeAnswerOutput(value) {
951
+ return {
952
+ answer: readString(value, "answer"),
953
+ sources: readArray(value, "sources").map((entry) => ({
954
+ title: readString(entry, "title"),
955
+ url: readString(entry, "url")
956
+ }))
957
+ };
958
+ }
959
+ function readArray(value, key) {
960
+ if (typeof value !== "object" || value === null || !(key in value)) {
961
+ throw new Error(`Claude output is missing '${key}'.`);
962
+ }
963
+ const entry = value[key];
964
+ if (!Array.isArray(entry)) {
965
+ throw new Error(`Claude output field '${key}' must be an array.`);
966
+ }
967
+ return entry;
968
+ }
969
+ function readString(value, key) {
970
+ if (typeof value !== "object" || value === null || !(key in value)) {
971
+ throw new Error(`Claude output is missing '${key}'.`);
972
+ }
973
+ const entry = value[key];
974
+ if (typeof entry !== "string") {
975
+ throw new Error(`Claude output field '${key}' must be a string.`);
976
+ }
977
+ return entry;
978
+ }
979
+
554
980
  // src/providers/codex.ts
981
+ import { existsSync as existsSync2 } from "node:fs";
982
+ import { homedir } from "node:os";
983
+ import { join as join3 } from "node:path";
984
+ import { Codex } from "@openai/codex-sdk";
555
985
  var OUTPUT_SCHEMA = {
556
986
  type: "object",
557
987
  additionalProperties: false,
@@ -596,9 +1026,23 @@ var CodexProvider = class {
596
1026
  if (config.enabled === false) {
597
1027
  return { available: false, summary: "disabled" };
598
1028
  }
1029
+ try {
1030
+ new Codex({
1031
+ codexPathOverride: config.codexPath,
1032
+ config: config.config
1033
+ });
1034
+ } catch (error) {
1035
+ return {
1036
+ available: false,
1037
+ summary: error.message
1038
+ };
1039
+ }
1040
+ if (!hasCodexCredentials(config)) {
1041
+ return { available: false, summary: "missing Codex auth" };
1042
+ }
599
1043
  return { available: true, summary: "enabled" };
600
1044
  }
601
- async search(query, maxResults, config, context) {
1045
+ async search(query2, maxResults, config, context) {
602
1046
  const codex = new Codex({
603
1047
  codexPathOverride: config.codexPath,
604
1048
  baseUrl: config.baseUrl,
@@ -626,7 +1070,7 @@ var CodexProvider = class {
626
1070
  "Prefer primary or official sources when they are available.",
627
1071
  "Each snippet should be short and specific.",
628
1072
  "",
629
- `User query: ${query}`
1073
+ `User query: ${query2}`
630
1074
  ].join("\n");
631
1075
  const streamed = await thread.runStreamed(prompt, {
632
1076
  outputSchema: OUTPUT_SCHEMA,
@@ -654,6 +1098,37 @@ var CodexProvider = class {
654
1098
  };
655
1099
  }
656
1100
  };
1101
+ function hasCodexCredentials(config) {
1102
+ if (hasConfiguredReference(config.apiKey)) {
1103
+ return true;
1104
+ }
1105
+ if (hasConfiguredReference(config.env?.CODEX_API_KEY) || hasConfiguredReference(config.env?.OPENAI_API_KEY)) {
1106
+ return true;
1107
+ }
1108
+ if (!config.env) {
1109
+ const inheritedKey = process.env.CODEX_API_KEY ?? process.env.OPENAI_API_KEY;
1110
+ if (typeof inheritedKey === "string" && inheritedKey.trim().length > 0) {
1111
+ return true;
1112
+ }
1113
+ }
1114
+ return existsSync2(join3(homedir(), ".codex", "auth.json"));
1115
+ }
1116
+ function hasConfiguredReference(reference) {
1117
+ if (!reference) {
1118
+ return false;
1119
+ }
1120
+ if (reference.startsWith("!")) {
1121
+ return reference.slice(1).trim().length > 0;
1122
+ }
1123
+ const envValue = process.env[reference];
1124
+ if (typeof envValue === "string") {
1125
+ return envValue.trim().length > 0;
1126
+ }
1127
+ if (/^[A-Z][A-Z0-9_]*$/.test(reference)) {
1128
+ return false;
1129
+ }
1130
+ return reference.trim().length > 0;
1131
+ }
657
1132
  function handleProgressEvent(event, seenQueries, onProgress) {
658
1133
  if (!onProgress) return;
659
1134
  if (event.type === "item.completed" && event.item.type === "web_search" && !seenQueries.has(event.item.query)) {
@@ -713,7 +1188,7 @@ var ExaProvider = class {
713
1188
  }
714
1189
  return { available: true, summary: "enabled" };
715
1190
  }
716
- async search(query, maxResults, config, context) {
1191
+ async search(query2, maxResults, config, context) {
717
1192
  const apiKey = resolveConfigValue(config.apiKey);
718
1193
  if (!apiKey) {
719
1194
  throw new Error("Exa is missing an API key.");
@@ -723,8 +1198,8 @@ var ExaProvider = class {
723
1198
  ...asJsonObject(config.defaults),
724
1199
  numResults: maxResults
725
1200
  };
726
- context.onProgress?.(`Searching Exa for: ${query}`);
727
- const response = await client.search(query, options);
1201
+ context.onProgress?.(`Searching Exa for: ${query2}`);
1202
+ const response = await client.search(query2, options);
728
1203
  return {
729
1204
  provider: this.id,
730
1205
  results: (response.results ?? []).slice(0, maxResults).map((result) => ({
@@ -743,11 +1218,15 @@ var ExaProvider = class {
743
1218
  throw new Error("Exa is missing an API key.");
744
1219
  }
745
1220
  const client = new Exa(apiKey, config.baseUrl);
746
- context.onProgress?.(`Fetching contents from Exa for ${urls.length} URL(s)`);
1221
+ context.onProgress?.(
1222
+ `Fetching contents from Exa for ${urls.length} URL(s)`
1223
+ );
747
1224
  const response = await client.getContents(urls, options);
748
1225
  const lines = [];
749
1226
  for (const [index, result] of (response.results ?? []).entries()) {
750
- lines.push(`${index + 1}. ${String(result.title ?? result.url ?? "Untitled")}`);
1227
+ lines.push(
1228
+ `${index + 1}. ${String(result.title ?? result.url ?? "Untitled")}`
1229
+ );
751
1230
  lines.push(` ${String(result.url ?? "")}`);
752
1231
  const summary = typeof result.summary === "string" ? result.summary : result.summary ? formatJson(result.summary) : void 0;
753
1232
  const text = typeof result.text === "string" ? result.text : Array.isArray(result.highlights) ? result.highlights.join(" ") : "";
@@ -764,14 +1243,14 @@ var ExaProvider = class {
764
1243
  itemCount: response.results?.length ?? 0
765
1244
  };
766
1245
  }
767
- async answer(query, options, config, context) {
1246
+ async answer(query2, options, config, context) {
768
1247
  const apiKey = resolveConfigValue(config.apiKey);
769
1248
  if (!apiKey) {
770
1249
  throw new Error("Exa is missing an API key.");
771
1250
  }
772
1251
  const client = new Exa(apiKey, config.baseUrl);
773
- context.onProgress?.(`Getting Exa answer for: ${query}`);
774
- const response = await client.answer(query, options);
1252
+ context.onProgress?.(`Getting Exa answer for: ${query2}`);
1253
+ const response = await client.answer(query2, options);
775
1254
  const lines = [];
776
1255
  lines.push(
777
1256
  typeof response.answer === "string" ? response.answer : formatJson(response.answer)
@@ -781,7 +1260,9 @@ var ExaProvider = class {
781
1260
  lines.push("");
782
1261
  lines.push("Sources:");
783
1262
  for (const [index, citation] of citations.entries()) {
784
- lines.push(`${index + 1}. ${String(citation.title ?? citation.url ?? "Untitled")}`);
1263
+ lines.push(
1264
+ `${index + 1}. ${String(citation.title ?? citation.url ?? "Untitled")}`
1265
+ );
785
1266
  lines.push(` ${String(citation.url ?? "")}`);
786
1267
  }
787
1268
  }
@@ -859,35 +1340,33 @@ var GeminiProvider = class {
859
1340
  }
860
1341
  return { available: true, summary: "enabled" };
861
1342
  }
862
- async search(query, maxResults, config, context) {
1343
+ async search(query2, maxResults, config, context) {
863
1344
  const ai = this.createClient(config);
864
1345
  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
- }));
1346
+ context.onProgress?.(`Searching Gemini for: ${query2}`);
1347
+ const interaction = await createSearchInteraction(ai, model, query2);
1348
+ const results = await Promise.all(
1349
+ extractGoogleSearchResults(interaction.outputs).slice(0, maxResults).map(async (result) => {
1350
+ const resolvedUrl = await resolveGoogleSearchUrl(result.url);
1351
+ return {
1352
+ title: result.title ?? resolvedUrl ?? result.url ?? "Untitled",
1353
+ url: resolvedUrl ?? result.url ?? "",
1354
+ snippet: ""
1355
+ };
1356
+ })
1357
+ );
879
1358
  return {
880
1359
  provider: this.id,
881
1360
  results
882
1361
  };
883
1362
  }
884
- async answer(query, options, config, context) {
1363
+ async answer(query2, options, config, context) {
885
1364
  const ai = this.createClient(config);
886
1365
  const model = config.defaults?.answerModel ?? DEFAULT_ANSWER_MODEL;
887
- context.onProgress?.(`Getting Gemini answer for: ${query}`);
1366
+ context.onProgress?.(`Getting Gemini answer for: ${query2}`);
888
1367
  const response = await ai.models.generateContent({
889
1368
  model,
890
- contents: query,
1369
+ contents: query2,
891
1370
  config: {
892
1371
  ...options ?? {},
893
1372
  tools: [{ googleSearch: {} }]
@@ -903,7 +1382,9 @@ var GeminiProvider = class {
903
1382
  lines.push("Sources:");
904
1383
  for (const [index, source] of sources.entries()) {
905
1384
  lines.push(`${index + 1}. ${source.title}`);
906
- lines.push(` ${source.url}`);
1385
+ if (source.url) {
1386
+ lines.push(` ${source.url}`);
1387
+ }
907
1388
  }
908
1389
  }
909
1390
  return {
@@ -918,6 +1399,8 @@ var GeminiProvider = class {
918
1399
  const agent = config.defaults?.researchAgent ?? DEFAULT_RESEARCH_AGENT;
919
1400
  const pollIntervalMs = getPollInterval(options);
920
1401
  const requestOptions = stripPollIntervalOption(options);
1402
+ const startedAt = Date.now();
1403
+ let lastStatus;
921
1404
  context.onProgress?.("Starting Gemini deep research");
922
1405
  const initialInteraction = await ai.interactions.create({
923
1406
  ...requestOptions,
@@ -931,7 +1414,13 @@ var GeminiProvider = class {
931
1414
  throw new Error("Gemini research aborted.");
932
1415
  }
933
1416
  const interaction = await ai.interactions.get(initialInteraction.id);
934
- context.onProgress?.(`Gemini research status: ${interaction.status}`);
1417
+ const now = Date.now();
1418
+ if (interaction.status !== lastStatus) {
1419
+ context.onProgress?.(
1420
+ `Gemini research status: ${interaction.status} (${formatElapsed(now - startedAt)} elapsed)`
1421
+ );
1422
+ lastStatus = interaction.status;
1423
+ }
935
1424
  if (interaction.status === "completed") {
936
1425
  const text = formatInteractionOutputs(interaction.outputs);
937
1426
  return {
@@ -988,19 +1477,29 @@ function extractGoogleSearchResults(outputs) {
988
1477
  function extractGroundingSources(chunks) {
989
1478
  const seen = /* @__PURE__ */ new Set();
990
1479
  const sources = [];
1480
+ const maxSources = 5;
991
1481
  if (!Array.isArray(chunks)) {
992
1482
  return sources;
993
1483
  }
994
1484
  for (const chunk of chunks) {
995
1485
  const web = typeof chunk === "object" && chunk !== null && "web" in chunk && typeof chunk.web === "object" && chunk.web !== null ? chunk.web : void 0;
996
1486
  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);
1487
+ const rawUrl = typeof web.uri === "string" ? web.uri : "";
1488
+ const title = formatGroundingSourceTitle(
1489
+ typeof web.title === "string" ? web.title : rawUrl,
1490
+ rawUrl
1491
+ );
1492
+ const url = formatGroundingSourceUrl(rawUrl);
1493
+ const key = [title.toLowerCase(), url.toLowerCase()].join("::");
1494
+ if (seen.has(key)) continue;
1495
+ seen.add(key);
1000
1496
  sources.push({
1001
- title: typeof web.title === "string" ? web.title : url,
1497
+ title,
1002
1498
  url
1003
1499
  });
1500
+ if (sources.length >= maxSources) {
1501
+ break;
1502
+ }
1004
1503
  }
1005
1504
  return sources;
1006
1505
  }
@@ -1019,6 +1518,87 @@ function formatInteractionOutputs(outputs) {
1019
1518
  }
1020
1519
  return lines.join("\n\n").trim();
1021
1520
  }
1521
+ function formatGroundingSourceTitle(title, url) {
1522
+ const trimmedTitle = title?.trim();
1523
+ if (trimmedTitle) {
1524
+ return trimmedTitle;
1525
+ }
1526
+ if (url) {
1527
+ try {
1528
+ return new URL(url).hostname;
1529
+ } catch {
1530
+ return url;
1531
+ }
1532
+ }
1533
+ return "Untitled";
1534
+ }
1535
+ function formatGroundingSourceUrl(url) {
1536
+ if (!url) {
1537
+ return "";
1538
+ }
1539
+ if (isGoogleGroundingRedirect(url)) {
1540
+ return "";
1541
+ }
1542
+ return url;
1543
+ }
1544
+ function isGoogleGroundingRedirect(url) {
1545
+ try {
1546
+ const parsed = new URL(url);
1547
+ return parsed.hostname === "vertexaisearch.cloud.google.com" && parsed.pathname.startsWith("/grounding-api-redirect/");
1548
+ } catch {
1549
+ return false;
1550
+ }
1551
+ }
1552
+ async function createSearchInteraction(ai, model, query2) {
1553
+ const request = {
1554
+ model,
1555
+ input: query2,
1556
+ tools: [{ type: "google_search" }]
1557
+ };
1558
+ try {
1559
+ return await ai.interactions.create({
1560
+ ...request,
1561
+ generation_config: {
1562
+ tool_choice: "any"
1563
+ }
1564
+ });
1565
+ } catch (error) {
1566
+ if (!isBuiltInToolChoiceError(error)) {
1567
+ throw error;
1568
+ }
1569
+ return ai.interactions.create(request);
1570
+ }
1571
+ }
1572
+ function isBuiltInToolChoiceError(error) {
1573
+ if (error instanceof Error) {
1574
+ return error.message.includes(
1575
+ "Function calling config is set without function_declarations"
1576
+ );
1577
+ }
1578
+ if (typeof error === "object" && error !== null && "message" in error && typeof error.message === "string") {
1579
+ return error.message.includes(
1580
+ "Function calling config is set without function_declarations"
1581
+ );
1582
+ }
1583
+ return false;
1584
+ }
1585
+ async function resolveGoogleSearchUrl(url) {
1586
+ if (!url) {
1587
+ return void 0;
1588
+ }
1589
+ if (!isGoogleGroundingRedirect(url)) {
1590
+ return url;
1591
+ }
1592
+ try {
1593
+ const response = await fetch(url, {
1594
+ method: "HEAD",
1595
+ redirect: "manual"
1596
+ });
1597
+ return response.headers.get("location") || url;
1598
+ } catch {
1599
+ return url;
1600
+ }
1601
+ }
1022
1602
  async function sleep(ms, signal) {
1023
1603
  if (signal?.aborted) {
1024
1604
  throw new Error("Operation aborted.");
@@ -1036,6 +1616,15 @@ async function sleep(ms, signal) {
1036
1616
  signal?.addEventListener("abort", onAbort, { once: true });
1037
1617
  });
1038
1618
  }
1619
+ function formatElapsed(ms) {
1620
+ const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
1621
+ const minutes = Math.floor(totalSeconds / 60);
1622
+ const seconds = totalSeconds % 60;
1623
+ if (minutes > 0) {
1624
+ return `${minutes}m ${seconds}s`;
1625
+ }
1626
+ return `${totalSeconds}s`;
1627
+ }
1039
1628
  function getPollInterval(options) {
1040
1629
  const raw = options?.pollIntervalMs;
1041
1630
  if (typeof raw === "number" && Number.isFinite(raw) && raw >= 1e3) {
@@ -1089,13 +1678,13 @@ var ParallelProvider = class {
1089
1678
  }
1090
1679
  return { available: true, summary: "enabled" };
1091
1680
  }
1092
- async search(query, maxResults, config, context) {
1681
+ async search(query2, maxResults, config, context) {
1093
1682
  const client = this.createClient(config);
1094
1683
  const defaults = asJsonObject(config.defaults?.search);
1095
- context.onProgress?.(`Searching Parallel for: ${query}`);
1684
+ context.onProgress?.(`Searching Parallel for: ${query2}`);
1096
1685
  const response = await client.beta.search({
1097
1686
  ...defaults,
1098
- objective: query,
1687
+ objective: query2,
1099
1688
  max_results: maxResults
1100
1689
  });
1101
1690
  return {
@@ -1192,7 +1781,7 @@ var ValyuProvider = class {
1192
1781
  }
1193
1782
  return { available: true, summary: "enabled" };
1194
1783
  }
1195
- async search(query, maxResults, config, context) {
1784
+ async search(query2, maxResults, config, context) {
1196
1785
  const apiKey = resolveConfigValue(config.apiKey);
1197
1786
  if (!apiKey) {
1198
1787
  throw new Error("Valyu is missing an API key.");
@@ -1202,8 +1791,8 @@ var ValyuProvider = class {
1202
1791
  ...asJsonObject(config.defaults),
1203
1792
  maxNumResults: maxResults
1204
1793
  };
1205
- context.onProgress?.(`Searching Valyu for: ${query}`);
1206
- const response = await client.search(query, options);
1794
+ context.onProgress?.(`Searching Valyu for: ${query2}`);
1795
+ const response = await client.search(query2, options);
1207
1796
  if (!response.success) {
1208
1797
  throw new Error(response.error || "Valyu search failed.");
1209
1798
  }
@@ -1225,7 +1814,9 @@ var ValyuProvider = class {
1225
1814
  throw new Error("Valyu is missing an API key.");
1226
1815
  }
1227
1816
  const client = new Valyu(apiKey, config.baseUrl);
1228
- context.onProgress?.(`Fetching contents from Valyu for ${urls.length} URL(s)`);
1817
+ context.onProgress?.(
1818
+ `Fetching contents from Valyu for ${urls.length} URL(s)`
1819
+ );
1229
1820
  const response = await client.contents(urls, options);
1230
1821
  const finalResponse = "jobId" in response ? await client.waitForJob(response.jobId, {
1231
1822
  onProgress: (status) => context.onProgress?.(
@@ -1257,14 +1848,14 @@ var ValyuProvider = class {
1257
1848
  itemCount: results.length
1258
1849
  };
1259
1850
  }
1260
- async answer(query, options, config, context) {
1851
+ async answer(query2, options, config, context) {
1261
1852
  const apiKey = resolveConfigValue(config.apiKey);
1262
1853
  if (!apiKey) {
1263
1854
  throw new Error("Valyu is missing an API key.");
1264
1855
  }
1265
1856
  const client = new Valyu(apiKey, config.baseUrl);
1266
- context.onProgress?.(`Getting Valyu answer for: ${query}`);
1267
- const response = await client.answer(query, {
1857
+ context.onProgress?.(`Getting Valyu answer for: ${query2}`);
1858
+ const response = await client.answer(query2, {
1268
1859
  ...options ?? {},
1269
1860
  streaming: false
1270
1861
  });
@@ -1343,6 +1934,7 @@ var ValyuProvider = class {
1343
1934
 
1344
1935
  // src/providers/index.ts
1345
1936
  var PROVIDERS = [
1937
+ new ClaudeProvider(),
1346
1938
  new CodexProvider(),
1347
1939
  new ExaProvider(),
1348
1940
  new GeminiProvider(),
@@ -1350,38 +1942,47 @@ var PROVIDERS = [
1350
1942
  new ValyuProvider()
1351
1943
  ];
1352
1944
  var PROVIDER_MAP = {
1353
- codex: PROVIDERS[0],
1354
- exa: PROVIDERS[1],
1355
- gemini: PROVIDERS[2],
1356
- parallel: PROVIDERS[3],
1357
- valyu: PROVIDERS[4]
1945
+ claude: PROVIDERS[0],
1946
+ codex: PROVIDERS[1],
1947
+ exa: PROVIDERS[2],
1948
+ gemini: PROVIDERS[3],
1949
+ parallel: PROVIDERS[4],
1950
+ valyu: PROVIDERS[5]
1358
1951
  };
1359
1952
 
1360
1953
  // src/provider-resolution.ts
1954
+ var IMPLICIT_PROVIDER_FALLBACKS = ["codex"];
1361
1955
  function resolveProviderChoice(config, explicit, cwd) {
1362
1956
  return resolveProviderForCapability(config, explicit, cwd, "search");
1363
1957
  }
1958
+ function getEffectiveProviderConfig(config, providerId) {
1959
+ const configured = config.providers?.[providerId];
1960
+ if (configured) {
1961
+ return configured;
1962
+ }
1963
+ if (IMPLICIT_PROVIDER_FALLBACKS.includes(providerId)) {
1964
+ return {
1965
+ ...PROVIDER_MAP[providerId].createTemplate(),
1966
+ enabled: true
1967
+ };
1968
+ }
1969
+ return void 0;
1970
+ }
1364
1971
  function resolveProviderForCapability(config, explicit, cwd, capability) {
1365
1972
  if (explicit) {
1366
1973
  const provider = PROVIDER_MAP[explicit];
1974
+ const providerConfig = getEffectiveProviderConfig(config, explicit);
1367
1975
  if (typeof provider[capability] !== "function") {
1368
1976
  throw new Error(
1369
1977
  `Provider '${explicit}' does not support '${capability}'.`
1370
1978
  );
1371
1979
  }
1372
- if (!isProviderToolEnabled(
1373
- explicit,
1374
- config.providers?.[explicit],
1375
- capability
1376
- )) {
1980
+ if (!isProviderToolEnabled(explicit, providerConfig, capability)) {
1377
1981
  throw new Error(
1378
1982
  `Provider '${explicit}' has '${capability}' disabled in config.`
1379
1983
  );
1380
1984
  }
1381
- const status = provider.getStatus(
1382
- config.providers?.[explicit],
1383
- cwd
1384
- );
1985
+ const status = provider.getStatus(providerConfig, cwd);
1385
1986
  if (!status.available) {
1386
1987
  throw new Error(
1387
1988
  `Provider '${explicit}' is not available: ${status.summary}.`
@@ -1403,19 +2004,14 @@ function resolveProviderForCapability(config, explicit, cwd, capability) {
1403
2004
  const status = provider.getStatus(providerConfig, cwd);
1404
2005
  if (status.available) return provider;
1405
2006
  }
1406
- for (const provider of PROVIDERS) {
2007
+ for (const providerId of IMPLICIT_PROVIDER_FALLBACKS) {
2008
+ const provider = PROVIDER_MAP[providerId];
1407
2009
  if (typeof provider[capability] !== "function") continue;
1408
- if (!isProviderToolEnabled(
1409
- provider.id,
1410
- config.providers?.[provider.id],
1411
- capability
1412
- )) {
2010
+ const providerConfig = getEffectiveProviderConfig(config, provider.id);
2011
+ if (!isProviderToolEnabled(provider.id, providerConfig, capability)) {
1413
2012
  continue;
1414
2013
  }
1415
- const status = provider.getStatus(
1416
- config.providers?.[provider.id],
1417
- cwd
1418
- );
2014
+ const status = provider.getStatus(providerConfig, cwd);
1419
2015
  if (status.available) return provider;
1420
2016
  }
1421
2017
  throw new Error(
@@ -1424,16 +2020,31 @@ function resolveProviderForCapability(config, explicit, cwd, capability) {
1424
2020
  }
1425
2021
 
1426
2022
  // src/types.ts
1427
- var PROVIDER_IDS = ["codex", "exa", "gemini", "parallel", "valyu"];
2023
+ var PROVIDER_IDS = [
2024
+ "claude",
2025
+ "codex",
2026
+ "exa",
2027
+ "gemini",
2028
+ "parallel",
2029
+ "valyu"
2030
+ ];
1428
2031
 
1429
2032
  // src/index.ts
1430
2033
  var DEFAULT_MAX_RESULTS = 5;
1431
2034
  var MAX_ALLOWED_RESULTS = 20;
2035
+ var RESEARCH_HEARTBEAT_MS = 15e3;
2036
+ var CAPABILITY_TOOL_NAMES = {
2037
+ search: "web_search",
2038
+ contents: "web_contents",
2039
+ answer: "web_answer",
2040
+ research: "web_research"
2041
+ };
2042
+ var MANAGED_TOOL_NAMES = Object.values(CAPABILITY_TOOL_NAMES);
2043
+ var PROVIDER_OVERRIDE_GUIDELINES = [
2044
+ "Do not set provider unless the user asks for one."
2045
+ ];
1432
2046
  function webProvidersExtension(pi) {
1433
- registerWebSearchTool(pi);
1434
- registerWebContentsTool(pi);
1435
- registerWebAnswerTool(pi);
1436
- registerWebResearchTool(pi);
2047
+ registerManagedTools(pi);
1437
2048
  pi.registerCommand("web-providers", {
1438
2049
  description: "Configure web search providers",
1439
2050
  handler: async (_args, ctx) => {
@@ -1441,15 +2052,38 @@ function webProvidersExtension(pi) {
1441
2052
  ctx.ui.notify("web-providers requires interactive mode", "error");
1442
2053
  return;
1443
2054
  }
1444
- await runWebProvidersConfig(ctx);
2055
+ await runWebProvidersConfig(pi, ctx);
1445
2056
  }
1446
2057
  });
2058
+ pi.on("session_start", async (_event, ctx) => {
2059
+ await refreshManagedTools(pi, ctx.cwd, { addAvailable: true });
2060
+ });
2061
+ pi.on("before_agent_start", async (_event, ctx) => {
2062
+ await refreshManagedTools(pi, ctx.cwd, { addAvailable: false });
2063
+ });
1447
2064
  }
1448
- function registerWebSearchTool(pi) {
2065
+ function registerManagedTools(pi, providerIdsByCapability = {}) {
2066
+ registerWebSearchTool(pi, providerIdsByCapability.search ?? PROVIDER_IDS);
2067
+ registerWebContentsTool(
2068
+ pi,
2069
+ providerIdsByCapability.contents ?? getProviderIdsForCapability("contents")
2070
+ );
2071
+ registerWebAnswerTool(
2072
+ pi,
2073
+ providerIdsByCapability.answer ?? getProviderIdsForCapability("answer")
2074
+ );
2075
+ registerWebResearchTool(
2076
+ pi,
2077
+ providerIdsByCapability.research ?? getProviderIdsForCapability("research")
2078
+ );
2079
+ }
2080
+ function registerWebSearchTool(pi, providerIds) {
2081
+ const visibleProviderIds = providerIds.length > 0 ? providerIds : PROVIDER_IDS;
1449
2082
  pi.registerTool({
1450
2083
  name: "web_search",
1451
2084
  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.`,
2085
+ 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.`,
2086
+ promptGuidelines: PROVIDER_OVERRIDE_GUIDELINES,
1453
2087
  parameters: Type.Object({
1454
2088
  query: Type.String({ description: "What to search for on the web" }),
1455
2089
  maxResults: Type.Optional(
@@ -1459,17 +2093,16 @@ function registerWebSearchTool(pi) {
1459
2093
  description: `Maximum number of results to return (default: ${DEFAULT_MAX_RESULTS})`
1460
2094
  })
1461
2095
  ),
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
- })
2096
+ provider: providerEnum(
2097
+ visibleProviderIds,
2098
+ "Provider override. If omitted, uses the active configured provider or falls back to Codex for search when it is not explicitly disabled."
1466
2099
  )
1467
2100
  }),
1468
2101
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
1469
2102
  const config = await loadConfig();
1470
2103
  const provider = resolveProviderChoice(config, params.provider, ctx.cwd);
1471
2104
  const maxResults = clampResults(params.maxResults);
1472
- const providerConfig = config.providers?.[provider.id];
2105
+ const providerConfig = getEffectiveProviderConfig(config, provider.id);
1473
2106
  if (!providerConfig) {
1474
2107
  throw new Error(`Provider '${provider.id}' is not configured.`);
1475
2108
  }
@@ -1524,13 +2157,12 @@ function registerWebSearchTool(pi) {
1524
2157
  }
1525
2158
  });
1526
2159
  }
1527
- function registerWebContentsTool(pi) {
1528
- const providerIds = getProviderIdsForCapability("contents");
2160
+ function registerWebContentsTool(pi, providerIds) {
1529
2161
  if (providerIds.length === 0) return;
1530
2162
  pi.registerTool({
1531
2163
  name: "web_contents",
1532
2164
  label: "Web Contents",
1533
- description: "Fetch extracted contents for one or more URLs using a configured provider.",
2165
+ description: "Read and extract the main contents of one or more web pages.",
1534
2166
  parameters: Type.Object({
1535
2167
  urls: Type.Array(Type.String({ minLength: 1 }), {
1536
2168
  minItems: 1,
@@ -1542,6 +2174,7 @@ function registerWebContentsTool(pi) {
1542
2174
  "Provider override. If omitted, uses the active configured provider that supports web contents."
1543
2175
  )
1544
2176
  }),
2177
+ promptGuidelines: PROVIDER_OVERRIDE_GUIDELINES,
1545
2178
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
1546
2179
  return executeProviderTool({
1547
2180
  capability: "contents",
@@ -1579,13 +2212,12 @@ function registerWebContentsTool(pi) {
1579
2212
  }
1580
2213
  });
1581
2214
  }
1582
- function registerWebAnswerTool(pi) {
1583
- const providerIds = getProviderIdsForCapability("answer");
2215
+ function registerWebAnswerTool(pi, providerIds) {
1584
2216
  if (providerIds.length === 0) return;
1585
2217
  pi.registerTool({
1586
2218
  name: "web_answer",
1587
2219
  label: "Web Answer",
1588
- description: "Get a provider-generated answer grounded in web results.",
2220
+ description: "Answer a question using web-grounded evidence.",
1589
2221
  parameters: Type.Object({
1590
2222
  query: Type.String({ description: "Question to answer" }),
1591
2223
  options: jsonOptionsSchema("Provider-specific answer options."),
@@ -1594,6 +2226,7 @@ function registerWebAnswerTool(pi) {
1594
2226
  "Provider override. If omitted, uses the active configured provider that supports web answers."
1595
2227
  )
1596
2228
  }),
2229
+ promptGuidelines: PROVIDER_OVERRIDE_GUIDELINES,
1597
2230
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
1598
2231
  return executeProviderTool({
1599
2232
  capability: "answer",
@@ -1613,8 +2246,10 @@ function registerWebAnswerTool(pi) {
1613
2246
  renderCall(args, theme) {
1614
2247
  return renderToolCallHeader(
1615
2248
  "web_answer",
1616
- `"${cleanSingleLine(String(args.query ?? "")).slice(0, 80)}"`,
1617
- [`provider=${String(args.provider ?? "auto")}`],
2249
+ formatQuotedPreview(String(args.query ?? "")),
2250
+ [
2251
+ `provider=${String(args.provider ?? "auto")}`
2252
+ ],
1618
2253
  theme
1619
2254
  );
1620
2255
  },
@@ -1629,13 +2264,12 @@ function registerWebAnswerTool(pi) {
1629
2264
  }
1630
2265
  });
1631
2266
  }
1632
- function registerWebResearchTool(pi) {
1633
- const providerIds = getProviderIdsForCapability("research");
2267
+ function registerWebResearchTool(pi, providerIds) {
1634
2268
  if (providerIds.length === 0) return;
1635
2269
  pi.registerTool({
1636
2270
  name: "web_research",
1637
2271
  label: "Web Research",
1638
- description: "Run a longer-form research task using a provider that supports research.",
2272
+ description: "Investigate a topic across web sources and produce a longer report.",
1639
2273
  parameters: Type.Object({
1640
2274
  input: Type.String({ description: "Research brief or question" }),
1641
2275
  options: jsonOptionsSchema("Provider-specific research options."),
@@ -1644,6 +2278,7 @@ function registerWebResearchTool(pi) {
1644
2278
  "Provider override. If omitted, uses the active configured provider that supports research."
1645
2279
  )
1646
2280
  }),
2281
+ promptGuidelines: PROVIDER_OVERRIDE_GUIDELINES,
1647
2282
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
1648
2283
  return executeProviderTool({
1649
2284
  capability: "research",
@@ -1663,8 +2298,10 @@ function registerWebResearchTool(pi) {
1663
2298
  renderCall(args, theme) {
1664
2299
  return renderToolCallHeader(
1665
2300
  "web_research",
1666
- `"${cleanSingleLine(String(args.input ?? "")).slice(0, 80)}"`,
1667
- [`provider=${String(args.provider ?? "auto")}`],
2301
+ formatQuotedPreview(String(args.input ?? "")),
2302
+ [
2303
+ `provider=${String(args.provider ?? "auto")}`
2304
+ ],
1668
2305
  theme
1669
2306
  );
1670
2307
  },
@@ -1679,7 +2316,7 @@ function registerWebResearchTool(pi) {
1679
2316
  }
1680
2317
  });
1681
2318
  }
1682
- async function runWebProvidersConfig(ctx) {
2319
+ async function runWebProvidersConfig(pi, ctx) {
1683
2320
  const config = await loadConfig();
1684
2321
  const activeProvider = await getPreferredProvider(ctx.cwd);
1685
2322
  await ctx.ui.custom(
@@ -1692,6 +2329,60 @@ async function runWebProvidersConfig(ctx) {
1692
2329
  activeProvider
1693
2330
  )
1694
2331
  );
2332
+ await refreshManagedTools(pi, ctx.cwd, { addAvailable: true });
2333
+ }
2334
+ function getAvailableProviderIdsForCapability(config, cwd, capability) {
2335
+ const providerIds = [];
2336
+ for (const providerId of getProviderIdsForCapability(capability)) {
2337
+ try {
2338
+ resolveProviderForCapability(config, providerId, cwd, capability);
2339
+ providerIds.push(providerId);
2340
+ } catch {
2341
+ }
2342
+ }
2343
+ return providerIds;
2344
+ }
2345
+ function getAvailableManagedToolNames(config, cwd) {
2346
+ return Object.keys(CAPABILITY_TOOL_NAMES).filter(
2347
+ (capability) => getAvailableProviderIdsForCapability(config, cwd, capability).length > 0
2348
+ ).map((capability) => CAPABILITY_TOOL_NAMES[capability]);
2349
+ }
2350
+ function getSyncedActiveTools(config, cwd, activeToolNames, options) {
2351
+ const availableToolNames = new Set(getAvailableManagedToolNames(config, cwd));
2352
+ const nextActiveTools = new Set(activeToolNames);
2353
+ for (const toolName of MANAGED_TOOL_NAMES) {
2354
+ if (availableToolNames.has(toolName)) {
2355
+ if (options.addAvailable) {
2356
+ nextActiveTools.add(toolName);
2357
+ }
2358
+ continue;
2359
+ }
2360
+ nextActiveTools.delete(toolName);
2361
+ }
2362
+ return nextActiveTools;
2363
+ }
2364
+ async function refreshManagedTools(pi, cwd, options) {
2365
+ const config = await loadConfig();
2366
+ const nextActiveTools = getSyncedActiveTools(
2367
+ config,
2368
+ cwd,
2369
+ pi.getActiveTools(),
2370
+ options
2371
+ );
2372
+ registerManagedTools(pi, {
2373
+ search: getAvailableProviderIdsForCapability(config, cwd, "search"),
2374
+ contents: getAvailableProviderIdsForCapability(config, cwd, "contents"),
2375
+ answer: getAvailableProviderIdsForCapability(config, cwd, "answer"),
2376
+ research: getAvailableProviderIdsForCapability(config, cwd, "research")
2377
+ });
2378
+ await syncManagedToolAvailability(pi, nextActiveTools);
2379
+ }
2380
+ async function syncManagedToolAvailability(pi, nextActiveTools) {
2381
+ const activeTools = pi.getActiveTools();
2382
+ const changed = activeTools.length !== nextActiveTools.size || activeTools.some((toolName) => !nextActiveTools.has(toolName));
2383
+ if (changed) {
2384
+ pi.setActiveTools(Array.from(nextActiveTools));
2385
+ }
1695
2386
  }
1696
2387
  function getProviderIdsForCapability(capability) {
1697
2388
  return PROVIDERS.filter(
@@ -1703,15 +2394,21 @@ function providerEnum(providerIds, description) {
1703
2394
  return Type.Optional(Type.Literal(providerIds[0], { description }));
1704
2395
  }
1705
2396
  return Type.Optional(
1706
- Type.Union(providerIds.map((id) => Type.Literal(id)), { description })
2397
+ Type.Union(
2398
+ providerIds.map((id) => Type.Literal(id)),
2399
+ { description }
2400
+ )
1707
2401
  );
1708
2402
  }
1709
2403
  function jsonOptionsSchema(description) {
1710
2404
  return Type.Optional(
1711
- Type.Object({}, {
1712
- additionalProperties: true,
1713
- description
1714
- })
2405
+ Type.Object(
2406
+ {},
2407
+ {
2408
+ additionalProperties: true,
2409
+ description
2410
+ }
2411
+ )
1715
2412
  );
1716
2413
  }
1717
2414
  async function executeProviderTool({
@@ -1729,36 +2426,74 @@ async function executeProviderTool({
1729
2426
  ctx.cwd,
1730
2427
  capability
1731
2428
  );
1732
- const providerConfig = config.providers?.[provider.id];
2429
+ const providerConfig = getEffectiveProviderConfig(config, provider.id);
1733
2430
  if (!providerConfig) {
1734
2431
  throw new Error(`Provider '${provider.id}' is not configured.`);
1735
2432
  }
1736
- const response = await invoke(
1737
- provider,
1738
- providerConfig,
1739
- {
2433
+ const progress = createToolProgressReporter(
2434
+ capability,
2435
+ provider.id,
2436
+ onUpdate
2437
+ );
2438
+ let response;
2439
+ try {
2440
+ response = await invoke(provider, providerConfig, {
1740
2441
  cwd: ctx.cwd,
1741
2442
  signal: signal ?? void 0,
1742
- onProgress: (message) => onUpdate?.({
1743
- content: [{ type: "text", text: message }],
1744
- details: {}
1745
- })
1746
- }
1747
- );
2443
+ onProgress: progress.report
2444
+ });
2445
+ } finally {
2446
+ progress.stop();
2447
+ }
1748
2448
  const details = {
1749
2449
  tool: `web_${capability}`,
1750
2450
  provider: response.provider,
1751
2451
  summary: response.summary,
1752
2452
  itemCount: response.itemCount
1753
2453
  };
2454
+ const text = await truncateAndSave(response.text, capability);
1754
2455
  return {
1755
- content: [{ type: "text", text: response.text }],
2456
+ content: [{ type: "text", text }],
1756
2457
  details
1757
2458
  };
1758
2459
  }
1759
2460
  function normalizeOptions(value) {
1760
2461
  return isJsonObject(value) ? value : void 0;
1761
2462
  }
2463
+ function createToolProgressReporter(capability, providerId, onUpdate) {
2464
+ if (!onUpdate) {
2465
+ return { report: void 0, stop: () => {
2466
+ } };
2467
+ }
2468
+ const emit = (message) => onUpdate({
2469
+ content: [{ type: "text", text: message }],
2470
+ details: {}
2471
+ });
2472
+ const startedAt = Date.now();
2473
+ let lastUpdateAt = startedAt;
2474
+ let timer;
2475
+ if (capability === "research") {
2476
+ timer = setInterval(() => {
2477
+ if (Date.now() - lastUpdateAt < RESEARCH_HEARTBEAT_MS) {
2478
+ return;
2479
+ }
2480
+ const elapsed = formatElapsed2(Date.now() - startedAt);
2481
+ emit(`web_research still running via ${providerId} (${elapsed} elapsed)`);
2482
+ lastUpdateAt = Date.now();
2483
+ }, RESEARCH_HEARTBEAT_MS);
2484
+ }
2485
+ return {
2486
+ report: (message) => {
2487
+ lastUpdateAt = Date.now();
2488
+ emit(message);
2489
+ },
2490
+ stop: () => {
2491
+ if (timer) {
2492
+ clearInterval(timer);
2493
+ }
2494
+ }
2495
+ };
2496
+ }
1762
2497
  function renderToolCallHeader(toolName, primary, details, theme) {
1763
2498
  return {
1764
2499
  invalidate() {
@@ -1829,6 +2564,30 @@ function buildProviderMenuOptions(providerId) {
1829
2564
  values
1830
2565
  });
1831
2566
  };
2567
+ if (providerId === "claude") {
2568
+ pushText(
2569
+ "model",
2570
+ "Model",
2571
+ "Optional Claude model override. Leave empty to use the local default."
2572
+ );
2573
+ pushValues(
2574
+ "claudeEffort",
2575
+ "Effort",
2576
+ "How much effort Claude should use. 'default' uses the SDK default.",
2577
+ ["default", "low", "medium", "high", "max"]
2578
+ );
2579
+ pushText(
2580
+ "claudeMaxTurns",
2581
+ "Max turns",
2582
+ "Optional maximum number of Claude turns. Leave empty to use the SDK default."
2583
+ );
2584
+ pushText(
2585
+ "claudePathToExecutable",
2586
+ "Executable path",
2587
+ "Optional path to the Claude Code executable. Leave empty to use the bundled/default executable."
2588
+ );
2589
+ return options;
2590
+ }
1832
2591
  if (providerId === "codex") {
1833
2592
  pushText(
1834
2593
  "model",
@@ -1983,17 +2742,24 @@ var WebProvidersSettingsView = class {
1983
2742
  }
1984
2743
  const lines = [];
1985
2744
  const providerItems = this.buildProviderSectionItems();
1986
- lines.push(...this.renderSection(width, "Provider", "provider", providerItems));
2745
+ lines.push(
2746
+ ...this.renderSection(width, "Provider", "provider", providerItems)
2747
+ );
1987
2748
  lines.push("");
1988
2749
  const toolItems = this.buildToolSectionItems();
1989
2750
  lines.push(...this.renderSection(width, "Tools", "tools", toolItems));
1990
2751
  lines.push("");
1991
2752
  const configItems = this.buildConfigSectionItems();
1992
- lines.push(...this.renderSection(width, "Provider config", "config", configItems));
2753
+ lines.push(
2754
+ ...this.renderSection(width, "Provider config", "config", configItems)
2755
+ );
1993
2756
  const selected = this.getSelectedEntry();
1994
2757
  if (selected) {
1995
2758
  lines.push("");
1996
- for (const line of wrapTextWithAnsi(selected.description, Math.max(10, width - 2))) {
2759
+ for (const line of wrapTextWithAnsi(
2760
+ selected.description,
2761
+ Math.max(10, width - 2)
2762
+ )) {
1997
2763
  lines.push(truncateToWidth(this.theme.fg("dim", line), width));
1998
2764
  }
1999
2765
  }
@@ -2090,7 +2856,10 @@ var WebProvidersSettingsView = class {
2090
2856
  }
2091
2857
  if (option.kind === "text") {
2092
2858
  const key = option.key;
2093
- const currentValue = key === "model" || key === "additionalDirectories" ? getCodexTextSettingValue(
2859
+ const currentValue = this.activeProvider === "claude" && (key === "model" || key === "claudePathToExecutable" || key === "claudeMaxTurns") ? getClaudeTextSettingValue(
2860
+ providerConfig,
2861
+ key
2862
+ ) : key === "model" || key === "additionalDirectories" ? getCodexTextSettingValue(
2094
2863
  providerConfig,
2095
2864
  key
2096
2865
  ) : key === "geminiSearchModel" || key === "geminiAnswerModel" || key === "geminiResearchAgent" ? getGeminiTextSettingValue(
@@ -2132,7 +2901,7 @@ var WebProvidersSettingsView = class {
2132
2901
  "tools",
2133
2902
  "config"
2134
2903
  ];
2135
- let index = sections.indexOf(this.activeSection);
2904
+ const index = sections.indexOf(this.activeSection);
2136
2905
  for (let offset = 1; offset <= sections.length; offset++) {
2137
2906
  const next = sections[(index + offset * direction + sections.length) % sections.length];
2138
2907
  if (this.getSectionEntries(next).length > 0) {
@@ -2221,6 +2990,12 @@ var WebProvidersSettingsView = class {
2221
2990
  if (id === "apiKey" || id === "baseUrl") {
2222
2991
  return getProviderStringValue(providerConfig, id);
2223
2992
  }
2993
+ if (this.activeProvider === "claude" && (id === "model" || id === "claudePathToExecutable" || id === "claudeMaxTurns")) {
2994
+ return getClaudeTextSettingValue(
2995
+ providerConfig,
2996
+ id
2997
+ );
2998
+ }
2224
2999
  if (id === "model" || id === "additionalDirectories") {
2225
3000
  return getCodexTextSettingValue(
2226
3001
  providerConfig,
@@ -2268,6 +3043,11 @@ var WebProvidersSettingsView = class {
2268
3043
  }
2269
3044
  if (id === "apiKey" || id === "baseUrl") {
2270
3045
  assignOptionalString(providerConfig, id, value);
3046
+ } else if (this.activeProvider === "claude" && applyClaudeSettingChange(
3047
+ providerConfig,
3048
+ id,
3049
+ value
3050
+ )) {
2271
3051
  } else if (this.activeProvider === "codex" && applyCodexSettingChange(
2272
3052
  providerConfig,
2273
3053
  id,
@@ -2408,6 +3188,12 @@ function getProviderStringValue(config, key) {
2408
3188
  return typeof value === "string" ? value : void 0;
2409
3189
  }
2410
3190
  function getProviderChoiceValue(providerId, config, key) {
3191
+ if (providerId === "claude") {
3192
+ const defaults = config?.defaults;
3193
+ if (key === "claudeEffort") {
3194
+ return typeof defaults?.effort === "string" ? defaults.effort : "default";
3195
+ }
3196
+ }
2411
3197
  if (providerId === "codex") {
2412
3198
  const defaults = config?.defaults;
2413
3199
  if (key === "networkAccessEnabled" || key === "webSearchEnabled") {
@@ -2462,6 +3248,17 @@ function getProviderChoiceValue(providerId, config, key) {
2462
3248
  }
2463
3249
  throw new Error(`Unsupported choice setting '${key}' for '${providerId}'.`);
2464
3250
  }
3251
+ function getClaudeTextSettingValue(config, key) {
3252
+ if (key === "claudePathToExecutable") {
3253
+ return config?.pathToClaudeCodeExecutable;
3254
+ }
3255
+ const defaults = config?.defaults;
3256
+ if (!defaults) return void 0;
3257
+ if (key === "claudeMaxTurns") {
3258
+ return typeof defaults.maxTurns === "number" ? String(defaults.maxTurns) : void 0;
3259
+ }
3260
+ return defaults.model;
3261
+ }
2465
3262
  function getCodexTextSettingValue(config, key) {
2466
3263
  const defaults = config?.defaults;
2467
3264
  if (!defaults) return void 0;
@@ -2485,6 +3282,51 @@ function assignOptionalString(target, key, value) {
2485
3282
  target[key] = trimmed;
2486
3283
  }
2487
3284
  }
3285
+ function applyClaudeSettingChange(target, key, value) {
3286
+ target.defaults ??= {};
3287
+ switch (key) {
3288
+ case "model":
3289
+ assignOptionalString(
3290
+ target.defaults,
3291
+ "model",
3292
+ value
3293
+ );
3294
+ cleanupClaudeDefaults(target);
3295
+ return true;
3296
+ case "claudePathToExecutable":
3297
+ assignOptionalString(
3298
+ target,
3299
+ "pathToClaudeCodeExecutable",
3300
+ value
3301
+ );
3302
+ cleanupClaudeDefaults(target);
3303
+ return true;
3304
+ case "claudeMaxTurns": {
3305
+ const trimmed = value.trim();
3306
+ if (!trimmed) {
3307
+ delete target.defaults.maxTurns;
3308
+ } else {
3309
+ const parsed = Number(trimmed);
3310
+ if (!Number.isInteger(parsed) || parsed < 1) {
3311
+ throw new Error("Claude max turns must be a positive integer.");
3312
+ }
3313
+ target.defaults.maxTurns = parsed;
3314
+ }
3315
+ cleanupClaudeDefaults(target);
3316
+ return true;
3317
+ }
3318
+ case "claudeEffort":
3319
+ if (value === "default") {
3320
+ delete target.defaults.effort;
3321
+ } else {
3322
+ target.defaults.effort = value;
3323
+ }
3324
+ cleanupClaudeDefaults(target);
3325
+ return true;
3326
+ default:
3327
+ return false;
3328
+ }
3329
+ }
2488
3330
  function applyCodexSettingChange(target, key, value) {
2489
3331
  target.defaults ??= {};
2490
3332
  switch (key) {
@@ -2653,6 +3495,11 @@ function applyParallelSettingChange(target, key, value) {
2653
3495
  return false;
2654
3496
  }
2655
3497
  }
3498
+ function cleanupClaudeDefaults(target) {
3499
+ if (target.defaults && Object.keys(target.defaults).length === 0) {
3500
+ delete target.defaults;
3501
+ }
3502
+ }
2656
3503
  function cleanupCodexDefaults(target) {
2657
3504
  if (target.defaults && Object.keys(target.defaults).length === 0) {
2658
3505
  delete target.defaults;
@@ -2699,9 +3546,9 @@ function renderCallHeader(params, theme) {
2699
3546
  },
2700
3547
  render(width) {
2701
3548
  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)}"`)} `;
3549
+ const query2 = cleanSingleLine(String(params.query ?? "")).trim();
3550
+ if (query2.length > 0) {
3551
+ header += ` ${theme.fg("accent", formatQuotedPreview(query2))} `;
2705
3552
  }
2706
3553
  const lines = [];
2707
3554
  const headerLine = truncateToWidth(header.trimEnd(), width);
@@ -2758,6 +3605,18 @@ function getExpandHint() {
2758
3605
  function cleanSingleLine(text) {
2759
3606
  return text.replace(/\s+/g, " ").trim();
2760
3607
  }
3608
+ function formatQuotedPreview(text, maxLength = 80) {
3609
+ return `"${truncateInline(cleanSingleLine(text), maxLength)}"`;
3610
+ }
3611
+ function formatElapsed2(ms) {
3612
+ const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
3613
+ const minutes = Math.floor(totalSeconds / 60);
3614
+ const seconds = totalSeconds % 60;
3615
+ if (minutes > 0) {
3616
+ return `${minutes}m ${seconds}s`;
3617
+ }
3618
+ return `${totalSeconds}s`;
3619
+ }
2761
3620
  function formatSearchResponse(response) {
2762
3621
  if (response.results.length === 0) {
2763
3622
  return "No results found.";
@@ -2779,9 +3638,9 @@ async function truncateAndSave(text, prefix) {
2779
3638
  maxBytes: DEFAULT_MAX_BYTES
2780
3639
  });
2781
3640
  if (!truncation.truncated) return truncation.content;
2782
- const dir = join2(tmpdir(), `pi-web-providers-${prefix}-${Date.now()}`);
3641
+ const dir = join4(tmpdir(), `pi-web-providers-${prefix}-${Date.now()}`);
2783
3642
  await mkdir2(dir, { recursive: true });
2784
- const fullPath = join2(dir, "output.txt");
3643
+ const fullPath = join4(dir, "output.txt");
2785
3644
  await writeFile2(fullPath, text, "utf-8");
2786
3645
  return truncation.content + `
2787
3646
 
@@ -2792,7 +3651,11 @@ function truncateInline(text, maxLength) {
2792
3651
  return `${text.slice(0, maxLength - 1)}\u2026`;
2793
3652
  }
2794
3653
  var __test__ = {
3654
+ executeProviderTool,
2795
3655
  extractTextContent,
3656
+ getAvailableManagedToolNames,
3657
+ getAvailableProviderIdsForCapability,
3658
+ getSyncedActiveTools,
2796
3659
  renderCallHeader,
2797
3660
  renderCollapsedSearchSummary
2798
3661
  };