@oh-my-pi/pi-coding-agent 15.0.1 → 15.1.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 (168) hide show
  1. package/CHANGELOG.md +94 -1
  2. package/examples/custom-tools/README.md +11 -7
  3. package/examples/custom-tools/hello/index.ts +2 -2
  4. package/examples/extensions/README.md +19 -8
  5. package/examples/extensions/api-demo.ts +15 -19
  6. package/examples/extensions/hello.ts +5 -6
  7. package/examples/extensions/plan-mode.ts +1 -1
  8. package/examples/extensions/reload-runtime.ts +4 -3
  9. package/examples/extensions/with-deps/index.ts +4 -3
  10. package/examples/sdk/06-extensions.ts +4 -2
  11. package/package.json +8 -18
  12. package/src/autoresearch/tools/init-experiment.ts +38 -41
  13. package/src/autoresearch/tools/log-experiment.ts +32 -41
  14. package/src/autoresearch/tools/run-experiment.ts +3 -3
  15. package/src/autoresearch/tools/update-notes.ts +11 -11
  16. package/src/commands/commit.ts +10 -0
  17. package/src/commit/agentic/tools/analyze-file.ts +4 -4
  18. package/src/commit/agentic/tools/git-file-diff.ts +4 -4
  19. package/src/commit/agentic/tools/git-hunk.ts +5 -5
  20. package/src/commit/agentic/tools/git-overview.ts +4 -4
  21. package/src/commit/agentic/tools/propose-changelog.ts +13 -13
  22. package/src/commit/agentic/tools/propose-commit.ts +6 -6
  23. package/src/commit/agentic/tools/recent-commits.ts +3 -3
  24. package/src/commit/agentic/tools/schemas.ts +28 -28
  25. package/src/commit/agentic/tools/split-commit.ts +22 -21
  26. package/src/commit/analysis/summary.ts +4 -4
  27. package/src/commit/changelog/generate.ts +7 -11
  28. package/src/commit/shared-llm.ts +22 -34
  29. package/src/config/config-file.ts +35 -13
  30. package/src/config/model-registry.ts +40 -191
  31. package/src/config/models-config-schema.ts +166 -0
  32. package/src/config/settings-schema.ts +29 -0
  33. package/src/discovery/claude-plugins.ts +19 -7
  34. package/src/edit/index.ts +2 -2
  35. package/src/edit/modes/apply-patch.ts +7 -6
  36. package/src/edit/modes/patch.ts +18 -25
  37. package/src/edit/modes/replace.ts +18 -20
  38. package/src/eval/js/shared/rewrite-imports.ts +131 -10
  39. package/src/eval/py/executor.ts +233 -623
  40. package/src/eval/py/kernel.ts +27 -2
  41. package/src/eval/py/runner.py +42 -11
  42. package/src/eval/py/runtime.ts +1 -0
  43. package/src/exa/factory.ts +5 -4
  44. package/src/exa/mcp-client.ts +1 -1
  45. package/src/exa/researcher.ts +9 -20
  46. package/src/exa/search.ts +26 -52
  47. package/src/exa/types.ts +1 -1
  48. package/src/exa/websets.ts +54 -53
  49. package/src/exec/bash-executor.ts +2 -1
  50. package/src/extensibility/custom-commands/loader.ts +5 -3
  51. package/src/extensibility/custom-commands/types.ts +4 -2
  52. package/src/extensibility/custom-tools/loader.ts +5 -3
  53. package/src/extensibility/custom-tools/types.ts +7 -6
  54. package/src/extensibility/custom-tools/wrapper.ts +1 -1
  55. package/src/extensibility/extensions/get-commands-handler.ts +77 -0
  56. package/src/extensibility/extensions/loader.ts +7 -3
  57. package/src/extensibility/extensions/types.ts +9 -5
  58. package/src/extensibility/extensions/wrapper.ts +1 -2
  59. package/src/extensibility/hooks/loader.ts +3 -1
  60. package/src/extensibility/hooks/tool-wrapper.ts +1 -1
  61. package/src/extensibility/hooks/types.ts +4 -2
  62. package/src/extensibility/plugins/legacy-pi-compat.ts +78 -31
  63. package/src/extensibility/shared-events.ts +1 -1
  64. package/src/extensibility/typebox.ts +391 -0
  65. package/src/goals/tools/goal-tool.ts +6 -12
  66. package/src/hashline/input.ts +2 -1
  67. package/src/hashline/parser.ts +27 -3
  68. package/src/hashline/types.ts +4 -4
  69. package/src/hindsight/state.ts +2 -2
  70. package/src/index.ts +0 -2
  71. package/src/internal-urls/docs-index.generated.ts +15 -15
  72. package/src/internal-urls/router.ts +8 -0
  73. package/src/internal-urls/types.ts +21 -0
  74. package/src/lsp/config.ts +15 -6
  75. package/src/lsp/defaults.json +6 -2
  76. package/src/lsp/types.ts +30 -38
  77. package/src/mcp/manager.ts +1 -1
  78. package/src/mcp/tool-bridge.ts +1 -1
  79. package/src/modes/acp/acp-agent.ts +248 -50
  80. package/src/modes/components/session-observer-overlay.ts +12 -1
  81. package/src/modes/components/status-line/segments.ts +39 -4
  82. package/src/modes/controllers/command-controller.ts +27 -2
  83. package/src/modes/controllers/event-controller.ts +3 -4
  84. package/src/modes/controllers/extension-ui-controller.ts +3 -2
  85. package/src/modes/interactive-mode.ts +1 -1
  86. package/src/modes/rpc/host-tools.ts +1 -1
  87. package/src/modes/rpc/host-uris.ts +235 -0
  88. package/src/modes/rpc/rpc-client.ts +1 -1
  89. package/src/modes/rpc/rpc-mode.ts +27 -1
  90. package/src/modes/rpc/rpc-types.ts +58 -1
  91. package/src/modes/runtime-init.ts +2 -1
  92. package/src/modes/theme/defaults/dark-poimandres.json +1 -0
  93. package/src/modes/theme/defaults/light-poimandres.json +1 -0
  94. package/src/modes/theme/theme.ts +117 -117
  95. package/src/modes/types.ts +1 -1
  96. package/src/modes/utils/context-usage.ts +2 -2
  97. package/src/prompts/tools/github.md +4 -4
  98. package/src/prompts/tools/hashline.md +22 -26
  99. package/src/prompts/tools/read.md +55 -37
  100. package/src/sdk.ts +31 -8
  101. package/src/session/agent-session.ts +74 -104
  102. package/src/session/messages.ts +16 -51
  103. package/src/session/session-manager.ts +22 -2
  104. package/src/session/streaming-output.ts +16 -6
  105. package/src/task/discovery.ts +5 -2
  106. package/src/task/executor.ts +210 -87
  107. package/src/task/index.ts +15 -11
  108. package/src/task/render.ts +32 -5
  109. package/src/task/types.ts +54 -39
  110. package/src/tools/ask.ts +12 -12
  111. package/src/tools/ast-edit.ts +11 -15
  112. package/src/tools/ast-grep.ts +9 -10
  113. package/src/tools/bash-command-fixup.ts +47 -0
  114. package/src/tools/bash.ts +48 -38
  115. package/src/tools/browser/render.ts +2 -2
  116. package/src/tools/browser.ts +39 -53
  117. package/src/tools/calculator.ts +12 -11
  118. package/src/tools/checkpoint.ts +7 -7
  119. package/src/tools/debug.ts +40 -43
  120. package/src/tools/eval.ts +16 -10
  121. package/src/tools/find.ts +10 -13
  122. package/src/tools/gh.ts +108 -132
  123. package/src/tools/hindsight-recall.ts +4 -6
  124. package/src/tools/hindsight-reflect.ts +5 -5
  125. package/src/tools/hindsight-retain.ts +15 -17
  126. package/src/tools/image-gen.ts +31 -81
  127. package/src/tools/index.ts +4 -1
  128. package/src/tools/inspect-image.ts +8 -9
  129. package/src/tools/irc.ts +15 -27
  130. package/src/tools/job.ts +30 -28
  131. package/src/tools/output-meta.ts +26 -0
  132. package/src/tools/read.ts +39 -12
  133. package/src/tools/recipe/index.ts +7 -9
  134. package/src/tools/render-mermaid.ts +12 -12
  135. package/src/tools/report-tool-issue.ts +4 -4
  136. package/src/tools/resolve.ts +11 -11
  137. package/src/tools/review.ts +14 -26
  138. package/src/tools/search-tool-bm25.ts +7 -9
  139. package/src/tools/search.ts +19 -22
  140. package/src/tools/ssh.ts +10 -9
  141. package/src/tools/todo-write.ts +26 -34
  142. package/src/tools/vim.ts +10 -26
  143. package/src/tools/write.ts +25 -5
  144. package/src/tools/yield.ts +100 -54
  145. package/src/web/search/index.ts +9 -24
  146. package/src/web/search/providers/anthropic.ts +5 -0
  147. package/src/web/search/providers/exa.ts +3 -0
  148. package/src/web/search/providers/gemini.ts +5 -0
  149. package/src/web/search/providers/jina.ts +5 -2
  150. package/src/web/search/providers/zai.ts +5 -2
  151. package/src/prompts/compaction/branch-summary-context.md +0 -5
  152. package/src/prompts/compaction/branch-summary-preamble.md +0 -2
  153. package/src/prompts/compaction/branch-summary.md +0 -30
  154. package/src/prompts/compaction/compaction-short-summary.md +0 -9
  155. package/src/prompts/compaction/compaction-summary-context.md +0 -5
  156. package/src/prompts/compaction/compaction-summary.md +0 -38
  157. package/src/prompts/compaction/compaction-turn-prefix.md +0 -17
  158. package/src/prompts/compaction/compaction-update-summary.md +0 -45
  159. package/src/prompts/system/auto-handoff-threshold-focus.md +0 -1
  160. package/src/prompts/system/file-operations.md +0 -10
  161. package/src/prompts/system/handoff-document.md +0 -49
  162. package/src/prompts/system/summarization-system.md +0 -3
  163. package/src/session/compaction/branch-summarization.ts +0 -324
  164. package/src/session/compaction/compaction.ts +0 -1420
  165. package/src/session/compaction/errors.ts +0 -31
  166. package/src/session/compaction/index.ts +0 -8
  167. package/src/session/compaction/pruning.ts +0 -91
  168. package/src/session/compaction/utils.ts +0 -184
