@oh-my-pi/pi-coding-agent 16.2.5 → 16.2.6

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 (51) hide show
  1. package/CHANGELOG.md +18 -1
  2. package/dist/cli.js +2991 -3011
  3. package/dist/types/cli/bench-cli.d.ts +6 -1
  4. package/dist/types/commands/bench.d.ts +8 -0
  5. package/dist/types/config/model-discovery.d.ts +6 -1
  6. package/dist/types/config/settings-schema.d.ts +1 -1
  7. package/dist/types/edit/index.d.ts +1 -0
  8. package/dist/types/edit/renderer.d.ts +4 -0
  9. package/dist/types/edit/snapshot-details.d.ts +33 -0
  10. package/dist/types/mcp/oauth-flow.d.ts +4 -6
  11. package/dist/types/modes/controllers/tool-args-reveal.d.ts +9 -2
  12. package/dist/types/session/agent-session.d.ts +4 -0
  13. package/dist/types/session/session-manager.d.ts +3 -4
  14. package/dist/types/web/search/providers/duckduckgo.d.ts +2 -2
  15. package/dist/types/web/search/types.d.ts +1 -1
  16. package/package.json +12 -12
  17. package/src/cli/args.ts +32 -1
  18. package/src/cli/bench-cli.ts +89 -21
  19. package/src/cli/web-search-cli.ts +6 -1
  20. package/src/commands/bench.ts +10 -1
  21. package/src/config/mcp-schema.json +1 -1
  22. package/src/config/model-discovery.ts +66 -8
  23. package/src/config/model-registry.ts +13 -6
  24. package/src/edit/hashline/execute.ts +15 -9
  25. package/src/edit/index.ts +19 -6
  26. package/src/edit/modes/patch.ts +3 -2
  27. package/src/edit/modes/replace.ts +3 -2
  28. package/src/edit/renderer.ts +4 -0
  29. package/src/edit/snapshot-details.ts +77 -0
  30. package/src/extensibility/plugins/legacy-pi-compat.ts +2 -2
  31. package/src/internal-urls/docs-index.generated.txt +1 -1
  32. package/src/mcp/oauth-flow.ts +10 -8
  33. package/src/mcp/transports/stdio.ts +9 -17
  34. package/src/modes/controllers/event-controller.ts +17 -8
  35. package/src/modes/controllers/tool-args-reveal.ts +100 -22
  36. package/src/prompts/bench.md +4 -10
  37. package/src/prompts/tools/irc.md +19 -29
  38. package/src/prompts/tools/job.md +8 -14
  39. package/src/prompts/tools/lsp.md +19 -30
  40. package/src/prompts/tools/task.md +42 -62
  41. package/src/sdk.ts +1 -0
  42. package/src/session/agent-session.ts +10 -0
  43. package/src/session/session-listing.ts +9 -8
  44. package/src/session/session-loader.ts +98 -3
  45. package/src/session/session-manager.ts +34 -4
  46. package/src/subprocess/worker-client.ts +12 -4
  47. package/src/tools/path-utils.ts +4 -2
  48. package/src/web/search/index.ts +14 -8
  49. package/src/web/search/providers/duckduckgo.ts +136 -78
  50. package/src/web/search/providers/gemini.ts +268 -185
  51. package/src/web/search/types.ts +1 -1
@@ -23,6 +23,8 @@ import { SearchProvider } from "./base";
23
23
  import { classifyProviderHttpError, withHardTimeout } from "./utils";
24
24
 
25
25
  const DEFAULT_ENDPOINT = "https://cloudcode-pa.googleapis.com";
26
+ const DEVELOPER_API_PROVIDER = "google";
27
+ const DEVELOPER_API_ENDPOINT = "https://generativelanguage.googleapis.com/v1beta";
26
28
  const ANTIGRAVITY_DAILY_ENDPOINT = "https://daily-cloudcode-pa.googleapis.com";
27
29
  const ANTIGRAVITY_SANDBOX_ENDPOINT = "https://daily-cloudcode-pa.sandbox.googleapis.com";
