@oh-my-pi/pi-coding-agent 15.12.4 → 15.13.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 (291) hide show
  1. package/CHANGELOG.md +304 -6
  2. package/dist/cli.js +1015 -881
  3. package/dist/types/async/job-manager.d.ts +15 -0
  4. package/dist/types/autolearn/controller.d.ts +25 -0
  5. package/dist/types/autolearn/managed-skills.d.ts +45 -0
  6. package/dist/types/autoresearch/state.d.ts +1 -1
  7. package/dist/types/autoresearch/types.d.ts +1 -1
  8. package/dist/types/cli/args.d.ts +19 -1
  9. package/dist/types/cli/session-picker.d.ts +1 -1
  10. package/dist/types/cli/setup-cli.d.ts +1 -1
  11. package/dist/types/cli/setup-model-picker.d.ts +14 -0
  12. package/dist/types/collab/protocol.d.ts +1 -1
  13. package/dist/types/commands/say.d.ts +24 -0
  14. package/dist/types/config/keybindings.d.ts +3 -3
  15. package/dist/types/config/model-registry.d.ts +10 -0
  16. package/dist/types/config/models-config-schema.d.ts +12 -0
  17. package/dist/types/config/models-config.d.ts +8 -2
  18. package/dist/types/config/settings-schema.d.ts +261 -58
  19. package/dist/types/export/html/index.d.ts +2 -1
  20. package/dist/types/extensibility/extensions/model-api.d.ts +17 -0
  21. package/dist/types/extensibility/extensions/runner.d.ts +3 -1
  22. package/dist/types/extensibility/extensions/types.d.ts +47 -1
  23. package/dist/types/extensibility/hooks/index.d.ts +2 -1
  24. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +9 -0
  25. package/dist/types/extensibility/plugins/loader.d.ts +11 -0
  26. package/dist/types/extensibility/shared-events.d.ts +1 -1
  27. package/dist/types/extensibility/skills.d.ts +10 -0
  28. package/dist/types/goals/guided-setup.d.ts +18 -0
  29. package/dist/types/goals/state.d.ts +1 -1
  30. package/dist/types/hindsight/transcript.d.ts +1 -1
  31. package/dist/types/index.d.ts +5 -0
  32. package/dist/types/internal-urls/local-protocol.d.ts +4 -2
  33. package/dist/types/main.d.ts +4 -3
  34. package/dist/types/mcp/startup-events.d.ts +11 -0
  35. package/dist/types/memories/index.d.ts +7 -0
  36. package/dist/types/memory-backend/local-backend.d.ts +4 -3
  37. package/dist/types/mnemopi/config.d.ts +4 -4
  38. package/dist/types/modes/components/agent-hub.d.ts +6 -0
  39. package/dist/types/modes/components/assistant-message.d.ts +1 -2
  40. package/dist/types/modes/components/compaction-summary-message.d.ts +15 -1
  41. package/dist/types/modes/components/custom-editor.d.ts +39 -1
  42. package/dist/types/modes/components/custom-editor.test.d.ts +1 -0
  43. package/dist/types/modes/components/session-selector.d.ts +1 -1
  44. package/dist/types/modes/components/tool-execution.d.ts +26 -16
  45. package/dist/types/modes/components/transcript-container.d.ts +23 -2
  46. package/dist/types/modes/components/tree-selector.d.ts +1 -1
  47. package/dist/types/modes/components/usage-row.d.ts +3 -0
  48. package/dist/types/modes/controllers/command-controller.d.ts +2 -2
  49. package/dist/types/modes/controllers/input-controller.d.ts +14 -0
  50. package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
  51. package/dist/types/modes/gradient-highlight.d.ts +9 -4
  52. package/dist/types/modes/image-references.d.ts +6 -0
  53. package/dist/types/modes/interactive-mode.d.ts +27 -3
  54. package/dist/types/modes/magic-keywords.d.ts +13 -1
  55. package/dist/types/modes/rpc/rpc-mode.d.ts +35 -1
  56. package/dist/types/modes/rpc/rpc-types.d.ts +9 -1
  57. package/dist/types/modes/runtime-init.d.ts +4 -0
  58. package/dist/types/modes/theme/theme.d.ts +13 -2
  59. package/dist/types/modes/types.d.ts +8 -2
  60. package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
  61. package/dist/types/registry/agent-registry.d.ts +17 -0
  62. package/dist/types/secrets/obfuscator.d.ts +1 -1
  63. package/dist/types/session/agent-session.d.ts +14 -2
  64. package/dist/types/session/indexed-session-storage.d.ts +3 -4
  65. package/dist/types/session/session-context.d.ts +39 -0
  66. package/dist/types/session/session-entries.d.ts +159 -0
  67. package/dist/types/session/session-listing.d.ts +69 -0
  68. package/dist/types/session/session-loader.d.ts +16 -0
  69. package/dist/types/session/session-manager.d.ts +82 -474
  70. package/dist/types/session/session-migrations.d.ts +12 -0
  71. package/dist/types/session/session-paths.d.ts +25 -0
  72. package/dist/types/session/session-persistence.d.ts +8 -0
  73. package/dist/types/session/session-storage.d.ts +11 -12
  74. package/dist/types/session/snapcompact-inline.d.ts +12 -1
  75. package/dist/types/session/snapcompact-savings-journal.d.ts +46 -0
  76. package/dist/types/session/tool-choice-queue.d.ts +6 -6
  77. package/dist/types/stt/asr-client.d.ts +90 -0
  78. package/dist/types/stt/asr-protocol.d.ts +97 -0
  79. package/dist/types/stt/asr-worker.d.ts +2 -0
  80. package/dist/types/stt/downloader.d.ts +38 -0
  81. package/dist/types/stt/endpointer.d.ts +59 -0
  82. package/dist/types/stt/index.d.ts +5 -1
  83. package/dist/types/stt/models.d.ts +120 -0
  84. package/dist/types/stt/recorder.d.ts +17 -0
  85. package/dist/types/stt/stt-controller.d.ts +6 -0
  86. package/dist/types/stt/transcriber.d.ts +5 -7
  87. package/dist/types/stt/wav.d.ts +29 -0
  88. package/dist/types/system-prompt.d.ts +4 -0
  89. package/dist/types/task/executor.d.ts +2 -0
  90. package/dist/types/task/index.d.ts +9 -1
  91. package/dist/types/task/types.d.ts +36 -0
  92. package/dist/types/tools/bash.d.ts +2 -2
  93. package/dist/types/tools/eval-render.d.ts +1 -1
  94. package/dist/types/tools/index.d.ts +11 -1
  95. package/dist/types/tools/irc.d.ts +1 -0
  96. package/dist/types/tools/learn.d.ts +51 -0
  97. package/dist/types/tools/manage-skill.d.ts +40 -0
  98. package/dist/types/tools/plan-mode-guard.d.ts +10 -0
  99. package/dist/types/tools/renderers.d.ts +7 -11
  100. package/dist/types/tools/ssh.d.ts +1 -1
  101. package/dist/types/tools/todo.d.ts +1 -1
  102. package/dist/types/tools/tts.d.ts +25 -0
  103. package/dist/types/tools/write.d.ts +1 -1
  104. package/dist/types/tts/downloader.d.ts +20 -0
  105. package/dist/types/tts/index.d.ts +8 -0
  106. package/dist/types/tts/models.d.ts +82 -0
  107. package/dist/types/tts/player.d.ts +32 -0
  108. package/dist/types/tts/runtime.d.ts +6 -0
  109. package/dist/types/tts/streaming-player.d.ts +41 -0
  110. package/dist/types/tts/tts-client.d.ts +93 -0
  111. package/dist/types/tts/tts-protocol.d.ts +95 -0
  112. package/dist/types/tts/tts-worker.d.ts +2 -0
  113. package/dist/types/tts/vocalizer.d.ts +41 -0
  114. package/dist/types/tts/wav.d.ts +8 -0
  115. package/dist/types/utils/tool-choice.d.ts +8 -0
  116. package/dist/types/utils/tools-manager.d.ts +2 -1
  117. package/dist/types/utils/tools-manager.test.d.ts +1 -0
  118. package/dist/types/web/scrapers/github.d.ts +1 -1
  119. package/package.json +15 -14
  120. package/src/async/job-manager.ts +49 -0
  121. package/src/autolearn/controller.ts +139 -0
  122. package/src/autolearn/managed-skills.ts +257 -0
  123. package/src/autoresearch/state.ts +1 -1
  124. package/src/autoresearch/types.ts +1 -1
  125. package/src/cli/args.ts +56 -2
  126. package/src/cli/session-picker.ts +2 -1
  127. package/src/cli/setup-cli.ts +148 -47
  128. package/src/cli/setup-model-picker.ts +43 -0
  129. package/src/cli-commands.ts +1 -0
  130. package/src/cli.ts +45 -13
  131. package/src/collab/host.ts +1 -1
  132. package/src/collab/protocol.ts +1 -1
  133. package/src/commands/say.ts +102 -0
  134. package/src/commands/setup.ts +1 -1
  135. package/src/commit/agentic/tools/analyze-file.ts +3 -0
  136. package/src/config/keybindings.ts +2 -2
  137. package/src/config/model-discovery.ts +11 -5
  138. package/src/config/model-registry.ts +64 -9
  139. package/src/config/models-config-schema.ts +4 -1
  140. package/src/config/models-config.ts +2 -1
  141. package/src/config/settings-schema.ts +248 -32
  142. package/src/config/settings.ts +10 -0
  143. package/src/discovery/builtin.ts +23 -1
  144. package/src/discovery/claude-plugins.ts +44 -5
  145. package/src/discovery/helpers.ts +41 -1
  146. package/src/eval/__tests__/budget-bridge.test.ts +1 -1
  147. package/src/eval/js/shared/prelude.txt +69 -17
  148. package/src/export/html/index.ts +3 -6
  149. package/src/extensibility/extensions/model-api.ts +41 -0
  150. package/src/extensibility/extensions/runner.ts +4 -0
  151. package/src/extensibility/extensions/types.ts +52 -1
  152. package/src/extensibility/extensions/wrapper.ts +41 -5
  153. package/src/extensibility/hooks/index.ts +2 -1
  154. package/src/extensibility/plugins/legacy-pi-compat.ts +43 -13
  155. package/src/extensibility/plugins/loader.ts +30 -19
  156. package/src/extensibility/plugins/manager.ts +221 -90
  157. package/src/extensibility/shared-events.ts +1 -1
  158. package/src/extensibility/skills.ts +96 -15
  159. package/src/goals/guided-setup.ts +133 -0
  160. package/src/goals/state.ts +1 -1
  161. package/src/hindsight/transcript.ts +1 -1
  162. package/src/index.ts +5 -0
  163. package/src/internal-urls/docs-index.generated.ts +10 -10
  164. package/src/internal-urls/history-protocol.ts +1 -1
  165. package/src/internal-urls/local-protocol.ts +29 -7
  166. package/src/main.ts +27 -7
  167. package/src/mcp/startup-events.ts +21 -0
  168. package/src/mcp/transports/stdio.ts +2 -1
  169. package/src/memories/index.ts +146 -11
  170. package/src/memory-backend/local-backend.ts +11 -5
  171. package/src/mnemopi/backend.ts +1 -0
  172. package/src/mnemopi/config.ts +26 -10
  173. package/src/modes/acp/acp-agent.ts +3 -5
  174. package/src/modes/components/agent-hub.ts +49 -4
  175. package/src/modes/components/assistant-message.ts +4 -37
  176. package/src/modes/components/compaction-summary-message.ts +125 -26
  177. package/src/modes/components/custom-editor.test.ts +96 -0
  178. package/src/modes/components/custom-editor.ts +164 -8
  179. package/src/modes/components/session-selector.ts +1 -1
  180. package/src/modes/components/settings-defs.ts +7 -0
  181. package/src/modes/components/tool-execution.ts +82 -43
  182. package/src/modes/components/transcript-container.ts +70 -1
  183. package/src/modes/components/tree-selector.ts +1 -1
  184. package/src/modes/components/usage-row.ts +18 -0
  185. package/src/modes/components/user-message.ts +4 -2
  186. package/src/modes/controllers/command-controller.ts +14 -4
  187. package/src/modes/controllers/event-controller.ts +78 -11
  188. package/src/modes/controllers/extension-ui-controller.ts +6 -0
  189. package/src/modes/controllers/input-controller.ts +258 -27
  190. package/src/modes/controllers/selector-controller.ts +12 -2
  191. package/src/modes/gradient-highlight.ts +21 -9
  192. package/src/modes/image-references.ts +20 -0
  193. package/src/modes/interactive-mode.ts +286 -40
  194. package/src/modes/magic-keywords.ts +27 -5
  195. package/src/modes/rpc/rpc-mode.ts +146 -14
  196. package/src/modes/rpc/rpc-subagents.ts +2 -2
  197. package/src/modes/rpc/rpc-types.ts +8 -2
  198. package/src/modes/runtime-init.ts +28 -3
  199. package/src/modes/theme/theme.ts +98 -50
  200. package/src/modes/types.ts +6 -2
  201. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  202. package/src/modes/utils/ui-helpers.ts +34 -6
  203. package/src/priority.json +5 -1
  204. package/src/prompts/agents/task.md +1 -0
  205. package/src/prompts/goals/guided-goal-interview.md +8 -0
  206. package/src/prompts/goals/guided-goal-system.md +12 -0
  207. package/src/prompts/memories/read-path.md +6 -0
  208. package/src/prompts/system/autolearn-guidance-learn.md +1 -0
  209. package/src/prompts/system/autolearn-guidance.md +7 -0
  210. package/src/prompts/system/autolearn-nudge.md +3 -0
  211. package/src/prompts/system/eager-task.md +7 -0
  212. package/src/prompts/system/eager-todo.md +11 -6
  213. package/src/prompts/system/subagent-system-prompt.md +4 -0
  214. package/src/prompts/system/system-prompt.md +10 -5
  215. package/src/prompts/system/title-marker-instruction.md +1 -0
  216. package/src/prompts/system/title-system-marker.md +16 -0
  217. package/src/prompts/tools/job.md +1 -0
  218. package/src/prompts/tools/learn.md +7 -0
  219. package/src/prompts/tools/manage-skill.md +9 -0
  220. package/src/prompts/tools/task.md +3 -0
  221. package/src/registry/agent-registry.ts +30 -0
  222. package/src/sdk.ts +88 -24
  223. package/src/secrets/obfuscator.ts +1 -1
  224. package/src/session/agent-session.ts +209 -87
  225. package/src/session/history-storage.ts +2 -2
  226. package/src/session/indexed-session-storage.ts +7 -17
  227. package/src/session/session-context.ts +352 -0
  228. package/src/session/session-entries.ts +194 -0
  229. package/src/session/session-listing.ts +588 -0
  230. package/src/session/session-loader.ts +106 -0
  231. package/src/session/session-manager.ts +933 -3145
  232. package/src/session/session-migrations.ts +78 -0
  233. package/src/session/session-paths.ts +193 -0
  234. package/src/session/session-persistence.ts +131 -0
  235. package/src/session/session-storage.ts +91 -50
  236. package/src/session/snapcompact-inline.ts +21 -1
  237. package/src/session/snapcompact-savings-journal.ts +113 -0
  238. package/src/session/tool-choice-queue.ts +23 -11
  239. package/src/slash-commands/builtin-registry.ts +25 -3
  240. package/src/stt/asr-client.ts +520 -0
  241. package/src/stt/asr-protocol.ts +65 -0
  242. package/src/stt/asr-worker.ts +790 -0
  243. package/src/stt/downloader.ts +107 -47
  244. package/src/stt/endpointer.ts +259 -0
  245. package/src/stt/index.ts +5 -1
  246. package/src/stt/models.ts +150 -0
  247. package/src/stt/recorder.ts +247 -60
  248. package/src/stt/stt-controller.ts +201 -22
  249. package/src/stt/transcriber.ts +37 -68
  250. package/src/stt/wav.ts +173 -0
  251. package/src/system-prompt.ts +8 -0
  252. package/src/task/agents.ts +1 -2
  253. package/src/task/executor.ts +49 -15
  254. package/src/task/index.ts +60 -6
  255. package/src/task/render.ts +83 -8
  256. package/src/task/types.ts +53 -0
  257. package/src/tools/ask.ts +8 -0
  258. package/src/tools/bash.ts +4 -3
  259. package/src/tools/eval-render.ts +4 -3
  260. package/src/tools/index.ts +40 -4
  261. package/src/tools/irc.ts +10 -2
  262. package/src/tools/job.ts +14 -2
  263. package/src/tools/learn.ts +144 -0
  264. package/src/tools/manage-skill.ts +104 -0
  265. package/src/tools/plan-mode-guard.ts +53 -19
  266. package/src/tools/renderers.ts +7 -11
  267. package/src/tools/ssh.ts +4 -3
  268. package/src/tools/todo.ts +1 -1
  269. package/src/tools/tts.ts +203 -92
  270. package/src/tools/write.ts +18 -2
  271. package/src/tts/downloader.ts +64 -0
  272. package/src/tts/index.ts +8 -0
  273. package/src/tts/models.ts +137 -0
  274. package/src/tts/player.ts +137 -0
  275. package/src/tts/runtime.ts +21 -0
  276. package/src/tts/streaming-player.ts +266 -0
  277. package/src/tts/tts-client.ts +647 -0
  278. package/src/tts/tts-protocol.ts +60 -0
  279. package/src/tts/tts-worker.ts +497 -0
  280. package/src/tts/vocalizer.ts +162 -0
  281. package/src/tts/wav.ts +58 -0
  282. package/src/utils/title-generator.ts +48 -5
  283. package/src/utils/tool-choice.ts +16 -0
  284. package/src/utils/tools-manager.test.ts +25 -0
  285. package/src/utils/tools-manager.ts +19 -1
  286. package/src/web/scrapers/github.ts +96 -0
  287. package/src/web/search/index.ts +13 -0
  288. package/src/web/search/providers/searxng.ts +13 -1
  289. package/dist/types/stt/setup.d.ts +0 -18
  290. package/src/stt/setup.ts +0 -52
  291. package/src/stt/transcribe.py +0 -70
