@oh-my-pi/pi-coding-agent 15.10.10 → 15.10.12

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 (415) hide show
  1. package/CHANGELOG.md +142 -7
  2. package/dist/cli.js +23108 -0
  3. package/dist/tokenizers.linux-x64-gnu-xcjh3jwk.node +0 -0
  4. package/dist/types/async/job-manager.d.ts +18 -0
  5. package/dist/types/cli/args.d.ts +2 -1
  6. package/dist/types/cli/dry-balance-cli.d.ts +1 -1
  7. package/dist/types/cli/gallery-cli.d.ts +1 -1
  8. package/dist/types/cli/gallery-fixtures/types.d.ts +1 -1
  9. package/dist/types/cli/usage-cli.d.ts +72 -0
  10. package/dist/types/cli-commands.d.ts +12 -0
  11. package/dist/types/commands/launch.d.ts +5 -1
  12. package/dist/types/commands/read.d.ts +1 -1
  13. package/dist/types/commands/usage.d.ts +25 -0
  14. package/dist/types/config/api-key-resolver.d.ts +3 -0
  15. package/dist/types/config/append-only-context-mode.d.ts +2 -1
  16. package/dist/types/config/model-discovery.d.ts +55 -0
  17. package/dist/types/config/model-registry.d.ts +8 -219
  18. package/dist/types/config/model-resolver.d.ts +34 -10
  19. package/dist/types/config/model-roles.d.ts +28 -0
  20. package/dist/types/config/models-config-schema.d.ts +523 -42
  21. package/dist/types/config/models-config.d.ts +385 -0
  22. package/dist/types/config/settings-schema.d.ts +41 -8
  23. package/dist/types/config/settings.d.ts +8 -1
  24. package/dist/types/debug/log-viewer.d.ts +1 -1
  25. package/dist/types/debug/raw-sse.d.ts +1 -1
  26. package/dist/types/edit/hashline/noop-loop-guard.d.ts +72 -0
  27. package/dist/types/eval/backend.d.ts +0 -2
  28. package/dist/types/eval/idle-timeout.d.ts +0 -4
  29. package/dist/types/eval/js/shared/rewrite-imports.d.ts +6 -6
  30. package/dist/types/eval/py/executor.d.ts +5 -0
  31. package/dist/types/eval/py/kernel.d.ts +6 -1
  32. package/dist/types/eval/py/runtime.d.ts +9 -0
  33. package/dist/types/exec/bash-executor.d.ts +2 -0
  34. package/dist/types/export/html/template.generated.d.ts +1 -1
  35. package/dist/types/extensibility/extensions/runner.d.ts +3 -2
  36. package/dist/types/extensibility/extensions/types.d.ts +6 -3
  37. package/dist/types/hindsight/mental-models.d.ts +17 -8
  38. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
  39. package/dist/types/internal-urls/types.d.ts +1 -1
  40. package/dist/types/lsp/edits.d.ts +9 -0
  41. package/dist/types/lsp/index.d.ts +2 -2
  42. package/dist/types/lsp/types.d.ts +2 -0
  43. package/dist/types/lsp/utils.d.ts +3 -0
  44. package/dist/types/mcp/json-rpc.d.ts +5 -0
  45. package/dist/types/memory-backend/index.d.ts +1 -0
  46. package/dist/types/memory-backend/runtime.d.ts +4 -0
  47. package/dist/types/memory-backend/types.d.ts +66 -1
  48. package/dist/types/mnemopi/state.d.ts +11 -1
  49. package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
  50. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  51. package/dist/types/modes/components/bash-execution.d.ts +1 -1
  52. package/dist/types/modes/components/copy-selector.d.ts +1 -1
  53. package/dist/types/modes/components/dynamic-border.d.ts +1 -1
  54. package/dist/types/modes/components/extensions/extension-dashboard.d.ts +1 -1
  55. package/dist/types/modes/components/extensions/extension-list.d.ts +1 -1
  56. package/dist/types/modes/components/extensions/inspector-panel.d.ts +1 -1
  57. package/dist/types/modes/components/footer.d.ts +1 -1
  58. package/dist/types/modes/components/hook-editor.d.ts +5 -0
  59. package/dist/types/modes/components/hook-input.d.ts +4 -0
  60. package/dist/types/modes/components/hook-selector.d.ts +1 -1
  61. package/dist/types/modes/components/model-selector.d.ts +1 -1
  62. package/dist/types/modes/components/plan-review-overlay.d.ts +1 -1
  63. package/dist/types/modes/components/session-observer-overlay.d.ts +1 -1
  64. package/dist/types/modes/components/session-selector.d.ts +1 -1
  65. package/dist/types/modes/components/status-line/component.d.ts +1 -1
  66. package/dist/types/modes/components/tiny-title-download-progress.d.ts +1 -1
  67. package/dist/types/modes/components/transcript-container.d.ts +25 -6
  68. package/dist/types/modes/components/tree-selector.d.ts +1 -1
  69. package/dist/types/modes/components/user-message-selector.d.ts +1 -1
  70. package/dist/types/modes/components/user-message.d.ts +2 -1
  71. package/dist/types/modes/components/visual-truncate.d.ts +1 -1
  72. package/dist/types/modes/components/welcome.d.ts +19 -3
  73. package/dist/types/modes/controllers/mcp-command-controller.d.ts +1 -1
  74. package/dist/types/modes/controllers/streaming-reveal.d.ts +1 -1
  75. package/dist/types/modes/index.d.ts +3 -3
  76. package/dist/types/modes/interactive-mode.d.ts +8 -3
  77. package/dist/types/modes/oauth-manual-input.d.ts +7 -0
  78. package/dist/types/modes/rpc/rpc-client.d.ts +39 -2
  79. package/dist/types/modes/rpc/rpc-mode.d.ts +31 -2
  80. package/dist/types/modes/rpc/rpc-subagents.d.ts +24 -0
  81. package/dist/types/modes/rpc/rpc-types.d.ts +75 -1
  82. package/dist/types/modes/setup-wizard/index.d.ts +5 -1
  83. package/dist/types/modes/setup-wizard/lazy.d.ts +2 -0
  84. package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
  85. package/dist/types/modes/setup-wizard/scenes/types.d.ts +1 -1
  86. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
  87. package/dist/types/modes/setup-wizard/wizard-overlay.d.ts +1 -1
  88. package/dist/types/modes/types.d.ts +4 -1
  89. package/dist/types/secrets/index.d.ts +1 -1
  90. package/dist/types/secrets/obfuscator.d.ts +8 -2
  91. package/dist/types/session/agent-session.d.ts +15 -3
  92. package/dist/types/session/auth-broker-config.d.ts +4 -0
  93. package/dist/types/session/session-manager.d.ts +1 -1
  94. package/dist/types/session/streaming-output.d.ts +23 -0
  95. package/dist/types/slash-commands/acp-builtins.d.ts +16 -0
  96. package/dist/types/slash-commands/builtin-registry.d.ts +1 -0
  97. package/dist/types/slash-commands/helpers/stats-dashboard.d.ts +13 -0
  98. package/dist/types/slash-commands/types.d.ts +1 -1
  99. package/dist/types/ssh/connection-manager.d.ts +8 -0
  100. package/dist/types/system-prompt.d.ts +2 -0
  101. package/dist/types/task/executor.d.ts +1 -0
  102. package/dist/types/task/index.d.ts +2 -2
  103. package/dist/types/task/parallel.d.ts +2 -2
  104. package/dist/types/task/types.d.ts +8 -0
  105. package/dist/types/task/worktree.d.ts +2 -0
  106. package/dist/types/thinking.d.ts +4 -0
  107. package/dist/types/tiny/title-client.d.ts +11 -0
  108. package/dist/types/tiny/title-protocol.d.ts +1 -0
  109. package/dist/types/tools/ask.d.ts +4 -0
  110. package/dist/types/tools/conflict-detect.d.ts +16 -0
  111. package/dist/types/tools/github-cache.d.ts +7 -0
  112. package/dist/types/tools/index.d.ts +6 -0
  113. package/dist/types/tools/sqlite-reader.d.ts +3 -0
  114. package/dist/types/tui/output-block.d.ts +3 -3
  115. package/dist/types/utils/changelog.d.ts +8 -0
  116. package/dist/types/utils/git.d.ts +15 -2
  117. package/dist/types/utils/title-generator.d.ts +3 -2
  118. package/dist/types/web/scrapers/readthedocs.d.ts +3 -0
  119. package/dist/types/web/scrapers/types.d.ts +12 -0
  120. package/dist/types/web/search/providers/codex.d.ts +1 -1
  121. package/dist/types/web/search/providers/gemini.d.ts +1 -1
  122. package/examples/extensions/tools.ts +5 -4
  123. package/package.json +14 -11
  124. package/scripts/build-binary.ts +18 -23
  125. package/scripts/bundle-dist.ts +81 -0
  126. package/scripts/{dev-launch → omp} +1 -1
  127. package/scripts/{dev-launch-preload.ts → omp.ts} +1 -1
  128. package/src/async/job-manager.ts +57 -3
  129. package/src/auto-thinking/classifier.ts +1 -0
  130. package/src/autoresearch/dashboard.ts +1 -1
  131. package/src/autoresearch/prompt-setup.md +6 -6
  132. package/src/autoresearch/prompt.md +6 -6
  133. package/src/capability/fs.ts +10 -0
  134. package/src/cli/args.ts +4 -1
  135. package/src/cli/auth-gateway-cli.ts +1 -3
  136. package/src/cli/dry-balance-cli.ts +1 -1
  137. package/src/cli/gallery-cli.ts +1 -1
  138. package/src/cli/gallery-fixtures/fs.ts +1 -1
  139. package/src/cli/gallery-fixtures/types.ts +5 -1
  140. package/src/cli/list-models.ts +2 -1
  141. package/src/cli/usage-cli.ts +603 -0
  142. package/src/cli-commands.ts +30 -0
  143. package/src/cli.ts +76 -13
  144. package/src/commands/complete.ts +1 -1
  145. package/src/commands/launch.ts +5 -1
  146. package/src/commands/read.ts +6 -3
  147. package/src/commands/usage.ts +35 -0
  148. package/src/commit/agentic/agent.ts +1 -1
  149. package/src/commit/model-selection.ts +4 -3
  150. package/src/config/api-key-resolver.ts +8 -6
  151. package/src/config/append-only-context-mode.ts +6 -12
  152. package/src/config/model-discovery.ts +554 -0
  153. package/src/config/model-registry.ts +320 -1041
  154. package/src/config/model-resolver.ts +173 -156
  155. package/src/config/model-roles.ts +74 -0
  156. package/src/config/models-config-schema.ts +57 -8
  157. package/src/config/models-config.ts +129 -0
  158. package/src/config/settings-schema.ts +61 -19
  159. package/src/config/settings.ts +98 -4
  160. package/src/dap/client.ts +124 -37
  161. package/src/dap/session.ts +259 -158
  162. package/src/debug/log-viewer.ts +1 -1
  163. package/src/debug/raw-sse.ts +1 -1
  164. package/src/edit/diff.ts +47 -3
  165. package/src/edit/hashline/block-resolver.ts +20 -1
  166. package/src/edit/hashline/diff.ts +36 -1
  167. package/src/edit/hashline/execute.ts +47 -4
  168. package/src/edit/hashline/noop-loop-guard.ts +99 -0
  169. package/src/edit/index.ts +16 -1
  170. package/src/edit/modes/patch.ts +52 -0
  171. package/src/edit/modes/replace.ts +56 -22
  172. package/src/edit/notebook.ts +22 -2
  173. package/src/edit/renderer.ts +36 -10
  174. package/src/eval/__tests__/completion-bridge.test.ts +1 -1
  175. package/src/eval/backend.ts +0 -2
  176. package/src/eval/completion-bridge.ts +3 -1
  177. package/src/eval/idle-timeout.ts +2 -9
  178. package/src/eval/js/context-manager.ts +6 -8
  179. package/src/eval/js/executor.ts +6 -2
  180. package/src/eval/js/index.ts +0 -2
  181. package/src/eval/js/shared/helpers.ts +5 -6
  182. package/src/eval/js/shared/local-module-loader.ts +1 -1
  183. package/src/eval/js/shared/prelude.txt +62 -1
  184. package/src/eval/js/shared/rewrite-imports.ts +40 -22
  185. package/src/eval/js/shared/runtime.ts +1 -1
  186. package/src/eval/py/executor.ts +29 -7
  187. package/src/eval/py/index.ts +6 -3
  188. package/src/eval/py/kernel.ts +43 -4
  189. package/src/eval/py/runner.py +107 -3
  190. package/src/eval/py/runtime.ts +37 -0
  191. package/src/exec/bash-executor.ts +85 -4
  192. package/src/export/html/template.generated.ts +1 -1
  193. package/src/export/html/template.js +3 -1
  194. package/src/extensibility/extensions/get-commands-handler.ts +2 -1
  195. package/src/extensibility/extensions/runner.ts +6 -1
  196. package/src/extensibility/extensions/types.ts +6 -2
  197. package/src/extensibility/plugins/legacy-pi-compat.ts +20 -3
  198. package/src/hindsight/bank.ts +17 -2
  199. package/src/hindsight/mental-models.ts +59 -12
  200. package/src/hindsight/state.ts +6 -1
  201. package/src/internal-urls/artifact-protocol.ts +11 -2
  202. package/src/internal-urls/docs-index.generated.ts +11 -11
  203. package/src/internal-urls/issue-pr-protocol.ts +12 -5
  204. package/src/internal-urls/router.ts +1 -1
  205. package/src/internal-urls/types.ts +1 -1
  206. package/src/lib/xai-http.ts +1 -1
  207. package/src/lsp/client.ts +118 -38
  208. package/src/lsp/clients/biome-client.ts +101 -39
  209. package/src/lsp/edits.ts +143 -95
  210. package/src/lsp/index.ts +31 -22
  211. package/src/lsp/render.ts +1 -1
  212. package/src/lsp/types.ts +2 -0
  213. package/src/lsp/utils.ts +28 -10
  214. package/src/main.ts +183 -23
  215. package/src/mcp/json-rpc.ts +35 -5
  216. package/src/mcp/transports/stdio.ts +7 -1
  217. package/src/memories/index.ts +4 -1
  218. package/src/memory-backend/index.ts +1 -0
  219. package/src/memory-backend/local-backend.ts +9 -0
  220. package/src/memory-backend/off-backend.ts +9 -0
  221. package/src/memory-backend/runtime.ts +66 -0
  222. package/src/memory-backend/types.ts +81 -1
  223. package/src/mnemopi/backend.ts +176 -7
  224. package/src/mnemopi/state.ts +38 -2
  225. package/src/modes/acp/acp-agent.ts +119 -11
  226. package/src/modes/components/agent-dashboard.ts +10 -7
  227. package/src/modes/components/assistant-message.ts +32 -28
  228. package/src/modes/components/bash-execution.ts +1 -1
  229. package/src/modes/components/copy-selector.ts +1 -1
  230. package/src/modes/components/diff.ts +13 -2
  231. package/src/modes/components/dynamic-border.ts +12 -3
  232. package/src/modes/components/extensions/extension-dashboard.ts +8 -5
  233. package/src/modes/components/extensions/extension-list.ts +1 -1
  234. package/src/modes/components/extensions/inspector-panel.ts +1 -1
  235. package/src/modes/components/footer.ts +4 -2
  236. package/src/modes/components/history-search.ts +1 -1
  237. package/src/modes/components/hook-editor.ts +8 -0
  238. package/src/modes/components/hook-input.ts +8 -0
  239. package/src/modes/components/hook-selector.ts +2 -2
  240. package/src/modes/components/model-selector.ts +4 -2
  241. package/src/modes/components/plan-review-overlay.ts +1 -1
  242. package/src/modes/components/session-observer-overlay.ts +2 -2
  243. package/src/modes/components/session-selector.ts +1 -1
  244. package/src/modes/components/settings-selector.ts +5 -1
  245. package/src/modes/components/status-line/component.ts +119 -35
  246. package/src/modes/components/tiny-title-download-progress.ts +1 -1
  247. package/src/modes/components/transcript-container.ts +258 -53
  248. package/src/modes/components/tree-selector.ts +3 -3
  249. package/src/modes/components/user-message-selector.ts +1 -1
  250. package/src/modes/components/user-message.ts +17 -5
  251. package/src/modes/components/visual-truncate.ts +1 -1
  252. package/src/modes/components/welcome.ts +108 -26
  253. package/src/modes/controllers/command-controller.ts +11 -4
  254. package/src/modes/controllers/event-controller.ts +73 -4
  255. package/src/modes/controllers/input-controller.ts +2 -1
  256. package/src/modes/controllers/mcp-command-controller.ts +39 -4
  257. package/src/modes/controllers/selector-controller.ts +1 -1
  258. package/src/modes/controllers/streaming-reveal.ts +85 -18
  259. package/src/modes/index.ts +3 -21
  260. package/src/modes/interactive-mode.ts +42 -18
  261. package/src/modes/oauth-manual-input.ts +30 -3
  262. package/src/modes/rpc/rpc-client.ts +154 -3
  263. package/src/modes/rpc/rpc-mode.ts +97 -12
  264. package/src/modes/rpc/rpc-subagents.ts +265 -0
  265. package/src/modes/rpc/rpc-types.ts +81 -1
  266. package/src/modes/setup-wizard/index.ts +12 -2
  267. package/src/modes/setup-wizard/lazy.ts +16 -0
  268. package/src/modes/setup-wizard/scenes/glyph.ts +1 -1
  269. package/src/modes/setup-wizard/scenes/providers.ts +1 -1
  270. package/src/modes/setup-wizard/scenes/sign-in.ts +1 -1
  271. package/src/modes/setup-wizard/scenes/theme.ts +1 -1
  272. package/src/modes/setup-wizard/scenes/types.ts +1 -1
  273. package/src/modes/setup-wizard/scenes/web-search.ts +1 -1
  274. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  275. package/src/modes/types.ts +4 -1
  276. package/src/prompts/agents/explore.md +2 -2
  277. package/src/prompts/agents/librarian.md +1 -2
  278. package/src/prompts/agents/oracle.md +1 -1
  279. package/src/prompts/agents/plan.md +5 -5
  280. package/src/prompts/agents/task.md +5 -5
  281. package/src/prompts/ci-green-request.md +5 -7
  282. package/src/prompts/goals/goal-budget-limit.md +2 -2
  283. package/src/prompts/goals/goal-continuation.md +4 -4
  284. package/src/prompts/goals/goal-mode-active.md +1 -1
  285. package/src/prompts/memories/read-path.md +1 -1
  286. package/src/prompts/memories/stage_one_system.md +2 -2
  287. package/src/prompts/review-custom-request.md +1 -1
  288. package/src/prompts/system/agent-creation-architect.md +2 -2
  289. package/src/prompts/system/auto-continue.md +1 -1
  290. package/src/prompts/system/background-tan-dispatch.md +1 -1
  291. package/src/prompts/system/btw-user.md +2 -2
  292. package/src/prompts/system/commit-message-system.md +13 -1
  293. package/src/prompts/system/custom-system-prompt.md +1 -1
  294. package/src/prompts/system/eager-todo.md +2 -2
  295. package/src/prompts/system/irc-incoming.md +1 -1
  296. package/src/prompts/system/manual-continue.md +1 -1
  297. package/src/prompts/system/omfg-user.md +3 -4
  298. package/src/prompts/system/orchestrate-notice.md +9 -9
  299. package/src/prompts/system/plan-mode-active.md +4 -4
  300. package/src/prompts/system/plan-mode-subagent.md +4 -5
  301. package/src/prompts/system/plan-mode-tool-decision-reminder.md +1 -1
  302. package/src/prompts/system/project-prompt.md +2 -2
  303. package/src/prompts/system/subagent-system-prompt.md +4 -4
  304. package/src/prompts/system/system-prompt.md +13 -24
  305. package/src/prompts/system/title-system.md +2 -2
  306. package/src/prompts/system/ttsr-tool-reminder.md +1 -1
  307. package/src/prompts/system/workflow-notice.md +1 -1
  308. package/src/prompts/tools/ast-edit.md +1 -1
  309. package/src/prompts/tools/ast-grep.md +2 -2
  310. package/src/prompts/tools/bash.md +5 -7
  311. package/src/prompts/tools/browser.md +7 -7
  312. package/src/prompts/tools/debug.md +1 -1
  313. package/src/prompts/tools/eval.md +3 -3
  314. package/src/prompts/tools/find.md +0 -1
  315. package/src/prompts/tools/github.md +8 -7
  316. package/src/prompts/tools/goal.md +1 -1
  317. package/src/prompts/tools/image-gen.md +1 -1
  318. package/src/prompts/tools/inspect-image-system.md +1 -1
  319. package/src/prompts/tools/irc.md +15 -15
  320. package/src/prompts/tools/lsp.md +2 -2
  321. package/src/prompts/tools/patch.md +2 -2
  322. package/src/prompts/tools/read.md +3 -4
  323. package/src/prompts/tools/recall.md +1 -1
  324. package/src/prompts/tools/reflect.md +1 -1
  325. package/src/prompts/tools/render-mermaid.md +2 -2
  326. package/src/prompts/tools/replace.md +4 -10
  327. package/src/prompts/tools/rewind.md +2 -2
  328. package/src/prompts/tools/search-tool-bm25.md +1 -9
  329. package/src/prompts/tools/search.md +0 -1
  330. package/src/prompts/tools/ssh.md +0 -4
  331. package/src/prompts/tools/task.md +2 -3
  332. package/src/prompts/tools/todo.md +1 -1
  333. package/src/sdk.ts +31 -11
  334. package/src/secrets/index.ts +8 -1
  335. package/src/secrets/obfuscator.ts +39 -18
  336. package/src/session/agent-session.ts +223 -64
  337. package/src/session/auth-broker-config.ts +30 -1
  338. package/src/session/session-manager.ts +2 -2
  339. package/src/session/streaming-output.ts +188 -11
  340. package/src/slash-commands/acp-builtins.ts +24 -0
  341. package/src/slash-commands/builtin-registry.ts +40 -0
  342. package/src/slash-commands/helpers/stats-dashboard.ts +85 -0
  343. package/src/slash-commands/types.ts +1 -1
  344. package/src/ssh/connection-manager.ts +27 -0
  345. package/src/system-prompt.ts +14 -0
  346. package/src/task/commands.ts +2 -1
  347. package/src/task/executor.ts +74 -65
  348. package/src/task/index.ts +146 -68
  349. package/src/task/parallel.ts +3 -3
  350. package/src/task/render.ts +20 -5
  351. package/src/task/types.ts +9 -0
  352. package/src/task/worktree.ts +64 -56
  353. package/src/thinking.ts +9 -1
  354. package/src/tiny/title-client.ts +60 -16
  355. package/src/tiny/title-protocol.ts +1 -1
  356. package/src/tiny/worker.ts +6 -4
  357. package/src/tools/archive-reader.ts +30 -2
  358. package/src/tools/ask.ts +104 -21
  359. package/src/tools/ast-edit.ts +25 -5
  360. package/src/tools/auto-generated-guard.ts +20 -3
  361. package/src/tools/bash-interactive.ts +27 -7
  362. package/src/tools/bash.ts +100 -18
  363. package/src/tools/browser/launch.ts +11 -2
  364. package/src/tools/browser/readable.ts +19 -2
  365. package/src/tools/browser/registry.ts +4 -1
  366. package/src/tools/browser/render.ts +2 -2
  367. package/src/tools/browser/tab-supervisor.ts +55 -16
  368. package/src/tools/conflict-detect.ts +50 -4
  369. package/src/tools/debug.ts +1 -1
  370. package/src/tools/eval-render.ts +5 -5
  371. package/src/tools/eval.ts +0 -2
  372. package/src/tools/fetch.ts +33 -10
  373. package/src/tools/gh-cache-invalidation.ts +63 -8
  374. package/src/tools/gh-renderer.ts +1 -1
  375. package/src/tools/gh.ts +172 -29
  376. package/src/tools/github-cache.ts +70 -6
  377. package/src/tools/image-gen.ts +14 -13
  378. package/src/tools/index.ts +13 -1
  379. package/src/tools/inspect-image.ts +1 -0
  380. package/src/tools/irc.ts +5 -1
  381. package/src/tools/job.ts +1 -1
  382. package/src/tools/read.ts +202 -61
  383. package/src/tools/render-utils.ts +3 -3
  384. package/src/tools/resolve.ts +1 -1
  385. package/src/tools/search.ts +92 -29
  386. package/src/tools/sqlite-reader.ts +17 -5
  387. package/src/tools/ssh.ts +8 -8
  388. package/src/tools/todo.ts +38 -8
  389. package/src/tools/write.ts +118 -18
  390. package/src/tui/output-block.ts +4 -4
  391. package/src/utils/changelog.ts +27 -1
  392. package/src/utils/commit-message-generator.ts +1 -0
  393. package/src/utils/file-mentions.ts +2 -1
  394. package/src/utils/git.ts +267 -13
  395. package/src/utils/title-generator.ts +24 -5
  396. package/src/web/scrapers/arxiv.ts +1 -1
  397. package/src/web/scrapers/go-pkg.ts +1 -1
  398. package/src/web/scrapers/iacr.ts +1 -1
  399. package/src/web/scrapers/readthedocs.ts +1 -1
  400. package/src/web/scrapers/twitter.ts +2 -1
  401. package/src/web/scrapers/types.ts +87 -8
  402. package/src/web/scrapers/wikipedia.ts +1 -1
  403. package/src/web/scrapers/youtube.ts +6 -1
  404. package/src/web/search/index.ts +1 -1
  405. package/src/web/search/providers/codex.ts +2 -1
  406. package/src/web/search/providers/gemini.ts +2 -3
  407. package/src/web/search/render.ts +8 -6
  408. package/dist/types/config/model-equivalence.d.ts +0 -24
  409. package/dist/types/config/model-id-affixes.d.ts +0 -12
  410. package/dist/types/config/model-provider-priority.d.ts +0 -1
  411. package/dist/types/exec/idle-timeout-watchdog.d.ts +0 -18
  412. package/src/config/model-equivalence.ts +0 -875
  413. package/src/config/model-id-affixes.ts +0 -81
  414. package/src/config/model-provider-priority.ts +0 -56
  415. package/src/exec/idle-timeout-watchdog.ts +0 -126
