@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.
- package/CHANGELOG.md +1228 -0
- package/README.md +1041 -0
- package/docs/compaction.md +403 -0
- package/docs/custom-tools.md +541 -0
- package/docs/extension-loading.md +1004 -0
- package/docs/hooks.md +867 -0
- package/docs/rpc.md +1040 -0
- package/docs/sdk.md +994 -0
- package/docs/session-tree-plan.md +441 -0
- package/docs/session.md +240 -0
- package/docs/skills.md +290 -0
- package/docs/theme.md +637 -0
- package/docs/tree.md +197 -0
- package/docs/tui.md +341 -0
- package/examples/README.md +21 -0
- package/examples/custom-tools/README.md +124 -0
- package/examples/custom-tools/hello/index.ts +20 -0
- package/examples/custom-tools/question/index.ts +84 -0
- package/examples/custom-tools/subagent/README.md +172 -0
- package/examples/custom-tools/subagent/agents/planner.md +37 -0
- package/examples/custom-tools/subagent/agents/reviewer.md +35 -0
- package/examples/custom-tools/subagent/agents/scout.md +50 -0
- package/examples/custom-tools/subagent/agents/worker.md +24 -0
- package/examples/custom-tools/subagent/agents.ts +156 -0
- package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
- package/examples/custom-tools/subagent/commands/implement.md +10 -0
- package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
- package/examples/custom-tools/subagent/index.ts +1002 -0
- package/examples/custom-tools/todo/index.ts +212 -0
- package/examples/hooks/README.md +56 -0
- package/examples/hooks/auto-commit-on-exit.ts +49 -0
- package/examples/hooks/confirm-destructive.ts +59 -0
- package/examples/hooks/custom-compaction.ts +116 -0
- package/examples/hooks/dirty-repo-guard.ts +52 -0
- package/examples/hooks/file-trigger.ts +41 -0
- package/examples/hooks/git-checkpoint.ts +53 -0
- package/examples/hooks/handoff.ts +150 -0
- package/examples/hooks/permission-gate.ts +34 -0
- package/examples/hooks/protected-paths.ts +30 -0
- package/examples/hooks/qna.ts +119 -0
- package/examples/hooks/snake.ts +343 -0
- package/examples/hooks/status-line.ts +40 -0
- package/examples/sdk/01-minimal.ts +22 -0
- package/examples/sdk/02-custom-model.ts +49 -0
- package/examples/sdk/03-custom-prompt.ts +44 -0
- package/examples/sdk/04-skills.ts +44 -0
- package/examples/sdk/05-tools.ts +90 -0
- package/examples/sdk/06-hooks.ts +61 -0
- package/examples/sdk/07-context-files.ts +36 -0
- package/examples/sdk/08-slash-commands.ts +42 -0
- package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
- package/examples/sdk/10-settings.ts +38 -0
- package/examples/sdk/11-sessions.ts +48 -0
- package/examples/sdk/12-full-control.ts +95 -0
- package/examples/sdk/README.md +154 -0
- package/package.json +81 -0
- package/src/cli/args.ts +246 -0
- package/src/cli/file-processor.ts +72 -0
- package/src/cli/list-models.ts +104 -0
- package/src/cli/plugin-cli.ts +650 -0
- package/src/cli/session-picker.ts +41 -0
- package/src/cli.ts +10 -0
- package/src/commands/init.md +20 -0
- package/src/config.ts +159 -0
- package/src/core/agent-session.ts +1900 -0
- package/src/core/auth-storage.ts +236 -0
- package/src/core/bash-executor.ts +196 -0
- package/src/core/compaction/branch-summarization.ts +343 -0
- package/src/core/compaction/compaction.ts +742 -0
- package/src/core/compaction/index.ts +7 -0
- package/src/core/compaction/utils.ts +154 -0
- package/src/core/custom-tools/index.ts +21 -0
- package/src/core/custom-tools/loader.ts +248 -0
- package/src/core/custom-tools/types.ts +169 -0
- package/src/core/custom-tools/wrapper.ts +28 -0
- package/src/core/exec.ts +129 -0
- package/src/core/export-html/index.ts +211 -0
- package/src/core/export-html/template.css +781 -0
- package/src/core/export-html/template.html +54 -0
- package/src/core/export-html/template.js +1185 -0
- package/src/core/export-html/vendor/highlight.min.js +1213 -0
- package/src/core/export-html/vendor/marked.min.js +6 -0
- package/src/core/hooks/index.ts +16 -0
- package/src/core/hooks/loader.ts +312 -0
- package/src/core/hooks/runner.ts +434 -0
- package/src/core/hooks/tool-wrapper.ts +99 -0
- package/src/core/hooks/types.ts +773 -0
- package/src/core/index.ts +52 -0
- package/src/core/mcp/client.ts +158 -0
- package/src/core/mcp/config.ts +154 -0
- package/src/core/mcp/index.ts +45 -0
- package/src/core/mcp/loader.ts +68 -0
- package/src/core/mcp/manager.ts +181 -0
- package/src/core/mcp/tool-bridge.ts +148 -0
- package/src/core/mcp/transports/http.ts +316 -0
- package/src/core/mcp/transports/index.ts +6 -0
- package/src/core/mcp/transports/stdio.ts +252 -0
- package/src/core/mcp/types.ts +220 -0
- package/src/core/messages.ts +189 -0
- package/src/core/model-registry.ts +317 -0
- package/src/core/model-resolver.ts +393 -0
- package/src/core/plugins/doctor.ts +59 -0
- package/src/core/plugins/index.ts +38 -0
- package/src/core/plugins/installer.ts +189 -0
- package/src/core/plugins/loader.ts +338 -0
- package/src/core/plugins/manager.ts +672 -0
- package/src/core/plugins/parser.ts +105 -0
- package/src/core/plugins/paths.ts +32 -0
- package/src/core/plugins/types.ts +190 -0
- package/src/core/sdk.ts +760 -0
- package/src/core/session-manager.ts +1128 -0
- package/src/core/settings-manager.ts +443 -0
- package/src/core/skills.ts +437 -0
- package/src/core/slash-commands.ts +248 -0
- package/src/core/system-prompt.ts +439 -0
- package/src/core/timings.ts +25 -0
- package/src/core/tools/ask.ts +211 -0
- package/src/core/tools/bash-interceptor.ts +120 -0
- package/src/core/tools/bash.ts +250 -0
- package/src/core/tools/context.ts +32 -0
- package/src/core/tools/edit-diff.ts +475 -0
- package/src/core/tools/edit.ts +208 -0
- package/src/core/tools/exa/company.ts +59 -0
- package/src/core/tools/exa/index.ts +64 -0
- package/src/core/tools/exa/linkedin.ts +59 -0
- package/src/core/tools/exa/logger.ts +56 -0
- package/src/core/tools/exa/mcp-client.ts +368 -0
- package/src/core/tools/exa/render.ts +196 -0
- package/src/core/tools/exa/researcher.ts +90 -0
- package/src/core/tools/exa/search.ts +337 -0
- package/src/core/tools/exa/types.ts +168 -0
- package/src/core/tools/exa/websets.ts +248 -0
- package/src/core/tools/find.ts +261 -0
- package/src/core/tools/grep.ts +555 -0
- package/src/core/tools/index.ts +202 -0
- package/src/core/tools/ls.ts +140 -0
- package/src/core/tools/lsp/client.ts +605 -0
- package/src/core/tools/lsp/config.ts +147 -0
- package/src/core/tools/lsp/edits.ts +101 -0
- package/src/core/tools/lsp/index.ts +804 -0
- package/src/core/tools/lsp/render.ts +447 -0
- package/src/core/tools/lsp/rust-analyzer.ts +145 -0
- package/src/core/tools/lsp/types.ts +463 -0
- package/src/core/tools/lsp/utils.ts +486 -0
- package/src/core/tools/notebook.ts +229 -0
- package/src/core/tools/path-utils.ts +61 -0
- package/src/core/tools/read.ts +240 -0
- package/src/core/tools/renderers.ts +540 -0
- package/src/core/tools/task/agents.ts +153 -0
- package/src/core/tools/task/artifacts.ts +114 -0
- package/src/core/tools/task/bundled-agents/browser.md +71 -0
- package/src/core/tools/task/bundled-agents/explore.md +82 -0
- package/src/core/tools/task/bundled-agents/plan.md +54 -0
- package/src/core/tools/task/bundled-agents/reviewer.md +59 -0
- package/src/core/tools/task/bundled-agents/task.md +53 -0
- package/src/core/tools/task/bundled-commands/architect-plan.md +10 -0
- package/src/core/tools/task/bundled-commands/implement-with-critic.md +11 -0
- package/src/core/tools/task/bundled-commands/implement.md +11 -0
- package/src/core/tools/task/commands.ts +213 -0
- package/src/core/tools/task/discovery.ts +208 -0
- package/src/core/tools/task/executor.ts +367 -0
- package/src/core/tools/task/index.ts +388 -0
- package/src/core/tools/task/model-resolver.ts +115 -0
- package/src/core/tools/task/parallel.ts +38 -0
- package/src/core/tools/task/render.ts +232 -0
- package/src/core/tools/task/types.ts +99 -0
- package/src/core/tools/truncate.ts +265 -0
- package/src/core/tools/web-fetch.ts +2370 -0
- package/src/core/tools/web-search/auth.ts +193 -0
- package/src/core/tools/web-search/index.ts +537 -0
- package/src/core/tools/web-search/providers/anthropic.ts +198 -0
- package/src/core/tools/web-search/providers/exa.ts +302 -0
- package/src/core/tools/web-search/providers/perplexity.ts +195 -0
- package/src/core/tools/web-search/render.ts +182 -0
- package/src/core/tools/web-search/types.ts +180 -0
- package/src/core/tools/write.ts +99 -0
- package/src/index.ts +176 -0
- package/src/main.ts +464 -0
- package/src/migrations.ts +135 -0
- package/src/modes/index.ts +43 -0
- package/src/modes/interactive/components/armin.ts +382 -0
- package/src/modes/interactive/components/assistant-message.ts +86 -0
- package/src/modes/interactive/components/bash-execution.ts +196 -0
- package/src/modes/interactive/components/bordered-loader.ts +41 -0
- package/src/modes/interactive/components/branch-summary-message.ts +42 -0
- package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
- package/src/modes/interactive/components/custom-editor.ts +122 -0
- package/src/modes/interactive/components/diff.ts +147 -0
- package/src/modes/interactive/components/dynamic-border.ts +25 -0
- package/src/modes/interactive/components/footer.ts +381 -0
- package/src/modes/interactive/components/hook-editor.ts +117 -0
- package/src/modes/interactive/components/hook-input.ts +64 -0
- package/src/modes/interactive/components/hook-message.ts +96 -0
- package/src/modes/interactive/components/hook-selector.ts +91 -0
- package/src/modes/interactive/components/model-selector.ts +247 -0
- package/src/modes/interactive/components/oauth-selector.ts +120 -0
- package/src/modes/interactive/components/plugin-settings.ts +479 -0
- package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
- package/src/modes/interactive/components/session-selector.ts +204 -0
- package/src/modes/interactive/components/settings-selector.ts +453 -0
- package/src/modes/interactive/components/show-images-selector.ts +45 -0
- package/src/modes/interactive/components/theme-selector.ts +62 -0
- package/src/modes/interactive/components/thinking-selector.ts +64 -0
- package/src/modes/interactive/components/tool-execution.ts +675 -0
- package/src/modes/interactive/components/tree-selector.ts +866 -0
- package/src/modes/interactive/components/user-message-selector.ts +159 -0
- package/src/modes/interactive/components/user-message.ts +18 -0
- package/src/modes/interactive/components/visual-truncate.ts +50 -0
- package/src/modes/interactive/components/welcome.ts +183 -0
- package/src/modes/interactive/interactive-mode.ts +2516 -0
- package/src/modes/interactive/theme/dark.json +101 -0
- package/src/modes/interactive/theme/light.json +98 -0
- package/src/modes/interactive/theme/theme-schema.json +308 -0
- package/src/modes/interactive/theme/theme.ts +998 -0
- package/src/modes/print-mode.ts +128 -0
- package/src/modes/rpc/rpc-client.ts +527 -0
- package/src/modes/rpc/rpc-mode.ts +483 -0
- package/src/modes/rpc/rpc-types.ts +203 -0
- package/src/utils/changelog.ts +99 -0
- package/src/utils/clipboard.ts +265 -0
- package/src/utils/fuzzy.ts +108 -0
- package/src/utils/mime.ts +30 -0
- package/src/utils/shell.ts +276 -0
- 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`
|