@@ -29,6 +29,7 @@ export const commands: CommandEntry[] = [
29
29
  { name: "join", load: () => import("./commands/join").then(m => m.default) },
30
30
  { name: "models", load: () => import("./commands/models").then(m => m.default) },
31
31
  { name: "plugin", load: () => import("./commands/plugin").then(m => m.default) },
32
+ { name: "say", load: () => import("./commands/say").then(m => m.default) },
32
33
  { name: "setup", load: () => import("./commands/setup").then(m => m.default) },
33
34
  { name: "shell", load: () => import("./commands/shell").then(m => m.default) },
34
35
  { name: "read", load: () => import("./commands/read").then(m => m.default) },
package/src/cli.ts CHANGED
@@ -56,6 +56,8 @@ async function showHelp(config: CliConfig): Promise<void> {
56
56
  async function runSmokeTest(): Promise<void> {
57
57
  const { smokeTestSyncWorker, startServer } = await import("@oh-my-pi/omp-stats");
58
58
  const { smokeTestTinyTitleWorker } = await import("./tiny/title-client");
59
+ const { smokeTestSttWorker } = await import("./stt/asr-client");
60
+ const { smokeTestTtsWorker } = await import("./tts/tts-client");
59
61
  await smokeTestSyncWorker();
60
62
 
61
63
  const statsServer = await startServer(0);
@@ -71,6 +73,8 @@ async function runSmokeTest(): Promise<void> {
71
73
  }
72
74
 
73
75
  await smokeTestTinyTitleWorker();
76
+ await smokeTestSttWorker();
77
+ await smokeTestTtsWorker();
74
78
  process.stdout.write("smoke-test: ok\n");
75
79
  }
76
80
 
@@ -78,6 +82,8 @@ const TINY_WORKER_ARGS = new Set(["--tiny-worker", "__tiny_worker"]);
78
82
  const STATS_SYNC_WORKER_ARG = "__omp_stats_sync_worker";
79
83
  const TAB_WORKER_ARG = "__omp_tab_worker";
80
84
  const JS_EVAL_WORKER_ARG = "__omp_js_eval_worker";
85
+ const STT_WORKER_ARG = "__omp_stt_worker";
86
+ const TTS_WORKER_ARG = "__omp_tts_worker";
81
87
 
82
88
  async function runWorkerEntrypoint(arg: string | undefined): Promise<boolean> {
83
89
  if (arg === STATS_SYNC_WORKER_ARG) {
@@ -110,21 +116,34 @@ async function runWorkerEntrypoint(arg: string | undefined): Promise<boolean> {
110
116
  await import("./eval/js/worker-entry");
111
117
  return true;
112
118
  }
119
+ if (arg === STT_WORKER_ARG) {
120
+ const { startSttWorker } = await import("./stt/asr-worker");
121
+ await runIpcSubprocessWorker(startSttWorker);
122
+ return true;
123
+ }
124
+ if (arg === TTS_WORKER_ARG) {
125
+ const { startTtsWorker } = await import("./tts/tts-worker");
126
+ await runIpcSubprocessWorker(startTtsWorker);
127
+ return true;
128
+ }
113
129
  return false;
114
130
  }
115
131
 
116
132
  /**
117
- * Hidden subcommand that boots the tiny-model worker inside this process
118
- * over the parent's IPC channel. The agent's main process spawns the same
119
- * binary with this flag so `onnxruntime-node` (loaded transitively by
120
- * `@huggingface/transformers`) lives in a child address space. The parent
121
- * `SIGKILL`s the child on shutdown so the NAPI finalizer never runs in
122
- * either process that finalizer segfaults Bun on Windows (issue #1606).
133
+ * Boot a subprocess-isolated transformers.js worker over the parent's IPC
134
+ * channel and block until the parent disconnects. The tiny-model, STT, and TTS
135
+ * workers each run `onnxruntime-node` (loaded transitively by
136
+ * `@huggingface/transformers`) in a child address space because its NAPI
137
+ * finalizer segfaults Bun on shutdown (issue #1606); the parent `SIGKILL`s the
138
+ * child so that finalizer never runs in either process. This wires `process`
139
+ * IPC to the worker's typed transport, keeps the event loop alive while the
140
+ * worker is idle, and hard-kills the process on parent `disconnect`.
123
141
  */
124
- async function runTinyWorker(): Promise<void> {
125
- const { startTinyTitleWorker } = await import("./tiny/worker");
142
+ async function runIpcSubprocessWorker<In, Out>(
143
+ start: (transport: { send(message: Out): void; onMessage(handler: (message: In) => void): () => void }) => void,
144
+ ): Promise<void> {
126
145
  const { promise: shuttingDown, resolve: shutdown } = Promise.withResolvers<void>();
127
- const send = (message: unknown): void => {
146
+ const send = (message: Out): void => {
128
147
  // `process.send` only exists when spawned with an IPC channel; the
129
148
  // parent always spawns us that way. If it's missing, the parent
130
149
  // vanished and there's no one to talk to.
@@ -139,10 +158,10 @@ async function runTinyWorker(): Promise<void> {
139
158
  shutdown();
140
159
  }
141
160
  };
142
- startTinyTitleWorker({
161
+ start({
143
162
  send,
144
163
  onMessage(handler) {
145
- const wrap = (data: unknown): void => handler(data as never);
164
+ const wrap = (data: unknown): void => handler(data as In);
146
165
  process.on("message", wrap);
147
166
  return () => {
148
167
  process.off("message", wrap);
@@ -151,8 +170,8 @@ async function runTinyWorker(): Promise<void> {
151
170
  });
152
171
  const keepalive = setInterval(() => {}, 2 ** 30);
153
172
  // Parent went away (crashed, SIGKILL, etc.) — commit suicide so we don't
154
- // linger as an orphan. SIGKILL via `process.kill` keeps us symmetrical
155
- // with the parent's hard-kill on shutdown: skip every JS/native finalizer.
173
+ // linger as an orphan. SIGKILL via `process.kill` keeps us symmetrical with
174
+ // the parent's hard-kill on shutdown: skip every JS/native finalizer.
156
175
  process.on("disconnect", () => shutdown());
157
176
  try {
158
177
  await shuttingDown;
@@ -162,6 +181,19 @@ async function runTinyWorker(): Promise<void> {
162
181
  process.kill(process.pid, "SIGKILL");
163
182
  }
164
183
 
184
+ /**
185
+ * Hidden subcommand that boots the tiny-model worker inside this process over
186
+ * the parent's IPC channel. The agent's main process spawns the same binary
187
+ * with this flag so `onnxruntime-node` (loaded transitively by
188
+ * `@huggingface/transformers`) lives in a child address space. The parent
189
+ * `SIGKILL`s the child on shutdown so the NAPI finalizer never runs in either
190
+ * process — that finalizer segfaults Bun on Windows (issue #1606).
191
+ */
192
+ async function runTinyWorker(): Promise<void> {
193
+ const { startTinyTitleWorker } = await import("./tiny/worker");
194
+ await runIpcSubprocessWorker(startTinyTitleWorker);
195
+ }
196
+
165
197
  /** Run the CLI with the given argv (no `process.argv` prefix). */
166
198
  export async function runCli(argv: string[]): Promise<void> {
167
199
  if (argv[0] === "--smoke-test") {
@@ -20,7 +20,7 @@ import { AgentLifecycleManager } from "../registry/agent-lifecycle";
20
20
  import { AgentRegistry } from "../registry/agent-registry";
21
21
  import type { AgentSessionEvent } from "../session/agent-session";
22
22
  import { stripImagesFromMessage, USER_INTERRUPT_LABEL } from "../session/messages";
23
- import type { SessionEntry as StoredSessionEntry } from "../session/session-manager";
23
+ import type { SessionEntry as StoredSessionEntry } from "../session/session-entries";
24
24
  import { TASK_SUBAGENT_LIFECYCLE_CHANNEL, TASK_SUBAGENT_PROGRESS_CHANNEL } from "../task";
25
25
  import { generateRoomKey, generateWriteToken, importRoomKey } from "./crypto";
26
26
  import {
@@ -25,7 +25,7 @@ import {
25
25
  } from "@oh-my-pi/pi-wire";
26
26
  import type { ContextUsage } from "../extensibility/extensions/types";
27
27
  import type { AgentSessionEvent } from "../session/agent-session";
28
- import type { SessionEntry, SessionHeader } from "../session/session-manager";
28
+ import type { SessionEntry, SessionHeader } from "../session/session-entries";
29
29
 
30
30
  export type {
31
31
  CollabPromptDetails,
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Synthesize text with the local TTS engine and play it (or save it with --out).
3
+ *
4
+ * Demonstrates the on-device speech stack end to end: the first run downloads
5
+ * the configured local model, synthesis happens in the TTS worker subprocess,
6
+ * and the resulting WAV is either played through the speakers or written to disk.
7
+ */
8
+ import * as os from "node:os";
9
+ import * as path from "node:path";
10
+ import { getProjectDir, Snowflake } from "@oh-my-pi/pi-utils";
11
+ import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
12
+ import chalk from "chalk";
13
+ import { Settings, settings } from "../config/settings";
14
+ import { playAudioFile, removeTempFile } from "../tts/player";
15
+ import { shutdownTtsClient, ttsClient } from "../tts/tts-client";
16
+ import { encodeWav } from "../tts/wav";
17
+
18
+ export default class Say extends Command {
19
+ static description = "Synthesize text with the local TTS engine and play it through the speakers";
20
+
21
+ static args = {
22
+ text: Args.string({ required: true, description: "Text to speak" }),
23
+ };
24
+
25
+ static flags = {
26
+ voice: Flags.string({ description: "Voice id" }),
27
+ model: Flags.string({ description: "Local TTS model key" }),
28
+ out: Flags.string({ char: "o", description: "Write WAV to this path instead of playing" }),
29
+ };
30
+
31
+ static examples = [
32
+ 'omp say "hello world"',
33
+ 'omp say "hello world" --out /tmp/hello.wav',
34
+ 'omp say "bonjour" --voice af_heart --model kokoro',
35
+ ];
36
+
37
+ async run(): Promise<void> {
38
+ const { args, flags } = await this.parse(Say);
39
+ const text = args.text ?? "";
40
+
41
+ await Settings.init({ cwd: getProjectDir() });
42
+ const model = flags.model ?? settings.get("tts.localModel");
43
+ const voice = flags.voice ?? settings.get("tts.localVoice");
44
+
45
+ let exitCode = 0;
46
+ const unsubscribe = ttsClient.onProgress(event => {
47
+ if (event.status === "progress" && typeof event.progress === "number") {
48
+ process.stderr.write(
49
+ `\r${chalk.dim(`downloading ${event.file ?? model}: ${Math.round(event.progress)}%`)}`,
50
+ );
51
+ } else if (event.status === "done" || event.status === "ready") {
52
+ // Clear the progress line once the download finishes.
53
+ process.stderr.write("\r\x1b[K");
54
+ }
55
+ });
56
+
57
+ try {
58
+ const audio = await ttsClient.synthesize(model, text, { voice });
59
+ if (!audio) {
60
+ process.stderr.write(
61
+ chalk.red(
62
+ `error: could not synthesize with local TTS model "${model}". ` +
63
+ "Run `omp setup speech` to install it.\n",
64
+ ),
65
+ );
66
+ exitCode = 1;
67
+ return;
68
+ }
69
+
70
+ const wav = encodeWav(audio.pcm, audio.sampleRate);
71
+ const durationSec = audio.pcm.length / audio.sampleRate;
72
+
73
+ if (flags.out) {
74
+ await Bun.write(flags.out, wav);
75
+ process.stdout.write(
76
+ `${chalk.green("saved")} ${flags.out} ` +
77
+ `${chalk.dim(`(${voice}, ${model}, ${durationSec.toFixed(1)}s, ${wav.byteLength} bytes)`)}\n`,
78
+ );
79
+ return;
80
+ }
81
+
82
+ const tmp = path.join(os.tmpdir(), `omp-say-${Snowflake.next()}.wav`);
83
+ await Bun.write(tmp, wav);
84
+ try {
85
+ await playAudioFile(tmp);
86
+ process.stdout.write(
87
+ `${chalk.green("spoke")} ${chalk.dim(`(${voice}, ${model}, ${durationSec.toFixed(1)}s)`)}\n`,
88
+ );
89
+ } finally {
90
+ await removeTempFile(tmp);
91
+ }
92
+ } catch (err) {
93
+ process.stderr.write(chalk.red(`error: ${err instanceof Error ? err.message : String(err)}\n`));
94
+ exitCode = 1;
95
+ } finally {
96
+ unsubscribe();
97
+ await shutdownTtsClient();
98
+ }
99
+
100
+ if (exitCode !== 0) process.exit(exitCode);
101
+ }
102
+ }
@@ -7,7 +7,7 @@ import { runSetupCommand, type SetupCommandArgs, type SetupComponent } from "../
7
7
  import { runRootCommand } from "../main";
8
8
  import { initTheme } from "../modes/theme/theme";
9
9
 
10
- const COMPONENTS: SetupComponent[] = ["python", "stt"];
10
+ const COMPONENTS: SetupComponent[] = ["python", "speech"];
11
11
 
12
12
  export interface OnboardingSetupDependencies {
13
13
  runRoot?: typeof runRootCommand;
@@ -38,6 +38,9 @@ function buildToolSession(
38
38
  return {
39
39
  cwd: options.cwd,
40
40
  hasUI: false,
41
+ // Programmatic fan-out: results feed the commit agent's evidence, not a
42
+ // model choosing further spawns, so the specialization nudge is noise here.
43
+ suppressSpawnAdvisory: true,
41
44
  getSessionFile: () => ctx.sessionManager.getSessionFile() ?? null,
42
45
  getSessionSpawns: () => options.spawns,
43
46
  settings: options.settings,
@@ -212,8 +212,8 @@ export const KEYBINDINGS = {
212
212
  description: "Search history",
213
213
  },
214
214
  "app.stt.toggle": {
215
- defaultKeys: "alt+h",
216
- description: "Toggle speech-to-text",
215
+ defaultKeys: [],
216
+ description: "Toggle speech-to-text (default gesture: hold Space)",
217
217
  },
218
218
  } as const satisfies KeybindingDefinitions;
219
219
 
@@ -393,12 +393,16 @@ export async function discoverOpenAIModelsList(
393
393
  const response = apiKey
394
394
  ? await withAuth(apiKey, key => attempt({ ...baseHeaders, Authorization: `Bearer ${key}` }))
395
395
  : await attempt(baseHeaders);
396
- const payload = (await response.json()) as { data?: Array<{ id: string }> };
396
+ const payload = (await response.json()) as {
397
+ data?: Array<{ id?: string; max_model_len?: unknown; context_length?: unknown }>;
398
+ };
397
399
  const models = payload.data ?? [];
398
400
  const discovered: Model<Api>[] = [];
399
401
  for (const item of models) {
400
402
  const id = item.id;
401
403
  if (!id) continue;
404
+ const contextWindow =
405
+ toPositiveNumberOrUndefined(item.max_model_len) ?? toPositiveNumberOrUndefined(item.context_length) ?? 128000;
402
406
  discovered.push(
403
407
  buildModel({
404
408
  id,
@@ -409,8 +413,8 @@ export async function discoverOpenAIModelsList(
409
413
  reasoning: false,
410
414
  input: ["text"],
411
415
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
412
- contextWindow: 128000,
413
- maxTokens: discoveryDefaultMaxTokens(providerConfig.api),
416
+ contextWindow,
417
+ maxTokens: Math.min(contextWindow, discoveryDefaultMaxTokens(providerConfig.api)),
414
418
  headers,
415
419
  compat: {
416
420
  supportsStore: false,
@@ -463,7 +467,7 @@ export async function discoverProxyModels(
463
467
  ? await withAuth(apiKey, key => attempt({ ...baseHeaders, Authorization: `Bearer ${key}` }))
464
468
  : await attempt(baseHeaders);
465
469
  const payload = (await response.json()) as {
466
- data?: Array<{ id?: string; name?: string; supported_endpoint_types?: string[] }>;
470
+ data?: Array<{ id?: string; name?: string; supported_endpoint_types?: string[]; context_length?: number }>;
467
471
  };
468
472
  const items = payload.data ?? [];
469
473
  const discovered: Model<Api>[] = [];
@@ -499,7 +503,9 @@ export async function discoverProxyModels(
499
503
  // upstream bundled catalogs, so keep costs local-unknown even when
500
504
  // we successfully recover the upstream model identity.
501
505
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
502
- contextWindow: reference?.contextWindow ?? 128000,
506
+ // Prefer the context_length the API reports for this model; fall
507
+ // back to the bundled reference, then a sane default.
508
+ contextWindow: toPositiveNumberOrUndefined(item.context_length) ?? reference?.contextWindow ?? 128000,
503
509
  maxTokens: reference?.maxTokens ?? discoveryDefaultMaxTokens(api),
504
510
  headers,
505
511
  // OpenAI-compat fields are no-ops on anthropic models; the
@@ -24,9 +24,11 @@ import {
24
24
  resolveVariantAlias,
25
25
  } from "@oh-my-pi/pi-catalog/variant-collapse";
26
26
 
27
- // Sentinel for local-only OAuth token (LM Studio, vLLM) — declared inline to avoid loading
28
- // any provider module at startup. Must match `DEFAULT_LOCAL_TOKEN` in oauth/lm-studio.ts.
27
+ // Sentinels for local-only OAuth tokens — declared inline to avoid loading
28
+ // provider modules at startup. Must match packages/ai/src/registry/lm-studio.ts
29
+ // and packages/ai/src/registry/vllm.ts.
29
30
  const DEFAULT_LOCAL_TOKEN = "lm-studio-local";
31
+ const DEFAULT_VLLM_LOCAL_TOKEN = "vllm-local";
30
32
 
31
33
  const SPECIAL_MODEL_MANAGER_PROVIDER_IDS: readonly string[] = [
32
34
  "google-antigravity",
@@ -82,6 +84,10 @@ export function isAuthenticated(apiKey: string | undefined | null): apiKey is st
82
84
  return Boolean(apiKey) && apiKey !== kNoAuth;
83
85
  }
84
86
 
87
+ function isDiscoveryBearerApiKey(apiKey: string | undefined | null): apiKey is string {
88
+ return isAuthenticated(apiKey) && apiKey !== DEFAULT_LOCAL_TOKEN && apiKey !== DEFAULT_VLLM_LOCAL_TOKEN;
89
+ }
90
+
85
91
  /** Provider override config (baseUrl, headers, apiKey, compat, transport) without custom models */
86
92
  interface ProviderOverride {
87
93
  baseUrl?: string;
@@ -102,9 +108,19 @@ interface ProviderOverride {
102
108
  * `token-plan-sgp.xiaomimimo.com` at discovery time)
103
109
  * 3. Existing bundled baseUrl (the host baked into `models.json`)
104
110
  *
111
+ * `transport` resolution priority:
112
+ * 1. `providerOverride.transport` (e.g. `pi-native` for auth-gateway users)
113
+ * 2. `existing.transport` (carried over from boot-time override application)
114
+ * 3. `model.transport` (rarely set — discovery defaults omit it)
115
+ *
105
116
  * Without (1), the user's override would lose to discovery; without (2)
106
117
  * preferred over (3), the bundled `api.xiaomimimo.com` would shadow the
107
118
  * tp- token-plan host and produce 401s on the first stream call.
119
+ * Without explicit transport propagation, an openrouter (or any) entry
120
+ * marked `transport: pi-native` in models.yml silently reverts to the
121
+ * default openai-completions transport after the background catalog
122
+ * refresh — so the first `/model` switch after boot hits the raw OpenAI
123
+ * chat-completions URL instead of the gateway's `/v1/pi/stream` (#2555).
108
124
  * See `xiaomi-tp-discovery-merge.test.ts` and the `refresh()` baseUrl-override
109
125
  * regression in `model-registry.test.ts`.
110
126
  */
@@ -118,6 +134,7 @@ export function mergeDiscoveredModel<TApi extends Api>(
118
134
  ...model,
119
135
  baseUrl: providerOverride?.baseUrl ?? model.baseUrl ?? existing.baseUrl,
120
136
  headers: existing.headers ? { ...existing.headers, ...model.headers } : model.headers,
137
+ transport: providerOverride?.transport ?? existing.transport ?? model.transport,
121
138
  compat: model.compatConfig,
122
139
  } as ModelSpec<TApi>);
123
140
  }
@@ -889,6 +906,18 @@ export class ModelRegistry {
889
906
  });
890
907
  }
891
908
 
909
+ #resolveStartupModelCacheProviderId(providerId: string): string {
910
+ const descriptor = PROVIDER_DESCRIPTORS.find(candidate => candidate.providerId === providerId);
911
+ if (!descriptor) {
912
+ return providerId;
913
+ }
914
+ const baseUrl =
915
+ this.#runtimeProviderOverrides.get(providerId)?.baseUrl ??
916
+ this.#providerOverrides.get(providerId)?.baseUrl ??
917
+ this.getProviderBaseUrl(providerId);
918
+ return descriptor.createModelManagerOptions({ baseUrl, fetch: this.#fetch }).cacheProviderId ?? providerId;
919
+ }
920
+
892
921
  #loadCachedStandardProviderModels(): { models: Model<Api>[]; authoritativeFreshProviders: Set<string> } {
893
922
  const configuredDiscoveryProviders = new Set(this.#discoverableProviders.map(provider => provider.provider));
894
923
  const cachedModels: Model<Api>[] = [];
@@ -897,7 +926,8 @@ export class ModelRegistry {
897
926
  if (configuredDiscoveryProviders.has(providerId)) {
898
927
  continue;
899
928
  }
900
- const cache = readModelCache<Api>(providerId, 24 * 60 * 60 * 1000, Date.now, this.#cacheDbPath);
929
+ const cacheProviderId = this.#resolveStartupModelCacheProviderId(providerId);
930
+ const cache = readModelCache<Api>(cacheProviderId, 24 * 60 * 60 * 1000, Date.now, this.#cacheDbPath);
901
931
  if (!cache) {
902
932
  continue;
903
933
  }
@@ -927,7 +957,12 @@ export class ModelRegistry {
927
957
  #loadCachedDiscoverableModels(): Model<Api>[] {
928
958
  const cachedModels: Model<Api>[] = [];
929
959
  for (const providerConfig of this.#discoverableProviders) {
930
- const cache = readModelCache<Api>(providerConfig.provider, 24 * 60 * 60 * 1000, Date.now, this.#cacheDbPath);
960
+ const cache = readModelCache<Api>(
961
+ this.#configuredDiscoveryCacheProviderId(providerConfig),
962
+ 24 * 60 * 60 * 1000,
963
+ Date.now,
964
+ this.#cacheDbPath,
965
+ );
931
966
  if (!cache) {
932
967
  this.#providerDiscoveryStates.set(providerConfig.provider, {
933
968
  provider: providerConfig.provider,
@@ -1189,11 +1224,19 @@ export class ModelRegistry {
1189
1224
  this.#rebuildCanonicalIndex();
1190
1225
  }
1191
1226
 
1227
+ #configuredDiscoveryCacheProviderId(providerConfig: DiscoveryProviderConfig): string {
1228
+ if (providerConfig.discovery.type === "openai-models-list") {
1229
+ return `${providerConfig.provider}:openai-models-list-context-v2`;
1230
+ }
1231
+ return providerConfig.provider;
1232
+ }
1233
+
1192
1234
  async #discoverProviderModels(
1193
1235
  providerConfig: DiscoveryProviderConfig,
1194
1236
  strategy: ModelRefreshStrategy,
1195
1237
  ): Promise<Model<Api>[]> {
1196
- const cached = readModelCache<Api>(providerConfig.provider, 24 * 60 * 60 * 1000, Date.now, this.#cacheDbPath);
1238
+ const cacheProviderId = this.#configuredDiscoveryCacheProviderId(providerConfig);
1239
+ const cached = readModelCache<Api>(cacheProviderId, 24 * 60 * 60 * 1000, Date.now, this.#cacheDbPath);
1197
1240
  const requiresAuth = !this.#keylessProviders.has(providerConfig.provider);
1198
1241
  if (requiresAuth) {
1199
1242
  const apiKey = await this.#peekApiKeyForProvider(providerConfig.provider);
@@ -1231,6 +1274,7 @@ export class ModelRegistry {
1231
1274
  providerId,
1232
1275
  staticModels: [],
1233
1276
  cacheDbPath: this.#cacheDbPath,
1277
+ cacheProviderId,
1234
1278
  cacheTtlMs: 24 * 60 * 60 * 1000,
1235
1279
  fetchDynamicModels,
1236
1280
  });
@@ -1272,7 +1316,9 @@ export class ModelRegistry {
1272
1316
  fetch: this.#fetch,
1273
1317
  getBearerApiKeyResolver: async provider => {
1274
1318
  const apiKey = await this.getApiKeyForProvider(provider);
1275
- if (!apiKey || apiKey === DEFAULT_LOCAL_TOKEN || apiKey === kNoAuth) return undefined;
1319
+ if (!isDiscoveryBearerApiKey(apiKey)) {
1320
+ return undefined;
1321
+ }
1276
1322
  return this.resolver(provider);
1277
1323
  },
1278
1324
  };
@@ -1377,11 +1423,20 @@ export class ModelRegistry {
1377
1423
  for (let i = 0; i < standardProviderDescriptors.length; i++) {
1378
1424
  const descriptor = standardProviderDescriptors[i];
1379
1425
  const apiKey = standardProviderKeys[i];
1380
- if (isAuthenticated(apiKey) || descriptor.allowUnauthenticated) {
1426
+ const hasExplicitVllmConfig =
1427
+ descriptor.providerId === "vllm" &&
1428
+ (this.#runtimeProviderOverrides.has(descriptor.providerId) ||
1429
+ this.#providerOverrides.has(descriptor.providerId) ||
1430
+ this.#keylessProviders.has(descriptor.providerId));
1431
+ if (isAuthenticated(apiKey) || descriptor.allowUnauthenticated || hasExplicitVllmConfig) {
1432
+ const discoveryBaseUrl =
1433
+ this.#runtimeProviderOverrides.get(descriptor.providerId)?.baseUrl ??
1434
+ this.#providerOverrides.get(descriptor.providerId)?.baseUrl ??
1435
+ this.getProviderBaseUrl(descriptor.providerId);
1381
1436
  options.push(
1382
1437
  descriptor.createModelManagerOptions({
1383
- apiKey: isAuthenticated(apiKey) ? apiKey : undefined,
1384
- baseUrl: this.getProviderBaseUrl(descriptor.providerId),
1438
+ apiKey: isDiscoveryBearerApiKey(apiKey) ? apiKey : undefined,
1439
+ baseUrl: discoveryBaseUrl,
1385
1440
  fetch: this.#fetch,
1386
1441
  }),
1387
1442
  );
@@ -35,6 +35,7 @@ const OpenAICompatFieldsSchema = z.object({
35
35
  allowsSyntheticReasoningContentForToolCalls: z.boolean().optional(),
36
36
  requiresAssistantContentForToolCalls: z.boolean().optional(),
37
37
  supportsToolChoice: z.boolean().optional(),
38
+ supportsForcedToolChoice: z.boolean().optional(),
38
39
  disableReasoningOnForcedToolChoice: z.boolean().optional(),
39
40
  disableReasoningOnToolChoice: z.boolean().optional(),
40
41
  thinkingFormat: z.enum(["openai", "openrouter", "zai", "qwen", "qwen-chat-template"]).optional(),
@@ -44,7 +45,7 @@ const OpenAICompatFieldsSchema = z.object({
44
45
  cacheControlFormat: z.enum(["anthropic"]).optional(),
45
46
  supportsStrictMode: z.boolean().optional(),
46
47
  toolStrictMode: z.enum(["all_strict", "none"]).optional(),
47
- streamIdleTimeoutMs: z.number().positive().optional(),
48
+ streamIdleTimeoutMs: z.number().nonnegative().optional(),
48
49
  supportsLongPromptCacheRetention: z.boolean().optional(),
49
50
  supportsReasoningParams: z.boolean().optional(),
50
51
  alwaysSendMaxTokens: z.boolean().optional(),
@@ -124,6 +125,7 @@ const ModelDefinitionSchema = z.object({
124
125
  "azure-openai-responses",
125
126
  "anthropic-messages",
126
127
  "google-generative-ai",
128
+ "google-gemini-cli",
127
129
  "google-vertex",
128
130
  ])
129
131
  .optional(),
@@ -192,6 +194,7 @@ const ProviderConfigSchema = z.object({
192
194
  "azure-openai-responses",
193
195
  "anthropic-messages",
194
196
  "google-generative-ai",
197
+ "google-gemini-cli",
195
198
  "google-vertex",
196
199
  ])
197
200
  .optional(),
@@ -50,12 +50,13 @@ export function validateProviderConfiguration(
50
50
  !config.headers &&
51
51
  !config.compat &&
52
52
  !config.apiKey &&
53
+ config.auth !== "none" &&
53
54
  !config.disableStrictTools &&
54
55
  !hasModelOverrides &&
55
56
  !config.discovery
56
57
  ) {
57
58
  throw new Error(
58
- `Provider ${providerName}: must specify "baseUrl", "headers", "apiKey", "compat", "disableStrictTools", "modelOverrides", "discovery", or "models"`,
59
+ `Provider ${providerName}: must specify "baseUrl", "headers", "apiKey", "auth: none", "compat", "disableStrictTools", "modelOverrides", "discovery", or "models"`,
59
60
  );
60
61
  }
61
62
  }