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

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 (345) hide show
  1. package/CHANGELOG.md +95 -4
  2. package/dist/cli.js +23087 -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 +1 -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/commands/launch.d.ts +1 -1
  11. package/dist/types/commands/read.d.ts +1 -1
  12. package/dist/types/commands/usage.d.ts +25 -0
  13. package/dist/types/config/append-only-context-mode.d.ts +2 -1
  14. package/dist/types/config/model-discovery.d.ts +55 -0
  15. package/dist/types/config/model-registry.d.ts +7 -219
  16. package/dist/types/config/model-resolver.d.ts +16 -10
  17. package/dist/types/config/model-roles.d.ts +28 -0
  18. package/dist/types/config/models-config-schema.d.ts +523 -42
  19. package/dist/types/config/models-config.d.ts +385 -0
  20. package/dist/types/config/settings-schema.d.ts +12 -7
  21. package/dist/types/config/settings.d.ts +1 -1
  22. package/dist/types/debug/log-viewer.d.ts +1 -1
  23. package/dist/types/debug/raw-sse.d.ts +1 -1
  24. package/dist/types/eval/backend.d.ts +0 -2
  25. package/dist/types/eval/idle-timeout.d.ts +0 -4
  26. package/dist/types/eval/js/shared/rewrite-imports.d.ts +6 -6
  27. package/dist/types/export/html/template.generated.d.ts +1 -1
  28. package/dist/types/extensibility/extensions/types.d.ts +3 -3
  29. package/dist/types/hindsight/mental-models.d.ts +17 -8
  30. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
  31. package/dist/types/internal-urls/types.d.ts +1 -1
  32. package/dist/types/lsp/edits.d.ts +9 -0
  33. package/dist/types/lsp/index.d.ts +2 -2
  34. package/dist/types/lsp/types.d.ts +2 -0
  35. package/dist/types/lsp/utils.d.ts +3 -0
  36. package/dist/types/mcp/json-rpc.d.ts +5 -0
  37. package/dist/types/mnemopi/state.d.ts +11 -1
  38. package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
  39. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  40. package/dist/types/modes/components/bash-execution.d.ts +1 -1
  41. package/dist/types/modes/components/copy-selector.d.ts +1 -1
  42. package/dist/types/modes/components/dynamic-border.d.ts +1 -1
  43. package/dist/types/modes/components/extensions/extension-dashboard.d.ts +1 -1
  44. package/dist/types/modes/components/extensions/extension-list.d.ts +1 -1
  45. package/dist/types/modes/components/extensions/inspector-panel.d.ts +1 -1
  46. package/dist/types/modes/components/footer.d.ts +1 -1
  47. package/dist/types/modes/components/hook-editor.d.ts +5 -0
  48. package/dist/types/modes/components/hook-input.d.ts +4 -0
  49. package/dist/types/modes/components/hook-selector.d.ts +1 -1
  50. package/dist/types/modes/components/model-selector.d.ts +1 -1
  51. package/dist/types/modes/components/plan-review-overlay.d.ts +1 -1
  52. package/dist/types/modes/components/session-observer-overlay.d.ts +1 -1
  53. package/dist/types/modes/components/session-selector.d.ts +1 -1
  54. package/dist/types/modes/components/status-line/component.d.ts +1 -1
  55. package/dist/types/modes/components/tiny-title-download-progress.d.ts +1 -1
  56. package/dist/types/modes/components/transcript-container.d.ts +25 -6
  57. package/dist/types/modes/components/tree-selector.d.ts +1 -1
  58. package/dist/types/modes/components/user-message-selector.d.ts +1 -1
  59. package/dist/types/modes/components/user-message.d.ts +2 -1
  60. package/dist/types/modes/components/visual-truncate.d.ts +1 -1
  61. package/dist/types/modes/components/welcome.d.ts +19 -3
  62. package/dist/types/modes/controllers/mcp-command-controller.d.ts +1 -1
  63. package/dist/types/modes/controllers/streaming-reveal.d.ts +1 -1
  64. package/dist/types/modes/interactive-mode.d.ts +1 -1
  65. package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
  66. package/dist/types/modes/setup-wizard/scenes/types.d.ts +1 -1
  67. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
  68. package/dist/types/modes/setup-wizard/wizard-overlay.d.ts +1 -1
  69. package/dist/types/modes/types.d.ts +2 -1
  70. package/dist/types/session/agent-session.d.ts +1 -1
  71. package/dist/types/session/auth-broker-config.d.ts +4 -0
  72. package/dist/types/session/session-manager.d.ts +1 -1
  73. package/dist/types/slash-commands/helpers/stats-dashboard.d.ts +13 -0
  74. package/dist/types/ssh/connection-manager.d.ts +8 -0
  75. package/dist/types/task/parallel.d.ts +2 -2
  76. package/dist/types/task/worktree.d.ts +2 -0
  77. package/dist/types/tools/ask.d.ts +4 -0
  78. package/dist/types/tools/conflict-detect.d.ts +16 -0
  79. package/dist/types/tools/github-cache.d.ts +7 -0
  80. package/dist/types/tools/sqlite-reader.d.ts +3 -0
  81. package/dist/types/tui/output-block.d.ts +3 -3
  82. package/dist/types/utils/changelog.d.ts +8 -0
  83. package/dist/types/web/scrapers/readthedocs.d.ts +3 -0
  84. package/dist/types/web/scrapers/types.d.ts +12 -0
  85. package/dist/types/web/search/providers/codex.d.ts +1 -1
  86. package/dist/types/web/search/providers/gemini.d.ts +1 -1
  87. package/examples/extensions/tools.ts +5 -4
  88. package/package.json +14 -11
  89. package/scripts/build-binary.ts +18 -23
  90. package/scripts/bundle-dist.ts +81 -0
  91. package/scripts/{dev-launch → omp} +1 -1
  92. package/scripts/{dev-launch-preload.ts → omp.ts} +1 -1
  93. package/src/async/job-manager.ts +57 -3
  94. package/src/autoresearch/dashboard.ts +1 -1
  95. package/src/autoresearch/prompt-setup.md +6 -6
  96. package/src/autoresearch/prompt.md +6 -6
  97. package/src/capability/fs.ts +10 -0
  98. package/src/cli/args.ts +1 -1
  99. package/src/cli/auth-gateway-cli.ts +1 -3
  100. package/src/cli/dry-balance-cli.ts +1 -1
  101. package/src/cli/gallery-cli.ts +1 -1
  102. package/src/cli/gallery-fixtures/fs.ts +1 -1
  103. package/src/cli/gallery-fixtures/types.ts +5 -1
  104. package/src/cli/list-models.ts +2 -1
  105. package/src/cli/usage-cli.ts +603 -0
  106. package/src/cli-commands.ts +1 -0
  107. package/src/cli.ts +69 -5
  108. package/src/commands/complete.ts +1 -1
  109. package/src/commands/launch.ts +1 -1
  110. package/src/commands/read.ts +6 -3
  111. package/src/commands/usage.ts +35 -0
  112. package/src/commit/agentic/agent.ts +1 -1
  113. package/src/commit/model-selection.ts +1 -1
  114. package/src/config/append-only-context-mode.ts +6 -12
  115. package/src/config/model-discovery.ts +554 -0
  116. package/src/config/model-registry.ts +231 -1019
  117. package/src/config/model-resolver.ts +113 -156
  118. package/src/config/model-roles.ts +74 -0
  119. package/src/config/models-config-schema.ts +57 -8
  120. package/src/config/models-config.ts +129 -0
  121. package/src/config/settings-schema.ts +18 -4
  122. package/src/config/settings.ts +37 -1
  123. package/src/dap/client.ts +124 -37
  124. package/src/dap/session.ts +259 -158
  125. package/src/debug/log-viewer.ts +1 -1
  126. package/src/debug/raw-sse.ts +1 -1
  127. package/src/edit/diff.ts +47 -3
  128. package/src/edit/hashline/block-resolver.ts +20 -1
  129. package/src/edit/hashline/diff.ts +36 -1
  130. package/src/edit/hashline/execute.ts +8 -2
  131. package/src/edit/index.ts +16 -1
  132. package/src/edit/modes/patch.ts +52 -0
  133. package/src/edit/modes/replace.ts +56 -22
  134. package/src/edit/notebook.ts +22 -2
  135. package/src/edit/renderer.ts +36 -10
  136. package/src/eval/__tests__/completion-bridge.test.ts +1 -1
  137. package/src/eval/backend.ts +0 -2
  138. package/src/eval/completion-bridge.ts +2 -1
  139. package/src/eval/idle-timeout.ts +2 -9
  140. package/src/eval/js/context-manager.ts +6 -8
  141. package/src/eval/js/executor.ts +6 -2
  142. package/src/eval/js/index.ts +0 -2
  143. package/src/eval/js/shared/helpers.ts +5 -6
  144. package/src/eval/js/shared/local-module-loader.ts +1 -1
  145. package/src/eval/js/shared/prelude.txt +62 -1
  146. package/src/eval/js/shared/rewrite-imports.ts +40 -22
  147. package/src/eval/js/shared/runtime.ts +1 -1
  148. package/src/eval/py/index.ts +0 -2
  149. package/src/eval/py/kernel.ts +19 -0
  150. package/src/eval/py/runner.py +107 -3
  151. package/src/exec/bash-executor.ts +3 -1
  152. package/src/export/html/template.generated.ts +1 -1
  153. package/src/export/html/template.js +3 -1
  154. package/src/extensibility/extensions/types.ts +3 -2
  155. package/src/extensibility/plugins/legacy-pi-compat.ts +20 -3
  156. package/src/hindsight/mental-models.ts +59 -12
  157. package/src/hindsight/state.ts +6 -1
  158. package/src/internal-urls/artifact-protocol.ts +11 -2
  159. package/src/internal-urls/docs-index.generated.ts +8 -8
  160. package/src/internal-urls/issue-pr-protocol.ts +12 -5
  161. package/src/internal-urls/router.ts +1 -1
  162. package/src/internal-urls/types.ts +1 -1
  163. package/src/lib/xai-http.ts +1 -1
  164. package/src/lsp/client.ts +118 -38
  165. package/src/lsp/clients/biome-client.ts +101 -39
  166. package/src/lsp/edits.ts +143 -95
  167. package/src/lsp/index.ts +31 -22
  168. package/src/lsp/render.ts +1 -1
  169. package/src/lsp/types.ts +2 -0
  170. package/src/lsp/utils.ts +28 -10
  171. package/src/main.ts +165 -17
  172. package/src/mcp/json-rpc.ts +35 -5
  173. package/src/mcp/transports/stdio.ts +7 -1
  174. package/src/memories/index.ts +2 -1
  175. package/src/mnemopi/backend.ts +25 -3
  176. package/src/mnemopi/state.ts +38 -2
  177. package/src/modes/components/agent-dashboard.ts +10 -7
  178. package/src/modes/components/assistant-message.ts +19 -13
  179. package/src/modes/components/bash-execution.ts +1 -1
  180. package/src/modes/components/copy-selector.ts +1 -1
  181. package/src/modes/components/diff.ts +13 -2
  182. package/src/modes/components/dynamic-border.ts +12 -3
  183. package/src/modes/components/extensions/extension-dashboard.ts +8 -5
  184. package/src/modes/components/extensions/extension-list.ts +1 -1
  185. package/src/modes/components/extensions/inspector-panel.ts +1 -1
  186. package/src/modes/components/footer.ts +1 -1
  187. package/src/modes/components/history-search.ts +1 -1
  188. package/src/modes/components/hook-editor.ts +8 -0
  189. package/src/modes/components/hook-input.ts +8 -0
  190. package/src/modes/components/hook-selector.ts +2 -2
  191. package/src/modes/components/model-selector.ts +4 -2
  192. package/src/modes/components/plan-review-overlay.ts +1 -1
  193. package/src/modes/components/session-observer-overlay.ts +2 -2
  194. package/src/modes/components/session-selector.ts +1 -1
  195. package/src/modes/components/settings-selector.ts +5 -1
  196. package/src/modes/components/status-line/component.ts +1 -1
  197. package/src/modes/components/tiny-title-download-progress.ts +1 -1
  198. package/src/modes/components/transcript-container.ts +258 -53
  199. package/src/modes/components/tree-selector.ts +3 -3
  200. package/src/modes/components/user-message-selector.ts +1 -1
  201. package/src/modes/components/user-message.ts +17 -5
  202. package/src/modes/components/visual-truncate.ts +1 -1
  203. package/src/modes/components/welcome.ts +108 -26
  204. package/src/modes/controllers/command-controller.ts +10 -3
  205. package/src/modes/controllers/event-controller.ts +73 -4
  206. package/src/modes/controllers/input-controller.ts +1 -1
  207. package/src/modes/controllers/mcp-command-controller.ts +1 -1
  208. package/src/modes/controllers/selector-controller.ts +1 -1
  209. package/src/modes/controllers/streaming-reveal.ts +85 -18
  210. package/src/modes/interactive-mode.ts +3 -9
  211. package/src/modes/setup-wizard/scenes/glyph.ts +1 -1
  212. package/src/modes/setup-wizard/scenes/providers.ts +1 -1
  213. package/src/modes/setup-wizard/scenes/sign-in.ts +1 -1
  214. package/src/modes/setup-wizard/scenes/theme.ts +1 -1
  215. package/src/modes/setup-wizard/scenes/types.ts +1 -1
  216. package/src/modes/setup-wizard/scenes/web-search.ts +1 -1
  217. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  218. package/src/modes/types.ts +2 -1
  219. package/src/prompts/agents/explore.md +2 -2
  220. package/src/prompts/agents/librarian.md +1 -2
  221. package/src/prompts/agents/oracle.md +1 -1
  222. package/src/prompts/agents/plan.md +5 -5
  223. package/src/prompts/agents/task.md +5 -5
  224. package/src/prompts/ci-green-request.md +5 -7
  225. package/src/prompts/goals/goal-budget-limit.md +2 -2
  226. package/src/prompts/goals/goal-continuation.md +4 -4
  227. package/src/prompts/goals/goal-mode-active.md +1 -1
  228. package/src/prompts/memories/read-path.md +1 -1
  229. package/src/prompts/memories/stage_one_system.md +2 -2
  230. package/src/prompts/review-custom-request.md +1 -1
  231. package/src/prompts/system/agent-creation-architect.md +2 -2
  232. package/src/prompts/system/auto-continue.md +1 -1
  233. package/src/prompts/system/background-tan-dispatch.md +1 -1
  234. package/src/prompts/system/btw-user.md +2 -2
  235. package/src/prompts/system/commit-message-system.md +13 -1
  236. package/src/prompts/system/custom-system-prompt.md +1 -1
  237. package/src/prompts/system/eager-todo.md +2 -2
  238. package/src/prompts/system/irc-incoming.md +1 -1
  239. package/src/prompts/system/manual-continue.md +1 -1
  240. package/src/prompts/system/omfg-user.md +3 -4
  241. package/src/prompts/system/orchestrate-notice.md +9 -9
  242. package/src/prompts/system/plan-mode-active.md +4 -4
  243. package/src/prompts/system/plan-mode-subagent.md +4 -5
  244. package/src/prompts/system/plan-mode-tool-decision-reminder.md +1 -1
  245. package/src/prompts/system/project-prompt.md +2 -2
  246. package/src/prompts/system/subagent-system-prompt.md +4 -4
  247. package/src/prompts/system/system-prompt.md +13 -24
  248. package/src/prompts/system/title-system.md +2 -2
  249. package/src/prompts/system/ttsr-tool-reminder.md +1 -1
  250. package/src/prompts/system/workflow-notice.md +1 -1
  251. package/src/prompts/tools/ast-edit.md +1 -1
  252. package/src/prompts/tools/ast-grep.md +2 -2
  253. package/src/prompts/tools/bash.md +5 -7
  254. package/src/prompts/tools/browser.md +7 -7
  255. package/src/prompts/tools/debug.md +1 -1
  256. package/src/prompts/tools/eval.md +3 -3
  257. package/src/prompts/tools/find.md +0 -1
  258. package/src/prompts/tools/github.md +8 -7
  259. package/src/prompts/tools/goal.md +1 -1
  260. package/src/prompts/tools/image-gen.md +1 -1
  261. package/src/prompts/tools/inspect-image-system.md +1 -1
  262. package/src/prompts/tools/irc.md +15 -15
  263. package/src/prompts/tools/lsp.md +2 -2
  264. package/src/prompts/tools/patch.md +2 -2
  265. package/src/prompts/tools/read.md +3 -4
  266. package/src/prompts/tools/recall.md +1 -1
  267. package/src/prompts/tools/reflect.md +1 -1
  268. package/src/prompts/tools/render-mermaid.md +2 -2
  269. package/src/prompts/tools/replace.md +4 -10
  270. package/src/prompts/tools/rewind.md +2 -2
  271. package/src/prompts/tools/search-tool-bm25.md +1 -9
  272. package/src/prompts/tools/search.md +0 -1
  273. package/src/prompts/tools/ssh.md +0 -4
  274. package/src/prompts/tools/task.md +2 -3
  275. package/src/prompts/tools/todo.md +1 -1
  276. package/src/sdk.ts +23 -10
  277. package/src/session/agent-session.ts +44 -10
  278. package/src/session/auth-broker-config.ts +30 -1
  279. package/src/session/session-manager.ts +2 -2
  280. package/src/session/streaming-output.ts +23 -2
  281. package/src/slash-commands/builtin-registry.ts +20 -0
  282. package/src/slash-commands/helpers/stats-dashboard.ts +85 -0
  283. package/src/ssh/connection-manager.ts +27 -0
  284. package/src/task/commands.ts +2 -1
  285. package/src/task/executor.ts +61 -53
  286. package/src/task/index.ts +137 -60
  287. package/src/task/parallel.ts +3 -3
  288. package/src/task/render.ts +2 -2
  289. package/src/task/worktree.ts +64 -56
  290. package/src/thinking.ts +2 -1
  291. package/src/tiny/title-client.ts +26 -11
  292. package/src/tools/archive-reader.ts +30 -2
  293. package/src/tools/ask.ts +104 -21
  294. package/src/tools/ast-edit.ts +25 -5
  295. package/src/tools/auto-generated-guard.ts +20 -3
  296. package/src/tools/bash-interactive.ts +27 -7
  297. package/src/tools/bash.ts +54 -13
  298. package/src/tools/browser/launch.ts +11 -2
  299. package/src/tools/browser/readable.ts +19 -2
  300. package/src/tools/browser/registry.ts +4 -1
  301. package/src/tools/browser/render.ts +2 -2
  302. package/src/tools/browser/tab-supervisor.ts +55 -16
  303. package/src/tools/conflict-detect.ts +50 -4
  304. package/src/tools/debug.ts +1 -1
  305. package/src/tools/eval-render.ts +5 -5
  306. package/src/tools/eval.ts +0 -2
  307. package/src/tools/fetch.ts +33 -10
  308. package/src/tools/gh-cache-invalidation.ts +63 -8
  309. package/src/tools/gh-renderer.ts +1 -1
  310. package/src/tools/gh.ts +172 -29
  311. package/src/tools/github-cache.ts +70 -6
  312. package/src/tools/image-gen.ts +3 -9
  313. package/src/tools/irc.ts +5 -1
  314. package/src/tools/job.ts +1 -1
  315. package/src/tools/read.ts +202 -61
  316. package/src/tools/render-utils.ts +3 -3
  317. package/src/tools/resolve.ts +1 -1
  318. package/src/tools/search.ts +92 -29
  319. package/src/tools/sqlite-reader.ts +17 -5
  320. package/src/tools/ssh.ts +8 -8
  321. package/src/tools/todo.ts +38 -8
  322. package/src/tools/write.ts +118 -18
  323. package/src/tui/output-block.ts +4 -4
  324. package/src/utils/changelog.ts +27 -1
  325. package/src/utils/file-mentions.ts +2 -1
  326. package/src/web/scrapers/arxiv.ts +1 -1
  327. package/src/web/scrapers/go-pkg.ts +1 -1
  328. package/src/web/scrapers/iacr.ts +1 -1
  329. package/src/web/scrapers/readthedocs.ts +1 -1
  330. package/src/web/scrapers/twitter.ts +2 -1
  331. package/src/web/scrapers/types.ts +87 -8
  332. package/src/web/scrapers/wikipedia.ts +1 -1
  333. package/src/web/scrapers/youtube.ts +6 -1
  334. package/src/web/search/index.ts +1 -1
  335. package/src/web/search/providers/codex.ts +2 -1
  336. package/src/web/search/providers/gemini.ts +2 -3
  337. package/src/web/search/render.ts +8 -6
  338. package/dist/types/config/model-equivalence.d.ts +0 -24
  339. package/dist/types/config/model-id-affixes.d.ts +0 -12
  340. package/dist/types/config/model-provider-priority.d.ts +0 -1
  341. package/dist/types/exec/idle-timeout-watchdog.d.ts +0 -18
  342. package/src/config/model-equivalence.ts +0 -875
  343. package/src/config/model-id-affixes.ts +0 -81
  344. package/src/config/model-provider-priority.ts +0 -56
  345. package/src/exec/idle-timeout-watchdog.ts +0 -126