@@ -1,10 +1,14 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { getAgentDir, isEnoent, logger } from "@oh-my-pi/pi-utils";
4
- import type { TSchema } from "@sinclair/typebox";
5
- import { Value } from "@sinclair/typebox/value";
6
- import type { ErrorObject } from "ajv";
7
4
  import { JSONC, YAML } from "bun";
5
+ import type { ZodType } from "zod/v4";
6
+
7
+ /** Minimal subset of the AJV ConfigSchemaError shape this module actually relies on. */
8
+ interface ConfigSchemaError {
9
+ instancePath: string;
10
+ message: string | undefined;
11
+ }
8
12
 
9
13
  function migrateJsonToYml(jsonPath: string, ymlPath: string) {
10
14
  try {
@@ -25,7 +29,7 @@ function migrateJsonToYml(jsonPath: string, ymlPath: string) {
25
29
 
26
30
  export interface IConfigFile<T> {
27
31
  readonly id: string;
28
- readonly schema: TSchema;
32
+ readonly schema: ZodType<T>;
29
33
  path?(): string;
30
34
  load(): T | null;
31
35
  invalidate?(): void;
@@ -35,7 +39,7 @@ export class ConfigError extends Error {
35
39
  readonly #message: string;
36
40
  constructor(
37
41
  public readonly id: string,
38
- public readonly schemaErrors: ErrorObject[] | null | undefined,
42
+ public readonly schemaErrors: ConfigSchemaError[] | null | undefined,
39
43
  public readonly other?: { err: unknown; stage: string },
40
44
  ) {
41
45
  let messages: string[] | undefined;
@@ -68,7 +72,6 @@ export class ConfigError extends Error {
68
72
  break;
69
73
  default:
70
74
  message = `${title}\n${messages!.map(m => ` - ${m}`).join("\n")}`;
71
- break;
72
75
  }
73
76
 
74
77
  super(message, { cause });
@@ -99,7 +102,7 @@ export class ConfigFile<T> implements IConfigFile<T> {
99
102
 
100
103
  constructor(
101
104
  readonly id: string,
102
- readonly schema: TSchema,
105
+ readonly schema: ZodType<T>,
103
106
  configPath: string = path.join(getAgentDir(), `${id}.yml`),
104
107
  ) {
105
108
  this.#basePath = configPath;
@@ -146,7 +149,14 @@ export class ConfigFile<T> implements IConfigFile<T> {
146
149
  }
147
150
 
148
151
  createDefault(): T {
149
- return Value.Default(this.schema, [], undefined) as T;
152
+ const parsed = this.schema.safeParse({});
153
+ if (parsed.success) return parsed.data;
154
+ const fallback = this.schema.safeParse(undefined);
155
+ if (fallback.success) return fallback.data;
156
+ throw new ConfigError(this.id, undefined, {
157
+ err: new Error("Schema produced no default value"),
158
+ stage: "createDefault",
159
+ });
150
160
  }
151
161
 
152
162
  #storeCache(result: LoadResult<T>): LoadResult<T> {
@@ -169,17 +179,29 @@ export class ConfigFile<T> implements IConfigFile<T> {
169
179
  throw new Error(`Invalid config file path: ${this.#basePath}`);
170
180
  }
171
181
 
172
- if (!Value.Check(this.schema, parsed)) {
173
- const schemaErrors: ErrorObject[] = [];
174
- for (const err of Value.Errors(this.schema, parsed)) {
175
- schemaErrors.push({ instancePath: err.path, message: err.message } as ErrorObject);
182
+ const checked = this.schema.safeParse(parsed);
183
+ if (!checked.success) {
184
+ const schemaErrors: ConfigSchemaError[] = [];
185
+ for (const issue of checked.error.issues) {
186
+ const instancePath = issue.path.length === 0 ? "" : `/${issue.path.map(String).join("/")}`;
187
+ schemaErrors.push({ instancePath, message: issue.message });
176
188
  if (schemaErrors.length >= 50) break;
177
189
  }
178
190
  const error = new ConfigError(this.id, schemaErrors);
179
191
  logger.warn("Failed to parse config file", { path: this.path(), error });
180
192
  return this.#storeCache({ error, status: "error" });
181
193
  }
182
- return this.#storeCache({ value: parsed as T, status: "ok" });
194
+ const value = checked.data;
195
+ try {
196
+ this.#auxValidate?.(value);
197
+ } catch (error) {
198
+ const wrapped =
199
+ error instanceof ConfigError
200
+ ? error
201
+ : new ConfigError(this.id, undefined, { err: error, stage: "AuxValidate" });
202
+ return this.#storeCache({ error: wrapped, status: "error" });
203
+ }
204
+ return this.#storeCache({ value, status: "ok" });
183
205
  } catch (error) {
184
206
  if (isEnoent(error)) {
185
207
  return this.#storeCache({ status: "not-found" });
@@ -30,7 +30,6 @@ const DEFAULT_LOCAL_TOKEN = "lm-studio-local";
30
30
  import { registerOAuthProvider, unregisterOAuthProviders } from "@oh-my-pi/pi-ai/utils/oauth";
31
31
  import type { OAuthCredentials, OAuthLoginCallbacks } from "@oh-my-pi/pi-ai/utils/oauth/types";
32
32
  import { isRecord, logger } from "@oh-my-pi/pi-utils";
33
- import { type Static, Type } from "@sinclair/typebox";
34
33
  import { parseModelString, resolveProviderModelReference } from "../config/model-resolver";
35
34
  import { isValidThemeColor, type ThemeColor } from "../modes/theme/theme";
36
35
  import type { AuthStorage, OAuthCredential } from "../session/auth-storage";
@@ -43,6 +42,13 @@ import {
43
42
  formatCanonicalVariantSelector,
44
43
  type ModelEquivalenceConfig,
45
44
  } from "./model-equivalence";
45
+ import {
46
+ type ModelOverride,
47
+ type ModelsConfig,
48
+ ModelsConfigSchema,
49
+ type ProviderAuthMode,
50
+ type ProviderDiscovery,
51
+ } from "./models-config-schema";
46
52
  import { type Settings, settings } from "./settings";
47
53
 
48
54
  export type { CanonicalModelIndex, CanonicalModelRecord, CanonicalModelVariant, ModelEquivalenceConfig };
@@ -121,194 +127,6 @@ export function getRoleInfo(role: string, settings: Settings): RoleInfo {
121
127
  return { name: role, color: "muted" };
122
128
  }
123
129
 
124
- const OpenRouterRoutingSchema = Type.Object({
125
- only: Type.Optional(Type.Array(Type.String())),
126
- order: Type.Optional(Type.Array(Type.String())),
127
- });
128
-
129
- // Schema for Vercel AI Gateway routing preferences
130
- const VercelGatewayRoutingSchema = Type.Object({
131
- only: Type.Optional(Type.Array(Type.String())),
132
- order: Type.Optional(Type.Array(Type.String())),
133
- });
134
-
135
- // Schema for OpenAI compatibility settings
136
- const ReasoningEffortMapSchema = Type.Object({
137
- minimal: Type.Optional(Type.String()),
138
- low: Type.Optional(Type.String()),
139
- medium: Type.Optional(Type.String()),
140
- high: Type.Optional(Type.String()),
141
- xhigh: Type.Optional(Type.String()),
142
- });
143
-
144
- const OpenAICompatSchema = Type.Object({
145
- supportsStore: Type.Optional(Type.Boolean()),
146
- supportsDeveloperRole: Type.Optional(Type.Boolean()),
147
- supportsReasoningEffort: Type.Optional(Type.Boolean()),
148
- reasoningEffortMap: Type.Optional(ReasoningEffortMapSchema),
149
- maxTokensField: Type.Optional(Type.Union([Type.Literal("max_completion_tokens"), Type.Literal("max_tokens")])),
150
- supportsUsageInStreaming: Type.Optional(Type.Boolean()),
151
- requiresToolResultName: Type.Optional(Type.Boolean()),
152
- requiresMistralToolIds: Type.Optional(Type.Boolean()),
153
- requiresAssistantAfterToolResult: Type.Optional(Type.Boolean()),
154
- requiresThinkingAsText: Type.Optional(Type.Boolean()),
155
- reasoningContentField: Type.Optional(
156
- Type.Union([Type.Literal("reasoning_content"), Type.Literal("reasoning"), Type.Literal("reasoning_text")]),
157
- ),
158
- requiresReasoningContentForToolCalls: Type.Optional(Type.Boolean()),
159
- requiresAssistantContentForToolCalls: Type.Optional(Type.Boolean()),
160
- supportsToolChoice: Type.Optional(Type.Boolean()),
161
- disableReasoningOnForcedToolChoice: Type.Optional(Type.Boolean()),
162
- thinkingFormat: Type.Optional(
163
- Type.Union([
164
- Type.Literal("openai"),
165
- Type.Literal("openrouter"),
166
- Type.Literal("zai"),
167
- Type.Literal("qwen"),
168
- Type.Literal("qwen-chat-template"),
169
- ]),
170
- ),
171
- openRouterRouting: Type.Optional(OpenRouterRoutingSchema),
172
- vercelGatewayRouting: Type.Optional(VercelGatewayRoutingSchema),
173
- extraBody: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
174
- supportsStrictMode: Type.Optional(Type.Boolean()),
175
- toolStrictMode: Type.Optional(Type.Union([Type.Literal("all_strict"), Type.Literal("none")])),
176
- });
177
-
178
- const EffortSchema = Type.Union([
179
- Type.Literal("minimal"),
180
- Type.Literal("low"),
181
- Type.Literal("medium"),
182
- Type.Literal("high"),
183
- Type.Literal("xhigh"),
184
- ]);
185
-
186
- const ThinkingControlModeSchema = Type.Union([
187
- Type.Literal("effort"),
188
- Type.Literal("budget"),
189
- Type.Literal("google-level"),
190
- Type.Literal("anthropic-adaptive"),
191
- Type.Literal("anthropic-budget-effort"),
192
- ]);
193
-
194
- const ModelThinkingSchema = Type.Object({
195
- minLevel: EffortSchema,
196
- maxLevel: EffortSchema,
197
- mode: ThinkingControlModeSchema,
198
- defaultLevel: Type.Optional(EffortSchema),
199
- });
200
-
201
- // Schema for custom model definition
202
- // Most fields are optional with sensible defaults for local models (Ollama, LM Studio, etc.)
203
- const ModelDefinitionSchema = Type.Object({
204
- id: Type.String({ minLength: 1 }),
205
- name: Type.Optional(Type.String({ minLength: 1 })),
206
- api: Type.Optional(
207
- Type.Union([
208
- Type.Literal("openai-completions"),
209
- Type.Literal("openai-responses"),
210
- Type.Literal("openai-codex-responses"),
211
- Type.Literal("azure-openai-responses"),
212
- Type.Literal("anthropic-messages"),
213
- Type.Literal("google-generative-ai"),
214
- Type.Literal("google-vertex"),
215
- ]),
216
- ),
217
- baseUrl: Type.Optional(Type.String({ minLength: 1 })),
218
- reasoning: Type.Optional(Type.Boolean()),
219
- thinking: Type.Optional(ModelThinkingSchema),
220
- input: Type.Optional(Type.Array(Type.Union([Type.Literal("text"), Type.Literal("image")]))),
221
- cost: Type.Optional(
222
- Type.Object({
223
- input: Type.Number(),
224
- output: Type.Number(),
225
- cacheRead: Type.Number(),
226
- cacheWrite: Type.Number(),
227
- }),
228
- ),
229
- premiumMultiplier: Type.Optional(Type.Number()),
230
- contextWindow: Type.Optional(Type.Number()),
231
- maxTokens: Type.Optional(Type.Number()),
232
- headers: Type.Optional(Type.Record(Type.String(), Type.String())),
233
- compat: Type.Optional(OpenAICompatSchema),
234
- contextPromotionTarget: Type.Optional(Type.String({ minLength: 1 })),
235
- });
236
-
237
- // Schema for per-model overrides (all fields optional, merged with built-in model)
238
- const ModelOverrideSchema = Type.Object({
239
- name: Type.Optional(Type.String({ minLength: 1 })),
240
- reasoning: Type.Optional(Type.Boolean()),
241
- thinking: Type.Optional(ModelThinkingSchema),
242
- input: Type.Optional(Type.Array(Type.Union([Type.Literal("text"), Type.Literal("image")]))),
243
- cost: Type.Optional(
244
- Type.Object({
245
- input: Type.Optional(Type.Number()),
246
- output: Type.Optional(Type.Number()),
247
- cacheRead: Type.Optional(Type.Number()),
248
- cacheWrite: Type.Optional(Type.Number()),
249
- }),
250
- ),
251
- premiumMultiplier: Type.Optional(Type.Number()),
252
- contextWindow: Type.Optional(Type.Number()),
253
- maxTokens: Type.Optional(Type.Number()),
254
- headers: Type.Optional(Type.Record(Type.String(), Type.String())),
255
- compat: Type.Optional(OpenAICompatSchema),
256
- contextPromotionTarget: Type.Optional(Type.String({ minLength: 1 })),
257
- });
258
-
259
- type ModelOverride = Static<typeof ModelOverrideSchema>;
260
-
261
- const ProviderDiscoverySchema = Type.Object({
262
- type: Type.Union([
263
- Type.Literal("ollama"),
264
- Type.Literal("llama.cpp"),
265
- Type.Literal("lm-studio"),
266
- Type.Literal("openai-models-list"),
267
- ]),
268
- });
269
-
270
- const ProviderAuthSchema = Type.Union([Type.Literal("apiKey"), Type.Literal("none"), Type.Literal("oauth")]);
271
-
272
- const ProviderConfigSchema = Type.Object({
273
- baseUrl: Type.Optional(Type.String({ minLength: 1 })),
274
- apiKey: Type.Optional(Type.String({ minLength: 1 })),
275
- api: Type.Optional(
276
- Type.Union([
277
- Type.Literal("openai-completions"),
278
- Type.Literal("openai-responses"),
279
- Type.Literal("openai-codex-responses"),
280
- Type.Literal("azure-openai-responses"),
281
- Type.Literal("anthropic-messages"),
282
- Type.Literal("google-generative-ai"),
283
- Type.Literal("google-vertex"),
284
- ]),
285
- ),
286
- headers: Type.Optional(Type.Record(Type.String(), Type.String())),
287
- compat: Type.Optional(OpenAICompatSchema),
288
- authHeader: Type.Optional(Type.Boolean()),
289
- auth: Type.Optional(ProviderAuthSchema),
290
- discovery: Type.Optional(ProviderDiscoverySchema),
291
- models: Type.Optional(Type.Array(ModelDefinitionSchema)),
292
- modelOverrides: Type.Optional(Type.Record(Type.String(), ModelOverrideSchema)),
293
- /** When true, disables strict tool schemas for this provider (for third-party Anthropic-compatible endpoints that reject the strict field). */
294
- disableStrictTools: Type.Optional(Type.Boolean()),
295
- });
296
-
297
- const EquivalenceConfigSchema = Type.Object({
298
- overrides: Type.Optional(Type.Record(Type.String(), Type.String({ minLength: 1 }))),
299
- exclude: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
300
- });
301
-
302
- const ModelsConfigSchema = Type.Object({
303
- providers: Type.Optional(Type.Record(Type.String(), ProviderConfigSchema)),
304
- equivalence: Type.Optional(EquivalenceConfigSchema),
305
- });
306
-
307
- type ModelsConfig = Static<typeof ModelsConfigSchema>;
308
-
309
- type ProviderAuthMode = Static<typeof ProviderAuthSchema>;
310
- type ProviderDiscovery = Static<typeof ProviderDiscoverySchema>;
311
-
312
130
  type ProviderValidationMode = "models-config" | "runtime-register";
313
131
 
314
132
  interface ProviderValidationModel {
@@ -347,12 +165,13 @@ function validateProviderConfiguration(
347
165
  !config.baseUrl &&
348
166
  !config.headers &&
349
167
  !config.compat &&
168
+ !config.apiKey &&
350
169
  !config.disableStrictTools &&
351
170
  !hasModelOverrides &&
352
171
  !config.discovery
353
172
  ) {
354
173
  throw new Error(
355
- `Provider ${providerName}: must specify "baseUrl", "headers", "compat", "disableStrictTools", "modelOverrides", "discovery", or "models"`,
174
+ `Provider ${providerName}: must specify "baseUrl", "headers", "apiKey", "compat", "disableStrictTools", "modelOverrides", "discovery", or "models"`,
356
175
  );
357
176
  }
358
177
  }
@@ -1015,8 +834,12 @@ export class ModelRegistry {
1015
834
 
1016
835
  this.#addImplicitDiscoverableProviders(configuredProviders);
1017
836
  const builtInModels = this.#applyHardcodedModelPolicies(this.#loadBuiltInModels(overrides));
837
+ const cachedStandardModels = this.#applyHardcodedModelPolicies(this.#loadCachedStandardProviderModels());
1018
838
  const cachedDiscoveries = this.#applyHardcodedModelPolicies(this.#loadCachedDiscoverableModels());
1019
- const resolvedDefaults = this.#mergeResolvedModels(builtInModels, cachedDiscoveries);
839
+ const resolvedDefaults = this.#mergeResolvedModels(
840
+ this.#mergeResolvedModels(builtInModels, cachedStandardModels),
841
+ cachedDiscoveries,
842
+ );
1020
843
  const withConfigModels = this.#mergeCustomModels(resolvedDefaults, this.#customModelOverlays);
1021
844
  // Merge runtime extension models so they survive refresh() cycles
1022
845
  const combined = this.#mergeCustomModels(withConfigModels, this.#runtimeModelOverlays);
@@ -1115,6 +938,32 @@ export class ModelRegistry {
1115
938
  return merged;
1116
939
  }
1117
940
 
941
+ #loadCachedStandardProviderModels(): Model<Api>[] {
942
+ const configuredDiscoveryProviders = new Set(this.#discoverableProviders.map(provider => provider.provider));
943
+ const cachedModels: Model<Api>[] = [];
944
+ for (const descriptor of PROVIDER_DESCRIPTORS) {
945
+ if (configuredDiscoveryProviders.has(descriptor.providerId)) {
946
+ continue;
947
+ }
948
+ const cache = readModelCache<Api>(descriptor.providerId, 24 * 60 * 60 * 1000, Date.now, this.#cacheDbPath);
949
+ if (!cache) {
950
+ continue;
951
+ }
952
+ const models = cache.models.map(model =>
953
+ model.provider === descriptor.providerId ? model : { ...model, provider: descriptor.providerId },
954
+ );
955
+ const providerOverride = this.#providerOverrides.get(descriptor.providerId);
956
+ const withTransport = providerOverride
957
+ ? models.map(model => this.#applyProviderTransportOverride(model, providerOverride))
958
+ : models;
959
+ const withCompat = providerOverride?.compat
960
+ ? withTransport.map(model => ({ ...model, compat: mergeCompat(model.compat, providerOverride.compat) }))
961
+ : withTransport;
962
+ cachedModels.push(...this.#applyProviderModelOverrides(descriptor.providerId, withCompat));
963
+ }
964
+ return cachedModels;
965
+ }
966
+
1118
967
  #loadCachedDiscoverableModels(): Model<Api>[] {
1119
968
  const cachedModels: Model<Api>[] = [];
1120
969
  for (const providerConfig of this.#discoverableProviders) {
@@ -0,0 +1,166 @@
1
+ import * as z from "zod/v4";
2
+
3
+ const OpenRouterRoutingSchema = z.object({
4
+ only: z.array(z.string()).optional(),
5
+ order: z.array(z.string()).optional(),
6
+ });
7
+
8
+ const VercelGatewayRoutingSchema = z.object({
9
+ only: z.array(z.string()).optional(),
10
+ order: z.array(z.string()).optional(),
11
+ });
12
+
13
+ const ReasoningEffortMapSchema = z.object({
14
+ minimal: z.string().optional(),
15
+ low: z.string().optional(),
16
+ medium: z.string().optional(),
17
+ high: z.string().optional(),
18
+ xhigh: z.string().optional(),
19
+ });
20
+
21
+ export const OpenAICompatSchema = z.object({
22
+ supportsStore: z.boolean().optional(),
23
+ supportsDeveloperRole: z.boolean().optional(),
24
+ supportsMultipleSystemMessages: z.boolean().optional(),
25
+ supportsReasoningEffort: z.boolean().optional(),
26
+ reasoningEffortMap: ReasoningEffortMapSchema.optional(),
27
+ maxTokensField: z.enum(["max_completion_tokens", "max_tokens"]).optional(),
28
+ supportsUsageInStreaming: z.boolean().optional(),
29
+ requiresToolResultName: z.boolean().optional(),
30
+ requiresMistralToolIds: z.boolean().optional(),
31
+ requiresAssistantAfterToolResult: z.boolean().optional(),
32
+ requiresThinkingAsText: z.boolean().optional(),
33
+ reasoningContentField: z.enum(["reasoning_content", "reasoning", "reasoning_text"]).optional(),
34
+ requiresReasoningContentForToolCalls: z.boolean().optional(),
35
+ allowsSyntheticReasoningContentForToolCalls: z.boolean().optional(),
36
+ requiresAssistantContentForToolCalls: z.boolean().optional(),
37
+ supportsToolChoice: z.boolean().optional(),
38
+ disableReasoningOnForcedToolChoice: z.boolean().optional(),
39
+ disableReasoningOnToolChoice: z.boolean().optional(),
40
+ thinkingFormat: z.enum(["openai", "openrouter", "zai", "qwen", "qwen-chat-template"]).optional(),
41
+ openRouterRouting: OpenRouterRoutingSchema.optional(),
42
+ vercelGatewayRouting: VercelGatewayRoutingSchema.optional(),
43
+ extraBody: z.record(z.string(), z.unknown()).optional(),
44
+ supportsStrictMode: z.boolean().optional(),
45
+ toolStrictMode: z.enum(["all_strict", "none"]).optional(),
46
+ });
47
+
48
+ const EffortSchema = z.enum(["minimal", "low", "medium", "high", "xhigh"]);
49
+
50
+ const ThinkingControlModeSchema = z.enum([
51
+ "effort",
52
+ "budget",
53
+ "google-level",
54
+ "anthropic-adaptive",
55
+ "anthropic-budget-effort",
56
+ ]);
57
+
58
+ const ModelThinkingSchema = z.object({
59
+ minLevel: EffortSchema,
60
+ maxLevel: EffortSchema,
61
+ mode: ThinkingControlModeSchema,
62
+ defaultLevel: EffortSchema.optional(),
63
+ levels: z.array(EffortSchema).optional(),
64
+ });
65
+
66
+ const ModelDefinitionSchema = z.object({
67
+ id: z.string().min(1),
68
+ name: z.string().min(1).optional(),
69
+ api: z
70
+ .enum([
71
+ "openai-completions",
72
+ "openai-responses",
73
+ "openai-codex-responses",
74
+ "azure-openai-responses",
75
+ "anthropic-messages",
76
+ "google-generative-ai",
77
+ "google-vertex",
78
+ ])
79
+ .optional(),
80
+ baseUrl: z.string().min(1).optional(),
81
+ reasoning: z.boolean().optional(),
82
+ thinking: ModelThinkingSchema.optional(),
83
+ input: z.array(z.enum(["text", "image"])).optional(),
84
+ cost: z
85
+ .object({
86
+ input: z.number(),
87
+ output: z.number(),
88
+ cacheRead: z.number(),
89
+ cacheWrite: z.number(),
90
+ })
91
+ .optional(),
92
+ premiumMultiplier: z.number().optional(),
93
+ contextWindow: z.number().optional(),
94
+ maxTokens: z.number().optional(),
95
+ headers: z.record(z.string(), z.string()).optional(),
96
+ compat: OpenAICompatSchema.optional(),
97
+ contextPromotionTarget: z.string().min(1).optional(),
98
+ });
99
+
100
+ export const ModelOverrideSchema = z.object({
101
+ name: z.string().min(1).optional(),
102
+ reasoning: z.boolean().optional(),
103
+ thinking: ModelThinkingSchema.optional(),
104
+ input: z.array(z.enum(["text", "image"])).optional(),
105
+ cost: z
106
+ .object({
107
+ input: z.number().optional(),
108
+ output: z.number().optional(),
109
+ cacheRead: z.number().optional(),
110
+ cacheWrite: z.number().optional(),
111
+ })
112
+ .optional(),
113
+ premiumMultiplier: z.number().optional(),
114
+ contextWindow: z.number().optional(),
115
+ maxTokens: z.number().optional(),
116
+ headers: z.record(z.string(), z.string()).optional(),
117
+ compat: OpenAICompatSchema.optional(),
118
+ contextPromotionTarget: z.string().min(1).optional(),
119
+ });
120
+
121
+ export type ModelOverride = z.infer<typeof ModelOverrideSchema>;
122
+
123
+ export const ProviderDiscoverySchema = z.object({
124
+ type: z.enum(["ollama", "llama.cpp", "lm-studio", "openai-models-list"]),
125
+ });
126
+
127
+ export const ProviderAuthSchema = z.enum(["apiKey", "none", "oauth"]);
128
+
129
+ export type ProviderAuthMode = z.infer<typeof ProviderAuthSchema>;
130
+ export type ProviderDiscovery = z.infer<typeof ProviderDiscoverySchema>;
131
+
132
+ const ProviderConfigSchema = z.object({
133
+ baseUrl: z.string().min(1).optional(),
134
+ apiKey: z.string().min(1).optional(),
135
+ api: z
136
+ .enum([
137
+ "openai-completions",
138
+ "openai-responses",
139
+ "openai-codex-responses",
140
+ "azure-openai-responses",
141
+ "anthropic-messages",
142
+ "google-generative-ai",
143
+ "google-vertex",
144
+ ])
145
+ .optional(),
146
+ headers: z.record(z.string(), z.string()).optional(),
147
+ compat: OpenAICompatSchema.optional(),
148
+ authHeader: z.boolean().optional(),
149
+ auth: ProviderAuthSchema.optional(),
150
+ discovery: ProviderDiscoverySchema.optional(),
151
+ models: z.array(ModelDefinitionSchema).optional(),
152
+ modelOverrides: z.record(z.string(), ModelOverrideSchema).optional(),
153
+ disableStrictTools: z.boolean().optional(),
154
+ });
155
+
156
+ const EquivalenceConfigSchema = z.object({
157
+ overrides: z.record(z.string(), z.string().min(1)).optional(),
158
+ exclude: z.array(z.string().min(1)).optional(),
159
+ });
160
+
161
+ export const ModelsConfigSchema = z.object({
162
+ providers: z.record(z.string(), ProviderConfigSchema).optional(),
163
+ equivalence: EquivalenceConfigSchema.optional(),
164
+ });
165
+
166
+ export type ModelsConfig = z.infer<typeof ModelsConfigSchema>;
@@ -1648,6 +1648,17 @@ export const SETTINGS_SCHEMA = {
1648
1648
  },
1649
1649
  "bashInterceptor.patterns": { type: "array", default: DEFAULT_BASH_INTERCEPTOR_RULES },
1650
1650
 
1651
+ "bash.stripTrailingHeadTail": {
1652
+ type: "boolean",
1653
+ default: true,
1654
+ ui: {
1655
+ tab: "editing",
1656
+ label: "Strip Trailing head/tail",
1657
+ description:
1658
+ "Silently drop trailing `| head`/`| tail` pipes from single-line bash commands. Output is already truncated automatically.",
1659
+ },
1660
+ },
1661
+
1651
1662
  // Shell output minimizer
1652
1663
  "shellMinimizer.enabled": {
1653
1664
  type: "boolean",
@@ -2318,6 +2329,24 @@ export const SETTINGS_SCHEMA = {
2318
2329
  },
2319
2330
  },
2320
2331
 
2332
+ "task.maxRuntimeMs": {
2333
+ type: "number",
2334
+ default: 0,
2335
+ ui: {
2336
+ tab: "tasks",
2337
+ label: "Max Subagent Runtime",
2338
+ description:
2339
+ "Hard wall-clock limit per subagent (ms). 0 disables it. Defense-in-depth against provider-side stream hangs that escape the inference-layer watchdog; triggers a normal subagent abort with a 'timed out' reason.",
2340
+ options: [
2341
+ { value: "0", label: "Unlimited", description: "Default" },
2342
+ { value: "300000", label: "5 minutes" },
2343
+ { value: "900000", label: "15 minutes" },
2344
+ { value: "1800000", label: "30 minutes" },
2345
+ { value: "3600000", label: "1 hour" },
2346
+ ],
2347
+ },
2348
+ },
2349
+
2321
2350
  "task.disabledAgents": {
2322
2351
  type: "array",
2323
2352
  default: [] as string[],
@@ -31,6 +31,7 @@ const PRIORITY = 70; // Below claude.ts (80) so user .claude/ overrides win
31
31
  interface ClaudePluginManifest {
32
32
  skills?: string;
33
33
  "slash-commands"?: string;
34
+ commands?: string;
34
35
  }
35
36
 
36
37
  interface ResolvedPluginDir {
@@ -59,24 +60,35 @@ function isWithinPluginRoot(rootPath: string, targetPath: string): boolean {
59
60
 
60
61
  async function resolvePluginDir(
61
62
  root: ClaudePluginRoot,
62
- manifestKey: keyof ClaudePluginManifest,
63
+ manifestKeys: ReadonlyArray<keyof ClaudePluginManifest>,
63
64
  fallback: string,
64
65
  ): Promise<ResolvedPluginDir> {
65
66
  const manifest = await readPluginManifest(root);
66
67
  const fallbackDir = path.join(root.path, fallback);
67
- const configured = manifest?.[manifestKey];
68
- if (typeof configured !== "string" || !configured.trim()) {
68
+
69
+ let configured: string | undefined;
70
+ let matchedKey: keyof ClaudePluginManifest | undefined;
71
+ for (const key of manifestKeys) {
72
+ const val = manifest?.[key];
73
+ if (typeof val === "string" && val.trim()) {
74
+ configured = val.trim();
75
+ matchedKey = key;
76
+ break;
77
+ }
78
+ }
79
+
80
+ if (configured === undefined) {
69
81
  return { dir: fallbackDir };
70
82
  }
71
83
 
72
- const resolved = path.resolve(root.path, configured.trim());
84
+ const resolved = path.resolve(root.path, configured);
73
85
  if (isWithinPluginRoot(root.path, resolved)) {
74
86
  return { dir: resolved };
75
87
  }
76
88
 
77
89
  return {
78
90
  dir: fallbackDir,
79
- warning: `[claude-plugins] Ignoring ${String(manifestKey)} path outside plugin root for ${root.id}: ${configured}`,
91
+ warning: `[claude-plugins] Ignoring ${String(matchedKey)} path outside plugin root for ${root.id}: ${configured}`,
80
92
  };
81
93
  }
82
94
 
@@ -93,7 +105,7 @@ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
93
105
 
94
106
  const results = await Promise.all(
95
107
  roots.map(async root => {
96
- const { dir: skillsDir, warning } = await resolvePluginDir(root, "skills", "skills");
108
+ const { dir: skillsDir, warning } = await resolvePluginDir(root, ["skills"], "skills");
97
109
  const result = await scanSkillsFromDir(ctx, {
98
110
  dir: skillsDir,
99
111
  providerId: PROVIDER_ID,
@@ -128,7 +140,7 @@ async function loadSlashCommands(ctx: LoadContext): Promise<LoadResult<SlashComm
128
140
 
129
141
  const results = await Promise.all(
130
142
  roots.map(async root => {
131
- const { dir: commandsDir, warning } = await resolvePluginDir(root, "slash-commands", "commands");
143
+ const { dir: commandsDir, warning } = await resolvePluginDir(root, ["commands", "slash-commands"], "commands");
132
144
  const result = await loadFilesFromDir<SlashCommand>(ctx, commandsDir, PROVIDER_ID, root.scope, {
133
145
  extensions: ["md"],
134
146
  transform: (name, content, filePath, source) => {
package/src/edit/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
2
2
  import { prompt } from "@oh-my-pi/pi-utils";
3
- import type { Static } from "@sinclair/typebox";
3
+ import type * as z from "zod/v4";
4
4
  import {
5
5
  executeHashlineSingle,
6
6
  HashlineMismatchError,
@@ -53,7 +53,7 @@ type TInput =
53
53
  | typeof vimSchema
54
54
  | typeof applyPatchSchema;
55
55
 
56
- type VimParams = Static<typeof vimSchema>;
56
+ type VimParams = z.infer<typeof vimSchema>;
57
57
  type EditParams = ReplaceParams | PatchParams | HashlineParams | VimParams | ApplyPatchParams;
58
58
  type EditToolResultDetails = EditToolDetails | VimToolDetails;
59
59