@oh-my-pi/pi-coding-agent 15.11.3 → 15.11.4

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 (103) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/cli.js +353 -294
  3. package/dist/types/config/api-key-resolver.d.ts +9 -3
  4. package/dist/types/config/keybindings.d.ts +1 -1
  5. package/dist/types/config/model-discovery.d.ts +6 -4
  6. package/dist/types/config/model-registry.d.ts +7 -4
  7. package/dist/types/config/settings-schema.d.ts +458 -155
  8. package/dist/types/export/html/template.generated.d.ts +1 -1
  9. package/dist/types/mnemopi/config.d.ts +3 -1
  10. package/dist/types/modes/components/settings-defs.d.ts +9 -2
  11. package/dist/types/modes/components/settings-selector.d.ts +9 -4
  12. package/dist/types/modes/components/tool-execution.d.ts +12 -1
  13. package/dist/types/modes/components/transcript-container.d.ts +12 -0
  14. package/dist/types/modes/controllers/input-controller.d.ts +9 -1
  15. package/dist/types/modes/theme/theme.d.ts +23 -3
  16. package/dist/types/session/agent-session.d.ts +14 -7
  17. package/dist/types/session/auth-storage.d.ts +1 -1
  18. package/dist/types/session/snapcompact-inline.d.ts +28 -0
  19. package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +14 -0
  20. package/dist/types/system-prompt.d.ts +3 -1
  21. package/dist/types/task/render.d.ts +16 -6
  22. package/dist/types/tools/gh.d.ts +3 -0
  23. package/dist/types/tools/render-utils.d.ts +8 -16
  24. package/dist/types/utils/session-color.d.ts +15 -3
  25. package/dist/types/web/kagi.d.ts +1 -2
  26. package/dist/types/web/search/providers/codex.d.ts +1 -1
  27. package/dist/types/web/search/providers/gemini.d.ts +9 -6
  28. package/package.json +11 -11
  29. package/src/auto-thinking/classifier.ts +1 -5
  30. package/src/commit/model-selection.ts +3 -6
  31. package/src/config/api-key-resolver.ts +10 -3
  32. package/src/config/keybindings.ts +1 -1
  33. package/src/config/model-discovery.ts +60 -46
  34. package/src/config/model-registry.ts +21 -8
  35. package/src/config/model-resolver.ts +57 -3
  36. package/src/config/settings-schema.ts +601 -153
  37. package/src/eval/completion-bridge.ts +1 -5
  38. package/src/export/html/template.generated.ts +1 -1
  39. package/src/export/html/template.js +13 -6
  40. package/src/internal-urls/docs-index.generated.ts +5 -5
  41. package/src/internal-urls/issue-pr-protocol.ts +10 -4
  42. package/src/memories/index.ts +2 -10
  43. package/src/mnemopi/backend.ts +30 -8
  44. package/src/mnemopi/config.ts +6 -1
  45. package/src/mnemopi/state.ts +6 -0
  46. package/src/modes/components/extensions/inspector-panel.ts +6 -2
  47. package/src/modes/components/plan-review-overlay.ts +15 -17
  48. package/src/modes/components/plugin-settings.ts +22 -5
  49. package/src/modes/components/settings-defs.ts +19 -4
  50. package/src/modes/components/settings-selector.ts +493 -93
  51. package/src/modes/components/status-line/component.ts +3 -1
  52. package/src/modes/components/status-line/segments.ts +3 -1
  53. package/src/modes/components/tool-execution.ts +69 -12
  54. package/src/modes/components/transcript-container.ts +26 -0
  55. package/src/modes/components/tree-selector.ts +16 -6
  56. package/src/modes/controllers/command-controller.ts +37 -7
  57. package/src/modes/controllers/event-controller.ts +1 -0
  58. package/src/modes/controllers/input-controller.ts +68 -6
  59. package/src/modes/controllers/selector-controller.ts +81 -61
  60. package/src/modes/interactive-mode.ts +4 -2
  61. package/src/modes/rpc/rpc-mode.ts +2 -1
  62. package/src/modes/shared.ts +2 -0
  63. package/src/modes/theme/theme.ts +100 -7
  64. package/src/modes/utils/context-usage.ts +3 -1
  65. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  66. package/src/modes/utils/ui-helpers.ts +9 -5
  67. package/src/prompts/system/personalities/default.md +26 -0
  68. package/src/prompts/system/personalities/friendly.md +17 -0
  69. package/src/prompts/system/personalities/pragmatic.md +15 -0
  70. package/src/prompts/system/snapcompact-system-frames-note.md +1 -0
  71. package/src/prompts/system/snapcompact-system-stub.md +1 -0
  72. package/src/prompts/system/snapcompact-toolresult-note.md +1 -0
  73. package/src/prompts/system/system-prompt.md +5 -22
  74. package/src/prompts/tools/task.md +3 -3
  75. package/src/sdk.ts +22 -1
  76. package/src/session/agent-session.ts +91 -24
  77. package/src/session/auth-storage.ts +1 -0
  78. package/src/session/session-dump-format.ts +8 -1
  79. package/src/session/session-manager.ts +5 -5
  80. package/src/session/snapcompact-inline.ts +187 -0
  81. package/src/slash-commands/helpers/active-oauth-account.ts +44 -0
  82. package/src/slash-commands/helpers/usage-report.ts +24 -3
  83. package/src/system-prompt.ts +15 -1
  84. package/src/task/render.ts +29 -19
  85. package/src/tool-discovery/tool-index.ts +2 -0
  86. package/src/tools/bash.ts +10 -3
  87. package/src/tools/eval-render.ts +13 -8
  88. package/src/tools/gh.ts +39 -1
  89. package/src/tools/image-gen.ts +114 -78
  90. package/src/tools/inspect-image.ts +1 -5
  91. package/src/tools/job.ts +25 -5
  92. package/src/tools/read.ts +1 -57
  93. package/src/tools/render-utils.ts +29 -31
  94. package/src/tools/ssh.ts +3 -3
  95. package/src/tools/tts.ts +40 -20
  96. package/src/utils/clipboard.ts +56 -4
  97. package/src/utils/commit-message-generator.ts +1 -5
  98. package/src/utils/session-color.ts +83 -9
  99. package/src/utils/title-generator.ts +1 -1
  100. package/src/web/kagi.ts +26 -27
  101. package/src/web/search/providers/codex.ts +42 -40
  102. package/src/web/search/providers/gemini.ts +42 -22
  103. package/src/web/search/providers/perplexity.ts +22 -10
