@mariozechner/pi-coding-agent 0.30.2 → 0.31.1

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 (297) hide show
  1. package/CHANGELOG.md +251 -1
  2. package/README.md +105 -84
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +5 -1
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/cli/file-processor.d.ts +3 -3
  7. package/dist/cli/file-processor.d.ts.map +1 -1
  8. package/dist/cli/file-processor.js +7 -10
  9. package/dist/cli/file-processor.js.map +1 -1
  10. package/dist/config.d.ts +9 -0
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/config.js +18 -0
  13. package/dist/config.js.map +1 -1
  14. package/dist/core/agent-session.d.ts +73 -34
  15. package/dist/core/agent-session.d.ts.map +1 -1
  16. package/dist/core/agent-session.js +464 -210
  17. package/dist/core/agent-session.js.map +1 -1
  18. package/dist/core/auth-storage.d.ts +2 -2
  19. package/dist/core/auth-storage.d.ts.map +1 -1
  20. package/dist/core/auth-storage.js +2 -2
  21. package/dist/core/auth-storage.js.map +1 -1
  22. package/dist/core/bash-executor.d.ts +2 -2
  23. package/dist/core/bash-executor.d.ts.map +1 -1
  24. package/dist/core/bash-executor.js +2 -2
  25. package/dist/core/bash-executor.js.map +1 -1
  26. package/dist/core/compaction/branch-summarization.d.ts +84 -0
  27. package/dist/core/compaction/branch-summarization.d.ts.map +1 -0
  28. package/dist/core/compaction/branch-summarization.js +233 -0
  29. package/dist/core/compaction/branch-summarization.js.map +1 -0
  30. package/dist/core/{compaction.d.ts → compaction/compaction.d.ts} +38 -19
  31. package/dist/core/compaction/compaction.d.ts.map +1 -0
  32. package/dist/core/compaction/compaction.js +558 -0
  33. package/dist/core/compaction/compaction.js.map +1 -0
  34. package/dist/core/compaction/index.d.ts +7 -0
  35. package/dist/core/compaction/index.d.ts.map +1 -0
  36. package/dist/core/compaction/index.js +7 -0
  37. package/dist/core/compaction/index.js.map +1 -0
  38. package/dist/core/compaction/utils.d.ts +35 -0
  39. package/dist/core/compaction/utils.d.ts.map +1 -0
  40. package/dist/core/compaction/utils.js +138 -0
  41. package/dist/core/compaction/utils.js.map +1 -0
  42. package/dist/core/custom-tools/index.d.ts +2 -1
  43. package/dist/core/custom-tools/index.d.ts.map +1 -1
  44. package/dist/core/custom-tools/index.js +1 -0
  45. package/dist/core/custom-tools/index.js.map +1 -1
  46. package/dist/core/custom-tools/loader.d.ts.map +1 -1
  47. package/dist/core/custom-tools/loader.js +13 -80
  48. package/dist/core/custom-tools/loader.js.map +1 -1
  49. package/dist/core/custom-tools/types.d.ts +84 -59
  50. package/dist/core/custom-tools/types.d.ts.map +1 -1
  51. package/dist/core/custom-tools/types.js.map +1 -1
  52. package/dist/core/custom-tools/wrapper.d.ts +15 -0
  53. package/dist/core/custom-tools/wrapper.d.ts.map +1 -0
  54. package/dist/core/custom-tools/wrapper.js +23 -0
  55. package/dist/core/custom-tools/wrapper.js.map +1 -0
  56. package/dist/core/exec.d.ts +29 -0
  57. package/dist/core/exec.d.ts.map +1 -0
  58. package/dist/core/exec.js +71 -0
  59. package/dist/core/exec.js.map +1 -0
  60. package/dist/core/export-html/index.d.ts +17 -0
  61. package/dist/core/export-html/index.d.ts.map +1 -0
  62. package/dist/core/export-html/index.js +171 -0
  63. package/dist/core/export-html/index.js.map +1 -0
  64. package/dist/core/export-html/template.css +781 -0
  65. package/dist/core/export-html/template.html +54 -0
  66. package/dist/core/export-html/template.js +1185 -0
  67. package/dist/core/export-html/vendor/highlight.min.js +1213 -0
  68. package/dist/core/export-html/vendor/marked.min.js +6 -0
  69. package/dist/core/hooks/index.d.ts +4 -4
  70. package/dist/core/hooks/index.d.ts.map +1 -1
  71. package/dist/core/hooks/index.js +4 -3
  72. package/dist/core/hooks/index.js.map +1 -1
  73. package/dist/core/hooks/loader.d.ts +40 -5
  74. package/dist/core/hooks/loader.d.ts.map +1 -1
  75. package/dist/core/hooks/loader.js +43 -10
  76. package/dist/core/hooks/loader.js.map +1 -1
  77. package/dist/core/hooks/runner.d.ts +94 -18
  78. package/dist/core/hooks/runner.d.ts.map +1 -1
  79. package/dist/core/hooks/runner.js +199 -120
  80. package/dist/core/hooks/runner.js.map +1 -1
  81. package/dist/core/hooks/tool-wrapper.d.ts +1 -1
  82. package/dist/core/hooks/tool-wrapper.d.ts.map +1 -1
  83. package/dist/core/hooks/tool-wrapper.js +36 -19
  84. package/dist/core/hooks/tool-wrapper.js.map +1 -1
  85. package/dist/core/hooks/types.d.ts +407 -96
  86. package/dist/core/hooks/types.d.ts.map +1 -1
  87. package/dist/core/hooks/types.js.map +1 -1
  88. package/dist/core/index.d.ts +4 -3
  89. package/dist/core/index.d.ts.map +1 -1
  90. package/dist/core/index.js.map +1 -1
  91. package/dist/core/messages.d.ts +44 -12
  92. package/dist/core/messages.d.ts.map +1 -1
  93. package/dist/core/messages.js +82 -34
  94. package/dist/core/messages.js.map +1 -1
  95. package/dist/core/model-registry.d.ts +5 -5
  96. package/dist/core/model-registry.d.ts.map +1 -1
  97. package/dist/core/model-registry.js +7 -7
  98. package/dist/core/model-registry.js.map +1 -1
  99. package/dist/core/model-resolver.d.ts +7 -7
  100. package/dist/core/model-resolver.d.ts.map +1 -1
  101. package/dist/core/model-resolver.js +45 -14
  102. package/dist/core/model-resolver.js.map +1 -1
  103. package/dist/core/sdk.d.ts +7 -10
  104. package/dist/core/sdk.d.ts.map +1 -1
  105. package/dist/core/sdk.js +88 -32
  106. package/dist/core/sdk.js.map +1 -1
  107. package/dist/core/session-manager.d.ts +202 -36
  108. package/dist/core/session-manager.d.ts.map +1 -1
  109. package/dist/core/session-manager.js +565 -133
  110. package/dist/core/session-manager.js.map +1 -1
  111. package/dist/core/settings-manager.d.ts +9 -3
  112. package/dist/core/settings-manager.d.ts.map +1 -1
  113. package/dist/core/settings-manager.js +13 -12
  114. package/dist/core/settings-manager.js.map +1 -1
  115. package/dist/core/system-prompt.d.ts.map +1 -1
  116. package/dist/core/system-prompt.js +6 -3
  117. package/dist/core/system-prompt.js.map +1 -1
  118. package/dist/core/tools/bash.d.ts +1 -1
  119. package/dist/core/tools/bash.d.ts.map +1 -1
  120. package/dist/core/tools/bash.js.map +1 -1
  121. package/dist/core/tools/edit-diff.d.ts +33 -0
  122. package/dist/core/tools/edit-diff.d.ts.map +1 -0
  123. package/dist/core/tools/edit-diff.js +171 -0
  124. package/dist/core/tools/edit-diff.js.map +1 -0
  125. package/dist/core/tools/edit.d.ts +7 -1
  126. package/dist/core/tools/edit.d.ts.map +1 -1
  127. package/dist/core/tools/edit.js +20 -95
  128. package/dist/core/tools/edit.js.map +1 -1
  129. package/dist/core/tools/find.d.ts +1 -1
  130. package/dist/core/tools/find.d.ts.map +1 -1
  131. package/dist/core/tools/find.js.map +1 -1
  132. package/dist/core/tools/grep.d.ts +1 -1
  133. package/dist/core/tools/grep.d.ts.map +1 -1
  134. package/dist/core/tools/grep.js.map +1 -1
  135. package/dist/core/tools/index.d.ts +1 -1
  136. package/dist/core/tools/index.d.ts.map +1 -1
  137. package/dist/core/tools/index.js.map +1 -1
  138. package/dist/core/tools/ls.d.ts +1 -1
  139. package/dist/core/tools/ls.d.ts.map +1 -1
  140. package/dist/core/tools/ls.js.map +1 -1
  141. package/dist/core/tools/read.d.ts +1 -1
  142. package/dist/core/tools/read.d.ts.map +1 -1
  143. package/dist/core/tools/read.js.map +1 -1
  144. package/dist/core/tools/write.d.ts +1 -1
  145. package/dist/core/tools/write.d.ts.map +1 -1
  146. package/dist/core/tools/write.js.map +1 -1
  147. package/dist/index.d.ts +8 -7
  148. package/dist/index.d.ts.map +1 -1
  149. package/dist/index.js +5 -3
  150. package/dist/index.js.map +1 -1
  151. package/dist/main.d.ts.map +1 -1
  152. package/dist/main.js +22 -21
  153. package/dist/main.js.map +1 -1
  154. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  155. package/dist/modes/interactive/components/assistant-message.js +3 -4
  156. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  157. package/dist/modes/interactive/components/bash-execution.d.ts +1 -1
  158. package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  159. package/dist/modes/interactive/components/bash-execution.js +6 -2
  160. package/dist/modes/interactive/components/bash-execution.js.map +1 -1
  161. package/dist/modes/interactive/components/bordered-loader.d.ts +12 -0
  162. package/dist/modes/interactive/components/bordered-loader.d.ts.map +1 -0
  163. package/dist/modes/interactive/components/bordered-loader.js +30 -0
  164. package/dist/modes/interactive/components/bordered-loader.js.map +1 -0
  165. package/dist/modes/interactive/components/branch-summary-message.d.ts +14 -0
  166. package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -0
  167. package/dist/modes/interactive/components/branch-summary-message.js +35 -0
  168. package/dist/modes/interactive/components/branch-summary-message.js.map +1 -0
  169. package/dist/modes/interactive/components/compaction-summary-message.d.ts +14 -0
  170. package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -0
  171. package/dist/modes/interactive/components/compaction-summary-message.js +36 -0
  172. package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -0
  173. package/dist/modes/interactive/components/dynamic-border.d.ts +5 -1
  174. package/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
  175. package/dist/modes/interactive/components/dynamic-border.js +5 -1
  176. package/dist/modes/interactive/components/dynamic-border.js.map +1 -1
  177. package/dist/modes/interactive/components/footer.d.ts +12 -6
  178. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  179. package/dist/modes/interactive/components/footer.js +57 -25
  180. package/dist/modes/interactive/components/footer.js.map +1 -1
  181. package/dist/modes/interactive/components/hook-editor.d.ts +15 -0
  182. package/dist/modes/interactive/components/hook-editor.d.ts.map +1 -0
  183. package/dist/modes/interactive/components/hook-editor.js +95 -0
  184. package/dist/modes/interactive/components/hook-editor.js.map +1 -0
  185. package/dist/modes/interactive/components/hook-message.d.ts +18 -0
  186. package/dist/modes/interactive/components/hook-message.d.ts.map +1 -0
  187. package/dist/modes/interactive/components/hook-message.js +80 -0
  188. package/dist/modes/interactive/components/hook-message.js.map +1 -0
  189. package/dist/modes/interactive/components/model-selector.d.ts +3 -3
  190. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  191. package/dist/modes/interactive/components/model-selector.js +6 -1
  192. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  193. package/dist/modes/interactive/components/tool-execution.d.ts +15 -2
  194. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  195. package/dist/modes/interactive/components/tool-execution.js +70 -21
  196. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  197. package/dist/modes/interactive/components/tree-selector.d.ts +52 -0
  198. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -0
  199. package/dist/modes/interactive/components/tree-selector.js +745 -0
  200. package/dist/modes/interactive/components/tree-selector.js.map +1 -0
  201. package/dist/modes/interactive/components/user-message-selector.d.ts +3 -3
  202. package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -1
  203. package/dist/modes/interactive/components/user-message-selector.js +1 -1
  204. package/dist/modes/interactive/components/user-message-selector.js.map +1 -1
  205. package/dist/modes/interactive/components/user-message.d.ts +1 -1
  206. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  207. package/dist/modes/interactive/components/user-message.js +2 -5
  208. package/dist/modes/interactive/components/user-message.js.map +1 -1
  209. package/dist/modes/interactive/interactive-mode.d.ts +29 -12
  210. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  211. package/dist/modes/interactive/interactive-mode.js +589 -208
  212. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  213. package/dist/modes/interactive/theme/dark.json +13 -1
  214. package/dist/modes/interactive/theme/light.json +13 -1
  215. package/dist/modes/interactive/theme/theme-schema.json +34 -0
  216. package/dist/modes/interactive/theme/theme.d.ts +20 -2
  217. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  218. package/dist/modes/interactive/theme/theme.js +135 -2
  219. package/dist/modes/interactive/theme/theme.js.map +1 -1
  220. package/dist/modes/print-mode.d.ts +3 -3
  221. package/dist/modes/print-mode.d.ts.map +1 -1
  222. package/dist/modes/print-mode.js +26 -20
  223. package/dist/modes/print-mode.js.map +1 -1
  224. package/dist/modes/rpc/rpc-client.d.ts +13 -10
  225. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  226. package/dist/modes/rpc/rpc-client.js +11 -10
  227. package/dist/modes/rpc/rpc-client.js.map +1 -1
  228. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  229. package/dist/modes/rpc/rpc-mode.js +88 -35
  230. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  231. package/dist/modes/rpc/rpc-types.d.ts +30 -11
  232. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  233. package/dist/modes/rpc/rpc-types.js.map +1 -1
  234. package/dist/utils/shell.d.ts +4 -2
  235. package/dist/utils/shell.d.ts.map +1 -1
  236. package/dist/utils/shell.js +36 -7
  237. package/dist/utils/shell.js.map +1 -1
  238. package/dist/utils/tools-manager.d.ts +1 -1
  239. package/dist/utils/tools-manager.d.ts.map +1 -1
  240. package/dist/utils/tools-manager.js +2 -2
  241. package/dist/utils/tools-manager.js.map +1 -1
  242. package/docs/compaction.md +388 -0
  243. package/docs/custom-tools.md +146 -43
  244. package/docs/extension-loading.md +1004 -0
  245. package/docs/hooks.md +562 -596
  246. package/docs/rpc.md +33 -19
  247. package/docs/sdk.md +93 -21
  248. package/docs/session-tree-plan.md +441 -0
  249. package/docs/session.md +172 -21
  250. package/docs/skills.md +2 -0
  251. package/docs/theme.md +31 -2
  252. package/docs/tree.md +197 -0
  253. package/docs/tui.md +343 -0
  254. package/examples/README.md +1 -9
  255. package/examples/custom-tools/hello/index.ts +4 -3
  256. package/examples/custom-tools/question/index.ts +4 -4
  257. package/examples/custom-tools/subagent/index.ts +7 -6
  258. package/examples/custom-tools/todo/index.ts +11 -5
  259. package/examples/hooks/README.md +29 -71
  260. package/examples/hooks/auto-commit-on-exit.ts +8 -9
  261. package/examples/hooks/confirm-destructive.ts +29 -30
  262. package/examples/hooks/custom-compaction.ts +20 -21
  263. package/examples/hooks/dirty-repo-guard.ts +41 -40
  264. package/examples/hooks/file-trigger.ts +10 -5
  265. package/examples/hooks/git-checkpoint.ts +16 -12
  266. package/examples/hooks/handoff.ts +150 -0
  267. package/examples/hooks/permission-gate.ts +1 -1
  268. package/examples/hooks/protected-paths.ts +1 -1
  269. package/examples/hooks/qna.ts +119 -0
  270. package/examples/hooks/snake.ts +343 -0
  271. package/examples/hooks/status-line.ts +40 -0
  272. package/examples/sdk/01-minimal.ts +1 -1
  273. package/examples/sdk/02-custom-model.ts +1 -1
  274. package/examples/sdk/03-custom-prompt.ts +1 -1
  275. package/examples/sdk/04-skills.ts +1 -1
  276. package/examples/sdk/05-tools.ts +4 -4
  277. package/examples/sdk/06-hooks.ts +1 -1
  278. package/examples/sdk/07-context-files.ts +1 -1
  279. package/examples/sdk/08-slash-commands.ts +6 -1
  280. package/examples/sdk/09-api-keys-and-oauth.ts +1 -1
  281. package/examples/sdk/10-settings.ts +1 -1
  282. package/examples/sdk/11-sessions.ts +1 -1
  283. package/examples/sdk/12-full-control.ts +4 -7
  284. package/package.json +6 -6
  285. package/dist/core/compaction.d.ts.map +0 -1
  286. package/dist/core/compaction.js +0 -412
  287. package/dist/core/compaction.js.map +0 -1
  288. package/dist/core/export-html.d.ts +0 -23
  289. package/dist/core/export-html.d.ts.map +0 -1
  290. package/dist/core/export-html.js +0 -1185
  291. package/dist/core/export-html.js.map +0 -1
  292. package/dist/modes/interactive/components/compaction.d.ts +0 -15
  293. package/dist/modes/interactive/components/compaction.d.ts.map +0 -1
  294. package/dist/modes/interactive/components/compaction.js +0 -41
  295. package/dist/modes/interactive/components/compaction.js.map +0 -1
  296. package/docs/hooks-v2.md +0 -385
  297. package/docs/session-tree.md +0 -452
