@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
@@ -1,514 +0,0 @@
1
- > pi can create custom tools. Ask it to build one for your use case.
2
-
3
- # Custom Tools
4
-
5
- Custom tools are additional tools that the LLM can call directly, just like the built-in `read`, `write`, `edit`, and `bash` tools. They are TypeScript modules that define callable functions with parameters, return values, and optional TUI rendering.
6
-
7
- **Key capabilities:**
8
- - **User interaction** - Prompt users via `pi.ui` (select, confirm, input dialogs)
9
- - **Custom rendering** - Control how tool calls and results appear via `renderCall`/`renderResult`
10
- - **TUI components** - Render custom components with `pi.ui.custom()` (see [tui.md](tui.md))
11
- - **State management** - Persist state in tool result `details` for proper branching support
12
- - **Streaming results** - Send partial updates via `onUpdate` callback
13
-
14
- **Example use cases:**
15
- - Interactive dialogs (questions with selectable options)
16
- - Stateful tools (todo lists, connection pools)
17
- - Rich output rendering (progress indicators, structured views)
18
- - External service integrations with confirmation flows
19
-
20
- **When to use custom tools vs. alternatives:**
21
-
22
- | Need | Solution |
23
- |------|----------|
24
- | Always-needed context (conventions, commands) | AGENTS.md |
25
- | User triggers a specific prompt template | Slash command |
26
- | On-demand capability package (workflows, scripts, setup) | Skill |
27
- | Additional tool directly callable by the LLM | **Custom tool** |
28
-
29
- See [examples/custom-tools/](../examples/custom-tools/) for working examples.
30
-
31
- ## Quick Start
32
-
33
- Create a file `~/.pi/agent/tools/hello/index.ts`:
34
-
35
- ```typescript
36
- import { Type } from "@sinclair/typebox";
37
- import type { CustomToolFactory } from "@mariozechner/pi-coding-agent";
38
-
39
- const factory: CustomToolFactory = (pi) => ({
40
- name: "hello",
41
- label: "Hello",
42
- description: "A simple greeting tool",
43
- parameters: Type.Object({
44
- name: Type.String({ description: "Name to greet" }),
45
- }),
46
-
47
- async execute(toolCallId, params, onUpdate, ctx, signal) {
48
- const { name } = params as { name: string };
49
- return {
50
- content: [{ type: "text", text: `Hello, ${name}!` }],
51
- details: { greeted: name },
52
- };
53
- },
54
- });
55
-
56
- export default factory;
57
- ```
58
-
59
- The tool is automatically discovered and available in your next pi session.
60
-
61
- ## Tool Locations
62
-
63
- Tools must be in a subdirectory with an `index.ts` entry point:
64
-
65
- | Location | Scope | Auto-discovered |
66
- |----------|-------|-----------------|
67
- | `~/.pi/agent/tools/*/index.ts` | Global (all projects) | Yes |
68
- | `.pi/tools/*/index.ts` | Project-local | Yes |
69
- | `settings.json` `customTools` array | Configured paths | Yes |
70
- | `--tool <path>` CLI flag | One-off/debugging | No |
71
-
72
- **Example structure:**
73
- ```
74
- ~/.pi/agent/tools/
75
- ├── hello/
76
- │ └── index.ts # Entry point (auto-discovered)
77
- └── complex-tool/
78
- ├── index.ts # Entry point (auto-discovered)
79
- ├── helpers.ts # Helper module (not loaded directly)
80
- └── types.ts # Type definitions (not loaded directly)
81
- ```
82
-
83
- **Priority:** Later sources win on name conflicts. CLI `--tool` takes highest priority.
84
-
85
- **Reserved names:** Custom tools cannot use built-in tool names (`read`, `write`, `edit`, `bash`, `grep`, `find`, `ls`).
86
-
87
- ## Available Imports
88
-
89
- Custom tools can import from these packages (automatically resolved by pi):
90
-
91
- | Package | Purpose |
92
- |---------|---------|
93
- | `@sinclair/typebox` | Schema definitions (`Type.Object`, `Type.String`, etc.) |
94
- | `@mariozechner/pi-coding-agent` | Types (`CustomToolFactory`, `CustomTool`, `CustomToolContext`, etc.) |
95
- | `@mariozechner/pi-ai` | AI utilities (`StringEnum` for Google-compatible enums) |
96
- | `@mariozechner/pi-tui` | TUI components (`Text`, `Box`, etc. for custom rendering) |
97
-
98
- Node.js built-in modules (`node:fs`, `node:path`, etc.) are also available.
99
-
100
- ## Tool Definition
101
-
102
- ```typescript
103
- import { Type } from "@sinclair/typebox";
104
- import { StringEnum } from "@mariozechner/pi-ai";
105
- import { Text } from "@mariozechner/pi-tui";
106
- import type {
107
- CustomTool,
108
- CustomToolContext,
109
- CustomToolFactory,
110
- CustomToolSessionEvent,
111
- } from "@mariozechner/pi-coding-agent";
112
-
113
- const factory: CustomToolFactory = (pi) => ({
114
- name: "my_tool",
115
- label: "My Tool",
116
- description: "What this tool does (be specific for LLM)",
117
- parameters: Type.Object({
118
- // Use StringEnum for string enums (Google API compatible)
119
- action: StringEnum(["list", "add", "remove"] as const),
120
- text: Type.Optional(Type.String()),
121
- }),
122
-
123
- async execute(toolCallId, params, onUpdate, ctx, signal) {
124
- // signal - AbortSignal for cancellation
125
- // onUpdate - Callback for streaming partial results
126
- // ctx - CustomToolContext with sessionManager, modelRegistry, model
127
- return {
128
- content: [{ type: "text", text: "Result for LLM" }],
129
- details: { /* structured data for rendering */ },
130
- };
131
- },
132
-
133
- // Optional: Session lifecycle callback
134
- onSession(event, ctx) {
135
- if (event.reason === "shutdown") {
136
- // Cleanup resources (close connections, save state, etc.)
137
- return;
138
- }
139
- // Reconstruct state from ctx.sessionManager.getBranch()
140
- },
141
-
142
- // Optional: Custom rendering
143
- renderCall(args, theme) { /* return Component */ },
144
- renderResult(result, options, theme) { /* return Component */ },
145
- });
146
-
147
- export default factory;
148
- ```
149
-
150
- **Important:** Use `StringEnum` from `@mariozechner/pi-ai` instead of `Type.Union`/`Type.Literal` for string enums. The latter doesn't work with Google's API.
151
-
152
- ## CustomToolAPI Object
153
-
154
- The factory receives a `CustomToolAPI` object (named `pi` by convention):
155
-
156
- ```typescript
157
- interface CustomToolAPI {
158
- cwd: string; // Current working directory
159
- exec(command: string, args: string[], options?: ExecOptions): Promise<ExecResult>;
160
- ui: ToolUIContext;
161
- hasUI: boolean; // false in --print or --mode rpc
162
- }
163
-
164
- interface ToolUIContext {
165
- select(title: string, options: string[]): Promise<string | undefined>;
166
- confirm(title: string, message: string): Promise<boolean>;
167
- input(title: string, placeholder?: string): Promise<string | undefined>;
168
- notify(message: string, type?: "info" | "warning" | "error"): void;
169
- custom(component: Component & { dispose?(): void }): { close: () => void; requestRender: () => void };
170
- }
171
-
172
- interface ExecOptions {
173
- signal?: AbortSignal; // Cancel the process
174
- timeout?: number; // Timeout in milliseconds
175
- }
176
-
177
- interface ExecResult {
178
- stdout: string;
179
- stderr: string;
180
- code: number;
181
- killed?: boolean; // True if process was killed by signal/timeout
182
- }
183
- ```
184
-
185
- Always check `pi.hasUI` before using UI methods.
186
-
187
- ### Cancellation Example
188
-
189
- Pass the `signal` from `execute` to `pi.exec` to support cancellation:
190
-
191
- ```typescript
192
- async execute(toolCallId, params, onUpdate, ctx, signal) {
193
- const result = await pi.exec("long-running-command", ["arg"], { signal });
194
- if (result.killed) {
195
- return { content: [{ type: "text", text: "Cancelled" }] };
196
- }
197
- return { content: [{ type: "text", text: result.stdout }] };
198
- }
199
- ```
200
-
201
- ### Error Handling
202
-
203
- **Throw an error** when the tool fails. Do not return an error message as content.
204
-
205
- ```typescript
206
- async execute(toolCallId, params, onUpdate, ctx, signal) {
207
- const { path } = params as { path: string };
208
-
209
- // Throw on error - pi will catch it and report to the LLM
210
- if (!fs.existsSync(path)) {
211
- throw new Error(`File not found: ${path}`);
212
- }
213
-
214
- // Return content only on success
215
- return { content: [{ type: "text", text: "Success" }] };
216
- }
217
- ```
218
-
219
- Thrown errors are:
220
- - Reported to the LLM as tool errors (with `isError: true`)
221
- - Emitted to hooks via `tool_result` event (hooks can inspect `event.isError`)
222
- - Displayed in the TUI with error styling
223
-
224
- ## CustomToolContext
225
-
226
- The `execute` and `onSession` callbacks receive a `CustomToolContext`:
227
-
228
- ```typescript
229
- interface CustomToolContext {
230
- sessionManager: ReadonlySessionManager; // Read-only access to session
231
- modelRegistry: ModelRegistry; // For API key resolution
232
- model: Model | undefined; // Current model (may be undefined)
233
- isIdle(): boolean; // Whether agent is streaming
234
- hasQueuedMessages(): boolean; // Whether user has queued messages
235
- abort(): void; // Abort current operation (fire-and-forget)
236
- }
237
- ```
238
-
239
- Use `ctx.sessionManager.getBranch()` to get entries on the current branch for state reconstruction.
240
-
241
- ### Checking Queue State
242
-
243
- Interactive tools can skip prompts when the user has already queued a message:
244
-
245
- ```typescript
246
- async execute(toolCallId, params, onUpdate, ctx, signal) {
247
- // If user already queued a message, skip the interactive prompt
248
- if (ctx.hasQueuedMessages()) {
249
- return {
250
- content: [{ type: "text", text: "Skipped - user has queued input" }],
251
- };
252
- }
253
-
254
- // Otherwise, prompt for input
255
- const answer = await pi.ui.input("What would you like to do?");
256
- // ...
257
- }
258
- ```
259
-
260
- ### Multi-line Editor
261
-
262
- For longer text editing, use `pi.ui.editor()` which supports Ctrl+G for external editor:
263
-
264
- ```typescript
265
- async execute(toolCallId, params, onUpdate, ctx, signal) {
266
- const text = await pi.ui.editor("Edit your response:", "prefilled text");
267
- // Returns edited text or undefined if cancelled (Escape)
268
- // Ctrl+Enter to submit, Ctrl+G to open $VISUAL or $EDITOR
269
-
270
- if (!text) {
271
- return { content: [{ type: "text", text: "Cancelled" }] };
272
- }
273
- // ...
274
- }
275
- ```
276
-
277
- ## Session Lifecycle
278
-
279
- Tools can implement `onSession` to react to session changes:
280
-
281
- ```typescript
282
- interface CustomToolSessionEvent {
283
- reason: "start" | "switch" | "branch" | "tree" | "shutdown";
284
- previousSessionFile: string | undefined;
285
- }
286
- ```
287
-
288
- **Reasons:**
289
- - `start`: Initial session load on startup
290
- - `switch`: User started a new session (`/new`) or switched to a different session (`/resume`)
291
- - `branch`: User branched from a previous message (`/branch`)
292
- - `tree`: User navigated to a different point in the session tree (`/tree`)
293
- - `shutdown`: Process is exiting (Ctrl+C, Ctrl+D, or SIGTERM) - use to cleanup resources
294
-
295
- To check if a session is fresh (no messages), use `ctx.sessionManager.getEntries().length === 0`.
296
-
297
- ### State Management Pattern
298
-
299
- Tools that maintain state should store it in `details` of their results, not external files. This allows branching to work correctly, as the state is reconstructed from the session history.
300
-
301
- ```typescript
302
- interface MyToolDetails {
303
- items: string[];
304
- }
305
-
306
- const factory: CustomToolFactory = (pi) => {
307
- // In-memory state
308
- let items: string[] = [];
309
-
310
- // Reconstruct state from session entries
311
- const reconstructState = (event: CustomToolSessionEvent, ctx: CustomToolContext) => {
312
- if (event.reason === "shutdown") return;
313
-
314
- items = [];
315
- for (const entry of ctx.sessionManager.getBranch()) {
316
- if (entry.type !== "message") continue;
317
- const msg = entry.message;
318
- if (msg.role !== "toolResult") continue;
319
- if (msg.toolName !== "my_tool") continue;
320
-
321
- const details = msg.details as MyToolDetails | undefined;
322
- if (details) {
323
- items = details.items;
324
- }
325
- }
326
- };
327
-
328
- return {
329
- name: "my_tool",
330
- label: "My Tool",
331
- description: "...",
332
- parameters: Type.Object({ ... }),
333
-
334
- onSession: reconstructState,
335
-
336
- async execute(toolCallId, params, onUpdate, ctx, signal) {
337
- // Modify items...
338
- items.push("new item");
339
-
340
- return {
341
- content: [{ type: "text", text: "Added item" }],
342
- // Store current state in details for reconstruction
343
- details: { items: [...items] },
344
- };
345
- },
346
- };
347
- };
348
- ```
349
-
350
- This pattern ensures:
351
- - When user branches, state is correct for that point in history
352
- - When user switches sessions, state matches that session
353
- - When user starts a new session, state resets
354
-
355
- ## Custom Rendering
356
-
357
- Custom tools can provide `renderCall` and `renderResult` methods to control how they appear in the TUI. Both are optional. See [tui.md](tui.md) for the full component API.
358
-
359
- ### How It Works
360
-
361
- Tool output is wrapped in a `Box` component that handles:
362
- - Padding (1 character horizontal, 1 line vertical)
363
- - Background color based on state (pending/success/error)
364
-
365
- Your render methods return `Component` instances (typically `Text`) that go inside this box. Use `Text(content, 0, 0)` since the Box handles padding.
366
-
367
- ### renderCall
368
-
369
- Renders the tool call (before/during execution):
370
-
371
- ```typescript
372
- renderCall(args, theme) {
373
- let text = theme.fg("toolTitle", theme.bold("my_tool "));
374
- text += theme.fg("muted", args.action);
375
- if (args.text) {
376
- text += " " + theme.fg("dim", `"${args.text}"`);
377
- }
378
- return new Text(text, 0, 0);
379
- }
380
- ```
381
-
382
- Called when:
383
- - Tool call starts (may have partial args during streaming)
384
- - Args are updated during streaming
385
-
386
- ### renderResult
387
-
388
- Renders the tool result:
389
-
390
- ```typescript
391
- renderResult(result, { expanded, isPartial }, theme) {
392
- const { details } = result;
393
-
394
- // Handle streaming/partial results
395
- if (isPartial) {
396
- return new Text(theme.fg("warning", "Processing..."), 0, 0);
397
- }
398
-
399
- // Handle errors
400
- if (details?.error) {
401
- return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
402
- }
403
-
404
- // Normal result
405
- let text = theme.fg("success", "✓ ") + theme.fg("muted", "Done");
406
-
407
- // Support expanded view (Ctrl+O)
408
- if (expanded && details?.items) {
409
- for (const item of details.items) {
410
- text += "\n" + theme.fg("dim", ` ${item}`);
411
- }
412
- }
413
-
414
- return new Text(text, 0, 0);
415
- }
416
- ```
417
-
418
- **Options:**
419
- - `expanded`: User pressed Ctrl+O to expand
420
- - `isPartial`: Result is from `onUpdate` (streaming), not final
421
-
422
- ### Best Practices
423
-
424
- 1. **Use `Text` with padding `(0, 0)`** - The Box handles padding
425
- 2. **Use `\n` for multi-line content** - Not multiple Text components
426
- 3. **Handle `isPartial`** - Show progress during streaming
427
- 4. **Support `expanded`** - Show more detail when user requests
428
- 5. **Use theme colors** - For consistent appearance
429
- 6. **Keep it compact** - Show summary by default, details when expanded
430
-
431
- ### Theme Colors
432
-
433
- ```typescript
434
- // Foreground
435
- theme.fg("toolTitle", text) // Tool names
436
- theme.fg("accent", text) // Highlights
437
- theme.fg("success", text) // Success
438
- theme.fg("error", text) // Errors
439
- theme.fg("warning", text) // Warnings
440
- theme.fg("muted", text) // Secondary text
441
- theme.fg("dim", text) // Tertiary text
442
- theme.fg("toolOutput", text) // Output content
443
-
444
- // Styles
445
- theme.bold(text)
446
- theme.italic(text)
447
- ```
448
-
449
- ### Fallback Behavior
450
-
451
- If `renderCall` or `renderResult` is not defined or throws an error:
452
- - `renderCall`: Shows tool name
453
- - `renderResult`: Shows raw text output from `content`
454
-
455
- ## Execute Function
456
-
457
- ```typescript
458
- async execute(toolCallId, args, onUpdate, ctx, signal) {
459
- // Type assertion for params (TypeBox schema doesn't flow through)
460
- const params = args as { action: "list" | "add"; text?: string };
461
-
462
- // Check for abort
463
- if (signal?.aborted) {
464
- return { content: [...], details: { status: "aborted" } };
465
- }
466
-
467
- // Stream progress
468
- onUpdate?.({
469
- content: [{ type: "text", text: "Working..." }],
470
- details: { progress: 50 },
471
- });
472
-
473
- // Return final result
474
- return {
475
- content: [{ type: "text", text: "Done" }], // Sent to LLM
476
- details: { data: result }, // For rendering only
477
- };
478
- }
479
- ```
480
-
481
- ## Multiple Tools from One File
482
-
483
- Return an array to share state between related tools:
484
-
485
- ```typescript
486
- const factory: CustomToolFactory = (pi) => {
487
- // Shared state
488
- let connection = null;
489
-
490
- const handleSession = (event: CustomToolSessionEvent, ctx: CustomToolContext) => {
491
- if (event.reason === "shutdown") {
492
- connection?.close();
493
- }
494
- };
495
-
496
- return [
497
- { name: "db_connect", onSession: handleSession, ... },
498
- { name: "db_query", onSession: handleSession, ... },
499
- { name: "db_close", onSession: handleSession, ... },
500
- ];
501
- };
502
- ```
503
-
504
- ## Examples
505
-
506
- See [`examples/custom-tools/todo/index.ts`](../examples/custom-tools/todo/index.ts) for a complete example with:
507
- - `onSession` for state reconstruction
508
- - Custom `renderCall` and `renderResult`
509
- - Proper branching support via details storage
510
-
511
- Test with:
512
- ```bash
513
- pi --tool packages/coding-agent/examples/custom-tools/todo/index.ts
514
- ```