package/src/web/kagi.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * through the shared {@link AuthStorage} broker (Bearer token), and responses
7
7
  * are categorized result buckets rather than the legacy flat object array.
8
8
  */
9
- import type { AuthStorage, FetchImpl } from "@oh-my-pi/pi-ai";
9
+ import { type AuthStorage, type FetchImpl, withAuth } from "@oh-my-pi/pi-ai";
10
10
  import { withHardTimeout } from "./search/providers/utils";
11
11
 
12
12
  const KAGI_SEARCH_URL = "https://kagi.com/api/v1/search";
@@ -173,14 +173,6 @@ export interface KagiSearchResult {
173
173
  answer?: string;
174
174
  }
175
175
 
176
- export async function findKagiApiKey(
177
- authStorage: AuthStorage,
178
- sessionId?: string,
179
- signal?: AbortSignal,
180
- ): Promise<string | null> {
181
- return (await authStorage.getApiKey("kagi", sessionId, { signal })) ?? null;
182
- }
183
-
184
176
  /**
185
177
  * Compute a YYYY-MM-DD date string `recency` units before now, in UTC.
186
178
  * UTC keeps the recency window deterministic regardless of host timezone and
@@ -247,27 +239,34 @@ export async function searchWithKagi(
247
239
  options: KagiSearchOptions = {},
248
240
  authStorage: AuthStorage,
249
241
  ): Promise<KagiSearchResult> {
250
- const apiKey = await findKagiApiKey(authStorage, options.sessionId, options.signal);
251
- if (!apiKey) {
252
- throw new KagiApiError("Kagi credentials not found. Set KAGI_API_KEY or login with 'omp /login kagi'.");
253
- }
254
-
255
242
  const fetchImpl = options.fetch ?? fetch;
243
+ const body = JSON.stringify(buildRequestBody(query, options));
244
+
245
+ const response = await withAuth(
246
+ authStorage.resolver("kagi", { sessionId: options.sessionId }),
247
+ async apiKey => {
248
+ const res = await fetchImpl(KAGI_SEARCH_URL, {
249
+ method: "POST",
250
+ headers: {
251
+ Authorization: `Bearer ${apiKey}`,
252
+ "Content-Type": "application/json",
253
+ Accept: "application/json",
254
+ },
255
+ body,
256
+ signal: withHardTimeout(options.signal),
257
+ });
258
+
259
+ if (!res.ok) {
260
+ throw parseKagiErrorResponse(res.status, await res.text());
261
+ }
256
262
 
257
- const response = await fetchImpl(KAGI_SEARCH_URL, {
258
- method: "POST",
259
- headers: {
260
- Authorization: `Bearer ${apiKey}`,
261
- "Content-Type": "application/json",
262
- Accept: "application/json",
263
+ return res;
263
264
  },
264
- body: JSON.stringify(buildRequestBody(query, options)),
265
- signal: withHardTimeout(options.signal),
266
- });
267
-
268
- if (!response.ok) {
269
- throw parseKagiErrorResponse(response.status, await response.text());
270
- }
265
+ {
266
+ signal: options.signal,
267
+ missingKeyMessage: "Kagi credentials not found. Set KAGI_API_KEY or login with 'omp /login kagi'.",
268
+ },
269
+ );
271
270
 
272
271
  const payload = (await response.json()) as KagiSearchResponse;
273
272
  if (payload.error && payload.error.length > 0) {
@@ -7,7 +7,7 @@
7
7
  * SQLite store, never POSTs the broker sentinel to an OpenAI token endpoint.
8
8
  */
