@oh-my-pi/pi-coding-agent 11.0.3 → 11.2.0

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 (143) hide show
  1. package/CHANGELOG.md +199 -49
  2. package/README.md +1 -1
  3. package/docs/config-usage.md +3 -4
  4. package/docs/sdk.md +6 -5
  5. package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
  6. package/examples/sdk/README.md +1 -1
  7. package/package.json +19 -11
  8. package/src/cli/args.ts +11 -94
  9. package/src/cli/config-cli.ts +1 -1
  10. package/src/cli/file-processor.ts +3 -3
  11. package/src/cli/oclif-help.ts +26 -0
  12. package/src/cli/web-search-cli.ts +148 -0
  13. package/src/cli.ts +8 -2
  14. package/src/commands/commit.ts +36 -0
  15. package/src/commands/config.ts +51 -0
  16. package/src/commands/grep.ts +41 -0
  17. package/src/commands/index/index.ts +136 -0
  18. package/src/commands/jupyter.ts +32 -0
  19. package/src/commands/plugin.ts +70 -0
  20. package/src/commands/setup.ts +39 -0
  21. package/src/commands/shell.ts +29 -0
  22. package/src/commands/stats.ts +29 -0
  23. package/src/commands/update.ts +21 -0
  24. package/src/commands/web-search.ts +50 -0
  25. package/src/commit/agentic/index.ts +3 -2
  26. package/src/commit/agentic/tools/analyze-file.ts +1 -3
  27. package/src/commit/git/errors.ts +4 -6
  28. package/src/commit/pipeline.ts +3 -2
  29. package/src/config/keybindings.ts +1 -3
  30. package/src/config/model-registry.ts +89 -162
  31. package/src/config/settings-schema.ts +10 -0
  32. package/src/config.ts +202 -132
  33. package/src/exa/mcp-client.ts +8 -41
  34. package/src/export/html/index.ts +1 -1
  35. package/src/extensibility/extensions/loader.ts +7 -10
  36. package/src/extensibility/extensions/runner.ts +5 -15
  37. package/src/extensibility/extensions/types.ts +1 -1
  38. package/src/extensibility/hooks/runner.ts +6 -9
  39. package/src/index.ts +0 -1
  40. package/src/ipy/kernel.ts +10 -22
  41. package/src/lsp/clients/biome-client.ts +4 -7
  42. package/src/lsp/clients/lsp-linter-client.ts +4 -6
  43. package/src/lsp/index.ts +5 -4
  44. package/src/lsp/utils.ts +18 -0
  45. package/src/main.ts +86 -181
  46. package/src/mcp/json-rpc.ts +2 -2
  47. package/src/mcp/transports/http.ts +12 -49
  48. package/src/modes/components/armin.ts +1 -3
  49. package/src/modes/components/assistant-message.ts +4 -4
  50. package/src/modes/components/bash-execution.ts +5 -3
  51. package/src/modes/components/branch-summary-message.ts +1 -3
  52. package/src/modes/components/compaction-summary-message.ts +1 -3
  53. package/src/modes/components/custom-message.ts +4 -5
  54. package/src/modes/components/extensions/extension-dashboard.ts +10 -16
  55. package/src/modes/components/extensions/extension-list.ts +5 -5
  56. package/src/modes/components/footer.ts +1 -4
  57. package/src/modes/components/hook-editor.ts +7 -32
  58. package/src/modes/components/hook-message.ts +4 -5
  59. package/src/modes/components/model-selector.ts +2 -2
  60. package/src/modes/components/plugin-settings.ts +16 -20
  61. package/src/modes/components/python-execution.ts +5 -5
  62. package/src/modes/components/session-selector.ts +6 -7
  63. package/src/modes/components/settings-defs.ts +49 -40
  64. package/src/modes/components/settings-selector.ts +8 -17
  65. package/src/modes/components/skill-message.ts +1 -3
  66. package/src/modes/components/status-line-segment-editor.ts +1 -3
  67. package/src/modes/components/status-line.ts +1 -3
  68. package/src/modes/components/todo-reminder.ts +5 -7
  69. package/src/modes/components/tree-selector.ts +10 -12
  70. package/src/modes/components/ttsr-notification.ts +1 -3
  71. package/src/modes/components/user-message-selector.ts +2 -4
  72. package/src/modes/components/welcome.ts +6 -18
  73. package/src/modes/controllers/event-controller.ts +1 -0
  74. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  75. package/src/modes/controllers/input-controller.ts +7 -34
  76. package/src/modes/controllers/selector-controller.ts +3 -3
  77. package/src/modes/interactive-mode.ts +27 -1
  78. package/src/modes/rpc/rpc-client.ts +2 -5
  79. package/src/modes/rpc/rpc-mode.ts +2 -2
  80. package/src/modes/theme/theme.ts +2 -6
  81. package/src/modes/types.ts +1 -0
  82. package/src/modes/utils/ui-helpers.ts +6 -1
  83. package/src/patch/index.ts +1 -4
  84. package/src/prompts/agents/explore.md +1 -0
  85. package/src/prompts/agents/frontmatter.md +2 -1
  86. package/src/prompts/agents/init.md +1 -0
  87. package/src/prompts/agents/plan.md +1 -0
  88. package/src/prompts/agents/reviewer.md +1 -0
  89. package/src/prompts/system/subagent-submit-reminder.md +2 -0
  90. package/src/prompts/system/subagent-system-prompt.md +2 -0
  91. package/src/prompts/system/subagent-user-prompt.md +8 -0
  92. package/src/prompts/system/system-prompt.md +5 -3
  93. package/src/prompts/system/web-search.md +6 -4
  94. package/src/prompts/tools/task.md +216 -163
  95. package/src/sdk.ts +11 -110
  96. package/src/session/agent-session.ts +117 -83
  97. package/src/session/auth-storage.ts +10 -51
  98. package/src/session/messages.ts +17 -3
  99. package/src/session/session-manager.ts +30 -30
  100. package/src/session/streaming-output.ts +1 -1
  101. package/src/ssh/ssh-executor.ts +6 -3
  102. package/src/task/agents.ts +2 -0
  103. package/src/task/discovery.ts +1 -1
  104. package/src/task/executor.ts +5 -10
  105. package/src/task/index.ts +43 -23
  106. package/src/task/render.ts +67 -64
  107. package/src/task/template.ts +17 -34
  108. package/src/task/types.ts +49 -22
  109. package/src/tools/ask.ts +1 -3
  110. package/src/tools/bash.ts +1 -4
  111. package/src/tools/browser.ts +5 -7
  112. package/src/tools/exit-plan-mode.ts +1 -4
  113. package/src/tools/fetch.ts +1 -3
  114. package/src/tools/find.ts +4 -3
  115. package/src/tools/gemini-image.ts +24 -55
  116. package/src/tools/grep.ts +4 -4
  117. package/src/tools/index.ts +12 -14
  118. package/src/tools/notebook.ts +1 -5
  119. package/src/tools/python.ts +4 -3
  120. package/src/tools/read.ts +2 -4
  121. package/src/tools/render-utils.ts +23 -0
  122. package/src/tools/ssh.ts +8 -12
  123. package/src/tools/todo-write.ts +1 -4
  124. package/src/tools/tool-errors.ts +1 -4
  125. package/src/tools/write.ts +1 -3
  126. package/src/utils/external-editor.ts +59 -0
  127. package/src/utils/file-mentions.ts +39 -1
  128. package/src/utils/image-convert.ts +1 -1
  129. package/src/utils/image-resize.ts +4 -4
  130. package/src/web/search/auth.ts +3 -33
  131. package/src/web/search/index.ts +73 -139
  132. package/src/web/search/provider.ts +58 -0
  133. package/src/web/search/providers/anthropic.ts +53 -14
  134. package/src/web/search/providers/base.ts +22 -0
  135. package/src/web/search/providers/codex.ts +38 -16
  136. package/src/web/search/providers/exa.ts +30 -6
  137. package/src/web/search/providers/gemini.ts +56 -20
  138. package/src/web/search/providers/jina.ts +28 -5
  139. package/src/web/search/providers/perplexity.ts +103 -36
  140. package/src/web/search/render.ts +84 -74
  141. package/src/web/search/types.ts +285 -59
  142. package/src/migrations.ts +0 -175
  143. package/src/session/storage-migration.ts +0 -173
