pi-web-search 1.0.2 → 1.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.
package/src/index.ts CHANGED
@@ -1,21 +1,83 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
- import { webSearch, WebSearchSchema } from "./web_search.js";
3
- import { urlContext, UrlContextSchema } from "./url_context.js";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { type Model } from "@earendil-works/pi-ai";
3
+ import { getProviderKind } from "./api.ts";
4
+ import { webSearch, WebSearchSchema } from "./web_search.ts";
5
+ import { urlContext, UrlContextSchema } from "./url_context.ts";
6
+
7
+ const WEB_SEARCH_TOOL = "web_search";
8
+ const URL_CONTEXT_TOOL = "url_context";
9
+
10
+ function supportsUrlContext(model: Model<any> | undefined) {
11
+ return !!model && getProviderKind(model) === "google";
12
+ }
13
+
14
+ function setEquals<T>(a: Set<T>, b: Set<T>) {
15
+ if (a.size !== b.size) return false;
16
+ for (const value of a) {
17
+ if (!b.has(value)) return false;
18
+ }
19
+ return true;
20
+ }
21
+
22
+ export function createModelScopedToolManager(pi: Pick<ExtensionAPI, "getActiveTools" | "setActiveTools">) {
23
+ let preferredActiveTools: Set<string> | undefined;
24
+ let lastAppliedActiveTools: Set<string> | undefined;
25
+ let suppressedTools = new Set<string>();
26
+
27
+ const sync = (model: Model<any> | undefined) => {
28
+ const currentActiveTools = new Set(pi.getActiveTools());
29
+
30
+ if (!preferredActiveTools) {
31
+ preferredActiveTools = new Set(currentActiveTools);
32
+ } else if (lastAppliedActiveTools) {
33
+ for (const tool of currentActiveTools) {
34
+ if (!lastAppliedActiveTools.has(tool)) preferredActiveTools.add(tool);
35
+ }
36
+ for (const tool of lastAppliedActiveTools) {
37
+ if (!currentActiveTools.has(tool) && !suppressedTools.has(tool)) {
38
+ preferredActiveTools.delete(tool);
39
+ }
40
+ }
41
+ }
42
+
43
+ const desiredActiveTools = new Set(preferredActiveTools);
44
+ suppressedTools = new Set<string>();
45
+ if (!supportsUrlContext(model)) {
46
+ desiredActiveTools.delete(URL_CONTEXT_TOOL);
47
+ if (preferredActiveTools.has(URL_CONTEXT_TOOL)) {
48
+ suppressedTools.add(URL_CONTEXT_TOOL);
49
+ }
50
+ }
51
+
52
+ if (!setEquals(currentActiveTools, desiredActiveTools)) {
53
+ pi.setActiveTools(Array.from(desiredActiveTools));
54
+ }
55
+ lastAppliedActiveTools = new Set(desiredActiveTools);
56
+ };
57
+
58
+ return { sync };
59
+ }
4
60
 
5
61
  export default function (pi: ExtensionAPI) {
6
62
  pi.registerTool({
7
- name: "web_search",
63
+ name: WEB_SEARCH_TOOL,
8
64
  label: "Web Search",
9
- description: "Search the web using Google Gemini's grounding. Optionally include URLs to analyze alongside search results.",
65
+ description: "Search the web using the current supported provider (Google Gemini, OpenAI, or Anthropic). Optionally include URLs to analyze alongside search results.",
10
66
  parameters: WebSearchSchema,
11
67
  execute: webSearch
12
68
  });
13
69
 
14
70
  pi.registerTool({
15
- name: "url_context",
71
+ name: URL_CONTEXT_TOOL,
16
72
  label: "URL Context",
17
- description: "Analyze the content of up to 20 public URLs. Supports web pages, documents, images, and YouTube videos.",
73
+ description: "Analyze the content of up to 20 public URLs using Gemini URL Context. Supports web pages, documents, images, and YouTube videos.",
18
74
  parameters: UrlContextSchema,
19
75
  execute: urlContext
20
76
  });
77
+
78
+ const toolManager = createModelScopedToolManager(pi);
79
+ pi.on("session_start", (_event, ctx) => toolManager.sync(ctx.model));
80
+ pi.on("session_switch", (_event, ctx) => toolManager.sync(ctx.model));
81
+ pi.on("session_tree", (_event, ctx) => toolManager.sync(ctx.model));
82
+ pi.on("model_select", (event) => toolManager.sync(event.model));
21
83
  }
