@oh-my-pi/pi-coding-agent 1.340.0 → 2.0.1337
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 +115 -1
- package/README.md +1 -1
- package/examples/custom-tools/subagent/index.ts +1 -1
- package/package.json +5 -3
- package/src/cli/args.ts +13 -6
- package/src/cli/file-processor.ts +3 -3
- package/src/cli/list-models.ts +2 -2
- package/src/cli/plugin-cli.ts +1 -1
- package/src/cli/session-picker.ts +2 -2
- package/src/cli.ts +1 -1
- package/src/config.ts +3 -3
- package/src/core/agent-session.ts +189 -29
- package/src/core/bash-executor.ts +50 -10
- package/src/core/compaction/branch-summarization.ts +5 -5
- package/src/core/compaction/compaction.ts +3 -3
- package/src/core/compaction/index.ts +3 -3
- package/src/core/custom-commands/bundled/review/index.ts +156 -0
- package/src/core/custom-commands/index.ts +15 -0
- package/src/core/custom-commands/loader.ts +232 -0
- package/src/core/custom-commands/types.ts +112 -0
- package/src/core/custom-tools/index.ts +3 -3
- package/src/core/custom-tools/loader.ts +10 -8
- package/src/core/custom-tools/types.ts +11 -6
- package/src/core/custom-tools/wrapper.ts +2 -1
- package/src/core/exec.ts +22 -12
- package/src/core/export-html/index.ts +5 -5
- package/src/core/file-mentions.ts +54 -0
- package/src/core/hooks/index.ts +5 -5
- package/src/core/hooks/loader.ts +21 -16
- package/src/core/hooks/runner.ts +6 -6
- package/src/core/hooks/tool-wrapper.ts +2 -2
- package/src/core/hooks/types.ts +12 -15
- package/src/core/index.ts +6 -6
- package/src/core/logger.ts +112 -0
- package/src/core/mcp/client.ts +3 -3
- package/src/core/mcp/config.ts +1 -1
- package/src/core/mcp/index.ts +12 -12
- package/src/core/mcp/loader.ts +2 -2
- package/src/core/mcp/manager.ts +6 -6
- package/src/core/mcp/tool-bridge.ts +3 -3
- package/src/core/mcp/transports/http.ts +1 -1
- package/src/core/mcp/transports/index.ts +2 -2
- package/src/core/mcp/transports/stdio.ts +1 -1
- package/src/core/messages.ts +22 -0
- package/src/core/model-registry.ts +2 -2
- package/src/core/model-resolver.ts +103 -2
- package/src/core/plugins/doctor.ts +1 -1
- package/src/core/plugins/index.ts +6 -6
- package/src/core/plugins/installer.ts +4 -4
- package/src/core/plugins/loader.ts +4 -9
- package/src/core/plugins/manager.ts +5 -5
- package/src/core/plugins/paths.ts +3 -3
- package/src/core/sdk.ts +127 -52
- package/src/core/session-manager.ts +123 -20
- package/src/core/settings-manager.ts +106 -22
- package/src/core/skills.ts +5 -5
- package/src/core/slash-commands.ts +60 -45
- package/src/core/system-prompt.ts +6 -6
- package/src/core/title-generator.ts +94 -0
- package/src/core/tools/bash.ts +33 -157
- package/src/core/tools/context.ts +2 -2
- package/src/core/tools/edit-diff.ts +5 -5
- package/src/core/tools/edit.ts +60 -9
- package/src/core/tools/exa/company.ts +3 -3
- package/src/core/tools/exa/index.ts +16 -17
- package/src/core/tools/exa/linkedin.ts +3 -3
- package/src/core/tools/exa/mcp-client.ts +9 -9
- package/src/core/tools/exa/render.ts +5 -5
- package/src/core/tools/exa/researcher.ts +3 -3
- package/src/core/tools/exa/search.ts +6 -5
- package/src/core/tools/exa/types.ts +5 -6
- package/src/core/tools/exa/websets.ts +3 -3
- package/src/core/tools/find.ts +3 -3
- package/src/core/tools/grep.ts +6 -5
- package/src/core/tools/index.ts +114 -40
- package/src/core/tools/ls.ts +4 -4
- package/src/core/tools/lsp/client.ts +204 -108
- package/src/core/tools/lsp/config.ts +709 -35
- package/src/core/tools/lsp/edits.ts +2 -2
- package/src/core/tools/lsp/index.ts +432 -30
- package/src/core/tools/lsp/render.ts +2 -2
- package/src/core/tools/lsp/rust-analyzer.ts +3 -3
- package/src/core/tools/lsp/types.ts +5 -0
- package/src/core/tools/lsp/utils.ts +1 -1
- package/src/core/tools/notebook.ts +1 -1
- package/src/core/tools/output.ts +175 -0
- package/src/core/tools/read.ts +7 -7
- package/src/core/tools/renderers.ts +92 -13
- package/src/core/tools/review.ts +268 -0
- package/src/core/tools/task/agents.ts +1 -1
- package/src/core/tools/task/bundled-agents/explore.md +1 -1
- package/src/core/tools/task/bundled-agents/reviewer.md +53 -38
- package/src/core/tools/task/discovery.ts +2 -2
- package/src/core/tools/task/executor.ts +145 -28
- package/src/core/tools/task/index.ts +78 -30
- package/src/core/tools/task/model-resolver.ts +72 -13
- package/src/core/tools/task/parallel.ts +1 -1
- package/src/core/tools/task/render.ts +219 -30
- package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
- package/src/core/tools/task/types.ts +36 -2
- package/src/core/tools/web-fetch.ts +5 -3
- package/src/core/tools/web-search/auth.ts +1 -1
- package/src/core/tools/web-search/index.ts +17 -15
- package/src/core/tools/web-search/providers/anthropic.ts +2 -2
- package/src/core/tools/web-search/providers/exa.ts +3 -5
- package/src/core/tools/web-search/providers/perplexity.ts +1 -1
- package/src/core/tools/web-search/render.ts +3 -3
- package/src/core/tools/write.ts +70 -7
- package/src/index.ts +33 -17
- package/src/main.ts +60 -34
- package/src/migrations.ts +3 -3
- package/src/modes/index.ts +5 -5
- package/src/modes/interactive/components/armin.ts +1 -1
- package/src/modes/interactive/components/assistant-message.ts +1 -1
- package/src/modes/interactive/components/bash-execution.ts +4 -4
- package/src/modes/interactive/components/bordered-loader.ts +2 -2
- package/src/modes/interactive/components/branch-summary-message.ts +2 -2
- package/src/modes/interactive/components/compaction-summary-message.ts +2 -2
- package/src/modes/interactive/components/diff.ts +1 -1
- package/src/modes/interactive/components/dynamic-border.ts +1 -1
- package/src/modes/interactive/components/footer.ts +5 -5
- package/src/modes/interactive/components/hook-editor.ts +2 -2
- package/src/modes/interactive/components/hook-input.ts +2 -2
- package/src/modes/interactive/components/hook-message.ts +3 -3
- package/src/modes/interactive/components/hook-selector.ts +2 -2
- package/src/modes/interactive/components/model-selector.ts +341 -41
- package/src/modes/interactive/components/oauth-selector.ts +3 -3
- package/src/modes/interactive/components/plugin-settings.ts +4 -4
- package/src/modes/interactive/components/queue-mode-selector.ts +2 -2
- package/src/modes/interactive/components/session-selector.ts +24 -11
- package/src/modes/interactive/components/settings-defs.ts +51 -3
- package/src/modes/interactive/components/settings-selector.ts +13 -16
- package/src/modes/interactive/components/show-images-selector.ts +2 -2
- package/src/modes/interactive/components/theme-selector.ts +2 -2
- package/src/modes/interactive/components/thinking-selector.ts +2 -2
- package/src/modes/interactive/components/tool-execution.ts +44 -8
- package/src/modes/interactive/components/tree-selector.ts +5 -5
- package/src/modes/interactive/components/user-message-selector.ts +2 -2
- package/src/modes/interactive/components/user-message.ts +1 -1
- package/src/modes/interactive/components/welcome.ts +42 -5
- package/src/modes/interactive/interactive-mode.ts +169 -48
- package/src/modes/interactive/theme/theme.ts +8 -7
- package/src/modes/print-mode.ts +4 -3
- package/src/modes/rpc/rpc-client.ts +4 -4
- package/src/modes/rpc/rpc-mode.ts +21 -11
- package/src/modes/rpc/rpc-types.ts +3 -3
- package/src/utils/changelog.ts +2 -2
- package/src/utils/clipboard.ts +1 -1
- package/src/utils/shell-snapshot.ts +218 -0
- package/src/utils/shell.ts +93 -13
- package/src/utils/tools-manager.ts +1 -1
- package/examples/custom-tools/subagent/agents/reviewer.md +0 -35
- package/src/core/tools/exa/logger.ts +0 -56
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate session titles using a smol, fast model.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Model } from "@oh-my-pi/pi-ai";
|
|
6
|
+
import { completeSimple } from "@oh-my-pi/pi-ai";
|
|
7
|
+
import type { ModelRegistry } from "./model-registry";
|
|
8
|
+
import { findSmolModel } from "./model-resolver";
|
|
9
|
+
|
|
10
|
+
const TITLE_SYSTEM_PROMPT = `Generate a very short title (3-6 words) for a coding session based on the user's first message. The title should capture the main task or topic. Output ONLY the title, nothing else. No quotes, no punctuation at the end.
|
|
11
|
+
|
|
12
|
+
Examples:
|
|
13
|
+
- "Fix TypeScript compilation errors"
|
|
14
|
+
- "Add user authentication"
|
|
15
|
+
- "Refactor database queries"
|
|
16
|
+
- "Debug payment webhook"
|
|
17
|
+
- "Update React components"`;
|
|
18
|
+
|
|
19
|
+
const MAX_INPUT_CHARS = 2000;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Find the best available model for title generation.
|
|
23
|
+
* Uses the configured smol model if set, otherwise auto-discovers using priority chain.
|
|
24
|
+
*
|
|
25
|
+
* @param registry Model registry
|
|
26
|
+
* @param savedSmolModel Optional saved smol model from settings (provider/modelId format)
|
|
27
|
+
*/
|
|
28
|
+
export async function findTitleModel(registry: ModelRegistry, savedSmolModel?: string): Promise<Model<any> | null> {
|
|
29
|
+
const model = await findSmolModel(registry, savedSmolModel);
|
|
30
|
+
return model ?? null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Generate a title for a session based on the first user message.
|
|
35
|
+
*
|
|
36
|
+
* @param firstMessage The first user message
|
|
37
|
+
* @param registry Model registry
|
|
38
|
+
* @param savedSmolModel Optional saved smol model from settings (provider/modelId format)
|
|
39
|
+
*/
|
|
40
|
+
export async function generateSessionTitle(
|
|
41
|
+
firstMessage: string,
|
|
42
|
+
registry: ModelRegistry,
|
|
43
|
+
savedSmolModel?: string,
|
|
44
|
+
): Promise<string | null> {
|
|
45
|
+
const model = await findTitleModel(registry, savedSmolModel);
|
|
46
|
+
if (!model) return null;
|
|
47
|
+
|
|
48
|
+
const apiKey = await registry.getApiKey(model);
|
|
49
|
+
if (!apiKey) return null;
|
|
50
|
+
|
|
51
|
+
// Truncate message if too long
|
|
52
|
+
const truncatedMessage =
|
|
53
|
+
firstMessage.length > MAX_INPUT_CHARS ? `${firstMessage.slice(0, MAX_INPUT_CHARS)}...` : firstMessage;
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const response = await completeSimple(
|
|
57
|
+
model,
|
|
58
|
+
{
|
|
59
|
+
systemPrompt: TITLE_SYSTEM_PROMPT,
|
|
60
|
+
messages: [{ role: "user", content: truncatedMessage, timestamp: Date.now() }],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
apiKey,
|
|
64
|
+
maxTokens: 30,
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// Extract title from response text content
|
|
69
|
+
let title = "";
|
|
70
|
+
for (const content of response.content) {
|
|
71
|
+
if (content.type === "text") {
|
|
72
|
+
title += content.text;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
title = title.trim();
|
|
76
|
+
|
|
77
|
+
if (!title || title.length > 60) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Clean up: remove quotes, trailing punctuation
|
|
82
|
+
return title.replace(/^["']|["']$/g, "").replace(/[.!?]$/, "");
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Set the terminal title using ANSI escape sequences.
|
|
90
|
+
*/
|
|
91
|
+
export function setTerminalTitle(title: string): void {
|
|
92
|
+
// OSC 2 sets the window title
|
|
93
|
+
process.stdout.write(`\x1b]2;${title}\x07`);
|
|
94
|
+
}
|
package/src/core/tools/bash.ts
CHANGED
|
@@ -1,20 +1,7 @@
|
|
|
1
|
-
import { createWriteStream } from "node:fs";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
1
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
5
2
|
import { Type } from "@sinclair/typebox";
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateTail } from "./truncate.js";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Generate a unique temp file path for bash output
|
|
12
|
-
*/
|
|
13
|
-
function getTempFilePath(): string {
|
|
14
|
-
const randomId = crypto.getRandomValues(new Uint8Array(8));
|
|
15
|
-
const id = Array.from(randomId, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
16
|
-
return join(tmpdir(), `pi-bash-${id}.log`);
|
|
17
|
-
}
|
|
3
|
+
import { executeBash } from "../bash-executor";
|
|
4
|
+
import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateTail } from "./truncate";
|
|
18
5
|
|
|
19
6
|
const bashSchema = Type.Object({
|
|
20
7
|
command: Type.String({ description: "Bash command to execute" }),
|
|
@@ -66,8 +53,7 @@ Usage notes:
|
|
|
66
53
|
- If the commands are independent and can run in parallel, make multiple bash tool calls in a single message. For example, if you need to run "git status" and "git diff", send a single message with two bash tool calls in parallel.
|
|
67
54
|
- If the commands depend on each other and must run sequentially, use a single bash call with '&&' to chain them together (e.g., \`git add . && git commit -m "message" && git push\`). For instance, if one operation must complete before another starts (like mkdir before cp, Write before Bash for git operations, or git add before git commit), run these operations sequentially instead.
|
|
68
55
|
- Use ';' only when you need to run commands sequentially but don't care if earlier commands fail
|
|
69
|
-
- DO NOT use newlines to separate commands (newlines are ok in quoted strings)
|
|
70
|
-
- Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of \`cd\`. You may use \`cd\` if the User explicitly requests it.`,
|
|
56
|
+
- DO NOT use newlines to separate commands (newlines are ok in quoted strings)`,
|
|
71
57
|
parameters: bashSchema,
|
|
72
58
|
execute: async (
|
|
73
59
|
_toolCallId: string,
|
|
@@ -75,140 +61,34 @@ Usage notes:
|
|
|
75
61
|
signal?: AbortSignal,
|
|
76
62
|
onUpdate?,
|
|
77
63
|
) => {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
cwd,
|
|
81
|
-
stdin: "ignore",
|
|
82
|
-
stdout: "pipe",
|
|
83
|
-
stderr: "pipe",
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
// We'll stream to a temp file if output gets large
|
|
87
|
-
let tempFilePath: string | undefined;
|
|
88
|
-
let tempFileStream: ReturnType<typeof createWriteStream> | undefined;
|
|
89
|
-
let totalBytes = 0;
|
|
90
|
-
|
|
91
|
-
// Keep a rolling buffer of the last chunks for tail truncation
|
|
92
|
-
const chunks: Buffer[] = [];
|
|
93
|
-
let chunksBytes = 0;
|
|
94
|
-
const maxChunksBytes = DEFAULT_MAX_BYTES * 2;
|
|
95
|
-
|
|
96
|
-
let timedOut = false;
|
|
97
|
-
let aborted = false;
|
|
98
|
-
|
|
99
|
-
// Handle abort signal
|
|
100
|
-
const onAbort = () => {
|
|
101
|
-
aborted = true;
|
|
102
|
-
if (child.pid) {
|
|
103
|
-
killProcessTree(child.pid);
|
|
104
|
-
}
|
|
105
|
-
};
|
|
64
|
+
// Track output for streaming updates
|
|
65
|
+
let currentOutput = "";
|
|
106
66
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}, timeout * 1000);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const handleData = (data: Buffer) => {
|
|
125
|
-
totalBytes += data.length;
|
|
126
|
-
|
|
127
|
-
// Start writing to temp file once we exceed the threshold
|
|
128
|
-
if (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) {
|
|
129
|
-
tempFilePath = getTempFilePath();
|
|
130
|
-
tempFileStream = createWriteStream(tempFilePath);
|
|
131
|
-
for (const chunk of chunks) {
|
|
132
|
-
tempFileStream.write(chunk);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (tempFileStream) {
|
|
137
|
-
tempFileStream.write(data);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Keep rolling buffer of recent data
|
|
141
|
-
chunks.push(data);
|
|
142
|
-
chunksBytes += data.length;
|
|
143
|
-
|
|
144
|
-
while (chunksBytes > maxChunksBytes && chunks.length > 1) {
|
|
145
|
-
const removed = chunks.shift()!;
|
|
146
|
-
chunksBytes -= removed.length;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Stream partial output to callback
|
|
150
|
-
if (onUpdate) {
|
|
151
|
-
const fullBuffer = Buffer.concat(chunks);
|
|
152
|
-
const fullText = fullBuffer.toString("utf-8");
|
|
153
|
-
const truncation = truncateTail(fullText);
|
|
154
|
-
onUpdate({
|
|
155
|
-
content: [{ type: "text", text: truncation.content || "" }],
|
|
156
|
-
details: {
|
|
157
|
-
truncation: truncation.truncated ? truncation : undefined,
|
|
158
|
-
fullOutputPath: tempFilePath,
|
|
159
|
-
},
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
// Read streams using Bun's ReadableStream API
|
|
165
|
-
const stdoutReader = (child.stdout as ReadableStream<Uint8Array>).getReader();
|
|
166
|
-
const stderrReader = (child.stderr as ReadableStream<Uint8Array>).getReader();
|
|
167
|
-
|
|
168
|
-
await Promise.all([
|
|
169
|
-
(async () => {
|
|
170
|
-
while (true) {
|
|
171
|
-
const { done, value } = await stdoutReader.read();
|
|
172
|
-
if (done) break;
|
|
173
|
-
handleData(Buffer.from(value));
|
|
174
|
-
}
|
|
175
|
-
})(),
|
|
176
|
-
(async () => {
|
|
177
|
-
while (true) {
|
|
178
|
-
const { done, value } = await stderrReader.read();
|
|
179
|
-
if (done) break;
|
|
180
|
-
handleData(Buffer.from(value));
|
|
67
|
+
const result = await executeBash(command, {
|
|
68
|
+
cwd,
|
|
69
|
+
timeout: timeout ? timeout * 1000 : undefined, // Convert to milliseconds
|
|
70
|
+
signal,
|
|
71
|
+
onChunk: (chunk) => {
|
|
72
|
+
currentOutput += chunk;
|
|
73
|
+
if (onUpdate) {
|
|
74
|
+
const truncation = truncateTail(currentOutput);
|
|
75
|
+
onUpdate({
|
|
76
|
+
content: [{ type: "text", text: truncation.content || "" }],
|
|
77
|
+
details: {
|
|
78
|
+
truncation: truncation.truncated ? truncation : undefined,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
181
81
|
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const exitCode = await child.exited;
|
|
186
|
-
|
|
187
|
-
// Cleanup
|
|
188
|
-
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
189
|
-
if (signal) signal.removeEventListener("abort", onAbort);
|
|
190
|
-
if (tempFileStream) tempFileStream.end();
|
|
191
|
-
|
|
192
|
-
// Combine all buffered chunks
|
|
193
|
-
const fullBuffer = Buffer.concat(chunks);
|
|
194
|
-
const fullOutput = fullBuffer.toString("utf-8");
|
|
195
|
-
|
|
196
|
-
if (aborted && !timedOut) {
|
|
197
|
-
let output = fullOutput;
|
|
198
|
-
if (output) output += "\n\n";
|
|
199
|
-
output += "Command aborted";
|
|
200
|
-
throw new Error(output);
|
|
201
|
-
}
|
|
82
|
+
},
|
|
83
|
+
});
|
|
202
84
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
output += `Command timed out after ${timeout} seconds`;
|
|
207
|
-
throw new Error(output);
|
|
85
|
+
// Handle errors
|
|
86
|
+
if (result.cancelled) {
|
|
87
|
+
throw new Error(result.output || "Command aborted");
|
|
208
88
|
}
|
|
209
89
|
|
|
210
|
-
// Apply tail truncation
|
|
211
|
-
const truncation = truncateTail(
|
|
90
|
+
// Apply tail truncation for final output
|
|
91
|
+
const truncation = truncateTail(result.output);
|
|
212
92
|
let outputText = truncation.content || "(no output)";
|
|
213
93
|
|
|
214
94
|
let details: BashToolDetails | undefined;
|
|
@@ -216,28 +96,24 @@ Usage notes:
|
|
|
216
96
|
if (truncation.truncated) {
|
|
217
97
|
details = {
|
|
218
98
|
truncation,
|
|
219
|
-
fullOutputPath:
|
|
99
|
+
fullOutputPath: result.fullOutputPath,
|
|
220
100
|
};
|
|
221
101
|
|
|
222
102
|
const startLine = truncation.totalLines - truncation.outputLines + 1;
|
|
223
103
|
const endLine = truncation.totalLines;
|
|
224
104
|
|
|
225
105
|
if (truncation.lastLinePartial) {
|
|
226
|
-
const lastLineSize = formatSize(Buffer.byteLength(
|
|
227
|
-
outputText += `\n\n[Showing last ${formatSize(
|
|
228
|
-
truncation.outputBytes,
|
|
229
|
-
)} of line ${endLine} (line is ${lastLineSize}). Full output: ${tempFilePath}]`;
|
|
106
|
+
const lastLineSize = formatSize(Buffer.byteLength(result.output.split("\n").pop() || "", "utf-8"));
|
|
107
|
+
outputText += `\n\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${result.fullOutputPath}]`;
|
|
230
108
|
} else if (truncation.truncatedBy === "lines") {
|
|
231
|
-
outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${
|
|
109
|
+
outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${result.fullOutputPath}]`;
|
|
232
110
|
} else {
|
|
233
|
-
outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(
|
|
234
|
-
DEFAULT_MAX_BYTES,
|
|
235
|
-
)} limit). Full output: ${tempFilePath}]`;
|
|
111
|
+
outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${result.fullOutputPath}]`;
|
|
236
112
|
}
|
|
237
113
|
}
|
|
238
114
|
|
|
239
|
-
if (exitCode !== 0 && exitCode !==
|
|
240
|
-
outputText += `\n\nCommand exited with code ${exitCode}`;
|
|
115
|
+
if (result.exitCode !== 0 && result.exitCode !== undefined) {
|
|
116
|
+
outputText += `\n\nCommand exited with code ${result.exitCode}`;
|
|
241
117
|
throw new Error(outputText);
|
|
242
118
|
}
|
|
243
119
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AgentToolContext } from "@oh-my-pi/pi-agent-core";
|
|
2
|
-
import type { CustomToolContext } from "../custom-tools/types
|
|
3
|
-
import type { HookUIContext } from "../hooks/types
|
|
2
|
+
import type { CustomToolContext } from "../custom-tools/types";
|
|
3
|
+
import type { HookUIContext } from "../hooks/types";
|
|
4
4
|
|
|
5
5
|
declare module "@oh-my-pi/pi-agent-core" {
|
|
6
6
|
interface AgentToolContext extends CustomToolContext {
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* Used by both edit.ts (for execution) and tool-execution.ts (for preview rendering).
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { constants } from "node:fs";
|
|
7
|
+
import { access, readFile } from "node:fs/promises";
|
|
6
8
|
import * as Diff from "diff";
|
|
7
|
-
import {
|
|
8
|
-
import { access, readFile } from "fs/promises";
|
|
9
|
-
import { resolveToCwd } from "./path-utils.js";
|
|
9
|
+
import { resolveToCwd } from "./path-utils";
|
|
10
10
|
|
|
11
11
|
export function detectLineEnding(content: string): "\r\n" | "\n" {
|
|
12
12
|
const crlfIdx = content.indexOf("\r\n");
|
|
@@ -271,7 +271,7 @@ export function formatEditMatchError(
|
|
|
271
271
|
? options.fuzzyMatches && options.fuzzyMatches > 1
|
|
272
272
|
? `Found ${options.fuzzyMatches} high-confidence matches. Provide more context to make it unique.`
|
|
273
273
|
: `Closest match was below the ${thresholdPercent}% similarity threshold.`
|
|
274
|
-
: "
|
|
274
|
+
: "Fuzzy matching is disabled. Enable 'Edit fuzzy match' in settings to accept high-confidence matches.";
|
|
275
275
|
|
|
276
276
|
return [
|
|
277
277
|
options.allowFuzzy
|
|
@@ -409,7 +409,7 @@ export async function computeEditDiff(
|
|
|
409
409
|
oldText: string,
|
|
410
410
|
newText: string,
|
|
411
411
|
cwd: string,
|
|
412
|
-
fuzzy =
|
|
412
|
+
fuzzy = true,
|
|
413
413
|
): Promise<EditDiffResult | EditDiffError> {
|
|
414
414
|
const absolutePath = resolveToCwd(path, cwd);
|
|
415
415
|
|
package/src/core/tools/edit.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { access, readFile, writeFile } from "node:fs/promises";
|
|
1
3
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
2
4
|
import { Type } from "@sinclair/typebox";
|
|
3
|
-
import { constants } from "fs";
|
|
4
|
-
import { access, readFile, writeFile } from "fs/promises";
|
|
5
5
|
import {
|
|
6
6
|
DEFAULT_FUZZY_THRESHOLD,
|
|
7
7
|
detectLineEnding,
|
|
@@ -11,8 +11,9 @@ import {
|
|
|
11
11
|
normalizeToLF,
|
|
12
12
|
restoreLineEndings,
|
|
13
13
|
stripBom,
|
|
14
|
-
} from "./edit-diff
|
|
15
|
-
import {
|
|
14
|
+
} from "./edit-diff";
|
|
15
|
+
import type { FileDiagnosticsResult } from "./lsp/index";
|
|
16
|
+
import { resolveToCwd } from "./path-utils";
|
|
16
17
|
|
|
17
18
|
const editSchema = Type.Object({
|
|
18
19
|
path: Type.String({ description: "Path to the file to edit (relative or absolute)" }),
|
|
@@ -27,9 +28,21 @@ export interface EditToolDetails {
|
|
|
27
28
|
diff: string;
|
|
28
29
|
/** Line number of the first change in the new file (for editor navigation) */
|
|
29
30
|
firstChangedLine?: number;
|
|
31
|
+
/** Whether LSP diagnostics were retrieved */
|
|
32
|
+
hasDiagnostics?: boolean;
|
|
33
|
+
/** Diagnostic result (if available) */
|
|
34
|
+
diagnostics?: FileDiagnosticsResult;
|
|
30
35
|
}
|
|
31
36
|
|
|
32
|
-
export
|
|
37
|
+
export interface EditToolOptions {
|
|
38
|
+
/** Whether to accept high-confidence fuzzy matches for whitespace/indentation (default: true) */
|
|
39
|
+
fuzzyMatch?: boolean;
|
|
40
|
+
/** Callback to get LSP diagnostics after editing a file */
|
|
41
|
+
getDiagnostics?: (absolutePath: string) => Promise<FileDiagnosticsResult>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createEditTool(cwd: string, options: EditToolOptions = {}): AgentTool<typeof editSchema> {
|
|
45
|
+
const allowFuzzy = options.fuzzyMatch ?? true;
|
|
33
46
|
return {
|
|
34
47
|
name: "edit",
|
|
35
48
|
label: "Edit",
|
|
@@ -50,6 +63,19 @@ Usage:
|
|
|
50
63
|
) => {
|
|
51
64
|
const absolutePath = resolveToCwd(path, cwd);
|
|
52
65
|
|
|
66
|
+
// Reject .ipynb files - use NotebookEdit tool instead
|
|
67
|
+
if (absolutePath.endsWith(".ipynb")) {
|
|
68
|
+
return {
|
|
69
|
+
content: [
|
|
70
|
+
{
|
|
71
|
+
type: "text",
|
|
72
|
+
text: "Cannot edit Jupyter notebooks with the Edit tool. Use the NotebookEdit tool instead.",
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
details: undefined,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
53
79
|
return new Promise<{
|
|
54
80
|
content: Array<{ type: "text"; text: string }>;
|
|
55
81
|
details: EditToolDetails | undefined;
|
|
@@ -108,7 +134,7 @@ Usage:
|
|
|
108
134
|
const normalizedNewText = normalizeToLF(newText);
|
|
109
135
|
|
|
110
136
|
const matchOutcome = findEditMatch(normalizedContent, normalizedOldText, {
|
|
111
|
-
allowFuzzy
|
|
137
|
+
allowFuzzy,
|
|
112
138
|
similarityThreshold: DEFAULT_FUZZY_THRESHOLD,
|
|
113
139
|
});
|
|
114
140
|
|
|
@@ -131,7 +157,7 @@ Usage:
|
|
|
131
157
|
reject(
|
|
132
158
|
new Error(
|
|
133
159
|
formatEditMatchError(path, normalizedOldText, matchOutcome.closest, {
|
|
134
|
-
allowFuzzy
|
|
160
|
+
allowFuzzy,
|
|
135
161
|
similarityThreshold: DEFAULT_FUZZY_THRESHOLD,
|
|
136
162
|
fuzzyMatches: matchOutcome.fuzzyMatches,
|
|
137
163
|
}),
|
|
@@ -179,14 +205,39 @@ Usage:
|
|
|
179
205
|
}
|
|
180
206
|
|
|
181
207
|
const diffResult = generateDiffString(normalizedContent, normalizedNewContent);
|
|
208
|
+
|
|
209
|
+
// Get LSP diagnostics if callback provided
|
|
210
|
+
let diagnosticsResult: FileDiagnosticsResult | undefined;
|
|
211
|
+
if (options.getDiagnostics) {
|
|
212
|
+
try {
|
|
213
|
+
diagnosticsResult = await options.getDiagnostics(absolutePath);
|
|
214
|
+
} catch {
|
|
215
|
+
// Ignore diagnostics errors - don't fail the edit
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Build result text
|
|
220
|
+
let resultText = `Successfully replaced text in ${path}.`;
|
|
221
|
+
|
|
222
|
+
// Append diagnostics if available and there are issues
|
|
223
|
+
if (diagnosticsResult?.available && diagnosticsResult.diagnostics.length > 0) {
|
|
224
|
+
resultText += `\n\nLSP Diagnostics (${diagnosticsResult.summary}):\n`;
|
|
225
|
+
resultText += diagnosticsResult.diagnostics.map((d) => ` ${d}`).join("\n");
|
|
226
|
+
}
|
|
227
|
+
|
|
182
228
|
resolve({
|
|
183
229
|
content: [
|
|
184
230
|
{
|
|
185
231
|
type: "text",
|
|
186
|
-
text:
|
|
232
|
+
text: resultText,
|
|
187
233
|
},
|
|
188
234
|
],
|
|
189
|
-
details: {
|
|
235
|
+
details: {
|
|
236
|
+
diff: diffResult.diff,
|
|
237
|
+
firstChangedLine: diffResult.firstChangedLine,
|
|
238
|
+
hasDiagnostics: diagnosticsResult?.available ?? false,
|
|
239
|
+
diagnostics: diagnosticsResult,
|
|
240
|
+
},
|
|
190
241
|
});
|
|
191
242
|
} catch (error: any) {
|
|
192
243
|
// Clean up abort handler
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { Type } from "@sinclair/typebox";
|
|
8
|
-
import type { CustomTool } from "../../custom-tools/types
|
|
9
|
-
import type { ExaRenderDetails } from "./types
|
|
8
|
+
import type { CustomTool } from "../../custom-tools/types";
|
|
9
|
+
import type { ExaRenderDetails } from "./types";
|
|
10
10
|
|
|
11
11
|
/** exa_company - Company research */
|
|
12
12
|
export const companyTool: CustomTool<any, ExaRenderDetails> = {
|
|
@@ -34,7 +34,7 @@ Parameters:
|
|
|
34
34
|
details: { error: "EXA_API_KEY not found", toolName: "exa_company" },
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
-
const response = await callExaTool("
|
|
37
|
+
const response = await callExaTool("company_research", params, apiKey);
|
|
38
38
|
|
|
39
39
|
if (isSearchResponse(response)) {
|
|
40
40
|
const formatted = formatSearchResults(response);
|
|
@@ -9,14 +9,14 @@
|
|
|
9
9
|
* - 14 websets tools (CRUD, items, search, enrichment, monitor)
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import type { CustomTool } from "../../custom-tools/types
|
|
13
|
-
import type { ExaSettings } from "../../settings-manager
|
|
14
|
-
import { companyTool } from "./company
|
|
15
|
-
import { linkedinTool } from "./linkedin
|
|
16
|
-
import { researcherTools } from "./researcher
|
|
17
|
-
import { searchTools } from "./search
|
|
18
|
-
import type { ExaRenderDetails } from "./types
|
|
19
|
-
import { websetsTools } from "./websets
|
|
12
|
+
import type { CustomTool } from "../../custom-tools/types";
|
|
13
|
+
import type { ExaSettings } from "../../settings-manager";
|
|
14
|
+
import { companyTool } from "./company";
|
|
15
|
+
import { linkedinTool } from "./linkedin";
|
|
16
|
+
import { researcherTools } from "./researcher";
|
|
17
|
+
import { searchTools } from "./search";
|
|
18
|
+
import type { ExaRenderDetails } from "./types";
|
|
19
|
+
import { websetsTools } from "./websets";
|
|
20
20
|
|
|
21
21
|
/** All Exa tools (22 total) - static export for backward compatibility */
|
|
22
22
|
export const exaTools: CustomTool<any, ExaRenderDetails>[] = [
|
|
@@ -42,9 +42,8 @@ export function getExaTools(settings: Required<ExaSettings>): CustomTool<any, Ex
|
|
|
42
42
|
return tools;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
export { companyTool } from "./company
|
|
46
|
-
export { linkedinTool } from "./linkedin
|
|
47
|
-
export { logExaError, logViewError } from "./logger.js";
|
|
45
|
+
export { companyTool } from "./company";
|
|
46
|
+
export { linkedinTool } from "./linkedin";
|
|
48
47
|
export {
|
|
49
48
|
callExaTool,
|
|
50
49
|
callWebsetsTool,
|
|
@@ -54,11 +53,11 @@ export {
|
|
|
54
53
|
findApiKey,
|
|
55
54
|
formatSearchResults,
|
|
56
55
|
isSearchResponse,
|
|
57
|
-
} from "./mcp-client
|
|
58
|
-
export { renderExaCall, renderExaResult } from "./render
|
|
59
|
-
export { researcherTools } from "./researcher
|
|
56
|
+
} from "./mcp-client";
|
|
57
|
+
export { renderExaCall, renderExaResult } from "./render";
|
|
58
|
+
export { researcherTools } from "./researcher";
|
|
60
59
|
// Re-export individual modules for selective importing
|
|
61
|
-
export { searchTools } from "./search
|
|
60
|
+
export { searchTools } from "./search";
|
|
62
61
|
// Re-export types and utilities
|
|
63
|
-
export type { ExaRenderDetails, ExaSearchResponse, ExaSearchResult, MCPToolWrapperConfig } from "./types
|
|
64
|
-
export { websetsTools } from "./websets
|
|
62
|
+
export type { ExaRenderDetails, ExaSearchResponse, ExaSearchResult, MCPToolWrapperConfig } from "./types";
|
|
63
|
+
export { websetsTools } from "./websets";
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { Type } from "@sinclair/typebox";
|
|
8
|
-
import type { CustomTool } from "../../custom-tools/types
|
|
9
|
-
import type { ExaRenderDetails } from "./types
|
|
8
|
+
import type { CustomTool } from "../../custom-tools/types";
|
|
9
|
+
import type { ExaRenderDetails } from "./types";
|
|
10
10
|
|
|
11
11
|
/** exa_linkedin - LinkedIn search */
|
|
12
12
|
export const linkedinTool: CustomTool<any, ExaRenderDetails> = {
|
|
@@ -34,7 +34,7 @@ Parameters:
|
|
|
34
34
|
details: { error: "EXA_API_KEY not found", toolName: "exa_linkedin" },
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
-
const response = await callExaTool("
|
|
37
|
+
const response = await callExaTool("linkedin_search", params, apiKey);
|
|
38
38
|
|
|
39
39
|
if (isSearchResponse(response)) {
|
|
40
40
|
const formatted = formatSearchResults(response);
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { TSchema } from "@sinclair/typebox";
|
|
8
|
-
import type { CustomTool } from "../../custom-tools/types
|
|
9
|
-
import {
|
|
8
|
+
import type { CustomTool } from "../../custom-tools/types";
|
|
9
|
+
import { logger } from "../../logger";
|
|
10
10
|
import type {
|
|
11
11
|
ExaRenderDetails,
|
|
12
12
|
ExaSearchResponse,
|
|
@@ -15,7 +15,7 @@ import type {
|
|
|
15
15
|
MCPTool,
|
|
16
16
|
MCPToolsResponse,
|
|
17
17
|
MCPToolWrapperConfig,
|
|
18
|
-
} from "./types
|
|
18
|
+
} from "./types";
|
|
19
19
|
|
|
20
20
|
/** Find EXA_API_KEY from process.env or .env files */
|
|
21
21
|
export async function findApiKey(): Promise<string | null> {
|
|
@@ -89,7 +89,7 @@ export async function callMCP(url: string, method: string, params?: Record<strin
|
|
|
89
89
|
|
|
90
90
|
if (!response.ok) {
|
|
91
91
|
const errorMsg = `MCP request failed: ${response.status} ${response.statusText}`;
|
|
92
|
-
|
|
92
|
+
logger.error(errorMsg, { url, method, params });
|
|
93
93
|
throw new Error(errorMsg);
|
|
94
94
|
}
|
|
95
95
|
|
|
@@ -97,7 +97,7 @@ export async function callMCP(url: string, method: string, params?: Record<strin
|
|
|
97
97
|
const result = parseSSE(text);
|
|
98
98
|
|
|
99
99
|
if (!result) {
|
|
100
|
-
|
|
100
|
+
logger.error("Failed to parse MCP response", { url, method, responseText: text.slice(0, 500) });
|
|
101
101
|
throw new Error("Failed to parse MCP response");
|
|
102
102
|
}
|
|
103
103
|
|
|
@@ -110,7 +110,7 @@ export async function fetchExaTools(apiKey: string, toolNames: string[]): Promis
|
|
|
110
110
|
const response = (await callMCP(url, "tools/list")) as MCPToolsResponse;
|
|
111
111
|
|
|
112
112
|
if (response.error) {
|
|
113
|
-
|
|
113
|
+
logger.error("MCP tools/list error", { toolNames, error: response.error });
|
|
114
114
|
throw new Error(`MCP error: ${response.error.message}`);
|
|
115
115
|
}
|
|
116
116
|
|
|
@@ -123,7 +123,7 @@ export async function fetchWebsetsTools(apiKey: string): Promise<MCPTool[]> {
|
|
|
123
123
|
const response = (await callMCP(url, "tools/list")) as MCPToolsResponse;
|
|
124
124
|
|
|
125
125
|
if (response.error) {
|
|
126
|
-
|
|
126
|
+
logger.error("Websets MCP tools/list error", { error: response.error });
|
|
127
127
|
throw new Error(`MCP error: ${response.error.message}`);
|
|
128
128
|
}
|
|
129
129
|
|
|
@@ -139,7 +139,7 @@ export async function callExaTool(toolName: string, args: Record<string, unknown
|
|
|
139
139
|
})) as MCPCallResponse;
|
|
140
140
|
|
|
141
141
|
if (response.error) {
|
|
142
|
-
|
|
142
|
+
logger.error("MCP tools/call error", { toolName, args, error: response.error });
|
|
143
143
|
throw new Error(`MCP error: ${response.error.message}`);
|
|
144
144
|
}
|
|
145
145
|
|
|
@@ -159,7 +159,7 @@ export async function callWebsetsTool(
|
|
|
159
159
|
})) as MCPCallResponse;
|
|
160
160
|
|
|
161
161
|
if (response.error) {
|
|
162
|
-
|
|
162
|
+
logger.error("Websets MCP tools/call error", { toolName, args, error: response.error });
|
|
163
163
|
throw new Error(`MCP error: ${response.error.message}`);
|
|
164
164
|
}
|
|
165
165
|
|