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