opencode-readseek 0.5.19 → 0.6.1

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.
Files changed (3) hide show
  1. package/README.md +6 -2
  2. package/index.ts +309 -37
  3. package/package.json +4 -3
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, 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
- for (const paths of this.#pathsBySession.values()) paths.delete(filePath);
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 { old_name: oldName, new_name: newName } = output as Record<string, unknown>;
32
- if (typeof oldName === "string" && typeof newName === "string") {
33
- this.#renamePlans.set(sessionID, `${oldName} -> ${newName}`);
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(directory: string, args: string[]): Promise<unknown> {
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
- execute: (args: any, directory: string) => Promise<unknown>,
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
- return render(await execute(args, context.directory));
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
- "Read a text file with stable LINE:HASH anchors. Use those anchors when describing a later edit.",
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
- async (input, directory) => {
133
- const filePath = resolvePath(directory, input.path as string);
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 as number) + (input.limit as number) - 1));
136
- return runReadSeek(directory, args);
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
- async (input, directory) => runReadSeek(directory, ["map", resolvePath(directory, input.path as string)]),
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
- async (input, directory) => {
155
- const target = resolvePath(directory, (input.path as string | undefined) ?? ".");
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
- const result = await runReadSeek(directory, args);
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
- async (input, directory) => {
174
- const args = ["def", resolvePath(directory, (input.path as string | undefined) ?? "."), "--format", "plain", input.name as string];
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
- return runReadSeek(directory, args);
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
- async (input, directory) => {
194
- const args = ["refs", resolvePath(directory, (input.path as string | undefined) ?? "."), input.name as string];
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
- return runReadSeek(directory, args);
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
- async (input, directory) => {
212
- const args = ["identify", `${resolvePath(directory, input.path as string)}:${input.line}`];
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(directory, args);
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
- async (input, directory) => {
228
- const args = ["rename", resolvePath(directory, input.path as string), "--line", String(input.line), "--to", input.to as string];
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(directory, args);
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
- async (input, directory) => runReadSeek(directory, ["check", resolvePath(directory, input.path as string)]),
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 !== "file.edited") return;
242
- anchors.forget(path.resolve(event.properties.file));
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,10 +1,11 @@
1
1
  {
2
2
  "name": "opencode-readseek",
3
- "version": "0.5.19",
3
+ "version": "0.6.1",
4
4
  "description": "OpenCode plugin for readseek-backed structural code navigation",
5
5
  "type": "module",
6
6
  "exports": {
7
- ".": "./index.ts"
7
+ ".": "./index.ts",
8
+ "./server": "./index.ts"
8
9
  },
9
10
  "keywords": [
10
11
  "opencode",
@@ -28,7 +29,7 @@
28
29
  "node": ">=20.0.0"
29
30
  },
30
31
  "dependencies": {
31
- "@jarkkojs/readseek": "^0.5.19",
32
+ "@jarkkojs/readseek": "^0.6.1",
32
33
  "@opencode-ai/plugin": "^1.17.20"
33
34
  },
34
35
  "devDependencies": {