@oh-my-pi/pi-coding-agent 17.0.0 → 17.0.2

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 (202) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/dist/cli.js +4006 -3966
  3. package/dist/types/advisor/advise-tool.d.ts +5 -2
  4. package/dist/types/advisor/config.d.ts +14 -6
  5. package/dist/types/advisor/runtime.d.ts +20 -11
  6. package/dist/types/autolearn/controller.d.ts +1 -0
  7. package/dist/types/cli-commands.d.ts +14 -1
  8. package/dist/types/config/model-discovery.d.ts +17 -0
  9. package/dist/types/config/model-registry.d.ts +4 -0
  10. package/dist/types/config/model-resolver.d.ts +6 -2
  11. package/dist/types/config/models-config-schema.d.ts +10 -0
  12. package/dist/types/config/models-config.d.ts +6 -0
  13. package/dist/types/config/settings-schema.d.ts +32 -7
  14. package/dist/types/config/settings.d.ts +50 -0
  15. package/dist/types/cursor.d.ts +11 -0
  16. package/dist/types/discovery/helpers.d.ts +5 -2
  17. package/dist/types/discovery/substitute-plugin-root.d.ts +20 -7
  18. package/dist/types/exec/bash-executor.d.ts +2 -0
  19. package/dist/types/extensibility/extensions/managed-timers.d.ts +15 -0
  20. package/dist/types/extensibility/extensions/runner.d.ts +7 -0
  21. package/dist/types/extensibility/extensions/types.d.ts +18 -0
  22. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +40 -0
  23. package/dist/types/extensibility/shared-events.d.ts +6 -0
  24. package/dist/types/extensibility/utils.d.ts +2 -2
  25. package/dist/types/internal-urls/agent-protocol.d.ts +1 -0
  26. package/dist/types/lib/xai-http.d.ts +11 -0
  27. package/dist/types/mcp/transports/stdio.d.ts +13 -1
  28. package/dist/types/modes/acp/acp-agent.d.ts +3 -0
  29. package/dist/types/modes/acp/acp-mode.d.ts +6 -2
  30. package/dist/types/modes/components/advisor-config.d.ts +8 -1
  31. package/dist/types/modes/components/assistant-message.d.ts +2 -0
  32. package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +1 -0
  33. package/dist/types/modes/components/custom-editor.d.ts +23 -1
  34. package/dist/types/modes/components/hook-editor.d.ts +16 -2
  35. package/dist/types/modes/components/login-dialog.test.d.ts +1 -0
  36. package/dist/types/modes/components/model-hub.d.ts +5 -2
  37. package/dist/types/modes/components/session-selector.d.ts +4 -0
  38. package/dist/types/modes/components/tool-execution.d.ts +8 -1
  39. package/dist/types/modes/components/tool-execution.test.d.ts +1 -0
  40. package/dist/types/modes/controllers/command-controller.d.ts +8 -0
  41. package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
  42. package/dist/types/modes/interactive-mode.d.ts +6 -0
  43. package/dist/types/modes/noninteractive-dispose.test.d.ts +1 -0
  44. package/dist/types/modes/print-mode.d.ts +1 -1
  45. package/dist/types/modes/types.d.ts +10 -0
  46. package/dist/types/modes/utils/transcript-render-helpers.d.ts +2 -2
  47. package/dist/types/modes/warp-events.d.ts +24 -0
  48. package/dist/types/modes/warp-events.test.d.ts +1 -0
  49. package/dist/types/plan-mode/model-transition.d.ts +47 -0
  50. package/dist/types/plan-mode/model-transition.test.d.ts +1 -0
  51. package/dist/types/registry/agent-lifecycle.d.ts +26 -1
  52. package/dist/types/sdk.d.ts +13 -2
  53. package/dist/types/session/agent-session.d.ts +43 -11
  54. package/dist/types/session/session-history-format.d.ts +10 -0
  55. package/dist/types/session/session-manager.d.ts +14 -0
  56. package/dist/types/session/streaming-output.d.ts +2 -0
  57. package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +9 -0
  58. package/dist/types/slash-commands/types.d.ts +4 -4
  59. package/dist/types/system-prompt.d.ts +1 -1
  60. package/dist/types/task/label.d.ts +1 -1
  61. package/dist/types/telemetry-export.d.ts +34 -9
  62. package/dist/types/tools/approval.d.ts +8 -0
  63. package/dist/types/tools/bash.d.ts +2 -0
  64. package/dist/types/tools/browser/registry.d.ts +7 -3
  65. package/dist/types/tools/browser/tab-supervisor.d.ts +2 -0
  66. package/dist/types/tools/essential-tools.d.ts +29 -0
  67. package/dist/types/tools/image-gen.d.ts +2 -1
  68. package/dist/types/tools/index.d.ts +4 -1
  69. package/dist/types/tools/manage-skill.d.ts +2 -0
  70. package/dist/types/tools/xdev.d.ts +11 -2
  71. package/dist/types/utils/title-generator.d.ts +2 -1
  72. package/dist/types/web/search/index.d.ts +2 -0
  73. package/dist/types/web/search/providers/base.d.ts +3 -0
  74. package/dist/types/web/search/providers/kimi.d.ts +4 -1
  75. package/dist/types/web/search/types.d.ts +1 -1
  76. package/package.json +21 -16
  77. package/src/advisor/__tests__/advisor.test.ts +1358 -86
  78. package/src/advisor/__tests__/config.test.ts +58 -2
  79. package/src/advisor/advise-tool.ts +7 -3
  80. package/src/advisor/config.ts +76 -24
  81. package/src/advisor/runtime.ts +445 -92
  82. package/src/autolearn/controller.ts +23 -28
  83. package/src/cli/bench-cli.ts +4 -1
  84. package/src/cli/grep-cli.ts +2 -1
  85. package/src/cli-commands.ts +57 -23
  86. package/src/cli.ts +5 -1
  87. package/src/config/model-discovery.ts +81 -21
  88. package/src/config/model-registry.ts +34 -6
  89. package/src/config/model-resolver.ts +57 -12
  90. package/src/config/models-config-schema.ts +1 -0
  91. package/src/config/settings-schema.ts +43 -6
  92. package/src/config/settings.ts +405 -25
  93. package/src/cursor.ts +20 -3
  94. package/src/debug/report-bundle.ts +40 -4
  95. package/src/discovery/codex.ts +14 -4
  96. package/src/discovery/helpers.ts +28 -5
  97. package/src/discovery/substitute-plugin-root.ts +23 -7
  98. package/src/exec/bash-executor.ts +14 -5
  99. package/src/export/html/template.js +2 -0
  100. package/src/extensibility/custom-tools/loader.ts +3 -3
  101. package/src/extensibility/custom-tools/wrapper.ts +2 -1
  102. package/src/extensibility/extensions/loader.ts +3 -3
  103. package/src/extensibility/extensions/managed-timers.ts +83 -0
  104. package/src/extensibility/extensions/runner.ts +26 -0
  105. package/src/extensibility/extensions/types.ts +18 -0
  106. package/src/extensibility/extensions/wrapper.ts +2 -1
  107. package/src/extensibility/hooks/loader.ts +3 -3
  108. package/src/extensibility/legacy-pi-coding-agent-shim.ts +96 -1
  109. package/src/extensibility/plugins/legacy-pi-compat.ts +225 -22
  110. package/src/extensibility/plugins/manager.ts +2 -2
  111. package/src/extensibility/shared-events.ts +6 -0
  112. package/src/extensibility/utils.ts +91 -25
  113. package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +73 -0
  114. package/src/internal-urls/agent-protocol.ts +73 -39
  115. package/src/irc/bus.ts +22 -3
  116. package/src/launch/broker.ts +3 -2
  117. package/src/launch/client.ts +2 -2
  118. package/src/launch/presence.ts +2 -2
  119. package/src/lib/xai-http.ts +28 -2
  120. package/src/lsp/client.ts +1 -1
  121. package/src/main.ts +11 -8
  122. package/src/mcp/manager.ts +9 -3
  123. package/src/mcp/transports/stdio.ts +103 -23
  124. package/src/modes/acp/acp-agent.ts +28 -9
  125. package/src/modes/acp/acp-event-mapper.ts +8 -0
  126. package/src/modes/acp/acp-mode.ts +18 -4
  127. package/src/modes/components/advisor-config.ts +65 -3
  128. package/src/modes/components/ask-dialog.ts +1 -1
  129. package/src/modes/components/assistant-message.ts +35 -14
  130. package/src/modes/components/chat-transcript-builder.ts +2 -0
  131. package/src/modes/components/custom-editor-plugin-ctor.test.ts +36 -0
  132. package/src/modes/components/custom-editor.ts +62 -1
  133. package/src/modes/components/hook-editor.ts +35 -5
  134. package/src/modes/components/login-dialog.test.ts +56 -0
  135. package/src/modes/components/login-dialog.ts +7 -3
  136. package/src/modes/components/model-hub.ts +138 -42
  137. package/src/modes/components/plan-review-overlay.ts +51 -10
  138. package/src/modes/components/session-selector.ts +10 -0
  139. package/src/modes/components/status-line/component.test.ts +1 -0
  140. package/src/modes/components/status-line/component.ts +2 -4
  141. package/src/modes/components/status-line/segments.ts +21 -6
  142. package/src/modes/components/tool-execution.test.ts +101 -0
  143. package/src/modes/components/tool-execution.ts +117 -9
  144. package/src/modes/components/transcript-container.ts +30 -2
  145. package/src/modes/controllers/command-controller-shared.ts +1 -1
  146. package/src/modes/controllers/command-controller.ts +168 -48
  147. package/src/modes/controllers/event-controller.ts +6 -0
  148. package/src/modes/controllers/extension-ui-controller.ts +4 -22
  149. package/src/modes/controllers/input-controller.ts +12 -12
  150. package/src/modes/controllers/selector-controller.ts +208 -46
  151. package/src/modes/interactive-mode.ts +205 -65
  152. package/src/modes/noninteractive-dispose.test.ts +60 -0
  153. package/src/modes/print-mode.ts +10 -5
  154. package/src/modes/rpc/host-tools.ts +2 -1
  155. package/src/modes/rpc/rpc-mode.ts +31 -7
  156. package/src/modes/theme/theme.ts +2 -2
  157. package/src/modes/types.ts +10 -0
  158. package/src/modes/utils/interactive-context-helpers.ts +3 -1
  159. package/src/modes/utils/transcript-render-helpers.ts +3 -2
  160. package/src/modes/warp-events.test.ts +794 -0
  161. package/src/modes/warp-events.ts +232 -0
  162. package/src/plan-mode/model-transition.test.ts +60 -0
  163. package/src/plan-mode/model-transition.ts +51 -0
  164. package/src/prompts/system/system-prompt.md +1 -1
  165. package/src/registry/agent-lifecycle.ts +133 -18
  166. package/src/sdk.ts +227 -44
  167. package/src/session/agent-session.ts +1364 -382
  168. package/src/session/session-history-format.ts +20 -5
  169. package/src/session/session-listing.ts +6 -5
  170. package/src/session/session-loader.ts +9 -0
  171. package/src/session/session-manager.ts +48 -0
  172. package/src/session/session-persistence.ts +11 -0
  173. package/src/session/streaming-output.ts +3 -1
  174. package/src/slash-commands/builtin-registry.ts +9 -0
  175. package/src/slash-commands/helpers/active-oauth-account.ts +16 -0
  176. package/src/slash-commands/types.ts +4 -4
  177. package/src/system-prompt.ts +2 -2
  178. package/src/task/executor.ts +1 -1
  179. package/src/task/label.ts +2 -0
  180. package/src/telemetry-export.ts +453 -97
  181. package/src/tools/approval.ts +11 -0
  182. package/src/tools/bash.ts +71 -38
  183. package/src/tools/browser/registry.ts +9 -2
  184. package/src/tools/browser/tab-supervisor.ts +65 -10
  185. package/src/tools/browser.ts +4 -3
  186. package/src/tools/essential-tools.ts +45 -0
  187. package/src/tools/eval.ts +3 -0
  188. package/src/tools/gh.ts +169 -2
  189. package/src/tools/image-gen.ts +624 -513
  190. package/src/tools/index.ts +10 -6
  191. package/src/tools/manage-skill.ts +5 -3
  192. package/src/tools/output-meta.ts +7 -0
  193. package/src/tools/path-utils.ts +8 -5
  194. package/src/tools/read.ts +48 -3
  195. package/src/tools/write.ts +22 -4
  196. package/src/tools/xdev.ts +14 -3
  197. package/src/utils/title-generator.ts +15 -4
  198. package/src/web/search/index.ts +21 -6
  199. package/src/web/search/providers/base.ts +3 -0
  200. package/src/web/search/providers/kimi.ts +18 -12
  201. package/src/web/search/providers/xai.ts +40 -9
  202. package/src/web/search/types.ts +6 -1
