@oh-my-pi/pi-coding-agent 1.337.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 (224) hide show
  1. package/CHANGELOG.md +1228 -0
  2. package/README.md +1041 -0
  3. package/docs/compaction.md +403 -0
  4. package/docs/custom-tools.md +541 -0
  5. package/docs/extension-loading.md +1004 -0
  6. package/docs/hooks.md +867 -0
  7. package/docs/rpc.md +1040 -0
  8. package/docs/sdk.md +994 -0
  9. package/docs/session-tree-plan.md +441 -0
  10. package/docs/session.md +240 -0
  11. package/docs/skills.md +290 -0
  12. package/docs/theme.md +637 -0
  13. package/docs/tree.md +197 -0
  14. package/docs/tui.md +341 -0
  15. package/examples/README.md +21 -0
  16. package/examples/custom-tools/README.md +124 -0
  17. package/examples/custom-tools/hello/index.ts +20 -0
  18. package/examples/custom-tools/question/index.ts +84 -0
  19. package/examples/custom-tools/subagent/README.md +172 -0
  20. package/examples/custom-tools/subagent/agents/planner.md +37 -0
  21. package/examples/custom-tools/subagent/agents/reviewer.md +35 -0
  22. package/examples/custom-tools/subagent/agents/scout.md +50 -0
  23. package/examples/custom-tools/subagent/agents/worker.md +24 -0
  24. package/examples/custom-tools/subagent/agents.ts +156 -0
  25. package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
  26. package/examples/custom-tools/subagent/commands/implement.md +10 -0
  27. package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
  28. package/examples/custom-tools/subagent/index.ts +1002 -0
  29. package/examples/custom-tools/todo/index.ts +212 -0
  30. package/examples/hooks/README.md +56 -0
  31. package/examples/hooks/auto-commit-on-exit.ts +49 -0
  32. package/examples/hooks/confirm-destructive.ts +59 -0
  33. package/examples/hooks/custom-compaction.ts +116 -0
  34. package/examples/hooks/dirty-repo-guard.ts +52 -0
  35. package/examples/hooks/file-trigger.ts +41 -0
  36. package/examples/hooks/git-checkpoint.ts +53 -0
  37. package/examples/hooks/handoff.ts +150 -0
  38. package/examples/hooks/permission-gate.ts +34 -0
  39. package/examples/hooks/protected-paths.ts +30 -0
  40. package/examples/hooks/qna.ts +119 -0
  41. package/examples/hooks/snake.ts +343 -0
  42. package/examples/hooks/status-line.ts +40 -0
  43. package/examples/sdk/01-minimal.ts +22 -0
  44. package/examples/sdk/02-custom-model.ts +49 -0
  45. package/examples/sdk/03-custom-prompt.ts +44 -0
  46. package/examples/sdk/04-skills.ts +44 -0
  47. package/examples/sdk/05-tools.ts +90 -0
  48. package/examples/sdk/06-hooks.ts +61 -0
  49. package/examples/sdk/07-context-files.ts +36 -0
  50. package/examples/sdk/08-slash-commands.ts +42 -0
  51. package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
  52. package/examples/sdk/10-settings.ts +38 -0
  53. package/examples/sdk/11-sessions.ts +48 -0
  54. package/examples/sdk/12-full-control.ts +95 -0
  55. package/examples/sdk/README.md +154 -0
  56. package/package.json +81 -0
  57. package/src/cli/args.ts +246 -0
  58. package/src/cli/file-processor.ts +72 -0
  59. package/src/cli/list-models.ts +104 -0
  60. package/src/cli/plugin-cli.ts +650 -0
  61. package/src/cli/session-picker.ts +41 -0
  62. package/src/cli.ts +10 -0
  63. package/src/commands/init.md +20 -0
  64. package/src/config.ts +159 -0
  65. package/src/core/agent-session.ts +1900 -0
  66. package/src/core/auth-storage.ts +236 -0
  67. package/src/core/bash-executor.ts +196 -0
  68. package/src/core/compaction/branch-summarization.ts +343 -0
  69. package/src/core/compaction/compaction.ts +742 -0
  70. package/src/core/compaction/index.ts +7 -0
  71. package/src/core/compaction/utils.ts +154 -0
  72. package/src/core/custom-tools/index.ts +21 -0
  73. package/src/core/custom-tools/loader.ts +248 -0
  74. package/src/core/custom-tools/types.ts +169 -0
  75. package/src/core/custom-tools/wrapper.ts +28 -0
  76. package/src/core/exec.ts +129 -0
  77. package/src/core/export-html/index.ts +211 -0
  78. package/src/core/export-html/template.css +781 -0
  79. package/src/core/export-html/template.html +54 -0
  80. package/src/core/export-html/template.js +1185 -0
  81. package/src/core/export-html/vendor/highlight.min.js +1213 -0
  82. package/src/core/export-html/vendor/marked.min.js +6 -0
  83. package/src/core/hooks/index.ts +16 -0
  84. package/src/core/hooks/loader.ts +312 -0
  85. package/src/core/hooks/runner.ts +434 -0
  86. package/src/core/hooks/tool-wrapper.ts +99 -0
  87. package/src/core/hooks/types.ts +773 -0
  88. package/src/core/index.ts +52 -0
  89. package/src/core/mcp/client.ts +158 -0
  90. package/src/core/mcp/config.ts +154 -0
  91. package/src/core/mcp/index.ts +45 -0
  92. package/src/core/mcp/loader.ts +68 -0
  93. package/src/core/mcp/manager.ts +181 -0
  94. package/src/core/mcp/tool-bridge.ts +148 -0
  95. package/src/core/mcp/transports/http.ts +316 -0
  96. package/src/core/mcp/transports/index.ts +6 -0
  97. package/src/core/mcp/transports/stdio.ts +252 -0
  98. package/src/core/mcp/types.ts +220 -0
  99. package/src/core/messages.ts +189 -0
  100. package/src/core/model-registry.ts +317 -0
  101. package/src/core/model-resolver.ts +393 -0
  102. package/src/core/plugins/doctor.ts +59 -0
  103. package/src/core/plugins/index.ts +38 -0
  104. package/src/core/plugins/installer.ts +189 -0
  105. package/src/core/plugins/loader.ts +338 -0
  106. package/src/core/plugins/manager.ts +672 -0
  107. package/src/core/plugins/parser.ts +105 -0
  108. package/src/core/plugins/paths.ts +32 -0
  109. package/src/core/plugins/types.ts +190 -0
  110. package/src/core/sdk.ts +760 -0
  111. package/src/core/session-manager.ts +1128 -0
  112. package/src/core/settings-manager.ts +443 -0
  113. package/src/core/skills.ts +437 -0
  114. package/src/core/slash-commands.ts +248 -0
  115. package/src/core/system-prompt.ts +439 -0
  116. package/src/core/timings.ts +25 -0
  117. package/src/core/tools/ask.ts +211 -0
  118. package/src/core/tools/bash-interceptor.ts +120 -0
  119. package/src/core/tools/bash.ts +250 -0
  120. package/src/core/tools/context.ts +32 -0
  121. package/src/core/tools/edit-diff.ts +475 -0
  122. package/src/core/tools/edit.ts +208 -0
  123. package/src/core/tools/exa/company.ts +59 -0
  124. package/src/core/tools/exa/index.ts +64 -0
  125. package/src/core/tools/exa/linkedin.ts +59 -0
  126. package/src/core/tools/exa/logger.ts +56 -0
  127. package/src/core/tools/exa/mcp-client.ts +368 -0
  128. package/src/core/tools/exa/render.ts +196 -0
  129. package/src/core/tools/exa/researcher.ts +90 -0
  130. package/src/core/tools/exa/search.ts +337 -0
  131. package/src/core/tools/exa/types.ts +168 -0
  132. package/src/core/tools/exa/websets.ts +248 -0
  133. package/src/core/tools/find.ts +261 -0
  134. package/src/core/tools/grep.ts +555 -0
  135. package/src/core/tools/index.ts +202 -0
  136. package/src/core/tools/ls.ts +140 -0
  137. package/src/core/tools/lsp/client.ts +605 -0
  138. package/src/core/tools/lsp/config.ts +147 -0
  139. package/src/core/tools/lsp/edits.ts +101 -0
  140. package/src/core/tools/lsp/index.ts +804 -0
  141. package/src/core/tools/lsp/render.ts +447 -0
  142. package/src/core/tools/lsp/rust-analyzer.ts +145 -0
  143. package/src/core/tools/lsp/types.ts +463 -0
  144. package/src/core/tools/lsp/utils.ts +486 -0
  145. package/src/core/tools/notebook.ts +229 -0
  146. package/src/core/tools/path-utils.ts +61 -0
  147. package/src/core/tools/read.ts +240 -0
  148. package/src/core/tools/renderers.ts +540 -0
  149. package/src/core/tools/task/agents.ts +153 -0
  150. package/src/core/tools/task/artifacts.ts +114 -0
  151. package/src/core/tools/task/bundled-agents/browser.md +71 -0
  152. package/src/core/tools/task/bundled-agents/explore.md +82 -0
  153. package/src/core/tools/task/bundled-agents/plan.md +54 -0
  154. package/src/core/tools/task/bundled-agents/reviewer.md +59 -0
  155. package/src/core/tools/task/bundled-agents/task.md +53 -0
  156. package/src/core/tools/task/bundled-commands/architect-plan.md +10 -0
  157. package/src/core/tools/task/bundled-commands/implement-with-critic.md +11 -0
  158. package/src/core/tools/task/bundled-commands/implement.md +11 -0
  159. package/src/core/tools/task/commands.ts +213 -0
  160. package/src/core/tools/task/discovery.ts +208 -0
  161. package/src/core/tools/task/executor.ts +367 -0
  162. package/src/core/tools/task/index.ts +388 -0
  163. package/src/core/tools/task/model-resolver.ts +115 -0
  164. package/src/core/tools/task/parallel.ts +38 -0
  165. package/src/core/tools/task/render.ts +232 -0
  166. package/src/core/tools/task/types.ts +99 -0
  167. package/src/core/tools/truncate.ts +265 -0
  168. package/src/core/tools/web-fetch.ts +2370 -0
  169. package/src/core/tools/web-search/auth.ts +193 -0
  170. package/src/core/tools/web-search/index.ts +537 -0
  171. package/src/core/tools/web-search/providers/anthropic.ts +198 -0
  172. package/src/core/tools/web-search/providers/exa.ts +302 -0
  173. package/src/core/tools/web-search/providers/perplexity.ts +195 -0
  174. package/src/core/tools/web-search/render.ts +182 -0
  175. package/src/core/tools/web-search/types.ts +180 -0
  176. package/src/core/tools/write.ts +99 -0
  177. package/src/index.ts +176 -0
  178. package/src/main.ts +464 -0
  179. package/src/migrations.ts +135 -0
  180. package/src/modes/index.ts +43 -0
  181. package/src/modes/interactive/components/armin.ts +382 -0
  182. package/src/modes/interactive/components/assistant-message.ts +86 -0
  183. package/src/modes/interactive/components/bash-execution.ts +196 -0
  184. package/src/modes/interactive/components/bordered-loader.ts +41 -0
  185. package/src/modes/interactive/components/branch-summary-message.ts +42 -0
  186. package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
  187. package/src/modes/interactive/components/custom-editor.ts +122 -0
  188. package/src/modes/interactive/components/diff.ts +147 -0
  189. package/src/modes/interactive/components/dynamic-border.ts +25 -0
  190. package/src/modes/interactive/components/footer.ts +381 -0
  191. package/src/modes/interactive/components/hook-editor.ts +117 -0
  192. package/src/modes/interactive/components/hook-input.ts +64 -0
  193. package/src/modes/interactive/components/hook-message.ts +96 -0
  194. package/src/modes/interactive/components/hook-selector.ts +91 -0
  195. package/src/modes/interactive/components/model-selector.ts +247 -0
  196. package/src/modes/interactive/components/oauth-selector.ts +120 -0
  197. package/src/modes/interactive/components/plugin-settings.ts +479 -0
  198. package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
  199. package/src/modes/interactive/components/session-selector.ts +204 -0
  200. package/src/modes/interactive/components/settings-selector.ts +453 -0
  201. package/src/modes/interactive/components/show-images-selector.ts +45 -0
  202. package/src/modes/interactive/components/theme-selector.ts +62 -0
  203. package/src/modes/interactive/components/thinking-selector.ts +64 -0
  204. package/src/modes/interactive/components/tool-execution.ts +675 -0
  205. package/src/modes/interactive/components/tree-selector.ts +866 -0
  206. package/src/modes/interactive/components/user-message-selector.ts +159 -0
  207. package/src/modes/interactive/components/user-message.ts +18 -0
  208. package/src/modes/interactive/components/visual-truncate.ts +50 -0
  209. package/src/modes/interactive/components/welcome.ts +183 -0
  210. package/src/modes/interactive/interactive-mode.ts +2516 -0
  211. package/src/modes/interactive/theme/dark.json +101 -0
  212. package/src/modes/interactive/theme/light.json +98 -0
  213. package/src/modes/interactive/theme/theme-schema.json +308 -0
  214. package/src/modes/interactive/theme/theme.ts +998 -0
  215. package/src/modes/print-mode.ts +128 -0
  216. package/src/modes/rpc/rpc-client.ts +527 -0
  217. package/src/modes/rpc/rpc-mode.ts +483 -0
  218. package/src/modes/rpc/rpc-types.ts +203 -0
  219. package/src/utils/changelog.ts +99 -0
  220. package/src/utils/clipboard.ts +265 -0
  221. package/src/utils/fuzzy.ts +108 -0
  222. package/src/utils/mime.ts +30 -0
  223. package/src/utils/shell.ts +276 -0
  224. package/src/utils/tools-manager.ts +274 -0
