@oh-my-pi/pi-coding-agent 12.7.5 → 12.8.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 (57) hide show
  1. package/CHANGELOG.md +37 -37
  2. package/README.md +9 -1052
  3. package/package.json +7 -7
  4. package/src/cli/args.ts +1 -0
  5. package/src/cli/update-cli.ts +49 -35
  6. package/src/cli/web-search-cli.ts +3 -2
  7. package/src/commands/web-search.ts +1 -0
  8. package/src/config/model-registry.ts +6 -0
  9. package/src/config/model-resolver.ts +2 -0
  10. package/src/config/settings-schema.ts +25 -3
  11. package/src/config/settings.ts +1 -0
  12. package/src/extensibility/extensions/wrapper.ts +20 -13
  13. package/src/extensibility/slash-commands.ts +12 -91
  14. package/src/lsp/client.ts +24 -27
  15. package/src/lsp/index.ts +92 -42
  16. package/src/mcp/config-writer.ts +33 -0
  17. package/src/mcp/config.ts +6 -1
  18. package/src/mcp/types.ts +1 -0
  19. package/src/modes/components/custom-editor.ts +8 -5
  20. package/src/modes/components/settings-defs.ts +2 -1
  21. package/src/modes/controllers/command-controller.ts +12 -6
  22. package/src/modes/controllers/input-controller.ts +21 -186
  23. package/src/modes/controllers/mcp-command-controller.ts +60 -3
  24. package/src/modes/interactive-mode.ts +2 -2
  25. package/src/modes/types.ts +1 -1
  26. package/src/sdk.ts +23 -1
  27. package/src/secrets/index.ts +116 -0
  28. package/src/secrets/obfuscator.ts +269 -0
  29. package/src/secrets/regex.ts +21 -0
  30. package/src/session/agent-session.ts +143 -21
  31. package/src/session/compaction/branch-summarization.ts +2 -2
  32. package/src/session/compaction/compaction.ts +10 -3
  33. package/src/session/compaction/utils.ts +25 -1
  34. package/src/slash-commands/builtin-registry.ts +419 -0
  35. package/src/web/scrapers/github.ts +50 -12
  36. package/src/web/search/index.ts +5 -5
  37. package/src/web/search/provider.ts +13 -2
  38. package/src/web/search/providers/brave.ts +165 -0
  39. package/src/web/search/types.ts +1 -1
  40. package/docs/compaction.md +0 -436
  41. package/docs/config-usage.md +0 -176
  42. package/docs/custom-tools.md +0 -585
  43. package/docs/environment-variables.md +0 -257
  44. package/docs/extension-loading.md +0 -106
  45. package/docs/extensions.md +0 -1342
  46. package/docs/fs-scan-cache-architecture.md +0 -50
  47. package/docs/hooks.md +0 -906
  48. package/docs/models.md +0 -234
  49. package/docs/python-repl.md +0 -110
  50. package/docs/rpc.md +0 -1173
  51. package/docs/sdk.md +0 -1039
  52. package/docs/session-tree-plan.md +0 -84
  53. package/docs/session.md +0 -368
  54. package/docs/skills.md +0 -254
  55. package/docs/theme.md +0 -696
  56. package/docs/tree.md +0 -206
  57. package/docs/tui.md +0 -487
