@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.
Files changed (153) hide show
  1. package/CHANGELOG.md +115 -1
  2. package/README.md +1 -1
  3. package/examples/custom-tools/subagent/index.ts +1 -1
  4. package/package.json +5 -3
  5. package/src/cli/args.ts +13 -6
  6. package/src/cli/file-processor.ts +3 -3
  7. package/src/cli/list-models.ts +2 -2
  8. package/src/cli/plugin-cli.ts +1 -1
  9. package/src/cli/session-picker.ts +2 -2
  10. package/src/cli.ts +1 -1
  11. package/src/config.ts +3 -3
  12. package/src/core/agent-session.ts +189 -29
  13. package/src/core/bash-executor.ts +50 -10
  14. package/src/core/compaction/branch-summarization.ts +5 -5
  15. package/src/core/compaction/compaction.ts +3 -3
  16. package/src/core/compaction/index.ts +3 -3
  17. package/src/core/custom-commands/bundled/review/index.ts +156 -0
  18. package/src/core/custom-commands/index.ts +15 -0
  19. package/src/core/custom-commands/loader.ts +232 -0
  20. package/src/core/custom-commands/types.ts +112 -0
  21. package/src/core/custom-tools/index.ts +3 -3
  22. package/src/core/custom-tools/loader.ts +10 -8
  23. package/src/core/custom-tools/types.ts +11 -6
  24. package/src/core/custom-tools/wrapper.ts +2 -1
  25. package/src/core/exec.ts +22 -12
  26. package/src/core/export-html/index.ts +5 -5
  27. package/src/core/file-mentions.ts +54 -0
  28. package/src/core/hooks/index.ts +5 -5
  29. package/src/core/hooks/loader.ts +21 -16
  30. package/src/core/hooks/runner.ts +6 -6
  31. package/src/core/hooks/tool-wrapper.ts +2 -2
  32. package/src/core/hooks/types.ts +12 -15
  33. package/src/core/index.ts +6 -6
  34. package/src/core/logger.ts +112 -0
  35. package/src/core/mcp/client.ts +3 -3
  36. package/src/core/mcp/config.ts +1 -1
  37. package/src/core/mcp/index.ts +12 -12
  38. package/src/core/mcp/loader.ts +2 -2
  39. package/src/core/mcp/manager.ts +6 -6
  40. package/src/core/mcp/tool-bridge.ts +3 -3
  41. package/src/core/mcp/transports/http.ts +1 -1
  42. package/src/core/mcp/transports/index.ts +2 -2
  43. package/src/core/mcp/transports/stdio.ts +1 -1
  44. package/src/core/messages.ts +22 -0
  45. package/src/core/model-registry.ts +2 -2
  46. package/src/core/model-resolver.ts +103 -2
  47. package/src/core/plugins/doctor.ts +1 -1
  48. package/src/core/plugins/index.ts +6 -6
  49. package/src/core/plugins/installer.ts +4 -4
  50. package/src/core/plugins/loader.ts +4 -9
  51. package/src/core/plugins/manager.ts +5 -5
  52. package/src/core/plugins/paths.ts +3 -3
  53. package/src/core/sdk.ts +127 -52
  54. package/src/core/session-manager.ts +123 -20
  55. package/src/core/settings-manager.ts +106 -22
  56. package/src/core/skills.ts +5 -5
  57. package/src/core/slash-commands.ts +60 -45
  58. package/src/core/system-prompt.ts +6 -6
  59. package/src/core/title-generator.ts +94 -0
  60. package/src/core/tools/bash.ts +33 -157
  61. package/src/core/tools/context.ts +2 -2
  62. package/src/core/tools/edit-diff.ts +5 -5
  63. package/src/core/tools/edit.ts +60 -9
  64. package/src/core/tools/exa/company.ts +3 -3
  65. package/src/core/tools/exa/index.ts +16 -17
  66. package/src/core/tools/exa/linkedin.ts +3 -3
  67. package/src/core/tools/exa/mcp-client.ts +9 -9
  68. package/src/core/tools/exa/render.ts +5 -5
  69. package/src/core/tools/exa/researcher.ts +3 -3
  70. package/src/core/tools/exa/search.ts +6 -5
  71. package/src/core/tools/exa/types.ts +5 -6
  72. package/src/core/tools/exa/websets.ts +3 -3
  73. package/src/core/tools/find.ts +3 -3
  74. package/src/core/tools/grep.ts +6 -5
  75. package/src/core/tools/index.ts +114 -40
  76. package/src/core/tools/ls.ts +4 -4
  77. package/src/core/tools/lsp/client.ts +204 -108
  78. package/src/core/tools/lsp/config.ts +709 -35
  79. package/src/core/tools/lsp/edits.ts +2 -2
  80. package/src/core/tools/lsp/index.ts +432 -30
  81. package/src/core/tools/lsp/render.ts +2 -2
  82. package/src/core/tools/lsp/rust-analyzer.ts +3 -3
  83. package/src/core/tools/lsp/types.ts +5 -0
  84. package/src/core/tools/lsp/utils.ts +1 -1
  85. package/src/core/tools/notebook.ts +1 -1
  86. package/src/core/tools/output.ts +175 -0
  87. package/src/core/tools/read.ts +7 -7
  88. package/src/core/tools/renderers.ts +92 -13
  89. package/src/core/tools/review.ts +268 -0
  90. package/src/core/tools/task/agents.ts +1 -1
  91. package/src/core/tools/task/bundled-agents/explore.md +1 -1
  92. package/src/core/tools/task/bundled-agents/reviewer.md +53 -38
  93. package/src/core/tools/task/discovery.ts +2 -2
  94. package/src/core/tools/task/executor.ts +145 -28
  95. package/src/core/tools/task/index.ts +78 -30
  96. package/src/core/tools/task/model-resolver.ts +72 -13
  97. package/src/core/tools/task/parallel.ts +1 -1
  98. package/src/core/tools/task/render.ts +219 -30
  99. package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
  100. package/src/core/tools/task/types.ts +36 -2
  101. package/src/core/tools/web-fetch.ts +5 -3
  102. package/src/core/tools/web-search/auth.ts +1 -1
  103. package/src/core/tools/web-search/index.ts +17 -15
  104. package/src/core/tools/web-search/providers/anthropic.ts +2 -2
  105. package/src/core/tools/web-search/providers/exa.ts +3 -5
  106. package/src/core/tools/web-search/providers/perplexity.ts +1 -1
  107. package/src/core/tools/web-search/render.ts +3 -3
  108. package/src/core/tools/write.ts +70 -7
  109. package/src/index.ts +33 -17
  110. package/src/main.ts +60 -34
  111. package/src/migrations.ts +3 -3
  112. package/src/modes/index.ts +5 -5
  113. package/src/modes/interactive/components/armin.ts +1 -1
  114. package/src/modes/interactive/components/assistant-message.ts +1 -1
  115. package/src/modes/interactive/components/bash-execution.ts +4 -4
  116. package/src/modes/interactive/components/bordered-loader.ts +2 -2
  117. package/src/modes/interactive/components/branch-summary-message.ts +2 -2
  118. package/src/modes/interactive/components/compaction-summary-message.ts +2 -2
  119. package/src/modes/interactive/components/diff.ts +1 -1
  120. package/src/modes/interactive/components/dynamic-border.ts +1 -1
  121. package/src/modes/interactive/components/footer.ts +5 -5
  122. package/src/modes/interactive/components/hook-editor.ts +2 -2
  123. package/src/modes/interactive/components/hook-input.ts +2 -2
  124. package/src/modes/interactive/components/hook-message.ts +3 -3
  125. package/src/modes/interactive/components/hook-selector.ts +2 -2
  126. package/src/modes/interactive/components/model-selector.ts +341 -41
  127. package/src/modes/interactive/components/oauth-selector.ts +3 -3
  128. package/src/modes/interactive/components/plugin-settings.ts +4 -4
  129. package/src/modes/interactive/components/queue-mode-selector.ts +2 -2
  130. package/src/modes/interactive/components/session-selector.ts +24 -11
  131. package/src/modes/interactive/components/settings-defs.ts +51 -3
  132. package/src/modes/interactive/components/settings-selector.ts +13 -16
  133. package/src/modes/interactive/components/show-images-selector.ts +2 -2
  134. package/src/modes/interactive/components/theme-selector.ts +2 -2
  135. package/src/modes/interactive/components/thinking-selector.ts +2 -2
  136. package/src/modes/interactive/components/tool-execution.ts +44 -8
  137. package/src/modes/interactive/components/tree-selector.ts +5 -5
  138. package/src/modes/interactive/components/user-message-selector.ts +2 -2
  139. package/src/modes/interactive/components/user-message.ts +1 -1
  140. package/src/modes/interactive/components/welcome.ts +42 -5
  141. package/src/modes/interactive/interactive-mode.ts +169 -48
  142. package/src/modes/interactive/theme/theme.ts +8 -7
  143. package/src/modes/print-mode.ts +4 -3
  144. package/src/modes/rpc/rpc-client.ts +4 -4
  145. package/src/modes/rpc/rpc-mode.ts +21 -11
  146. package/src/modes/rpc/rpc-types.ts +3 -3
  147. package/src/utils/changelog.ts +2 -2
  148. package/src/utils/clipboard.ts +1 -1
  149. package/src/utils/shell-snapshot.ts +218 -0
  150. package/src/utils/shell.ts +93 -13
  151. package/src/utils/tools-manager.ts +1 -1
  152. package/examples/custom-tools/subagent/agents/reviewer.md +0 -35
  153. 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
