@oh-my-pi/pi-coding-agent 16.2.9 → 16.2.12

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 (110) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/dist/cli.js +3464 -3306
  3. package/dist/types/cli/bench-cli.d.ts +0 -5
  4. package/dist/types/cli/dry-balance-cli.d.ts +0 -5
  5. package/dist/types/cli/models-cli.d.ts +1 -1
  6. package/dist/types/config/inline-tool-descriptors-mode.d.ts +1 -2
  7. package/dist/types/config/model-registry.d.ts +1 -23
  8. package/dist/types/config/model-resolver.d.ts +10 -15
  9. package/dist/types/config/models-config-schema.d.ts +0 -6
  10. package/dist/types/config/models-config.d.ts +0 -6
  11. package/dist/types/config/settings-schema.d.ts +31 -1
  12. package/dist/types/eval/py/__tests__/runner-shell-output.test.d.ts +1 -0
  13. package/dist/types/extensibility/extensions/runner.d.ts +15 -0
  14. package/dist/types/extensibility/skills.d.ts +29 -0
  15. package/dist/types/lsp/client.d.ts +9 -3
  16. package/dist/types/modes/components/todo-reminder.d.ts +3 -1
  17. package/dist/types/modes/interactive-mode.d.ts +0 -1
  18. package/dist/types/modes/skill-command.d.ts +1 -1
  19. package/dist/types/modes/types.d.ts +0 -1
  20. package/dist/types/stt/asr-client.d.ts +7 -3
  21. package/dist/types/task/spawn-policy.d.ts +17 -0
  22. package/dist/types/task/spawn-policy.test.d.ts +1 -0
  23. package/dist/types/task/types.d.ts +8 -2
  24. package/dist/types/tools/__tests__/eval-description.test.d.ts +1 -0
  25. package/dist/types/tools/browser/cmux/cmux-tab.d.ts +2 -2
  26. package/dist/types/tools/browser/registry.d.ts +2 -0
  27. package/dist/types/tools/browser/run-cancellation.d.ts +4 -0
  28. package/dist/types/tools/browser/tab-supervisor.d.ts +30 -1
  29. package/dist/types/tools/eval.d.ts +3 -6
  30. package/dist/types/tools/write.d.ts +1 -1
  31. package/dist/types/utils/image-vision-fallback.d.ts +1 -1
  32. package/package.json +14 -13
  33. package/scripts/bundle-dist.ts +23 -4
  34. package/scripts/generate-docs-index.ts +116 -24
  35. package/src/advisor/__tests__/advisor.test.ts +1 -1
  36. package/src/async/job-manager.ts +27 -3
  37. package/src/auto-thinking/classifier.ts +1 -1
  38. package/src/cli/bench-cli.ts +3 -12
  39. package/src/cli/dry-balance-cli.ts +1 -6
  40. package/src/cli/grep-cli.ts +1 -1
  41. package/src/cli/models-cli.ts +3 -81
  42. package/src/commands/models.ts +1 -2
  43. package/src/commit/model-selection.ts +4 -4
  44. package/src/config/inline-tool-descriptors-mode.ts +1 -2
  45. package/src/config/model-discovery.ts +150 -82
  46. package/src/config/model-registry.ts +11 -225
  47. package/src/config/model-resolver.ts +23 -135
  48. package/src/config/models-config-schema.ts +0 -22
  49. package/src/config/prompt-templates.ts +20 -0
  50. package/src/config/settings-schema.ts +35 -1
  51. package/src/config/settings.ts +60 -24
  52. package/src/eval/__tests__/agent-bridge.test.ts +17 -3
  53. package/src/eval/agent-bridge.ts +7 -9
  54. package/src/eval/completion-bridge.ts +1 -1
  55. package/src/eval/py/__tests__/runner-shell-output.test.ts +157 -0
  56. package/src/eval/py/runner.py +169 -18
  57. package/src/extensibility/extensions/model-api.ts +1 -3
  58. package/src/extensibility/extensions/runner.ts +62 -5
  59. package/src/extensibility/skills.ts +77 -0
  60. package/src/internal-urls/docs-index.generated.txt +2 -2
  61. package/src/lsp/client.ts +162 -45
  62. package/src/lsp/config.ts +17 -3
  63. package/src/lsp/index.ts +31 -21
  64. package/src/main.ts +0 -1
  65. package/src/mcp/transports/http.ts +19 -17
  66. package/src/mcp/transports/stdio.test.ts +51 -1
  67. package/src/mcp/transports/stdio.ts +19 -9
  68. package/src/memories/index.ts +0 -1
  69. package/src/mnemopi/backend.ts +1 -1
  70. package/src/modes/acp/acp-agent.ts +6 -9
  71. package/src/modes/components/model-selector.ts +32 -186
  72. package/src/modes/components/todo-reminder.ts +5 -1
  73. package/src/modes/controllers/event-controller.ts +1 -16
  74. package/src/modes/controllers/selector-controller.ts +57 -36
  75. package/src/modes/interactive-mode.ts +0 -6
  76. package/src/modes/rpc/rpc-mode.ts +5 -8
  77. package/src/modes/skill-command.ts +8 -20
  78. package/src/modes/types.ts +0 -1
  79. package/src/prompts/system/subagent-system-prompt.md +2 -2
  80. package/src/prompts/system/tool-call-loop-redirect.md +8 -0
  81. package/src/prompts/tools/eval.md +2 -2
  82. package/src/prompts/tools/task.md +1 -1
  83. package/src/sdk.ts +2 -9
  84. package/src/session/agent-session.ts +89 -8
  85. package/src/session/session-context.ts +2 -1
  86. package/src/session/session-manager.ts +2 -1
  87. package/src/session/unexpected-stop-classifier.ts +1 -1
  88. package/src/stt/asr-client.ts +87 -27
  89. package/src/stt/downloader.ts +8 -2
  90. package/src/task/executor.ts +1 -3
  91. package/src/task/index.ts +37 -32
  92. package/src/task/spawn-policy.test.ts +63 -0
  93. package/src/task/spawn-policy.ts +58 -0
  94. package/src/task/types.ts +77 -6
  95. package/src/tools/__tests__/eval-description.test.ts +19 -0
  96. package/src/tools/ast-grep.ts +34 -12
  97. package/src/tools/browser/cmux/cmux-tab.ts +66 -24
  98. package/src/tools/browser/registry.ts +25 -0
  99. package/src/tools/browser/run-cancellation.ts +47 -0
  100. package/src/tools/browser/tab-supervisor.ts +120 -7
  101. package/src/tools/browser/tab-worker.ts +22 -10
  102. package/src/tools/browser.ts +1 -0
  103. package/src/tools/eval.ts +15 -10
  104. package/src/tools/grep.ts +11 -8
  105. package/src/tools/inspect-image.ts +1 -1
  106. package/src/tools/write.ts +49 -9
  107. package/src/utils/commit-message-generator.ts +0 -1
  108. package/src/utils/git.ts +22 -1
  109. package/src/utils/image-vision-fallback.ts +2 -3
  110. package/src/utils/title-generator.ts +1 -1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "16.2.9",
