@oh-my-pi/pi-coding-agent 16.1.16 → 16.1.18

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 (75) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/dist/cli.js +16646 -16608
  3. package/dist/types/auto-thinking/classifier.d.ts +4 -2
  4. package/dist/types/cli/usage-cli.d.ts +14 -0
  5. package/dist/types/commands/token.d.ts +9 -0
  6. package/dist/types/config/model-discovery.d.ts +1 -0
  7. package/dist/types/config/model-registry.d.ts +4 -0
  8. package/dist/types/extensibility/plugins/legacy-pi-bundled-registry.d.ts +7 -0
  9. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +34 -15
  10. package/dist/types/extensibility/plugins/loader.d.ts +16 -9
  11. package/dist/types/extensibility/tool-event-input.d.ts +2 -0
  12. package/dist/types/hindsight/content.d.ts +2 -10
  13. package/dist/types/modes/components/chat-transcript-builder.d.ts +2 -0
  14. package/dist/types/modes/components/custom-editor.d.ts +3 -0
  15. package/dist/types/modes/components/extensions/extension-dashboard.d.ts +26 -10
  16. package/dist/types/modes/components/extensions/extension-list.d.ts +13 -0
  17. package/dist/types/modes/components/status-line/types.d.ts +1 -0
  18. package/dist/types/modes/controllers/command-controller.d.ts +2 -0
  19. package/dist/types/modes/controllers/input-controller.d.ts +14 -0
  20. package/dist/types/modes/controllers/selector-controller.d.ts +11 -0
  21. package/dist/types/session/agent-session.d.ts +6 -6
  22. package/dist/types/session/provider-image-budget.d.ts +3 -0
  23. package/dist/types/session/session-context.d.ts +6 -5
  24. package/dist/types/task/parallel.d.ts +4 -0
  25. package/dist/types/thinking.d.ts +8 -1
  26. package/dist/types/tiny/title-client.d.ts +2 -2
  27. package/dist/types/utils/tools-manager.d.ts +2 -0
  28. package/package.json +12 -12
  29. package/scripts/build-binary.ts +9 -16
  30. package/src/auto-thinking/classifier.ts +7 -2
  31. package/src/cli/profile-alias.ts +38 -7
  32. package/src/cli/usage-cli.ts +40 -5
  33. package/src/commands/token.ts +54 -0
  34. package/src/config/model-discovery.ts +59 -8
  35. package/src/config/model-registry.ts +74 -3
  36. package/src/discovery/omp-extension-roots.ts +1 -3
  37. package/src/extensibility/extensions/wrapper.ts +3 -2
  38. package/src/extensibility/hooks/tool-wrapper.ts +4 -3
  39. package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +40 -0
  40. package/src/extensibility/plugins/legacy-pi-compat.ts +240 -90
  41. package/src/extensibility/plugins/loader.ts +71 -23
  42. package/src/extensibility/plugins/marketplace/manager.ts +134 -0
  43. package/src/extensibility/tool-event-input.ts +57 -0
  44. package/src/hindsight/content.ts +21 -12
  45. package/src/mcp/tool-bridge.ts +27 -2
  46. package/src/mnemopi/state.ts +5 -2
  47. package/src/modes/components/agent-transcript-viewer.ts +193 -41
  48. package/src/modes/components/chat-transcript-builder.ts +6 -0
  49. package/src/modes/components/custom-editor.test.ts +18 -1
  50. package/src/modes/components/custom-editor.ts +77 -45
  51. package/src/modes/components/extensions/extension-dashboard.ts +221 -165
  52. package/src/modes/components/extensions/extension-list.ts +66 -33
  53. package/src/modes/components/hook-editor.ts +15 -2
  54. package/src/modes/components/settings-selector.ts +2 -2
  55. package/src/modes/components/status-line/component.ts +52 -8
  56. package/src/modes/components/status-line/segments.ts +5 -1
  57. package/src/modes/components/status-line/types.ts +1 -0
  58. package/src/modes/components/welcome.ts +12 -14
  59. package/src/modes/controllers/command-controller.ts +16 -5
  60. package/src/modes/controllers/input-controller.ts +115 -3
  61. package/src/modes/controllers/selector-controller.ts +23 -1
  62. package/src/modes/interactive-mode.ts +3 -3
  63. package/src/modes/utils/ui-helpers.ts +3 -3
  64. package/src/sdk.ts +8 -10
  65. package/src/session/agent-session.ts +193 -49
  66. package/src/session/provider-image-budget.ts +86 -0
  67. package/src/session/session-context.ts +14 -7
  68. package/src/session/session-storage.ts +24 -2
  69. package/src/session/snapcompact-inline.ts +19 -3
  70. package/src/slash-commands/builtin-registry.ts +0 -22
  71. package/src/slash-commands/helpers/usage-report.ts +9 -1
  72. package/src/task/parallel.ts +6 -1
  73. package/src/thinking.ts +9 -2
  74. package/src/tiny/title-client.ts +75 -21
  75. package/src/utils/tools-manager.ts +67 -10
