@oh-my-pi/pi-coding-agent 8.0.20 → 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 (421) hide show
  1. package/CHANGELOG.md +125 -0
  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 +54 -23
  38. package/scripts/format-prompts.ts +0 -1
  39. package/src/capability/context-file.ts +3 -4
  40. package/src/capability/extension-module.ts +3 -4
  41. package/src/capability/extension.ts +3 -4
  42. package/src/capability/fs.ts +20 -21
  43. package/src/capability/hook.ts +3 -4
  44. package/src/capability/index.ts +15 -16
  45. package/src/capability/instruction.ts +3 -4
  46. package/src/capability/mcp.ts +3 -4
  47. package/src/capability/prompt.ts +3 -4
  48. package/src/capability/rule.ts +3 -4
  49. package/src/capability/settings.ts +2 -3
  50. package/src/capability/skill.ts +3 -4
  51. package/src/capability/slash-command.ts +3 -4
  52. package/src/capability/ssh.ts +3 -4
  53. package/src/capability/system-prompt.ts +3 -4
  54. package/src/capability/tool.ts +3 -4
  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 -25
  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 +307 -0
  66. package/src/commit/agentic/fallback.ts +96 -0
  67. package/src/commit/agentic/index.ts +351 -0
  68. package/src/commit/agentic/prompts/analyze-file.md +22 -0
  69. package/src/commit/agentic/prompts/session-user.md +26 -0
  70. package/src/commit/agentic/prompts/split-confirm.md +1 -0
  71. package/src/commit/agentic/prompts/system.md +40 -0
  72. package/src/commit/agentic/state.ts +69 -0
  73. package/src/commit/agentic/tools/analyze-file.ts +131 -0
  74. package/src/commit/agentic/tools/git-file-diff.ts +194 -0
  75. package/src/commit/agentic/tools/git-hunk.ts +50 -0
  76. package/src/commit/agentic/tools/git-overview.ts +84 -0
  77. package/src/commit/agentic/tools/index.ts +56 -0
  78. package/src/commit/agentic/tools/propose-changelog.ts +128 -0
  79. package/src/commit/agentic/tools/propose-commit.ts +154 -0
  80. package/src/commit/agentic/tools/recent-commits.ts +81 -0
  81. package/src/commit/agentic/tools/split-commit.ts +280 -0
  82. package/src/commit/agentic/topo-sort.ts +44 -0
  83. package/src/commit/agentic/trivial.ts +51 -0
  84. package/src/commit/agentic/validation.ts +200 -0
  85. package/src/commit/analysis/conventional.ts +165 -0
  86. package/src/commit/analysis/index.ts +4 -0
  87. package/src/commit/analysis/scope.ts +242 -0
  88. package/src/commit/analysis/summary.ts +112 -0
  89. package/src/commit/analysis/validation.ts +66 -0
  90. package/src/commit/changelog/detect.ts +36 -0
  91. package/src/commit/changelog/generate.ts +110 -0
  92. package/src/commit/changelog/index.ts +233 -0
  93. package/src/commit/changelog/parse.ts +44 -0
  94. package/src/commit/cli.ts +93 -0
  95. package/src/commit/git/diff.ts +148 -0
  96. package/src/commit/git/errors.ts +11 -0
  97. package/src/commit/git/index.ts +212 -0
  98. package/src/commit/git/operations.ts +53 -0
  99. package/src/commit/index.ts +5 -0
  100. package/src/commit/map-reduce/index.ts +63 -0
  101. package/src/commit/map-reduce/map-phase.ts +178 -0
  102. package/src/commit/map-reduce/reduce-phase.ts +145 -0
  103. package/src/commit/map-reduce/utils.ts +9 -0
  104. package/src/commit/message.ts +11 -0
  105. package/src/commit/model-selection.ts +80 -0
  106. package/src/commit/pipeline.ts +240 -0
  107. package/src/commit/prompts/analysis-system.md +155 -0
  108. package/src/commit/prompts/analysis-user.md +41 -0
  109. package/src/commit/prompts/changelog-system.md +56 -0
  110. package/src/commit/prompts/changelog-user.md +19 -0
  111. package/src/commit/prompts/file-observer-system.md +26 -0
  112. package/src/commit/prompts/file-observer-user.md +9 -0
  113. package/src/commit/prompts/reduce-system.md +60 -0
  114. package/src/commit/prompts/reduce-user.md +17 -0
  115. package/src/commit/prompts/summary-retry.md +4 -0
  116. package/src/commit/prompts/summary-system.md +52 -0
  117. package/src/commit/prompts/summary-user.md +13 -0
  118. package/src/commit/prompts/types-description.md +2 -0
  119. package/src/commit/types.ts +109 -0
  120. package/src/commit/utils/exclusions.ts +42 -0
  121. package/src/config/file-lock.ts +121 -0
  122. package/src/config/keybindings.ts +6 -8
  123. package/src/config/model-registry.ts +65 -38
  124. package/src/config/model-resolver.ts +18 -19
  125. package/src/config/prompt-templates.ts +11 -11
  126. package/src/config/settings-manager.ts +141 -50
  127. package/src/config.ts +64 -66
  128. package/src/cursor.ts +11 -9
  129. package/src/discovery/agents-md.ts +11 -12
  130. package/src/discovery/builtin.ts +68 -73
  131. package/src/discovery/claude.ts +41 -42
  132. package/src/discovery/cline.ts +11 -12
  133. package/src/discovery/codex.ts +52 -53
  134. package/src/discovery/cursor.ts +9 -10
  135. package/src/discovery/gemini.ts +17 -22
  136. package/src/discovery/github.ts +13 -14
  137. package/src/discovery/helpers.ts +35 -34
  138. package/src/discovery/index.ts +22 -24
  139. package/src/discovery/mcp-json.ts +8 -9
  140. package/src/discovery/ssh.ts +8 -9
  141. package/src/discovery/vscode.ts +4 -5
  142. package/src/discovery/windsurf.ts +6 -7
  143. package/src/exa/company.ts +1 -2
  144. package/src/exa/index.ts +2 -3
  145. package/src/exa/linkedin.ts +1 -2
  146. package/src/exa/mcp-client.ts +14 -16
  147. package/src/exa/render.ts +10 -11
  148. package/src/exa/researcher.ts +1 -2
  149. package/src/exa/search.ts +1 -2
  150. package/src/exa/types.ts +0 -1
  151. package/src/exa/websets.ts +1 -2
  152. package/src/exec/bash-executor.ts +3 -4
  153. package/src/exec/exec.ts +0 -1
  154. package/src/export/custom-share.ts +5 -6
  155. package/src/export/html/index.ts +24 -21
  156. package/src/export/ttsr.ts +2 -3
  157. package/src/extensibility/custom-commands/bundled/review/index.ts +7 -8
  158. package/src/extensibility/custom-commands/loader.ts +18 -15
  159. package/src/extensibility/custom-commands/types.ts +2 -3
  160. package/src/extensibility/custom-tools/loader.ts +11 -12
  161. package/src/extensibility/custom-tools/types.ts +7 -8
  162. package/src/extensibility/custom-tools/wrapper.ts +2 -3
  163. package/src/extensibility/extensions/loader.ts +76 -54
  164. package/src/extensibility/extensions/runner.ts +11 -12
  165. package/src/extensibility/extensions/types.ts +20 -27
  166. package/src/extensibility/extensions/wrapper.ts +3 -4
  167. package/src/extensibility/hooks/index.ts +1 -1
  168. package/src/extensibility/hooks/loader.ts +9 -10
  169. package/src/extensibility/hooks/runner.ts +7 -8
  170. package/src/extensibility/hooks/tool-wrapper.ts +0 -1
  171. package/src/extensibility/hooks/types.ts +11 -18
  172. package/src/extensibility/plugins/doctor.ts +3 -3
  173. package/src/extensibility/plugins/installer.ts +27 -27
  174. package/src/extensibility/plugins/loader.ts +59 -56
  175. package/src/extensibility/plugins/manager.ts +211 -171
  176. package/src/extensibility/plugins/parser.ts +1 -1
  177. package/src/extensibility/plugins/paths.ts +8 -8
  178. package/src/extensibility/skills.ts +63 -60
  179. package/src/extensibility/slash-commands.ts +10 -10
  180. package/src/index.ts +54 -54
  181. package/src/internal-urls/agent-protocol.ts +21 -11
  182. package/src/internal-urls/artifact-protocol.ts +17 -13
  183. package/src/internal-urls/router.ts +1 -2
  184. package/src/internal-urls/rule-protocol.ts +3 -4
  185. package/src/internal-urls/skill-protocol.ts +3 -4
  186. package/src/ipy/executor.ts +109 -9
  187. package/src/ipy/gateway-coordinator.ts +79 -90
  188. package/src/ipy/kernel.ts +32 -30
  189. package/src/ipy/modules.ts +13 -13
  190. package/src/lsp/client.ts +21 -10
  191. package/src/lsp/clients/biome-client.ts +1 -2
  192. package/src/lsp/clients/index.ts +3 -3
  193. package/src/lsp/clients/lsp-linter-client.ts +4 -5
  194. package/src/lsp/config.ts +15 -15
  195. package/src/lsp/edits.ts +4 -5
  196. package/src/lsp/index.ts +43 -44
  197. package/src/lsp/lspmux.ts +8 -8
  198. package/src/lsp/render.ts +99 -61
  199. package/src/lsp/utils.ts +3 -3
  200. package/src/main.ts +71 -37
  201. package/src/mcp/client.ts +2 -3
  202. package/src/mcp/config.ts +5 -6
  203. package/src/mcp/json-rpc.ts +0 -1
  204. package/src/mcp/loader.ts +6 -7
  205. package/src/mcp/manager.ts +17 -18
  206. package/src/mcp/tool-bridge.ts +4 -9
  207. package/src/mcp/tool-cache.ts +2 -3
  208. package/src/mcp/transports/http.ts +2 -4
  209. package/src/mcp/transports/stdio.ts +1 -2
  210. package/src/migrations.ts +63 -52
  211. package/src/modes/components/armin.ts +4 -5
  212. package/src/modes/components/assistant-message.ts +33 -5
  213. package/src/modes/components/bash-execution.ts +7 -8
  214. package/src/modes/components/bordered-loader.ts +3 -3
  215. package/src/modes/components/branch-summary-message.ts +3 -3
  216. package/src/modes/components/compaction-summary-message.ts +3 -3
  217. package/src/modes/components/countdown-timer.ts +0 -1
  218. package/src/modes/components/custom-message.ts +5 -5
  219. package/src/modes/components/diff.ts +1 -1
  220. package/src/modes/components/dynamic-border.ts +2 -2
  221. package/src/modes/components/extensions/extension-dashboard.ts +6 -7
  222. package/src/modes/components/extensions/extension-list.ts +2 -3
  223. package/src/modes/components/extensions/inspector-panel.ts +3 -4
  224. package/src/modes/components/extensions/state-manager.ts +25 -26
  225. package/src/modes/components/extensions/types.ts +1 -2
  226. package/src/modes/components/footer.ts +47 -43
  227. package/src/modes/components/history-search.ts +2 -2
  228. package/src/modes/components/hook-editor.ts +3 -4
  229. package/src/modes/components/hook-input.ts +2 -3
  230. package/src/modes/components/hook-message.ts +5 -5
  231. package/src/modes/components/hook-selector.ts +2 -3
  232. package/src/modes/components/keybinding-hints.ts +2 -3
  233. package/src/modes/components/login-dialog.ts +2 -2
  234. package/src/modes/components/model-selector.ts +12 -12
  235. package/src/modes/components/oauth-selector.ts +2 -2
  236. package/src/modes/components/plugin-settings.ts +20 -20
  237. package/src/modes/components/python-execution.ts +7 -8
  238. package/src/modes/components/queue-mode-selector.ts +3 -3
  239. package/src/modes/components/read-tool-group.ts +2 -2
  240. package/src/modes/components/session-selector.ts +4 -4
  241. package/src/modes/components/settings-defs.ts +77 -69
  242. package/src/modes/components/settings-selector.ts +16 -16
  243. package/src/modes/components/show-images-selector.ts +2 -2
  244. package/src/modes/components/status-line/segments.ts +4 -4
  245. package/src/modes/components/status-line/separators.ts +1 -1
  246. package/src/modes/components/status-line/types.ts +2 -2
  247. package/src/modes/components/status-line-segment-editor.ts +7 -8
  248. package/src/modes/components/status-line.ts +12 -12
  249. package/src/modes/components/theme-selector.ts +8 -7
  250. package/src/modes/components/thinking-selector.ts +4 -4
  251. package/src/modes/components/todo-display.ts +2 -2
  252. package/src/modes/components/todo-reminder.ts +4 -4
  253. package/src/modes/components/tool-execution.ts +16 -19
  254. package/src/modes/components/tree-selector.ts +12 -12
  255. package/src/modes/components/ttsr-notification.ts +5 -5
  256. package/src/modes/components/user-message-selector.ts +1 -1
  257. package/src/modes/components/user-message.ts +1 -1
  258. package/src/modes/components/visual-truncate.ts +0 -1
  259. package/src/modes/components/welcome.ts +4 -4
  260. package/src/modes/controllers/command-controller.ts +46 -47
  261. package/src/modes/controllers/event-controller.ts +16 -20
  262. package/src/modes/controllers/extension-ui-controller.ts +40 -46
  263. package/src/modes/controllers/input-controller.ts +17 -18
  264. package/src/modes/controllers/selector-controller.ts +103 -91
  265. package/src/modes/index.ts +3 -3
  266. package/src/modes/interactive-mode.ts +31 -31
  267. package/src/modes/print-mode.ts +12 -13
  268. package/src/modes/rpc/rpc-client.ts +7 -8
  269. package/src/modes/rpc/rpc-mode.ts +24 -28
  270. package/src/modes/rpc/rpc-types.ts +3 -4
  271. package/src/modes/theme/mermaid-cache.ts +89 -0
  272. package/src/modes/theme/theme.ts +130 -53
  273. package/src/modes/types.ts +10 -10
  274. package/src/modes/utils/ui-helpers.ts +17 -17
  275. package/src/patch/applicator.ts +18 -19
  276. package/src/patch/diff.ts +1 -2
  277. package/src/patch/fuzzy.ts +1 -2
  278. package/src/patch/index.ts +11 -18
  279. package/src/patch/normalize.ts +4 -4
  280. package/src/patch/normative.ts +1 -2
  281. package/src/patch/parser.ts +8 -9
  282. package/src/patch/shared.ts +43 -16
  283. package/src/prompts/tools/task.md +2 -0
  284. package/src/sdk.ts +100 -65
  285. package/src/session/agent-session.ts +84 -85
  286. package/src/session/agent-storage.ts +43 -39
  287. package/src/session/artifacts.ts +32 -10
  288. package/src/session/auth-storage.ts +50 -39
  289. package/src/session/compaction/branch-summarization.ts +7 -10
  290. package/src/session/compaction/compaction.ts +8 -19
  291. package/src/session/compaction/utils.ts +6 -9
  292. package/src/session/history-storage.ts +10 -10
  293. package/src/session/messages.ts +4 -5
  294. package/src/session/session-manager.ts +76 -65
  295. package/src/session/session-storage.ts +57 -69
  296. package/src/session/storage-migration.ts +14 -56
  297. package/src/session/streaming-output.ts +2 -2
  298. package/src/ssh/connection-manager.ts +43 -50
  299. package/src/ssh/ssh-executor.ts +2 -2
  300. package/src/ssh/sshfs-mount.ts +11 -18
  301. package/src/system-prompt.ts +28 -35
  302. package/src/task/agents.ts +45 -30
  303. package/src/task/commands.ts +6 -7
  304. package/src/task/discovery.ts +39 -76
  305. package/src/task/executor.ts +14 -15
  306. package/src/task/index.ts +40 -34
  307. package/src/task/output-manager.ts +93 -0
  308. package/src/task/parallel.ts +0 -1
  309. package/src/task/render.ts +24 -30
  310. package/src/task/subprocess-tool-registry.ts +1 -2
  311. package/src/task/worker-protocol.ts +3 -3
  312. package/src/task/worker.ts +33 -39
  313. package/src/task/worktree.ts +19 -19
  314. package/src/tools/ask.ts +41 -20
  315. package/src/tools/bash-interceptor.ts +1 -5
  316. package/src/tools/bash.ts +91 -97
  317. package/src/tools/calculator.ts +49 -47
  318. package/src/tools/complete.ts +4 -5
  319. package/src/tools/context.ts +2 -2
  320. package/src/tools/fetch.ts +84 -124
  321. package/src/tools/find.ts +94 -98
  322. package/src/tools/gemini-image.ts +14 -14
  323. package/src/tools/grep.ts +100 -116
  324. package/src/tools/index.ts +80 -55
  325. package/src/tools/list-limit.ts +1 -1
  326. package/src/tools/ls.ts +44 -70
  327. package/src/tools/notebook.ts +51 -67
  328. package/src/tools/output-meta.ts +3 -4
  329. package/src/tools/output-utils.ts +2 -2
  330. package/src/tools/path-utils.ts +5 -5
  331. package/src/tools/python.ts +104 -217
  332. package/src/tools/read.ts +92 -33
  333. package/src/tools/render-utils.ts +8 -23
  334. package/src/tools/renderers.ts +6 -7
  335. package/src/tools/review.ts +8 -11
  336. package/src/tools/ssh.ts +69 -49
  337. package/src/tools/todo-write.ts +37 -25
  338. package/src/tools/tool-errors.ts +3 -3
  339. package/src/tools/tool-result.ts +3 -8
  340. package/src/tools/write.ts +99 -75
  341. package/src/tui/code-cell.ts +109 -0
  342. package/src/tui/file-list.ts +47 -0
  343. package/src/tui/index.ts +11 -0
  344. package/src/tui/output-block.ts +72 -0
  345. package/src/tui/status-line.ts +39 -0
  346. package/src/tui/tree-list.ts +55 -0
  347. package/src/tui/types.ts +16 -0
  348. package/src/tui/utils.ts +48 -0
  349. package/src/utils/changelog.ts +9 -10
  350. package/src/utils/clipboard.ts +11 -11
  351. package/src/utils/file-mentions.ts +4 -10
  352. package/src/utils/frontmatter.ts +6 -3
  353. package/src/utils/fuzzy.ts +2 -2
  354. package/src/utils/image-convert.ts +1 -1
  355. package/src/utils/image-resize.ts +1 -1
  356. package/src/utils/mime.ts +2 -2
  357. package/src/utils/shell-snapshot.ts +11 -13
  358. package/src/utils/shell.ts +4 -5
  359. package/src/utils/title-generator.ts +8 -9
  360. package/src/utils/tools-manager.ts +23 -23
  361. package/src/vendor/photon/index.js +1099 -1059
  362. package/src/vendor/photon/photon_rs_bg.wasm +0 -0
  363. package/src/web/scrapers/artifacthub.ts +1 -1
  364. package/src/web/scrapers/arxiv.ts +2 -2
  365. package/src/web/scrapers/bluesky.ts +2 -2
  366. package/src/web/scrapers/cheatsh.ts +1 -1
  367. package/src/web/scrapers/chocolatey.ts +2 -2
  368. package/src/web/scrapers/choosealicense.ts +5 -5
  369. package/src/web/scrapers/cisa-kev.ts +1 -1
  370. package/src/web/scrapers/crossref.ts +2 -2
  371. package/src/web/scrapers/devto.ts +3 -3
  372. package/src/web/scrapers/discogs.ts +3 -4
  373. package/src/web/scrapers/discourse.ts +1 -1
  374. package/src/web/scrapers/dockerhub.ts +1 -1
  375. package/src/web/scrapers/fdroid.ts +2 -2
  376. package/src/web/scrapers/firefox-addons.ts +3 -3
  377. package/src/web/scrapers/flathub.ts +1 -1
  378. package/src/web/scrapers/github.ts +3 -3
  379. package/src/web/scrapers/gitlab.ts +4 -4
  380. package/src/web/scrapers/hackernews.ts +2 -2
  381. package/src/web/scrapers/huggingface.ts +1 -1
  382. package/src/web/scrapers/iacr.ts +2 -2
  383. package/src/web/scrapers/index.ts +0 -1
  384. package/src/web/scrapers/jetbrains-marketplace.ts +1 -1
  385. package/src/web/scrapers/lemmy.ts +2 -2
  386. package/src/web/scrapers/maven.ts +2 -2
  387. package/src/web/scrapers/mdn.ts +2 -4
  388. package/src/web/scrapers/metacpan.ts +2 -2
  389. package/src/web/scrapers/musicbrainz.ts +1 -2
  390. package/src/web/scrapers/npm.ts +1 -1
  391. package/src/web/scrapers/nuget.ts +2 -2
  392. package/src/web/scrapers/nvd.ts +3 -3
  393. package/src/web/scrapers/ollama.ts +7 -9
  394. package/src/web/scrapers/opencorporates.ts +2 -2
  395. package/src/web/scrapers/openlibrary.ts +6 -6
  396. package/src/web/scrapers/orcid.ts +0 -1
  397. package/src/web/scrapers/osv.ts +2 -2
  398. package/src/web/scrapers/packagist.ts +1 -1
  399. package/src/web/scrapers/pubmed.ts +1 -2
  400. package/src/web/scrapers/rawg.ts +2 -2
  401. package/src/web/scrapers/readthedocs.ts +1 -2
  402. package/src/web/scrapers/repology.ts +2 -2
  403. package/src/web/scrapers/rfc.ts +1 -1
  404. package/src/web/scrapers/searchcode.ts +2 -2
  405. package/src/web/scrapers/semantic-scholar.ts +1 -1
  406. package/src/web/scrapers/snapcraft.ts +2 -2
  407. package/src/web/scrapers/sourcegraph.ts +1 -1
  408. package/src/web/scrapers/spdx.ts +3 -3
  409. package/src/web/scrapers/spotify.ts +0 -1
  410. package/src/web/scrapers/twitter.ts +1 -1
  411. package/src/web/scrapers/types.ts +1 -2
  412. package/src/web/scrapers/utils.ts +5 -5
  413. package/src/web/scrapers/wikidata.ts +3 -3
  414. package/src/web/scrapers/youtube.ts +9 -14
  415. package/src/web/search/auth.ts +5 -10
  416. package/src/web/search/index.ts +11 -21
  417. package/src/web/search/providers/anthropic.ts +3 -9
  418. package/src/web/search/providers/exa.ts +6 -10
  419. package/src/web/search/providers/perplexity.ts +5 -5
  420. package/src/web/search/render.ts +129 -175
  421. package/tsconfig.json +0 -42
