@oh-my-pi/pi-coding-agent 8.12.10 → 8.13.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/CHANGELOG.md +14 -0
- package/package.json +7 -7
- package/src/debug/index.ts +362 -0
- package/src/debug/profiler.ts +158 -0
- package/src/debug/report-bundle.ts +280 -0
- package/src/debug/system-info.ts +91 -0
- package/src/lsp/index.ts +1 -0
- package/src/main.ts +1 -1
- package/src/modes/controllers/command-controller.ts +3 -42
- package/src/modes/controllers/input-controller.ts +2 -2
- package/src/modes/controllers/selector-controller.ts +8 -0
- package/src/modes/interactive-mode.ts +8 -5
- package/src/modes/types.ts +2 -2
- package/src/sdk.ts +1 -1
- package/src/session/agent-storage.ts +26 -2
- package/src/tools/fetch.ts +55 -44
- package/src/tools/gemini-image.ts +2 -7
- package/src/tools/read.ts +1 -1
- package/src/utils/tools-manager.ts +35 -28
- package/src/web/scrapers/github.ts +11 -14
- package/src/web/scrapers/types.ts +2 -36
- package/src/web/scrapers/utils.ts +11 -14
- package/src/web/scrapers/youtube.ts +5 -15
- package/src/web/search/providers/codex.ts +358 -0
- package/src/web/search/providers/gemini.ts +426 -0
- package/src/web/search/types.ts +1 -1
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Gemini Web Search Provider
|
|
3
|
+
*
|
|
4
|
+
* Uses Gemini's Google Search grounding via Cloud Code Assist API.
|
|
5
|
+
* Requires OAuth credentials stored in agent.db for provider "google-gemini-cli" or "google-antigravity".
|
|
6
|
+
* Returns synthesized answers with citations and source metadata from grounding chunks.
|
|
7
|
+
*/
|
|
8
|
+
import { refreshGoogleCloudToken } from "@oh-my-pi/pi-ai";
|
|
9
|
+
import { getAgentDbPath, getConfigDirPaths } from "../../../config";
|
|
10
|
+
import { AgentStorage } from "../../../session/agent-storage";
|
|
11
|
+
import type { WebSearchCitation, WebSearchResponse, WebSearchSource } from "../../../web/search/types";
|
|
12
|
+
import { WebSearchProviderError } from "../../../web/search/types";
|
|
13
|
+
|
|
14
|
+
const DEFAULT_ENDPOINT = "https://cloudcode-pa.googleapis.com";
|
|
15
|
+
const ANTIGRAVITY_ENDPOINT = "https://daily-cloudcode-pa.sandbox.googleapis.com";
|
|
16
|
+
const DEFAULT_MODEL = "gemini-2.5-flash";
|
|
17
|
+
|
|
18
|
+
// Headers for Gemini CLI (prod endpoint)
|
|
19
|
+
const GEMINI_CLI_HEADERS = {
|
|
20
|
+
"User-Agent": "google-cloud-sdk vscode_cloudshelleditor/0.1",
|
|
21
|
+
"X-Goog-Api-Client": "gl-node/22.17.0",
|
|
22
|
+
"Client-Metadata": JSON.stringify({
|
|
23
|
+
ideType: "IDE_UNSPECIFIED",
|
|
24
|
+
platform: "PLATFORM_UNSPECIFIED",
|
|
25
|
+
pluginType: "GEMINI",
|
|
26
|
+
}),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Headers for Antigravity (sandbox endpoint)
|
|
30
|
+
const ANTIGRAVITY_HEADERS = {
|
|
31
|
+
"User-Agent": "antigravity/1.11.5 darwin/arm64",
|
|
32
|
+
"X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
|
|
33
|
+
"Client-Metadata": JSON.stringify({
|
|
34
|
+
ideType: "IDE_UNSPECIFIED",
|
|
35
|
+
platform: "PLATFORM_UNSPECIFIED",
|
|
36
|
+
pluginType: "GEMINI",
|
|
37
|
+
}),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export interface GeminiSearchParams {
|
|
41
|
+
query: string;
|
|
42
|
+
system_prompt?: string;
|
|
43
|
+
num_results?: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** OAuth credential stored in agent.db */
|
|
47
|
+
interface GeminiOAuthCredential {
|
|
48
|
+
type: "oauth";
|
|
49
|
+
access: string;
|
|
50
|
+
refresh?: string;
|
|
51
|
+
expires: number;
|
|
52
|
+
projectId?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Auth info for Gemini API requests */
|
|
56
|
+
interface GeminiAuth {
|
|
57
|
+
accessToken: string;
|
|
58
|
+
refreshToken?: string;
|
|
59
|
+
projectId: string;
|
|
60
|
+
isAntigravity: boolean;
|
|
61
|
+
storage: AgentStorage;
|
|
62
|
+
credentialId: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Finds valid Gemini OAuth credentials from agent.db.
|
|
67
|
+
* Checks google-antigravity first (daily sandbox, more quota), then google-gemini-cli (prod).
|
|
68
|
+
* @returns OAuth credential with access token and project ID, or null if none found
|
|
69
|
+
*/
|
|
70
|
+
async function findGeminiAuth(): Promise<GeminiAuth | null> {
|
|
71
|
+
const configDirs = getConfigDirPaths("", { project: false });
|
|
72
|
+
const expiryBuffer = 5 * 60 * 1000; // 5 minutes
|
|
73
|
+
const now = Date.now();
|
|
74
|
+
|
|
75
|
+
// Try providers in order: antigravity first (more quota), then gemini-cli
|
|
76
|
+
const providers = ["google-antigravity", "google-gemini-cli"] as const;
|
|
77
|
+
|
|
78
|
+
for (const configDir of configDirs) {
|
|
79
|
+
try {
|
|
80
|
+
const storage = await AgentStorage.open(getAgentDbPath(configDir));
|
|
81
|
+
|
|
82
|
+
for (const provider of providers) {
|
|
83
|
+
const records = storage.listAuthCredentials(provider);
|
|
84
|
+
|
|
85
|
+
for (const record of records) {
|
|
86
|
+
const credential = record.credential;
|
|
87
|
+
if (credential.type !== "oauth") continue;
|
|
88
|
+
|
|
89
|
+
const oauthCred = credential as GeminiOAuthCredential;
|
|
90
|
+
if (!oauthCred.access) continue;
|
|
91
|
+
|
|
92
|
+
// Get projectId from credential
|
|
93
|
+
const projectId = oauthCred.projectId;
|
|
94
|
+
if (!projectId) continue;
|
|
95
|
+
|
|
96
|
+
// Check if token is expired (or about to expire)
|
|
97
|
+
if (oauthCred.expires <= now + expiryBuffer) {
|
|
98
|
+
// Try to refresh if we have a refresh token
|
|
99
|
+
if (oauthCred.refresh) {
|
|
100
|
+
try {
|
|
101
|
+
const refreshed = await refreshGoogleCloudToken(oauthCred.refresh, projectId);
|
|
102
|
+
// Update the credential in storage
|
|
103
|
+
const updated = {
|
|
104
|
+
...oauthCred,
|
|
105
|
+
access: refreshed.access,
|
|
106
|
+
refresh: refreshed.refresh ?? oauthCred.refresh,
|
|
107
|
+
expires: refreshed.expires,
|
|
108
|
+
};
|
|
109
|
+
storage.updateAuthCredential(record.id, updated);
|
|
110
|
+
return {
|
|
111
|
+
accessToken: refreshed.access,
|
|
112
|
+
refreshToken: refreshed.refresh ?? oauthCred.refresh,
|
|
113
|
+
projectId,
|
|
114
|
+
isAntigravity: provider === "google-antigravity",
|
|
115
|
+
storage,
|
|
116
|
+
credentialId: record.id,
|
|
117
|
+
};
|
|
118
|
+
} catch {
|
|
119
|
+
// Refresh failed, skip this credential
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// No refresh token or refresh failed
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
accessToken: oauthCred.access,
|
|
129
|
+
refreshToken: oauthCred.refresh,
|
|
130
|
+
projectId,
|
|
131
|
+
isAntigravity: provider === "google-antigravity",
|
|
132
|
+
storage,
|
|
133
|
+
credentialId: record.id,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
// Continue to next config directory
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Cloud Code Assist API response types */
|
|
146
|
+
interface GeminiGroundingChunk {
|
|
147
|
+
web?: {
|
|
148
|
+
uri?: string;
|
|
149
|
+
title?: string;
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
interface GeminiGroundingSupport {
|
|
154
|
+
segment?: {
|
|
155
|
+
startIndex?: number;
|
|
156
|
+
endIndex?: number;
|
|
157
|
+
text?: string;
|
|
158
|
+
};
|
|
159
|
+
groundingChunkIndices?: number[];
|
|
160
|
+
confidenceScores?: number[];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
interface GeminiGroundingMetadata {
|
|
164
|
+
groundingChunks?: GeminiGroundingChunk[];
|
|
165
|
+
groundingSupports?: GeminiGroundingSupport[];
|
|
166
|
+
webSearchQueries?: string[];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
interface CloudCodeResponseChunk {
|
|
170
|
+
response?: {
|
|
171
|
+
candidates?: Array<{
|
|
172
|
+
content?: {
|
|
173
|
+
role: string;
|
|
174
|
+
parts?: Array<{ text?: string }>;
|
|
175
|
+
};
|
|
176
|
+
finishReason?: string;
|
|
177
|
+
groundingMetadata?: GeminiGroundingMetadata;
|
|
178
|
+
}>;
|
|
179
|
+
usageMetadata?: {
|
|
180
|
+
promptTokenCount?: number;
|
|
181
|
+
candidatesTokenCount?: number;
|
|
182
|
+
totalTokenCount?: number;
|
|
183
|
+
};
|
|
184
|
+
modelVersion?: string;
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Calls the Cloud Code Assist API with Google Search grounding enabled.
|
|
190
|
+
* @param auth - Authentication info (access token and project ID)
|
|
191
|
+
* @param query - Search query from the user
|
|
192
|
+
* @param systemPrompt - Optional system prompt
|
|
193
|
+
* @returns Parsed response with answer, sources, and usage
|
|
194
|
+
* @throws {WebSearchProviderError} If the API request fails
|
|
195
|
+
*/
|
|
196
|
+
async function callGeminiSearch(
|
|
197
|
+
auth: GeminiAuth,
|
|
198
|
+
query: string,
|
|
199
|
+
systemPrompt?: string,
|
|
200
|
+
): Promise<{
|
|
201
|
+
answer: string;
|
|
202
|
+
sources: WebSearchSource[];
|
|
203
|
+
citations: WebSearchCitation[];
|
|
204
|
+
searchQueries: string[];
|
|
205
|
+
model: string;
|
|
206
|
+
usage?: { inputTokens: number; outputTokens: number; totalTokens: number };
|
|
207
|
+
}> {
|
|
208
|
+
const endpoint = auth.isAntigravity ? ANTIGRAVITY_ENDPOINT : DEFAULT_ENDPOINT;
|
|
209
|
+
const url = `${endpoint}/v1internal:streamGenerateContent?alt=sse`;
|
|
210
|
+
const headers = auth.isAntigravity ? ANTIGRAVITY_HEADERS : GEMINI_CLI_HEADERS;
|
|
211
|
+
|
|
212
|
+
const requestBody = {
|
|
213
|
+
project: auth.projectId,
|
|
214
|
+
model: DEFAULT_MODEL,
|
|
215
|
+
request: {
|
|
216
|
+
contents: [
|
|
217
|
+
{
|
|
218
|
+
role: "user",
|
|
219
|
+
parts: [{ text: query }],
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
// Add googleSearch tool for grounding
|
|
223
|
+
tools: [{ googleSearch: {} }],
|
|
224
|
+
...(systemPrompt && {
|
|
225
|
+
systemInstruction: {
|
|
226
|
+
parts: [{ text: systemPrompt }],
|
|
227
|
+
},
|
|
228
|
+
}),
|
|
229
|
+
},
|
|
230
|
+
userAgent: "pi-web-search",
|
|
231
|
+
requestId: `search-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const response = await fetch(url, {
|
|
235
|
+
method: "POST",
|
|
236
|
+
headers: {
|
|
237
|
+
Authorization: `Bearer ${auth.accessToken}`,
|
|
238
|
+
"Content-Type": "application/json",
|
|
239
|
+
Accept: "text/event-stream",
|
|
240
|
+
...headers,
|
|
241
|
+
},
|
|
242
|
+
body: JSON.stringify(requestBody),
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
if (!response.ok) {
|
|
246
|
+
const errorText = await response.text();
|
|
247
|
+
throw new WebSearchProviderError(
|
|
248
|
+
"gemini",
|
|
249
|
+
`Gemini Cloud Code API error (${response.status}): ${errorText}`,
|
|
250
|
+
response.status,
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (!response.body) {
|
|
255
|
+
throw new WebSearchProviderError("gemini", "Gemini API returned no response body", 500);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Parse SSE stream
|
|
259
|
+
const answerParts: string[] = [];
|
|
260
|
+
const sources: WebSearchSource[] = [];
|
|
261
|
+
const citations: WebSearchCitation[] = [];
|
|
262
|
+
const searchQueries: string[] = [];
|
|
263
|
+
const seenUrls = new Set<string>();
|
|
264
|
+
let model = DEFAULT_MODEL;
|
|
265
|
+
let usage: { inputTokens: number; outputTokens: number; totalTokens: number } | undefined;
|
|
266
|
+
|
|
267
|
+
const reader = response.body.getReader();
|
|
268
|
+
const decoder = new TextDecoder();
|
|
269
|
+
let buffer = "";
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
while (true) {
|
|
273
|
+
const { done, value } = await reader.read();
|
|
274
|
+
if (done) break;
|
|
275
|
+
|
|
276
|
+
buffer += decoder.decode(value, { stream: true });
|
|
277
|
+
const lines = buffer.split("\n");
|
|
278
|
+
buffer = lines.pop() || "";
|
|
279
|
+
|
|
280
|
+
for (const line of lines) {
|
|
281
|
+
if (!line.startsWith("data:")) continue;
|
|
282
|
+
|
|
283
|
+
const jsonStr = line.slice(5).trim();
|
|
284
|
+
if (!jsonStr) continue;
|
|
285
|
+
|
|
286
|
+
let chunk: CloudCodeResponseChunk;
|
|
287
|
+
try {
|
|
288
|
+
chunk = JSON.parse(jsonStr) as CloudCodeResponseChunk;
|
|
289
|
+
} catch {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const responseData = chunk.response;
|
|
294
|
+
if (!responseData) continue;
|
|
295
|
+
|
|
296
|
+
const candidate = responseData.candidates?.[0];
|
|
297
|
+
|
|
298
|
+
// Extract text content
|
|
299
|
+
if (candidate?.content?.parts) {
|
|
300
|
+
for (const part of candidate.content.parts) {
|
|
301
|
+
if (part.text) {
|
|
302
|
+
answerParts.push(part.text);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Extract grounding metadata
|
|
308
|
+
const groundingMetadata = candidate?.groundingMetadata;
|
|
309
|
+
if (groundingMetadata) {
|
|
310
|
+
// Extract sources from grounding chunks
|
|
311
|
+
if (groundingMetadata.groundingChunks) {
|
|
312
|
+
for (const grChunk of groundingMetadata.groundingChunks) {
|
|
313
|
+
if (grChunk.web?.uri) {
|
|
314
|
+
const sourceUrl = grChunk.web.uri;
|
|
315
|
+
if (!seenUrls.has(sourceUrl)) {
|
|
316
|
+
seenUrls.add(sourceUrl);
|
|
317
|
+
sources.push({
|
|
318
|
+
title: grChunk.web.title ?? sourceUrl,
|
|
319
|
+
url: sourceUrl,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Extract citations from grounding supports
|
|
327
|
+
if (groundingMetadata.groundingSupports && groundingMetadata.groundingChunks) {
|
|
328
|
+
for (const support of groundingMetadata.groundingSupports) {
|
|
329
|
+
const citedText = support.segment?.text;
|
|
330
|
+
const chunkIndices = support.groundingChunkIndices ?? [];
|
|
331
|
+
|
|
332
|
+
for (const idx of chunkIndices) {
|
|
333
|
+
const grChunk = groundingMetadata.groundingChunks[idx];
|
|
334
|
+
if (grChunk?.web?.uri) {
|
|
335
|
+
citations.push({
|
|
336
|
+
url: grChunk.web.uri,
|
|
337
|
+
title: grChunk.web.title ?? grChunk.web.uri,
|
|
338
|
+
citedText,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Extract search queries
|
|
346
|
+
if (groundingMetadata.webSearchQueries) {
|
|
347
|
+
for (const q of groundingMetadata.webSearchQueries) {
|
|
348
|
+
if (!searchQueries.includes(q)) {
|
|
349
|
+
searchQueries.push(q);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Extract usage metadata
|
|
356
|
+
if (responseData.usageMetadata) {
|
|
357
|
+
usage = {
|
|
358
|
+
inputTokens: responseData.usageMetadata.promptTokenCount ?? 0,
|
|
359
|
+
outputTokens: responseData.usageMetadata.candidatesTokenCount ?? 0,
|
|
360
|
+
totalTokens: responseData.usageMetadata.totalTokenCount ?? 0,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Extract model version
|
|
365
|
+
if (responseData.modelVersion) {
|
|
366
|
+
model = responseData.modelVersion;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
} finally {
|
|
371
|
+
reader.releaseLock();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
answer: answerParts.join(""),
|
|
376
|
+
sources,
|
|
377
|
+
citations,
|
|
378
|
+
searchQueries,
|
|
379
|
+
model,
|
|
380
|
+
usage,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Executes a web search using Google Gemini with Google Search grounding.
|
|
386
|
+
* Requires OAuth credentials stored in agent.db for provider "google-gemini-cli" or "google-antigravity".
|
|
387
|
+
* @param params - Search parameters including query and optional settings
|
|
388
|
+
* @returns Search response with synthesized answer, sources, and citations
|
|
389
|
+
* @throws {Error} If no Gemini OAuth credentials are configured
|
|
390
|
+
*/
|
|
391
|
+
export async function searchGemini(params: GeminiSearchParams): Promise<WebSearchResponse> {
|
|
392
|
+
const auth = await findGeminiAuth();
|
|
393
|
+
if (!auth) {
|
|
394
|
+
throw new Error(
|
|
395
|
+
"No Gemini OAuth credentials found. Login with 'omp /login google-gemini-cli' or 'omp /login google-antigravity' to enable Gemini web search.",
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const result = await callGeminiSearch(auth, params.query, params.system_prompt);
|
|
400
|
+
|
|
401
|
+
let sources = result.sources;
|
|
402
|
+
|
|
403
|
+
// Apply num_results limit if specified
|
|
404
|
+
if (params.num_results && sources.length > params.num_results) {
|
|
405
|
+
sources = sources.slice(0, params.num_results);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
provider: "gemini",
|
|
410
|
+
answer: result.answer || undefined,
|
|
411
|
+
sources,
|
|
412
|
+
citations: result.citations.length > 0 ? result.citations : undefined,
|
|
413
|
+
searchQueries: result.searchQueries.length > 0 ? result.searchQueries : undefined,
|
|
414
|
+
usage: result.usage,
|
|
415
|
+
model: result.model,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Checks if Gemini web search is available.
|
|
421
|
+
* @returns True if valid OAuth credentials exist for google-gemini-cli or google-antigravity
|
|
422
|
+
*/
|
|
423
|
+
export async function hasGeminiWebSearch(): Promise<boolean> {
|
|
424
|
+
const auth = await findGeminiAuth();
|
|
425
|
+
return auth !== null;
|
|
426
|
+
}
|
package/src/web/search/types.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
/** Supported web search providers */
|
|
8
|
-
export type WebSearchProvider = "exa" | "anthropic" | "perplexity";
|
|
8
|
+
export type WebSearchProvider = "exa" | "anthropic" | "perplexity" | "gemini" | "codex";
|
|
9
9
|
|
|
10
10
|
/** Source returned by search (all providers) */
|
|
11
11
|
export interface WebSearchSource {
|