@@ -1 +1 @@
1
- {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/runner.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,KAAK,EAGX,SAAS,EACT,SAAS,EAET,aAAa,EAEb,kBAAkB,EAClB,aAAa,EACb,mBAAmB,EACnB,qBAAqB,EACrB,MAAM,YAAY,CAAC;AAOpB;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;AA4F3D;;GAEG;AACH,qBAAa,UAAU;IACtB,OAAO,CAAC,KAAK,CAAe;IAC5B,OAAO,CAAC,SAAS,CAAgB;IACjC,OAAO,CAAC,KAAK,CAAU;IACvB,OAAO,CAAC,GAAG,CAAS;IACpB,OAAO,CAAC,WAAW,CAAgB;IACnC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,cAAc,CAAqC;IAE3D,YAAY,KAAK,EAAE,UAAU,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,MAAwB,EAO9E;IAED;;;OAGG;IACH,YAAY,CAAC,SAAS,EAAE,aAAa,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAG3D;IAED;;OAEG;IACH,YAAY,IAAI,MAAM,EAAE,CAEvB;IAED;;OAEG;IACH,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAE/C;IAED;;;OAGG;IACH,cAAc,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,CAIzC;IAED;;;OAGG;IACH,OAAO,CAAC,QAAQ,EAAE,iBAAiB,GAAG,MAAM,IAAI,CAG/C;IAED;;OAEG;IACH,OAAO,CAAC,SAAS;IAMjB;;OAEG;IACH,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAQtC;IAED;;OAEG;IACH,OAAO,CAAC,aAAa;IAUrB;;;OAGG;IACG,IAAI,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,kBAAkB,GAAG,qBAAqB,GAAG,SAAS,CAAC,CA+C5F;IAED;;;;OAIG;IACG,YAAY,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CAuBjF;CACD","sourcesContent":["/**\n * Hook runner - executes hooks and manages their lifecycle.\n */\n\nimport { spawn } from \"node:child_process\";\nimport type { LoadedHook, SendHandler } from \"./loader.js\";\nimport type {\n\tExecOptions,\n\tExecResult,\n\tHookError,\n\tHookEvent,\n\tHookEventContext,\n\tHookUIContext,\n\tSessionEvent,\n\tSessionEventResult,\n\tToolCallEvent,\n\tToolCallEventResult,\n\tToolResultEventResult,\n} from \"./types.js\";\n\n/**\n * Default timeout for hook execution (30 seconds).\n */\nconst DEFAULT_TIMEOUT = 30000;\n\n/**\n * Listener for hook errors.\n */\nexport type HookErrorListener = (error: HookError) => void;\n\n/**\n * Execute a command and return stdout/stderr/code.\n * Supports cancellation via AbortSignal and timeout.\n */\nasync function exec(command: string, args: string[], cwd: string, options?: ExecOptions): Promise<ExecResult> {\n\treturn new Promise((resolve) => {\n\t\tconst proc = spawn(command, args, { cwd, shell: false });\n\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\t\tlet killed = false;\n\t\tlet timeoutId: NodeJS.Timeout | undefined;\n\n\t\tconst killProcess = () => {\n\t\t\tif (!killed) {\n\t\t\t\tkilled = true;\n\t\t\t\tproc.kill(\"SIGTERM\");\n\t\t\t\t// Force kill after 5 seconds if SIGTERM doesn't work\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\tif (!proc.killed) {\n\t\t\t\t\t\tproc.kill(\"SIGKILL\");\n\t\t\t\t\t}\n\t\t\t\t}, 5000);\n\t\t\t}\n\t\t};\n\n\t\t// Handle abort signal\n\t\tif (options?.signal) {\n\t\t\tif (options.signal.aborted) {\n\t\t\t\tkillProcess();\n\t\t\t} else {\n\t\t\t\toptions.signal.addEventListener(\"abort\", killProcess, { once: true });\n\t\t\t}\n\t\t}\n\n\t\t// Handle timeout\n\t\tif (options?.timeout && options.timeout > 0) {\n\t\t\ttimeoutId = setTimeout(() => {\n\t\t\t\tkillProcess();\n\t\t\t}, options.timeout);\n\t\t}\n\n\t\tproc.stdout?.on(\"data\", (data) => {\n\t\t\tstdout += data.toString();\n\t\t});\n\n\t\tproc.stderr?.on(\"data\", (data) => {\n\t\t\tstderr += data.toString();\n\t\t});\n\n\t\tproc.on(\"close\", (code) => {\n\t\t\tif (timeoutId) clearTimeout(timeoutId);\n\t\t\tif (options?.signal) {\n\t\t\t\toptions.signal.removeEventListener(\"abort\", killProcess);\n\t\t\t}\n\t\t\tresolve({ stdout, stderr, code: code ?? 0, killed });\n\t\t});\n\n\t\tproc.on(\"error\", (_err) => {\n\t\t\tif (timeoutId) clearTimeout(timeoutId);\n\t\t\tif (options?.signal) {\n\t\t\t\toptions.signal.removeEventListener(\"abort\", killProcess);\n\t\t\t}\n\t\t\tresolve({ stdout, stderr, code: 1, killed });\n\t\t});\n\t});\n}\n\n/**\n * Create a promise that rejects after a timeout.\n */\nfunction createTimeout(ms: number): { promise: Promise<never>; clear: () => void } {\n\tlet timeoutId: NodeJS.Timeout;\n\tconst promise = new Promise<never>((_, reject) => {\n\t\ttimeoutId = setTimeout(() => reject(new Error(`Hook timed out after ${ms}ms`)), ms);\n\t});\n\treturn {\n\t\tpromise,\n\t\tclear: () => clearTimeout(timeoutId),\n\t};\n}\n\n/** No-op UI context used when no UI is available */\nconst noOpUIContext: HookUIContext = {\n\tselect: async () => null,\n\tconfirm: async () => false,\n\tinput: async () => null,\n\tnotify: () => {},\n};\n\n/**\n * HookRunner executes hooks and manages event emission.\n */\nexport class HookRunner {\n\tprivate hooks: LoadedHook[];\n\tprivate uiContext: HookUIContext;\n\tprivate hasUI: boolean;\n\tprivate cwd: string;\n\tprivate sessionFile: string | null;\n\tprivate timeout: number;\n\tprivate errorListeners: Set<HookErrorListener> = new Set();\n\n\tconstructor(hooks: LoadedHook[], cwd: string, timeout: number = DEFAULT_TIMEOUT) {\n\t\tthis.hooks = hooks;\n\t\tthis.uiContext = noOpUIContext;\n\t\tthis.hasUI = false;\n\t\tthis.cwd = cwd;\n\t\tthis.sessionFile = null;\n\t\tthis.timeout = timeout;\n\t}\n\n\t/**\n\t * Set the UI context for hooks.\n\t * Call this when the mode initializes and UI is available.\n\t */\n\tsetUIContext(uiContext: HookUIContext, hasUI: boolean): void {\n\t\tthis.uiContext = uiContext;\n\t\tthis.hasUI = hasUI;\n\t}\n\n\t/**\n\t * Get the paths of all loaded hooks.\n\t */\n\tgetHookPaths(): string[] {\n\t\treturn this.hooks.map((h) => h.path);\n\t}\n\n\t/**\n\t * Set the session file path.\n\t */\n\tsetSessionFile(sessionFile: string | null): void {\n\t\tthis.sessionFile = sessionFile;\n\t}\n\n\t/**\n\t * Set the send handler for all hooks' pi.send().\n\t * Call this when the mode initializes.\n\t */\n\tsetSendHandler(handler: SendHandler): void {\n\t\tfor (const hook of this.hooks) {\n\t\t\thook.setSendHandler(handler);\n\t\t}\n\t}\n\n\t/**\n\t * Subscribe to hook errors.\n\t * @returns Unsubscribe function\n\t */\n\tonError(listener: HookErrorListener): () => void {\n\t\tthis.errorListeners.add(listener);\n\t\treturn () => this.errorListeners.delete(listener);\n\t}\n\n\t/**\n\t * Emit an error to all listeners.\n\t */\n\tprivate emitError(error: HookError): void {\n\t\tfor (const listener of this.errorListeners) {\n\t\t\tlistener(error);\n\t\t}\n\t}\n\n\t/**\n\t * Check if any hooks have handlers for the given event type.\n\t */\n\thasHandlers(eventType: string): boolean {\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst handlers = hook.handlers.get(eventType);\n\t\t\tif (handlers && handlers.length > 0) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * Create the event context for handlers.\n\t */\n\tprivate createContext(): HookEventContext {\n\t\treturn {\n\t\t\texec: (command: string, args: string[], options?: ExecOptions) => exec(command, args, this.cwd, options),\n\t\t\tui: this.uiContext,\n\t\t\thasUI: this.hasUI,\n\t\t\tcwd: this.cwd,\n\t\t\tsessionFile: this.sessionFile,\n\t\t};\n\t}\n\n\t/**\n\t * Emit an event to all hooks.\n\t * Returns the result from session/tool_result events (if any handler returns one).\n\t */\n\tasync emit(event: HookEvent): Promise<SessionEventResult | ToolResultEventResult | undefined> {\n\t\tconst ctx = this.createContext();\n\t\tlet result: SessionEventResult | ToolResultEventResult | undefined;\n\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst handlers = hook.handlers.get(event.type);\n\t\t\tif (!handlers || handlers.length === 0) continue;\n\n\t\t\tfor (const handler of handlers) {\n\t\t\t\ttry {\n\t\t\t\t\t// No timeout for before_compact events (like tool_call, they may take a while)\n\t\t\t\t\tconst isBeforeCompact = event.type === \"session\" && (event as SessionEvent).reason === \"before_compact\";\n\t\t\t\t\tlet handlerResult: unknown;\n\n\t\t\t\t\tif (isBeforeCompact) {\n\t\t\t\t\t\thandlerResult = await handler(event, ctx);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst timeout = createTimeout(this.timeout);\n\t\t\t\t\t\thandlerResult = await Promise.race([handler(event, ctx), timeout.promise]);\n\t\t\t\t\t\ttimeout.clear();\n\t\t\t\t\t}\n\n\t\t\t\t\t// For session events, capture the result (for before_* cancellation)\n\t\t\t\t\tif (event.type === \"session\" && handlerResult) {\n\t\t\t\t\t\tresult = handlerResult as SessionEventResult;\n\t\t\t\t\t\t// If cancelled, stop processing further hooks\n\t\t\t\t\t\tif (result.cancel) {\n\t\t\t\t\t\t\treturn result;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// For tool_result events, capture the result\n\t\t\t\t\tif (event.type === \"tool_result\" && handlerResult) {\n\t\t\t\t\t\tresult = handlerResult as ToolResultEventResult;\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\t\t\t\tthis.emitError({\n\t\t\t\t\t\thookPath: hook.path,\n\t\t\t\t\t\tevent: event.type,\n\t\t\t\t\t\terror: message,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Emit a tool_call event to all hooks.\n\t * No timeout - user prompts can take as long as needed.\n\t * Errors are thrown (not swallowed) so caller can block on failure.\n\t */\n\tasync emitToolCall(event: ToolCallEvent): Promise<ToolCallEventResult | undefined> {\n\t\tconst ctx = this.createContext();\n\t\tlet result: ToolCallEventResult | undefined;\n\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst handlers = hook.handlers.get(\"tool_call\");\n\t\t\tif (!handlers || handlers.length === 0) continue;\n\n\t\t\tfor (const handler of handlers) {\n\t\t\t\t// No timeout - let user take their time\n\t\t\t\tconst handlerResult = await handler(event, ctx);\n\n\t\t\t\tif (handlerResult) {\n\t\t\t\t\tresult = handlerResult as ToolCallEventResult;\n\t\t\t\t\t// If blocked, stop processing further hooks\n\t\t\t\t\tif (result.block) {\n\t\t\t\t\t\treturn result;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n}\n"]}
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/runner.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Hook runner - executes hooks and manages their lifecycle.\n */\n\nimport type { AgentMessage } from \"@mariozechner/pi-agent-core\";\nimport type { Model } from \"@mariozechner/pi-ai\";\nimport { theme } from \"../../modes/interactive/theme/theme.js\";\nimport type { ModelRegistry } from \"../model-registry.js\";\nimport type { SessionManager } from \"../session-manager.js\";\nimport type {\n\tAppendEntryHandler,\n\tBranchHandler,\n\tLoadedHook,\n\tNavigateTreeHandler,\n\tNewSessionHandler,\n\tSendMessageHandler,\n} from \"./loader.js\";\nimport type {\n\tBeforeAgentStartEvent,\n\tBeforeAgentStartEventResult,\n\tContextEvent,\n\tContextEventResult,\n\tHookCommandContext,\n\tHookContext,\n\tHookError,\n\tHookEvent,\n\tHookMessageRenderer,\n\tHookUIContext,\n\tRegisteredCommand,\n\tSessionBeforeCompactResult,\n\tSessionBeforeTreeResult,\n\tToolCallEvent,\n\tToolCallEventResult,\n\tToolResultEventResult,\n} from \"./types.js\";\n\n/**\n * Listener for hook errors.\n */\nexport type HookErrorListener = (error: HookError) => void;\n\n// Re-export execCommand for backward compatibility\nexport { execCommand } from \"../exec.js\";\n\n/** No-op UI context used when no UI is available */\nconst noOpUIContext: HookUIContext = {\n\tselect: async () => undefined,\n\tconfirm: async () => false,\n\tinput: async () => undefined,\n\tnotify: () => {},\n\tsetStatus: () => {},\n\tcustom: async () => undefined as never,\n\tsetEditorText: () => {},\n\tgetEditorText: () => \"\",\n\teditor: async () => undefined,\n\tget theme() {\n\t\treturn theme;\n\t},\n};\n\n/**\n * HookRunner executes hooks and manages event emission.\n */\nexport class HookRunner {\n\tprivate hooks: LoadedHook[];\n\tprivate uiContext: HookUIContext;\n\tprivate hasUI: boolean;\n\tprivate cwd: string;\n\tprivate sessionManager: SessionManager;\n\tprivate modelRegistry: ModelRegistry;\n\tprivate errorListeners: Set<HookErrorListener> = new Set();\n\tprivate getModel: () => Model<any> | undefined = () => undefined;\n\tprivate isIdleFn: () => boolean = () => true;\n\tprivate waitForIdleFn: () => Promise<void> = async () => {};\n\tprivate abortFn: () => void = () => {};\n\tprivate hasQueuedMessagesFn: () => boolean = () => false;\n\tprivate newSessionHandler: NewSessionHandler = async () => ({ cancelled: false });\n\tprivate branchHandler: BranchHandler = async () => ({ cancelled: false });\n\tprivate navigateTreeHandler: NavigateTreeHandler = async () => ({ cancelled: false });\n\n\tconstructor(hooks: LoadedHook[], cwd: string, sessionManager: SessionManager, modelRegistry: ModelRegistry) {\n\t\tthis.hooks = hooks;\n\t\tthis.uiContext = noOpUIContext;\n\t\tthis.hasUI = false;\n\t\tthis.cwd = cwd;\n\t\tthis.sessionManager = sessionManager;\n\t\tthis.modelRegistry = modelRegistry;\n\t}\n\n\t/**\n\t * Initialize HookRunner with all required context.\n\t * Modes call this once the agent session is fully set up.\n\t */\n\tinitialize(options: {\n\t\t/** Function to get the current model */\n\t\tgetModel: () => Model<any> | undefined;\n\t\t/** Handler for hooks to send messages */\n\t\tsendMessageHandler: SendMessageHandler;\n\t\t/** Handler for hooks to append entries */\n\t\tappendEntryHandler: AppendEntryHandler;\n\t\t/** Handler for creating new sessions (for HookCommandContext) */\n\t\tnewSessionHandler?: NewSessionHandler;\n\t\t/** Handler for branching sessions (for HookCommandContext) */\n\t\tbranchHandler?: BranchHandler;\n\t\t/** Handler for navigating session tree (for HookCommandContext) */\n\t\tnavigateTreeHandler?: NavigateTreeHandler;\n\t\t/** Function to check if agent is idle */\n\t\tisIdle?: () => boolean;\n\t\t/** Function to wait for agent to be idle */\n\t\twaitForIdle?: () => Promise<void>;\n\t\t/** Function to abort current operation (fire-and-forget) */\n\t\tabort?: () => void;\n\t\t/** Function to check if there are queued messages */\n\t\thasQueuedMessages?: () => boolean;\n\t\t/** UI context for interactive prompts */\n\t\tuiContext?: HookUIContext;\n\t\t/** Whether UI is available */\n\t\thasUI?: boolean;\n\t}): void {\n\t\tthis.getModel = options.getModel;\n\t\tthis.isIdleFn = options.isIdle ?? (() => true);\n\t\tthis.waitForIdleFn = options.waitForIdle ?? (async () => {});\n\t\tthis.abortFn = options.abort ?? (() => {});\n\t\tthis.hasQueuedMessagesFn = options.hasQueuedMessages ?? (() => false);\n\t\t// Store session handlers for HookCommandContext\n\t\tif (options.newSessionHandler) {\n\t\t\tthis.newSessionHandler = options.newSessionHandler;\n\t\t}\n\t\tif (options.branchHandler) {\n\t\t\tthis.branchHandler = options.branchHandler;\n\t\t}\n\t\tif (options.navigateTreeHandler) {\n\t\t\tthis.navigateTreeHandler = options.navigateTreeHandler;\n\t\t}\n\t\t// Set per-hook handlers for pi.sendMessage() and pi.appendEntry()\n\t\tfor (const hook of this.hooks) {\n\t\t\thook.setSendMessageHandler(options.sendMessageHandler);\n\t\t\thook.setAppendEntryHandler(options.appendEntryHandler);\n\t\t}\n\t\tthis.uiContext = options.uiContext ?? noOpUIContext;\n\t\tthis.hasUI = options.hasUI ?? false;\n\t}\n\n\t/**\n\t * Get the UI context (set by mode).\n\t */\n\tgetUIContext(): HookUIContext | null {\n\t\treturn this.uiContext;\n\t}\n\n\t/**\n\t * Get whether UI is available.\n\t */\n\tgetHasUI(): boolean {\n\t\treturn this.hasUI;\n\t}\n\n\t/**\n\t * Get the paths of all loaded hooks.\n\t */\n\tgetHookPaths(): string[] {\n\t\treturn this.hooks.map((h) => h.path);\n\t}\n\n\t/**\n\t * Subscribe to hook errors.\n\t * @returns Unsubscribe function\n\t */\n\tonError(listener: HookErrorListener): () => void {\n\t\tthis.errorListeners.add(listener);\n\t\treturn () => this.errorListeners.delete(listener);\n\t}\n\n\t/**\n\t * Emit an error to all listeners.\n\t */\n\t/**\n\t * Emit an error to all error listeners.\n\t */\n\temitError(error: HookError): void {\n\t\tfor (const listener of this.errorListeners) {\n\t\t\tlistener(error);\n\t\t}\n\t}\n\n\t/**\n\t * Check if any hooks have handlers for the given event type.\n\t */\n\thasHandlers(eventType: string): boolean {\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst handlers = hook.handlers.get(eventType);\n\t\t\tif (handlers && handlers.length > 0) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * Get a message renderer for the given customType.\n\t * Returns the first renderer found across all hooks, or undefined if none.\n\t */\n\tgetMessageRenderer(customType: string): HookMessageRenderer | undefined {\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst renderer = hook.messageRenderers.get(customType);\n\t\t\tif (renderer) {\n\t\t\t\treturn renderer;\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Get all registered commands from all hooks.\n\t */\n\tgetRegisteredCommands(): RegisteredCommand[] {\n\t\tconst commands: RegisteredCommand[] = [];\n\t\tfor (const hook of this.hooks) {\n\t\t\tfor (const command of hook.commands.values()) {\n\t\t\t\tcommands.push(command);\n\t\t\t}\n\t\t}\n\t\treturn commands;\n\t}\n\n\t/**\n\t * Get a registered command by name.\n\t * Returns the first command found across all hooks, or undefined if none.\n\t */\n\tgetCommand(name: string): RegisteredCommand | undefined {\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst command = hook.commands.get(name);\n\t\t\tif (command) {\n\t\t\t\treturn command;\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Create the event context for handlers.\n\t */\n\tprivate createContext(): HookContext {\n\t\treturn {\n\t\t\tui: this.uiContext,\n\t\t\thasUI: this.hasUI,\n\t\t\tcwd: this.cwd,\n\t\t\tsessionManager: this.sessionManager,\n\t\t\tmodelRegistry: this.modelRegistry,\n\t\t\tmodel: this.getModel(),\n\t\t\tisIdle: () => this.isIdleFn(),\n\t\t\tabort: () => this.abortFn(),\n\t\t\thasQueuedMessages: () => this.hasQueuedMessagesFn(),\n\t\t};\n\t}\n\n\t/**\n\t * Create the command context for slash command handlers.\n\t * Extends HookContext with session control methods that are only safe in commands.\n\t */\n\tcreateCommandContext(): HookCommandContext {\n\t\treturn {\n\t\t\t...this.createContext(),\n\t\t\twaitForIdle: () => this.waitForIdleFn(),\n\t\t\tnewSession: (options) => this.newSessionHandler(options),\n\t\t\tbranch: (entryId) => this.branchHandler(entryId),\n\t\t\tnavigateTree: (targetId, options) => this.navigateTreeHandler(targetId, options),\n\t\t};\n\t}\n\n\t/**\n\t * Check if event type is a session \"before_*\" event that can be cancelled.\n\t */\n\tprivate isSessionBeforeEvent(\n\t\ttype: string,\n\t): type is \"session_before_switch\" | \"session_before_branch\" | \"session_before_compact\" | \"session_before_tree\" {\n\t\treturn (\n\t\t\ttype === \"session_before_switch\" ||\n\t\t\ttype === \"session_before_branch\" ||\n\t\t\ttype === \"session_before_compact\" ||\n\t\t\ttype === \"session_before_tree\"\n\t\t);\n\t}\n\n\t/**\n\t * Emit an event to all hooks.\n\t * Returns the result from session before_* / tool_result events (if any handler returns one).\n\t */\n\tasync emit(\n\t\tevent: HookEvent,\n\t): Promise<SessionBeforeCompactResult | SessionBeforeTreeResult | ToolResultEventResult | undefined> {\n\t\tconst ctx = this.createContext();\n\t\tlet result: SessionBeforeCompactResult | SessionBeforeTreeResult | ToolResultEventResult | undefined;\n\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst handlers = hook.handlers.get(event.type);\n\t\t\tif (!handlers || handlers.length === 0) continue;\n\n\t\t\tfor (const handler of handlers) {\n\t\t\t\ttry {\n\t\t\t\t\tconst handlerResult = await handler(event, ctx);\n\n\t\t\t\t\t// For session before_* events, capture the result (for cancellation)\n\t\t\t\t\tif (this.isSessionBeforeEvent(event.type) && handlerResult) {\n\t\t\t\t\t\tresult = handlerResult as SessionBeforeCompactResult | SessionBeforeTreeResult;\n\t\t\t\t\t\t// If cancelled, stop processing further hooks\n\t\t\t\t\t\tif (result.cancel) {\n\t\t\t\t\t\t\treturn result;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// For tool_result events, capture the result\n\t\t\t\t\tif (event.type === \"tool_result\" && handlerResult) {\n\t\t\t\t\t\tresult = handlerResult as ToolResultEventResult;\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\t\t\t\tthis.emitError({\n\t\t\t\t\t\thookPath: hook.path,\n\t\t\t\t\t\tevent: event.type,\n\t\t\t\t\t\terror: message,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Emit a tool_call event to all hooks.\n\t * No timeout - user prompts can take as long as needed.\n\t * Errors are thrown (not swallowed) so caller can block on failure.\n\t */\n\tasync emitToolCall(event: ToolCallEvent): Promise<ToolCallEventResult | undefined> {\n\t\tconst ctx = this.createContext();\n\t\tlet result: ToolCallEventResult | undefined;\n\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst handlers = hook.handlers.get(\"tool_call\");\n\t\t\tif (!handlers || handlers.length === 0) continue;\n\n\t\t\tfor (const handler of handlers) {\n\t\t\t\t// No timeout - let user take their time\n\t\t\t\tconst handlerResult = await handler(event, ctx);\n\n\t\t\t\tif (handlerResult) {\n\t\t\t\t\tresult = handlerResult as ToolCallEventResult;\n\t\t\t\t\t// If blocked, stop processing further hooks\n\t\t\t\t\tif (result.block) {\n\t\t\t\t\t\treturn result;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Emit a context event to all hooks.\n\t * Handlers are chained - each gets the previous handler's output (if any).\n\t * Returns the final modified messages, or the original if no modifications.\n\t *\n\t * Note: Messages are already deep-copied by the caller (pi-ai preprocessor).\n\t */\n\tasync emitContext(messages: AgentMessage[]): Promise<AgentMessage[]> {\n\t\tconst ctx = this.createContext();\n\t\tlet currentMessages = messages;\n\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst handlers = hook.handlers.get(\"context\");\n\t\t\tif (!handlers || handlers.length === 0) continue;\n\n\t\t\tfor (const handler of handlers) {\n\t\t\t\ttry {\n\t\t\t\t\tconst event: ContextEvent = { type: \"context\", messages: currentMessages };\n\t\t\t\t\tconst handlerResult = await handler(event, ctx);\n\n\t\t\t\t\tif (handlerResult && (handlerResult as ContextEventResult).messages) {\n\t\t\t\t\t\tcurrentMessages = (handlerResult as ContextEventResult).messages!;\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\t\t\t\tthis.emitError({\n\t\t\t\t\t\thookPath: hook.path,\n\t\t\t\t\t\tevent: \"context\",\n\t\t\t\t\t\terror: message,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn currentMessages;\n\t}\n\n\t/**\n\t * Emit before_agent_start event to all hooks.\n\t * Returns the first message to inject (if any handler returns one).\n\t */\n\tasync emitBeforeAgentStart(\n\t\tprompt: string,\n\t\timages?: import(\"@mariozechner/pi-ai\").ImageContent[],\n\t): Promise<BeforeAgentStartEventResult | undefined> {\n\t\tconst ctx = this.createContext();\n\t\tlet result: BeforeAgentStartEventResult | undefined;\n\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst handlers = hook.handlers.get(\"before_agent_start\");\n\t\t\tif (!handlers || handlers.length === 0) continue;\n\n\t\t\tfor (const handler of handlers) {\n\t\t\t\ttry {\n\t\t\t\t\tconst event: BeforeAgentStartEvent = { type: \"before_agent_start\", prompt, images };\n\t\t\t\t\tconst handlerResult = await handler(event, ctx);\n\n\t\t\t\t\t// Take the first message returned\n\t\t\t\t\tif (handlerResult && (handlerResult as BeforeAgentStartEventResult).message && !result) {\n\t\t\t\t\t\tresult = handlerResult as BeforeAgentStartEventResult;\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\t\t\t\tthis.emitError({\n\t\t\t\t\t\thookPath: hook.path,\n\t\t\t\t\t\tevent: \"before_agent_start\",\n\t\t\t\t\t\terror: message,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n}\n"]}
@@ -1,92 +1,23 @@
1
1
  /**
2
2
  * Hook runner - executes hooks and manages their lifecycle.
3
3
  */
