@oh-my-pi/pi-coding-agent 14.5.14 → 14.6.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 +39 -0
- package/package.json +7 -7
- package/src/autoresearch/command-resume.md +5 -8
- package/src/autoresearch/git.ts +41 -51
- package/src/autoresearch/helpers.ts +43 -359
- package/src/autoresearch/index.ts +281 -273
- package/src/autoresearch/prompt-setup.md +43 -0
- package/src/autoresearch/prompt.md +52 -193
- package/src/autoresearch/resume-message.md +2 -8
- package/src/autoresearch/state.ts +59 -166
- package/src/autoresearch/storage.ts +687 -0
- package/src/autoresearch/tools/init-experiment.ts +201 -290
- package/src/autoresearch/tools/log-experiment.ts +304 -517
- package/src/autoresearch/tools/run-experiment.ts +117 -296
- package/src/autoresearch/tools/update-notes.ts +116 -0
- package/src/autoresearch/types.ts +16 -66
- package/src/config/settings-schema.ts +1 -1
- package/src/config/settings.ts +20 -1
- package/src/cursor.ts +1 -1
- package/src/edit/index.ts +9 -31
- package/src/edit/line-hash.ts +70 -43
- package/src/edit/modes/hashline.lark +26 -0
- package/src/edit/modes/hashline.ts +898 -1099
- package/src/edit/modes/patch.ts +0 -7
- package/src/edit/modes/replace.ts +0 -4
- package/src/edit/renderer.ts +22 -20
- package/src/edit/streaming.ts +8 -28
- package/src/eval/eval.lark +24 -30
- package/src/eval/js/context-manager.ts +5 -162
- package/src/eval/js/prelude.txt +0 -12
- package/src/eval/parse.ts +129 -129
- package/src/eval/py/prelude.py +1 -219
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +2 -2
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/modes/components/session-observer-overlay.ts +5 -2
- package/src/modes/components/status-line/segments.ts +1 -1
- package/src/modes/components/status-line.ts +3 -5
- package/src/modes/components/tree-selector.ts +4 -5
- package/src/modes/components/welcome.ts +11 -1
- package/src/modes/controllers/command-controller.ts +2 -6
- package/src/modes/controllers/event-controller.ts +1 -2
- package/src/modes/controllers/extension-ui-controller.ts +3 -15
- package/src/modes/controllers/input-controller.ts +0 -1
- package/src/modes/controllers/selector-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +5 -7
- package/src/prompts/system/system-prompt.md +14 -38
- package/src/prompts/tools/ast-edit.md +8 -8
- package/src/prompts/tools/ast-grep.md +10 -10
- package/src/prompts/tools/eval.md +13 -31
- package/src/prompts/tools/find.md +2 -1
- package/src/prompts/tools/hashline.md +66 -57
- package/src/prompts/tools/search.md +2 -2
- package/src/session/session-manager.ts +17 -13
- package/src/tools/ast-edit.ts +141 -44
- package/src/tools/ast-grep.ts +112 -36
- package/src/tools/eval.ts +2 -53
- package/src/tools/find.ts +16 -15
- package/src/tools/path-utils.ts +36 -196
- package/src/tools/search.ts +56 -35
- package/src/utils/edit-mode.ts +2 -11
- package/src/utils/file-display-mode.ts +1 -1
- package/src/utils/git.ts +17 -0
- package/src/utils/session-color.ts +0 -12
- package/src/utils/title-generator.ts +22 -38
- package/src/autoresearch/apply-contract-to-state.ts +0 -24
- package/src/autoresearch/contract.ts +0 -288
- package/src/edit/modes/atom.lark +0 -29
- package/src/edit/modes/atom.ts +0 -1773
- package/src/prompts/tools/atom.md +0 -150
package/src/tools/ast-edit.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
1
2
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
2
|
-
import { type AstReplaceChange, astEdit } from "@oh-my-pi/pi-natives";
|
|
3
|
+
import { type AstReplaceChange, type AstReplaceFileChange, astEdit } from "@oh-my-pi/pi-natives";
|
|
3
4
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
4
5
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
5
6
|
import { $envpos, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
@@ -19,7 +20,7 @@ import {
|
|
|
19
20
|
hasGlobPathChars,
|
|
20
21
|
normalizePathLikeInput,
|
|
21
22
|
parseSearchPath,
|
|
22
|
-
|
|
23
|
+
resolveExplicitSearchPaths,
|
|
23
24
|
resolveToCwd,
|
|
24
25
|
} from "./path-utils";
|
|
25
26
|
import {
|
|
@@ -46,12 +47,106 @@ const astEditSchema = Type.Object({
|
|
|
46
47
|
minItems: 1,
|
|
47
48
|
description: "rewrite ops",
|
|
48
49
|
}),
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
paths: Type.Array(Type.String({ description: "file, directory, glob, or internal URL to rewrite" }), {
|
|
51
|
+
minItems: 1,
|
|
52
|
+
description: "files, directories, globs, or internal URLs to rewrite",
|
|
53
|
+
examples: [["src/"], ["src/foo.ts"], ["src/**/*.ts"], ["src/", "packages/"]],
|
|
52
54
|
}),
|
|
53
55
|
});
|
|
54
56
|
|
|
57
|
+
interface AstEditCallOptions {
|
|
58
|
+
rewrites: Record<string, string>;
|
|
59
|
+
dryRun: boolean;
|
|
60
|
+
maxFiles: number;
|
|
61
|
+
failOnParseError: boolean;
|
|
62
|
+
signal?: AbortSignal;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface AstEditAggregatedResult {
|
|
66
|
+
changes: AstReplaceChange[];
|
|
67
|
+
fileChanges: AstReplaceFileChange[];
|
|
68
|
+
totalReplacements: number;
|
|
69
|
+
filesTouched: number;
|
|
70
|
+
filesSearched: number;
|
|
71
|
+
applied: boolean;
|
|
72
|
+
limitReached: boolean;
|
|
73
|
+
parseErrors?: string[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function runAstEditTargets(
|
|
77
|
+
targets: Array<{ basePath: string; glob?: string }>,
|
|
78
|
+
commonBasePath: string,
|
|
79
|
+
options: AstEditCallOptions,
|
|
80
|
+
): Promise<AstEditAggregatedResult> {
|
|
81
|
+
const aggregatedChanges: AstReplaceChange[] = [];
|
|
82
|
+
const fileCounts = new Map<string, number>();
|
|
83
|
+
const parseErrors: string[] = [];
|
|
84
|
+
let totalReplacements = 0;
|
|
85
|
+
let filesSearched = 0;
|
|
86
|
+
let limitReached = false;
|
|
87
|
+
let applied = !options.dryRun;
|
|
88
|
+
for (const target of targets) {
|
|
89
|
+
const targetResult = await astEdit({
|
|
90
|
+
rewrites: options.rewrites,
|
|
91
|
+
path: target.basePath,
|
|
92
|
+
glob: target.glob,
|
|
93
|
+
dryRun: options.dryRun,
|
|
94
|
+
maxFiles: options.maxFiles,
|
|
95
|
+
failOnParseError: options.failOnParseError,
|
|
96
|
+
signal: options.signal,
|
|
97
|
+
});
|
|
98
|
+
totalReplacements += targetResult.totalReplacements;
|
|
99
|
+
filesSearched += targetResult.filesSearched;
|
|
100
|
+
limitReached = limitReached || targetResult.limitReached;
|
|
101
|
+
applied = applied && targetResult.applied;
|
|
102
|
+
if (targetResult.parseErrors) parseErrors.push(...targetResult.parseErrors);
|
|
103
|
+
for (const change of targetResult.changes) {
|
|
104
|
+
const absolute = path.resolve(target.basePath, change.path);
|
|
105
|
+
const rebased = path.relative(commonBasePath, absolute).replace(/\\/g, "/");
|
|
106
|
+
aggregatedChanges.push({ ...change, path: rebased });
|
|
107
|
+
}
|
|
108
|
+
for (const fileChange of targetResult.fileChanges) {
|
|
109
|
+
const absolute = path.resolve(target.basePath, fileChange.path);
|
|
110
|
+
const rebased = path.relative(commonBasePath, absolute).replace(/\\/g, "/");
|
|
111
|
+
fileCounts.set(rebased, (fileCounts.get(rebased) ?? 0) + fileChange.count);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const fileChanges: AstReplaceFileChange[] = Array.from(fileCounts, ([changePath, count]) => ({
|
|
115
|
+
path: changePath,
|
|
116
|
+
count,
|
|
117
|
+
}));
|
|
118
|
+
return {
|
|
119
|
+
changes: aggregatedChanges,
|
|
120
|
+
fileChanges,
|
|
121
|
+
totalReplacements,
|
|
122
|
+
filesTouched: fileChanges.length,
|
|
123
|
+
filesSearched,
|
|
124
|
+
applied,
|
|
125
|
+
limitReached,
|
|
126
|
+
parseErrors: parseErrors.length > 0 ? parseErrors : undefined,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function runAstEditOnce(
|
|
131
|
+
targets: Array<{ basePath: string; glob?: string }> | undefined,
|
|
132
|
+
resolvedSearchPath: string,
|
|
133
|
+
globFilter: string | undefined,
|
|
134
|
+
options: AstEditCallOptions,
|
|
135
|
+
): Promise<AstEditAggregatedResult> {
|
|
136
|
+
if (targets) {
|
|
137
|
+
return runAstEditTargets(targets, resolvedSearchPath, options);
|
|
138
|
+
}
|
|
139
|
+
return astEdit({
|
|
140
|
+
rewrites: options.rewrites,
|
|
141
|
+
path: resolvedSearchPath,
|
|
142
|
+
glob: globFilter,
|
|
143
|
+
dryRun: options.dryRun,
|
|
144
|
+
maxFiles: options.maxFiles,
|
|
145
|
+
failOnParseError: options.failOnParseError,
|
|
146
|
+
signal: options.signal,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
55
150
|
export interface AstEditToolDetails {
|
|
56
151
|
totalReplacements: number;
|
|
57
152
|
filesTouched: number;
|
|
@@ -107,40 +202,46 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
107
202
|
const maxFiles = $envpos("PI_MAX_AST_FILES", 1000);
|
|
108
203
|
|
|
109
204
|
const formatScopePath = (targetPath: string): string => formatPathRelativeToCwd(targetPath, this.session.cwd);
|
|
110
|
-
let searchPath: string
|
|
111
|
-
let scopePath: string
|
|
205
|
+
let searchPath: string;
|
|
206
|
+
let scopePath: string;
|
|
112
207
|
let globFilter: string | undefined;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
208
|
+
let multiTargets: Array<{ basePath: string; glob?: string }> | undefined;
|
|
209
|
+
const rawPaths = params.paths.map(normalizePathLikeInput);
|
|
210
|
+
if (rawPaths.some(rawPath => rawPath.length === 0)) {
|
|
211
|
+
throw new ToolError("`paths` must contain non-empty paths or globs");
|
|
116
212
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
213
|
+
const internalRouter = this.session.internalRouter;
|
|
214
|
+
const resolvedPathInputs: string[] = [];
|
|
215
|
+
for (const rawPath of rawPaths) {
|
|
216
|
+
if (!internalRouter?.canHandle(rawPath)) {
|
|
217
|
+
resolvedPathInputs.push(rawPath);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (hasGlobPathChars(rawPath)) {
|
|
221
|
+
throw new ToolError(`Glob patterns are not supported for internal URLs: ${rawPath}`);
|
|
222
|
+
}
|
|
223
|
+
const resource = await internalRouter.resolve(rawPath);
|
|
224
|
+
if (!resource.sourcePath) {
|
|
225
|
+
throw new ToolError(`Cannot rewrite internal URL without backing file: ${rawPath}`);
|
|
226
|
+
}
|
|
227
|
+
resolvedPathInputs.push(resource.sourcePath);
|
|
228
|
+
}
|
|
229
|
+
if (resolvedPathInputs.length === 1) {
|
|
230
|
+
const parsedPath = parseSearchPath(resolvedPathInputs[0] ?? ".");
|
|
231
|
+
searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
|
|
232
|
+
globFilter = parsedPath.glob;
|
|
233
|
+
scopePath = formatScopePath(searchPath);
|
|
234
|
+
} else {
|
|
235
|
+
const multiSearchPath = await resolveExplicitSearchPaths(resolvedPathInputs, this.session.cwd, globFilter);
|
|
236
|
+
if (!multiSearchPath) {
|
|
237
|
+
throw new ToolError("`paths` must contain at least one path or glob");
|
|
141
238
|
}
|
|
239
|
+
searchPath = multiSearchPath.basePath;
|
|
240
|
+
globFilter = multiSearchPath.targets ? undefined : multiSearchPath.glob;
|
|
241
|
+
multiTargets = multiSearchPath.targets;
|
|
242
|
+
scopePath = multiSearchPath.scopePath;
|
|
142
243
|
}
|
|
143
|
-
const resolvedSearchPath = searchPath
|
|
244
|
+
const resolvedSearchPath = searchPath;
|
|
144
245
|
scopePath = scopePath ?? formatScopePath(resolvedSearchPath);
|
|
145
246
|
let isDirectory: boolean;
|
|
146
247
|
try {
|
|
@@ -150,10 +251,8 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
150
251
|
throw new ToolError(`Path not found: ${scopePath}`);
|
|
151
252
|
}
|
|
152
253
|
|
|
153
|
-
const result = await
|
|
254
|
+
const result = await runAstEditOnce(multiTargets, resolvedSearchPath, globFilter, {
|
|
154
255
|
rewrites: normalizedRewrites,
|
|
155
|
-
path: resolvedSearchPath,
|
|
156
|
-
glob: globFilter,
|
|
157
256
|
dryRun: true,
|
|
158
257
|
maxFiles,
|
|
159
258
|
failOnParseError: false,
|
|
@@ -256,7 +355,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
256
355
|
count: fileReplacementCounts.get(filePath) ?? 0,
|
|
257
356
|
}));
|
|
258
357
|
if (result.limitReached) {
|
|
259
|
-
outputLines.push("", "Limit reached; narrow
|
|
358
|
+
outputLines.push("", "Limit reached; narrow paths.");
|
|
260
359
|
}
|
|
261
360
|
if (dedupedParseErrors.length) {
|
|
262
361
|
outputLines.push("", ...formatParseErrors(dedupedParseErrors));
|
|
@@ -270,10 +369,8 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
270
369
|
label: `AST Edit: ${result.totalReplacements} replacement${previewReplacementPlural} in ${result.filesTouched} file${previewFilePlural}`,
|
|
271
370
|
sourceToolName: this.name,
|
|
272
371
|
apply: async (_reason: string) => {
|
|
273
|
-
const applyResult = await
|
|
372
|
+
const applyResult = await runAstEditOnce(multiTargets, resolvedSearchPath, globFilter, {
|
|
274
373
|
rewrites: normalizedRewrites,
|
|
275
|
-
path: resolvedSearchPath,
|
|
276
|
-
glob: globFilter,
|
|
277
374
|
dryRun: false,
|
|
278
375
|
maxFiles,
|
|
279
376
|
failOnParseError: false,
|
|
@@ -349,7 +446,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
349
446
|
|
|
350
447
|
interface AstEditRenderArgs {
|
|
351
448
|
ops?: Array<{ pat?: string; out?: string }>;
|
|
352
|
-
|
|
449
|
+
paths?: string[];
|
|
353
450
|
}
|
|
354
451
|
|
|
355
452
|
const COLLAPSED_CHANGE_LIMIT = PREVIEW_LIMITS.COLLAPSED_LINES * 2;
|
|
@@ -358,7 +455,7 @@ export const astEditToolRenderer = {
|
|
|
358
455
|
inline: true,
|
|
359
456
|
renderCall(args: AstEditRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
360
457
|
const meta: string[] = [];
|
|
361
|
-
if (args.
|
|
458
|
+
if (args.paths?.length) meta.push(`in ${args.paths.join(", ")}`);
|
|
362
459
|
const rewriteCount = args.ops?.length ?? 0;
|
|
363
460
|
if (rewriteCount > 1) meta.push(`${rewriteCount} rewrites`);
|
|
364
461
|
|
package/src/tools/ast-grep.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
1
2
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
2
3
|
import { type AstFindMatch, astGrep } from "@oh-my-pi/pi-natives";
|
|
3
4
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
@@ -19,7 +20,7 @@ import {
|
|
|
19
20
|
hasGlobPathChars,
|
|
20
21
|
normalizePathLikeInput,
|
|
21
22
|
parseSearchPath,
|
|
22
|
-
|
|
23
|
+
resolveExplicitSearchPaths,
|
|
23
24
|
resolveToCwd,
|
|
24
25
|
} from "./path-utils";
|
|
25
26
|
import {
|
|
@@ -37,13 +38,71 @@ import { toolResult } from "./tool-result";
|
|
|
37
38
|
|
|
38
39
|
const astGrepSchema = Type.Object({
|
|
39
40
|
pat: Type.String({ description: "ast pattern", examples: ["console.log($$$)"] }),
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
paths: Type.Array(Type.String({ description: "file, directory, glob, or internal URL to search" }), {
|
|
42
|
+
minItems: 1,
|
|
43
|
+
description: "files, directories, globs, or internal URLs to search",
|
|
44
|
+
examples: [["src/"], ["src/foo.ts"], ["src/**/*.ts"], ["src/", "packages/"]],
|
|
43
45
|
}),
|
|
44
46
|
skip: Type.Optional(Type.Number({ description: "matches to skip", default: 0 })),
|
|
45
47
|
});
|
|
46
48
|
|
|
49
|
+
async function runMultiTargetAstGrep(
|
|
50
|
+
targets: Array<{ basePath: string; glob?: string }>,
|
|
51
|
+
options: { patterns: string[]; commonBasePath: string; skip: number; limit: number; signal?: AbortSignal },
|
|
52
|
+
): Promise<{
|
|
53
|
+
matches: AstFindMatch[];
|
|
54
|
+
totalMatches: number;
|
|
55
|
+
filesWithMatches: number;
|
|
56
|
+
filesSearched: number;
|
|
57
|
+
limitReached: boolean;
|
|
58
|
+
parseErrors?: string[];
|
|
59
|
+
}> {
|
|
60
|
+
const aggregatedMatches: AstFindMatch[] = [];
|
|
61
|
+
const parseErrors: string[] = [];
|
|
62
|
+
let totalMatches = 0;
|
|
63
|
+
let filesSearched = 0;
|
|
64
|
+
let limitReached = false;
|
|
65
|
+
for (const target of targets) {
|
|
66
|
+
const targetResult = await astGrep({
|
|
67
|
+
patterns: options.patterns,
|
|
68
|
+
path: target.basePath,
|
|
69
|
+
glob: target.glob,
|
|
70
|
+
offset: 0,
|
|
71
|
+
limit: options.skip + options.limit + 1,
|
|
72
|
+
includeMeta: true,
|
|
73
|
+
signal: options.signal,
|
|
74
|
+
});
|
|
75
|
+
totalMatches += targetResult.totalMatches;
|
|
76
|
+
filesSearched += targetResult.filesSearched;
|
|
77
|
+
limitReached = limitReached || targetResult.limitReached;
|
|
78
|
+
if (targetResult.parseErrors) parseErrors.push(...targetResult.parseErrors);
|
|
79
|
+
for (const match of targetResult.matches) {
|
|
80
|
+
const absolute = path.resolve(target.basePath, match.path);
|
|
81
|
+
const rebased = path.relative(options.commonBasePath, absolute).replace(/\\/g, "/");
|
|
82
|
+
aggregatedMatches.push({ ...match, path: rebased });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
aggregatedMatches.sort((left, right) => {
|
|
86
|
+
const pathCmp = left.path.localeCompare(right.path);
|
|
87
|
+
if (pathCmp !== 0) return pathCmp;
|
|
88
|
+
if (left.startLine !== right.startLine) return left.startLine - right.startLine;
|
|
89
|
+
if (left.startColumn !== right.startColumn) return left.startColumn - right.startColumn;
|
|
90
|
+
if (left.byteStart !== right.byteStart) return left.byteStart - right.byteStart;
|
|
91
|
+
return left.byteEnd - right.byteEnd;
|
|
92
|
+
});
|
|
93
|
+
const visible = aggregatedMatches.slice(options.skip);
|
|
94
|
+
const paged = visible.slice(0, options.limit);
|
|
95
|
+
const filesWithMatches = new Set(aggregatedMatches.map(match => match.path)).size;
|
|
96
|
+
return {
|
|
97
|
+
matches: paged,
|
|
98
|
+
totalMatches,
|
|
99
|
+
filesWithMatches,
|
|
100
|
+
filesSearched,
|
|
101
|
+
limitReached: limitReached || visible.length > options.limit,
|
|
102
|
+
parseErrors: parseErrors.length > 0 ? parseErrors : undefined,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
47
106
|
export interface AstGrepToolDetails {
|
|
48
107
|
matchCount: number;
|
|
49
108
|
fileCount: number;
|
|
@@ -88,15 +147,21 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
88
147
|
throw new ToolError("skip must be a non-negative number");
|
|
89
148
|
}
|
|
90
149
|
const formatScopePath = (targetPath: string): string => formatPathRelativeToCwd(targetPath, this.session.cwd);
|
|
91
|
-
let searchPath: string
|
|
92
|
-
let scopePath: string
|
|
150
|
+
let searchPath: string;
|
|
151
|
+
let scopePath: string;
|
|
93
152
|
let globFilter: string | undefined;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
153
|
+
let multiTargets: Array<{ basePath: string; glob?: string }> | undefined;
|
|
154
|
+
const rawPaths = params.paths.map(normalizePathLikeInput);
|
|
155
|
+
if (rawPaths.some(rawPath => rawPath.length === 0)) {
|
|
156
|
+
throw new ToolError("`paths` must contain non-empty paths or globs");
|
|
97
157
|
}
|
|
98
158
|
const internalRouter = this.session.internalRouter;
|
|
99
|
-
|
|
159
|
+
const resolvedPathInputs: string[] = [];
|
|
160
|
+
for (const rawPath of rawPaths) {
|
|
161
|
+
if (!internalRouter?.canHandle(rawPath)) {
|
|
162
|
+
resolvedPathInputs.push(rawPath);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
100
165
|
if (hasGlobPathChars(rawPath)) {
|
|
101
166
|
throw new ToolError(`Glob patterns are not supported for internal URLs: ${rawPath}`);
|
|
102
167
|
}
|
|
@@ -104,23 +169,25 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
104
169
|
if (!resource.sourcePath) {
|
|
105
170
|
throw new ToolError(`Cannot search internal URL without backing file: ${rawPath}`);
|
|
106
171
|
}
|
|
107
|
-
|
|
172
|
+
resolvedPathInputs.push(resource.sourcePath);
|
|
173
|
+
}
|
|
174
|
+
if (resolvedPathInputs.length === 1) {
|
|
175
|
+
const parsedPath = parseSearchPath(resolvedPathInputs[0] ?? ".");
|
|
176
|
+
searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
|
|
177
|
+
globFilter = parsedPath.glob;
|
|
108
178
|
scopePath = formatScopePath(searchPath);
|
|
109
179
|
} else {
|
|
110
|
-
const multiSearchPath = await
|
|
111
|
-
if (multiSearchPath) {
|
|
112
|
-
|
|
113
|
-
globFilter = multiSearchPath.glob;
|
|
114
|
-
scopePath = multiSearchPath.scopePath;
|
|
115
|
-
} else {
|
|
116
|
-
const parsedPath = parseSearchPath(rawPath);
|
|
117
|
-
searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
|
|
118
|
-
globFilter = parsedPath.glob;
|
|
119
|
-
scopePath = formatScopePath(searchPath);
|
|
180
|
+
const multiSearchPath = await resolveExplicitSearchPaths(resolvedPathInputs, this.session.cwd, globFilter);
|
|
181
|
+
if (!multiSearchPath) {
|
|
182
|
+
throw new ToolError("`paths` must contain at least one path or glob");
|
|
120
183
|
}
|
|
184
|
+
searchPath = multiSearchPath.basePath;
|
|
185
|
+
globFilter = multiSearchPath.targets ? undefined : multiSearchPath.glob;
|
|
186
|
+
multiTargets = multiSearchPath.targets;
|
|
187
|
+
scopePath = multiSearchPath.scopePath;
|
|
121
188
|
}
|
|
122
189
|
|
|
123
|
-
const resolvedSearchPath = searchPath
|
|
190
|
+
const resolvedSearchPath = searchPath;
|
|
124
191
|
scopePath = scopePath ?? formatScopePath(resolvedSearchPath);
|
|
125
192
|
let isDirectory: boolean;
|
|
126
193
|
try {
|
|
@@ -130,14 +197,23 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
130
197
|
throw new ToolError(`Path not found: ${scopePath}`);
|
|
131
198
|
}
|
|
132
199
|
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
200
|
+
const DEFAULT_AST_LIMIT = 50;
|
|
201
|
+
const result = multiTargets
|
|
202
|
+
? await runMultiTargetAstGrep(multiTargets, {
|
|
203
|
+
patterns,
|
|
204
|
+
commonBasePath: resolvedSearchPath,
|
|
205
|
+
skip,
|
|
206
|
+
limit: DEFAULT_AST_LIMIT,
|
|
207
|
+
signal,
|
|
208
|
+
})
|
|
209
|
+
: await astGrep({
|
|
210
|
+
patterns,
|
|
211
|
+
path: resolvedSearchPath,
|
|
212
|
+
glob: globFilter,
|
|
213
|
+
offset: skip,
|
|
214
|
+
includeMeta: true,
|
|
215
|
+
signal,
|
|
216
|
+
});
|
|
141
217
|
|
|
142
218
|
const normalizedParseErrors = (result.parseErrors ?? []).map(error => {
|
|
143
219
|
const parseError = error.match(/^.+: (.+: parse error \(syntax tree contains error nodes\))$/);
|
|
@@ -172,7 +248,7 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
172
248
|
|
|
173
249
|
if (result.matches.length === 0) {
|
|
174
250
|
const noMatchMessage = dedupedParseErrors.length
|
|
175
|
-
? "No matches found. Parse issues mean the query may be mis-scoped; narrow `
|
|
251
|
+
? "No matches found. Parse issues mean the query may be mis-scoped; narrow `paths` before concluding absence."
|
|
176
252
|
: "No matches found";
|
|
177
253
|
const parseMessage = dedupedParseErrors.length
|
|
178
254
|
? `\n${formatParseErrors(dedupedParseErrors).join("\n")}`
|
|
@@ -238,7 +314,7 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
238
314
|
displayContent: displayLines.join("\n"),
|
|
239
315
|
};
|
|
240
316
|
if (result.limitReached) {
|
|
241
|
-
outputLines.push("", "Result limit reached; narrow
|
|
317
|
+
outputLines.push("", "Result limit reached; narrow paths or increase limit.");
|
|
242
318
|
}
|
|
243
319
|
if (dedupedParseErrors.length) {
|
|
244
320
|
outputLines.push("", ...formatParseErrors(dedupedParseErrors));
|
|
@@ -255,7 +331,7 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
255
331
|
|
|
256
332
|
interface AstGrepRenderArgs {
|
|
257
333
|
pat?: string;
|
|
258
|
-
|
|
334
|
+
paths?: string[];
|
|
259
335
|
skip?: number;
|
|
260
336
|
}
|
|
261
337
|
|
|
@@ -265,7 +341,7 @@ export const astGrepToolRenderer = {
|
|
|
265
341
|
inline: true,
|
|
266
342
|
renderCall(args: AstGrepRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
267
343
|
const meta: string[] = [];
|
|
268
|
-
if (args.
|
|
344
|
+
if (args.paths?.length) meta.push(`in ${args.paths.join(", ")}`);
|
|
269
345
|
if (args.skip !== undefined && args.skip > 0) meta.push(`skip:${args.skip}`);
|
|
270
346
|
|
|
271
347
|
const description = args.pat ?? "?";
|
|
@@ -299,7 +375,7 @@ export const astGrepToolRenderer = {
|
|
|
299
375
|
const header = renderStatusLine({ icon: "warning", title: "AST Grep", description, meta }, uiTheme);
|
|
300
376
|
const lines = [header, formatEmptyMessage("No matches found", uiTheme)];
|
|
301
377
|
if (details?.parseErrors?.length) {
|
|
302
|
-
lines.push(uiTheme.fg("warning", "Query may be mis-scoped; narrow `
|
|
378
|
+
lines.push(uiTheme.fg("warning", "Query may be mis-scoped; narrow `paths` before concluding absence"));
|
|
303
379
|
const capped = details.parseErrors.slice(0, PARSE_ERRORS_LIMIT);
|
|
304
380
|
for (const err of capped) {
|
|
305
381
|
lines.push(uiTheme.fg("warning", ` - ${err}`));
|
|
@@ -351,7 +427,7 @@ export const astGrepToolRenderer = {
|
|
|
351
427
|
|
|
352
428
|
const extraLines: string[] = [];
|
|
353
429
|
if (limitReached) {
|
|
354
|
-
extraLines.push(uiTheme.fg("warning", "limit reached; narrow
|
|
430
|
+
extraLines.push(uiTheme.fg("warning", "limit reached; narrow paths or increase limit"));
|
|
355
431
|
}
|
|
356
432
|
if (details?.parseErrors?.length) {
|
|
357
433
|
const total = details.parseErrors.length;
|
package/src/tools/eval.ts
CHANGED
|
@@ -26,7 +26,7 @@ export const EVAL_DEFAULT_PREVIEW_LINES = 10;
|
|
|
26
26
|
|
|
27
27
|
export const evalSchema = Type.Object({
|
|
28
28
|
input: Type.String({
|
|
29
|
-
description: "
|
|
29
|
+
description: "eval input as a sequence of `===== <info> =====` cell headers followed by code",
|
|
30
30
|
}),
|
|
31
31
|
});
|
|
32
32
|
export type EvalToolParams = Static<typeof evalSchema>;
|
|
@@ -250,7 +250,7 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
|
|
|
250
250
|
let previousRuntimeLanguage: EvalLanguage | undefined;
|
|
251
251
|
const cells: ResolvedEvalCell[] = [];
|
|
252
252
|
for (const cell of parsedInput.cells) {
|
|
253
|
-
const requested = cell.languageOrigin === "
|
|
253
|
+
const requested = cell.languageOrigin === "header" ? cell.language : (previousRuntimeLanguage ?? undefined);
|
|
254
254
|
const resolved = await resolveBackend(session, requested, cell.code);
|
|
255
255
|
previousRuntimeLanguage = resolved.backend.id;
|
|
256
256
|
cells.push({
|
|
@@ -600,12 +600,6 @@ function formatStatusEvent(event: EvalStatusEvent, theme: Theme): string {
|
|
|
600
600
|
pwd: "icon.folder",
|
|
601
601
|
mkdir: "icon.folder",
|
|
602
602
|
tree: "icon.folder",
|
|
603
|
-
stat: "icon.folder",
|
|
604
|
-
find: "icon.file",
|
|
605
|
-
grep: "icon.file",
|
|
606
|
-
rgrep: "icon.file",
|
|
607
|
-
glob: "icon.file",
|
|
608
|
-
sed: "icon.file",
|
|
609
603
|
git_status: "icon.git",
|
|
610
604
|
git_diff: "icon.git",
|
|
611
605
|
git_log: "icon.git",
|
|
@@ -642,19 +636,6 @@ function formatStatusEvent(event: EvalStatusEvent, theme: Theme): string {
|
|
|
642
636
|
parts.push(`${data.files} file${(data.files as number) !== 1 ? "s" : ""}`);
|
|
643
637
|
parts.push(`${data.chars} chars`);
|
|
644
638
|
break;
|
|
645
|
-
case "find":
|
|
646
|
-
case "glob":
|
|
647
|
-
parts.push(`${data.count} match${(data.count as number) !== 1 ? "es" : ""}`);
|
|
648
|
-
if (data.pattern) parts.push(`for "${truncateToWidth(String(data.pattern), 20)}"`);
|
|
649
|
-
break;
|
|
650
|
-
case "grep":
|
|
651
|
-
parts.push(`${data.count} match${(data.count as number) !== 1 ? "es" : ""}`);
|
|
652
|
-
if (data.path) parts.push(`in ${shortenPath(String(data.path))}`);
|
|
653
|
-
break;
|
|
654
|
-
case "rgrep":
|
|
655
|
-
parts.push(`${data.count} match${(data.count as number) !== 1 ? "es" : ""}`);
|
|
656
|
-
if (data.pattern) parts.push(`for "${truncateToWidth(String(data.pattern), 20)}"`);
|
|
657
|
-
break;
|
|
658
639
|
case "ls":
|
|
659
640
|
parts.push(`${data.count} entr${(data.count as number) !== 1 ? "ies" : "y"}`);
|
|
660
641
|
break;
|
|
@@ -667,18 +648,6 @@ function formatStatusEvent(event: EvalStatusEvent, theme: Theme): string {
|
|
|
667
648
|
parts.push(`${data.count} variable${(data.count as number) !== 1 ? "s" : ""}`);
|
|
668
649
|
}
|
|
669
650
|
break;
|
|
670
|
-
case "stat":
|
|
671
|
-
if (data.is_dir) {
|
|
672
|
-
parts.push("directory");
|
|
673
|
-
} else {
|
|
674
|
-
parts.push(`${data.size} bytes`);
|
|
675
|
-
}
|
|
676
|
-
if (data.path) parts.push(shortenPath(String(data.path)));
|
|
677
|
-
break;
|
|
678
|
-
case "sed":
|
|
679
|
-
parts.push(`${data.count} replacement${(data.count as number) !== 1 ? "s" : ""}`);
|
|
680
|
-
if (data.path) parts.push(`in ${shortenPath(String(data.path))}`);
|
|
681
|
-
break;
|
|
682
651
|
case "git_status":
|
|
683
652
|
if (data.clean) {
|
|
684
653
|
parts.push("clean");
|
|
@@ -759,29 +728,9 @@ function formatStatusEventExpanded(event: EvalStatusEvent, theme: Theme): string
|
|
|
759
728
|
};
|
|
760
729
|
|
|
761
730
|
switch (op) {
|
|
762
|
-
case "find":
|
|
763
|
-
case "glob":
|
|
764
|
-
if (data.matches) addItems(data.matches as unknown[], m => String(m));
|
|
765
|
-
break;
|
|
766
731
|
case "ls":
|
|
767
732
|
if (data.items) addItems(data.items as unknown[], m => String(m));
|
|
768
733
|
break;
|
|
769
|
-
case "grep":
|
|
770
|
-
if (data.hits) {
|
|
771
|
-
addItems(data.hits as unknown[], h => {
|
|
772
|
-
const hit = h as { line: number; text: string };
|
|
773
|
-
return `${hit.line}: ${truncateToWidth(hit.text, 60)}`;
|
|
774
|
-
});
|
|
775
|
-
}
|
|
776
|
-
break;
|
|
777
|
-
case "rgrep":
|
|
778
|
-
if (data.hits) {
|
|
779
|
-
addItems(data.hits as unknown[], h => {
|
|
780
|
-
const hit = h as { file: string; line: number; text: string };
|
|
781
|
-
return `${shortenPath(hit.file)}:${hit.line}: ${truncateToWidth(hit.text, 50)}`;
|
|
782
|
-
});
|
|
783
|
-
}
|
|
784
|
-
break;
|
|
785
734
|
case "env":
|
|
786
735
|
if (data.keys) addItems(data.keys as unknown[], k => String(k), 10);
|
|
787
736
|
break;
|