28
30
  const ANTIGRAVITY_ENDPOINT_FALLBACKS = [ANTIGRAVITY_DAILY_ENDPOINT, ANTIGRAVITY_SANDBOX_ENDPOINT] as const;
@@ -80,6 +82,15 @@ interface GeminiAuthSeed {
80
82
  projectId: string;
81
83
  }
82
84
 
85
+ interface GeminiSearchResult {
86
+ answer: string;
87
+ sources: SearchSource[];
88
+ citations: SearchCitation[];
89
+ searchQueries: string[];
90
+ model: string;
91
+ usage?: { inputTokens: number; outputTokens: number; totalTokens: number };
92
+ }
93
+
83
94
  /**
84
95
  * Walks the configured Gemini OAuth providers in deterministic order and
85
96
  * returns the first one that yields a usable access token + projectId via
@@ -128,22 +139,141 @@ interface GeminiGroundingMetadata {
128
139
  webSearchQueries?: string[];
129
140
  }
130
141
 
131
- interface CloudCodeResponseChunk {
132
- response?: {
133
- candidates?: Array<{
134
- content?: {
135
- role: string;
136
- parts?: Array<{ text?: string }>;
137
- };
138
- finishReason?: string;
139
- groundingMetadata?: GeminiGroundingMetadata;
140
- }>;
141
- usageMetadata?: {
142
- promptTokenCount?: number;
143
- candidatesTokenCount?: number;
144
- totalTokenCount?: number;
142
+ interface GeminiModelResponse {
143
+ candidates?: Array<{
144
+ content?: {
145
+ role: string;
146
+ parts?: Array<{ text?: string }>;
145
147
  };
146
- modelVersion?: string;
148
+ finishReason?: string;
149
+ groundingMetadata?: GeminiGroundingMetadata;
150
+ }>;
151
+ usageMetadata?: {
152
+ promptTokenCount?: number;
153
+ candidatesTokenCount?: number;
154
+ totalTokenCount?: number;
155
+ };
156
+ modelVersion?: string;
157
+ }
158
+
159
+ interface CloudCodeResponseChunk {
160
+ response?: GeminiModelResponse;
161
+ }
162
+
163
+ async function parseGeminiSearchStream(body: ReadableStream<Uint8Array>): Promise<GeminiSearchResult> {
164
+ const answerParts: string[] = [];
165
+ const sources: SearchSource[] = [];
166
+ const citations: SearchCitation[] = [];
167
+ const searchQueries: string[] = [];
168
+ const seenUrls = new Set<string>();
169
+ let model = DEFAULT_MODEL;
170
+ let usage: { inputTokens: number; outputTokens: number; totalTokens: number } | undefined;
171
+
172
+ const reader = body.getReader();
173
+ const decoder = new TextDecoder();
174
+ let buffer = "";
175
+
176
+ try {
177
+ while (true) {
178
+ const { done, value } = await reader.read();
179
+ if (done) break;
180
+
181
+ buffer += decoder.decode(value, { stream: true });
182
+ const lines = buffer.split("\n");
183
+ buffer = lines.pop() || "";
184
+
185
+ for (const line of lines) {
186
+ if (!line.startsWith("data:")) continue;
187
+
188
+ const jsonStr = line.slice(5).trim();
189
+ if (!jsonStr) continue;
190
+
191
+ let chunk: CloudCodeResponseChunk & GeminiModelResponse;
192
+ try {
193
+ chunk = JSON.parse(jsonStr) as CloudCodeResponseChunk & GeminiModelResponse;
194
+ } catch {
195
+ continue;
196
+ }
197
+
198
+ const responseData = chunk.response ?? chunk;
199
+ const candidate = responseData.candidates?.[0];
200
+
201
+ if (candidate?.content?.parts) {
202
+ for (const part of candidate.content.parts) {
203
+ if (part.text) {
204
+ answerParts.push(part.text);
205
+ }
206
+ }
207
+ }
208
+
209
+ const groundingMetadata = candidate?.groundingMetadata;
210
+ if (groundingMetadata) {
211
+ if (groundingMetadata.groundingChunks) {
212
+ for (const grChunk of groundingMetadata.groundingChunks) {
213
+ if (grChunk.web?.uri) {
214
+ const sourceUrl = grChunk.web.uri;
215
+ if (!seenUrls.has(sourceUrl)) {
216
+ seenUrls.add(sourceUrl);
217
+ sources.push({
218
+ title: grChunk.web.title ?? sourceUrl,
219
+ url: sourceUrl,
220
+ });
221
+ }
222
+ }
223
+ }
224
+ }
225
+
226
+ if (groundingMetadata.groundingSupports && groundingMetadata.groundingChunks) {
227
+ for (const support of groundingMetadata.groundingSupports) {
228
+ const citedText = support.segment?.text;
229
+ const chunkIndices = support.groundingChunkIndices ?? [];
230
+
231
+ for (const idx of chunkIndices) {
232
+ const grChunk = groundingMetadata.groundingChunks[idx];
233
+ if (grChunk?.web?.uri) {
234
+ citations.push({
235
+ url: grChunk.web.uri,
236
+ title: grChunk.web.title ?? grChunk.web.uri,
237
+ citedText,
238
+ });
239
+ }
240
+ }
241
+ }
242
+ }
243
+
244
+ if (groundingMetadata.webSearchQueries) {
245
+ for (const q of groundingMetadata.webSearchQueries) {
246
+ if (!searchQueries.includes(q)) {
247
+ searchQueries.push(q);
248
+ }
249
+ }
250
+ }
251
+ }
252
+
253
+ if (responseData.usageMetadata) {
254
+ usage = {
255
+ inputTokens: responseData.usageMetadata.promptTokenCount ?? 0,
256
+ outputTokens: responseData.usageMetadata.candidatesTokenCount ?? 0,
257
+ totalTokens: responseData.usageMetadata.totalTokenCount ?? 0,
258
+ };
259
+ }
260
+
261
+ if (responseData.modelVersion) {
262
+ model = responseData.modelVersion;
263
+ }
264
+ }
265
+ }
266
+ } finally {
267
+ reader.releaseLock();
268
+ }
269
+
270
+ return {
271
+ answer: answerParts.join(""),
272
+ sources,
273
+ citations,
274
+ searchQueries,
275
+ model,
276
+ usage,
147
277
  };
148
278
  }
149
279
 
@@ -165,14 +295,7 @@ async function callGeminiSearch(
165
295
  fetchImpl: FetchImpl | undefined,
166
296
  signal: AbortSignal | undefined,
167
297
  mode?: "auto" | "production" | "sandbox",
168
- ): Promise<{
169
- answer: string;
170
- sources: SearchSource[];
171
- citations: SearchCitation[];
172
- searchQueries: string[];
173
- model: string;
174
- usage?: { inputTokens: number; outputTokens: number; totalTokens: number };
175
- }> {
298
+ ): Promise<GeminiSearchResult> {
176
299
  let endpoints: string[];
177
300
  if (auth.isAntigravity) {
178
301
  const m = mode ?? "auto";
@@ -287,13 +410,75 @@ async function callGeminiSearch(
287
410
  throw new SearchProviderError("gemini", `Gemini Cloud Code API error (${status}): ${errorText}`, status);
288
411
  }
289
412
 
413
+ if (!response.body) {
414
+ throw new SearchProviderError("gemini", "Gemini API returned no response body", 500);
415
+ }
416
+
417
+ return parseGeminiSearchStream(response.body);
418
+ }
419
+
420
+ async function callGeminiDeveloperSearch(
421
+ apiKey: string,
422
+ query: string,
423
+ systemPrompt: string | undefined,
424
+ maxOutputTokens: number | undefined,
425
+ temperature: number | undefined,
426
+ toolParams: GeminiToolParams,
427
+ fetchImpl: FetchImpl | undefined,
428
+ signal: AbortSignal | undefined,
429
+ ): Promise<GeminiSearchResult> {
430
+ const normalizedSystemPrompt = systemPrompt?.toWellFormed();
431
+ const requestBody: Record<string, unknown> = {
432
+ contents: [
433
+ {
434
+ role: "user",
435
+ parts: [{ text: query }],
436
+ },
437
+ ],
438
+ tools: buildGeminiRequestTools(toolParams),
439
+ ...(normalizedSystemPrompt && {
440
+ systemInstruction: {
441
+ parts: [{ text: normalizedSystemPrompt }],
442
+ },
443
+ }),
444
+ };
445
+
446
+ if (maxOutputTokens !== undefined || temperature !== undefined) {
447
+ const generationConfig: Record<string, number> = {};
448
+ if (maxOutputTokens !== undefined) {
449
+ generationConfig.maxOutputTokens = maxOutputTokens;
450
+ }
451
+ if (temperature !== undefined) {
452
+ generationConfig.temperature = temperature;
453
+ }
454
+ requestBody.generationConfig = generationConfig;
455
+ }
456
+
457
+ const response = await fetchWithRetry(
458
+ () => `${DEVELOPER_API_ENDPOINT}/models/${DEFAULT_MODEL}:streamGenerateContent?alt=sse`,
459
+ {
460
+ method: "POST",
461
+ headers: {
462
+ "x-goog-api-key": apiKey,
463
+ "Content-Type": "application/json",
464
+ Accept: "text/event-stream",
465
+ },
466
+ body: JSON.stringify(requestBody),
467
+ signal: withHardTimeout(signal),
468
+ fetch: fetchImpl,
469
+ maxAttempts: MAX_RETRIES + 1,
470
+ defaultDelayMs: attempt => BASE_DELAY_MS * 2 ** attempt,
471
+ maxDelayMs: RATE_LIMIT_BUDGET_MS,
472
+ },
473
+ );
474
+
290
475
  if (!response.ok) {
291
476
  const errorText = await response.text();
292
477
  const classified = classifyProviderHttpError("gemini", response.status, errorText);
293
478
  if (classified) throw classified;
294
479
  throw new SearchProviderError(
295
480
  "gemini",
296
- `Gemini Cloud Code API error (${response.status}): ${errorText}`,
481
+ `Gemini Developer API error (${response.status}): ${errorText}`,
297
482
  response.status,
298
483
  );
299
484
  }
@@ -302,130 +487,7 @@ async function callGeminiSearch(
302
487
  throw new SearchProviderError("gemini", "Gemini API returned no response body", 500);
303
488
  }
304
489
 
305
- // Parse SSE stream
306
- const answerParts: string[] = [];
307
- const sources: SearchSource[] = [];
308
- const citations: SearchCitation[] = [];
309
- const searchQueries: string[] = [];
310
- const seenUrls = new Set<string>();
311
- let model = DEFAULT_MODEL;
312
- let usage: { inputTokens: number; outputTokens: number; totalTokens: number } | undefined;
313
-
314
- const reader = response.body.getReader();
315
- const decoder = new TextDecoder();
316
- let buffer = "";
317
-
318
- try {
319
- while (true) {
320
- const { done, value } = await reader.read();
321
- if (done) break;
322
-
323
- buffer += decoder.decode(value, { stream: true });
324
- const lines = buffer.split("\n");
325
- buffer = lines.pop() || "";
326
-
327
- for (const line of lines) {
328
- if (!line.startsWith("data:")) continue;
329
-
330
- const jsonStr = line.slice(5).trim();
331
- if (!jsonStr) continue;
332
-
333
- let chunk: CloudCodeResponseChunk;
334
- try {
335
- chunk = JSON.parse(jsonStr) as CloudCodeResponseChunk;
336
- } catch {
337
- continue;
338
- }
339
-
340
- const responseData = chunk.response;
341
- if (!responseData) continue;
342
-
343
- const candidate = responseData.candidates?.[0];
344
-
345
- // Extract text content
346
- if (candidate?.content?.parts) {
347
- for (const part of candidate.content.parts) {
348
- if (part.text) {
349
- answerParts.push(part.text);
350
- }
351
- }
352
- }
353
-
354
- // Extract grounding metadata
355
- const groundingMetadata = candidate?.groundingMetadata;
356
- if (groundingMetadata) {
357
- // Extract sources from grounding chunks
358
- if (groundingMetadata.groundingChunks) {
359
- for (const grChunk of groundingMetadata.groundingChunks) {
360
- if (grChunk.web?.uri) {
361
- const sourceUrl = grChunk.web.uri;
362
- if (!seenUrls.has(sourceUrl)) {
363
- seenUrls.add(sourceUrl);
364
- sources.push({
365
- title: grChunk.web.title ?? sourceUrl,
366
- url: sourceUrl,
367
- });
368
- }
369
- }
370
- }
371
- }
372
-
373
- // Extract citations from grounding supports
374
- if (groundingMetadata.groundingSupports && groundingMetadata.groundingChunks) {
375
- for (const support of groundingMetadata.groundingSupports) {
376
- const citedText = support.segment?.text;
377
- const chunkIndices = support.groundingChunkIndices ?? [];
378
-
379
- for (const idx of chunkIndices) {
380
- const grChunk = groundingMetadata.groundingChunks[idx];
381
- if (grChunk?.web?.uri) {
382
- citations.push({
383
- url: grChunk.web.uri,
384
- title: grChunk.web.title ?? grChunk.web.uri,
385
- citedText,
386
- });
387
- }
388
- }
389
- }
390
- }
391
-
392
- // Extract search queries
393
- if (groundingMetadata.webSearchQueries) {
394
- for (const q of groundingMetadata.webSearchQueries) {
395
- if (!searchQueries.includes(q)) {
396
- searchQueries.push(q);
397
- }
398
- }
399
- }
400
- }
401
-
402
- // Extract usage metadata
403
- if (responseData.usageMetadata) {
404
- usage = {
405
- inputTokens: responseData.usageMetadata.promptTokenCount ?? 0,
406
- outputTokens: responseData.usageMetadata.candidatesTokenCount ?? 0,
407
- totalTokens: responseData.usageMetadata.totalTokenCount ?? 0,
408
- };
409
- }
410
-
411
- // Extract model version
412
- if (responseData.modelVersion) {
413
- model = responseData.modelVersion;
414
- }
415
- }
416
- }
417
- } finally {
418
- reader.releaseLock();
419
- }
420
-
421
- return {
422
- answer: answerParts.join(""),
423
- sources,
424
- citations,
425
- searchQueries,
426
- model,
427
- usage,
428
- };
490
+ return parseGeminiSearchStream(response.body);
429
491
  }
430
492
 
431
493
  /**
@@ -433,43 +495,64 @@ async function callGeminiSearch(
433
495
  */