@@ -198,37 +198,68 @@ export function resolveProfileAliasCommandFromProcess(
198
198
  if (!runtime || !script || !/\.[cm]?[jt]s$/.test(script)) return DEFAULT_ALIAS_COMMAND;
199
199
 
200
200
  const scriptPath = path.resolve(cwd, script);
201
- const posix = `${quoteForShell(runtime)} ${quoteForShell(scriptPath)}`;
201
+ // Normalize to forward slashes for POSIX shell fields — bash/zsh/fish
202
+ // can't resolve backslash-separated paths, even on Windows (Git Bash, WSL).
203
+ const posixScriptPath = scriptPath.replace(/\\/g, "/");
204
+ const posixRuntime = runtime.replace(/\\/g, "/");
205
+ const posix = `${quoteForShell(posixRuntime)} ${quoteForShell(posixScriptPath)}`;
202
206
  return {
203
- display: `${runtime} ${scriptPath}`,
207
+ display: `${posixRuntime} ${posixScriptPath}`,
204
208
  posix,
205
209
  fish: posix,
206
210
  powerShell: `${quoteForPowerShell(runtime)} ${quoteForPowerShell(scriptPath)}`,
207
211
  };
208
212
  }
209
213
 
214
+ /** Normalize backslashes to forward slashes for POSIX-shell paths.
215
+ * path.posix.join only adds / separators — it preserves existing backslashes
216
+ * in input segments like homeDir ("C:\Users\me"), producing mixed paths.
217
+ * Windows UNC paths (\\server\share) become //server/share — path.posix.join
218
+ * would collapse the leading // to /, so we restore it after joining. */
219
+ function toPosix(p: string): string {
220
+ return p.replace(/\\/g, "/");
221
+ }
222
+
223
+ /** Like path.posix.join, but preserves leading // (UNC roots) which
224
+ * path.posix.join collapses to a single /. */
225
+ function posixJoinUnc(...segments: string[]): string {
226
+ const joined = path.posix.join(...segments);
227
+ // path.posix.join normalizes // at the start to /, breaking UNC roots.
228
+ // Restore it if any input segment started with // (a toPosix'd UNC path).
229
+ if (segments.some(s => s.startsWith("//") && !s.startsWith("///"))) {
230
+ return `/${joined}`;
231
+ }
232
+ return joined;
233
+ }
234
+
210
235
  function resolveShellConfigPath(
211
236
  shell: ProfileAliasShell,
212
237
  homeDir: string,
213
238
  platform: NodeJS.Platform,
214
239
  env: NodeJS.ProcessEnv,
215
240
  ): string {
241
+ // POSIX shells (bash/zsh/fish) always need forward-slash config paths,
242
+ // even on Windows. path.posix.join adds / separators but preserves existing
243
+ // backslashes in input segments, so we normalize each component with toPosix.
244
+ // PowerShell profiles use the platform-native path.join (backslashes on
245
+ // Windows, forward slashes elsewhere).
246
+ const posixHome = toPosix(homeDir);
216
247
  switch (shell) {
217
248
  case "zsh":
218
- return path.join(env.ZDOTDIR || homeDir, ".zshrc");
249
+ return posixJoinUnc(env.ZDOTDIR ? toPosix(env.ZDOTDIR) : posixHome, ".zshrc");
219
250
  case "bash":
220
- return platform === "darwin" ? path.join(homeDir, ".bash_profile") : path.join(homeDir, ".bashrc");
251
+ return platform === "darwin" ? posixJoinUnc(posixHome, ".bash_profile") : posixJoinUnc(posixHome, ".bashrc");
221
252
  case "fish": {
222
253
  // fish sources conf.d from $XDG_CONFIG_HOME/fish (default ~/.config/fish);
223
254
  // a hard-coded ~/.config would be silently ignored when the user relocates
224
255
  // their XDG config root, leaving the alias unsourced after a restart.
225
- const configHome = env.XDG_CONFIG_HOME || path.join(homeDir, ".config");
226
- return path.join(configHome, "fish", "conf.d", "omp-profiles.fish");
256
+ const configHome = env.XDG_CONFIG_HOME ? toPosix(env.XDG_CONFIG_HOME) : posixJoinUnc(posixHome, ".config");
257
+ return posixJoinUnc(configHome, "fish", "conf.d", "omp-profiles.fish");
227
258
  }
228
259
  case "pwsh":
229
260
  return platform === "win32"
230
261
  ? path.join(homeDir, "Documents", "PowerShell", "Microsoft.PowerShell_profile.ps1")
231
- : path.join(homeDir, ".config", "powershell", "Microsoft.PowerShell_profile.ps1");
262
+ : posixJoinUnc(posixHome, ".config", "powershell", "Microsoft.PowerShell_profile.ps1");
232
263
  case "powershell":
233
264
  return path.join(homeDir, "Documents", "WindowsPowerShell", "Microsoft.PowerShell_profile.ps1");
234
265
  }
@@ -15,7 +15,7 @@ import {
15
15
  type UsageReport,
16
16
  type UsageUnit,
17
17
  } from "@oh-my-pi/pi-ai";
18
- import { formatDuration, formatNumber } from "@oh-my-pi/pi-utils";
18
+ import { formatDuration, formatNumber, sanitizeText } from "@oh-my-pi/pi-utils";
19
19
  import chalk from "chalk";
20
20
  import { ModelRegistry } from "../config/model-registry";
21
21
  import { discoverAuthStorage } from "../sdk";
@@ -450,6 +450,10 @@ export function formatUsageBreakdown(
450
450
  lines.push(
451
451
  `${chalk.bold.cyan(formatProviderName(provider))} ${chalk.dim(`— ${accountCount} ${accountCount === 1 ? "account" : "accounts"}`)}`,
452
452
  );
453
+ // Provider-wide disclaimers render once per provider, not per limit.
454
+ const providerNotes = [...new Set(providerReports.flatMap(report => report.notes ?? []))];
455
+ for (const note of providerNotes)
456
+ lines.push(` ${chalk.dim(sanitizeText(note.replace(/[\r\n]+/g, " ").replace(/\t/g, " ")))}`);
453
457
 
454
458
  const labelWidth = providerReports
455
459
  .flatMap(report => report.limits)
@@ -646,6 +650,28 @@ function collectStoredAccounts(authStorage: AuthStorage): UsageAccountIdentity[]
646
650
  return accounts;
647
651
  }
648
652
 
653
+ /**
654
+ * Keep only accounts worth a usage row: those whose provider has a usage
655
+ * provider, so a missing report is a real gap rather than the absence of any
656
+ * usage concept. Providers with no usage endpoint (web-search keys, local /
657
+ * keyless servers, inference providers without a usage API) would only ever
658
+ * render as noise, so they are dropped.
659
+ *
660
+ * `hasUsageProvider` is injected (in practice {@link AuthStorage.usageProviderFor})
661
+ * so custom/broker resolvers stay authoritative — no provider list is duplicated
662
+ * here. An explicit `--provider` request bypasses the cull, so
663
+ * `omp usage --provider xai` can still confirm the stored credential has no
664
+ * usage endpoint.
665
+ */
666
+ export function selectReportableAccounts(
667
+ accounts: UsageAccountIdentity[],
668
+ hasUsageProvider: (provider: string) => boolean,
669
+ explicitProvider?: string,
670
+ ): UsageAccountIdentity[] {
671
+ if (explicitProvider) return accounts;
672
+ return accounts.filter(account => hasUsageProvider(account.provider));
673
+ }
674
+
649
675
  /** Apply a redaction mask to an optional identity field. */
650
676
  function maskIdentity(redaction: Map<string, string>, value: string | undefined): string | undefined {
651
677
  return value === undefined ? undefined : (redaction.get(value) ?? value);
@@ -717,7 +743,12 @@ export async function runUsageCommand(cmd: UsageCommandArgs): Promise<void> {
717
743
  (await authStorage.fetchUsageReports({
718
744
  baseUrlResolver: provider => modelRegistry.getProviderBaseUrl(provider),
719
745
  })) ?? [];
720
- let accounts = collectStoredAccounts(authStorage);
746
+ const storedAccounts = collectStoredAccounts(authStorage);
747
+ let accounts = selectReportableAccounts(
748
+ storedAccounts,
749
+ provider => authStorage.usageProviderFor(provider) !== undefined,
750
+ cmd.provider,
751
+ );
721
752
  let filteredReports = reports;
722
753
  if (cmd.provider) {
723
754
  const wanted = cmd.provider.toLowerCase();
@@ -760,9 +791,13 @@ export async function runUsageCommand(cmd: UsageCommandArgs): Promise<void> {
760
791
 
761
792
  if (filteredReports.length === 0 && accounts.length === 0) {
762
793
  const scope = cmd.provider ? ` for provider "${cmd.provider}"` : "";
763
- process.stderr.write(
764
- chalk.yellow(`No credentials found${scope}. Run \`omp\` and use /login to add accounts.\n`),
765
- );
794
+ // Credentials exist but every one is for a provider without a usage
795
+ // endpoint say so rather than implying nothing is logged in.
796
+ const message =
797
+ storedAccounts.length > 0
798
+ ? `No usage data${scope}. Stored credentials are for providers without a usage endpoint.\n`
799
+ : `No credentials found${scope}. Run \`omp\` and use /login to add accounts.\n`;
800
+ process.stderr.write(chalk.yellow(message));
766
801
  process.exitCode = 1;
767
802
  return;
768
803
  }
@@ -28,12 +28,23 @@ export default class Token extends Command {
28
28
  description: "Force refresh the OAuth token even if it has not expired",
29
29
  default: false,
30
30
  }),
31
+ account: Flags.integer({
32
+ char: "a",
33
+ description: "Select the Nth OAuth account (1-based) in stored order instead of the round-robin default",
34
+ }),
35
+ list: Flags.boolean({
36
+ char: "l",
37
+ description: "List the provider's OAuth accounts (index + identity) and exit",
38
+ default: false,
39
+ }),
31
40
  };
32
41
 
33
42
  static examples = [
34
43
  "# Get API key for Anthropic\n omp token anthropic",
35
44
  "# Get raw Copilot credential JSON\n omp token github-copilot --raw",
36
45
  "# Force refresh and get Gemini CLI token\n omp token google-gemini-cli --force-refresh",
46
+ "# List Anthropic OAuth accounts\n omp token anthropic --list",
47
+ "# Get the 2nd Anthropic OAuth account's token\n omp token anthropic --account 2",
37
48
  ];
38
49
 
39
50
  async run(): Promise<void> {
@@ -43,6 +54,49 @@ export default class Token extends Command {
43
54
 
44
55
  const authStorage = await discoverAuthStorage();
45
56
  try {
57
+ if (flags.list || flags.account !== undefined) {
58
+ const accounts = authStorage.listOAuthAccounts(provider);
59
+ if (accounts.length === 0) {
60
+ process.stderr.write(`${chalk.red(`No OAuth accounts found for provider "${providerName}".`)}\n`);
61
+ process.stderr.write("--account/--list select among OAuth accounts; this provider has none stored.\n");
62
+ process.exitCode = 1;
63
+ return;
64
+ }
65
+ if (flags.list) {
66
+ for (const acct of accounts) {
67
+ const label =
68
+ acct.email ??
69
+ acct.accountId ??
70
+ acct.projectId ??
71
+ acct.enterpriseUrl ??
72
+ `credential #${acct.credentialId}`;
73
+ process.stdout.write(`${acct.position + 1}. ${label}\n`);
74
+ }
75
+ return;
76
+ }
77
+ const n = flags.account;
78
+ if (n === undefined || n < 1 || n > accounts.length) {
79
+ process.stderr.write(
80
+ `${chalk.red(`Invalid --account ${n ?? "(missing)"}.`)} Provider "${providerName}" has ${accounts.length} OAuth account(s) (1-${accounts.length}).\n`,
81
+ );
82
+ process.exitCode = 1;
83
+ return;
84
+ }
85
+ const resolution = await authStorage.getOAuthAccessAt(provider, n - 1, {
86
+ forceRefresh: flags["force-refresh"],
87
+ });
88
+ if (!resolution?.ok) {
89
+ const reason = resolution && !resolution.ok ? resolution.error : "no OAuth credential available";
90
+ process.stderr.write(
91
+ `${chalk.red(`Could not get token for account ${n} of "${providerName}": ${reason}`)}\n`,
92
+ );
93
+ process.exitCode = 1;
94
+ return;
95
+ }
96
+ process.stdout.write(`${resolution.accessToken}\n`);
97
+ return;
98
+ }
99
+
46
100
  const modelRegistry = new ModelRegistry(authStorage);
47
101
 
48
102
  // Resolve the API key / token
@@ -117,6 +117,11 @@ type LlamaCppDiscoveredServerMetadata = {
117
117
  input?: ("text" | "image")[];
118
118
  };
119
119
 
120
+ type LlamaCppModelListEntry = {
121
+ id: string;
122
+ contextWindow?: number;
123
+ };
124
+
120
125
  function toPositiveNumberOrUndefined(value: unknown): number | undefined {
121
126
  if (typeof value === "number" && Number.isFinite(value) && value > 0) {
122
127
  return value;
@@ -162,6 +167,26 @@ function extractLlamaCppContextWindow(payload: Record<string, unknown>): number
162
167
  return toPositiveNumberOrUndefined(payload.n_ctx);
163
168
  }
164
169
 
170
+ function extractLlamaCppModelContextWindow(item: Record<string, unknown>): number | undefined {
171
+ const meta = item.meta;
172
+ if (!isRecord(meta)) {
173
+ return undefined;
174
+ }
175
+ return toPositiveNumberOrUndefined(meta.n_ctx) ?? toPositiveNumberOrUndefined(meta.n_ctx_train);
176
+ }
177
+
178
+ function parseLlamaCppModelList(payload: unknown): LlamaCppModelListEntry[] {
179
+ if (!isRecord(payload) || !Array.isArray(payload.data)) {
180
+ return [];
181
+ }
182
+ return payload.data.flatMap(item => {
183
+ if (!isRecord(item) || typeof item.id !== "string" || !item.id) {
184
+ return [];
185
+ }
186
+ return [{ id: item.id, contextWindow: extractLlamaCppModelContextWindow(item) }];
187
+ });
188
+ }
189
+
165
190
  function extractLlamaCppInputCapabilities(payload: Record<string, unknown>): ("text" | "image")[] | undefined {
166
191
  const modalities = payload.modalities;
167
192
  if (!isRecord(modalities)) {
@@ -338,12 +363,13 @@ export async function discoverLlamaCppModels(
338
363
  const [response, serverMetadata] = apiKey
339
364
  ? await withAuth(apiKey, key => attempt({ ...baseHeaders, Authorization: `Bearer ${key}` }))
340
365
  : await attempt(baseHeaders);
341
- const payload = (await response.json()) as { data?: Array<{ id: string }> };
342
- const models = payload.data ?? [];
366
+ const payload = (await response.json()) as unknown;
367
+ const models = parseLlamaCppModelList(payload);
343
368
  const discovered: Model<Api>[] = [];
344
369
  for (const item of models) {
345
- const id = item.id;
370
+ const { id } = item;
346
371
  if (!id) continue;
372
+ const contextWindow = item.contextWindow ?? serverMetadata?.contextWindow ?? 128000;
347
373
  discovered.push(
348
374
  buildModel({
349
375
  id,
@@ -355,11 +381,8 @@ export async function discoverLlamaCppModels(
355
381
  input: serverMetadata?.input ?? ["text"],
356
382
  imageInputDecoder: "stb",
357
383
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
358
- contextWindow: serverMetadata?.contextWindow ?? 128000,
359
- maxTokens: Math.min(
360
- serverMetadata?.contextWindow ?? Number.POSITIVE_INFINITY,
361
- DISCOVERY_DEFAULT_MAX_TOKENS,
362
- ),
384
+ contextWindow,
385
+ maxTokens: Math.min(contextWindow, DISCOVERY_DEFAULT_MAX_TOKENS),
363
386
  headers,
364
387
  compat: {
365
388
  supportsStore: false,
@@ -372,6 +395,34 @@ export async function discoverLlamaCppModels(
372
395
  return discovered;
373
396
  }
374
397
 
398
+ export async function discoverLlamaCppModelContextWindow(
399
+ model: Pick<Model<Api>, "provider" | "id" | "baseUrl" | "headers">,
400
+ ctx: DiscoveryContext,
401
+ ): Promise<number | undefined> {
402
+ const baseUrl = normalizeLlamaCppBaseUrl(model.baseUrl);
403
+ const modelsUrl = `${baseUrl}/models`;
404
+ const baseHeaders: Record<string, string> = { ...(model.headers ?? {}) };
405
+ const attempt = async (headers: Record<string, string>) => {
406
+ const response = await ctx.fetch(modelsUrl, {
407
+ headers,
408
+ signal: AbortSignal.timeout(250),
409
+ });
410
+ if (!response.ok) {
411
+ return undefined;
412
+ }
413
+ const entries = parseLlamaCppModelList(await response.json());
414
+ return entries.find(entry => entry.id === model.id)?.contextWindow;
415
+ };
416
+ try {
417
+ const apiKey = await ctx.getBearerApiKeyResolver(model.provider);
418
+ return apiKey
419
+ ? await withAuth(apiKey, key => attempt({ ...baseHeaders, Authorization: `Bearer ${key}` }))
420
+ : await attempt(baseHeaders);
421
+ } catch {
422
+ return undefined;
423
+ }
424
+ }
425
+
375
426
  export async function discoverOpenAIModelsList(
376
427
  providerConfig: DiscoveryProviderConfig,
377
428
  ctx: DiscoveryContext,
@@ -67,6 +67,7 @@ import {
67
67
  DISCOVERY_DEFAULT_MAX_TOKENS,
68
68
  type DiscoveryContext,
69
69
  type DiscoveryProviderConfig,
70
+ discoverLlamaCppModelContextWindow,
70
71
  discoverModelsByProviderType,
71
72
  getImplicitOllamaBaseUrl,
72
73
  getOllamaContextLengthOverride,
@@ -759,6 +760,50 @@ export class ModelRegistry {
759
760
  }
760
761
  }
761
762
 
763
+ /**
764
+ * Refresh dynamic metadata that can appear only after a local model loads.
765
+ */
766
+ async refreshSelectedModelMetadata(model: Model<Api>): Promise<Model<Api>> {
767
+ const isLlamaCppDiscovery = this.#discoverableProviders.some(
768
+ providerConfig => providerConfig.provider === model.provider && providerConfig.discovery.type === "llama.cpp",
769
+ );
770
+ if (!isLlamaCppDiscovery) {
771
+ return model;
772
+ }
773
+ const contextWindow = await discoverLlamaCppModelContextWindow(model, this.#nonResolvingDiscoveryContext());
774
+ if (contextWindow === undefined) {
775
+ return this.find(model.provider, model.id) ?? model;
776
+ }
777
+ const current = this.find(model.provider, model.id) ?? model;
778
+ const override = this.#resolveLiveModelOverride(current);
779
+ const customModel = this.#resolveLiveCustomModelOverlay(current);
780
+ const patch: ModelPatch = {};
781
+ if (
782
+ override?.contextWindow === undefined &&
783
+ customModel?.contextWindow === undefined &&
784
+ current.contextWindow !== contextWindow
785
+ ) {
786
+ patch.contextWindow = contextWindow;
787
+ }
788
+ const maxTokens = Math.min(contextWindow, DISCOVERY_DEFAULT_MAX_TOKENS);
789
+ if (
790
+ override?.maxTokens === undefined &&
791
+ customModel?.maxTokens === undefined &&
792
+ current.maxTokens !== maxTokens
793
+ ) {
794
+ patch.maxTokens = maxTokens;
795
+ }
796
+ if (patch.contextWindow === undefined && patch.maxTokens === undefined) {
797
+ return current;
798
+ }
799
+ const patched = applyModelPatch(current, patch, "merge");
800
+ this.#models = this.#models.map(candidate =>
801
+ candidate.provider === current.provider && candidate.id === current.id ? patched : candidate,
802
+ );
803
+ this.#rebuildCanonicalIndex();
804
+ return patched;
805
+ }
806
+
762
807
  /**
763
808
  * Discover models for providers registered at runtime via `fetchDynamicModels`
764
809
  * (extension providers). Merges the discovered catalog into the existing model
@@ -1008,7 +1053,7 @@ export class ModelRegistry {
1008
1053
  provider: providerConfig.provider,
1009
1054
  status: "cached",
1010
1055
  optional: providerConfig.optional ?? false,
1011
- stale: !cache.fresh || !cache.authoritative || configStale,
1056
+ stale: providerConfig.discovery.type === "llama.cpp" || !cache.fresh || !cache.authoritative || configStale,
1012
1057
  fetchedAt: cache.updatedAt,
1013
1058
  models: models.map(model => model.id),
1014
1059
  });
@@ -1272,7 +1317,9 @@ export class ModelRegistry {
1272
1317
  const cacheProviderId = this.#configuredDiscoveryCacheProviderId(providerConfig);
1273
1318
  const cached = readModelCache<Api>(cacheProviderId, 24 * 60 * 60 * 1000, Date.now, this.#cacheDbPath);
1274
1319
  const cacheOlderThanConfig = cached !== null && this.#isDiscoveryCacheOlderThanModelsConfig(cached.updatedAt);
1275
- const effectiveStrategy = strategy === "online-if-uncached" && cacheOlderThanConfig ? "online" : strategy;
1320
+ const bypassFreshCache = providerConfig.discovery.type === "llama.cpp" && strategy === "online-if-uncached";
1321
+ const effectiveStrategy =
1322
+ strategy === "online-if-uncached" && (cacheOlderThanConfig || bypassFreshCache) ? "online" : strategy;
1276
1323
  const requiresAuth = !this.#keylessProviders.has(providerConfig.provider);
1277
1324
  if (requiresAuth) {
1278
1325
  const apiKey = await this.#peekApiKeyForProvider(providerConfig.provider);
@@ -1335,7 +1382,7 @@ export class ModelRegistry {
1335
1382
  provider: providerId,
1336
1383
  status,
1337
1384
  optional: providerConfig.optional ?? false,
1338
- stale: result.stale || status === "cached" || (cacheOlderThanConfig && status !== "ok"),
1385
+ stale: result.stale || status === "cached" || ((cacheOlderThanConfig || bypassFreshCache) && status !== "ok"),
1339
1386
  fetchedAt: discoveryError ? cached?.updatedAt : Date.now(),
1340
1387
  models: result.models.map(model => model.id),
1341
1388
  error: discoveryError,
@@ -1365,6 +1412,13 @@ export class ModelRegistry {
1365
1412
  };
1366
1413
  }
1367
1414
 
1415
+ #nonResolvingDiscoveryContext(): DiscoveryContext {
1416
+ return {
1417
+ fetch: this.#fetch,
1418
+ getBearerApiKeyResolver: async () => undefined,
1419
+ };
1420
+ }
1421
+
1368
1422
  #warnProviderDiscoveryFailure(providerConfig: DiscoveryProviderConfig, error: string): void {
1369
1423
  const previous = this.#lastDiscoveryWarnings.get(providerConfig.provider);
1370
1424
  if (previous === error) {
@@ -1574,6 +1628,23 @@ export class ModelRegistry {
1574
1628
  return this.#applyProviderTransportOverride(model, override);
1575
1629
  });
1576
1630
  }
1631
+ #resolveLiveModelOverride(model: Model<Api>): ModelOverride | undefined {
1632
+ const providerOverrides = this.#modelOverrides.get(model.provider);
1633
+ if (!providerOverrides) return undefined;
1634
+ return resolveModelOverrideWithAliases(
1635
+ providerOverrides,
1636
+ model,
1637
+ (provider, id) => this.find(provider, id) !== undefined,
1638
+ );
1639
+ }
1640
+
1641
+ #resolveLiveCustomModelOverlay(model: Model<Api>): CustomModelOverlay | undefined {
1642
+ return (
1643
+ this.#customModelOverlays.find(overlay => overlay.provider === model.provider && overlay.id === model.id) ??
1644
+ this.#runtimeModelOverlays.find(overlay => overlay.provider === model.provider && overlay.id === model.id)
1645
+ );
1646
+ }
1647
+
1577
1648
  #applyModelOverrides(models: Model<Api>[], overrides: Map<string, Map<string, ModelOverride>>): Model<Api>[] {
1578
1649
  if (overrides.size === 0) return models;
1579
1650
  let liveKeys: Set<string> | null = null;
@@ -180,9 +180,7 @@ export async function listOmpExtensionRoots(ctx: LoadContext): Promise<OmpExtens
180
180
  async function listInstalledPluginRoots(ctx: LoadContext): Promise<InjectedRoot[]> {
181
181
  try {
182
182
  const plugins = await getEnabledPlugins(ctx.cwd, { home: ctx.home });
183
- // Installed plugins are always user-scope; project disablement is already
184
- // honored by `getEnabledPlugins` via `loadProjectOverrides`.
185
- return plugins.map(({ path: p }) => ({ path: p, level: "user" }));
183
+ return plugins.map(({ path: p, scope }) => ({ path: p, level: scope }));
186
184
  } catch (err) {
187
185
  logger.debug("listInstalledPluginRoots: enumeration failed", { error: String(err) });
188
186
  return [];
@@ -6,6 +6,7 @@ import type { ImageContent, Static, TextContent, TSchema } from "@oh-my-pi/pi-ai
6
6
  import type { Settings } from "../../config/settings";
7
7
  import type { Theme } from "../../modes/theme/theme";
8
8
  import { type ApprovalMode, formatApprovalPrompt, requiresApproval } from "../../tools/approval";
9
+ import { normalizeToolEventInput } from "../tool-event-input";
9
10
  import { applyToolProxy } from "../tool-proxy";
10
11
  import type { ExtensionRunner } from "./runner";
11
12
  import type { RegisteredTool, ToolCallEventResult } from "./types";
@@ -185,7 +186,7 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
185
186
  type: "tool_call",
186
187
  toolName: this.tool.name,
187
188
  toolCallId,
188
- input: params as Record<string, unknown>,
189
+ input: normalizeToolEventInput(this.tool.name, params as Record<string, unknown>),
189
190
  })) as ToolCallEventResult | undefined;
190
191
 
191
192
  if (callResult?.block) {
@@ -220,7 +221,7 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
220
221
  type: "tool_result",
221
222
  toolName: this.tool.name,
222
223
  toolCallId,
223
- input: params as Record<string, unknown>,
224
+ input: normalizeToolEventInput(this.tool.name, params as Record<string, unknown>),
224
225
  content: result.content,
225
226
  details: result.details,
226
227
  isError: !!executionError,
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import type { AgentTool, AgentToolContext, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
5
5
  import type { Static, TSchema } from "@oh-my-pi/pi-ai";
6
+ import { normalizeToolEventInput } from "../tool-event-input";
6
7
  import { applyToolProxy } from "../tool-proxy";
7
8
  import type { HookRunner } from "./runner";
8
9
  import type { ToolCallEventResult, ToolResultEventResult } from "./types";
@@ -46,7 +47,7 @@ export class HookToolWrapper<TParameters extends TSchema = TSchema, TDetails = u
46
47
  type: "tool_call",
47
48
  toolName: this.tool.name,
48
49
  toolCallId,
49
- input: params as Record<string, unknown>,
50
+ input: normalizeToolEventInput(this.tool.name, params as Record<string, unknown>),
50
51
  })) as ToolCallEventResult | undefined;
51
52
 
52
53
  if (callResult?.block) {
@@ -72,7 +73,7 @@ export class HookToolWrapper<TParameters extends TSchema = TSchema, TDetails = u
72
73
  type: "tool_result",
73
74
  toolName: this.tool.name,
74
75
  toolCallId,
75
- input: params as Record<string, unknown>,
76
+ input: normalizeToolEventInput(this.tool.name, params as Record<string, unknown>),
76
77
  content: result.content,
77
78
  details: result.details,
78
79
  isError: false,
@@ -95,7 +96,7 @@ export class HookToolWrapper<TParameters extends TSchema = TSchema, TDetails = u
95
96
  type: "tool_result",
96
97
  toolName: this.tool.name,
97
98
  toolCallId,
98
- input: params as Record<string, unknown>,
99
+ input: normalizeToolEventInput(this.tool.name, params as Record<string, unknown>),
99
100
  content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }],
100
101
  details: undefined,
101
102
  isError: true,
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Static handles on every bundled `@oh-my-pi/pi-*` surface a legacy extension
3
+ * may import. Loaded lazily by `legacy-pi-compat.ts` in compiled-binary mode
4
+ * (issue #3423) and re-exported through the `omp-legacy-pi-bundled:` virtual
5
+ * namespace — bunfs paths cannot be resolved at runtime on Bun 1.3.14+, so
6
+ * the only way to re-route extension imports onto the host's in-process copy
7
+ * is via live module references captured at compile time.
8
+ *
9
+ * This module is split out from `legacy-pi-compat.ts` so dev/test runs that
10
+ * touch the compat layer never trigger the cascade through
11
+ * `legacy-pi-coding-agent-shim.ts → ../index → export/html/...` (which
12
+ * requires generated artifacts that only exist after a `bun run build`).
13
+ *
14
+ * The bundler reaches every entry below via standard static-import analysis,
15
+ * so the matching `--compile` extras can be dropped from
16
+ * `scripts/build-binary.ts`.
17
+ */
18
+ import * as bundledPiAgentCore from "@oh-my-pi/pi-agent-core";
19
+ import * as bundledPiNatives from "@oh-my-pi/pi-natives";
20
+ import * as bundledPiTui from "@oh-my-pi/pi-tui";
21
+ import * as bundledPiUtils from "@oh-my-pi/pi-utils";
22
+ import * as bundledLegacyPiAiShim from "../legacy-pi-ai-shim";
23
+ import * as bundledLegacyPiCodingAgentShim from "../legacy-pi-coding-agent-shim";
24
+ import * as bundledTypeBoxShim from "../typebox";
25
+
26
+ /**
27
+ * Canonical specifier → live module namespace. Keys MUST match the right-hand
28
+ * side of `bundledRegistryVirtualSpecifier(...)` calls in
29
+ * `legacy-pi-compat.ts`; the synthesizer enumerates each namespace's own
30
+ * enumerable exports at extension load time.
31
+ */
32
+ export const BUNDLED_PI_REGISTRY: Readonly<Record<string, Readonly<Record<string, unknown>>>> = {
33
+ "@oh-my-pi/pi-agent-core": bundledPiAgentCore,
34
+ "@oh-my-pi/pi-ai": bundledLegacyPiAiShim,
35
+ "@oh-my-pi/pi-coding-agent": bundledLegacyPiCodingAgentShim,
36
+ "@oh-my-pi/pi-natives": bundledPiNatives,
37
+ "@oh-my-pi/pi-tui": bundledPiTui,
38
+ "@oh-my-pi/pi-utils": bundledPiUtils,
39
+ typebox: bundledTypeBoxShim,
40
+ };