@@ -1,8 +1,3 @@
1
- /**
2
- * Model registry - manages built-in and custom models, provides API key resolution.
3
- */
4
- import * as fs from "node:fs";
5
- import * as path from "node:path";
6
1
  import {
7
2
  type Api,
8
3
  getGitHubCopilotBaseUrl,
@@ -11,10 +6,9 @@ import {
11
6
  type Model,
12
7
  normalizeDomain,
13
8
  } from "@oh-my-pi/pi-ai";
14
- import { isEnoent, logger } from "@oh-my-pi/pi-utils";
9
+ import { type ConfigError, ConfigFile } from "@oh-my-pi/pi-coding-agent/config";
15
10
  import { type Static, Type } from "@sinclair/typebox";
16
11
  import AjvModule from "ajv";
17
- import { YAML } from "bun";
18
12
  import type { ThemeColor } from "../modes/theme/theme";
19
13
  import type { AuthStorage } from "../session/auth-storage";
20
14
 
@@ -36,7 +30,7 @@ export const MODEL_ROLES: Record<ModelRole, ModelRoleInfo> = {
36
30
 
37
31
  export const MODEL_ROLE_IDS: ModelRole[] = ["default", "smol", "slow", "plan", "commit"];
38
32
 
39
- const Ajv = (AjvModule as any).default || AjvModule;
33
+ const _Ajv = (AjvModule as any).default || AjvModule;
40
34
 
41
35
  const OpenRouterRoutingSchema = Type.Object({
42
36
  only: Type.Optional(Type.Array(Type.String())),
@@ -113,6 +107,50 @@ const ModelsConfigSchema = Type.Object({
113
107
 
114
108
  type ModelsConfig = Static<typeof ModelsConfigSchema>;
115
109
 
110
+ export const ModelsConfigFile = new ConfigFile<ModelsConfig>("models", ModelsConfigSchema).withValidation(
111
+ "models",
112
+ config => {
113
+ for (const [providerName, providerConfig] of Object.entries(config.providers)) {
114
+ const hasProviderApi = !!providerConfig.api;
115
+ const models = providerConfig.models ?? [];
116
+
117
+ if (models.length === 0) {
118
+ // Override-only config: just needs baseUrl (to override built-in)
119
+ if (!providerConfig.baseUrl) {
120
+ throw new Error(
121
+ `Provider ${providerName}: must specify either "baseUrl" (for override) or "models" (for replacement).`,
122
+ );
123
+ }
124
+ } else {
125
+ // Full replacement: needs baseUrl and apiKey
126
+ if (!providerConfig.baseUrl) {
127
+ throw new Error(`Provider ${providerName}: "baseUrl" is required when defining custom models.`);
128
+ }
129
+ if (!providerConfig.apiKey) {
130
+ throw new Error(`Provider ${providerName}: "apiKey" is required when defining custom models.`);
131
+ }
132
+ }
133
+
134
+ for (const modelDef of models) {
135
+ const hasModelApi = !!modelDef.api;
136
+
137
+ if (!hasProviderApi && !hasModelApi) {
138
+ throw new Error(
139
+ `Provider ${providerName}, model ${modelDef.id}: no "api" specified. Set at provider or model level.`,
140
+ );
141
+ }
142
+
143
+ if (!modelDef.id) throw new Error(`Provider ${providerName}: model missing "id"`);
144
+ if (!modelDef.name) throw new Error(`Provider ${providerName}: model missing "name"`);
145
+ if (modelDef.contextWindow <= 0)
146
+ throw new Error(`Provider ${providerName}, model ${modelDef.id}: invalid contextWindow`);
147
+ if (modelDef.maxTokens <= 0)
148
+ throw new Error(`Provider ${providerName}, model ${modelDef.id}: invalid maxTokens`);
149
+ }
150
+ }
151
+ },
152
+ );
153
+
116
154
  /** Provider override config (baseUrl, headers, apiKey) without custom models */
117
155
  interface ProviderOverride {
118
156
  baseUrl?: string;
@@ -126,25 +164,19 @@ interface ProviderOverride {
126
164
  export interface SerializedModelRegistry {
127
165
  models: Model<Api>[];
128
166
  customProviderApiKeys?: Record<string, string>;
129
- loadError?: string;
130
167
  }
131
168
 
132
169
  /** Result of loading custom models from models.json */
133
170
  interface CustomModelsResult {
134
- models: Model<Api>[];
171
+ models?: Model<Api>[];
135
172
  /** Providers with custom models (full replacement) */
136
- replacedProviders: Set<string>;
173
+ replacedProviders?: Set<string>;
137
174
  /** Providers with only baseUrl/headers override (no custom models) */
138
- overrides: Map<string, ProviderOverride>;
139
- error: string | undefined;
140
- /** Whether the file was found (true) or didn't exist (false) */
175
+ overrides?: Map<string, ProviderOverride>;
176
+ error?: ConfigError;
141
177
  found: boolean;
142
178
  }
143
179
 
144
- function emptyCustomModelsResult(error?: string): CustomModelsResult {
145
- return { models: [], replacedProviders: new Set(), overrides: new Map(), error, found: false };
146
- }
147
-
148
180
  /**
149
181
  * Resolve an API key config value to an actual key.
150
182
  * Checks environment variable first, then treats as literal.
@@ -161,18 +193,17 @@ function resolveApiKeyConfig(keyConfig: string): string | undefined {
161
193
  export class ModelRegistry {
162
194
  private models: Model<Api>[] = [];
163
195
  private customProviderApiKeys: Map<string, string> = new Map();
164
- private loadError: string | undefined = undefined;
196
+ private configError: ConfigError | undefined = undefined;
197
+ private modelsConfigFile: ConfigFile<ModelsConfig>;
165
198
 
166
199
  /**
167
200
  * @param authStorage - Auth storage for API key resolution
168
- * @param modelsJsonPath - Primary path for models.json
169
- * @param fallbackPaths - Additional paths to check (legacy support)
170
201
  */
171
202
  constructor(
172
203
  readonly authStorage: AuthStorage,
173
- private modelsJsonPath: string | undefined = undefined,
174
- private fallbackPaths: string[] = [],
204
+ modelsPath?: string,
175
205
  ) {
206
+ this.modelsConfigFile = ModelsConfigFile.relocate(modelsPath);
176
207
  // Set up fallback resolver for custom provider API keys
177
208
  this.authStorage.setFallbackResolver(provider => {
178
209
  const keyConfig = this.customProviderApiKeys.get(provider);
@@ -194,7 +225,6 @@ export class ModelRegistry {
194
225
  (instance as any).authStorage = authStorage;
195
226
  instance.models = data.models;
196
227
  instance.customProviderApiKeys = new Map(Object.entries(data.customProviderApiKeys ?? {}));
197
- instance.loadError = data.loadError;
198
228
 
199
229
  authStorage.setFallbackResolver(provider => {
200
230
  const keyConfig = instance.customProviderApiKeys.get(provider);
@@ -218,7 +248,6 @@ export class ModelRegistry {
218
248
  return {
219
249
  models: this.models,
220
250
  customProviderApiKeys: Object.keys(customProviderApiKeys).length > 0 ? customProviderApiKeys : undefined,
221
- loadError: this.loadError,
222
251
  };
223
252
  }
224
253
 
@@ -226,45 +255,28 @@ export class ModelRegistry {
226
255
  * Reload models from disk (built-in + custom from models.json).
227
256
  */
228
257
  refresh(): void {
258
+ this.modelsConfigFile.invalidate();
229
259
  this.customProviderApiKeys.clear();
230
- this.loadError = undefined;
260
+ this.configError = undefined;
231
261
  this.loadModels();
232
262
  }
233
263
 
234
264
  /**
235
265
  * Get any error from loading models.json (undefined if no error).
236
266
  */
237
- getError(): string | undefined {
238
- return this.loadError;
267
+ getError(): ConfigError | undefined {
268
+ return this.configError;
239
269
  }
240
270
 
241
271
  private loadModels() {
242
272
  // Load custom models from models.json first (to know which providers to skip/override)
243
- let customModels: Model<Api>[] = [];
244
- let replacedProviders: Set<string> = new Set();
245
- let overrides: Map<string, ProviderOverride> = new Map();
246
- const pathsToCheck = this.modelsJsonPath ? [this.modelsJsonPath, ...this.fallbackPaths] : this.fallbackPaths;
247
-
248
- if (pathsToCheck.length > 0) {
249
- logger.debug("ModelRegistry.loadModels checking paths", { paths: pathsToCheck });
250
- }
251
-
252
- for (const modelsPath of pathsToCheck) {
253
- const result = this.loadCustomModels(modelsPath);
254
- if (!result.found) {
255
- continue; // File doesn't exist, try next path
256
- }
257
- logger.debug("ModelRegistry.loadModels loading", { path: modelsPath });
258
- if (result.error) {
259
- this.loadError = result.error;
260
- // Keep built-in models even if custom models failed to load
261
- } else {
262
- customModels = result.models;
263
- replacedProviders = result.replacedProviders;
264
- overrides = result.overrides;
265
- }
266
- break; // Use first existing file
267
- }
273
+ const {
274
+ models: customModels = [],
275
+ replacedProviders = new Set(),
276
+ overrides = new Map(),
277
+ error: configError,
278
+ } = this.loadCustomModels();
279
+ this.configError = configError;
268
280
 
269
281
  const builtInModels = this.loadBuiltInModels(replacedProviders, overrides);
270
282
  const combined = [...builtInModels, ...customModels];
@@ -300,123 +312,38 @@ export class ModelRegistry {
300
312
  });
301
313
  }
302
314
 
303
- private loadCustomModels(modelsPath: string): CustomModelsResult {
304
- let content: string;
305
- try {
306
- content = fs.readFileSync(modelsPath, "utf-8");
307
- } catch (error) {
308
- if (isEnoent(error)) {
309
- return emptyCustomModelsResult();
310
- }
311
- return {
312
- ...emptyCustomModelsResult(
313
- `Failed to load models config: ${error instanceof Error ? error.message : error}\n\nFile: ${modelsPath}`,
314
- ),
315
- found: true,
316
- };
317
- }
318
-
319
- try {
320
- const ext = path.extname(modelsPath).toLowerCase();
321
- let config: ModelsConfig;
322
-
323
- if (ext === ".yaml" || ext === ".yml") {
324
- config = YAML.parse(content) as ModelsConfig;
325
- } else {
326
- config = JSON.parse(content) as ModelsConfig;
327
- }
328
-
329
- // Validate schema
330
- const ajv = new Ajv();
331
- const validate = ajv.compile(ModelsConfigSchema);
332
- if (!validate(config)) {
333
- const errors =
334
- validate.errors?.map((e: any) => ` - ${e.instancePath || "root"}: ${e.message}`).join("\n") ||
335
- "Unknown schema error";
336
- return emptyCustomModelsResult(`Invalid models config schema:\n${errors}\n\nFile: ${modelsPath}`);
337
- }
338
-
339
- // Additional validation
340
- this.validateConfig(config);
341
-
342
- // Separate providers into "full replacement" (has models) vs "override-only" (no models)
343
- const replacedProviders = new Set<string>();
344
- const overrides = new Map<string, ProviderOverride>();
345
-
346
- for (const [providerName, providerConfig] of Object.entries(config.providers)) {
347
- if (providerConfig.models && providerConfig.models.length > 0) {
348
- // Has custom models -> full replacement
349
- replacedProviders.add(providerName);
350
- } else {
351
- // No models -> just override baseUrl/headers on built-in
352
- overrides.set(providerName, {
353
- baseUrl: providerConfig.baseUrl,
354
- headers: providerConfig.headers,
355
- apiKey: providerConfig.apiKey,
356
- });
357
- // Store API key for fallback resolver
358
- if (providerConfig.apiKey) {
359
- this.customProviderApiKeys.set(providerName, providerConfig.apiKey);
360
- }
361
- }
362
- }
315
+ private loadCustomModels(): CustomModelsResult {
316
+ const { value, error, status } = this.modelsConfigFile.tryLoad();
363
317
 
364
- return { models: this.parseModels(config), replacedProviders, overrides, error: undefined, found: true };
365
- } catch (error) {
366
- if (error instanceof SyntaxError) {
367
- return {
368
- ...emptyCustomModelsResult(`Failed to parse models config: ${error.message}\n\nFile: ${modelsPath}`),
369
- found: true,
370
- };
371
- }
372
- return {
373
- ...emptyCustomModelsResult(
374
- `Failed to load models config: ${error instanceof Error ? error.message : error}\n\nFile: ${modelsPath}`,
375
- ),
376
- found: true,
377
- };
318
+ if (status === "error") {
319
+ return { models: [], replacedProviders: new Set(), overrides: new Map(), error, found: true };
320
+ } else if (status === "not-found") {
321
+ return { models: [], replacedProviders: new Set(), overrides: new Map(), found: false };
378
322
  }
379
- }
380
323
 
381
- private validateConfig(config: ModelsConfig): void {
382
- for (const [providerName, providerConfig] of Object.entries(config.providers)) {
383
- const hasProviderApi = !!providerConfig.api;
384
- const models = providerConfig.models ?? [];
324
+ // Separate providers into "full replacement" (has models) vs "override-only" (no models)
325
+ const replacedProviders = new Set<string>();
326
+ const overrides = new Map<string, ProviderOverride>();
385
327
 
386
- if (models.length === 0) {
387
- // Override-only config: just needs baseUrl (to override built-in)
388
- if (!providerConfig.baseUrl) {
389
- throw new Error(
390
- `Provider ${providerName}: must specify either "baseUrl" (for override) or "models" (for replacement).`,
391
- );
392
- }
328
+ for (const [providerName, providerConfig] of Object.entries(value.providers)) {
329
+ if (providerConfig.models && providerConfig.models.length > 0) {
330
+ // Has custom models -> full replacement
331
+ replacedProviders.add(providerName);
393
332
  } else {
394
- // Full replacement: needs baseUrl and apiKey
395
- if (!providerConfig.baseUrl) {
396
- throw new Error(`Provider ${providerName}: "baseUrl" is required when defining custom models.`);
397
- }
398
- if (!providerConfig.apiKey) {
399
- throw new Error(`Provider ${providerName}: "apiKey" is required when defining custom models.`);
400
- }
401
- }
402
-
403
- for (const modelDef of models) {
404
- const hasModelApi = !!modelDef.api;
405
-
406
- if (!hasProviderApi && !hasModelApi) {
407
- throw new Error(
408
- `Provider ${providerName}, model ${modelDef.id}: no "api" specified. Set at provider or model level.`,
409
- );
333
+ // No models -> just override baseUrl/headers on built-in
334
+ overrides.set(providerName, {
335
+ baseUrl: providerConfig.baseUrl,
336
+ headers: providerConfig.headers,
337
+ apiKey: providerConfig.apiKey,
338
+ });
339
+ // Store API key for fallback resolver
340
+ if (providerConfig.apiKey) {
341
+ this.customProviderApiKeys.set(providerName, providerConfig.apiKey);
410
342
  }
411
-
412
- if (!modelDef.id) throw new Error(`Provider ${providerName}: model missing "id"`);
413
- if (!modelDef.name) throw new Error(`Provider ${providerName}: model missing "name"`);
414
- if (modelDef.contextWindow <= 0)
415
- throw new Error(`Provider ${providerName}, model ${modelDef.id}: invalid contextWindow`);
416
- if (modelDef.maxTokens <= 0)
417
- throw new Error(`Provider ${providerName}, model ${modelDef.id}: invalid maxTokens`);
418
343
  }
419
344
  }
345
+
346
+ return { models: this.parseModels(value), replacedProviders, overrides, found: true };
420
347
  }
421
348
 
422
349
  private parseModels(config: ModelsConfig): Model<Api>[] {
@@ -399,6 +399,16 @@ export const SETTINGS_SCHEMA = {
399
399
  // ─────────────────────────────────────────────────────────────────────────
400
400
  // Task tool settings
401
401
  // ─────────────────────────────────────────────────────────────────────────
402
+ "task.isolation.enabled": {
403
+ type: "boolean",
404
+ default: false,
405
+ ui: {
406
+ tab: "tools",
407
+ label: "Task isolation",
408
+ description: "Run subagents in isolated git worktrees",
409
+ submenu: true,
410
+ },
411
+ },
402
412
  "task.maxConcurrency": {
403
413
  type: "number",
404
414
  default: 32,