@oh-my-pi/pi-coding-agent 6.2.0 → 6.7.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 +46 -0
- package/docs/sdk.md +1 -1
- package/package.json +5 -5
- package/scripts/generate-template.ts +6 -6
- package/src/cli/args.ts +3 -0
- package/src/core/agent-session.ts +39 -0
- package/src/core/bash-executor.ts +3 -3
- package/src/core/cursor/exec-bridge.ts +95 -88
- package/src/core/custom-commands/bundled/review/index.ts +142 -145
- package/src/core/custom-commands/bundled/wt/index.ts +68 -66
- package/src/core/custom-commands/loader.ts +4 -6
- package/src/core/custom-tools/index.ts +2 -2
- package/src/core/custom-tools/loader.ts +66 -61
- package/src/core/custom-tools/types.ts +4 -4
- package/src/core/custom-tools/wrapper.ts +61 -25
- package/src/core/event-bus.ts +19 -47
- package/src/core/extensions/index.ts +8 -4
- package/src/core/extensions/loader.ts +160 -120
- package/src/core/extensions/types.ts +4 -4
- package/src/core/extensions/wrapper.ts +149 -100
- package/src/core/hooks/index.ts +1 -1
- package/src/core/hooks/tool-wrapper.ts +96 -70
- package/src/core/hooks/types.ts +1 -2
- package/src/core/index.ts +1 -0
- package/src/core/mcp/index.ts +6 -2
- package/src/core/mcp/json-rpc.ts +88 -0
- package/src/core/mcp/loader.ts +22 -4
- package/src/core/mcp/manager.ts +202 -48
- package/src/core/mcp/tool-bridge.ts +143 -55
- package/src/core/mcp/tool-cache.ts +122 -0
- package/src/core/python-executor.ts +3 -9
- package/src/core/sdk.ts +33 -32
- package/src/core/session-manager.ts +30 -0
- package/src/core/settings-manager.ts +34 -1
- package/src/core/ssh/ssh-executor.ts +6 -84
- package/src/core/streaming-output.ts +107 -53
- package/src/core/tools/ask.ts +92 -93
- package/src/core/tools/bash.ts +103 -94
- package/src/core/tools/calculator.ts +41 -26
- package/src/core/tools/complete.ts +76 -66
- package/src/core/tools/context.ts +22 -24
- package/src/core/tools/exa/index.ts +1 -1
- package/src/core/tools/exa/mcp-client.ts +56 -101
- package/src/core/tools/find.ts +250 -253
- package/src/core/tools/git.ts +39 -33
- package/src/core/tools/grep.ts +440 -427
- package/src/core/tools/index.ts +62 -61
- package/src/core/tools/ls.ts +119 -114
- package/src/core/tools/lsp/clients/biome-client.ts +5 -7
- package/src/core/tools/lsp/clients/index.ts +4 -4
- package/src/core/tools/lsp/clients/lsp-linter-client.ts +5 -7
- package/src/core/tools/lsp/config.ts +2 -2
- package/src/core/tools/lsp/index.ts +604 -578
- package/src/core/tools/notebook.ts +121 -119
- package/src/core/tools/output.ts +163 -147
- package/src/core/tools/patch/applicator.ts +1100 -0
- package/src/core/tools/patch/diff.ts +362 -0
- package/src/core/tools/patch/fuzzy.ts +647 -0
- package/src/core/tools/patch/index.ts +430 -0
- package/src/core/tools/patch/normalize.ts +220 -0
- package/src/core/tools/patch/normative.ts +49 -0
- package/src/core/tools/patch/parser.ts +528 -0
- package/src/core/tools/patch/shared.ts +228 -0
- package/src/core/tools/patch/types.ts +244 -0
- package/src/core/tools/python.ts +139 -136
- package/src/core/tools/read.ts +237 -216
- package/src/core/tools/render-utils.ts +196 -77
- package/src/core/tools/renderers.ts +1 -1
- package/src/core/tools/ssh.ts +99 -80
- package/src/core/tools/task/executor.ts +11 -7
- package/src/core/tools/task/index.ts +352 -343
- package/src/core/tools/task/worker.ts +13 -23
- package/src/core/tools/todo-write.ts +74 -59
- package/src/core/tools/web-fetch.ts +54 -47
- package/src/core/tools/web-search/index.ts +27 -16
- package/src/core/tools/write.ts +73 -44
- package/src/core/ttsr.ts +106 -152
- package/src/core/voice.ts +49 -39
- package/src/index.ts +16 -12
- package/src/lib/worktree/index.ts +1 -9
- package/src/modes/interactive/components/diff.ts +15 -8
- package/src/modes/interactive/components/settings-defs.ts +24 -0
- package/src/modes/interactive/components/tool-execution.ts +34 -6
- package/src/modes/interactive/controllers/event-controller.ts +6 -19
- package/src/modes/interactive/controllers/input-controller.ts +1 -1
- package/src/modes/interactive/utils/ui-helpers.ts +5 -1
- package/src/modes/rpc/rpc-mode.ts +99 -81
- package/src/prompts/tools/patch.md +76 -0
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/{edit.md → replace.md} +1 -0
- package/src/utils/shell.ts +0 -40
- package/src/core/tools/edit-diff.ts +0 -574
- package/src/core/tools/edit.ts +0 -345
package/src/core/tools/find.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
3
3
|
import { StringEnum } from "@oh-my-pi/pi-ai";
|
|
4
4
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
5
5
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
6
|
+
import type { Static } from "@sinclair/typebox";
|
|
6
7
|
import { Type } from "@sinclair/typebox";
|
|
7
8
|
import { getLanguageFromPath, type Theme } from "../../modes/interactive/theme/theme";
|
|
8
9
|
import findDescription from "../../prompts/tools/find.md" with { type: "text" };
|
|
@@ -109,264 +110,68 @@ async function captureCommandOutput(
|
|
|
109
110
|
return { stdout, stderr, exitCode, aborted: scope.aborted };
|
|
110
111
|
}
|
|
111
112
|
|
|
112
|
-
export
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (customOps?.glob) {
|
|
152
|
-
if (!(await customOps.exists(searchPath))) {
|
|
153
|
-
throw new Error(`Path not found: ${searchPath}`);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const results = await customOps.glob(pattern, searchPath, {
|
|
157
|
-
ignore: ["**/node_modules/**", "**/.git/**"],
|
|
158
|
-
limit: effectiveLimit,
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
if (results.length === 0) {
|
|
162
|
-
return {
|
|
163
|
-
content: [{ type: "text", text: "No files found matching pattern" }],
|
|
164
|
-
details: { scopePath, fileCount: 0, files: [], truncated: false },
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Relativize paths
|
|
169
|
-
const relativized = results.map((p) => {
|
|
170
|
-
if (p.startsWith(searchPath)) {
|
|
171
|
-
return p.slice(searchPath.length + 1);
|
|
172
|
-
}
|
|
173
|
-
return path.relative(searchPath, p);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
const resultLimitReached = relativized.length >= effectiveLimit;
|
|
177
|
-
const rawOutput = relativized.join("\n");
|
|
178
|
-
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
|
179
|
-
|
|
180
|
-
let resultOutput = truncation.content;
|
|
181
|
-
const details: FindToolDetails = {
|
|
182
|
-
scopePath,
|
|
183
|
-
fileCount: relativized.length,
|
|
184
|
-
files: relativized,
|
|
185
|
-
truncated: resultLimitReached || truncation.truncated,
|
|
186
|
-
};
|
|
187
|
-
const notices: string[] = [];
|
|
188
|
-
|
|
189
|
-
if (resultLimitReached) {
|
|
190
|
-
notices.push(
|
|
191
|
-
`${effectiveLimit} results limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`,
|
|
192
|
-
);
|
|
193
|
-
details.resultLimitReached = effectiveLimit;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (truncation.truncated) {
|
|
197
|
-
notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
|
|
198
|
-
details.truncation = truncation;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (notices.length > 0) {
|
|
202
|
-
resultOutput += `\n\n[${notices.join(". ")}]`;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return {
|
|
206
|
-
content: [{ type: "text", text: resultOutput }],
|
|
207
|
-
details: Object.keys(details).length > 0 ? details : undefined,
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Default: use fd
|
|
212
|
-
const fdPath = await ensureTool("fd", true);
|
|
213
|
-
if (!fdPath) {
|
|
214
|
-
throw new Error("fd is not available and could not be downloaded");
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Build fd arguments
|
|
218
|
-
// When pattern contains path separators (e.g. "reports/**"), use --full-path
|
|
219
|
-
// so fd matches against the full path, not just the filename.
|
|
220
|
-
// Also prepend **/ to anchor the pattern at any depth in the search path.
|
|
221
|
-
// Note: "**/foo.rs" is a glob construct (filename at any depth), not a path.
|
|
222
|
-
// Only patterns with real path components like "foo/bar" or "foo/**/bar" need --full-path.
|
|
223
|
-
const patternWithoutLeadingStarStar = pattern.replace(/^\*\*\//, "");
|
|
224
|
-
const hasPathSeparator =
|
|
225
|
-
patternWithoutLeadingStarStar.includes("/") || patternWithoutLeadingStarStar.includes("\\");
|
|
226
|
-
const effectivePattern = hasPathSeparator && !pattern.startsWith("**/") ? `**/${pattern}` : pattern;
|
|
227
|
-
const args: string[] = [
|
|
228
|
-
"--glob", // Use glob pattern
|
|
229
|
-
...(hasPathSeparator ? ["--full-path"] : []),
|
|
230
|
-
"--color=never", // No ANSI colors
|
|
231
|
-
"--max-results",
|
|
232
|
-
String(effectiveLimit),
|
|
233
|
-
];
|
|
234
|
-
|
|
235
|
-
if (includeHidden) {
|
|
236
|
-
args.push("--hidden");
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// Add type filter
|
|
240
|
-
if (effectiveType === "file") {
|
|
241
|
-
args.push("--type", "f");
|
|
242
|
-
} else if (effectiveType === "dir") {
|
|
243
|
-
args.push("--type", "d");
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Include .gitignore files (root + nested) so fd respects them even outside git repos
|
|
247
|
-
const gitignoreFiles = new Set<string>();
|
|
248
|
-
const rootGitignore = path.join(searchPath, ".gitignore");
|
|
249
|
-
if (await Bun.file(rootGitignore).exists()) {
|
|
250
|
-
gitignoreFiles.add(rootGitignore);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
try {
|
|
254
|
-
const gitignoreArgs = [
|
|
255
|
-
"--hidden",
|
|
256
|
-
"--no-ignore",
|
|
257
|
-
"--type",
|
|
258
|
-
"f",
|
|
259
|
-
"--name",
|
|
260
|
-
".gitignore",
|
|
261
|
-
"--exclude",
|
|
262
|
-
".git",
|
|
263
|
-
"--exclude",
|
|
264
|
-
"node_modules",
|
|
265
|
-
"--absolute-path",
|
|
266
|
-
searchPath,
|
|
267
|
-
];
|
|
268
|
-
const { stdout: gitignoreStdout, aborted: gitignoreAborted } = await captureCommandOutput(
|
|
269
|
-
fdPath,
|
|
270
|
-
gitignoreArgs,
|
|
271
|
-
signal,
|
|
272
|
-
);
|
|
273
|
-
if (gitignoreAborted) {
|
|
274
|
-
throw new Error("Operation aborted");
|
|
275
|
-
}
|
|
276
|
-
for (const rawLine of gitignoreStdout.split("\n")) {
|
|
277
|
-
const file = rawLine.trim();
|
|
278
|
-
if (!file) continue;
|
|
279
|
-
gitignoreFiles.add(file);
|
|
280
|
-
}
|
|
281
|
-
} catch (err) {
|
|
282
|
-
if (signal?.aborted) {
|
|
283
|
-
throw err instanceof Error ? err : new Error("Operation aborted");
|
|
284
|
-
}
|
|
285
|
-
// Ignore lookup errors
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
for (const gitignorePath of gitignoreFiles) {
|
|
289
|
-
args.push("--ignore-file", gitignorePath);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Pattern and path
|
|
293
|
-
args.push(effectivePattern, searchPath);
|
|
294
|
-
|
|
295
|
-
// Run fd
|
|
296
|
-
const { stdout, stderr, exitCode, aborted } = await captureCommandOutput(fdPath, args, signal);
|
|
297
|
-
|
|
298
|
-
if (aborted) {
|
|
299
|
-
throw new Error("Operation aborted");
|
|
113
|
+
export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
114
|
+
public readonly name = "find";
|
|
115
|
+
public readonly label = "Find";
|
|
116
|
+
public readonly description: string;
|
|
117
|
+
public readonly parameters = findSchema;
|
|
118
|
+
|
|
119
|
+
private readonly session: ToolSession;
|
|
120
|
+
private readonly customOps?: FindOperations;
|
|
121
|
+
|
|
122
|
+
constructor(session: ToolSession, options?: FindToolOptions) {
|
|
123
|
+
this.session = session;
|
|
124
|
+
this.customOps = options?.operations;
|
|
125
|
+
this.description = renderPromptTemplate(findDescription);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
public async execute(
|
|
129
|
+
_toolCallId: string,
|
|
130
|
+
params: Static<typeof findSchema>,
|
|
131
|
+
signal?: AbortSignal,
|
|
132
|
+
_onUpdate?: AgentToolUpdateCallback<FindToolDetails>,
|
|
133
|
+
_context?: AgentToolContext,
|
|
134
|
+
): Promise<AgentToolResult<FindToolDetails>> {
|
|
135
|
+
const { pattern, path: searchDir, limit, hidden, sortByMtime, type } = params;
|
|
136
|
+
|
|
137
|
+
return untilAborted(signal, async () => {
|
|
138
|
+
const searchPath = resolveToCwd(searchDir || ".", this.session.cwd);
|
|
139
|
+
const scopePath = (() => {
|
|
140
|
+
const relative = path.relative(this.session.cwd, searchPath).replace(/\\/g, "/");
|
|
141
|
+
return relative.length === 0 ? "." : relative;
|
|
142
|
+
})();
|
|
143
|
+
const effectiveLimit = limit ?? DEFAULT_LIMIT;
|
|
144
|
+
const effectiveType = type ?? "all";
|
|
145
|
+
const includeHidden = hidden ?? true;
|
|
146
|
+
const shouldSortByMtime = sortByMtime ?? false;
|
|
147
|
+
|
|
148
|
+
// If custom operations provided with glob, use that instead of fd
|
|
149
|
+
if (this.customOps?.glob) {
|
|
150
|
+
if (!(await this.customOps.exists(searchPath))) {
|
|
151
|
+
throw new Error(`Path not found: ${searchPath}`);
|
|
300
152
|
}
|
|
301
153
|
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
// fd returns non-zero for some errors but may still have partial output
|
|
307
|
-
if (!output) {
|
|
308
|
-
throw new Error(errorMsg);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
154
|
+
const results = await this.customOps.glob(pattern, searchPath, {
|
|
155
|
+
ignore: ["**/node_modules/**", "**/.git/**"],
|
|
156
|
+
limit: effectiveLimit,
|
|
157
|
+
});
|
|
311
158
|
|
|
312
|
-
if (
|
|
159
|
+
if (results.length === 0) {
|
|
313
160
|
return {
|
|
314
161
|
content: [{ type: "text", text: "No files found matching pattern" }],
|
|
315
162
|
details: { scopePath, fileCount: 0, files: [], truncated: false },
|
|
316
163
|
};
|
|
317
164
|
}
|
|
318
165
|
|
|
319
|
-
|
|
320
|
-
const relativized
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
for (const rawLine of lines) {
|
|
324
|
-
signal?.throwIfAborted();
|
|
325
|
-
const line = rawLine.replace(/\r$/, "").trim();
|
|
326
|
-
if (!line) {
|
|
327
|
-
continue;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const hadTrailingSlash = line.endsWith("/") || line.endsWith("\\");
|
|
331
|
-
let relativePath = line;
|
|
332
|
-
if (line.startsWith(searchPath)) {
|
|
333
|
-
relativePath = line.slice(searchPath.length + 1); // +1 for the /
|
|
334
|
-
} else {
|
|
335
|
-
relativePath = path.relative(searchPath, line);
|
|
166
|
+
// Relativize paths
|
|
167
|
+
const relativized = results.map((p) => {
|
|
168
|
+
if (p.startsWith(searchPath)) {
|
|
169
|
+
return p.slice(searchPath.length + 1);
|
|
336
170
|
}
|
|
171
|
+
return path.relative(searchPath, p);
|
|
172
|
+
});
|
|
337
173
|
|
|
338
|
-
if (hadTrailingSlash && !relativePath.endsWith("/")) {
|
|
339
|
-
relativePath += "/";
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// When sorting by mtime, keep files that fail to stat with mtime 0
|
|
343
|
-
if (shouldSortByMtime) {
|
|
344
|
-
try {
|
|
345
|
-
const fullPath = path.join(searchPath, relativePath);
|
|
346
|
-
const stat = await Bun.file(fullPath).stat();
|
|
347
|
-
relativized.push(relativePath);
|
|
348
|
-
mtimes.push(stat.mtimeMs);
|
|
349
|
-
} catch {
|
|
350
|
-
relativized.push(relativePath);
|
|
351
|
-
mtimes.push(0);
|
|
352
|
-
}
|
|
353
|
-
} else {
|
|
354
|
-
relativized.push(relativePath);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// Sort by mtime if requested (most recent first)
|
|
359
|
-
if (shouldSortByMtime && relativized.length > 0) {
|
|
360
|
-
const indexed = relativized.map((path, idx) => ({ path, mtime: mtimes[idx] }));
|
|
361
|
-
indexed.sort((a, b) => b.mtime - a.mtime);
|
|
362
|
-
relativized.length = 0;
|
|
363
|
-
relativized.push(...indexed.map((item) => item.path));
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// Check if we hit the result limit
|
|
367
174
|
const resultLimitReached = relativized.length >= effectiveLimit;
|
|
368
|
-
|
|
369
|
-
// Apply byte truncation (no line limit since we already have result limit)
|
|
370
175
|
const rawOutput = relativized.join("\n");
|
|
371
176
|
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
|
372
177
|
|
|
@@ -377,8 +182,6 @@ export function createFindTool(session: ToolSession, options?: FindToolOptions):
|
|
|
377
182
|
files: relativized,
|
|
378
183
|
truncated: resultLimitReached || truncation.truncated,
|
|
379
184
|
};
|
|
380
|
-
|
|
381
|
-
// Build notices
|
|
382
185
|
const notices: string[] = [];
|
|
383
186
|
|
|
384
187
|
if (resultLimitReached) {
|
|
@@ -399,11 +202,205 @@ export function createFindTool(session: ToolSession, options?: FindToolOptions):
|
|
|
399
202
|
|
|
400
203
|
return {
|
|
401
204
|
content: [{ type: "text", text: resultOutput }],
|
|
402
|
-
details
|
|
205
|
+
details,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Default: use fd
|
|
210
|
+
const fdPath = await ensureTool("fd", true);
|
|
211
|
+
if (!fdPath) {
|
|
212
|
+
throw new Error("fd is not available and could not be downloaded");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Build fd arguments
|
|
216
|
+
// When pattern contains path separators (e.g. "reports/**"), use --full-path
|
|
217
|
+
// so fd matches against the full path, not just the filename.
|
|
218
|
+
// Also prepend **/ to anchor the pattern at any depth in the search path.
|
|
219
|
+
// Note: "**/foo.rs" is a glob construct (filename at any depth), not a path.
|
|
220
|
+
// Only patterns with real path components like "foo/bar" or "foo/**/bar" need --full-path.
|
|
221
|
+
const patternWithoutLeadingStarStar = pattern.replace(/^\*\*\//, "");
|
|
222
|
+
const hasPathSeparator =
|
|
223
|
+
patternWithoutLeadingStarStar.includes("/") || patternWithoutLeadingStarStar.includes("\\");
|
|
224
|
+
const effectivePattern = hasPathSeparator && !pattern.startsWith("**/") ? `**/${pattern}` : pattern;
|
|
225
|
+
const args: string[] = [
|
|
226
|
+
"--glob", // Use glob pattern
|
|
227
|
+
...(hasPathSeparator ? ["--full-path"] : []),
|
|
228
|
+
"--color=never", // No ANSI colors
|
|
229
|
+
"--max-results",
|
|
230
|
+
String(effectiveLimit),
|
|
231
|
+
];
|
|
232
|
+
|
|
233
|
+
if (includeHidden) {
|
|
234
|
+
args.push("--hidden");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Add type filter
|
|
238
|
+
if (effectiveType === "file") {
|
|
239
|
+
args.push("--type", "f");
|
|
240
|
+
} else if (effectiveType === "dir") {
|
|
241
|
+
args.push("--type", "d");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Include .gitignore files (root + nested) so fd respects them even outside git repos
|
|
245
|
+
const gitignoreFiles = new Set<string>();
|
|
246
|
+
const rootGitignore = path.join(searchPath, ".gitignore");
|
|
247
|
+
if (await Bun.file(rootGitignore).exists()) {
|
|
248
|
+
gitignoreFiles.add(rootGitignore);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
const gitignoreArgs = [
|
|
253
|
+
"--hidden",
|
|
254
|
+
"--no-ignore",
|
|
255
|
+
"--type",
|
|
256
|
+
"f",
|
|
257
|
+
"--name",
|
|
258
|
+
".gitignore",
|
|
259
|
+
"--exclude",
|
|
260
|
+
".git",
|
|
261
|
+
"--exclude",
|
|
262
|
+
"node_modules",
|
|
263
|
+
"--absolute-path",
|
|
264
|
+
searchPath,
|
|
265
|
+
];
|
|
266
|
+
const { stdout: gitignoreStdout, aborted: gitignoreAborted } = await captureCommandOutput(
|
|
267
|
+
fdPath,
|
|
268
|
+
gitignoreArgs,
|
|
269
|
+
signal,
|
|
270
|
+
);
|
|
271
|
+
if (gitignoreAborted) {
|
|
272
|
+
throw new Error("Operation aborted");
|
|
273
|
+
}
|
|
274
|
+
for (const rawLine of gitignoreStdout.split("\n")) {
|
|
275
|
+
const file = rawLine.trim();
|
|
276
|
+
if (!file) continue;
|
|
277
|
+
gitignoreFiles.add(file);
|
|
278
|
+
}
|
|
279
|
+
} catch (err) {
|
|
280
|
+
if (signal?.aborted) {
|
|
281
|
+
throw err instanceof Error ? err : new Error("Operation aborted");
|
|
282
|
+
}
|
|
283
|
+
// Ignore lookup errors
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
for (const gitignorePath of gitignoreFiles) {
|
|
287
|
+
args.push("--ignore-file", gitignorePath);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Pattern and path
|
|
291
|
+
args.push(effectivePattern, searchPath);
|
|
292
|
+
|
|
293
|
+
// Run fd
|
|
294
|
+
const { stdout, stderr, exitCode, aborted } = await captureCommandOutput(fdPath, args, signal);
|
|
295
|
+
|
|
296
|
+
if (aborted) {
|
|
297
|
+
throw new Error("Operation aborted");
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const output = stdout.trim();
|
|
301
|
+
|
|
302
|
+
if (exitCode !== 0) {
|
|
303
|
+
const errorMsg = stderr.trim() || `fd exited with code ${exitCode ?? -1}`;
|
|
304
|
+
// fd returns non-zero for some errors but may still have partial output
|
|
305
|
+
if (!output) {
|
|
306
|
+
throw new Error(errorMsg);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (!output) {
|
|
311
|
+
return {
|
|
312
|
+
content: [{ type: "text", text: "No files found matching pattern" }],
|
|
313
|
+
details: { scopePath, fileCount: 0, files: [], truncated: false },
|
|
403
314
|
};
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const lines = output.split("\n");
|
|
318
|
+
const relativized: string[] = [];
|
|
319
|
+
const mtimes: number[] = [];
|
|
320
|
+
|
|
321
|
+
for (const rawLine of lines) {
|
|
322
|
+
signal?.throwIfAborted();
|
|
323
|
+
const line = rawLine.replace(/\r$/, "").trim();
|
|
324
|
+
if (!line) {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const hadTrailingSlash = line.endsWith("/") || line.endsWith("\\");
|
|
329
|
+
let relativePath = line;
|
|
330
|
+
if (line.startsWith(searchPath)) {
|
|
331
|
+
relativePath = line.slice(searchPath.length + 1); // +1 for the /
|
|
332
|
+
} else {
|
|
333
|
+
relativePath = path.relative(searchPath, line);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (hadTrailingSlash && !relativePath.endsWith("/")) {
|
|
337
|
+
relativePath += "/";
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// When sorting by mtime, keep files that fail to stat with mtime 0
|
|
341
|
+
if (shouldSortByMtime) {
|
|
342
|
+
try {
|
|
343
|
+
const fullPath = path.join(searchPath, relativePath);
|
|
344
|
+
const stat = await Bun.file(fullPath).stat();
|
|
345
|
+
relativized.push(relativePath);
|
|
346
|
+
mtimes.push(stat.mtimeMs);
|
|
347
|
+
} catch {
|
|
348
|
+
relativized.push(relativePath);
|
|
349
|
+
mtimes.push(0);
|
|
350
|
+
}
|
|
351
|
+
} else {
|
|
352
|
+
relativized.push(relativePath);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Sort by mtime if requested (most recent first)
|
|
357
|
+
if (shouldSortByMtime && relativized.length > 0) {
|
|
358
|
+
const indexed = relativized.map((path, idx) => ({ path, mtime: mtimes[idx] }));
|
|
359
|
+
indexed.sort((a, b) => b.mtime - a.mtime);
|
|
360
|
+
relativized.length = 0;
|
|
361
|
+
relativized.push(...indexed.map((item) => item.path));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Check if we hit the result limit
|
|
365
|
+
const resultLimitReached = relativized.length >= effectiveLimit;
|
|
366
|
+
|
|
367
|
+
// Apply byte truncation (no line limit since we already have result limit)
|
|
368
|
+
const rawOutput = relativized.join("\n");
|
|
369
|
+
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
|
370
|
+
|
|
371
|
+
let resultOutput = truncation.content;
|
|
372
|
+
const details: FindToolDetails = {
|
|
373
|
+
scopePath,
|
|
374
|
+
fileCount: relativized.length,
|
|
375
|
+
files: relativized,
|
|
376
|
+
truncated: resultLimitReached || truncation.truncated,
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
// Build notices
|
|
380
|
+
const notices: string[] = [];
|
|
381
|
+
|
|
382
|
+
if (resultLimitReached) {
|
|
383
|
+
notices.push(
|
|
384
|
+
`${effectiveLimit} results limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`,
|
|
385
|
+
);
|
|
386
|
+
details.resultLimitReached = effectiveLimit;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (truncation.truncated) {
|
|
390
|
+
notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
|
|
391
|
+
details.truncation = truncation;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (notices.length > 0) {
|
|
395
|
+
resultOutput += `\n\n[${notices.join(". ")}]`;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return {
|
|
399
|
+
content: [{ type: "text", text: resultOutput }],
|
|
400
|
+
details,
|
|
401
|
+
};
|
|
402
|
+
});
|
|
403
|
+
}
|
|
407
404
|
}
|
|
408
405
|
|
|
409
406
|
// =============================================================================
|
package/src/core/tools/git.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
1
|
+
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import { StringEnum } from "@oh-my-pi/pi-ai";
|
|
3
3
|
import { type GitParams, gitTool as gitToolCore, type ToolResponse } from "@oh-my-pi/pi-git-tool";
|
|
4
4
|
import { type Static, Type } from "@sinclair/typebox";
|
|
@@ -171,37 +171,43 @@ const gitSchema = Type.Object({
|
|
|
171
171
|
|
|
172
172
|
export type GitToolDetails = ToolResponse<unknown>;
|
|
173
173
|
|
|
174
|
-
export
|
|
175
|
-
|
|
176
|
-
|
|
174
|
+
export class GitTool implements AgentTool<typeof gitSchema, GitToolDetails> {
|
|
175
|
+
public readonly name = "git";
|
|
176
|
+
public readonly label = "Git";
|
|
177
|
+
public readonly description: string;
|
|
178
|
+
public readonly parameters = gitSchema;
|
|
179
|
+
|
|
180
|
+
private readonly session: ToolSession;
|
|
181
|
+
|
|
182
|
+
constructor(session: ToolSession) {
|
|
183
|
+
this.session = session;
|
|
184
|
+
this.description = renderPromptTemplate(gitDescription);
|
|
177
185
|
}
|
|
178
|
-
return {
|
|
179
|
-
name: "git",
|
|
180
|
-
label: "Git",
|
|
181
|
-
description: renderPromptTemplate(gitDescription),
|
|
182
|
-
parameters: gitSchema,
|
|
183
|
-
execute: async (_toolCallId, params: Static<typeof gitSchema>, _signal?: AbortSignal) => {
|
|
184
|
-
if (params.operation === "commit" && !params.message) {
|
|
185
|
-
throw new Error("Git commit requires a message to avoid an interactive editor. Provide `message`.");
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const result = await gitToolCore(params as GitParams, session.cwd);
|
|
189
|
-
if ("error" in result) {
|
|
190
|
-
const message = result._rendered ?? result.error;
|
|
191
|
-
return { content: [{ type: "text", text: message }], details: result };
|
|
192
|
-
}
|
|
193
|
-
if ("confirm" in result) {
|
|
194
|
-
const message = result._rendered ?? result.confirm;
|
|
195
|
-
return { content: [{ type: "text", text: message }], details: result };
|
|
196
|
-
}
|
|
197
|
-
return { content: [{ type: "text", text: result._rendered }], details: result };
|
|
198
|
-
},
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
186
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
187
|
+
static createIf(session: ToolSession): GitTool | null {
|
|
188
|
+
return session.settings?.getGitToolEnabled() === false ? null : new GitTool(session);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
public async execute(
|
|
192
|
+
_toolCallId: string,
|
|
193
|
+
params: Static<typeof gitSchema>,
|
|
194
|
+
_signal?: AbortSignal,
|
|
195
|
+
_onUpdate?: AgentToolUpdateCallback<GitToolDetails>,
|
|
196
|
+
_context?: AgentToolContext,
|
|
197
|
+
): Promise<AgentToolResult<GitToolDetails>> {
|
|
198
|
+
if (params.operation === "commit" && !params.message) {
|
|
199
|
+
throw new Error("Git commit requires a message to avoid an interactive editor. Provide `message`.");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const result = await gitToolCore(params as GitParams, this.session.cwd);
|
|
203
|
+
if ("error" in result) {
|
|
204
|
+
const message = result._rendered ?? result.error;
|
|
205
|
+
return { content: [{ type: "text", text: message }], details: result };
|
|
206
|
+
}
|
|
207
|
+
if ("confirm" in result) {
|
|
208
|
+
const message = result._rendered ?? result.confirm;
|
|
209
|
+
return { content: [{ type: "text", text: message }], details: result };
|
|
210
|
+
}
|
|
211
|
+
return { content: [{ type: "text", text: result._rendered }], details: result };
|
|
212
|
+
}
|
|
213
|
+
}
|