9
9
  import * as os from "node:os";
10
- import type { AuthStorage, FetchImpl } from "@oh-my-pi/pi-ai";
10
+ import { type AuthStorage, type FetchImpl, type OAuthAccess, withOAuthAccess } from "@oh-my-pi/pi-ai";
11
11
  import { decodeJwt } from "@oh-my-pi/pi-ai/oauth/openai-codex";
12
12
  import { getBundledModels } from "@oh-my-pi/pi-catalog/models";
13
13
  import { $env, readSseJson } from "@oh-my-pi/pi-utils";
@@ -287,12 +287,12 @@ async function findCodexAuth(
287
287
  authStorage: AuthStorage,
288
288
  sessionId: string | undefined,
289
289
  signal: AbortSignal | undefined,
290
- ): Promise<{ accessToken: string; accountId: string } | null> {
290
+ ): Promise<{ access: OAuthAccess; accountId: string } | null> {
291
291
  const access = await authStorage.getOAuthAccess("openai-codex", sessionId, { signal });
292
292
  if (!access) return null;
293
293
  const accountId = access.accountId ?? getAccountIdFromJwt(access.accessToken);
294
294
  if (!accountId) return null;
295
- return { accessToken: access.accessToken, accountId };
295
+ return { access, accountId };
296
296
  }
297
297
 
298
298
  /**
@@ -495,8 +495,8 @@ async function callCodexSearch(
495
495
  * `gpt-5-codex-mini` first on ChatGPT accounts, which OpenAI rejects.
496
496
  */
