pi-readseek 0.3.20 → 0.3.21
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/index.ts +11 -11
- package/package.json +1 -1
- package/src/edit.ts +3 -2
- package/src/grep.ts +3 -2
- package/src/read.ts +3 -2
- package/src/refs.ts +79 -60
- package/src/session-anchors.ts +20 -0
- package/src/sg.ts +119 -100
- package/src/tool-types.ts +9 -0
- package/src/write.ts +2 -1
package/index.ts
CHANGED
|
@@ -5,22 +5,22 @@ import { registerGrepTool } from "./src/grep.js";
|
|
|
5
5
|
import { registerSgTool, isSgAvailable } from "./src/sg.js";
|
|
6
6
|
import { registerRefsTool } from "./src/refs.js";
|
|
7
7
|
import { registerWriteTool } from "./src/write.js";
|
|
8
|
+
import { SessionAnchors } from "./src/session-anchors.js";
|
|
9
|
+
|
|
8
10
|
export default function piReadseekExtension(pi: ExtensionAPI): void {
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
};
|
|
13
|
-
const wasReadInSession = (absolutePath: string) => readPaths.has(absolutePath);
|
|
11
|
+
const sessionAnchors = new SessionAnchors();
|
|
12
|
+
const markAnchored = (absolutePath: string) => sessionAnchors.markAnchored(absolutePath);
|
|
13
|
+
const hasFreshAnchors = (absolutePath: string) => sessionAnchors.hasFreshAnchors(absolutePath);
|
|
14
14
|
|
|
15
|
-
registerReadTool(pi, { onSuccessfulRead:
|
|
16
|
-
registerEditTool(pi, { wasReadInSession });
|
|
15
|
+
registerReadTool(pi, { onSuccessfulRead: markAnchored });
|
|
16
|
+
registerEditTool(pi, { wasReadInSession: hasFreshAnchors });
|
|
17
17
|
const sgAvailable = isSgAvailable();
|
|
18
18
|
const searchGuideline = sgAvailable
|
|
19
19
|
? "Use grep summary for counts; use search for structural code patterns."
|
|
20
20
|
: "Use grep summary for counts; install @jarkkojs/readseek to enable search.";
|
|
21
21
|
|
|
22
|
-
registerGrepTool(pi, { searchGuideline, onFileAnchored:
|
|
23
|
-
registerSgTool(pi, { onFileAnchored:
|
|
24
|
-
registerRefsTool(pi, { onFileAnchored:
|
|
25
|
-
registerWriteTool(pi, { onFileAnchored:
|
|
22
|
+
registerGrepTool(pi, { searchGuideline, onFileAnchored: markAnchored });
|
|
23
|
+
registerSgTool(pi, { onFileAnchored: markAnchored });
|
|
24
|
+
registerRefsTool(pi, { onFileAnchored: markAnchored });
|
|
25
|
+
registerWriteTool(pi, { onFileAnchored: markAnchored });
|
|
26
26
|
}
|
package/package.json
CHANGED
package/src/edit.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { buildEditPreviewKey, buildPendingEditPreviewData, resolvePendingDiffPre
|
|
|
22
22
|
import { buildDiffData, type DiffBlockRange } from "./diff-data.js";
|
|
23
23
|
import { clampLineToWidth, clampLinesToWidth, linkToolPath, resolveRenderResultContext, summaryLine } from "./tui-render-utils.js";
|
|
24
24
|
import { DiffPreviewComponent } from "./tui-diff-component.js";
|
|
25
|
+
import type { FreshAnchorsPredicate } from "./tool-types.js";
|
|
25
26
|
|
|
26
27
|
import { resolveEditDiffDisplay } from "./readseek-settings.js";
|
|
27
28
|
|
|
@@ -134,7 +135,7 @@ function mapEditFileError(err: any, filePath: string, displayPath: string, phase
|
|
|
134
135
|
}
|
|
135
136
|
|
|
136
137
|
export interface EditToolOptions {
|
|
137
|
-
wasReadInSession?:
|
|
138
|
+
wasReadInSession?: FreshAnchorsPredicate;
|
|
138
139
|
syntaxValidate?: SyntaxValidateOptions["syntaxValidate"];
|
|
139
140
|
}
|
|
140
141
|
|
|
@@ -142,7 +143,7 @@ export interface ExecuteEditOptions {
|
|
|
142
143
|
params: unknown;
|
|
143
144
|
signal: AbortSignal | undefined;
|
|
144
145
|
cwd: string;
|
|
145
|
-
wasReadInSession?:
|
|
146
|
+
wasReadInSession?: FreshAnchorsPredicate;
|
|
146
147
|
syntaxValidate?: SyntaxValidateOptions["syntaxValidate"];
|
|
147
148
|
}
|
|
148
149
|
|
package/src/grep.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { Text } from "@earendil-works/pi-tui";
|
|
|
18
18
|
import { formatGrepCallText, formatGrepResultText } from "./grep-render-helpers.js";
|
|
19
19
|
import { coerceObviousBase10Int } from "./coerce-obvious-int.js";
|
|
20
20
|
import { clampLineToWidth, clampLinesToWidth, linkToolPath, renderToolLabel, resolveRenderResultContext, summaryLine } from "./tui-render-utils.js";
|
|
21
|
+
import type { FileAnchoredCallback } from "./tool-types.js";
|
|
21
22
|
|
|
22
23
|
const GREP_PROMPT_METADATA = defineToolPromptMetadata({
|
|
23
24
|
promptUrl: new URL("../prompts/grep.md", import.meta.url),
|
|
@@ -180,7 +181,7 @@ function escapeForRegex(s: string): string {
|
|
|
180
181
|
|
|
181
182
|
interface GrepToolOptions {
|
|
182
183
|
searchGuideline?: string;
|
|
183
|
-
onFileAnchored?:
|
|
184
|
+
onFileAnchored?: FileAnchoredCallback;
|
|
184
185
|
}
|
|
185
186
|
|
|
186
187
|
export interface ExecuteGrepOptions {
|
|
@@ -189,7 +190,7 @@ export interface ExecuteGrepOptions {
|
|
|
189
190
|
signal: AbortSignal | undefined;
|
|
190
191
|
onUpdate: any;
|
|
191
192
|
cwd: string;
|
|
192
|
-
onFileAnchored?:
|
|
193
|
+
onFileAnchored?: FileAnchoredCallback;
|
|
193
194
|
}
|
|
194
195
|
|
|
195
196
|
export async function executeGrep(opts: ExecuteGrepOptions): Promise<any> {
|
package/src/read.ts
CHANGED
|
@@ -27,6 +27,7 @@ import { readseekRead } from "./readseek-client.js";
|
|
|
27
27
|
import { Text } from "@earendil-works/pi-tui";
|
|
28
28
|
import { formatReadCallText, formatReadResultText } from "./read-render-helpers.js";
|
|
29
29
|
import { clampLineToWidth, clampLinesToWidth, linkToolPath, renderToolLabel, resolveRenderResultContext, summaryLine, wrapReadHashlinesForWidth } from "./tui-render-utils.js";
|
|
30
|
+
import type { FileAnchoredCallback } from "./tool-types.js";
|
|
30
31
|
|
|
31
32
|
const READ_PROMPT_METADATA = defineToolPromptMetadata({
|
|
32
33
|
promptUrl: new URL("../prompts/read.md", import.meta.url),
|
|
@@ -43,7 +44,7 @@ interface ReadParams {
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
interface ReadToolOptions {
|
|
46
|
-
onSuccessfulRead?:
|
|
47
|
+
onSuccessfulRead?: FileAnchoredCallback;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
export interface ExecuteReadOptions {
|
|
@@ -52,7 +53,7 @@ export interface ExecuteReadOptions {
|
|
|
52
53
|
signal: AbortSignal | undefined;
|
|
53
54
|
onUpdate: any;
|
|
54
55
|
cwd: string;
|
|
55
|
-
onSuccessfulRead?:
|
|
56
|
+
onSuccessfulRead?: FileAnchoredCallback;
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
export async function executeRead(opts: ExecuteReadOptions): Promise<AgentToolResult<any>> {
|
package/src/refs.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { buildReadseekLineWithHash, buildToolErrorResult } from "./readseek-valu
|
|
|
8
8
|
import { resolveToCwd } from "./path-utils.js";
|
|
9
9
|
import { isReadseekAvailable, readseekRefs, type ReadseekReference } from "./readseek-client.js";
|
|
10
10
|
import { buildRefsOutput, type RefsOutputFile, type RefsOutputLine } from "./refs-output.js";
|
|
11
|
+
import type { FileAnchoredCallback } from "./tool-types.js";
|
|
11
12
|
|
|
12
13
|
import { clampLineToWidth, renderAnchoredFilesResult, renderToolLabel } from "./tui-render-utils.js";
|
|
13
14
|
|
|
@@ -33,7 +34,17 @@ export function isRefsAvailable(): boolean {
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
interface RefsToolOptions {
|
|
36
|
-
onFileAnchored?:
|
|
37
|
+
onFileAnchored?: FileAnchoredCallback;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Inputs for executing the references tool without registering it.
|
|
42
|
+
*/
|
|
43
|
+
export interface ExecuteRefsOptions {
|
|
44
|
+
params: unknown;
|
|
45
|
+
signal: AbortSignal | undefined;
|
|
46
|
+
cwd: string;
|
|
47
|
+
onFileAnchored?: FileAnchoredCallback;
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
function refsLine(reference: ReadseekReference): RefsOutputLine {
|
|
@@ -61,6 +72,72 @@ function groupReferences(references: ReadseekReference[], cwd: string): RefsOutp
|
|
|
61
72
|
return [...files.values()];
|
|
62
73
|
}
|
|
63
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Executes identifier reference lookup and returns readseek-anchored matches.
|
|
77
|
+
*/
|
|
78
|
+
export async function executeRefs(opts: ExecuteRefsOptions): Promise<any> {
|
|
79
|
+
const { params, signal, cwd, onFileAnchored } = opts;
|
|
80
|
+
const p = params as RefsParams;
|
|
81
|
+
if (p.ignored && !p.others) {
|
|
82
|
+
return buildToolErrorResult("refs", "invalid-parameter", "Error: refs parameter 'ignored' requires 'others'");
|
|
83
|
+
}
|
|
84
|
+
if (p.scope && p.line === undefined) {
|
|
85
|
+
return buildToolErrorResult("refs", "invalid-parameter", "Error: refs parameter 'scope' requires 'line'");
|
|
86
|
+
}
|
|
87
|
+
const searchPath = resolveToCwd(p.path ?? ".", cwd);
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
await fsStat(searchPath);
|
|
91
|
+
} catch (err: any) {
|
|
92
|
+
if (err?.code === "ENOENT") {
|
|
93
|
+
return buildToolErrorResult("refs", "path-not-found", `Error: path '${p.path ?? "."}' does not exist`, {
|
|
94
|
+
path: p.path ?? searchPath,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if (err?.code === "EACCES" || err?.code === "EPERM") {
|
|
98
|
+
return buildToolErrorResult("refs", "permission-denied", `Error: permission denied for path '${p.path ?? "."}'`, {
|
|
99
|
+
path: p.path ?? searchPath,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const message = `Error: could not access path '${p.path ?? "."}': ${err?.message ?? String(err)}`;
|
|
103
|
+
return buildToolErrorResult("refs", "fs-error", message, {
|
|
104
|
+
path: p.path ?? searchPath,
|
|
105
|
+
details: { fsCode: err?.code, fsMessage: err?.message },
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const references = await readseekRefs(searchPath, p.name, {
|
|
111
|
+
scope: p.scope,
|
|
112
|
+
line: p.line,
|
|
113
|
+
column: p.column,
|
|
114
|
+
language: p.lang,
|
|
115
|
+
cached: p.cached,
|
|
116
|
+
others: p.others,
|
|
117
|
+
ignored: p.ignored,
|
|
118
|
+
signal,
|
|
119
|
+
});
|
|
120
|
+
const files = groupReferences(references, cwd);
|
|
121
|
+
const builtOutput = buildRefsOutput({ name: p.name, files });
|
|
122
|
+
for (const file of files) {
|
|
123
|
+
onFileAnchored?.(file.path);
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
content: [{ type: "text", text: builtOutput.text }],
|
|
127
|
+
details: { readseekValue: builtOutput.readseekValue },
|
|
128
|
+
};
|
|
129
|
+
} catch (err: any) {
|
|
130
|
+
const message = String(err?.message || err);
|
|
131
|
+
const missingReadseek = err?.code === "ENOENT" || /Cannot find package|Cannot find module|no such file/i.test(message);
|
|
132
|
+
return buildToolErrorResult(
|
|
133
|
+
"refs",
|
|
134
|
+
missingReadseek ? "readseek-not-installed" : "readseek-execution-error",
|
|
135
|
+
message,
|
|
136
|
+
missingReadseek ? { hint: "Run npm install to install @jarkkojs/readseek." } : {},
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
64
141
|
export function registerRefsTool(pi: ExtensionAPI, options: RefsToolOptions = {}) {
|
|
65
142
|
const toolConfig = {
|
|
66
143
|
callable: true,
|
|
@@ -90,65 +167,7 @@ export function registerRefsTool(pi: ExtensionAPI, options: RefsToolOptions = {}
|
|
|
90
167
|
}),
|
|
91
168
|
ptc: toolConfig,
|
|
92
169
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
93
|
-
|
|
94
|
-
if (p.ignored && !p.others) {
|
|
95
|
-
return buildToolErrorResult("refs", "invalid-parameter", "Error: refs parameter 'ignored' requires 'others'");
|
|
96
|
-
}
|
|
97
|
-
if (p.scope && p.line === undefined) {
|
|
98
|
-
return buildToolErrorResult("refs", "invalid-parameter", "Error: refs parameter 'scope' requires 'line'");
|
|
99
|
-
}
|
|
100
|
-
const searchPath = resolveToCwd(p.path ?? ".", ctx.cwd);
|
|
101
|
-
|
|
102
|
-
try {
|
|
103
|
-
await fsStat(searchPath);
|
|
104
|
-
} catch (err: any) {
|
|
105
|
-
if (err?.code === "ENOENT") {
|
|
106
|
-
return buildToolErrorResult("refs", "path-not-found", `Error: path '${p.path ?? "."}' does not exist`, {
|
|
107
|
-
path: p.path ?? searchPath,
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
if (err?.code === "EACCES" || err?.code === "EPERM") {
|
|
111
|
-
return buildToolErrorResult("refs", "permission-denied", `Error: permission denied for path '${p.path ?? "."}'`, {
|
|
112
|
-
path: p.path ?? searchPath,
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
const message = `Error: could not access path '${p.path ?? "."}': ${err?.message ?? String(err)}`;
|
|
116
|
-
return buildToolErrorResult("refs", "fs-error", message, {
|
|
117
|
-
path: p.path ?? searchPath,
|
|
118
|
-
details: { fsCode: err?.code, fsMessage: err?.message },
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
try {
|
|
123
|
-
const references = await readseekRefs(searchPath, p.name, {
|
|
124
|
-
scope: p.scope,
|
|
125
|
-
line: p.line,
|
|
126
|
-
column: p.column,
|
|
127
|
-
language: p.lang,
|
|
128
|
-
cached: p.cached,
|
|
129
|
-
others: p.others,
|
|
130
|
-
ignored: p.ignored,
|
|
131
|
-
signal,
|
|
132
|
-
});
|
|
133
|
-
const files = groupReferences(references, ctx.cwd);
|
|
134
|
-
const builtOutput = buildRefsOutput({ name: p.name, files });
|
|
135
|
-
for (const file of files) {
|
|
136
|
-
options.onFileAnchored?.(file.path);
|
|
137
|
-
}
|
|
138
|
-
return {
|
|
139
|
-
content: [{ type: "text", text: builtOutput.text }],
|
|
140
|
-
details: { readseekValue: builtOutput.readseekValue },
|
|
141
|
-
};
|
|
142
|
-
} catch (err: any) {
|
|
143
|
-
const message = String(err?.message || err);
|
|
144
|
-
const missingReadseek = err?.code === "ENOENT" || /Cannot find package|Cannot find module|no such file/i.test(message);
|
|
145
|
-
return buildToolErrorResult(
|
|
146
|
-
"refs",
|
|
147
|
-
missingReadseek ? "readseek-not-installed" : "readseek-execution-error",
|
|
148
|
-
message,
|
|
149
|
-
missingReadseek ? { hint: "Run npm install to install @jarkkojs/readseek." } : {},
|
|
150
|
-
);
|
|
151
|
-
}
|
|
170
|
+
return executeRefs({ params, signal, cwd: ctx.cwd, onFileAnchored: options.onFileAnchored });
|
|
152
171
|
},
|
|
153
172
|
renderCall(args: any, theme: any, ...rest: any[]) {
|
|
154
173
|
const context = rest[0] ?? {};
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
* Returns whether a file has fresh anchors in the current session.
|
|
16
|
+
*/
|
|
17
|
+
hasFreshAnchors(absolutePath: string): boolean {
|
|
18
|
+
return this.#paths.has(absolutePath);
|
|
19
|
+
}
|
|
20
|
+
}
|
package/src/sg.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { buildReadseekLineWithHash, buildToolErrorResult, type ReadseekLine } fr
|
|
|
8
8
|
import { resolveToCwd } from "./path-utils.js";
|
|
9
9
|
import { isReadseekAvailable, readseekSearch, type ReadseekHashline, type ReadseekSearchFileOutput } from "./readseek-client.js";
|
|
10
10
|
import { buildSgOutput } from "./sg-output.js";
|
|
11
|
+
import type { FileAnchoredCallback } from "./tool-types.js";
|
|
11
12
|
|
|
12
13
|
import { clampLineToWidth, renderAnchoredFilesResult, renderToolLabel } from "./tui-render-utils.js";
|
|
13
14
|
|
|
@@ -53,7 +54,17 @@ export function isSgAvailable(): boolean {
|
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
interface SgToolOptions {
|
|
56
|
-
onFileAnchored?:
|
|
57
|
+
onFileAnchored?: FileAnchoredCallback;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Inputs for executing the structural search tool without registering it.
|
|
62
|
+
*/
|
|
63
|
+
export interface ExecuteSgOptions {
|
|
64
|
+
params: unknown;
|
|
65
|
+
signal: AbortSignal | undefined;
|
|
66
|
+
cwd: string;
|
|
67
|
+
onFileAnchored?: FileAnchoredCallback;
|
|
57
68
|
}
|
|
58
69
|
|
|
59
70
|
function readseekLineFromSearch(line: ReadseekHashline): ReadseekLine {
|
|
@@ -87,6 +98,112 @@ function readseekLanguageForPath(language: string | undefined, searchPath: strin
|
|
|
87
98
|
return language;
|
|
88
99
|
}
|
|
89
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Executes structural search and returns readseek-anchored matches.
|
|
103
|
+
*/
|
|
104
|
+
export async function executeSg(opts: ExecuteSgOptions): Promise<any> {
|
|
105
|
+
const { params, signal, cwd, onFileAnchored } = opts;
|
|
106
|
+
const p = params as SgParams;
|
|
107
|
+
if (p.ignored && !p.others) {
|
|
108
|
+
const message = "Error: search parameter 'ignored' requires 'others'";
|
|
109
|
+
return buildToolErrorResult("search", "invalid-parameter", message);
|
|
110
|
+
}
|
|
111
|
+
const searchPath = resolveToCwd(p.path ?? ".", cwd);
|
|
112
|
+
let searchPathIsFile = false;
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const stat = await fsStat(searchPath);
|
|
116
|
+
searchPathIsFile = stat.isFile();
|
|
117
|
+
} catch (err: any) {
|
|
118
|
+
if (err?.code === "ENOENT") {
|
|
119
|
+
const message = `Error: path '${p.path ?? "."}' does not exist`;
|
|
120
|
+
return buildToolErrorResult("search", "path-not-found", message, { path: p.path ?? searchPath });
|
|
121
|
+
}
|
|
122
|
+
if (err?.code === "EACCES" || err?.code === "EPERM") {
|
|
123
|
+
const message = `Error: permission denied for path '${p.path ?? "."}'`;
|
|
124
|
+
return buildToolErrorResult("search", "permission-denied", message, { path: p.path ?? searchPath });
|
|
125
|
+
}
|
|
126
|
+
const message = `Error: could not access path '${p.path ?? "."}': ${err?.message ?? String(err)}`;
|
|
127
|
+
return buildToolErrorResult("search", "fs-error", message, { path: p.path ?? searchPath, details: { fsCode: err?.code, fsMessage: err?.message } });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const effectiveLang = readseekLanguageForPath(p.lang, searchPath, searchPathIsFile);
|
|
132
|
+
const results = await readseekSearch(searchPath, p.pattern, {
|
|
133
|
+
language: effectiveLang,
|
|
134
|
+
cached: p.cached,
|
|
135
|
+
others: p.others,
|
|
136
|
+
ignored: p.ignored,
|
|
137
|
+
signal,
|
|
138
|
+
});
|
|
139
|
+
if (results.length === 0) {
|
|
140
|
+
const emptyOutput = buildSgOutput({ pattern: p.pattern, files: [] });
|
|
141
|
+
return {
|
|
142
|
+
content: [{ type: "text", text: emptyOutput.text }],
|
|
143
|
+
details: {
|
|
144
|
+
readseekValue: emptyOutput.readseekValue,
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const readseekFiles: Array<{
|
|
150
|
+
displayPath: string;
|
|
151
|
+
path: string;
|
|
152
|
+
ranges: SgRange[];
|
|
153
|
+
lines: ReadseekLine[];
|
|
154
|
+
symbols?: SgEnclosingSymbol[];
|
|
155
|
+
}> = [];
|
|
156
|
+
|
|
157
|
+
for (const result of results) {
|
|
158
|
+
const abs = path.isAbsolute(result.file) ? result.file : path.resolve(cwd, result.file);
|
|
159
|
+
const display = path.relative(cwd, abs) || abs;
|
|
160
|
+
const ranges = result.matches.map((match) => ({ startLine: match.start_line, endLine: match.end_line }));
|
|
161
|
+
const mergedRanges = mergeRanges(ranges);
|
|
162
|
+
const lines = linesFromSearchResult(result, mergedRanges);
|
|
163
|
+
if (lines.length === 0) continue;
|
|
164
|
+
readseekFiles.push({
|
|
165
|
+
displayPath: display,
|
|
166
|
+
path: abs,
|
|
167
|
+
ranges: mergedRanges.map((range) => ({ ...range })),
|
|
168
|
+
lines,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (readseekFiles.length === 0) {
|
|
173
|
+
const emptyOutput = buildSgOutput({ pattern: p.pattern, files: [] });
|
|
174
|
+
return {
|
|
175
|
+
content: [{ type: "text", text: emptyOutput.text }],
|
|
176
|
+
details: {
|
|
177
|
+
readseekValue: emptyOutput.readseekValue,
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const builtOutput = buildSgOutput({
|
|
183
|
+
pattern: p.pattern,
|
|
184
|
+
files: readseekFiles,
|
|
185
|
+
});
|
|
186
|
+
for (const readseekFile of readseekFiles) {
|
|
187
|
+
onFileAnchored?.(readseekFile.path);
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
content: [{ type: "text", text: builtOutput.text }],
|
|
191
|
+
details: {
|
|
192
|
+
readseekValue: builtOutput.readseekValue,
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
} catch (err: any) {
|
|
196
|
+
const message = String(err?.message || err);
|
|
197
|
+
const missingReadseek = err?.code === "ENOENT" || /Cannot find package|Cannot find module|no such file/i.test(message);
|
|
198
|
+
return buildToolErrorResult(
|
|
199
|
+
"search",
|
|
200
|
+
missingReadseek ? "readseek-not-installed" : "readseek-execution-error",
|
|
201
|
+
message,
|
|
202
|
+
missingReadseek ? { hint: "Run npm install to install @jarkkojs/readseek." } : {},
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
90
207
|
export function registerSgTool(pi: ExtensionAPI, options: SgToolOptions = {}) {
|
|
91
208
|
const toolConfig = {
|
|
92
209
|
callable: true,
|
|
@@ -113,105 +230,7 @@ export function registerSgTool(pi: ExtensionAPI, options: SgToolOptions = {}) {
|
|
|
113
230
|
}),
|
|
114
231
|
ptc: toolConfig,
|
|
115
232
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
116
|
-
|
|
117
|
-
if (p.ignored && !p.others) {
|
|
118
|
-
const message = "Error: search parameter 'ignored' requires 'others'";
|
|
119
|
-
return buildToolErrorResult("search", "invalid-parameter", message);
|
|
120
|
-
}
|
|
121
|
-
const searchPath = resolveToCwd(p.path ?? ".", ctx.cwd);
|
|
122
|
-
let searchPathIsFile = false;
|
|
123
|
-
|
|
124
|
-
try {
|
|
125
|
-
const stat = await fsStat(searchPath);
|
|
126
|
-
searchPathIsFile = stat.isFile();
|
|
127
|
-
} catch (err: any) {
|
|
128
|
-
if (err?.code === "ENOENT") {
|
|
129
|
-
const message = `Error: path '${p.path ?? "."}' does not exist`;
|
|
130
|
-
return buildToolErrorResult("search", "path-not-found", message, { path: p.path ?? searchPath });
|
|
131
|
-
}
|
|
132
|
-
if (err?.code === "EACCES" || err?.code === "EPERM") {
|
|
133
|
-
const message = `Error: permission denied for path '${p.path ?? "."}'`;
|
|
134
|
-
return buildToolErrorResult("search", "permission-denied", message, { path: p.path ?? searchPath });
|
|
135
|
-
}
|
|
136
|
-
const message = `Error: could not access path '${p.path ?? "."}': ${err?.message ?? String(err)}`;
|
|
137
|
-
return buildToolErrorResult("search", "fs-error", message, { path: p.path ?? searchPath, details: { fsCode: err?.code, fsMessage: err?.message } });
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
try {
|
|
141
|
-
const effectiveLang = readseekLanguageForPath(p.lang, searchPath, searchPathIsFile);
|
|
142
|
-
const results = await readseekSearch(searchPath, p.pattern, {
|
|
143
|
-
language: effectiveLang,
|
|
144
|
-
cached: p.cached,
|
|
145
|
-
others: p.others,
|
|
146
|
-
ignored: p.ignored,
|
|
147
|
-
signal,
|
|
148
|
-
});
|
|
149
|
-
if (results.length === 0) {
|
|
150
|
-
const emptyOutput = buildSgOutput({ pattern: p.pattern, files: [] });
|
|
151
|
-
return {
|
|
152
|
-
content: [{ type: "text", text: emptyOutput.text }],
|
|
153
|
-
details: {
|
|
154
|
-
readseekValue: emptyOutput.readseekValue,
|
|
155
|
-
},
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const readseekFiles: Array<{
|
|
160
|
-
displayPath: string;
|
|
161
|
-
path: string;
|
|
162
|
-
ranges: SgRange[];
|
|
163
|
-
lines: ReadseekLine[];
|
|
164
|
-
symbols?: SgEnclosingSymbol[];
|
|
165
|
-
}> = [];
|
|
166
|
-
|
|
167
|
-
for (const result of results) {
|
|
168
|
-
const abs = path.isAbsolute(result.file) ? result.file : path.resolve(ctx.cwd, result.file);
|
|
169
|
-
const display = path.relative(ctx.cwd, abs) || abs;
|
|
170
|
-
const ranges = result.matches.map((match) => ({ startLine: match.start_line, endLine: match.end_line }));
|
|
171
|
-
const mergedRanges = mergeRanges(ranges);
|
|
172
|
-
const lines = linesFromSearchResult(result, mergedRanges);
|
|
173
|
-
if (lines.length === 0) continue;
|
|
174
|
-
readseekFiles.push({
|
|
175
|
-
displayPath: display,
|
|
176
|
-
path: abs,
|
|
177
|
-
ranges: mergedRanges.map((range) => ({ ...range })),
|
|
178
|
-
lines,
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (readseekFiles.length === 0) {
|
|
183
|
-
const emptyOutput = buildSgOutput({ pattern: p.pattern, files: [] });
|
|
184
|
-
return {
|
|
185
|
-
content: [{ type: "text", text: emptyOutput.text }],
|
|
186
|
-
details: {
|
|
187
|
-
readseekValue: emptyOutput.readseekValue,
|
|
188
|
-
},
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const builtOutput = buildSgOutput({
|
|
193
|
-
pattern: p.pattern,
|
|
194
|
-
files: readseekFiles,
|
|
195
|
-
});
|
|
196
|
-
for (const readseekFile of readseekFiles) {
|
|
197
|
-
options.onFileAnchored?.(readseekFile.path);
|
|
198
|
-
}
|
|
199
|
-
return {
|
|
200
|
-
content: [{ type: "text", text: builtOutput.text }],
|
|
201
|
-
details: {
|
|
202
|
-
readseekValue: builtOutput.readseekValue,
|
|
203
|
-
},
|
|
204
|
-
};
|
|
205
|
-
} catch (err: any) {
|
|
206
|
-
const message = String(err?.message || err);
|
|
207
|
-
const missingReadseek = err?.code === "ENOENT" || /Cannot find package|Cannot find module|no such file/i.test(message);
|
|
208
|
-
return buildToolErrorResult(
|
|
209
|
-
"search",
|
|
210
|
-
missingReadseek ? "readseek-not-installed" : "readseek-execution-error",
|
|
211
|
-
message,
|
|
212
|
-
missingReadseek ? { hint: "Run npm install to install @jarkkojs/readseek." } : {},
|
|
213
|
-
);
|
|
214
|
-
}
|
|
233
|
+
return executeSg({ params, signal, cwd: ctx.cwd, onFileAnchored: options.onFileAnchored });
|
|
215
234
|
},
|
|
216
235
|
renderCall(args: any, theme: any, ...rest: any[]) {
|
|
217
236
|
const context = rest[0] ?? {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Callback used when a tool returns fresh anchors for a file path.
|
|
3
|
+
*/
|
|
4
|
+
export type FileAnchoredCallback = (absolutePath: string) => void;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Predicate used by mutating tools to check whether a file has session-fresh anchors.
|
|
8
|
+
*/
|
|
9
|
+
export type FreshAnchorsPredicate = (absolutePath: string) => boolean;
|
package/src/write.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { generateCompactOrFullDiff, normalizeToLF, hasBareCarriageReturn } from
|
|
|
16
16
|
import { buildDiffData, type DiffData } from "./diff-data.js";
|
|
17
17
|
import { clampLineToWidth, clampLinesToWidth, isRendererExpanded, linkToolPath, renderToolLabel, summaryLine } from "./tui-render-utils.js";
|
|
18
18
|
import { DiffPreviewComponent } from "./tui-diff-component.js";
|
|
19
|
+
import type { FileAnchoredCallback } from "./tool-types.js";
|
|
19
20
|
|
|
20
21
|
const WRITE_PENDING_PREVIEW_STATE_KEY = "hashline-write-pending-preview";
|
|
21
22
|
|
|
@@ -113,7 +114,7 @@ function generateWriteDiff(previousContent: string, nextContent: string): { diff
|
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
export interface WriteToolOptions {
|
|
116
|
-
onFileAnchored?:
|
|
117
|
+
onFileAnchored?: FileAnchoredCallback;
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
type MappedFsError = {
|