pi-readseek 0.7.6 → 0.7.8
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/README.md +28 -46
- package/dist/index.js +7333 -0
- package/package.json +8 -7
- package/index.ts +0 -147
- package/src/binary-detect.ts +0 -22
- package/src/check.ts +0 -92
- package/src/coerce-obvious-int.ts +0 -41
- package/src/confusable-hyphens.ts +0 -6
- package/src/def.ts +0 -143
- package/src/diff-data.ts +0 -302
- package/src/edit-classify.ts +0 -58
- package/src/edit-diff.ts +0 -359
- package/src/edit-output.ts +0 -110
- package/src/edit-render-helpers.ts +0 -141
- package/src/edit-syntax-validate.ts +0 -65
- package/src/edit.ts +0 -686
- package/src/file-map.ts +0 -17
- package/src/fs-error.ts +0 -33
- package/src/grep-budget.ts +0 -35
- package/src/grep-output.ts +0 -181
- package/src/grep-render-helpers.ts +0 -77
- package/src/grep-symbol-scope.ts +0 -184
- package/src/grep.ts +0 -608
- package/src/hashline.ts +0 -736
- package/src/hover.ts +0 -140
- package/src/path-utils.ts +0 -24
- package/src/pending-diff-preview.ts +0 -269
- package/src/read-local-bundle.ts +0 -75
- package/src/read-output.ts +0 -186
- package/src/read-render-helpers.ts +0 -104
- package/src/read.ts +0 -661
- package/src/readseek/constants.ts +0 -17
- package/src/readseek/enums.ts +0 -38
- package/src/readseek/formatter.ts +0 -350
- package/src/readseek/symbol-error-format.ts +0 -18
- package/src/readseek/symbol-lookup.ts +0 -240
- package/src/readseek/symbol-tree.ts +0 -21
- package/src/readseek/types.ts +0 -49
- package/src/readseek-client.ts +0 -1250
- package/src/readseek-params.ts +0 -39
- package/src/readseek-settings.ts +0 -263
- package/src/readseek-value.ts +0 -184
- package/src/refs-output.ts +0 -47
- package/src/refs.ts +0 -161
- package/src/register-tool.ts +0 -29
- package/src/rename.ts +0 -182
- package/src/replace-symbol.ts +0 -77
- package/src/runtime.ts +0 -3
- package/src/search-output.ts +0 -50
- package/src/search.ts +0 -214
- package/src/session-anchors.ts +0 -34
- package/src/stat-search-path.ts +0 -29
- package/src/syntax-validate-mode.ts +0 -15
- package/src/tool-prompt-metadata.ts +0 -107
- package/src/tool-types.ts +0 -12
- package/src/tui-diff-component.ts +0 -108
- package/src/tui-diff-renderer.ts +0 -92
- package/src/tui-render-utils.ts +0 -286
- package/src/view.ts +0 -158
- package/src/write.ts +0 -357
package/src/register-tool.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { Type } from "@sinclair/typebox";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Optional tool parameter accepting an integer or its string form, since models
|
|
6
|
-
* frequently pass integers as quoted strings. Both union halves share
|
|
7
|
-
* {@link description}.
|
|
8
|
-
*/
|
|
9
|
-
export function optionalIntOrString(description: string) {
|
|
10
|
-
return Type.Optional(Type.Union([Type.Number({ description }), Type.String({ description })]));
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/** Required file-path parameter shared by the read, edit, and write tools. */
|
|
14
|
-
export function filePathParam() {
|
|
15
|
-
return Type.String({ description: "Absolute or relative file path" });
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** Optional structural-map toggle shared by the read and write tools. */
|
|
19
|
-
export function mapParam() {
|
|
20
|
-
return Type.Optional(Type.Boolean({ description: "Append structural map" }));
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
type ToolSpec = Parameters<ExtensionAPI["registerTool"]>[0];
|
|
24
|
-
|
|
25
|
-
/** Register a readseek-backed tool definition. */
|
|
26
|
-
export function registerReadSeekTool<T extends ToolSpec>(pi: ExtensionAPI, tool: T): T {
|
|
27
|
-
pi.registerTool(tool);
|
|
28
|
-
return tool;
|
|
29
|
-
}
|
package/src/rename.ts
DELETED
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
import type { ExtensionAPI, ToolRenderResultOptions } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { Type } from "@sinclair/typebox";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
5
|
-
|
|
6
|
-
import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
|
|
7
|
-
import { buildToolErrorResult } from "./readseek-value.js";
|
|
8
|
-
import { resolveToCwd } from "./path-utils.js";
|
|
9
|
-
import { classifyReadSeekFailure, readSeekRename, type RenameOutput } from "./readseek-client.js";
|
|
10
|
-
import { filePathParam, registerReadSeekTool } from "./register-tool.js";
|
|
11
|
-
import type { FileMutatedCallback } from "./tool-types.js";
|
|
12
|
-
|
|
13
|
-
import { clampLineToWidth, clampLinesToWidth, linkedPathLine, linkToolPath, renderErrorResult, renderPendingResult, resolveRenderResultContext, summaryLine } from "./tui-render-utils.js";
|
|
14
|
-
|
|
15
|
-
const RENAME_PROMPT_METADATA = defineToolPromptMetadata({
|
|
16
|
-
promptUrl: new URL("../prompts/rename.md", import.meta.url),
|
|
17
|
-
promptSnippet: "Rename the symbol at a cursor without touching shadows",
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
const renameSchema = Type.Object({
|
|
21
|
-
path: filePathParam(),
|
|
22
|
-
line: Type.Integer({ minimum: 1, description: "One-based line of the symbol to rename" }),
|
|
23
|
-
column: Type.Optional(Type.Integer({ minimum: 1, description: "One-based byte column for disambiguation" })),
|
|
24
|
-
to: Type.String({ description: "New symbol name" }),
|
|
25
|
-
workspace: Type.Optional(Type.Boolean({ description: "Rename across the project" })),
|
|
26
|
-
apply: Type.Optional(Type.Boolean({ description: "Apply the verified edits; default true" })),
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
interface RenameParams {
|
|
30
|
-
path: string;
|
|
31
|
-
line: number;
|
|
32
|
-
column?: number;
|
|
33
|
-
to: string;
|
|
34
|
-
workspace?: boolean;
|
|
35
|
-
apply?: boolean;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface ExecuteRenameOptions {
|
|
39
|
-
params: unknown;
|
|
40
|
-
signal: AbortSignal | undefined;
|
|
41
|
-
cwd: string;
|
|
42
|
-
onFileMutated?: FileMutatedCallback;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export async function executeRename(opts: ExecuteRenameOptions): Promise<any> {
|
|
46
|
-
const { params, signal, cwd, onFileMutated } = opts;
|
|
47
|
-
const p = params as RenameParams;
|
|
48
|
-
|
|
49
|
-
if (!p.to.trim()) {
|
|
50
|
-
return buildToolErrorResult("rename", "invalid-parameter", "rename parameter 'to' must not be empty");
|
|
51
|
-
}
|
|
52
|
-
if (!Number.isSafeInteger(p.line) || p.line < 1) {
|
|
53
|
-
return buildToolErrorResult("rename", "invalid-parameter", "rename parameter 'line' must be a positive integer");
|
|
54
|
-
}
|
|
55
|
-
if (p.column !== undefined && (!Number.isSafeInteger(p.column) || p.column < 1)) {
|
|
56
|
-
return buildToolErrorResult("rename", "invalid-parameter", "rename parameter 'column' must be a positive integer");
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const filePath = resolveToCwd(p.path, cwd);
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
const output = await readSeekRename(filePath, {
|
|
63
|
-
to: p.to,
|
|
64
|
-
line: p.line,
|
|
65
|
-
column: p.column,
|
|
66
|
-
workspace: p.workspace ? cwd : undefined,
|
|
67
|
-
apply: p.apply ?? true,
|
|
68
|
-
signal,
|
|
69
|
-
});
|
|
70
|
-
if (output.applied) {
|
|
71
|
-
for (const file of [output, ...output.others]) {
|
|
72
|
-
if (file.edits.length === 0) continue;
|
|
73
|
-
const absoluteFile = path.isAbsolute(file.file) ? file.file : path.resolve(cwd, file.file);
|
|
74
|
-
onFileMutated?.(absoluteFile);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const files: string[] = [output.file];
|
|
79
|
-
for (const other of output.others) {
|
|
80
|
-
const abs = path.isAbsolute(other.file) ? other.file : path.resolve(cwd, other.file);
|
|
81
|
-
if (!files.includes(abs)) files.push(abs);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const totalEdits = output.edits.length + output.others.reduce((sum, o) => sum + o.edits.length, 0);
|
|
85
|
-
const totalConflicts = output.conflicts.length + output.others.reduce((sum, o) => sum + o.conflicts.length, 0);
|
|
86
|
-
|
|
87
|
-
const parts: string[] = [];
|
|
88
|
-
if (totalConflicts > 0) {
|
|
89
|
-
parts.push(`${totalConflicts} naming conflict(s)`);
|
|
90
|
-
}
|
|
91
|
-
parts.push(`renamed ${output.old_name} to ${output.new_name} in ${totalEdits} location(s) across ${files.length} file(s)`);
|
|
92
|
-
|
|
93
|
-
let text = parts.join("; ");
|
|
94
|
-
if (output.applied) text += " (applied)";
|
|
95
|
-
else text += " (dry-run)";
|
|
96
|
-
|
|
97
|
-
// Surface first conflict reason for visibility
|
|
98
|
-
const firstConflict = output.conflicts[0]
|
|
99
|
-
?? output.others.find((o) => o.conflicts.length > 0)?.conflicts[0];
|
|
100
|
-
if (firstConflict) {
|
|
101
|
-
text += `\nFirst conflict at ${firstConflict.line}:${firstConflict.column}: ${firstConflict.reason}`;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return {
|
|
105
|
-
content: [{ type: "text", text }],
|
|
106
|
-
details: {
|
|
107
|
-
readSeekValue: {
|
|
108
|
-
tool: "rename",
|
|
109
|
-
ok: true,
|
|
110
|
-
path: filePath,
|
|
111
|
-
output,
|
|
112
|
-
},
|
|
113
|
-
},
|
|
114
|
-
};
|
|
115
|
-
} catch (err: any) {
|
|
116
|
-
const failure = classifyReadSeekFailure(err);
|
|
117
|
-
return buildToolErrorResult("rename", failure.code, failure.message, failure.hint ? { hint: failure.hint } : {});
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export function registerRenameTool(pi: ExtensionAPI, options: { onFileMutated?: FileMutatedCallback } = {}) {
|
|
122
|
-
registerReadSeekTool(pi, {
|
|
123
|
-
name: "readSeek_rename",
|
|
124
|
-
label: "Rename",
|
|
125
|
-
description: RENAME_PROMPT_METADATA.description,
|
|
126
|
-
promptSnippet: RENAME_PROMPT_METADATA.promptSnippet,
|
|
127
|
-
promptGuidelines: RENAME_PROMPT_METADATA.promptGuidelines,
|
|
128
|
-
parameters: renameSchema,
|
|
129
|
-
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
130
|
-
return executeRename({ params, signal, cwd: ctx.cwd, onFileMutated: options.onFileMutated });
|
|
131
|
-
},
|
|
132
|
-
renderCall(args: any, theme: any, ...rest: any[]) {
|
|
133
|
-
const context = rest[0] ?? {};
|
|
134
|
-
const cwd = context.cwd ?? process.cwd();
|
|
135
|
-
const displayPath = typeof args?.path === "string" ? args.path : "?";
|
|
136
|
-
let text = theme.fg("toolTitle", theme.bold("rename"));
|
|
137
|
-
text += ` ${linkToolPath(theme.fg("accent", displayPath), displayPath, cwd)}`;
|
|
138
|
-
if (args?.to) text += theme.fg("dim", ` → ${args.to}`);
|
|
139
|
-
return new Text(clampLineToWidth(text, context.width), 0, 0);
|
|
140
|
-
},
|
|
141
|
-
renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
|
|
142
|
-
const { isPartial, isError, expanded, cwd, width } = resolveRenderResultContext(options, rest);
|
|
143
|
-
|
|
144
|
-
if (isPartial) return renderPendingResult("pending rename", width, theme);
|
|
145
|
-
|
|
146
|
-
const content = result.content?.[0];
|
|
147
|
-
const textContent = content?.type === "text" ? content.text : "";
|
|
148
|
-
const readSeekValue = (result.details as any)?.readSeekValue;
|
|
149
|
-
const output = readSeekValue?.output as RenameOutput | undefined;
|
|
150
|
-
|
|
151
|
-
if (isError || result.isError) {
|
|
152
|
-
return renderErrorResult(textContent, { expanded, width, fallback: "rename failed", theme });
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const outputs = output ? [output, ...output.others] : [];
|
|
156
|
-
const editCount = outputs.reduce((total, file) => total + file.edits.length, 0);
|
|
157
|
-
const conflictCount = outputs.reduce((total, file) => total + file.conflicts.length, 0);
|
|
158
|
-
const fileCount = outputs.length;
|
|
159
|
-
const action = output?.applied
|
|
160
|
-
? `renamed ${output.old_name} → ${output.new_name}`
|
|
161
|
-
: `rename plan for ${output?.old_name ?? "?"} → ${output?.new_name ?? "?"} (dry-run)`;
|
|
162
|
-
const details = output
|
|
163
|
-
? ` • ${editCount} ${editCount === 1 ? "edit" : "edits"} in ${fileCount} ${fileCount === 1 ? "file" : "files"}`
|
|
164
|
-
: "";
|
|
165
|
-
const conflicts = conflictCount > 0 ? ` • ${conflictCount} ${conflictCount === 1 ? "conflict" : "conflicts"}` : "";
|
|
166
|
-
let text = summaryLine(`${action}${details}${conflicts}`, {
|
|
167
|
-
theme,
|
|
168
|
-
style: conflictCount > 0 ? "warning" : "success",
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
if (expanded && output) {
|
|
172
|
-
for (const file of outputs) {
|
|
173
|
-
const display = path.relative(cwd, file.file) || file.file;
|
|
174
|
-
text += `\n${linkedPathLine(theme, file.file, display, cwd, ` (${file.edits.length} edits)`, width)}`;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const lines = clampLinesToWidth(text.split("\n"), width);
|
|
179
|
-
return new Text(lines.join("\n"), 0, 0);
|
|
180
|
-
},
|
|
181
|
-
});
|
|
182
|
-
}
|
package/src/replace-symbol.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { readSeekMapContent } from "./readseek-client.js";
|
|
2
|
-
import { findSymbol } from "./readseek/symbol-lookup.js";
|
|
3
|
-
import { formatAmbiguous, formatNotFound } from "./readseek/symbol-error-format.js";
|
|
4
|
-
import { normalizeToLF } from "./edit-diff.js";
|
|
5
|
-
|
|
6
|
-
interface ReplaceSymbolInput {
|
|
7
|
-
filePath: string;
|
|
8
|
-
content: string;
|
|
9
|
-
symbol: string;
|
|
10
|
-
newBody: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
type ReplaceSymbolResult =
|
|
14
|
-
| { type: "ok"; content: string; replacement: string; warnings: string[]; range: { start: number; end: number } }
|
|
15
|
-
| { type: "not-found"; message: string }
|
|
16
|
-
| { type: "ambiguous"; message: string }
|
|
17
|
-
| { type: "unsupported"; message: string };
|
|
18
|
-
|
|
19
|
-
function detectIndent(line: string): string {
|
|
20
|
-
return line.match(/^\s*/)?.[0] ?? "";
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function dedent(text: string): string {
|
|
24
|
-
const lines = text.split("\n");
|
|
25
|
-
const nonEmpty = lines.filter((l) => l.trim().length);
|
|
26
|
-
if (!nonEmpty.length) return text;
|
|
27
|
-
const minIndent = Math.min(...nonEmpty.map((l) => l.match(/^\s*/)?.[0].length ?? 0));
|
|
28
|
-
return lines.map((l) => l.slice(minIndent)).join("\n");
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function reindent(text: string, indent: string): string {
|
|
32
|
-
return text.split("\n").map((l) => (l.length ? indent + l : l)).join("\n");
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export async function replaceSymbol(input: ReplaceSymbolInput): Promise<ReplaceSymbolResult> {
|
|
36
|
-
const map = await readSeekMapContent(input.filePath, input.content);
|
|
37
|
-
if (!map) {
|
|
38
|
-
return {
|
|
39
|
-
type: "unsupported",
|
|
40
|
-
message: `Unsupported file for replace_symbol: readseek could not map '${input.filePath}'. Use anchored edits for this file type.`,
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
const lookup = findSymbol(map, input.symbol);
|
|
44
|
-
if (lookup.type === "not-found") {
|
|
45
|
-
return { type: "not-found", message: formatNotFound(input.symbol, map) };
|
|
46
|
-
}
|
|
47
|
-
if (lookup.type === "ambiguous") {
|
|
48
|
-
return { type: "ambiguous", message: formatAmbiguous(input.symbol, lookup.candidates) };
|
|
49
|
-
}
|
|
50
|
-
const sym = lookup.symbol;
|
|
51
|
-
const lines = input.content.split("\n");
|
|
52
|
-
const sigLine = lines[sym.startLine - 1] ?? "";
|
|
53
|
-
const indent = detectIndent(sigLine);
|
|
54
|
-
const reindented = reindent(dedent(normalizeToLF(input.newBody)), indent);
|
|
55
|
-
const warnings: string[] = [];
|
|
56
|
-
const leaf = input.symbol.replace(/@\d+$/, "").split(".").pop() ?? "";
|
|
57
|
-
const declNameRe =
|
|
58
|
-
/\b(?:(?:export\s+(?:default\s+)?|async\s+|public\s+|private\s+|protected\s+|static\s+)*)(?:function\*?|class|const|let|var|fn|def|method|type|interface|enum)\s+([A-Za-z_$][\w$]*)/s;
|
|
59
|
-
const firstDeclName =
|
|
60
|
-
reindented.match(declNameRe)?.[1]
|
|
61
|
-
?? reindented.match(/^\s*(?:(?:async\s+)?[\w$<>,?\s]+\s+)?([A-Za-z_$][\w$]*)\s*\(/)?.[1];
|
|
62
|
-
if (leaf && firstDeclName && firstDeclName !== leaf) {
|
|
63
|
-
warnings.push(`name-mismatch: expected ${leaf}, got ${firstDeclName}`);
|
|
64
|
-
}
|
|
65
|
-
const before = lines.slice(0, sym.startLine - 1).join("\n");
|
|
66
|
-
const after = lines.slice(sym.endLine).join("\n");
|
|
67
|
-
const beforePart = before.length ? before + "\n" : "";
|
|
68
|
-
const afterPart = after.length ? "\n" + after : "";
|
|
69
|
-
const newContent = beforePart + reindented + afterPart;
|
|
70
|
-
return {
|
|
71
|
-
type: "ok",
|
|
72
|
-
content: newContent,
|
|
73
|
-
replacement: reindented,
|
|
74
|
-
warnings,
|
|
75
|
-
range: { start: sym.startLine, end: sym.endLine },
|
|
76
|
-
};
|
|
77
|
-
}
|
package/src/runtime.ts
DELETED
package/src/search-output.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { formatAnchoredFileBlocks, type ReadSeekLine, type ReadSeekRange } from "./readseek-value.js";
|
|
2
|
-
|
|
3
|
-
export interface SearchOutputFile {
|
|
4
|
-
displayPath: string;
|
|
5
|
-
path: string;
|
|
6
|
-
ranges: ReadSeekRange[];
|
|
7
|
-
lines: ReadSeekLine[];
|
|
8
|
-
symbols?: Array<{ name: string; kind?: string }>;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface BuildSearchOutputInput {
|
|
12
|
-
pattern: string;
|
|
13
|
-
files: SearchOutputFile[];
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface SearchOutputResult {
|
|
17
|
-
text: string;
|
|
18
|
-
readSeekValue: {
|
|
19
|
-
tool: "search";
|
|
20
|
-
files: Array<{
|
|
21
|
-
path: string;
|
|
22
|
-
ranges: ReadSeekRange[];
|
|
23
|
-
lines: ReadSeekLine[];
|
|
24
|
-
}>;
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function buildSearchOutput(input: BuildSearchOutputInput): SearchOutputResult {
|
|
29
|
-
if (input.files.length === 0) {
|
|
30
|
-
return {
|
|
31
|
-
text: `No matches found for pattern: ${input.pattern}`,
|
|
32
|
-
readSeekValue: {
|
|
33
|
-
tool: "search",
|
|
34
|
-
files: [],
|
|
35
|
-
},
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return {
|
|
40
|
-
text: formatAnchoredFileBlocks(input.files),
|
|
41
|
-
readSeekValue: {
|
|
42
|
-
tool: "search",
|
|
43
|
-
files: input.files.map((file) => ({
|
|
44
|
-
path: file.path,
|
|
45
|
-
ranges: file.ranges.map((range) => ({ ...range })),
|
|
46
|
-
lines: file.lines.map((line) => ({ ...line })),
|
|
47
|
-
})),
|
|
48
|
-
},
|
|
49
|
-
};
|
|
50
|
-
}
|
package/src/search.ts
DELETED
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
|
|
3
|
-
import type { ExtensionAPI, ToolRenderResultOptions } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { Type } from "@sinclair/typebox";
|
|
5
|
-
|
|
6
|
-
import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
|
|
7
|
-
import { buildReadSeekLineWithHash, buildToolErrorResult, type ReadSeekLine } from "./readseek-value.js";
|
|
8
|
-
import { resolveToCwd } from "./path-utils.js";
|
|
9
|
-
import { statSearchPathOrError } from "./stat-search-path.js";
|
|
10
|
-
import { classifyReadSeekFailure, readSeekSearch, type ReadSeekSearchFileOutput } from "./readseek-client.js";
|
|
11
|
-
import { buildSearchOutput } from "./search-output.js";
|
|
12
|
-
import type { FileAnchoredCallback } from "./tool-types.js";
|
|
13
|
-
import { langParam, readSeekGitSearchParams, searchPathParam, validateIgnoredRequiresOthers } from "./readseek-params.js";
|
|
14
|
-
import { registerReadSeekTool } from "./register-tool.js";
|
|
15
|
-
|
|
16
|
-
import { renderAnchoredFilesResult, renderReadSeekSearchCall } from "./tui-render-utils.js";
|
|
17
|
-
|
|
18
|
-
type SearchParams = { pattern: string; lang?: string; path?: string; cached?: boolean; others?: boolean; ignored?: boolean };
|
|
19
|
-
|
|
20
|
-
export interface SearchRange {
|
|
21
|
-
startLine: number;
|
|
22
|
-
endLine: number;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface SearchEnclosingSymbol {
|
|
26
|
-
name: string;
|
|
27
|
-
kind: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function mergeRanges(ranges: SearchRange[]): SearchRange[] {
|
|
31
|
-
if (ranges.length === 0) return [];
|
|
32
|
-
if (ranges.length === 1) return [{ ...ranges[0] }];
|
|
33
|
-
|
|
34
|
-
const sorted = [...ranges].sort((a, b) => a.startLine - b.startLine);
|
|
35
|
-
const merged: SearchRange[] = [{ ...sorted[0] }];
|
|
36
|
-
|
|
37
|
-
for (let i = 1; i < sorted.length; i++) {
|
|
38
|
-
const current = sorted[i];
|
|
39
|
-
const last = merged[merged.length - 1];
|
|
40
|
-
if (current.startLine <= last.endLine + 2) {
|
|
41
|
-
last.endLine = Math.max(last.endLine, current.endLine);
|
|
42
|
-
} else {
|
|
43
|
-
merged.push({ ...current });
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return merged;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const SEARCH_PROMPT_METADATA = defineToolPromptMetadata({
|
|
51
|
-
promptUrl: new URL("../prompts/search.md", import.meta.url),
|
|
52
|
-
promptSnippet: "Search syntax-aware code shapes with AST patterns",
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
interface SearchToolOptions {
|
|
56
|
-
onFileAnchored?: FileAnchoredCallback;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Inputs for executing the structural search tool without registering it.
|
|
61
|
-
*/
|
|
62
|
-
export interface ExecuteSearchOptions {
|
|
63
|
-
params: unknown;
|
|
64
|
-
signal: AbortSignal | undefined;
|
|
65
|
-
cwd: string;
|
|
66
|
-
onFileAnchored?: FileAnchoredCallback;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function linesFromSearchResult(result: ReadSeekSearchFileOutput, ranges: SearchRange[]): ReadSeekLine[] {
|
|
70
|
-
const lineMap = new Map<number, ReadSeekLine>();
|
|
71
|
-
for (const match of result.matches) {
|
|
72
|
-
for (const line of match.hashlines) {
|
|
73
|
-
lineMap.set(line.line, buildReadSeekLineWithHash(line.line, line.hash, line.text));
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const lines: ReadSeekLine[] = [];
|
|
78
|
-
const seen = new Set<number>();
|
|
79
|
-
for (const range of ranges) {
|
|
80
|
-
for (let line = range.startLine; line <= range.endLine; line++) {
|
|
81
|
-
if (seen.has(line)) continue;
|
|
82
|
-
const readSeekLine = lineMap.get(line);
|
|
83
|
-
if (!readSeekLine) continue;
|
|
84
|
-
seen.add(line);
|
|
85
|
-
lines.push(readSeekLine);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return lines;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function readSeekLanguageForPath(language: string | undefined, searchPath: string, isFile: boolean): string | undefined {
|
|
92
|
-
if (language === "typescript" && isFile && path.extname(searchPath).toLowerCase() === ".tsx") return "tsx";
|
|
93
|
-
return language;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Executes structural search and returns readseek-anchored matches.
|
|
98
|
-
*/
|
|
99
|
-
export async function executeSearch(opts: ExecuteSearchOptions): Promise<any> {
|
|
100
|
-
const { params, signal, cwd, onFileAnchored } = opts;
|
|
101
|
-
const p = params as SearchParams;
|
|
102
|
-
const ignoredError = validateIgnoredRequiresOthers("search", p);
|
|
103
|
-
if (ignoredError) return ignoredError;
|
|
104
|
-
const searchPath = resolveToCwd(p.path ?? ".", cwd);
|
|
105
|
-
|
|
106
|
-
const statResult = await statSearchPathOrError("search", p.path, searchPath);
|
|
107
|
-
if (!statResult.ok) return statResult.error;
|
|
108
|
-
const searchPathIsFile = statResult.stats.isFile();
|
|
109
|
-
|
|
110
|
-
try {
|
|
111
|
-
const effectiveLang = readSeekLanguageForPath(p.lang, searchPath, searchPathIsFile);
|
|
112
|
-
const results = await readSeekSearch(searchPath, p.pattern, {
|
|
113
|
-
language: effectiveLang,
|
|
114
|
-
cached: p.cached,
|
|
115
|
-
others: p.others,
|
|
116
|
-
ignored: p.ignored,
|
|
117
|
-
signal,
|
|
118
|
-
});
|
|
119
|
-
if (results.length === 0) {
|
|
120
|
-
const emptyOutput = buildSearchOutput({ pattern: p.pattern, files: [] });
|
|
121
|
-
return {
|
|
122
|
-
content: [{ type: "text", text: emptyOutput.text }],
|
|
123
|
-
details: {
|
|
124
|
-
readSeekValue: emptyOutput.readSeekValue,
|
|
125
|
-
},
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const readSeekFiles: Array<{
|
|
130
|
-
displayPath: string;
|
|
131
|
-
path: string;
|
|
132
|
-
ranges: SearchRange[];
|
|
133
|
-
lines: ReadSeekLine[];
|
|
134
|
-
symbols?: SearchEnclosingSymbol[];
|
|
135
|
-
}> = [];
|
|
136
|
-
|
|
137
|
-
for (const result of results) {
|
|
138
|
-
const abs = path.isAbsolute(result.file) ? result.file : path.resolve(cwd, result.file);
|
|
139
|
-
const display = path.relative(cwd, abs) || abs;
|
|
140
|
-
const ranges = result.matches.map((match) => ({ startLine: match.start_line, endLine: match.end_line }));
|
|
141
|
-
const mergedRanges = mergeRanges(ranges);
|
|
142
|
-
const lines = linesFromSearchResult(result, mergedRanges);
|
|
143
|
-
if (lines.length === 0) continue;
|
|
144
|
-
readSeekFiles.push({
|
|
145
|
-
displayPath: display,
|
|
146
|
-
path: abs,
|
|
147
|
-
ranges: mergedRanges.map((range) => ({ ...range })),
|
|
148
|
-
lines,
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (readSeekFiles.length === 0) {
|
|
153
|
-
const emptyOutput = buildSearchOutput({ pattern: p.pattern, files: [] });
|
|
154
|
-
return {
|
|
155
|
-
content: [{ type: "text", text: emptyOutput.text }],
|
|
156
|
-
details: {
|
|
157
|
-
readSeekValue: emptyOutput.readSeekValue,
|
|
158
|
-
},
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const builtOutput = buildSearchOutput({
|
|
163
|
-
pattern: p.pattern,
|
|
164
|
-
files: readSeekFiles,
|
|
165
|
-
});
|
|
166
|
-
for (const readSeekFile of readSeekFiles) {
|
|
167
|
-
onFileAnchored?.(readSeekFile.path);
|
|
168
|
-
}
|
|
169
|
-
return {
|
|
170
|
-
content: [{ type: "text", text: builtOutput.text }],
|
|
171
|
-
details: {
|
|
172
|
-
readSeekValue: builtOutput.readSeekValue,
|
|
173
|
-
},
|
|
174
|
-
};
|
|
175
|
-
} catch (err: any) {
|
|
176
|
-
const failure = classifyReadSeekFailure(err);
|
|
177
|
-
return buildToolErrorResult("search", failure.code, failure.message, failure.hint ? { hint: failure.hint } : {});
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export function registerSearchTool(pi: ExtensionAPI, options: SearchToolOptions = {}) {
|
|
182
|
-
const tool = registerReadSeekTool(pi, {
|
|
183
|
-
name: "readSeek_search",
|
|
184
|
-
label: "Structural Search",
|
|
185
|
-
description: SEARCH_PROMPT_METADATA.description,
|
|
186
|
-
promptSnippet: SEARCH_PROMPT_METADATA.promptSnippet,
|
|
187
|
-
promptGuidelines: SEARCH_PROMPT_METADATA.promptGuidelines,
|
|
188
|
-
parameters: Type.Object({
|
|
189
|
-
pattern: Type.String({ description: "ast-grep pattern, such as console.log($$$ARGS)" }),
|
|
190
|
-
lang: langParam(),
|
|
191
|
-
path: searchPathParam(),
|
|
192
|
-
...readSeekGitSearchParams(),
|
|
193
|
-
}),
|
|
194
|
-
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
195
|
-
return executeSearch({ params, signal, cwd: ctx.cwd, onFileAnchored: options.onFileAnchored });
|
|
196
|
-
},
|
|
197
|
-
renderCall(args: any, theme: any, ...rest: any[]) {
|
|
198
|
-
return renderReadSeekSearchCall(args, theme, rest, {
|
|
199
|
-
label: "search",
|
|
200
|
-
accent: `/${args.pattern}/`,
|
|
201
|
-
flags: [args.cached && "cached", args.others && "others", args.ignored && "ignored"],
|
|
202
|
-
});
|
|
203
|
-
},
|
|
204
|
-
renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
|
|
205
|
-
return renderAnchoredFilesResult(result, options, theme, rest, {
|
|
206
|
-
pendingLabel: "pending search",
|
|
207
|
-
emptyLabel: "no matches",
|
|
208
|
-
unitSingular: "match",
|
|
209
|
-
unitPlural: "matches",
|
|
210
|
-
});
|
|
211
|
-
},
|
|
212
|
-
});
|
|
213
|
-
return tool;
|
|
214
|
-
}
|
package/src/session-anchors.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tracks files that have fresh hashline anchors in the current extension session.
|
|
3
|
-
*/
|
|
4
|
-
export class SessionAnchors {
|
|
5
|
-
readonly #paths = new Set<string>();
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Records that a file has produced anchors usable by later edit calls.
|
|
9
|
-
*/
|
|
10
|
-
markAnchored(absolutePath: string): void {
|
|
11
|
-
this.#paths.add(absolutePath);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Forgets anchors after a file mutation that did not return current anchors.
|
|
16
|
-
*/
|
|
17
|
-
forget(absolutePath: string): void {
|
|
18
|
-
this.#paths.delete(absolutePath);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Forgets every anchor, such as when resetting extension session state.
|
|
23
|
-
*/
|
|
24
|
-
clear(): void {
|
|
25
|
-
this.#paths.clear();
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Returns whether a file has fresh anchors in the current session.
|
|
30
|
-
*/
|
|
31
|
-
hasFreshAnchors(absolutePath: string): boolean {
|
|
32
|
-
return this.#paths.has(absolutePath);
|
|
33
|
-
}
|
|
34
|
-
}
|
package/src/stat-search-path.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { stat as fsStat } from "node:fs/promises";
|
|
2
|
-
import type { Stats } from "node:fs";
|
|
3
|
-
|
|
4
|
-
import { buildToolErrorResult, type ToolErrorResult } from "./readseek-value.js";
|
|
5
|
-
import { formatFsError } from "./fs-error.js";
|
|
6
|
-
|
|
7
|
-
export type StatSearchPathResult =
|
|
8
|
-
| { ok: true; stats: Stats }
|
|
9
|
-
| { ok: false; error: ToolErrorResult };
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Stat a tool's search path, mapping access failures into the shared readseek
|
|
13
|
-
* error taxonomy. Used by refs and search, which reject a missing or
|
|
14
|
-
* unreadable path with identical codes and messages.
|
|
15
|
-
*/
|
|
16
|
-
export async function statSearchPathOrError(
|
|
17
|
-
tool: string,
|
|
18
|
-
rawPath: string | undefined,
|
|
19
|
-
searchPath: string,
|
|
20
|
-
): Promise<StatSearchPathResult> {
|
|
21
|
-
try {
|
|
22
|
-
return { ok: true, stats: await fsStat(searchPath) };
|
|
23
|
-
} catch (err: any) {
|
|
24
|
-
const display = rawPath ?? ".";
|
|
25
|
-
const path = rawPath ?? searchPath;
|
|
26
|
-
const { code, message } = formatFsError(err, "stat-error");
|
|
27
|
-
return { ok: false, error: buildToolErrorResult(tool, code, message, { path }) };
|
|
28
|
-
}
|
|
29
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { resolveReadSeekSyntaxValidation } from "./readseek-settings.js";
|
|
2
|
-
|
|
3
|
-
export type SyntaxValidateMode = "warn" | "block" | "off";
|
|
4
|
-
|
|
5
|
-
export interface SyntaxValidateOptions {
|
|
6
|
-
syntaxValidate?: SyntaxValidateMode;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const DEFAULT: SyntaxValidateMode = "warn";
|
|
10
|
-
|
|
11
|
-
export function resolveSyntaxValidateMode(
|
|
12
|
-
opts: SyntaxValidateOptions,
|
|
13
|
-
): SyntaxValidateMode {
|
|
14
|
-
return opts.syntaxValidate ?? resolveReadSeekSyntaxValidation() ?? DEFAULT;
|
|
15
|
-
}
|