@oh-my-pi/pi-coding-agent 8.1.0 → 8.2.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 (402) hide show
  1. package/CHANGELOG.md +21 -1
  2. package/docs/session.md +111 -46
  3. package/examples/custom-tools/hello/index.ts +1 -1
  4. package/examples/custom-tools/todo/index.ts +3 -4
  5. package/examples/extensions/api-demo.ts +0 -1
  6. package/examples/extensions/chalk-logger.ts +2 -3
  7. package/examples/extensions/hello.ts +0 -1
  8. package/examples/extensions/pirate.ts +0 -1
  9. package/examples/extensions/plan-mode.ts +15 -16
  10. package/examples/extensions/todo.ts +3 -4
  11. package/examples/extensions/tools.ts +1 -2
  12. package/examples/extensions/with-deps/index.ts +0 -1
  13. package/examples/hooks/auto-commit-on-exit.ts +1 -2
  14. package/examples/hooks/confirm-destructive.ts +0 -1
  15. package/examples/hooks/custom-compaction.ts +1 -2
  16. package/examples/hooks/dirty-repo-guard.ts +0 -1
  17. package/examples/hooks/file-trigger.ts +3 -4
  18. package/examples/hooks/git-checkpoint.ts +0 -1
  19. package/examples/hooks/handoff.ts +3 -4
  20. package/examples/hooks/permission-gate.ts +1 -2
  21. package/examples/hooks/protected-paths.ts +1 -2
  22. package/examples/hooks/qna.ts +2 -3
  23. package/examples/hooks/snake.ts +4 -5
  24. package/examples/hooks/status-line.ts +0 -1
  25. package/examples/sdk/01-minimal.ts +2 -3
  26. package/examples/sdk/02-custom-model.ts +2 -3
  27. package/examples/sdk/03-custom-prompt.ts +3 -4
  28. package/examples/sdk/04-skills.ts +2 -3
  29. package/examples/sdk/06-extensions.ts +1 -2
  30. package/examples/sdk/06-hooks.ts +6 -7
  31. package/examples/sdk/07-context-files.ts +0 -1
  32. package/examples/sdk/08-prompt-templates.ts +0 -1
  33. package/examples/sdk/08-slash-commands.ts +0 -1
  34. package/examples/sdk/09-api-keys-and-oauth.ts +0 -1
  35. package/examples/sdk/10-settings.ts +0 -1
  36. package/examples/sdk/11-sessions.ts +0 -1
  37. package/package.json +51 -23
  38. package/scripts/format-prompts.ts +0 -1
  39. package/src/capability/context-file.ts +2 -3
  40. package/src/capability/extension-module.ts +2 -3
  41. package/src/capability/extension.ts +2 -3
  42. package/src/capability/fs.ts +20 -21
  43. package/src/capability/hook.ts +2 -3
  44. package/src/capability/index.ts +15 -16
  45. package/src/capability/instruction.ts +2 -3
  46. package/src/capability/mcp.ts +2 -3
  47. package/src/capability/prompt.ts +2 -3
  48. package/src/capability/rule.ts +2 -3
  49. package/src/capability/settings.ts +1 -2
  50. package/src/capability/skill.ts +2 -3
  51. package/src/capability/slash-command.ts +2 -3
  52. package/src/capability/ssh.ts +2 -3
  53. package/src/capability/system-prompt.ts +2 -3
  54. package/src/capability/tool.ts +2 -3
  55. package/src/cli/args.ts +5 -6
  56. package/src/cli/config-cli.ts +6 -7
  57. package/src/cli/file-processor.ts +19 -17
  58. package/src/cli/jupyter-cli.ts +105 -0
  59. package/src/cli/list-models.ts +10 -11
  60. package/src/cli/plugin-cli.ts +20 -21
  61. package/src/cli/session-picker.ts +2 -3
  62. package/src/cli/setup-cli.ts +2 -3
  63. package/src/cli/stats-cli.ts +2 -3
  64. package/src/cli/update-cli.ts +25 -22
  65. package/src/commit/agentic/agent.ts +21 -23
  66. package/src/commit/agentic/fallback.ts +9 -9
  67. package/src/commit/agentic/index.ts +30 -38
  68. package/src/commit/agentic/state.ts +1 -6
  69. package/src/commit/agentic/tools/analyze-file.ts +15 -15
  70. package/src/commit/agentic/tools/git-file-diff.ts +3 -3
  71. package/src/commit/agentic/tools/git-hunk.ts +7 -7
  72. package/src/commit/agentic/tools/git-overview.ts +5 -5
  73. package/src/commit/agentic/tools/index.ts +14 -14
  74. package/src/commit/agentic/tools/propose-changelog.ts +6 -6
  75. package/src/commit/agentic/tools/propose-commit.ts +8 -8
  76. package/src/commit/agentic/tools/recent-commits.ts +2 -2
  77. package/src/commit/agentic/tools/split-commit.ts +19 -23
  78. package/src/commit/agentic/topo-sort.ts +1 -1
  79. package/src/commit/agentic/trivial.ts +3 -3
  80. package/src/commit/agentic/validation.ts +12 -12
  81. package/src/commit/analysis/conventional.ts +7 -11
  82. package/src/commit/analysis/index.ts +4 -4
  83. package/src/commit/analysis/scope.ts +4 -4
  84. package/src/commit/analysis/summary.ts +7 -9
  85. package/src/commit/analysis/validation.ts +1 -1
  86. package/src/commit/changelog/detect.ts +6 -6
  87. package/src/commit/changelog/generate.ts +7 -9
  88. package/src/commit/changelog/index.ts +13 -13
  89. package/src/commit/changelog/parse.ts +2 -2
  90. package/src/commit/cli.ts +1 -1
  91. package/src/commit/git/diff.ts +3 -3
  92. package/src/commit/git/index.ts +19 -24
  93. package/src/commit/index.ts +1 -1
  94. package/src/commit/map-reduce/index.ts +9 -9
  95. package/src/commit/map-reduce/map-phase.ts +19 -34
  96. package/src/commit/map-reduce/reduce-phase.ts +9 -11
  97. package/src/commit/message.ts +2 -2
  98. package/src/commit/model-selection.ts +3 -7
  99. package/src/commit/pipeline.ts +20 -22
  100. package/src/commit/utils/exclusions.ts +3 -3
  101. package/src/config/file-lock.ts +17 -7
  102. package/src/config/keybindings.ts +6 -8
  103. package/src/config/model-registry.ts +55 -37
  104. package/src/config/model-resolver.ts +18 -19
  105. package/src/config/prompt-templates.ts +11 -11
  106. package/src/config/settings-manager.ts +50 -34
  107. package/src/config.ts +60 -62
  108. package/src/cursor.ts +11 -9
  109. package/src/discovery/agents-md.ts +11 -12
  110. package/src/discovery/builtin.ts +68 -73
  111. package/src/discovery/claude.ts +41 -42
  112. package/src/discovery/cline.ts +11 -12
  113. package/src/discovery/codex.ts +52 -53
  114. package/src/discovery/cursor.ts +9 -10
  115. package/src/discovery/gemini.ts +17 -22
  116. package/src/discovery/github.ts +13 -14
  117. package/src/discovery/helpers.ts +35 -34
  118. package/src/discovery/index.ts +16 -18
  119. package/src/discovery/mcp-json.ts +8 -9
  120. package/src/discovery/ssh.ts +8 -9
  121. package/src/discovery/vscode.ts +4 -5
  122. package/src/discovery/windsurf.ts +6 -7
  123. package/src/exa/company.ts +1 -2
  124. package/src/exa/index.ts +2 -3
  125. package/src/exa/linkedin.ts +1 -2
  126. package/src/exa/mcp-client.ts +14 -16
  127. package/src/exa/render.ts +10 -11
  128. package/src/exa/researcher.ts +1 -2
  129. package/src/exa/search.ts +1 -2
  130. package/src/exa/types.ts +0 -1
  131. package/src/exa/websets.ts +1 -2
  132. package/src/exec/bash-executor.ts +3 -4
  133. package/src/exec/exec.ts +0 -1
  134. package/src/export/custom-share.ts +5 -6
  135. package/src/export/html/index.ts +24 -21
  136. package/src/export/ttsr.ts +2 -3
  137. package/src/extensibility/custom-commands/bundled/review/index.ts +7 -8
  138. package/src/extensibility/custom-commands/loader.ts +17 -14
  139. package/src/extensibility/custom-commands/types.ts +1 -2
  140. package/src/extensibility/custom-tools/loader.ts +10 -11
  141. package/src/extensibility/custom-tools/types.ts +6 -7
  142. package/src/extensibility/custom-tools/wrapper.ts +2 -3
  143. package/src/extensibility/extensions/loader.ts +75 -53
  144. package/src/extensibility/extensions/runner.ts +11 -12
  145. package/src/extensibility/extensions/types.ts +19 -26
  146. package/src/extensibility/extensions/wrapper.ts +3 -4
  147. package/src/extensibility/hooks/index.ts +1 -1
  148. package/src/extensibility/hooks/loader.ts +8 -9
  149. package/src/extensibility/hooks/runner.ts +7 -8
  150. package/src/extensibility/hooks/tool-wrapper.ts +0 -1
  151. package/src/extensibility/hooks/types.ts +10 -17
  152. package/src/extensibility/plugins/doctor.ts +3 -3
  153. package/src/extensibility/plugins/installer.ts +27 -27
  154. package/src/extensibility/plugins/loader.ts +59 -56
  155. package/src/extensibility/plugins/manager.ts +211 -171
  156. package/src/extensibility/plugins/parser.ts +1 -1
  157. package/src/extensibility/plugins/paths.ts +8 -8
  158. package/src/extensibility/skills.ts +63 -60
  159. package/src/extensibility/slash-commands.ts +10 -10
  160. package/src/index.ts +46 -46
  161. package/src/internal-urls/agent-protocol.ts +21 -11
  162. package/src/internal-urls/artifact-protocol.ts +17 -13
  163. package/src/internal-urls/router.ts +1 -2
  164. package/src/internal-urls/rule-protocol.ts +3 -4
  165. package/src/internal-urls/skill-protocol.ts +3 -4
  166. package/src/ipy/executor.ts +14 -10
  167. package/src/ipy/gateway-coordinator.ts +79 -90
  168. package/src/ipy/kernel.ts +32 -30
  169. package/src/ipy/modules.ts +13 -13
  170. package/src/lsp/client.ts +21 -10
  171. package/src/lsp/clients/biome-client.ts +1 -2
  172. package/src/lsp/clients/index.ts +3 -3
  173. package/src/lsp/clients/lsp-linter-client.ts +4 -5
  174. package/src/lsp/config.ts +15 -15
  175. package/src/lsp/edits.ts +4 -5
  176. package/src/lsp/index.ts +43 -44
  177. package/src/lsp/lspmux.ts +8 -8
  178. package/src/lsp/render.ts +10 -16
  179. package/src/lsp/utils.ts +3 -3
  180. package/src/main.ts +55 -34
  181. package/src/mcp/client.ts +2 -3
  182. package/src/mcp/config.ts +5 -6
  183. package/src/mcp/json-rpc.ts +0 -1
  184. package/src/mcp/loader.ts +3 -4
  185. package/src/mcp/manager.ts +17 -18
  186. package/src/mcp/tool-bridge.ts +4 -9
  187. package/src/mcp/tool-cache.ts +2 -3
  188. package/src/mcp/transports/http.ts +2 -4
  189. package/src/mcp/transports/stdio.ts +1 -2
  190. package/src/migrations.ts +60 -49
  191. package/src/modes/components/armin.ts +4 -5
  192. package/src/modes/components/assistant-message.ts +6 -6
  193. package/src/modes/components/bash-execution.ts +7 -8
  194. package/src/modes/components/bordered-loader.ts +3 -3
  195. package/src/modes/components/branch-summary-message.ts +3 -3
  196. package/src/modes/components/compaction-summary-message.ts +3 -3
  197. package/src/modes/components/countdown-timer.ts +0 -1
  198. package/src/modes/components/custom-message.ts +5 -5
  199. package/src/modes/components/diff.ts +1 -1
  200. package/src/modes/components/dynamic-border.ts +2 -2
  201. package/src/modes/components/extensions/extension-dashboard.ts +6 -7
  202. package/src/modes/components/extensions/extension-list.ts +2 -3
  203. package/src/modes/components/extensions/inspector-panel.ts +3 -4
  204. package/src/modes/components/extensions/state-manager.ts +25 -26
  205. package/src/modes/components/extensions/types.ts +1 -2
  206. package/src/modes/components/footer.ts +47 -43
  207. package/src/modes/components/history-search.ts +2 -2
  208. package/src/modes/components/hook-editor.ts +3 -4
  209. package/src/modes/components/hook-input.ts +2 -3
  210. package/src/modes/components/hook-message.ts +5 -5
  211. package/src/modes/components/hook-selector.ts +2 -3
  212. package/src/modes/components/keybinding-hints.ts +2 -3
  213. package/src/modes/components/login-dialog.ts +2 -2
  214. package/src/modes/components/model-selector.ts +12 -12
  215. package/src/modes/components/oauth-selector.ts +2 -2
  216. package/src/modes/components/plugin-settings.ts +20 -20
  217. package/src/modes/components/python-execution.ts +7 -8
  218. package/src/modes/components/queue-mode-selector.ts +3 -3
  219. package/src/modes/components/read-tool-group.ts +2 -2
  220. package/src/modes/components/session-selector.ts +4 -4
  221. package/src/modes/components/settings-defs.ts +77 -69
  222. package/src/modes/components/settings-selector.ts +16 -16
  223. package/src/modes/components/show-images-selector.ts +2 -2
  224. package/src/modes/components/status-line/segments.ts +4 -4
  225. package/src/modes/components/status-line/separators.ts +1 -1
  226. package/src/modes/components/status-line/types.ts +2 -2
  227. package/src/modes/components/status-line-segment-editor.ts +7 -8
  228. package/src/modes/components/status-line.ts +12 -12
  229. package/src/modes/components/theme-selector.ts +8 -7
  230. package/src/modes/components/thinking-selector.ts +4 -4
  231. package/src/modes/components/todo-display.ts +2 -2
  232. package/src/modes/components/todo-reminder.ts +4 -4
  233. package/src/modes/components/tool-execution.ts +11 -16
  234. package/src/modes/components/tree-selector.ts +11 -11
  235. package/src/modes/components/ttsr-notification.ts +5 -5
  236. package/src/modes/components/user-message-selector.ts +1 -1
  237. package/src/modes/components/user-message.ts +1 -1
  238. package/src/modes/components/visual-truncate.ts +0 -1
  239. package/src/modes/components/welcome.ts +4 -4
  240. package/src/modes/controllers/command-controller.ts +46 -47
  241. package/src/modes/controllers/event-controller.ts +16 -20
  242. package/src/modes/controllers/extension-ui-controller.ts +40 -46
  243. package/src/modes/controllers/input-controller.ts +17 -18
  244. package/src/modes/controllers/selector-controller.ts +103 -91
  245. package/src/modes/index.ts +3 -3
  246. package/src/modes/interactive-mode.ts +27 -29
  247. package/src/modes/print-mode.ts +12 -13
  248. package/src/modes/rpc/rpc-client.ts +7 -8
  249. package/src/modes/rpc/rpc-mode.ts +24 -25
  250. package/src/modes/rpc/rpc-types.ts +3 -4
  251. package/src/modes/theme/mermaid-cache.ts +2 -2
  252. package/src/modes/theme/theme.ts +128 -53
  253. package/src/modes/types.ts +10 -10
  254. package/src/modes/utils/ui-helpers.ts +17 -17
  255. package/src/patch/applicator.ts +18 -19
  256. package/src/patch/diff.ts +1 -2
  257. package/src/patch/fuzzy.ts +1 -2
  258. package/src/patch/index.ts +10 -11
  259. package/src/patch/normalize.ts +4 -4
  260. package/src/patch/normative.ts +1 -2
  261. package/src/patch/parser.ts +8 -9
  262. package/src/patch/shared.ts +12 -13
  263. package/src/sdk.ts +60 -63
  264. package/src/session/agent-session.ts +83 -84
  265. package/src/session/agent-storage.ts +11 -11
  266. package/src/session/artifacts.ts +8 -9
  267. package/src/session/auth-storage.ts +25 -29
  268. package/src/session/compaction/branch-summarization.ts +7 -10
  269. package/src/session/compaction/compaction.ts +8 -19
  270. package/src/session/compaction/utils.ts +6 -9
  271. package/src/session/history-storage.ts +10 -10
  272. package/src/session/messages.ts +4 -5
  273. package/src/session/session-manager.ts +76 -65
  274. package/src/session/session-storage.ts +57 -69
  275. package/src/session/storage-migration.ts +2 -3
  276. package/src/session/streaming-output.ts +2 -2
  277. package/src/ssh/connection-manager.ts +43 -50
  278. package/src/ssh/ssh-executor.ts +2 -2
  279. package/src/ssh/sshfs-mount.ts +11 -18
  280. package/src/system-prompt.ts +27 -34
  281. package/src/task/agents.ts +45 -30
  282. package/src/task/commands.ts +6 -7
  283. package/src/task/discovery.ts +39 -76
  284. package/src/task/executor.ts +14 -15
  285. package/src/task/index.ts +33 -36
  286. package/src/task/output-manager.ts +3 -4
  287. package/src/task/parallel.ts +0 -1
  288. package/src/task/render.ts +19 -20
  289. package/src/task/subprocess-tool-registry.ts +1 -2
  290. package/src/task/worker-protocol.ts +3 -3
  291. package/src/task/worker.ts +32 -38
  292. package/src/task/worktree.ts +19 -19
  293. package/src/tools/ask.ts +8 -9
  294. package/src/tools/bash-interceptor.ts +1 -5
  295. package/src/tools/bash.ts +19 -18
  296. package/src/tools/calculator.ts +12 -12
  297. package/src/tools/complete.ts +3 -4
  298. package/src/tools/context.ts +2 -2
  299. package/src/tools/fetch.ts +23 -26
  300. package/src/tools/find.ts +15 -16
  301. package/src/tools/gemini-image.ts +14 -14
  302. package/src/tools/grep.ts +27 -27
  303. package/src/tools/index.ts +78 -56
  304. package/src/tools/list-limit.ts +1 -1
  305. package/src/tools/ls.ts +7 -7
  306. package/src/tools/notebook.ts +5 -5
  307. package/src/tools/output-meta.ts +3 -4
  308. package/src/tools/output-utils.ts +1 -1
  309. package/src/tools/path-utils.ts +5 -5
  310. package/src/tools/python.ts +36 -37
  311. package/src/tools/read.ts +23 -23
  312. package/src/tools/render-utils.ts +8 -9
  313. package/src/tools/renderers.ts +6 -7
  314. package/src/tools/review.ts +8 -11
  315. package/src/tools/ssh.ts +31 -30
  316. package/src/tools/todo-write.ts +13 -13
  317. package/src/tools/tool-errors.ts +3 -3
  318. package/src/tools/tool-result.ts +3 -8
  319. package/src/tools/write.ts +11 -16
  320. package/src/tui/code-cell.ts +3 -9
  321. package/src/tui/file-list.ts +3 -4
  322. package/src/tui/output-block.ts +1 -2
  323. package/src/tui/status-line.ts +2 -3
  324. package/src/tui/tree-list.ts +2 -3
  325. package/src/tui/types.ts +1 -2
  326. package/src/tui/utils.ts +2 -3
  327. package/src/utils/changelog.ts +9 -10
  328. package/src/utils/clipboard.ts +11 -11
  329. package/src/utils/file-mentions.ts +4 -10
  330. package/src/utils/frontmatter.ts +6 -3
  331. package/src/utils/fuzzy.ts +2 -2
  332. package/src/utils/image-convert.ts +1 -1
  333. package/src/utils/image-resize.ts +1 -1
  334. package/src/utils/mime.ts +2 -2
  335. package/src/utils/shell-snapshot.ts +11 -13
  336. package/src/utils/shell.ts +4 -5
  337. package/src/utils/title-generator.ts +8 -9
  338. package/src/utils/tools-manager.ts +23 -23
  339. package/src/vendor/photon/index.js +1099 -1059
  340. package/src/vendor/photon/photon_rs_bg.wasm +0 -0
  341. package/src/web/scrapers/artifacthub.ts +1 -1
  342. package/src/web/scrapers/arxiv.ts +2 -2
  343. package/src/web/scrapers/bluesky.ts +2 -2
  344. package/src/web/scrapers/cheatsh.ts +1 -1
  345. package/src/web/scrapers/chocolatey.ts +2 -2
  346. package/src/web/scrapers/choosealicense.ts +5 -5
  347. package/src/web/scrapers/cisa-kev.ts +1 -1
  348. package/src/web/scrapers/crossref.ts +2 -2
  349. package/src/web/scrapers/devto.ts +3 -3
  350. package/src/web/scrapers/discogs.ts +3 -4
  351. package/src/web/scrapers/discourse.ts +1 -1
  352. package/src/web/scrapers/dockerhub.ts +1 -1
  353. package/src/web/scrapers/fdroid.ts +2 -2
  354. package/src/web/scrapers/firefox-addons.ts +3 -3
  355. package/src/web/scrapers/flathub.ts +1 -1
  356. package/src/web/scrapers/github.ts +3 -3
  357. package/src/web/scrapers/gitlab.ts +4 -4
  358. package/src/web/scrapers/hackernews.ts +2 -2
  359. package/src/web/scrapers/huggingface.ts +1 -1
  360. package/src/web/scrapers/iacr.ts +2 -2
  361. package/src/web/scrapers/index.ts +0 -1
  362. package/src/web/scrapers/jetbrains-marketplace.ts +1 -1
  363. package/src/web/scrapers/lemmy.ts +2 -2
  364. package/src/web/scrapers/maven.ts +2 -2
  365. package/src/web/scrapers/mdn.ts +2 -4
  366. package/src/web/scrapers/metacpan.ts +2 -2
  367. package/src/web/scrapers/musicbrainz.ts +1 -2
  368. package/src/web/scrapers/npm.ts +1 -1
  369. package/src/web/scrapers/nuget.ts +2 -2
  370. package/src/web/scrapers/nvd.ts +3 -3
  371. package/src/web/scrapers/ollama.ts +7 -9
  372. package/src/web/scrapers/opencorporates.ts +2 -2
  373. package/src/web/scrapers/openlibrary.ts +6 -6
  374. package/src/web/scrapers/orcid.ts +0 -1
  375. package/src/web/scrapers/osv.ts +2 -2
  376. package/src/web/scrapers/packagist.ts +1 -1
  377. package/src/web/scrapers/pubmed.ts +1 -2
  378. package/src/web/scrapers/rawg.ts +2 -2
  379. package/src/web/scrapers/readthedocs.ts +1 -2
  380. package/src/web/scrapers/repology.ts +2 -2
  381. package/src/web/scrapers/rfc.ts +1 -1
  382. package/src/web/scrapers/searchcode.ts +2 -2
  383. package/src/web/scrapers/semantic-scholar.ts +1 -1
  384. package/src/web/scrapers/snapcraft.ts +2 -2
  385. package/src/web/scrapers/sourcegraph.ts +1 -1
  386. package/src/web/scrapers/spdx.ts +3 -3
  387. package/src/web/scrapers/spotify.ts +0 -1
  388. package/src/web/scrapers/twitter.ts +1 -1
  389. package/src/web/scrapers/types.ts +1 -2
  390. package/src/web/scrapers/utils.ts +5 -5
  391. package/src/web/scrapers/wikidata.ts +3 -3
  392. package/src/web/scrapers/youtube.ts +9 -14
  393. package/src/web/search/auth.ts +4 -9
  394. package/src/web/search/index.ts +11 -21
  395. package/src/web/search/providers/anthropic.ts +3 -9
  396. package/src/web/search/providers/exa.ts +6 -10
  397. package/src/web/search/providers/perplexity.ts +5 -5
  398. package/src/web/search/render.ts +16 -18
  399. package/scripts/generate-wasm-b64.ts +0 -24
  400. package/src/commit/map-reduce/.map-phase.ts.kate-swp +0 -0
  401. package/src/task/.executor.ts.kate-swp +0 -0
  402. package/src/vendor/photon/photon_rs_bg.wasm.b64.js +0 -1
