@oh-my-pi/pi-coding-agent 15.12.2 → 15.12.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 (231) hide show
  1. package/CHANGELOG.md +49 -1
  2. package/dist/cli.js +1121 -871
  3. package/dist/types/autoresearch/tools/init-experiment.d.ts +1 -1
  4. package/dist/types/autoresearch/tools/log-experiment.d.ts +1 -1
  5. package/dist/types/autoresearch/tools/run-experiment.d.ts +1 -1
  6. package/dist/types/autoresearch/tools/update-notes.d.ts +1 -1
  7. package/dist/types/cli/args.d.ts +0 -1
  8. package/dist/types/cli/models-cli.d.ts +49 -0
  9. package/dist/types/commands/launch.d.ts +0 -3
  10. package/dist/types/commands/models.d.ts +33 -0
  11. package/dist/types/commands/token.d.ts +25 -0
  12. package/dist/types/commit/agentic/tools/analyze-file.d.ts +1 -1
  13. package/dist/types/commit/agentic/tools/git-file-diff.d.ts +1 -1
  14. package/dist/types/commit/agentic/tools/git-hunk.d.ts +1 -1
  15. package/dist/types/commit/agentic/tools/git-overview.d.ts +1 -1
  16. package/dist/types/commit/agentic/tools/propose-changelog.d.ts +1 -1
  17. package/dist/types/commit/agentic/tools/propose-commit.d.ts +1 -1
  18. package/dist/types/commit/agentic/tools/recent-commits.d.ts +1 -1
  19. package/dist/types/commit/agentic/tools/schemas.d.ts +1 -1
  20. package/dist/types/commit/agentic/tools/split-commit.d.ts +1 -1
  21. package/dist/types/commit/changelog/generate.d.ts +1 -1
  22. package/dist/types/commit/shared-llm.d.ts +1 -1
  23. package/dist/types/config/model-registry.d.ts +7 -0
  24. package/dist/types/config/models-config-schema.d.ts +1 -1
  25. package/dist/types/config/settings-schema.d.ts +21 -1
  26. package/dist/types/edit/hashline/params.d.ts +1 -1
  27. package/dist/types/edit/modes/apply-patch.d.ts +1 -1
  28. package/dist/types/edit/modes/patch.d.ts +1 -1
  29. package/dist/types/edit/modes/replace.d.ts +1 -1
  30. package/dist/types/extensibility/custom-commands/types.d.ts +2 -2
  31. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  32. package/dist/types/extensibility/extensions/types.d.ts +2 -2
  33. package/dist/types/extensibility/hooks/types.d.ts +2 -2
  34. package/dist/types/goals/tools/goal-tool.d.ts +1 -1
  35. package/dist/types/lsp/types.d.ts +1 -1
  36. package/dist/types/mcp/manager.d.ts +8 -0
  37. package/dist/types/mnemopi/config.d.ts +28 -0
  38. package/dist/types/modes/acp/acp-agent.d.ts +1 -2
  39. package/dist/types/modes/components/index.d.ts +1 -0
  40. package/dist/types/modes/components/logout-account-selector.d.ts +8 -0
  41. package/dist/types/modes/components/status-line/component.d.ts +9 -5
  42. package/dist/types/modes/components/status-line/types.d.ts +2 -1
  43. package/dist/types/modes/controllers/event-controller.d.ts +0 -17
  44. package/dist/types/modes/interactive-mode.d.ts +0 -3
  45. package/dist/types/modes/types.d.ts +0 -5
  46. package/dist/types/session/agent-session.d.ts +14 -33
  47. package/dist/types/session/agent-storage.d.ts +2 -1
  48. package/dist/types/session/indexed-session-storage.d.ts +1 -0
  49. package/dist/types/session/messages.d.ts +8 -10
  50. package/dist/types/session/session-manager.d.ts +15 -0
  51. package/dist/types/session/session-storage.d.ts +5 -0
  52. package/dist/types/slash-commands/helpers/logout.d.ts +15 -0
  53. package/dist/types/task/types.d.ts +1 -1
  54. package/dist/types/tools/ask.d.ts +1 -1
  55. package/dist/types/tools/ast-edit.d.ts +1 -1
  56. package/dist/types/tools/ast-grep.d.ts +1 -1
  57. package/dist/types/tools/bash.d.ts +1 -1
  58. package/dist/types/tools/browser/cmux/cmux-tab.d.ts +202 -0
  59. package/dist/types/tools/browser/cmux/rpc.d.ts +70 -0
  60. package/dist/types/tools/browser/cmux/socket-client.d.ts +19 -0
  61. package/dist/types/tools/browser/registry.d.ts +16 -3
  62. package/dist/types/tools/browser/render.d.ts +2 -0
  63. package/dist/types/tools/browser/tab-protocol.d.ts +2 -0
  64. package/dist/types/tools/browser/tab-supervisor.d.ts +16 -4
  65. package/dist/types/tools/browser.d.ts +3 -1
  66. package/dist/types/tools/checkpoint.d.ts +1 -1
  67. package/dist/types/tools/debug.d.ts +1 -1
  68. package/dist/types/tools/eval.d.ts +1 -1
  69. package/dist/types/tools/find.d.ts +1 -1
  70. package/dist/types/tools/gh.d.ts +1 -1
  71. package/dist/types/tools/image-gen.d.ts +1 -1
  72. package/dist/types/tools/index.d.ts +3 -1
  73. package/dist/types/tools/inspect-image.d.ts +1 -1
  74. package/dist/types/tools/irc.d.ts +1 -1
  75. package/dist/types/tools/job.d.ts +1 -1
  76. package/dist/types/tools/memory-edit.d.ts +1 -1
  77. package/dist/types/tools/memory-recall.d.ts +1 -1
  78. package/dist/types/tools/memory-reflect.d.ts +1 -1
  79. package/dist/types/tools/memory-retain.d.ts +1 -1
  80. package/dist/types/tools/read.d.ts +1 -1
  81. package/dist/types/tools/render-mermaid.d.ts +1 -1
  82. package/dist/types/tools/resolve.d.ts +1 -1
  83. package/dist/types/tools/review.d.ts +1 -1
  84. package/dist/types/tools/search-tool-bm25.d.ts +1 -1
  85. package/dist/types/tools/search.d.ts +1 -1
  86. package/dist/types/tools/ssh.d.ts +1 -1
  87. package/dist/types/tools/todo.d.ts +1 -1
  88. package/dist/types/tools/tts.d.ts +1 -1
  89. package/dist/types/tools/write.d.ts +1 -1
  90. package/dist/types/utils/clipboard.d.ts +4 -3
  91. package/dist/types/utils/image-loading.d.ts +18 -1
  92. package/dist/types/utils/thinking-display.d.ts +17 -0
  93. package/dist/types/web/search/index.d.ts +1 -1
  94. package/package.json +14 -14
  95. package/src/autoresearch/storage.ts +2 -1
  96. package/src/autoresearch/tools/init-experiment.ts +1 -1
  97. package/src/autoresearch/tools/log-experiment.ts +1 -1
  98. package/src/autoresearch/tools/run-experiment.ts +1 -1
  99. package/src/autoresearch/tools/update-notes.ts +1 -1
  100. package/src/cli/args.ts +0 -8
  101. package/src/cli/auth-gateway-cli.ts +1 -1
  102. package/src/cli/bench-cli.ts +1 -1
  103. package/src/cli/dry-balance-cli.ts +1 -1
  104. package/src/cli/models-cli.ts +427 -0
  105. package/src/cli-commands.ts +2 -0
  106. package/src/collab/host.ts +9 -12
  107. package/src/commands/launch.ts +0 -3
  108. package/src/commands/models.ts +61 -0
  109. package/src/commands/token.ts +89 -0
  110. package/src/commit/agentic/tools/analyze-file.ts +1 -1
  111. package/src/commit/agentic/tools/git-file-diff.ts +1 -1
  112. package/src/commit/agentic/tools/git-hunk.ts +1 -1
  113. package/src/commit/agentic/tools/git-overview.ts +1 -1
  114. package/src/commit/agentic/tools/propose-changelog.ts +1 -1
  115. package/src/commit/agentic/tools/propose-commit.ts +1 -1
  116. package/src/commit/agentic/tools/recent-commits.ts +1 -1
  117. package/src/commit/agentic/tools/schemas.ts +1 -1
  118. package/src/commit/agentic/tools/split-commit.ts +1 -1
  119. package/src/commit/analysis/summary.ts +1 -1
  120. package/src/commit/changelog/generate.ts +1 -1
  121. package/src/commit/shared-llm.ts +1 -1
  122. package/src/config/model-registry.ts +15 -12
  123. package/src/config/model-resolver.ts +2 -2
  124. package/src/config/models-config-schema.ts +1 -1
  125. package/src/config/settings-schema.ts +19 -1
  126. package/src/edit/hashline/params.ts +1 -1
  127. package/src/edit/modes/apply-patch.ts +1 -1
  128. package/src/edit/modes/patch.ts +1 -1
  129. package/src/edit/modes/replace.ts +1 -1
  130. package/src/eval/agent-bridge.ts +1 -1
  131. package/src/eval/completion-bridge.ts +1 -1
  132. package/src/export/html/template.js +24 -2
  133. package/src/export/html/tool-views.generated.js +2 -2
  134. package/src/extensibility/custom-commands/loader.ts +1 -1
  135. package/src/extensibility/custom-commands/types.ts +2 -2
  136. package/src/extensibility/custom-tools/loader.ts +1 -1
  137. package/src/extensibility/custom-tools/types.ts +2 -2
  138. package/src/extensibility/extensions/loader.ts +2 -2
  139. package/src/extensibility/extensions/types.ts +2 -2
  140. package/src/extensibility/hooks/loader.ts +1 -1
  141. package/src/extensibility/hooks/types.ts +2 -2
  142. package/src/extensibility/skills.ts +18 -3
  143. package/src/goals/tools/goal-tool.ts +1 -1
  144. package/src/internal-urls/docs-index.generated.ts +6 -3
  145. package/src/lsp/types.ts +1 -1
  146. package/src/main.ts +0 -25
  147. package/src/mcp/config-writer.ts +7 -3
  148. package/src/mcp/manager.ts +11 -0
  149. package/src/memories/index.ts +3 -1
  150. package/src/memories/storage.ts +2 -1
  151. package/src/mnemopi/config.ts +95 -11
  152. package/src/modes/acp/acp-agent.ts +5 -48
  153. package/src/modes/acp/acp-event-mapper.ts +5 -1
  154. package/src/modes/components/agent-hub.ts +2 -1
  155. package/src/modes/components/assistant-message.ts +8 -7
  156. package/src/modes/components/index.ts +1 -0
  157. package/src/modes/components/logout-account-selector.ts +130 -0
  158. package/src/modes/components/mcp-add-wizard.ts +1 -1
  159. package/src/modes/components/model-selector.ts +2 -2
  160. package/src/modes/components/status-line/component.ts +54 -157
  161. package/src/modes/components/status-line/segments.ts +1 -1
  162. package/src/modes/components/status-line/types.ts +2 -1
  163. package/src/modes/controllers/command-controller.ts +0 -12
  164. package/src/modes/controllers/event-controller.ts +23 -62
  165. package/src/modes/controllers/input-controller.ts +60 -31
  166. package/src/modes/controllers/mcp-command-controller.ts +44 -3
  167. package/src/modes/controllers/selector-controller.ts +56 -10
  168. package/src/modes/controllers/streaming-reveal.ts +4 -3
  169. package/src/modes/interactive-mode.ts +2 -8
  170. package/src/modes/theme/theme.ts +1 -1
  171. package/src/modes/types.ts +0 -5
  172. package/src/modes/utils/ui-helpers.ts +2 -1
  173. package/src/prompts/system/empty-stop-retry.md +4 -6
  174. package/src/sdk.ts +15 -19
  175. package/src/session/agent-session.ts +125 -234
  176. package/src/session/agent-storage.ts +18 -9
  177. package/src/session/history-storage.ts +2 -1
  178. package/src/session/indexed-session-storage.ts +7 -0
  179. package/src/session/messages.ts +9 -11
  180. package/src/session/session-dump-format.ts +4 -2
  181. package/src/session/session-manager.ts +116 -0
  182. package/src/session/session-storage.ts +20 -0
  183. package/src/slash-commands/builtin-registry.ts +15 -1
  184. package/src/slash-commands/helpers/logout.ts +88 -0
  185. package/src/task/types.ts +1 -1
  186. package/src/tools/ask.ts +1 -1
  187. package/src/tools/ast-edit.ts +13 -4
  188. package/src/tools/ast-grep.ts +1 -1
  189. package/src/tools/bash.ts +1 -1
  190. package/src/tools/browser/cmux/cmux-tab.ts +1264 -0
  191. package/src/tools/browser/cmux/rpc.ts +156 -0
  192. package/src/tools/browser/cmux/socket-client.ts +309 -0
  193. package/src/tools/browser/registry.ts +37 -3
  194. package/src/tools/browser/render.ts +6 -1
  195. package/src/tools/browser/tab-protocol.ts +2 -0
  196. package/src/tools/browser/tab-supervisor.ts +189 -18
  197. package/src/tools/browser/tab-worker.ts +1 -1
  198. package/src/tools/browser.ts +16 -1
  199. package/src/tools/checkpoint.ts +1 -1
  200. package/src/tools/debug.ts +1 -1
  201. package/src/tools/eval.ts +11 -6
  202. package/src/tools/fetch.ts +13 -2
  203. package/src/tools/find.ts +1 -1
  204. package/src/tools/gh.ts +1 -1
  205. package/src/tools/github-cache.ts +2 -1
  206. package/src/tools/image-gen.ts +1 -1
  207. package/src/tools/index.ts +3 -1
  208. package/src/tools/inspect-image.ts +3 -1
  209. package/src/tools/irc.ts +1 -1
  210. package/src/tools/job.ts +1 -1
  211. package/src/tools/memory-edit.ts +1 -1
  212. package/src/tools/memory-recall.ts +1 -1
  213. package/src/tools/memory-reflect.ts +1 -1
  214. package/src/tools/memory-retain.ts +1 -1
  215. package/src/tools/read.ts +8 -2
  216. package/src/tools/render-mermaid.ts +1 -1
  217. package/src/tools/report-tool-issue.ts +3 -2
  218. package/src/tools/resolve.ts +1 -1
  219. package/src/tools/review.ts +1 -1
  220. package/src/tools/search-tool-bm25.ts +1 -1
  221. package/src/tools/search.ts +1 -1
  222. package/src/tools/ssh.ts +1 -1
  223. package/src/tools/todo.ts +1 -1
  224. package/src/tools/tts.ts +1 -1
  225. package/src/tools/write.ts +1 -1
  226. package/src/utils/clipboard.ts +35 -18
  227. package/src/utils/image-loading.ts +35 -4
  228. package/src/utils/thinking-display.ts +37 -0
  229. package/src/web/search/index.ts +1 -1
  230. package/dist/types/cli/list-models.d.ts +0 -30
  231. package/src/cli/list-models.ts +0 -194
