@mariozechner/pi-coding-agent 0.34.2 → 0.35.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 (251) hide show
  1. package/CHANGELOG.md +204 -0
  2. package/README.md +233 -105
  3. package/dist/cli/args.d.ts +3 -4
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +13 -18
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/config.d.ts +2 -2
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +3 -3
  10. package/dist/config.js.map +1 -1
  11. package/dist/core/agent-session.d.ts +39 -50
  12. package/dist/core/agent-session.d.ts.map +1 -1
  13. package/dist/core/agent-session.js +166 -197
  14. package/dist/core/agent-session.js.map +1 -1
  15. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  16. package/dist/core/compaction/branch-summarization.js +3 -3
  17. package/dist/core/compaction/branch-summarization.js.map +1 -1
  18. package/dist/core/compaction/compaction.d.ts +1 -1
  19. package/dist/core/compaction/compaction.d.ts.map +1 -1
  20. package/dist/core/compaction/compaction.js +6 -5
  21. package/dist/core/compaction/compaction.js.map +1 -1
  22. package/dist/core/event-bus.d.ts +9 -0
  23. package/dist/core/event-bus.d.ts.map +1 -0
  24. package/dist/core/event-bus.js +25 -0
  25. package/dist/core/event-bus.js.map +1 -0
  26. package/dist/core/exec.d.ts +1 -1
  27. package/dist/core/exec.d.ts.map +1 -1
  28. package/dist/core/exec.js +1 -1
  29. package/dist/core/exec.js.map +1 -1
  30. package/dist/core/extensions/index.d.ts +10 -0
  31. package/dist/core/extensions/index.d.ts.map +1 -0
  32. package/dist/core/extensions/index.js +9 -0
  33. package/dist/core/extensions/index.js.map +1 -0
  34. package/dist/core/extensions/loader.d.ts +21 -0
  35. package/dist/core/extensions/loader.d.ts.map +1 -0
  36. package/dist/core/extensions/loader.js +400 -0
  37. package/dist/core/extensions/loader.js.map +1 -0
  38. package/dist/core/extensions/runner.d.ts +88 -0
  39. package/dist/core/extensions/runner.d.ts.map +1 -0
  40. package/dist/core/{hooks → extensions}/runner.js +52 -141
  41. package/dist/core/extensions/runner.js.map +1 -0
  42. package/dist/core/extensions/types.d.ts +461 -0
  43. package/dist/core/extensions/types.d.ts.map +1 -0
  44. package/dist/core/{hooks → extensions}/types.js +7 -4
  45. package/dist/core/extensions/types.js.map +1 -0
  46. package/dist/core/extensions/wrapper.d.ts +25 -0
  47. package/dist/core/extensions/wrapper.d.ts.map +1 -0
  48. package/dist/core/{hooks/tool-wrapper.js → extensions/wrapper.js} +39 -24
  49. package/dist/core/extensions/wrapper.js.map +1 -0
  50. package/dist/core/index.d.ts +2 -2
  51. package/dist/core/index.d.ts.map +1 -1
  52. package/dist/core/index.js +3 -2
  53. package/dist/core/index.js.map +1 -1
  54. package/dist/core/messages.d.ts +7 -7
  55. package/dist/core/messages.d.ts.map +1 -1
  56. package/dist/core/messages.js +4 -4
  57. package/dist/core/messages.js.map +1 -1
  58. package/dist/core/prompt-templates.d.ts +40 -0
  59. package/dist/core/prompt-templates.d.ts.map +1 -0
  60. package/dist/core/{slash-commands.js → prompt-templates.js} +31 -31
  61. package/dist/core/prompt-templates.js.map +1 -0
  62. package/dist/core/sdk.d.ts +29 -52
  63. package/dist/core/sdk.d.ts.map +1 -1
  64. package/dist/core/sdk.js +111 -211
  65. package/dist/core/sdk.js.map +1 -1
  66. package/dist/core/session-manager.d.ts +17 -17
  67. package/dist/core/session-manager.d.ts.map +1 -1
  68. package/dist/core/session-manager.js +25 -10
  69. package/dist/core/session-manager.js.map +1 -1
  70. package/dist/core/settings-manager.d.ts +3 -6
  71. package/dist/core/settings-manager.d.ts.map +1 -1
  72. package/dist/core/settings-manager.js +4 -11
  73. package/dist/core/settings-manager.js.map +1 -1
  74. package/dist/core/system-prompt.d.ts.map +1 -1
  75. package/dist/core/system-prompt.js +4 -2
  76. package/dist/core/system-prompt.js.map +1 -1
  77. package/dist/index.d.ts +4 -5
  78. package/dist/index.d.ts.map +1 -1
  79. package/dist/index.js +5 -6
  80. package/dist/index.js.map +1 -1
  81. package/dist/main.d.ts.map +1 -1
  82. package/dist/main.js +36 -33
  83. package/dist/main.js.map +1 -1
  84. package/dist/migrations.d.ts +7 -2
  85. package/dist/migrations.d.ts.map +1 -1
  86. package/dist/migrations.js +93 -4
  87. package/dist/migrations.js.map +1 -1
  88. package/dist/modes/interactive/components/bordered-loader.d.ts +1 -1
  89. package/dist/modes/interactive/components/bordered-loader.d.ts.map +1 -1
  90. package/dist/modes/interactive/components/bordered-loader.js +1 -1
  91. package/dist/modes/interactive/components/bordered-loader.js.map +1 -1
  92. package/dist/modes/interactive/components/branch-summary-message.d.ts +1 -1
  93. package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
  94. package/dist/modes/interactive/components/branch-summary-message.js +1 -1
  95. package/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
  96. package/dist/modes/interactive/components/compaction-summary-message.d.ts +1 -1
  97. package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
  98. package/dist/modes/interactive/components/compaction-summary-message.js +1 -1
  99. package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
  100. package/dist/modes/interactive/components/custom-editor.d.ts +2 -2
  101. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  102. package/dist/modes/interactive/components/custom-editor.js +4 -4
  103. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  104. package/dist/modes/interactive/components/custom-message.d.ts +18 -0
  105. package/dist/modes/interactive/components/custom-message.d.ts.map +1 -0
  106. package/dist/modes/interactive/components/{hook-message.js → custom-message.js} +3 -3
  107. package/dist/modes/interactive/components/custom-message.js.map +1 -0
  108. package/dist/modes/interactive/components/dynamic-border.d.ts +2 -2
  109. package/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
  110. package/dist/modes/interactive/components/dynamic-border.js +2 -2
  111. package/dist/modes/interactive/components/dynamic-border.js.map +1 -1
  112. package/dist/modes/interactive/components/{hook-editor.d.ts → extension-editor.d.ts} +3 -3
  113. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -0
  114. package/dist/modes/interactive/components/{hook-editor.js → extension-editor.js} +4 -4
  115. package/dist/modes/interactive/components/extension-editor.js.map +1 -0
  116. package/dist/modes/interactive/components/{hook-input.d.ts → extension-input.d.ts} +3 -3
  117. package/dist/modes/interactive/components/extension-input.d.ts.map +1 -0
  118. package/dist/modes/interactive/components/{hook-input.js → extension-input.js} +3 -3
  119. package/dist/modes/interactive/components/extension-input.js.map +1 -0
  120. package/dist/modes/interactive/components/{hook-selector.d.ts → extension-selector.d.ts} +3 -3
  121. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -0
  122. package/dist/modes/interactive/components/{hook-selector.js → extension-selector.js} +3 -3
  123. package/dist/modes/interactive/components/extension-selector.js.map +1 -0
  124. package/dist/modes/interactive/components/footer.d.ts +3 -3
  125. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  126. package/dist/modes/interactive/components/footer.js +8 -8
  127. package/dist/modes/interactive/components/footer.js.map +1 -1
  128. package/dist/modes/interactive/components/tool-execution.d.ts +3 -3
  129. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  130. package/dist/modes/interactive/components/tool-execution.js +9 -9
  131. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  132. package/dist/modes/interactive/interactive-mode.d.ts +37 -44
  133. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  134. package/dist/modes/interactive/interactive-mode.js +143 -189
  135. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  136. package/dist/modes/print-mode.d.ts.map +1 -1
  137. package/dist/modes/print-mode.js +10 -33
  138. package/dist/modes/print-mode.js.map +1 -1
  139. package/dist/modes/rpc/rpc-client.d.ts +3 -3
  140. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  141. package/dist/modes/rpc/rpc-client.js +3 -3
  142. package/dist/modes/rpc/rpc-client.js.map +1 -1
  143. package/dist/modes/rpc/rpc-mode.d.ts +2 -2
  144. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  145. package/dist/modes/rpc/rpc-mode.js +33 -57
  146. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  147. package/dist/modes/rpc/rpc-types.d.ts +16 -16
  148. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  149. package/dist/modes/rpc/rpc-types.js.map +1 -1
  150. package/docs/extensions.md +1053 -0
  151. package/docs/rpc.md +4 -4
  152. package/docs/sdk.md +62 -93
  153. package/docs/session.md +22 -19
  154. package/docs/skills.md +1 -1
  155. package/docs/tui.md +1 -1
  156. package/examples/README.md +9 -15
  157. package/examples/extensions/README.md +141 -0
  158. package/examples/{hooks → extensions}/auto-commit-on-exit.ts +3 -3
  159. package/examples/extensions/chalk-logger.ts +26 -0
  160. package/examples/{hooks → extensions}/confirm-destructive.ts +3 -3
  161. package/examples/{hooks → extensions}/custom-compaction.ts +6 -6
  162. package/examples/{hooks → extensions}/dirty-repo-guard.ts +8 -4
  163. package/examples/{hooks → extensions}/file-trigger.ts +3 -3
  164. package/examples/{hooks → extensions}/git-checkpoint.ts +3 -3
  165. package/examples/{hooks → extensions}/handoff.ts +3 -3
  166. package/examples/extensions/hello.ts +25 -0
  167. package/examples/{hooks → extensions}/permission-gate.ts +3 -3
  168. package/examples/{hooks → extensions}/pirate.ts +5 -5
  169. package/examples/{hooks → extensions}/plan-mode.ts +6 -6
  170. package/examples/{hooks → extensions}/protected-paths.ts +3 -3
  171. package/examples/{hooks → extensions}/qna.ts +3 -3
  172. package/examples/{custom-tools/question/index.ts → extensions/question.ts} +13 -17
  173. package/examples/{hooks → extensions}/snake.ts +3 -3
  174. package/examples/{hooks → extensions}/status-line.ts +3 -3
  175. package/examples/{custom-tools → extensions}/subagent/README.md +15 -15
  176. package/examples/{custom-tools → extensions}/subagent/index.ts +22 -43
  177. package/examples/{custom-tools/todo/index.ts → extensions/todo.ts} +122 -39
  178. package/examples/{hooks → extensions}/tools.ts +5 -5
  179. package/examples/extensions/with-deps/index.ts +40 -0
  180. package/examples/extensions/with-deps/package-lock.json +31 -0
  181. package/examples/extensions/with-deps/package.json +16 -0
  182. package/examples/sdk/01-minimal.ts +1 -1
  183. package/examples/sdk/05-tools.ts +7 -41
  184. package/examples/sdk/06-extensions.ts +81 -0
  185. package/examples/sdk/08-prompt-templates.ts +42 -0
  186. package/examples/sdk/12-full-control.ts +10 -29
  187. package/examples/sdk/README.md +5 -5
  188. package/package.json +4 -4
  189. package/dist/core/custom-tools/index.d.ts +0 -7
  190. package/dist/core/custom-tools/index.d.ts.map +0 -1
  191. package/dist/core/custom-tools/index.js +0 -6
  192. package/dist/core/custom-tools/index.js.map +0 -1
  193. package/dist/core/custom-tools/loader.d.ts +0 -30
  194. package/dist/core/custom-tools/loader.d.ts.map +0 -1
  195. package/dist/core/custom-tools/loader.js +0 -276
  196. package/dist/core/custom-tools/loader.js.map +0 -1
  197. package/dist/core/custom-tools/types.d.ts +0 -144
  198. package/dist/core/custom-tools/types.d.ts.map +0 -1
  199. package/dist/core/custom-tools/types.js +0 -8
  200. package/dist/core/custom-tools/types.js.map +0 -1
  201. package/dist/core/custom-tools/wrapper.d.ts +0 -15
  202. package/dist/core/custom-tools/wrapper.d.ts.map +0 -1
  203. package/dist/core/custom-tools/wrapper.js +0 -23
  204. package/dist/core/custom-tools/wrapper.js.map +0 -1
  205. package/dist/core/hooks/index.d.ts +0 -6
  206. package/dist/core/hooks/index.d.ts.map +0 -1
  207. package/dist/core/hooks/index.js +0 -6
  208. package/dist/core/hooks/index.js.map +0 -1
  209. package/dist/core/hooks/loader.d.ts +0 -146
  210. package/dist/core/hooks/loader.d.ts.map +0 -1
  211. package/dist/core/hooks/loader.js +0 -275
  212. package/dist/core/hooks/loader.js.map +0 -1
  213. package/dist/core/hooks/runner.d.ts +0 -173
  214. package/dist/core/hooks/runner.d.ts.map +0 -1
  215. package/dist/core/hooks/runner.js.map +0 -1
  216. package/dist/core/hooks/tool-wrapper.d.ts +0 -17
  217. package/dist/core/hooks/tool-wrapper.d.ts.map +0 -1
  218. package/dist/core/hooks/tool-wrapper.js.map +0 -1
  219. package/dist/core/hooks/types.d.ts +0 -767
  220. package/dist/core/hooks/types.d.ts.map +0 -1
  221. package/dist/core/hooks/types.js.map +0 -1
  222. package/dist/core/slash-commands.d.ts +0 -40
  223. package/dist/core/slash-commands.d.ts.map +0 -1
  224. package/dist/core/slash-commands.js.map +0 -1
  225. package/dist/modes/interactive/components/hook-editor.d.ts.map +0 -1
  226. package/dist/modes/interactive/components/hook-editor.js.map +0 -1
  227. package/dist/modes/interactive/components/hook-input.d.ts.map +0 -1
  228. package/dist/modes/interactive/components/hook-input.js.map +0 -1
  229. package/dist/modes/interactive/components/hook-message.d.ts +0 -18
  230. package/dist/modes/interactive/components/hook-message.d.ts.map +0 -1
  231. package/dist/modes/interactive/components/hook-message.js.map +0 -1
  232. package/dist/modes/interactive/components/hook-selector.d.ts.map +0 -1
  233. package/dist/modes/interactive/components/hook-selector.js.map +0 -1
  234. package/docs/custom-tools.md +0 -514
  235. package/docs/extension-loading.md +0 -1004
  236. package/docs/hooks.md +0 -979
  237. package/docs/session-tree-plan.md +0 -441
  238. package/examples/custom-tools/README.md +0 -114
  239. package/examples/custom-tools/hello/index.ts +0 -21
  240. package/examples/hooks/README.md +0 -60
  241. package/examples/hooks/todo/index.ts +0 -134
  242. package/examples/sdk/06-hooks.ts +0 -61
  243. package/examples/sdk/08-slash-commands.ts +0 -42
  244. /package/examples/{custom-tools → extensions}/subagent/agents/planner.md +0 -0
  245. /package/examples/{custom-tools → extensions}/subagent/agents/reviewer.md +0 -0
  246. /package/examples/{custom-tools → extensions}/subagent/agents/scout.md +0 -0
  247. /package/examples/{custom-tools → extensions}/subagent/agents/worker.md +0 -0
  248. /package/examples/{custom-tools → extensions}/subagent/agents.ts +0 -0
  249. /package/examples/{custom-tools/subagent/commands → extensions/subagent/prompts}/implement-and-review.md +0 -0
  250. /package/examples/{custom-tools/subagent/commands → extensions/subagent/prompts}/implement.md +0 -0
  251. /package/examples/{custom-tools/subagent/commands → extensions/subagent/prompts}/scout-and-plan.md +0 -0