4
- import { spawn } from "node:child_process";
5
- /**
6
- * Default timeout for hook execution (30 seconds).
7
- */
8
- const DEFAULT_TIMEOUT = 30000;
9
- /**
10
- * Execute a command and return stdout/stderr/code.
11
- * Supports cancellation via AbortSignal and timeout.
12
- */
13
- async function exec(command, args, cwd, options) {
14
- return new Promise((resolve) => {
15
- const proc = spawn(command, args, { cwd, shell: false });
16
- let stdout = "";
17
- let stderr = "";
18
- let killed = false;
19
- let timeoutId;
20
- const killProcess = () => {
21
- if (!killed) {
22
- killed = true;
23
- proc.kill("SIGTERM");
24
- // Force kill after 5 seconds if SIGTERM doesn't work
25
- setTimeout(() => {
26
- if (!proc.killed) {
27
- proc.kill("SIGKILL");
28
- }
29
- }, 5000);
30
- }
31
- };
32
- // Handle abort signal
33
- if (options?.signal) {
34
- if (options.signal.aborted) {
35
- killProcess();
36
- }
37
- else {
38
- options.signal.addEventListener("abort", killProcess, { once: true });
39
- }
40
- }
41
- // Handle timeout
42
- if (options?.timeout && options.timeout > 0) {
43
- timeoutId = setTimeout(() => {
44
- killProcess();
45
- }, options.timeout);
46
- }
47
- proc.stdout?.on("data", (data) => {
48
- stdout += data.toString();
49
- });
50
- proc.stderr?.on("data", (data) => {
51
- stderr += data.toString();
52
- });
53
- proc.on("close", (code) => {
54
- if (timeoutId)
55
- clearTimeout(timeoutId);
56
- if (options?.signal) {
57
- options.signal.removeEventListener("abort", killProcess);
58
- }
59
- resolve({ stdout, stderr, code: code ?? 0, killed });
60
- });
61
- proc.on("error", (_err) => {
62
- if (timeoutId)
63
- clearTimeout(timeoutId);
64
- if (options?.signal) {
65
- options.signal.removeEventListener("abort", killProcess);
66
- }
67
- resolve({ stdout, stderr, code: 1, killed });
68
- });
69
- });
70
- }
71
- /**
72
- * Create a promise that rejects after a timeout.
73
- */
74
- function createTimeout(ms) {
75
- let timeoutId;
76
- const promise = new Promise((_, reject) => {
77
- timeoutId = setTimeout(() => reject(new Error(`Hook timed out after ${ms}ms`)), ms);
78
- });
79
- return {
80
- promise,
81
- clear: () => clearTimeout(timeoutId),
82
- };
83
- }
4
+ import { theme } from "../../modes/interactive/theme/theme.js";
5
+ // Re-export execCommand for backward compatibility
6
+ export { execCommand } from "../exec.js";
84
7
  /** No-op UI context used when no UI is available */
