@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,10 +1,10 @@
1
- import { basename, join, resolve } from "node:path";
1
+ import * as path from "node:path";
2
2
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
3
3
  import type { ImageContent, Message, TextContent, Usage } from "@oh-my-pi/pi-ai";
4
- import { getAgentDir as getDefaultAgentDir } from "@oh-my-pi/pi-coding-agent/config";
5
- import { resizeImage } from "@oh-my-pi/pi-coding-agent/utils/image-resize";
6
- import { logger } from "@oh-my-pi/pi-utils";
4
+ import { isEnoent, logger } from "@oh-my-pi/pi-utils";
7
5
  import { nanoid } from "nanoid";
6
+ import { getAgentDir as getDefaultAgentDir } from "../config";
7
+ import { resizeImage } from "../utils/image-resize";
8
8
  import {
9
9
  type BashExecutionMessage,
10
10
  type CustomMessage,
@@ -252,7 +252,7 @@ function migrateV2ToV3(entries: FileEntry[]): void {
252
252
  * Mutates entries in place. Returns true if any migration was applied.
253
253
  */
254
254
  function migrateToCurrentVersion(entries: FileEntry[]): boolean {
255
- const header = entries.find((e) => e.type === "session") as SessionHeader | undefined;
255
+ const header = entries.find(e => e.type === "session") as SessionHeader | undefined;
256
256
  const version = header?.version ?? 1;
257
257
 
258
258
  if (version >= CURRENT_SESSION_VERSION) return false;
@@ -397,7 +397,7 @@ export function buildSessionContext(
397
397
  messages.push(createCompactionSummaryMessage(compaction.summary, compaction.tokensBefore, compaction.timestamp));
398
398
 
399
399
  // Find compaction index in path
400
- const compactionIdx = path.findIndex((e) => e.type === "compaction" && e.id === compaction.id);
400
+ const compactionIdx = path.findIndex(e => e.type === "compaction" && e.id === compaction.id);
401
401
 
402
402
  // Emit kept messages (before compaction, starting from firstKeptEntryId)
403
403
  let foundFirstKept = false;
@@ -432,16 +432,24 @@ export function buildSessionContext(
432
432
  */
433
433
  function getDefaultSessionDir(cwd: string, storage: SessionStorage): string {
434
434
  const safePath = `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
435
- const sessionDir = join(getDefaultAgentDir(), "sessions", safePath);
435
+ const sessionDir = path.join(getDefaultAgentDir(), "sessions", safePath);
436
436
  storage.ensureDirSync(sessionDir);
437
437
  return sessionDir;
438
438
  }
439
439
 
440
440
  /** Exported for testing */
441
- export function loadEntriesFromFile(filePath: string, storage: SessionStorage = new FileSessionStorage()): FileEntry[] {
442
- if (!storage.existsSync(filePath)) return [];
441
+ export async function loadEntriesFromFile(
442
+ filePath: string,
443
+ storage: SessionStorage = new FileSessionStorage(),
444
+ ): Promise<FileEntry[]> {
445
+ let content: string;
446
+ try {
447
+ content = await storage.readText(filePath);
448
+ } catch (err) {
449
+ if (isEnoent(err)) return [];
450
+ throw err;
451
+ }
443
452
 
444
- const content = storage.readTextSync(filePath);
445
453
  const entries: FileEntry[] = [];
446
454
  const lines = content.trim().split("\n");
447
455
 
@@ -555,15 +563,13 @@ function extractFirstUserPrompt(lines: string[]): string | undefined {
555
563
  * Uses low-level file I/O to efficiently read only the first 4KB of each file
556
564
  * to extract the JSON header and first user message without loading entire session logs into memory.
557
565
  */
558
- function getSortedSessions(sessionDir: string, storage: SessionStorage): RecentSessionInfo[] {
566
+ async function getSortedSessions(sessionDir: string, storage: SessionStorage): Promise<RecentSessionInfo[]> {
559
567
  try {
560
- const buf = Buffer.alloc(4096);
561
568
  const files: string[] = storage.listFilesSync(sessionDir, "*.jsonl");
562
- return files
563
- .map((path: string) => {
569
+ const results = await Promise.all(
570
+ files.map(async (path: string) => {
564
571
  try {
565
- const length = storage.readTextPrefixSync(path, buf);
566
- const content = buf.toString("utf-8", 0, length);
572
+ const content = await storage.readTextPrefix(path, 4096);
567
573
  const lines = content.split("\n");
568
574
  const firstLine = lines[0];
569
575
  if (!firstLine || !firstLine.trim()) return null;
@@ -575,20 +581,20 @@ function getSortedSessions(sessionDir: string, storage: SessionStorage): RecentS
575
581
  } catch {
576
582
  return null;
577
583
  }
578
- })
579
- .filter((item): item is RecentSessionInfo => item !== null)
580
- .sort((a, b) => b.mtime - a.mtime);
584
+ }),
585
+ );
586
+ return results.filter((item): item is RecentSessionInfo => item !== null).sort((a, b) => b.mtime - a.mtime);
581
587
  } catch {
582
588
  return [];
583
589
  }
584
590
  }
585
591
 
586
592
  /** Exported for testing */
587
- export function findMostRecentSession(
593
+ export async function findMostRecentSession(
588
594
  sessionDir: string,
589
595
  storage: SessionStorage = new FileSessionStorage(),
590
- ): string | null {
591
- const sessions = getSortedSessions(sessionDir, storage);
596
+ ): Promise<string | null> {
597
+ const sessions = await getSortedSessions(sessionDir, storage);
592
598
  return sessions[0]?.path || null;
593
599
  }
594
600
 
@@ -676,7 +682,7 @@ async function truncateForPersistence<T>(obj: T, key?: string): Promise<T> {
676
682
  if (Array.isArray(obj)) {
677
683
  let changed = false;
678
684
  const result = await Promise.all(
679
- obj.map(async (item) => {
685
+ obj.map(async item => {
680
686
  // Special handling: compress oversized images while preserving shape
681
687
  if (key === TEXT_CONTENT_KEY && isImageBlock(item)) {
682
688
  if (item.data.length > MAX_PERSIST_CHARS) {
@@ -843,12 +849,13 @@ class NdjsonFileWriter {
843
849
  }
844
850
 
845
851
  /** Get recent sessions for display in welcome screen */
846
- export function getRecentSessions(
852
+ export async function getRecentSessions(
847
853
  sessionDir: string,
848
854
  limit = 3,
849
855
  storage: SessionStorage = new FileSessionStorage(),
850
- ): RecentSessionInfo[] {
851
- return getSortedSessions(sessionDir, storage).slice(0, limit);
856
+ ): Promise<RecentSessionInfo[]> {
857
+ const sessions = await getSortedSessions(sessionDir, storage);
858
+ return sessions.slice(0, limit);
852
859
  }
853
860
 
854
861
  /**
@@ -882,16 +889,16 @@ function extractTextFromContent(content: Message["content"]): string {
882
889
  if (typeof content === "string") return content;
883
890
  return content
884
891
  .filter((block): block is TextContent => block.type === "text")
885
- .map((block) => block.text)
892
+ .map(block => block.text)
886
893
  .join(" ");
887
894
  }
888
895
 
889
- function collectSessionsFromFiles(files: string[], storage: SessionStorage): SessionInfo[] {
896
+ async function collectSessionsFromFiles(files: string[], storage: SessionStorage): Promise<SessionInfo[]> {
890
897
  const sessions: SessionInfo[] = [];
891
898
 
892
899
  for (const file of files) {
893
900
  try {
894
- const content = storage.readTextSync(file);
901
+ const content = await storage.readText(file);
895
902
  const lines = content.trim().split("\n");
896
903
  if (lines.length === 0) continue;
897
904
 
@@ -1003,10 +1010,10 @@ export class SessionManager {
1003
1010
  await this._closePersistWriter();
1004
1011
  this.persistError = undefined;
1005
1012
  this.persistErrorReported = false;
1006
- this.sessionFile = resolve(sessionFile);
1007
- if (this.storage.existsSync(this.sessionFile)) {
1008
- this.fileEntries = loadEntriesFromFile(this.sessionFile!, this.storage);
1009
- const header = this.fileEntries.find((e) => e.type === "session") as SessionHeader | undefined;
1013
+ this.sessionFile = path.resolve(sessionFile);
1014
+ this.fileEntries = await loadEntriesFromFile(this.sessionFile, this.storage);
1015
+ if (this.fileEntries.length > 0) {
1016
+ const header = this.fileEntries.find(e => e.type === "session") as SessionHeader | undefined;
1010
1017
  this.sessionId = header?.id ?? nanoid();
1011
1018
  this.sessionTitle = header?.title;
1012
1019
 
@@ -1053,7 +1060,7 @@ export class SessionManager {
1053
1060
 
1054
1061
  if (this.persist) {
1055
1062
  const fileTimestamp = timestamp.replace(/[:.]/g, "-");
1056
- this.sessionFile = join(this.getSessionDir(), `${fileTimestamp}_${this.sessionId}.jsonl`);
1063
+ this.sessionFile = path.join(this.getSessionDir(), `${fileTimestamp}_${this.sessionId}.jsonl`);
1057
1064
  }
1058
1065
  return this.sessionFile;
1059
1066
  }
@@ -1115,7 +1122,7 @@ export class SessionManager {
1115
1122
  if (this.persistError && !options?.ignoreError) throw this.persistError;
1116
1123
  await task();
1117
1124
  });
1118
- this.persistChain = next.catch((err) => {
1125
+ this.persistChain = next.catch(err => {
1119
1126
  this._recordPersistError(err);
1120
1127
  });
1121
1128
  return next;
@@ -1127,7 +1134,7 @@ export class SessionManager {
1127
1134
  if (this.persistWriter && this.persistWriterPath === this.sessionFile) return this.persistWriter;
1128
1135
  // Note: caller must await _closePersistWriter() before calling this if switching files
1129
1136
  this.persistWriter = new NdjsonFileWriter(this.storage, this.sessionFile, {
1130
- onError: (err) => {
1137
+ onError: err => {
1131
1138
  this._recordPersistError(err);
1132
1139
  },
1133
1140
  });
@@ -1154,8 +1161,8 @@ export class SessionManager {
1154
1161
 
1155
1162
  private async _writeEntriesAtomically(entries: FileEntry[]): Promise<void> {
1156
1163
  if (!this.sessionFile) return;
1157
- const dir = resolve(this.sessionFile, "..");
1158
- const tempPath = join(dir, `.${basename(this.sessionFile)}.${nanoid(6)}.tmp`);
1164
+ const dir = path.resolve(this.sessionFile, "..");
1165
+ const tempPath = path.join(dir, `.${path.basename(this.sessionFile)}.${nanoid(6)}.tmp`);
1159
1166
  const writer = new NdjsonFileWriter(this.storage, tempPath, { flags: "w" });
1160
1167
  try {
1161
1168
  for (const entry of entries) {
@@ -1185,7 +1192,7 @@ export class SessionManager {
1185
1192
  if (!this.persist || !this.sessionFile) return;
1186
1193
  await this._queuePersistTask(async () => {
1187
1194
  await this._closePersistWriterInternal();
1188
- const entries = await Promise.all(this.fileEntries.map((entry) => prepareEntryForPersistence(entry)));
1195
+ const entries = await Promise.all(this.fileEntries.map(entry => prepareEntryForPersistence(entry)));
1189
1196
  await this._writeEntriesAtomically(entries);
1190
1197
  this.flushed = true;
1191
1198
  });
@@ -1236,7 +1243,7 @@ export class SessionManager {
1236
1243
  this.sessionTitle = title;
1237
1244
 
1238
1245
  // Update the in-memory header (so first flush includes title)
1239
- const header = this.fileEntries.find((e) => e.type === "session") as SessionHeader | undefined;
1246
+ const header = this.fileEntries.find(e => e.type === "session") as SessionHeader | undefined;
1240
1247
  if (header) {
1241
1248
  header.title = title;
1242
1249
  }
@@ -1252,7 +1259,7 @@ export class SessionManager {
1252
1259
  if (!this.persist || !this.sessionFile) return;
1253
1260
  if (this.persistError) throw this.persistError;
1254
1261
 
1255
- const hasAssistant = this.fileEntries.some((e) => e.type === "message" && e.message.role === "assistant");
1262
+ const hasAssistant = this.fileEntries.some(e => e.type === "message" && e.message.role === "assistant");
1256
1263
  if (!hasAssistant) return;
1257
1264
 
1258
1265
  if (!this.flushed) {
@@ -1260,7 +1267,7 @@ export class SessionManager {
1260
1267
  void this._queuePersistTask(async () => {
1261
1268
  const writer = this._ensurePersistWriter();
1262
1269
  if (!writer) return;
1263
- const entries = await Promise.all(this.fileEntries.map((e) => prepareEntryForPersistence(e)));
1270
+ const entries = await Promise.all(this.fileEntries.map(e => prepareEntryForPersistence(e)));
1264
1271
  for (const persistedEntry of entries) {
1265
1272
  await writer.write(persistedEntry);
1266
1273
  }
@@ -1596,7 +1603,7 @@ export class SessionManager {
1596
1603
  * Get session header.
1597
1604
  */
1598
1605
  getHeader(): SessionHeader | null {
1599
- const h = this.fileEntries.find((e) => e.type === "session");
1606
+ const h = this.fileEntries.find(e => e.type === "session");
1600
1607
  return h ? (h as SessionHeader) : null;
1601
1608
  }
1602
1609
 
@@ -1709,18 +1716,18 @@ export class SessionManager {
1709
1716
  * Returns the new session file path, or undefined if not persisting.
1710
1717
  */
1711
1718
  createBranchedSession(leafId: string): string | undefined {
1712
- const path = this.getBranch(leafId);
1713
- if (path.length === 0) {
1719
+ const branchPath = this.getBranch(leafId);
1720
+ if (branchPath.length === 0) {
1714
1721
  throw new Error(`Entry ${leafId} not found`);
1715
1722
  }
1716
1723
 
1717
1724
  // Filter out LabelEntry from path - we'll recreate them from the resolved map
1718
- const pathWithoutLabels = path.filter((e) => e.type !== "label");
1725
+ const pathWithoutLabels = branchPath.filter(e => e.type !== "label");
1719
1726
 
1720
1727
  const newSessionId = nanoid();
1721
1728
  const timestamp = new Date().toISOString();
1722
1729
  const fileTimestamp = timestamp.replace(/[:.]/g, "-");
1723
- const newSessionFile = join(this.getSessionDir(), `${fileTimestamp}_${newSessionId}.jsonl`);
1730
+ const newSessionFile = path.join(this.getSessionDir(), `${fileTimestamp}_${newSessionId}.jsonl`);
1724
1731
 
1725
1732
  const header: SessionHeader = {
1726
1733
  type: "session",
@@ -1732,7 +1739,7 @@ export class SessionManager {
1732
1739
  };
1733
1740
 
1734
1741
  // Collect labels for entries in the path
1735
- const pathEntryIds = new Set(pathWithoutLabels.map((e) => e.id));
1742
+ const pathEntryIds = new Set(pathWithoutLabels.map(e => e.id));
1736
1743
  const labelsToWrite: Array<{ targetId: string; label: string }> = [];
1737
1744
  for (const [targetId, label] of this.labelsById) {
1738
1745
  if (pathEntryIds.has(targetId)) {
@@ -1777,7 +1784,7 @@ export class SessionManager {
1777
1784
  for (const { targetId, label } of labelsToWrite) {
1778
1785
  const labelEntry: LabelEntry = {
1779
1786
  type: "label",
1780
- id: generateId(new Set([...pathEntryIds, ...labelEntries.map((e) => e.id)])),
1787
+ id: generateId(new Set([...pathEntryIds, ...labelEntries.map(e => e.id)])),
1781
1788
  parentId,
1782
1789
  timestamp: new Date().toISOString(),
1783
1790
  targetId,
@@ -1816,10 +1823,10 @@ export class SessionManager {
1816
1823
  ): Promise<SessionManager> {
1817
1824
  const dir = sessionDir ?? getDefaultSessionDir(cwd, storage);
1818
1825
  const manager = new SessionManager(cwd, dir, true, storage);
1819
- const forkEntries = structuredClone(loadEntriesFromFile(sourcePath, storage)) as FileEntry[];
1826
+ const forkEntries = structuredClone(await loadEntriesFromFile(sourcePath, storage)) as FileEntry[];
1820
1827
  migrateToCurrentVersion(forkEntries);
1821
- const sourceHeader = forkEntries.find((e) => e.type === "session") as SessionHeader | undefined;
1822
- const historyEntries = forkEntries.filter((entry) => entry.type !== "session") as SessionEntry[];
1828
+ const sourceHeader = forkEntries.find(e => e.type === "session") as SessionHeader | undefined;
1829
+ const historyEntries = forkEntries.filter(entry => entry.type !== "session") as SessionEntry[];
1823
1830
  manager._newSessionSync({ parentSession: sourceHeader?.id });
1824
1831
  const newHeader = manager.fileEntries[0] as SessionHeader;
1825
1832
  newHeader.title = sourceHeader?.title;
@@ -1836,18 +1843,18 @@ export class SessionManager {
1836
1843
  * @param sessionDir Optional session directory for /new or /branch. If omitted, derives from file's parent.
1837
1844
  */
1838
1845
  static async open(
1839
- path: string,
1846
+ filePath: string,
1840
1847
  sessionDir?: string,
1841
1848
  storage: SessionStorage = new FileSessionStorage(),
1842
1849
  ): Promise<SessionManager> {
1843
1850
  // Extract cwd from session header if possible, otherwise use process.cwd()
1844
- const entries = loadEntriesFromFile(path, storage);
1845
- const header = entries.find((e) => e.type === "session") as SessionHeader | undefined;
1851
+ const entries = await loadEntriesFromFile(filePath, storage);
1852
+ const header = entries.find(e => e.type === "session") as SessionHeader | undefined;
1846
1853
  const cwd = header?.cwd ?? process.cwd();
1847
1854
  // If no sessionDir provided, derive from file's parent directory
1848
- const dir = sessionDir ?? resolve(path, "..");
1855
+ const dir = sessionDir ?? path.resolve(filePath, "..");
1849
1856
  const manager = new SessionManager(cwd, dir, true, storage);
1850
- await manager._initSessionFile(path);
1857
+ await manager._initSessionFile(filePath);
1851
1858
  return manager;
1852
1859
  }
1853
1860
 
@@ -1862,7 +1869,7 @@ export class SessionManager {
1862
1869
  storage: SessionStorage = new FileSessionStorage(),
1863
1870
  ): Promise<SessionManager> {
1864
1871
  const dir = sessionDir ?? getDefaultSessionDir(cwd, storage);
1865
- const mostRecent = findMostRecentSession(dir, storage);
1872
+ const mostRecent = await findMostRecentSession(dir, storage);
1866
1873
  const manager = new SessionManager(cwd, dir, true, storage);
1867
1874
  if (mostRecent) {
1868
1875
  await manager._initSessionFile(mostRecent);
@@ -1884,11 +1891,15 @@ export class SessionManager {
1884
1891
  * @param cwd Working directory (used to compute default session directory)
1885
1892
  * @param sessionDir Optional session directory. If omitted, uses default (~/.omp/agent/sessions/<encoded-cwd>/).
1886
1893
  */
1887
- static list(cwd: string, sessionDir?: string, storage: SessionStorage = new FileSessionStorage()): SessionInfo[] {
1894
+ static async list(
1895
+ cwd: string,
1896
+ sessionDir?: string,
1897
+ storage: SessionStorage = new FileSessionStorage(),
1898
+ ): Promise<SessionInfo[]> {
1888
1899
  const dir = sessionDir ?? getDefaultSessionDir(cwd, storage);
1889
1900
  try {
1890
1901
  const files = storage.listFilesSync(dir, "*.jsonl");
1891
- return collectSessionsFromFiles(files, storage);
1902
+ return await collectSessionsFromFiles(files, storage);
1892
1903
  } catch {
1893
1904
  return [];
1894
1905
  }
@@ -1897,13 +1908,13 @@ export class SessionManager {
1897
1908
  /**
1898
1909
  * List all sessions across all project directories.
1899
1910
  */
1900
- static listAll(storage: SessionStorage = new FileSessionStorage()): SessionInfo[] {
1901
- const sessionsRoot = join(getDefaultAgentDir(), "sessions");
1911
+ static async listAll(storage: SessionStorage = new FileSessionStorage()): Promise<SessionInfo[]> {
1912
+ const sessionsRoot = path.join(getDefaultAgentDir(), "sessions");
1902
1913
  try {
1903
- const files = Array.from(new Bun.Glob("**/*.jsonl").scanSync(sessionsRoot)).map((name) =>
1904
- join(sessionsRoot, name),
1914
+ const files = Array.from(new Bun.Glob("**/*.jsonl").scanSync(sessionsRoot)).map(name =>
1915
+ path.join(sessionsRoot, name),
1905
1916
  );
1906
- return collectSessionsFromFiles(files, storage);
1917
+ return await collectSessionsFromFiles(files, storage);
1907
1918
  } catch {
1908
1919
  return [];
1909
1920
  }
@@ -1,17 +1,6 @@
1
- import {
2
- closeSync,
3
- existsSync,
4
- fsyncSync,
5
- mkdirSync,
6
- openSync,
7
- readFileSync,
8
- readSync,
9
- statSync,
10
- writeFileSync,
11
- writeSync,
12
- } from "node:fs";
13
- import { rename as renameAsync } from "node:fs/promises";
14
- import { dirname, join } from "node:path";
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { isEnoent } from "@oh-my-pi/pi-utils";
15
4
 
16
5
  export interface SessionStorageStat {
17
6
  size: number;
@@ -30,14 +19,13 @@ export interface SessionStorageWriter {
30
19
  export interface SessionStorage {
31
20
  ensureDirSync(dir: string): void;
32
21
  existsSync(path: string): boolean;
33
- readTextSync(path: string): string;
34
- readTextPrefixSync(path: string, buf: Buffer): number;
35
22
  writeTextSync(path: string, content: string): void;
36
23
  statSync(path: string): SessionStorageStat;
37
24
  listFilesSync(dir: string, pattern: string): string[];
38
25
 
39
26
  exists(path: string): Promise<boolean>;
40
27
  readText(path: string): Promise<string>;
28
+ readTextPrefix(path: string, maxBytes: number): Promise<string>;
41
29
  writeText(path: string, content: string): Promise<void>;
42
30
  rename(path: string, nextPath: string): Promise<void>;
43
31
  unlink(path: string): Promise<void>;
@@ -50,9 +38,9 @@ function toError(value: unknown): Error {
50
38
  }
51
39
 
52
40
  // FinalizationRegistry to clean up leaked file descriptors
53
- const writerRegistry = new FinalizationRegistry<number>((fd) => {
41
+ const writerRegistry = new FinalizationRegistry<number>(fd => {
54
42
  try {
55
- closeSync(fd);
43
+ fs.closeSync(fd);
56
44
  } catch {
57
45
  // Ignore - fd may already be closed or invalid
58
46
  }
@@ -64,16 +52,16 @@ class FileSessionStorageWriter implements SessionStorageWriter {
64
52
  private error: Error | undefined;
65
53
  private onError: ((err: Error) => void) | undefined;
66
54
 
67
- constructor(path: string, options?: { flags?: "a" | "w"; onError?: (err: Error) => void }) {
55
+ constructor(fpath: string, options?: { flags?: "a" | "w"; onError?: (err: Error) => void }) {
68
56
  this.onError = options?.onError;
69
57
  const flags = options?.flags ?? "a";
70
58
  // Ensure parent directory exists
71
- const dir = dirname(path);
72
- if (!existsSync(dir)) {
73
- mkdirSync(dir, { recursive: true });
59
+ const dir = path.dirname(fpath);
60
+ if (!fs.existsSync(dir)) {
61
+ fs.mkdirSync(dir, { recursive: true });
74
62
  }
75
63
  // Open file once, keep fd for lifetime
76
- this.fd = openSync(path, flags === "w" ? "w" : "a");
64
+ this.fd = fs.openSync(fpath, flags === "w" ? "w" : "a");
77
65
  // Register for cleanup if abandoned without close()
78
66
  writerRegistry.register(this, this.fd, this);
79
67
  }
@@ -92,7 +80,7 @@ class FileSessionStorageWriter implements SessionStorageWriter {
92
80
  const buf = Buffer.from(line, "utf-8");
93
81
  let offset = 0;
94
82
  while (offset < buf.length) {
95
- const written = writeSync(this.fd, buf, offset, buf.length - offset);
83
+ const written = fs.writeSync(this.fd, buf, offset, buf.length - offset);
96
84
  if (written === 0) {
97
85
  throw new Error("Short write");
98
86
  }
@@ -112,7 +100,7 @@ class FileSessionStorageWriter implements SessionStorageWriter {
112
100
  if (this.closed) throw new Error("Writer closed");
113
101
  if (this.error) throw this.error;
114
102
  try {
115
- fsyncSync(this.fd);
103
+ fs.fsyncSync(this.fd);
116
104
  } catch (err) {
117
105
  throw this.recordError(err);
118
106
  }
@@ -124,7 +112,7 @@ class FileSessionStorageWriter implements SessionStorageWriter {
124
112
  // Unregister from finalization - we're closing properly
125
113
  writerRegistry.unregister(this);
126
114
  try {
127
- closeSync(this.fd);
115
+ fs.closeSync(this.fd);
128
116
  } catch {
129
117
  // Ignore close errors
130
118
  }
@@ -137,53 +125,56 @@ class FileSessionStorageWriter implements SessionStorageWriter {
137
125
 
138
126
  export class FileSessionStorage implements SessionStorage {
139
127
  ensureDirSync(dir: string): void {
140
- if (!existsSync(dir)) {
141
- mkdirSync(dir, { recursive: true });
128
+ if (!fs.existsSync(dir)) {
129
+ fs.mkdirSync(dir, { recursive: true });
142
130
  }
143
131
  }
144
132
 
145
133
  existsSync(path: string): boolean {
146
- return existsSync(path);
134
+ return fs.existsSync(path);
147
135
  }
148
136
 
149
- readTextSync(path: string): string {
150
- return readFileSync(path, "utf-8");
151
- }
152
-
153
- readTextPrefixSync(path: string, buf: Buffer): number {
154
- const fd = openSync(path, "r");
155
- try {
156
- const bytesRead = readSync(fd, buf, 0, buf.length, 0);
157
- return bytesRead;
158
- } finally {
159
- closeSync(fd);
160
- }
161
- }
162
-
163
- writeTextSync(path: string, content: string): void {
164
- this.ensureDirSync(dirname(path));
165
- writeFileSync(path, content);
137
+ writeTextSync(fpath: string, content: string): void {
138
+ this.ensureDirSync(path.dirname(fpath));
139
+ fs.writeFileSync(fpath, content);
166
140
  }
167
141
 
168
142
  statSync(path: string): SessionStorageStat {
169
- const stats = statSync(path);
143
+ const stats = fs.statSync(path);
170
144
  return { size: stats.size, mtimeMs: stats.mtimeMs, mtime: stats.mtime };
171
145
  }
172
146
 
173
147
  listFilesSync(dir: string, pattern: string): string[] {
174
148
  try {
175
- return Array.from(new Bun.Glob(pattern).scanSync(dir)).map((name) => join(dir, name));
149
+ return Array.from(new Bun.Glob(pattern).scanSync(dir)).map(name => path.join(dir, name));
176
150
  } catch {
177
151
  return [];
178
152
  }
179
153
  }
180
154
 
181
- exists(path: string): Promise<boolean> {
182
- return Bun.file(path).exists();
155
+ async exists(path: string): Promise<boolean> {
156
+ try {
157
+ await fs.promises.access(path);
158
+ return true;
159
+ } catch (err) {
160
+ if (isEnoent(err)) return false;
161
+ throw err;
162
+ }
183
163
  }
184
164
 
185
165
  readText(path: string): Promise<string> {
186
- return Bun.file(path).text();
166
+ return fs.promises.readFile(path, "utf-8");
167
+ }
168
+
169
+ async readTextPrefix(path: string, maxBytes: number): Promise<string> {
170
+ const handle = await fs.promises.open(path, "r");
171
+ try {
172
+ const buffer = Buffer.alloc(maxBytes);
173
+ const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
174
+ return buffer.subarray(0, bytesRead).toString("utf-8");
175
+ } finally {
176
+ await handle.close();
177
+ }
187
178
  }
188
179
 
189
180
  async writeText(path: string, content: string): Promise<void> {
@@ -192,23 +183,23 @@ export class FileSessionStorage implements SessionStorage {
192
183
 
193
184
  async rename(path: string, nextPath: string): Promise<void> {
194
185
  try {
195
- await renameAsync(path, nextPath);
186
+ await fs.promises.rename(path, nextPath);
196
187
  } catch (err) {
197
188
  throw toError(err);
198
189
  }
199
190
  }
200
191
 
201
192
  unlink(path: string): Promise<void> {
202
- return Bun.file(path).unlink();
193
+ return fs.promises.unlink(path);
203
194
  }
204
195
 
205
196
  fsyncDirSync(dir: string): void {
206
197
  try {
207
- const fd = openSync(dir, "r");
198
+ const fd = fs.openSync(dir, "r");
208
199
  try {
209
- fsyncSync(fd);
200
+ fs.fsyncSync(fd);
210
201
  } finally {
211
- closeSync(fd);
202
+ fs.closeSync(fd);
212
203
  }
213
204
  } catch {
214
205
  // Best-effort: some platforms/filesystems don't support fsync on directories.
@@ -265,7 +256,7 @@ class MemorySessionStorageWriter implements SessionStorageWriter {
265
256
  await this.ready;
266
257
  if (this.error) throw this.error;
267
258
  try {
268
- const existing = this.storage.existsSync(this.path) ? this.storage.readTextSync(this.path) : "";
259
+ const existing = this.storage.existsSync(this.path) ? await this.storage.readText(this.path) : "";
269
260
  await this.storage.writeText(this.path, `${existing}${line}`);
270
261
  } catch (err) {
271
262
  throw this.recordError(err);
@@ -305,17 +296,6 @@ export class MemorySessionStorage implements SessionStorage {
305
296
  return this.files.has(path);
306
297
  }
307
298
 
308
- readTextSync(path: string): string {
309
- const entry = this.files.get(path);
310
- if (!entry) throw new Error(`File not found: ${path}`);
311
- return entry.content;
312
- }
313
-
314
- readTextPrefixSync(path: string, buf: Buffer): number {
315
- const content = this.readTextSync(path);
316
- return buf.write(content, 0, buf.length, "utf-8");
317
- }
318
-
319
299
  writeTextSync(path: string, content: string): void {
320
300
  this.files.set(path, { content, mtimeMs: Date.now() });
321
301
  }
@@ -348,7 +328,15 @@ export class MemorySessionStorage implements SessionStorage {
348
328
  }
349
329
 
350
330
  readText(path: string): Promise<string> {
351
- return Promise.resolve(this.readTextSync(path));
331
+ const entry = this.files.get(path);
332
+ if (!entry) return Promise.reject(new Error(`File not found: ${path}`));
333
+ return Promise.resolve(entry.content);
334
+ }
335
+
336
+ readTextPrefix(path: string, maxBytes: number): Promise<string> {
337
+ const entry = this.files.get(path);
338
+ if (!entry) return Promise.reject(new Error(`File not found: ${path}`));
339
+ return Promise.resolve(entry.content.slice(0, maxBytes));
352
340
  }
353
341
 
354
342
  writeText(path: string, content: string): Promise<void> {
@@ -6,9 +6,8 @@
6
6
  * NOTE: Settings migration is now handled by SettingsManager.migrateToYaml(),
7
7
  * which migrates from both settings.json and agent.db to config.yaml.
8
8
  */
9
-
10
- import { getAgentDbPath } from "@oh-my-pi/pi-coding-agent/config";
11
9
  import { logger } from "@oh-my-pi/pi-utils";
10
+ import { getAgentDbPath } from "../config";
12
11
  import { AgentStorage } from "./agent-storage";
13
12
  import type { AuthCredential, AuthCredentialEntry, AuthStorageData } from "./auth-storage";
14
13
 
@@ -128,7 +127,7 @@ async function migrateAuth(storage: AgentStorage, authPaths: string[], warnings:
128
127
  for (const [provider, entry] of Object.entries(authJson.data)) {
129
128
  const credentials = normalizeCredentialEntry(entry)
130
129
  .filter(isValidCredential)
131
- .map((credential) => credential);
130
+ .map(credential => credential);
132
131
 
133
132
  if (credentials.length === 0) continue;
134
133
  sawValid = true;
@@ -1,5 +1,5 @@
1
- import { DEFAULT_MAX_BYTES } from "@oh-my-pi/pi-coding-agent/tools/truncate";
2
1
  import { sanitizeText } from "@oh-my-pi/pi-utils";
2
+ import { DEFAULT_MAX_BYTES } from "../tools/truncate";
3
3
 
4
4
  export interface OutputSummary {
5
5
  output: string;
@@ -141,7 +141,7 @@ export class OutputSink {
141
141
  };
142
142
 
143
143
  return new WritableStream<Uint8Array | string>({
144
- write: async (chunk) => {
144
+ write: async chunk => {
145
145
  if (typeof chunk === "string") {
146
146
  await this.push(chunk);
147
147
  } else {