opencode-readseek 0.9.0 → 0.9.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 (2) hide show
  1. package/index.ts +56 -10
  2. package/package.json +2 -2
package/index.ts CHANGED
@@ -10,6 +10,8 @@ import { tool, type Plugin, type PluginOptions, type ToolAttachment, type ToolCo
10
10
 
11
11
  const require = createRequire(import.meta.url);
12
12
  const MAX_OUTPUT_BYTES = 32 * 1024 * 1024;
13
+ const DEFAULT_READSEEK_TIMEOUT_MS = 120_000;
14
+ const DEFAULT_VISION_TIMEOUT_MS = 30 * 60 * 1000;
13
15
  const DEFAULT_READ_LIMIT = 2000;
14
16
  const EDIT_RESULT_READ_LIMIT = 40;
15
17
  const MAX_DOCUMENT_OUTPUT_BYTES = 256 * 1024;
@@ -386,7 +388,7 @@ function readRangeArgs(filePath: string, start: number, end: number): string[] {
386
388
  async function runDigestContent(
387
389
  context: ToolContext,
388
390
  args: string[],
389
- options: { cancelable?: boolean } = {},
391
+ options: { cancelable?: boolean; timeoutMs?: number } = {},
390
392
  ): Promise<unknown> {
391
393
  return requireDigestFacet(await runReadSeek(context, args, options), "content");
392
394
  }
@@ -439,7 +441,7 @@ async function readBoundedText(
439
441
  async function runReadSeekRaw(
440
442
  context: ToolContext,
441
443
  args: string[],
442
- options: { cancelable?: boolean; stdin?: string } = {},
444
+ options: { cancelable?: boolean; stdin?: string; timeoutMs?: number } = {},
443
445
  ): Promise<string> {
444
446
  context.abort.throwIfAborted();
445
447
  const cancelable = options.cancelable !== false;
@@ -465,11 +467,31 @@ async function runReadSeekRaw(
465
467
  // Preserve the output-limit error if the process exited concurrently.
466
468
  }
467
469
  };
468
- const [stdout, stderr, exitCode] = await Promise.all([
470
+ const timeoutMs = options.timeoutMs ?? DEFAULT_READSEEK_TIMEOUT_MS;
471
+ let timeout: ReturnType<typeof setTimeout> | undefined;
472
+ const timeoutResult = new Promise<never>((_resolve, reject) => {
473
+ timeout = setTimeout(() => {
474
+ try {
475
+ child.kill("SIGKILL");
476
+ } catch {
477
+ // Preserve the timeout error if the process exited concurrently.
478
+ }
479
+ reject(new Error(`readseek timed out after ${timeoutMs} ms`));
480
+ }, timeoutMs);
481
+ timeout.unref?.();
482
+ });
483
+ const pending = Promise.all([
469
484
  readBoundedText(child.stdout, "stdout", killForOverflow),
470
485
  readBoundedText(child.stderr, "stderr", killForOverflow),
471
486
  child.exited,
472
487
  ]);
488
+ let result: Awaited<typeof pending>;
489
+ try {
490
+ result = await Promise.race([pending, timeoutResult]);
491
+ } finally {
492
+ if (timeout !== undefined) clearTimeout(timeout);
493
+ }
494
+ const [stdout, stderr, exitCode] = result;
473
495
  if (cancelable) context.abort.throwIfAborted();
474
496
  if (exitCode !== 0) throw new Error(stderr.trim() || `readseek exited with status ${exitCode}`);
475
497
  return stdout;
@@ -478,7 +500,7 @@ async function runReadSeekRaw(
478
500
  async function runReadSeek(
479
501
  context: ToolContext,
480
502
  args: string[],
481
- options: { cancelable?: boolean; stdin?: string } = {},
503
+ options: { cancelable?: boolean; stdin?: string; timeoutMs?: number } = {},
482
504
  ): Promise<unknown> {
483
505
  const stdout = await runReadSeekRaw(context, args, options);
484
506
  try {
@@ -495,7 +517,10 @@ async function postMutationPreview(
495
517
  changed: boolean,
496
518
  ): Promise<unknown> {
497
519
  try {
498
- return await runDigestContent(context, args, { cancelable: false });
520
+ return await runDigestContent(context, args, {
521
+ cancelable: false,
522
+ timeoutMs: DEFAULT_READSEEK_TIMEOUT_MS,
523
+ });
499
524
  } catch (error) {
500
525
  const previewError = error instanceof Error ? error.message : String(error);
501
526
  return {
@@ -1150,7 +1175,11 @@ export const ReadSeekPlugin: Plugin = async (_input, options) => {
1150
1175
 
1151
1176
  let envelope: unknown;
1152
1177
  try {
1153
- envelope = await runReadSeek(context, digestArgs(filePath, "content", extra));
1178
+ envelope = await runReadSeek(
1179
+ context,
1180
+ digestArgs(filePath, "content", extra),
1181
+ image !== undefined && image !== "none" ? { timeoutMs: DEFAULT_VISION_TIMEOUT_MS } : {},
1182
+ );
1154
1183
  } catch (error) {
1155
1184
  // A visual or document file with an uncommon extension rejects text bounds before
1156
1185
  // an envelope can be returned. Probe metadata only on this cold path.
@@ -1224,7 +1253,11 @@ export const ReadSeekPlugin: Plugin = async (_input, options) => {
1224
1253
  optionalFlag(args, input.outline as boolean | undefined, "--outline");
1225
1254
  if (visionMode !== undefined) args.push("--vision-mode", visionMode);
1226
1255
  if (visionLevel !== undefined) args.push("--vision-level", visionLevel);
1227
- return runReadSeekRaw(context, args);
1256
+ return runReadSeekRaw(
1257
+ context,
1258
+ args,
1259
+ visionMode === undefined ? {} : { timeoutMs: DEFAULT_VISION_TIMEOUT_MS },
1260
+ );
1228
1261
  },
1229
1262
  (result) => ({ output: boundDocumentOutput(String(result)) }),
1230
1263
  ),
@@ -1262,7 +1295,13 @@ export const ReadSeekPlugin: Plugin = async (_input, options) => {
1262
1295
  if (input.language) args.push("--language", input.language as string);
1263
1296
  if (visionMode !== undefined) args.push("--vision-mode", visionMode);
1264
1297
  if (visionLevel !== undefined) args.push("--vision-level", visionLevel);
1265
- return runReadSeek(context, args);
1298
+ return runReadSeek(
1299
+ context,
1300
+ args,
1301
+ visionMode === undefined || visionMode === "none"
1302
+ ? {}
1303
+ : { timeoutMs: DEFAULT_VISION_TIMEOUT_MS },
1304
+ );
1266
1305
  },
1267
1306
  ),
1268
1307
  readseek_grep: readseekTool(
@@ -1457,7 +1496,10 @@ export const ReadSeekPlugin: Plugin = async (_input, options) => {
1457
1496
  throw new Error("readseek rename plan did not include a plan hash");
1458
1497
  }
1459
1498
  await authorizeEdit(context, files);
1460
- return runReadSeek(context, [...args, "--plan-hash", planHash, "--apply"], { cancelable: false });
1499
+ return runReadSeek(context, [...args, "--plan-hash", planHash, "--apply"], {
1500
+ cancelable: false,
1501
+ timeoutMs: DEFAULT_READSEEK_TIMEOUT_MS,
1502
+ });
1461
1503
  },
1462
1504
  ),
1463
1505
  readseek_edit: readseekTool(
@@ -1532,7 +1574,11 @@ export const ReadSeekPlugin: Plugin = async (_input, options) => {
1532
1574
  const applied = record(await runReadSeek(
1533
1575
  context,
1534
1576
  [...baseArgs, "--apply", "--plan-hash", plan.plan_hash],
1535
- { cancelable: false, stdin: request },
1577
+ {
1578
+ cancelable: false,
1579
+ stdin: request,
1580
+ timeoutMs: DEFAULT_READSEEK_TIMEOUT_MS,
1581
+ },
1536
1582
  ));
1537
1583
  if (applied.applied !== true) {
1538
1584
  throw new Error("readseek did not confirm that the edit was applied");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-readseek",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
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.9.0",
34
+ "@jarkkojs/readseek": "^0.9.1",
35
35
  "@opencode-ai/plugin": "^1.17.20"
36
36
  },
37
37
  "devDependencies": {