+ }
@@ -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 type { Subprocess } from "bun";
7
- import { getShellConfig, killProcessTree } from "../../utils/shell.js";
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
- const { shell, args } = getShellConfig();
79
- const child: Subprocess = Bun.spawn([shell, ...args, command], {
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
- if (signal) {
108
- if (signal.aborted) {
109
- child.kill();
110
- throw new Error("Command aborted");
111
- }
112
- signal.addEventListener("abort", onAbort, { once: true });
113
- }
114
-
115
- // Set timeout if provided
116
- let timeoutHandle: Timer | undefined;
117
- if (timeout !== undefined && timeout > 0) {
118
- timeoutHandle = setTimeout(() => {
119
- timedOut = true;
120
- onAbort();
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
- if (timedOut) {
204
- let output = fullOutput;
205
- if (output) output += "\n\n";
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(fullOutput);
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: tempFilePath,
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(fullOutput.split("\n").pop() || "", "utf-8"));
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: ${tempFilePath}]`;
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 !== null) {
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.js";
3
- import type { HookUIContext } from "../hooks/types.js";
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 { constants } from "fs";
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
- : "Hint: Use fuzzy=true to accept high-confidence matches.";
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 = false,
412
+ fuzzy = true,
413
413
  ): Promise<EditDiffResult | EditDiffError> {
414
414
  const absolutePath = resolveToCwd(path, cwd);
415
415
 
@@ -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.js";
15
- import { resolveToCwd } from "./path-utils.js";
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 function createEditTool(cwd: string): AgentTool<typeof editSchema> {
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: true,
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: true,
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: `Successfully replaced text in ${path}.`,
232
+ text: resultText,
187
233
  },
188
234
  ],
189
- details: { diff: diffResult.diff, firstChangedLine: diffResult.firstChangedLine },
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.js";
9
- import type { ExaRenderDetails } from "./types.js";
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("company_research_exa", params, apiKey);
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.js";
13
- import type { ExaSettings } from "../../settings-manager.js";
14
- import { companyTool } from "./company.js";
15
- import { linkedinTool } from "./linkedin.js";
16
- import { researcherTools } from "./researcher.js";
17
- import { searchTools } from "./search.js";
18
- import type { ExaRenderDetails } from "./types.js";
19
- import { websetsTools } from "./websets.js";
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.js";
46
- export { linkedinTool } from "./linkedin.js";
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.js";
58
- export { renderExaCall, renderExaResult } from "./render.js";
59
- export { researcherTools } from "./researcher.js";
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.js";
60
+ export { searchTools } from "./search";
62
61
  // Re-export types and utilities
63
- export type { ExaRenderDetails, ExaSearchResponse, ExaSearchResult, MCPToolWrapperConfig } from "./types.js";
64
- export { websetsTools } from "./websets.js";
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.js";
9
- import type { ExaRenderDetails } from "./types.js";
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("linkedin_search_exa", params, apiKey);
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.js";
9
- import { logExaError } from "./logger.js";
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.js";
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
- logExaError(errorMsg, { url, method, params });
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
- logExaError("Failed to parse MCP response", { url, method, responseText: text.slice(0, 500) });
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
- logExaError("MCP tools/list error", { toolNames, error: response.error });
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
- logExaError("Websets MCP tools/list error", { error: response.error });
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
- logExaError("MCP tools/call error", { toolName, args, error: response.error });
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
- logExaError("Websets MCP tools/call error", { toolName, args, error: response.error });
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