@@ -0,0 +1,427 @@
1
+ /**
2
+ * `omp models` — list, search, and refresh available models.
3
+ *
4
+ * Subcommands:
5
+ * - `ls` (default): list every available model with a canonical + provider view.
6
+ * - `find <substring>`: list models whose provider, id, or name contains the substring.
7
+ * - `refresh`: force an online catalog re-fetch (ignoring the model cache TTL),
8
+ * then list. This is the supported replacement for `rm -rf ~/.omp/models.db`
9
+ * when a provider ships a new model that the 24h cache has not picked up yet.
10
+ *
11
+ * `ls`/`find` use the cache when fresh (`online-if-uncached`); only `refresh`
12
+ * forces the network (`online`).
13
+ */
14
+ import type { Api, Effort, Model } from "@oh-my-pi/pi-ai";
15
+ import { getSupportedEfforts } from "@oh-my-pi/pi-catalog/model-thinking";
16
+ import { formatNumber, getProjectDir } from "@oh-my-pi/pi-utils";
17
+ import chalk from "chalk";
18
+ import { ModelRegistry } from "../config/model-registry";
19
+ import { Settings } from "../config/settings";
20
+ import { discoverAndLoadExtensions, loadExtensions } from "../extensibility/extensions";
21
+ import { discoverAuthStorage } from "../sdk";
22
+ import { EventBus } from "../utils/event-bus";
23
+
24
+ export type ModelsAction = "ls" | "find" | "refresh" | "canonical";
25
+
26
+ export interface ModelsCommandArgs {
27
+ action: ModelsAction;
28
+ /** Search substring for `find`, or optional filter for `ls`. */
29
+ pattern?: string;
30
+ flags: {
31
+ json?: boolean;
32
+ /** CLI `-e <path>` extension paths to load before listing (issue #905). */
33
+ extensions?: string[];
34
+ /** Skip extension discovery; only load explicit `extensions`. */
35
+ noExtensions?: boolean;
36
+ /** Extra `config.yml` overlays to apply for this invocation. */
37
+ config?: string[];
38
+ };
39
+ }
40
+
41
+ /**
42
+ * Known action keywords. Any other first token (e.g. `openai-codex`) is treated
43
+ * as a provider/substring filter for the default `ls` view, so every provider
44
+ * name doubles as an `omp models <provider>` shortcut.
45
+ */
46
+ const KNOWN_ACTIONS: Record<string, ModelsAction> = {
47
+ ls: "ls",
48
+ list: "ls",
49
+ find: "find",
50
+ refresh: "refresh",
51
+ canonical: "canonical",
52
+ };
53
+
54
+ /** Resolve the two positional args into an action + filter (provider names fall through to `ls`). */
55
+ export function resolveModelsArgs(
56
+ first: string | undefined,
57
+ second: string | undefined,
58
+ ): { action: ModelsAction; pattern: string | undefined } {
59
+ const known = first === undefined ? undefined : KNOWN_ACTIONS[first];
60
+ if (known) {
61
+ return { action: known, pattern: second };
62
+ }
63
+ return { action: "ls", pattern: first };
64
+ }
65
+
66
+ interface ModelJson {
67
+ provider: string;
68
+ id: string;
69
+ selector: string;
70
+ name: string;
71
+ contextWindow: number | null;
72
+ maxTokens: number | null;
73
+ reasoning: boolean;
74
+ /** Supported thinking efforts when the model thinks, otherwise null. */
75
+ thinking: readonly Effort[] | null;
76
+ input: ("text" | "image")[];
77
+ cost: Model<Api>["cost"];
78
+ }
79
+
80
+ interface CanonicalJson {
81
+ id: string;
82
+ selected: string;
83
+ variants: number;
84
+ contextWindow: number | null;
85
+ maxTokens: number | null;
86
+ }
87
+
88
+ interface ModelsJson {
89
+ models: ModelJson[];
90
+ }
91
+
92
+ interface CanonicalModelsJson {
93
+ canonical: CanonicalJson[];
94
+ }
95
+
96
+ function writeLine(line = ""): void {
97
+ process.stdout.write(`${line}\n`);
98
+ }
99
+
100
+ function formatLimit(n: number | null): string {
101
+ return n === null ? "-" : formatNumber(n);
102
+ }
103
+
104
+ function byProviderThenId(left: Model<Api>, right: Model<Api>): number {
105
+ const providerCmp = left.provider.localeCompare(right.provider);
106
+ if (providerCmp !== 0) return providerCmp;
107
+ return left.id.localeCompare(right.id);
108
+ }
109
+
110
+ function toModelJson(model: Model<Api>): ModelJson {
111
+ return {
112
+ provider: model.provider,
113
+ id: model.id,
114
+ selector: `${model.provider}/${model.id}`,
115
+ name: model.name,
116
+ contextWindow: model.contextWindow,
117
+ maxTokens: model.maxTokens,
118
+ reasoning: model.reasoning,
119
+ thinking: model.thinking ? getSupportedEfforts(model) : null,
120
+ input: model.input,
121
+ cost: model.cost,
122
+ };
123
+ }
124
+
125
+ type ColumnAlign = "left" | "right";
126
+
127
+ interface BoxColumn {
128
+ header: string;
129
+ align?: ColumnAlign;
130
+ }
131
+
132
+ /** Right- or left-pad a plain (ANSI-free) cell to `width` display columns. */
133
+ function padCell(text: string, width: number, align: ColumnAlign = "left"): string {
134
+ const space = width - Bun.stringWidth(text);
135
+ if (space <= 0) return text;
136
+ const fill = " ".repeat(space);
137
+ return align === "right" ? fill + text : text + fill;
138
+ }
139
+
140
+ /**
141
+ * Render `rows` as a box-drawing table. Cells must be plain text (no ANSI); the
142
+ * header row is bolded and the borders dimmed (both no-ops on non-TTY output).
143
+ */
144
+ function boxTable(columns: BoxColumn[], rows: string[][]): string[] {
145
+ const widths = columns.map((column, index) =>
146
+ Math.max(Bun.stringWidth(column.header), ...rows.map(row => Bun.stringWidth(row[index] ?? ""))),
147
+ );
148
+ const bar = chalk.dim("│");
149
+ const segments = widths.map(width => "─".repeat(width + 2));
150
+ const renderRow = (cells: string[], bold: boolean): string => {
151
+ const padded = columns.map((column, index) => {
152
+ const cell = padCell(cells[index] ?? "", widths[index]!, column.align);
153
+ return bold ? chalk.bold(cell) : cell;
154
+ });
155
+ return `${bar} ${padded.join(` ${bar} `)} ${bar}`;
156
+ };
157
+ const lines = [chalk.dim(`┌${segments.join("┬")}┐`)];
158
+ lines.push(
159
+ renderRow(
160
+ columns.map(column => column.header),
161
+ true,
162
+ ),
163
+ );
164
+ lines.push(chalk.dim(`├${segments.join("┼")}┤`));
165
+ for (const row of rows) {
166
+ lines.push(renderRow(row, false));
167
+ }
168
+ lines.push(chalk.dim(`└${segments.join("┴")}┘`));
169
+ return lines;
170
+ }
171
+
172
+ /** `omp models ls`/`find`: provider-grouped listing (one box table per provider). */
173
+ function renderProviderModels(
174
+ modelRegistry: ModelRegistry,
175
+ action: ModelsAction,
176
+ pattern: string | undefined,
177
+ json: boolean,
178
+ ): void {
179
+ const available = modelRegistry.getAvailable();
180
+ const needle = pattern?.toLowerCase();
181
+ let filtered = available;
182
+
183
+ if (needle) {
184
+ let exactFound = false;
185
+ if (action !== "find") {
186
+ const exact = available.filter(m => m.provider.toLowerCase() === needle);
187
+ if (exact.length > 0) {
188
+ filtered = exact;
189
+ exactFound = true;
190
+ }
191
+ }
192
+ if (!exactFound) {
193
+ filtered = available.filter(
194
+ model =>
195
+ model.id.toLowerCase().includes(needle) ||
196
+ model.provider.toLowerCase().includes(needle) ||
197
+ `${model.provider}/${model.id}`.toLowerCase().includes(needle) ||
198
+ model.name.toLowerCase().includes(needle),
199
+ );
200
+ }
201
+ }
202
+
203
+ if (json) {
204
+ const output: ModelsJson = { models: filtered.slice().sort(byProviderThenId).map(toModelJson) };
205
+ writeLine(JSON.stringify(output));
206
+ return;
207
+ }
208
+
209
+ if (available.length === 0) {
210
+ writeLine("No models available. Set API keys in environment variables.");
211
+ return;
212
+ }
213
+ if (filtered.length === 0) {
214
+ writeLine(`No models matching "${pattern}"`);
215
+ return;
216
+ }
217
+
218
+ // One section per provider: bold heading + a box table of that provider's models.
219
+ const byProvider = new Map<string, Model<Api>[]>();
220
+ for (const model of filtered.slice().sort(byProviderThenId)) {
221
+ let group = byProvider.get(model.provider);
222
+ if (!group) {
223
+ group = [];
224
+ byProvider.set(model.provider, group);
225
+ }
226
+ group.push(model);
227
+ }
228
+
229
+ let firstProvider = true;
230
+ for (const [provider, models] of byProvider) {
231
+ if (!firstProvider) writeLine();
232
+ firstProvider = false;
233
+ writeLine(`${chalk.bold.cyan(provider)} ${chalk.dim(`(${models.length})`)}`);
234
+ const rows = models.map(model => [
235
+ model.id,
236
+ formatLimit(model.contextWindow),
237
+ formatLimit(model.maxTokens),
238
+ model.thinking ? getSupportedEfforts(model).join(",") : model.reasoning ? "yes" : "-",
239
+ model.input.includes("image") ? "yes" : "no",
240
+ ]);
241
+ for (const line of boxTable(
242
+ [
243
+ { header: "model" },
244
+ { header: "context", align: "right" },
245
+ { header: "max-out", align: "right" },
246
+ { header: "thinking" },
247
+ { header: "images" },
248
+ ],
249
+ rows,
250
+ )) {
251
+ writeLine(line);
252
+ }
253
+ }
254
+ }
255
+
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
+ /**
318
+ * Options for {@link runModelsListing}: render the catalog from a caller-supplied
319
+ * registry. Loads extensions (CLI `-e` paths and configured `settings.extensions`)
320
+ * and discovers their providers before rendering so extension-contributed models
321
+ * appear (issue #905). The caller is responsible for refreshing built-in providers.
322
+ */
323
+ export interface RunModelsListingOptions {
324
+ modelRegistry: ModelRegistry;
325
+ cwd: string;
326
+ action?: ModelsAction;
327
+ pattern?: string;
328
+ json?: boolean;
329
+ /** CLI-supplied extension paths (e.g. from `-e <path>`). */
330
+ additionalExtensionPaths?: string[];
331
+ /** Extension paths configured under `extensions:` in user settings. */
332
+ settingsExtensions?: string[];
333
+ /** Disabled extension ids from settings (`disabledExtensions`). */
334
+ disabledExtensionIds?: string[];
335
+ /** When true, skip discovery and only load `additionalExtensionPaths`. */
336
+ disableExtensionDiscovery?: boolean;
337
+ }
338
+
339
+ export async function runModelsListing(options: RunModelsListingOptions): Promise<void> {
340
+ const {
341
+ modelRegistry,
342
+ cwd,
343
+ action = "ls",
344
+ pattern,
345
+ json = false,
346
+ additionalExtensionPaths = [],
347
+ settingsExtensions = [],
348
+ disabledExtensionIds = [],
349
+ disableExtensionDiscovery = false,
350
+ } = options;
351
+
352
+ const eventBus = new EventBus();
353
+ const extensionsResult = disableExtensionDiscovery
354
+ ? await loadExtensions(additionalExtensionPaths, cwd, eventBus)
355
+ : await discoverAndLoadExtensions(
356
+ [...additionalExtensionPaths, ...settingsExtensions],
357
+ cwd,
358
+ eventBus,
359
+ disabledExtensionIds,
360
+ );
361
+
362
+ for (const { path: extPath, error } of extensionsResult.errors) {
363
+ process.stderr.write(`Failed to load extension: ${extPath}: ${error}\n`);
364
+ }
365
+
366
+ // Mirror sdk.ts: drain pending provider registrations into the registry.
367
+ const activeSources = extensionsResult.extensions.map(extension => extension.path);
368
+ modelRegistry.syncExtensionSources(activeSources);
369
+ for (const sourceId of new Set(activeSources)) {
370
+ modelRegistry.clearSourceRegistrations(sourceId);
371
+ }
372
+ for (const { name, config, sourceId } of extensionsResult.runtime.pendingProviderRegistrations) {
373
+ modelRegistry.registerProvider(name, config, sourceId);
374
+ }
375
+ extensionsResult.runtime.pendingProviderRegistrations = [];
376
+ // Discover runtime (extension) provider catalogs now that they are registered.
377
+ await modelRegistry.refreshRuntimeProviders(action === "refresh" ? "online" : "online-if-uncached");
378
+
379
+ if (action === "canonical") {
380
+ renderCanonicalModels(modelRegistry, pattern, json);
381
+ } else {
382
+ renderProviderModels(modelRegistry, action, pattern, json);
383
+ }
384
+ }
385
+
386
+ /**
387
+ * Entry point for the standalone `omp models` command: bootstraps auth storage,
388
+ * settings, and the model registry, force/cache-refreshes built-in providers per
389
+ * the chosen action, then delegates to {@link runModelsListing}.
390
+ */
391
+ export async function runModelsCommand(command: ModelsCommandArgs): Promise<void> {
392
+ const { action, pattern } = command;
393
+ const json = command.flags.json ?? false;
394
+
395
+ if (action === "find" && (!pattern || pattern.trim().length === 0)) {
396
+ process.stderr.write("`omp models find` requires a search substring, e.g. `omp models find minimax`\n");
397
+ process.exitCode = 1;
398
+ return;
399
+ }
400
+
401
+ const cwd = getProjectDir();
402
+ const authStorage = await discoverAuthStorage();
403
+ try {
404
+ const settings = await Settings.init({ cwd, configFiles: command.flags.config });
405
+ const modelRegistry = new ModelRegistry(authStorage);
406
+
407
+ if (action === "refresh" && !json && process.stderr.isTTY) {
408
+ process.stderr.write("Refreshing models from all providers…\n");
409
+ }
410
+ await modelRegistry.refresh(action === "refresh" ? "online" : "online-if-uncached");
411
+
412
+ const cliExtensionPaths = command.flags.noExtensions ? [] : (command.flags.extensions ?? []);
413
+ await runModelsListing({
414
+ modelRegistry,
415
+ cwd,
416
+ action,
417
+ pattern,
418
+ json,
419
+ additionalExtensionPaths: cliExtensionPaths,
420
+ settingsExtensions: settings.get("extensions") ?? [],
421
+ disabledExtensionIds: settings.get("disabledExtensions") ?? [],
422
+ disableExtensionDiscovery: Boolean(command.flags.noExtensions),
423
+ });
424
+ } finally {
425
+ authStorage.close();
426
+ }
427
+ }
@@ -27,6 +27,7 @@ export const commands: CommandEntry[] = [
27
27
  { name: "grievances", load: () => import("./commands/grievances").then(m => m.default) },
28
28
  { name: "install", load: () => import("./commands/install").then(m => m.default) },
29
29
  { name: "join", load: () => import("./commands/join").then(m => m.default) },
30
+ { name: "models", load: () => import("./commands/models").then(m => m.default) },
30
31
  { name: "plugin", load: () => import("./commands/plugin").then(m => m.default) },
31
32
  { name: "setup", load: () => import("./commands/setup").then(m => m.default) },
32
33
  { name: "shell", load: () => import("./commands/shell").then(m => m.default) },
@@ -36,6 +37,7 @@ export const commands: CommandEntry[] = [
36
37
  { name: "update", load: () => import("./commands/update").then(m => m.default) },
37
38
  { name: "usage", load: () => import("./commands/usage").then(m => m.default) },
38
39
  { name: "tiny-models", load: () => import("./commands/tiny-models").then(m => m.default) },
40
+ { name: "token", load: () => import("./commands/token").then(m => m.default) },
39
41
  { name: "worktree", load: () => import("./commands/worktree").then(m => m.default), aliases: ["wt"] },
40
42
  { name: "search", load: () => import("./commands/web-search").then(m => m.default), aliases: ["q"] },
41
43
  ];
@@ -365,13 +365,8 @@ export class CollabHost {
365
365
  const name = peer.name;
366
366
  const content: string | (TextContent | ImageContent)[] =
367
367
  images && images.length > 0 ? [{ type: "text", text }, ...images] : text;
368
- const details: CollabPromptDetails & { __pendingDisplayTag?: string } = { from: name };
368
+ const details: CollabPromptDetails = { from: name };
369
369
  if (this.#ctx.session.isStreaming) {
370
- // Mid-turn guest prompts are steered: register the pending-display twin
371
- // so queuedMessageCount reflects the queued steer (host pending bar +
372
- // guests' "queued ×N" badge). The tag dequeues the entry when the agent
373
- // consumes the message (mirrors the skill-prompt path).
374
- details.__pendingDisplayTag = this.#ctx.session.enqueueCustomMessageDisplay(text, "steer");
375
370
  this.#ctx.updatePendingMessagesDisplay();
376
371
  this.#ctx.ui.requestRender();
377
372
  this.#scheduleStateBroadcast();
@@ -385,7 +380,7 @@ export class CollabHost {
385
380
  details,
386
381
  attribution: "user",
387
382
  },
388
- { streamingBehavior: "steer" },
383
+ { streamingBehavior: "steer", queueChipText: text },
389
384
  )
390
385
  .catch(err => {
391
386
  logger.warn("collab guest prompt failed", { error: String(err) });
@@ -416,21 +411,23 @@ export class CollabHost {
416
411
 
417
412
  #buildState(): CollabSessionState {
418
413
  const session = this.#ctx.session;
419
- // Context numbers come from the status line's breakdown not
420
- // session.getContextUsage() so guests render exactly what the host's
421
- // own footer shows.
414
+ // Context numbers come from the status line's memoized breakdown so guests
415
+ // render exactly the same anchored, provider-real count the host's own
416
+ // status line shows.
422
417
  const breakdown = this.#ctx.statusLine.getCachedContextBreakdown();
418
+ const tokens = breakdown.usedTokens;
423
419
  return {
424
420
  isStreaming: session.isStreaming,
421
+ isAborting: session.isAborting,
425
422
  queuedMessageCount: session.queuedMessageCount,
426
423
  sessionName: session.sessionName,
427
424
  cwd: this.#ctx.sessionManager.getCwd(),
428
425
  model: session.model,
429
426
  thinkingLevel: session.thinkingLevel,
430
427
  contextUsage: {
431
- tokens: breakdown.usedTokens,
428
+ tokens,
432
429
  contextWindow: breakdown.contextWindow,
433
- percent: breakdown.contextWindow > 0 ? (breakdown.usedTokens / breakdown.contextWindow) * 100 : null,
430
+ percent: tokens !== null && breakdown.contextWindow > 0 ? (tokens / breakdown.contextWindow) * 100 : null,
434
431
  },
435
432
  participants: this.participants,
436
433
  };
@@ -124,9 +124,6 @@ export default class Index extends Command {
124
124
  export: Flags.string({
125
125
  description: "Export session file to HTML and exit",
126
126
  }),
127
- "list-models": Flags.string({
128
- description: "List available models (with optional fuzzy search)",
129
- }),
130
127
  "no-title": Flags.boolean({
131
128
  description: "Disable title auto-generation",
132
129
  }),
@@ -0,0 +1,61 @@
1
+ /**
2
+ * List, search, and refresh available models.
3
+ */
4
+ import { APP_NAME } from "@oh-my-pi/pi-utils";
5
+ import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
6
+ import { resolveModelsArgs, runModelsCommand } from "../cli/models-cli";
7
+
8
+ export default class Models extends Command {
9
+ static description = "List, search, and refresh available models";
10
+
11
+ static args = {
12
+ action: Args.string({
13
+ description: "ls (default) | find | refresh | canonical | <provider>",
14
+ required: false,
15
+ }),
16
+ pattern: Args.string({
17
+ description: "Filter/search substring, or provider name (required for find)",
18
+ required: false,
19
+ }),
20
+ };
21
+
22
+ static flags = {
23
+ json: Flags.boolean({ description: "Output JSON" }),
24
+ extension: Flags.string({
25
+ char: "e",
26
+ description: "Load an extension file before listing (repeatable)",
27
+ multiple: true,
28
+ }),
29
+ "no-extensions": Flags.boolean({
30
+ description: "Disable extension discovery (explicit -e paths still work)",
31
+ }),
32
+ config: Flags.string({
33
+ description: "Load an extra config.yml-style overlay for this run (repeatable)",
34
+ multiple: true,
35
+ }),
36
+ };
37
+
38
+ static examples = [
39
+ `# List every available model, grouped by provider\n ${APP_NAME} models`,
40
+ `# List one provider's models (any provider name works)\n ${APP_NAME} models openai-codex`,
41
+ `# Find models by substring\n ${APP_NAME} models find minimax`,
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
+ `# Machine-readable output\n ${APP_NAME} models --json`,
45
+ ];
46
+
47
+ async run(): Promise<void> {
48
+ const { args, flags } = await this.parse(Models);
49
+ const { action, pattern } = resolveModelsArgs(args.action, args.pattern);
50
+ await runModelsCommand({
51
+ action,
52
+ pattern,
53
+ flags: {
54
+ json: flags.json,
55
+ extensions: flags.extension,
56
+ noExtensions: flags["no-extensions"],
57
+ config: flags.config,
58
+ },
59
+ });
60
+ }
61
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Get the API key or OAuth token for a provider.
3
+ */
4
+
5
+ import { PROVIDER_REGISTRY } from "@oh-my-pi/pi-ai";
6
+ import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
7
+ import chalk from "chalk";
8
+ import { isAuthenticated, ModelRegistry } from "../config/model-registry";
9
+ import { discoverAuthStorage } from "../sdk";
10
+
11
+ export default class Token extends Command {
12
+ static description = "Get the API key or OAuth token for a provider";
13
+
14
+ static args = {
15
+ provider: Args.string({
16
+ description: "Provider ID (e.g. anthropic, openai)",
17
+ required: true,
18
+ }),
19
+ };
20
+
21
+ static flags = {
22
+ raw: Flags.boolean({
23
+ description: "Output the raw credential value without parsing nested JSON structures",
24
+ default: false,
25
+ }),
26
+ "force-refresh": Flags.boolean({
27
+ description: "Force refresh the OAuth token even if it has not expired",
28
+ default: false,
29
+ }),
30
+ };
31
+
32
+ static examples = [
33
+ "# Get API key for Anthropic\n omp token anthropic",
34
+ "# Get raw Copilot credential JSON\n omp token github-copilot --raw",
35
+ "# Force refresh and get Gemini CLI token\n omp token google-gemini-cli --force-refresh",
36
+ ];
37
+
38
+ async run(): Promise<void> {
39
+ const { args, flags } = await this.parse(Token);
40
+ const providerName = args.provider ?? "";
41
+ const provider = providerName.toLowerCase();
42
+
43
+ const authStorage = await discoverAuthStorage();
44
+ const modelRegistry = new ModelRegistry(authStorage);
45
+
46
+ // Resolve the API key / token
47
+ const apiKey = await modelRegistry.getApiKeyForProvider(provider, undefined, {
48
+ forceRefresh: flags["force-refresh"],
49
+ });
50
+
51
+ if (!isAuthenticated(apiKey)) {
52
+ // Find all active/configured providers
53
+ const activeProviders = new Set<string>();
54
+ for (const p of PROVIDER_REGISTRY) {
55
+ if (authStorage.hasAuth(p.id)) {
56
+ activeProviders.add(p.id);
57
+ }
58
+ }
59
+ const all = authStorage.getAll();
60
+ for (const p in all) {
61
+ if (authStorage.hasAuth(p)) {
62
+ activeProviders.add(p);
63
+ }
64
+ }
65
+
66
+ const msg = `No active credential found for provider "${providerName}".`;
67
+ process.stderr.write(`${chalk.red(msg)}\n`);
68
+ if (activeProviders.size > 0) {
69
+ process.stderr.write(`Configured providers: ${Array.from(activeProviders).sort().join(", ")}\n`);
70
+ }
71
+ process.exitCode = 1;
72
+ return;
73
+ }
74
+
75
+ if (!flags.raw) {
76
+ try {
77
+ const parsed = JSON.parse(apiKey);
78
+ if (parsed && typeof parsed === "object" && typeof parsed.token === "string") {
79
+ process.stdout.write(`${parsed.token}\n`);
80
+ return;
81
+ }
82
+ } catch {
83
+ // Not a JSON string, print as-is
84
+ }
85
+ }
86
+
87
+ process.stdout.write(`${apiKey}\n`);
88
+ }
89
+ }
@@ -1,5 +1,5 @@
1
1
  import { prompt } from "@oh-my-pi/pi-utils";
2
- import * as z from "zod/v4";
2
+ import { z } from "zod/v4";
3
3
  import analyzeFilePrompt from "../../../commit/agentic/prompts/analyze-file.md" with { type: "text" };
4
4
  import type { CommitAgentState } from "../../../commit/agentic/state";
5
5
  import type { NumstatEntry } from "../../../commit/types";
@@ -1,4 +1,4 @@
1
- import * as z from "zod/v4";
1
+ import { z } from "zod/v4";
2
2
  import type { CommitAgentState } from "../../../commit/agentic/state";
3
3
  import type { CustomTool } from "../../../extensibility/custom-tools/types";
4
4
  import * as git from "../../../utils/git";
@@ -1,4 +1,4 @@
1
- import * as z from "zod/v4";
1
+ import { z } from "zod/v4";
2
2
  import type { DiffHunk, FileHunks } from "../../../commit/types";
3
3
  import type { CustomTool } from "../../../extensibility/custom-tools/types";
4
4
  import * as git from "../../../utils/git";
@@ -1,4 +1,4 @@
1
- import * as z from "zod/v4";
1
+ import { z } from "zod/v4";
2
2
  import type { CommitAgentState, GitOverviewSnapshot } from "../../../commit/agentic/state";
3
3
  import { extractScopeCandidates } from "../../../commit/analysis/scope";
4
4
  import type { CustomTool } from "../../../extensibility/custom-tools/types";