opencode-readseek 0.8.21 → 0.8.22
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 +124 -32
- package/package.json +2 -2
package/index.ts
CHANGED
|
@@ -27,15 +27,15 @@ const PREFERRED_TOOL_DESCRIPTIONS: Record<string, string> = {
|
|
|
27
27
|
readseek_read: "Preferred file reader; returns LINE:HASH anchors for durable references.",
|
|
28
28
|
readseek_edit: "Preferred tool for editing existing text files with fresh LINE:HASH anchors.",
|
|
29
29
|
readseek_write: "Preferred tool for creating or replacing a complete text file; returns LINE:HASH anchors.",
|
|
30
|
-
readseek_rename: "
|
|
30
|
+
readseek_rename: "Use to rename a symbol with binding-aware, verified edits.",
|
|
31
31
|
readseek_grep: "Preferred text/regex search; returns edit-ready LINE:HASH anchors.",
|
|
32
|
-
readseek_search: "
|
|
33
|
-
readseek_map: "
|
|
34
|
-
readseek_def: "
|
|
35
|
-
readseek_refs: "
|
|
36
|
-
readseek_hover: "
|
|
37
|
-
readseek_view: "
|
|
38
|
-
readseek_check: "
|
|
32
|
+
readseek_search: "Use for AST or structural code search with ast-grep-style patterns.",
|
|
33
|
+
readseek_map: "Use to inspect a source file's symbol outline without reading the whole file.",
|
|
34
|
+
readseek_def: "Use for go-to-definition or declaration lookup by name.",
|
|
35
|
+
readseek_refs: "Use to find identifier-name matches or binding-specific references before rename or delete.",
|
|
36
|
+
readseek_hover: "Use to inspect symbol identity at a source location.",
|
|
37
|
+
readseek_view: "Use to inspect PDF structure or selected content.",
|
|
38
|
+
readseek_check: "Use for a quick parser syntax check after source edits; unsupported languages are not validated.",
|
|
39
39
|
};
|
|
40
40
|
const FALLBACK_TOOL_DESCRIPTIONS: Record<string, string> = {
|
|
41
41
|
read: "Fallback only. Prefer readseek_read so edits can use LINE:HASH anchors.",
|
|
@@ -316,6 +316,39 @@ function readSeekBinaryPath(): string {
|
|
|
316
316
|
return path.join(path.dirname(packageJson), "bin", process.platform === "win32" ? "readseek.exe" : "readseek");
|
|
317
317
|
}
|
|
318
318
|
|
|
319
|
+
async function readBoundedText(
|
|
320
|
+
stream: ReadableStream<Uint8Array>,
|
|
321
|
+
streamName: "stdout" | "stderr",
|
|
322
|
+
onOverflow: () => void,
|
|
323
|
+
): Promise<string> {
|
|
324
|
+
const reader = stream.getReader();
|
|
325
|
+
const chunks: Uint8Array[] = [];
|
|
326
|
+
let totalBytes = 0;
|
|
327
|
+
try {
|
|
328
|
+
while (true) {
|
|
329
|
+
const { done, value } = await reader.read();
|
|
330
|
+
if (done) break;
|
|
331
|
+
totalBytes += value.byteLength;
|
|
332
|
+
if (totalBytes > MAX_OUTPUT_BYTES) {
|
|
333
|
+
onOverflow();
|
|
334
|
+
void reader.cancel().catch(() => undefined);
|
|
335
|
+
throw new Error(`readseek ${streamName} exceeded the ${MAX_OUTPUT_BYTES}-byte output limit`);
|
|
336
|
+
}
|
|
337
|
+
chunks.push(value);
|
|
338
|
+
}
|
|
339
|
+
} finally {
|
|
340
|
+
reader.releaseLock();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const bytes = new Uint8Array(totalBytes);
|
|
344
|
+
let offset = 0;
|
|
345
|
+
for (const chunk of chunks) {
|
|
346
|
+
bytes.set(chunk, offset);
|
|
347
|
+
offset += chunk.byteLength;
|
|
348
|
+
}
|
|
349
|
+
return new TextDecoder().decode(bytes);
|
|
350
|
+
}
|
|
351
|
+
|
|
319
352
|
async function runReadSeekRaw(
|
|
320
353
|
context: ToolContext,
|
|
321
354
|
args: string[],
|
|
@@ -325,7 +358,6 @@ async function runReadSeekRaw(
|
|
|
325
358
|
const cancelable = options.cancelable !== false;
|
|
326
359
|
const spawnOptions = {
|
|
327
360
|
cwd: context.directory,
|
|
328
|
-
maxBuffer: MAX_OUTPUT_BYTES,
|
|
329
361
|
stderr: "pipe" as const,
|
|
330
362
|
stdout: "pipe" as const,
|
|
331
363
|
};
|
|
@@ -335,9 +367,19 @@ async function runReadSeekRaw(
|
|
|
335
367
|
? { ...spawnOptions, killSignal: "SIGKILL" as const, signal: context.abort }
|
|
336
368
|
: spawnOptions,
|
|
337
369
|
);
|
|
370
|
+
let killedForOverflow = false;
|
|
371
|
+
const killForOverflow = () => {
|
|
372
|
+
if (killedForOverflow) return;
|
|
373
|
+
killedForOverflow = true;
|
|
374
|
+
try {
|
|
375
|
+
child.kill("SIGKILL");
|
|
376
|
+
} catch {
|
|
377
|
+
// Preserve the output-limit error if the process exited concurrently.
|
|
378
|
+
}
|
|
379
|
+
};
|
|
338
380
|
const [stdout, stderr, exitCode] = await Promise.all([
|
|
339
|
-
|
|
340
|
-
|
|
381
|
+
readBoundedText(child.stdout, "stdout", killForOverflow),
|
|
382
|
+
readBoundedText(child.stderr, "stderr", killForOverflow),
|
|
341
383
|
child.exited,
|
|
342
384
|
]);
|
|
343
385
|
if (cancelable) context.abort.throwIfAborted();
|
|
@@ -358,6 +400,25 @@ async function runReadSeek(
|
|
|
358
400
|
}
|
|
359
401
|
}
|
|
360
402
|
|
|
403
|
+
async function postMutationPreview(
|
|
404
|
+
context: ToolContext,
|
|
405
|
+
args: string[],
|
|
406
|
+
filePath: string,
|
|
407
|
+
changed: boolean,
|
|
408
|
+
): Promise<unknown> {
|
|
409
|
+
try {
|
|
410
|
+
return await runReadSeek(context, args, { cancelable: false });
|
|
411
|
+
} catch (error) {
|
|
412
|
+
const previewError = error instanceof Error ? error.message : String(error);
|
|
413
|
+
return {
|
|
414
|
+
file: filePath,
|
|
415
|
+
changed,
|
|
416
|
+
preview_error: previewError,
|
|
417
|
+
warning: "File mutation completed, but the post-write preview failed. Do not retry the mutation; read the file to verify content and refresh anchors.",
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
361
422
|
let readSeekCacheInit: Promise<string> | undefined;
|
|
362
423
|
|
|
363
424
|
function readSeekCacheDir(): string {
|
|
@@ -954,8 +1015,10 @@ function resultPresentation(kind: PresentationKind, args: any, value: unknown):
|
|
|
954
1015
|
return { title, metadata: { edits, conflicts, others } };
|
|
955
1016
|
}
|
|
956
1017
|
case "edit":
|
|
957
|
-
case "write":
|
|
958
|
-
|
|
1018
|
+
case "write": {
|
|
1019
|
+
const preview = typeof output.preview_error === "string" ? " (preview unavailable)" : "";
|
|
1020
|
+
return { title: `${kind === "edit" ? "Edited" : "Wrote"} ${args.path}${preview}`, metadata: {} };
|
|
1021
|
+
}
|
|
959
1022
|
case "check": {
|
|
960
1023
|
const errors = typeof output.error_count === "number" ? output.error_count : 0;
|
|
961
1024
|
const missing = typeof output.missing_count === "number" ? output.missing_count : 0;
|
|
@@ -1064,7 +1127,7 @@ export const ReadSeekPlugin: Plugin = async (_input, options) => {
|
|
|
1064
1127
|
formatReadResult,
|
|
1065
1128
|
),
|
|
1066
1129
|
readseek_view: readseekTool(
|
|
1067
|
-
"
|
|
1130
|
+
"Inspect a PDF's structure or selected content. Start with the overview, then narrow by page, node, kind, or depth instead of reading the whole document.",
|
|
1068
1131
|
{
|
|
1069
1132
|
path: tool.schema.string().describe("PDF path relative to the project directory"),
|
|
1070
1133
|
node: tool.schema.string().min(1).optional().describe("Node ID to use as the view root"),
|
|
@@ -1089,7 +1152,7 @@ export const ReadSeekPlugin: Plugin = async (_input, options) => {
|
|
|
1089
1152
|
(result) => ({ output: boundDocumentOutput(String(result)) }),
|
|
1090
1153
|
),
|
|
1091
1154
|
readseek_map: readseekTool(
|
|
1092
|
-
"
|
|
1155
|
+
"Inspect a source file's symbol outline. Use this instead of reading the whole file when only declarations, names, kinds, and ranges are needed.",
|
|
1093
1156
|
{
|
|
1094
1157
|
path: tool.schema.string().describe("Path relative to the project directory"),
|
|
1095
1158
|
language: tool.schema.string().optional().describe("Language override when auto-detection is ambiguous"),
|
|
@@ -1141,32 +1204,55 @@ export const ReadSeekPlugin: Plugin = async (_input, options) => {
|
|
|
1141
1204
|
const contextLines = (input.context as number | undefined) ?? 0;
|
|
1142
1205
|
const results: { file: string; matches: unknown[]; hashlines: unknown[] }[] = [];
|
|
1143
1206
|
let totalMatches = 0;
|
|
1207
|
+
let truncated = false;
|
|
1144
1208
|
for (const filePath of files.sort()) {
|
|
1145
1209
|
context.abort.throwIfAborted();
|
|
1146
|
-
if (
|
|
1210
|
+
if (truncated) break;
|
|
1147
1211
|
const buffer = await readFile(filePath);
|
|
1148
1212
|
if (buffer.includes(0)) continue;
|
|
1149
1213
|
const lines = buffer.toString("utf8").replace(/\r\n/g, "\n").split("\n");
|
|
1214
|
+
const matchLines = new Set<number>();
|
|
1150
1215
|
const ranges: [number, number][] = [];
|
|
1151
|
-
for (let index = 0; index < lines.length
|
|
1216
|
+
for (let index = 0; index < lines.length; index++) {
|
|
1152
1217
|
expression.lastIndex = 0;
|
|
1153
1218
|
if (!expression.test(lines[index] ?? "")) continue;
|
|
1154
|
-
|
|
1219
|
+
if (totalMatches === maxMatches) {
|
|
1220
|
+
truncated = true;
|
|
1221
|
+
break;
|
|
1222
|
+
}
|
|
1223
|
+
const line = index + 1;
|
|
1224
|
+
matchLines.add(line);
|
|
1225
|
+
ranges.push([Math.max(1, line - contextLines), Math.min(lines.length, line + contextLines)]);
|
|
1155
1226
|
totalMatches++;
|
|
1156
1227
|
}
|
|
1157
1228
|
if (ranges.length === 0) continue;
|
|
1158
|
-
|
|
1229
|
+
|
|
1230
|
+
const mergedRanges: [number, number][] = [];
|
|
1159
1231
|
for (const [start, end] of ranges) {
|
|
1232
|
+
const previous = mergedRanges.at(-1);
|
|
1233
|
+
if (previous && start <= previous[1] + 1) previous[1] = Math.max(previous[1], end);
|
|
1234
|
+
else mergedRanges.push([start, end]);
|
|
1235
|
+
}
|
|
1236
|
+
const linesByNumber = new Map<number, unknown>();
|
|
1237
|
+
for (const [start, end] of mergedRanges) {
|
|
1160
1238
|
const output = record(await runReadSeek(context, ["read", `${filePath}:${start}`, "--end", String(end)]));
|
|
1161
|
-
|
|
1239
|
+
for (const hashline of items(output.hashlines)) {
|
|
1240
|
+
const line = record(hashline).line;
|
|
1241
|
+
if (typeof line === "number" && !linesByNumber.has(line)) linesByNumber.set(line, hashline);
|
|
1242
|
+
}
|
|
1162
1243
|
}
|
|
1163
|
-
|
|
1244
|
+
const hashlines = [...linesByNumber.values()];
|
|
1245
|
+
const matches = hashlines.filter((hashline) => {
|
|
1246
|
+
const line = record(hashline).line;
|
|
1247
|
+
return typeof line === "number" && matchLines.has(line);
|
|
1248
|
+
});
|
|
1249
|
+
results.push({ file: filePath, matches, hashlines });
|
|
1164
1250
|
}
|
|
1165
|
-
return { results, truncated
|
|
1251
|
+
return { results, truncated };
|
|
1166
1252
|
},
|
|
1167
1253
|
),
|
|
1168
1254
|
readseek_search: readseekTool(
|
|
1169
|
-
"Search AST
|
|
1255
|
+
"Search code by AST structure using ast-grep-style patterns. Results include edit-ready LINE:HASH anchors. Use readseek_grep for identifiers, strings, comments, configuration, documentation, or other plain text and regex.",
|
|
1170
1256
|
{
|
|
1171
1257
|
pattern: tool.schema.string().describe("AST pattern (ast-grep style), such as console.log($$$ARGS)"),
|
|
1172
1258
|
path: tool.schema.string().optional().describe("File or directory, defaulting to the project directory"),
|
|
@@ -1187,7 +1273,7 @@ export const ReadSeekPlugin: Plugin = async (_input, options) => {
|
|
|
1187
1273
|
},
|
|
1188
1274
|
),
|
|
1189
1275
|
readseek_def: readseekTool(
|
|
1190
|
-
"
|
|
1276
|
+
"Go to a symbol definition or declaration by name. Accepts qualified or unqualified names and returns LINE:HASH anchors; use this instead of text search for functions, methods, classes, types, and other declarations.",
|
|
1191
1277
|
{
|
|
1192
1278
|
name: tool.schema.string().describe("Symbol name"),
|
|
1193
1279
|
path: tool.schema.string().optional().describe("File or directory, defaulting to the project directory"),
|
|
@@ -1207,7 +1293,7 @@ export const ReadSeekPlugin: Plugin = async (_input, options) => {
|
|
|
1207
1293
|
},
|
|
1208
1294
|
),
|
|
1209
1295
|
readseek_refs: readseekTool(
|
|
1210
|
-
"Find
|
|
1296
|
+
"Find references or usages of an identifier by name or cursor binding. Results include enclosing symbols and edit-ready LINE:HASH anchors. Without cursor scope, results are identifier-name matches; with scope, they follow one binding and exclude shadows. Use text search for literal occurrences in strings or comments, and use this tool before renaming or deleting a symbol.",
|
|
1211
1297
|
{
|
|
1212
1298
|
name: tool.schema.string().describe("Identifier name"),
|
|
1213
1299
|
path: tool.schema.string().optional().describe("File or directory, defaulting to the project directory"),
|
|
@@ -1237,7 +1323,7 @@ export const ReadSeekPlugin: Plugin = async (_input, options) => {
|
|
|
1237
1323
|
},
|
|
1238
1324
|
),
|
|
1239
1325
|
readseek_hover: readseekTool(
|
|
1240
|
-
"
|
|
1326
|
+
"Inspect the symbol at a source location. This identifies the token and enclosing symbol; use it before definition lookup or binding-specific references.",
|
|
1241
1327
|
{
|
|
1242
1328
|
path: tool.schema.string().describe("Path relative to the project directory"),
|
|
1243
1329
|
line: tool.schema.number().int().positive().describe("One-based cursor line"),
|
|
@@ -1255,14 +1341,14 @@ export const ReadSeekPlugin: Plugin = async (_input, options) => {
|
|
|
1255
1341
|
},
|
|
1256
1342
|
),
|
|
1257
1343
|
readseek_rename: readseekTool(
|
|
1258
|
-
"Rename
|
|
1344
|
+
"Rename a symbol at a source location. Resolvable same-named bindings are preserved; verified edits apply by default. Set `apply` to false to preview the plan before changing files.",
|
|
1259
1345
|
{
|
|
1260
1346
|
path: tool.schema.string().describe("Path relative to the project directory"),
|
|
1261
1347
|
line: tool.schema.number().int().positive().describe("One-based cursor line of the symbol to rename"),
|
|
1262
1348
|
column: tool.schema.number().int().positive().optional().describe("One-based cursor byte column for disambiguation"),
|
|
1263
1349
|
to: tool.schema.string().min(1).describe("New symbol name"),
|
|
1264
1350
|
workspace: tool.schema.boolean().optional().describe("Include project-wide occurrences"),
|
|
1265
|
-
apply: tool.schema.boolean().optional().describe("Apply verified edits
|
|
1351
|
+
apply: tool.schema.boolean().optional().describe("Apply verified edits; defaults to true"),
|
|
1266
1352
|
},
|
|
1267
1353
|
"rename",
|
|
1268
1354
|
async (input, context) => {
|
|
@@ -1326,10 +1412,11 @@ export const ReadSeekPlugin: Plugin = async (_input, options) => {
|
|
|
1326
1412
|
if (before !== after) await writeText(filePath, { exists: true, content: before }, after);
|
|
1327
1413
|
const firstEditedLine = Math.min(...edits.flatMap((edit) => anchorRefs(edit).map((ref) => ref.line)));
|
|
1328
1414
|
const previewStart = Math.max(1, firstEditedLine - 3);
|
|
1329
|
-
return
|
|
1415
|
+
return postMutationPreview(
|
|
1330
1416
|
context,
|
|
1331
1417
|
["read", `${filePath}:${previewStart}`, "--end", String(previewStart + EDIT_RESULT_READ_LIMIT - 1)],
|
|
1332
|
-
|
|
1418
|
+
filePath,
|
|
1419
|
+
before !== after,
|
|
1333
1420
|
);
|
|
1334
1421
|
});
|
|
1335
1422
|
},
|
|
@@ -1353,12 +1440,17 @@ export const ReadSeekPlugin: Plugin = async (_input, options) => {
|
|
|
1353
1440
|
await authorizeEdit(context, [filePath], simpleDiff(filePath, before.content, content), false);
|
|
1354
1441
|
context.abort.throwIfAborted();
|
|
1355
1442
|
await writeText(filePath, before, content);
|
|
1356
|
-
return
|
|
1443
|
+
return postMutationPreview(
|
|
1444
|
+
context,
|
|
1445
|
+
["read", filePath, "--end", String(DEFAULT_READ_LIMIT)],
|
|
1446
|
+
filePath,
|
|
1447
|
+
!before.exists || before.content !== content,
|
|
1448
|
+
);
|
|
1357
1449
|
});
|
|
1358
1450
|
},
|
|
1359
1451
|
),
|
|
1360
1452
|
readseek_check: readseekTool(
|
|
1361
|
-
"
|
|
1453
|
+
"Check a source file for parser errors or missing syntax after edits. Validation requires a supported tree-sitter language; files without a supported parser are not checked. This is not a compiler, type checker, test runner, or linter.",
|
|
1362
1454
|
{
|
|
1363
1455
|
path: tool.schema.string().describe("Path relative to the project directory"),
|
|
1364
1456
|
language: tool.schema.string().optional().describe("Language override when auto-detection is ambiguous"),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-readseek",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.22",
|
|
4
4
|
"description": "OpenCode plugin for anchored file operations and structural code navigation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.ts",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"node": ">=20.0.0"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@jarkkojs/readseek": "^0.8.
|
|
34
|
+
"@jarkkojs/readseek": "^0.8.22",
|
|
35
35
|
"@opencode-ai/plugin": "^1.17.20"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|