@mariozechner/pi-coding-agent 0.30.2 → 0.31.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 (297) hide show
  1. package/CHANGELOG.md +244 -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 +3 -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 -5
  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 +1 -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
@@ -2,17 +2,16 @@
2
2
  * Confirm Destructive Actions Hook
3
3
  *
4
4
  * Prompts for confirmation before destructive session actions (clear, switch, branch).
5
- * Demonstrates how to cancel session events using the before_* variants.
5
+ * Demonstrates how to cancel session events using the before_* events.
6
6
  */
7
7
 
8
- import type { HookAPI } from "@mariozechner/pi-coding-agent/hooks";
8
+ import type { HookAPI, SessionBeforeSwitchEvent, SessionMessageEntry } from "@mariozechner/pi-coding-agent";
9
9
 
10
10
  export default function (pi: HookAPI) {
11
- pi.on("session", async (event, ctx) => {
12
- // Only handle before_* events (the ones that can be cancelled)
13
- if (event.reason === "before_new") {
14
- if (!ctx.hasUI) return;
11
+ pi.on("session_before_switch", async (event: SessionBeforeSwitchEvent, ctx) => {
12
+ if (!ctx.hasUI) return;
15
13
 
14
+ if (event.reason === "new") {
16
15
  const confirmed = await ctx.ui.confirm(
17
16
  "Clear session?",
18
17
  "This will delete all messages in the current session.",
@@ -22,39 +21,39 @@ export default function (pi: HookAPI) {
22
21
  ctx.ui.notify("Clear cancelled", "info");
23
22
  return { cancel: true };
24
23
  }
24
+ return;
25
25
  }
26
26
 
27
- if (event.reason === "before_switch") {
28
- if (!ctx.hasUI) return;
27
+ // reason === "resume" - check if there are unsaved changes (messages since last assistant response)
28
+ const entries = ctx.sessionManager.getEntries();
29
+ const hasUnsavedWork = entries.some(
30
+ (e): e is SessionMessageEntry => e.type === "message" && e.message.role === "user",
31
+ );
29
32
 
30
- // Check if there are unsaved changes (messages since last assistant response)
31
- const hasUnsavedWork = event.entries.some((e) => e.type === "message" && e.message.role === "user");
32
-
33
- if (hasUnsavedWork) {
34
- const confirmed = await ctx.ui.confirm(
35
- "Switch session?",
36
- "You have messages in the current session. Switch anyway?",
37
- );
33
+ if (hasUnsavedWork) {
34
+ const confirmed = await ctx.ui.confirm(
35
+ "Switch session?",
36
+ "You have messages in the current session. Switch anyway?",
37
+ );
38
38
 
39
- if (!confirmed) {
40
- ctx.ui.notify("Switch cancelled", "info");
41
- return { cancel: true };
42
- }
39
+ if (!confirmed) {
40
+ ctx.ui.notify("Switch cancelled", "info");
41
+ return { cancel: true };
43
42
  }
44
43
  }
44
+ });
45
45
 
46
- if (event.reason === "before_branch") {
47
- if (!ctx.hasUI) return;
46
+ pi.on("session_before_branch", async (event, ctx) => {
47
+ if (!ctx.hasUI) return;
48
48
 
49
- const choice = await ctx.ui.select(`Branch from turn ${event.targetTurnIndex}?`, [
50
- "Yes, create branch",
51
- "No, stay in current session",
52
- ]);
49
+ const choice = await ctx.ui.select(`Branch from entry ${event.entryId.slice(0, 8)}?`, [
50
+ "Yes, create branch",
51
+ "No, stay in current session",
52
+ ]);
53
53
 
54
- if (choice !== "Yes, create branch") {
55
- ctx.ui.notify("Branch cancelled", "info");
56
- return { cancel: true };
57
- }
54
+ if (choice !== "Yes, create branch") {
55
+ ctx.ui.notify("Branch cancelled", "info");
56
+ return { cancel: true };
58
57
  }
59
58
  });
60
59
  }
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Replaces the default compaction behavior with a full summary of the entire context.
5
5
  * Instead of keeping the last 20k tokens of conversation turns, this hook:
6
- * 1. Summarizes ALL messages (both messagesToSummarize and messagesToKeep and previousSummary)
6
+ * 1. Summarizes ALL messages (messagesToSummarize + turnPrefixMessages)
7
7
  * 2. Discards all old turns completely, keeping only the summary
8
8
  *
9
9
  * This example also demonstrates using a different model (Gemini Flash) for summarization,
@@ -14,17 +14,15 @@
14
14
  */
15
15
 
16
16
  import { complete, getModel } from "@mariozechner/pi-ai";
17
- import { messageTransformer } from "@mariozechner/pi-coding-agent";
18
- import type { HookAPI } from "@mariozechner/pi-coding-agent/hooks";
17
+ import type { HookAPI } from "@mariozechner/pi-coding-agent";
18
+ import { convertToLlm, serializeConversation } from "@mariozechner/pi-coding-agent";
19
19
 
20
20
  export default function (pi: HookAPI) {
21
- pi.on("session", async (event, ctx) => {
22
- if (event.reason !== "before_compact") return;
23
-
21
+ pi.on("session_before_compact", async (event, ctx) => {
24
22
  ctx.ui.notify("Custom compaction hook triggered", "info");
25
23
 
26
- const { messagesToSummarize, messagesToKeep, previousSummary, tokensBefore, resolveApiKey, entries, signal } =
27
- event;
24
+ const { preparation, branchEntries: _, signal } = event;
25
+ const { messagesToSummarize, turnPrefixMessages, tokensBefore, firstKeptEntryId, previousSummary } = preparation;
28
26
 
29
27
  // Use Gemini Flash for summarization (cheaper/faster than most conversation models)
30
28
  const model = getModel("google", "gemini-2.5-flash");
@@ -34,35 +32,34 @@ export default function (pi: HookAPI) {
34
32
  }
35
33
 
36
34
  // Resolve API key for the summarization model
37
- const apiKey = await resolveApiKey(model);
35
+ const apiKey = await ctx.modelRegistry.getApiKey(model);
38
36
  if (!apiKey) {
39
37
  ctx.ui.notify(`No API key for ${model.provider}, using default compaction`, "warning");
40
38
  return;
41
39
  }
42
40
 
43
41
  // Combine all messages for full summary
44
- const allMessages = [...messagesToSummarize, ...messagesToKeep];
42
+ const allMessages = [...messagesToSummarize, ...turnPrefixMessages];
45
43
 
46
44
  ctx.ui.notify(
47
45
  `Custom compaction: summarizing ${allMessages.length} messages (${tokensBefore.toLocaleString()} tokens) with ${model.id}...`,
48
46
  "info",
49
47
  );
50
48
 
51
- // Transform app messages to pi-ai package format
52
- const transformedMessages = messageTransformer(allMessages);
49
+ // Convert messages to readable text format
50
+ const conversationText = serializeConversation(convertToLlm(allMessages));
53
51
 
54
52
  // Include previous summary context if available
55
53
  const previousContext = previousSummary ? `\n\nPrevious session summary for context:\n${previousSummary}` : "";
56
54
 
57
55
  // Build messages that ask for a comprehensive summary
58
56
  const summaryMessages = [
59
- ...transformedMessages,
60
57
  {
61
58
  role: "user" as const,
62
59
  content: [
63
60
  {
64
61
  type: "text" as const,
65
- text: `You are a conversation summarizer. Create a comprehensive summary of this entire conversation that captures:${previousContext}
62
+ text: `You are a conversation summarizer. Create a comprehensive summary of this conversation that captures:${previousContext}
66
63
 
67
64
  1. The main goals and objectives discussed
68
65
  2. Key decisions made and their rationale
@@ -73,7 +70,11 @@ export default function (pi: HookAPI) {
73
70
 
74
71
  Be thorough but concise. The summary will replace the ENTIRE conversation history, so include all information needed to continue the work effectively.
75
72
 
76
- Format the summary as structured markdown with clear sections.`,
73
+ Format the summary as structured markdown with clear sections.
74
+
75
+ <conversation>
76
+ ${conversationText}
77
+ </conversation>`,
77
78
  },
78
79
  ],
79
80
  timestamp: Date.now(),
@@ -94,14 +95,12 @@ Format the summary as structured markdown with clear sections.`,
94
95
  return;
95
96
  }
96
97
 
97
- // Return a compaction entry that discards ALL messages
98
- // firstKeptEntryIndex points past all current entries
98
+ // Return compaction content - SessionManager adds id/parentId
99
+ // Use firstKeptEntryId from preparation to keep recent messages
99
100
  return {
100
- compactionEntry: {
101
- type: "compaction" as const,
102
- timestamp: new Date().toISOString(),
101
+ compaction: {
103
102
  summary,
104
- firstKeptEntryIndex: entries.length,
103
+ firstKeptEntryId,
105
104
  tokensBefore,
106
105
  },
107
106
  };
@@ -5,47 +5,48 @@
5
5
  * Useful to ensure work is committed before switching context.
6
6
  */
7
7
 
8
- import type { HookAPI } from "@mariozechner/pi-coding-agent/hooks";
8
+ import type { HookAPI, HookContext } from "@mariozechner/pi-coding-agent";
9
+
10
+ async function checkDirtyRepo(pi: HookAPI, ctx: HookContext, action: string): Promise<{ cancel: boolean } | undefined> {
11
+ // Check for uncommitted changes
12
+ const { stdout, code } = await pi.exec("git", ["status", "--porcelain"]);
13
+
14
+ if (code !== 0) {
15
+ // Not a git repo, allow the action
16
+ return;
17
+ }
18
+
19
+ const hasChanges = stdout.trim().length > 0;
20
+ if (!hasChanges) {
21
+ return;
22
+ }
23
+
24
+ if (!ctx.hasUI) {
25
+ // In non-interactive mode, block by default
26
+ return { cancel: true };
27
+ }
28
+
29
+ // Count changed files
30
+ const changedFiles = stdout.trim().split("\n").filter(Boolean).length;
31
+
32
+ const choice = await ctx.ui.select(`You have ${changedFiles} uncommitted file(s). ${action} anyway?`, [
33
+ "Yes, proceed anyway",
34
+ "No, let me commit first",
35
+ ]);
36
+
37
+ if (choice !== "Yes, proceed anyway") {
38
+ ctx.ui.notify("Commit your changes first", "warning");
39
+ return { cancel: true };
40
+ }
41
+ }
9
42
 
10
43
  export default function (pi: HookAPI) {
11
- pi.on("session", async (event, ctx) => {
12
- // Only guard destructive actions
13
- if (event.reason !== "before_new" && event.reason !== "before_switch" && event.reason !== "before_branch") {
14
- return;
15
- }
16
-
17
- // Check for uncommitted changes
18
- const { stdout, code } = await ctx.exec("git", ["status", "--porcelain"]);
19
-
20
- if (code !== 0) {
21
- // Not a git repo, allow the action
22
- return;
23
- }
24
-
25
- const hasChanges = stdout.trim().length > 0;
26
- if (!hasChanges) {
27
- return;
28
- }
29
-
30
- if (!ctx.hasUI) {
31
- // In non-interactive mode, block by default
32
- return { cancel: true };
33
- }
34
-
35
- // Count changed files
36
- const changedFiles = stdout.trim().split("\n").filter(Boolean).length;
37
-
38
- const action =
39
- event.reason === "before_new" ? "new session" : event.reason === "before_switch" ? "switch session" : "branch";
40
-
41
- const choice = await ctx.ui.select(`You have ${changedFiles} uncommitted file(s). ${action} anyway?`, [
42
- "Yes, proceed anyway",
43
- "No, let me commit first",
44
- ]);
45
-
46
- if (choice !== "Yes, proceed anyway") {
47
- ctx.ui.notify("Commit your changes first", "warning");
48
- return { cancel: true };
49
- }
44
+ pi.on("session_before_switch", async (event, ctx) => {
45
+ const action = event.reason === "new" ? "new session" : "switch session";
46
+ return checkDirtyRepo(pi, ctx, action);
47
+ });
48
+
49
+ pi.on("session_before_branch", async (_event, ctx) => {
50
+ return checkDirtyRepo(pi, ctx, "branch");
50
51
  });
51
52
  }
@@ -9,19 +9,24 @@
9
9
  */
10
10
 
11
11
  import * as fs from "node:fs";
12
- import type { HookAPI } from "@mariozechner/pi-coding-agent/hooks";
12
+ import type { HookAPI } from "@mariozechner/pi-coding-agent";
13
13
 
14
14
  export default function (pi: HookAPI) {
15
- pi.on("session", async (event, ctx) => {
16
- if (event.reason !== "start") return;
17
-
15
+ pi.on("session_start", async (_event, ctx) => {
18
16
  const triggerFile = "/tmp/agent-trigger.txt";
19
17
 
20
18
  fs.watch(triggerFile, () => {
21
19
  try {
22
20
  const content = fs.readFileSync(triggerFile, "utf-8").trim();
23
21
  if (content) {
24
- pi.send(`External trigger: ${content}`);
22
+ pi.sendMessage(
23
+ {
24
+ customType: "file-trigger",
25
+ content: `External trigger: ${content}`,
26
+ display: true,
27
+ },
28
+ true, // triggerTurn - get LLM to respond
29
+ );
25
30
  fs.writeFileSync(triggerFile, ""); // Clear after reading
26
31
  }
27
32
  } catch {
@@ -5,25 +5,29 @@
5
5
  * When branching, offers to restore code to that point in history.
6
6
  */
7
7
 
8
- import type { HookAPI } from "@mariozechner/pi-coding-agent/hooks";
8
+ import type { HookAPI } from "@mariozechner/pi-coding-agent";
9
9
 
10
10
  export default function (pi: HookAPI) {
11
- const checkpoints = new Map<number, string>();
11
+ const checkpoints = new Map<string, string>();
12
+ let currentEntryId: string | undefined;
12
13
 
13
- pi.on("turn_start", async (event, ctx) => {
14
+ // Track the current entry ID when user messages are saved
15
+ pi.on("tool_result", async (_event, ctx) => {
16
+ const leaf = ctx.sessionManager.getLeafEntry();
17
+ if (leaf) currentEntryId = leaf.id;
18
+ });
19
+
20
+ pi.on("turn_start", async () => {
14
21
  // Create a git stash entry before LLM makes changes
15
- const { stdout } = await ctx.exec("git", ["stash", "create"]);
22
+ const { stdout } = await pi.exec("git", ["stash", "create"]);
16
23
  const ref = stdout.trim();
17
- if (ref) {
18
- checkpoints.set(event.turnIndex, ref);
24
+ if (ref && currentEntryId) {
25
+ checkpoints.set(currentEntryId, ref);
19
26
  }
20
27
  });
21
28
 
22
- pi.on("session", async (event, ctx) => {
23
- // Only handle before_branch events
24
- if (event.reason !== "before_branch") return;
25
-
26
- const ref = checkpoints.get(event.targetTurnIndex);
29
+ pi.on("session_before_branch", async (event, ctx) => {
30
+ const ref = checkpoints.get(event.entryId);
27
31
  if (!ref) return;
28
32
 
29
33
  if (!ctx.hasUI) {
@@ -37,7 +41,7 @@ export default function (pi: HookAPI) {
37
41
  ]);
38
42
 
39
43
  if (choice?.startsWith("Yes")) {
40
- await ctx.exec("git", ["stash", "apply", ref]);
44
+ await pi.exec("git", ["stash", "apply", ref]);
41
45
  ctx.ui.notify("Code restored to checkpoint", "info");
42
46
  }
43
47
  });
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Handoff hook - transfer context to a new focused session
3
+ *
4
+ * Instead of compacting (which is lossy), handoff extracts what matters
5
+ * for your next task and creates a new session with a generated prompt.
6
+ *
7
+ * Usage:
8
+ * /handoff now implement this for teams as well
9
+ * /handoff execute phase one of the plan
10
+ * /handoff check other places that need this fix
11
+ *
12
+ * The generated prompt appears as a draft in the editor for review/editing.
13
+ */
14
+
15
+ import { complete, type Message } from "@mariozechner/pi-ai";
16
+ import type { HookAPI, SessionEntry } from "@mariozechner/pi-coding-agent";
17
+ import { BorderedLoader, convertToLlm, serializeConversation } from "@mariozechner/pi-coding-agent";
18
+
19
+ const SYSTEM_PROMPT = `You are a context transfer assistant. Given a conversation history and the user's goal for a new thread, generate a focused prompt that:
20
+
21
+ 1. Summarizes relevant context from the conversation (decisions made, approaches taken, key findings)
22
+ 2. Lists any relevant files that were discussed or modified
23
+ 3. Clearly states the next task based on the user's goal
24
+ 4. Is self-contained - the new thread should be able to proceed without the old conversation
25
+
26
+ Format your response as a prompt the user can send to start the new thread. Be concise but include all necessary context. Do not include any preamble like "Here's the prompt" - just output the prompt itself.
27
+
28
+ Example output format:
29
+ ## Context
30
+ We've been working on X. Key decisions:
31
+ - Decision 1
32
+ - Decision 2
33
+
34
+ Files involved:
35
+ - path/to/file1.ts
36
+ - path/to/file2.ts
37
+
38
+ ## Task
39
+ [Clear description of what to do next based on user's goal]`;
40
+
41
+ export default function (pi: HookAPI) {
42
+ pi.registerCommand("handoff", {
43
+ description: "Transfer context to a new focused session",
44
+ handler: async (args, ctx) => {
45
+ if (!ctx.hasUI) {
46
+ ctx.ui.notify("handoff requires interactive mode", "error");
47
+ return;
48
+ }
49
+
50
+ if (!ctx.model) {
51
+ ctx.ui.notify("No model selected", "error");
52
+ return;
53
+ }
54
+
55
+ const goal = args.trim();
56
+ if (!goal) {
57
+ ctx.ui.notify("Usage: /handoff <goal for new thread>", "error");
58
+ return;
59
+ }
60
+
61
+ // Gather conversation context from current branch
62
+ const branch = ctx.sessionManager.getBranch();
63
+ const messages = branch
64
+ .filter((entry): entry is SessionEntry & { type: "message" } => entry.type === "message")
65
+ .map((entry) => entry.message);
66
+
67
+ if (messages.length === 0) {
68
+ ctx.ui.notify("No conversation to hand off", "error");
69
+ return;
70
+ }
71
+
72
+ // Convert to LLM format and serialize
73
+ const llmMessages = convertToLlm(messages);
74
+ const conversationText = serializeConversation(llmMessages);
75
+ const currentSessionFile = ctx.sessionManager.getSessionFile();
76
+
77
+ // Generate the handoff prompt with loader UI
78
+ const result = await ctx.ui.custom<string | null>((tui, theme, done) => {
79
+ const loader = new BorderedLoader(tui, theme, `Generating handoff prompt...`);
80
+ loader.onAbort = () => done(null);
81
+
82
+ const doGenerate = async () => {
83
+ const apiKey = await ctx.modelRegistry.getApiKey(ctx.model!);
84
+
85
+ const userMessage: Message = {
86
+ role: "user",
87
+ content: [
88
+ {
89
+ type: "text",
90
+ text: `## Conversation History\n\n${conversationText}\n\n## User's Goal for New Thread\n\n${goal}`,
91
+ },
92
+ ],
93
+ timestamp: Date.now(),
94
+ };
95
+
96
+ const response = await complete(
97
+ ctx.model!,
98
+ { systemPrompt: SYSTEM_PROMPT, messages: [userMessage] },
99
+ { apiKey, signal: loader.signal },
100
+ );
101
+
102
+ if (response.stopReason === "aborted") {
103
+ return null;
104
+ }
105
+
106
+ return response.content
107
+ .filter((c): c is { type: "text"; text: string } => c.type === "text")
108
+ .map((c) => c.text)
109
+ .join("\n");
110
+ };
111
+
112
+ doGenerate()
113
+ .then(done)
114
+ .catch((err) => {
115
+ console.error("Handoff generation failed:", err);
116
+ done(null);
117
+ });
118
+
119
+ return loader;
120
+ });
121
+
122
+ if (result === null) {
123
+ ctx.ui.notify("Cancelled", "info");
124
+ return;
125
+ }
126
+
127
+ // Let user edit the generated prompt
128
+ const editedPrompt = await ctx.ui.editor("Edit handoff prompt (ctrl+enter to submit, esc to cancel)", result);
129
+
130
+ if (editedPrompt === undefined) {
131
+ ctx.ui.notify("Cancelled", "info");
132
+ return;
133
+ }
134
+
135
+ // Create new session with parent tracking
136
+ const newSessionResult = await ctx.newSession({
137
+ parentSession: currentSessionFile,
138
+ });
139
+
140
+ if (newSessionResult.cancelled) {
141
+ ctx.ui.notify("New session cancelled", "info");
142
+ return;
143
+ }
144
+
145
+ // Set the edited prompt in the main editor for submission
146
+ ctx.ui.setEditorText(editedPrompt);
147
+ ctx.ui.notify("Handoff ready. Submit when ready.", "info");
148
+ },
149
+ });
150
+ }
@@ -5,7 +5,7 @@
5
5
  * Patterns checked: rm -rf, sudo, chmod/chown 777
6
6
  */
7
7
 
8
- import type { HookAPI } from "@mariozechner/pi-coding-agent/hooks";
8
+ import type { HookAPI } from "@mariozechner/pi-coding-agent";
9
9
 
10
10
  export default function (pi: HookAPI) {
11
11
  const dangerousPatterns = [/\brm\s+(-rf?|--recursive)/i, /\bsudo\b/i, /\b(chmod|chown)\b.*777/i];
@@ -5,7 +5,7 @@
5
5
  * Useful for preventing accidental modifications to sensitive files.
6
6
  */
7
7
 
8
- import type { HookAPI } from "@mariozechner/pi-coding-agent/hooks";
8
+ import type { HookAPI } from "@mariozechner/pi-coding-agent";
9
9
 
10
10
  export default function (pi: HookAPI) {
11
11
  const protectedPaths = [".env", ".git/", "node_modules/"];
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Q&A extraction hook - extracts questions from assistant responses
3
+ *
4
+ * Demonstrates the "prompt generator" pattern:
5
+ * 1. /qna command gets the last assistant message
6
+ * 2. Shows a spinner while extracting (hides editor)
7
+ * 3. Loads the result into the editor for user to fill in answers
8
+ */
9
+
10
+ import { complete, type UserMessage } from "@mariozechner/pi-ai";
11
+ import type { HookAPI } from "@mariozechner/pi-coding-agent";
12
+ import { BorderedLoader } from "@mariozechner/pi-coding-agent";
13
+
14
+ const SYSTEM_PROMPT = `You are a question extractor. Given text from a conversation, extract any questions that need answering and format them for the user to fill in.
15
+
16
+ Output format:
17
+ - List each question on its own line, prefixed with "Q: "
18
+ - After each question, add a blank line for the answer prefixed with "A: "
19
+ - If no questions are found, output "No questions found in the last message."
20
+
21
+ Example output:
22
+ Q: What is your preferred database?
23
+ A:
24
+
25
+ Q: Should we use TypeScript or JavaScript?
26
+ A:
27
+
28
+ Keep questions in the order they appeared. Be concise.`;
29
+
30
+ export default function (pi: HookAPI) {
31
+ pi.registerCommand("qna", {
32
+ description: "Extract questions from last assistant message into editor",
33
+ handler: async (_args, ctx) => {
34
+ if (!ctx.hasUI) {
35
+ ctx.ui.notify("qna requires interactive mode", "error");
36
+ return;
37
+ }
38
+
39
+ if (!ctx.model) {
40
+ ctx.ui.notify("No model selected", "error");
41
+ return;
42
+ }
43
+
44
+ // Find the last assistant message on the current branch
45
+ const branch = ctx.sessionManager.getBranch();
46
+ let lastAssistantText: string | undefined;
47
+
48
+ for (let i = branch.length - 1; i >= 0; i--) {
49
+ const entry = branch[i];
50
+ if (entry.type === "message") {
51
+ const msg = entry.message;
52
+ if ("role" in msg && msg.role === "assistant") {
53
+ if (msg.stopReason !== "stop") {
54
+ ctx.ui.notify(`Last assistant message incomplete (${msg.stopReason})`, "error");
55
+ return;
56
+ }
57
+ const textParts = msg.content
58
+ .filter((c): c is { type: "text"; text: string } => c.type === "text")
59
+ .map((c) => c.text);
60
+ if (textParts.length > 0) {
61
+ lastAssistantText = textParts.join("\n");
62
+ break;
63
+ }
64
+ }
65
+ }
66
+ }
67
+
68
+ if (!lastAssistantText) {
69
+ ctx.ui.notify("No assistant messages found", "error");
70
+ return;
71
+ }
72
+
73
+ // Run extraction with loader UI
74
+ const result = await ctx.ui.custom<string | null>((tui, theme, done) => {
75
+ const loader = new BorderedLoader(tui, theme, `Extracting questions using ${ctx.model!.id}...`);
76
+ loader.onAbort = () => done(null);
77
+
78
+ // Do the work
79
+ const doExtract = async () => {
80
+ const apiKey = await ctx.modelRegistry.getApiKey(ctx.model!);
81
+ const userMessage: UserMessage = {
82
+ role: "user",
83
+ content: [{ type: "text", text: lastAssistantText! }],
84
+ timestamp: Date.now(),
85
+ };
86
+
87
+ const response = await complete(
88
+ ctx.model!,
89
+ { systemPrompt: SYSTEM_PROMPT, messages: [userMessage] },
90
+ { apiKey, signal: loader.signal },
91
+ );
92
+
93
+ if (response.stopReason === "aborted") {
94
+ return null;
95
+ }
96
+
97
+ return response.content
98
+ .filter((c): c is { type: "text"; text: string } => c.type === "text")
99
+ .map((c) => c.text)
100
+ .join("\n");
101
+ };
102
+
103
+ doExtract()
104
+ .then(done)
105
+ .catch(() => done(null));
106
+
107
+ return loader;
108
+ });
109
+
110
+ if (result === null) {
111
+ ctx.ui.notify("Cancelled", "info");
112
+ return;
113
+ }
114
+
115
+ ctx.ui.setEditorText(result);
116
+ ctx.ui.notify("Questions loaded. Edit and submit when ready.", "info");
117
+ },
118
+ });
119
+ }