@@ -1,6 +1,4 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { getAgentDir } from "@oh-my-pi/pi-coding-agent/config";
1
+ import * as path from "node:path";
4
2
  import {
5
3
  DEFAULT_EDITOR_KEYBINDINGS,
6
4
  type EditorAction,
@@ -11,6 +9,7 @@ import {
11
9
  setEditorKeybindings,
12
10
  } from "@oh-my-pi/pi-tui";
13
11
  import { logger } from "@oh-my-pi/pi-utils";
12
+ import { getAgentDir } from "../config";
14
13
 
15
14
  /**
16
15
  * Application-level actions (coding agent specific).
@@ -162,7 +161,7 @@ export class KeybindingsManager {
162
161
  * Create from config file and set up editor keybindings.
163
162
  */
164
163
  static async create(agentDir: string = getAgentDir()): Promise<KeybindingsManager> {
165
- const configPath = join(agentDir, "keybindings.json");
164
+ const configPath = path.join(agentDir, "keybindings.json");
166
165
  const config = await KeybindingsManager.loadFromFile(configPath);
167
166
  const manager = new KeybindingsManager(config);
168
167
 
@@ -186,9 +185,8 @@ export class KeybindingsManager {
186
185
  }
187
186
 
188
187
  private static async loadFromFile(path: string): Promise<KeybindingsConfig> {
189
- if (!existsSync(path)) return {};
190
188
  try {
191
- return JSON.parse(readFileSync(path, "utf-8"));
189
+ return await Bun.file(path).json();
192
190
  } catch (error) {
193
191
  logger.warn("Failed to parse keybindings config", { path, error: String(error) });
194
192
  return {};
@@ -203,7 +201,7 @@ export class KeybindingsManager {
203
201
  const keyArray = Array.isArray(keys) ? keys : [keys];
204
202
  this.appActionToKeys.set(
205
203
  action as AppAction,
206
- keyArray.map((key) => normalizeKeyId(key as KeyId)),
204
+ keyArray.map(key => normalizeKeyId(key as KeyId)),
207
205
  );
208
206
  }
209
207
 
@@ -213,7 +211,7 @@ export class KeybindingsManager {
213
211
  const keyArray = Array.isArray(keys) ? keys : [keys];
214
212
  this.appActionToKeys.set(
215
213
  action,
216
- keyArray.map((key) => normalizeKeyId(key as KeyId)),
214
+ keyArray.map(key => normalizeKeyId(key as KeyId)),
217
215
  );
218
216
  }
219
217
  }
@@ -1,9 +1,8 @@
1
1
  /**
2
2
  * Model registry - manages built-in and custom models, provides API key resolution.
3
3
  */
4
-
5
- import { existsSync, readFileSync } from "node:fs";
6
- import { extname } from "node:path";
4
+ import * as fs from "node:fs";
5
+ import * as path from "node:path";
7
6
  import {
8
7
  type Api,
9
8
  getGitHubCopilotBaseUrl,
@@ -12,11 +11,11 @@ import {
12
11
  type Model,
13
12
  normalizeDomain,
14
13
  } from "@oh-my-pi/pi-ai";
15
- import type { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage";
16
- import { logger } from "@oh-my-pi/pi-utils";
14
+ import { isEnoent, logger } from "@oh-my-pi/pi-utils";
17
15
  import { type Static, Type } from "@sinclair/typebox";
18
16
  import AjvModule from "ajv";
19
17
  import { YAML } from "bun";
18
+ import type { AuthStorage } from "../session/auth-storage";
20
19
 
21
20
  const Ajv = (AjvModule as any).default || AjvModule;
22
21
 
@@ -104,10 +103,12 @@ interface CustomModelsResult {
104
103
  /** Providers with only baseUrl/headers override (no custom models) */
105
104
  overrides: Map<string, ProviderOverride>;
106
105
  error: string | undefined;
106
+ /** Whether the file was found (true) or didn't exist (false) */
107
+ found: boolean;
107
108
  }
108
109
 
109
110
  function emptyCustomModelsResult(error?: string): CustomModelsResult {
110
- return { models: [], replacedProviders: new Set(), overrides: new Map(), error };
111
+ return { models: [], replacedProviders: new Set(), overrides: new Map(), error, found: false };
111
112
  }
112
113
 
113
114
  /**
@@ -139,7 +140,7 @@ export class ModelRegistry {
139
140
  private fallbackPaths: string[] = [],
140
141
  ) {
141
142
  // Set up fallback resolver for custom provider API keys
142
- this.authStorage.setFallbackResolver((provider) => {
143
+ this.authStorage.setFallbackResolver(provider => {
143
144
  const keyConfig = this.customProviderApiKeys.get(provider);
144
145
  if (keyConfig) {
145
146
  return resolveApiKeyConfig(keyConfig);
@@ -161,7 +162,7 @@ export class ModelRegistry {
161
162
  instance.customProviderApiKeys = new Map(Object.entries(data.customProviderApiKeys ?? {}));
162
163
  instance.loadError = data.loadError;
163
164
 
164
- authStorage.setFallbackResolver((provider) => {
165
+ authStorage.setFallbackResolver(provider => {
165
166
  const keyConfig = instance.customProviderApiKeys.get(provider);
166
167
  if (keyConfig) {
167
168
  return resolveApiKeyConfig(keyConfig);
@@ -203,7 +204,7 @@ export class ModelRegistry {
203
204
  return this.loadError;
204
205
  }
205
206
 
206
- private loadModels(): void {
207
+ private loadModels() {
207
208
  // Load custom models from models.json first (to know which providers to skip/override)
208
209
  let customModels: Model<Api>[] = [];
209
210
  let replacedProviders: Set<string> = new Set();
@@ -215,19 +216,20 @@ export class ModelRegistry {
215
216
  }
216
217
 
217
218
  for (const modelsPath of pathsToCheck) {
218
- if (existsSync(modelsPath)) {
219
- logger.debug("ModelRegistry.loadModels loading", { path: modelsPath });
220
- const result = this.loadCustomModels(modelsPath);
221
- if (result.error) {
222
- this.loadError = result.error;
223
- // Keep built-in models even if custom models failed to load
224
- } else {
225
- customModels = result.models;
226
- replacedProviders = result.replacedProviders;
227
- overrides = result.overrides;
228
- }
229
- break; // Use first existing file
219
+ const result = this.loadCustomModels(modelsPath);
220
+ if (!result.found) {
221
+ continue; // File doesn't exist, try next path
222
+ }
223
+ logger.debug("ModelRegistry.loadModels loading", { path: modelsPath });
224
+ if (result.error) {
225
+ this.loadError = result.error;
226
+ // Keep built-in models even if custom models failed to load
227
+ } else {
228
+ customModels = result.models;
229
+ replacedProviders = result.replacedProviders;
230
+ overrides = result.overrides;
230
231
  }
232
+ break; // Use first existing file
231
233
  }
232
234
 
233
235
  const builtInModels = this.loadBuiltInModels(replacedProviders, overrides);
@@ -240,7 +242,7 @@ export class ModelRegistry {
240
242
  ? (normalizeDomain(copilotCred.enterpriseUrl) ?? undefined)
241
243
  : undefined;
242
244
  const baseUrl = getGitHubCopilotBaseUrl(copilotCred.access, domain);
243
- this.models = combined.map((m) => (m.provider === "github-copilot" ? { ...m, baseUrl } : m));
245
+ this.models = combined.map(m => (m.provider === "github-copilot" ? { ...m, baseUrl } : m));
244
246
  } else {
245
247
  this.models = combined;
246
248
  }
@@ -249,14 +251,14 @@ export class ModelRegistry {
249
251
  /** Load built-in models, skipping replaced providers and applying overrides */
250
252
  private loadBuiltInModels(replacedProviders: Set<string>, overrides: Map<string, ProviderOverride>): Model<Api>[] {
251
253
  return getProviders()
252
- .filter((provider) => !replacedProviders.has(provider))
253
- .flatMap((provider) => {
254
+ .filter(provider => !replacedProviders.has(provider))
255
+ .flatMap(provider => {
254
256
  const models = getModels(provider as any) as Model<Api>[];
255
257
  const override = overrides.get(provider);
256
258
  if (!override) return models;
257
259
 
258
260
  // Apply baseUrl/headers override to all models of this provider
259
- return models.map((m) => ({
261
+ return models.map(m => ({
260
262
  ...m,
261
263
  baseUrl: override.baseUrl ?? m.baseUrl,
262
264
  headers: override.headers ? { ...m.headers, ...override.headers } : m.headers,
@@ -265,13 +267,23 @@ export class ModelRegistry {
265
267
  }
266
268
 
267
269
  private loadCustomModels(modelsPath: string): CustomModelsResult {
268
- if (!existsSync(modelsPath)) {
269
- return emptyCustomModelsResult();
270
+ let content: string;
271
+ try {
272
+ content = fs.readFileSync(modelsPath, "utf-8");
273
+ } catch (error) {
274
+ if (isEnoent(error)) {
275
+ return emptyCustomModelsResult();
276
+ }
277
+ return {
278
+ ...emptyCustomModelsResult(
279
+ `Failed to load models config: ${error instanceof Error ? error.message : error}\n\nFile: ${modelsPath}`,
280
+ ),
281
+ found: true,
282
+ };
270
283
  }
271
284
 
272
285
  try {
273
- const content = readFileSync(modelsPath, "utf-8");
274
- const ext = extname(modelsPath).toLowerCase();
286
+ const ext = path.extname(modelsPath).toLowerCase();
275
287
  let config: ModelsConfig;
276
288
 
277
289
  if (ext === ".yaml" || ext === ".yml") {
@@ -315,14 +327,20 @@ export class ModelRegistry {
315
327
  }
316
328
  }
317
329
 
318
- return { models: this.parseModels(config), replacedProviders, overrides, error: undefined };
330
+ return { models: this.parseModels(config), replacedProviders, overrides, error: undefined, found: true };
319
331
  } catch (error) {
320
332
  if (error instanceof SyntaxError) {
321
- return emptyCustomModelsResult(`Failed to parse models config: ${error.message}\n\nFile: ${modelsPath}`);
333
+ return {
334
+ ...emptyCustomModelsResult(`Failed to parse models config: ${error.message}\n\nFile: ${modelsPath}`),
335
+ found: true,
336
+ };
322
337
  }
323
- return emptyCustomModelsResult(
324
- `Failed to load models config: ${error instanceof Error ? error.message : error}\n\nFile: ${modelsPath}`,
325
- );
338
+ return {
339
+ ...emptyCustomModelsResult(
340
+ `Failed to load models config: ${error instanceof Error ? error.message : error}\n\nFile: ${modelsPath}`,
341
+ ),
342
+ found: true,
343
+ };
326
344
  }
327
345
  }
328
346
 
@@ -431,21 +449,21 @@ export class ModelRegistry {
431
449
  * This is a fast check that doesn't refresh OAuth tokens.
432
450
  */
433
451
  getAvailable(): Model<Api>[] {
434
- return this.models.filter((m) => this.authStorage.hasAuth(m.provider));
452
+ return this.models.filter(m => this.authStorage.hasAuth(m.provider));
435
453
  }
436
454
 
437
455
  /**
438
456
  * Find a model by provider and ID.
439
457
  */
440
458
  find(provider: string, modelId: string): Model<Api> | undefined {
441
- return this.models.find((m) => m.provider === provider && m.id === modelId);
459
+ return this.models.find(m => m.provider === provider && m.id === modelId);
442
460
  }
443
461
 
444
462
  /**
445
463
  * Get the base URL associated with a provider, if any model defines one.
446
464
  */
447
465
  getProviderBaseUrl(provider: string): string | undefined {
448
- return this.models.find((m) => m.provider === provider && m.baseUrl)?.baseUrl;
466
+ return this.models.find(m => m.provider === provider && m.baseUrl)?.baseUrl;
449
467
  }
450
468
 
451
469
  /**
@@ -1,11 +1,10 @@
1
1
  /**
2
2
  * Model resolution, scoping, and initial selection
3
3
  */
4
-
5
4
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
6
5
  import { type Api, type KnownProvider, type Model, modelsAreEqual } from "@oh-my-pi/pi-ai";
7
- import { isValidThinkingLevel } from "@oh-my-pi/pi-coding-agent/cli/args";
8
6
  import chalk from "chalk";
7
+ import { isValidThinkingLevel } from "../cli/args";
9
8
  import type { ModelRegistry } from "./model-registry";
10
9
 
11
10
  /** Default model IDs for each known provider */
@@ -84,7 +83,7 @@ function tryMatchModel(modelPattern: string, availableModels: Model<Api>[]): Mod
84
83
  const provider = modelPattern.substring(0, slashIndex);
85
84
  const modelId = modelPattern.substring(slashIndex + 1);
86
85
  const providerMatch = availableModels.find(
87
- (m) => m.provider.toLowerCase() === provider.toLowerCase() && m.id.toLowerCase() === modelId.toLowerCase(),
86
+ m => m.provider.toLowerCase() === provider.toLowerCase() && m.id.toLowerCase() === modelId.toLowerCase(),
88
87
  );
89
88
  if (providerMatch) {
90
89
  return providerMatch;
@@ -93,14 +92,14 @@ function tryMatchModel(modelPattern: string, availableModels: Model<Api>[]): Mod
93
92
  }
94
93
 
95
94
  // Check for exact ID match (case-insensitive)
96
- const exactMatch = availableModels.find((m) => m.id.toLowerCase() === modelPattern.toLowerCase());
95
+ const exactMatch = availableModels.find(m => m.id.toLowerCase() === modelPattern.toLowerCase());
97
96
  if (exactMatch) {
98
97
  return exactMatch;
99
98
  }
100
99
 
101
100
  // No exact match - fall back to partial matching
102
101
  const matches = availableModels.filter(
103
- (m) =>
102
+ m =>
104
103
  m.id.toLowerCase().includes(modelPattern.toLowerCase()) ||
105
104
  m.name?.toLowerCase().includes(modelPattern.toLowerCase()),
106
105
  );
@@ -110,8 +109,8 @@ function tryMatchModel(modelPattern: string, availableModels: Model<Api>[]): Mod
110
109
  }
111
110
 
112
111
  // Separate into aliases and dated versions
113
- const aliases = matches.filter((m) => isAlias(m.id));
114
- const datedVersions = matches.filter((m) => !isAlias(m.id));
112
+ const aliases = matches.filter(m => isAlias(m.id));
113
+ const datedVersions = matches.filter(m => !isAlias(m.id));
115
114
 
116
115
  if (aliases.length > 0) {
117
116
  // Prefer alias - if multiple aliases, pick the one that sorts highest
@@ -226,7 +225,7 @@ export async function resolveModelScope(patterns: string[], modelRegistry: Model
226
225
 
227
226
  // Match against "provider/modelId" format OR just model ID
228
227
  // This allows "*sonnet*" to match without requiring "anthropic/*sonnet*"
229
- const matchingModels = availableModels.filter((m) => {
228
+ const matchingModels = availableModels.filter(m => {
230
229
  const fullId = `${m.provider}/${m.id}`;
231
230
  const glob = new Bun.Glob(globPattern.toLowerCase());
232
231
  return glob.match(fullId.toLowerCase()) || glob.match(m.id.toLowerCase());
@@ -238,7 +237,7 @@ export async function resolveModelScope(patterns: string[], modelRegistry: Model
238
237
  }
239
238
 
240
239
  for (const model of matchingModels) {
241
- if (!scopedModels.find((sm) => modelsAreEqual(sm.model, model))) {
240
+ if (!scopedModels.find(sm => modelsAreEqual(sm.model, model))) {
242
241
  scopedModels.push({ model, thinkingLevel, explicitThinkingLevel });
243
242
  }
244
243
  }
@@ -257,7 +256,7 @@ export async function resolveModelScope(patterns: string[], modelRegistry: Model
257
256
  }
258
257
 
259
258
  // Avoid duplicates
260
- if (!scopedModels.find((sm) => modelsAreEqual(sm.model, model))) {
259
+ if (!scopedModels.find(sm => modelsAreEqual(sm.model, model))) {
261
260
  scopedModels.push({ model, thinkingLevel, explicitThinkingLevel });
262
261
  }
263
262
  }
@@ -343,7 +342,7 @@ export async function findInitialModel(options: {
343
342
  // Try to find a default model from known providers
344
343
  for (const provider of Object.keys(defaultModelPerProvider) as KnownProvider[]) {
345
344
  const defaultId = defaultModelPerProvider[provider];
346
- const match = availableModels.find((m) => m.provider === provider && m.id === defaultId);
345
+ const match = availableModels.find(m => m.provider === provider && m.id === defaultId);
347
346
  if (match) {
348
347
  return { model: match, thinkingLevel: "off", fallbackMessage: undefined };
349
348
  }
@@ -405,7 +404,7 @@ export async function restoreModelFromSession(
405
404
  let fallbackModel: Model<Api> | undefined;
406
405
  for (const provider of Object.keys(defaultModelPerProvider) as KnownProvider[]) {
407
406
  const defaultId = defaultModelPerProvider[provider];
408
- const match = availableModels.find((m) => m.provider === provider && m.id === defaultId);
407
+ const match = availableModels.find(m => m.provider === provider && m.id === defaultId);
409
408
  if (match) {
410
409
  fallbackModel = match;
411
410
  break;
@@ -450,7 +449,7 @@ export async function findSmolModel(
450
449
  if (savedModel) {
451
450
  const parsed = parseModelString(savedModel);
452
451
  if (parsed) {
453
- const match = availableModels.find((m) => m.provider === parsed.provider && m.id === parsed.id);
452
+ const match = availableModels.find(m => m.provider === parsed.provider && m.id === parsed.id);
454
453
  if (match) return match;
455
454
  }
456
455
  }
@@ -458,15 +457,15 @@ export async function findSmolModel(
458
457
  // 2. Try priority chain
459
458
  for (const pattern of SMOL_MODEL_PRIORITY) {
460
459
  // Try exact match with provider prefix
461
- const providerMatch = availableModels.find((m) => `${m.provider}/${m.id}`.toLowerCase() === pattern);
460
+ const providerMatch = availableModels.find(m => `${m.provider}/${m.id}`.toLowerCase() === pattern);
462
461
  if (providerMatch) return providerMatch;
463
462
 
464
463
  // Try exact match first
465
- const exactMatch = availableModels.find((m) => m.id.toLowerCase() === pattern);
464
+ const exactMatch = availableModels.find(m => m.id.toLowerCase() === pattern);
466
465
  if (exactMatch) return exactMatch;
467
466
 
468
467
  // Try fuzzy match (substring)
469
- const fuzzyMatch = availableModels.find((m) => m.id.toLowerCase().includes(pattern));
468
+ const fuzzyMatch = availableModels.find(m => m.id.toLowerCase().includes(pattern));
470
469
  if (fuzzyMatch) return fuzzyMatch;
471
470
  }
472
471
 
@@ -493,7 +492,7 @@ export async function findSlowModel(
493
492
  if (savedModel) {
494
493
  const parsed = parseModelString(savedModel);
495
494
  if (parsed) {
496
- const match = availableModels.find((m) => m.provider === parsed.provider && m.id === parsed.id);
495
+ const match = availableModels.find(m => m.provider === parsed.provider && m.id === parsed.id);
497
496
  if (match) return match;
498
497
  }
499
498
  }
@@ -501,11 +500,11 @@ export async function findSlowModel(
501
500
  // 2. Try priority chain
502
501
  for (const pattern of SLOW_MODEL_PRIORITY) {
503
502
  // Try exact match first
504
- const exactMatch = availableModels.find((m) => m.id.toLowerCase() === pattern.toLowerCase());
503
+ const exactMatch = availableModels.find(m => m.id.toLowerCase() === pattern.toLowerCase());
505
504
  if (exactMatch) return exactMatch;
506
505
 
507
506
  // Try fuzzy match (substring)
508
- const fuzzyMatch = availableModels.find((m) => m.id.toLowerCase().includes(pattern.toLowerCase()));
507
+ const fuzzyMatch = availableModels.find(m => m.id.toLowerCase().includes(pattern.toLowerCase()));
509
508
  if (fuzzyMatch) return fuzzyMatch;
510
509
  }
511
510
 
@@ -1,8 +1,8 @@
1
- import { join, resolve } from "node:path";
2
- import { CONFIG_DIR_NAME, getPromptsDir } from "@oh-my-pi/pi-coding-agent/config";
3
- import { parseFrontmatter } from "@oh-my-pi/pi-coding-agent/utils/frontmatter";
1
+ import * as path from "node:path";
4
2
  import { logger } from "@oh-my-pi/pi-utils";
5
3
  import Handlebars from "handlebars";
4
+ import { CONFIG_DIR_NAME, getPromptsDir } from "../config";
5
+ import { parseFrontmatter } from "../utils/frontmatter";
6
6
 
7
7
  /**
8
8
  * Represents a prompt template loaded from a markdown file
@@ -44,7 +44,7 @@ handlebars.registerHelper(
44
44
  const suffix = (options.hash.suffix as string) ?? "";
45
45
  const rawSeparator = (options.hash.join as string) ?? "\n";
46
46
  const separator = rawSeparator.replace(/\\n/g, "\n").replace(/\\t/g, "\t");
47
- return context.map((item) => `${prefix}${options.fn(item)}${suffix}`).join(separator);
47
+ return context.map(item => `${prefix}${options.fn(item)}${suffix}`).join(separator);
48
48
  },
49
49
  );
50
50
 
@@ -126,7 +126,7 @@ handlebars.registerHelper(
126
126
  const headers = headersStr?.split("|") ?? [];
127
127
  const separator = headers.map(() => "---").join(" | ");
128
128
  const headerRow = headers.length > 0 ? `| ${headers.join(" | ")} |\n| ${separator} |\n` : "";
129
- const rows = context.map((item) => `| ${options.fn(item).trim()} |`).join("\n");
129
+ const rows = context.map(item => `| ${options.fn(item).trim()} |`).join("\n");
130
130
  return headerRow + rows;
131
131
  },
132
132
  );
@@ -235,7 +235,7 @@ function optimizePromptLayout(input: string): string {
235
235
  // normalize NBSP -> space
236
236
  s = s.replace(/\u00A0/g, " ");
237
237
 
238
- const lines = s.split("\n").map((line) => {
238
+ const lines = s.split("\n").map(line => {
239
239
  // 2) remove trailing whitespace (spaces/tabs) per line
240
240
  let l = line.replace(/[ \t]+$/g, "");
241
241
 
@@ -382,7 +382,7 @@ async function loadTemplatesFromDir(
382
382
  entries.sort((a, b) => a.split("/").length - b.split("/").length);
383
383
 
384
384
  for (const entry of entries) {
385
- const fullPath = join(dir, entry);
385
+ const fullPath = path.join(dir, entry);
386
386
  const file = Bun.file(fullPath);
387
387
 
388
388
  try {
@@ -409,7 +409,7 @@ async function loadTemplatesFromDir(
409
409
  // Get description from frontmatter or first non-empty line
410
410
  let description = String(frontmatter.description || "");
411
411
  if (!description) {
412
- const firstLine = body.split("\n").find((line) => line.trim());
412
+ const firstLine = body.split("\n").find(line => line.trim());
413
413
  if (firstLine) {
414
414
  // Truncate if too long
415
415
  description = firstLine.slice(0, 60);
@@ -461,11 +461,11 @@ export async function loadPromptTemplates(options: LoadPromptTemplatesOptions =
461
461
 
462
462
  // 1. Load global templates from agentDir/prompts/
463
463
  // Note: if agentDir is provided, it should be the agent dir, not the prompts dir
464
- const globalPromptsDir = options.agentDir ? join(options.agentDir, "prompts") : resolvedAgentDir;
464
+ const globalPromptsDir = options.agentDir ? path.join(options.agentDir, "prompts") : resolvedAgentDir;
465
465
  templates.push(...(await loadTemplatesFromDir(globalPromptsDir, "user")));
466
466
 
467
467
  // 2. Load project templates from cwd/{CONFIG_DIR_NAME}/prompts/
468
- const projectPromptsDir = resolve(resolvedCwd, CONFIG_DIR_NAME, "prompts");
468
+ const projectPromptsDir = path.resolve(resolvedCwd, CONFIG_DIR_NAME, "prompts");
469
469
  templates.push(...(await loadTemplatesFromDir(projectPromptsDir, "project")));
470
470
 
471
471
  return templates;
@@ -482,7 +482,7 @@ export function expandPromptTemplate(text: string, templates: PromptTemplate[]):
482
482
  const templateName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
483
483
  const argsString = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1);
484
484
 
485
- const template = templates.find((t) => t.name === templateName);
485
+ const template = templates.find(t => t.name === templateName);
486
486
  if (template) {
487
487
  const args = parseCommandArgs(argsString);
488
488
  const argsText = args.join(" ");
@@ -1,14 +1,13 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { rename } from "node:fs/promises";
3
- import { join } from "node:path";
4
- import { type Settings as SettingsItem, settingsCapability } from "@oh-my-pi/pi-coding-agent/capability/settings";
5
- import { getAgentDbPath, getAgentDir } from "@oh-my-pi/pi-coding-agent/config";
6
- import { withFileLock } from "@oh-my-pi/pi-coding-agent/config/file-lock";
7
- import { loadCapability } from "@oh-my-pi/pi-coding-agent/discovery";
8
- import type { SymbolPreset } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
9
- import { AgentStorage } from "@oh-my-pi/pi-coding-agent/session/agent-storage";
10
- import { logger } from "@oh-my-pi/pi-utils";
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import { isEnoent, logger } from "@oh-my-pi/pi-utils";
11
4
  import { YAML } from "bun";
5
+ import { type Settings as SettingsItem, settingsCapability } from "../capability/settings";
6
+ import { getAgentDbPath, getAgentDir } from "../config";
7
+ import { loadCapability } from "../discovery";
8
+ import type { SymbolPreset } from "../modes/theme/theme";
9
+ import { AgentStorage } from "../session/agent-storage";
10
+ import { withFileLock } from "./file-lock";
12
11
 
13
12
  export interface CompactionSettings {
14
13
  enabled?: boolean; // default: true
@@ -203,6 +202,7 @@ export interface Settings {
203
202
  interruptMode?: "immediate" | "wait";
204
203
  theme?: string;
205
204
  symbolPreset?: SymbolPreset; // default: uses theme's preset or "unicode"
205
+ colorBlindMode?: boolean; // default: false (use blue instead of green for diff additions)
206
206
  compaction?: CompactionSettings;
207
207
  branchSummary?: BranchSummarySettings;
208
208
  retry?: RetrySettings;
@@ -343,7 +343,7 @@ function normalizeBashInterceptorSettings(
343
343
  patterns = DEFAULT_BASH_INTERCEPTOR_RULES;
344
344
  } else if (Array.isArray(rawPatterns)) {
345
345
  patterns = rawPatterns
346
- .map((rule) => normalizeBashInterceptorRule(rule))
346
+ .map(rule => normalizeBashInterceptorRule(rule))
347
347
  .filter((rule): rule is BashInterceptorRule => rule !== null);
348
348
  } else {
349
349
  patterns = DEFAULT_BASH_INTERCEPTOR_RULES;
@@ -372,7 +372,7 @@ function hasNerdFonts(): boolean {
372
372
  const termProgram = (process.env.TERM_PROGRAM || "").toLowerCase();
373
373
  const term = (process.env.TERM || "").toLowerCase();
374
374
  const nerdTerms = ["iterm", "wezterm", "kitty", "ghostty", "alacritty"];
375
- cachedNerdFonts = nerdTerms.some((candidate) => termProgram.includes(candidate) || term.includes(candidate));
375
+ cachedNerdFonts = nerdTerms.some(candidate => termProgram.includes(candidate) || term.includes(candidate));
376
376
  return cachedNerdFonts;
377
377
  }
378
378
 
@@ -503,7 +503,7 @@ export class SettingsManager {
503
503
  * @returns Configured SettingsManager with merged global and user settings
504
504
  */
505
505
  static async create(cwd: string = process.cwd(), agentDir: string = getAgentDir()): Promise<SettingsManager> {
506
- const configPath = join(agentDir, "config.yml");
506
+ const configPath = path.join(agentDir, "config.yml");
507
507
  const storage = await AgentStorage.open(getAgentDbPath(agentDir));
508
508
 
509
509
  // Migrate from legacy storage if config.yml doesn't exist
@@ -521,7 +521,7 @@ export class SettingsManager {
521
521
  }
522
522
 
523
523
  // Load persisted settings from config.yml
524
- const storedSettings = SettingsManager.loadFromYaml(configPath);
524
+ const storedSettings = await SettingsManager.loadFromYaml(configPath);
525
525
  globalSettings = deepMergeSettings(globalSettings, storedSettings);
526
526
 
527
527
  // Load project settings before construction (constructor is sync)
@@ -559,18 +559,19 @@ export class SettingsManager {
559
559
  * @param configPath - Path to config.yml, or null for in-memory mode
560
560
  * @returns Parsed and migrated settings, or empty object if file doesn't exist
561
561
  */
562
- private static loadFromYaml(configPath: string | null): Settings {
563
- if (!configPath || !existsSync(configPath)) {
562
+ private static async loadFromYaml(configPath: string | null): Promise<Settings> {
563
+ if (!configPath) {
564
564
  return {};
565
565
  }
566
566
  try {
567
- const content = readFileSync(configPath, "utf-8");
567
+ const content = await Bun.file(configPath).text();
568
568
  const parsed = YAML.parse(content);
569
569
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
570
570
  return {};
571
571
  }
572
572
  return SettingsManager.migrateSettings(parsed as Record<string, unknown>);
573
573
  } catch (error) {
574
+ if (isEnoent(error)) return {};
574
575
  logger.warn("SettingsManager failed to load config.yml", { path: configPath, error: String(error) });
575
576
  return {};
576
577
  }
@@ -582,31 +583,37 @@ export class SettingsManager {
582
583
  * Only migrates if config.yml doesn't exist.
583
584
  */
584
585
  private static async migrateToYaml(storage: AgentStorage, agentDir: string, configPath: string): Promise<void> {
585
- // Skip if config.yml already exists
586
- if (existsSync(configPath)) return;
586
+ try {
587
+ await Bun.file(configPath).text();
588
+ return;
589
+ } catch (err) {
590
+ if (!isEnoent(err)) {
591
+ logger.warn("SettingsManager failed to check config.yml", { path: configPath, error: String(err) });
592
+ return;
593
+ }
594
+ }
587
595
 
588
596
  let settings: Settings = {};
589
597
  let migrated = false;
590
598
 
591
599
  // 1. Try to migrate from settings.json (oldest legacy format)
592
- const settingsJsonPath = join(agentDir, "settings.json");
600
+ const settingsJsonPath = path.join(agentDir, "settings.json");
593
601
  try {
594
- const settingsFile = Bun.file(settingsJsonPath);
595
- if (await settingsFile.exists()) {
596
- const parsed = JSON.parse(await settingsFile.text());
597
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
598
- settings = deepMergeSettings(settings, SettingsManager.migrateSettings(parsed));
599
- migrated = true;
600
- // Backup settings.json
601
- try {
602
- await rename(settingsJsonPath, `${settingsJsonPath}.bak`);
603
- } catch (error) {
604
- logger.warn("SettingsManager failed to backup settings.json", { error: String(error) });
605
- }
602
+ const parsed = JSON.parse(await Bun.file(settingsJsonPath).text());
603
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
604
+ settings = deepMergeSettings(settings, SettingsManager.migrateSettings(parsed));
605
+ migrated = true;
606
+ // Backup settings.json
607
+ try {
608
+ await fs.rename(settingsJsonPath, `${settingsJsonPath}.bak`);
609
+ } catch (error) {
610
+ logger.warn("SettingsManager failed to backup settings.json", { error: String(error) });
606
611
  }
607
612
  }
608
613
  } catch (error) {
609
- logger.warn("SettingsManager failed to read settings.json", { error: String(error) });
614
+ if (!isEnoent(error)) {
615
+ logger.warn("SettingsManager failed to read settings.json", { error: String(error) });
616
+ }
610
617
  }
611
618
 
612
619
  // 2. Migrate from agent.db settings table
@@ -690,7 +697,7 @@ export class SettingsManager {
690
697
  const configPath = this.configPath;
691
698
  try {
692
699
  await withFileLock(configPath, async () => {
693
- const currentSettings = SettingsManager.loadFromYaml(configPath);
700
+ const currentSettings = await SettingsManager.loadFromYaml(configPath);
694
701
  const mergedSettings = deepMergeSettings(currentSettings, this.globalSettings);
695
702
  this.globalSettings = mergedSettings;
696
703
  await Bun.write(configPath, YAML.stringify(this.globalSettings, null, 2));
@@ -788,6 +795,15 @@ export class SettingsManager {
788
795
  await this.save();
789
796
  }
790
797
 
798
+ getColorBlindMode(): boolean {
799
+ return this.settings.colorBlindMode ?? false;
800
+ }
801
+
802
+ async setColorBlindMode(enabled: boolean): Promise<void> {
803
+ this.globalSettings.colorBlindMode = enabled;
804
+ await this.save();
805
+ }
806
+
791
807
  getDefaultThinkingLevel(): "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | undefined {
792
808
  return this.settings.defaultThinkingLevel;
793
809
  }