@@ -1,585 +0,0 @@
1
- > omp 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 `~/.omp/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;
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 omp session.
61
-
62
- ## Tool Locations
63
-
64
- OMP discovers custom tools through the capability system. Native OMP tools live in a subdirectory with an `index.ts`
65
- entry point; `.pi` mirrors the same layout as a compatibility alias.
66
-
67
- | Location | Scope | Auto-discovered |
68
- | ------------------------------- | -------------- | --------------- |
69
- | `~/.omp/agent/tools/*/index.ts` | User (OMP) | Yes |
70
- | `.omp/tools/*/index.ts` | Project (OMP) | Yes |
71
- | `~/.pi/agent/tools/*/index.ts` | User (alias) | Yes |
72
- | `.pi/tools/*/index.ts` | Project (alias) | Yes |
73
-
74
- Compatibility sources load flat modules (no subdirectory):
75
-
76
- - `~/.claude/tools/<tool>.ts` (or `.js`, `.sh`, `.bash`, `.py`), `.claude/tools/<tool>.*`
77
- - `~/.codex/tools/<tool>.ts` or `<tool>.js`, `.codex/tools/<tool>.ts` or `<tool>.js`
78
-
79
- Tools declared by installed plugins (via `~/.omp/plugins/node_modules` manifests) are also auto-discovered.
80
-
81
- Only TypeScript/JavaScript modules are executable. `.md` and `.json` files in tools directories are treated as metadata
82
- and are not loaded as tool modules.
83
-
84
- **Example structure:**
85
-
86
- ```
87
- ~/.omp/agent/tools/
88
- ├── hello/
89
- │ └── index.ts # Entry point (auto-discovered)
90
- └── complex-tool/
91
- ├── index.ts # Entry point (auto-discovered)
92
- ├── helpers.ts # Helper module (not loaded directly)
93
- └── types.ts # Type definitions (not loaded directly)
94
- ```
95
-
96
- **Name conflicts:** Duplicate tool names are rejected; the first loaded tool keeps its name and later conflicts are
97
- reported as load errors.
98
-
99
- **Reserved names:** Custom tools cannot use built-in tool names (`read`, `write`, `edit`, `bash`, `grep`, `find`, `python`, `fetch`, `task`, `browser`, `web_search`, etc.).
100
-
101
- ## Available Imports
102
-
103
- Custom tools can import from these packages:
104
-
105
- | Package | Purpose | Import Method |
106
- | --------------------------- | --------------------------------------------------------- | --------------------------------------------------- |
107
- | `@sinclair/typebox` | Schema definitions (`Type.Object`, `Type.String`, etc.) | Via `pi.typebox.*` (injected) |
108
- | `@oh-my-pi/pi-coding-agent` | Types and utilities | Via `pi.pi.*` (injected) or direct import for types |
109
- | `@oh-my-pi/pi-ai` | AI utilities (`StringEnum` for Google-compatible enums) | Via `pi.pi.*` (re-exported through coding-agent) |
110
- | `@oh-my-pi/pi-tui` | TUI components (`Text`, `Box`, etc. for custom rendering) | Via `pi.pi.*` (re-exported through coding-agent) |
111
- | `@oh-my-pi/pi-utils` | Logging (`logger`) | Via `pi.logger` (injected) |
112
-
113
- Node.js built-in modules (`node:fs`, `node:path`, etc.) are also available.
114
-
115
- **Important:** Use `pi.typebox.Type.*` instead of importing from `@sinclair/typebox` directly. Dependencies are injected via the `CustomToolAPI` to avoid import resolution issues.
116
-
117
- ## Tool Definition
118
-
119
- ```typescript
120
- import type {
121
- CustomTool,
122
- CustomToolContext,
123
- CustomToolFactory,
124
- CustomToolSessionEvent,
125
- } from "@oh-my-pi/pi-coding-agent";
126
-
127
- const factory: CustomToolFactory = (pi) => {
128
- // Destructure injected dependencies
129
- const { Type } = pi.typebox;
130
- const { StringEnum } = pi.pi;
131
- const { Text } = pi.pi;
132
-
133
- return {
134
- name: "my_tool",
135
- label: "My Tool",
136
- description: "What this tool does (be specific for LLM)",
137
- parameters: Type.Object({
138
- // Use StringEnum for string enums (Google API compatible)
139
- action: StringEnum(["list", "add", "remove"] as const),
140
- text: Type.Optional(Type.String()),
141
- }),
142
-
143
- async execute(toolCallId, params, onUpdate, ctx, signal) {
144
- // signal - AbortSignal for cancellation
145
- // onUpdate - Callback for streaming partial results
146
- // ctx - CustomToolContext with sessionManager, modelRegistry, model
147
- return {
148
- content: [{ type: "text", text: "Result for LLM" }],
149
- details: {
150
- /* structured data for rendering */
151
- },
152
- };
153
- },
154
-
155
- // Optional: Session lifecycle callback
156
- onSession(event, ctx) {
157
- if (event.reason === "shutdown") {
158
- // Cleanup resources (close connections, save state, etc.)
159
- return;
160
- }
161
- // Reconstruct state from ctx.sessionManager.getBranch()
162
- },
163
-
164
- // Optional: Custom rendering
165
- renderCall(args, theme) {
166
- /* return Component */
167
- },
168
- renderResult(result, options, theme, args) {
169
- /* return Component */
170
- },
171
- };
172
- };
173
-
174
- export default factory;
175
- ```
176
-
177
- Set `hidden: true` to exclude a tool from the default tool list; hidden tools must be explicitly enabled by the session.
178
-
179
- **Important:** Use `StringEnum` from `pi.pi` instead of `Type.Union`/`Type.Literal` for string enums. The latter doesn't work with Google's API.
180
-
181
- ## CustomToolAPI Object
182
-
183
- The factory receives a `CustomToolAPI` object (named `pi` by convention):
184
-
185
- ```typescript
186
- interface CustomToolAPI {
187
- cwd: string; // Current working directory
188
- exec(command: string, args: string[], options?: ExecOptions): Promise<ExecResult>;
189
- ui: ToolUIContext;
190
- hasUI: boolean; // false in --print or --mode rpc
191
- logger: typeof import("@oh-my-pi/pi-utils").logger; // File logger
192
- typebox: typeof import("@sinclair/typebox"); // Injected @sinclair/typebox
193
- pi: typeof import("@oh-my-pi/pi-coding-agent"); // Injected pi-coding-agent exports
194
- }
195
-
196
- interface ToolUIContext {
197
- select(title: string, options: string[]): Promise<string | undefined>;
198
- confirm(title: string, message: string): Promise<boolean>;
199
- input(title: string, placeholder?: string): Promise<string | undefined>;
200
- notify(message: string, type?: "info" | "warning" | "error"): void;
201
- setStatus(key: string, text: string | undefined): void;
202
- custom<T>(
203
- factory: (tui: TUI, theme: Theme, done: (result: T) => void) =>
204
- | (Component & { dispose?(): void })
205
- | Promise<Component & { dispose?(): void }>,
206
- ): Promise<T>;
207
- setEditorText(text: string): void;
208
- getEditorText(): string;
209
- editor(title: string, prefill?: string): Promise<string | undefined>;
210
- readonly theme: Theme;
211
- }
212
-
213
- interface ExecOptions {
214
- signal?: AbortSignal; // Cancel the process
215
- timeout?: number; // Timeout in milliseconds
216
- cwd?: string; // Working directory
217
- }
218
-
219
- interface ExecResult {
220
- stdout: string;
221
- stderr: string;
222
- code: number;
223
- killed: boolean; // True if process was killed by signal/timeout
224
- }
225
- ```
226
-
227
- `TUI` and `Theme` are from `@oh-my-pi/pi-tui` (available via `pi.pi`).
228
-
229
- Always check `pi.hasUI` before using UI methods.
230
-
231
- ### Cancellation Example
232
-
233
- Pass the `signal` from `execute` to `pi.exec` to support cancellation:
234
-
235
- ```typescript
236
- async execute(toolCallId, params, onUpdate, ctx, signal) {
237
- const result = await pi.exec("long-running-command", ["arg"], { signal });
238
- if (result.killed) {
239
- return { content: [{ type: "text", text: "Cancelled" }] };
240
- }
241
- return { content: [{ type: "text", text: result.stdout }] };
242
- }
243
- ```
244
-
245
- ### Error Handling
246
-
247
- **Throw an error** when the tool fails. Do not return an error message as content.
248
-
249
- ```typescript
250
- async execute(toolCallId, params, onUpdate, ctx, signal) {
251
- const { path } = params as { path: string };
252
-
253
- // Throw on error - omp will catch it and report to the LLM
254
- if (!fs.existsSync(path)) {
255
- throw new Error(`File not found: ${path}`);
256
- }
257
-
258
- // Return content only on success
259
- return { content: [{ type: "text", text: "Success" }] };
260
- }
261
- ```
262
-
263
- Thrown errors are:
264
-
265
- - Reported to the LLM as tool errors (with `isError: true`)
266
- - Emitted to hooks via `tool_result` event (hooks can inspect `event.isError`)
267
- - Displayed in the TUI with error styling
268
-
269
- ## CustomToolContext
270
-
271
- The `execute` and `onSession` callbacks receive a `CustomToolContext`:
272
-
273
- ```typescript
274
- interface CustomToolContext {
275
- sessionManager: ReadonlySessionManager; // Read-only access to session
276
- modelRegistry: ModelRegistry; // For API key resolution
277
- model: Model | undefined; // Current model (may be undefined)
278
- isIdle(): boolean; // Whether agent is idle (not streaming)
279
- hasQueuedMessages(): boolean; // Whether user has queued messages
280
- abort(): void; // Abort current operation (fire-and-forget)
281
- }
282
- ```
283
-
284
- Use `ctx.sessionManager.getBranch()` to get entries on the current branch for state reconstruction.
285
-
286
- ### Checking Queue State
287
-
288
- Interactive tools can skip prompts when the user has already queued a message:
289
-
290
- ```typescript
291
- async execute(toolCallId, params, onUpdate, ctx, signal) {
292
- // If user already queued a message, skip the interactive prompt
293
- if (ctx.hasQueuedMessages()) {
294
- return {
295
- content: [{ type: "text", text: "Skipped - user has queued input" }],
296
- };
297
- }
298
-
299
- // Otherwise, prompt for input
300
- const answer = await pi.ui.input("What would you like to do?");
301
- // ...
302
- }
303
- ```
304
-
305
- ### Multi-line Editor
306
-
307
- For longer text editing, use `pi.ui.editor()` which supports Ctrl+G for external editor:
308
-
309
- ```typescript
310
- async execute(toolCallId, params, onUpdate, ctx, signal) {
311
- const text = await pi.ui.editor("Edit your response:", "prefilled text");
312
- // Returns edited text or undefined if cancelled (Escape)
313
- // Ctrl+Enter to submit, Ctrl+G to open $VISUAL or $EDITOR
314
-
315
- if (!text) {
316
- return { content: [{ type: "text", text: "Cancelled" }] };
317
- }
318
- // ...
319
- }
320
- ```
321
-
322
- ## Session Lifecycle
323
-
324
- Tools can implement `onSession` to react to session changes:
325
-
326
- ```typescript
327
- type CustomToolSessionEvent =
328
- | { reason: "start" | "switch" | "branch" | "tree" | "shutdown"; previousSessionFile: string | undefined }
329
- | { reason: "auto_compaction_start"; trigger: "threshold" | "overflow" }
330
- | {
331
- reason: "auto_compaction_end";
332
- result: CompactionResult | undefined;
333
- aborted: boolean;
334
- willRetry: boolean;
335
- errorMessage?: string;
336
- }
337
- | { reason: "auto_retry_start"; attempt: number; maxAttempts: number; delayMs: number; errorMessage: string }
338
- | { reason: "auto_retry_end"; success: boolean; attempt: number; finalError?: string }
339
- | { reason: "ttsr_triggered"; rules: Rule[] }
340
- | { reason: "todo_reminder"; todos: TodoItem[]; attempt: number; maxAttempts: number };
341
- ```
342
-
343
- **Reasons:**
344
- - `start`: Initial session load (fresh start or resuming an existing session) - use to reconstruct state from session entries
345
- - `switch`: User started a new session (`/new`) or switched to a different session (`/resume`)
346
- - `branch`: User branched from a previous message (`/branch`)
347
- - `tree`: User navigated to a different point in the session tree (`/tree`)
348
- - `shutdown`: Process is exiting (Ctrl+C, Ctrl+D, or SIGTERM) - use to cleanup resources
349
- - `auto_compaction_start`: Auto-compaction kicked off (`threshold` or `overflow`)
350
- - `auto_compaction_end`: Auto-compaction finished (includes result/abort/error metadata)
351
- - `auto_retry_start`: Automatic retry scheduled after an assistant error
352
- - `auto_retry_end`: Automatic retry completed/failed/cancelled
353
- - `ttsr_triggered`: Time-travel stream rule interrupted generation
354
- - `todo_reminder`: Todo reminder fired with outstanding items
355
-
356
- To check if a session is fresh (no messages), use `ctx.sessionManager.getEntries().length === 0`.
357
-
358
- ### State Management Pattern
359
-
360
- 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.
361
-
362
- ```typescript
363
- interface MyToolDetails {
364
- items: string[];
365
- }
366
-
367
- const factory: CustomToolFactory = (pi) => {
368
- const { Type } = pi.typebox;
369
-
370
- // In-memory state
371
- let items: string[] = [];
372
-
373
- // Reconstruct state from session entries
374
- const reconstructState = (event: CustomToolSessionEvent, ctx: CustomToolContext) => {
375
- if (event.reason === "shutdown") return;
376
-
377
- items = [];
378
- for (const entry of ctx.sessionManager.getBranch()) {
379
- if (entry.type !== "message") continue;
380
- const msg = entry.message;
381
- if (msg.role !== "toolResult") continue;
382
- if (msg.toolName !== "my_tool") continue;
383
-
384
- const details = msg.details as MyToolDetails | undefined;
385
- if (details) {
386
- items = details.items;
387
- }
388
- }
389
- };
390
-
391
- return {
392
- name: "my_tool",
393
- label: "My Tool",
394
- description: "...",
395
- parameters: Type.Object({ ... }),
396
-
397
- onSession: reconstructState,
398
-
399
- async execute(toolCallId, params, onUpdate, ctx, signal) {
400
- // Modify items...
401
- items.push("new item");
402
-
403
- return {
404
- content: [{ type: "text", text: "Added item" }],
405
- // Store current state in details for reconstruction
406
- details: { items: [...items] },
407
- };
408
- },
409
- };
410
- };
411
- ```
412
-
413
- This pattern ensures:
414
-
415
- - When user branches, state is correct for that point in history
416
- - When user switches sessions, state matches that session
417
- - When user starts a new session, state resets
418
-
419
- ## Custom Rendering
420
-
421
- 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.
422
-
423
- ### How It Works
424
-
425
- Tool output is wrapped in a `Box` component that handles:
426
-
427
- - Padding (1 character horizontal, 1 line vertical)
428
- - Background color based on state (pending/success/error)
429
-
430
- Your render methods return `Component` instances (typically `Text`) that go inside this box. Use `Text(content, 0, 0)` since the Box handles padding.
431
-
432
- ### renderCall
433
-
434
- Renders the tool call (before/during execution):
435
-
436
- ```typescript
437
- renderCall(args, theme) {
438
- let text = theme.fg("toolTitle", theme.bold("my_tool "));
439
- text += theme.fg("muted", args.action);
440
- if (args.text) {
441
- text += " " + theme.fg("dim", `"${args.text}"`);
442
- }
443
- return new Text(text, 0, 0);
444
- }
445
- ```
446
-
447
- Called when:
448
-
449
- - Tool call starts (may have partial args during streaming)
450
- - Args are updated during streaming
451
-
452
- ### renderResult
453
-
454
- Renders the tool result:
455
-
456
- ```typescript
457
- renderResult(result, { expanded, isPartial }, theme) {
458
- const { details } = result;
459
-
460
- // Handle streaming/partial results
461
- if (isPartial) {
462
- return new Text(theme.fg("warning", "Processing..."), 0, 0);
463
- }
464
-
465
- // Handle errors
466
- if (details?.error) {
467
- return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
468
- }
469
-
470
- // Normal result
471
- let text = theme.fg("success", "✓ ") + theme.fg("muted", "Done");
472
-
473
- // Support expanded view (Ctrl+O)
474
- if (expanded && details?.items) {
475
- for (const item of details.items) {
476
- text += "\n" + theme.fg("dim", ` ${item}`);
477
- }
478
- }
479
-
480
- return new Text(text, 0, 0);
481
- }
482
- ```
483
-
484
- **Options:**
485
-
486
- - `expanded`: User pressed Ctrl+O to expand
487
- - `isPartial`: Result is from `onUpdate` (streaming), not final
488
- - `spinnerFrame`: Spinner frame index (0-9) during partial updates
489
-
490
- ### Best Practices
491
-
492
- 1. **Use `Text` with padding `(0, 0)`** - The Box handles padding
493
- 2. **Use `\n` for multi-line content** - Not multiple Text components
494
- 3. **Handle `isPartial`** - Show progress during streaming
495
- 4. **Support `expanded`** - Show more detail when user requests
496
- 5. **Use theme colors** - For consistent appearance
497
- 6. **Keep it compact** - Show summary by default, details when expanded
498
-
499
- ### Theme Colors
500
-
501
- ```typescript
502
- // Foreground
503
- theme.fg("toolTitle", text); // Tool names
504
- theme.fg("accent", text); // Highlights
505
- theme.fg("success", text); // Success
506
- theme.fg("error", text); // Errors
507
- theme.fg("warning", text); // Warnings
508
- theme.fg("muted", text); // Secondary text
509
- theme.fg("dim", text); // Tertiary text
510
- theme.fg("toolOutput", text); // Output content
511
-
512
- // Styles
513
- theme.bold(text);
514
- theme.italic(text);
515
- ```
516
-
517
- ### Fallback Behavior
518
-
519
- If `renderCall` or `renderResult` is not defined or throws an error:
520
-
521
- - `renderCall`: Shows tool name
522
- - `renderResult`: Shows raw text output from `content`
523
-
524
- ## Execute Function
525
-
526
- ```typescript
527
- async execute(toolCallId, args, onUpdate, ctx, signal) {
528
- // Type assertion for params (TypeBox schema doesn't flow through)
529
- const params = args as { action: "list" | "add"; text?: string };
530
-
531
- // Check for abort
532
- if (signal?.aborted) {
533
- return { content: [...], details: { status: "aborted" } };
534
- }
535
-
536
- // Stream progress
537
- onUpdate?.({
538
- content: [{ type: "text", text: "Working..." }],
539
- details: { progress: 50 },
540
- });
541
-
542
- // Return final result
543
- return {
544
- content: [{ type: "text", text: "Done" }], // Sent to LLM
545
- details: { data: result }, // For rendering only
546
- };
547
- }
548
- ```
549
-
550
- ## Multiple Tools from One File
551
-
552
- Return an array to share state between related tools:
553
-
554
- ```typescript
555
- const factory: CustomToolFactory = (pi) => {
556
- // Shared state
557
- let connection = null;
558
-
559
- const handleSession = (event: CustomToolSessionEvent, ctx: CustomToolContext) => {
560
- if (event.reason === "shutdown") {
561
- connection?.close();
562
- }
563
- };
564
-
565
- return [
566
- { name: "db_connect", onSession: handleSession, ... },
567
- { name: "db_query", onSession: handleSession, ... },
568
- { name: "db_close", onSession: handleSession, ... },
569
- ];
570
- };
571
- ```
572
-
573
- ## Examples
574
-
575
- See [`examples/custom-tools/todo/index.ts`](../examples/custom-tools/todo/index.ts) for a complete example with:
576
-
577
- - `onSession` for state reconstruction
578
- - Custom `renderCall` and `renderResult`
579
- - Proper branching support via details storage
580
-
581
- Test by copying the example into your tools directory and restarting omp:
582
-
583
- ```bash
584
- cp -r packages/coding-agent/examples/custom-tools/todo ~/.omp/agent/tools/
585
- ```