@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
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Todo Tool - Demonstrates state management via session entries
3
+ *
4
+ * This tool stores state in tool result details (not external files),
5
+ * which allows proper branching - when you branch, the todo state
6
+ * is automatically correct for that point in history.
7
+ *
8
+ * The onSession callback reconstructs state by scanning past tool results.
9
+ */
10
+
11
+ import type {
12
+ CustomTool,
13
+ CustomToolContext,
14
+ CustomToolFactory,
15
+ CustomToolSessionEvent,
16
+ } from "@oh-my-pi/pi-coding-agent";
17
+
18
+ interface Todo {
19
+ id: number;
20
+ text: string;
21
+ done: boolean;
22
+ }
23
+
24
+ // State stored in tool result details
25
+ interface TodoDetails {
26
+ action: "list" | "add" | "toggle" | "clear";
27
+ todos: Todo[];
28
+ nextId: number;
29
+ error?: string;
30
+ }
31
+
32
+ const factory: CustomToolFactory = (pi) => {
33
+ const { Type } = pi.typebox;
34
+ const { StringEnum, Text } = pi.pi;
35
+
36
+ // Define schema separately for proper type inference
37
+ const TodoParams = Type.Object({
38
+ action: StringEnum(["list", "add", "toggle", "clear"] as const),
39
+ text: Type.Optional(Type.String({ description: "Todo text (for add)" })),
40
+ id: Type.Optional(Type.Number({ description: "Todo ID (for toggle)" })),
41
+ });
42
+ // In-memory state (reconstructed from session on load)
43
+ let todos: Todo[] = [];
44
+ let nextId = 1;
45
+
46
+ /**
47
+ * Reconstruct state from session entries.
48
+ * Scans tool results for this tool and applies them in order.
49
+ */
50
+ const reconstructState = (_event: CustomToolSessionEvent, ctx: CustomToolContext) => {
51
+ todos = [];
52
+ nextId = 1;
53
+
54
+ // Use getBranch() to get entries on the current branch
55
+ for (const entry of ctx.sessionManager.getBranch()) {
56
+ if (entry.type !== "message") continue;
57
+ const msg = entry.message;
58
+
59
+ // Tool results have role "toolResult"
60
+ if (msg.role !== "toolResult") continue;
61
+ if (msg.toolName !== "todo") continue;
62
+
63
+ const details = msg.details as TodoDetails | undefined;
64
+ if (details) {
65
+ todos = details.todos;
66
+ nextId = details.nextId;
67
+ }
68
+ }
69
+ };
70
+
71
+ const tool: CustomTool<typeof TodoParams, TodoDetails> = {
72
+ name: "todo",
73
+ label: "Todo",
74
+ description: "Manage a todo list. Actions: list, add (text), toggle (id), clear",
75
+ parameters: TodoParams,
76
+
77
+ // Called on session start/switch/branch/clear
78
+ onSession: reconstructState,
79
+
80
+ async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
81
+ switch (params.action) {
82
+ case "list":
83
+ return {
84
+ content: [
85
+ {
86
+ type: "text",
87
+ text: todos.length
88
+ ? todos.map((t) => `[${t.done ? "x" : " "}] #${t.id}: ${t.text}`).join("\n")
89
+ : "No todos",
90
+ },
91
+ ],
92
+ details: { action: "list", todos: [...todos], nextId },
93
+ };
94
+
95
+ case "add": {
96
+ if (!params.text) {
97
+ return {
98
+ content: [{ type: "text", text: "Error: text required for add" }],
99
+ details: { action: "add", todos: [...todos], nextId, error: "text required" },
100
+ };
101
+ }
102
+ const newTodo: Todo = { id: nextId++, text: params.text, done: false };
103
+ todos.push(newTodo);
104
+ return {
105
+ content: [{ type: "text", text: `Added todo #${newTodo.id}: ${newTodo.text}` }],
106
+ details: { action: "add", todos: [...todos], nextId },
107
+ };
108
+ }
109
+
110
+ case "toggle": {
111
+ if (params.id === undefined) {
112
+ return {
113
+ content: [{ type: "text", text: "Error: id required for toggle" }],
114
+ details: { action: "toggle", todos: [...todos], nextId, error: "id required" },
115
+ };
116
+ }
117
+ const todo = todos.find((t) => t.id === params.id);
118
+ if (!todo) {
119
+ return {
120
+ content: [{ type: "text", text: `Todo #${params.id} not found` }],
121
+ details: { action: "toggle", todos: [...todos], nextId, error: `#${params.id} not found` },
122
+ };
123
+ }
124
+ todo.done = !todo.done;
125
+ return {
126
+ content: [{ type: "text", text: `Todo #${todo.id} ${todo.done ? "completed" : "uncompleted"}` }],
127
+ details: { action: "toggle", todos: [...todos], nextId },
128
+ };
129
+ }
130
+
131
+ case "clear": {
132
+ const count = todos.length;
133
+ todos = [];
134
+ nextId = 1;
135
+ return {
136
+ content: [{ type: "text", text: `Cleared ${count} todos` }],
137
+ details: { action: "clear", todos: [], nextId: 1 },
138
+ };
139
+ }
140
+
141
+ default:
142
+ return {
143
+ content: [{ type: "text", text: `Unknown action: ${params.action}` }],
144
+ details: { action: "list", todos: [...todos], nextId, error: `unknown action: ${params.action}` },
145
+ };
146
+ }
147
+ },
148
+
149
+ renderCall(args, theme) {
150
+ let text = theme.fg("toolTitle", theme.bold("todo ")) + theme.fg("muted", String(args.action));
151
+ if (args.text) text += ` ${theme.fg("dim", `"${args.text}"`)}`;
152
+ if (args.id !== undefined) text += ` ${theme.fg("accent", `#${args.id}`)}`;
153
+ return new Text(text, 0, 0);
154
+ },
155
+
156
+ renderResult(result, { expanded }, theme) {
157
+ const { details } = result;
158
+ if (!details) {
159
+ const text = result.content[0];
160
+ return new Text(text?.type === "text" ? text.text : "", 0, 0);
161
+ }
162
+
163
+ // Error
164
+ if (details.error) {
165
+ return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
166
+ }
167
+
168
+ const todoList = details.todos;
169
+
170
+ switch (details.action) {
171
+ case "list": {
172
+ if (todoList.length === 0) {
173
+ return new Text(theme.fg("dim", "No todos"), 0, 0);
174
+ }
175
+ let listText = theme.fg("muted", `${todoList.length} todo(s):`);
176
+ const display = expanded ? todoList : todoList.slice(0, 5);
177
+ for (const t of display) {
178
+ const check = t.done ? theme.fg("success", "✓") : theme.fg("dim", "○");
179
+ const itemText = t.done ? theme.fg("dim", t.text) : theme.fg("muted", t.text);
180
+ listText += `\n${check} ${theme.fg("accent", `#${t.id}`)} ${itemText}`;
181
+ }
182
+ if (!expanded && todoList.length > 5) {
183
+ listText += `\n${theme.fg("dim", `... ${todoList.length - 5} more`)}`;
184
+ }
185
+ return new Text(listText, 0, 0);
186
+ }
187
+
188
+ case "add": {
189
+ const added = todoList[todoList.length - 1];
190
+ return new Text(
191
+ `${theme.fg("success", "✓ Added ") + theme.fg("accent", `#${added.id}`)} ${theme.fg("muted", added.text)}`,
192
+ 0,
193
+ 0,
194
+ );
195
+ }
196
+
197
+ case "toggle": {
198
+ const text = result.content[0];
199
+ const msg = text?.type === "text" ? text.text : "";
200
+ return new Text(theme.fg("success", "✓ ") + theme.fg("muted", msg), 0, 0);
201
+ }
202
+
203
+ case "clear":
204
+ return new Text(theme.fg("success", "✓ ") + theme.fg("muted", "Cleared all todos"), 0, 0);
205
+ }
206
+ },
207
+ };
208
+
209
+ return tool;
210
+ };
211
+
212
+ export default factory;
@@ -0,0 +1,56 @@
1
+ # Hooks Examples
2
+
3
+ Example hooks for pi-coding-agent.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ # Load a hook with --hook flag
9
+ pi --hook examples/hooks/permission-gate.ts
10
+
11
+ # Or copy to hooks directory for auto-discovery
12
+ cp permission-gate.ts ~/.pi/agent/hooks/
13
+ ```
14
+
15
+ ## Examples
16
+
17
+ | Hook | Description |
18
+ | ------------------------ | ------------------------------------------------------------------------------ |
19
+ | `permission-gate.ts` | Prompts for confirmation before dangerous bash commands (rm -rf, sudo, etc.) |
20
+ | `git-checkpoint.ts` | Creates git stash checkpoints at each turn for code restoration on branch |
21
+ | `protected-paths.ts` | Blocks writes to protected paths (.env, .git/, node_modules/) |
22
+ | `file-trigger.ts` | Watches a trigger file and injects contents into conversation |
23
+ | `confirm-destructive.ts` | Confirms before destructive session actions (clear, switch, branch) |
24
+ | `dirty-repo-guard.ts` | Prevents session changes with uncommitted git changes |
25
+ | `auto-commit-on-exit.ts` | Auto-commits on exit using last assistant message for commit message |
26
+ | `custom-compaction.ts` | Custom compaction that summarizes entire conversation |
27
+ | `qna.ts` | Extracts questions from last response into editor via `ctx.ui.setEditorText()` |
28
+ | `snake.ts` | Snake game with custom UI, keyboard handling, and session persistence |
29
+ | `status-line.ts` | Shows turn progress in footer via `ctx.ui.setStatus()` with themed colors |
30
+ | `handoff.ts` | Transfer context to a new focused session via `/handoff <goal>` |
31
+
32
+ ## Writing Hooks
33
+
34
+ See [docs/hooks.md](../../docs/hooks.md) for full documentation.
35
+
36
+ ```typescript
37
+ import type { HookAPI } from "@oh-my-pi/pi-coding-agent/hooks";
38
+
39
+ export default function (pi: HookAPI) {
40
+ // Subscribe to events
41
+ pi.on("tool_call", async (event, ctx) => {
42
+ if (event.toolName === "bash" && event.input.command?.includes("rm -rf")) {
43
+ const ok = await ctx.ui.confirm("Dangerous!", "Allow rm -rf?");
44
+ if (!ok) return { block: true, reason: "Blocked by user" };
45
+ }
46
+ });
47
+
48
+ // Register custom commands
49
+ pi.registerCommand("hello", {
50
+ description: "Say hello",
51
+ handler: async (args, ctx) => {
52
+ ctx.ui.notify("Hello!", "info");
53
+ },
54
+ });
55
+ }
56
+ ```
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Auto-Commit on Exit Hook
3
+ *
4
+ * Automatically commits changes when the agent exits.
5
+ * Uses the last assistant message to generate a commit message.
6
+ */
7
+
8
+ import type { HookAPI } from "@oh-my-pi/pi-coding-agent";
9
+
10
+ export default function (pi: HookAPI) {
11
+ pi.on("session_shutdown", async (_event, ctx) => {
12
+ // Check for uncommitted changes
13
+ const { stdout: status, code } = await pi.exec("git", ["status", "--porcelain"]);
14
+
15
+ if (code !== 0 || status.trim().length === 0) {
16
+ // Not a git repo or no changes
17
+ return;
18
+ }
19
+
20
+ // Find the last assistant message for commit context
21
+ const entries = ctx.sessionManager.getEntries();
22
+ let lastAssistantText = "";
23
+ for (let i = entries.length - 1; i >= 0; i--) {
24
+ const entry = entries[i];
25
+ if (entry.type === "message" && entry.message.role === "assistant") {
26
+ const content = entry.message.content;
27
+ if (Array.isArray(content)) {
28
+ lastAssistantText = content
29
+ .filter((c): c is { type: "text"; text: string } => c.type === "text")
30
+ .map((c) => c.text)
31
+ .join("\n");
32
+ }
33
+ break;
34
+ }
35
+ }
36
+
37
+ // Generate a simple commit message
38
+ const firstLine = lastAssistantText.split("\n")[0] || "Work in progress";
39
+ const commitMessage = `[pi] ${firstLine.slice(0, 50)}${firstLine.length > 50 ? "..." : ""}`;
40
+
41
+ // Stage and commit
42
+ await pi.exec("git", ["add", "-A"]);
43
+ const { code: commitCode } = await pi.exec("git", ["commit", "-m", commitMessage]);
44
+
45
+ if (commitCode === 0 && ctx.hasUI) {
46
+ ctx.ui.notify(`Auto-committed: ${commitMessage}`, "info");
47
+ }
48
+ });
49
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Confirm Destructive Actions Hook
3
+ *
4
+ * Prompts for confirmation before destructive session actions (clear, switch, branch).
5
+ * Demonstrates how to cancel session events using the before_* events.
6
+ */
7
+
8
+ import type { HookAPI, SessionBeforeSwitchEvent, SessionMessageEntry } from "@oh-my-pi/pi-coding-agent";
9
+
10
+ export default function (pi: HookAPI) {
11
+ pi.on("session_before_switch", async (event: SessionBeforeSwitchEvent, ctx) => {
12
+ if (!ctx.hasUI) return;
13
+
14
+ if (event.reason === "new") {
15
+ const confirmed = await ctx.ui.confirm(
16
+ "Clear session?",
17
+ "This will delete all messages in the current session.",
18
+ );
19
+
20
+ if (!confirmed) {
21
+ ctx.ui.notify("Clear cancelled", "info");
22
+ return { cancel: true };
23
+ }
24
+ return;
25
+ }
26
+
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
+ );
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
+ );
38
+
39
+ if (!confirmed) {
40
+ ctx.ui.notify("Switch cancelled", "info");
41
+ return { cancel: true };
42
+ }
43
+ }
44
+ });
45
+
46
+ pi.on("session_before_branch", async (event, ctx) => {
47
+ if (!ctx.hasUI) return;
48
+
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
+
54
+ if (choice !== "Yes, create branch") {
55
+ ctx.ui.notify("Branch cancelled", "info");
56
+ return { cancel: true };
57
+ }
58
+ });
59
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Custom Compaction Hook
3
+ *
4
+ * Replaces the default compaction behavior with a full summary of the entire context.
5
+ * Instead of keeping the last 20k tokens of conversation turns, this hook:
6
+ * 1. Summarizes ALL messages (messagesToSummarize + turnPrefixMessages)
7
+ * 2. Discards all old turns completely, keeping only the summary
8
+ *
9
+ * This example also demonstrates using a different model (Gemini Flash) for summarization,
10
+ * which can be cheaper/faster than the main conversation model.
11
+ *
12
+ * Usage:
13
+ * pi --hook examples/hooks/custom-compaction.ts
14
+ */
15
+
16
+ import { complete, getModel } from "@oh-my-pi/pi-ai";
17
+ import type { HookAPI } from "@oh-my-pi/pi-coding-agent";
18
+ import { convertToLlm, serializeConversation } from "@oh-my-pi/pi-coding-agent";
19
+
20
+ export default function (pi: HookAPI) {
21
+ pi.on("session_before_compact", async (event, ctx) => {
22
+ ctx.ui.notify("Custom compaction hook triggered", "info");
23
+
24
+ const { preparation, branchEntries: _, signal } = event;
25
+ const { messagesToSummarize, turnPrefixMessages, tokensBefore, firstKeptEntryId, previousSummary } = preparation;
26
+
27
+ // Use Gemini Flash for summarization (cheaper/faster than most conversation models)
28
+ const model = getModel("google", "gemini-2.5-flash");
29
+ if (!model) {
30
+ ctx.ui.notify(`Could not find Gemini Flash model, using default compaction`, "warning");
31
+ return;
32
+ }
33
+
34
+ // Resolve API key for the summarization model
35
+ const apiKey = await ctx.modelRegistry.getApiKey(model);
36
+ if (!apiKey) {
37
+ ctx.ui.notify(`No API key for ${model.provider}, using default compaction`, "warning");
38
+ return;
39
+ }
40
+
41
+ // Combine all messages for full summary
42
+ const allMessages = [...messagesToSummarize, ...turnPrefixMessages];
43
+
44
+ ctx.ui.notify(
45
+ `Custom compaction: summarizing ${allMessages.length} messages (${tokensBefore.toLocaleString()} tokens) with ${
46
+ model.id
47
+ }...`,
48
+ "info",
49
+ );
50
+
51
+ // Convert messages to readable text format
52
+ const conversationText = serializeConversation(convertToLlm(allMessages));
53
+
54
+ // Include previous summary context if available
55
+ const previousContext = previousSummary ? `\n\nPrevious session summary for context:\n${previousSummary}` : "";
56
+
57
+ // Build messages that ask for a comprehensive summary
58
+ const summaryMessages = [
59
+ {
60
+ role: "user" as const,
61
+ content: [
62
+ {
63
+ type: "text" as const,
64
+ text: `You are a conversation summarizer. Create a comprehensive summary of this conversation that captures:${previousContext}
65
+
66
+ 1. The main goals and objectives discussed
67
+ 2. Key decisions made and their rationale
68
+ 3. Important code changes, file modifications, or technical details
69
+ 4. Current state of any ongoing work
70
+ 5. Any blockers, issues, or open questions
71
+ 6. Next steps that were planned or suggested
72
+
73
+ Be thorough but concise. The summary will replace the ENTIRE conversation history, so include all information needed to continue the work effectively.
74
+
75
+ Format the summary as structured markdown with clear sections.
76
+
77
+ <conversation>
78
+ ${conversationText}
79
+ </conversation>`,
80
+ },
81
+ ],
82
+ timestamp: Date.now(),
83
+ },
84
+ ];
85
+
86
+ try {
87
+ // Pass signal to honor abort requests (e.g., user cancels compaction)
88
+ const response = await complete(model, { messages: summaryMessages }, { apiKey, maxTokens: 8192, signal });
89
+
90
+ const summary = response.content
91
+ .filter((c): c is { type: "text"; text: string } => c.type === "text")
92
+ .map((c) => c.text)
93
+ .join("\n");
94
+
95
+ if (!summary.trim()) {
96
+ if (!signal.aborted) ctx.ui.notify("Compaction summary was empty, using default compaction", "warning");
97
+ return;
98
+ }
99
+
100
+ // Return compaction content - SessionManager adds id/parentId
101
+ // Use firstKeptEntryId from preparation to keep recent messages
102
+ return {
103
+ compaction: {
104
+ summary,
105
+ firstKeptEntryId,
106
+ tokensBefore,
107
+ },
108
+ };
109
+ } catch (error) {
110
+ const message = error instanceof Error ? error.message : String(error);
111
+ ctx.ui.notify(`Compaction failed: ${message}`, "error");
112
+ // Fall back to default compaction on error
113
+ return;
114
+ }
115
+ });
116
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Dirty Repo Guard Hook
3
+ *
4
+ * Prevents session changes when there are uncommitted git changes.
5
+ * Useful to ensure work is committed before switching context.
6
+ */
7
+
8
+ import type { HookAPI, HookContext } from "@oh-my-pi/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
+ }
42
+
43
+ export default function (pi: HookAPI) {
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");
51
+ });
52
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * File Trigger Hook
3
+ *
4
+ * Watches a trigger file and injects its contents into the conversation.
5
+ * Useful for external systems to send messages to the agent.
6
+ *
7
+ * Usage:
8
+ * echo "Run the tests" > /tmp/agent-trigger.txt
9
+ */
10
+
11
+ import * as fs from "node:fs";
12
+ import type { HookAPI } from "@oh-my-pi/pi-coding-agent";
13
+
14
+ export default function (pi: HookAPI) {
15
+ pi.on("session_start", async (_event, ctx) => {
16
+ const triggerFile = "/tmp/agent-trigger.txt";
17
+
18
+ fs.watch(triggerFile, () => {
19
+ try {
20
+ const content = fs.readFileSync(triggerFile, "utf-8").trim();
21
+ if (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
+ );
30
+ fs.writeFileSync(triggerFile, ""); // Clear after reading
31
+ }
32
+ } catch {
33
+ // File might not exist yet
34
+ }
35
+ });
36
+
37
+ if (ctx.hasUI) {
38
+ ctx.ui.notify(`Watching ${triggerFile}`, "info");
39
+ }
40
+ });
41
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Git Checkpoint Hook
3
+ *
4
+ * Creates git stash checkpoints at each turn so /branch can restore code state.
5
+ * When branching, offers to restore code to that point in history.
6
+ */
7
+
8
+ import type { HookAPI } from "@oh-my-pi/pi-coding-agent";
9
+
10
+ export default function (pi: HookAPI) {
11
+ const checkpoints = new Map<string, string>();
12
+ let currentEntryId: string | undefined;
13
+
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 () => {
21
+ // Create a git stash entry before LLM makes changes
22
+ const { stdout } = await pi.exec("git", ["stash", "create"]);
23
+ const ref = stdout.trim();
24
+ if (ref && currentEntryId) {
25
+ checkpoints.set(currentEntryId, ref);
26
+ }
27
+ });
28
+
29
+ pi.on("session_before_branch", async (event, ctx) => {
30
+ const ref = checkpoints.get(event.entryId);
31
+ if (!ref) return;
32
+
33
+ if (!ctx.hasUI) {
34
+ // In non-interactive mode, don't restore automatically
35
+ return;
36
+ }
37
+
38
+ const choice = await ctx.ui.select("Restore code state?", [
39
+ "Yes, restore code to that point",
40
+ "No, keep current code",
41
+ ]);
42
+
43
+ if (choice?.startsWith("Yes")) {
44
+ await pi.exec("git", ["stash", "apply", ref]);
45
+ ctx.ui.notify("Code restored to checkpoint", "info");
46
+ }
47
+ });
48
+
49
+ pi.on("agent_end", async () => {
50
+ // Clear checkpoints after agent completes
51
+ checkpoints.clear();
52
+ });
53
+ }