497
497
  export async function searchCodex(params: SearchParams): Promise<SearchResponse> {
498
- const auth = await findCodexAuth(params.authStorage, params.sessionId, params.signal);
499
- if (!auth) {
498
+ const seed = await findCodexAuth(params.authStorage, params.sessionId, params.signal);
499
+ if (!seed) {
500
500
  throw new Error(
501
501
  "No Codex OAuth credentials found. Login with 'omp /login openai-codex' to enable Codex web search.",
502
502
  );
@@ -505,42 +505,44 @@ export async function searchCodex(params: SearchParams): Promise<SearchResponse>
505
505
  const configuredModel = getConfiguredModel();
506
506
  const modelCandidates = configuredModel ? [configuredModel] : getDefaultModelCandidates();
507
507
 
508
- let result:
509
- | {
510
- answer: string;
511
- sources: SearchSource[];
512
- model: string;
513
- requestId: string;
514
- usage?: { inputTokens: number; outputTokens: number; totalTokens: number };
515
- }
516
- | undefined;
517
- let lastError: unknown;
518
-
519
- for (let index = 0; index < modelCandidates.length; index += 1) {
520
- const modelId = modelCandidates[index];
521
- if (!modelId) continue;
522
-
523
- try {
524
- result = await callCodexSearch(auth, params.query, {
525
- signal: params.signal,
526
- systemPrompt: params.systemPrompt,
527
- searchContextSize: "high",
528
- modelId,
529
- fetch: params.fetch,
530
- });
531
- break;
532
- } catch (error) {
533
- lastError = error;
534
- const isLastCandidate = index === modelCandidates.length - 1;
535
- if (configuredModel || isLastCandidate || !shouldRetryWithNextDefaultModel(error)) {
536
- throw error;
508
+ const result = await withOAuthAccess(
509
+ params.authStorage,
510
+ "openai-codex",
511
+ async access => {
512
+ // Derive ALL auth material from the access this attempt received —
513
+ // a refreshed/rotated credential carries a different bearer and
514
+ // ChatGPT account id than the seed.
515
+ const accountId = access.accountId ?? getAccountIdFromJwt(access.accessToken);
516
+ if (!accountId) {
517
+ throw new Error("Codex OAuth credential is missing a ChatGPT account id");
537
518
  }
538
- }
539
- }
540
-
541
- if (!result) {
542
- throw lastError ?? new Error("Codex search failed without returning a result");
543
- }
519
+ const auth = { accessToken: access.accessToken, accountId };
520
+
521
+ let lastError: unknown;
522
+ for (let index = 0; index < modelCandidates.length; index += 1) {
523
+ const modelId = modelCandidates[index];
524
+ if (!modelId) continue;
525
+
526
+ try {
527
+ return await callCodexSearch(auth, params.query, {
528
+ signal: params.signal,
529
+ systemPrompt: params.systemPrompt,
530
+ searchContextSize: "high",
531
+ modelId,
532
+ fetch: params.fetch,
533
+ });
534
+ } catch (error) {
535
+ lastError = error;
536
+ const isLastCandidate = index === modelCandidates.length - 1;
537
+ if (configuredModel || isLastCandidate || !shouldRetryWithNextDefaultModel(error)) {
538
+ throw error;
539
+ }
540
+ }
541
+ }
542
+ throw lastError ?? new Error("Codex search failed without returning a result");
543
+ },
544
+ { sessionId: params.sessionId, signal: params.signal, seed: seed.access },
545
+ );
544
546
 
545
547
  let sources = result.sources;
546
548
 
@@ -8,7 +8,7 @@
8
8
  * sibling SQLite store and never POSTs the broker sentinel to a Google token
9
9
  * endpoint.
10
10
  */
11
- import type { AuthStorage, FetchImpl } from "@oh-my-pi/pi-ai";
11
+ import { type AuthStorage, type FetchImpl, type OAuthAccess, withOAuthAccess } from "@oh-my-pi/pi-ai";
12
12
  import {
13
13
  ANTIGRAVITY_SYSTEM_INSTRUCTION,
14
14
  getAntigravityUserAgent,
@@ -72,25 +72,29 @@ interface GeminiAuth {
72
72
  isAntigravity: boolean;
73
73
  }
74
74
 
75
+ /** First configured Gemini OAuth provider plus its pre-resolved access. */
76
+ interface GeminiAuthSeed {
77
+ provider: GeminiProviderId;
78
+ access: OAuthAccess;
79
+ projectId: string;
80
+ }
81
+
75
82
  /**
76
83
  * Walks the configured Gemini OAuth providers in deterministic order and
77
84
  * returns the first one that yields a usable access token + projectId via
78
85
  * {@link AuthStorage.getOAuthAccess}. AuthStorage handles refresh + broker
79
86
  * routing internally; this helper never touches refresh tokens directly.
87
+ * The resolved access seeds `withOAuthAccess` so the happy path resolves once.
80
88
  */
81
89
  export async function findGeminiAuth(
82
90
  authStorage: AuthStorage,
83
91
  sessionId: string | undefined,
84
92
  signal: AbortSignal | undefined,
85
- ): Promise<GeminiAuth | null> {
93
+ ): Promise<GeminiAuthSeed | null> {
86
94
  for (const provider of GEMINI_PROVIDERS) {
87
95
  const access = await authStorage.getOAuthAccess(provider, sessionId, { signal });
88
96
  if (!access?.accessToken || !access.projectId) continue;
89
- return {
90
- accessToken: access.accessToken,
91
- projectId: access.projectId,
92
- isAntigravity: provider === "google-antigravity",
93
- };
97
+ return { provider, access, projectId: access.projectId };
94
98
  }
95
99
  return null;
96
100
  }
@@ -390,26 +394,42 @@ async function callGeminiSearch(
390
394
  * Executes a web search using Google Gemini with Google Search grounding.
391
395
  */
392
396
  export async function searchGemini(params: GeminiSearchParams): Promise<SearchResponse> {
393
- const auth = await findGeminiAuth(params.authStorage, params.sessionId, params.signal);
394
- if (!auth) {
397
+ const seed = await findGeminiAuth(params.authStorage, params.sessionId, params.signal);
398
+ if (!seed) {
395
399
  throw new Error(
396
400
  "No Gemini OAuth credentials found. Login with 'omp /login google-gemini-cli' or 'omp /login google-antigravity' to enable Gemini web search.",
397
401
  );
398
402
  }
399
403
 
400
- const result = await callGeminiSearch(
401
- auth,
402
- params.query,
403
- params.system_prompt,
404
- params.max_output_tokens,
405
- params.temperature,
406
- {
407
- google_search: params.google_search,
408
- code_execution: params.code_execution,
409
- url_context: params.url_context,
410
- },
411
- params.fetch,
412
- params.signal,
404
+ const isAntigravity = seed.provider === "google-antigravity";
405
+ const result = await withOAuthAccess(
406
+ params.authStorage,
407
+ seed.provider,
408
+ access =>
409
+ // Derive bearer + projectId from the access this attempt received; a
410
+ // re-resolved access may omit projectId, in which case the seed's
411
+ // project is still the right tenant for the credential. The
412
+ // `fetchWithRetry` transport backoff stays INSIDE this attempt — auth
413
+ // retry wraps transport retry.
414
+ callGeminiSearch(
415
+ {
416
+ accessToken: access.accessToken,
417
+ projectId: access.projectId ?? seed.projectId,
418
+ isAntigravity,
419
+ },
420
+ params.query,
421
+ params.system_prompt,
422
+ params.max_output_tokens,
423
+ params.temperature,
424
+ {
425
+ google_search: params.google_search,
426
+ code_execution: params.code_execution,
427
+ url_context: params.url_context,
428
+ },
429
+ params.fetch,
430
+ params.signal,
431
+ ),
432
+ { sessionId: params.sessionId, signal: params.signal, seed: seed.access },
413
433
  );
414
434
 
415
435
  let sources = result.sources;
@@ -8,7 +8,7 @@
8
8
  * - Anonymous via `www.perplexity.ai/rest/sse/perplexity_ask`
9
9
  */
10
10
 
11
- import { type AuthStorage, type FetchImpl, getEnvApiKey } from "@oh-my-pi/pi-ai";
11
+ import { type AuthStorage, type FetchImpl, getEnvApiKey, type OAuthAccess, withOAuthAccess } from "@oh-my-pi/pi-ai";
12
12
  import { $env, readSseJson } from "@oh-my-pi/pi-utils";
13
13
  import type {
14
14
  PerplexityMessageOutput,
@@ -43,7 +43,7 @@ type PerplexityAuth =
43
43
  }
44
44
  | {
45
45
  type: "oauth";
46
- token: string;
46
+ access: OAuthAccess;
47
47
  }
48
48
  | {
49
49
  type: "cookies";
@@ -302,11 +302,11 @@ function jwtExpiryMs(token: string): number | undefined {
302
302
  }
303
303
  }
304
304
 
305
- async function findOAuthToken(
305
+ async function findOAuthAccess(
306
306
  authStorage: AuthStorage,
307
307
  sessionId: string | undefined,
308
308
  signal: AbortSignal | undefined,
309
- ): Promise<string | null> {
309
+ ): Promise<OAuthAccess | null> {
310
310
  try {
311
311
  // `getOAuthAccess` returns the raw OAuth bearer only — runtime/config
312
312
  // api_key overrides and stored api_key credentials are intentionally
@@ -314,12 +314,12 @@ async function findOAuthToken(
314
314
  // `www.perplexity.ai` session/SSE endpoint.
315
315
  const access = await authStorage.getOAuthAccess("perplexity", sessionId, { signal });
316
316
  const token = access?.accessToken;
317
- if (!token) return null;
317
+ if (!access || !token) return null;
318
318
  // Trust the JWT's own `exp` claim if it has one; otherwise treat as
319
319
  // non-expiring. Perplexity session JWTs commonly omit `exp`.
320
320
  const jwtExpiry = jwtExpiryMs(token);
321
321
  if (jwtExpiry !== undefined && jwtExpiry <= Date.now() + OAUTH_EXPIRY_BUFFER_MS) return null;
322
- return token;
322
+ return access;
323
323
  } catch {
324
324
  return null;
325
325
  }
@@ -339,9 +339,9 @@ async function findPerplexityAuth(
339
339
  const apiKey = findApiKey();
340
340
 
341
341
  // 2. OAuth/session bearer from AuthStorage.
342
- const oauthToken = await findOAuthToken(authStorage, sessionId, signal);
343
- if (oauthToken) {
344
- return { type: "oauth", token: oauthToken };
342
+ const oauthAccess = await findOAuthAccess(authStorage, sessionId, signal);
343
+ if (oauthAccess) {
344
+ return { type: "oauth", access: oauthAccess };
345
345
  }
346
346
 
347
347
  // 3. PERPLEXITY_API_KEY env var
@@ -646,7 +646,19 @@ export async function searchPerplexity(params: PerplexitySearchParams): Promise<
646
646
  const auth = await findPerplexityAuth(params.authStorage, params.sessionId, params.signal);
647
647
 
648
648
  if (auth.type !== "api_key") {
649
- const askResult = await callPerplexityAsk(auth, params);
649
+ // OAuth bearer mode routes the whole authenticated unit (the ask
650
+ // session/SSE request) through the central auth-retry policy so a 401 or
651
+ // usage-limit force-refreshes, then rotates to a sibling credential.
652
+ // Cookie/env/anonymous modes have no rotatable credential — untouched.
653
+ const askResult =
654
+ auth.type === "oauth"
655
+ ? await withOAuthAccess(
656
+ params.authStorage,
657
+ "perplexity",
658
+ access => callPerplexityAsk({ type: "oauth", token: access.accessToken }, params),
659
+ { sessionId: params.sessionId, signal: params.signal, seed: auth.access },
660
+ )
661
+ : await callPerplexityAsk(auth, params);
650
662
  return applySourceLimit(
651
663
  {
652
664
  provider: "perplexity",