@@ -0,0 +1,129 @@
1
+ /**
2
+ * models.json config file handle and provider configuration validation.
3
+ */
4
+
5
+ import type { Api, ModelSpec } from "@oh-my-pi/pi-ai/types";
6
+ import { ConfigFile } from "./config-file";
7
+ import {
8
+ type ModelsConfig,
9
+ ModelsConfigSchema,
10
+ type ProviderAuthMode,
11
+ type ProviderDiscovery,
12
+ } from "./models-config-schema";
13
+
14
+ export type ProviderValidationMode = "models-config" | "runtime-register";
15
+
16
+ export interface ProviderValidationModel {
17
+ id: string;
18
+ api?: Api;
19
+ contextWindow?: number;
20
+ maxTokens?: number;
21
+ }
22
+
23
+ export interface ProviderValidationConfig {
24
+ baseUrl?: string;
25
+ headers?: Record<string, string>;
26
+ apiKey?: string;
27
+ api?: Api;
28
+ auth?: ProviderAuthMode;
29
+ oauthConfigured?: boolean;
30
+ discovery?: ProviderDiscovery;
31
+ compat?: ModelSpec<Api>["compat"];
32
+ disableStrictTools?: boolean;
33
+ modelOverrides?: Record<string, unknown>;
34
+ models: ProviderValidationModel[];
35
+ }
36
+
37
+ export function validateProviderConfiguration(
38
+ providerName: string,
39
+ config: ProviderValidationConfig,
40
+ mode: ProviderValidationMode,
41
+ ): void {
42
+ const hasProviderApi = !!config.api;
43
+ const models = config.models;
44
+
45
+ if (models.length === 0) {
46
+ if (mode === "models-config") {
47
+ const hasModelOverrides = config.modelOverrides && Object.keys(config.modelOverrides).length > 0;
48
+ if (
49
+ !config.baseUrl &&
50
+ !config.headers &&
51
+ !config.compat &&
52
+ !config.apiKey &&
53
+ !config.disableStrictTools &&
54
+ !hasModelOverrides &&
55
+ !config.discovery
56
+ ) {
57
+ throw new Error(
58
+ `Provider ${providerName}: must specify "baseUrl", "headers", "apiKey", "compat", "disableStrictTools", "modelOverrides", "discovery", or "models"`,
59
+ );
60
+ }
61
+ }
62
+ } else {
63
+ if (!config.baseUrl) {
64
+ throw new Error(`Provider ${providerName}: "baseUrl" is required when defining custom models.`);
65
+ }
66
+ const requiresAuth =
67
+ mode === "runtime-register"
68
+ ? !config.apiKey && !config.oauthConfigured
69
+ : !config.apiKey && (config.auth ?? "apiKey") !== "none";
70
+ if (requiresAuth) {
71
+ throw new Error(
72
+ mode === "runtime-register"
73
+ ? `Provider ${providerName}: "apiKey" or "oauth" is required when defining models.`
74
+ : `Provider ${providerName}: "apiKey" is required when defining custom models unless auth is "none".`,
75
+ );
76
+ }
77
+ }
78
+
79
+ if (mode === "models-config" && config.discovery && !config.api && config.discovery.type !== "proxy") {
80
+ throw new Error(`Provider ${providerName}: "api" is required when discovery is enabled at provider level.`);
81
+ }
82
+
83
+ for (const modelDef of models) {
84
+ if (!hasProviderApi && !modelDef.api) {
85
+ throw new Error(
86
+ mode === "runtime-register"
87
+ ? `Provider ${providerName}, model ${modelDef.id}: no "api" specified.`
88
+ : `Provider ${providerName}, model ${modelDef.id}: no "api" specified. Set at provider or model level.`,
89
+ );
90
+ }
91
+ if (!modelDef.id) {
92
+ throw new Error(`Provider ${providerName}: model missing "id"`);
93
+ }
94
+ if (mode === "models-config") {
95
+ if (modelDef.contextWindow !== undefined && modelDef.contextWindow <= 0) {
96
+ throw new Error(`Provider ${providerName}, model ${modelDef.id}: invalid contextWindow`);
97
+ }
98
+ if (modelDef.maxTokens !== undefined && modelDef.maxTokens <= 0) {
99
+ throw new Error(`Provider ${providerName}, model ${modelDef.id}: invalid maxTokens`);
100
+ }
101
+ }
102
+ }
103
+ }
104
+
105
+ export const ModelsConfigFile = new ConfigFile<ModelsConfig>("models", ModelsConfigSchema).withValidation(
106
+ "models",
107
+ config => {
108
+ const providers = config.providers ?? {};
109
+ for (const providerName in providers) {
110
+ const providerConfig = providers[providerName];
111
+ validateProviderConfiguration(
112
+ providerName,
113
+ {
114
+ baseUrl: providerConfig.baseUrl,
115
+ headers: providerConfig.headers,
116
+ apiKey: providerConfig.apiKey,
117
+ api: providerConfig.api as Api | undefined,
118
+ auth: (providerConfig.auth ?? "apiKey") as ProviderAuthMode,
119
+ discovery: providerConfig.discovery as ProviderDiscovery | undefined,
120
+ compat: providerConfig.compat,
121
+ disableStrictTools: providerConfig.disableStrictTools,
122
+ modelOverrides: providerConfig.modelOverrides,
123
+ models: (providerConfig.models ?? []) as ProviderValidationModel[],
124
+ },
125
+ "models-config",
126
+ );
127
+ }
128
+ },
129
+ );
@@ -151,7 +151,7 @@ export type AnyUiMetadata = UiBase & {
151
151
 
152
152
  interface BooleanDef {
153
153
  type: "boolean";
154
- default: boolean;
154
+ default: boolean | undefined;
155
155
  ui?: UiBoolean;
156
156
  }
157
157
 
@@ -246,7 +246,11 @@ export const DEFAULT_BASH_INTERCEPTOR_RULES: BashInterceptorRule[] = [
246
246
  message: "Use the `edit` tool instead of awk -i inplace. It provides diff preview and fuzzy matching.",
247
247
  },
248
248
  {
249
- pattern: "^\\s*(echo|printf|cat\\s*<<)\\s+.*[^|]>\\s*\\S",
249
+ // `>` must sit outside quoted regions (so `echo "a -> b"` passes) and be
250
+ // followed by a plausible filename — including `$VAR` targets; `>|`
251
+ // (clobber) counts as a redirect; `>&2`/`2>&1` style fd duplication is
252
+ // not matched.
253
+ pattern: "^\\s*(echo|printf|cat\\s*<<)\\s+(?:[^\"'>]|\"[^\"]*\"|'[^']*')*(?<!\\|)>{1,2}\\|?\\s*[$\\w./~\"'-]",
250
254
  tool: "write",
251
255
  message: "Use the `write` tool instead of echo/cat redirection. It handles encoding and provides confirmation.",
252
256
  },
@@ -256,7 +260,6 @@ export const SETTINGS_SCHEMA = {
256
260
  // ────────────────────────────────────────────────────────────────────────
257
261
  // General settings (no UI)
258
262
  // ────────────────────────────────────────────────────────────────────────
259
- lastChangelogVersion: { type: "string", default: undefined },
260
263
  setupVersion: { type: "number", default: 0 },
261
264
 
262
265
  // Auth broker — credentials proxied through a remote `omp auth-broker serve`
@@ -876,7 +879,7 @@ export const SETTINGS_SCHEMA = {
876
879
 
877
880
  "retry.maxRetries": {
878
881
  type: "number",
879
- default: 3,
882
+ default: 10,
880
883
  ui: {
881
884
  tab: "model",
882
885
  label: "Retry Attempts",
@@ -891,7 +894,7 @@ export const SETTINGS_SCHEMA = {
891
894
  },
892
895
  },
893
896
 
894
- "retry.baseDelayMs": { type: "number", default: 2000 },
897
+ "retry.baseDelayMs": { type: "number", default: 500 },
895
898
  "retry.maxDelayMs": {
896
899
  type: "number",
897
900
  default: 5 * 60 * 1000,
@@ -1974,6 +1977,17 @@ export const SETTINGS_SCHEMA = {
1974
1977
  ui: { tab: "editing", label: "LSP", description: "Enable the lsp tool for language server protocol" },
1975
1978
  },
1976
1979
 
1980
+ "lsp.lazy": {
1981
+ type: "boolean",
1982
+ default: true,
1983
+ ui: {
1984
+ tab: "editing",
1985
+ label: "Lazy LSP Startup",
1986
+ description:
1987
+ "Start language servers on first use (lsp tool or editing a matching file type) instead of at session startup",
1988
+ },
1989
+ },
1990
+
1977
1991
  "lsp.formatOnWrite": {
1978
1992
  type: "boolean",
1979
1993
  default: false,
@@ -2053,6 +2067,20 @@ export const SETTINGS_SCHEMA = {
2053
2067
  type: "number",
2054
2068
  default: 4 * 1024 * 1024,
2055
2069
  },
2070
+ "shellMinimizer.sourceOutlineLevel": {
2071
+ type: "enum",
2072
+ values: ["default", "aggressive"] as const,
2073
+ default: "default",
2074
+ ui: {
2075
+ tab: "editing",
2076
+ label: "Shell Minimizer Source Outline",
2077
+ description: "Source outline mode for cat/read of source files: default or aggressive",
2078
+ },
2079
+ },
2080
+ "shellMinimizer.legacyFilters": {
2081
+ type: "boolean",
2082
+ default: undefined,
2083
+ },
2056
2084
 
2057
2085
  // Eval (per-backend toggles; add more as new backends ship, e.g. eval.ts)
2058
2086
  "eval.py": {
@@ -2086,6 +2114,16 @@ export const SETTINGS_SCHEMA = {
2086
2114
  description: "Whether to keep IPython kernel alive across calls",
2087
2115
  },
2088
2116
  },
2117
+ "python.interpreter": {
2118
+ type: "string",
2119
+ default: "",
2120
+ ui: {
2121
+ tab: "editing",
2122
+ label: "Python Interpreter",
2123
+ description:
2124
+ "Optional path to an exact Python executable. When set, automatic Python runtime discovery is skipped.",
2125
+ },
2126
+ },
2089
2127
 
2090
2128
  // ────────────────────────────────────────────────────────────────────────
2091
2129
  // Tools
@@ -3243,21 +3281,23 @@ type Schema = typeof SETTINGS_SCHEMA;
3243
3281
  export type SettingPath = keyof Schema;
3244
3282
 
3245
3283
  /** Infer the value type for a setting path */
3246
- export type SettingValue<P extends SettingPath> = Schema[P] extends { type: "boolean" }
3247
- ? boolean
3248
- : Schema[P] extends { type: "string" }
3249
- ? string | undefined
3250
- : Schema[P] extends { type: "number" }
3251
- ? number
3252
- : Schema[P] extends { type: "enum"; values: infer V }
3253
- ? V extends readonly string[]
3254
- ? V[number]
3255
- : never
3256
- : Schema[P] extends { type: "array"; default: infer D }
3257
- ? D
3258
- : Schema[P] extends { type: "record"; default: infer D }
3284
+ export type SettingValue<P extends SettingPath> = Schema[P] extends { type: "boolean"; default: undefined }
3285
+ ? boolean | undefined
3286
+ : Schema[P] extends { type: "boolean" }
3287
+ ? boolean
3288
+ : Schema[P] extends { type: "string" }
3289
+ ? string | undefined
3290
+ : Schema[P] extends { type: "number" }
3291
+ ? number
3292
+ : Schema[P] extends { type: "enum"; values: infer V }
3293
+ ? V extends readonly string[]
3294
+ ? V[number]
3295
+ : never
3296
+ : Schema[P] extends { type: "array"; default: infer D }
3259
3297
  ? D
3260
- : never;
3298
+ : Schema[P] extends { type: "record"; default: infer D }
3299
+ ? D
3300
+ : never;
3261
3301
 
3262
3302
  /** Get the default value for a setting path */
3263
3303
  export function getDefault<P extends SettingPath>(path: P): SettingValue<P> {
@@ -3447,6 +3487,8 @@ export interface ShellMinimizerSettings {
3447
3487
  only: string[];
3448
3488
  except: string[];
3449
3489
  maxCaptureBytes: number;
3490
+ sourceOutlineLevel: "default" | "aggressive";
3491
+ legacyFilters: boolean | undefined;
3450
3492
  }
3451
3493
 
3452
3494
  /** Map group prefix -> typed settings interface */
@@ -17,6 +17,7 @@ import * as path from "node:path";
17
17
  import {
18
18
  getAgentDbPath,
19
19
  getAgentDir,
20
+ getLastChangelogVersionPath,
20
21
  getProjectDir,
21
22
  isEnoent,
22
23
  logger,
@@ -25,7 +26,7 @@ import {
25
26
  } from "@oh-my-pi/pi-utils";
26
27
  import { YAML } from "bun";
27
28
  import { type Settings as SettingsCapabilityItem, settingsCapability } from "../capability/settings";
28
- import type { ModelRole } from "../config/model-registry";
29
+ import type { ModelRole } from "../config/model-roles";
29
30
  import { loadCapability } from "../discovery";
30
31
  import { isLightTheme, setAutoThemeMapping, setColorBlindMode, setSymbolPreset } from "../modes/theme/theme";
31
32
  import { AgentStorage } from "../session/agent-storage";
@@ -63,6 +64,8 @@ export interface SettingsOptions {
63
64
  inMemory?: boolean;
64
65
  /** Initial overrides */
65
66
  overrides?: Partial<Record<SettingPath, unknown>>;
67
+ /** Extra config.yml-style overlays loaded after global/project settings */
68
+ configFiles?: string[];
66
69
  }
67
70
 
68
71
  // ═══════════════════════════════════════════════════════════════════════════
@@ -115,10 +118,12 @@ type PathScopedStringArrayEntry = {
115
118
  providers?: unknown;
116
119
  };
117
120
 
121
+ function expandTilde(p: string): string {
122
+ return p === "~" ? os.homedir() : p.startsWith("~/") ? path.join(os.homedir(), p.slice(2)) : p;
123
+ }
124
+
118
125
  function normalizePathPrefix(prefix: string): string {
119
- const expanded =
120
- prefix === "~" ? os.homedir() : prefix.startsWith("~/") ? path.join(os.homedir(), prefix.slice(2)) : prefix;
121
- return path.resolve(expanded);
126
+ return path.resolve(expandTilde(prefix));
122
127
  }
123
128
 
124
129
  function pathMatchesPrefix(cwd: string, prefix: string): boolean {
@@ -192,10 +197,13 @@ export class Settings {
192
197
  #agentDir: string;
193
198
  #storage: AgentStorage | null = null;
194
199
 
200
+ #configFiles: string[] = [];
195
201
  /** Global settings from config.yml */
196
202
  #global: RawSettings = {};
197
203
  /** Project settings from .claude/settings.yml etc */
198
204
  #project: RawSettings = {};
205
+ /** Extra config.yml-style overlays passed by CLI */
206
+ #configOverlay: RawSettings = {};
199
207
  /** Runtime overrides (not persisted) */
200
208
  #overrides: RawSettings = {};
201
209
  /** Merged view (global + project + overrides) */
@@ -206,6 +214,9 @@ export class Settings {
206
214
  /** Paths modified during this session (for partial save) */
207
215
  #modified = new Set<string>();
208
216
 
217
+ /** Legacy `lastChangelogVersion` captured from config.yml during migration (now a marker file). */
218
+ #legacyLastChangelogVersion?: string;
219
+
209
220
  /** Pending save (debounced) */
210
221
  #saveTimer?: NodeJS.Timeout;
211
222
  #savePromise?: Promise<void>;
@@ -217,6 +228,7 @@ export class Settings {
217
228
  this.#cwd = path.normalize(options.cwd ?? getProjectDir());
218
229
  this.#agentDir = path.normalize(options.agentDir ?? getAgentDir());
219
230
  this.#configPath = options.inMemory ? null : path.join(this.#agentDir, "config.yml");
231
+ this.#configFiles = options.configFiles?.map(file => path.resolve(this.#cwd, expandTilde(file))) ?? [];
220
232
  this.#persist = !options.inMemory;
221
233
 
222
234
  if (options.overrides) {
@@ -252,6 +264,7 @@ export class Settings {
252
264
  },
253
265
  error => {
254
266
  globalInstance = null;
267
+ globalInstancePromise = null;
255
268
  clearBoundSettingsMethods();
256
269
  throw error;
257
270
  },
@@ -299,6 +312,14 @@ export class Settings {
299
312
  return resolved as SettingValue<P>;
300
313
  }
301
314
 
315
+ /**
316
+ * Whether `path` has an explicitly configured value (global config, project
317
+ * config, or runtime override) rather than falling back to the schema default.
318
+ */
319
+ isConfigured(path: SettingPath): boolean {
320
+ return getByPath(this.#merged, SETTING_PATH_SEGMENTS[path]) !== undefined;
321
+ }
322
+
302
323
  /**
303
324
  * Set a setting value (sync).
304
325
  * Updates global settings and queues a background save.
@@ -382,6 +403,8 @@ export class Settings {
382
403
  cloned.#storage = this.#storage;
383
404
  cloned.#global = structuredClone(this.#global);
384
405
  cloned.#project = this.#persist ? await cloned.#loadProjectSettings() : structuredClone(this.#project);
406
+ cloned.#configFiles = [...this.#configFiles];
407
+ cloned.#configOverlay = structuredClone(this.#configOverlay);
385
408
  cloned.#overrides = structuredClone(this.#overrides);
386
409
  cloned.#rebuildMerged();
387
410
  cloned.#fireAllHooks();
@@ -549,9 +572,11 @@ export class Settings {
549
572
  this.#storage = await AgentStorage.open(getAgentDbPath(this.#agentDir));
550
573
  await this.#migrateFromLegacy();
551
574
  this.#global = await this.#loadYaml(this.#configPath!);
575
+ await this.#seedLastChangelogVersionMarker();
552
576
  }
553
577
 
554
578
  this.#project = await projectPromise;
579
+ this.#configOverlay = await this.#loadConfigOverlays();
555
580
 
556
581
  // Build merged view (global → project → overrides; project wins over global)
557
582
  this.#rebuildMerged();
@@ -589,6 +614,43 @@ export class Settings {
589
614
  }
590
615
  }
591
616
 
617
+ async #loadConfigOverlays(): Promise<RawSettings> {
618
+ let merged: RawSettings = {};
619
+ for (const filePath of this.#configFiles) {
620
+ merged = this.#deepMerge(merged, await this.#loadOverlayYaml(filePath));
621
+ }
622
+ return merged;
623
+ }
624
+
625
+ /**
626
+ * Strict loader for explicit `--config` overlays: unlike `#loadYaml`,
627
+ * missing or malformed files are hard errors so a typo'd path cannot
628
+ * silently fall back to the persistent settings.
629
+ */
630
+ async #loadOverlayYaml(filePath: string): Promise<RawSettings> {
631
+ let content: string;
632
+ try {
633
+ content = await Bun.file(filePath).text();
634
+ } catch (error) {
635
+ throw new Error(
636
+ isEnoent(error)
637
+ ? `Config overlay not found: ${filePath}`
638
+ : `Failed to read config overlay ${filePath}: ${String(error)}`,
639
+ );
640
+ }
641
+ let parsed: unknown;
642
+ try {
643
+ parsed = YAML.parse(content);
644
+ } catch (error) {
645
+ throw new Error(`Failed to parse config overlay ${filePath}: ${String(error)}`);
646
+ }
647
+ if (parsed === null || parsed === undefined) return {};
648
+ if (typeof parsed !== "object" || Array.isArray(parsed)) {
649
+ throw new Error(`Config overlay must be a YAML mapping: ${filePath}`);
650
+ }
651
+ return this.#migrateRawSettings(parsed as RawSettings);
652
+ }
653
+
592
654
  async #migrateFromLegacy(): Promise<void> {
593
655
  if (!this.#configPath) return;
594
656
 
@@ -642,6 +704,16 @@ export class Settings {
642
704
  delete raw.queueMode;
643
705
  }
644
706
 
707
+ // lastChangelogVersion moved out of config.yml into the
708
+ // <agentDir>/last-changelog-version marker file so version bumps no
709
+ // longer dirty user-tracked configs. Capture for marker seeding (see
710
+ // #seedLastChangelogVersionMarker), then strip the key — the next
711
+ // config save drops it from disk.
712
+ if (typeof raw.lastChangelogVersion === "string") {
713
+ this.#legacyLastChangelogVersion ??= raw.lastChangelogVersion;
714
+ }
715
+ delete raw.lastChangelogVersion;
716
+
645
717
  // ask.timeout: ms -> seconds (if value > 1000, it's old ms format)
646
718
  if (raw.ask && typeof (raw.ask as Record<string, unknown>).timeout === "number") {
647
719
  const oldValue = (raw.ask as Record<string, unknown>).timeout as number;
@@ -803,6 +875,27 @@ export class Settings {
803
875
  return raw;
804
876
  }
805
877
 
878
+ /**
879
+ * One-time migration: seed the last-changelog-version marker file from the
880
+ * legacy config.yml key. An existing marker always wins — it is the newer
881
+ * source of truth.
882
+ */
883
+ async #seedLastChangelogVersionMarker(): Promise<void> {
884
+ const legacy = this.#legacyLastChangelogVersion;
885
+ if (!legacy) return;
886
+ const markerPath = getLastChangelogVersionPath(this.#agentDir);
887
+ try {
888
+ if ((await Bun.file(markerPath).text()).trim()) return;
889
+ } catch (error) {
890
+ if (!isEnoent(error)) return;
891
+ }
892
+ try {
893
+ await Bun.write(markerPath, legacy);
894
+ } catch (error) {
895
+ logger.warn("Settings: failed to seed last-changelog-version marker", { error: String(error) });
896
+ }
897
+ }
898
+
806
899
  // ─────────────────────────────────────────────────────────────────────────
807
900
  // Saving
808
901
  // ─────────────────────────────────────────────────────────────────────────
@@ -862,6 +955,7 @@ export class Settings {
862
955
 
863
956
  #rebuildMerged(): void {
864
957
  this.#merged = this.#deepMerge(this.#deepMerge({}, this.#global), this.#project);
958
+ this.#merged = this.#deepMerge(this.#merged, this.#configOverlay);
865
959
  this.#merged = this.#deepMerge(this.#merged, this.#overrides);
866
960
  this.#resolvedCache.clear();
867
961
  }
package/src/dap/client.ts CHANGED
@@ -29,32 +29,67 @@ type DapReverseRequestHandler = (args: unknown) => unknown | Promise<unknown>;
29
29
 
30
30
  const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
31
31
 
32
- function findHeaderEnd(buffer: Uint8Array): number {
33
- for (let index = 0; index < buffer.length - 3; index += 1) {
34
- if (buffer[index] === 13 && buffer[index + 1] === 10 && buffer[index + 2] === 13 && buffer[index + 3] === 10) {
35
- return index;
32
+ // Reused for all full decodes; each decode() resets state, so a single
33
+ // instance is safe and avoids per-message TextDecoder allocation.
34
+ const MESSAGE_DECODER = new TextDecoder("utf-8");
35
+
36
+ /**
37
+ * Locate the `\r\n\r\n` header terminator across the pending chunk list.
38
+ * Returns the absolute byte index of the first `\r`, or -1 when not present.
39
+ * Equivalent to scanning the contiguous concatenation of the chunks.
40
+ */
41
+ function findHeaderEndInChunks(chunks: Buffer[]): number {
42
+ let global = 0;
43
+ let b0 = -1;
44
+ let b1 = -1;
45
+ let b2 = -1;
46
+ for (const chunk of chunks) {
47
+ for (let i = 0; i < chunk.length; i++) {
48
+ const b3 = chunk[i];
49
+ if (b0 === 13 && b1 === 10 && b2 === 13 && b3 === 10) {
50
+ return global - 3;
51
+ }
52
+ b0 = b1;
53
+ b1 = b2;
54
+ b2 = b3;
55
+ global++;
36
56
  }
37
57
  }
38
58
  return -1;
39
59
  }
40
60
 
41
- function parseMessage(
42
- buffer: Buffer,
43
- ): { message: DapResponseMessage | DapEventMessage | DapRequestMessage; remaining: Buffer } | null {
44
- const headerEndIndex = findHeaderEnd(buffer);
45
- if (headerEndIndex === -1) return null;
46
- const headerText = new TextDecoder().decode(buffer.slice(0, headerEndIndex));
47
- const contentLengthMatch = headerText.match(/Content-Length: (\d+)/i);
48
- if (!contentLengthMatch) return null;
49
- const contentLength = Number.parseInt(contentLengthMatch[1], 10);
50
- const messageStart = headerEndIndex + 4;
51
- const messageEnd = messageStart + contentLength;
52
- if (buffer.length < messageEnd) return null;
53
- const messageText = new TextDecoder().decode(buffer.subarray(messageStart, messageEnd));
54
- return {
55
- message: JSON.parse(messageText) as DapResponseMessage | DapEventMessage | DapRequestMessage,
56
- remaining: buffer.subarray(messageEnd),
57
- };
61
+ /** Copy the byte range [from, to) out of the pending chunk list into one Buffer. */
62
+ function copyChunkRange(chunks: Buffer[], from: number, to: number): Buffer {
63
+ const out = Buffer.allocUnsafe(to - from);
64
+ let global = 0;
65
+ let written = 0;
66
+ for (const chunk of chunks) {
67
+ const chunkEnd = global + chunk.length;
68
+ if (chunkEnd > from && global < to) {
69
+ const start = Math.max(from, global) - global;
70
+ const end = Math.min(to, chunkEnd) - global;
71
+ chunk.copy(out, written, start, end);
72
+ written += end - start;
73
+ }
74
+ global = chunkEnd;
75
+ if (global >= to) break;
76
+ }
77
+ return out;
78
+ }
79
+
80
+ /** Drop the first `count` bytes from the pending chunk list in place. */
81
+ function dropChunkFront(chunks: Buffer[], count: number): void {
82
+ let removed = 0;
83
+ while (chunks.length > 0) {
84
+ const head = chunks[0];
85
+ if (removed + head.length <= count) {
86
+ removed += head.length;
87
+ chunks.shift();
88
+ } else {
89
+ chunks[0] = head.subarray(count - removed);
90
+ break;
91
+ }
92
+ }
58
93
  }
59
94
 
60
95
  async function writeMessage(sink: DapWriteSink, message: DapRequestMessage | DapResponseMessage): Promise<void> {
@@ -81,7 +116,7 @@ export class DapClient {
81
116
  readonly #socket?: { end(): void };
82
117
  #requestSeq = 0;
83
118
  #pendingRequests = new Map<number, DapPendingRequest>();
84
- #messageBuffer = Buffer.alloc(0);
119
+ #messageBuffer: Buffer = Buffer.alloc(0);
85
120
  #isReading = false;
86
121
  #disposed = false;
87
122
  #lastActivity = Date.now();
@@ -416,32 +451,84 @@ export class DapClient {
416
451
  if (this.#isReading) return;
417
452
  this.#isReading = true;
418
453
  const reader = this.#readable.getReader();
454
+
455
+ // Incoming bytes are buffered as a list of chunks and only joined when a
456
+ // full message is framed (mirrors the LSP reader) — concatenating the
457
+ // accumulator on every read is O(n^2) for messages spanning many reads.
458
+ const pendingChunks: Buffer[] = [];
459
+ let pendingLen = 0;
460
+ if (this.#messageBuffer.length > 0) {
461
+ pendingChunks.push(this.#messageBuffer);
462
+ pendingLen = this.#messageBuffer.length;
463
+ }
464
+
419
465
  try {
420
466
  while (true) {
421
467
  const { done, value } = await reader.read();
422
468
  if (done) break;
423
- const currentBuffer = Buffer.concat([this.#messageBuffer, value]);
424
- this.#messageBuffer = currentBuffer;
425
- let workingBuffer = currentBuffer;
426
- let parsed = parseMessage(workingBuffer);
427
- while (parsed) {
428
- const { message, remaining } = parsed;
429
- workingBuffer = Buffer.from(remaining);
469
+
470
+ pendingChunks.push(Buffer.from(value));
471
+ pendingLen += value.length;
472
+
473
+ // Drain every complete message currently buffered.
474
+ while (true) {
475
+ const headerEnd = findHeaderEndInChunks(pendingChunks);
476
+ if (headerEnd === -1) break;
477
+
478
+ const headerText = MESSAGE_DECODER.decode(copyChunkRange(pendingChunks, 0, headerEnd));
479
+ const contentLengthMatch = headerText.match(/Content-Length: (\d+)/i);
480
+ if (!contentLengthMatch) {
481
+ // Non-protocol bytes (e.g. an adapter printing to stdout).
482
+ // Drop past the bogus terminator and resync instead of
483
+ // stalling on the same junk header forever.
484
+ logger.warn("DAP framing resync: header block without Content-Length", {
485
+ adapter: this.adapter.name,
486
+ header: headerText.slice(0, 200),
487
+ });
488
+ dropChunkFront(pendingChunks, headerEnd + 4);
489
+ pendingLen -= headerEnd + 4;
490
+ continue;
491
+ }
492
+
493
+ const contentLength = Number.parseInt(contentLengthMatch[1], 10);
494
+ const messageStart = headerEnd + 4; // Skip \r\n\r\n
495
+ const messageEnd = messageStart + contentLength;
496
+ if (pendingLen < messageEnd) break;
497
+
498
+ const messageText = MESSAGE_DECODER.decode(copyChunkRange(pendingChunks, messageStart, messageEnd));
499
+ dropChunkFront(pendingChunks, messageEnd);
500
+ pendingLen -= messageEnd;
430
501
  this.#lastActivity = Date.now();
431
- if (message.type === "response") {
432
- this.#handleResponse(message);
433
- } else if (message.type === "event") {
434
- await this.#dispatchEvent(message);
435
- } else {
436
- await this.#handleAdapterRequest(message);
502
+
503
+ // A malformed message must not kill the reader — later
504
+ // messages are still well-framed.
505
+ try {
506
+ const message = JSON.parse(messageText) as DapResponseMessage | DapEventMessage | DapRequestMessage;
507
+ if (message.type === "response") {
508
+ this.#handleResponse(message);
509
+ } else if (message.type === "event") {
510
+ await this.#dispatchEvent(message);
511
+ } else {
512
+ await this.#handleAdapterRequest(message);
513
+ }
514
+ } catch (error) {
515
+ logger.warn("DAP message handling failed", {
516
+ adapter: this.adapter.name,
517
+ error: toErrorMessage(error),
518
+ });
437
519
  }
438
- parsed = parseMessage(workingBuffer);
439
520
  }
440
- this.#messageBuffer = workingBuffer;
441
521
  }
442
522
  } catch (error) {
443
523
  this.#rejectPendingRequests(new Error(`DAP connection closed: ${toErrorMessage(error)}`));
444
524
  } finally {
525
+ // Persist any unparsed remainder so a restarted reader resumes mid-message.
526
+ this.#messageBuffer =
527
+ pendingChunks.length === 0
528
+ ? Buffer.alloc(0)
529
+ : pendingChunks.length === 1
530
+ ? pendingChunks[0]
531
+ : Buffer.concat(pendingChunks, pendingLen);
445
532
  reader.releaseLock();
446
533
  this.#isReading = false;
447
534
  }