4
+ "version": "16.2.12",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -32,7 +32,8 @@
32
32
  },
33
33
  "scripts": {
34
34
  "build": "bun scripts/build-binary.ts",
35
- "check": "biome check . && bun run check:types",
35
+ "check": "biome check . && bun run check:docs && bun run check:types",
36
+ "check:docs": "bun scripts/generate-docs-index.ts --check",
36
37
  "check:types": "tsgo -p tsconfig.json --noEmit",
37
38
  "lint": "biome lint .",
38
39
  "test": "bun ../../scripts/ci-test-ts.ts coding-agent-heavy --full",
@@ -55,17 +56,17 @@
55
56
  "@agentclientprotocol/sdk": "0.25.0",
56
57
  "@babel/parser": "^7.29.7",
57
58
  "@mozilla/readability": "^0.6.0",
58
- "@oh-my-pi/hashline": "16.2.9",
59
- "@oh-my-pi/omp-stats": "16.2.9",
60
- "@oh-my-pi/pi-agent-core": "16.2.9",
61
- "@oh-my-pi/pi-ai": "16.2.9",
62
- "@oh-my-pi/pi-catalog": "16.2.9",
63
- "@oh-my-pi/pi-mnemopi": "16.2.9",
64
- "@oh-my-pi/pi-natives": "16.2.9",
65
- "@oh-my-pi/pi-tui": "16.2.9",
66
- "@oh-my-pi/pi-utils": "16.2.9",
67
- "@oh-my-pi/pi-wire": "16.2.9",
68
- "@oh-my-pi/snapcompact": "16.2.9",
59
+ "@oh-my-pi/hashline": "16.2.12",
60
+ "@oh-my-pi/omp-stats": "16.2.12",
61
+ "@oh-my-pi/pi-agent-core": "16.2.12",
62
+ "@oh-my-pi/pi-ai": "16.2.12",
63
+ "@oh-my-pi/pi-catalog": "16.2.12",
64
+ "@oh-my-pi/pi-mnemopi": "16.2.12",
65
+ "@oh-my-pi/pi-natives": "16.2.12",
66
+ "@oh-my-pi/pi-tui": "16.2.12",
67
+ "@oh-my-pi/pi-utils": "16.2.12",
68
+ "@oh-my-pi/pi-wire": "16.2.12",
69
+ "@oh-my-pi/snapcompact": "16.2.12",
69
70
  "@opentelemetry/api": "^1.9.1",
70
71
  "@opentelemetry/context-async-hooks": "^2.7.1",
71
72
  "@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
@@ -3,6 +3,7 @@
3
3
  import * as fs from "node:fs/promises";
4
4
  import * as path from "node:path";
5
5
  import { isEnoent } from "@oh-my-pi/pi-utils";
6
+ import { assertDocsIndexFresh, buildDocsIndexPayload } from "./generate-docs-index";
6
7
 
7
8
  const packageDir = path.join(import.meta.dir, "..");
8
9
  const outDir = path.join(packageDir, "dist");
@@ -72,13 +73,31 @@ async function cleanBundleOutputs(): Promise<void> {
72
73
  );
73
74
  }
