opencode-readseek 0.5.19 → 0.6.0
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 +6 -2
- package/index.ts +309 -37
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Add the plugin to `opencode.json`:
|
|
|
10
10
|
|
|
11
11
|
```json
|
|
12
12
|
{
|
|
13
|
-
"plugin": ["opencode-readseek"]
|
|
13
|
+
"plugin": [["opencode-readseek", { "imageMode": "auto" }]]
|
|
14
14
|
}
|
|
15
15
|
```
|
|
16
16
|
|
|
@@ -19,7 +19,7 @@ binary dependency with Bun at startup.
|
|
|
19
19
|
|
|
20
20
|
## Tools
|
|
21
21
|
|
|
22
|
-
- `readseek_read`: read text with `LINE:HASH` anchors.
|
|
22
|
+
- `readseek_read`: read text with `LINE:HASH` anchors; image/PDF handling is explicit.
|
|
23
23
|
- `readseek_map`: generate a structural symbol map.
|
|
24
24
|
- `readseek_search`: AST-pattern search.
|
|
25
25
|
- `readseek_def`, `readseek_refs`, `readseek_hover`: symbol navigation.
|
|
@@ -31,6 +31,10 @@ records anchors from successful ReadSeek tool results, refuses any attempt to
|
|
|
31
31
|
apply a rename directly, and adds current anchors plus a pending rename plan to
|
|
32
32
|
the OpenCode compaction context.
|
|
33
33
|
|
|
34
|
+
`imageMode` defaults to `"auto"`: it exposes `none`, `ocr`, `caption`, and
|
|
35
|
+
`objects`. `"on"` omits `none`; `"off"` skips image/PDF files. Omitting
|
|
36
|
+
`image` also skips visual files.
|
|
37
|
+
|
|
34
38
|
## Licensing
|
|
35
39
|
|
|
36
40
|
This package is Apache-2.0. `@jarkkojs/readseek` is licensed separately as
|
package/index.ts
CHANGED
|
@@ -1,17 +1,47 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// Copyright (C) Jarkko Sakkinen 2026
|
|
3
3
|
|
|
4
|
+
import { stat } from "node:fs/promises";
|
|
4
5
|
import { createRequire } from "node:module";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
|
|
7
|
-
import { tool, type Plugin } from "@opencode-ai/plugin";
|
|
8
|
+
import { tool, type Plugin, type PluginOptions, type ToolContext } from "@opencode-ai/plugin";
|
|
8
9
|
|
|
9
10
|
const require = createRequire(import.meta.url);
|
|
10
11
|
const readseekScript = require.resolve("@jarkkojs/readseek/bin/readseek.js");
|
|
12
|
+
const MAX_OUTPUT_BYTES = 32 * 1024 * 1024;
|
|
13
|
+
|
|
14
|
+
type RenamePlan = {
|
|
15
|
+
summary: string;
|
|
16
|
+
files: Set<string>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type PresentationKind = "read" | "map" | "search" | "def" | "refs" | "hover" | "rename" | "check";
|
|
20
|
+
|
|
21
|
+
type Presentation = {
|
|
22
|
+
title: string;
|
|
23
|
+
metadata: Record<string, number>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type ImagePolicy = "on" | "auto" | "off";
|
|
27
|
+
type ImageMode = "none" | "ocr" | "caption" | "objects";
|
|
28
|
+
|
|
29
|
+
function resolveImagePolicy(options: PluginOptions | undefined): ImagePolicy {
|
|
30
|
+
const value = options?.imageMode;
|
|
31
|
+
if (value === undefined) return "auto";
|
|
32
|
+
if (value === "on" || value === "auto" || value === "off") return value;
|
|
33
|
+
throw new Error('opencode-readseek imageMode must be "on", "auto", or "off"');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isVisualFile(value: unknown): boolean {
|
|
37
|
+
const output = record(value);
|
|
38
|
+
const type = output.type;
|
|
39
|
+
return typeof output.width === "number" || type === "application/pdf";
|
|
40
|
+
}
|
|
11
41
|
|
|
12
42
|
class SessionAnchors {
|
|
13
43
|
#pathsBySession = new Map<string, Set<string>>();
|
|
14
|
-
#renamePlans = new Map<string,
|
|
44
|
+
#renamePlans = new Map<string, RenamePlan>();
|
|
15
45
|
|
|
16
46
|
mark(sessionID: string, filePath: string): void {
|
|
17
47
|
let paths = this.#pathsBySession.get(sessionID);
|
|
@@ -23,15 +53,33 @@ class SessionAnchors {
|
|
|
23
53
|
}
|
|
24
54
|
|
|
25
55
|
forget(filePath: string): void {
|
|
26
|
-
|
|
56
|
+
const absolutePath = path.resolve(filePath);
|
|
57
|
+
for (const paths of this.#pathsBySession.values()) paths.delete(absolutePath);
|
|
58
|
+
for (const [sessionID, plan] of this.#renamePlans) {
|
|
59
|
+
if (plan.files.has(absolutePath)) this.#renamePlans.delete(sessionID);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
deleteSession(sessionID: string): void {
|
|
64
|
+
this.#pathsBySession.delete(sessionID);
|
|
65
|
+
this.#renamePlans.delete(sessionID);
|
|
27
66
|
}
|
|
28
67
|
|
|
29
68
|
planRename(sessionID: string, output: unknown): void {
|
|
30
69
|
if (!output || typeof output !== "object") return;
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
70
|
+
const record = output as Record<string, unknown>;
|
|
71
|
+
const { file, old_name: oldName, new_name: newName, others } = record;
|
|
72
|
+
if (typeof file !== "string" || typeof oldName !== "string" || typeof newName !== "string") return;
|
|
73
|
+
|
|
74
|
+
const files = new Set([path.resolve(file)]);
|
|
75
|
+
if (Array.isArray(others)) {
|
|
76
|
+
for (const item of others) {
|
|
77
|
+
if (!item || typeof item !== "object") continue;
|
|
78
|
+
const otherFile = (item as Record<string, unknown>).file;
|
|
79
|
+
if (typeof otherFile === "string") files.add(path.resolve(otherFile));
|
|
80
|
+
}
|
|
34
81
|
}
|
|
82
|
+
this.#renamePlans.set(sessionID, { summary: `${oldName} -> ${newName}`, files });
|
|
35
83
|
}
|
|
36
84
|
|
|
37
85
|
render(sessionID: string): string | undefined {
|
|
@@ -44,7 +92,7 @@ class SessionAnchors {
|
|
|
44
92
|
.join("\n")}`);
|
|
45
93
|
}
|
|
46
94
|
const renamePlan = this.#renamePlans.get(sessionID);
|
|
47
|
-
if (renamePlan) sections.push(`## Pending ReadSeek Rename Plan\n- ${renamePlan}`);
|
|
95
|
+
if (renamePlan) sections.push(`## Pending ReadSeek Rename Plan\n- ${renamePlan.summary}`);
|
|
48
96
|
return sections.length === 0 ? undefined : sections.join("\n\n");
|
|
49
97
|
}
|
|
50
98
|
}
|
|
@@ -53,13 +101,58 @@ function resolvePath(directory: string, filePath: string): string {
|
|
|
53
101
|
return path.resolve(directory, filePath);
|
|
54
102
|
}
|
|
55
103
|
|
|
104
|
+
function containsPath(directory: string, filePath: string): boolean {
|
|
105
|
+
const relative = path.relative(directory, filePath);
|
|
106
|
+
return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function authorizeExternal(context: ToolContext, filePath: string): Promise<void> {
|
|
110
|
+
if (containsPath(context.directory, filePath) || (context.worktree !== "/" && containsPath(context.worktree, filePath))) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const info = await stat(filePath).catch(() => undefined);
|
|
115
|
+
const parentDir = info?.isDirectory() ? filePath : path.dirname(filePath);
|
|
116
|
+
const pattern = path.join(parentDir, "*").replaceAll("\\", "/");
|
|
117
|
+
await context.ask({
|
|
118
|
+
permission: "external_directory",
|
|
119
|
+
patterns: [pattern],
|
|
120
|
+
always: [pattern],
|
|
121
|
+
metadata: { filepath: filePath, parentDir },
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function authorizeRead(context: ToolContext, filePath: string): Promise<void> {
|
|
126
|
+
await authorizeExternal(context, filePath);
|
|
127
|
+
await context.ask({
|
|
128
|
+
permission: "read",
|
|
129
|
+
patterns: [path.relative(context.worktree, filePath).replaceAll("\\", "/")],
|
|
130
|
+
always: ["*"],
|
|
131
|
+
metadata: {},
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function authorizeSearch(context: ToolContext, filePath: string, pattern: string): Promise<void> {
|
|
136
|
+
await context.ask({
|
|
137
|
+
permission: "grep",
|
|
138
|
+
patterns: [pattern],
|
|
139
|
+
always: ["*"],
|
|
140
|
+
metadata: { pattern, path: filePath },
|
|
141
|
+
});
|
|
142
|
+
await authorizeExternal(context, filePath);
|
|
143
|
+
}
|
|
144
|
+
|
|
56
145
|
function optionalFlag(args: string[], enabled: boolean | undefined, flag: string): void {
|
|
57
146
|
if (enabled) args.push(flag);
|
|
58
147
|
}
|
|
59
148
|
|
|
60
|
-
async function runReadSeek(
|
|
149
|
+
async function runReadSeek(context: ToolContext, args: string[]): Promise<unknown> {
|
|
150
|
+
context.abort.throwIfAborted();
|
|
61
151
|
const child = Bun.spawn([process.execPath, readseekScript, ...args], {
|
|
62
|
-
cwd: directory,
|
|
152
|
+
cwd: context.directory,
|
|
153
|
+
killSignal: "SIGKILL",
|
|
154
|
+
maxBuffer: MAX_OUTPUT_BYTES,
|
|
155
|
+
signal: context.abort,
|
|
63
156
|
stderr: "pipe",
|
|
64
157
|
stdout: "pipe",
|
|
65
158
|
});
|
|
@@ -68,6 +161,7 @@ async function runReadSeek(directory: string, args: string[]): Promise<unknown>
|
|
|
68
161
|
new Response(child.stderr).text(),
|
|
69
162
|
child.exited,
|
|
70
163
|
]);
|
|
164
|
+
context.abort.throwIfAborted();
|
|
71
165
|
if (exitCode !== 0) throw new Error(stderr.trim() || `readseek exited with status ${exitCode}`);
|
|
72
166
|
|
|
73
167
|
try {
|
|
@@ -94,16 +188,131 @@ function collectFiles(value: unknown, files: Set<string>): void {
|
|
|
94
188
|
}
|
|
95
189
|
}
|
|
96
190
|
|
|
191
|
+
function identifiedName(value: unknown): string | undefined {
|
|
192
|
+
if (!value || typeof value !== "object") return undefined;
|
|
193
|
+
const identifier = (value as Record<string, unknown>).identifier;
|
|
194
|
+
if (!identifier || typeof identifier !== "object") return undefined;
|
|
195
|
+
const text = (identifier as Record<string, unknown>).text;
|
|
196
|
+
return typeof text === "string" ? text : undefined;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function record(value: unknown): Record<string, unknown> {
|
|
200
|
+
return value && typeof value === "object" ? (value as Record<string, unknown>) : {};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function items(value: unknown): unknown[] {
|
|
204
|
+
return Array.isArray(value) ? value : [];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function initialTitle(kind: PresentationKind, args: any): string {
|
|
208
|
+
switch (kind) {
|
|
209
|
+
case "read":
|
|
210
|
+
return `Read ${args.path}`;
|
|
211
|
+
case "map":
|
|
212
|
+
return `Map ${args.path}`;
|
|
213
|
+
case "search":
|
|
214
|
+
return `Search ${args.pattern}`;
|
|
215
|
+
case "def":
|
|
216
|
+
return `Find definition ${args.name}`;
|
|
217
|
+
case "refs":
|
|
218
|
+
return `Find references to ${args.name}`;
|
|
219
|
+
case "hover":
|
|
220
|
+
return `Identify ${args.path}:${args.line}`;
|
|
221
|
+
case "rename":
|
|
222
|
+
return `Rename ${args.path}:${args.line} to ${args.to}`;
|
|
223
|
+
case "check":
|
|
224
|
+
return `Check ${args.path}`;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function resultPresentation(kind: PresentationKind, args: any, value: unknown): Presentation {
|
|
229
|
+
const output = record(value);
|
|
230
|
+
switch (kind) {
|
|
231
|
+
case "read": {
|
|
232
|
+
const startLine = output.start_line;
|
|
233
|
+
const endLine = output.end_line;
|
|
234
|
+
if (typeof startLine === "number" && typeof endLine === "number") {
|
|
235
|
+
return {
|
|
236
|
+
title: `Read ${args.path}:${startLine}-${endLine}`,
|
|
237
|
+
metadata: { start_line: startLine, end_line: endLine, line_count: endLine - startLine + 1 },
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
const width = output.width;
|
|
241
|
+
const height = output.height;
|
|
242
|
+
if (typeof width === "number" && typeof height === "number") {
|
|
243
|
+
return { title: `Read ${args.path} (${width}x${height})`, metadata: { width, height } };
|
|
244
|
+
}
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
case "map": {
|
|
248
|
+
const symbols = items(output.symbols).length;
|
|
249
|
+
return { title: `Mapped ${args.path} (${symbols} symbols)`, metadata: { symbols } };
|
|
250
|
+
}
|
|
251
|
+
case "search": {
|
|
252
|
+
const results = items(output.results);
|
|
253
|
+
const matches = results.reduce<number>((total, result) => total + items(record(result).matches).length, 0);
|
|
254
|
+
return { title: `Found ${matches} matches`, metadata: { results: results.length, matches } };
|
|
255
|
+
}
|
|
256
|
+
case "def": {
|
|
257
|
+
const locations = items(output.locations).length;
|
|
258
|
+
return { title: `Found ${locations} definitions`, metadata: { locations } };
|
|
259
|
+
}
|
|
260
|
+
case "refs": {
|
|
261
|
+
const references = items(output.references).length;
|
|
262
|
+
return { title: `Found ${references} references`, metadata: { references } };
|
|
263
|
+
}
|
|
264
|
+
case "hover": {
|
|
265
|
+
const identifier = record(output.identifier).text;
|
|
266
|
+
const line = output.line;
|
|
267
|
+
const column = output.column;
|
|
268
|
+
const metadata: Record<string, number> = {};
|
|
269
|
+
if (typeof line === "number") metadata.line = line;
|
|
270
|
+
if (typeof column === "number") metadata.column = column;
|
|
271
|
+
return { title: typeof identifier === "string" ? `Identified ${identifier}` : initialTitle(kind, args), metadata };
|
|
272
|
+
}
|
|
273
|
+
case "rename": {
|
|
274
|
+
const oldName = output.old_name;
|
|
275
|
+
const newName = output.new_name;
|
|
276
|
+
const otherOutputs = items(output.others).map(record);
|
|
277
|
+
const edits = otherOutputs.reduce((total, item) => total + items(item.edits).length, items(output.edits).length);
|
|
278
|
+
const conflicts = otherOutputs.reduce(
|
|
279
|
+
(total, item) => total + items(item.conflicts).length,
|
|
280
|
+
items(output.conflicts).length,
|
|
281
|
+
);
|
|
282
|
+
const others = otherOutputs.length;
|
|
283
|
+
const title =
|
|
284
|
+
typeof oldName === "string" && typeof newName === "string"
|
|
285
|
+
? `Plan ${oldName} -> ${newName}`
|
|
286
|
+
: initialTitle(kind, args);
|
|
287
|
+
return { title, metadata: { edits, conflicts, others } };
|
|
288
|
+
}
|
|
289
|
+
case "check": {
|
|
290
|
+
const errors = typeof output.error_count === "number" ? output.error_count : 0;
|
|
291
|
+
const missing = typeof output.missing_count === "number" ? output.missing_count : 0;
|
|
292
|
+
return {
|
|
293
|
+
title: `Checked ${args.path} (${errors} errors, ${missing} missing)`,
|
|
294
|
+
metadata: { error_count: errors, missing_count: missing },
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return { title: initialTitle(kind, args), metadata: {} };
|
|
299
|
+
}
|
|
300
|
+
|
|
97
301
|
function readseekTool(
|
|
98
302
|
description: string,
|
|
99
303
|
args: Record<string, any>,
|
|
100
|
-
|
|
304
|
+
kind: PresentationKind,
|
|
305
|
+
execute: (args: any, context: ToolContext) => Promise<unknown>,
|
|
101
306
|
) {
|
|
102
307
|
return tool({
|
|
103
308
|
description,
|
|
104
309
|
args,
|
|
105
310
|
async execute(args, context) {
|
|
106
|
-
|
|
311
|
+
const title = initialTitle(kind, args);
|
|
312
|
+
context.metadata({ title });
|
|
313
|
+
const result = await execute(args, context);
|
|
314
|
+
const presentation = resultPresentation(kind, args, result);
|
|
315
|
+
return { title: presentation.title, output: render(result), metadata: presentation.metadata };
|
|
107
316
|
},
|
|
108
317
|
});
|
|
109
318
|
}
|
|
@@ -112,9 +321,14 @@ function readseekTool(
|
|
|
112
321
|
* Adds readseek's anchored reads and structural navigation without replacing
|
|
113
322
|
* OpenCode's built-in file tools.
|
|
114
323
|
*/
|
|
115
|
-
export const ReadSeekPlugin: Plugin = async () => {
|
|
324
|
+
export const ReadSeekPlugin: Plugin = async (_input, options) => {
|
|
116
325
|
const anchors = new SessionAnchors();
|
|
326
|
+
const imagePolicy = resolveImagePolicy(options);
|
|
327
|
+
const imageModes: readonly ImageMode[] = imagePolicy === "auto"
|
|
328
|
+
? ["none", "ocr", "caption", "objects"]
|
|
329
|
+
: ["ocr", "caption", "objects"];
|
|
117
330
|
const withSearchFlags = (args: string[], input: { cached?: boolean; others?: boolean; ignored?: boolean }) => {
|
|
331
|
+
if (input.ignored && !input.others) throw new Error("ignored requires others");
|
|
118
332
|
optionalFlag(args, input.cached, "--cached");
|
|
119
333
|
optionalFlag(args, input.others, "--others");
|
|
120
334
|
optionalFlag(args, input.ignored, "--ignored");
|
|
@@ -123,23 +337,48 @@ export const ReadSeekPlugin: Plugin = async () => {
|
|
|
123
337
|
return {
|
|
124
338
|
tool: {
|
|
125
339
|
readseek_read: readseekTool(
|
|
126
|
-
|
|
340
|
+
imagePolicy === "off"
|
|
341
|
+
? "Read text with stable LINE:HASH anchors. Image and PDF files are skipped."
|
|
342
|
+
: `Read text with stable LINE:HASH anchors. For images and PDFs, explicitly select image: ${imageModes.join(", ")}; omitting image skips the file.`,
|
|
127
343
|
{
|
|
128
344
|
path: tool.schema.string().describe("Path relative to the project directory"),
|
|
129
345
|
offset: tool.schema.number().int().positive().optional().describe("One-based starting line"),
|
|
130
346
|
limit: tool.schema.number().int().positive().optional().describe("Maximum number of lines"),
|
|
347
|
+
...(imagePolicy === "off"
|
|
348
|
+
? {}
|
|
349
|
+
: {
|
|
350
|
+
image: tool.schema.enum(imageModes as [ImageMode, ...ImageMode[]]).optional()
|
|
351
|
+
.describe(`Image/PDF mode: ${imageModes.join(", ")}. Must be selected explicitly.`),
|
|
352
|
+
}),
|
|
131
353
|
},
|
|
132
|
-
|
|
133
|
-
|
|
354
|
+
"read",
|
|
355
|
+
async (input, context) => {
|
|
356
|
+
const filePath = resolvePath(context.directory, input.path as string);
|
|
357
|
+
await authorizeRead(context, filePath);
|
|
358
|
+
const image = input.image as ImageMode | undefined;
|
|
359
|
+
if (imagePolicy === "off" && image !== undefined) throw new Error("image and PDF reads are disabled");
|
|
360
|
+
if (imagePolicy === "on" && image === "none") throw new Error('image="none" requires imageMode="auto"');
|
|
361
|
+
if (image === undefined) {
|
|
362
|
+
const detection = await runReadSeek(context, ["detect", filePath]);
|
|
363
|
+
if (isVisualFile(detection)) {
|
|
364
|
+
return { file: filePath, skipped: true, reason: "image mode not selected" };
|
|
365
|
+
}
|
|
366
|
+
}
|
|
134
367
|
const args = ["read", input.offset === undefined ? filePath : `${filePath}:${input.offset}`];
|
|
135
|
-
if (input.limit !== undefined) args.push("--end", String((input.offset
|
|
136
|
-
|
|
368
|
+
if (input.limit !== undefined) args.push("--end", String((input.offset ?? 1) + input.limit - 1));
|
|
369
|
+
if (image !== undefined) args.push("--image", image);
|
|
370
|
+
return runReadSeek(context, args);
|
|
137
371
|
},
|
|
138
372
|
),
|
|
139
373
|
readseek_map: readseekTool(
|
|
140
374
|
"Build a structural symbol map for a source file.",
|
|
141
375
|
{ path: tool.schema.string().describe("Path relative to the project directory") },
|
|
142
|
-
|
|
376
|
+
"map",
|
|
377
|
+
async (input, context) => {
|
|
378
|
+
const filePath = resolvePath(context.directory, input.path as string);
|
|
379
|
+
await authorizeRead(context, filePath);
|
|
380
|
+
return runReadSeek(context, ["map", filePath]);
|
|
381
|
+
},
|
|
143
382
|
),
|
|
144
383
|
readseek_search: readseekTool(
|
|
145
384
|
"Search source code using an ast-grep structural pattern. Results include LINE:HASH anchors.",
|
|
@@ -151,12 +390,14 @@ export const ReadSeekPlugin: Plugin = async () => {
|
|
|
151
390
|
others: tool.schema.boolean().optional(),
|
|
152
391
|
ignored: tool.schema.boolean().optional(),
|
|
153
392
|
},
|
|
154
|
-
|
|
155
|
-
|
|
393
|
+
"search",
|
|
394
|
+
async (input, context) => {
|
|
395
|
+
const target = resolvePath(context.directory, (input.path as string | undefined) ?? ".");
|
|
156
396
|
const args = ["search", target, input.pattern as string];
|
|
157
397
|
if (input.language) args.push("--language", input.language as string);
|
|
158
398
|
withSearchFlags(args, input);
|
|
159
|
-
|
|
399
|
+
await authorizeSearch(context, target, input.pattern as string);
|
|
400
|
+
const result = await runReadSeek(context, args);
|
|
160
401
|
return result;
|
|
161
402
|
},
|
|
162
403
|
),
|
|
@@ -170,11 +411,14 @@ export const ReadSeekPlugin: Plugin = async () => {
|
|
|
170
411
|
others: tool.schema.boolean().optional(),
|
|
171
412
|
ignored: tool.schema.boolean().optional(),
|
|
172
413
|
},
|
|
173
|
-
|
|
174
|
-
|
|
414
|
+
"def",
|
|
415
|
+
async (input, context) => {
|
|
416
|
+
const target = resolvePath(context.directory, (input.path as string | undefined) ?? ".");
|
|
417
|
+
const args = ["def", target, "--format", "plain", input.name as string];
|
|
175
418
|
if (input.language) args.push("--language", input.language as string);
|
|
176
419
|
withSearchFlags(args, input);
|
|
177
|
-
|
|
420
|
+
await authorizeSearch(context, target, input.name as string);
|
|
421
|
+
return runReadSeek(context, args);
|
|
178
422
|
},
|
|
179
423
|
),
|
|
180
424
|
readseek_refs: readseekTool(
|
|
@@ -190,14 +434,21 @@ export const ReadSeekPlugin: Plugin = async () => {
|
|
|
190
434
|
others: tool.schema.boolean().optional(),
|
|
191
435
|
ignored: tool.schema.boolean().optional(),
|
|
192
436
|
},
|
|
193
|
-
|
|
194
|
-
|
|
437
|
+
"refs",
|
|
438
|
+
async (input, context) => {
|
|
439
|
+
if (input.scope && input.line === undefined) throw new Error("scope requires line");
|
|
440
|
+
if (!input.scope && (input.line !== undefined || input.column !== undefined)) {
|
|
441
|
+
throw new Error("line and column require scope");
|
|
442
|
+
}
|
|
443
|
+
const target = resolvePath(context.directory, (input.path as string | undefined) ?? ".");
|
|
444
|
+
const args = ["refs", target, input.name as string];
|
|
195
445
|
optionalFlag(args, input.scope as boolean | undefined, "--scope");
|
|
196
446
|
if (input.line) args.push("--line", String(input.line));
|
|
197
447
|
if (input.column) args.push("--column", String(input.column));
|
|
198
448
|
if (input.language) args.push("--language", input.language as string);
|
|
199
449
|
withSearchFlags(args, input);
|
|
200
|
-
|
|
450
|
+
await authorizeSearch(context, target, input.name as string);
|
|
451
|
+
return runReadSeek(context, args);
|
|
201
452
|
},
|
|
202
453
|
),
|
|
203
454
|
readseek_hover: readseekTool(
|
|
@@ -208,11 +459,14 @@ export const ReadSeekPlugin: Plugin = async () => {
|
|
|
208
459
|
column: tool.schema.number().int().positive().optional().describe("One-based cursor byte column"),
|
|
209
460
|
language: tool.schema.string().optional(),
|
|
210
461
|
},
|
|
211
|
-
|
|
212
|
-
|
|
462
|
+
"hover",
|
|
463
|
+
async (input, context) => {
|
|
464
|
+
const filePath = resolvePath(context.directory, input.path as string);
|
|
465
|
+
await authorizeRead(context, filePath);
|
|
466
|
+
const args = ["identify", `${filePath}:${input.line}`];
|
|
213
467
|
if (input.column) args.push("--column", String(input.column));
|
|
214
468
|
if (input.language) args.push("--language", input.language as string);
|
|
215
|
-
return runReadSeek(
|
|
469
|
+
return runReadSeek(context, args);
|
|
216
470
|
},
|
|
217
471
|
),
|
|
218
472
|
readseek_rename: readseekTool(
|
|
@@ -224,22 +478,40 @@ export const ReadSeekPlugin: Plugin = async () => {
|
|
|
224
478
|
to: tool.schema.string().min(1).describe("New binding name"),
|
|
225
479
|
workspace: tool.schema.boolean().optional().describe("Include project-wide occurrences"),
|
|
226
480
|
},
|
|
227
|
-
|
|
228
|
-
|
|
481
|
+
"rename",
|
|
482
|
+
async (input, context) => {
|
|
483
|
+
const filePath = resolvePath(context.directory, input.path as string);
|
|
484
|
+
await authorizeRead(context, filePath);
|
|
485
|
+
if (input.workspace) {
|
|
486
|
+
const identifyArgs = ["identify", `${filePath}:${input.line}`];
|
|
487
|
+
if (input.column) identifyArgs.push("--column", String(input.column));
|
|
488
|
+
const name = identifiedName(await runReadSeek(context, identifyArgs));
|
|
489
|
+
if (!name) throw new Error("readseek could not identify a binding at the rename cursor");
|
|
490
|
+
await authorizeSearch(context, context.directory, name);
|
|
491
|
+
}
|
|
492
|
+
const args = ["rename", filePath, "--line", String(input.line), "--to", input.to as string];
|
|
229
493
|
if (input.column) args.push("--column", String(input.column));
|
|
230
|
-
if (input.workspace) args.push("--workspace", directory);
|
|
231
|
-
return runReadSeek(
|
|
494
|
+
if (input.workspace) args.push("--workspace", context.directory);
|
|
495
|
+
return runReadSeek(context, args);
|
|
232
496
|
},
|
|
233
497
|
),
|
|
234
498
|
readseek_check: readseekTool(
|
|
235
499
|
"Check a source file for parser errors and missing syntax.",
|
|
236
500
|
{ path: tool.schema.string().describe("Path relative to the project directory") },
|
|
237
|
-
|
|
501
|
+
"check",
|
|
502
|
+
async (input, context) => {
|
|
503
|
+
const filePath = resolvePath(context.directory, input.path as string);
|
|
504
|
+
await authorizeRead(context, filePath);
|
|
505
|
+
return runReadSeek(context, ["check", filePath]);
|
|
506
|
+
},
|
|
238
507
|
),
|
|
239
508
|
},
|
|
240
509
|
event: async ({ event }) => {
|
|
241
|
-
if (event.type
|
|
242
|
-
|
|
510
|
+
if (event.type === "file.edited" || event.type === "file.watcher.updated") {
|
|
511
|
+
anchors.forget(event.properties.file);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
if (event.type === "session.deleted") anchors.deleteSession(event.properties.info.id);
|
|
243
515
|
},
|
|
244
516
|
"tool.execute.before": async (input, output) => {
|
|
245
517
|
if (input.tool === "readseek_rename" && output.args.apply === true) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-readseek",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "OpenCode plugin for readseek-backed structural code navigation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"node": ">=20.0.0"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@jarkkojs/readseek": "^0.
|
|
31
|
+
"@jarkkojs/readseek": "^0.6.0",
|
|
32
32
|
"@opencode-ai/plugin": "^1.17.20"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|