@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,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for compaction and branch summarization.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
6
|
+
import type { Message } from "@oh-my-pi/pi-ai";
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// File Operation Tracking
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
export interface FileOperations {
|
|
13
|
+
read: Set<string>;
|
|
14
|
+
written: Set<string>;
|
|
15
|
+
edited: Set<string>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createFileOps(): FileOperations {
|
|
19
|
+
return {
|
|
20
|
+
read: new Set(),
|
|
21
|
+
written: new Set(),
|
|
22
|
+
edited: new Set(),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Extract file operations from tool calls in an assistant message.
|
|
28
|
+
*/
|
|
29
|
+
export function extractFileOpsFromMessage(message: AgentMessage, fileOps: FileOperations): void {
|
|
30
|
+
if (message.role !== "assistant") return;
|
|
31
|
+
if (!("content" in message) || !Array.isArray(message.content)) return;
|
|
32
|
+
|
|
33
|
+
for (const block of message.content) {
|
|
34
|
+
if (typeof block !== "object" || block === null) continue;
|
|
35
|
+
if (!("type" in block) || block.type !== "toolCall") continue;
|
|
36
|
+
if (!("arguments" in block) || !("name" in block)) continue;
|
|
37
|
+
|
|
38
|
+
const args = block.arguments as Record<string, unknown> | undefined;
|
|
39
|
+
if (!args) continue;
|
|
40
|
+
|
|
41
|
+
const path = typeof args.path === "string" ? args.path : undefined;
|
|
42
|
+
if (!path) continue;
|
|
43
|
+
|
|
44
|
+
switch (block.name) {
|
|
45
|
+
case "read":
|
|
46
|
+
fileOps.read.add(path);
|
|
47
|
+
break;
|
|
48
|
+
case "write":
|
|
49
|
+
fileOps.written.add(path);
|
|
50
|
+
break;
|
|
51
|
+
case "edit":
|
|
52
|
+
fileOps.edited.add(path);
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Compute final file lists from file operations.
|
|
60
|
+
* Returns readFiles (files only read, not modified) and modifiedFiles.
|
|
61
|
+
*/
|
|
62
|
+
export function computeFileLists(fileOps: FileOperations): { readFiles: string[]; modifiedFiles: string[] } {
|
|
63
|
+
const modified = new Set([...fileOps.edited, ...fileOps.written]);
|
|
64
|
+
const readOnly = [...fileOps.read].filter((f) => !modified.has(f)).sort();
|
|
65
|
+
const modifiedFiles = [...modified].sort();
|
|
66
|
+
return { readFiles: readOnly, modifiedFiles };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Format file operations as XML tags for summary.
|
|
71
|
+
*/
|
|
72
|
+
export function formatFileOperations(readFiles: string[], modifiedFiles: string[]): string {
|
|
73
|
+
const sections: string[] = [];
|
|
74
|
+
if (readFiles.length > 0) {
|
|
75
|
+
sections.push(`<read-files>\n${readFiles.join("\n")}\n</read-files>`);
|
|
76
|
+
}
|
|
77
|
+
if (modifiedFiles.length > 0) {
|
|
78
|
+
sections.push(`<modified-files>\n${modifiedFiles.join("\n")}\n</modified-files>`);
|
|
79
|
+
}
|
|
80
|
+
if (sections.length === 0) return "";
|
|
81
|
+
return `\n\n${sections.join("\n\n")}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ============================================================================
|
|
85
|
+
// Message Serialization
|
|
86
|
+
// ============================================================================
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Serialize LLM messages to text for summarization.
|
|
90
|
+
* This prevents the model from treating it as a conversation to continue.
|
|
91
|
+
* Call convertToLlm() first to handle custom message types.
|
|
92
|
+
*/
|
|
93
|
+
export function serializeConversation(messages: Message[]): string {
|
|
94
|
+
const parts: string[] = [];
|
|
95
|
+
|
|
96
|
+
for (const msg of messages) {
|
|
97
|
+
if (msg.role === "user") {
|
|
98
|
+
const content =
|
|
99
|
+
typeof msg.content === "string"
|
|
100
|
+
? msg.content
|
|
101
|
+
: msg.content
|
|
102
|
+
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
103
|
+
.map((c) => c.text)
|
|
104
|
+
.join("");
|
|
105
|
+
if (content) parts.push(`[User]: ${content}`);
|
|
106
|
+
} else if (msg.role === "assistant") {
|
|
107
|
+
const textParts: string[] = [];
|
|
108
|
+
const thinkingParts: string[] = [];
|
|
109
|
+
const toolCalls: string[] = [];
|
|
110
|
+
|
|
111
|
+
for (const block of msg.content) {
|
|
112
|
+
if (block.type === "text") {
|
|
113
|
+
textParts.push(block.text);
|
|
114
|
+
} else if (block.type === "thinking") {
|
|
115
|
+
thinkingParts.push(block.thinking);
|
|
116
|
+
} else if (block.type === "toolCall") {
|
|
117
|
+
const args = block.arguments as Record<string, unknown>;
|
|
118
|
+
const argsStr = Object.entries(args)
|
|
119
|
+
.map(([k, v]) => `${k}=${JSON.stringify(v)}`)
|
|
120
|
+
.join(", ");
|
|
121
|
+
toolCalls.push(`${block.name}(${argsStr})`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (thinkingParts.length > 0) {
|
|
126
|
+
parts.push(`[Assistant thinking]: ${thinkingParts.join("\n")}`);
|
|
127
|
+
}
|
|
128
|
+
if (textParts.length > 0) {
|
|
129
|
+
parts.push(`[Assistant]: ${textParts.join("\n")}`);
|
|
130
|
+
}
|
|
131
|
+
if (toolCalls.length > 0) {
|
|
132
|
+
parts.push(`[Assistant tool calls]: ${toolCalls.join("; ")}`);
|
|
133
|
+
}
|
|
134
|
+
} else if (msg.role === "toolResult") {
|
|
135
|
+
const content = msg.content
|
|
136
|
+
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
137
|
+
.map((c) => c.text)
|
|
138
|
+
.join("");
|
|
139
|
+
if (content) {
|
|
140
|
+
parts.push(`[Tool result]: ${content}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return parts.join("\n\n");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ============================================================================
|
|
149
|
+
// Summarization System Prompt
|
|
150
|
+
// ============================================================================
|
|
151
|
+
|
|
152
|
+
export const SUMMARIZATION_SYSTEM_PROMPT = `You are a context summarization assistant. Your task is to read a conversation between a user and an AI coding assistant, then produce a structured summary following the exact format specified.
|
|
153
|
+
|
|
154
|
+
Do NOT continue the conversation. Do NOT respond to any questions in the conversation. ONLY output the structured summary.`;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom tools module.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { discoverAndLoadCustomTools, loadCustomTools } from "./loader.js";
|
|
6
|
+
export type {
|
|
7
|
+
AgentToolResult,
|
|
8
|
+
AgentToolUpdateCallback,
|
|
9
|
+
CustomTool,
|
|
10
|
+
CustomToolAPI,
|
|
11
|
+
CustomToolContext,
|
|
12
|
+
CustomToolFactory,
|
|
13
|
+
CustomToolResult,
|
|
14
|
+
CustomToolSessionEvent,
|
|
15
|
+
CustomToolsLoadResult,
|
|
16
|
+
CustomToolUIContext,
|
|
17
|
+
ExecResult,
|
|
18
|
+
LoadedCustomTool,
|
|
19
|
+
RenderResultOptions,
|
|
20
|
+
} from "./types.js";
|
|
21
|
+
export { wrapCustomTool, wrapCustomTools } from "./wrapper.js";
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom tool loader - loads TypeScript tool modules using native Bun import.
|
|
3
|
+
*
|
|
4
|
+
* Dependencies (@sinclair/typebox and pi-coding-agent) are injected via the CustomToolAPI
|
|
5
|
+
* to avoid import resolution issues with custom tools loaded from user directories.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
import * as os from "node:os";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import * as typebox from "@sinclair/typebox";
|
|
12
|
+
import { getAgentDir } from "../../config.js";
|
|
13
|
+
import * as piCodingAgent from "../../index.js";
|
|
14
|
+
import { theme } from "../../modes/interactive/theme/theme.js";
|
|
15
|
+
import type { ExecOptions } from "../exec.js";
|
|
16
|
+
import { execCommand } from "../exec.js";
|
|
17
|
+
import type { HookUIContext } from "../hooks/types.js";
|
|
18
|
+
import { getAllPluginToolPaths } from "../plugins/loader.js";
|
|
19
|
+
import type { CustomToolAPI, CustomToolFactory, CustomToolsLoadResult, LoadedCustomTool } from "./types.js";
|
|
20
|
+
|
|
21
|
+
const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
|
22
|
+
|
|
23
|
+
function normalizeUnicodeSpaces(str: string): string {
|
|
24
|
+
return str.replace(UNICODE_SPACES, " ");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function expandPath(p: string): string {
|
|
28
|
+
const normalized = normalizeUnicodeSpaces(p);
|
|
29
|
+
if (normalized.startsWith("~/")) {
|
|
30
|
+
return path.join(os.homedir(), normalized.slice(2));
|
|
31
|
+
}
|
|
32
|
+
if (normalized.startsWith("~")) {
|
|
33
|
+
return path.join(os.homedir(), normalized.slice(1));
|
|
34
|
+
}
|
|
35
|
+
return normalized;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resolve tool path.
|
|
40
|
+
* - Absolute paths used as-is
|
|
41
|
+
* - Paths starting with ~ expanded to home directory
|
|
42
|
+
* - Relative paths resolved from cwd
|
|
43
|
+
*/
|
|
44
|
+
function resolveToolPath(toolPath: string, cwd: string): string {
|
|
45
|
+
const expanded = expandPath(toolPath);
|
|
46
|
+
|
|
47
|
+
if (path.isAbsolute(expanded)) {
|
|
48
|
+
return expanded;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Relative paths resolved from cwd
|
|
52
|
+
return path.resolve(cwd, expanded);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Create a no-op UI context for headless modes.
|
|
57
|
+
*/
|
|
58
|
+
function createNoOpUIContext(): HookUIContext {
|
|
59
|
+
return {
|
|
60
|
+
select: async () => undefined,
|
|
61
|
+
confirm: async () => false,
|
|
62
|
+
input: async () => undefined,
|
|
63
|
+
notify: () => {},
|
|
64
|
+
setStatus: () => {},
|
|
65
|
+
custom: async () => undefined as never,
|
|
66
|
+
setEditorText: () => {},
|
|
67
|
+
getEditorText: () => "",
|
|
68
|
+
editor: async () => undefined,
|
|
69
|
+
get theme() {
|
|
70
|
+
return theme;
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Load a single tool module using native Bun import.
|
|
77
|
+
*/
|
|
78
|
+
async function loadTool(
|
|
79
|
+
toolPath: string,
|
|
80
|
+
cwd: string,
|
|
81
|
+
sharedApi: CustomToolAPI,
|
|
82
|
+
): Promise<{ tools: LoadedCustomTool[] | null; error: string | null }> {
|
|
83
|
+
const resolvedPath = resolveToolPath(toolPath, cwd);
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const module = await import(resolvedPath);
|
|
87
|
+
const factory = (module.default ?? module) as CustomToolFactory;
|
|
88
|
+
|
|
89
|
+
if (typeof factory !== "function") {
|
|
90
|
+
return { tools: null, error: "Tool must export a default function" };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const toolResult = await factory(sharedApi);
|
|
94
|
+
const toolsArray = Array.isArray(toolResult) ? toolResult : [toolResult];
|
|
95
|
+
|
|
96
|
+
const loadedTools: LoadedCustomTool[] = toolsArray.map((tool) => ({
|
|
97
|
+
path: toolPath,
|
|
98
|
+
resolvedPath,
|
|
99
|
+
tool,
|
|
100
|
+
}));
|
|
101
|
+
|
|
102
|
+
return { tools: loadedTools, error: null };
|
|
103
|
+
} catch (err) {
|
|
104
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
105
|
+
return { tools: null, error: `Failed to load tool: ${message}` };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Load all tools from configuration.
|
|
111
|
+
* @param paths - Array of tool file paths
|
|
112
|
+
* @param cwd - Current working directory for resolving relative paths
|
|
113
|
+
* @param builtInToolNames - Names of built-in tools to check for conflicts
|
|
114
|
+
*/
|
|
115
|
+
export async function loadCustomTools(
|
|
116
|
+
paths: string[],
|
|
117
|
+
cwd: string,
|
|
118
|
+
builtInToolNames: string[],
|
|
119
|
+
): Promise<CustomToolsLoadResult> {
|
|
120
|
+
const tools: LoadedCustomTool[] = [];
|
|
121
|
+
const errors: Array<{ path: string; error: string }> = [];
|
|
122
|
+
const seenNames = new Set<string>(builtInToolNames);
|
|
123
|
+
|
|
124
|
+
// Shared API object - all tools get the same instance
|
|
125
|
+
const sharedApi: CustomToolAPI = {
|
|
126
|
+
cwd,
|
|
127
|
+
exec: (command: string, args: string[], options?: ExecOptions) =>
|
|
128
|
+
execCommand(command, args, options?.cwd ?? cwd, options),
|
|
129
|
+
ui: createNoOpUIContext(),
|
|
130
|
+
hasUI: false,
|
|
131
|
+
typebox,
|
|
132
|
+
pi: piCodingAgent,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
for (const toolPath of paths) {
|
|
136
|
+
const { tools: loadedTools, error } = await loadTool(toolPath, cwd, sharedApi);
|
|
137
|
+
|
|
138
|
+
if (error) {
|
|
139
|
+
errors.push({ path: toolPath, error });
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (loadedTools) {
|
|
144
|
+
for (const loadedTool of loadedTools) {
|
|
145
|
+
// Check for name conflicts
|
|
146
|
+
if (seenNames.has(loadedTool.tool.name)) {
|
|
147
|
+
errors.push({
|
|
148
|
+
path: toolPath,
|
|
149
|
+
error: `Tool name "${loadedTool.tool.name}" conflicts with existing tool`,
|
|
150
|
+
});
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
seenNames.add(loadedTool.tool.name);
|
|
155
|
+
tools.push(loadedTool);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
tools,
|
|
162
|
+
errors,
|
|
163
|
+
setUIContext(uiContext, hasUI) {
|
|
164
|
+
sharedApi.ui = uiContext;
|
|
165
|
+
sharedApi.hasUI = hasUI;
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Discover tool files from a directory.
|
|
172
|
+
* Only loads index.ts files from subdirectories (e.g., tools/mytool/index.ts).
|
|
173
|
+
*/
|
|
174
|
+
function discoverToolsInDir(dir: string): string[] {
|
|
175
|
+
if (!fs.existsSync(dir)) {
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const tools: string[] = [];
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
183
|
+
|
|
184
|
+
for (const entry of entries) {
|
|
185
|
+
if (entry.isDirectory() || entry.isSymbolicLink()) {
|
|
186
|
+
// Check for index.ts in subdirectory
|
|
187
|
+
const indexPath = path.join(dir, entry.name, "index.ts");
|
|
188
|
+
if (fs.existsSync(indexPath)) {
|
|
189
|
+
tools.push(indexPath);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
} catch {
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return tools;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Discover and load tools from standard locations:
|
|
202
|
+
* 1. agentDir/tools/*.ts (global)
|
|
203
|
+
* 2. cwd/.pi/tools/*.ts (project-local)
|
|
204
|
+
* 3. Installed plugins (~/.pi/plugins/node_modules/*)
|
|
205
|
+
*
|
|
206
|
+
* Plus any explicitly configured paths from settings or CLI.
|
|
207
|
+
*
|
|
208
|
+
* @param configuredPaths - Explicit paths from settings.json and CLI --tool flags
|
|
209
|
+
* @param cwd - Current working directory
|
|
210
|
+
* @param builtInToolNames - Names of built-in tools to check for conflicts
|
|
211
|
+
* @param agentDir - Agent config directory. Default: from getAgentDir()
|
|
212
|
+
*/
|
|
213
|
+
export async function discoverAndLoadCustomTools(
|
|
214
|
+
configuredPaths: string[],
|
|
215
|
+
cwd: string,
|
|
216
|
+
builtInToolNames: string[],
|
|
217
|
+
agentDir: string = getAgentDir(),
|
|
218
|
+
): Promise<CustomToolsLoadResult> {
|
|
219
|
+
const allPaths: string[] = [];
|
|
220
|
+
const seen = new Set<string>();
|
|
221
|
+
|
|
222
|
+
// Helper to add paths without duplicates
|
|
223
|
+
const addPaths = (paths: string[]) => {
|
|
224
|
+
for (const p of paths) {
|
|
225
|
+
const resolved = path.resolve(p);
|
|
226
|
+
if (!seen.has(resolved)) {
|
|
227
|
+
seen.add(resolved);
|
|
228
|
+
allPaths.push(p);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// 1. Global tools: agentDir/tools/
|
|
234
|
+
const globalToolsDir = path.join(agentDir, "tools");
|
|
235
|
+
addPaths(discoverToolsInDir(globalToolsDir));
|
|
236
|
+
|
|
237
|
+
// 2. Project-local tools: cwd/.pi/tools/
|
|
238
|
+
const localToolsDir = path.join(cwd, ".pi", "tools");
|
|
239
|
+
addPaths(discoverToolsInDir(localToolsDir));
|
|
240
|
+
|
|
241
|
+
// 3. Plugin tools: ~/.pi/plugins/node_modules/*/
|
|
242
|
+
addPaths(getAllPluginToolPaths(cwd));
|
|
243
|
+
|
|
244
|
+
// 4. Explicitly configured paths (can override/add)
|
|
245
|
+
addPaths(configuredPaths.map((p) => resolveToolPath(p, cwd)));
|
|
246
|
+
|
|
247
|
+
return loadCustomTools(allPaths, cwd, builtInToolNames);
|
|
248
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom tool types.
|
|
3
|
+
*
|
|
4
|
+
* Custom tools are TypeScript modules that define additional tools for the agent.
|
|
5
|
+
* They can provide custom rendering for tool calls and results in the TUI.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
9
|
+
import type { Model } from "@oh-my-pi/pi-ai";
|
|
10
|
+
import type { Component } from "@oh-my-pi/pi-tui";
|
|
11
|
+
import type { Static, TSchema } from "@sinclair/typebox";
|
|
12
|
+
import type { Theme } from "../../modes/interactive/theme/theme.js";
|
|
13
|
+
import type { ExecOptions, ExecResult } from "../exec.js";
|
|
14
|
+
import type { HookUIContext } from "../hooks/types.js";
|
|
15
|
+
import type { ModelRegistry } from "../model-registry.js";
|
|
16
|
+
import type { ReadonlySessionManager } from "../session-manager.js";
|
|
17
|
+
|
|
18
|
+
/** Alias for clarity */
|
|
19
|
+
export type CustomToolUIContext = HookUIContext;
|
|
20
|
+
|
|
21
|
+
/** Re-export for custom tools to use in execute signature */
|
|
22
|
+
export type { AgentToolResult, AgentToolUpdateCallback };
|
|
23
|
+
|
|
24
|
+
// Re-export for backward compatibility
|
|
25
|
+
export type { ExecOptions, ExecResult } from "../exec.js";
|
|
26
|
+
|
|
27
|
+
/** API passed to custom tool factory (stable across session changes) */
|
|
28
|
+
export interface CustomToolAPI {
|
|
29
|
+
/** Current working directory */
|
|
30
|
+
cwd: string;
|
|
31
|
+
/** Execute a command */
|
|
32
|
+
exec(command: string, args: string[], options?: ExecOptions): Promise<ExecResult>;
|
|
33
|
+
/** UI methods for user interaction (select, confirm, input, notify, custom) */
|
|
34
|
+
ui: CustomToolUIContext;
|
|
35
|
+
/** Whether UI is available (false in print/RPC mode) */
|
|
36
|
+
hasUI: boolean;
|
|
37
|
+
/** Injected @sinclair/typebox module */
|
|
38
|
+
typebox: typeof import("@sinclair/typebox");
|
|
39
|
+
/** Injected pi-coding-agent exports */
|
|
40
|
+
pi: typeof import("../../index.js");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Context passed to tool execute and onSession callbacks.
|
|
45
|
+
* Provides access to session state and model information.
|
|
46
|
+
*/
|
|
47
|
+
export interface CustomToolContext {
|
|
48
|
+
/** Session manager (read-only) */
|
|
49
|
+
sessionManager: ReadonlySessionManager;
|
|
50
|
+
/** Model registry - use for API key resolution and model retrieval */
|
|
51
|
+
modelRegistry: ModelRegistry;
|
|
52
|
+
/** Current model (may be undefined if no model is selected yet) */
|
|
53
|
+
model: Model<any> | undefined;
|
|
54
|
+
/** Whether the agent is idle (not streaming) */
|
|
55
|
+
isIdle(): boolean;
|
|
56
|
+
/** Whether there are queued messages waiting to be processed */
|
|
57
|
+
hasQueuedMessages(): boolean;
|
|
58
|
+
/** Abort the current agent operation (fire-and-forget, does not wait) */
|
|
59
|
+
abort(): void;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Session event passed to onSession callback */
|
|
63
|
+
export interface CustomToolSessionEvent {
|
|
64
|
+
/** Reason for the session event */
|
|
65
|
+
reason: "start" | "switch" | "branch" | "tree" | "shutdown";
|
|
66
|
+
/** Previous session file path, or undefined for "start" and "shutdown" */
|
|
67
|
+
previousSessionFile: string | undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Rendering options passed to renderResult */
|
|
71
|
+
export interface RenderResultOptions {
|
|
72
|
+
/** Whether the result view is expanded */
|
|
73
|
+
expanded: boolean;
|
|
74
|
+
/** Whether this is a partial/streaming result */
|
|
75
|
+
isPartial: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type CustomToolResult<TDetails = any> = AgentToolResult<TDetails>;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Custom tool definition.
|
|
82
|
+
*
|
|
83
|
+
* Custom tools are standalone - they don't extend AgentTool directly.
|
|
84
|
+
* When loaded, they are wrapped in an AgentTool for the agent to use.
|
|
85
|
+
*
|
|
86
|
+
* The execute callback receives a ToolContext with access to session state,
|
|
87
|
+
* model registry, and current model.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```typescript
|
|
91
|
+
* const factory: CustomToolFactory = (pi) => ({
|
|
92
|
+
* name: "my_tool",
|
|
93
|
+
* label: "My Tool",
|
|
94
|
+
* description: "Does something useful",
|
|
95
|
+
* parameters: Type.Object({ input: Type.String() }),
|
|
96
|
+
*
|
|
97
|
+
* async execute(toolCallId, params, onUpdate, ctx, signal) {
|
|
98
|
+
* // Access session state via ctx.sessionManager
|
|
99
|
+
* // Access model registry via ctx.modelRegistry
|
|
100
|
+
* // Current model via ctx.model
|
|
101
|
+
* return { content: [{ type: "text", text: "Done" }] };
|
|
102
|
+
* },
|
|
103
|
+
*
|
|
104
|
+
* onSession(event, ctx) {
|
|
105
|
+
* if (event.reason === "shutdown") {
|
|
106
|
+
* // Cleanup
|
|
107
|
+
* }
|
|
108
|
+
* // Reconstruct state from ctx.sessionManager.getEntries()
|
|
109
|
+
* }
|
|
110
|
+
* });
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
export interface CustomTool<TParams extends TSchema = TSchema, TDetails = any> {
|
|
114
|
+
/** Tool name (used in LLM tool calls) */
|
|
115
|
+
name: string;
|
|
116
|
+
/** Human-readable label for UI */
|
|
117
|
+
label: string;
|
|
118
|
+
/** Description for LLM */
|
|
119
|
+
description: string;
|
|
120
|
+
/** Parameter schema (TypeBox) */
|
|
121
|
+
parameters: TParams;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Execute the tool.
|
|
125
|
+
* @param toolCallId - Unique ID for this tool call
|
|
126
|
+
* @param params - Parsed parameters matching the schema
|
|
127
|
+
* @param onUpdate - Callback for streaming partial results (for UI, not LLM)
|
|
128
|
+
* @param ctx - Context with session manager, model registry, and current model
|
|
129
|
+
* @param signal - Optional abort signal for cancellation
|
|
130
|
+
*/
|
|
131
|
+
execute(
|
|
132
|
+
toolCallId: string,
|
|
133
|
+
params: Static<TParams>,
|
|
134
|
+
onUpdate: AgentToolUpdateCallback<TDetails> | undefined,
|
|
135
|
+
ctx: CustomToolContext,
|
|
136
|
+
signal?: AbortSignal,
|
|
137
|
+
): Promise<AgentToolResult<TDetails>>;
|
|
138
|
+
|
|
139
|
+
/** Called on session lifecycle events - use to reconstruct state or cleanup resources */
|
|
140
|
+
onSession?: (event: CustomToolSessionEvent, ctx: CustomToolContext) => void | Promise<void>;
|
|
141
|
+
/** Custom rendering for tool call display - return a Component */
|
|
142
|
+
renderCall?: (args: Static<TParams>, theme: Theme) => Component;
|
|
143
|
+
|
|
144
|
+
/** Custom rendering for tool result display - return a Component */
|
|
145
|
+
renderResult?: (result: CustomToolResult<TDetails>, options: RenderResultOptions, theme: Theme) => Component;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Factory function that creates a custom tool or array of tools */
|
|
149
|
+
export type CustomToolFactory = (
|
|
150
|
+
pi: CustomToolAPI,
|
|
151
|
+
) => CustomTool<any, any> | CustomTool<any, any>[] | Promise<CustomTool<any, any> | CustomTool<any, any>[]>;
|
|
152
|
+
|
|
153
|
+
/** Loaded custom tool with metadata and wrapped AgentTool */
|
|
154
|
+
export interface LoadedCustomTool {
|
|
155
|
+
/** Original path (as specified) */
|
|
156
|
+
path: string;
|
|
157
|
+
/** Resolved absolute path */
|
|
158
|
+
resolvedPath: string;
|
|
159
|
+
/** The original custom tool instance */
|
|
160
|
+
tool: CustomTool;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Result from loading custom tools */
|
|
164
|
+
export interface CustomToolsLoadResult {
|
|
165
|
+
tools: LoadedCustomTool[];
|
|
166
|
+
errors: Array<{ path: string; error: string }>;
|
|
167
|
+
/** Update the UI context for all loaded tools. Call when mode initializes. */
|
|
168
|
+
setUIContext(uiContext: CustomToolUIContext, hasUI: boolean): void;
|
|
169
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wraps CustomTool instances into AgentTool for use with the agent.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
6
|
+
import type { CustomTool, CustomToolContext, LoadedCustomTool } from "./types.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Wrap a CustomTool into an AgentTool.
|
|
10
|
+
* The wrapper injects the ToolContext into execute calls.
|
|
11
|
+
*/
|
|
12
|
+
export function wrapCustomTool(tool: CustomTool, getContext: () => CustomToolContext): AgentTool {
|
|
13
|
+
return {
|
|
14
|
+
name: tool.name,
|
|
15
|
+
label: tool.label,
|
|
16
|
+
description: tool.description,
|
|
17
|
+
parameters: tool.parameters,
|
|
18
|
+
execute: (toolCallId, params, signal, onUpdate, context) =>
|
|
19
|
+
tool.execute(toolCallId, params, onUpdate, context ?? getContext(), signal),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Wrap all loaded custom tools into AgentTools.
|
|
25
|
+
*/
|
|
26
|
+
export function wrapCustomTools(loadedTools: LoadedCustomTool[], getContext: () => CustomToolContext): AgentTool[] {
|
|
27
|
+
return loadedTools.map((lt) => wrapCustomTool(lt.tool, getContext));
|
|
28
|
+
}
|