@@ -1,7 +1,7 @@
1
- import type { ExtensionContext, AgentToolUpdateCallback } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionContext, AgentToolUpdateCallback } from "@earendil-works/pi-coding-agent";
2
2
  import { Type, type Static } from "@sinclair/typebox";
3
- import { callApiStream, getConfig, applyCitations } from "./api.js";
4
- import { getModel, missingConfigResult, errorResult, formatResult } from "./utils.js";
3
+ import { callApiStream, getConfig, applyCitations } from "./api.ts";
4
+ import { getModel, missingConfigResult, errorResult, formatResult } from "./utils.ts";
5
5
 
6
6
  export const UrlContextSchema = Type.Object({
7
7
  query: Type.String({ description: "Question or task to perform on the URLs" }),
@@ -30,9 +30,21 @@ export async function urlContext(
30
30
 
31
31
  try {
32
32
  const config = getConfig(model);
33
+ if (config.kind !== "google") {
34
+ return formatResult(
35
+ `url_context currently requires a Google Gemini-compatible model. Current model: ${model.id} (${model.provider}/${model.api}).\n\nUse web_search for cross-provider web search, or switch to Gemini for provider-native URL context retrieval.`,
36
+ {
37
+ error: "unsupported_provider",
38
+ providerKind: config.kind,
39
+ model: model.id,
40
+ supportedProviders: ["google", "google-generative-ai"],
41
+ grounded: false,
42
+ }
43
+ );
44
+ }
33
45
 
34
46
  let contents: any[] = [];
35
- let tools: any[] | undefined = [{ [config.urlContextTool]: {} }];
47
+ let tools: any[] | undefined = [{ [config.urlContextTool!]: {} }];
36
48
 
37
49
  // Special handling for YouTube videos on Gemini
38
50
  if (model.api === "google-generative-ai") {
@@ -83,10 +95,13 @@ export async function urlContext(
83
95
 
84
96
  const result = await callApiStream(ctx, model, {
85
97
  contents,
86
- tools
87
- }, onUpdate);
98
+ ...(tools ? { tools } : {})
99
+ }, onUpdate, signal);
88
100
 
89
- const { text, sources } = applyCitations(result.text, result.groundingMetadata);
101
+ const cited = applyCitations(result.text, result.groundingMetadata);
102
+ const text = cited.text;
103
+ const sources = result.sources?.length ? result.sources : cited.sources;
104
+ const extraSearchResults = (result.searchResults || []).filter((item) => item.url && !sources.some((source) => source.url === item.url));
90
105
 
91
106
  // Handle both camelCase and snake_case metadata
92
107
  const urlMeta = result.urlContextMetadata?.urlMetadata
@@ -108,14 +123,40 @@ export async function urlContext(
108
123
  summary += `\n\n## URL Status\n✅ Retrieved: ${retrieved.length}\n❌ Failed: ${failed.length}`;
109
124
  failed.forEach((f: any) => { summary += `\n- ${f.url}: ${f.status}`; });
110
125
  }
126
+ const hasUrlContextMetadata = urlMeta.length > 0 || retrieved.length > 0 || sources.length > 0 || extraSearchResults.length > 0;
127
+ if (!hasUrlContextMetadata) {
128
+ summary += `\n\n## URL Context Verification\n⚠️ No verified URL context metadata was returned by provider ${result.providerKind || "unknown"}. Treat the answer as ungrounded unless sources, retrieved URLs, or searchResults are present in tool details.`;
129
+ }
111
130
  if (sources.length > 0 && !summary.includes("## Sources")) {
112
131
  summary += `\n\n## Sources\n${sources.map((s, i) => `${i + 1}. [${s.title}](${s.url})`).join("\n")}`;
113
132
  }
133
+ if (extraSearchResults.length) {
134
+ const visibleResults = extraSearchResults.slice(0, 8);
135
+ summary += `\n\n## Additional Search Results\n${visibleResults.map((r, i) => {
136
+ const label = r.title || r.url || `Result ${i + 1}`;
137
+ const url = r.url ? ` - ${r.url}` : "";
138
+ const meta = [r.source, r.type, r.status, r.query ? `query=${r.query}` : undefined].filter(Boolean).join(", ");
139
+ return `${i + 1}. ${label}${url}${meta ? ` (${meta})` : ""}`;
140
+ }).join("\n")}`;
141
+ if (extraSearchResults.length > visibleResults.length) {
142
+ summary += `\n... and ${extraSearchResults.length - visibleResults.length} more results in tool details.`;
143
+ }
144
+ }
114
145
 
115
146
  return formatResult(summary, {
147
+ sources,
148
+ providerKind: result.providerKind,
149
+ nativeSearchUsed: result.nativeSearchUsed,
150
+ nativeSearchEvents: result.nativeSearchEvents,
151
+ nativeSearchCalls: result.nativeSearchCalls,
152
+ searchQueries: result.searchQueries,
153
+ searchResults: result.searchResults,
154
+ citations: result.citations,
116
155
  retrieved,
117
156
  failed: failed.length > 0 ? failed : undefined,
118
- model: model.id
157
+ model: model.id,
158
+ grounded: sources.length > 0 || (result.searchResults?.length || 0) > 0,
159
+ resultCount: result.searchResults?.length || sources.length
119
160
  });
120
161
  } catch (e: any) {
121
162
  return errorResult(e);
package/src/utils.ts CHANGED
@@ -1,6 +1,7 @@
1
- import type { ExtensionContext, AgentToolResult } from "@mariozechner/pi-coding-agent";
2
- import { type Model } from "@mariozechner/pi-ai";
3
- import { truncateHead, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionContext, AgentToolResult } from "@earendil-works/pi-coding-agent";
2
+ import { type Model } from "@earendil-works/pi-ai";
3
+ import { truncateHead, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES } from "@earendil-works/pi-coding-agent";
4
+ import { getProviderKind } from "./api.ts";
4
5
 
5
6
  // --- Formatting ---
6
7
 
@@ -14,63 +15,62 @@ export function formatResult(text: string, details: any): AgentToolResult<any> {
14
15
 
15
16
  // --- Model Selection ---
16
17
 
17
- export async function getModel(ctx: ExtensionContext): Promise<Model<any> | undefined> {
18
- // flash first, big first: 3-flash -> 2.5-flash -> 2.0-flash
19
- // provider priority: google-gemini-cli -> google-antigravity -> google -> google-generative-ai
20
- const models = ctx.modelRegistry.getAvailable();
21
-
22
- const flashModels = [
18
+ function isSupportedSearchModel(model: Model<any> | undefined): model is Model<any> {
19
+ if (!model) return false;
20
+ return getProviderKind(model) !== "unsupported";
21
+ }
22
+
23
+ function providerPriority(model: Model<any>): number {
24
+ const priorities = [
25
+ "google",
26
+ "google-generative-ai",
27
+ "openai",
28
+ "anthropic",
29
+ ];
30
+ const providerIndex = priorities.indexOf(model.provider);
31
+ if (providerIndex >= 0) return providerIndex;
32
+ return priorities.length;
33
+ }
34
+
35
+ function modelPriority(model: Model<any>): number {
36
+ const id = model.id;
37
+ const patterns = [
23
38
  /gemini-3.*flash/i,
24
39
  /gemini-2\.5.*flash/i,
25
40
  /gemini-2\.0.*flash/i,
26
41
  /gemini.*flash/i,
42
+ /gpt-5\..*mini/i,
43
+ /gpt-4\.1.*mini/i,
44
+ /gpt-4o-mini/i,
45
+ /claude.*haiku/i,
46
+ /claude.*sonnet/i,
27
47
  ];
28
-
29
- const providers = [
30
- "google-gemini-cli",
31
- "google-antigravity",
32
- "google",
33
- "google-generative-ai",
34
- ];
35
-
36
- // Filter to only Google-compatible models (those with supported api/provider)
37
- const googleModels = models.filter(m =>
38
- providers.includes(m.provider) ||
39
- m.api === "google-generative-ai" ||
40
- m.api === "google-gemini-cli"
41
- );
42
-
43
- // Try each flash pattern in priority order
44
- for (const pattern of flashModels) {
45
- const matching = googleModels.filter(m => pattern.test(m.id));
46
- if (matching.length === 0) continue;
47
-
48
- // Among matches, pick by provider priority
49
- for (const provider of providers) {
50
- const model = matching.find(m => m.provider === provider);
51
- if (model) return model;
52
- }
53
-
54
- // Fall back to first match if no priority provider found
55
- return matching[0];
56
- }
57
-
58
- // No flash model found, try any Google model by provider priority
59
- for (const provider of providers) {
60
- const model = googleModels.find(m => m.provider === provider);
61
- if (model) return model;
48
+ const patternIndex = patterns.findIndex((pattern) => pattern.test(id));
49
+ return patternIndex >= 0 ? patternIndex : patterns.length;
50
+ }
51
+
52
+ export async function getModel(ctx: ExtensionContext): Promise<Model<any> | undefined> {
53
+ // Use the current model first so the tool follows the user's selected provider.
54
+ // This lets pi --provider openai/anthropic/google automatically pick the matching API.
55
+ if (isSupportedSearchModel(ctx.model)) {
56
+ return ctx.model;
62
57
  }
63
-
64
- // Return first available Google model if any
65
- return googleModels[0];
58
+
59
+ const models = ctx.modelRegistry.getAvailable().filter(isSupportedSearchModel);
60
+ if (models.length === 0) return undefined;
61
+
62
+ return models.sort((a, b) => {
63
+ const byProvider = providerPriority(a) - providerPriority(b);
64
+ if (byProvider !== 0) return byProvider;
65
+ return modelPriority(a) - modelPriority(b);
66
+ })[0];
66
67
  }
67
68
 
68
69
  // --- Error Results ---
69
70
 
70
71
  export function missingConfigResult(ctx: ExtensionContext): AgentToolResult<any> {
71
- const msg = ctx.model && ["google-gemini-cli", "google-antigravity"].includes(ctx.model.provider)
72
- ? `Provider ${ctx.model.provider} requires valid OAuth credentials.`
73
- : "No Google Gemini configuration found. Please configure GEMINI_API_KEY.";
72
+ const current = ctx.model ? `${ctx.model.provider} (${ctx.model.api})` : "none";
73
+ const msg = `No supported web-search model configuration found. Current model: ${current}. Configure or select a supported provider: google-generative-ai, openai, or anthropic.`;
74
74
  return { content: [{ type: "text", text: `Failed: ${msg}` }], details: { error: "missing_config" } };
75
75
  }
76
76
 
package/src/web_search.ts CHANGED
@@ -1,7 +1,7 @@
1
- import type { ExtensionContext, AgentToolUpdateCallback } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionContext, AgentToolUpdateCallback } from "@earendil-works/pi-coding-agent";
2
2
  import { Type, type Static } from "@sinclair/typebox";
3
- import { callApiStream, getConfig, applyCitations } from "./api.js";
4
- import { getModel, missingConfigResult, errorResult, formatResult } from "./utils.js";
3
+ import { callApiStream, getConfig, applyCitations } from "./api.ts";
4
+ import { getModel, missingConfigResult, errorResult, formatResult } from "./utils.ts";
5
5
 
6
6
  export const WebSearchSchema = Type.Object({
7
7
  query: Type.String({ description: "The search query or question to answer" }),
@@ -43,17 +43,23 @@ export async function webSearch(
43
43
  ? `${params.query}\n\nAlso analyze these URLs:\n${params.urls!.join("\n")}`
44
44
  : params.query;
45
45
 
46
- // Enable google_search, add url_context if URLs provided
47
- const tools = hasUrls
48
- ? [{ [config.searchTool]: {} }, { [config.urlContextTool]: {} }]
49
- : [{ [config.searchTool]: {} }];
46
+ // Enable provider-native search tools. Google needs explicit Gemini tool names;
47
+ // OpenAI/Anthropic are handled inside callApiStream based on the current model.
48
+ const tools = config.kind === "google"
49
+ ? (hasUrls
50
+ ? [{ [config.searchTool!]: {} }, { [config.urlContextTool!]: {} }]
51
+ : [{ [config.searchTool!]: {} }])
52
+ : undefined;
50
53
 
51
54
  const result = await callApiStream(ctx, model, {
52
55
  contents: [{ role: "user", parts: [{ text: prompt }] }],
53
- tools
54
- }, onUpdate);
56
+ ...(tools ? { tools } : {})
57
+ }, onUpdate, signal);
55
58
 
56
- const { text, sources } = applyCitations(result.text, result.groundingMetadata);
59
+ const cited = applyCitations(result.text, result.groundingMetadata);
60
+ const text = cited.text;
61
+ const sources = result.sources?.length ? result.sources : cited.sources;
62
+ const extraSearchResults = (result.searchResults || []).filter((item) => item.url && !sources.some((source) => source.url === item.url));
57
63
 
58
64
  // Handle URL context metadata
59
65
  const urlMeta = result.urlContextMetadata?.urlMetadata
@@ -78,18 +84,42 @@ export async function webSearch(
78
84
  failed.forEach((f: any) => { summary += `\n- ${f.url}: ${f.status}`; });
79
85
  }
80
86
 
87
+ if (result.nativeSearchUsed === false) {
88
+ summary += `\n\n## Search Verification\n⚠️ No verified native search metadata was returned by provider ${result.providerKind || "unknown"}. Treat the answer as ungrounded unless sources/searchResults are present in tool details.`;
89
+ }
90
+
81
91
  // Add sources
82
92
  if (sources.length > 0) {
83
93
  summary += `\n\n## Sources\n${sources.map((s, i) => `${i + 1}. [${s.title}](${s.url})`).join("\n")}`;
84
94
  }
85
95
 
96
+ if (extraSearchResults.length) {
97
+ const visibleResults = extraSearchResults.slice(0, 8);
98
+ summary += `\n\n## Additional Search Results\n${visibleResults.map((r, i) => {
99
+ const label = r.title || r.url || `Result ${i + 1}`;
100
+ const url = r.url ? ` - ${r.url}` : "";
101
+ const meta = [r.source, r.type, r.status, r.query ? `query=${r.query}` : undefined].filter(Boolean).join(", ");
102
+ return `${i + 1}. ${label}${url}${meta ? ` (${meta})` : ""}`;
103
+ }).join("\n")}`;
104
+ if (extraSearchResults.length > visibleResults.length) {
105
+ summary += `\n... and ${extraSearchResults.length - visibleResults.length} more results in tool details.`;
106
+ }
107
+ }
108
+
86
109
  return formatResult(summary, {
87
110
  sources,
88
- searchQueries: result.groundingMetadata?.webSearchQueries,
111
+ providerKind: result.providerKind,
112
+ nativeSearchUsed: result.nativeSearchUsed,
113
+ nativeSearchEvents: result.nativeSearchEvents,
114
+ nativeSearchCalls: result.nativeSearchCalls,
115
+ searchQueries: result.searchQueries || result.groundingMetadata?.webSearchQueries,
116
+ searchResults: result.searchResults,
117
+ citations: result.citations,
89
118
  retrieved: retrieved.length > 0 ? retrieved : undefined,
90
119
  failed: failed.length > 0 ? failed : undefined,
91
120
  model: model.id,
92
- grounded: sources.length > 0
121
+ grounded: sources.length > 0 || (result.searchResults?.length || 0) > 0,
122
+ resultCount: result.searchResults?.length || sources.length
93
123
  });
94
124
  } catch (e: any) {
95
125
  return errorResult(e);
package/dist/api.d.ts DELETED
@@ -1,26 +0,0 @@
1
- import type { ExtensionContext, AgentToolUpdateCallback } from "@mariozechner/pi-coding-agent";
2
- import { type Model } from "@mariozechner/pi-ai";
3
- type ProviderConfig = {
4
- searchTool: string;
5
- urlContextTool: string;
6
- buildRequest: (model: Model<any>, body: any, projectId?: string) => {
7
- url: string;
8
- headers: Record<string, string>;
9
- body: any;
10
- };
11
- };
12
- export declare function getConfig(model: Model<any>): ProviderConfig;
13
- export interface StreamResult {
14
- text: string;
15
- groundingMetadata?: any;
16
- urlContextMetadata?: any;
17
- }
18
- export declare function callApiStream(ctx: ExtensionContext, model: Model<any>, body: any, onUpdate?: AgentToolUpdateCallback): Promise<StreamResult>;
19
- export declare function applyCitations(text: string, groundingMetadata: any): {
20
- text: string;
21
- sources: {
22
- title: string;
23
- url: string;
24
- }[];
25
- };
26
- export {};
package/dist/api.js DELETED
@@ -1,182 +0,0 @@
1
- import { TextEncoder, TextDecoder } from "util";
2
- const PROVIDERS = {
3
- "google-generative-ai": {
4
- searchTool: "google_search",
5
- urlContextTool: "url_context",
6
- buildRequest: (model, body) => ({
7
- url: `${model.baseUrl}/models/${model.id}:streamGenerateContent?alt=sse`,
8
- headers: {
9
- "Content-Type": "application/json",
10
- "Accept": "text/event-stream",
11
- },
12
- body
13
- })
14
- },
15
- "google-gemini-cli": {
16
- searchTool: "googleSearch",
17
- urlContextTool: "urlContext",
18
- buildRequest: (model, body, projectId) => ({
19
- url: `${model.baseUrl}/v1internal:streamGenerateContent?alt=sse`,
20
- headers: {
21
- "Content-Type": "application/json",
22
- "Accept": "text/event-stream",
23
- "User-Agent": "google-cloud-sdk vscode_cloudshelleditor/0.1",
24
- "X-Goog-Api-Client": "gl-node/22.17.0",
25
- "Client-Metadata": JSON.stringify({ ideType: "IDE_UNSPECIFIED", platform: "PLATFORM_UNSPECIFIED", pluginType: "GEMINI" }),
26
- },
27
- body: { project: projectId, model: model.id, request: body }
28
- })
29
- },
30
- "google-antigravity": {
31
- searchTool: "googleSearch",
32
- urlContextTool: "urlContext",
33
- buildRequest: (model, body, projectId) => ({
34
- url: `${model.baseUrl}/v1internal:streamGenerateContent?alt=sse`,
35
- headers: {
36
- "Content-Type": "application/json",
37
- "Accept": "text/event-stream",
38
- "User-Agent": "antigravity/1.15.8 darwin/arm64",
39
- "X-Goog-Api-Client": "gl-node/22.17.0",
40
- "Client-Metadata": JSON.stringify({ ideType: "IDE_UNSPECIFIED", platform: "PLATFORM_UNSPECIFIED", pluginType: "GEMINI" }),
41
- },
42
- body: {
43
- project: projectId,
44
- model: model.id,
45
- request: body,
46
- requestType: "agent",
47
- userAgent: "antigravity",
48
- requestId: `agent-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
49
- }
50
- })
51
- }
52
- };
53
- export function getConfig(model) {
54
- return PROVIDERS[model.provider] || PROVIDERS[model.api] || PROVIDERS["google-generative-ai"];
55
- }
56
- export async function callApiStream(ctx, model, body, onUpdate) {
57
- const config = getConfig(model);
58
- const apiKey = await ctx.modelRegistry.getApiKey(model) || "";
59
- let projectId;
60
- if (model.api !== "google-generative-ai") {
61
- const parsed = JSON.parse(apiKey);
62
- projectId = parsed.projectId;
63
- }
64
- const req = config.buildRequest(model, body, projectId);
65
- // Handle auth
66
- if (model.api === "google-generative-ai") {
67
- req.headers["x-goog-api-key"] = apiKey;
68
- }
69
- else {
70
- const parsed = JSON.parse(apiKey);
71
- req.headers["Authorization"] = `Bearer ${parsed.token}`;
72
- }
73
- const response = await fetch(req.url, {
74
- method: "POST",
75
- headers: req.headers,
76
- body: JSON.stringify(req.body)
77
- });
78
- if (!response.ok) {
79
- throw new Error(`API error (${response.status}): ${await response.text()}`);
80
- }
81
- if (!response.body) {
82
- throw new Error("No response body");
83
- }
84
- // Parse SSE stream
85
- const reader = response.body.getReader();
86
- const decoder = new TextDecoder();
87
- let buffer = "";
88
- let accumulatedText = "";
89
- let groundingMetadata;
90
- let urlContextMetadata;
91
- while (true) {
92
- const { done, value } = await reader.read();
93
- if (done)
94
- break;
95
- buffer += decoder.decode(value, { stream: true });
96
- const lines = buffer.split("\n");
97
- buffer = lines.pop() || "";
98
- for (const line of lines) {
99
- if (!line.startsWith("data:"))
100
- continue;
101
- const jsonStr = line.slice(5).trim();
102
- if (!jsonStr)
103
- continue;
104
- let chunk;
105
- try {
106
- chunk = JSON.parse(jsonStr);
107
- }
108
- catch {
109
- continue;
110
- }
111
- // Unwrap response for internal APIs
112
- const data = chunk.response || chunk;
113
- const candidate = data.candidates?.[0];
114
- if (candidate?.content?.parts) {
115
- for (const part of candidate.content.parts) {
116
- if (part.text) {
117
- accumulatedText += part.text;
118
- // Stream update
119
- onUpdate?.({
120
- content: [{ type: "text", text: accumulatedText }],
121
- details: { streaming: true }
122
- });
123
- }
124
- }
125
- }
126
- // Capture metadata from final chunk
127
- if (candidate?.groundingMetadata) {
128
- groundingMetadata = candidate.groundingMetadata;
129
- }
130
- // Handle both camelCase and snake_case
131
- if (candidate?.urlContextMetadata || candidate?.url_context_metadata) {
132
- urlContextMetadata = candidate.urlContextMetadata || candidate.url_context_metadata;
133
- }
134
- }
135
- }
136
- return {
137
- text: accumulatedText || "No answer available.",
138
- groundingMetadata,
139
- urlContextMetadata
140
- };
141
- }
142
- // --- Citation Processing (byte-safe) ---
143
- export function applyCitations(text, groundingMetadata) {
144
- const chunks = groundingMetadata?.groundingChunks || [];
145
- const supports = groundingMetadata?.groundingSupports || [];
146
- const sources = chunks
147
- .filter((c) => c.web)
148
- .map((c) => ({ title: c.web.title || "Unknown", url: c.web.uri || "" }));
149
- if (!supports.length || !sources.length)
150
- return { text, sources };
151
- // Collect insertions, sort descending
152
- const insertions = supports
153
- .filter((s) => s.segment?.endIndex !== undefined && s.groundingChunkIndices?.length)
154
- .map((s) => ({
155
- index: s.segment.endIndex,
156
- marker: s.groundingChunkIndices.map((i) => `[${i + 1}]`).join("")
157
- }))
158
- .sort((a, b) => b.index - a.index);
159
- // Byte-safe insertion
160
- const encoder = new TextEncoder();
161
- const decoder = new TextDecoder();
162
- const bytes = encoder.encode(text);
163
- const parts = [];
164
- let lastIndex = bytes.length;
165
- for (const ins of insertions) {
166
- const pos = Math.min(ins.index, lastIndex);
167
- if (pos < lastIndex)
168
- parts.unshift(bytes.subarray(pos, lastIndex));
169
- parts.unshift(encoder.encode(ins.marker));
170
- lastIndex = pos;
171
- }
172
- if (lastIndex > 0)
173
- parts.unshift(bytes.subarray(0, lastIndex));
174
- const total = parts.reduce((acc, p) => acc + p.length, 0);
175
- const final = new Uint8Array(total);
176
- let offset = 0;
177
- for (const part of parts) {
178
- final.set(part, offset);
179
- offset += part.length;
180
- }
181
- return { text: decoder.decode(final), sources };
182
- }
package/dist/index.d.ts DELETED
@@ -1,2 +0,0 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
- export default function (pi: ExtensionAPI): void;
package/dist/index.js DELETED
@@ -1,18 +0,0 @@
1
- import { webSearch, WebSearchSchema } from "./web_search.js";
2
- import { urlContext, UrlContextSchema } from "./url_context.js";
3
- export default function (pi) {
4
- pi.registerTool({
5
- name: "web_search",
6
- label: "Web Search",
7
- description: "Search the web using Google Gemini's grounding. Optionally include URLs to analyze alongside search results.",
8
- parameters: WebSearchSchema,
9
- execute: webSearch
10
- });
11
- pi.registerTool({
12
- name: "url_context",
13
- label: "URL Context",
14
- description: "Analyze the content of up to 20 public URLs. Supports web pages, documents, images, and YouTube videos.",
15
- parameters: UrlContextSchema,
16
- execute: urlContext
17
- });
18
- }
@@ -1,8 +0,0 @@
1
- import type { ExtensionContext, AgentToolUpdateCallback } from "@mariozechner/pi-coding-agent";
2
- import { type Static } from "@sinclair/typebox";
3
- export declare const UrlContextSchema: import("@sinclair/typebox").TObject<{
4
- query: import("@sinclair/typebox").TString;
5
- urls: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>;
6
- }>;
7
- export type UrlContextInput = Static<typeof UrlContextSchema>;
8
- export declare function urlContext(id: string, params: UrlContextInput, signal: AbortSignal, onUpdate: AgentToolUpdateCallback | undefined, ctx: ExtensionContext): Promise<import("@mariozechner/pi-coding-agent").AgentToolResult<any>>;