package/docs/rpc.md CHANGED
@@ -52,9 +52,9 @@ With images:
52
52
 
53
53
  If the agent is streaming and no `streamingBehavior` is specified, the command returns an error.
54
54
 
55
- **Hook commands**: If the message is a hook command (e.g., `/mycommand`), it executes immediately even during streaming. Hook commands manage their own LLM interaction via `pi.sendMessage()`.
55
+ **Extension commands**: If the message is a hook command (e.g., `/mycommand`), it executes immediately even during streaming. Extension commands manage their own LLM interaction via `pi.sendMessage()`.
56
56
 
57
- **Slash commands**: File-based slash commands (from `.md` files) are expanded before sending/queueing.
57
+ **Prompt templates**: File-based prompt templates (from `.md` files) are expanded before sending/queueing.
58
58
 
59
59
  Response:
60
60
  ```json
@@ -65,7 +65,7 @@ The `images` field is optional. Each image uses `ImageContent` format with base6
65
65
 
66
66
  #### steer
67
67
 
68
- Queue a steering message to interrupt the agent mid-run. Delivered after current tool execution, remaining tools are skipped. File-based slash commands are expanded. Hook commands are not allowed (use `prompt` instead).
68
+ Queue a steering message to interrupt the agent mid-run. Delivered after current tool execution, remaining tools are skipped. File-based prompt templates are expanded. Extension commands are not allowed (use `prompt` instead).
69
69
 
70
70
  ```json
