@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
@@ -5,7 +5,7 @@
5
5
  * `discoverModelsByProviderType` with a `DiscoveryContext`; built-in provider
6
6
  * discovery lives in pi-catalog's provider-models.
7
7
  */
8
- import type { FetchImpl } from "@oh-my-pi/pi-ai";
8
+ import { type ApiKey, type FetchImpl, withAuth } from "@oh-my-pi/pi-ai";
9
9
  import type { Api, Model } from "@oh-my-pi/pi-ai/types";
10
10
  import { buildModel } from "@oh-my-pi/pi-catalog/build";
11
11
  import {
@@ -97,10 +97,12 @@ export interface DiscoveryContext {
97
97
  /** Injected fetch implementation (tests stub this). */
98
98
  fetch: FetchImpl;
99
99
  /**
100
- * Resolve a provider's API key for `Authorization: Bearer …`. Returns
101
- * undefined when no key is stored or it is a local/no-auth sentinel.
100
+ * Resolve a provider's bearer credential for `Authorization: Bearer …`.
101
+ * Returns undefined when no key is stored or it is a local/no-auth
102
+ * sentinel; otherwise an {@link ApiKey} whose resolver participates in the
103
+ * central force-refresh/rotate auth-retry policy on 401/usage-limit.
102
104
  */
103
- getBearerApiKey(provider: string): Promise<string | undefined>;
105
+ getBearerApiKeyResolver(provider: string): Promise<ApiKey | undefined>;
104
106
  }
105
107
 
106
108
  type OllamaDiscoveredModelMetadata = {
@@ -314,22 +316,26 @@ export async function discoverLlamaCppModels(
314
316
  const baseUrl = normalizeLlamaCppBaseUrl(providerConfig.baseUrl);
315
317
  const modelsUrl = `${baseUrl}/models`;
316
318
 
317
- const headers: Record<string, string> = { ...(providerConfig.headers ?? {}) };
318
- const apiKey = await ctx.getBearerApiKey(providerConfig.provider);
319
- if (apiKey) {
320
- headers.Authorization = `Bearer ${apiKey}`;
321
- }
322
-
323
- const [response, serverMetadata] = await Promise.all([
324
- ctx.fetch(modelsUrl, {
325
- headers,
326
- signal: AbortSignal.timeout(250),
327
- }),
328
- discoverLlamaCppServerMetadata(ctx, baseUrl, headers),
329
- ]);
330
- if (!response.ok) {
331
- throw new Error(`HTTP ${response.status} from ${modelsUrl}`);
332
- }
319
+ const baseHeaders: Record<string, string> = { ...(providerConfig.headers ?? {}) };
320
+ let headers = baseHeaders;
321
+ const attempt = async (h: Record<string, string>) => {
322
+ const [response, metadata] = await Promise.all([
323
+ ctx.fetch(modelsUrl, {
324
+ headers: h,
325
+ signal: AbortSignal.timeout(250),
326
+ }),
327
+ discoverLlamaCppServerMetadata(ctx, baseUrl, h),
328
+ ]);
329
+ if (!response.ok) {
330
+ throw new Error(`HTTP ${response.status} from ${modelsUrl}`);
331
+ }
332
+ headers = h;
333
+ return [response, metadata] as const;
334
+ };
335
+ const apiKey = await ctx.getBearerApiKeyResolver(providerConfig.provider);
336
+ const [response, serverMetadata] = apiKey
337
+ ? await withAuth(apiKey, key => attempt({ ...baseHeaders, Authorization: `Bearer ${key}` }))
338
+ : await attempt(baseHeaders);
333
339
  const payload = (await response.json()) as { data?: Array<{ id: string }> };
334
340
  const models = payload.data ?? [];
335
341
  const discovered: Model<Api>[] = [];
@@ -370,19 +376,23 @@ export async function discoverOpenAIModelsList(
370
376
  const baseUrl = normalizeOpenAIModelsListBaseUrl(providerConfig.baseUrl);
371
377
  const modelsUrl = `${baseUrl}/models`;
372
378
 
373
- const headers: Record<string, string> = { ...(providerConfig.headers ?? {}) };
374
- const apiKey = await ctx.getBearerApiKey(providerConfig.provider);
375
- if (apiKey) {
376
- headers.Authorization = `Bearer ${apiKey}`;
377
- }
378
-
379
- const response = await ctx.fetch(modelsUrl, {
380
- headers,
381
- signal: AbortSignal.timeout(10_000),
382
- });
383
- if (!response.ok) {
384
- throw new Error(`HTTP ${response.status} from ${modelsUrl}`);
385
- }
379
+ const baseHeaders: Record<string, string> = { ...(providerConfig.headers ?? {}) };
380
+ let headers = baseHeaders;
381
+ const attempt = async (h: Record<string, string>) => {
382
+ const res = await ctx.fetch(modelsUrl, {
383
+ headers: h,
384
+ signal: AbortSignal.timeout(10_000),
385
+ });
386
+ if (!res.ok) {
387
+ throw new Error(`HTTP ${res.status} from ${modelsUrl}`);
388
+ }
389
+ headers = h;
390
+ return res;
391
+ };
392
+ const apiKey = await ctx.getBearerApiKeyResolver(providerConfig.provider);
393
+ const response = apiKey
394
+ ? await withAuth(apiKey, key => attempt({ ...baseHeaders, Authorization: `Bearer ${key}` }))
395
+ : await attempt(baseHeaders);
386
396
  const payload = (await response.json()) as { data?: Array<{ id: string }> };
387
397
  const models = payload.data ?? [];
388
398
  const discovered: Model<Api>[] = [];
@@ -435,19 +445,23 @@ export async function discoverProxyModels(
435
445
  const baseUrl = normalizeOpenAIModelsListBaseUrl(providerConfig.baseUrl);
436
446
  const modelsUrl = `${baseUrl}/models`;
437
447
 
438
- const headers: Record<string, string> = { ...(providerConfig.headers ?? {}) };
439
- const apiKey = await ctx.getBearerApiKey(providerConfig.provider);
440
- if (apiKey) {
441
- headers.Authorization = `Bearer ${apiKey}`;
442
- }
443
-
444
- const response = await ctx.fetch(modelsUrl, {
445
- headers,
446
- signal: AbortSignal.timeout(10_000),
447
- });
448
- if (!response.ok) {
449
- throw new Error(`HTTP ${response.status} from ${modelsUrl}`);
450
- }
448
+ const baseHeaders: Record<string, string> = { ...(providerConfig.headers ?? {}) };
449
+ let headers = baseHeaders;
450
+ const attempt = async (h: Record<string, string>) => {
451
+ const res = await ctx.fetch(modelsUrl, {
452
+ headers: h,
453
+ signal: AbortSignal.timeout(10_000),
454
+ });
455
+ if (!res.ok) {
456
+ throw new Error(`HTTP ${res.status} from ${modelsUrl}`);
457
+ }
458
+ headers = h;
459
+ return res;
460
+ };
461
+ const apiKey = await ctx.getBearerApiKeyResolver(providerConfig.provider);
462
+ const response = apiKey
463
+ ? await withAuth(apiKey, key => attempt({ ...baseHeaders, Authorization: `Bearer ${key}` }))
464
+ : await attempt(baseHeaders);
451
465
  const payload = (await response.json()) as {
452
466
  data?: Array<{ id?: string; name?: string; supported_endpoint_types?: string[] }>;
453
467
  };
@@ -57,7 +57,7 @@ import {
57
57
  import { isRecord, logger } from "@oh-my-pi/pi-utils";
58
58
  import { parseModelString, resolveProviderModelReference } from "../config/model-resolver";
59
59
  import type { AuthStorage, OAuthCredential } from "../session/auth-storage";
60
- import { type ApiKeyResolverOptions, createApiKeyResolver } from "./api-key-resolver";
60
+ import { type ApiKeyResolverModel, type ApiKeyResolverOptions, createApiKeyResolver } from "./api-key-resolver";
61
61
  import type { ConfigError, ConfigFile } from "./config-file";
62
62
  import {
63
63
  DISCOVERY_DEFAULT_MAX_TOKENS,
@@ -1238,9 +1238,10 @@ export class ModelRegistry {
1238
1238
  #discoveryContext(): DiscoveryContext {
1239
1239
  return {
1240
1240
  fetch: this.#fetch,
1241
- getBearerApiKey: async provider => {
1241
+ getBearerApiKeyResolver: async provider => {
1242
1242
  const apiKey = await this.getApiKeyForProvider(provider);
1243
- return apiKey && apiKey !== DEFAULT_LOCAL_TOKEN && apiKey !== kNoAuth ? apiKey : undefined;
1243
+ if (!apiKey || apiKey === DEFAULT_LOCAL_TOKEN || apiKey === kNoAuth) return undefined;
1244
+ return this.resolver(provider);
1244
1245
  },
1245
1246
  };
1246
1247
  }
@@ -1754,12 +1755,24 @@ export class ModelRegistry {
1754
1755
  }
1755
1756
 
1756
1757
  /**
1757
- * Build an {@link ApiKeyResolver} for this provider, implementing the
1758
- * central a/b/c auth-retry policy. Callers that need the initial key for
1759
- * a guard can call `resolveApiKeyOnce(resolver)`.
1758
+ * Build an {@link ApiKeyResolver} implementing the central a/b/c auth-retry
1759
+ * policy. Accepts a provider id with options, or a model with an optional
1760
+ * session id (`resolver(model, sessionId)`) which derives `baseUrl`/`modelId`
1761
+ * from the model. Callers that need the initial key for a guard can call
1762
+ * `resolveApiKeyOnce(resolver)`.
1760
1763
  */
1761
- resolver(provider: string, options?: ApiKeyResolverOptions): ApiKeyResolver {
1762
- return createApiKeyResolver(this, provider, options);
1764
+ resolver(provider: string, options?: ApiKeyResolverOptions): ApiKeyResolver;
1765
+ resolver(model: ApiKeyResolverModel, sessionId?: string): ApiKeyResolver;
1766
+ resolver(target: string | ApiKeyResolverModel, optionsOrSessionId?: ApiKeyResolverOptions | string): ApiKeyResolver {
1767
+ const options = typeof optionsOrSessionId === "string" ? { sessionId: optionsOrSessionId } : optionsOrSessionId;
1768
+ if (typeof target === "string") {
1769
+ return createApiKeyResolver(this, target, options);
1770
+ }
1771
+ return createApiKeyResolver(this, target.provider, {
1772
+ ...options,
1773
+ baseUrl: target.baseUrl,
1774
+ modelId: target.id,
1775
+ });
1763
1776
  }
1764
1777
 
1765
1778
  async #peekApiKeyForProvider(provider: string): Promise<string | undefined> {
@@ -633,18 +633,72 @@ function isSessionInheritedAgentPattern(value: string): boolean {
633
633
  return value === DEFAULT_MODEL_ROLE || value === `${PREFIX_MODEL_ROLE}${DEFAULT_MODEL_ROLE}` || value === "pi/task";
634
634
  }
635
635
 
636
- function resolveConfiguredRolePattern(value: string, settings?: Settings): string[] | undefined {
636
+ function shouldInheritDefaultBeforePriority(role: ModelRole): boolean {
637
+ return role === "smol" || role === "slow" || role === "designer";
638
+ }
639
+
640
+ function resolveDefaultInheritedPatterns(
641
+ role: ModelRole,
642
+ configuredDefault: string | undefined,
643
+ roleDefaults: string[],
644
+ settings: Settings | undefined,
645
+ visited: Set<ModelRole>,
646
+ ): string[] {
647
+ if (!shouldInheritDefaultBeforePriority(role) || !configuredDefault) return [];
648
+
649
+ const resolved: string[] = [];
650
+ for (const pattern of normalizeModelPatternList(configuredDefault)) {
651
+ const { base: aliasCandidate, level: thinkingLevel } = splitThinkingSuffix(pattern, PREFIX_MODEL_ROLE.length);
652
+ const aliasRole = getModelRoleAlias(aliasCandidate);
653
+ if (aliasRole === role) {
654
+ // Self-alias (e.g. modelRoles.default = "pi/smol") would loop back to the
655
+ // same unset role; collapse straight to the built-in priority chain.
656
+ resolved.push(
657
+ ...(thinkingLevel
658
+ ? roleDefaults.map(defaultPattern => `${defaultPattern}:${thinkingLevel}`)
659
+ : roleDefaults),
660
+ );
661
+ continue;
662
+ }
663
+ if (aliasRole && !visited.has(aliasRole)) {
664
+ // Cross-role alias (e.g. modelRoles.default = "pi/slow"): resolve the
665
+ // target role's patterns now so downstream one-layer expanders see
666
+ // concrete model patterns instead of another role alias.
667
+ const recursed = resolveConfiguredRolePattern(pattern, settings, new Set(visited));
668
+ if (recursed && recursed.length > 0) {
669
+ resolved.push(...recursed);
670
+ continue;
671
+ }
672
+ }
673
+ resolved.push(pattern);
674
+ }
675
+ return resolved;
676
+ }
677
+
678
+ function resolveConfiguredRolePattern(
679
+ value: string,
680
+ settings?: Settings,
681
+ visited: Set<ModelRole> = new Set(),
682
+ ): string[] | undefined {
637
683
  const normalized = value.trim();
638
684
  if (!normalized) return undefined;
639
685
 
640
686
  const { base: aliasCandidate, level: thinkingLevel } = splitThinkingSuffix(normalized, PREFIX_MODEL_ROLE.length);
641
687
  const role = getModelRoleAlias(aliasCandidate);
642
688
  if (!role) return [normalized];
689
+ if (visited.has(role)) return undefined;
690
+ visited.add(role);
643
691
 
644
692
  const configured = settings?.getModelRole(role)?.trim();
693
+ const configuredDefault = settings?.getModelRole(DEFAULT_MODEL_ROLE)?.trim();
645
694
  const roleDefaults = normalizeModelPatternList(MODEL_PRIO[role as keyof typeof MODEL_PRIO]);
646
- const resolved = configured ? normalizeModelPatternList(configured) : roleDefaults;
647
- if (!resolved || resolved.length === 0) {
695
+ const resolved = configured
696
+ ? normalizeModelPatternList(configured)
697
+ : resolveDefaultInheritedPatterns(role, configuredDefault, roleDefaults, settings, visited);
698
+ if (resolved.length === 0) {
699
+ resolved.push(...roleDefaults);
700
+ }
701
+ if (resolved.length === 0) {
648
702
  return undefined;
649
703
  }
650
704