434
496
  export async function searchGemini(params: GeminiSearchParams): Promise<SearchResponse> {
435
497
  const seed = await findGeminiAuth(params.authStorage, params.sessionId, params.signal);
436
- if (!seed) {
437
- throw new Error(
438
- "No Gemini OAuth credentials found. Login with 'omp /login google-gemini-cli' or 'omp /login google-antigravity' to enable Gemini web search.",
498
+ let result: GeminiSearchResult;
499
+
500
+ if (seed) {
501
+ const isAntigravity = seed.provider === "google-antigravity";
502
+ result = await withOAuthAccess(
503
+ params.authStorage,
504
+ seed.provider,
505
+ access =>
506
+ // Derive bearer + projectId from the access this attempt received; a
507
+ // re-resolved access may omit projectId, in which case the seed's
508
+ // project is still the right tenant for the credential. The
509
+ // `fetchWithRetry` transport backoff stays INSIDE this attempt — auth
510
+ callGeminiSearch(
511
+ {
512
+ accessToken: access.accessToken,
513
+ projectId: access.projectId ?? seed.projectId,
514
+ isAntigravity,
515
+ },
516
+ params.query,
517
+ params.system_prompt,
518
+ params.max_output_tokens,
519
+ params.temperature,
520
+ {
521
+ google_search: params.google_search,
522
+ code_execution: params.code_execution,
523
+ url_context: params.url_context,
524
+ },
525
+ params.fetch,
526
+ params.signal,
527
+ params.antigravityEndpointMode,
528
+ ),
529
+ { sessionId: params.sessionId, signal: params.signal, seed: seed.access },
530
+ );
531
+ } else {
532
+ const apiKey = await params.authStorage.getApiKey(DEVELOPER_API_PROVIDER, params.sessionId, {
533
+ signal: params.signal,
534
+ });
535
+ if (!apiKey) {
536
+ throw new Error(
537
+ "No Gemini credentials found. Set GEMINI_API_KEY, configure an API key for provider \"google\", or login with 'omp /login google-gemini-cli' / 'omp /login google-antigravity' to enable Gemini web search.",
538
+ );
539
+ }
540
+ result = await callGeminiDeveloperSearch(
541
+ apiKey,
542
+ params.query,
543
+ params.system_prompt,
544
+ params.max_output_tokens,
545
+ params.temperature,
546
+ {
547
+ google_search: params.google_search,
548
+ code_execution: params.code_execution,
549
+ url_context: params.url_context,
550
+ },
551
+ params.fetch,
552
+ params.signal,
439
553
  );
440
554
  }
441
555
 
442
- const isAntigravity = seed.provider === "google-antigravity";
443
- const result = await withOAuthAccess(
444
- params.authStorage,
445
- seed.provider,
446
- access =>
447
- // Derive bearer + projectId from the access this attempt received; a
448
- // re-resolved access may omit projectId, in which case the seed's
449
- // project is still the right tenant for the credential. The
450
- // `fetchWithRetry` transport backoff stays INSIDE this attempt — auth
451
- callGeminiSearch(
452
- {
453
- accessToken: access.accessToken,
454
- projectId: access.projectId ?? seed.projectId,
455
- isAntigravity,
456
- },
457
- params.query,
458
- params.system_prompt,
459
- params.max_output_tokens,
460
- params.temperature,
461
- {
462
- google_search: params.google_search,
463
- code_execution: params.code_execution,
464
- url_context: params.url_context,
465
- },
466
- params.fetch,
467
- params.signal,
468
- params.antigravityEndpointMode,
469
- ),
470
- { sessionId: params.sessionId, signal: params.signal, seed: seed.access },
471
- );
472
-
473
556
  let sources = result.sources;
474
557
 
475
558
  if (params.num_results && sources.length > params.num_results) {
@@ -494,9 +577,9 @@ export class GeminiProvider extends SearchProvider {
494
577
 
495
578
  isAvailable(authStorage: AuthStorage): boolean {
496
579
  // Cheap, in-memory check — avoids driving the refresh pipeline during
497
- // the provider-chain probe. `searchGemini` calls `getOAuthAccess` which
498
- // will refresh lazily on the actual request.
499
- return hasGeminiOAuth(authStorage);
580
+ // the provider-chain probe. `searchGemini` refreshes OAuth lazily on the
581
+ // actual request and resolves developer API keys through AuthStorage.
582
+ return hasGeminiOAuth(authStorage) || authStorage.hasAuth(DEVELOPER_API_PROVIDER);
500
583
  }
501
584
 
502
585
  search(params: SearchParams): Promise<SearchResponse> {
@@ -43,7 +43,7 @@ export const SEARCH_PROVIDER_OPTIONS = [
43
43
  { value: "parallel", label: "Parallel", description: "Requires PARALLEL_API_KEY" },
44
44
  { value: "synthetic", label: "Synthetic", description: "Requires SYNTHETIC_API_KEY" },
45
45
  { value: "searxng", label: "SearXNG", description: "Requires SEARXNG_ENDPOINT or searxng.endpoint" },
46
- { value: "duckduckgo", label: "DuckDuckGo", description: "Uses DuckDuckGo Instant Answer API (no API key)" },
46
+ { value: "duckduckgo", label: "DuckDuckGo", description: "Scrapes the DuckDuckGo HTML frontend (no API key)" },
47
47
  ] as const;
48
48
 
49
49
  /** Supported web search providers (every option except `auto`). */