71
71
  {"type": "steer", "message": "Stop and do this instead"}
@@ -80,7 +80,7 @@ See [set_steering_mode](#set_steering_mode) for controlling how steering message
80
80
 
81
81
  #### follow_up
82
82
 
83
- Queue a follow-up message to be processed after the agent finishes. Delivered only when agent has no more tool calls or steering messages. File-based slash commands are expanded. Hook commands are not allowed (use `prompt` instead).
83
+ Queue a follow-up message to be processed after the agent finishes. Delivered only when agent has no more tool calls or steering messages. File-based prompt templates are expanded. Extension commands are not allowed (use `prompt` instead).
84
84
 
85
85
  ```json
86
86
  {"type": "follow_up", "message": "After you're done, also do this"}
package/docs/sdk.md CHANGED
@@ -129,7 +129,7 @@ interface AgentSession {
129
129
 
130
130
  ### Prompting and Message Queueing
131
131
 
132
- The `prompt()` method handles slash commands, hook commands, and message sending:
132
+ The `prompt()` method handles prompt templates, extension commands, and message sending:
133
133
 
134
134
  ```typescript
135
135
  // Basic prompt (when not streaming)
@@ -146,8 +146,8 @@ await session.prompt("After you're done, also check X", { streamingBehavior: "fo
146
146
  ```
147
147
 
148
148
  **Behavior:**
149
- - **Hook commands** (e.g., `/mycommand`): Execute immediately, even during streaming. They manage their own LLM interaction via `pi.sendMessage()`.
150
- - **File-based slash commands** (from `.md` files): Expanded to their content before sending/queueing.
149
+ - **Extension commands** (e.g., `/mycommand`): Execute immediately, even during streaming. They manage their own LLM interaction via `pi.sendMessage()`.
150
+ - **File-based prompt templates** (from `.md` files): Expanded to their content before sending/queueing.
151
151
  - **During streaming without `streamingBehavior`**: Throws an error. Use `steer()` or `followUp()` directly, or specify the option.
152
152
 
153
153
  For explicit queueing during streaming:
@@ -160,7 +160,7 @@ await session.steer("New instruction");
160
160
  await session.followUp("After you're done, also do this");
161
161
  ```
162
162
 
163
- Both `steer()` and `followUp()` expand file-based slash commands but error on hook commands (hook commands cannot be queued).
163
+ Both `steer()` and `followUp()` expand file-based prompt templates but error on extension commands (extension commands cannot be queued).
164
164
 
165
165
  ### Agent and AgentState
166
166
 
@@ -260,18 +260,16 @@ const { session } = await createAgentSession({
260
260
  ```
261
261
 
262
262
  `cwd` is used for:
263
- - Project hooks (`.pi/hooks/`)
264
- - Project tools (`.pi/tools/`)
263
+ - Project extensions (`.pi/extensions/`)
265
264
  - Project skills (`.pi/skills/`)
266
- - Project commands (`.pi/commands/`)
265
+ - Project prompts (`.pi/prompts/`)
267
266
  - Context files (`AGENTS.md` walking up from cwd)
268
267
  - Session directory naming
269
268
 
270
269
  `agentDir` is used for:
271
- - Global hooks (`hooks/`)
272
- - Global tools (`tools/`)
270
+ - Global extensions (`extensions/`)
273
271
  - Global skills (`skills/`)
274
- - Global commands (`commands/`)
272
+ - Global prompts (`prompts/`)
275
273
  - Global context file (`AGENTS.md`)
276
274
  - Settings (`settings.json`)
277
275
  - Custom models (`models.json`)
@@ -442,113 +440,79 @@ const { session } = await createAgentSession({
442
440
 
443
441
  ```typescript
444
442
  import { Type } from "@sinclair/typebox";
445
- import { createAgentSession, discoverCustomTools, type CustomTool } from "@mariozechner/pi-coding-agent";
443
+ import { createAgentSession, type ToolDefinition } from "@mariozechner/pi-coding-agent";
446
444
 
447
445
  // Inline custom tool
448
- const myTool: CustomTool = {
446
+ const myTool: ToolDefinition = {
449
447
  name: "my_tool",
450
448
  label: "My Tool",
451
449
  description: "Does something useful",
452
450
  parameters: Type.Object({
453
451
  input: Type.String({ description: "Input value" }),
454
452
  }),
455
- execute: async (toolCallId, params) => ({
453
+ execute: async (toolCallId, params, onUpdate, ctx, signal) => ({
456
454
  content: [{ type: "text", text: `Result: ${params.input}` }],
457
455
  details: {},
458
456
  }),
459
457
  };
460
458
 
461
- // Replace discovery with inline tools
459
+ // Pass custom tools directly
462
460
  const { session } = await createAgentSession({
463
- customTools: [{ tool: myTool }],
464
- });
465
-
466
- // Merge with discovered tools
467
- const discovered = await discoverCustomTools();
468
- const { session } = await createAgentSession({
469
- customTools: [...discovered, { tool: myTool }],
470
- });
471
-
472
- // Add paths without replacing discovery
473
- const { session } = await createAgentSession({
474
- additionalCustomToolPaths: ["/extra/tools"],
461
+ customTools: [myTool],
475
462
  });
476
463
  ```
477
464
 
465
+ Custom tools passed via `customTools` are combined with extension-registered tools. Extensions discovered from `~/.pi/agent/extensions/` and `.pi/extensions/` can also register tools via `pi.registerTool()`.
466
+
478
467
  > See [examples/sdk/05-tools.ts](../examples/sdk/05-tools.ts)
479
468
 
480
- ### Hooks
469
+ ### Extensions
470
+
471
+ Extensions are discovered from `~/.pi/agent/extensions/` and `.pi/extensions/`. You can also pass inline extensions or additional paths:
481
472
 
482
473
  ```typescript
483
- import { createAgentSession, discoverHooks, type HookFactory } from "@mariozechner/pi-coding-agent";
474
+ import { createAgentSession, type ExtensionFactory } from "@mariozechner/pi-coding-agent";
484
475
 
485
- // Inline hook
486
- const loggingHook: HookFactory = (api) => {
487
- // Log tool calls
488
- api.on("tool_call", async (event) => {
476
+ // Inline extension
477
+ const myExtension: ExtensionFactory = (pi) => {
478
+ pi.on("tool_call", async (event, ctx) => {
489
479
  console.log(`Tool: ${event.toolName}`);
490
- return undefined; // Don't block
491
480
  });
492
-
493
- // Block dangerous commands
494
- api.on("tool_call", async (event) => {
495
- if (event.toolName === "bash" && event.input.command?.includes("rm -rf")) {
496
- return { block: true, reason: "Dangerous command" };
497
- }
498
- return undefined;
499
- });
500
-
501
- // Register custom slash command
502
- api.registerCommand("stats", {
503
- description: "Show session stats",
504
- handler: async (ctx) => {
505
- const entries = ctx.sessionManager.getEntries();
506
- ctx.ui.notify(`${entries.length} entries`, "info");
481
+
482
+ pi.registerCommand("hello", {
483
+ description: "Say hello",
484
+ handler: async (args, ctx) => {
485
+ ctx.ui.notify("Hello!", "info");
507
486
  },
508
487
  });
509
-
510
- // Inject messages
511
- api.sendMessage({
512
- customType: "my-hook",
513
- content: "Hook initialized",
514
- display: false, // Hidden from TUI
515
- }, false); // Don't trigger agent turn
516
-
517
- // Persist hook state
518
- api.appendEntry("my-hook", { initialized: true });
519
488
  };
520
489
 
521
- // Replace discovery
490
+ // Pass inline extensions (merged with discovery)
522
491
  const { session } = await createAgentSession({
523
- hooks: [{ factory: loggingHook }],
492
+ extensions: [myExtension],
524
493
  });
525
494
 
526
- // Disable all hooks
495
+ // Or add paths to load (merged with discovery)
527
496
  const { session } = await createAgentSession({
528
- hooks: [],
497
+ additionalExtensionPaths: ["/path/to/my-extension.ts"],
529
498
  });
499
+ ```
530
500
 
531
- // Merge with discovered
532
- const discovered = await discoverHooks();
533
- const { session } = await createAgentSession({
534
- hooks: [...discovered, { factory: loggingHook }],
535
- });
501
+ Extensions can register tools, subscribe to events, add commands, and more. See [extensions.md](extensions.md) for the full API.
536
502
 
537
- // Add paths without replacing
538
- const { session } = await createAgentSession({
539
- additionalHookPaths: ["/extra/hooks"],
540
- });
541
- ```
503
+ **Event Bus:** Extensions can communicate via `pi.events`. Pass a shared `eventBus` to `createAgentSession()` if you need to emit/listen from outside:
542
504
 
543
- Hook API methods:
544
- - `api.on(event, handler)` - Subscribe to events
545
- - `api.sendMessage(message, triggerTurn?)` - Inject message (creates `CustomMessageEntry`)
546
- - `api.appendEntry(customType, data?)` - Persist hook state (not in LLM context)
547
- - `api.registerCommand(name, options)` - Register custom slash command
548
- - `api.registerMessageRenderer(customType, renderer)` - Custom TUI rendering
549
- - `api.exec(command, args, options?)` - Execute shell commands
505
+ ```typescript
506
+ import { createAgentSession, createEventBus } from "@mariozechner/pi-coding-agent";
550
507
 
551
- > See [examples/sdk/06-hooks.ts](../examples/sdk/06-hooks.ts) and [docs/hooks.md](hooks.md)
508
+ const eventBus = createEventBus();
509
+ const { session } = await createAgentSession({ eventBus });
510
+
511
+ // Listen for events from extensions
512
+ eventBus.on("my-extension:status", (data) => console.log(data));
513
+ ```
514
+
515
+ > See [examples/sdk/06-extensions.ts](../examples/sdk/06-extensions.ts) and [docs/extensions.md](extensions.md)
552
516
 
553
517
  ### Skills
554
518
 
@@ -616,11 +580,11 @@ const { session } = await createAgentSession({
616
580
  ### Slash Commands
617
581
 
618
582
  ```typescript
619
- import { createAgentSession, discoverSlashCommands, type FileSlashCommand } from "@mariozechner/pi-coding-agent";
583
+ import { createAgentSession, discoverPromptTemplates, type PromptTemplate } from "@mariozechner/pi-coding-agent";
620
584
 
621
- const discovered = discoverSlashCommands();
585
+ const discovered = discoverPromptTemplates();
622
586
 
623
- const customCommand: FileSlashCommand = {
587
+ const customCommand: PromptTemplate = {
624
588
  name: "deploy",
625
589
  description: "Deploy the application",
626
590
  source: "(custom)",
@@ -628,11 +592,11 @@ const customCommand: FileSlashCommand = {
628
592
  };
629
593
 
630
594
  const { session } = await createAgentSession({
631
- slashCommands: [...discovered, customCommand],
595
+ promptTemplates: [...discovered, customCommand],
632
596
  });
633
597
  ```
634
598
 
635
- > See [examples/sdk/08-slash-commands.ts](../examples/sdk/08-slash-commands.ts)
599
+ > See [examples/sdk/08-prompt-templates.ts](../examples/sdk/08-prompt-templates.ts)
636
600
 
637
601
  ### Session Management
638
602
 
@@ -761,7 +725,7 @@ import {
761
725
  discoverHooks,
762
726
  discoverCustomTools,
763
727
  discoverContextFiles,
764
- discoverSlashCommands,
728
+ discoverPromptTemplates,
765
729
  loadSettings,
766
730
  buildSystemPrompt,
767
731
  } from "@mariozechner/pi-coding-agent";
@@ -778,16 +742,18 @@ const builtIn = getModel("anthropic", "claude-opus-4-5"); // Built-in only
778
742
  const skills = discoverSkills(cwd, agentDir, skillsSettings);
779
743
 
780
744
  // Hooks (async - loads TypeScript)
781
- const hooks = await discoverHooks(cwd, agentDir);
745
+ // Pass eventBus to share pi.events across hooks/tools
746
+ const eventBus = createEventBus();
747
+ const hooks = await discoverHooks(eventBus, cwd, agentDir);
782
748
 
783
749
  // Custom tools (async - loads TypeScript)
784
- const tools = await discoverCustomTools(cwd, agentDir);
750
+ const tools = await discoverCustomTools(eventBus, cwd, agentDir);
785
751
 
786
752
  // Context files
787
753
  const contextFiles = discoverContextFiles(cwd, agentDir);
788
754
 
789
- // Slash commands
790
- const commands = discoverSlashCommands(cwd, agentDir);
755
+ // Prompt templates
756
+ const commands = discoverPromptTemplates(cwd, agentDir);
791
757
 
792
758
  // Settings (global + project merged)
793
759
  const settings = loadSettings(cwd, agentDir);
@@ -894,7 +860,7 @@ const { session } = await createAgentSession({
894
860
  hooks: [{ factory: auditHook }],
895
861
  skills: [],
896
862
  contextFiles: [],
897
- slashCommands: [],
863
+ promptTemplates: [],
898
864
 
899
865
  sessionManager: SessionManager.inMemory(),
900
866
  settingsManager,
@@ -949,7 +915,10 @@ discoverSkills
949
915
  discoverHooks
950
916
  discoverCustomTools
951
917
  discoverContextFiles
952
- discoverSlashCommands
918
+ discoverPromptTemplates
919
+
920
+ // Event Bus (for shared hook/tool communication)
921
+ createEventBus
953
922
 
954
923
  // Helpers
955
924
  loadSettings
@@ -977,7 +946,7 @@ type CreateAgentSessionResult
977
946
  type CustomTool
978
947
  type HookFactory
979
948
  type Skill
980
- type FileSlashCommand
949
+ type PromptTemplate
981
950
  type Settings
982
951
  type SkillsSettings
983
952
  type Tool
package/docs/session.md CHANGED
@@ -16,14 +16,15 @@ Sessions have a version field in the header:
16
16
 
17
17
  - **Version 1**: Linear entry sequence (legacy, auto-migrated on load)
18
18
  - **Version 2**: Tree structure with `id`/`parentId` linking
19
+ - **Version 3**: Renamed `hookMessage` role to `custom` (extensions unification)
19
20
 
20
- Existing v1 sessions are automatically migrated to v2 when loaded.
21
+ Existing sessions are automatically migrated to the current version (v3) when loaded.
21
22
 
22
23
  ## Type Definitions
23
24
 
24
25
  - [`src/core/session-manager.ts`](../src/core/session-manager.ts) - Session entry types
25
- - [`packages/agent/src/types.ts`](../../agent/src/types.ts) - `AgentMessage`, `Attachment`, `ThinkingLevel`
26
- - [`packages/ai/src/types.ts`](../../ai/src/types.ts) - `UserMessage`, `AssistantMessage`, `ToolResultMessage`, `Usage`, `ToolCall`
26
+ - [`packages/agent-core/src/types.ts`](../../agent-core/src/types.ts) - `AgentMessage`
27
+ - [`packages/ai/src/types.ts`](../../ai/src/types.ts) - `UserMessage`, `AssistantMessage`, `ToolResultMessage`, `Usage`, `ToolCall`, `ImageContent`, `TextContent`
27
28
 
28
29
  ## Entry Base
29
30
 
@@ -45,13 +46,13 @@ interface SessionEntryBase {
45
46
  First line of the file. Metadata only, not part of the tree (no `id`/`parentId`).
46
47
 
47
48
  ```json
48
- {"type":"session","version":2,"id":"uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path/to/project"}
49
+ {"type":"session","version":3,"id":"uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path/to/project"}
49
50
  ```
50
51
 
51
52
  For sessions with a parent (created via `/branch` or `newSession({ parentSession })`):
52
53
 
53
54
  ```json
54
- {"type":"session","version":2,"id":"uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path/to/project","parentSession":"/path/to/original/session.jsonl"}
55
+ {"type":"session","version":3,"id":"uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path/to/project","parentSession":"/path/to/original/session.jsonl"}
55
56
  ```
56
57
 
57
58
  ### SessionMessageEntry
@@ -89,8 +90,8 @@ Created when context is compacted. Stores a summary of earlier messages.
89
90
  ```
90
91
 
91
92
  Optional fields:
92
- - `details`: Compaction-implementation specific data (e.g., file operations for default implementation, or custom data for custom hook implementations)
93
- - `fromHook`: `true` if generated by a hook, `false`/`undefined` if pi-generated
93
+ - `details`: Compaction-implementation specific data (e.g., file operations for default implementation, or custom data for extension implementations)
94
+ - `fromHook`: `true` if generated by an extension, `false`/`undefined` if pi-generated
94
95
 
95
96
  ### BranchSummaryEntry
96
97
 
@@ -102,30 +103,30 @@ Created when switching branches via `/tree` with an LLM generated summary of the
102
103
 
103
104
  Optional fields:
104
105
  - `details`: File tracking data (`{ readFiles: string[], modifiedFiles: string[] }`) for default implementation, arbitrary for custom implementation
105
- - `fromHook`: `true` if generated by a hook
106
+ - `fromHook`: `true` if generated by an extension
106
107
 
107
108
  ### CustomEntry
108
109
 
109
- Hook state persistence. Does NOT participate in LLM context.
110
+ Extension state persistence. Does NOT participate in LLM context.
110
111
 
111
112
  ```json
112
- {"type":"custom","id":"h8i9j0k1","parentId":"g7h8i9j0","timestamp":"2024-12-03T14:20:00.000Z","customType":"my-hook","data":{"count":42}}
113
+ {"type":"custom","id":"h8i9j0k1","parentId":"g7h8i9j0","timestamp":"2024-12-03T14:20:00.000Z","customType":"my-extension","data":{"count":42}}
113
114
  ```
114
115
 
115
- Use `customType` to identify your hook's entries on reload.
116
+ Use `customType` to identify your extension's entries on reload.
116
117
 
117
118
  ### CustomMessageEntry
118
119
 
119
- Hook-injected messages that DO participate in LLM context.
120
+ Extension-injected messages that DO participate in LLM context.
120
121
 
121
122
  ```json
122
- {"type":"custom_message","id":"i9j0k1l2","parentId":"h8i9j0k1","timestamp":"2024-12-03T14:25:00.000Z","customType":"my-hook","content":"Injected context...","display":true}
123
+ {"type":"custom_message","id":"i9j0k1l2","parentId":"h8i9j0k1","timestamp":"2024-12-03T14:25:00.000Z","customType":"my-extension","content":"Injected context...","display":true}
123
124
  ```
124
125
 
125
126
  Fields:
126
127
  - `content`: String or `(TextContent | ImageContent)[]` (same as UserMessage)
127
- - `display`: `true` = show in TUI with purple styling, `false` = hidden
128
- - `details`: Optional hook-specific metadata (not sent to LLM)
128
+ - `display`: `true` = show in TUI with distinct styling, `false` = hidden
129
+ - `details`: Optional extension-specific metadata (not sent to LLM)
129
130
 
130
131
  ### LabelEntry
131
132
 
@@ -190,7 +191,7 @@ for (const line of lines) {
190
191
  console.log(`[${entry.id}] Custom (${entry.customType}): ${JSON.stringify(entry.data)}`);
191
192
  break;
192
193
  case "custom_message":
193
- console.log(`[${entry.id}] Hook message (${entry.customType}): ${entry.content}`);
194
+ console.log(`[${entry.id}] Extension message (${entry.customType}): ${entry.content}`);
194
195
  break;
195
196
  case "label":
196
197
  console.log(`[${entry.id}] Label "${entry.label}" on ${entry.targetId}`);
@@ -220,18 +221,20 @@ Key methods for working with sessions programmatically:
220
221
  - `appendThinkingLevelChange(level)` - Record thinking change
221
222
  - `appendModelChange(provider, modelId)` - Record model change
222
223
  - `appendCompaction(summary, firstKeptEntryId, tokensBefore, details?, fromHook?)` - Add compaction
223
- - `appendCustomEntry(customType, data?)` - Hook state (not in context)
224
- - `appendCustomMessageEntry(customType, content, display, details?)` - Hook message (in context)
224
+ - `appendCustomEntry(customType, data?)` - Extension state (not in context)
225
+ - `appendCustomMessageEntry(customType, content, display, details?)` - Extension message (in context)
225
226
  - `appendLabelChange(targetId, label)` - Set/clear label
226
227
 
227
228
  ### Tree Navigation
228
229
  - `getLeafId()` - Current position
230
+ - `getLeafEntry()` - Get current leaf entry
229
231
  - `getEntry(id)` - Get entry by ID
230
- - `getPath(fromId?)` - Walk from entry to root
232
+ - `getBranch(fromId?)` - Walk from entry to root
231
233
  - `getTree()` - Get full tree structure
232
234
  - `getChildren(parentId)` - Get direct children
233
235
  - `getLabel(id)` - Get label for entry
234
236
  - `branch(entryId)` - Move leaf to earlier entry
237
+ - `resetLeaf()` - Reset leaf to null (before any entries)
235
238
  - `branchWithSummary(entryId, summary, details?, fromHook?)` - Branch with context summary
236
239
 
237
240
  ### Context
package/docs/skills.md CHANGED
@@ -37,7 +37,7 @@ Skills are loaded when:
37
37
 
38
38
  **Not a good fit for skills:**
39
39
  - "Always use TypeScript strict mode" → put in AGENTS.md
40
- - "Review my code" → make a slash command
40
+ - "Review my code" → make a prompt template
41
41
  - Need user confirmation dialogs or custom TUI rendering → make a custom tool
42
42
 
43
43
  ## Skill Structure
package/docs/tui.md CHANGED
@@ -343,4 +343,4 @@ Call `invalidate()` when state changes, then `handle.requestRender()` to trigger
343
343
  ## Examples
344
344
 
345
345
  - **Snake game**: [examples/hooks/snake.ts](../examples/hooks/snake.ts) - Full game with keyboard input, game loop, state persistence
346
- - **Custom tool rendering**: [examples/custom-tools/todo/](../examples/custom-tools/todo/) - Custom `renderCall` and `renderResult`
346
+ - **Custom tool rendering**: [examples/extensions/todo.ts](../examples/extensions/todo.ts) - Custom `renderCall` and `renderResult`
@@ -1,27 +1,21 @@
1
1
  # Examples
2
2
 
3
- Example code for pi-coding-agent SDK, hooks, and custom tools.
3
+ Example code for pi-coding-agent SDK and extensions.
4
4
 
5
5
  ## Directories
6
6
 
7
7
  ### [sdk/](sdk/)
8
- Programmatic usage via `createAgentSession()`. Shows how to customize models, prompts, tools, hooks, and session management.
8
+ Programmatic usage via `createAgentSession()`. Shows how to customize models, prompts, tools, extensions, and session management.
9
9
 
10
- ### [hooks/](hooks/)
11
- Example hooks for intercepting tool calls, adding safety gates, and integrating with external systems.
12
-
13
- ### [custom-tools/](custom-tools/)
14
- Example custom tools that extend the agent's capabilities.
15
-
16
- ## Tool + Hook Combinations
17
-
18
- Some examples are designed to work together:
19
-
20
- - **todo/** - The [custom tool](custom-tools/todo/) lets the LLM manage a todo list, while the [hook](hooks/todo/) adds a `/todos` command for users to view todos at any time.
10
+ ### [extensions/](extensions/)
11
+ Example extensions demonstrating:
12
+ - Lifecycle event handlers (tool interception, safety gates, context modifications)
13
+ - Custom tools (todo lists, subagents)
14
+ - Commands and keyboard shortcuts
15
+ - External integrations (git, file watchers)
21
16
 
22
17
  ## Documentation
23
18
 
24
19
  - [SDK Reference](sdk/README.md)
25
- - [Hooks Documentation](../docs/hooks.md)
26
- - [Custom Tools Documentation](../docs/custom-tools.md)
20
+ - [Extensions Documentation](../docs/extensions.md)
27
21
  - [Skills Documentation](../docs/skills.md)
@@ -0,0 +1,141 @@
1
+ # Extension Examples
2
+
3
+ Example extensions for pi-coding-agent.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ # Load an extension with --extension flag
9
+ pi --extension examples/extensions/permission-gate.ts
10
+
11
+ # Or copy to extensions directory for auto-discovery
12
+ cp permission-gate.ts ~/.pi/agent/extensions/
13
+ ```
14
+
15
+ ## Examples
16
+
17
+ ### Lifecycle & Safety
18
+
19
+ | Extension | Description |
20
+ |-----------|-------------|
21
+ | `permission-gate.ts` | Prompts for confirmation before dangerous bash commands (rm -rf, sudo, etc.) |
22
+ | `protected-paths.ts` | Blocks writes to protected paths (.env, .git/, node_modules/) |
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
+
26
+ ### Custom Tools
27
+
28
+ | Extension | Description |
29
+ |-----------|-------------|
30
+ | `todo.ts` | Todo list tool + `/todos` command with custom rendering and state persistence |
31
+ | `hello.ts` | Minimal custom tool example |
32
+ | `question.ts` | Demonstrates `ctx.ui.select()` for asking the user questions |
33
+ | `subagent/` | Delegate tasks to specialized subagents with isolated context windows |
34
+
35
+ ### Commands & UI
36
+
37
+ | Extension | Description |
38
+ |-----------|-------------|
39
+ | `plan-mode.ts` | Claude Code-style plan mode for read-only exploration with `/plan` command |
40
+ | `tools.ts` | Interactive `/tools` command to enable/disable tools with session persistence |
41
+ | `handoff.ts` | Transfer context to a new focused session via `/handoff <goal>` |
42
+ | `qna.ts` | Extracts questions from last response into editor via `ctx.ui.setEditorText()` |
43
+ | `status-line.ts` | Shows turn progress in footer via `ctx.ui.setStatus()` with themed colors |
44
+ | `snake.ts` | Snake game with custom UI, keyboard handling, and session persistence |
45
+
46
+ ### Git Integration
47
+
48
+ | Extension | Description |
49
+ |-----------|-------------|
50
+ | `git-checkpoint.ts` | Creates git stash checkpoints at each turn for code restoration on branch |
51
+ | `auto-commit-on-exit.ts` | Auto-commits on exit using last assistant message for commit message |
52
+
53
+ ### System Prompt & Compaction
54
+
55
+ | Extension | Description |
56
+ |-----------|-------------|
57
+ | `pirate.ts` | Demonstrates `systemPromptAppend` to dynamically modify system prompt |
58
+ | `custom-compaction.ts` | Custom compaction that summarizes entire conversation |
59
+
60
+ ### External Dependencies
61
+
62
+ | Extension | Description |
63
+ |-----------|-------------|
64
+ | `chalk-logger.ts` | Uses chalk from parent node_modules (demonstrates jiti module resolution) |
65
+ | `with-deps/` | Extension with its own package.json and dependencies |
66
+ | `file-trigger.ts` | Watches a trigger file and injects contents into conversation |
67
+
68
+ ## Writing Extensions
69
+
70
+ See [docs/extensions.md](../../docs/extensions.md) for full documentation.
71
+
72
+ ```typescript
73
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
74
+ import { Type } from "@sinclair/typebox";
75
+
76
+ export default function (pi: ExtensionAPI) {
77
+ // Subscribe to lifecycle events
78
+ pi.on("tool_call", async (event, ctx) => {
79
+ if (event.toolName === "bash" && event.input.command?.includes("rm -rf")) {
80
+ const ok = await ctx.ui.confirm("Dangerous!", "Allow rm -rf?");
81
+ if (!ok) return { block: true, reason: "Blocked by user" };
82
+ }
83
+ });
84
+
85
+ // Register custom tools
86
+ pi.registerTool({
87
+ name: "greet",
88
+ label: "Greeting",
89
+ description: "Generate a greeting",
90
+ parameters: Type.Object({
91
+ name: Type.String({ description: "Name to greet" }),
92
+ }),
93
+ async execute(toolCallId, params, onUpdate, ctx, signal) {
94
+ return {
95
+ content: [{ type: "text", text: `Hello, ${params.name}!` }],
96
+ details: {},
97
+ };
98
+ },
99
+ });
100
+
101
+ // Register commands
102
+ pi.registerCommand("hello", {
103
+ description: "Say hello",
104
+ handler: async (args, ctx) => {
105
+ ctx.ui.notify("Hello!", "info");
106
+ },
107
+ });
108
+ }
109
+ ```
110
+
111
+ ## Key Patterns
112
+
113
+ **Use StringEnum for string parameters** (required for Google API compatibility):
114
+ ```typescript
115
+ import { StringEnum } from "@mariozechner/pi-ai";
116
+
117
+ // Good
118
+ action: StringEnum(["list", "add"] as const)
119
+
120
+ // Bad - doesn't work with Google
121
+ action: Type.Union([Type.Literal("list"), Type.Literal("add")])
122
+ ```
123
+
124
+ **State persistence via details:**
125
+ ```typescript
126
+ // Store state in tool result details for proper branching support
127
+ return {
128
+ content: [{ type: "text", text: "Done" }],
129
+ details: { todos: [...todos], nextId }, // Persisted in session
130
+ };
131
+
132
+ // Reconstruct on session events
133
+ pi.on("session_start", async (_event, ctx) => {
134
+ for (const entry of ctx.sessionManager.getBranch()) {
135
+ if (entry.type === "message" && entry.message.toolName === "my_tool") {
136
+ const details = entry.message.details;
137
+ // Reconstruct state from details
138
+ }
139
+ }
140
+ });
141
+ ```
@@ -1,13 +1,13 @@
1
1
  /**
2
- * Auto-Commit on Exit Hook
2
+ * Auto-Commit on Exit Extension
3
3
  *
4
4
  * Automatically commits changes when the agent exits.
5
5
  * Uses the last assistant message to generate a commit message.
6
6
  */
7
7
 
8
- import type { HookAPI } from "@mariozechner/pi-coding-agent";
8
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
9
9
 
10
- export default function (pi: HookAPI) {
10
+ export default function (pi: ExtensionAPI) {
11
11
  pi.on("session_shutdown", async (_event, ctx) => {
12
12
  // Check for uncommitted changes
13
13
  const { stdout: status, code } = await pi.exec("git", ["status", "--porcelain"]);