85
8
  const noOpUIContext = {
86
- select: async () => null,
9
+ select: async () => undefined,
87
10
  confirm: async () => false,
88
- input: async () => null,
11
+ input: async () => undefined,
89
12
  notify: () => { },
13
+ setStatus: () => { },
14
+ custom: async () => undefined,
15
+ setEditorText: () => { },
16
+ getEditorText: () => "",
17
+ editor: async () => undefined,
18
+ get theme() {
19
+ return theme;
20
+ },
90
21
  };
91
22
  /**
92
23
  * HookRunner executes hooks and manages event emission.
@@ -96,45 +27,70 @@ export class HookRunner {
96
27
  uiContext;
97
28
  hasUI;
98
29
  cwd;
99
- sessionFile;
100
- timeout;
30
+ sessionManager;
31
+ modelRegistry;
101
32
  errorListeners = new Set();
102
- constructor(hooks, cwd, timeout = DEFAULT_TIMEOUT) {
33
+ getModel = () => undefined;
34
+ isIdleFn = () => true;
35
+ waitForIdleFn = async () => { };
36
+ abortFn = () => { };
37
+ hasQueuedMessagesFn = () => false;
38
+ newSessionHandler = async () => ({ cancelled: false });
39
+ branchHandler = async () => ({ cancelled: false });
40
+ navigateTreeHandler = async () => ({ cancelled: false });
41
+ constructor(hooks, cwd, sessionManager, modelRegistry) {
103
42
  this.hooks = hooks;
104
43
  this.uiContext = noOpUIContext;
105
44
  this.hasUI = false;
106
45
  this.cwd = cwd;
107
- this.sessionFile = null;
108
- this.timeout = timeout;
46
+ this.sessionManager = sessionManager;
47
+ this.modelRegistry = modelRegistry;
109
48
  }
110
49
  /**
111
- * Set the UI context for hooks.
112
- * Call this when the mode initializes and UI is available.
50
+ * Initialize HookRunner with all required context.
51
+ * Modes call this once the agent session is fully set up.
113
52
  */
