@oh-my-pi/pi-coding-agent 11.8.2 → 11.8.3

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 (122) hide show
  1. package/docs/tui.md +9 -9
  2. package/package.json +7 -7
  3. package/src/cli/file-processor.ts +8 -13
  4. package/src/cli/oclif-help.ts +1 -1
  5. package/src/cli.ts +14 -0
  6. package/src/commit/git/index.ts +16 -16
  7. package/src/config/keybindings.ts +11 -11
  8. package/src/config/model-registry.ts +31 -66
  9. package/src/config/settings.ts +88 -95
  10. package/src/config.ts +2 -2
  11. package/src/cursor.ts +4 -4
  12. package/src/debug/index.ts +28 -28
  13. package/src/discovery/codex.ts +5 -13
  14. package/src/discovery/cursor.ts +2 -7
  15. package/src/exa/mcp-client.ts +2 -2
  16. package/src/exa/websets.ts +2 -2
  17. package/src/export/html/index.ts +3 -3
  18. package/src/export/ttsr.ts +27 -27
  19. package/src/extensibility/custom-tools/loader.ts +9 -9
  20. package/src/extensibility/extensions/runner.ts +64 -64
  21. package/src/extensibility/hooks/runner.ts +46 -46
  22. package/src/extensibility/plugins/manager.ts +49 -49
  23. package/src/index.ts +0 -1
  24. package/src/internal-urls/router.ts +5 -5
  25. package/src/ipy/kernel.ts +61 -57
  26. package/src/lsp/client.ts +1 -1
  27. package/src/lsp/clients/biome-client.ts +2 -2
  28. package/src/lsp/clients/lsp-linter-client.ts +7 -7
  29. package/src/lsp/index.ts +9 -9
  30. package/src/mcp/manager.ts +47 -47
  31. package/src/mcp/tool-bridge.ts +12 -12
  32. package/src/mcp/transports/http.ts +34 -34
  33. package/src/mcp/transports/stdio.ts +47 -47
  34. package/src/modes/components/assistant-message.ts +25 -25
  35. package/src/modes/components/bash-execution.ts +51 -51
  36. package/src/modes/components/bordered-loader.ts +7 -7
  37. package/src/modes/components/branch-summary-message.ts +7 -7
  38. package/src/modes/components/compaction-summary-message.ts +7 -7
  39. package/src/modes/components/countdown-timer.ts +15 -15
  40. package/src/modes/components/custom-editor.ts +22 -22
  41. package/src/modes/components/custom-message.ts +21 -21
  42. package/src/modes/components/dynamic-border.ts +3 -3
  43. package/src/modes/components/extensions/extension-dashboard.ts +72 -72
  44. package/src/modes/components/extensions/extension-list.ts +99 -97
  45. package/src/modes/components/extensions/inspector-panel.ts +26 -26
  46. package/src/modes/components/footer.ts +36 -36
  47. package/src/modes/components/history-search.ts +52 -52
  48. package/src/modes/components/hook-editor.ts +20 -20
  49. package/src/modes/components/hook-input.ts +20 -20
  50. package/src/modes/components/hook-message.ts +22 -22
  51. package/src/modes/components/hook-selector.ts +52 -52
  52. package/src/modes/components/index.ts +0 -1
  53. package/src/modes/components/login-dialog.ts +57 -57
  54. package/src/modes/components/model-selector.ts +173 -173
  55. package/src/modes/components/oauth-selector.ts +45 -45
  56. package/src/modes/components/plugin-settings.ts +52 -52
  57. package/src/modes/components/python-execution.ts +53 -53
  58. package/src/modes/components/queue-mode-selector.ts +7 -7
  59. package/src/modes/components/read-tool-group.ts +23 -23
  60. package/src/modes/components/session-selector.ts +40 -37
  61. package/src/modes/components/settings-selector.ts +80 -80
  62. package/src/modes/components/show-images-selector.ts +7 -7
  63. package/src/modes/components/skill-message.ts +27 -27
  64. package/src/modes/components/status-line-segment-editor.ts +81 -81
  65. package/src/modes/components/status-line.ts +73 -73
  66. package/src/modes/components/theme-selector.ts +11 -11
  67. package/src/modes/components/thinking-selector.ts +7 -7
  68. package/src/modes/components/todo-display.ts +19 -19
  69. package/src/modes/components/todo-reminder.ts +9 -9
  70. package/src/modes/components/tool-execution.ts +204 -196
  71. package/src/modes/components/tree-selector.ts +144 -144
  72. package/src/modes/components/ttsr-notification.ts +17 -17
  73. package/src/modes/components/user-message-selector.ts +18 -18
  74. package/src/modes/components/welcome.ts +10 -10
  75. package/src/modes/controllers/command-controller.ts +0 -7
  76. package/src/modes/controllers/event-controller.ts +23 -23
  77. package/src/modes/controllers/extension-ui-controller.ts +13 -13
  78. package/src/modes/controllers/input-controller.ts +4 -9
  79. package/src/modes/interactive-mode.ts +234 -241
  80. package/src/modes/rpc/rpc-client.ts +77 -77
  81. package/src/modes/rpc/rpc-mode.ts +5 -5
  82. package/src/modes/theme/theme.ts +113 -113
  83. package/src/modes/types.ts +0 -1
  84. package/src/patch/index.ts +45 -45
  85. package/src/prompts/tools/task.md +22 -2
  86. package/src/session/agent-session.ts +463 -476
  87. package/src/session/agent-storage.ts +72 -75
  88. package/src/session/auth-storage.ts +186 -252
  89. package/src/session/history-storage.ts +36 -38
  90. package/src/session/session-manager.ts +300 -299
  91. package/src/session/session-storage.ts +65 -90
  92. package/src/ssh/connection-manager.ts +9 -9
  93. package/src/task/agents.ts +1 -1
  94. package/src/task/executor.ts +2 -2
  95. package/src/task/index.ts +13 -12
  96. package/src/task/subprocess-tool-registry.ts +5 -5
  97. package/src/tools/ask.ts +7 -7
  98. package/src/tools/bash.ts +8 -7
  99. package/src/tools/browser.ts +123 -123
  100. package/src/tools/calculator.ts +46 -46
  101. package/src/tools/context.ts +9 -9
  102. package/src/tools/exit-plan-mode.ts +5 -5
  103. package/src/tools/fetch.ts +5 -5
  104. package/src/tools/find.ts +16 -16
  105. package/src/tools/grep.ts +10 -10
  106. package/src/tools/notebook.ts +6 -6
  107. package/src/tools/output-meta.ts +10 -2
  108. package/src/tools/python.ts +12 -11
  109. package/src/tools/read.ts +17 -17
  110. package/src/tools/ssh.ts +9 -9
  111. package/src/tools/submit-result.ts +13 -13
  112. package/src/tools/todo-write.ts +6 -6
  113. package/src/tools/write.ts +10 -10
  114. package/src/tui/output-block.ts +6 -6
  115. package/src/tui/utils.ts +9 -9
  116. package/src/utils/event-bus.ts +10 -10
  117. package/src/utils/frontmatter.ts +1 -1
  118. package/src/utils/ignore-files.ts +1 -1
  119. package/src/web/search/index.ts +5 -5
  120. package/src/web/search/providers/anthropic.ts +7 -2
  121. package/examples/hooks/snake.ts +0 -342
  122. package/src/modes/components/armin.ts +0 -379