@@ -0,0 +1,165 @@
1
+ import type { Api, AssistantMessage, Model, ToolCall } from "@oh-my-pi/pi-ai";
2
+ import { completeSimple, validateToolCall } from "@oh-my-pi/pi-ai";
3
+ import { Type } from "@sinclair/typebox";
4
+ import analysisSystemPrompt from "../../commit/prompts/analysis-system.md" with { type: "text" };
5
+ import analysisUserPrompt from "../../commit/prompts/analysis-user.md" with { type: "text" };
6
+ import type { ChangelogCategory, ConventionalAnalysis, ConventionalDetail } from "../../commit/types";
7
+ import { renderPromptTemplate } from "../../config/prompt-templates";
8
+
9
+ const ConventionalAnalysisTool = {
10
+ name: "create_conventional_analysis",
11
+ description: "Analyze a diff and return conventional commit classification.",
12
+ parameters: Type.Object({
13
+ type: Type.Union([
14
+ Type.Literal("feat"),
15
+ Type.Literal("fix"),
16
+ Type.Literal("refactor"),
17
+ Type.Literal("docs"),
18
+ Type.Literal("test"),
19
+ Type.Literal("chore"),
20
+ Type.Literal("style"),
21
+ Type.Literal("perf"),
22
+ Type.Literal("build"),
23
+ Type.Literal("ci"),
24
+ Type.Literal("revert"),
25
+ ]),
26
+ scope: Type.Union([Type.String(), Type.Null()]),
27
+ details: Type.Array(
28
+ Type.Object({
29
+ text: Type.String(),
30
+ changelog_category: Type.Optional(
31
+ Type.Union([
32
+ Type.Literal("Added"),
33
+ Type.Literal("Changed"),
34
+ Type.Literal("Fixed"),
35
+ Type.Literal("Deprecated"),
36
+ Type.Literal("Removed"),
37
+ Type.Literal("Security"),
38
+ Type.Literal("Breaking Changes"),
39
+ ]),
40
+ ),
41
+ user_visible: Type.Optional(Type.Boolean()),
42
+ }),
43
+ ),
44
+ issue_refs: Type.Array(Type.String()),
45
+ }),
46
+ };
47
+
48
+ export interface ConventionalAnalysisInput {
49
+ model: Model<Api>;
50
+ apiKey: string;
51
+ contextFiles?: Array<{ path: string; content: string }>;
52
+ userContext?: string;
53
+ typesDescription?: string;
54
+ recentCommits?: string[];
55
+ scopeCandidates: string;
56
+ stat: string;
57
+ diff: string;
58
+ }
59
+
60
+ /**
61
+ * Generate conventional analysis data from a diff and metadata.
62
+ */
63
+ export async function generateConventionalAnalysis({
64
+ model,
65
+ apiKey,
66
+ contextFiles,
67
+ userContext,
68
+ typesDescription,
69
+ recentCommits,
70
+ scopeCandidates,
71
+ stat,
72
+ diff,
73
+ }: ConventionalAnalysisInput): Promise<ConventionalAnalysis> {
74
+ const prompt = renderPromptTemplate(analysisUserPrompt, {
75
+ context_files: contextFiles && contextFiles.length > 0 ? contextFiles : undefined,
76
+ user_context: userContext,
77
+ types_description: typesDescription,
78
+ recent_commits: recentCommits?.join("\n"),
79
+ scope_candidates: scopeCandidates,
80
+ stat,
81
+ diff,
82
+ });
83
+
84
+ const response = await completeSimple(
85
+ model,
86
+ {
87
+ systemPrompt: renderPromptTemplate(analysisSystemPrompt),
88
+ messages: [{ role: "user", content: prompt, timestamp: Date.now() }],
89
+ tools: [ConventionalAnalysisTool],
90
+ },
91
+ { apiKey, maxTokens: 2400 },
92
+ );
93
+
94
+ return parseAnalysisFromResponse(response);
95
+ }
96
+
97
+ function parseAnalysisFromResponse(message: AssistantMessage): ConventionalAnalysis {
98
+ const toolCall = extractToolCall(message, "create_conventional_analysis");
99
+ if (toolCall) {
100
+ const parsed = validateToolCall([ConventionalAnalysisTool], toolCall) as {
101
+ type: ConventionalAnalysis["type"];
102
+ scope: string | null;
103
+ details: Array<{ text: string; changelog_category?: ChangelogCategory; user_visible?: boolean }>;
104
+ issue_refs: string[];
105
+ };
106
+ return normalizeAnalysis(parsed);
107
+ }
108
+
109
+ const text = extractTextContent(message);
110
+ const parsed = parseJsonPayload(text) as {
111
+ type: ConventionalAnalysis["type"];
112
+ scope: string | null;
113
+ details: Array<{ text: string; changelog_category?: ChangelogCategory; user_visible?: boolean }>;
114
+ issue_refs: string[];
115
+ };
116
+ return normalizeAnalysis(parsed);
117
+ }
118
+
119
+ function normalizeAnalysis(parsed: {
120
+ type: ConventionalAnalysis["type"];
121
+ scope: string | null;
122
+ details: Array<{ text: string; changelog_category?: ChangelogCategory; user_visible?: boolean }>;
123
+ issue_refs: string[];
124
+ }): ConventionalAnalysis {
125
+ const details: ConventionalDetail[] = parsed.details.map(detail => ({
126
+ text: detail.text.trim(),
127
+ changelogCategory: detail.user_visible ? detail.changelog_category : undefined,
128
+ userVisible: detail.user_visible ?? false,
129
+ }));
130
+ return {
131
+ type: parsed.type,
132
+ scope: parsed.scope?.trim() || null,
133
+ details,
134
+ issueRefs: parsed.issue_refs ?? [],
135
+ };
136
+ }
137
+
138
+ function extractToolCall(message: AssistantMessage, name: string): ToolCall | undefined {
139
+ for (const content of message.content) {
140
+ if (content.type === "toolCall" && content.name === name) {
141
+ return content;
142
+ }
143
+ }
144
+ return undefined;
145
+ }
146
+
147
+ function extractTextContent(message: AssistantMessage): string {
148
+ return message.content
149
+ .filter(content => content.type === "text")
150
+ .map(content => content.text)
151
+ .join("")
152
+ .trim();
153
+ }
154
+
155
+ function parseJsonPayload(text: string): unknown {
156
+ const trimmed = text.trim();
157
+ if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
158
+ return JSON.parse(trimmed) as unknown;
159
+ }
160
+ const match = trimmed.match(/\{[\s\S]*\}/);
161
+ if (!match) {
162
+ throw new Error("No JSON payload found in analysis response");
163
+ }
164
+ return JSON.parse(match[0]) as unknown;
165
+ }
@@ -0,0 +1,4 @@
1
+ export { generateConventionalAnalysis } from "./conventional";
2
+ export { extractScopeCandidates } from "./scope";
3
+ export { generateSummary, stripTypePrefix } from "./summary";
4
+ export { validateAnalysis, validateScope, validateSummary } from "./validation";
@@ -0,0 +1,242 @@
1
+ import type { NumstatEntry } from "../../commit/types";
2
+ import { isExcludedFile } from "../../commit/utils/exclusions";
3
+
4
+ interface ScopeCandidate {
5
+ path: string;
6
+ percentage: number;
7
+ confidence: number;
8
+ }
9
+
10
+ const PLACEHOLDER_DIRS = new Set([
11
+ "src",
12
+ "lib",
13
+ "bin",
14
+ "crates",
15
+ "benches",
16
+ "examples",
17
+ "internal",
18
+ "pkg",
19
+ "include",
20
+ "tests",
21
+ "test",
22
+ "docs",
23
+ "packages",
24
+ "modules",
25
+ ]);
26
+
27
+ const SKIP_DIRS = new Set(["test", "tests", "benches", "examples", "target", "build", "node_modules", ".github"]);
28
+
29
+ export interface ScopeCandidatesResult {
30
+ scopeCandidates: string;
31
+ isWide: boolean;
32
+ }
33
+
34
+ export function extractScopeCandidates(numstat: NumstatEntry[]): ScopeCandidatesResult {
35
+ const componentLines = new Map<string, number>();
36
+ const paths: string[] = [];
37
+ const distinctRoots = new Set<string>();
38
+ let totalLines = 0;
39
+
40
+ for (const entry of numstat) {
41
+ const linesChanged = entry.additions + entry.deletions;
42
+ if (linesChanged === 0) continue;
43
+ const normalizedPath = normalizePathForScope(entry.path);
44
+ if (isExcludedFile(normalizedPath)) continue;
45
+ paths.push(normalizedPath);
46
+ const root = extractTopLevelRoot(normalizedPath);
47
+ if (root) {
48
+ distinctRoots.add(root);
49
+ }
50
+ totalLines += linesChanged;
51
+ const components = extractComponentsFromPath(normalizedPath);
52
+ for (const component of components) {
53
+ if (component.split("/").some(segment => segment.includes("."))) {
54
+ continue;
55
+ }
56
+ componentLines.set(component, (componentLines.get(component) ?? 0) + linesChanged);
57
+ }
58
+ }
59
+
60
+ if (totalLines === 0) {
61
+ return { scopeCandidates: "(none - no measurable changes)", isWide: false };
62
+ }
63
+
64
+ const candidates = buildScopeCandidates(componentLines, totalLines);
65
+ const isWide = isWideChange(candidates, 0.6, distinctRoots.size);
66
+ if (isWide) {
67
+ const pattern = analyzeWideChange(paths);
68
+ if (pattern) {
69
+ return { scopeCandidates: `(cross-cutting: ${pattern})`, isWide: true };
70
+ }
71
+ return { scopeCandidates: "(none - multi-component change)", isWide: true };
72
+ }
73
+
74
+ const suggestionParts: string[] = [];
75
+ for (const candidate of candidates.slice(0, 5)) {
76
+ if (candidate.percentage < 10) continue;
77
+ const confidenceLabel = candidate.path.includes("/")
78
+ ? candidate.percentage > 60
79
+ ? "high confidence"
80
+ : "moderate confidence"
81
+ : "high confidence";
82
+ suggestionParts.push(`${candidate.path} (${candidate.percentage.toFixed(0)}%, ${confidenceLabel})`);
83
+ }
84
+
85
+ const scopeCandidates =
86
+ suggestionParts.length === 0
87
+ ? "(none - unclear component)"
88
+ : `${suggestionParts.join(", ")}\nPrefer 2-segment scopes marked 'high confidence'`;
89
+
90
+ return { scopeCandidates, isWide: false };
91
+ }
92
+
93
+ function buildScopeCandidates(componentLines: Map<string, number>, totalLines: number): ScopeCandidate[] {
94
+ const candidates: ScopeCandidate[] = [];
95
+ for (const [path, lines] of componentLines.entries()) {
96
+ if (!path.includes("/") && PLACEHOLDER_DIRS.has(path)) continue;
97
+ const root = path.split("/")[0] ?? "";
98
+ if (PLACEHOLDER_DIRS.has(root)) continue;
99
+ const percentage = (lines / totalLines) * 100;
100
+ const isTwoSegment = path.includes("/");
101
+ const confidence = isTwoSegment ? (percentage > 60 ? percentage * 1.2 : percentage * 0.8) : percentage;
102
+ candidates.push({ path, percentage, confidence });
103
+ }
104
+ return candidates.sort((a, b) => b.confidence - a.confidence);
105
+ }
106
+
107
+ function isWideChange(candidates: ScopeCandidate[], threshold: number, distinctRoots: number): boolean {
108
+ if (distinctRoots >= 3) return true;
109
+ const top = candidates[0];
110
+ if (!top) return false;
111
+ return top.percentage / 100 < threshold;
112
+ }
113
+
114
+ function extractComponentsFromPath(path: string): string[] {
115
+ const segments = path.split("/");
116
+ const meaningful: string[] = [];
117
+
118
+ const stripExt = (segment: string): string => {
119
+ const index = segment.lastIndexOf(".");
120
+ return index > 0 ? segment.slice(0, index) : segment;
121
+ };
122
+
123
+ const isFile = (segment: string): boolean => {
124
+ return segment.includes(".") && !segment.startsWith(".") && segment.lastIndexOf(".") > 0;
125
+ };
126
+
127
+ for (let index = 0; index < segments.length; index += 1) {
128
+ const segment = segments[index] ?? "";
129
+ if (PLACEHOLDER_DIRS.has(segment) && segments.length > index + 1) {
130
+ continue;
131
+ }
132
+ if (isFile(segment)) continue;
133
+ if (SKIP_DIRS.has(segment)) continue;
134
+
135
+ const stripped = stripExt(segment);
136
+ if (stripped && !stripped.startsWith(".")) {
137
+ meaningful.push(stripped);
138
+ }
139
+ }
140
+
141
+ const components: string[] = [];
142
+ if (meaningful.length > 0) {
143
+ components.push(meaningful[0]!);
144
+ if (meaningful.length >= 2) {
145
+ components.push(`${meaningful[0]}/${meaningful[1]}`);
146
+ }
147
+ }
148
+
149
+ return components;
150
+ }
151
+
152
+ function extractTopLevelRoot(path: string): string | null {
153
+ const segments = path.split("/").filter(segment => segment.length > 0);
154
+ if (segments.length === 0) return null;
155
+ if (segments.length === 1) {
156
+ return segments[0]!.startsWith(".") ? null : "(root)";
157
+ }
158
+
159
+ for (let index = 0; index < segments.length; index += 1) {
160
+ const segment = segments[index] ?? "";
161
+ if (PLACEHOLDER_DIRS.has(segment) && segments.length > index + 1) {
162
+ continue;
163
+ }
164
+ if (SKIP_DIRS.has(segment)) continue;
165
+ if (segment.startsWith(".")) continue;
166
+ return segment;
167
+ }
168
+
169
+ return null;
170
+ }
171
+
172
+ function normalizePathForScope(path: string): string {
173
+ const braceStart = path.indexOf("{");
174
+ if (braceStart !== -1) {
175
+ const arrowPos = path.indexOf(" => ", braceStart);
176
+ if (arrowPos !== -1) {
177
+ const braceEnd = path.indexOf("}", arrowPos);
178
+ if (braceEnd !== -1) {
179
+ const prefix = path.slice(0, braceStart);
180
+ const newName = path.slice(arrowPos + 4, braceEnd).trim();
181
+ return `${prefix}${newName}`;
182
+ }
183
+ }
184
+ }
185
+
186
+ if (path.includes(" => ")) {
187
+ const parts = path.split(" => ");
188
+ return parts[1]?.trim() ?? path.trim();
189
+ }
190
+
191
+ return path.trim();
192
+ }
193
+
194
+ function analyzeWideChange(paths: string[]): string | null {
195
+ if (paths.length === 0) return null;
196
+ const total = paths.length;
197
+ let mdCount = 0;
198
+ let testCount = 0;
199
+ let configCount = 0;
200
+ let hasCargoToml = false;
201
+ let hasPackageJson = false;
202
+ let errorKeywords = 0;
203
+ let typeKeywords = 0;
204
+
205
+ for (const path of paths) {
206
+ const lowerPath = path.toLowerCase();
207
+ if (lowerPath.endsWith(".md")) {
208
+ mdCount += 1;
209
+ }
210
+ if (lowerPath.includes("/test") || lowerPath.includes("_test.")) {
211
+ testCount += 1;
212
+ }
213
+ if (
214
+ lowerPath.endsWith(".toml") ||
215
+ lowerPath.endsWith(".yaml") ||
216
+ lowerPath.endsWith(".yml") ||
217
+ lowerPath.endsWith(".json")
218
+ ) {
219
+ configCount += 1;
220
+ }
221
+ if (path.includes("Cargo.toml")) {
222
+ hasCargoToml = true;
223
+ }
224
+ if (path.includes("package.json")) {
225
+ hasPackageJson = true;
226
+ }
227
+ if (lowerPath.includes("error") || lowerPath.includes("result") || lowerPath.includes("err")) {
228
+ errorKeywords += 1;
229
+ }
230
+ if (lowerPath.includes("type") || lowerPath.includes("struct") || lowerPath.includes("enum")) {
231
+ typeKeywords += 1;
232
+ }
233
+ }
234
+
235
+ if (hasCargoToml || hasPackageJson) return "deps";
236
+ if ((mdCount * 100) / total > 70) return "docs";
237
+ if ((testCount * 100) / total > 60) return "tests";
238
+ if ((errorKeywords * 100) / total > 40) return "error-handling";
239
+ if ((typeKeywords * 100) / total > 40) return "type-refactor";
240
+ if ((configCount * 100) / total > 50) return "config";
241
+ return null;
242
+ }
@@ -0,0 +1,112 @@
1
+ import type { Api, AssistantMessage, Model, ToolCall } from "@oh-my-pi/pi-ai";
2
+ import { completeSimple, validateToolCall } from "@oh-my-pi/pi-ai";
3
+ import { Type } from "@sinclair/typebox";
4
+ import summarySystemPrompt from "../../commit/prompts/summary-system.md" with { type: "text" };
5
+ import summaryUserPrompt from "../../commit/prompts/summary-user.md" with { type: "text" };
6
+ import type { CommitSummary } from "../../commit/types";
7
+ import { renderPromptTemplate } from "../../config/prompt-templates";
8
+
9
+ const SummaryTool = {
10
+ name: "create_commit_summary",
11
+ description: "Generate the summary line for a conventional commit message.",
12
+ parameters: Type.Object({
13
+ summary: Type.String(),
14
+ }),
15
+ };
16
+
17
+ export interface SummaryInput {
18
+ model: Model<Api>;
19
+ apiKey: string;
20
+ commitType: string;
21
+ scope: string | null;
22
+ details: string[];
23
+ stat: string;
24
+ maxChars: number;
25
+ userContext?: string;
26
+ }
27
+
28
+ /**
29
+ * Generate a commit summary line for the conventional commit header.
30
+ */
31
+ export async function generateSummary({
32
+ model,
33
+ apiKey,
34
+ commitType,
35
+ scope,
36
+ details,
37
+ stat,
38
+ maxChars,
39
+ userContext,
40
+ }: SummaryInput): Promise<CommitSummary> {
41
+ const systemPrompt = renderSummaryPrompt({ commitType, scope, maxChars });
42
+ const userPrompt = renderPromptTemplate(summaryUserPrompt, {
43
+ user_context: userContext,
44
+ details: details.join("\n"),
45
+ stat,
46
+ });
47
+
48
+ const response = await completeSimple(
49
+ model,
50
+ {
51
+ systemPrompt,
52
+ messages: [{ role: "user", content: userPrompt, timestamp: Date.now() }],
53
+ tools: [SummaryTool],
54
+ },
55
+ { apiKey, maxTokens: 200 },
56
+ );
57
+
58
+ return parseSummaryFromResponse(response, commitType, scope);
59
+ }
60
+
61
+ function renderSummaryPrompt({
62
+ commitType,
63
+ scope,
64
+ maxChars,
65
+ }: {
66
+ commitType: string;
67
+ scope: string | null;
68
+ maxChars: number;
69
+ }): string {
70
+ const scopePrefix = scope ? `(${scope})` : "";
71
+ return renderPromptTemplate(summarySystemPrompt, {
72
+ commit_type: commitType,
73
+ scope_prefix: scopePrefix,
74
+ chars: String(maxChars),
75
+ });
76
+ }
77
+
78
+ function parseSummaryFromResponse(message: AssistantMessage, commitType: string, scope: string | null): CommitSummary {
79
+ const toolCall = extractToolCall(message, "create_commit_summary");
80
+ if (toolCall) {
81
+ const parsed = validateToolCall([SummaryTool], toolCall) as { summary: string };
82
+ return { summary: stripTypePrefix(parsed.summary, commitType, scope) };
83
+ }
84
+ const text = extractTextContent(message);
85
+ return { summary: stripTypePrefix(text, commitType, scope) };
86
+ }
87
+
88
+ function extractToolCall(message: AssistantMessage, name: string): ToolCall | undefined {
89
+ return message.content.find(content => content.type === "toolCall" && content.name === name) as ToolCall | undefined;
90
+ }
91
+
92
+ function extractTextContent(message: AssistantMessage): string {
93
+ return message.content
94
+ .filter(content => content.type === "text")
95
+ .map(content => content.text)
96
+ .join("")
97
+ .trim();
98
+ }
99
+
100
+ export function stripTypePrefix(summary: string, commitType: string, scope: string | null): string {
101
+ const trimmed = summary.trim();
102
+ const scopePart = scope ? `(${scope})` : "";
103
+ const withScope = `${commitType}${scopePart}: `;
104
+ if (trimmed.startsWith(withScope)) {
105
+ return trimmed.slice(withScope.length).trim();
106
+ }
107
+ const withoutScope = `${commitType}: `;
108
+ if (trimmed.startsWith(withoutScope)) {
109
+ return trimmed.slice(withoutScope.length).trim();
110
+ }
111
+ return trimmed;
112
+ }
@@ -0,0 +1,66 @@
1
+ import type { ConventionalAnalysis } from "../../commit/types";
2
+
3
+ export interface ValidationResult {
4
+ valid: boolean;
5
+ errors: string[];
6
+ }
7
+
8
+ export function validateSummary(summary: string, maxChars: number): ValidationResult {
9
+ const errors: string[] = [];
10
+ if (!summary.trim()) {
11
+ errors.push("Summary is empty");
12
+ }
13
+ if (summary.length > maxChars) {
14
+ errors.push(`Summary exceeds ${maxChars} characters`);
15
+ }
16
+ if (summary.trimEnd().endsWith(".")) {
17
+ errors.push("Summary must not end with a period");
18
+ }
19
+ if (summary.includes("\n")) {
20
+ errors.push("Summary must be a single line");
21
+ }
22
+ return { valid: errors.length === 0, errors };
23
+ }
24
+
25
+ export function validateScope(scope: string | null): ValidationResult {
26
+ if (!scope) return { valid: true, errors: [] };
27
+ const errors: string[] = [];
28
+ const segments = scope.split("/");
29
+ if (segments.length > 2) {
30
+ errors.push("Scope may contain at most two segments");
31
+ }
32
+ for (const segment of segments) {
33
+ if (!segment) {
34
+ errors.push("Scope segments cannot be empty");
35
+ continue;
36
+ }
37
+ if (segment !== segment.toLowerCase()) {
38
+ errors.push("Scope must be lowercase");
39
+ }
40
+ if (!/^[a-z0-9][a-z0-9-_]*$/.test(segment)) {
41
+ errors.push(`Scope segment has invalid characters: ${segment}`);
42
+ }
43
+ }
44
+ return { valid: errors.length === 0, errors };
45
+ }
46
+
47
+ export function validateAnalysis(analysis: ConventionalAnalysis): ValidationResult {
48
+ const errors: string[] = [];
49
+ const scopeResult = validateScope(analysis.scope);
50
+ if (!scopeResult.valid) {
51
+ errors.push(...scopeResult.errors);
52
+ }
53
+ for (const detail of analysis.details) {
54
+ if (!detail.text.trim()) {
55
+ errors.push("Detail text is empty");
56
+ continue;
57
+ }
58
+ if (!detail.text.trim().endsWith(".")) {
59
+ errors.push(`Detail must end with a period: ${detail.text}`);
60
+ }
61
+ if (detail.text.length > 120) {
62
+ errors.push(`Detail exceeds 120 characters: ${detail.text}`);
63
+ }
64
+ }
65
+ return { valid: errors.length === 0, errors };
66
+ }
@@ -0,0 +1,36 @@
1
+ import * as path from "node:path";
2
+ import type { ChangelogBoundary } from "../../commit/types";
3
+
4
+ const CHANGELOG_NAME = "CHANGELOG.md";
5
+
6
+ export async function detectChangelogBoundaries(cwd: string, stagedFiles: string[]): Promise<ChangelogBoundary[]> {
7
+ const boundaries = new Map<string, string[]>();
8
+ for (const file of stagedFiles) {
9
+ if (file.toLowerCase().endsWith("changelog.md")) continue;
10
+ const changelogPath = await findNearestChangelog(cwd, file);
11
+ if (!changelogPath) continue;
12
+ const list = boundaries.get(changelogPath) ?? [];
13
+ list.push(file);
14
+ boundaries.set(changelogPath, list);
15
+ }
16
+
17
+ return Array.from(boundaries.entries()).map(([changelogPath, files]) => ({
18
+ changelogPath,
19
+ files,
20
+ }));
21
+ }
22
+
23
+ async function findNearestChangelog(cwd: string, filePath: string): Promise<string | null> {
24
+ let current = path.resolve(cwd, path.dirname(filePath));
25
+ const root = path.resolve(cwd);
26
+ while (true) {
27
+ const candidate = path.resolve(current, CHANGELOG_NAME);
28
+ if (await Bun.file(candidate).exists()) {
29
+ return candidate;
30
+ }
31
+ if (current === root) return null;
32
+ const parent = path.dirname(current);
33
+ if (parent === current) return null;
34
+ current = parent;
35
+ }
36
+ }