114
- setUIContext(uiContext, hasUI) {
115
- this.uiContext = uiContext;
116
- this.hasUI = hasUI;
53
+ initialize(options) {
54
+ this.getModel = options.getModel;
55
+ this.isIdleFn = options.isIdle ?? (() => true);
56
+ this.waitForIdleFn = options.waitForIdle ?? (async () => { });
57
+ this.abortFn = options.abort ?? (() => { });
58
+ this.hasQueuedMessagesFn = options.hasQueuedMessages ?? (() => false);
59
+ // Store session handlers for HookCommandContext
60
+ if (options.newSessionHandler) {
61
+ this.newSessionHandler = options.newSessionHandler;
62
+ }
63
+ if (options.branchHandler) {
64
+ this.branchHandler = options.branchHandler;
65
+ }
66
+ if (options.navigateTreeHandler) {
67
+ this.navigateTreeHandler = options.navigateTreeHandler;
68
+ }
69
+ // Set per-hook handlers for pi.sendMessage() and pi.appendEntry()
70
+ for (const hook of this.hooks) {
71
+ hook.setSendMessageHandler(options.sendMessageHandler);
72
+ hook.setAppendEntryHandler(options.appendEntryHandler);
73
+ }
74
+ this.uiContext = options.uiContext ?? noOpUIContext;
75
+ this.hasUI = options.hasUI ?? false;
117
76
  }
118
77
  /**
119
- * Get the paths of all loaded hooks.
78
+ * Get the UI context (set by mode).
120
79
  */
121
- getHookPaths() {
122
- return this.hooks.map((h) => h.path);
80
+ getUIContext() {
81
+ return this.uiContext;
123
82
  }
124
83
  /**
125
- * Set the session file path.
84
+ * Get whether UI is available.
126
85
  */
127
- setSessionFile(sessionFile) {
128
- this.sessionFile = sessionFile;
86
+ getHasUI() {
87
+ return this.hasUI;
129
88
  }
130
89
  /**
131
- * Set the send handler for all hooks' pi.send().
132
- * Call this when the mode initializes.
90
+ * Get the paths of all loaded hooks.
133
91
  */
134
- setSendHandler(handler) {
135
- for (const hook of this.hooks) {
136
- hook.setSendHandler(handler);
137
- }
92
+ getHookPaths() {
93
+ return this.hooks.map((h) => h.path);
138
94
  }
139
95
  /**
140
96
  * Subscribe to hook errors.
@@ -147,6 +103,9 @@ export class HookRunner {
147
103
  /**
148
104
  * Emit an error to all listeners.
149
105
  */
106
+ /**
107
+ * Emit an error to all error listeners.
108
+ */
150
109
  emitError(error) {
151
110
  for (const listener of this.errorListeners) {
152
111
  listener(error);
@@ -164,21 +123,85 @@ export class HookRunner {
164
123
  }
165
124
  return false;
166
125
  }
126
+ /**
127
+ * Get a message renderer for the given customType.
128
+ * Returns the first renderer found across all hooks, or undefined if none.
129
+ */
130
+ getMessageRenderer(customType) {
131
+ for (const hook of this.hooks) {
132
+ const renderer = hook.messageRenderers.get(customType);
133
+ if (renderer) {
134
+ return renderer;
135
+ }
136
+ }
137
+ return undefined;
138
+ }
139
+ /**
140
+ * Get all registered commands from all hooks.
141
+ */
142
+ getRegisteredCommands() {
143
+ const commands = [];
144
+ for (const hook of this.hooks) {
145
+ for (const command of hook.commands.values()) {
146
+ commands.push(command);
147
+ }
148
+ }
149
+ return commands;
150
+ }
151
+ /**
152
+ * Get a registered command by name.
153
+ * Returns the first command found across all hooks, or undefined if none.
154
+ */
155
+ getCommand(name) {
156
+ for (const hook of this.hooks) {
157
+ const command = hook.commands.get(name);
158
+ if (command) {
159
+ return command;
160
+ }
161
+ }
162
+ return undefined;
163
+ }
167
164
  /**
168
165
  * Create the event context for handlers.
169
166
  */
170
167
  createContext() {
171
168
  return {
172
- exec: (command, args, options) => exec(command, args, this.cwd, options),
173
169
  ui: this.uiContext,
174
170
  hasUI: this.hasUI,
175
171
  cwd: this.cwd,
176
- sessionFile: this.sessionFile,
172
+ sessionManager: this.sessionManager,
173
+ modelRegistry: this.modelRegistry,
174
+ model: this.getModel(),
175
+ isIdle: () => this.isIdleFn(),
176
+ abort: () => this.abortFn(),
177
+ hasQueuedMessages: () => this.hasQueuedMessagesFn(),
177
178
  };
178
179
  }
180
+ /**
181
+ * Create the command context for slash command handlers.
182
+ * Extends HookContext with session control methods that are only safe in commands.
183
+ */
184
+ createCommandContext() {
185
+ return {
186
+ ...this.createContext(),
187
+ waitForIdle: () => this.waitForIdleFn(),
188
+ newSession: (options) => this.newSessionHandler(options),
189
+ branch: (entryId) => this.branchHandler(entryId),
190
+ navigateTree: (targetId, options) => this.navigateTreeHandler(targetId, options),
191
+ };
192
+ }
193
+ /**
194
+ * Check if event type is a session "before_*" event that can be cancelled.
195
+ */
196
+ isSessionBeforeEvent(type) {
197
+ return (type === "session_before_switch" ||
198
+ type === "session_before_branch" ||
199
+ type === "session_before_compact" ||
200
+ type === "session_before_tree");
201
+ }
179
202
  /**
180
203
  * Emit an event to all hooks.
181
- * Returns the result from session/tool_result events (if any handler returns one).
204
+ * Returns the result from session before_* / tool_result events (if any handler returns one).
182
205
  */
183
206
  async emit(event) {
184
207
  const ctx = this.createContext();
@@ -189,19 +212,9 @@ export class HookRunner {
189
212
  continue;
190
213
  for (const handler of handlers) {
191
214
  try {
192
- // No timeout for before_compact events (like tool_call, they may take a while)
193
- const isBeforeCompact = event.type === "session" && event.reason === "before_compact";
194
- let handlerResult;
195
- if (isBeforeCompact) {
196
- handlerResult = await handler(event, ctx);
197
- }
198
- else {
199
- const timeout = createTimeout(this.timeout);
200
- handlerResult = await Promise.race([handler(event, ctx), timeout.promise]);
201
- timeout.clear();
202
- }
203
- // For session events, capture the result (for before_* cancellation)
204
- if (event.type === "session" && handlerResult) {
215
+ const handlerResult = await handler(event, ctx);
216
+ // For session before_* events, capture the result (for cancellation)
217
+ if (this.isSessionBeforeEvent(event.type) && handlerResult) {
205
218
  result = handlerResult;
206
219
  // If cancelled, stop processing further hooks
207
220
  if (result.cancel) {
@@ -251,5 +264,71 @@ export class HookRunner {
251
264
  }
252
265
  return result;
253
266
  }
267
+ /**
268
+ * Emit a context event to all hooks.
269
+ * Handlers are chained - each gets the previous handler's output (if any).
270
+ * Returns the final modified messages, or the original if no modifications.
271
+ *
272
+ * Note: Messages are already deep-copied by the caller (pi-ai preprocessor).
273
+ */
274
+ async emitContext(messages) {
275
+ const ctx = this.createContext();
276
+ let currentMessages = messages;
277
+ for (const hook of this.hooks) {
278
+ const handlers = hook.handlers.get("context");
279
+ if (!handlers || handlers.length === 0)
280
+ continue;
281
+ for (const handler of handlers) {
282
+ try {
283
+ const event = { type: "context", messages: currentMessages };
284
+ const handlerResult = await handler(event, ctx);
285
+ if (handlerResult && handlerResult.messages) {
286
+ currentMessages = handlerResult.messages;
287
+ }
288
+ }
289
+ catch (err) {
290
+ const message = err instanceof Error ? err.message : String(err);
291
+ this.emitError({
292
+ hookPath: hook.path,
293
+ event: "context",
294
+ error: message,
295
+ });
296
+ }
297
+ }
298
+ }
299
+ return currentMessages;
300
+ }
301
+ /**
302
+ * Emit before_agent_start event to all hooks.
303
+ * Returns the first message to inject (if any handler returns one).
304
+ */
305
+ async emitBeforeAgentStart(prompt, images) {
306
+ const ctx = this.createContext();
307
+ let result;
308
+ for (const hook of this.hooks) {
309
+ const handlers = hook.handlers.get("before_agent_start");
310
+ if (!handlers || handlers.length === 0)
311
+ continue;
312
+ for (const handler of handlers) {
313
+ try {
314
+ const event = { type: "before_agent_start", prompt, images };
315
+ const handlerResult = await handler(event, ctx);
316
+ // Take the first message returned
317
+ if (handlerResult && handlerResult.message && !result) {
318
+ result = handlerResult;
319
+ }
320
+ }
321
+ catch (err) {
322
+ const message = err instanceof Error ? err.message : String(err);
323
+ this.emitError({
324
+ hookPath: hook.path,
325
+ event: "before_agent_start",
326
+ error: message,
327
+ });
328
+ }
329
+ }
330
+ }
331
+ return result;
332
+ }
254
333
  }
255
334
  //# sourceMappingURL=runner.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"runner.js","sourceRoot":"","sources":["../../../src/core/hooks/runner.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Hook runner - executes hooks and manages their lifecycle.\n */\n\nimport { spawn } from \"node:child_process\";\nimport type { LoadedHook, SendHandler } from \"./loader.js\";\nimport type {\n\tExecOptions,\n\tExecResult,\n\tHookError,\n\tHookEvent,\n\tHookEventContext,\n\tHookUIContext,\n\tSessionEvent,\n\tSessionEventResult,\n\tToolCallEvent,\n\tToolCallEventResult,\n\tToolResultEventResult,\n} from \"./types.js\";\n\n/**\n * Default timeout for hook execution (30 seconds).\n */\nconst DEFAULT_TIMEOUT = 30000;\n\n/**\n * Listener for hook errors.\n */\nexport type HookErrorListener = (error: HookError) => void;\n\n/**\n * Execute a command and return stdout/stderr/code.\n * Supports cancellation via AbortSignal and timeout.\n */\nasync function exec(command: string, args: string[], cwd: string, options?: ExecOptions): Promise<ExecResult> {\n\treturn new Promise((resolve) => {\n\t\tconst proc = spawn(command, args, { cwd, shell: false });\n\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\t\tlet killed = false;\n\t\tlet timeoutId: NodeJS.Timeout | undefined;\n\n\t\tconst killProcess = () => {\n\t\t\tif (!killed) {\n\t\t\t\tkilled = true;\n\t\t\t\tproc.kill(\"SIGTERM\");\n\t\t\t\t// Force kill after 5 seconds if SIGTERM doesn't work\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\tif (!proc.killed) {\n\t\t\t\t\t\tproc.kill(\"SIGKILL\");\n\t\t\t\t\t}\n\t\t\t\t}, 5000);\n\t\t\t}\n\t\t};\n\n\t\t// Handle abort signal\n\t\tif (options?.signal) {\n\t\t\tif (options.signal.aborted) {\n\t\t\t\tkillProcess();\n\t\t\t} else {\n\t\t\t\toptions.signal.addEventListener(\"abort\", killProcess, { once: true });\n\t\t\t}\n\t\t}\n\n\t\t// Handle timeout\n\t\tif (options?.timeout && options.timeout > 0) {\n\t\t\ttimeoutId = setTimeout(() => {\n\t\t\t\tkillProcess();\n\t\t\t}, options.timeout);\n\t\t}\n\n\t\tproc.stdout?.on(\"data\", (data) => {\n\t\t\tstdout += data.toString();\n\t\t});\n\n\t\tproc.stderr?.on(\"data\", (data) => {\n\t\t\tstderr += data.toString();\n\t\t});\n\n\t\tproc.on(\"close\", (code) => {\n\t\t\tif (timeoutId) clearTimeout(timeoutId);\n\t\t\tif (options?.signal) {\n\t\t\t\toptions.signal.removeEventListener(\"abort\", killProcess);\n\t\t\t}\n\t\t\tresolve({ stdout, stderr, code: code ?? 0, killed });\n\t\t});\n\n\t\tproc.on(\"error\", (_err) => {\n\t\t\tif (timeoutId) clearTimeout(timeoutId);\n\t\t\tif (options?.signal) {\n\t\t\t\toptions.signal.removeEventListener(\"abort\", killProcess);\n\t\t\t}\n\t\t\tresolve({ stdout, stderr, code: 1, killed });\n\t\t});\n\t});\n}\n\n/**\n * Create a promise that rejects after a timeout.\n */\nfunction createTimeout(ms: number): { promise: Promise<never>; clear: () => void } {\n\tlet timeoutId: NodeJS.Timeout;\n\tconst promise = new Promise<never>((_, reject) => {\n\t\ttimeoutId = setTimeout(() => reject(new Error(`Hook timed out after ${ms}ms`)), ms);\n\t});\n\treturn {\n\t\tpromise,\n\t\tclear: () => clearTimeout(timeoutId),\n\t};\n}\n\n/** No-op UI context used when no UI is available */\nconst noOpUIContext: HookUIContext = {\n\tselect: async () => null,\n\tconfirm: async () => false,\n\tinput: async () => null,\n\tnotify: () => {},\n};\n\n/**\n * HookRunner executes hooks and manages event emission.\n */\nexport class HookRunner {\n\tprivate hooks: LoadedHook[];\n\tprivate uiContext: HookUIContext;\n\tprivate hasUI: boolean;\n\tprivate cwd: string;\n\tprivate sessionFile: string | null;\n\tprivate timeout: number;\n\tprivate errorListeners: Set<HookErrorListener> = new Set();\n\n\tconstructor(hooks: LoadedHook[], cwd: string, timeout: number = DEFAULT_TIMEOUT) {\n\t\tthis.hooks = hooks;\n\t\tthis.uiContext = noOpUIContext;\n\t\tthis.hasUI = false;\n\t\tthis.cwd = cwd;\n\t\tthis.sessionFile = null;\n\t\tthis.timeout = timeout;\n\t}\n\n\t/**\n\t * Set the UI context for hooks.\n\t * Call this when the mode initializes and UI is available.\n\t */\n\tsetUIContext(uiContext: HookUIContext, hasUI: boolean): void {\n\t\tthis.uiContext = uiContext;\n\t\tthis.hasUI = hasUI;\n\t}\n\n\t/**\n\t * Get the paths of all loaded hooks.\n\t */\n\tgetHookPaths(): string[] {\n\t\treturn this.hooks.map((h) => h.path);\n\t}\n\n\t/**\n\t * Set the session file path.\n\t */\n\tsetSessionFile(sessionFile: string | null): void {\n\t\tthis.sessionFile = sessionFile;\n\t}\n\n\t/**\n\t * Set the send handler for all hooks' pi.send().\n\t * Call this when the mode initializes.\n\t */\n\tsetSendHandler(handler: SendHandler): void {\n\t\tfor (const hook of this.hooks) {\n\t\t\thook.setSendHandler(handler);\n\t\t}\n\t}\n\n\t/**\n\t * Subscribe to hook errors.\n\t * @returns Unsubscribe function\n\t */\n\tonError(listener: HookErrorListener): () => void {\n\t\tthis.errorListeners.add(listener);\n\t\treturn () => this.errorListeners.delete(listener);\n\t}\n\n\t/**\n\t * Emit an error to all listeners.\n\t */\n\tprivate emitError(error: HookError): void {\n\t\tfor (const listener of this.errorListeners) {\n\t\t\tlistener(error);\n\t\t}\n\t}\n\n\t/**\n\t * Check if any hooks have handlers for the given event type.\n\t */\n\thasHandlers(eventType: string): boolean {\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst handlers = hook.handlers.get(eventType);\n\t\t\tif (handlers && handlers.length > 0) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * Create the event context for handlers.\n\t */\n\tprivate createContext(): HookEventContext {\n\t\treturn {\n\t\t\texec: (command: string, args: string[], options?: ExecOptions) => exec(command, args, this.cwd, options),\n\t\t\tui: this.uiContext,\n\t\t\thasUI: this.hasUI,\n\t\t\tcwd: this.cwd,\n\t\t\tsessionFile: this.sessionFile,\n\t\t};\n\t}\n\n\t/**\n\t * Emit an event to all hooks.\n\t * Returns the result from session/tool_result events (if any handler returns one).\n\t */\n\tasync emit(event: HookEvent): Promise<SessionEventResult | ToolResultEventResult | undefined> {\n\t\tconst ctx = this.createContext();\n\t\tlet result: SessionEventResult | ToolResultEventResult | undefined;\n\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst handlers = hook.handlers.get(event.type);\n\t\t\tif (!handlers || handlers.length === 0) continue;\n\n\t\t\tfor (const handler of handlers) {\n\t\t\t\ttry {\n\t\t\t\t\t// No timeout for before_compact events (like tool_call, they may take a while)\n\t\t\t\t\tconst isBeforeCompact = event.type === \"session\" && (event as SessionEvent).reason === \"before_compact\";\n\t\t\t\t\tlet handlerResult: unknown;\n\n\t\t\t\t\tif (isBeforeCompact) {\n\t\t\t\t\t\thandlerResult = await handler(event, ctx);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst timeout = createTimeout(this.timeout);\n\t\t\t\t\t\thandlerResult = await Promise.race([handler(event, ctx), timeout.promise]);\n\t\t\t\t\t\ttimeout.clear();\n\t\t\t\t\t}\n\n\t\t\t\t\t// For session events, capture the result (for before_* cancellation)\n\t\t\t\t\tif (event.type === \"session\" && handlerResult) {\n\t\t\t\t\t\tresult = handlerResult as SessionEventResult;\n\t\t\t\t\t\t// If cancelled, stop processing further hooks\n\t\t\t\t\t\tif (result.cancel) {\n\t\t\t\t\t\t\treturn result;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// For tool_result events, capture the result\n\t\t\t\t\tif (event.type === \"tool_result\" && handlerResult) {\n\t\t\t\t\t\tresult = handlerResult as ToolResultEventResult;\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\t\t\t\tthis.emitError({\n\t\t\t\t\t\thookPath: hook.path,\n\t\t\t\t\t\tevent: event.type,\n\t\t\t\t\t\terror: message,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Emit a tool_call event to all hooks.\n\t * No timeout - user prompts can take as long as needed.\n\t * Errors are thrown (not swallowed) so caller can block on failure.\n\t */\n\tasync emitToolCall(event: ToolCallEvent): Promise<ToolCallEventResult | undefined> {\n\t\tconst ctx = this.createContext();\n\t\tlet result: ToolCallEventResult | undefined;\n\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst handlers = hook.handlers.get(\"tool_call\");\n\t\t\tif (!handlers || handlers.length === 0) continue;\n\n\t\t\tfor (const handler of handlers) {\n\t\t\t\t// No timeout - let user take their time\n\t\t\t\tconst handlerResult = await handler(event, ctx);\n\n\t\t\t\tif (handlerResult) {\n\t\t\t\t\tresult = handlerResult as ToolCallEventResult;\n\t\t\t\t\t// If blocked, stop processing further hooks\n\t\t\t\t\tif (result.block) {\n\t\t\t\t\t\treturn result;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n}\n"]}
1
+ {"version":3,"file":"runner.js","sourceRoot":"","sources":["../../../src/core/hooks/runner.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Hook runner - executes hooks and manages their lifecycle.\n */\n\nimport type { AgentMessage } from \"@mariozechner/pi-agent-core\";\nimport type { Model } from \"@mariozechner/pi-ai\";\nimport { theme } from \"../../modes/interactive/theme/theme.js\";\nimport type { ModelRegistry } from \"../model-registry.js\";\nimport type { SessionManager } from \"../session-manager.js\";\nimport type {\n\tAppendEntryHandler,\n\tBranchHandler,\n\tLoadedHook,\n\tNavigateTreeHandler,\n\tNewSessionHandler,\n\tSendMessageHandler,\n} from \"./loader.js\";\nimport type {\n\tBeforeAgentStartEvent,\n\tBeforeAgentStartEventResult,\n\tContextEvent,\n\tContextEventResult,\n\tHookCommandContext,\n\tHookContext,\n\tHookError,\n\tHookEvent,\n\tHookMessageRenderer,\n\tHookUIContext,\n\tRegisteredCommand,\n\tSessionBeforeCompactResult,\n\tSessionBeforeTreeResult,\n\tToolCallEvent,\n\tToolCallEventResult,\n\tToolResultEventResult,\n} from \"./types.js\";\n\n/**\n * Listener for hook errors.\n */\nexport type HookErrorListener = (error: HookError) => void;\n\n// Re-export execCommand for backward compatibility\nexport { execCommand } from \"../exec.js\";\n\n/** No-op UI context used when no UI is available */\nconst noOpUIContext: HookUIContext = {\n\tselect: async () => undefined,\n\tconfirm: async () => false,\n\tinput: async () => undefined,\n\tnotify: () => {},\n\tsetStatus: () => {},\n\tcustom: async () => undefined as never,\n\tsetEditorText: () => {},\n\tgetEditorText: () => \"\",\n\teditor: async () => undefined,\n\tget theme() {\n\t\treturn theme;\n\t},\n};\n\n/**\n * HookRunner executes hooks and manages event emission.\n */\nexport class HookRunner {\n\tprivate hooks: LoadedHook[];\n\tprivate uiContext: HookUIContext;\n\tprivate hasUI: boolean;\n\tprivate cwd: string;\n\tprivate sessionManager: SessionManager;\n\tprivate modelRegistry: ModelRegistry;\n\tprivate errorListeners: Set<HookErrorListener> = new Set();\n\tprivate getModel: () => Model<any> | undefined = () => undefined;\n\tprivate isIdleFn: () => boolean = () => true;\n\tprivate waitForIdleFn: () => Promise<void> = async () => {};\n\tprivate abortFn: () => void = () => {};\n\tprivate hasQueuedMessagesFn: () => boolean = () => false;\n\tprivate newSessionHandler: NewSessionHandler = async () => ({ cancelled: false });\n\tprivate branchHandler: BranchHandler = async () => ({ cancelled: false });\n\tprivate navigateTreeHandler: NavigateTreeHandler = async () => ({ cancelled: false });\n\n\tconstructor(hooks: LoadedHook[], cwd: string, sessionManager: SessionManager, modelRegistry: ModelRegistry) {\n\t\tthis.hooks = hooks;\n\t\tthis.uiContext = noOpUIContext;\n\t\tthis.hasUI = false;\n\t\tthis.cwd = cwd;\n\t\tthis.sessionManager = sessionManager;\n\t\tthis.modelRegistry = modelRegistry;\n\t}\n\n\t/**\n\t * Initialize HookRunner with all required context.\n\t * Modes call this once the agent session is fully set up.\n\t */\n\tinitialize(options: {\n\t\t/** Function to get the current model */\n\t\tgetModel: () => Model<any> | undefined;\n\t\t/** Handler for hooks to send messages */\n\t\tsendMessageHandler: SendMessageHandler;\n\t\t/** Handler for hooks to append entries */\n\t\tappendEntryHandler: AppendEntryHandler;\n\t\t/** Handler for creating new sessions (for HookCommandContext) */\n\t\tnewSessionHandler?: NewSessionHandler;\n\t\t/** Handler for branching sessions (for HookCommandContext) */\n\t\tbranchHandler?: BranchHandler;\n\t\t/** Handler for navigating session tree (for HookCommandContext) */\n\t\tnavigateTreeHandler?: NavigateTreeHandler;\n\t\t/** Function to check if agent is idle */\n\t\tisIdle?: () => boolean;\n\t\t/** Function to wait for agent to be idle */\n\t\twaitForIdle?: () => Promise<void>;\n\t\t/** Function to abort current operation (fire-and-forget) */\n\t\tabort?: () => void;\n\t\t/** Function to check if there are queued messages */\n\t\thasQueuedMessages?: () => boolean;\n\t\t/** UI context for interactive prompts */\n\t\tuiContext?: HookUIContext;\n\t\t/** Whether UI is available */\n\t\thasUI?: boolean;\n\t}): void {\n\t\tthis.getModel = options.getModel;\n\t\tthis.isIdleFn = options.isIdle ?? (() => true);\n\t\tthis.waitForIdleFn = options.waitForIdle ?? (async () => {});\n\t\tthis.abortFn = options.abort ?? (() => {});\n\t\tthis.hasQueuedMessagesFn = options.hasQueuedMessages ?? (() => false);\n\t\t// Store session handlers for HookCommandContext\n\t\tif (options.newSessionHandler) {\n\t\t\tthis.newSessionHandler = options.newSessionHandler;\n\t\t}\n\t\tif (options.branchHandler) {\n\t\t\tthis.branchHandler = options.branchHandler;\n\t\t}\n\t\tif (options.navigateTreeHandler) {\n\t\t\tthis.navigateTreeHandler = options.navigateTreeHandler;\n\t\t}\n\t\t// Set per-hook handlers for pi.sendMessage() and pi.appendEntry()\n\t\tfor (const hook of this.hooks) {\n\t\t\thook.setSendMessageHandler(options.sendMessageHandler);\n\t\t\thook.setAppendEntryHandler(options.appendEntryHandler);\n\t\t}\n\t\tthis.uiContext = options.uiContext ?? noOpUIContext;\n\t\tthis.hasUI = options.hasUI ?? false;\n\t}\n\n\t/**\n\t * Get the UI context (set by mode).\n\t */\n\tgetUIContext(): HookUIContext | null {\n\t\treturn this.uiContext;\n\t}\n\n\t/**\n\t * Get whether UI is available.\n\t */\n\tgetHasUI(): boolean {\n\t\treturn this.hasUI;\n\t}\n\n\t/**\n\t * Get the paths of all loaded hooks.\n\t */\n\tgetHookPaths(): string[] {\n\t\treturn this.hooks.map((h) => h.path);\n\t}\n\n\t/**\n\t * Subscribe to hook errors.\n\t * @returns Unsubscribe function\n\t */\n\tonError(listener: HookErrorListener): () => void {\n\t\tthis.errorListeners.add(listener);\n\t\treturn () => this.errorListeners.delete(listener);\n\t}\n\n\t/**\n\t * Emit an error to all listeners.\n\t */\n\t/**\n\t * Emit an error to all error listeners.\n\t */\n\temitError(error: HookError): void {\n\t\tfor (const listener of this.errorListeners) {\n\t\t\tlistener(error);\n\t\t}\n\t}\n\n\t/**\n\t * Check if any hooks have handlers for the given event type.\n\t */\n\thasHandlers(eventType: string): boolean {\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst handlers = hook.handlers.get(eventType);\n\t\t\tif (handlers && handlers.length > 0) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * Get a message renderer for the given customType.\n\t * Returns the first renderer found across all hooks, or undefined if none.\n\t */\n\tgetMessageRenderer(customType: string): HookMessageRenderer | undefined {\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst renderer = hook.messageRenderers.get(customType);\n\t\t\tif (renderer) {\n\t\t\t\treturn renderer;\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Get all registered commands from all hooks.\n\t */\n\tgetRegisteredCommands(): RegisteredCommand[] {\n\t\tconst commands: RegisteredCommand[] = [];\n\t\tfor (const hook of this.hooks) {\n\t\t\tfor (const command of hook.commands.values()) {\n\t\t\t\tcommands.push(command);\n\t\t\t}\n\t\t}\n\t\treturn commands;\n\t}\n\n\t/**\n\t * Get a registered command by name.\n\t * Returns the first command found across all hooks, or undefined if none.\n\t */\n\tgetCommand(name: string): RegisteredCommand | undefined {\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst command = hook.commands.get(name);\n\t\t\tif (command) {\n\t\t\t\treturn command;\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Create the event context for handlers.\n\t */\n\tprivate createContext(): HookContext {\n\t\treturn {\n\t\t\tui: this.uiContext,\n\t\t\thasUI: this.hasUI,\n\t\t\tcwd: this.cwd,\n\t\t\tsessionManager: this.sessionManager,\n\t\t\tmodelRegistry: this.modelRegistry,\n\t\t\tmodel: this.getModel(),\n\t\t\tisIdle: () => this.isIdleFn(),\n\t\t\tabort: () => this.abortFn(),\n\t\t\thasQueuedMessages: () => this.hasQueuedMessagesFn(),\n\t\t};\n\t}\n\n\t/**\n\t * Create the command context for slash command handlers.\n\t * Extends HookContext with session control methods that are only safe in commands.\n\t */\n\tcreateCommandContext(): HookCommandContext {\n\t\treturn {\n\t\t\t...this.createContext(),\n\t\t\twaitForIdle: () => this.waitForIdleFn(),\n\t\t\tnewSession: (options) => this.newSessionHandler(options),\n\t\t\tbranch: (entryId) => this.branchHandler(entryId),\n\t\t\tnavigateTree: (targetId, options) => this.navigateTreeHandler(targetId, options),\n\t\t};\n\t}\n\n\t/**\n\t * Check if event type is a session \"before_*\" event that can be cancelled.\n\t */\n\tprivate isSessionBeforeEvent(\n\t\ttype: string,\n\t): type is \"session_before_switch\" | \"session_before_branch\" | \"session_before_compact\" | \"session_before_tree\" {\n\t\treturn (\n\t\t\ttype === \"session_before_switch\" ||\n\t\t\ttype === \"session_before_branch\" ||\n\t\t\ttype === \"session_before_compact\" ||\n\t\t\ttype === \"session_before_tree\"\n\t\t);\n\t}\n\n\t/**\n\t * Emit an event to all hooks.\n\t * Returns the result from session before_* / tool_result events (if any handler returns one).\n\t */\n\tasync emit(\n\t\tevent: HookEvent,\n\t): Promise<SessionBeforeCompactResult | SessionBeforeTreeResult | ToolResultEventResult | undefined> {\n\t\tconst ctx = this.createContext();\n\t\tlet result: SessionBeforeCompactResult | SessionBeforeTreeResult | ToolResultEventResult | undefined;\n\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst handlers = hook.handlers.get(event.type);\n\t\t\tif (!handlers || handlers.length === 0) continue;\n\n\t\t\tfor (const handler of handlers) {\n\t\t\t\ttry {\n\t\t\t\t\tconst handlerResult = await handler(event, ctx);\n\n\t\t\t\t\t// For session before_* events, capture the result (for cancellation)\n\t\t\t\t\tif (this.isSessionBeforeEvent(event.type) && handlerResult) {\n\t\t\t\t\t\tresult = handlerResult as SessionBeforeCompactResult | SessionBeforeTreeResult;\n\t\t\t\t\t\t// If cancelled, stop processing further hooks\n\t\t\t\t\t\tif (result.cancel) {\n\t\t\t\t\t\t\treturn result;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// For tool_result events, capture the result\n\t\t\t\t\tif (event.type === \"tool_result\" && handlerResult) {\n\t\t\t\t\t\tresult = handlerResult as ToolResultEventResult;\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\t\t\t\tthis.emitError({\n\t\t\t\t\t\thookPath: hook.path,\n\t\t\t\t\t\tevent: event.type,\n\t\t\t\t\t\terror: message,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Emit a tool_call event to all hooks.\n\t * No timeout - user prompts can take as long as needed.\n\t * Errors are thrown (not swallowed) so caller can block on failure.\n\t */\n\tasync emitToolCall(event: ToolCallEvent): Promise<ToolCallEventResult | undefined> {\n\t\tconst ctx = this.createContext();\n\t\tlet result: ToolCallEventResult | undefined;\n\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst handlers = hook.handlers.get(\"tool_call\");\n\t\t\tif (!handlers || handlers.length === 0) continue;\n\n\t\t\tfor (const handler of handlers) {\n\t\t\t\t// No timeout - let user take their time\n\t\t\t\tconst handlerResult = await handler(event, ctx);\n\n\t\t\t\tif (handlerResult) {\n\t\t\t\t\tresult = handlerResult as ToolCallEventResult;\n\t\t\t\t\t// If blocked, stop processing further hooks\n\t\t\t\t\tif (result.block) {\n\t\t\t\t\t\treturn result;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Emit a context event to all hooks.\n\t * Handlers are chained - each gets the previous handler's output (if any).\n\t * Returns the final modified messages, or the original if no modifications.\n\t *\n\t * Note: Messages are already deep-copied by the caller (pi-ai preprocessor).\n\t */\n\tasync emitContext(messages: AgentMessage[]): Promise<AgentMessage[]> {\n\t\tconst ctx = this.createContext();\n\t\tlet currentMessages = messages;\n\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst handlers = hook.handlers.get(\"context\");\n\t\t\tif (!handlers || handlers.length === 0) continue;\n\n\t\t\tfor (const handler of handlers) {\n\t\t\t\ttry {\n\t\t\t\t\tconst event: ContextEvent = { type: \"context\", messages: currentMessages };\n\t\t\t\t\tconst handlerResult = await handler(event, ctx);\n\n\t\t\t\t\tif (handlerResult && (handlerResult as ContextEventResult).messages) {\n\t\t\t\t\t\tcurrentMessages = (handlerResult as ContextEventResult).messages!;\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\t\t\t\tthis.emitError({\n\t\t\t\t\t\thookPath: hook.path,\n\t\t\t\t\t\tevent: \"context\",\n\t\t\t\t\t\terror: message,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn currentMessages;\n\t}\n\n\t/**\n\t * Emit before_agent_start event to all hooks.\n\t * Returns the first message to inject (if any handler returns one).\n\t */\n\tasync emitBeforeAgentStart(\n\t\tprompt: string,\n\t\timages?: import(\"@mariozechner/pi-ai\").ImageContent[],\n\t): Promise<BeforeAgentStartEventResult | undefined> {\n\t\tconst ctx = this.createContext();\n\t\tlet result: BeforeAgentStartEventResult | undefined;\n\n\t\tfor (const hook of this.hooks) {\n\t\t\tconst handlers = hook.handlers.get(\"before_agent_start\");\n\t\t\tif (!handlers || handlers.length === 0) continue;\n\n\t\t\tfor (const handler of handlers) {\n\t\t\t\ttry {\n\t\t\t\t\tconst event: BeforeAgentStartEvent = { type: \"before_agent_start\", prompt, images };\n\t\t\t\t\tconst handlerResult = await handler(event, ctx);\n\n\t\t\t\t\t// Take the first message returned\n\t\t\t\t\tif (handlerResult && (handlerResult as BeforeAgentStartEventResult).message && !result) {\n\t\t\t\t\t\tresult = handlerResult as BeforeAgentStartEventResult;\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\t\t\t\tthis.emitError({\n\t\t\t\t\t\thookPath: hook.path,\n\t\t\t\t\t\tevent: \"before_agent_start\",\n\t\t\t\t\t\terror: message,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n}\n"]}
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Tool wrapper - wraps tools with hook callbacks for interception.
3
3
  */
4
- import type { AgentTool } from "@mariozechner/pi-ai";
4
+ import type { AgentTool } from "@mariozechner/pi-agent-core";
5
5
  import type { HookRunner } from "./runner.js";
6
6
  /**
7
7
  * Wrap a tool with hook callbacks.
@@ -1 +1 @@
1
- {"version":3,"file":"tool-wrapper.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/tool-wrapper.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,SAAS,EAA2B,MAAM,qBAAqB,CAAC;AAC9E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAG9C;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,UAAU,GAAG,SAAS,CAAC,GAAG,EAAE,CAAC,CAAC,CA4DvG;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,UAAU,EAAE,UAAU,GAAG,SAAS,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAE7G","sourcesContent":["/**\n * Tool wrapper - wraps tools with hook callbacks for interception.\n */\n\nimport type { AgentTool, AgentToolUpdateCallback } from \"@mariozechner/pi-ai\";\nimport type { HookRunner } from \"./runner.js\";\nimport type { ToolCallEventResult, ToolResultEventResult } from \"./types.js\";\n\n/**\n * Wrap a tool with hook callbacks.\n * - Emits tool_call event before execution (can block)\n * - Emits tool_result event after execution (can modify result)\n * - Forwards onUpdate callback to wrapped tool for progress streaming\n */\nexport function wrapToolWithHooks<T>(tool: AgentTool<any, T>, hookRunner: HookRunner): AgentTool<any, T> {\n\treturn {\n\t\t...tool,\n\t\texecute: async (\n\t\t\ttoolCallId: string,\n\t\t\tparams: Record<string, unknown>,\n\t\t\tsignal?: AbortSignal,\n\t\t\tonUpdate?: AgentToolUpdateCallback<T>,\n\t\t) => {\n\t\t\t// Emit tool_call event - hooks can block execution\n\t\t\t// If hook errors/times out, block by default (fail-safe)\n\t\t\tif (hookRunner.hasHandlers(\"tool_call\")) {\n\t\t\t\ttry {\n\t\t\t\t\tconst callResult = (await hookRunner.emitToolCall({\n\t\t\t\t\t\ttype: \"tool_call\",\n\t\t\t\t\t\ttoolName: tool.name,\n\t\t\t\t\t\ttoolCallId,\n\t\t\t\t\t\tinput: params,\n\t\t\t\t\t})) as ToolCallEventResult | undefined;\n\n\t\t\t\t\tif (callResult?.block) {\n\t\t\t\t\t\tconst reason = callResult.reason || \"Tool execution was blocked by a hook\";\n\t\t\t\t\t\tthrow new Error(reason);\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\t// Hook error or block - throw to mark as error\n\t\t\t\t\tif (err instanceof Error) {\n\t\t\t\t\t\tthrow err;\n\t\t\t\t\t}\n\t\t\t\t\tthrow new Error(`Hook failed, blocking execution: ${String(err)}`);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Execute the actual tool, forwarding onUpdate for progress streaming\n\t\t\tconst result = await tool.execute(toolCallId, params, signal, onUpdate);\n\n\t\t\t// Emit tool_result event - hooks can modify the result\n\t\t\tif (hookRunner.hasHandlers(\"tool_result\")) {\n\t\t\t\tconst resultResult = (await hookRunner.emit({\n\t\t\t\t\ttype: \"tool_result\",\n\t\t\t\t\ttoolName: tool.name,\n\t\t\t\t\ttoolCallId,\n\t\t\t\t\tinput: params,\n\t\t\t\t\tcontent: result.content,\n\t\t\t\t\tdetails: result.details,\n\t\t\t\t\tisError: false,\n\t\t\t\t})) as ToolResultEventResult | undefined;\n\n\t\t\t\t// Apply modifications if any\n\t\t\t\tif (resultResult) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tcontent: resultResult.content ?? result.content,\n\t\t\t\t\t\tdetails: (resultResult.details ?? result.details) as T,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn result;\n\t\t},\n\t};\n}\n\n/**\n * Wrap all tools with hook callbacks.\n */\nexport function wrapToolsWithHooks<T>(tools: AgentTool<any, T>[], hookRunner: HookRunner): AgentTool<any, T>[] {\n\treturn tools.map((tool) => wrapToolWithHooks(tool, hookRunner));\n}\n"]}
1
+ {"version":3,"file":"tool-wrapper.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/tool-wrapper.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,SAAS,EAA2B,MAAM,6BAA6B,CAAC;AACtF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAG9C;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,UAAU,GAAG,SAAS,CAAC,GAAG,EAAE,CAAC,CAAC,CA4EvG;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,UAAU,EAAE,UAAU,GAAG,SAAS,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAE7G","sourcesContent":["/**\n * Tool wrapper - wraps tools with hook callbacks for interception.\n */\n\nimport type { AgentTool, AgentToolUpdateCallback } from \"@mariozechner/pi-agent-core\";\nimport type { HookRunner } from \"./runner.js\";\nimport type { ToolCallEventResult, ToolResultEventResult } from \"./types.js\";\n\n/**\n * Wrap a tool with hook callbacks.\n * - Emits tool_call event before execution (can block)\n * - Emits tool_result event after execution (can modify result)\n * - Forwards onUpdate callback to wrapped tool for progress streaming\n */\nexport function wrapToolWithHooks<T>(tool: AgentTool<any, T>, hookRunner: HookRunner): AgentTool<any, T> {\n\treturn {\n\t\t...tool,\n\t\texecute: async (\n\t\t\ttoolCallId: string,\n\t\t\tparams: Record<string, unknown>,\n\t\t\tsignal?: AbortSignal,\n\t\t\tonUpdate?: AgentToolUpdateCallback<T>,\n\t\t) => {\n\t\t\t// Emit tool_call event - hooks can block execution\n\t\t\t// If hook errors/times out, block by default (fail-safe)\n\t\t\tif (hookRunner.hasHandlers(\"tool_call\")) {\n\t\t\t\ttry {\n\t\t\t\t\tconst callResult = (await hookRunner.emitToolCall({\n\t\t\t\t\t\ttype: \"tool_call\",\n\t\t\t\t\t\ttoolName: tool.name,\n\t\t\t\t\t\ttoolCallId,\n\t\t\t\t\t\tinput: params,\n\t\t\t\t\t})) as ToolCallEventResult | undefined;\n\n\t\t\t\t\tif (callResult?.block) {\n\t\t\t\t\t\tconst reason = callResult.reason || \"Tool execution was blocked by a hook\";\n\t\t\t\t\t\tthrow new Error(reason);\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\t// Hook error or block - throw to mark as error\n\t\t\t\t\tif (err instanceof Error) {\n\t\t\t\t\t\tthrow err;\n\t\t\t\t\t}\n\t\t\t\t\tthrow new Error(`Hook failed, blocking execution: ${String(err)}`);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Execute the actual tool, forwarding onUpdate for progress streaming\n\t\t\ttry {\n\t\t\t\tconst result = await tool.execute(toolCallId, params, signal, onUpdate);\n\n\t\t\t\t// Emit tool_result event - hooks can modify the result\n\t\t\t\tif (hookRunner.hasHandlers(\"tool_result\")) {\n\t\t\t\t\tconst resultResult = (await hookRunner.emit({\n\t\t\t\t\t\ttype: \"tool_result\",\n\t\t\t\t\t\ttoolName: tool.name,\n\t\t\t\t\t\ttoolCallId,\n\t\t\t\t\t\tinput: params,\n\t\t\t\t\t\tcontent: result.content,\n\t\t\t\t\t\tdetails: result.details,\n\t\t\t\t\t\tisError: false,\n\t\t\t\t\t})) as ToolResultEventResult | undefined;\n\n\t\t\t\t\t// Apply modifications if any\n\t\t\t\t\tif (resultResult) {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tcontent: resultResult.content ?? result.content,\n\t\t\t\t\t\t\tdetails: (resultResult.details ?? result.details) as T,\n\t\t\t\t\t\t};\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn result;\n\t\t\t} catch (err) {\n\t\t\t\t// Emit tool_result event for errors so hooks can observe failures\n\t\t\t\tif (hookRunner.hasHandlers(\"tool_result\")) {\n\t\t\t\t\tawait hookRunner.emit({\n\t\t\t\t\t\ttype: \"tool_result\",\n\t\t\t\t\t\ttoolName: tool.name,\n\t\t\t\t\t\ttoolCallId,\n\t\t\t\t\t\tinput: params,\n\t\t\t\t\t\tcontent: [{ type: \"text\", text: err instanceof Error ? err.message : String(err) }],\n\t\t\t\t\t\tdetails: undefined,\n\t\t\t\t\t\tisError: true,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tthrow err; // Re-throw original error for agent-loop\n\t\t\t}\n\t\t},\n\t};\n}\n\n/**\n * Wrap all tools with hook callbacks.\n */\nexport function wrapToolsWithHooks<T>(tools: AgentTool<any, T>[], hookRunner: HookRunner): AgentTool<any, T>[] {\n\treturn tools.map((tool) => wrapToolWithHooks(tool, hookRunner));\n}\n"]}
@@ -35,27 +35,44 @@ export function wrapToolWithHooks(tool, hookRunner) {
35
35
  }
36
36
  }
37
37
  // Execute the actual tool, forwarding onUpdate for progress streaming
38
- const result = await tool.execute(toolCallId, params, signal, onUpdate);
39
- // Emit tool_result event - hooks can modify the result
40
- if (hookRunner.hasHandlers("tool_result")) {
41
- const resultResult = (await hookRunner.emit({
42
- type: "tool_result",
43
- toolName: tool.name,
44
- toolCallId,
45
- input: params,
46
- content: result.content,
47
- details: result.details,
48
- isError: false,
49
- }));
50
- // Apply modifications if any
51
- if (resultResult) {
52
- return {
53
- content: resultResult.content ?? result.content,
54
- details: (resultResult.details ?? result.details),
55
- };
38
+ try {
39
+ const result = await tool.execute(toolCallId, params, signal, onUpdate);
40
+ // Emit tool_result event - hooks can modify the result
41
+ if (hookRunner.hasHandlers("tool_result")) {
42
+ const resultResult = (await hookRunner.emit({
43
+ type: "tool_result",
44
+ toolName: tool.name,
45
+ toolCallId,
46
+ input: params,
47
+ content: result.content,
48
+ details: result.details,
49
+ isError: false,
50
+ }));
51
+ // Apply modifications if any
52
+ if (resultResult) {
53
+ return {
54
+ content: resultResult.content ?? result.content,
55
+ details: (resultResult.details ?? result.details),
56
+ };
57
+ }
58
+ }
59
+ return result;
60
+ }
61
+ catch (err) {
62
+ // Emit tool_result event for errors so hooks can observe failures
63
+ if (hookRunner.hasHandlers("tool_result")) {
64
+ await hookRunner.emit({
65
+ type: "tool_result",
66
+ toolName: tool.name,
67
+ toolCallId,
68
+ input: params,
69
+ content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }],
70
+ details: undefined,
71
+ isError: true,
72
+ });
56
73
  }
74
+ throw err; // Re-throw original error for agent-loop
57
75
  }
58
- return result;
59
76
  },
60
77
  };
61
78
  }