package/docs/tui.md CHANGED
@@ -20,13 +20,13 @@ interface Component {
20
20
  }
21
21
  ```
22
22
 
23
- | Member | Description |
24
- | ---------------------------- | --------------------------------------------------------------------------------------------------------- |
25
- | `render(width)` | Return array of strings (one per line). Each line **must not exceed `width`**. |
26
- | `handleInput?(data)` | Receive keyboard input when component has focus. |
27
- | `wantsKeyRelease?` | Opt-in to key release events (Kitty protocol). Default is `false` (release events are filtered out). |
28
- | `getCursorPosition?(width)` | Optional cursor position within the rendered output (0-based row/col) for hardware cursor placement. |
29
- | `invalidate()` | Clear cached render state (called when themes change or the component needs a full re-render). |
23
+ | Member | Description |
24
+ | --------------------------- | ---------------------------------------------------------------------------------------------------- |
25
+ | `render(width)` | Return array of strings (one per line). Each line **must not exceed `width`**. |
26
+ | `handleInput?(data)` | Receive keyboard input when component has focus. |
27
+ | `wantsKeyRelease?` | Opt-in to key release events (Kitty protocol). Default is `false` (release events are filtered out). |
28
+ | `getCursorPosition?(width)` | Optional cursor position within the rendered output (0-based row/col) for hardware cursor placement. |
29
+ | `invalidate()` | Clear cached render state (called when themes change or the component needs a full re-render). |
30
30
 
31
31
  ## Using Components
32
32
 
@@ -333,8 +333,8 @@ class MySelector implements Component {
333
333
  private cachedWidth?: number;
334
334
  private cachedLines?: string[];
335
335
 
336
- public onSelect?: (item: string) => void;
337
- public onCancel?: () => void;
336
+ onSelect?: (item: string) => void;
337
+ onCancel?: () => void;
338
338
 
339
339
  constructor(items: string[]) {
340
340
  this.items = items;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "11.8.2",
3
+ "version": "11.8.3",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "ompConfig": {
@@ -90,12 +90,12 @@
90
90
  "@mozilla/readability": "0.6.0",
91
91
  "@oclif/core": "^4.8.0",
92
92
  "@oclif/plugin-autocomplete": "^3.2.40",
93
- "@oh-my-pi/omp-stats": "11.8.2",
94
- "@oh-my-pi/pi-agent-core": "11.8.2",
95
- "@oh-my-pi/pi-ai": "11.8.2",
96
- "@oh-my-pi/pi-natives": "11.8.2",
97
- "@oh-my-pi/pi-tui": "11.8.2",
98
- "@oh-my-pi/pi-utils": "11.8.2",
93
+ "@oh-my-pi/omp-stats": "11.8.3",
94
+ "@oh-my-pi/pi-agent-core": "11.8.3",
95
+ "@oh-my-pi/pi-ai": "11.8.3",
96
+ "@oh-my-pi/pi-natives": "11.8.3",
97
+ "@oh-my-pi/pi-tui": "11.8.3",
98
+ "@oh-my-pi/pi-utils": "11.8.3",
99
99
  "@sinclair/typebox": "^0.34.48",
100
100
  "ajv": "^8.17.1",
101
101
  "chalk": "^5.6.2",
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Process @file CLI arguments into text content and image attachments
3
3
  */
4
- import * as fs from "node:fs/promises";
4
+ import * as fs from "node:fs";
5
5
  import * as path from "node:path";
6
6
  import type { ImageContent } from "@oh-my-pi/pi-ai";
7
7
  import { isEnoent } from "@oh-my-pi/pi-utils";
@@ -36,15 +36,10 @@ export async function processFileArguments(fileArgs: string[], options?: Process
36
36
  // Expand and resolve path (handles ~ expansion and macOS screenshot Unicode spaces)
37
37
  const absolutePath = path.resolve(resolveReadPath(fileArg, process.cwd()));
38
38
 
39
- let stat: Awaited<ReturnType<typeof fs.stat>>;
40
- try {
41
- stat = await fs.stat(absolutePath);
42
- } catch (err) {
43
- if (isEnoent(err)) {
44
- console.error(chalk.red(`Error: File not found: ${absolutePath}`));
45
- process.exit(1);
46
- }
47
- throw err;
39
+ const stat = fs.statSync(absolutePath, { throwIfNoEntry: false });
40
+ if (!stat) {
41
+ console.error(chalk.red(`Error: File not found: ${absolutePath}`));
42
+ process.exit(1);
48
43
  }
49
44
 
50
45
  const mimeType = await detectSupportedImageMimeTypeFromFile(absolutePath);
@@ -58,9 +53,9 @@ export async function processFileArguments(fileArgs: string[], options?: Process
58
53
  }
59
54
 
60
55
  // Read file, handling not-found gracefully
61
- let buffer: Buffer;
56
+ let buffer: Uint8Array;
62
57
  try {
63
- buffer = await fs.readFile(absolutePath);
58
+ buffer = await Bun.file(absolutePath).bytes();
64
59
  } catch (err) {
65
60
  if (isEnoent(err)) {
66
61
  console.error(chalk.red(`Error: File not found: ${absolutePath}`));
@@ -114,7 +109,7 @@ export async function processFileArguments(fileArgs: string[], options?: Process
114
109
  } else {
115
110
  // Handle text file
116
111
  try {
117
- const content = buffer.toString("utf-8");
112
+ const content = new TextDecoder().decode(buffer);
118
113
  text += `<file name="${absolutePath}">\n${content}\n</file>\n`;
119
114
  } catch (error: unknown) {
120
115
  const message = error instanceof Error ? error.message : String(error);
@@ -5,7 +5,7 @@ import { CommandHelp, Help } from "@oclif/core";
5
5
  import { getExtraHelpText } from "./args";
6
6
 
7
7
  export default class OclifHelp extends Help {
8
- protected async showRootHelp(): Promise<void> {
8
+ async showRootHelp(): Promise<void> {
9
9
  await super.showRootHelp();
10
10
  const rootCommand = this.config.findCommand("index");
11
11
  if (rootCommand) {
package/src/cli.ts CHANGED
@@ -8,6 +8,20 @@
8
8
  import { run } from "@oclif/core";
9
9
  import { APP_NAME } from "./config";
10
10
 
11
+ // oclif's warn() doesn't unwrap AggregateError — override to surface the real messages
12
+ const originalWarn = console.warn;
13
+ console.warn = (...args: unknown[]) => {
14
+ for (const arg of args) {
15
+ if (arg instanceof AggregateError) {
16
+ for (const err of arg.errors) {
17
+ originalWarn(err instanceof Error ? (err.stack ?? err.message) : String(err));
18
+ }
19
+ return;
20
+ }
21
+ }
22
+ originalWarn(...args);
23
+ };
24
+
11
25
  process.title = APP_NAME;
12
26
  const argv = process.argv.slice(2);
13
27
  const runArgv = argv.length === 0 || argv[0]?.startsWith("-") ? ["index", ...argv] : argv;
@@ -18,21 +18,21 @@ export class ControlledGit {
18
18
  async getDiff(staged: boolean): Promise<string> {
19
19
  const args = staged ? ["diff", "--cached"] : ["diff"];
20
20
  const result = await runGitCommand(this.cwd, args);
21
- this.ensureSuccess(result, "git diff");
21
+ this.#ensureSuccess(result, "git diff");
22
22
  return result.stdout;
23
23
  }
24
24
 
25
25
  async getDiffForFiles(files: string[], staged = true): Promise<string> {
26
26
  const args = staged ? ["diff", "--cached", "--", ...files] : ["diff", "--", ...files];
27
27
  const result = await runGitCommand(this.cwd, args);
28
- this.ensureSuccess(result, "git diff (files)");
28
+ this.#ensureSuccess(result, "git diff (files)");
29
29
  return result.stdout;
30
30
  }
31
31
 
32
32
  async getChangedFiles(staged: boolean): Promise<string[]> {
33
33
  const args = staged ? ["diff", "--cached", "--name-only"] : ["diff", "--name-only"];
34
34
  const result = await runGitCommand(this.cwd, args);
35
- this.ensureSuccess(result, "git diff --name-only");
35
+ this.#ensureSuccess(result, "git diff --name-only");
36
36
  return result.stdout
37
37
  .split("\n")
38
38
  .map(line => line.trim())
@@ -42,27 +42,27 @@ export class ControlledGit {
42
42
  async getStat(staged: boolean): Promise<string> {
43
43
  const args = staged ? ["diff", "--cached", "--stat"] : ["diff", "--stat"];
44
44
  const result = await runGitCommand(this.cwd, args);
45
- this.ensureSuccess(result, "git diff --stat");
45
+ this.#ensureSuccess(result, "git diff --stat");
46
46
  return result.stdout;
47
47
  }
48
48
 
49
49
  async getStatForFiles(files: string[], staged = true): Promise<string> {
50
50
  const args = staged ? ["diff", "--cached", "--stat", "--", ...files] : ["diff", "--stat", "--", ...files];
51
51
  const result = await runGitCommand(this.cwd, args);
52
- this.ensureSuccess(result, "git diff --stat (files)");
52
+ this.#ensureSuccess(result, "git diff --stat (files)");
53
53
  return result.stdout;
54
54
  }
55
55
 
56
56
  async getNumstat(staged: boolean): Promise<NumstatEntry[]> {
57
57
  const args = staged ? ["diff", "--cached", "--numstat"] : ["diff", "--numstat"];
58
58
  const result = await runGitCommand(this.cwd, args);
59
- this.ensureSuccess(result, "git diff --numstat");
59
+ this.#ensureSuccess(result, "git diff --numstat");
60
60
  return parseNumstat(result.stdout);
61
61
  }
62
62
 
63
63
  async getRecentCommits(count: number): Promise<string[]> {
64
64
  const result = await runGitCommand(this.cwd, ["log", `-n${count}`, "--pretty=format:%s"]);
65
- this.ensureSuccess(result, "git log");
65
+ this.#ensureSuccess(result, "git log");
66
66
  return result.stdout
67
67
  .split("\n")
68
68
  .map(line => line.trim())
@@ -71,7 +71,7 @@ export class ControlledGit {
71
71
 
72
72
  async getStagedFiles(): Promise<string[]> {
73
73
  const result = await runGitCommand(this.cwd, ["diff", "--cached", "--name-only"]);
74
- this.ensureSuccess(result, "git diff --cached --name-only");
74
+ this.#ensureSuccess(result, "git diff --cached --name-only");
75
75
  return result.stdout
76
76
  .split("\n")
77
77
  .map(line => line.trim())
@@ -80,7 +80,7 @@ export class ControlledGit {
80
80
 
81
81
  async getUntrackedFiles(): Promise<string[]> {
82
82
  const result = await runGitCommand(this.cwd, ["ls-files", "--others", "--exclude-standard"]);
83
- this.ensureSuccess(result, "git ls-files --others --exclude-standard");
83
+ this.#ensureSuccess(result, "git ls-files --others --exclude-standard");
84
84
  return result.stdout
85
85
  .split("\n")
86
86
  .map(line => line.trim())
@@ -89,12 +89,12 @@ export class ControlledGit {
89
89
 
90
90
  async stageAll(): Promise<void> {
91
91
  const result = await stageFiles(this.cwd, []);
92
- this.ensureSuccess(result, "git add -A");
92
+ this.#ensureSuccess(result, "git add -A");
93
93
  }
94
94
 
95
95
  async stageFiles(files: string[]): Promise<void> {
96
96
  const result = await stageFiles(this.cwd, files);
97
- this.ensureSuccess(result, "git add");
97
+ this.#ensureSuccess(result, "git add");
98
98
  }
99
99
 
100
100
  async stageHunks(selections: HunkSelection[]): Promise<void> {
@@ -137,7 +137,7 @@ export class ControlledGit {
137
137
  try {
138
138
  await Bun.write(tempPath, patch);
139
139
  const result = await runGitCommand(this.cwd, ["apply", "--cached", "--binary", tempPath]);
140
- this.ensureSuccess(result, "git apply --cached");
140
+ this.#ensureSuccess(result, "git apply --cached");
141
141
  } finally {
142
142
  await fs.rm(tempPath, { force: true });
143
143
  }
@@ -145,17 +145,17 @@ export class ControlledGit {
145
145
 
146
146
  async resetStaging(files: string[] = []): Promise<void> {
147
147
  const result = await resetStaging(this.cwd, files);
148
- this.ensureSuccess(result, "git reset");
148
+ this.#ensureSuccess(result, "git reset");
149
149
  }
150
150
 
151
151
  async commit(message: string): Promise<void> {
152
152
  const result = await commit(this.cwd, message);
153
- this.ensureSuccess(result, "git commit");
153
+ this.#ensureSuccess(result, "git commit");
154
154
  }
155
155
 
156
156
  async push(): Promise<void> {
157
157
  const result = await push(this.cwd);
158
- this.ensureSuccess(result, "git push");
158
+ this.#ensureSuccess(result, "git push");
159
159
  }
160
160
 
161
161
  parseDiffFiles(diff: string): FileDiff[] {
@@ -171,7 +171,7 @@ export class ControlledGit {
171
171
  return this.parseDiffHunks(diff);
172
172
  }
173
173
 
174
- private ensureSuccess(result: { exitCode: number; stderr: string }, label: string): void {
174
+ #ensureSuccess(result: { exitCode: number; stderr: string }, label: string): void {
175
175
  if (result.exitCode !== 0) {
176
176
  logger.error("commit git command failed", { label, stderr: result.stderr });
177
177
  throw new GitError(label, result.stderr);
@@ -166,11 +166,11 @@ export function formatKeyHints(keys: KeyId | KeyId[]): string {
166
166
  * Manages all keybindings (app + editor).
167
167
  */
168
168
  export class KeybindingsManager {
169
- private appActionToKeys: Map<AppAction, KeyId[]>;
169
+ #appActionToKeys: Map<AppAction, KeyId[]>;
170
170
 
171
171
  private constructor(private readonly config: KeybindingsConfig) {
172
- this.appActionToKeys = new Map();
173
- this.buildMaps();
172
+ this.#appActionToKeys = new Map();
173
+ this.#buildMaps();
174
174
  }
175
175
 
176
176
  /**
@@ -178,7 +178,7 @@ export class KeybindingsManager {
178
178
  */
179
179
  static async create(agentDir: string = getAgentDir()): Promise<KeybindingsManager> {
180
180
  const configPath = path.join(agentDir, "keybindings.json");
181
- const config = await KeybindingsManager.loadFromFile(configPath);
181
+ const config = await KeybindingsManager.#loadFromFile(configPath);
182
182
  const manager = new KeybindingsManager(config);
183
183
 
184
184
  // Set up editor keybindings globally
@@ -200,7 +200,7 @@ export class KeybindingsManager {
200
200
  return new KeybindingsManager(config);
201
201
  }
202
202
 
203
- private static async loadFromFile(path: string): Promise<KeybindingsConfig> {
203
+ static async #loadFromFile(path: string): Promise<KeybindingsConfig> {
204
204
  try {
205
205
  return await Bun.file(path).json();
206
206
  } catch (error) {
@@ -209,13 +209,13 @@ export class KeybindingsManager {
209
209
  }
210
210
  }
211
211
 
212
- private buildMaps(): void {
213
- this.appActionToKeys.clear();
212
+ #buildMaps(): void {
213
+ this.#appActionToKeys.clear();
214
214
 
215
215
  // Set defaults for app actions
216
216
  for (const [action, keys] of Object.entries(DEFAULT_APP_KEYBINDINGS)) {
217
217
  const keyArray = Array.isArray(keys) ? keys : [keys];
218
- this.appActionToKeys.set(
218
+ this.#appActionToKeys.set(
219
219
  action as AppAction,
220
220
  keyArray.map(key => normalizeKeyId(key as KeyId)),
221
221
  );
@@ -225,7 +225,7 @@ export class KeybindingsManager {
225
225
  for (const [action, keys] of Object.entries(this.config)) {
226
226
  if (keys === undefined || !isAppAction(action)) continue;
227
227
  const keyArray = Array.isArray(keys) ? keys : [keys];
228
- this.appActionToKeys.set(
228
+ this.#appActionToKeys.set(
229
229
  action,
230
230
  keyArray.map(key => normalizeKeyId(key as KeyId)),
231
231
  );
@@ -236,7 +236,7 @@ export class KeybindingsManager {
236
236
  * Check if input matches an app action.
237
237
  */
238
238
  matches(data: string, action: AppAction): boolean {
239
- const keys = this.appActionToKeys.get(action);
239
+ const keys = this.#appActionToKeys.get(action);
240
240
  if (!keys) return false;
241
241
  for (const key of keys) {
242
242
  if (matchesKey(data, key)) return true;
@@ -248,7 +248,7 @@ export class KeybindingsManager {
248
248
  * Get keys bound to an app action.
249
249
  */
250
250
  getKeys(action: AppAction): KeyId[] {
251
- return this.appActionToKeys.get(action) ?? [];
251
+ return this.#appActionToKeys.get(action) ?? [];
252
252
  }
253
253
 
254
254
  /**
@@ -253,10 +253,10 @@ function applyModelOverride(model: Model<Api>, override: ModelOverride): Model<A
253
253
  * Model registry - loads and manages models, resolves API keys via AuthStorage.
254
254
  */
255
255
  export class ModelRegistry {
256
- private models: Model<Api>[] = [];
257
- private customProviderApiKeys: Map<string, string> = new Map();
258
- private configError: ConfigError | undefined = undefined;
259
- private modelsConfigFile: ConfigFile<ModelsConfig>;
256
+ #models: Model<Api>[] = [];
257
+ #customProviderApiKeys: Map<string, string> = new Map();
258
+ #configError: ConfigError | undefined = undefined;
259
+ #modelsConfigFile: ConfigFile<ModelsConfig>;
260
260
 
261
261
  /**
262
262
  * @param authStorage - Auth storage for API key resolution
@@ -265,83 +265,48 @@ export class ModelRegistry {
265
265
  readonly authStorage: AuthStorage,
266
266
  modelsPath?: string,
267
267
  ) {
268
- this.modelsConfigFile = ModelsConfigFile.relocate(modelsPath);
268
+ this.#modelsConfigFile = ModelsConfigFile.relocate(modelsPath);
269
269
  // Set up fallback resolver for custom provider API keys
270
270
  this.authStorage.setFallbackResolver(provider => {
271
- const keyConfig = this.customProviderApiKeys.get(provider);
271
+ const keyConfig = this.#customProviderApiKeys.get(provider);
272
272
  if (keyConfig) {
273
273
  return resolveApiKeyConfig(keyConfig);
274
274
  }
275
275
  return undefined;
276
276
  });
277
277
  // Load models synchronously in constructor
278
- this.loadModels();
279
- }
280
-
281
- /**
282
- * Create an in-memory ModelRegistry instance from serialized data.
283
- * Used by subagent workers to bypass discovery and use parent's models.
284
- */
285
- static fromSerialized(data: SerializedModelRegistry, authStorage: AuthStorage): ModelRegistry {
286
- const instance = Object.create(ModelRegistry.prototype) as ModelRegistry;
287
- (instance as any).authStorage = authStorage;
288
- instance.models = data.models;
289
- instance.customProviderApiKeys = new Map(Object.entries(data.customProviderApiKeys ?? {}));
290
-
291
- authStorage.setFallbackResolver(provider => {
292
- const keyConfig = instance.customProviderApiKeys.get(provider);
293
- if (keyConfig) {
294
- return resolveApiKeyConfig(keyConfig);
295
- }
296
- return undefined;
297
- });
298
-
299
- return instance;
300
- }
301
-
302
- /**
303
- * Serialize ModelRegistry for passing to subagent workers.
304
- */
305
- serialize(): SerializedModelRegistry {
306
- const customProviderApiKeys: Record<string, string> = {};
307
- for (const [k, v] of this.customProviderApiKeys.entries()) {
308
- customProviderApiKeys[k] = v;
309
- }
310
- return {
311
- models: this.models,
312
- customProviderApiKeys: Object.keys(customProviderApiKeys).length > 0 ? customProviderApiKeys : undefined,
313
- };
278
+ this.#loadModels();
314
279
  }
315
280
 
316
281
  /**
317
282
  * Reload models from disk (built-in + custom from models.json).
318
283
  */
319
284
  refresh(): void {
320
- this.modelsConfigFile.invalidate();
321
- this.customProviderApiKeys.clear();
322
- this.configError = undefined;
323
- this.loadModels();
285
+ this.#modelsConfigFile.invalidate();
286
+ this.#customProviderApiKeys.clear();
287
+ this.#configError = undefined;
288
+ this.#loadModels();
324
289
  }
325
290
 
326
291
  /**
327
292
  * Get any error from loading models.json (undefined if no error).
328
293
  */
329
294
  getError(): ConfigError | undefined {
330
- return this.configError;
295
+ return this.#configError;
331
296
  }
332
297
 
333
- private loadModels() {
298
+ #loadModels() {
334
299
  // Load custom models from models.json first (to know which providers to override)
335
300
  const {
336
301
  models: customModels = [],
337
302
  overrides = new Map(),
338
303
  modelOverrides = new Map(),
339
304
  error: configError,
340
- } = this.loadCustomModels();
341
- this.configError = configError;
305
+ } = this.#loadCustomModels();
306
+ this.#configError = configError;
342
307
 
343
- const builtInModels = this.loadBuiltInModels(overrides, modelOverrides);
344
- const combined = this.mergeCustomModels(builtInModels, customModels);
308
+ const builtInModels = this.#loadBuiltInModels(overrides, modelOverrides);
309
+ const combined = this.#mergeCustomModels(builtInModels, customModels);
345
310
 
346
311
  // Update github-copilot base URL based on OAuth credentials
347
312
  const copilotCred = this.authStorage.getOAuthCredential("github-copilot");
@@ -350,14 +315,14 @@ export class ModelRegistry {
350
315
  ? (normalizeDomain(copilotCred.enterpriseUrl) ?? undefined)
351
316
  : undefined;
352
317
  const baseUrl = getGitHubCopilotBaseUrl(copilotCred.access, domain);
353
- this.models = combined.map(m => (m.provider === "github-copilot" ? { ...m, baseUrl } : m));
318
+ this.#models = combined.map(m => (m.provider === "github-copilot" ? { ...m, baseUrl } : m));
354
319
  } else {
355
- this.models = combined;
320
+ this.#models = combined;
356
321
  }
357
322
  }
358
323
 
359
324
  /** Load built-in models, applying provider and per-model overrides */
360
- private loadBuiltInModels(
325
+ #loadBuiltInModels(
361
326
  overrides: Map<string, ProviderOverride>,
362
327
  modelOverrides: Map<string, Map<string, ModelOverride>>,
363
328
  ): Model<Api>[] {
@@ -385,7 +350,7 @@ export class ModelRegistry {
385
350
  }
386
351
 
387
352
  /** Merge custom models with built-in, replacing by provider+id match */
388
- private mergeCustomModels(builtInModels: Model<Api>[], customModels: Model<Api>[]): Model<Api>[] {
353
+ #mergeCustomModels(builtInModels: Model<Api>[], customModels: Model<Api>[]): Model<Api>[] {
389
354
  const merged = [...builtInModels];
390
355
  for (const customModel of customModels) {
391
356
  const existingIndex = merged.findIndex(m => m.provider === customModel.provider && m.id === customModel.id);
@@ -398,8 +363,8 @@ export class ModelRegistry {
398
363
  return merged;
399
364
  }
400
365
 
401
- private loadCustomModels(): CustomModelsResult {
402
- const { value, error, status } = this.modelsConfigFile.tryLoad();
366
+ #loadCustomModels(): CustomModelsResult {
367
+ const { value, error, status } = this.#modelsConfigFile.tryLoad();
403
368
 
404
369
  if (status === "error") {
405
370
  return { models: [], overrides: new Map(), modelOverrides: new Map(), error, found: true };
@@ -422,7 +387,7 @@ export class ModelRegistry {
422
387
 
423
388
  // Always store API key for fallback resolver
424
389
  if (providerConfig.apiKey) {
425
- this.customProviderApiKeys.set(providerName, providerConfig.apiKey);
390
+ this.#customProviderApiKeys.set(providerName, providerConfig.apiKey);
426
391
  }
427
392
 
428
393
  // Parse per-model overrides
@@ -435,10 +400,10 @@ export class ModelRegistry {
435
400
  }
436
401
  }
437
402
 
438
- return { models: this.parseModels(value), overrides, modelOverrides: allModelOverrides, found: true };
403
+ return { models: this.#parseModels(value), overrides, modelOverrides: allModelOverrides, found: true };
439
404
  }
440
405
 
441
- private parseModels(config: ModelsConfig): Model<Api>[] {
406
+ #parseModels(config: ModelsConfig): Model<Api>[] {
442
407
  const models: Model<Api>[] = [];
443
408
 
444
409
  for (const [providerName, providerConfig] of Object.entries(config.providers)) {
@@ -447,7 +412,7 @@ export class ModelRegistry {
447
412
 
448
413
  // Store API key config for fallback resolver
449
414
  if (providerConfig.apiKey) {
450
- this.customProviderApiKeys.set(providerName, providerConfig.apiKey);
415
+ this.#customProviderApiKeys.set(providerName, providerConfig.apiKey);
451
416
  }
452
417
 
453
418
  for (const modelDef of modelDefs) {
@@ -496,7 +461,7 @@ export class ModelRegistry {
496
461
  * If models.json had errors, returns only built-in models.
497
462
  */
498
463
  getAll(): Model<Api>[] {
499
- return this.models;
464
+ return this.#models;
500
465
  }
501
466
 
502
467
  /**
@@ -504,21 +469,21 @@ export class ModelRegistry {
504
469
  * This is a fast check that doesn't refresh OAuth tokens.
505
470
  */
506
471
  getAvailable(): Model<Api>[] {
507
- return this.models.filter(m => this.authStorage.hasAuth(m.provider));
472
+ return this.#models.filter(m => this.authStorage.hasAuth(m.provider));
508
473
  }
509
474
 
510
475
  /**
511
476
  * Find a model by provider and ID.
512
477
  */
513
478
  find(provider: string, modelId: string): Model<Api> | undefined {
514
- return this.models.find(m => m.provider === provider && m.id === modelId);
479
+ return this.#models.find(m => m.provider === provider && m.id === modelId);
515
480
  }
516
481
 
517
482
  /**
518
483
  * Get the base URL associated with a provider, if any model defines one.
519
484
  */
520
485
  getProviderBaseUrl(provider: string): string | undefined {
521
- return this.models.find(m => m.provider === provider && m.baseUrl)?.baseUrl;
486
+ return this.#models.find(m => m.provider === provider && m.baseUrl)?.baseUrl;
522
487
  }
523
488
 
524
489
  /**