74
75
 
76
+ async function assertDocsEmbedPopulated(): Promise<void> {
77
+ // bundle-dist runs from prepack (which calls `gen:docs` first) or directly.
78
+ // Direct invocations must fail — the tarball ships src/, and an empty embed
79
+ // would make src/internal-urls/docs-index.ts fall through to the missing
80
+ // repo `docs/` tree at runtime in published packages (codex review, PR #3941).
81
+ const embedPath = path.join(packageDir, "src/internal-urls/docs-index.generated.txt");
82
+ const embed = await Bun.file(embedPath).text();
83
+ if (embed.length === 0) {
84
+ throw new Error(
85
+ "docs-index embed is empty. Run `bun run gen:docs` before `bun run gen:bundle`, or use `bun pm pack` which runs the prepack chain.",
86
+ );
87
+ }
88
+ const expected = await buildDocsIndexPayload();
89
+ assertDocsIndexFresh(embed, expected);
90
+ }
91
+
75
92
  async function main(): Promise<void> {
76
93
  const start = Bun.nanoseconds();
77
94
  await cleanBundleOutputs();
78
- // The npm bundle ships no stats dashboard sources or prebuilt dist/client,
79
- // so embed the dashboard archive the same way compiled binaries do
80
- // (scripts/build-binary.ts). Reset afterwards to keep the checked-in
81
- // placeholder empty.
95
+ await assertDocsEmbedPopulated();
96
+ // The npm bundle ships no stats dashboard sources, so embed the dashboard
97
+ // archive the same way compiled binaries do (scripts/build-binary.ts). Reset
98
+ // afterwards to keep the checked-in placeholder empty. The docs embed stays
99
+ // populated on disk — postpack owns its reset so `bun pm pack` can pack a
100
+ // tarball whose src copy is still valid for subpath imports.
82
101
  await runCommand(["bun", "--cwd=../stats", "run", "gen:stats"]);
83
102
  try {
84
103
  await runCommand([
@@ -1,56 +1,148 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
3
  /**
4
- * Populate (or reset) the embedded harness documentation index for `omp://`.
4
+ * Populate, check, or reset the embedded harness documentation index for `omp://`.
5
5
  *
6
6
  * `--generate` writes `src/internal-urls/docs-index.generated.txt` as two lines:
7
7
  * a plain JSON array of the sorted `docs/**\/*.md` file names, then a base64
8
- * gzip blob of the index-aligned doc bodies (`string[]`). Keeping the filename
9
- * list out of the blob lets the loader list docs without inflating it.
10
- * Compiled binaries and the prepacked npm bundle inline this (~0.5MB) instead of
11
- * the ~1.6MB raw map; `--reset` restores the checked-in empty placeholder so the
8
+ * gzip blob of the index-aligned doc bodies (`string[]`). `--check` rebuilds
9
+ * that payload from the real docs corpus and compares it to the embed when
10
+ * present; the checked-in empty placeholder is accepted after verifying that a
11
+ * fresh generated payload round-trips. `--reset` restores the placeholder so the
12
12
  * dev tree reads `docs/` from disk. Mirrors the stats / model-catalog embeds.
13
13
  */
14
14
 
15
15
  import * as path from "node:path";
16
- import { gzipSync } from "node:zlib";
16
+ import { gunzipSync, gzipSync } from "node:zlib";
17
17
  import { Glob } from "bun";
18
18
 
19
19
  const docsDir = path.resolve(import.meta.dir, "../../../docs");
20
20
  const outputPath = path.resolve(import.meta.dir, "../src/internal-urls/docs-index.generated.txt");
21
21
  const GENERATE_FLAG = "--generate";
22
22
  const RESET_FLAG = "--reset";
23
+ const CHECK_FLAG = "--check";
24
+
25
+ export interface DocsIndexPayload {
26
+ /** Sorted `docs/**\/*.md` file names plus index-aligned bodies and embed text. */
27
+ readonly files: readonly string[];
28
+ readonly bodies: readonly string[];
29
+ readonly payload: string;
30
+ }
31
+
32
+ export interface DecodedDocsIndexPayload {
33
+ /** Sorted `docs/**\/*.md` file names decoded from an embed payload. */
34
+ readonly files: readonly string[];
35
+ /** Index-aligned Markdown bodies decoded from an embed payload. */
36
+ readonly bodies: readonly string[];
37
+ }
38
+
39
+ function isStringArray(value: unknown): value is string[] {
40
+ return Array.isArray(value) && value.every(item => typeof item === "string");
41
+ }
42
+
43
+ /** Build the exact two-line `omp://` docs embed from the source `docs/**\/*.md` corpus. */
44
+ export async function buildDocsIndexPayload(): Promise<DocsIndexPayload> {
45
+ const glob = new Glob("**/*.md");
46
+ const files: string[] = [];
47
+ for await (const relativePath of glob.scan(docsDir)) {
48
+ files.push(relativePath.split(path.sep).join("/"));
49
+ }
50
+ files.sort();
51
+
52
+ const bodies = await Promise.all(files.map(file => Bun.file(path.join(docsDir, file)).text()));
53
+ const bodiesB64 = Buffer.from(gzipSync(Buffer.from(JSON.stringify(bodies)), { level: 9 })).toString("base64");
54
+ return {
55
+ files,
56
+ bodies,
57
+ payload: `${JSON.stringify(files)}\n${bodiesB64}`,
58
+ };
59
+ }
60
+
61
+ /** Decode a populated docs embed payload into filenames and index-aligned Markdown bodies. */
62
+ export function decodeDocsIndexPayload(embed: string): DecodedDocsIndexPayload | null {
63
+ const newline = embed.indexOf("\n");
64
+ if (newline === -1) return null;
65
+
66
+ const filenames: unknown = JSON.parse(embed.slice(0, newline));
67
+ if (!isStringArray(filenames)) {
68
+ throw new Error("Embedded docs index filename line is not a JSON string array.");
69
+ }
70
+
71
+ const inflated = gunzipSync(Buffer.from(embed.slice(newline + 1), "base64"));
72
+ const bodies: unknown = JSON.parse(inflated.toString("utf8"));
73
+ if (!isStringArray(bodies)) {
74
+ throw new Error("Embedded docs index body blob is not a JSON string array.");
75
+ }
76
+
77
+ return { files: filenames, bodies };
78
+ }
79
+
80
+ /**
81
+ * Assert that an embed payload is fresh against the current source docs payload.
82
+ * An empty placeholder is accepted by round-tripping the expected payload (the
83
+ * dev tree and post-build reset state both checked-in placeholders).
84
+ */
85
+ export function assertDocsIndexFresh(embed: string, expected: DecodedDocsIndexPayload): void {
86
+ const source =
87
+ embed.length > 0
88
+ ? embed
89
+ : `${JSON.stringify(expected.files)}\n${Buffer.from(gzipSync(Buffer.from(JSON.stringify(expected.bodies)), { level: 9 })).toString("base64")}`;
90
+ const decoded = decodeDocsIndexPayload(source);
91
+ if (decoded === null) {
92
+ throw new Error("Embedded docs index is malformed: missing newline separator.");
93
+ }
94
+ if (decoded.files.length !== expected.files.length) {
95
+ throw new Error(
96
+ `Embedded docs index has ${decoded.files.length} docs; source corpus has ${expected.files.length}.`,
97
+ );
98
+ }
99
+ if (decoded.bodies.length !== expected.bodies.length) {
100
+ throw new Error(
101
+ `Embedded docs index has ${decoded.bodies.length} bodies; source corpus has ${expected.bodies.length}.`,
102
+ );
103
+ }
104
+ for (let i = 0; i < expected.files.length; i++) {
105
+ if (decoded.files[i] !== expected.files[i]) {
106
+ throw new Error(
107
+ `Embedded docs index filename mismatch at ${i}: ${decoded.files[i] ?? "<missing>"} !== ${expected.files[i]}.`,
108
+ );
109
+ }
110
+ if (decoded.bodies[i] !== expected.bodies[i]) {
111
+ throw new Error(`Embedded docs index body mismatch for ${expected.files[i]}. Run \`bun run gen:docs\`.`);
112
+ }
113
+ }
114
+ }
23
115
 
24
116
  async function main(): Promise<void> {
25
117
  const rel = path.relative(process.cwd(), outputPath);
26
118
 
27
119
  if (process.argv.includes(RESET_FLAG)) {
28
120
  await Bun.write(outputPath, "");
29
- console.log(`Reset ${rel}`);
121
+ process.stdout.write(`Reset ${rel}\n`);
30
122
  return;
31
123
  }
32
124
 
33
- if (!process.argv.includes(GENERATE_FLAG)) {
34
- console.log(`Skipping ${rel}; pass ${GENERATE_FLAG} to embed docs (the dev tree reads docs/ from disk)`);
125
+ if (process.argv.includes(CHECK_FLAG)) {
126
+ const current = await buildDocsIndexPayload();
127
+ const embed = await Bun.file(outputPath).text();
128
+ assertDocsIndexFresh(embed, current);
129
+ process.stdout.write(`Docs index fresh for ${current.files.length} docs (${rel})\n`);
35
130
  return;
36
131
  }
37
132
 
38
- const glob = new Glob("**/*.md");
39
- const files: string[] = [];
40
- for await (const relativePath of glob.scan(docsDir)) {
41
- files.push(relativePath.split(path.sep).join("/"));
133
+ if (!process.argv.includes(GENERATE_FLAG)) {
134
+ process.stdout.write(
135
+ `Skipping ${rel}; pass ${GENERATE_FLAG} to embed docs (the dev tree reads docs/ from disk)\n`,
136
+ );
137
+ return;
42
138
  }
43
- files.sort();
44
-
45
- // Index-aligned bodies (Promise.all preserves order), kept separate from the
46
- // filename list so the loader can list docs without inflating the blob.
47
- const bodies = await Promise.all(files.map(file => Bun.file(path.join(docsDir, file)).text()));
48
139
 
49
- const bodiesB64 = Buffer.from(gzipSync(Buffer.from(JSON.stringify(bodies)), { level: 9 })).toString("base64");
50
- // Two lines: plain filename array, then the base64 gzip blob.
51
- const payload = `${JSON.stringify(files)}\n${bodiesB64}`;
52
- await Bun.write(outputPath, payload);
53
- console.log(`Generated ${rel} (${files.length} docs, ${payload.length} bytes)`);
140
+ const current = await buildDocsIndexPayload();
141
+ assertDocsIndexFresh(current.payload, current);
142
+ await Bun.write(outputPath, current.payload);
143
+ process.stdout.write(`Generated ${rel} (${current.files.length} docs, ${current.payload.length} bytes)\n`);
54
144
  }
55
145
 
56
- await main();
146
+ if (import.meta.main) {
147
+ await main();
148
+ }
@@ -1579,7 +1579,7 @@ describe("advisor", () => {
1579
1579
 
1580
1580
  describe("AdvisorConfigOverlayComponent", () => {
1581
1581
  const deps = {
1582
- modelRegistry: { getCanonicalModelSelections: () => [] } as unknown as ModelRegistry,
1582
+ modelRegistry: {} as unknown as ModelRegistry,
1583
1583
  settings: {} as unknown as Settings,
1584
1584
  scopedModels: [],
1585
1585
  availableToolNames: ["read", "grep", "glob", "lsp", "web_search"],
@@ -397,6 +397,27 @@ export class AsyncJobManager {
397
397
  await Promise.all(Array.from(this.#jobs.values()).map(job => job.promise));
398
398
  }
399
399
 
400
+ async #waitForAllUntil(deadline: number): Promise<boolean> {
401
+ const promises = Array.from(this.#jobs.values()).map(job => job.promise);
402
+ if (promises.length === 0) return true;
403
+ if (deadline === Number.POSITIVE_INFINITY) {
404
+ await Promise.all(promises);
405
+ return true;
406
+ }
407
+ const remainingMs = deadline - Date.now();
408
+ if (remainingMs <= 0) return false;
409
+
410
+ const timeout = Promise.withResolvers<"timeout">();
411
+ const timer = setTimeout(() => timeout.resolve("timeout"), remainingMs);
412
+ timer.unref();
413
+ try {
414
+ const result = await Promise.race([Promise.all(promises).then(() => "settled" as const), timeout.promise]);
415
+ return result === "settled";
416
+ } finally {
417
+ clearTimeout(timer);
418
+ }
419
+ }
420
+
400
421
  async drainDeliveries(options?: { timeoutMs?: number; filter?: AsyncJobFilter }): Promise<boolean> {
401
422
  const timeoutMs = options?.timeoutMs;
402
423
  const filter = options?.filter;
@@ -445,8 +466,10 @@ export class AsyncJobManager {
445
466
  this.#disposed = true;
446
467
  this.#clearEvictionTimers();
447
468
  this.cancelAll();
448
- await this.waitForAll();
449
- const drained = await this.drainDeliveries({ timeoutMs: options?.timeoutMs ?? 3_000 });
469
+ const timeoutMs = Math.max(options?.timeoutMs ?? 3_000, 0);
470
+ const deadline = Date.now() + timeoutMs;
471
+ const jobsSettled = await this.#waitForAllUntil(deadline);
472
+ const drained = await this.drainDeliveries({ timeoutMs: Math.max(deadline - Date.now(), 0) });
450
473
  this.#clearEvictionTimers();
451
474
  this.#jobs.clear();
452
475
  this.#deliveries.length = 0;
@@ -454,7 +477,7 @@ export class AsyncJobManager {
454
477
  this.#suppressedDeliveries.clear();
455
478
  this.#watchedJobs.clear();
456
479
  this.#pollEscalation.clear();
457
- return drained;
480
+ return jobsSettled && drained;
458
481
  }
459
482
 
460
483
  #resolveJobId(preferredId?: string): string {
@@ -483,6 +506,7 @@ export class AsyncJobManager {
483
506
  }
484
507
 
485
508
  #scheduleEviction(jobId: string): void {
509
+ if (this.#disposed) return;
486
510
  if (this.#retentionMs <= 0) {
487
511
  this.#jobs.delete(jobId);
488
512
  this.#suppressedDeliveries.delete(jobId);
@@ -74,7 +74,7 @@ export async function classifyDifficulty(
74
74
  }
75
75
 
76
76
  async function classifyOnline(input: string, deps: ClassifyDifficultyDeps): Promise<Effort> {
77
- const resolved = resolveRoleSelection(["tiny", "smol"], deps.settings, deps.registry.getAvailable(), deps.registry);
77
+ const resolved = resolveRoleSelection(["tiny", "smol"], deps.settings, deps.registry.getAvailable());
78
78
  const model = resolved?.model;
79
79
  if (!model) {
80
80
  throw new Error("auto-thinking: no tiny/smol model available for classification");
@@ -14,12 +14,12 @@ import type {
14
14
  SimpleStreamOptions,
15
15
  } from "@oh-my-pi/pi-ai";
16
16
  import { resolveModelServiceTier, streamSimple } from "@oh-my-pi/pi-ai";
17
- import { buildModelProviderPriorityRank, type CanonicalModelVariant } from "@oh-my-pi/pi-catalog/identity";
17
+ import { buildModelProviderPriorityRank } from "@oh-my-pi/pi-catalog/identity";
18
18
  import { replaceTabs, truncateToWidth } from "@oh-my-pi/pi-tui";
19
19
  import { formatDuration, getProjectDir } from "@oh-my-pi/pi-utils";
20
20
  import chalk from "chalk";
21
21
  import type { ApiKeyResolverModel } from "../config/api-key-resolver";
22
- import { type CanonicalModelQueryOptions, ModelRegistry } from "../config/model-registry";
22
+ import { ModelRegistry } from "../config/model-registry";
23
23
  import {
24
24
  formatModelSelectorValue,
25
25
  formatModelString,
@@ -55,9 +55,6 @@ export interface BenchModelRegistry {
55
55
  getAll(): Model<Api>[];
56
56
  getApiKey(model: Model<Api>, sessionId?: string): Promise<string | undefined>;
57
57
  resolver(model: ApiKeyResolverModel, sessionId?: string): ApiKeyResolver;
58
- resolveCanonicalModel?(canonicalId: string, options?: CanonicalModelQueryOptions): Model<Api> | undefined;
59
- getCanonicalVariants?(canonicalId: string, options?: CanonicalModelQueryOptions): CanonicalModelVariant[];
60
- getCanonicalId?(model: Model<Api>): string | undefined;
61
58
  hasConfiguredAuth?(model: Model<Api>): boolean;
62
59
  }
63
60
 
@@ -440,13 +437,7 @@ function resolveAuthenticatedAlternative(
440
437
  seen.add(key);
441
438
  if (modelRegistry.hasConfiguredAuth?.(candidate)) authenticated.push(candidate);
442
439
  };
443
- // Canonical variants link the same logical model across providers even when
444
- // ids differ (e.g. fireworks `gpt-oss-20b` <-> openrouter `openai/gpt-oss-20b`).
445
- const canonicalId = modelRegistry.getCanonicalId?.(model);
446
- if (canonicalId) {
447
- for (const variant of modelRegistry.getCanonicalVariants?.(canonicalId) ?? []) consider(variant.model);
448
- }
449
- // Same-id fallback for entries outside the canonical index.
440
+ // Same-id fallback for equivalent entries under providers with configured auth.
450
441
  for (const candidate of modelRegistry.getAll()) {
451
442
  if (candidate.id === model.id) consider(candidate);
452
443
  }
@@ -12,11 +12,10 @@ import type {
12
12
  SimpleStreamOptions,
13
13
  } from "@oh-my-pi/pi-ai";
14
14
  import { streamSimple } from "@oh-my-pi/pi-ai";
15
- import type { CanonicalModelVariant } from "@oh-my-pi/pi-catalog/identity";
16
15
  import { replaceTabs, truncateToWidth } from "@oh-my-pi/pi-tui";
17
16
  import { formatDuration, getProjectDir } from "@oh-my-pi/pi-utils";
18
17
  import chalk from "chalk";
19
- import { type CanonicalModelQueryOptions, ModelRegistry } from "../config/model-registry";
18
+ import { ModelRegistry } from "../config/model-registry";
20
19
  import {
21
20
  formatModelString,
22
21
  getModelMatchPreferences,
@@ -74,9 +73,6 @@ export interface DryBalanceModelRegistry {
74
73
  getAll(): Model<Api>[];
75
74
  getAvailable(): Model<Api>[];
76
75
  getApiKey(model: Model<Api>, sessionId?: string): Promise<string | undefined>;
77
- getCanonicalVariants(canonicalId: string, options?: CanonicalModelQueryOptions): CanonicalModelVariant[];
78
- resolveCanonicalModel?(canonicalId: string, options?: CanonicalModelQueryOptions): Model<Api> | undefined;
79
- getCanonicalId?(model: Model<Api>): string | undefined;
80
76
  }
81
77
 
82
78
  export interface DryBalanceRuntime {
@@ -566,7 +562,6 @@ async function resolveDryBalanceModel(
566
562
  const defaultRoleSpec = resolveModelRoleValue(settings?.getModelRole("default"), allowedModels, {
567
563
  settings,
568
564
  matchPreferences: preferences,
569
- modelRegistry,
570
565
  });
571
566
  if (defaultRoleSpec.model) {
572
567
  return { model: defaultRoleSpec.model, warning: defaultRoleSpec.warning };
@@ -150,7 +150,7 @@ ${chalk.bold("Options:")}
150
150
  --no-gitignore Include files excluded by .gitignore
151
151
 
152
152
  ${chalk.bold("Environment:")}
153
- PI_GREP_WORKERS=N Set filesystem walker workers (default 4, 0 = auto)
153
+ PI_WALK_WORKERS=N Set filesystem walker workers (default 4, 0 = auto)
154
154
 
155
155
  ${chalk.bold("Examples:")}
156
156
  ${APP_NAME} grep "import" src/
@@ -2,7 +2,7 @@
2
2
  * `omp models` — list, search, and refresh available models.
3
3
  *
4
4
  * Subcommands:
5
- * - `ls` (default): list every available model with a canonical + provider view.
5
+ * - `ls` (default): list every available model grouped by provider.
6
6
  * - `find <substring>`: list models whose provider, id, or name contains the substring.
7
7
  * - `refresh`: force an online catalog re-fetch (ignoring the model cache TTL),
8
8
  * then list. This is the supported replacement for `rm -rf ~/.omp/models.db`
@@ -21,7 +21,7 @@ import { discoverAndLoadExtensions, loadExtensions } from "../extensibility/exte
21
21
  import { discoverAuthStorage } from "../sdk";
22
22
  import { EventBus } from "../utils/event-bus";
23
23
 
24
- export type ModelsAction = "ls" | "find" | "refresh" | "canonical";
24
+ export type ModelsAction = "ls" | "find" | "refresh";
25
25
 
26
26
  export interface ModelsCommandArgs {
27
27
  action: ModelsAction;
@@ -48,7 +48,6 @@ const KNOWN_ACTIONS: Record<string, ModelsAction> = {
48
48
  list: "ls",
49
49
  find: "find",
50
50
  refresh: "refresh",
51
- canonical: "canonical",
52
51
  };
53
52
 
54
53
  /** Resolve the two positional args into an action + filter (provider names fall through to `ls`). */
@@ -77,22 +76,10 @@ interface ModelJson {
77
76
  cost: Model<Api>["cost"];
78
77
  }
79
78
 
80
- interface CanonicalJson {
81
- id: string;
82
- selected: string;
83
- variants: number;
84
- contextWindow: number | null;
85
- maxTokens: number | null;
86
- }
87
-
88
79
  interface ModelsJson {
89
80
  models: ModelJson[];
90
81
  }
91
82
 
92
- interface CanonicalModelsJson {
93
- canonical: CanonicalJson[];
94
- }
95
-
96
83
  function writeLine(line = ""): void {
97
84
  process.stdout.write(`${line}\n`);
98
85
  }
@@ -253,67 +240,6 @@ function renderProviderModels(
253
240
  }
254
241
  }
255
242
 
256
- /** `omp models canonical`: the coalesced canonical view (one row per canonical id). */
257
- function renderCanonicalModels(modelRegistry: ModelRegistry, pattern: string | undefined, json: boolean): void {
258
- const selections = modelRegistry.getCanonicalModelSelections({ availableOnly: true });
259
- const needle = pattern?.toLowerCase();
260
- const filtered = needle
261
- ? selections.filter(
262
- ({ record, model }) =>
263
- record.id.toLowerCase().includes(needle) ||
264
- `${model.provider}/${model.id}`.toLowerCase().includes(needle),
265
- )
266
- : selections;
267
-
268
- if (json) {
269
- const output: CanonicalModelsJson = {
270
- canonical: filtered
271
- .map(({ record, model }) => ({
272
- id: record.id,
273
- selected: `${model.provider}/${model.id}`,
274
- variants: record.variants.length,
275
- contextWindow: model.contextWindow,
276
- maxTokens: model.maxTokens,
277
- }))
278
- .sort((left, right) => left.id.localeCompare(right.id)),
279
- };
280
- writeLine(JSON.stringify(output));
281
- return;
282
- }
283
-
284
- if (selections.length === 0) {
285
- writeLine("No models available. Set API keys in environment variables.");
286
- return;
287
- }
288
- if (filtered.length === 0) {
289
- writeLine(`No canonical models matching "${pattern}"`);
290
- return;
291
- }
292
-
293
- const rows = filtered
294
- .slice()
295
- .sort((left, right) => left.record.id.localeCompare(right.record.id))
296
- .map(({ record, model }) => [
297
- record.id,
298
- `${model.provider}/${model.id}`,
299
- String(record.variants.length),
300
- formatLimit(model.contextWindow),
301
- formatLimit(model.maxTokens),
302
- ]);
303
- for (const line of boxTable(
304
- [
305
- { header: "canonical" },
306
- { header: "selected" },
307
- { header: "variants", align: "right" },
308
- { header: "context", align: "right" },
309
- { header: "max-out", align: "right" },
310
- ],
311
- rows,
312
- )) {
313
- writeLine(line);
314
- }
315
- }
316
-
317
243
  /**
318
244
  * Options for {@link runModelsListing}: render the catalog from a caller-supplied
319
245
  * registry. Loads extensions (CLI `-e` paths and configured `settings.extensions`)
@@ -376,11 +302,7 @@ export async function runModelsListing(options: RunModelsListingOptions): Promis
376
302
  // Discover runtime (extension) provider catalogs now that they are registered.
377
303
  await modelRegistry.refreshRuntimeProviders(action === "refresh" ? "online" : "online-if-uncached");
378
304
 
379
- if (action === "canonical") {
380
- renderCanonicalModels(modelRegistry, pattern, json);
381
- } else {
382
- renderProviderModels(modelRegistry, action, pattern, json);
383
- }
305
+ renderProviderModels(modelRegistry, action, pattern, json);
384
306
  }
385
307
 
386
308
  /**
@@ -10,7 +10,7 @@ export default class Models extends Command {
10
10
 
11
11
  static args = {
12
12
  action: Args.string({
13
- description: "ls (default) | find | refresh | canonical | <provider>",
13
+ description: "ls (default) | find | refresh | <provider>",
14
14
  required: false,
15
15
  }),
16
16
  pattern: Args.string({
@@ -40,7 +40,6 @@ export default class Models extends Command {
40
40
  `# List one provider's models (any provider name works)\n ${APP_NAME} models openai-codex`,
41
41
  `# Find models by substring\n ${APP_NAME} models find minimax`,
42
42
  `# Force a fresh catalog fetch (replaces rm -rf ~/.omp/models.db)\n ${APP_NAME} models refresh`,
43
- `# Show the coalesced canonical model view\n ${APP_NAME} models canonical`,
44
43
  `# Machine-readable output\n ${APP_NAME} models --json`,
45
44
  ];
46
45
 
@@ -36,8 +36,8 @@ export async function resolvePrimaryModel(
36
36
  const available = modelRegistry.getAvailable();
37
37
  const matchPreferences = getModelMatchPreferences(settings);
38
38
  const resolved = override
39
- ? resolveModelRoleValue(override, available, { settings, matchPreferences, modelRegistry })
40
- : resolveRoleSelection(["commit", "smol", ...MODEL_ROLE_IDS], settings, available, modelRegistry);
39
+ ? resolveModelRoleValue(override, available, { settings, matchPreferences })
40
+ : resolveRoleSelection(["commit", "smol", ...MODEL_ROLE_IDS], settings, available);
41
41
  const model = resolved?.model;
42
42
  if (!model) {
43
43
  throw new Error("No model available for commit generation");
@@ -60,7 +60,7 @@ export async function resolveSmolModel(
60
60
  fallbackApiKey: ApiKey,
61
61
  ): Promise<ResolvedCommitModel> {
62
62
  const available = modelRegistry.getAvailable();
63
- const resolvedSmol = resolveRoleSelection(["smol"], settings, available, modelRegistry);
63
+ const resolvedSmol = resolveRoleSelection(["smol"], settings, available);
64
64
  if (resolvedSmol?.model) {
65
65
  const apiKey = await modelRegistry.getApiKey(resolvedSmol.model);
66
66
  if (apiKey) {
@@ -74,7 +74,7 @@ export async function resolveSmolModel(
74
74
 
75
75
  const matchPreferences = getModelMatchPreferences(settings);
76
76
  for (const pattern of MODEL_PRIO.smol) {
77
- const candidate = parseModelPattern(pattern, available, matchPreferences, { modelRegistry }).model;
77
+ const candidate = parseModelPattern(pattern, available, matchPreferences).model;
78
78
  if (!candidate) continue;
79
79
  const apiKey = await modelRegistry.getApiKey(candidate);
80
80
  if (apiKey) {
@@ -9,8 +9,7 @@ import { modelFamilyToken } from "@oh-my-pi/pi-catalog/identity";
9
9
  * Gemini benefits from descriptors in-prompt; other providers keep them in the
10
10
  * tool schemas. `on`/`off` are explicit user overrides.
11
11
  *
12
- * @param modelId Canonical model id (e.g. `gemini-3-pro`); resolve aliases via
13
- * `ModelRegistry.getCanonicalId` before calling so `auto` classifies correctly.
12
+ * @param modelId Model id (e.g. `gemini-3-pro`) used to classify `auto`.
14
13
  */
15
14
  export function shouldInlineToolDescriptors(
16
15
  setting: "auto" | "on" | "off" | undefined,