package/docs/hooks.md ADDED
@@ -0,0 +1,867 @@
1
+ > pi can create hooks. Ask it to build one for your use case.
2
+
3
+ # Hooks
4
+
5
+ Hooks are TypeScript modules that extend pi's behavior by subscribing to lifecycle events. They can intercept tool calls, prompt the user, modify results, inject messages, and more.
6
+
7
+ **Key capabilities:**
8
+
9
+ - **User interaction** - Hooks can prompt users via `ctx.ui` (select, confirm, input, notify)
10
+ - **Custom UI components** - Full TUI components with keyboard input via `ctx.ui.custom()`
11
+ - **Custom slash commands** - Register commands like `/mycommand` via `pi.registerCommand()`
12
+ - **Event interception** - Block or modify tool calls, inject context, customize compaction
13
+ - **Session persistence** - Store hook state that survives restarts via `pi.appendEntry()`
14
+
15
+ **Example use cases:**
16
+
17
+ - Permission gates (confirm before `rm -rf`, `sudo`, etc.)
18
+ - Git checkpointing (stash at each turn, restore on `/branch`)
19
+ - Path protection (block writes to `.env`, `node_modules/`)
20
+ - External integrations (file watchers, webhooks, CI triggers)
21
+ - Interactive tools (games, wizards, custom dialogs)
22
+
23
+ See [examples/hooks/](../examples/hooks/) for working implementations, including a [snake game](../examples/hooks/snake.ts) demonstrating custom UI.
24
+
25
+ ## Quick Start
26
+
27
+ Create `~/.pi/agent/hooks/my-hook.ts`:
28
+
29
+ ```typescript
30
+ import type { HookAPI } from "@oh-my-pi/pi-coding-agent";
31
+
32
+ export default function (pi: HookAPI) {
33
+ pi.on("session_start", async (_event, ctx) => {
34
+ ctx.ui.notify("Hook loaded!", "info");
35
+ });
36
+
37
+ pi.on("tool_call", async (event, ctx) => {
38
+ if (event.toolName === "bash" && event.input.command?.includes("rm -rf")) {
39
+ const ok = await ctx.ui.confirm("Dangerous!", "Allow rm -rf?");
40
+ if (!ok) return { block: true, reason: "Blocked by user" };
41
+ }
42
+ });
43
+ }
44
+ ```
45
+
46
+ Test with `--hook` flag:
47
+
48
+ ```bash
49
+ pi --hook ./my-hook.ts
50
+ ```
51
+
52
+ ## Hook Locations
53
+
54
+ Hooks are auto-discovered from:
55
+
56
+ | Location | Scope |
57
+ | ------------------------ | --------------------- |
58
+ | `~/.pi/agent/hooks/*.ts` | Global (all projects) |
59
+ | `.pi/hooks/*.ts` | Project-local |
60
+
61
+ Additional paths via `settings.json`:
62
+
63
+ ```json
64
+ {
65
+ "hooks": ["/path/to/hook.ts"]
66
+ }
67
+ ```
68
+
69
+ ## Available Imports
70
+
71
+ | Package | Purpose |
72
+ | --------------------------------- | --------------------------------------------- |
73
+ | `@oh-my-pi/pi-coding-agent/hooks` | Hook types (`HookAPI`, `HookContext`, events) |
74
+ | `@oh-my-pi/pi-coding-agent` | Additional types if needed |
75
+ | `@oh-my-pi/pi-ai` | AI utilities |
76
+ | `@oh-my-pi/pi-tui` | TUI components |
77
+
78
+ Node.js built-ins (`node:fs`, `node:path`, etc.) are also available.
79
+
80
+ ## Writing a Hook
81
+
82
+ A hook exports a default function that receives `HookAPI`:
83
+
84
+ ```typescript
85
+ import type { HookAPI } from "@oh-my-pi/pi-coding-agent";
86
+
87
+ export default function (pi: HookAPI) {
88
+ // Subscribe to events
89
+ pi.on("event_name", async (event, ctx) => {
90
+ // Handle event
91
+ });
92
+ }
93
+ ```
94
+
95
+ Hooks are loaded via [jiti](https://github.com/unjs/jiti), so TypeScript works without compilation.
96
+
97
+ ## Events
98
+
99
+ ### Lifecycle Overview
100
+
101
+ ```
102
+ pi starts
103
+
104
+ └─► session_start
105
+
106
+
107
+ user sends prompt ─────────────────────────────────────────┐
108
+ │ │
109
+ ├─► before_agent_start (can inject message) │
110
+ ├─► agent_start │
111
+ │ │
112
+ │ ┌─── turn (repeats while LLM calls tools) ───┐ │
113
+ │ │ │ │
114
+ │ ├─► turn_start │ │
115
+ │ ├─► context (can modify messages) │ │
116
+ │ │ │ │
117
+ │ │ LLM responds, may call tools: │ │
118
+ │ │ ├─► tool_call (can block) │ │
119
+ │ │ │ tool executes │ │
120
+ │ │ └─► tool_result (can modify) │ │
121
+ │ │ │ │
122
+ │ └─► turn_end │ │
123
+ │ │
124
+ └─► agent_end │
125
+
126
+ user sends another prompt ◄────────────────────────────────┘
127
+
128
+ /new (new session) or /resume (switch session)
129
+ ├─► session_before_switch (can cancel, has reason: "new" | "resume")
130
+ └─► session_switch (has reason: "new" | "resume")
131
+
132
+ /branch
133
+ ├─► session_before_branch (can cancel)
134
+ └─► session_branch
135
+
136
+ /compact or auto-compaction
137
+ ├─► session_before_compact (can cancel or customize)
138
+ └─► session_compact
139
+
140
+ /tree navigation
141
+ ├─► session_before_tree (can cancel or customize)
142
+ └─► session_tree
143
+
144
+ exit (Ctrl+C, Ctrl+D)
145
+ └─► session_shutdown
146
+ ```
147
+
148
+ ### Session Events
149
+
150
+ #### session_start
151
+
152
+ Fired on initial session load.
153
+
154
+ ```typescript
155
+ pi.on("session_start", async (_event, ctx) => {
156
+ ctx.ui.notify(`Session: ${ctx.sessionManager.getSessionFile() ?? "ephemeral"}`, "info");
157
+ });
158
+ ```
159
+
160
+ #### session_before_switch / session_switch
161
+
162
+ Fired when starting a new session (`/new`) or switching sessions (`/resume`).
163
+
164
+ ```typescript
165
+ pi.on("session_before_switch", async (event, ctx) => {
166
+ // event.reason - "new" (starting fresh) or "resume" (switching to existing)
167
+ // event.targetSessionFile - session we're switching to (only for "resume")
168
+
169
+ if (event.reason === "new") {
170
+ const ok = await ctx.ui.confirm("Clear?", "Delete all messages?");
171
+ if (!ok) return { cancel: true };
172
+ }
173
+
174
+ return { cancel: true }; // Cancel the switch/new
175
+ });
176
+
177
+ pi.on("session_switch", async (event, ctx) => {
178
+ // event.reason - "new" or "resume"
179
+ // event.previousSessionFile - session we came from
180
+ });
181
+ ```
182
+
183
+ #### session_before_branch / session_branch
184
+
185
+ Fired when branching via `/branch`.
186
+
187
+ ```typescript
188
+ pi.on("session_before_branch", async (event, ctx) => {
189
+ // event.entryId - ID of the entry being branched from
190
+
191
+ return { cancel: true }; // Cancel branch
192
+ // OR
193
+ return { skipConversationRestore: true }; // Branch but don't rewind messages
194
+ });
195
+
196
+ pi.on("session_branch", async (event, ctx) => {
197
+ // event.previousSessionFile - previous session file
198
+ });
199
+ ```
200
+
201
+ The `skipConversationRestore` option is useful for checkpoint hooks that restore code state separately.
202
+
203
+ #### session_before_compact / session_compact
204
+
205
+ Fired on compaction. See [compaction.md](compaction.md) for details.
206
+
207
+ ```typescript
208
+ pi.on("session_before_compact", async (event, ctx) => {
209
+ const { preparation, branchEntries, customInstructions, signal } = event;
210
+
211
+ // Cancel:
212
+ return { cancel: true };
213
+
214
+ // Custom summary:
215
+ return {
216
+ compaction: {
217
+ summary: "...",
218
+ firstKeptEntryId: preparation.firstKeptEntryId,
219
+ tokensBefore: preparation.tokensBefore,
220
+ },
221
+ };
222
+ });
223
+
224
+ pi.on("session_compact", async (event, ctx) => {
225
+ // event.compactionEntry - the saved compaction
226
+ // event.fromHook - whether hook provided it
227
+ });
228
+ ```
229
+
230
+ #### session_before_tree / session_tree
231
+
232
+ Fired on `/tree` navigation. Always fires regardless of user's summarization choice. See [compaction.md](compaction.md) for details.
233
+
234
+ ```typescript
235
+ pi.on("session_before_tree", async (event, ctx) => {
236
+ const { preparation, signal } = event;
237
+ // preparation.targetId, oldLeafId, commonAncestorId, entriesToSummarize
238
+ // preparation.userWantsSummary - whether user chose to summarize
239
+
240
+ return { cancel: true };
241
+ // OR provide custom summary (only used if userWantsSummary is true):
242
+ return { summary: { summary: "...", details: {} } };
243
+ });
244
+
245
+ pi.on("session_tree", async (event, ctx) => {
246
+ // event.newLeafId, oldLeafId, summaryEntry, fromHook
247
+ });
248
+ ```
249
+
250
+ #### session_shutdown
251
+
252
+ Fired on exit (Ctrl+C, Ctrl+D, SIGTERM).
253
+
254
+ ```typescript
255
+ pi.on("session_shutdown", async (_event, ctx) => {
256
+ // Cleanup, save state, etc.
257
+ });
258
+ ```
259
+
260
+ ### Agent Events
261
+
262
+ #### before_agent_start
263
+
264
+ Fired after user submits prompt, before agent loop. Can inject a persistent message.
265
+
266
+ ```typescript
267
+ pi.on("before_agent_start", async (event, ctx) => {
268
+ // event.prompt - user's prompt text
269
+ // event.images - attached images (if any)
270
+
271
+ return {
272
+ message: {
273
+ customType: "my-hook",
274
+ content: "Additional context for the LLM",
275
+ display: true, // Show in TUI
276
+ },
277
+ };
278
+ });
279
+ ```
280
+
281
+ The injected message is persisted as `CustomMessageEntry` and sent to the LLM.
282
+
283
+ #### agent_start / agent_end
284
+
285
+ Fired once per user prompt.
286
+
287
+ ```typescript
288
+ pi.on("agent_start", async (_event, ctx) => {});
289
+
290
+ pi.on("agent_end", async (event, ctx) => {
291
+ // event.messages - messages from this prompt
292
+ });
293
+ ```
294
+
295
+ #### turn_start / turn_end
296
+
297
+ Fired for each turn (one LLM response + tool calls).
298
+
299
+ ```typescript
300
+ pi.on("turn_start", async (event, ctx) => {
301
+ // event.turnIndex, event.timestamp
302
+ });
303
+
304
+ pi.on("turn_end", async (event, ctx) => {
305
+ // event.turnIndex
306
+ // event.message - assistant's response
307
+ // event.toolResults - tool results from this turn
308
+ });
309
+ ```
310
+
311
+ #### context
312
+
313
+ Fired before each LLM call. Modify messages non-destructively (session unchanged).
314
+
315
+ ```typescript
316
+ pi.on("context", async (event, ctx) => {
317
+ // event.messages - deep copy, safe to modify
318
+
319
+ // Filter or transform messages
320
+ const filtered = event.messages.filter((m) => !shouldPrune(m));
321
+ return { messages: filtered };
322
+ });
323
+ ```
324
+
325
+ ### Tool Events
326
+
327
+ #### tool_call
328
+
329
+ Fired before tool executes. **Can block.**
330
+
331
+ ```typescript
332
+ pi.on("tool_call", async (event, ctx) => {
333
+ // event.toolName - "bash", "read", "write", "edit", etc.
334
+ // event.toolCallId
335
+ // event.input - tool parameters
336
+
337
+ if (shouldBlock(event)) {
338
+ return { block: true, reason: "Not allowed" };
339
+ }
340
+ });
341
+ ```
342
+
343
+ Tool inputs:
344
+
345
+ - `bash`: `{ command, timeout? }`
346
+ - `read`: `{ path, offset?, limit? }`
347
+ - `write`: `{ path, content }`
348
+ - `edit`: `{ path, oldText, newText }`
349
+ - `ls`: `{ path?, limit? }`
350
+ - `find`: `{ pattern, path?, limit? }`
351
+ - `grep`: `{ pattern, path?, glob?, ignoreCase?, literal?, context?, limit? }`
352
+
353
+ #### tool_result
354
+
355
+ Fired after tool executes (including errors). **Can modify result.**
356
+
357
+ Check `event.isError` to distinguish successful executions from failures.
358
+
359
+ ```typescript
360
+ pi.on("tool_result", async (event, ctx) => {
361
+ // event.toolName, event.toolCallId, event.input
362
+ // event.content - array of TextContent | ImageContent
363
+ // event.details - tool-specific (see below)
364
+ // event.isError - true if the tool threw an error
365
+
366
+ if (event.isError) {
367
+ // Handle error case
368
+ }
369
+
370
+ // Modify result:
371
+ return { content: [...], details: {...}, isError: false };
372
+ });
373
+ ```
374
+
375
+ Use type guards for typed details:
376
+
377
+ ```typescript
378
+ import { isBashToolResult } from "@oh-my-pi/pi-coding-agent";
379
+
380
+ pi.on("tool_result", async (event, ctx) => {
381
+ if (isBashToolResult(event)) {
382
+ // event.details is BashToolDetails | undefined
383
+ if (event.details?.truncation?.truncated) {
384
+ // Full output at event.details.fullOutputPath
385
+ }
386
+ }
387
+ });
388
+ ```
389
+
390
+ Available guards: `isBashToolResult`, `isReadToolResult`, `isEditToolResult`, `isWriteToolResult`, `isGrepToolResult`, `isFindToolResult`, `isLsToolResult`.
391
+
392
+ ## HookContext
393
+
394
+ Every handler receives `ctx: HookContext`:
395
+
396
+ ### ctx.ui
397
+
398
+ UI methods for user interaction. Hooks can prompt users and even render custom TUI components.
399
+
400
+ **Built-in dialogs:**
401
+
402
+ ```typescript
403
+ // Select from options
404
+ const choice = await ctx.ui.select("Pick one:", ["A", "B", "C"]);
405
+ // Returns selected string or undefined if cancelled
406
+
407
+ // Confirm dialog
408
+ const ok = await ctx.ui.confirm("Delete?", "This cannot be undone");
409
+ // Returns true or false
410
+
411
+ // Text input (single line)
412
+ const name = await ctx.ui.input("Name:", "placeholder");
413
+ // Returns string or undefined if cancelled
414
+
415
+ // Multi-line editor (with Ctrl+G for external editor)
416
+ const text = await ctx.ui.editor("Edit prompt:", "prefilled text");
417
+ // Returns edited text or undefined if cancelled (Escape)
418
+ // Ctrl+Enter to submit, Ctrl+G to open $VISUAL or $EDITOR
419
+
420
+ // Notification (non-blocking)
421
+ ctx.ui.notify("Done!", "info"); // "info" | "warning" | "error"
422
+
423
+ // Set status text in footer (persistent until cleared)
424
+ ctx.ui.setStatus("my-hook", "Processing 5/10..."); // Set status
425
+ ctx.ui.setStatus("my-hook", undefined); // Clear status
426
+
427
+ // Set the core input editor text (pre-fill prompts, generated content)
428
+ ctx.ui.setEditorText("Generated prompt text here...");
429
+
430
+ // Get current editor text
431
+ const currentText = ctx.ui.getEditorText();
432
+ ```
433
+
434
+ **Status text notes:**
435
+
436
+ - Multiple hooks can set their own status using unique keys
437
+ - Statuses are displayed on a single line in the footer, sorted alphabetically by key
438
+ - Text is sanitized (newlines/tabs replaced with spaces) and truncated to terminal width
439
+ - Use `ctx.ui.theme` to style status text with theme colors (see below)
440
+
441
+ **Styling with theme colors:**
442
+
443
+ Use `ctx.ui.theme` to apply consistent colors that respect the user's theme:
444
+
445
+ ```typescript
446
+ const theme = ctx.ui.theme;
447
+
448
+ // Foreground colors
449
+ ctx.ui.setStatus("my-hook", theme.fg("success", "✓") + theme.fg("dim", " Ready"));
450
+ ctx.ui.setStatus("my-hook", theme.fg("error", "✗") + theme.fg("dim", " Failed"));
451
+ ctx.ui.setStatus("my-hook", theme.fg("accent", "●") + theme.fg("dim", " Working..."));
452
+
453
+ // Available fg colors: accent, success, error, warning, muted, dim, text, and more
454
+ // See docs/theme.md for the full list of theme colors
455
+ ```
456
+
457
+ See [examples/hooks/status-line.ts](../examples/hooks/status-line.ts) for a complete example.
458
+
459
+ **Custom components:**
460
+
461
+ Show a custom TUI component with keyboard focus:
462
+
463
+ ```typescript
464
+ import { BorderedLoader } from "@oh-my-pi/pi-coding-agent";
465
+
466
+ const result = await ctx.ui.custom((tui, theme, done) => {
467
+ const loader = new BorderedLoader(tui, theme, "Working...");
468
+ loader.onAbort = () => done(null);
469
+
470
+ doWork(loader.signal)
471
+ .then(done)
472
+ .catch(() => done(null));
473
+
474
+ return loader;
475
+ });
476
+ ```
477
+
478
+ Your component can:
479
+
480
+ - Implement `handleInput(data: string)` to receive keyboard input
481
+ - Implement `render(width: number): string[]` to render lines
482
+ - Implement `invalidate()` to clear cached render
483
+ - Implement `dispose()` for cleanup when closed
484
+ - Call `tui.requestRender()` to trigger re-render
485
+ - Call `done(result)` when done to restore normal UI
486
+
487
+ See [examples/hooks/qna.ts](../examples/hooks/qna.ts) for a loader pattern and [examples/hooks/snake.ts](../examples/hooks/snake.ts) for a game. See [tui.md](tui.md) for the full component API.
488
+
489
+ ### ctx.hasUI
490
+
491
+ `false` in print mode (`-p`), JSON print mode, and RPC mode. Always check before using `ctx.ui`:
492
+
493
+ ```typescript
494
+ if (ctx.hasUI) {
495
+ const choice = await ctx.ui.select(...);
496
+ } else {
497
+ // Default behavior
498
+ }
499
+ ```
500
+
501
+ ### ctx.cwd
502
+
503
+ Current working directory.
504
+
505
+ ### ctx.sessionManager
506
+
507
+ Read-only access to session state. See `ReadonlySessionManager` in [`src/core/session-manager.ts`](../src/core/session-manager.ts).
508
+
509
+ ```typescript
510
+ // Session info
511
+ ctx.sessionManager.getCwd(); // Working directory
512
+ ctx.sessionManager.getSessionDir(); // Session directory (~/.pi/agent/sessions)
513
+ ctx.sessionManager.getSessionId(); // Current session ID
514
+ ctx.sessionManager.getSessionFile(); // Session file path (undefined with --no-session)
515
+
516
+ // Entries
517
+ ctx.sessionManager.getEntries(); // All entries (excludes header)
518
+ ctx.sessionManager.getHeader(); // Session header entry
519
+ ctx.sessionManager.getEntry(id); // Specific entry by ID
520
+ ctx.sessionManager.getLabel(id); // Entry label (if any)
521
+
522
+ // Tree navigation
523
+ ctx.sessionManager.getBranch(); // Current branch (root to leaf)
524
+ ctx.sessionManager.getBranch(leafId); // Specific branch
525
+ ctx.sessionManager.getTree(); // Full tree structure
526
+ ctx.sessionManager.getLeafId(); // Current leaf entry ID
527
+ ctx.sessionManager.getLeafEntry(); // Current leaf entry
528
+ ```
529
+
530
+ Use `pi.sendMessage()` or `pi.appendEntry()` for writes.
531
+
532
+ ### ctx.modelRegistry
533
+
534
+ Access to models and API keys:
535
+
536
+ ```typescript
537
+ // Get API key for a model
538
+ const apiKey = await ctx.modelRegistry.getApiKey(model);
539
+
540
+ // Get available models
541
+ const models = ctx.modelRegistry.getAvailableModels();
542
+ ```
543
+
544
+ ### ctx.model
545
+
546
+ Current model, or `undefined` if none selected yet. Use for LLM calls in hooks:
547
+
548
+ ```typescript
549
+ if (ctx.model) {
550
+ const apiKey = await ctx.modelRegistry.getApiKey(ctx.model);
551
+ // Use with @oh-my-pi/pi-ai complete()
552
+ }
553
+ ```
554
+
555
+ ### ctx.isIdle()
556
+
557
+ Returns `true` if the agent is not currently streaming:
558
+
559
+ ```typescript
560
+ if (ctx.isIdle()) {
561
+ // Agent is not processing
562
+ }
563
+ ```
564
+
565
+ ### ctx.abort()
566
+
567
+ Abort the current agent operation (fire-and-forget, does not wait):
568
+
569
+ ```typescript
570
+ await ctx.abort();
571
+ ```
572
+
573
+ ### ctx.hasQueuedMessages()
574
+
575
+ Check if there are messages queued (user typed while agent was streaming):
576
+
577
+ ```typescript
578
+ if (ctx.hasQueuedMessages()) {
579
+ // Skip interactive prompt, let queued message take over
580
+ return;
581
+ }
582
+ ```
583
+
584
+ ## HookCommandContext (Slash Commands Only)
585
+
586
+ Slash command handlers receive `HookCommandContext`, which extends `HookContext` with session control methods. These methods are only safe in user-initiated commands because they can cause deadlocks if called from event handlers (which run inside the agent loop).
587
+
588
+ ### ctx.waitForIdle()
589
+
590
+ Wait for the agent to finish streaming:
591
+
592
+ ```typescript
593
+ await ctx.waitForIdle();
594
+ // Agent is now idle
595
+ ```
596
+
597
+ ### ctx.newSession(options?)
598
+
599
+ Create a new session, optionally with initialization:
600
+
601
+ ```typescript
602
+ const result = await ctx.newSession({
603
+ parentSession: ctx.sessionManager.getSessionFile(), // Track lineage
604
+ setup: async (sm) => {
605
+ // Initialize the new session
606
+ sm.appendMessage({
607
+ role: "user",
608
+ content: [{ type: "text", text: "Context from previous session..." }],
609
+ timestamp: Date.now(),
610
+ });
611
+ },
612
+ });
613
+
614
+ if (result.cancelled) {
615
+ // A hook cancelled the new session
616
+ }
617
+ ```
618
+
619
+ ### ctx.branch(entryId)
620
+
621
+ Branch from a specific entry, creating a new session file:
622
+
623
+ ```typescript
624
+ const result = await ctx.branch("entry-id-123");
625
+ if (!result.cancelled) {
626
+ // Now in the branched session
627
+ }
628
+ ```
629
+
630
+ ### ctx.navigateTree(targetId, options?)
631
+
632
+ Navigate to a different point in the session tree:
633
+
634
+ ```typescript
635
+ const result = await ctx.navigateTree("entry-id-456", {
636
+ summarize: true, // Summarize the abandoned branch
637
+ });
638
+ ```
639
+
640
+ ## HookAPI Methods
641
+
642
+ ### pi.on(event, handler)
643
+
644
+ Subscribe to events. See [Events](#events) for all event types.
645
+
646
+ ### pi.sendMessage(message, triggerTurn?)
647
+
648
+ Inject a message into the session. Creates a `CustomMessageEntry` that participates in the LLM context.
649
+
650
+ ```typescript
651
+ pi.sendMessage({
652
+ customType: "my-hook", // Your hook's identifier
653
+ content: "Message text", // string or (TextContent | ImageContent)[]
654
+ display: true, // Show in TUI
655
+ details: { ... }, // Optional metadata (not sent to LLM)
656
+ }, triggerTurn); // If true, triggers LLM response
657
+ ```
658
+
659
+ **Storage and timing:**
660
+
661
+ - The message is appended to the session file immediately as a `CustomMessageEntry`
662
+ - If the agent is currently streaming, the message is queued and appended after the current turn
663
+ - If `triggerTurn` is true and the agent is idle, a new agent loop starts
664
+
665
+ **LLM context:**
666
+
667
+ - `CustomMessageEntry` is converted to a user message when building context for the LLM
668
+ - Only `content` is sent to the LLM; `details` is for rendering/state only
669
+
670
+ **TUI display:**
671
+
672
+ - If `display: true`, the message appears in the chat with purple styling (customMessageBg, customMessageText, customMessageLabel theme colors)
673
+ - If `display: false`, the message is hidden from the TUI but still sent to the LLM
674
+ - Use `pi.registerMessageRenderer()` to customize how your messages render (see below)
675
+
676
+ ### pi.appendEntry(customType, data?)
677
+
678
+ Persist hook state. Creates `CustomEntry` (does NOT participate in LLM context).
679
+
680
+ ```typescript
681
+ // Save state
682
+ pi.appendEntry("my-hook-state", { count: 42 });
683
+
684
+ // Restore on reload
685
+ pi.on("session_start", async (_event, ctx) => {
686
+ for (const entry of ctx.sessionManager.getEntries()) {
687
+ if (entry.type === "custom" && entry.customType === "my-hook-state") {
688
+ // Reconstruct from entry.data
689
+ }
690
+ }
691
+ });
692
+ ```
693
+
694
+ ### pi.registerCommand(name, options)
695
+
696
+ Register a custom slash command:
697
+
698
+ ```typescript
699
+ pi.registerCommand("stats", {
700
+ description: "Show session statistics",
701
+ handler: async (args, ctx) => {
702
+ // args = everything after /stats
703
+ const count = ctx.sessionManager.getEntries().length;
704
+ ctx.ui.notify(`${count} entries`, "info");
705
+ },
706
+ });
707
+ ```
708
+
709
+ For long-running commands (e.g., LLM calls), use `ctx.ui.custom()` with a loader. See [examples/hooks/qna.ts](../examples/hooks/qna.ts).
710
+
711
+ To trigger LLM after command, call `pi.sendMessage(..., true)`.
712
+
713
+ ### pi.registerMessageRenderer(customType, renderer)
714
+
715
+ Register a custom TUI renderer for `CustomMessageEntry` messages with your `customType`. Without a custom renderer, messages display with default purple styling showing the content as-is.
716
+
717
+ ```typescript
718
+ import { Text } from "@oh-my-pi/pi-tui";
719
+
720
+ pi.registerMessageRenderer("my-hook", (message, options, theme) => {
721
+ // message.content - the message content (string or content array)
722
+ // message.details - your custom metadata
723
+ // options.expanded - true if user pressed Ctrl+O
724
+
725
+ const prefix = theme.fg("accent", `[${message.details?.label ?? "INFO"}] `);
726
+ const text =
727
+ typeof message.content === "string"
728
+ ? message.content
729
+ : message.content.map((c) => (c.type === "text" ? c.text : "[image]")).join("");
730
+
731
+ return new Text(prefix + theme.fg("text", text), 0, 0);
732
+ });
733
+ ```
734
+
735
+ **Renderer signature:**
736
+
737
+ ```typescript
738
+ type HookMessageRenderer = (
739
+ message: CustomMessageEntry,
740
+ options: { expanded: boolean },
741
+ theme: Theme
742
+ ) => Component | null;
743
+ ```
744
+
745
+ Return `null` to use default rendering. The returned component is wrapped in a styled Box by the TUI. See [tui.md](tui.md) for component details.
746
+
747
+ ### pi.exec(command, args, options?)
748
+
749
+ Execute a shell command:
750
+
751
+ ```typescript
752
+ const result = await pi.exec("git", ["status"], {
753
+ signal, // AbortSignal
754
+ timeout, // Milliseconds
755
+ });
756
+
757
+ // result.stdout, result.stderr, result.code, result.killed
758
+ ```
759
+
760
+ ## Examples
761
+
762
+ ### Permission Gate
763
+
764
+ ```typescript
765
+ import type { HookAPI } from "@oh-my-pi/pi-coding-agent";
766
+
767
+ export default function (pi: HookAPI) {
768
+ const dangerous = [/\brm\s+(-rf?|--recursive)/i, /\bsudo\b/i];
769
+
770
+ pi.on("tool_call", async (event, ctx) => {
771
+ if (event.toolName !== "bash") return;
772
+
773
+ const cmd = event.input.command as string;
774
+ if (dangerous.some((p) => p.test(cmd))) {
775
+ if (!ctx.hasUI) {
776
+ return { block: true, reason: "Dangerous (no UI)" };
777
+ }
778
+ const ok = await ctx.ui.confirm("Dangerous!", `Allow: ${cmd}?`);
779
+ if (!ok) return { block: true, reason: "Blocked by user" };
780
+ }
781
+ });
782
+ }
783
+ ```
784
+
785
+ ### Protected Paths
786
+
787
+ ```typescript
788
+ import type { HookAPI } from "@oh-my-pi/pi-coding-agent";
789
+
790
+ export default function (pi: HookAPI) {
791
+ const protectedPaths = [".env", ".git/", "node_modules/"];
792
+
793
+ pi.on("tool_call", async (event, ctx) => {
794
+ if (event.toolName !== "write" && event.toolName !== "edit") return;
795
+
796
+ const path = event.input.path as string;
797
+ if (protectedPaths.some((p) => path.includes(p))) {
798
+ ctx.ui.notify(`Blocked: ${path}`, "warning");
799
+ return { block: true, reason: `Protected: ${path}` };
800
+ }
801
+ });
802
+ }
803
+ ```
804
+
805
+ ### Git Checkpoint
806
+
807
+ ```typescript
808
+ import type { HookAPI } from "@oh-my-pi/pi-coding-agent";
809
+
810
+ export default function (pi: HookAPI) {
811
+ const checkpoints = new Map<string, string>();
812
+ let currentEntryId: string | undefined;
813
+
814
+ pi.on("tool_result", async (_event, ctx) => {
815
+ const leaf = ctx.sessionManager.getLeafEntry();
816
+ if (leaf) currentEntryId = leaf.id;
817
+ });
818
+
819
+ pi.on("turn_start", async () => {
820
+ const { stdout } = await pi.exec("git", ["stash", "create"]);
821
+ if (stdout.trim() && currentEntryId) {
822
+ checkpoints.set(currentEntryId, stdout.trim());
823
+ }
824
+ });
825
+
826
+ pi.on("session_before_branch", async (event, ctx) => {
827
+ const ref = checkpoints.get(event.entryId);
828
+ if (!ref || !ctx.hasUI) return;
829
+
830
+ const ok = await ctx.ui.confirm("Restore?", "Restore code to checkpoint?");
831
+ if (ok) {
832
+ await pi.exec("git", ["stash", "apply", ref]);
833
+ ctx.ui.notify("Code restored", "info");
834
+ }
835
+ });
836
+
837
+ pi.on("agent_end", () => checkpoints.clear());
838
+ }
839
+ ```
840
+
841
+ ### Custom Command
842
+
843
+ See [examples/hooks/snake.ts](../examples/hooks/snake.ts) for a complete example with `registerCommand()`, `ui.custom()`, and session persistence.
844
+
845
+ ## Mode Behavior
846
+
847
+ | Mode | UI Methods | Notes |
848
+ | ------------ | -------------------------- | -------------------------- |
849
+ | Interactive | Full TUI | Normal operation |
850
+ | RPC | JSON protocol | Host handles UI |
851
+ | Print (`-p`) | No-op (returns null/false) | Hooks run but can't prompt |
852
+
853
+ In print mode, `select()` returns `undefined`, `confirm()` returns `false`, `input()` returns `undefined`, `getEditorText()` returns `""`, and `setEditorText()`/`setStatus()` are no-ops. Design hooks to handle this by checking `ctx.hasUI`.
854
+
855
+ ## Error Handling
856
+
857
+ - Hook errors are logged, agent continues
858
+ - `tool_call` errors block the tool (fail-safe)
859
+ - Errors display in UI with hook path and message
860
+ - If a hook hangs, use Ctrl+C to abort
861
+
862
+ ## Debugging
863
+
864
+ 1. Open VS Code in hooks directory
865
+ 2. Open JavaScript Debug Terminal (Ctrl+Shift+P → "JavaScript Debug Terminal")
866
+ 3. Set breakpoints
867
+ 4. Run `pi --hook ./my-hook.ts`