pi-readseek 0.3.8 → 0.3.10
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/package.json +2 -2
- package/src/edit-syntax-validate.ts +31 -16
- package/src/edit.ts +3 -13
- package/src/grep.ts +8 -72
- package/src/image-detect.ts +47 -0
- package/src/map-cache.ts +14 -87
- package/src/read.ts +20 -266
- package/src/readseek/mapper.ts +15 -39
- package/src/readseek-settings.ts +0 -13
- package/src/readseek-value.ts +36 -0
- package/src/sg.ts +13 -74
- package/src/tool-prompt-metadata.ts +1 -2
- package/src/tui-render-utils.ts +27 -0
- package/src/write.ts +0 -5
- package/src/binary-resolution.ts +0 -77
- package/src/find-parsers.ts +0 -89
- package/src/find-stat.ts +0 -36
- package/src/persistent-map-cache.ts +0 -230
- package/src/readseek/language-detect.ts +0 -29
package/src/read.ts
CHANGED
|
@@ -11,8 +11,9 @@ import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
|
|
|
11
11
|
import { readFile as fsReadFile } from "fs/promises";
|
|
12
12
|
import { normalizeToLF, stripBom, hasBareCarriageReturn } from "./edit-diff.js";
|
|
13
13
|
import { ensureHashInit, escapeControlCharsForDisplay } from "./hashline.js";
|
|
14
|
-
import {
|
|
14
|
+
import { buildReadseekWarning, buildToolErrorResult, renderReadseekLines, type ReadseekLine, type ReadseekWarning } from "./readseek-value.js";
|
|
15
15
|
import { looksLikeBinary } from "./binary-detect.js";
|
|
16
|
+
import { isSupportedImageBuffer } from "./image-detect.js";
|
|
16
17
|
import { resolveToCwd } from "./path-utils.js";
|
|
17
18
|
import { throwIfAborted } from "./runtime.js";
|
|
18
19
|
import { getOrGenerateMap } from "./map-cache.js";
|
|
@@ -26,17 +27,11 @@ import { coerceObviousBase10Int } from "./coerce-obvious-int.js";
|
|
|
26
27
|
import { readseekRead } from "./readseek-client.js";
|
|
27
28
|
import { Text } from "@earendil-works/pi-tui";
|
|
28
29
|
import { formatReadCallText, formatReadResultText } from "./read-render-helpers.js";
|
|
29
|
-
import { clampLineToWidth, clampLinesToWidth,
|
|
30
|
+
import { clampLineToWidth, clampLinesToWidth, linkToolPath, renderToolLabel, resolveRenderResultContext, summaryLine, wrapReadHashlinesForWidth } from "./tui-render-utils.js";
|
|
30
31
|
|
|
31
32
|
const READ_PROMPT_METADATA = defineToolPromptMetadata({
|
|
32
33
|
promptUrl: new URL("../prompts/read.md", import.meta.url),
|
|
33
34
|
promptSnippet: "Read text files or images; text reads include hashline anchors and optional maps/symbol lookup",
|
|
34
|
-
promptGuidelines: [
|
|
35
|
-
"Use read instead of bash cat/head/tail/sed for file inspection.",
|
|
36
|
-
"Use read for images/screenshots; supported images return attachments like stock pi read.",
|
|
37
|
-
"Use read offset/limit, symbol, or map to keep large files focused.",
|
|
38
|
-
"Use read anchors as fresh inputs for edit.",
|
|
39
|
-
],
|
|
40
35
|
});
|
|
41
36
|
|
|
42
37
|
interface ReadParams {
|
|
@@ -58,55 +53,6 @@ function splitReadseekLines(text: string): string[] {
|
|
|
58
53
|
return withoutTrailingTerminator.split("\n");
|
|
59
54
|
}
|
|
60
55
|
|
|
61
|
-
const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
|
|
62
|
-
|
|
63
|
-
function startsWithBytes(buffer: Buffer, bytes: number[]): boolean {
|
|
64
|
-
return buffer.length >= bytes.length && bytes.every((byte, index) => buffer[index] === byte);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function startsWithAscii(buffer: Buffer, offset: number, text: string): boolean {
|
|
68
|
-
if (buffer.length < offset + text.length) return false;
|
|
69
|
-
for (let index = 0; index < text.length; index++) {
|
|
70
|
-
if (buffer[offset + index] !== text.charCodeAt(index)) return false;
|
|
71
|
-
}
|
|
72
|
-
return true;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function readUint32BE(buffer: Buffer, offset: number): number {
|
|
76
|
-
return (
|
|
77
|
-
((buffer[offset] ?? 0) * 0x1000000) +
|
|
78
|
-
((buffer[offset + 1] ?? 0) << 16) +
|
|
79
|
-
((buffer[offset + 2] ?? 0) << 8) +
|
|
80
|
-
(buffer[offset + 3] ?? 0)
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function isPng(buffer: Buffer): boolean {
|
|
85
|
-
return buffer.length >= 16 && readUint32BE(buffer, PNG_SIGNATURE.length) === 13 && startsWithAscii(buffer, 12, "IHDR");
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function isAnimatedPng(buffer: Buffer): boolean {
|
|
89
|
-
let offset = PNG_SIGNATURE.length;
|
|
90
|
-
while (offset + 8 <= buffer.length) {
|
|
91
|
-
const chunkLength = readUint32BE(buffer, offset);
|
|
92
|
-
const chunkTypeOffset = offset + 4;
|
|
93
|
-
if (startsWithAscii(buffer, chunkTypeOffset, "acTL")) return true;
|
|
94
|
-
if (startsWithAscii(buffer, chunkTypeOffset, "IDAT")) return false;
|
|
95
|
-
const nextOffset = offset + 8 + chunkLength + 4;
|
|
96
|
-
if (nextOffset <= offset || nextOffset > buffer.length) return false;
|
|
97
|
-
offset = nextOffset;
|
|
98
|
-
}
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function isSupportedImageBuffer(buffer: Buffer): boolean {
|
|
103
|
-
if (startsWithBytes(buffer, [0xff, 0xd8, 0xff])) return buffer[3] !== 0xf7;
|
|
104
|
-
if (startsWithBytes(buffer, PNG_SIGNATURE)) return isPng(buffer) && !isAnimatedPng(buffer);
|
|
105
|
-
if (startsWithAscii(buffer, 0, "GIF")) return true;
|
|
106
|
-
return startsWithAscii(buffer, 0, "RIFF") && startsWithAscii(buffer, 8, "WEBP");
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
56
|
export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}) {
|
|
111
57
|
const toolConfig = {
|
|
112
58
|
callable: true,
|
|
@@ -151,63 +97,19 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
|
|
|
151
97
|
const rawParams = params as ReadParams;
|
|
152
98
|
const offset = coerceObviousBase10Int(rawParams.offset, "offset");
|
|
153
99
|
if (!offset.ok) {
|
|
154
|
-
return {
|
|
155
|
-
content: [{ type: "text", text: offset.message }],
|
|
156
|
-
isError: true,
|
|
157
|
-
details: {
|
|
158
|
-
readseekValue: {
|
|
159
|
-
tool: "read",
|
|
160
|
-
ok: false,
|
|
161
|
-
path: rawParams.path,
|
|
162
|
-
error: buildReadseekError("invalid-offset", offset.message),
|
|
163
|
-
},
|
|
164
|
-
},
|
|
165
|
-
};
|
|
100
|
+
return buildToolErrorResult("read", "invalid-offset", offset.message, { path: rawParams.path });
|
|
166
101
|
}
|
|
167
102
|
const limit = coerceObviousBase10Int(rawParams.limit, "limit");
|
|
168
103
|
if (!limit.ok) {
|
|
169
|
-
return {
|
|
170
|
-
content: [{ type: "text", text: limit.message }],
|
|
171
|
-
isError: true,
|
|
172
|
-
details: {
|
|
173
|
-
readseekValue: {
|
|
174
|
-
tool: "read",
|
|
175
|
-
ok: false,
|
|
176
|
-
path: rawParams.path,
|
|
177
|
-
error: buildReadseekError("invalid-limit", limit.message),
|
|
178
|
-
},
|
|
179
|
-
},
|
|
180
|
-
};
|
|
104
|
+
return buildToolErrorResult("read", "invalid-limit", limit.message, { path: rawParams.path });
|
|
181
105
|
}
|
|
182
106
|
if (limit.value !== undefined && limit.value < 1) {
|
|
183
107
|
const message = `Invalid limit: expected a positive integer, received ${limit.value}.`;
|
|
184
|
-
return {
|
|
185
|
-
content: [{ type: "text", text: message }],
|
|
186
|
-
isError: true,
|
|
187
|
-
details: {
|
|
188
|
-
readseekValue: {
|
|
189
|
-
tool: "read",
|
|
190
|
-
ok: false,
|
|
191
|
-
path: rawParams.path,
|
|
192
|
-
error: buildReadseekError("invalid-limit", message),
|
|
193
|
-
},
|
|
194
|
-
},
|
|
195
|
-
};
|
|
108
|
+
return buildToolErrorResult("read", "invalid-limit", message, { path: rawParams.path });
|
|
196
109
|
}
|
|
197
110
|
if (offset.value !== undefined && offset.value < 1) {
|
|
198
111
|
const message = `Invalid offset: expected a positive integer, received ${offset.value}.`;
|
|
199
|
-
return {
|
|
200
|
-
content: [{ type: "text", text: message }],
|
|
201
|
-
isError: true,
|
|
202
|
-
details: {
|
|
203
|
-
readseekValue: {
|
|
204
|
-
tool: "read",
|
|
205
|
-
ok: false,
|
|
206
|
-
path: rawParams.path,
|
|
207
|
-
error: buildReadseekError("invalid-offset", message),
|
|
208
|
-
},
|
|
209
|
-
},
|
|
210
|
-
};
|
|
112
|
+
return buildToolErrorResult("read", "invalid-offset", message, { path: rawParams.path });
|
|
211
113
|
}
|
|
212
114
|
const p = {
|
|
213
115
|
...rawParams,
|
|
@@ -218,18 +120,7 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
|
|
|
218
120
|
const trimmedSymbol = typeof rawParams.symbol === "string" ? rawParams.symbol.trim() : "";
|
|
219
121
|
if (trimmedSymbol.length === 0) {
|
|
220
122
|
const message = "Invalid symbol: expected a non-empty string.";
|
|
221
|
-
return {
|
|
222
|
-
content: [{ type: "text", text: message }],
|
|
223
|
-
isError: true,
|
|
224
|
-
details: {
|
|
225
|
-
readseekValue: {
|
|
226
|
-
tool: "read",
|
|
227
|
-
ok: false,
|
|
228
|
-
path: rawParams.path,
|
|
229
|
-
error: buildReadseekError("invalid-params-combo", message),
|
|
230
|
-
},
|
|
231
|
-
},
|
|
232
|
-
};
|
|
123
|
+
return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
|
|
233
124
|
}
|
|
234
125
|
p.symbol = trimmedSymbol;
|
|
235
126
|
}
|
|
@@ -246,63 +137,19 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
|
|
|
246
137
|
throwIfAborted(signal);
|
|
247
138
|
if (p.symbol && (p.offset !== undefined || p.limit !== undefined)) {
|
|
248
139
|
const message = "Cannot combine symbol with offset/limit. Use one or the other.";
|
|
249
|
-
return {
|
|
250
|
-
content: [{ type: "text", text: message }],
|
|
251
|
-
isError: true,
|
|
252
|
-
details: {
|
|
253
|
-
readseekValue: {
|
|
254
|
-
tool: "read",
|
|
255
|
-
ok: false,
|
|
256
|
-
path: rawParams.path,
|
|
257
|
-
error: buildReadseekError("invalid-params-combo", message),
|
|
258
|
-
},
|
|
259
|
-
},
|
|
260
|
-
};
|
|
140
|
+
return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
|
|
261
141
|
}
|
|
262
142
|
if (p.bundle && !p.symbol) {
|
|
263
143
|
const message = 'Cannot use bundle without symbol. Use read({ path, symbol, bundle: "local" }).';
|
|
264
|
-
return {
|
|
265
|
-
content: [{ type: "text", text: message }],
|
|
266
|
-
isError: true,
|
|
267
|
-
details: {
|
|
268
|
-
readseekValue: {
|
|
269
|
-
tool: "read",
|
|
270
|
-
ok: false,
|
|
271
|
-
path: rawParams.path,
|
|
272
|
-
error: buildReadseekError("invalid-params-combo", message),
|
|
273
|
-
},
|
|
274
|
-
},
|
|
275
|
-
};
|
|
144
|
+
return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
|
|
276
145
|
}
|
|
277
146
|
if (p.bundle && p.map) {
|
|
278
147
|
const message = "Cannot combine bundle with map. Use one or the other.";
|
|
279
|
-
return {
|
|
280
|
-
content: [{ type: "text", text: message }],
|
|
281
|
-
isError: true,
|
|
282
|
-
details: {
|
|
283
|
-
readseekValue: {
|
|
284
|
-
tool: "read",
|
|
285
|
-
ok: false,
|
|
286
|
-
path: rawParams.path,
|
|
287
|
-
error: buildReadseekError("invalid-params-combo", message),
|
|
288
|
-
},
|
|
289
|
-
},
|
|
290
|
-
};
|
|
148
|
+
return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
|
|
291
149
|
}
|
|
292
150
|
if (p.map && p.symbol) {
|
|
293
151
|
const message = "Cannot combine map with symbol. Use one or the other.";
|
|
294
|
-
return {
|
|
295
|
-
content: [{ type: "text", text: message }],
|
|
296
|
-
isError: true,
|
|
297
|
-
details: {
|
|
298
|
-
readseekValue: {
|
|
299
|
-
tool: "read",
|
|
300
|
-
ok: false,
|
|
301
|
-
path: rawParams.path,
|
|
302
|
-
error: buildReadseekError("invalid-params-combo", message),
|
|
303
|
-
},
|
|
304
|
-
},
|
|
305
|
-
};
|
|
152
|
+
return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
|
|
306
153
|
}
|
|
307
154
|
// Delegate images to the built-in read tool
|
|
308
155
|
throwIfAborted(signal);
|
|
@@ -320,69 +167,18 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
|
|
|
320
167
|
const code = err?.code;
|
|
321
168
|
if (code === "EISDIR") {
|
|
322
169
|
const message = `Path is a directory: ${rawPath}. Use ls to inspect directories.`;
|
|
323
|
-
return {
|
|
324
|
-
content: [{ type: "text", text: message }],
|
|
325
|
-
isError: true,
|
|
326
|
-
details: {
|
|
327
|
-
readseekValue: {
|
|
328
|
-
tool: "read",
|
|
329
|
-
ok: false,
|
|
330
|
-
path: rawParams.path,
|
|
331
|
-
error: buildReadseekError(
|
|
332
|
-
"path-is-directory",
|
|
333
|
-
message,
|
|
334
|
-
`Use ls(${JSON.stringify(rawPath)}) to inspect directories.`,
|
|
335
|
-
),
|
|
336
|
-
},
|
|
337
|
-
},
|
|
338
|
-
};
|
|
170
|
+
return buildToolErrorResult("read", "path-is-directory", message, { path: rawParams.path, hint: `Use ls(${JSON.stringify(rawPath)}) to inspect directories.` });
|
|
339
171
|
}
|
|
340
172
|
if (code === "EACCES" || code === "EPERM") {
|
|
341
173
|
const message = `Permission denied — cannot access: ${rawPath}`;
|
|
342
|
-
return {
|
|
343
|
-
content: [{ type: "text", text: message }],
|
|
344
|
-
isError: true,
|
|
345
|
-
details: {
|
|
346
|
-
readseekValue: {
|
|
347
|
-
tool: "read",
|
|
348
|
-
ok: false,
|
|
349
|
-
path: rawParams.path,
|
|
350
|
-
error: buildReadseekError("permission-denied", message),
|
|
351
|
-
},
|
|
352
|
-
},
|
|
353
|
-
};
|
|
174
|
+
return buildToolErrorResult("read", "permission-denied", message, { path: rawParams.path });
|
|
354
175
|
}
|
|
355
176
|
if (code === "ENOENT") {
|
|
356
177
|
const message = `File not found: ${rawPath}`;
|
|
357
|
-
return {
|
|
358
|
-
content: [{ type: "text", text: message }],
|
|
359
|
-
isError: true,
|
|
360
|
-
details: {
|
|
361
|
-
readseekValue: {
|
|
362
|
-
tool: "read",
|
|
363
|
-
ok: false,
|
|
364
|
-
path: rawParams.path,
|
|
365
|
-
error: buildReadseekError("file-not-found", message),
|
|
366
|
-
},
|
|
367
|
-
},
|
|
368
|
-
};
|
|
178
|
+
return buildToolErrorResult("read", "file-not-found", message, { path: rawParams.path });
|
|
369
179
|
}
|
|
370
180
|
const message = `File not readable: ${rawPath}${err?.message ? ` — ${err.message}` : ""}`;
|
|
371
|
-
return {
|
|
372
|
-
content: [{ type: "text", text: message }],
|
|
373
|
-
isError: true,
|
|
374
|
-
details: {
|
|
375
|
-
readseekValue: {
|
|
376
|
-
tool: "read",
|
|
377
|
-
ok: false,
|
|
378
|
-
path: rawParams.path,
|
|
379
|
-
error: buildReadseekError("fs-error", message, undefined, {
|
|
380
|
-
fsCode: code,
|
|
381
|
-
fsMessage: err?.message,
|
|
382
|
-
}),
|
|
383
|
-
},
|
|
384
|
-
},
|
|
385
|
-
};
|
|
181
|
+
return buildToolErrorResult("read", "fs-error", message, { path: rawParams.path, details: { fsCode: code, fsMessage: err?.message } });
|
|
386
182
|
}
|
|
387
183
|
|
|
388
184
|
if (isSupportedImageBuffer(rawBuffer)) {
|
|
@@ -399,18 +195,7 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
|
|
|
399
195
|
let endIdx = p.limit !== undefined ? Math.min(startLine - 1 + p.limit, total) : total;
|
|
400
196
|
if (p.offset !== undefined && startLine > total) {
|
|
401
197
|
const message = `[offset ${p.offset} is past end of file (${total} lines)]`;
|
|
402
|
-
return {
|
|
403
|
-
content: [{ type: "text", text: message }],
|
|
404
|
-
isError: true,
|
|
405
|
-
details: {
|
|
406
|
-
readseekValue: {
|
|
407
|
-
tool: "read",
|
|
408
|
-
ok: false,
|
|
409
|
-
path: rawParams.path,
|
|
410
|
-
error: buildReadseekError("offset-past-end", message),
|
|
411
|
-
},
|
|
412
|
-
},
|
|
413
|
-
};
|
|
198
|
+
return buildToolErrorResult("read", "offset-past-end", message, { path: rawParams.path });
|
|
414
199
|
}
|
|
415
200
|
let symbolMatch: SymbolMatch | undefined;
|
|
416
201
|
let symbolFileMap: Awaited<ReturnType<typeof getOrGenerateMap>> | null = null;
|
|
@@ -550,23 +335,7 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
|
|
|
550
335
|
} catch (err: any) {
|
|
551
336
|
const detail = err?.message ? ` — ${err.message}` : "";
|
|
552
337
|
const message = `readseek failed while reading ${rawPath}${detail}`;
|
|
553
|
-
return {
|
|
554
|
-
content: [{ type: "text", text: message }],
|
|
555
|
-
isError: true,
|
|
556
|
-
details: {
|
|
557
|
-
readseekValue: {
|
|
558
|
-
tool: "read",
|
|
559
|
-
ok: false,
|
|
560
|
-
path: rawParams.path,
|
|
561
|
-
error: buildReadseekError(
|
|
562
|
-
"readseek-error",
|
|
563
|
-
message,
|
|
564
|
-
"Ensure @jarkkojs/readseek and its npm platform package are installed.",
|
|
565
|
-
{ message: err?.message },
|
|
566
|
-
),
|
|
567
|
-
},
|
|
568
|
-
},
|
|
569
|
-
};
|
|
338
|
+
return buildToolErrorResult("read", "readseek-error", message, { path: rawParams.path, hint: "Ensure @jarkkojs/readseek and its npm platform package are installed.", details: { message: err?.message } });
|
|
570
339
|
}
|
|
571
340
|
const expectedLineCount = Math.max(0, endIdx - startLine + 1);
|
|
572
341
|
const invalidLine = readseekOutput.hashlines.find((line, index) => line.line !== startLine + index);
|
|
@@ -574,18 +343,7 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
|
|
|
574
343
|
const message = invalidLine
|
|
575
344
|
? `readseek returned non-sequential line ${invalidLine.line} for requested range ${startLine}-${endIdx}`
|
|
576
345
|
: `readseek returned ${readseekOutput.hashlines.length} lines for requested range ${startLine}-${endIdx} (${expectedLineCount} expected)`;
|
|
577
|
-
return {
|
|
578
|
-
content: [{ type: "text", text: message }],
|
|
579
|
-
isError: true,
|
|
580
|
-
details: {
|
|
581
|
-
readseekValue: {
|
|
582
|
-
tool: "read",
|
|
583
|
-
ok: false,
|
|
584
|
-
path: rawParams.path,
|
|
585
|
-
error: buildReadseekError("readseek-output-mismatch", message),
|
|
586
|
-
},
|
|
587
|
-
},
|
|
588
|
-
};
|
|
346
|
+
return buildToolErrorResult("read", "readseek-output-mismatch", message, { path: rawParams.path });
|
|
589
347
|
}
|
|
590
348
|
const readseekLines: ReadseekLine[] = readseekOutput.hashlines.map((line) => ({
|
|
591
349
|
line: line.line,
|
|
@@ -708,11 +466,7 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
|
|
|
708
466
|
return new Text(clampLineToWidth(text, context.width), 0, 0);
|
|
709
467
|
},
|
|
710
468
|
renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
|
|
711
|
-
const
|
|
712
|
-
const isPartial = context.isPartial ?? (options as any)?.isPartial ?? false;
|
|
713
|
-
const isError = context.isError ?? false;
|
|
714
|
-
const expanded = isRendererExpanded(options as any, context as any);
|
|
715
|
-
const width = context.width ?? (options as any)?.width;
|
|
469
|
+
const { isPartial, isError, expanded, width } = resolveRenderResultContext(options, rest);
|
|
716
470
|
if (isPartial) return new Text(clampLinesToWidth([summaryLine("pending read")], width).join("\n"), 0, 0);
|
|
717
471
|
|
|
718
472
|
const content = result.content?.[0];
|
package/src/readseek/mapper.ts
CHANGED
|
@@ -4,49 +4,25 @@ import { readseekMap, readseekMapContent } from "../readseek-client.js";
|
|
|
4
4
|
import { throwIfAborted } from "../runtime.js";
|
|
5
5
|
import type { FileMap, MapOptions } from "./types.js";
|
|
6
6
|
|
|
7
|
-
export const READSEEK_MAPPER_NAME = "readseek";
|
|
8
|
-
export const READSEEK_MAPPER_VERSION = 2;
|
|
9
|
-
|
|
10
|
-
export interface MapperIdentity {
|
|
11
|
-
mapperName: string;
|
|
12
|
-
mapperVersion: number;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface MapResultWithIdentity extends MapperIdentity {
|
|
16
|
-
map: FileMap | null;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export const READSEEK_MAPPER_IDENTITY: MapperIdentity = {
|
|
20
|
-
mapperName: READSEEK_MAPPER_NAME,
|
|
21
|
-
mapperVersion: READSEEK_MAPPER_VERSION,
|
|
22
|
-
};
|
|
23
|
-
export async function generateMapWithIdentity(
|
|
24
|
-
filePath: string,
|
|
25
|
-
options: MapOptions = {},
|
|
26
|
-
): Promise<MapResultWithIdentity> {
|
|
27
|
-
throwIfAborted(options.signal);
|
|
28
|
-
const fileStat = await stat(filePath);
|
|
29
|
-
throwIfAborted(options.signal);
|
|
30
|
-
const map = await readseekMap(filePath, fileStat.size, { signal: options.signal });
|
|
31
|
-
throwIfAborted(options.signal);
|
|
32
|
-
return { map, ...READSEEK_MAPPER_IDENTITY };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
7
|
export async function generateMap(
|
|
36
|
-
|
|
37
|
-
|
|
8
|
+
filePath: string,
|
|
9
|
+
options: MapOptions = {},
|
|
38
10
|
): Promise<FileMap | null> {
|
|
39
|
-
|
|
11
|
+
throwIfAborted(options.signal);
|
|
12
|
+
const fileStat = await stat(filePath);
|
|
13
|
+
throwIfAborted(options.signal);
|
|
14
|
+
const map = await readseekMap(filePath, fileStat.size, { signal: options.signal });
|
|
15
|
+
throwIfAborted(options.signal);
|
|
16
|
+
return map;
|
|
40
17
|
}
|
|
41
18
|
|
|
42
19
|
export async function generateMapFromContent(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
20
|
+
filePath: string,
|
|
21
|
+
content: string,
|
|
22
|
+
options: MapOptions = {},
|
|
46
23
|
): Promise<FileMap | null> {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
24
|
+
throwIfAborted(options.signal);
|
|
25
|
+
const map = await readseekMapContent(filePath, content, { signal: options.signal });
|
|
26
|
+
throwIfAborted(options.signal);
|
|
27
|
+
return map;
|
|
51
28
|
}
|
|
52
|
-
|
package/src/readseek-settings.ts
CHANGED
|
@@ -4,7 +4,6 @@ import { join } from "node:path";
|
|
|
4
4
|
|
|
5
5
|
export interface ReadseekJsonSettings {
|
|
6
6
|
grep?: { maxLines?: number; maxBytes?: number };
|
|
7
|
-
mapCache?: { dir?: string; enabled?: boolean };
|
|
8
7
|
edit?: { diffDisplay?: "collapsed" | "expanded" };
|
|
9
8
|
}
|
|
10
9
|
|
|
@@ -77,16 +76,6 @@ function validateSettings(raw: unknown, source: string): ReadseekSettingsResult
|
|
|
77
76
|
if (Object.keys(grep).length > 0) settings.grep = grep;
|
|
78
77
|
}
|
|
79
78
|
|
|
80
|
-
if (isRecord(raw.mapCache)) {
|
|
81
|
-
const mapCache: NonNullable<ReadseekJsonSettings["mapCache"]> = {};
|
|
82
|
-
if ("dir" in raw.mapCache) {
|
|
83
|
-
if (typeof raw.mapCache.dir === "string" && raw.mapCache.dir.length > 0) mapCache.dir = raw.mapCache.dir;
|
|
84
|
-
else warnings.push(invalid(source, "mapCache.dir"));
|
|
85
|
-
}
|
|
86
|
-
const enabled = readBoolean(raw.mapCache, "enabled", "mapCache.enabled", source, warnings);
|
|
87
|
-
if (enabled !== undefined) mapCache.enabled = enabled;
|
|
88
|
-
if (Object.keys(mapCache).length > 0) settings.mapCache = mapCache;
|
|
89
|
-
}
|
|
90
79
|
|
|
91
80
|
if (isRecord(raw.edit)) {
|
|
92
81
|
const edit: NonNullable<ReadseekJsonSettings["edit"]> = {};
|
|
@@ -117,8 +106,6 @@ function mergeSettings(base: ReadseekJsonSettings, override: ReadseekJsonSetting
|
|
|
117
106
|
const merged: ReadseekJsonSettings = {};
|
|
118
107
|
const grep = { ...(base.grep ?? {}), ...(override.grep ?? {}) };
|
|
119
108
|
if (Object.keys(grep).length > 0) merged.grep = grep;
|
|
120
|
-
const mapCache = { ...(base.mapCache ?? {}), ...(override.mapCache ?? {}) };
|
|
121
|
-
if (Object.keys(mapCache).length > 0) merged.mapCache = mapCache;
|
|
122
109
|
const edit = { ...(base.edit ?? {}), ...(override.edit ?? {}) };
|
|
123
110
|
if (Object.keys(edit).length > 0) merged.edit = edit;
|
|
124
111
|
return merged;
|
package/src/readseek-value.ts
CHANGED
|
@@ -100,6 +100,42 @@ export function buildReadseekError(
|
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
export interface ToolErrorResult {
|
|
104
|
+
content: [{ type: "text"; text: string }];
|
|
105
|
+
isError: true;
|
|
106
|
+
details: { readseekValue: Record<string, unknown> };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Build the standard failure envelope shared by every read-family tool: a text
|
|
111
|
+
* content block plus a `readseekValue` carrying `ok: false` and a
|
|
112
|
+
* {@link buildReadseekError} payload.
|
|
113
|
+
*
|
|
114
|
+
* `path` is included only when provided, and `extra` is merged into
|
|
115
|
+
* `readseekValue` so callers can attach tool-specific fields (e.g. write's
|
|
116
|
+
* `lines`/`warnings`).
|
|
117
|
+
*/
|
|
118
|
+
export function buildToolErrorResult(
|
|
119
|
+
tool: string,
|
|
120
|
+
code: string,
|
|
121
|
+
message: string,
|
|
122
|
+
opts: { path?: string; hint?: string; details?: unknown; extra?: Record<string, unknown> } = {},
|
|
123
|
+
): ToolErrorResult {
|
|
124
|
+
return {
|
|
125
|
+
content: [{ type: "text", text: message }],
|
|
126
|
+
isError: true,
|
|
127
|
+
details: {
|
|
128
|
+
readseekValue: {
|
|
129
|
+
tool,
|
|
130
|
+
...(opts.extra ?? {}),
|
|
131
|
+
ok: false,
|
|
132
|
+
...(opts.path !== undefined ? { path: opts.path } : {}),
|
|
133
|
+
error: buildReadseekError(code, message, opts.hint, opts.details),
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
103
139
|
export function buildReadseekEditResult(input: {
|
|
104
140
|
ok?: boolean;
|
|
105
141
|
path: string;
|
package/src/sg.ts
CHANGED
|
@@ -5,12 +5,12 @@ import path from "node:path";
|
|
|
5
5
|
import { stat as fsStat } from "node:fs/promises";
|
|
6
6
|
import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
|
|
7
7
|
import { escapeControlCharsForDisplay } from "./hashline.js";
|
|
8
|
-
import {
|
|
8
|
+
import { buildToolErrorResult, type ReadseekLine } from "./readseek-value.js";
|
|
9
9
|
import { resolveToCwd } from "./path-utils.js";
|
|
10
10
|
import { isReadseekAvailable, readseekSearch, type ReadseekHashline, type ReadseekSearchFileOutput } from "./readseek-client.js";
|
|
11
11
|
import { buildSgOutput } from "./sg-output.js";
|
|
12
12
|
|
|
13
|
-
import { clampLineToWidth, clampLinesToWidth,
|
|
13
|
+
import { clampLineToWidth, clampLinesToWidth, renderToolLabel, resolveRenderResultContext, summaryLine } from "./tui-render-utils.js";
|
|
14
14
|
|
|
15
15
|
type SgParams = { pattern: string; lang?: string; path?: string; cached?: boolean; others?: boolean; ignored?: boolean };
|
|
16
16
|
|
|
@@ -47,11 +47,6 @@ export function mergeRanges(ranges: SgRange[]): SgRange[] {
|
|
|
47
47
|
const SG_PROMPT_METADATA = defineToolPromptMetadata({
|
|
48
48
|
promptUrl: new URL("../prompts/sg.md", import.meta.url),
|
|
49
49
|
promptSnippet: "Search code structurally with readseek and return edit-ready anchors",
|
|
50
|
-
promptGuidelines: [
|
|
51
|
-
"Use search when text search is too broad or brittle and the query depends on code shape.",
|
|
52
|
-
"Use search for calls, imports, declarations, JSX, and similar syntax patterns.",
|
|
53
|
-
"Use grep instead of search for plain text search.",
|
|
54
|
-
],
|
|
55
50
|
});
|
|
56
51
|
|
|
57
52
|
export function isSgAvailable(): boolean {
|
|
@@ -128,17 +123,7 @@ export function registerSgTool(pi: ExtensionAPI, options: SgToolOptions = {}) {
|
|
|
128
123
|
const p = params as SgParams;
|
|
129
124
|
if (p.ignored && !p.others) {
|
|
130
125
|
const message = "Error: search parameter 'ignored' requires 'others'";
|
|
131
|
-
return
|
|
132
|
-
content: [{ type: "text", text: message }],
|
|
133
|
-
isError: true,
|
|
134
|
-
details: {
|
|
135
|
-
readseekValue: {
|
|
136
|
-
tool: "search",
|
|
137
|
-
ok: false,
|
|
138
|
-
error: buildReadseekError("invalid-parameter", message),
|
|
139
|
-
},
|
|
140
|
-
},
|
|
141
|
-
};
|
|
126
|
+
return buildToolErrorResult("search", "invalid-parameter", message);
|
|
142
127
|
}
|
|
143
128
|
const searchPath = resolveToCwd(p.path ?? ".", ctx.cwd);
|
|
144
129
|
let searchPathIsFile = false;
|
|
@@ -149,47 +134,14 @@ export function registerSgTool(pi: ExtensionAPI, options: SgToolOptions = {}) {
|
|
|
149
134
|
} catch (err: any) {
|
|
150
135
|
if (err?.code === "ENOENT") {
|
|
151
136
|
const message = `Error: path '${p.path ?? "."}' does not exist`;
|
|
152
|
-
return {
|
|
153
|
-
content: [{ type: "text", text: message }],
|
|
154
|
-
isError: true,
|
|
155
|
-
details: {
|
|
156
|
-
readseekValue: {
|
|
157
|
-
tool: "search",
|
|
158
|
-
ok: false,
|
|
159
|
-
path: p.path ?? searchPath,
|
|
160
|
-
error: buildReadseekError("path-not-found", message),
|
|
161
|
-
},
|
|
162
|
-
},
|
|
163
|
-
};
|
|
137
|
+
return buildToolErrorResult("search", "path-not-found", message, { path: p.path ?? searchPath });
|
|
164
138
|
}
|
|
165
139
|
if (err?.code === "EACCES" || err?.code === "EPERM") {
|
|
166
140
|
const message = `Error: permission denied for path '${p.path ?? "."}'`;
|
|
167
|
-
return {
|
|
168
|
-
content: [{ type: "text", text: message }],
|
|
169
|
-
isError: true,
|
|
170
|
-
details: {
|
|
171
|
-
readseekValue: {
|
|
172
|
-
tool: "search",
|
|
173
|
-
ok: false,
|
|
174
|
-
path: p.path ?? searchPath,
|
|
175
|
-
error: buildReadseekError("permission-denied", message),
|
|
176
|
-
},
|
|
177
|
-
},
|
|
178
|
-
};
|
|
141
|
+
return buildToolErrorResult("search", "permission-denied", message, { path: p.path ?? searchPath });
|
|
179
142
|
}
|
|
180
143
|
const message = `Error: could not access path '${p.path ?? "."}': ${err?.message ?? String(err)}`;
|
|
181
|
-
return {
|
|
182
|
-
content: [{ type: "text", text: message }],
|
|
183
|
-
isError: true,
|
|
184
|
-
details: {
|
|
185
|
-
readseekValue: {
|
|
186
|
-
tool: "search",
|
|
187
|
-
ok: false,
|
|
188
|
-
path: p.path ?? searchPath,
|
|
189
|
-
error: buildReadseekError("fs-error", message, undefined, { fsCode: err?.code, fsMessage: err?.message }),
|
|
190
|
-
},
|
|
191
|
-
},
|
|
192
|
-
};
|
|
144
|
+
return buildToolErrorResult("search", "fs-error", message, { path: p.path ?? searchPath, details: { fsCode: err?.code, fsMessage: err?.message } });
|
|
193
145
|
}
|
|
194
146
|
|
|
195
147
|
try {
|
|
@@ -260,19 +212,12 @@ export function registerSgTool(pi: ExtensionAPI, options: SgToolOptions = {}) {
|
|
|
260
212
|
} catch (err: any) {
|
|
261
213
|
const message = String(err?.message || err);
|
|
262
214
|
const missingReadseek = err?.code === "ENOENT" || /Cannot find package|Cannot find module|no such file/i.test(message);
|
|
263
|
-
return
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
ok: false,
|
|
270
|
-
error: missingReadseek
|
|
271
|
-
? buildReadseekError("readseek-not-installed", message, "Run npm install to install @jarkkojs/readseek.")
|
|
272
|
-
: buildReadseekError("readseek-execution-error", message),
|
|
273
|
-
},
|
|
274
|
-
},
|
|
275
|
-
};
|
|
215
|
+
return buildToolErrorResult(
|
|
216
|
+
"search",
|
|
217
|
+
missingReadseek ? "readseek-not-installed" : "readseek-execution-error",
|
|
218
|
+
message,
|
|
219
|
+
missingReadseek ? { hint: "Run npm install to install @jarkkojs/readseek." } : {},
|
|
220
|
+
);
|
|
276
221
|
}
|
|
277
222
|
},
|
|
278
223
|
renderCall(args: any, theme: any, ...rest: any[]) {
|
|
@@ -285,13 +230,7 @@ export function registerSgTool(pi: ExtensionAPI, options: SgToolOptions = {}) {
|
|
|
285
230
|
return new Text(clampLineToWidth(text, context.width), 0, 0);
|
|
286
231
|
},
|
|
287
232
|
renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
|
|
288
|
-
const
|
|
289
|
-
rest[0] ?? options ?? {};
|
|
290
|
-
const isPartial = context.isPartial ?? (options as any)?.isPartial ?? false;
|
|
291
|
-
const isError = context.isError ?? false;
|
|
292
|
-
const expanded = isRendererExpanded(options as any, context as any);
|
|
293
|
-
const cwd = context.cwd ?? process.cwd();
|
|
294
|
-
const width = (context as any).width ?? (options as any)?.width;
|
|
233
|
+
const { isPartial, isError, expanded, cwd, width } = resolveRenderResultContext(options, rest);
|
|
295
234
|
|
|
296
235
|
if (isPartial) return new Text(clampLinesToWidth([summaryLine("pending search")], width).join("\n"), 0, 0);
|
|
297
236
|
|
|
@@ -59,7 +59,6 @@ function promptFileName(promptUrl: URL): string {
|
|
|
59
59
|
export function defineToolPromptMetadata(options: {
|
|
60
60
|
promptUrl: URL;
|
|
61
61
|
promptSnippet: string;
|
|
62
|
-
promptGuidelines: string[];
|
|
63
62
|
}): ToolPromptMetadata {
|
|
64
63
|
const prompt = loadPrompt(options.promptUrl);
|
|
65
64
|
const fileName = promptFileName(options.promptUrl);
|
|
@@ -67,6 +66,6 @@ export function defineToolPromptMetadata(options: {
|
|
|
67
66
|
return {
|
|
68
67
|
description: compactDescription ?? firstPromptParagraph(prompt),
|
|
69
68
|
promptSnippet: options.promptSnippet,
|
|
70
|
-
promptGuidelines: COMPACT_GUIDELINES[fileName] ??
|
|
69
|
+
promptGuidelines: COMPACT_GUIDELINES[fileName] ?? [],
|
|
71
70
|
};
|
|
72
71
|
}
|