@@ -71,17 +71,24 @@ function parseListOptions(url: InternalUrl, scheme: Scheme, repo: string | undef
71
71
  const stateRaw = url.searchParams.get("state");
72
72
  const allowedStates: ParsedList["state"][] =
73
73
  scheme === "pr" ? ["open", "closed", "merged", "all"] : ["open", "closed", "all"];
74
- const state = (
75
- stateRaw && (allowedStates as string[]).includes(stateRaw) ? stateRaw : "open"
76
- ) as ParsedList["state"];
74
+ if (stateRaw !== null && !(allowedStates as string[]).includes(stateRaw)) {
75
+ // Reject instead of silently falling back to "open": a typo'd state
76
+ // would otherwise return the open list, indistinguishable from "no
77
+ // matches for the requested state".
78
+ throw new Error(`Invalid ${scheme}:// list state '${stateRaw}'. Expected one of: ${allowedStates.join(", ")}.`);
79
+ }
80
+ const state = (stateRaw ?? "open") as ParsedList["state"];
77
81
 
78
82
  const limitRaw = url.searchParams.get("limit");
79
83
  let limit = LIST_LIMIT_DEFAULT;
80
84
  if (limitRaw !== null) {
81
85
  const parsed = parsePositiveDecimalInt(limitRaw);
82
- if (parsed !== undefined) {
83
- limit = Math.min(parsed, LIST_LIMIT_MAX);
86
+ if (parsed === undefined) {
87
+ throw new Error(
88
+ `Invalid ${scheme}:// list limit '${limitRaw}'. Expected a positive integer (max ${LIST_LIMIT_MAX}).`,
89
+ );
84
90
  }
91
+ limit = Math.min(parsed, LIST_LIMIT_MAX);
85
92
  }
86
93
  return {
87
94
  kind: "list",
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Internal URL router for internal protocols (agent://, artifact://, memory://, skill://, rule://, mcp://, omp://, local://).
2
+ * Internal URL router for internal protocols (`agent://`, `artifact://`, `issue://`, `local://`, `mcp://`, `memory://`, `omp://`, `pr://`, `rule://`, `skill://`, and `vault://`).
3
3
  *
4
4
  * One process-global router with one handler per scheme. Access via
5
5
  * `InternalUrlRouter.instance()`. Handlers are stateless; per-session and
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Types for the internal URL routing system.
3
3
  *
4
- * Internal URLs (agent://, artifact://, memory://, skill://, rule://, mcp://, omp://, local://) are resolved by tools like read,
4
+ * Internal URLs (`agent://`, `artifact://`, `issue://`, `local://`, `mcp://`, `memory://`, `omp://`, `pr://`, `rule://`, `skill://`, and `vault://`) are resolved by tools like read,
5
5
  * providing access to agent outputs and server resources without exposing filesystem paths.
6
6
  */
7
7
 
@@ -1,6 +1,6 @@
1
1
  // Ported from NousResearch/hermes-agent (MIT) — tools/xai_http.py.
2
2
 
3
- import { getBundledModels } from "@oh-my-pi/pi-ai";
3
+ import { getBundledModels } from "@oh-my-pi/pi-catalog/models";
4
4
  import { $env } from "@oh-my-pi/pi-utils";
5
5
  import type { ModelRegistry } from "../config/model-registry";
6
6
 
package/src/lsp/client.ts CHANGED
@@ -22,6 +22,10 @@ const clients = new Map<string, LspClient>();
22
22
  const clientLocks = new Map<string, Promise<LspClient>>();
23
23
  const fileOperationLocks = new Map<string, Promise<void>>();
24
24
 
25
+ /** Negative cache of recent init failures so a broken server fails fast instead of re-spawning per call. */
26
+ const INIT_FAILURE_BACKOFF_MS = 3 * 60 * 1000;
27
+ const initFailures = new Map<string, { at: number; message: string }>();
28
+
25
29
  // Idle timeout configuration (disabled by default)
26
30
  let idleTimeoutMs: number | null = null;
27
31
  let idleCheckInterval: NodeJS.Timeout | null = null;
@@ -295,7 +299,18 @@ async function startMessageReader(client: LspClient): Promise<void> {
295
299
 
296
300
  const headerText = MESSAGE_DECODER.decode(copyChunkRange(pendingChunks, 0, headerEnd));
297
301
  const contentLengthMatch = headerText.match(/Content-Length: (\d+)/i);
298
- if (!contentLengthMatch) break;
302
+ if (!contentLengthMatch) {
303
+ // Non-protocol bytes on stdout (e.g. a wrapper script printing).
304
+ // Drop past the bogus terminator and resync instead of stalling
305
+ // on the same junk header forever.
306
+ logger.warn("LSP framing resync: header block without Content-Length", {
307
+ server: client.name,
308
+ header: headerText.slice(0, 200),
309
+ });
310
+ dropChunkFront(pendingChunks, headerEnd + 4);
311
+ pendingLen -= headerEnd + 4;
312
+ continue;
313
+ }
299
314
 
300
315
  const contentLength = Number.parseInt(contentLengthMatch[1], 10);
301
316
  const messageStart = headerEnd + 4; // Skip \r\n\r\n
@@ -303,44 +318,54 @@ async function startMessageReader(client: LspClient): Promise<void> {
303
318
  if (pendingLen < messageEnd) break;
304
319
 
305
320
  const messageText = MESSAGE_DECODER.decode(copyChunkRange(pendingChunks, messageStart, messageEnd));
306
- const message: LspJsonRpcResponse | LspJsonRpcNotification = JSON.parse(messageText);
307
321
  dropChunkFront(pendingChunks, messageEnd);
308
322
  pendingLen -= messageEnd;
309
323
 
310
- // Route message
311
- if ("id" in message && message.id !== undefined) {
312
- // Response to a request
313
- const pending = client.pendingRequests.get(message.id);
314
- if (pending) {
315
- client.pendingRequests.delete(message.id);
316
- if ("error" in message && message.error) {
317
- pending.reject(new Error(`LSP error: ${message.error.message}`));
318
- } else {
319
- pending.resolve(message.result);
324
+ // A malformed message or a throwing server-request handler must not
325
+ // kill the reader later messages are still well-framed.
326
+ try {
327
+ const message: LspJsonRpcResponse | LspJsonRpcNotification = JSON.parse(messageText);
328
+
329
+ // Route message
330
+ if ("id" in message && message.id !== undefined) {
331
+ // Response to a request
332
+ const pending = client.pendingRequests.get(message.id);
333
+ if (pending) {
334
+ client.pendingRequests.delete(message.id);
335
+ if ("error" in message && message.error) {
336
+ pending.reject(new Error(`LSP error: ${message.error.message}`));
337
+ } else {
338
+ pending.resolve(message.result);
339
+ }
340
+ } else if ("method" in message) {
341
+ await handleServerRequest(client, message as LspJsonRpcRequest);
320
342
  }
321
343
  } else if ("method" in message) {
322
- await handleServerRequest(client, message as LspJsonRpcRequest);
323
- }
324
- } else if ("method" in message) {
325
- // Server notification
326
- if (message.method === "textDocument/publishDiagnostics" && message.params) {
327
- const params = message.params as PublishDiagnosticsParams;
328
- client.diagnostics.set(params.uri, {
329
- diagnostics: params.diagnostics,
330
- version: params.version ?? null,
331
- });
332
- client.diagnosticsVersion += 1;
333
- } else if (message.method === "$/progress" && message.params) {
334
- const params = message.params as { token: string | number; value?: { kind?: string } };
335
- if (params.value?.kind === "begin") {
336
- client.activeProgressTokens.add(params.token);
337
- } else if (params.value?.kind === "end") {
338
- client.activeProgressTokens.delete(params.token);
339
- if (client.activeProgressTokens.size === 0) {
340
- client.resolveProjectLoaded();
344
+ // Server notification
345
+ if (message.method === "textDocument/publishDiagnostics" && message.params) {
346
+ const params = message.params as PublishDiagnosticsParams;
347
+ client.diagnostics.set(params.uri, {
348
+ diagnostics: params.diagnostics,
349
+ version: params.version ?? null,
350
+ });
351
+ client.diagnosticsVersion += 1;
352
+ } else if (message.method === "$/progress" && message.params) {
353
+ const params = message.params as { token: string | number; value?: { kind?: string } };
354
+ if (params.value?.kind === "begin") {
355
+ client.activeProgressTokens.add(params.token);
356
+ } else if (params.value?.kind === "end") {
357
+ client.activeProgressTokens.delete(params.token);
358
+ if (client.activeProgressTokens.size === 0) {
359
+ client.resolveProjectLoaded();
360
+ }
341
361
  }
342
362
  }
343
363
  }
364
+ } catch (err) {
365
+ logger.warn("LSP message handling failed", {
366
+ server: client.name,
367
+ error: err instanceof Error ? err.message : String(err),
368
+ });
344
369
  }
345
370
  }
346
371
  }
@@ -360,6 +385,22 @@ async function startMessageReader(client: LspClient): Promise<void> {
360
385
  : Buffer.concat(pendingChunks, pendingLen);
361
386
  reader.releaseLock();
362
387
  client.isReading = false;
388
+ // Reader exited while the server process is still alive (unrecoverable
389
+ // read error or bad stream state): nothing will route responses anymore,
390
+ // so tear the client down — the next call respawns instead of timing out.
391
+ if (client.proc.exitCode === null) {
392
+ client.status = "error";
393
+ if (clients.get(client.name) === client) {
394
+ clients.delete(client.name);
395
+ }
396
+ const teardownErr = new Error("LSP reader stopped; client torn down");
397
+ for (const pending of client.pendingRequests.values()) {
398
+ pending.reject(teardownErr);
399
+ }
400
+ client.pendingRequests.clear();
401
+ client.resolveProjectLoaded();
402
+ client.proc.kill();
403
+ }
363
404
  }
364
405
  }
365
406
 
@@ -565,6 +606,16 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string, initT
565
606
  return existingLock;
566
607
  }
567
608
 
609
+ // Fail fast on a recent deterministic init failure instead of re-spawning
610
+ // a broken server (and paying its full init wait) on every call.
611
+ const recentFailure = initFailures.get(key);
612
+ if (recentFailure) {
613
+ if (Date.now() - recentFailure.at < INIT_FAILURE_BACKOFF_MS) {
614
+ throw new Error(`LSP server ${config.command} failed to initialize recently: ${recentFailure.message}`);
615
+ }
616
+ initFailures.delete(key);
617
+ }
618
+
568
619
  // Create new client with lock
569
620
  const clientPromise = (async () => {
570
621
  const baseCommand = config.resolvedCommand ?? config.command;
@@ -605,18 +656,18 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string, initT
605
656
  pendingRequests: new Map(),
606
657
  messageBuffer: new Uint8Array(0),
607
658
  isReading: false,
659
+ status: "connecting",
608
660
  lastActivity: Date.now(),
609
661
  writeQueue: Promise.resolve(),
610
662
  activeProgressTokens: new Set(),
611
663
  projectLoaded,
612
664
  resolveProjectLoaded,
613
665
  };
614
- clients.set(key, client);
615
666
 
616
667
  // Register crash recovery - remove client on process exit
617
668
  proc.exited.then(() => {
618
- clients.delete(key);
619
- clientLocks.delete(key);
669
+ if (clients.get(key) === client) clients.delete(key);
670
+ if (clientLocks.get(key) === clientPromise) clientLocks.delete(key);
620
671
  client.resolveProjectLoaded();
621
672
 
622
673
  // Reject any pending requests — the server is gone, they will never complete.
@@ -669,12 +720,26 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string, initT
669
720
  // Send initialized notification
670
721
  await sendNotification(client, "initialized", {});
671
722
 
723
+ client.status = "ready";
724
+ // Publish only after init succeeds: pre-init clients are reachable
725
+ // solely through clientLocks, so concurrent callers (warmup vs first
726
+ // tool call) wait for init instead of using an unacknowledged client.
727
+ clients.set(key, client);
728
+ initFailures.delete(key);
672
729
  return client;
673
730
  } catch (err) {
674
731
  // Clean up on initialization failure
675
- clients.delete(key);
676
- clientLocks.delete(key);
732
+ client.status = "error";
733
+ if (clients.get(key) === client) clients.delete(key);
677
734
  proc.kill();
735
+ const message = err instanceof Error ? err.message : String(err);
736
+ // Negative-cache deterministic failures. Timeouts under a
737
+ // caller-shortened deadline (warmup/writethrough) are not cached —
738
+ // the server may simply be slow and a later call with the full
739
+ // deadline can still succeed.
740
+ if (!(initTimeoutMs !== undefined && message.includes("timed out"))) {
741
+ initFailures.set(key, { at: Date.now(), message });
742
+ }
678
743
  throw err;
679
744
  } finally {
680
745
  clientLocks.delete(key);
@@ -1067,7 +1132,22 @@ export async function sendNotification(client: LspClient, method: string, params
1067
1132
  export async function shutdownAll(): Promise<void> {
1068
1133
  const clientsToShutdown = Array.from(clients.values());
1069
1134
  clients.clear();
1070
- await Promise.allSettled(clientsToShutdown.map(client => shutdownClientInstance(client)));
1135
+ // Mid-initialize clients live only in clientLocks (publication is deferred
1136
+ // until init succeeds) — without this, their server processes outlive
1137
+ // shutdown. Failed init promises already cleaned up after themselves.
1138
+ const pendingClients = Array.from(clientLocks.values());
1139
+ clientLocks.clear();
1140
+ const seen = new Set<LspClient>(clientsToShutdown);
1141
+ await Promise.allSettled([
1142
+ ...clientsToShutdown.map(client => shutdownClientInstance(client)),
1143
+ ...pendingClients.map(pending =>
1144
+ pending.then(client => {
1145
+ if (seen.has(client)) return;
1146
+ seen.add(client);
1147
+ return shutdownClientInstance(client);
1148
+ }),
1149
+ ),
1150
+ ]);
1071
1151
  }
1072
1152
 
1073
1153
  /** Status of an LSP server */
@@ -1084,7 +1164,7 @@ export interface LspServerStatus {
1084
1164
  export function getActiveClients(): LspServerStatus[] {
1085
1165
  return Array.from(clients.values()).map(client => ({
1086
1166
  name: client.config.command,
1087
- status: "ready" as const,
1167
+ status: client.status,
1088
1168
  fileTypes: client.config.fileTypes,
1089
1169
  }));
1090
1170
  }
@@ -3,6 +3,7 @@
3
3
  * Uses Biome's CLI with JSON output instead of LSP (which has stale diagnostics issues).
4
4
  */
5
5
  import path from "node:path";
6
+ import { logger } from "@oh-my-pi/pi-utils";
6
7
  import type { Diagnostic, DiagnosticSeverity, LinterClient, ServerConfig } from "../../lsp/types";
7
8
 
8
9
  // =============================================================================
@@ -29,17 +30,23 @@ interface BiomeDiagnostic {
29
30
  // =============================================================================
30
31
 
31
32
  /**
32
- * Convert byte offset to line:column using source code.
33
+ * Convert byte offsets to line:column positions in a single pass over the source.
33
34
  */
34
- function offsetToPosition(source: string, offset: number): { line: number; column: number } {
35
+ function offsetsToPositions(source: string, offsets: number[]): Map<number, { line: number; column: number }> {
36
+ const sorted = [...new Set(offsets)].sort((a, b) => a - b);
37
+ const result = new Map<number, { line: number; column: number }>();
35
38
  let line = 1;
36
39
  let column = 1;
37
40
  let byteIndex = 0;
41
+ let next = 0;
38
42
 
39
43
  for (const ch of source) {
40
- const byteLen = Buffer.byteLength(ch);
41
- if (byteIndex + byteLen > offset) {
42
- break;
44
+ if (next >= sorted.length) break;
45
+ const cp = ch.codePointAt(0) as number;
46
+ const byteLen = cp < 0x80 ? 1 : cp < 0x800 ? 2 : cp < 0x10000 ? 3 : 4;
47
+ while (next < sorted.length && byteIndex + byteLen > sorted[next]) {
48
+ result.set(sorted[next], { line, column });
49
+ next++;
43
50
  }
44
51
  if (ch === "\n") {
45
52
  line++;
@@ -50,7 +57,13 @@ function offsetToPosition(source: string, offset: number): { line: number; colum
50
57
  byteIndex += byteLen;
51
58
  }
52
59
 
53
- return { line, column };
60
+ // Offsets at or past end-of-file map to the final position.
61
+ while (next < sorted.length) {
62
+ result.set(sorted[next], { line, column });
63
+ next++;
64
+ }
65
+
66
+ return result;
54
67
  }
55
68
 
56
69
  /**
@@ -98,6 +111,16 @@ async function runBiome(
98
111
  }
99
112
  }
100
113
 
114
+ // Surface broken-binary / CLI failures once instead of silently reporting
115
+ // "no diagnostics" forever (and instead of spamming every writethrough).
116
+ const reportedBiomeFailures = new Set<string>();
117
+
118
+ function warnBiomeOnce(key: string, message: string, meta: Record<string, unknown>): void {
119
+ if (reportedBiomeFailures.has(key)) return;
120
+ reportedBiomeFailures.add(key);
121
+ logger.warn(message, meta);
122
+ }
123
+
101
124
  // =============================================================================
102
125
  // Biome Client
103
126
  // =============================================================================
@@ -137,6 +160,16 @@ export class BiomeClient implements LinterClient {
137
160
  // Run biome lint with JSON reporter
138
161
  const result = await runBiome(["lint", "--reporter=json", filePath], this.cwd, this.config.resolvedCommand);
139
162
 
163
+ // Biome exits non-zero when diagnostics are found, so only an empty
164
+ // stdout signals an actual run failure (missing binary, CLI error).
165
+ if (!result.success && result.stdout.trim().length === 0) {
166
+ warnBiomeOnce(`run:${this.cwd}`, "Biome lint failed; reporting no diagnostics", {
167
+ cwd: this.cwd,
168
+ stderr: result.stderr.slice(0, 500),
169
+ });
170
+ return [];
171
+ }
172
+
140
173
  return this.#parseJsonOutput(result.stdout, filePath);
141
174
  }
142
175
 
@@ -146,51 +179,80 @@ export class BiomeClient implements LinterClient {
146
179
  #parseJsonOutput(jsonOutput: string, targetFile: string): Diagnostic[] {
147
180
  const diagnostics: Diagnostic[] = [];
148
181
 
182
+ let parsed: BiomeJsonOutput;
149
183
  try {
150
- const parsed: BiomeJsonOutput = JSON.parse(jsonOutput);
184
+ parsed = JSON.parse(jsonOutput);
185
+ } catch {
186
+ warnBiomeOnce(`parse:${this.cwd}`, "Failed to parse Biome JSON output; reporting no diagnostics", {
187
+ cwd: this.cwd,
188
+ file: targetFile,
189
+ });
190
+ return diagnostics;
191
+ }
151
192
 
152
- for (const diag of parsed.diagnostics) {
153
- const location = diag.location;
154
- if (!location?.path?.file) continue;
193
+ const target = path.resolve(targetFile);
194
+ const relevant: BiomeDiagnostic[] = [];
195
+ // Batch all span offsets per source text so each source is scanned once
196
+ // instead of twice per diagnostic.
197
+ const offsetsBySource = new Map<string, number[]>();
198
+ for (const diag of parsed.diagnostics ?? []) {
199
+ const location = diag.location;
200
+ if (!location?.path?.file) continue;
155
201
 
156
- // Resolve file path
157
- const diagFile = path.isAbsolute(location.path.file)
158
- ? location.path.file
159
- : path.join(this.cwd, location.path.file);
202
+ // Resolve file path
203
+ const diagFile = path.isAbsolute(location.path.file)
204
+ ? location.path.file
205
+ : path.join(this.cwd, location.path.file);
160
206
 
161
- // Only include diagnostics for the target file
162
- if (path.resolve(diagFile) !== path.resolve(targetFile)) {
163
- continue;
164
- }
207
+ // Only include diagnostics for the target file
208
+ if (path.resolve(diagFile) !== target) {
209
+ continue;
210
+ }
211
+
212
+ relevant.push(diag);
213
+ if (location.span && location.sourceCode) {
214
+ const offsets = offsetsBySource.get(location.sourceCode);
215
+ if (offsets) offsets.push(location.span[0], location.span[1]);
216
+ else offsetsBySource.set(location.sourceCode, [location.span[0], location.span[1]]);
217
+ }
218
+ }
219
+
220
+ const positionsBySource = new Map<string, Map<number, { line: number; column: number }>>();
221
+ for (const [source, offsets] of offsetsBySource) {
222
+ positionsBySource.set(source, offsetsToPositions(source, offsets));
223
+ }
165
224
 
166
- // Convert byte offset to line:column
167
- let startLine = 1;
168
- let startColumn = 1;
169
- let endLine = 1;
170
- let endColumn = 1;
225
+ for (const diag of relevant) {
226
+ const location = diag.location;
227
+ let startLine = 1;
228
+ let startColumn = 1;
229
+ let endLine = 1;
230
+ let endColumn = 1;
171
231
 
172
- if (location.span && location.sourceCode) {
173
- const startPos = offsetToPosition(location.sourceCode, location.span[0]);
174
- const endPos = offsetToPosition(location.sourceCode, location.span[1]);
232
+ if (location?.span && location.sourceCode) {
233
+ const positions = positionsBySource.get(location.sourceCode);
234
+ const startPos = positions?.get(location.span[0]);
235
+ const endPos = positions?.get(location.span[1]);
236
+ if (startPos) {
175
237
  startLine = startPos.line;
176
238
  startColumn = startPos.column;
239
+ }
240
+ if (endPos) {
177
241
  endLine = endPos.line;
178
242
  endColumn = endPos.column;
179
243
  }
180
-
181
- diagnostics.push({
182
- range: {
183
- start: { line: startLine - 1, character: startColumn - 1 },
184
- end: { line: endLine - 1, character: endColumn - 1 },
185
- },
186
- severity: parseSeverity(diag.severity),
187
- message: diag.description,
188
- source: "biome",
189
- code: diag.category,
190
- });
191
244
  }
192
- } catch {
193
- // JSON parse failed, return empty
245
+
246
+ diagnostics.push({
247
+ range: {
248
+ start: { line: startLine - 1, character: startColumn - 1 },
249
+ end: { line: endLine - 1, character: endColumn - 1 },
250
+ },
251
+ severity: parseSeverity(diag.severity),
252
+ message: diag.description,
253
+ source: "biome",
254
+ code: diag.category,
255
+ });
194
256
  }
195
257
 
196
258
  return diagnostics;