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/read.ts
DELETED
|
@@ -1,661 +0,0 @@
|
|
|
1
|
-
import { readFile as fsReadFile } from "node:fs/promises";
|
|
2
|
-
|
|
3
|
-
import type { ExtensionAPI, ToolRenderResultOptions, AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import {
|
|
5
|
-
truncateHead,
|
|
6
|
-
DEFAULT_MAX_BYTES,
|
|
7
|
-
DEFAULT_MAX_LINES,
|
|
8
|
-
} from "@earendil-works/pi-coding-agent";
|
|
9
|
-
import { Type } from "@sinclair/typebox";
|
|
10
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
11
|
-
|
|
12
|
-
import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
|
|
13
|
-
import { normalizeToLF, stripBom, hasBareCarriageReturn } from "./edit-diff.js";
|
|
14
|
-
import { ensureHashInit, escapeControlCharsForDisplay } from "./hashline.js";
|
|
15
|
-
import { buildReadSeekWarning, buildToolErrorResult, renderReadSeekLines, type ReadSeekLine, type ReadSeekWarning } from "./readseek-value.js";
|
|
16
|
-
import { looksLikeBinary } from "./binary-detect.js";
|
|
17
|
-
import { resolveToCwd } from "./path-utils.js";
|
|
18
|
-
import { throwIfAborted } from "./runtime.js";
|
|
19
|
-
import { formatFsError } from "./fs-error.js";
|
|
20
|
-
import { getOrGenerateMap } from "./file-map.js";
|
|
21
|
-
import { formatFileMapWithBudget } from "./readseek/formatter.js";
|
|
22
|
-
import { findSymbol, type SymbolMatch } from "./readseek/symbol-lookup.js";
|
|
23
|
-
import { formatAmbiguous, formatNotFound } from "./readseek/symbol-error-format.js";
|
|
24
|
-
import { buildReadOutput } from "./read-output.js";
|
|
25
|
-
|
|
26
|
-
import { buildLocalBundle } from "./read-local-bundle.js";
|
|
27
|
-
import { coerceObviousBase10Int } from "./coerce-obvious-int.js";
|
|
28
|
-
import { readSeekRead, readSeekDetect, readSeekImage, readSeekPdf, readSeekPreparedImage, type ReadSeekDetection, type ReadSeekPdfOutput } from "./readseek-client.js";
|
|
29
|
-
import { formatReadCallText, formatReadResultText } from "./read-render-helpers.js";
|
|
30
|
-
import { resolveReadSeekImageMode } from "./readseek-settings.js";
|
|
31
|
-
import { clampLineToWidth, clampLinesToWidth, linkToolPath, renderPendingResult, renderToolLabel, resolveRenderResultContext, summaryLine, wrapReadHashlinesForWidthCached } from "./tui-render-utils.js";
|
|
32
|
-
import type { FileAnchoredCallback } from "./tool-types.js";
|
|
33
|
-
import { filePathParam, mapParam, optionalIntOrString, registerReadSeekTool } from "./register-tool.js";
|
|
34
|
-
|
|
35
|
-
interface ReadParams {
|
|
36
|
-
path: string;
|
|
37
|
-
offset?: number | string;
|
|
38
|
-
limit?: number | string;
|
|
39
|
-
page?: number | string;
|
|
40
|
-
symbol?: string;
|
|
41
|
-
map?: boolean;
|
|
42
|
-
bundle?: string;
|
|
43
|
-
image?: "none" | "ocr" | "caption" | "objects";
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
interface ReadToolOptions {
|
|
47
|
-
onSuccessfulRead?: FileAnchoredCallback;
|
|
48
|
-
name?: string;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export interface ExecuteReadOptions {
|
|
52
|
-
toolCallId: string;
|
|
53
|
-
params: unknown;
|
|
54
|
-
signal: AbortSignal | undefined;
|
|
55
|
-
onUpdate: any;
|
|
56
|
-
cwd: string;
|
|
57
|
-
onSuccessfulRead?: FileAnchoredCallback;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function hasReadAnchors(result: AgentToolResult<any>): boolean {
|
|
61
|
-
const details = (result as { details?: unknown }).details;
|
|
62
|
-
if (!details || typeof details !== "object") return false;
|
|
63
|
-
const readSeekValue = (details as { readSeekValue?: unknown }).readSeekValue;
|
|
64
|
-
if (!readSeekValue || typeof readSeekValue !== "object") return false;
|
|
65
|
-
const lines = (readSeekValue as { lines?: unknown }).lines;
|
|
66
|
-
return Array.isArray(lines) && lines.length > 0;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function formatImageAnalysis(detection: ReadSeekDetection, mode: "all" | "ocr" | "caption" | "objects"): string | undefined {
|
|
70
|
-
if (detection.kind !== "image") return undefined;
|
|
71
|
-
const sections: string[] = [];
|
|
72
|
-
const ocr = detection.ocr?.trim();
|
|
73
|
-
if (ocr) sections.push(`OCR text from image:\n${ocr}`);
|
|
74
|
-
const caption = detection.caption?.trim();
|
|
75
|
-
if (caption) sections.push(`Image caption:\n${caption}`);
|
|
76
|
-
if (detection.objects?.length) {
|
|
77
|
-
const lines = detection.objects.map((object) => `- ${object.label} [${object.bbox.join(", ")}]`);
|
|
78
|
-
sections.push(`Detected objects:\n${lines.join("\n")}`);
|
|
79
|
-
}
|
|
80
|
-
if (sections.length > 0) return sections.join("\n\n");
|
|
81
|
-
if (mode === "ocr") return "No OCR text detected in image.";
|
|
82
|
-
if (mode === "objects") return "No objects detected in image.";
|
|
83
|
-
if (mode === "all") return "No image analysis returned.";
|
|
84
|
-
return "Image caption returned no text.";
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function formatPdfAnalysis(pdf: ReadSeekPdfOutput): string {
|
|
88
|
-
const sections = [pdf.markdown.trimEnd()];
|
|
89
|
-
for (const image of pdf.images) {
|
|
90
|
-
const details: string[] = [];
|
|
91
|
-
const ocr = image.ocr?.trim();
|
|
92
|
-
if (ocr) details.push(`OCR text:\n${ocr}`);
|
|
93
|
-
const caption = image.caption?.trim();
|
|
94
|
-
if (caption) details.push(`Caption:\n${caption}`);
|
|
95
|
-
if (image.objects?.length) {
|
|
96
|
-
const lines = image.objects.map((object) => `- ${object.label} [${object.bbox.join(", ")}]`);
|
|
97
|
-
details.push(`Detected objects:\n${lines.join("\n")}`);
|
|
98
|
-
}
|
|
99
|
-
if (details.length > 0) sections.push(`PDF page ${image.page} image:\n${details.join("\n\n")}`);
|
|
100
|
-
}
|
|
101
|
-
return sections.filter(Boolean).join("\n\n");
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function truncateDocumentText(text: string): string {
|
|
105
|
-
const truncation = truncateHead(text, { maxLines: DEFAULT_MAX_LINES, maxBytes: DEFAULT_MAX_BYTES });
|
|
106
|
-
if (!truncation.truncated) return truncation.content;
|
|
107
|
-
return `${truncation.content}\n[… document output truncated; use readSeek_view with page or node selectors]`;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function skippedVisualFile(path: string): AgentToolResult<any> {
|
|
111
|
-
return {
|
|
112
|
-
content: [{ type: "text", text: `[Skipped image/PDF: ${path}; no image mode selected]` }],
|
|
113
|
-
details: {},
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export async function executeRead(opts: ExecuteReadOptions): Promise<AgentToolResult<any>> {
|
|
118
|
-
const { params, signal, cwd, onSuccessfulRead } = opts;
|
|
119
|
-
await ensureHashInit();
|
|
120
|
-
const rawParams = params as ReadParams;
|
|
121
|
-
const offset = coerceObviousBase10Int(rawParams.offset, "offset");
|
|
122
|
-
if (!offset.ok) {
|
|
123
|
-
return buildToolErrorResult("read", "invalid-offset", offset.message, { path: rawParams.path });
|
|
124
|
-
}
|
|
125
|
-
const limit = coerceObviousBase10Int(rawParams.limit, "limit");
|
|
126
|
-
if (!limit.ok) {
|
|
127
|
-
return buildToolErrorResult("read", "invalid-limit", limit.message, { path: rawParams.path });
|
|
128
|
-
}
|
|
129
|
-
if (limit.value !== undefined && limit.value < 1) {
|
|
130
|
-
const message = `Invalid limit: expected a positive integer, received ${limit.value}.`;
|
|
131
|
-
return buildToolErrorResult("read", "invalid-limit", message, { path: rawParams.path });
|
|
132
|
-
}
|
|
133
|
-
if (offset.value !== undefined && offset.value < 1) {
|
|
134
|
-
const message = `Invalid offset: expected a positive integer, received ${offset.value}.`;
|
|
135
|
-
return buildToolErrorResult("read", "invalid-offset", message, { path: rawParams.path });
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const page = coerceObviousBase10Int(rawParams.page, "page");
|
|
139
|
-
if (!page.ok) {
|
|
140
|
-
return buildToolErrorResult("read", "invalid-page", page.message, { path: rawParams.path });
|
|
141
|
-
}
|
|
142
|
-
if (page.value !== undefined && page.value < 1) {
|
|
143
|
-
const message = `Invalid page: expected a positive integer, received ${page.value}.`;
|
|
144
|
-
return buildToolErrorResult("read", "invalid-page", message, { path: rawParams.path });
|
|
145
|
-
}
|
|
146
|
-
const rawBundle = typeof rawParams.bundle === "string" ? rawParams.bundle.trim() : undefined;
|
|
147
|
-
const requestedMapViaBundle =
|
|
148
|
-
rawBundle === "map" ||
|
|
149
|
-
(rawBundle === "local" && rawParams.symbol === undefined && (rawParams.map ?? true));
|
|
150
|
-
const p = {
|
|
151
|
-
...rawParams,
|
|
152
|
-
offset: offset.value,
|
|
153
|
-
limit: limit.value,
|
|
154
|
-
page: page.value,
|
|
155
|
-
map: rawParams.map === true || requestedMapViaBundle,
|
|
156
|
-
bundle: requestedMapViaBundle ? undefined : rawBundle,
|
|
157
|
-
};
|
|
158
|
-
if (rawParams.symbol !== undefined) {
|
|
159
|
-
const trimmedSymbol = typeof rawParams.symbol === "string" ? rawParams.symbol.trim() : "";
|
|
160
|
-
if (trimmedSymbol.length === 0) {
|
|
161
|
-
const message = "Invalid symbol: expected a non-empty string.";
|
|
162
|
-
return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
|
|
163
|
-
}
|
|
164
|
-
p.symbol = trimmedSymbol;
|
|
165
|
-
}
|
|
166
|
-
const rawPath = p.path.replace(/^@/, "");
|
|
167
|
-
const absolutePath = resolveToCwd(rawPath, cwd);
|
|
168
|
-
const succeed = <T extends AgentToolResult<any>>(result: T): T => {
|
|
169
|
-
const isError = (result as { isError?: boolean }).isError;
|
|
170
|
-
if (!isError && hasReadAnchors(result)) {
|
|
171
|
-
onSuccessfulRead?.(absolutePath);
|
|
172
|
-
}
|
|
173
|
-
return result;
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
throwIfAborted(signal);
|
|
177
|
-
if (p.bundle !== undefined && p.bundle !== "local") {
|
|
178
|
-
const message = `Invalid bundle: expected "local", received ${JSON.stringify(p.bundle)}.`;
|
|
179
|
-
return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
|
|
180
|
-
}
|
|
181
|
-
if (p.symbol && (p.offset !== undefined || p.limit !== undefined)) {
|
|
182
|
-
const message = "Cannot combine symbol with offset/limit. Use one or the other.";
|
|
183
|
-
return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
|
|
184
|
-
}
|
|
185
|
-
if (p.bundle && !p.symbol) {
|
|
186
|
-
const message = 'Cannot use bundle without symbol. Use read({ path, symbol, bundle: "local" }).';
|
|
187
|
-
return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
|
|
188
|
-
}
|
|
189
|
-
if (p.bundle && p.map) {
|
|
190
|
-
const message = "Cannot combine bundle with map. Use one or the other.";
|
|
191
|
-
return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
|
|
192
|
-
}
|
|
193
|
-
if (p.map && p.symbol) {
|
|
194
|
-
const message = "Cannot combine map with symbol. Use one or the other.";
|
|
195
|
-
return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
|
|
196
|
-
}
|
|
197
|
-
throwIfAborted(signal);
|
|
198
|
-
const ext = rawPath.split(".").pop()?.toLowerCase() ?? "";
|
|
199
|
-
let rawBuffer: Buffer;
|
|
200
|
-
try {
|
|
201
|
-
rawBuffer = await fsReadFile(absolutePath);
|
|
202
|
-
} catch (err: any) {
|
|
203
|
-
const { code, message } = formatFsError(err, "read-error");
|
|
204
|
-
return buildToolErrorResult("read", code, message, {
|
|
205
|
-
path: rawParams.path,
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const hasBinaryContent = looksLikeBinary(rawBuffer);
|
|
210
|
-
const hasPdfHeader = rawBuffer.subarray(0, 1024).includes(Buffer.from("%PDF-"));
|
|
211
|
-
if (hasBinaryContent || ext === "pdf" || hasPdfHeader) {
|
|
212
|
-
let detection: ReadSeekDetection | undefined;
|
|
213
|
-
try {
|
|
214
|
-
detection = await readSeekDetect(absolutePath, { signal });
|
|
215
|
-
} catch {
|
|
216
|
-
return buildToolErrorResult("read", "read-error", `Visual file detection failed for ${absolutePath}.`, {
|
|
217
|
-
path: rawParams.path,
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
if (!detection) {
|
|
221
|
-
return buildToolErrorResult("read", "read-error", `Visual file detection returned no data for ${absolutePath}.`, {
|
|
222
|
-
path: rawParams.path,
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
if (detection.kind === "image" || detection.type === "application/pdf") {
|
|
226
|
-
if (p.offset !== undefined || p.limit !== undefined) {
|
|
227
|
-
const message = "Cannot combine offset/limit with image or PDF reads.";
|
|
228
|
-
return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
|
|
229
|
-
}
|
|
230
|
-
if (detection.kind !== "pdf" && p.page !== undefined) {
|
|
231
|
-
const message = "The page parameter applies to PDFs only.";
|
|
232
|
-
return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
|
|
233
|
-
}
|
|
234
|
-
const pdfPage = detection.kind === "pdf" ? (p.page ?? 1) : undefined;
|
|
235
|
-
const imageMode = resolveReadSeekImageMode();
|
|
236
|
-
if (imageMode === "off" || p.image === undefined) {
|
|
237
|
-
return skippedVisualFile(rawParams.path);
|
|
238
|
-
}
|
|
239
|
-
if (imageMode === "on" && p.image === "none") {
|
|
240
|
-
return buildToolErrorResult("read", "invalid-params-combo", 'image="none" is only available when imageMode is "auto".', {
|
|
241
|
-
path: rawParams.path,
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
if (p.image === "none") {
|
|
245
|
-
try {
|
|
246
|
-
if (detection.kind === "pdf") {
|
|
247
|
-
const pdf = await readSeekPdf(absolutePath, p.image, { page: pdfPage, signal });
|
|
248
|
-
const content: AgentToolResult<any>["content"] = [{ type: "text", text: truncateDocumentText(pdf.markdown) }];
|
|
249
|
-
for (const image of pdf.images) {
|
|
250
|
-
if (image.encoding !== "base64" || image.data === undefined) continue;
|
|
251
|
-
content.push({ type: "text", text: `[PDF page ${image.page} image]` });
|
|
252
|
-
content.push({ type: "image", data: image.data, mimeType: image.mime });
|
|
253
|
-
}
|
|
254
|
-
return succeed({ content, details: {} });
|
|
255
|
-
}
|
|
256
|
-
const image = await readSeekPreparedImage(absolutePath, { signal });
|
|
257
|
-
return succeed({
|
|
258
|
-
content: [{ type: "image" as const, data: image.data, mimeType: image.mime }],
|
|
259
|
-
details: {},
|
|
260
|
-
});
|
|
261
|
-
} catch (error) {
|
|
262
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
263
|
-
return buildToolErrorResult("read", "read-error", `Image preprocessing unavailable: ${message}`, {
|
|
264
|
-
path: rawParams.path,
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
try {
|
|
270
|
-
if (detection.kind === "pdf") {
|
|
271
|
-
const pdf = await readSeekPdf(absolutePath, p.image, { page: pdfPage, signal });
|
|
272
|
-
return succeed({
|
|
273
|
-
content: [{ type: "text" as const, text: truncateDocumentText(formatPdfAnalysis(pdf)) }],
|
|
274
|
-
details: {},
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
const analysis = await readSeekImage(absolutePath, [p.image], { signal });
|
|
278
|
-
const imageAnalysis = formatImageAnalysis(analysis, p.image);
|
|
279
|
-
if (imageAnalysis) {
|
|
280
|
-
return succeed({
|
|
281
|
-
content: [{ type: "text" as const, text: imageAnalysis }],
|
|
282
|
-
details: {},
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
} catch (error) {
|
|
286
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
287
|
-
const label = detection.kind === "pdf" ? "PDF" : "Image";
|
|
288
|
-
return buildToolErrorResult("read", "read-error", `${label} analysis unavailable: ${message}`, {
|
|
289
|
-
path: rawParams.path,
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
return buildToolErrorResult("read", "read-error", "Image analysis returned no content.", {
|
|
293
|
-
path: rawParams.path,
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
if (p.page !== undefined) {
|
|
298
|
-
const message = "The page parameter applies to PDFs only.";
|
|
299
|
-
return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
|
|
300
|
-
}
|
|
301
|
-
throwIfAborted(signal);
|
|
302
|
-
const rawText = rawBuffer.toString("utf-8");
|
|
303
|
-
const normalized = normalizeToLF(stripBom(rawText).text);
|
|
304
|
-
const allLines = splitReadSeekLines(normalized);
|
|
305
|
-
const total = allLines.length;
|
|
306
|
-
const structuredWarnings: ReadSeekWarning[] = [];
|
|
307
|
-
let startLine = p.offset !== undefined ? p.offset : 1;
|
|
308
|
-
let endIdx = p.limit !== undefined ? Math.min(startLine - 1 + p.limit, total) : total;
|
|
309
|
-
if (p.offset !== undefined && startLine > total) {
|
|
310
|
-
const message = `offset ${p.offset} is past end of file (${total} lines)`;
|
|
311
|
-
return buildToolErrorResult("read", "offset-past-end", message, { path: rawParams.path });
|
|
312
|
-
}
|
|
313
|
-
let symbolMatch: SymbolMatch | undefined;
|
|
314
|
-
let symbolFileMap: Awaited<ReturnType<typeof getOrGenerateMap>> | null = null;
|
|
315
|
-
let symbolWarning: string | undefined;
|
|
316
|
-
let bundleMetadata:
|
|
317
|
-
| {
|
|
318
|
-
mode: "local";
|
|
319
|
-
applied: boolean;
|
|
320
|
-
localSupport: Array<{
|
|
321
|
-
symbol: {
|
|
322
|
-
query: string;
|
|
323
|
-
name: string;
|
|
324
|
-
kind: string;
|
|
325
|
-
parentName?: string;
|
|
326
|
-
startLine: number;
|
|
327
|
-
endLine: number;
|
|
328
|
-
};
|
|
329
|
-
lines: string[];
|
|
330
|
-
}>;
|
|
331
|
-
warnings: ReadSeekWarning[];
|
|
332
|
-
}
|
|
333
|
-
| null = null;
|
|
334
|
-
if (p.symbol) {
|
|
335
|
-
symbolFileMap = await getOrGenerateMap(absolutePath);
|
|
336
|
-
if (!symbolFileMap) {
|
|
337
|
-
const extLabel = ext || "unknown";
|
|
338
|
-
symbolWarning = `[Warning: symbol lookup not available for .${extLabel} files — showing full file]\n\n`;
|
|
339
|
-
} else {
|
|
340
|
-
const lookup = findSymbol(symbolFileMap, p.symbol);
|
|
341
|
-
if (lookup.type === "ambiguous") {
|
|
342
|
-
return succeed({
|
|
343
|
-
content: [
|
|
344
|
-
{
|
|
345
|
-
type: "text",
|
|
346
|
-
text: formatAmbiguous(p.symbol, lookup.candidates),
|
|
347
|
-
},
|
|
348
|
-
],
|
|
349
|
-
isError: false,
|
|
350
|
-
details: {},
|
|
351
|
-
});
|
|
352
|
-
}
|
|
353
|
-
if (lookup.type === "not-found") {
|
|
354
|
-
symbolWarning = `${formatNotFound(p.symbol, symbolFileMap)}\n\n`;
|
|
355
|
-
}
|
|
356
|
-
if (lookup.type === "found") {
|
|
357
|
-
startLine = Math.max(1, lookup.symbol.startLine);
|
|
358
|
-
endIdx = Math.min(total, lookup.symbol.endLine);
|
|
359
|
-
symbolMatch = lookup.symbol;
|
|
360
|
-
}
|
|
361
|
-
if (lookup.type === "fuzzy") {
|
|
362
|
-
startLine = Math.max(1, lookup.symbol.startLine);
|
|
363
|
-
endIdx = Math.min(total, lookup.symbol.endLine);
|
|
364
|
-
symbolMatch = lookup.symbol;
|
|
365
|
-
|
|
366
|
-
const tierLabel = lookup.tier === "camelCase" ? "camelCase word boundary" : "substring";
|
|
367
|
-
const otherNames = lookup.otherCandidates.map((c) => `\`${c.name}\``).join(", ");
|
|
368
|
-
const confirmHint = `read({ symbol: "${lookup.symbol.name}" }) or ${lookup.symbol.name}@${lookup.symbol.startLine} to select by start line`;
|
|
369
|
-
const lines = [
|
|
370
|
-
`[Symbol '${p.symbol}' not exact-matched. Closest match: \`${lookup.symbol.name}\` (${lookup.symbol.kind}, lines ${lookup.symbol.startLine}-${lookup.symbol.endLine}) via ${tierLabel}.`,
|
|
371
|
-
];
|
|
372
|
-
if (otherNames) lines.push(` Other candidates: ${otherNames}.`);
|
|
373
|
-
lines.push(` To confirm: ${confirmHint}.]`);
|
|
374
|
-
const bannerText = lines.join("\n");
|
|
375
|
-
structuredWarnings.push(
|
|
376
|
-
buildReadSeekWarning("fuzzy-symbol-match", bannerText, {
|
|
377
|
-
tier: lookup.tier,
|
|
378
|
-
symbol: lookup.symbol,
|
|
379
|
-
otherCandidates: lookup.otherCandidates,
|
|
380
|
-
}),
|
|
381
|
-
);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
if (p.bundle === "local") {
|
|
387
|
-
if (!symbolFileMap) {
|
|
388
|
-
const extLabel = ext || "unknown";
|
|
389
|
-
const warning = buildReadSeekWarning(
|
|
390
|
-
"bundle-unmappable",
|
|
391
|
-
`[Warning: local bundle unavailable because symbol mapping is not available for .${extLabel} files — showing plain symbol read]`,
|
|
392
|
-
);
|
|
393
|
-
structuredWarnings.push(warning);
|
|
394
|
-
bundleMetadata = {
|
|
395
|
-
mode: "local",
|
|
396
|
-
applied: false,
|
|
397
|
-
localSupport: [],
|
|
398
|
-
warnings: [warning],
|
|
399
|
-
};
|
|
400
|
-
} else if (!symbolMatch) {
|
|
401
|
-
bundleMetadata = {
|
|
402
|
-
mode: "local",
|
|
403
|
-
applied: false,
|
|
404
|
-
localSupport: [],
|
|
405
|
-
warnings: [],
|
|
406
|
-
};
|
|
407
|
-
} else {
|
|
408
|
-
const bundle = buildLocalBundle(symbolFileMap, symbolMatch, allLines);
|
|
409
|
-
if (!bundle) {
|
|
410
|
-
const warning = buildReadSeekWarning(
|
|
411
|
-
"bundle-context-unavailable",
|
|
412
|
-
`[Warning: local bundle context could not be determined for symbol '${symbolMatch.name}' — showing plain symbol read]`,
|
|
413
|
-
);
|
|
414
|
-
structuredWarnings.push(warning);
|
|
415
|
-
bundleMetadata = {
|
|
416
|
-
mode: "local",
|
|
417
|
-
applied: false,
|
|
418
|
-
localSupport: [],
|
|
419
|
-
warnings: [warning],
|
|
420
|
-
};
|
|
421
|
-
} else {
|
|
422
|
-
bundleMetadata = {
|
|
423
|
-
mode: "local",
|
|
424
|
-
applied: true,
|
|
425
|
-
localSupport: bundle.support.map((item) => ({
|
|
426
|
-
symbol: {
|
|
427
|
-
query: item.symbol.name,
|
|
428
|
-
name: item.symbol.name,
|
|
429
|
-
kind: item.symbol.kind,
|
|
430
|
-
parentName: item.symbol.parentName,
|
|
431
|
-
startLine: item.symbol.startLine,
|
|
432
|
-
endLine: item.symbol.endLine,
|
|
433
|
-
},
|
|
434
|
-
lines: item.lines,
|
|
435
|
-
})),
|
|
436
|
-
warnings: [],
|
|
437
|
-
};
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
throwIfAborted(signal);
|
|
443
|
-
let readSeekOutput: Awaited<ReturnType<typeof readSeekRead>>;
|
|
444
|
-
try {
|
|
445
|
-
readSeekOutput = total === 0
|
|
446
|
-
? await readSeekRead(absolutePath, undefined, undefined, { signal })
|
|
447
|
-
: await readSeekRead(absolutePath, startLine, endIdx, { signal });
|
|
448
|
-
} catch (err: any) {
|
|
449
|
-
const detail = err?.message ? ` — ${err.message}` : "";
|
|
450
|
-
const message = `readseek failed while reading ${rawPath}${detail}`;
|
|
451
|
-
return buildToolErrorResult("read", "readseek-error", message, { path: rawParams.path, hint: "Ensure @jarkkojs/readseek and its npm platform package are installed.", details: { message: err?.message } });
|
|
452
|
-
}
|
|
453
|
-
const expectedLineCount = Math.max(0, endIdx - startLine + 1);
|
|
454
|
-
const invalidLine = readSeekOutput.hashlines.find((line, index) => line.line !== startLine + index);
|
|
455
|
-
if (readSeekOutput.hashlines.length !== expectedLineCount || invalidLine) {
|
|
456
|
-
const message = invalidLine
|
|
457
|
-
? `readseek returned non-sequential line ${invalidLine.line} for requested range ${startLine}-${endIdx}`
|
|
458
|
-
: `readseek returned ${readSeekOutput.hashlines.length} lines for requested range ${startLine}-${endIdx} (${expectedLineCount} expected)`;
|
|
459
|
-
return buildToolErrorResult("read", "readseek-output-mismatch", message, { path: rawParams.path });
|
|
460
|
-
}
|
|
461
|
-
const readSeekLines: ReadSeekLine[] = readSeekOutput.hashlines.map((line) => ({
|
|
462
|
-
line: line.line,
|
|
463
|
-
hash: line.hash,
|
|
464
|
-
anchor: `${line.line}:${line.hash}`,
|
|
465
|
-
raw: line.text,
|
|
466
|
-
display: escapeControlCharsForDisplay(line.text),
|
|
467
|
-
}));
|
|
468
|
-
const selected = readSeekLines.map((line) => line.raw);
|
|
469
|
-
const formatted = renderReadSeekLines(readSeekLines);
|
|
470
|
-
|
|
471
|
-
const truncation = truncateHead(formatted, { maxLines: DEFAULT_MAX_LINES, maxBytes: DEFAULT_MAX_BYTES });
|
|
472
|
-
|
|
473
|
-
// Append structural map: on-demand (p.map) or auto on truncated full-file reads
|
|
474
|
-
const shouldAppendMap = !!p.map || (!!truncation.truncated && !p.offset && !p.limit && !symbolMatch);
|
|
475
|
-
let appendedMap = false;
|
|
476
|
-
let mapText: string | null = null;
|
|
477
|
-
if (shouldAppendMap) {
|
|
478
|
-
try {
|
|
479
|
-
const fileMap = await getOrGenerateMap(absolutePath);
|
|
480
|
-
if (fileMap) {
|
|
481
|
-
const formattedMap = formatFileMapWithBudget(fileMap);
|
|
482
|
-
mapText = formattedMap;
|
|
483
|
-
appendedMap = true;
|
|
484
|
-
}
|
|
485
|
-
} catch {
|
|
486
|
-
// Map formatting failed — still return hashlined content without map
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
if (symbolWarning) {
|
|
491
|
-
structuredWarnings.push(buildReadSeekWarning("symbol-warning", symbolWarning.trim()));
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
if (hasBinaryContent) {
|
|
495
|
-
const warning = "[Warning: file appears to be binary — output may be garbled]";
|
|
496
|
-
structuredWarnings.push(buildReadSeekWarning("binary-content", warning));
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
if (hasBareCarriageReturn(rawText)) {
|
|
500
|
-
const warning = "[Warning: file contains bare CR (\\r) line endings — line numbering may be inconsistent with grep and other tools]";
|
|
501
|
-
structuredWarnings.push(buildReadSeekWarning("bare-cr", warning));
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
const readOutput = buildReadOutput({
|
|
505
|
-
path: absolutePath,
|
|
506
|
-
startLine,
|
|
507
|
-
endLine: endIdx,
|
|
508
|
-
totalLines: total,
|
|
509
|
-
selectedLines: selected,
|
|
510
|
-
lines: readSeekLines,
|
|
511
|
-
warnings: structuredWarnings,
|
|
512
|
-
truncation: truncation.truncated
|
|
513
|
-
? {
|
|
514
|
-
outputLines: truncation.outputLines,
|
|
515
|
-
totalLines: total,
|
|
516
|
-
outputBytes: truncation.outputBytes,
|
|
517
|
-
totalBytes: truncation.totalBytes,
|
|
518
|
-
}
|
|
519
|
-
: null,
|
|
520
|
-
continuation: !truncation.truncated && endIdx < total ? { nextOffset: endIdx + 1 } : null,
|
|
521
|
-
symbol: symbolMatch
|
|
522
|
-
? {
|
|
523
|
-
query: p.symbol ?? symbolMatch.name,
|
|
524
|
-
name: symbolMatch.name,
|
|
525
|
-
kind: symbolMatch.kind,
|
|
526
|
-
parentName: symbolMatch.parentName,
|
|
527
|
-
startLine: symbolMatch.startLine,
|
|
528
|
-
endLine: symbolMatch.endLine,
|
|
529
|
-
}
|
|
530
|
-
: null,
|
|
531
|
-
map: {
|
|
532
|
-
requested: !!p.map,
|
|
533
|
-
appended: appendedMap,
|
|
534
|
-
text: mapText,
|
|
535
|
-
},
|
|
536
|
-
...(bundleMetadata ? { bundle: bundleMetadata } : {}),
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
return succeed({
|
|
540
|
-
content: [{ type: "text", text: readOutput.text }],
|
|
541
|
-
details: {
|
|
542
|
-
truncation: truncation.truncated ? truncation : undefined,
|
|
543
|
-
readSeekValue: readOutput.readSeekValue,
|
|
544
|
-
},
|
|
545
|
-
});
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
function splitReadSeekLines(text: string): string[] {
|
|
549
|
-
if (text.length === 0) return [];
|
|
550
|
-
const withoutTrailingTerminator = text.endsWith("\n") ? text.slice(0, -1) : text;
|
|
551
|
-
return withoutTrailingTerminator.split("\n");
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}) {
|
|
555
|
-
const name = options.name ?? "readSeek_read";
|
|
556
|
-
const imageMode = resolveReadSeekImageMode();
|
|
557
|
-
const imageModes = imageMode === "auto" ? ["none", "all", "ocr", "caption", "objects"] as const : ["all", "ocr", "caption", "objects"] as const;
|
|
558
|
-
const promptMetadata = defineToolPromptMetadata({
|
|
559
|
-
promptUrl: new URL("../prompts/read.md", import.meta.url),
|
|
560
|
-
promptSnippet: "Read anchored text, symbols, maps, images, or PDFs",
|
|
561
|
-
registeredName: name,
|
|
562
|
-
});
|
|
563
|
-
const tool = registerReadSeekTool(pi, {
|
|
564
|
-
name,
|
|
565
|
-
label: "Read",
|
|
566
|
-
description: imageMode === "off"
|
|
567
|
-
? 'Read anchored text by range, map, or symbol; image and PDF files are skipped because imageMode is "off".'
|
|
568
|
-
: promptMetadata.description,
|
|
569
|
-
promptSnippet: promptMetadata.promptSnippet,
|
|
570
|
-
promptGuidelines: [
|
|
571
|
-
...promptMetadata.promptGuidelines,
|
|
572
|
-
imageMode === "off"
|
|
573
|
-
? "The image parameter is unavailable; image and PDF files are always skipped."
|
|
574
|
-
: `For an image or PDF, explicitly choose image: ${imageModes.join(", ")}; omitting image skips the file.`,
|
|
575
|
-
],
|
|
576
|
-
parameters: Type.Object({
|
|
577
|
-
path: filePathParam(),
|
|
578
|
-
offset: optionalIntOrString("Start line (1-indexed)"),
|
|
579
|
-
limit: optionalIntOrString("Maximum lines to return"),
|
|
580
|
-
page: optionalIntOrString("One-based PDF page; defaults to 1"),
|
|
581
|
-
symbol: Type.Optional(Type.String({ description: "Symbol name to read" })),
|
|
582
|
-
map: mapParam(),
|
|
583
|
-
bundle: Type.Optional(
|
|
584
|
-
Type.Literal("local", {
|
|
585
|
-
description: "Include related symbols from the same file; requires symbol",
|
|
586
|
-
}),
|
|
587
|
-
),
|
|
588
|
-
...(imageMode === "off"
|
|
589
|
-
? {}
|
|
590
|
-
: {
|
|
591
|
-
image: Type.Optional(Type.Union(imageModes.map((mode) => Type.Literal(mode)), {
|
|
592
|
-
description: `Image/PDF mode: ${imageModes.join(", ")}. Must be selected explicitly.`,
|
|
593
|
-
})),
|
|
594
|
-
}),
|
|
595
|
-
}),
|
|
596
|
-
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
597
|
-
return executeRead({
|
|
598
|
-
toolCallId,
|
|
599
|
-
params,
|
|
600
|
-
signal,
|
|
601
|
-
onUpdate,
|
|
602
|
-
cwd: ctx.cwd,
|
|
603
|
-
onSuccessfulRead: options.onSuccessfulRead,
|
|
604
|
-
});
|
|
605
|
-
},
|
|
606
|
-
renderCall(args: any, theme: any, ...rest: any[]) {
|
|
607
|
-
const context = rest[0] ?? {};
|
|
608
|
-
const cwd = context.cwd ?? process.cwd();
|
|
609
|
-
const { path: filePath, suffix } = formatReadCallText(args);
|
|
610
|
-
const rangeSuffix = typeof args?.offset === "number" && typeof args?.limit === "number" && args.offset > 0 && args.limit > 0
|
|
611
|
-
? `:${args.offset}-${args.offset + args.limit - 1}`
|
|
612
|
-
: "";
|
|
613
|
-
let text = renderToolLabel(theme, "read");
|
|
614
|
-
if (filePath) {
|
|
615
|
-
text += ` ${linkToolPath(theme.fg("accent", `${filePath}${rangeSuffix}`), filePath, cwd)}`;
|
|
616
|
-
} else {
|
|
617
|
-
text += ` ${theme.fg("toolOutput", "...")}`;
|
|
618
|
-
}
|
|
619
|
-
if (!rangeSuffix && suffix) text += ` ${theme.fg("dim", suffix)}`;
|
|
620
|
-
return new Text(clampLineToWidth(text, context.width), 0, 0);
|
|
621
|
-
},
|
|
622
|
-
renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
|
|
623
|
-
const { isPartial, isError, expanded, width } = resolveRenderResultContext(options, rest);
|
|
624
|
-
if (isPartial) return renderPendingResult("pending read", width, theme);
|
|
625
|
-
|
|
626
|
-
const content = result.content?.[0];
|
|
627
|
-
const textContent = content?.type === "text" ? content.text : "";
|
|
628
|
-
if (isError || result.isError) {
|
|
629
|
-
const firstLine = textContent.split("\n")[0] || "Error";
|
|
630
|
-
const errorText = expanded ? (textContent || firstLine) : firstLine;
|
|
631
|
-
return new Text(clampLinesToWidth([summaryLine(errorText, { theme, style: "error" })], width).join("\n"), 0, 0);
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
const readSeekValue = (result.details as any)?.readSeekValue as { range: { startLine: number; endLine: number; totalLines: number }; truncation: any; symbol: any; map: any; warnings: ReadSeekWarning[] } | undefined;
|
|
635
|
-
if (!readSeekValue) {
|
|
636
|
-
const lines = textContent.split("\n").filter(Boolean).length || textContent.split("\n").length;
|
|
637
|
-
return new Text(
|
|
638
|
-
summaryLine(`loaded ${lines} ${lines === 1 ? "line" : "lines"}`, {
|
|
639
|
-
hidden: !!textContent && !expanded,
|
|
640
|
-
theme,
|
|
641
|
-
style: "success",
|
|
642
|
-
}),
|
|
643
|
-
0,
|
|
644
|
-
0,
|
|
645
|
-
);
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
const info = formatReadResultText({ range: readSeekValue.range, truncation: readSeekValue.truncation, symbol: readSeekValue.symbol, map: readSeekValue.map, warnings: readSeekValue.warnings });
|
|
649
|
-
const visibleLines = info.truncated && readSeekValue.truncation ? readSeekValue.truncation.outputLines : readSeekValue.range.endLine - readSeekValue.range.startLine + 1;
|
|
650
|
-
const loadedWord = visibleLines === 1 ? "line" : "lines";
|
|
651
|
-
const summaryParts: string[] = [info.truncated ? `loaded ${visibleLines} of ${readSeekValue.truncation?.totalLines ?? readSeekValue.range.totalLines} ${loadedWord} (truncated)` : `loaded ${visibleLines} ${loadedWord}`];
|
|
652
|
-
if (info.symbolBadge) summaryParts.push(info.symbolBadge);
|
|
653
|
-
for (const badge of info.badges) summaryParts.push(badge);
|
|
654
|
-
const summary = summaryParts.join(" • ");
|
|
655
|
-
let text = summaryLine(summary, { hidden: !!textContent && !expanded, theme, style: "success" });
|
|
656
|
-
if (expanded && textContent) text += "\n" + wrapReadHashlinesForWidthCached(content, textContent, width);
|
|
657
|
-
return new Text(clampLinesToWidth(text.split("\n"), width).join("\n"), 0, 0);
|
|
658
|
-
},
|
|
659
|
-
});
|
|
660
|
-
return tool;
|
|
661
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Constants for thresholds.
|
|
3
|
-
*/
|
|
4
|
-
export const THRESHOLDS = {
|
|
5
|
-
/** Maximum map size in bytes */
|
|
6
|
-
MAX_MAP_BYTES: 25 * 1024,
|
|
7
|
-
/** Target size for full detail */
|
|
8
|
-
FULL_TARGET_BYTES: 10 * 1024,
|
|
9
|
-
/** Target size for compact detail */
|
|
10
|
-
COMPACT_TARGET_BYTES: 20 * 1024,
|
|
11
|
-
/** Maximum size for outline level */
|
|
12
|
-
MAX_OUTLINE_BYTES: 50 * 1024,
|
|
13
|
-
/** Maximum size for truncated level (hard cap) */
|
|
14
|
-
MAX_TRUNCATED_BYTES: 100 * 1024,
|
|
15
|
-
/** Number of symbols to show at each end for truncated outline */
|
|
16
|
-
TRUNCATED_SYMBOLS_EACH: 50,
|
|
17
|
-
} as const;
|