@@ -41,11 +41,13 @@ export function buildAutoLearnInstructions(available: { manageSkill: boolean; le
41
41
  export interface AutoLearnControllerOptions {
42
42
  session: AgentSession;
43
43
  settings: Settings;
44
+ capture: (content: string) => Promise<void>;
44
45
  }
45
46
 
46
47
  export class AutoLearnController {
47
48
  readonly #session: AgentSession;
48
49
  readonly #settings: Settings;
50
+ readonly #capture: (content: string) => Promise<void>;
49
51
  #toolCalls = 0;
50
52
  /**
51
53
  * Whether the in-flight turn BEGAN while goal mode was active. Captured at
@@ -54,12 +56,15 @@ export class AutoLearnController {
54
56
  * would let a goal-continuation turn slip through and get nudged.
55
57
  */
56
58
  #turnStartedInGoalMode = false;
57
- /** Swallow the agent_end produced by an auto-run capture turn so it cannot re-trigger. */
58
- #suppressNext = false;
59
+ /** Prevent overlapping private capture runs while real primary turns continue. */
60
+ #captureInFlight = false;
61
+ /** One newer eligible primary stop arrived while capture was running. */
62
+ #capturePending = false;
59
63
 
60
64
  constructor(options: AutoLearnControllerOptions) {
61
65
  this.#session = options.session;
62
66
  this.#settings = options.settings;
67
+ this.#capture = options.capture;
63
68
  // The listener closure captures `this`, so the session's listener array
64
69
  // keeps the controller alive — no stored unsubscribe needed.
65
70
  this.#session.subscribe(event => this.#onEvent(event));
@@ -91,10 +96,6 @@ export class AutoLearnController {
91
96
  const startedInGoalMode = this.#turnStartedInGoalMode;
92
97
  this.#turnStartedInGoalMode = false;
93
98
 
94
- if (this.#suppressNext) {
95
- this.#suppressNext = false;
96
- return;
97
- }
98
99
  // Never nudge a turn that ended in an abort (ESC, cancel, etc.). The
99
100
  // abort flag on the session is unreliable by the time agent_end is
100
101
  // deferred to subscribers; read stopReason from the event messages.
@@ -128,30 +129,24 @@ export class AutoLearnController {
128
129
  const autoContinue = this.#settings.get("autolearn.autoContinue") === true;
129
130
  if (!autoContinue) return;
130
131
 
131
- const content = AUTOLEARN_NUDGE_AUTOCONTINUE;
132
- // Arm suppression synchronously: the synthetic capture turn's agent_end
133
- // fires inside sendCustomMessage (before it resolves), so the flag must be
134
- // set before then. Disarm when no turn actually started — a deferred/queued
135
- // dispatch or a failed send produces no agent_end, and a latched flag would
136
- // otherwise swallow the next real stop.
137
- this.#suppressNext = true;
132
+ if (this.#captureInFlight) {
133
+ this.#capturePending = true;
134
+ return;
135
+ }
136
+ this.#startCapture();
137
+ }
138
138
 
139
- this.#session
140
- .sendCustomMessage(
141
- {
142
- customType: "autolearn-nudge",
143
- content,
144
- display: false,
145
- attribution: "user",
146
- },
147
- { deliverAs: "nextTurn", triggerTurn: true, acceptTerminalEmptyStop: true },
148
- )
149
- .then(started => {
150
- if (!started) this.#suppressNext = false;
151
- })
139
+ #startCapture(): void {
140
+ this.#captureInFlight = true;
141
+ void this.#capture(AUTOLEARN_NUDGE_AUTOCONTINUE)
152
142
  .catch(err => {
153
- this.#suppressNext = false;
154
- logger.warn("auto-learn nudge delivery failed", { err });
143
+ logger.warn("auto-learn capture failed", { err });
144
+ })
145
+ .finally(() => {
146
+ this.#captureInFlight = false;
147
+ if (!this.#capturePending) return;
148
+ this.#capturePending = false;
149
+ this.#startCapture();
155
150
  });
156
151
  }
157
152
  }
@@ -159,12 +159,14 @@ function isFirstTokenEvent(event: AssistantMessageEvent): boolean {
159
159
  case "text_end":
160
160
  case "thinking_end":
161
161
  return event.content.length > 0;
162
+ case "image_end":
163
+ return true;
162
164
  default:
163
165
  return false;
164
166
  }
165
167
  }
166
168
 
167
- /** Final message carries visible output — non-empty text/thinking or a tool call. */
169
+ /** Final message carries visible output — non-empty text/thinking, an image, or a tool call. */
168
170
  function hasVisibleFinalContent(message: AssistantMessage): boolean {
169
171
  return message.content.some(block => {
170
172
  switch (block.type) {
@@ -172,6 +174,7 @@ function hasVisibleFinalContent(message: AssistantMessage): boolean {
172
174
  return block.text.length > 0;
173
175
  case "thinking":
174
176
  return block.thinking.length > 0;
177
+ case "image":
175
178
  case "redactedThinking":
176
179
  case "toolCall":
177
180
  return true;
@@ -7,6 +7,7 @@ import * as path from "node:path";
7
7
  import { GrepOutputMode, grep } from "@oh-my-pi/pi-natives";
8
8
  import { APP_NAME } from "@oh-my-pi/pi-utils";
9
9
  import chalk from "chalk";
10
+ import { expandPath } from "../tools/path-utils";
10
11
 
11
12
  export interface GrepCommandArgs {
12
13
  pattern: string;
@@ -73,7 +74,7 @@ export async function runGrepCommand(cmd: GrepCommandArgs): Promise<void> {
73
74
  process.exit(1);
74
75
  }
75
76
 
76
- const searchPath = path.resolve(cmd.path);
77
+ const searchPath = path.resolve(expandPath(cmd.path));
77
78
  console.log(chalk.dim(`Searching in: ${searchPath}`));
78
79
  console.log(chalk.dim(`Pattern: ${cmd.pattern}`));
79
80
  console.log(
@@ -46,31 +46,65 @@ export const commands: CommandEntry[] = [
46
46
  { name: "search", load: () => import("./commands/web-search").then(m => m.default), aliases: ["q"] },
47
47
  ];
48
48
 
49
- // Documented-looking plugin-management verbs that are NOT registered top-level
50
- // commands. Without a guard `resolveCliArgv` rewrites e.g. `omp list` to
51
- // `omp launch list`, silently forwarding the bare verb to the model as a prompt
52
- // instead of managing plugins (#2935; same class as the `install` leak fixed in
53
- // #1496/#1498). A bare (single-arg) use gets a hint pointing at the real
54
- // `omp plugin <action>` command; multi-word invocations still fall through to
55
- // `launch`, so genuine prompts that merely begin with one of these words work.
56
- const RESERVED_TOP_LEVEL_WORDS = new Map<string, string>([
57
- [
58
- "extensions",
49
+ // Documented-looking plugin/marketplace verbs that are NOT registered top-level
50
+ // commands. Without a guard `resolveCliArgv` rewrites e.g. `omp marketplace add
51
+ // xyz` to `omp launch marketplace add xyz`, silently forwarding the argv to the
52
+ // model as a prompt instead of managing plugins (#4845; same class as the
53
+ // `list`/`remove` leak fixed in #2935 and the `install` leak in #1496/#1498).
54
+ // The real commands live under `omp plugin <action>`; each entry maps a verb to
55
+ // a hint pointing there. See {@link reservedTopLevelWordMessage} for when a hint
56
+ // fires vs. when the argv still falls through to `launch`.
57
+ const RESERVED_TOP_LEVEL_WORDS: Record<string, string> = {
58
+ extensions:
59
59
  '`omp extensions` is not a management command. Use `omp plugin list` / `omp plugin install`, or run `omp launch extensions` if you meant to send "extensions" as a prompt.',
60
- ],
61
- [
62
- "list",
63
- '`omp list` is not a top-level command. Use `omp plugin list` to list installed plugins, or run `omp launch list` if you meant to send "list" as a prompt.',
64
- ],
65
- [
66
- "remove",
60
+ list: '`omp list` is not a top-level command. Use `omp plugin list` to list installed plugins, or run `omp launch list` if you meant to send "list" as a prompt.',
61
+ remove:
67
62
  '`omp remove` is not a top-level command. Use `omp plugin uninstall <name>` to remove a plugin, or run `omp launch remove` if you meant to send "remove" as a prompt.',
68
- ],
69
- ]);
63
+ uninstall:
64
+ '`omp uninstall` is not a top-level command. Use `omp plugin uninstall <name@marketplace>` to remove a plugin, or run `omp launch uninstall` if you meant to send "uninstall" as a prompt.',
65
+ marketplace:
66
+ '`omp marketplace` is not a top-level command. Use `omp plugin marketplace <add|remove|update|list>` to manage marketplaces, or run `omp launch marketplace` if you meant to send "marketplace" as a prompt.',
67
+ discover:
68
+ '`omp discover` is not a top-level command. Use `omp plugin discover [marketplace]` to browse available plugins, or run `omp launch discover` if you meant to send "discover" as a prompt.',
69
+ upgrade:
70
+ '`omp upgrade` is not a top-level command. Use `omp plugin upgrade [name@marketplace]` to upgrade plugins, or run `omp launch upgrade` if you meant to send "upgrade" as a prompt.',
71
+ enable:
72
+ '`omp enable` is not a top-level command. Use `omp plugin enable <name@marketplace>` to enable a plugin, or run `omp launch enable` if you meant to send "enable" as a prompt.',
73
+ disable:
74
+ '`omp disable` is not a top-level command. Use `omp plugin disable <name@marketplace>` to disable a plugin, or run `omp launch disable` if you meant to send "disable" as a prompt.',
75
+ };
70
76
 
71
- export function reservedTopLevelWordMessage(first: string | undefined, argc = 1): string | undefined {
72
- if (argc !== 1 || !first || first.startsWith("-") || first.startsWith("@")) return undefined;
73
- return RESERVED_TOP_LEVEL_WORDS.get(first);
77
+ // Sub-actions that make `omp marketplace <sub>` unambiguously a management
78
+ // command even when multi-word (the reporter's `omp marketplace add xyz`,
79
+ // #4845). Mirrors the switch in `handleMarketplace` (cli/plugin-cli.ts).
80
+ const MARKETPLACE_SUBCOMMANDS: Record<string, true> = { add: true, remove: true, rm: true, update: true, list: true };
81
+
82
+ /**
83
+ * Hint for a reserved plugin/marketplace verb used as a top-level command, or
84
+ * `undefined` when the argv should fall through to `launch`.
85
+ *
86
+ * A bare verb (`omp marketplace`) always hints. A multi-word invocation only
87
+ * hints when the arguments follow the documented plugin grammar — a marketplace
88
+ * sub-action (`omp marketplace add …`) or a `name@marketplace` plugin id
89
+ * (`omp uninstall foo@bar`) — so genuine prompts that merely begin with one of
90
+ * these words (`omp list all my files`, `omp upgrade the deps`) still launch.
91
+ *
92
+ * Flags (`-…`) and `@file` arguments in the verb slot are never management
93
+ * commands; those fall through to the default `launch` command.
94
+ */
95
+ export function reservedTopLevelWordMessage(argv: readonly string[]): string | undefined {
96
+ const first = argv[0];
97
+ if (!first || first.startsWith("-") || first.startsWith("@")) return undefined;
98
+ const hint = RESERVED_TOP_LEVEL_WORDS[first];
99
+ if (!hint) return undefined;
100
+ const second = argv[1];
101
+ if (second === undefined) return hint;
102
+ if (first === "marketplace" && MARKETPLACE_SUBCOMMANDS[second]) return hint;
103
+ for (let index = 1; index < argv.length; index += 1) {
104
+ const arg = argv[index];
105
+ if (!arg.startsWith("-") && arg.includes("@")) return hint;
106
+ }
107
+ return undefined;
74
108
  }
75
109
 
76
110
  /**
@@ -112,7 +146,7 @@ function leadingSubcommandIndex(argv: string[]): number {
112
146
  */
113
147
  export function resolveCliArgv(argv: string[]): ResolvedCliArgv {
114
148
  const first = argv[0];
115
- const reservedMessage = reservedTopLevelWordMessage(first, argv.length);
149
+ const reservedMessage = reservedTopLevelWordMessage(argv);
116
150
  if (reservedMessage) return { error: reservedMessage };
117
151
  if (first === "--help" || first === "-h" || first === "--version" || first === "-v" || first === "help") {
118
152
  return { argv };
package/src/cli.ts CHANGED
@@ -338,7 +338,11 @@ export async function runCli(argv: string[]): Promise<void> {
338
338
  // worker's parked initial messages as soon as the entry module's
339
339
  // top-level evaluation finishes.
340
340
  if (resolvedArgv[0]?.startsWith("__omp_worker_")) {
341
- await runWorkerEntrypoint(resolvedArgv[0]);
341
+ const dispatched = await runWorkerEntrypoint(resolvedArgv[0]);
342
+ if (!dispatched) {
343
+ process.stderr.write(`Error: unknown worker selector: ${resolvedArgv[0]}\n`);
344
+ process.exitCode = 1;
345
+ }
342
346
  return;
343
347
  }
344
348
 
@@ -10,6 +10,7 @@ import type { Api, Model, RemoteCompactionConfig } from "@oh-my-pi/pi-ai/types";
10
10
  import { buildModel } from "@oh-my-pi/pi-catalog/build";
11
11
  import {
12
12
  getBundledModelReferenceIndex,
13
+ isQwenModelId,
13
14
  resolveModelReference,
14
15
  stripBracketedModelIdAffixes,
15
16
  } from "@oh-my-pi/pi-catalog/identity";
@@ -512,6 +513,48 @@ async function discoverLlamaCppServerMetadata(
512
513
  }
513
514
  }
514
515
 
516
+ /**
517
+ * PrismLM Ternary/1-bit Bonsai GGUFs are Qwen3.6-27B derivatives served locally
518
+ * via llama.cpp; their ids do not contain "qwen", so match them explicitly here
519
+ * rather than broadening the global `isQwenModelId` predicate.
520
+ */
521
+ function isBonsaiQwenGguf(id: string): boolean {
522
+ return /(?:ternary-)?bonsai-27b/i.test(id);
523
+ }
524
+
525
+ /**
526
+ * applyLlamaCppQwenThinking rewrites a discovered or cached llama.cpp model so a
527
+ * Qwen-family chat template (which defaults `enable_thinking: true`) can be
528
+ * turned off. Qwen ids and the Qwen3.6-based PrismLM Ternary Bonsai GGUFs are
529
+ * routed through chat-completions (the implicit llama.cpp provider defaults to
530
+ * `openai-responses`, whose disable path has no Qwen encoding) with the
531
+ * `qwen-template-false` dialect; omp emits `preserve_thinking` inside
532
+ * `chat_template_kwargs` for Qwen, so the toggle rides there too and history
533
+ * `<think>` blocks survive (`qwenPreserveThinking`). The runtime base URL gets a
534
+ * `/v1` suffix because the chat-completions request would otherwise POST to the
535
+ * native root, which does not serve it. A model with a custom transport (e.g.
536
+ * `pi-native`, whose client appends `/v1/pi/stream`) keeps its base URL so the
537
+ * suffix is not doubled. Non-Qwen models pass through unchanged. Applied on both
538
+ * fresh discovery and cache load, so an upgraded cache is corrected without
539
+ * waiting for re-discovery.
540
+ */
541
+ export function applyLlamaCppQwenThinking(model: Model<Api>): Model<Api> {
542
+ if (!isQwenModelId(model.id) && !isBonsaiQwenGguf(model.id)) return model;
543
+ return buildModel({
544
+ ...model,
545
+ api: "openai-completions",
546
+ baseUrl: model.transport ? model.baseUrl : ensureLlamaCppV1BaseUrl(normalizeLlamaCppBaseUrl(model.baseUrl)),
547
+ reasoning: true,
548
+ compat: {
549
+ ...model.compatConfig,
550
+ supportsReasoningParams: true,
551
+ thinkingFormat: "qwen-chat-template",
552
+ reasoningDisableMode: "qwen-template-false",
553
+ qwenPreserveThinking: true,
554
+ },
555
+ } as unknown as ModelSpec<Api>);
556
+ }
557
+
515
558
  export async function discoverLlamaCppModels(
516
559
  providerConfig: DiscoveryProviderConfig,
517
560
  ctx: DiscoveryContext,
@@ -553,26 +596,31 @@ export async function discoverLlamaCppModels(
553
596
  serverMetadata?.contextWindow ??
554
597
  item.trainingContextWindow ??
555
598
  DISCOVERY_DEFAULT_CONTEXT_WINDOW;
599
+ // Local llama.cpp models stamp `reasoning: false` with a minimal compat;
600
+ // applyLlamaCppQwenThinking upgrades Qwen-family ids (which cannot disable
601
+ // their default-on thinking otherwise) after the base model is built.
556
602
  discovered.push(
557
- buildModel({
558
- id,
559
- name: id,
560
- api: providerConfig.api,
561
- provider: providerConfig.provider,
562
- baseUrl,
563
- reasoning: false,
564
- input: item.input ?? serverMetadata?.input ?? ["text"],
565
- imageInputDecoder: "stb",
566
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
567
- contextWindow,
568
- maxTokens: resolveLlamaCppMaxTokens(contextWindow, serverMetadata?.maxTokens),
569
- headers,
570
- compat: {
571
- supportsStore: false,
572
- supportsDeveloperRole: false,
573
- supportsReasoningEffort: false,
574
- },
575
- } as ModelSpec<Api>),
603
+ applyLlamaCppQwenThinking(
604
+ buildModel({
605
+ id,
606
+ name: id,
607
+ api: providerConfig.api,
608
+ provider: providerConfig.provider,
609
+ baseUrl,
610
+ reasoning: false,
611
+ input: item.input ?? serverMetadata?.input ?? ["text"],
612
+ imageInputDecoder: "stb",
613
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
614
+ contextWindow,
615
+ maxTokens: resolveLlamaCppMaxTokens(contextWindow, serverMetadata?.maxTokens),
616
+ headers,
617
+ compat: {
618
+ supportsStore: false,
619
+ supportsDeveloperRole: false,
620
+ supportsReasoningEffort: false,
621
+ },
622
+ } as ModelSpec<Api>),
623
+ ),
576
624
  );
577
625
  }
578
626
  return discovered;
@@ -583,7 +631,12 @@ export async function discoverLlamaCppModelRuntimeMetadata(
583
631
  ctx: DiscoveryContext,
584
632
  ): Promise<LlamaCppDiscoveredModelRuntimeMetadata | undefined> {
585
633
  const baseUrl = normalizeLlamaCppBaseUrl(model.baseUrl);
586
- const modelsUrl = `${baseUrl}/models`;
634
+ // Probe the native `/models` endpoint (not the OpenAI-compatible `/v1/models`)
635
+ // so the runtime `meta`, `status.args`, and `architecture.input_modalities`
636
+ // fields survive; a Qwen model routed to chat-completions carries a `/v1`
637
+ // base URL, which would otherwise send this to `/v1/models`.
638
+ const nativeBaseUrl = toLlamaCppNativeBaseUrl(baseUrl);
639
+ const modelsUrl = `${nativeBaseUrl}/models`;
587
640
  const baseHeaders: Record<string, string> = { ...(model.headers ?? {}) };
588
641
  const attempt = async (headers: Record<string, string>) => {
589
642
  const [entries, serverMetadata] = await Promise.all([
@@ -597,7 +650,7 @@ export async function discoverLlamaCppModelRuntimeMetadata(
597
650
  }
598
651
  return parseLlamaCppModelList(await response.json());
599
652
  }),
600
- discoverLlamaCppServerMetadata(ctx, baseUrl, headers),
653
+ discoverLlamaCppServerMetadata(ctx, nativeBaseUrl, headers),
601
654
  ]);
602
655
  if (!entries) {
603
656
  return undefined;
@@ -898,6 +951,13 @@ function normalizeLlamaCppBaseUrl(baseUrl?: string): string {
898
951
  }
899
952
  }
900
953
 
954
+ // ensureLlamaCppV1BaseUrl appends the OpenAI-compatible `/v1` prefix a
955
+ // chat-completions request needs; native discovery keeps the bare root, which
956
+ // serves `/models` and `/props` but not `/chat/completions`.
957
+ function ensureLlamaCppV1BaseUrl(baseUrl: string): string {
958
+ return baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1`;
959
+ }
960
+
901
961
  function toLlamaCppNativeBaseUrl(baseUrl: string): string {
902
962
  try {
903
963
  const parsed = new URL(baseUrl);
@@ -71,6 +71,7 @@ import type { AuthStorage, OAuthCredential } from "../session/auth-storage";
71
71
  import { type ApiKeyResolverModel, type ApiKeyResolverOptions, createApiKeyResolver } from "./api-key-resolver";
72
72
  import type { ConfigError, ConfigFile } from "./config-file";
73
73
  import {
74
+ applyLlamaCppQwenThinking,
74
75
  DISCOVERY_DEFAULT_MAX_TOKENS,
75
76
  type DiscoveryContext,
76
77
  type DiscoveryProviderConfig,
@@ -1020,7 +1021,7 @@ export class ModelRegistry {
1020
1021
  // Custom/config providers bypass the model-manager merge point —
1021
1022
  // collapse effort-tier variants here so X/X-thinking twins fold.
1022
1023
  const withModelOverrides = this.#applyModelOverrides(collapseBuiltModelVariants(combined), this.#modelOverrides);
1023
- this.#models = this.#applyRuntimeProviderOverrides(withModelOverrides);
1024
+ this.#models = this.#applyLlamaCppQwenThinkingToModels(this.#applyRuntimeProviderOverrides(withModelOverrides));
1024
1025
  this.#lastStaticLoadMtime = this.#modelsConfigFile.getMtimeMs();
1025
1026
  }
1026
1027
 
@@ -1417,7 +1418,7 @@ export class ModelRegistry {
1417
1418
  // Merge runtime extension models so they survive online discovery completion
1418
1419
  const combined = this.#mergeCustomModels(withConfigModels, this.#runtimeModelOverlays);
1419
1420
  const withModelOverrides = this.#applyModelOverrides(collapseBuiltModelVariants(combined), this.#modelOverrides);
1420
- this.#models = this.#applyRuntimeProviderOverrides(withModelOverrides);
1421
+ this.#models = this.#applyLlamaCppQwenThinkingToModels(this.#applyRuntimeProviderOverrides(withModelOverrides));
1421
1422
  }
1422
1423
 
1423
1424
  #configuredDiscoveryCacheProviderId(providerConfig: DiscoveryProviderConfig): string {
@@ -1779,6 +1780,22 @@ export class ModelRegistry {
1779
1780
  });
1780
1781
  }
1781
1782
 
1783
+ // #applyLlamaCppQwenThinkingToModels re-runs applyLlamaCppQwenThinking as the
1784
+ // outermost transform for llama.cpp-provider models, after discovery merges,
1785
+ // cache fallbacks, and provider/transport overrides have run. It is
1786
+ // idempotent, so it restores the routed Qwen model's chat-completions api,
1787
+ // `/v1` runtime base URL, and disable dialect even when a configured `baseUrl`
1788
+ // override (which wins in mergeDiscoveredModel) or a fallback to a pre-fix
1789
+ // cached row would otherwise leave the old spec in place.
1790
+ #applyLlamaCppQwenThinkingToModels(models: Model<Api>[]): Model<Api>[] {
1791
+ const llamaCppProviders = new Set<string>();
1792
+ for (const provider of this.#discoverableProviders) {
1793
+ if (provider.discovery.type === "llama.cpp") llamaCppProviders.add(provider.provider);
1794
+ }
1795
+ if (llamaCppProviders.size === 0) return models;
1796
+ return models.map(model => (llamaCppProviders.has(model.provider) ? applyLlamaCppQwenThinking(model) : model));
1797
+ }
1798
+
1782
1799
  #mergeProviderOverride(baseOverride: ProviderOverride | undefined, override: ProviderOverride): ProviderOverride {
1783
1800
  return {
1784
1801
  baseUrl: override.baseUrl ?? baseOverride?.baseUrl,
@@ -1993,6 +2010,15 @@ export class ModelRegistry {
1993
2010
  getProviderBaseUrl(provider: string): string | undefined {
1994
2011
  return this.#models.find(m => m.provider === provider && m.baseUrl)?.baseUrl;
1995
2012
  }
2013
+ /**
2014
+ * Get provider-level headers without including per-model overrides.
2015
+ */
2016
+ getProviderHeaders(provider: string): Record<string, string> | undefined {
2017
+ return createLiveConfigHeaders([
2018
+ this.#providerOverrides.get(provider)?.headers,
2019
+ this.#runtimeProviderOverrides.get(provider)?.headers,
2020
+ ]);
2021
+ }
1996
2022
 
1997
2023
  /**
1998
2024
  * Get API key for a model.
@@ -2297,10 +2323,12 @@ export class ModelRegistry {
2297
2323
  transportOverride,
2298
2324
  );
2299
2325
  this.#runtimeProviderOverrides.set(providerName, nextRuntimeOverride);
2300
- this.#models = this.#models.map(m => {
2301
- if (m.provider !== providerName) return m;
2302
- return this.#applyProviderTransportOverride(m, transportOverride);
2303
- });
2326
+ this.#models = this.#applyLlamaCppQwenThinkingToModels(
2327
+ this.#models.map(m => {
2328
+ if (m.provider !== providerName) return m;
2329
+ return this.#applyProviderTransportOverride(m, transportOverride);
2330
+ }),
2331
+ );
2304
2332
  }
2305
2333
  }
2306
2334
 
@@ -623,11 +623,18 @@ function findExactModelReferenceMatch(modelReference: string, availableModels: M
623
623
  * 4. provider-scoped fuzzy match,
624
624
  * 5. substring match with the alias-vs-dated pick.
625
625
  * Returns the matched model or undefined if no match found.
626
+ *
627
+ * `exactOnly` stops after the exact phases (1-3), skipping the fuzzy/substring
628
+ * fallbacks (4-5). Callers use it to resolve the full selector exactly before
629
+ * a trailing `:<level>` thinking suffix is split off, so the suffix can never
630
+ * be fuzzily absorbed into a longer sibling id (e.g. `kimi-for-coding:high`
631
+ * must not match `kimi-for-coding-highspeed`).
626
632
  */
627
633
  function matchModel(
628
634
  modelPattern: string,
629
635
  availableModels: Model<Api>[],
630
636
  context: ModelPreferenceContext,
637
+ options?: { exactOnly?: boolean },
631
638
  ): Model<Api> | undefined {
632
639
  const exactRefMatch = findExactModelReferenceMatch(modelPattern, availableModels);
633
640
  if (exactRefMatch) {
@@ -663,6 +670,14 @@ function matchModel(
663
670
  return pickPreferredModel(preferred.length > 0 ? preferred : aliasMatches, context);
664
671
  }
665
672
  }
673
+
674
+ // Exact phases exhausted. Fuzzy/substring fallbacks (below) subsequence-match
675
+ // the whole pattern and would let a trailing `:<level>` thinking suffix bleed
676
+ // into a longer sibling id; callers that still hold an unstripped suffix ask
677
+ // for exact-only so the suffix is split off before any fuzzy attempt.
678
+ if (options?.exactOnly) {
679
+ return undefined;
680
+ }
666
681
  // Check for provider/modelId format — fuzzy match within provider only.
667
682
  const slashIndex = modelPattern.indexOf("/");
668
683
  if (slashIndex !== -1) {
@@ -766,17 +781,33 @@ function parseModelPatternWithContext(
766
781
  context: ModelPreferenceContext,
767
782
  options?: { allowInvalidThinkingSelectorFallback?: boolean },
768
783
  ): ParsedModelResult {
769
- // Try exact match first
770
- const exactMatch = matchModel(pattern, availableModels, context);
784
+ // Exact match on the full pattern first (no fuzzy): a literal id that
785
+ // contains a colon (`coding-router:max`) wins over any suffix split.
786
+ const exactMatch = matchModel(pattern, availableModels, context, { exactOnly: true });
771
787
  if (exactMatch) {
772
788
  return { model: exactMatch, thinkingLevel: undefined, warning: undefined, explicitThinkingLevel: false };
773
789
  }
774
790
 
775
- // No match - try stripping a valid thinking suffix and recursing.
776
- // `max` is accepted only after the full pattern failed, so literal model IDs
777
- // ending in `:max` keep winning over the thinking suffix.
791
+ // Prefer a fuzzy match whose actual id ends in the suffix, preserving
792
+ // shorthand selectors for literal tier models such as `router:low`. Other
793
+ // fuzzy results (e.g. `kimi-for-coding-highspeed`) cannot absorb the suffix.
778
794
  const { base, level } = splitThinkingSuffix(pattern, -1, MAX_THINKING_SUFFIX_OPTIONS);
779
795
  if (level) {
796
+ const literalSuffixMatch = matchModel(pattern, availableModels, context);
797
+ if (literalSuffixMatch?.id.toLowerCase().endsWith(`:${level}`)) {
798
+ return {
799
+ model: literalSuffixMatch,
800
+ thinkingLevel: undefined,
801
+ warning: undefined,
802
+ explicitThinkingLevel: false,
803
+ };
804
+ }
805
+
806
+ // Strip a valid thinking suffix and recurse before accepting any other
807
+ // fuzzy match, so `:<level>` cannot be absorbed into a longer sibling
808
+ // id (e.g. `kimi-for-coding:high` must not match
809
+ // `kimi-for-coding-highspeed`). `max` is accepted only after the exact
810
+ // match above failed, so literal model IDs ending in `:max` keep winning.
780
811
  const result = parseModelPatternWithContext(base, availableModels, context, options);
781
812
  if (result.model) {
782
813
  // Only use this thinking level if no warning from inner recursion
@@ -791,6 +822,13 @@ function parseModelPatternWithContext(
791
822
  return result;
792
823
  }
793
824
 
825
+ // No valid thinking suffix: fall back to fuzzy/substring matching on the
826
+ // whole pattern.
827
+ const fallbackMatch = matchModel(pattern, availableModels, context);
828
+ if (fallbackMatch) {
829
+ return { model: fallbackMatch, thinkingLevel: undefined, warning: undefined, explicitThinkingLevel: false };
830
+ }
831
+
794
832
  const lastColonIndex = pattern.lastIndexOf(":");
795
833
  if (lastColonIndex === -1) {
796
834
  // No colons, pattern simply doesn't match any model
@@ -859,6 +897,10 @@ export function parseModelPattern(
859
897
  const DEFAULT_MODEL_ROLE = "default";
860
898
  const MODEL_ROLE_ALIAS_PREFIXES = [MODEL_ROLE_ALIAS_PREFIX, LEGACY_MODEL_ROLE_ALIAS_PREFIX];
861
899
 
900
+ export interface ModelRoleLookup {
901
+ getModelRole(role: ModelRole | string): string | undefined;
902
+ }
903
+
862
904
  function isModelRole(role: string): role is ModelRole {
863
905
  return (MODEL_ROLE_IDS as string[]).includes(role);
864
906
  }
@@ -875,7 +917,7 @@ function modelRoleAliasPrefixLength(value: string): number | undefined {
875
917
  return MODEL_ROLE_ALIAS_PREFIXES.find(prefix => value.startsWith(prefix))?.length;
876
918
  }
877
919
 
878
- function getModelRoleAlias(value: string, settings?: Settings): string | undefined {
920
+ function getModelRoleAlias(value: string, settings?: ModelRoleLookup): string | undefined {
879
921
  const normalized = value.trim();
880
922
  const prefixLength = modelRoleAliasPrefixLength(normalized);
881
923
  if (prefixLength === undefined) return undefined;
@@ -931,7 +973,7 @@ function resolveDefaultInheritedPatterns(
931
973
  role: ModelRole,
932
974
  configuredDefault: string | undefined,
933
975
  roleDefaults: string[],
934
- settings: Settings | undefined,
976
+ settings: ModelRoleLookup | undefined,
935
977
  visited: Set<string>,
936
978
  ): string[] {
937
979
  if (!shouldInheritDefaultBeforePriority(role) || !configuredDefault) return [];
@@ -969,7 +1011,7 @@ function resolveDefaultInheritedPatterns(
969
1011
 
970
1012
  function resolveConfiguredRolePattern(
971
1013
  value: string,
972
- settings?: Settings,
1014
+ settings?: ModelRoleLookup,
973
1015
  visited: Set<string> = new Set(),
974
1016
  ): string[] | undefined {
975
1017
  const normalized = value.trim();
@@ -1006,7 +1048,7 @@ function resolveConfiguredRolePattern(
1006
1048
  /**
1007
1049
  * Expand a role alias like "@smol" to the configured model string.
1008
1050
  */
1009
- export function expandRoleAlias(value: string, settings?: Settings): string {
1051
+ export function expandRoleAlias(value: string, settings?: ModelRoleLookup): string {
1010
1052
  const normalized = value.trim();
1011
1053
  if (normalized === DEFAULT_MODEL_ROLE) {
1012
1054
  return settings?.getModelRole("default") ?? value;
@@ -1016,7 +1058,10 @@ export function expandRoleAlias(value: string, settings?: Settings): string {
1016
1058
  return resolved ?? value;
1017
1059
  }
1018
1060
 
1019
- export function resolveConfiguredModelPatterns(value: string | string[] | undefined, settings?: Settings): string[] {
1061
+ export function resolveConfiguredModelPatterns(
1062
+ value: string | string[] | undefined,
1063
+ settings?: ModelRoleLookup,
1064
+ ): string[] {
1020
1065
  const patterns = normalizeModelPatternList(value);
1021
1066
  return patterns.flatMap(pattern => {
1022
1067
  const resolved = resolveConfiguredRolePattern(pattern, settings);
@@ -1100,7 +1145,7 @@ export interface ResolvedModelRoleValue {
1100
1145
  export function resolveModelRoleValue(
1101
1146
  roleValue: string | undefined,
1102
1147
  availableModels: Model<Api>[],
1103
- options?: { settings?: Settings; matchPreferences?: ModelMatchPreferences },
1148
+ options?: { settings?: Settings; roleLookup?: ModelRoleLookup; matchPreferences?: ModelMatchPreferences },
1104
1149
  ): ResolvedModelRoleValue {
1105
1150
  if (!roleValue) {
1106
1151
  return { model: undefined, thinkingLevel: undefined, explicitThinkingLevel: false, warning: undefined };
@@ -1111,7 +1156,7 @@ export function resolveModelRoleValue(
1111
1156
  return { model: undefined, thinkingLevel: undefined, explicitThinkingLevel: false, warning: undefined };
1112
1157
  }
1113
1158
 
1114
- const effectivePatterns = resolveConfiguredModelPatterns(normalized, options?.settings);
1159
+ const effectivePatterns = resolveConfiguredModelPatterns(normalized, options?.roleLookup ?? options?.settings);
1115
1160
  if (!effectivePatterns || effectivePatterns.length === 0) {
1116
1161
  return { model: undefined, thinkingLevel: undefined, explicitThinkingLevel: false, warning: undefined };
1117
1162
  }
@@ -60,6 +60,7 @@ const OpenAICompatFields = {
60
60
  "strictResponsesPairing?": "boolean",
61
61
  "supportsImageDetailOriginal?": "boolean",
62
62
  // anthropic-messages compat flags (same `compat` slot, per-api interpretation)
63
+ "supportsEagerToolInputStreaming?": "boolean",
63
64
  "requiresToolResultId?": "boolean",
64
65
  "replayUnsignedThinking?": "boolean",
65
66
  } as const;