opencode-readseek 0.9.4 → 0.9.6

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/dist/index.js +1962 -0
  2. package/package.json +8 -6
  3. package/index.ts +0 -1663
package/dist/index.js ADDED
@@ -0,0 +1,1962 @@
1
+ // index.ts
2
+ import { lstat, mkdir, open, readFile, readlink, realpath, stat } from "node:fs/promises";
3
+ import { createRequire as createRequire2 } from "node:module";
4
+ import { homedir } from "node:os";
5
+ import path2 from "node:path";
6
+ import { tool } from "@opencode-ai/plugin";
7
+
8
+ // ../readseek-api/src/contract.ts
9
+ var VISION_MODES = ["none", "all", "ocr", "caption", "objects"];
10
+ var VISION_ANALYSIS_MODES = ["all", "ocr", "caption", "objects"];
11
+ var VISION_LEVELS = ["low", "medium", "high"];
12
+ var DOCUMENT_NODE_KINDS = [
13
+ "artifact",
14
+ "footer",
15
+ "header",
16
+ "heading",
17
+ "marginal_label",
18
+ "page",
19
+ "page_number",
20
+ "paragraph",
21
+ "section",
22
+ "structural_section"
23
+ ];
24
+ var TOOL_NAMES = [
25
+ "digest",
26
+ "edit",
27
+ "write",
28
+ "grep",
29
+ "view",
30
+ "search",
31
+ "def",
32
+ "refs",
33
+ "rename"
34
+ ];
35
+ var PARAM_DESCRIPTIONS = {
36
+ path: "Absolute or relative file path",
37
+ searchPath: "File or directory to search; defaults to the current directory",
38
+ language: "Language override when auto-detection is ambiguous",
39
+ languageOverride: "Language override",
40
+ languageSymbol: "Language override for symbol replacement",
41
+ pattern: "AST pattern (ast-grep style), such as console.log($$$ARGS)",
42
+ grepPattern: "Regex pattern; set literal for exact text",
43
+ name: "Qualified or unqualified symbol name",
44
+ refsName: "Identifier to find references for",
45
+ scope: "Restrict results to the binding at the cursor",
46
+ line: "One-based cursor line; required with scope",
47
+ renameLine: "One-based cursor line of the symbol to rename",
48
+ column: "One-based cursor byte column for disambiguation",
49
+ cached: "In a Git repository, search tracked/indexed files",
50
+ others: "In a Git repository, search untracked files",
51
+ ignored: "With others=true, include ignored untracked files",
52
+ select: "Facets: metadata, content, map, diagnostics, identity (default content)",
53
+ selectComma: "Comma-separated facets: metadata, content, map, diagnostics, identity",
54
+ at: "Location: line:N[:COLUMN], hash:HASH[:COLUMN], or symbol:NAME",
55
+ end: "Last line to include",
56
+ limit: "Maximum number of lines to include",
57
+ offset: "One-based starting line",
58
+ readLimit: "Maximum lines to return",
59
+ symbolRead: "Symbol name to read",
60
+ bundle: "Include related symbols from the same file; requires symbol",
61
+ image: "Standalone image mode: none, all, ocr, caption, objects. Required for standalone images.",
62
+ visionModeDigest: "Standalone image mode: none, all, caption, objects, or ocr",
63
+ visionModeView: "Analyze selected assets: all, ocr, caption, or objects",
64
+ visionLevel: "Vision inference level: low (default), medium, or high",
65
+ node: "Node ID to use as the view root",
66
+ page: "One-based source page",
67
+ kind: "Node kind filter",
68
+ depth: "Maximum depth below selected roots",
69
+ outline: "Return outline nodes only",
70
+ edits: "Use set_line, replace_lines, insert_after, replace_symbol, or replace",
71
+ editVariant: "Exactly one nested edit variant",
72
+ setLine: 'Replace one line: {"set_line":{"anchor":"LINE:HASH","new_text":"..."}}',
73
+ replaceLines: 'Replace a range: {"replace_lines":{"start_anchor":"LINE:HASH","end_anchor":"LINE:HASH","new_text":"..."}}',
74
+ insertAfter: 'Insert text: {"insert_after":{"anchor":"LINE:HASH","new_text":"..."}}',
75
+ replace: 'Replace text: {"replace":{"old_text":"...","new_text":"..."}}',
76
+ replaceSymbol: 'Replace a symbol: {"replace_symbol":{"symbol":"name","new_body":"..."}}',
77
+ anchor: "Fresh LINE:HASH anchor for the line to replace",
78
+ startAnchor: "Fresh LINE:HASH anchor for the first line in the range",
79
+ endAnchor: "Fresh LINE:HASH anchor for the last line in the range",
80
+ insertAnchor: "Fresh LINE:HASH anchor for the line after which to insert",
81
+ newTextLine: "Replacement text; use an empty string to delete the line",
82
+ newTextRange: "Replacement text; use an empty string to delete the range",
83
+ newTextInsert: "Text to insert after the anchored line",
84
+ oldText: "Exact text to find",
85
+ newText: "Replacement text",
86
+ all: "Replace every exact match",
87
+ symbol: "Mapped symbol name",
88
+ newBody: "Complete replacement body for the symbol",
89
+ apply: "When supported by the host, apply the verified plan instead of previewing only",
90
+ planHash: "Require the apply plan to match this dry-run plan hash",
91
+ workspace: "Rename across the project",
92
+ to: "New symbol name",
93
+ postEditVerify: "Read back and verify persisted content",
94
+ content: "Complete text file content",
95
+ glob: "File-name glob, such as *.ts",
96
+ ignoreCase: "Ignore case",
97
+ literal: "Treat pattern literally",
98
+ context: "Surrounding lines for each match",
99
+ matchLimit: "Maximum matches to return",
100
+ summary: "Return per-file counts",
101
+ grepScope: "Group matches by enclosing symbol",
102
+ scopeContext: "Context lines within each symbol"
103
+ };
104
+ var TOOL_DESCRIPTIONS = {
105
+ digest: "Digest source content and images into the native CLI envelope. Returns metadata plus requested facets unchanged from `readseek digest`. Use it before edits to obtain fresh `LINE:HASH` anchors; use for structural maps, diagnostics, and cursor identity.",
106
+ edit: 'Edit existing text files with fresh `LINE:HASH` anchors. Read or search first when no fresh anchors are available. Each `edits[]` item must use exactly one nested object: `{"set_line":{"anchor":"LINE:HASH","new_text":"..."}}`, `{"replace_lines":{"start_anchor":"LINE:HASH","end_anchor":"LINE:HASH","new_text":"..."}}`, `{"insert_after":{"anchor":"LINE:HASH","new_text":"..."}}`, `{"replace_symbol":{"symbol":"name","new_body":"..."}}`, or `{"replace":{"old_text":"...","new_text":"..."}}`.',
107
+ write: "Create or replace a complete text file and return `LINE:HASH` anchors. Use `edit` for small changes to an existing file.",
108
+ grep: "Search text or regex in files and return edit-ready `LINE:HASH` anchors. Use for identifiers, strings, configuration, errors, comments, and documentation.",
109
+ view: "Digest a document's structure or selected content. Start with the overview, then narrow by page, node, kind, or depth instead of reading the whole document. PDF is the first supported document format.",
110
+ search: "Search code by AST structure using ast-grep-style patterns. Results include line hashes usable as `LINE:HASH` anchors. Use `grep` for identifiers, strings, comments, configuration, documentation, or other plain text and regex.",
111
+ def: "Go to a symbol definition or declaration by name. Accepts qualified or unqualified names; use this instead of text search for functions, methods, classes, types, and other declarations.",
112
+ refs: "Find references or usages of an identifier by name or cursor binding. Results include enclosing symbols and line hashes usable as `LINE:HASH` anchors. Without cursor scope, results are identifier-name matches; with scope, they follow one binding and exclude shadows. Use text search for literals, and use this before renaming or deleting a symbol.",
113
+ rename: "Rename a symbol at a source location. Resolvable same-named bindings are preserved; verified edits apply by default. Set `apply: false` to preview the plan before changing files."
114
+ };
115
+ function renderToolDescription(name, aliases = {}) {
116
+ const desc = TOOL_DESCRIPTIONS[name];
117
+ if (!desc)
118
+ throw new Error(`Missing TOOL_DESCRIPTIONS for ${name}`);
119
+ return TOOL_NAMES.reduce((description, toolName) => description.replaceAll(`\`${toolName}\``, `\`${aliases[toolName] ?? toolName}\``), desc);
120
+ }
121
+ var gitFlagsSchema = {
122
+ cached: {
123
+ type: "boolean",
124
+ description: PARAM_DESCRIPTIONS.cached
125
+ },
126
+ others: {
127
+ type: "boolean",
128
+ description: PARAM_DESCRIPTIONS.others
129
+ },
130
+ ignored: {
131
+ type: "boolean",
132
+ description: PARAM_DESCRIPTIONS.ignored
133
+ }
134
+ };
135
+ var editItemSchema = {
136
+ type: "object",
137
+ additionalProperties: false,
138
+ minProperties: 1,
139
+ maxProperties: 1,
140
+ properties: {
141
+ set_line: {
142
+ type: "object",
143
+ additionalProperties: false,
144
+ required: ["anchor", "new_text"],
145
+ properties: {
146
+ anchor: { type: "string", description: PARAM_DESCRIPTIONS.anchor },
147
+ new_text: { type: "string", description: PARAM_DESCRIPTIONS.newTextLine }
148
+ },
149
+ description: PARAM_DESCRIPTIONS.setLine
150
+ },
151
+ replace_lines: {
152
+ type: "object",
153
+ additionalProperties: false,
154
+ required: ["start_anchor", "end_anchor", "new_text"],
155
+ properties: {
156
+ start_anchor: { type: "string", description: PARAM_DESCRIPTIONS.startAnchor },
157
+ end_anchor: { type: "string", description: PARAM_DESCRIPTIONS.endAnchor },
158
+ new_text: { type: "string", description: PARAM_DESCRIPTIONS.newTextRange }
159
+ },
160
+ description: PARAM_DESCRIPTIONS.replaceLines
161
+ },
162
+ insert_after: {
163
+ type: "object",
164
+ additionalProperties: false,
165
+ required: ["anchor", "new_text"],
166
+ properties: {
167
+ anchor: { type: "string", description: PARAM_DESCRIPTIONS.insertAnchor },
168
+ new_text: { type: "string", description: PARAM_DESCRIPTIONS.newTextInsert }
169
+ },
170
+ description: PARAM_DESCRIPTIONS.insertAfter
171
+ },
172
+ replace: {
173
+ type: "object",
174
+ additionalProperties: false,
175
+ required: ["old_text", "new_text"],
176
+ properties: {
177
+ old_text: { type: "string", description: PARAM_DESCRIPTIONS.oldText },
178
+ new_text: { type: "string", description: PARAM_DESCRIPTIONS.newText },
179
+ all: { type: "boolean", description: PARAM_DESCRIPTIONS.all }
180
+ },
181
+ description: PARAM_DESCRIPTIONS.replace
182
+ },
183
+ replace_symbol: {
184
+ type: "object",
185
+ additionalProperties: false,
186
+ required: ["symbol", "new_body"],
187
+ properties: {
188
+ symbol: { type: "string", description: PARAM_DESCRIPTIONS.symbol },
189
+ new_body: { type: "string", description: PARAM_DESCRIPTIONS.newBody }
190
+ },
191
+ description: PARAM_DESCRIPTIONS.replaceSymbol
192
+ }
193
+ },
194
+ description: PARAM_DESCRIPTIONS.editVariant
195
+ };
196
+ var TOOL_CONTRACTS = [
197
+ {
198
+ name: "digest",
199
+ title: "Digest source",
200
+ description: TOOL_DESCRIPTIONS.digest,
201
+ inputSchema: {
202
+ type: "object",
203
+ properties: {
204
+ path: { type: "string", description: PARAM_DESCRIPTIONS.path },
205
+ select: { type: "string", description: PARAM_DESCRIPTIONS.select },
206
+ at: { type: "string", description: PARAM_DESCRIPTIONS.at },
207
+ end: { type: "integer", minimum: 1, description: PARAM_DESCRIPTIONS.end },
208
+ limit: { type: "integer", minimum: 1, description: PARAM_DESCRIPTIONS.limit },
209
+ language: { type: "string", description: PARAM_DESCRIPTIONS.languageOverride },
210
+ visionMode: {
211
+ type: "string",
212
+ enum: [...VISION_MODES],
213
+ description: PARAM_DESCRIPTIONS.visionModeDigest
214
+ },
215
+ visionLevel: {
216
+ type: "string",
217
+ enum: [...VISION_LEVELS],
218
+ description: PARAM_DESCRIPTIONS.visionLevel
219
+ }
220
+ },
221
+ required: ["path"],
222
+ additionalProperties: false
223
+ }
224
+ },
225
+ {
226
+ name: "edit",
227
+ title: "Hash-verified edit",
228
+ description: TOOL_DESCRIPTIONS.edit,
229
+ inputSchema: {
230
+ type: "object",
231
+ properties: {
232
+ path: { type: "string", description: PARAM_DESCRIPTIONS.path },
233
+ edits: {
234
+ type: "array",
235
+ minItems: 1,
236
+ description: PARAM_DESCRIPTIONS.edits,
237
+ items: editItemSchema
238
+ },
239
+ language: { type: "string", description: PARAM_DESCRIPTIONS.languageSymbol },
240
+ apply: { type: "boolean", description: PARAM_DESCRIPTIONS.apply },
241
+ planHash: { type: "string", description: PARAM_DESCRIPTIONS.planHash },
242
+ postEditVerify: { type: "boolean", description: PARAM_DESCRIPTIONS.postEditVerify }
243
+ },
244
+ required: ["path", "edits"],
245
+ additionalProperties: false
246
+ }
247
+ },
248
+ {
249
+ name: "write",
250
+ title: "Write file",
251
+ description: TOOL_DESCRIPTIONS.write,
252
+ inputSchema: {
253
+ type: "object",
254
+ properties: {
255
+ path: { type: "string", description: PARAM_DESCRIPTIONS.path },
256
+ content: { type: "string", description: PARAM_DESCRIPTIONS.content }
257
+ },
258
+ required: ["path", "content"],
259
+ additionalProperties: false
260
+ }
261
+ },
262
+ {
263
+ name: "grep",
264
+ title: "Text search",
265
+ description: TOOL_DESCRIPTIONS.grep,
266
+ inputSchema: {
267
+ type: "object",
268
+ properties: {
269
+ pattern: { type: "string", description: PARAM_DESCRIPTIONS.grepPattern },
270
+ path: { type: "string", description: PARAM_DESCRIPTIONS.searchPath },
271
+ glob: { type: "string", description: PARAM_DESCRIPTIONS.glob },
272
+ ignoreCase: { type: "boolean", description: PARAM_DESCRIPTIONS.ignoreCase },
273
+ literal: { type: "boolean", description: PARAM_DESCRIPTIONS.literal },
274
+ context: { type: "integer", minimum: 0, description: PARAM_DESCRIPTIONS.context },
275
+ limit: { type: "integer", minimum: 1, description: PARAM_DESCRIPTIONS.matchLimit },
276
+ summary: { type: "boolean", description: PARAM_DESCRIPTIONS.summary },
277
+ scope: {
278
+ type: "string",
279
+ enum: ["symbol"],
280
+ description: PARAM_DESCRIPTIONS.grepScope
281
+ },
282
+ scopeContext: {
283
+ type: "integer",
284
+ minimum: 0,
285
+ description: PARAM_DESCRIPTIONS.scopeContext
286
+ }
287
+ },
288
+ required: ["pattern"],
289
+ additionalProperties: false
290
+ }
291
+ },
292
+ {
293
+ name: "view",
294
+ title: "View document",
295
+ description: TOOL_DESCRIPTIONS.view,
296
+ inputSchema: {
297
+ type: "object",
298
+ properties: {
299
+ path: { type: "string", description: PARAM_DESCRIPTIONS.path },
300
+ node: { type: "string", description: PARAM_DESCRIPTIONS.node },
301
+ page: { type: "integer", minimum: 1, description: PARAM_DESCRIPTIONS.page },
302
+ kind: {
303
+ type: "string",
304
+ enum: [...DOCUMENT_NODE_KINDS],
305
+ description: PARAM_DESCRIPTIONS.kind
306
+ },
307
+ depth: { type: "integer", minimum: 0, description: PARAM_DESCRIPTIONS.depth },
308
+ outline: { type: "boolean", description: PARAM_DESCRIPTIONS.outline },
309
+ visionMode: {
310
+ type: "string",
311
+ enum: [...VISION_ANALYSIS_MODES],
312
+ description: PARAM_DESCRIPTIONS.visionModeView
313
+ },
314
+ visionLevel: {
315
+ type: "string",
316
+ enum: [...VISION_LEVELS],
317
+ description: PARAM_DESCRIPTIONS.visionLevel
318
+ }
319
+ },
320
+ required: ["path"],
321
+ additionalProperties: false
322
+ }
323
+ },
324
+ {
325
+ name: "search",
326
+ title: "AST search",
327
+ description: TOOL_DESCRIPTIONS.search,
328
+ inputSchema: {
329
+ type: "object",
330
+ properties: {
331
+ pattern: { type: "string", description: PARAM_DESCRIPTIONS.pattern },
332
+ path: { type: "string", description: PARAM_DESCRIPTIONS.searchPath },
333
+ language: { type: "string", description: PARAM_DESCRIPTIONS.language },
334
+ ...gitFlagsSchema
335
+ },
336
+ required: ["pattern"],
337
+ additionalProperties: false
338
+ }
339
+ },
340
+ {
341
+ name: "def",
342
+ title: "Find definition",
343
+ description: TOOL_DESCRIPTIONS.def,
344
+ inputSchema: {
345
+ type: "object",
346
+ properties: {
347
+ name: { type: "string", description: PARAM_DESCRIPTIONS.name },
348
+ path: { type: "string", description: PARAM_DESCRIPTIONS.searchPath },
349
+ language: { type: "string", description: PARAM_DESCRIPTIONS.language },
350
+ ...gitFlagsSchema
351
+ },
352
+ required: ["name"],
353
+ additionalProperties: false
354
+ }
355
+ },
356
+ {
357
+ name: "refs",
358
+ title: "Find references",
359
+ description: TOOL_DESCRIPTIONS.refs,
360
+ inputSchema: {
361
+ type: "object",
362
+ properties: {
363
+ name: { type: "string", description: PARAM_DESCRIPTIONS.refsName },
364
+ path: { type: "string", description: PARAM_DESCRIPTIONS.searchPath },
365
+ language: { type: "string", description: PARAM_DESCRIPTIONS.language },
366
+ scope: { type: "boolean", description: PARAM_DESCRIPTIONS.scope },
367
+ line: { type: "integer", minimum: 1, description: PARAM_DESCRIPTIONS.line },
368
+ column: { type: "integer", minimum: 1, description: PARAM_DESCRIPTIONS.column },
369
+ ...gitFlagsSchema
370
+ },
371
+ additionalProperties: false
372
+ }
373
+ },
374
+ {
375
+ name: "rename",
376
+ title: "Rename symbol",
377
+ description: TOOL_DESCRIPTIONS.rename,
378
+ inputSchema: {
379
+ type: "object",
380
+ properties: {
381
+ path: { type: "string", description: PARAM_DESCRIPTIONS.path },
382
+ line: { type: "integer", minimum: 1, description: PARAM_DESCRIPTIONS.renameLine },
383
+ column: { type: "integer", minimum: 1, description: PARAM_DESCRIPTIONS.column },
384
+ to: { type: "string", description: PARAM_DESCRIPTIONS.to },
385
+ workspace: { type: "boolean", description: PARAM_DESCRIPTIONS.workspace },
386
+ apply: { type: "boolean", description: PARAM_DESCRIPTIONS.apply },
387
+ planHash: { type: "string", description: PARAM_DESCRIPTIONS.planHash }
388
+ },
389
+ required: ["path", "line", "to"],
390
+ additionalProperties: false
391
+ }
392
+ }
393
+ ];
394
+ // ../readseek-api/src/validation.ts
395
+ function validateGitSelection(input) {
396
+ if (input.ignored && !input.others) {
397
+ return { ok: false, error: "ignored requires others=true" };
398
+ }
399
+ return { ok: true };
400
+ }
401
+ function validateRefsNameVsScope(input) {
402
+ if (input.scope) {
403
+ if (input.line === undefined) {
404
+ return { ok: false, error: "scope requires line" };
405
+ }
406
+ return { ok: true };
407
+ }
408
+ if (input.line !== undefined || input.column !== undefined) {
409
+ return { ok: false, error: "line/column require scope=true" };
410
+ }
411
+ if (typeof input.name !== "string" || input.name.length === 0) {
412
+ return { ok: false, error: "name is required when scope is not set" };
413
+ }
414
+ return { ok: true };
415
+ }
416
+ function validateVisionLevelVsMode(input) {
417
+ if (input.outline && (input.visionMode !== undefined || input.visionLevel !== undefined)) {
418
+ return { ok: false, error: "Cannot combine outline with visionMode or visionLevel" };
419
+ }
420
+ if (input.visionLevel !== undefined && input.visionMode === undefined) {
421
+ return { ok: false, error: "visionLevel requires visionMode" };
422
+ }
423
+ return { ok: true };
424
+ }
425
+ // ../readseek-api/src/argv.ts
426
+ function buildLineLocation(line, column) {
427
+ return column === undefined ? `line:${line}` : `line:${line}:${column}`;
428
+ }
429
+ function appendGitSelectionArgs(args, input) {
430
+ const validated = validateGitSelection(input);
431
+ if (!validated.ok) {
432
+ throw new Error(validated.error);
433
+ }
434
+ const selection = [
435
+ input.cached ? "cached" : undefined,
436
+ input.others ? "others" : undefined,
437
+ input.ignored ? "ignored" : undefined
438
+ ].filter((value) => value !== undefined);
439
+ if (selection.length > 0) {
440
+ args.push("--git", selection.join(","));
441
+ }
442
+ return args;
443
+ }
444
+ function pushOptionalString(args, flag, value) {
445
+ if (value !== undefined)
446
+ args.push(flag, value);
447
+ }
448
+ function pushOptionalNumber(args, flag, value) {
449
+ if (value !== undefined)
450
+ args.push(flag, String(value));
451
+ }
452
+ function appendApplyAndPlanHash(args, input) {
453
+ const order = input.flagOrder ?? "apply-first";
454
+ if (order === "plan-hash-first") {
455
+ pushOptionalString(args, "--plan-hash", input.planHash);
456
+ if (input.apply)
457
+ args.push("--apply");
458
+ return args;
459
+ }
460
+ if (input.apply)
461
+ args.push("--apply");
462
+ pushOptionalString(args, "--plan-hash", input.planHash);
463
+ return args;
464
+ }
465
+ function selectArg(select) {
466
+ if (select === undefined)
467
+ return;
468
+ return Array.isArray(select) ? select.join(",") : select;
469
+ }
470
+ function buildDigestArgs(input) {
471
+ const vision = validateVisionLevelVsMode({
472
+ visionMode: input.visionMode,
473
+ visionLevel: input.visionLevel
474
+ });
475
+ if (!vision.ok)
476
+ throw new Error(vision.error);
477
+ const args = [];
478
+ if (input.readseekDir !== undefined)
479
+ args.push("--readseek-dir", input.readseekDir);
480
+ args.push("digest", input.path);
481
+ const select = selectArg(input.select);
482
+ if (select !== undefined)
483
+ args.push("--select", select);
484
+ pushOptionalString(args, "--at", input.at);
485
+ pushOptionalNumber(args, "--end", input.end);
486
+ pushOptionalNumber(args, "--limit", input.limit);
487
+ pushOptionalString(args, "--language", input.language);
488
+ pushOptionalString(args, "--vision-mode", input.visionMode);
489
+ pushOptionalString(args, "--vision-level", input.visionLevel);
490
+ pushOptionalString(args, "--stdin-name", input.stdinName);
491
+ return args;
492
+ }
493
+ function buildViewArgs(input) {
494
+ const vision = validateVisionLevelVsMode({
495
+ visionMode: input.visionMode,
496
+ visionLevel: input.visionLevel,
497
+ outline: input.outline
498
+ });
499
+ if (!vision.ok)
500
+ throw new Error(vision.error);
501
+ const args = [];
502
+ if (input.readseekDir !== undefined)
503
+ args.push("--readseek-dir", input.readseekDir);
504
+ args.push("view", input.path);
505
+ if (input.format !== undefined)
506
+ args.push("--format", input.format);
507
+ pushOptionalString(args, "--node", input.node);
508
+ pushOptionalNumber(args, "--page", input.page);
509
+ pushOptionalString(args, "--kind", input.kind);
510
+ pushOptionalNumber(args, "--depth", input.depth);
511
+ if (input.outline)
512
+ args.push("--outline");
513
+ pushOptionalString(args, "--vision-mode", input.visionMode);
514
+ pushOptionalString(args, "--vision-level", input.visionLevel);
515
+ return args;
516
+ }
517
+ function buildSearchArgs(input) {
518
+ const args = ["search", input.path, input.pattern];
519
+ pushOptionalString(args, "--language", input.language);
520
+ return appendGitSelectionArgs(args, input);
521
+ }
522
+ function buildDefArgs(input) {
523
+ const args = ["def", input.path];
524
+ if (input.format !== undefined)
525
+ args.push("--format", input.format);
526
+ args.push(input.name);
527
+ pushOptionalString(args, "--language", input.language);
528
+ return appendGitSelectionArgs(args, input);
529
+ }
530
+ function buildRefsArgs(input) {
531
+ const validated = validateRefsNameVsScope(input);
532
+ if (!validated.ok)
533
+ throw new Error(validated.error);
534
+ const args = ["refs", input.path];
535
+ if (input.scope) {
536
+ args.push("--at", buildLineLocation(input.line, input.column));
537
+ } else if (input.name !== undefined) {
538
+ args.push(input.name);
539
+ }
540
+ if (input.format !== undefined)
541
+ args.push("--format", input.format);
542
+ pushOptionalString(args, "--language", input.language);
543
+ return appendGitSelectionArgs(args, input);
544
+ }
545
+ function buildEditArgs(input) {
546
+ const args = ["edit", input.path];
547
+ appendApplyAndPlanHash(args, {
548
+ apply: input.apply,
549
+ planHash: input.planHash,
550
+ flagOrder: input.flagOrder ?? "apply-first"
551
+ });
552
+ pushOptionalString(args, "--language", input.language);
553
+ if (input.requestFromStdin)
554
+ args.push("--request", "-");
555
+ return args;
556
+ }
557
+ function buildRenameArgs(input) {
558
+ const args = ["rename", input.path, "--at", buildLineLocation(input.line, input.column), "--to", input.to];
559
+ if (input.workspace !== undefined)
560
+ args.push("--workspace", input.workspace);
561
+ appendApplyAndPlanHash(args, {
562
+ apply: input.apply,
563
+ planHash: input.planHash,
564
+ flagOrder: input.flagOrder ?? "plan-hash-first"
565
+ });
566
+ pushOptionalString(args, "--language", input.language);
567
+ return appendGitSelectionArgs(args, input);
568
+ }
569
+ // ../readseek-api/src/platform.ts
570
+ import { createRequire } from "node:module";
571
+ import path from "node:path";
572
+ var READSEEK_PLATFORM_PACKAGES = {
573
+ "darwin-arm64": "@jarkkojs/readseek-darwin-arm64",
574
+ "linux-arm64": "@jarkkojs/readseek-linux-arm64",
575
+ "linux-x64": "@jarkkojs/readseek-linux-x64",
576
+ "win32-x64": "@jarkkojs/readseek-win32-x64"
577
+ };
578
+ var resolveFromPackage = createRequire(import.meta.url).resolve;
579
+ function readSeekBinaryName(platform = process.platform) {
580
+ return platform === "win32" ? "readseek.exe" : "readseek";
581
+ }
582
+ function readSeekPlatformKey(platform = process.platform, arch = process.arch) {
583
+ return `${platform}-${arch}`;
584
+ }
585
+ function resolveReadSeekBinaryFromPackages(resolver = resolveFromPackage, options = {}) {
586
+ const resolve = typeof resolver === "function" ? resolver : resolver.resolve.bind(resolver);
587
+ const platform = options.platform ?? process.platform;
588
+ const arch = options.arch ?? process.arch;
589
+ const key = readSeekPlatformKey(platform, arch);
590
+ const platformPackage = READSEEK_PLATFORM_PACKAGES[key];
591
+ if (!platformPackage) {
592
+ const supported = Object.keys(READSEEK_PLATFORM_PACKAGES).join(", ");
593
+ throw new Error(`@jarkkojs/readseek ships no binary for ${key}; it supports ${supported}`);
594
+ }
595
+ const metaPackage = options.metaPackage ?? "@jarkkojs/readseek";
596
+ const metaPackageJson = resolve(`${metaPackage}/package.json`);
597
+ const metaPackageDir = path.dirname(metaPackageJson);
598
+ const packageJsonPath = resolve(`${platformPackage}/package.json`, {
599
+ paths: [metaPackageDir]
600
+ });
601
+ return path.join(path.dirname(packageJsonPath), "bin", readSeekBinaryName(platform));
602
+ }
603
+ // ../readseek-api/src/json.ts
604
+ var READSEEK_USAGE_HINT = /\n\s*Run readseek(?: [\w-]+)* --help for more information\.?\s*$/;
605
+ function cleanReadSeekErrorText(stderr) {
606
+ return stderr.replace(READSEEK_USAGE_HINT, "").replace(/^error:\s*/i, "").replace(/\s*\n\s*/g, " ").trim();
607
+ }
608
+ function parseReadSeekJson(stdout) {
609
+ try {
610
+ return JSON.parse(stdout);
611
+ } catch {
612
+ const preview = stdout.trim().slice(0, 200);
613
+ throw new Error(`readseek returned invalid JSON: ${preview}`);
614
+ }
615
+ }
616
+ // ../readseek-api/src/process.ts
617
+ var DEFAULT_READSEEK_TIMEOUT_MS = 120000;
618
+ var DEFAULT_MAX_OUTPUT_BYTES = 32 * 1024 * 1024;
619
+ function resolveProcessLimits(options = {}) {
620
+ return {
621
+ timeoutMs: options.timeoutMs ?? DEFAULT_READSEEK_TIMEOUT_MS,
622
+ maxOutputBytes: options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES,
623
+ cancelable: options.cancelable !== false
624
+ };
625
+ }
626
+ function readSeekCancellationError(signal) {
627
+ const reason = signal?.reason;
628
+ return reason instanceof Error && reason.name !== "AbortError" ? reason : new Error("readseek cancelled");
629
+ }
630
+ function failureFromProcessResult(result, options = {}) {
631
+ if (options.overflow) {
632
+ return new Error(`readseek ${options.overflow} exceeded the ${options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES}-byte output limit`);
633
+ }
634
+ if (options.timedOut) {
635
+ return new Error(`readseek timed out after ${options.timeoutMs ?? DEFAULT_READSEEK_TIMEOUT_MS} ms`);
636
+ }
637
+ const cleaned = cleanReadSeekErrorText(result.stderr);
638
+ if (cleaned)
639
+ return new Error(cleaned);
640
+ if (result.signal)
641
+ return new Error(`readseek killed by signal ${result.signal}`);
642
+ return new Error(`readseek exited with status ${result.exitCode}`);
643
+ }
644
+ function ensureSuccessfulStdout(result) {
645
+ if (result.exitCode === 0)
646
+ return result.stdout;
647
+ throw failureFromProcessResult(result);
648
+ }
649
+ function parseSuccessfulJson(result) {
650
+ return parseReadSeekJson(ensureSuccessfulStdout(result));
651
+ }
652
+ // ../readseek-api/src/process-bun.ts
653
+ function getBunSpawn() {
654
+ const bunGlobal = globalThis;
655
+ const spawn = bunGlobal.Bun?.spawn;
656
+ if (typeof spawn !== "function") {
657
+ throw new Error("Bun.spawn is unavailable in this runtime");
658
+ }
659
+ return spawn;
660
+ }
661
+ async function readBoundedText(stream, streamName, maxOutputBytes, onOverflow) {
662
+ const reader = stream.getReader();
663
+ const chunks = [];
664
+ let totalBytes = 0;
665
+ try {
666
+ while (true) {
667
+ const { done, value } = await reader.read();
668
+ if (done)
669
+ break;
670
+ totalBytes += value.byteLength;
671
+ if (totalBytes > maxOutputBytes) {
672
+ onOverflow();
673
+ await reader.cancel().catch(() => {
674
+ return;
675
+ });
676
+ throw failureFromProcessResult({ stdout: "", stderr: "", exitCode: null, signal: null }, { overflow: streamName, maxOutputBytes });
677
+ }
678
+ chunks.push(value);
679
+ }
680
+ } finally {
681
+ reader.releaseLock();
682
+ }
683
+ const bytes = new Uint8Array(totalBytes);
684
+ let offset = 0;
685
+ for (const chunk of chunks) {
686
+ bytes.set(chunk, offset);
687
+ offset += chunk.byteLength;
688
+ }
689
+ return new TextDecoder().decode(bytes);
690
+ }
691
+ async function runReadSeekBun(binaryPath, args, options = {}) {
692
+ const limits = resolveProcessLimits(options);
693
+ if (limits.cancelable && options.signal?.aborted) {
694
+ throw readSeekCancellationError(options.signal);
695
+ }
696
+ const spawn = getBunSpawn();
697
+ const stdin = options.stdin === undefined ? undefined : new TextEncoder().encode(options.stdin);
698
+ const spawnOptions = {
699
+ cwd: options.cwd,
700
+ env: process.env,
701
+ stderr: "pipe",
702
+ stdin,
703
+ stdout: "pipe"
704
+ };
705
+ const child = spawn([binaryPath, ...args], limits.cancelable && options.signal ? { ...spawnOptions, killSignal: "SIGKILL", signal: options.signal } : spawnOptions);
706
+ const killForOverflow = () => {
707
+ try {
708
+ child.kill("SIGKILL");
709
+ } catch {}
710
+ };
711
+ let timeout;
712
+ const timeoutResult = new Promise((_resolve, reject) => {
713
+ timeout = setTimeout(() => {
714
+ try {
715
+ child.kill("SIGKILL");
716
+ } catch {}
717
+ reject(failureFromProcessResult({ stdout: "", stderr: "", exitCode: null, signal: null }, { timedOut: true, timeoutMs: limits.timeoutMs }));
718
+ }, limits.timeoutMs);
719
+ timeout.unref?.();
720
+ });
721
+ const pending = Promise.all([
722
+ readBoundedText(child.stdout, "stdout", limits.maxOutputBytes, killForOverflow),
723
+ readBoundedText(child.stderr, "stderr", limits.maxOutputBytes, killForOverflow),
724
+ child.exited
725
+ ]);
726
+ try {
727
+ const [stdout, stderr, exitCode] = await Promise.race([pending, timeoutResult]);
728
+ if (limits.cancelable && options.signal?.aborted)
729
+ throw readSeekCancellationError(options.signal);
730
+ return { stdout, stderr, exitCode, signal: null };
731
+ } finally {
732
+ if (timeout !== undefined)
733
+ clearTimeout(timeout);
734
+ }
735
+ }
736
+ async function runReadSeekBunText(binaryPath, args, options = {}) {
737
+ return ensureSuccessfulStdout(await runReadSeekBun(binaryPath, args, options));
738
+ }
739
+ async function runReadSeekBunJson(binaryPath, args, options = {}) {
740
+ return parseSuccessfulJson(await runReadSeekBun(binaryPath, args, options));
741
+ }
742
+ // index.ts
743
+ var resolveFromHost = createRequire2(import.meta.url).resolve;
744
+ var DEFAULT_VISION_TIMEOUT_MS = 30 * 60 * 1000;
745
+ var DEFAULT_READ_LIMIT = 2000;
746
+ var EDIT_RESULT_READ_LIMIT = 40;
747
+ var MAX_DOCUMENT_OUTPUT_BYTES = 256 * 1024;
748
+ var MAX_DOCUMENT_OUTPUT_LINES = 2000;
749
+ var TOOL_POLICY = [
750
+ "ReadSeek tool policy:",
751
+ "- Prefer readseek_* tools over built-ins when they can do the job.",
752
+ "- Read with readseek_digest first; readseek_edit needs fresh LINE:HASH anchors.",
753
+ "- Use readseek_grep for text/regex, readseek_search for AST patterns, and readseek_digest/def/refs for symbol navigation.",
754
+ "- Use readseek_edit for existing files, readseek_write for whole-file creation or replacement, and readseek_rename for symbol renames.",
755
+ "- Do not use built-in read, edit, write, grep, or apply_patch when a ReadSeek tool can do the job.",
756
+ "- Run readseek_digest with select=diagnostics after source edits for a quick syntax check."
757
+ ].join(`
758
+ `);
759
+ var OPENCODE_TOOL_ALIASES = Object.fromEntries(TOOL_NAMES.map((name) => [name, `readseek_${name}`]));
760
+ function openCodeToolDescription(name) {
761
+ return renderToolDescription(name, OPENCODE_TOOL_ALIASES);
762
+ }
763
+ var PREFERRED_TOOL_DESCRIPTIONS = {
764
+ readseek_edit: "Preferred tool for editing existing text files with fresh LINE:HASH anchors.",
765
+ readseek_write: "Preferred tool for creating or replacing a complete text file; returns LINE:HASH anchors.",
766
+ readseek_rename: "Use to rename a symbol with binding-aware, verified edits.",
767
+ readseek_grep: "Preferred text/regex search; returns edit-ready LINE:HASH anchors.",
768
+ readseek_search: "Use for AST or structural code search with ast-grep-style patterns.",
769
+ readseek_digest: "Use to digest native facets (metadata, content, map, diagnostics, identity) and return the full envelope.",
770
+ readseek_def: "Use for go-to-definition or declaration lookup by name.",
771
+ readseek_refs: "Use to find identifier-name matches or binding-specific references before rename or delete.",
772
+ readseek_view: "Use to digest document structure or selected content."
773
+ };
774
+ var FALLBACK_TOOL_DESCRIPTIONS = {
775
+ edit: "Fallback only. Prefer readseek_edit with fresh LINE:HASH anchors.",
776
+ write: "Fallback only. Prefer readseek_write so the result returns LINE:HASH anchors.",
777
+ grep: "Fallback only. Prefer readseek_grep for text/regex search with edit-ready anchors.",
778
+ apply_patch: "Fallback only. Prefer readseek_edit for existing text files."
779
+ };
780
+ var IMAGE_MODES = [...VISION_MODES];
781
+ var VISION_ANALYSIS_MODE_VALUES = [...VISION_ANALYSIS_MODES];
782
+ var VISION_LEVEL_VALUES = [...VISION_LEVELS];
783
+ var DOCUMENT_NODE_KIND_VALUES = [...DOCUMENT_NODE_KINDS];
784
+ function resolveImagePolicy(options) {
785
+ const value = options?.imageMode;
786
+ if (value === undefined)
787
+ return "auto";
788
+ if (value === "on" || value === "auto" || value === "off")
789
+ return value;
790
+ throw new Error('opencode-readseek imageMode must be "on", "auto", or "off"');
791
+ }
792
+ function digestArgs(filePath, select, extra = []) {
793
+ return [...buildDigestArgs({ path: filePath, select }), ...extra];
794
+ }
795
+ function requireDigestFacet(envelope, facet) {
796
+ const value = record(envelope)[facet];
797
+ if (value === undefined || value === null || typeof value !== "object") {
798
+ throw new Error(`digest returned no ${facet} facet`);
799
+ }
800
+ return value;
801
+ }
802
+ class SessionAnchors {
803
+ #pathsBySession = new Map;
804
+ #renamePlans = new Map;
805
+ mark(sessionID, filePath) {
806
+ let paths = this.#pathsBySession.get(sessionID);
807
+ if (!paths) {
808
+ paths = new Set;
809
+ this.#pathsBySession.set(sessionID, paths);
810
+ }
811
+ paths.add(filePath);
812
+ }
813
+ forget(filePath) {
814
+ const absolutePath = path2.resolve(filePath);
815
+ for (const paths of this.#pathsBySession.values())
816
+ paths.delete(absolutePath);
817
+ for (const [sessionID, plan] of this.#renamePlans) {
818
+ if (plan.files.has(absolutePath))
819
+ this.#renamePlans.delete(sessionID);
820
+ }
821
+ }
822
+ deleteSession(sessionID) {
823
+ this.#pathsBySession.delete(sessionID);
824
+ this.#renamePlans.delete(sessionID);
825
+ }
826
+ planRename(sessionID, output) {
827
+ if (!output || typeof output !== "object")
828
+ return;
829
+ const record = output;
830
+ if (record.applied === true) {
831
+ this.#renamePlans.delete(sessionID);
832
+ return;
833
+ }
834
+ const { file, old_name: oldName, new_name: newName, others } = record;
835
+ if (typeof file !== "string" || typeof oldName !== "string" || typeof newName !== "string")
836
+ return;
837
+ const files = new Set([path2.resolve(file)]);
838
+ if (Array.isArray(others)) {
839
+ for (const item of others) {
840
+ if (!item || typeof item !== "object")
841
+ continue;
842
+ const otherFile = item.file;
843
+ if (typeof otherFile === "string")
844
+ files.add(path2.resolve(otherFile));
845
+ }
846
+ }
847
+ this.#renamePlans.set(sessionID, { summary: `${oldName} -> ${newName}`, files });
848
+ }
849
+ render(sessionID) {
850
+ const paths = this.#pathsBySession.get(sessionID);
851
+ const sections = [];
852
+ if (paths?.size) {
853
+ sections.push(`## ReadSeek Anchors
854
+ The following files have fresh LINE:HASH anchors:
855
+ ${[...paths].sort().map((filePath) => `- ${filePath}`).join(`
856
+ `)}`);
857
+ }
858
+ const renamePlan = this.#renamePlans.get(sessionID);
859
+ if (renamePlan)
860
+ sections.push(`## Pending ReadSeek Rename Plan
861
+ - ${renamePlan.summary}`);
862
+ return sections.length === 0 ? undefined : sections.join(`
863
+
864
+ `);
865
+ }
866
+ }
867
+ function resolvePath(directory, filePath) {
868
+ return path2.resolve(directory, filePath);
869
+ }
870
+ function containsPath(directory, filePath) {
871
+ const relative = path2.relative(directory, filePath);
872
+ return relative === "" || !relative.startsWith(`..${path2.sep}`) && relative !== ".." && !path2.isAbsolute(relative);
873
+ }
874
+ async function resolveSymlinks(filePath) {
875
+ let unresolved = path2.resolve(filePath);
876
+ let symlinks = 0;
877
+ while (true) {
878
+ const parsed = path2.parse(unresolved);
879
+ const parts = unresolved.slice(parsed.root.length).split(path2.sep).filter(Boolean);
880
+ let resolved = parsed.root;
881
+ let restart = false;
882
+ for (let index = 0;index < parts.length; index++) {
883
+ const candidate = path2.join(resolved, parts[index]);
884
+ const info = await lstat(candidate);
885
+ if (!info.isSymbolicLink()) {
886
+ resolved = candidate;
887
+ continue;
888
+ }
889
+ symlinks++;
890
+ if (symlinks > 40) {
891
+ const error = new Error(`too many symbolic links: ${filePath}`);
892
+ error.code = "ELOOP";
893
+ throw error;
894
+ }
895
+ const target = await readlink(candidate);
896
+ unresolved = path2.resolve(path2.dirname(candidate), target, ...parts.slice(index + 1));
897
+ restart = true;
898
+ break;
899
+ }
900
+ if (!restart)
901
+ return resolved;
902
+ }
903
+ }
904
+ async function portableRealpath(filePath) {
905
+ try {
906
+ return await realpath(filePath);
907
+ } catch (error) {
908
+ if (error.code !== "EACCES")
909
+ throw error;
910
+ return resolveSymlinks(filePath);
911
+ }
912
+ }
913
+ async function canonicalPath(filePath) {
914
+ const missing = [];
915
+ let existing = filePath;
916
+ while (true) {
917
+ try {
918
+ return path2.join(await portableRealpath(existing), ...missing.reverse());
919
+ } catch (error) {
920
+ if (error.code !== "ENOENT")
921
+ throw error;
922
+ const parent = path2.dirname(existing);
923
+ if (parent === existing)
924
+ throw error;
925
+ missing.push(path2.basename(existing));
926
+ existing = parent;
927
+ }
928
+ }
929
+ }
930
+ async function authorizeExternal(context, filePath) {
931
+ const [accessPath, directory, worktree] = await Promise.all([
932
+ canonicalPath(filePath),
933
+ canonicalPath(context.directory),
934
+ context.worktree === "/" ? Promise.resolve("/") : canonicalPath(context.worktree)
935
+ ]);
936
+ if (containsPath(directory, accessPath) || worktree !== "/" && containsPath(worktree, accessPath)) {
937
+ return;
938
+ }
939
+ const info = await stat(accessPath).catch(() => {
940
+ return;
941
+ });
942
+ const parentDir = info?.isDirectory() ? accessPath : path2.dirname(accessPath);
943
+ const pattern = path2.join(parentDir, "*").replaceAll("\\", "/");
944
+ await context.ask({
945
+ permission: "external_directory",
946
+ patterns: [pattern],
947
+ always: [pattern],
948
+ metadata: { filepath: filePath, parentDir }
949
+ });
950
+ }
951
+ async function rejectSymlinkMutation(filePath) {
952
+ const info = await lstat(filePath).catch((error) => {
953
+ if (error.code === "ENOENT")
954
+ return;
955
+ throw error;
956
+ });
957
+ if (info?.isSymbolicLink())
958
+ throw new Error(`refusing to modify symbolic link: ${filePath}`);
959
+ }
960
+ async function authorizeRead(context, filePath) {
961
+ await authorizeExternal(context, filePath);
962
+ await context.ask({
963
+ permission: "read",
964
+ patterns: [path2.relative(context.worktree, filePath).replaceAll("\\", "/")],
965
+ always: ["*"],
966
+ metadata: {}
967
+ });
968
+ }
969
+ async function authorizeSearch(context, filePath, pattern) {
970
+ await context.ask({
971
+ permission: "grep",
972
+ patterns: [pattern],
973
+ always: ["*"],
974
+ metadata: { pattern, path: filePath }
975
+ });
976
+ await authorizeExternal(context, filePath);
977
+ }
978
+ async function authorizeEdit(context, filePaths, diff = "", authorizeExternalPaths = true) {
979
+ if (authorizeExternalPaths) {
980
+ for (const filePath of filePaths)
981
+ await authorizeExternal(context, filePath);
982
+ }
983
+ const patterns = filePaths.map((filePath) => path2.relative(context.worktree, filePath).replaceAll("\\", "/"));
984
+ await context.ask({
985
+ permission: "edit",
986
+ patterns,
987
+ always: ["*"],
988
+ metadata: { filepath: filePaths.join(", "), diff }
989
+ });
990
+ }
991
+ function withAt(args, line, column) {
992
+ args.push("--at", buildLineLocation(line, column));
993
+ }
994
+ function readRangeArgs(filePath, start, end) {
995
+ const extra = [];
996
+ if (start > 1)
997
+ withAt(extra, start);
998
+ extra.push("--end", String(end));
999
+ return digestArgs(filePath, "content", extra);
1000
+ }
1001
+ async function runDigestContent(context, args, options = {}) {
1002
+ return requireDigestFacet(await runReadSeek(context, args, options), "content");
1003
+ }
1004
+ function readSeekBinaryPath() {
1005
+ return resolveReadSeekBinaryFromPackages(resolveFromHost);
1006
+ }
1007
+ async function preserveAbortReason(signal, cancelable, invoke) {
1008
+ try {
1009
+ const value = await invoke();
1010
+ if (cancelable)
1011
+ signal.throwIfAborted();
1012
+ return value;
1013
+ } catch (error) {
1014
+ if (cancelable)
1015
+ signal.throwIfAborted();
1016
+ throw error;
1017
+ }
1018
+ }
1019
+ async function runReadSeekRaw(context, args, options = {}) {
1020
+ context.abort.throwIfAborted();
1021
+ return preserveAbortReason(context.abort, options.cancelable !== false, () => runReadSeekBunText(readSeekBinaryPath(), args, {
1022
+ cwd: context.directory,
1023
+ signal: context.abort,
1024
+ cancelable: options.cancelable,
1025
+ stdin: options.stdin,
1026
+ timeoutMs: options.timeoutMs
1027
+ }));
1028
+ }
1029
+ async function runReadSeek(context, args, options = {}) {
1030
+ context.abort.throwIfAborted();
1031
+ return preserveAbortReason(context.abort, options.cancelable !== false, () => runReadSeekBunJson(readSeekBinaryPath(), args, {
1032
+ cwd: context.directory,
1033
+ signal: context.abort,
1034
+ cancelable: options.cancelable,
1035
+ stdin: options.stdin,
1036
+ timeoutMs: options.timeoutMs
1037
+ }));
1038
+ }
1039
+ async function postMutationPreview(context, args, filePath, changed) {
1040
+ try {
1041
+ return await runDigestContent(context, args, {
1042
+ cancelable: false,
1043
+ timeoutMs: DEFAULT_READSEEK_TIMEOUT_MS
1044
+ });
1045
+ } catch (error) {
1046
+ const previewError = error instanceof Error ? error.message : String(error);
1047
+ return {
1048
+ file: filePath,
1049
+ changed,
1050
+ preview_error: previewError,
1051
+ warning: "File mutation completed, but the post-write preview failed. Do not retry the mutation; read the file to verify content and refresh anchors."
1052
+ };
1053
+ }
1054
+ }
1055
+ var readSeekCacheInit;
1056
+ function readSeekCacheDir() {
1057
+ const xdgCacheHome = process.env.XDG_CACHE_HOME?.trim();
1058
+ const cacheHome = xdgCacheHome || path2.join(homedir(), ".cache");
1059
+ return path2.join(cacheHome, "opencode", "readseek");
1060
+ }
1061
+ async function ensureReadSeekCache(context) {
1062
+ const cacheDir = readSeekCacheDir();
1063
+ try {
1064
+ const info = await stat(cacheDir);
1065
+ if (!info.isDirectory())
1066
+ throw new Error(`readseek cache is not a directory: ${cacheDir}`);
1067
+ return cacheDir;
1068
+ } catch (error) {
1069
+ if (error.code !== "ENOENT")
1070
+ throw error;
1071
+ }
1072
+ readSeekCacheInit ??= runReadSeekRaw(context, ["--readseek-dir", cacheDir, "init"]).then(() => cacheDir).finally(() => {
1073
+ readSeekCacheInit = undefined;
1074
+ });
1075
+ return readSeekCacheInit;
1076
+ }
1077
+ function render(value) {
1078
+ return JSON.stringify(value, null, 2);
1079
+ }
1080
+ function collectAnchoredFiles(value, files) {
1081
+ if (Array.isArray(value)) {
1082
+ for (const item of value)
1083
+ collectAnchoredFiles(item, files);
1084
+ return;
1085
+ }
1086
+ if (!value || typeof value !== "object")
1087
+ return;
1088
+ const output = value;
1089
+ const hashlines = output.hashlines;
1090
+ if (typeof output.file === "string" && Array.isArray(hashlines) && hashlines.some((line) => typeof record(line).line === "number" && typeof record(line).hash === "string"))
1091
+ files.add(output.file);
1092
+ for (const item of Object.values(output))
1093
+ collectAnchoredFiles(item, files);
1094
+ }
1095
+ var fileMutationTails = new Map;
1096
+ var MAX_PERMISSION_DIFF_BYTES = 32 * 1024;
1097
+ async function withFileMutationQueue(filePath, operation) {
1098
+ const key = path2.resolve(filePath);
1099
+ const previous = fileMutationTails.get(key);
1100
+ let release = () => {};
1101
+ const current = new Promise((resolve) => {
1102
+ release = resolve;
1103
+ });
1104
+ fileMutationTails.set(key, current);
1105
+ if (previous)
1106
+ await previous;
1107
+ try {
1108
+ return await operation();
1109
+ } finally {
1110
+ release();
1111
+ if (fileMutationTails.get(key) === current)
1112
+ fileMutationTails.delete(key);
1113
+ }
1114
+ }
1115
+ function validateEdits(value) {
1116
+ if (!Array.isArray(value) || value.length === 0)
1117
+ throw new Error("edits must contain at least one edit");
1118
+ const requiredFields = {
1119
+ set_line: ["anchor", "new_text"],
1120
+ replace_lines: ["end_anchor", "new_text", "start_anchor"],
1121
+ insert_after: ["anchor", "new_text"],
1122
+ replace: ["new_text", "old_text"],
1123
+ replace_symbol: ["new_body", "symbol"]
1124
+ };
1125
+ for (const [index, item] of value.entries()) {
1126
+ if (!item || typeof item !== "object")
1127
+ throw new Error(`edits[${index}] must be an edit object`);
1128
+ const keys = Object.keys(item);
1129
+ const variant = keys[0];
1130
+ if (keys.length !== 1 || !Object.hasOwn(requiredFields, variant)) {
1131
+ throw new Error(`edits[${index}] must contain exactly one of: set_line, replace_lines, insert_after, replace, replace_symbol`);
1132
+ }
1133
+ const payload = item[variant];
1134
+ if (!payload || typeof payload !== "object")
1135
+ throw new Error(`edits[${index}].${variant} must be an object`);
1136
+ const expectedFields = requiredFields[variant];
1137
+ const allowedFields = variant === "replace" ? [...expectedFields, "all"] : expectedFields;
1138
+ const actualFields = Object.keys(payload).sort();
1139
+ if (actualFields.some((field) => !allowedFields.includes(field)) || expectedFields.some((field) => !actualFields.includes(field))) {
1140
+ throw new Error(`edits[${index}].${variant} contains invalid fields`);
1141
+ }
1142
+ const values = payload;
1143
+ for (const field of expectedFields) {
1144
+ if (typeof values[field] !== "string") {
1145
+ throw new Error(`edits[${index}].${variant}.${field} must be a string`);
1146
+ }
1147
+ }
1148
+ if (variant === "replace" && values.all !== undefined && typeof values.all !== "boolean") {
1149
+ throw new Error(`edits[${index}].replace.all must be a boolean`);
1150
+ }
1151
+ for (const field of expectedFields.filter((field2) => field2.endsWith("anchor"))) {
1152
+ parseAnchor(values[field]);
1153
+ }
1154
+ }
1155
+ }
1156
+ function parseAnchor(anchor) {
1157
+ const match = /^(\d+):([0-9a-fA-F]{3})$/.exec(anchor.trim());
1158
+ if (!match)
1159
+ throw new Error(`invalid LINE:HASH anchor: ${anchor}`);
1160
+ const line = Number(match[1]);
1161
+ if (line === 0)
1162
+ throw new Error(`anchor line must be greater than zero: ${anchor}`);
1163
+ return { line, hash: match[2].toLowerCase() };
1164
+ }
1165
+ function firstChangedLine(before, after) {
1166
+ const beforeLines = normalizeToLf(before).split(`
1167
+ `);
1168
+ const afterLines = normalizeToLf(after).split(`
1169
+ `);
1170
+ const count = Math.max(beforeLines.length, afterLines.length);
1171
+ for (let index = 0;index < count; index++) {
1172
+ if (beforeLines[index] !== afterLines[index])
1173
+ return index + 1;
1174
+ }
1175
+ return 1;
1176
+ }
1177
+ async function readTextSnapshot(filePath) {
1178
+ try {
1179
+ return { exists: true, content: await readFile(filePath, "utf8") };
1180
+ } catch (error) {
1181
+ if (error.code === "ENOENT")
1182
+ return { exists: false, content: "" };
1183
+ throw error;
1184
+ }
1185
+ }
1186
+ async function writeHandle(handle, content) {
1187
+ const data = Buffer.from(content, "utf8");
1188
+ await handle.truncate(0);
1189
+ let offset = 0;
1190
+ while (offset < data.length) {
1191
+ const { bytesWritten } = await handle.write(data, offset, data.length - offset, offset);
1192
+ if (bytesWritten === 0)
1193
+ throw new Error("write made no progress");
1194
+ offset += bytesWritten;
1195
+ }
1196
+ }
1197
+ async function writeText(filePath, expected, content) {
1198
+ await mkdir(path2.dirname(filePath), { recursive: true });
1199
+ if (!expected.exists) {
1200
+ let handle2;
1201
+ try {
1202
+ handle2 = await open(filePath, "wx+");
1203
+ } catch (error) {
1204
+ if (error.code === "EEXIST") {
1205
+ throw new Error(`refusing to overwrite changed file: ${filePath}`);
1206
+ }
1207
+ throw error;
1208
+ }
1209
+ try {
1210
+ await writeHandle(handle2, content);
1211
+ } finally {
1212
+ await handle2.close();
1213
+ }
1214
+ return;
1215
+ }
1216
+ const pathInfo = await lstat(filePath);
1217
+ if (pathInfo.isSymbolicLink())
1218
+ throw new Error(`refusing to modify symbolic link: ${filePath}`);
1219
+ const handle = await open(filePath, "r+");
1220
+ try {
1221
+ const handleInfo = await handle.stat();
1222
+ const currentPathInfo = await lstat(filePath);
1223
+ if (currentPathInfo.isSymbolicLink() || currentPathInfo.dev !== handleInfo.dev || currentPathInfo.ino !== handleInfo.ino) {
1224
+ throw new Error(`refusing to overwrite changed file: ${filePath}`);
1225
+ }
1226
+ const current = (await handle.readFile()).toString("utf8");
1227
+ if (current !== expected.content)
1228
+ throw new Error(`refusing to overwrite changed file: ${filePath}`);
1229
+ if (current !== content)
1230
+ await writeHandle(handle, content);
1231
+ } finally {
1232
+ await handle.close();
1233
+ }
1234
+ }
1235
+ function renameFiles(value) {
1236
+ const output = record(value);
1237
+ const files = [];
1238
+ if (typeof output.file === "string" && items(output.edits).length > 0)
1239
+ files.push(path2.resolve(output.file));
1240
+ for (const other of items(output.others)) {
1241
+ const item = record(other);
1242
+ if (typeof item.file === "string" && items(item.edits).length > 0)
1243
+ files.push(path2.resolve(item.file));
1244
+ }
1245
+ return files;
1246
+ }
1247
+ function normalizeToLf(content) {
1248
+ return content.replace(/\r\n/g, `
1249
+ `).replace(/\r/g, `
1250
+ `);
1251
+ }
1252
+ function diffLines(content) {
1253
+ if (content === "")
1254
+ return [];
1255
+ const lines = content.split(`
1256
+ `);
1257
+ if (lines.at(-1) === "")
1258
+ lines.pop();
1259
+ return lines.map((line) => line.replace(/\r/g, "\\r"));
1260
+ }
1261
+ function hasFinalNewline(content) {
1262
+ return content === "" || /[\r\n]$/.test(content);
1263
+ }
1264
+ function diffRange(start, count) {
1265
+ return count === 0 ? `${start},0` : `${start + 1},${count}`;
1266
+ }
1267
+ function utf8Prefix(value, maxBytes) {
1268
+ const bytes = Buffer.from(value, "utf8");
1269
+ if (bytes.length <= maxBytes)
1270
+ return value;
1271
+ let end = maxBytes;
1272
+ const decoder = new TextDecoder("utf-8", { fatal: true });
1273
+ while (end > 0) {
1274
+ try {
1275
+ return decoder.decode(bytes.subarray(0, end));
1276
+ } catch {
1277
+ end--;
1278
+ }
1279
+ }
1280
+ return "";
1281
+ }
1282
+ function boundDocumentOutput(value) {
1283
+ const lines = value.split(`
1284
+ `);
1285
+ let output = lines.length > MAX_DOCUMENT_OUTPUT_LINES ? lines.slice(0, MAX_DOCUMENT_OUTPUT_LINES).join(`
1286
+ `) : value;
1287
+ const truncated = output !== value || Buffer.byteLength(output, "utf8") > MAX_DOCUMENT_OUTPUT_BYTES;
1288
+ output = utf8Prefix(output, MAX_DOCUMENT_OUTPUT_BYTES);
1289
+ return truncated ? `${output}
1290
+ [… document view truncated; narrow it with page, node, kind, or depth]` : output;
1291
+ }
1292
+ function previewDiffLine(prefix, line) {
1293
+ const value = `${prefix}${line}`;
1294
+ const maxBytes = 256;
1295
+ if (Buffer.byteLength(value) <= maxBytes)
1296
+ return value;
1297
+ return `${utf8Prefix(value, maxBytes - Buffer.byteLength("…"))}…`;
1298
+ }
1299
+ function sampleDiffLines(lines, start, end, prefix) {
1300
+ const sampleSize = 20;
1301
+ const count = end - start;
1302
+ if (count <= sampleSize * 2) {
1303
+ return lines.slice(start, end).map((line) => previewDiffLine(prefix, line));
1304
+ }
1305
+ return [
1306
+ ...lines.slice(start, start + sampleSize).map((line) => previewDiffLine(prefix, line)),
1307
+ ` ${count - sampleSize * 2} ${prefix === "-" ? "removed" : "added"} lines omitted`,
1308
+ ...lines.slice(end - sampleSize, end).map((line) => previewDiffLine(prefix, line))
1309
+ ];
1310
+ }
1311
+ function simpleDiff(filePath, before, after) {
1312
+ if (before === after)
1313
+ return "";
1314
+ const oldLines = diffLines(before);
1315
+ const newLines = diffLines(after);
1316
+ const oldHasFinalNewline = hasFinalNewline(before);
1317
+ const newHasFinalNewline = hasFinalNewline(after);
1318
+ let prefix = 0;
1319
+ while (prefix < oldLines.length && prefix < newLines.length && oldLines[prefix] === newLines[prefix])
1320
+ prefix++;
1321
+ let suffix = 0;
1322
+ while (suffix < oldLines.length - prefix && suffix < newLines.length - prefix && oldLines[oldLines.length - 1 - suffix] === newLines[newLines.length - 1 - suffix])
1323
+ suffix++;
1324
+ if (prefix === oldLines.length && prefix === newLines.length) {
1325
+ prefix = Math.max(0, prefix - 1);
1326
+ suffix = 0;
1327
+ }
1328
+ const oldChangeEnd = oldLines.length - suffix;
1329
+ const newChangeEnd = newLines.length - suffix;
1330
+ const oldStart = Math.max(0, prefix - 3);
1331
+ const newStart = Math.max(0, prefix - 3);
1332
+ const oldEnd = Math.min(oldLines.length, oldChangeEnd + 3);
1333
+ const newEnd = Math.min(newLines.length, newChangeEnd + 3);
1334
+ const safePath = utf8Prefix(filePath.replace(/[\r\n]/g, ""), 1024);
1335
+ const header = [
1336
+ `--- ${safePath}`,
1337
+ `+++ ${safePath}`,
1338
+ `@@ -${diffRange(oldStart, oldEnd - oldStart)} +${diffRange(newStart, newEnd - newStart)} @@`
1339
+ ];
1340
+ const fullDiff = [...header];
1341
+ let fullDiffBytes = Buffer.byteLength(header.join(`
1342
+ `));
1343
+ let complete = true;
1344
+ const ranges = [
1345
+ [oldLines, oldStart, prefix, " ", oldHasFinalNewline],
1346
+ [oldLines, prefix, oldChangeEnd, "-", oldHasFinalNewline],
1347
+ [newLines, prefix, newChangeEnd, "+", newHasFinalNewline],
1348
+ [newLines, newChangeEnd, newEnd, " ", newHasFinalNewline]
1349
+ ];
1350
+ outer:
1351
+ for (const [lines, start, end, marker, hasNewline] of ranges) {
1352
+ for (let index = start;index < end; index++) {
1353
+ const lineBytes = 2 + Buffer.byteLength(lines[index]);
1354
+ if (fullDiffBytes + lineBytes > MAX_PERMISSION_DIFF_BYTES) {
1355
+ complete = false;
1356
+ break outer;
1357
+ }
1358
+ fullDiff.push(`${marker}${lines[index]}`);
1359
+ fullDiffBytes += lineBytes;
1360
+ if (index === lines.length - 1 && !hasNewline) {
1361
+ const eofMarker = "\";
1362
+ if (fullDiffBytes + Buffer.byteLength(eofMarker) + 1 > MAX_PERMISSION_DIFF_BYTES) {
1363
+ complete = false;
1364
+ break outer;
1365
+ }
1366
+ fullDiff.push(eofMarker);
1367
+ fullDiffBytes += Buffer.byteLength(eofMarker) + 1;
1368
+ }
1369
+ }
1370
+ }
1371
+ if (complete)
1372
+ return fullDiff.join(`
1373
+ `);
1374
+ const newlineSummary = oldHasFinalNewline === newHasFinalNewline ? [] : [` final newline: ${oldHasFinalNewline ? "present" : "missing"} -> ${newHasFinalNewline ? "present" : "missing"}`];
1375
+ return [
1376
+ ...header,
1377
+ ` diff truncated: ${oldChangeEnd - prefix} removed, ${newChangeEnd - prefix} added lines`,
1378
+ ...newlineSummary,
1379
+ ...sampleDiffLines(oldLines, prefix, oldChangeEnd, "-"),
1380
+ ...sampleDiffLines(newLines, prefix, newChangeEnd, "+")
1381
+ ].join(`
1382
+ `);
1383
+ }
1384
+ function identifiedName(value) {
1385
+ if (!value || typeof value !== "object")
1386
+ return;
1387
+ const identifier = value.identifier;
1388
+ if (!identifier || typeof identifier !== "object")
1389
+ return;
1390
+ const text = identifier.text;
1391
+ return typeof text === "string" ? text : undefined;
1392
+ }
1393
+ function record(value) {
1394
+ return value && typeof value === "object" ? value : {};
1395
+ }
1396
+ function items(value) {
1397
+ return Array.isArray(value) ? value : [];
1398
+ }
1399
+ function initialTitle(kind, args) {
1400
+ switch (kind) {
1401
+ case "read":
1402
+ return `Read ${args.path}`;
1403
+ case "view":
1404
+ return `View ${args.path}`;
1405
+ case "digest":
1406
+ return `Digest ${args.path}`;
1407
+ case "search":
1408
+ return `Search ${args.pattern}`;
1409
+ case "grep":
1410
+ return `Grep ${args.pattern}`;
1411
+ case "def":
1412
+ return `Find definition ${args.name}`;
1413
+ case "refs":
1414
+ return `Find references to ${args.name}`;
1415
+ case "rename":
1416
+ return `Rename ${args.path}:${args.line} to ${args.to}`;
1417
+ case "edit":
1418
+ return `Edit ${args.path}`;
1419
+ case "write":
1420
+ return `Write ${args.path}`;
1421
+ }
1422
+ }
1423
+ function resultPresentation(kind, args, value) {
1424
+ const output = record(value);
1425
+ switch (kind) {
1426
+ case "read": {
1427
+ const startLine = output.start_line;
1428
+ const endLine = output.end_line;
1429
+ if (typeof startLine === "number" && typeof endLine === "number") {
1430
+ return {
1431
+ title: `Read ${args.path}:${startLine}-${endLine}`,
1432
+ metadata: { start_line: startLine, end_line: endLine, line_count: endLine - startLine + 1 }
1433
+ };
1434
+ }
1435
+ const width = output.width;
1436
+ const height = output.height;
1437
+ if (typeof width === "number" && typeof height === "number") {
1438
+ return { title: `Read ${args.path} (${width}x${height})`, metadata: { width, height } };
1439
+ }
1440
+ break;
1441
+ }
1442
+ case "view":
1443
+ return { title: `Viewed ${args.path}`, metadata: {} };
1444
+ case "digest": {
1445
+ const map = record(output.map);
1446
+ const diagnostics = record(output.diagnostics);
1447
+ const identity = record(output.identity);
1448
+ const content = record(output.content);
1449
+ const metadata = {};
1450
+ const summary = [];
1451
+ if (output.map !== undefined) {
1452
+ const symbols = items(map.symbols).length;
1453
+ metadata.symbols = symbols;
1454
+ summary.push(`${symbols} symbols`);
1455
+ }
1456
+ if (output.diagnostics !== undefined) {
1457
+ const errors = typeof diagnostics.error_count === "number" ? diagnostics.error_count : 0;
1458
+ const missing = typeof diagnostics.missing_count === "number" ? diagnostics.missing_count : 0;
1459
+ metadata.error_count = errors;
1460
+ metadata.missing_count = missing;
1461
+ summary.push(`${errors} errors, ${missing} missing`);
1462
+ }
1463
+ if (output.identity !== undefined) {
1464
+ if (typeof identity.line === "number")
1465
+ metadata.line = identity.line;
1466
+ if (typeof identity.column === "number")
1467
+ metadata.column = identity.column;
1468
+ const identifier = record(identity.identifier).text;
1469
+ if (typeof identifier === "string")
1470
+ summary.push(identifier);
1471
+ }
1472
+ if (typeof content.start_line === "number")
1473
+ metadata.start_line = content.start_line;
1474
+ if (typeof content.end_line === "number")
1475
+ metadata.end_line = content.end_line;
1476
+ return {
1477
+ title: summary.length > 0 ? `Digested ${args.path} (${summary.join(", ")})` : `Digested ${args.path}`,
1478
+ metadata
1479
+ };
1480
+ }
1481
+ case "search": {
1482
+ const results = items(output.results);
1483
+ const matches = results.reduce((total, result) => total + items(record(result).matches).length, 0);
1484
+ return { title: `Found ${matches} matches`, metadata: { results: results.length, matches } };
1485
+ }
1486
+ case "grep": {
1487
+ const results = items(output.results);
1488
+ const matches = results.reduce((total, result) => total + items(record(result).matches).length, 0);
1489
+ return { title: `Found ${matches} matches`, metadata: { results: results.length, matches } };
1490
+ }
1491
+ case "def": {
1492
+ const locations = items(output.locations).length;
1493
+ return { title: `Found ${locations} definitions`, metadata: { locations } };
1494
+ }
1495
+ case "refs": {
1496
+ const references = items(output.references).length;
1497
+ return { title: `Found ${references} references`, metadata: { references } };
1498
+ }
1499
+ case "rename": {
1500
+ const oldName = output.old_name;
1501
+ const newName = output.new_name;
1502
+ const otherOutputs = items(output.others).map(record);
1503
+ const edits = otherOutputs.reduce((total, item) => total + items(item.edits).length, items(output.edits).length);
1504
+ const conflicts = otherOutputs.reduce((total, item) => total + items(item.conflicts).length, items(output.conflicts).length);
1505
+ const others = otherOutputs.length;
1506
+ const title = typeof oldName === "string" && typeof newName === "string" ? `${output.applied === true ? "Renamed" : "Plan"} ${oldName} -> ${newName}` : initialTitle(kind, args);
1507
+ return { title, metadata: { edits, conflicts, others } };
1508
+ }
1509
+ case "edit":
1510
+ case "write": {
1511
+ const preview = typeof output.preview_error === "string" ? " (preview unavailable)" : "";
1512
+ return { title: `${kind === "edit" ? "Edited" : "Wrote"} ${args.path}${preview}`, metadata: {} };
1513
+ }
1514
+ }
1515
+ return { title: initialTitle(kind, args), metadata: {} };
1516
+ }
1517
+ function readseekTool(description, args, kind, execute, formatResult = (result) => ({ output: render(result) })) {
1518
+ return tool({
1519
+ description,
1520
+ args,
1521
+ async execute(args2, context) {
1522
+ const title = initialTitle(kind, args2);
1523
+ context.metadata({ title });
1524
+ const result = await execute(args2, context);
1525
+ const presentation = resultPresentation(kind, args2, result);
1526
+ return {
1527
+ title: presentation.title,
1528
+ ...formatResult(result),
1529
+ metadata: presentation.metadata
1530
+ };
1531
+ }
1532
+ });
1533
+ }
1534
+ var ReadSeekPlugin = async (_input, options) => {
1535
+ const anchors = new SessionAnchors;
1536
+ const imagePolicy = resolveImagePolicy(options);
1537
+ const imageModes = imagePolicy === "auto" ? ["none", "all", "ocr", "caption", "objects"] : ["all", "ocr", "caption", "objects"];
1538
+ return {
1539
+ tool: {
1540
+ readseek_view: readseekTool(openCodeToolDescription("view"), {
1541
+ path: tool.schema.string().describe("Document path relative to the project directory"),
1542
+ node: tool.schema.string().min(1).optional().describe(PARAM_DESCRIPTIONS.node),
1543
+ page: tool.schema.number().int().positive().optional().describe(PARAM_DESCRIPTIONS.page),
1544
+ kind: tool.schema.enum(DOCUMENT_NODE_KIND_VALUES).optional().describe(PARAM_DESCRIPTIONS.kind),
1545
+ depth: tool.schema.number().int().nonnegative().optional().describe(PARAM_DESCRIPTIONS.depth),
1546
+ outline: tool.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.outline),
1547
+ visionMode: tool.schema.enum(VISION_ANALYSIS_MODE_VALUES).optional().describe(PARAM_DESCRIPTIONS.visionModeView),
1548
+ visionLevel: tool.schema.enum(VISION_LEVEL_VALUES).optional().describe(PARAM_DESCRIPTIONS.visionLevel)
1549
+ }, "view", async (input, context) => {
1550
+ const filePath = resolvePath(context.directory, input.path);
1551
+ await authorizeRead(context, filePath);
1552
+ const visionMode = input.visionMode;
1553
+ const visionLevel = input.visionLevel;
1554
+ const vision = validateVisionLevelVsMode({
1555
+ visionMode,
1556
+ visionLevel,
1557
+ outline: input.outline
1558
+ });
1559
+ if (!vision.ok)
1560
+ throw new Error(vision.error);
1561
+ const cacheDir = await ensureReadSeekCache(context);
1562
+ const args = buildViewArgs({
1563
+ path: filePath,
1564
+ node: input.node,
1565
+ page: input.page,
1566
+ kind: input.kind,
1567
+ depth: input.depth,
1568
+ outline: input.outline,
1569
+ visionMode,
1570
+ visionLevel,
1571
+ readseekDir: cacheDir
1572
+ });
1573
+ return runReadSeekRaw(context, args, visionMode === undefined ? {} : { timeoutMs: DEFAULT_VISION_TIMEOUT_MS });
1574
+ }, (result) => ({ output: boundDocumentOutput(String(result)) })),
1575
+ readseek_digest: readseekTool(openCodeToolDescription("digest"), {
1576
+ path: tool.schema.string().describe("Path relative to the project directory"),
1577
+ select: tool.schema.string().optional().describe("Comma-separated facets: metadata, content, map, diagnostics, identity; defaults to content"),
1578
+ at: tool.schema.string().optional().describe(PARAM_DESCRIPTIONS.at),
1579
+ end: tool.schema.number().int().positive().optional().describe("Last line to include; content facet only"),
1580
+ limit: tool.schema.number().int().positive().optional().describe("Maximum number of lines; content facet only"),
1581
+ language: tool.schema.string().optional().describe(PARAM_DESCRIPTIONS.language),
1582
+ visionMode: tool.schema.enum(IMAGE_MODES).optional().describe("Standalone image mode for the content facet: none, all, ocr, caption, or objects"),
1583
+ visionLevel: tool.schema.enum(VISION_LEVEL_VALUES).optional().describe("Vision inference level: low (default), medium, or high; content facet only")
1584
+ }, "digest", async (input, context) => {
1585
+ const filePath = resolvePath(context.directory, input.path);
1586
+ await authorizeRead(context, filePath);
1587
+ const select = (input.select ?? "content").trim() || "content";
1588
+ const visionMode = input.visionMode;
1589
+ const visionLevel = input.visionLevel;
1590
+ const vision = validateVisionLevelVsMode({ visionMode, visionLevel });
1591
+ if (!vision.ok)
1592
+ throw new Error(vision.error);
1593
+ const cacheDir = await ensureReadSeekCache(context);
1594
+ const args = buildDigestArgs({
1595
+ path: filePath,
1596
+ select,
1597
+ at: input.at,
1598
+ end: input.end,
1599
+ limit: input.limit,
1600
+ language: input.language,
1601
+ visionMode,
1602
+ visionLevel,
1603
+ readseekDir: cacheDir
1604
+ });
1605
+ return runReadSeek(context, args, visionMode === undefined || visionMode === "none" ? {} : { timeoutMs: DEFAULT_VISION_TIMEOUT_MS });
1606
+ }),
1607
+ readseek_grep: readseekTool(openCodeToolDescription("grep"), {
1608
+ pattern: tool.schema.string().describe("Text or regular expression to search for; set literal for exact text"),
1609
+ path: tool.schema.string().optional().describe("File or directory to search; defaults to the project directory"),
1610
+ glob: tool.schema.string().optional().describe("File-name glob, such as **/*.ts"),
1611
+ literal: tool.schema.boolean().optional().describe("Treat pattern as literal text"),
1612
+ ignoreCase: tool.schema.boolean().optional().describe("Ignore case"),
1613
+ context: tool.schema.number().int().nonnegative().optional().describe("Surrounding lines for each match"),
1614
+ limit: tool.schema.number().int().positive().optional().describe("Maximum matching lines; defaults to 100")
1615
+ }, "grep", async (input, context) => {
1616
+ const target = resolvePath(context.directory, input.path ?? ".");
1617
+ const pattern = input.pattern;
1618
+ await authorizeSearch(context, target, pattern);
1619
+ let expression;
1620
+ try {
1621
+ const source = input.literal ? pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : pattern;
1622
+ expression = new RegExp(source, input.ignoreCase ? "i" : "");
1623
+ } catch (error) {
1624
+ throw new Error(`invalid regular expression: ${error instanceof Error ? error.message : String(error)}`);
1625
+ }
1626
+ const info = await stat(target);
1627
+ const files = [];
1628
+ if (info.isFile())
1629
+ files.push(target);
1630
+ else if (info.isDirectory()) {
1631
+ const glob = new Bun.Glob(input.glob ?? "**/*");
1632
+ for await (const filePath of glob.scan({ cwd: target, absolute: true, onlyFiles: true, followSymlinks: false })) {
1633
+ files.push(filePath);
1634
+ }
1635
+ } else
1636
+ throw new Error(`grep target is not a file or directory: ${target}`);
1637
+ const maxMatches = input.limit ?? 100;
1638
+ const contextLines = input.context ?? 0;
1639
+ const results = [];
1640
+ let totalMatches = 0;
1641
+ let truncated = false;
1642
+ for (const filePath of files.sort()) {
1643
+ context.abort.throwIfAborted();
1644
+ if (truncated)
1645
+ break;
1646
+ const buffer = await readFile(filePath);
1647
+ if (buffer.includes(0))
1648
+ continue;
1649
+ const lines = buffer.toString("utf8").replace(/\r\n/g, `
1650
+ `).split(`
1651
+ `);
1652
+ const matchLines = new Set;
1653
+ const ranges = [];
1654
+ for (let index = 0;index < lines.length; index++) {
1655
+ expression.lastIndex = 0;
1656
+ if (!expression.test(lines[index] ?? ""))
1657
+ continue;
1658
+ if (totalMatches === maxMatches) {
1659
+ truncated = true;
1660
+ break;
1661
+ }
1662
+ const line = index + 1;
1663
+ matchLines.add(line);
1664
+ ranges.push([Math.max(1, line - contextLines), Math.min(lines.length, line + contextLines)]);
1665
+ totalMatches++;
1666
+ }
1667
+ if (ranges.length === 0)
1668
+ continue;
1669
+ const mergedRanges = [];
1670
+ for (const [start, end] of ranges) {
1671
+ const previous = mergedRanges.at(-1);
1672
+ if (previous && start <= previous[1] + 1)
1673
+ previous[1] = Math.max(previous[1], end);
1674
+ else
1675
+ mergedRanges.push([start, end]);
1676
+ }
1677
+ const linesByNumber = new Map;
1678
+ for (const [start, end] of mergedRanges) {
1679
+ const output = record(await runDigestContent(context, readRangeArgs(filePath, start, end)));
1680
+ for (const hashline of items(output.hashlines)) {
1681
+ const line = record(hashline).line;
1682
+ if (typeof line === "number" && !linesByNumber.has(line))
1683
+ linesByNumber.set(line, hashline);
1684
+ }
1685
+ }
1686
+ const hashlines = [...linesByNumber.values()];
1687
+ const matches = hashlines.filter((hashline) => {
1688
+ const line = record(hashline).line;
1689
+ return typeof line === "number" && matchLines.has(line);
1690
+ });
1691
+ results.push({ file: filePath, matches, hashlines });
1692
+ }
1693
+ return { results, truncated };
1694
+ }),
1695
+ readseek_search: readseekTool(openCodeToolDescription("search"), {
1696
+ pattern: tool.schema.string().describe(PARAM_DESCRIPTIONS.pattern),
1697
+ path: tool.schema.string().optional().describe("File or directory to search; defaults to the project directory"),
1698
+ language: tool.schema.string().optional().describe(PARAM_DESCRIPTIONS.language),
1699
+ cached: tool.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.cached),
1700
+ others: tool.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.others),
1701
+ ignored: tool.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.ignored)
1702
+ }, "search", async (input, context) => {
1703
+ const target = resolvePath(context.directory, input.path ?? ".");
1704
+ const args = buildSearchArgs({
1705
+ path: target,
1706
+ pattern: input.pattern,
1707
+ language: input.language,
1708
+ cached: input.cached,
1709
+ others: input.others,
1710
+ ignored: input.ignored
1711
+ });
1712
+ await authorizeSearch(context, target, input.pattern);
1713
+ return runReadSeek(context, args);
1714
+ }),
1715
+ readseek_def: readseekTool(openCodeToolDescription("def"), {
1716
+ name: tool.schema.string().describe(PARAM_DESCRIPTIONS.name),
1717
+ path: tool.schema.string().optional().describe("File or directory to search; defaults to the project directory"),
1718
+ language: tool.schema.string().optional().describe(PARAM_DESCRIPTIONS.language),
1719
+ cached: tool.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.cached),
1720
+ others: tool.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.others),
1721
+ ignored: tool.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.ignored)
1722
+ }, "def", async (input, context) => {
1723
+ const target = resolvePath(context.directory, input.path ?? ".");
1724
+ const args = buildDefArgs({
1725
+ path: target,
1726
+ name: input.name,
1727
+ language: input.language,
1728
+ format: "plain",
1729
+ cached: input.cached,
1730
+ others: input.others,
1731
+ ignored: input.ignored
1732
+ });
1733
+ await authorizeSearch(context, target, input.name);
1734
+ return runReadSeek(context, args);
1735
+ }),
1736
+ readseek_refs: readseekTool(openCodeToolDescription("refs"), {
1737
+ name: tool.schema.string().optional().describe(PARAM_DESCRIPTIONS.refsName),
1738
+ path: tool.schema.string().optional().describe("File or directory to search; defaults to the project directory"),
1739
+ language: tool.schema.string().optional().describe(PARAM_DESCRIPTIONS.language),
1740
+ scope: tool.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.scope),
1741
+ line: tool.schema.number().int().positive().optional().describe(PARAM_DESCRIPTIONS.line),
1742
+ column: tool.schema.number().int().positive().optional().describe(PARAM_DESCRIPTIONS.column),
1743
+ cached: tool.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.cached),
1744
+ others: tool.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.others),
1745
+ ignored: tool.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.ignored)
1746
+ }, "refs", async (input, context) => {
1747
+ const target = resolvePath(context.directory, input.path ?? ".");
1748
+ const name = input.name;
1749
+ const args = buildRefsArgs({
1750
+ path: target,
1751
+ name,
1752
+ scope: input.scope,
1753
+ line: input.line,
1754
+ column: input.column,
1755
+ language: input.language,
1756
+ cached: input.cached,
1757
+ others: input.others,
1758
+ ignored: input.ignored
1759
+ });
1760
+ await authorizeSearch(context, target, name ?? "*");
1761
+ return runReadSeek(context, args);
1762
+ }),
1763
+ readseek_rename: readseekTool(openCodeToolDescription("rename"), {
1764
+ path: tool.schema.string().describe("Path relative to the project directory"),
1765
+ line: tool.schema.number().int().positive().describe(PARAM_DESCRIPTIONS.renameLine),
1766
+ column: tool.schema.number().int().positive().optional().describe(PARAM_DESCRIPTIONS.column),
1767
+ to: tool.schema.string().min(1).describe(PARAM_DESCRIPTIONS.to),
1768
+ workspace: tool.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.workspace),
1769
+ apply: tool.schema.boolean().optional().describe("Apply the verified edits; defaults to true")
1770
+ }, "rename", async (input, context) => {
1771
+ const filePath = resolvePath(context.directory, input.path);
1772
+ await authorizeRead(context, filePath);
1773
+ if (input.workspace) {
1774
+ const identifyArgs = digestArgs(filePath, "identity");
1775
+ withAt(identifyArgs, input.line, input.column);
1776
+ const envelope = await runReadSeek(context, identifyArgs);
1777
+ const name = identifiedName(requireDigestFacet(envelope, "identity"));
1778
+ if (!name)
1779
+ throw new Error("readseek could not identify a binding at the rename cursor");
1780
+ await authorizeSearch(context, context.directory, name);
1781
+ }
1782
+ const args = buildRenameArgs({
1783
+ path: filePath,
1784
+ to: input.to,
1785
+ line: input.line,
1786
+ column: input.column,
1787
+ workspace: input.workspace ? context.directory : undefined
1788
+ });
1789
+ const plan = await runReadSeek(context, args);
1790
+ if (input.apply === false)
1791
+ return plan;
1792
+ const files = renameFiles(plan);
1793
+ const planHash = record(plan).plan_hash;
1794
+ if (typeof planHash !== "string" || planHash.length === 0) {
1795
+ throw new Error("readseek rename plan did not include a plan hash");
1796
+ }
1797
+ await authorizeEdit(context, files);
1798
+ return runReadSeek(context, buildRenameArgs({
1799
+ path: filePath,
1800
+ to: input.to,
1801
+ line: input.line,
1802
+ column: input.column,
1803
+ workspace: input.workspace ? context.directory : undefined,
1804
+ apply: true,
1805
+ planHash
1806
+ }), {
1807
+ cancelable: false,
1808
+ timeoutMs: DEFAULT_READSEEK_TIMEOUT_MS
1809
+ });
1810
+ }),
1811
+ readseek_edit: readseekTool(openCodeToolDescription("edit"), {
1812
+ path: tool.schema.string().describe("Path relative to the project directory"),
1813
+ edits: tool.schema.array(tool.schema.union([
1814
+ tool.schema.object({
1815
+ set_line: tool.schema.object({
1816
+ anchor: tool.schema.string().describe(PARAM_DESCRIPTIONS.anchor),
1817
+ new_text: tool.schema.string().describe(PARAM_DESCRIPTIONS.newTextLine)
1818
+ }).strict()
1819
+ }).strict(),
1820
+ tool.schema.object({
1821
+ replace_lines: tool.schema.object({
1822
+ start_anchor: tool.schema.string().describe(PARAM_DESCRIPTIONS.startAnchor),
1823
+ end_anchor: tool.schema.string().describe(PARAM_DESCRIPTIONS.endAnchor),
1824
+ new_text: tool.schema.string().describe(PARAM_DESCRIPTIONS.newTextRange)
1825
+ }).strict()
1826
+ }).strict(),
1827
+ tool.schema.object({
1828
+ insert_after: tool.schema.object({
1829
+ anchor: tool.schema.string().describe(PARAM_DESCRIPTIONS.insertAnchor),
1830
+ new_text: tool.schema.string().describe(PARAM_DESCRIPTIONS.newTextInsert)
1831
+ }).strict()
1832
+ }).strict(),
1833
+ tool.schema.object({
1834
+ replace: tool.schema.object({
1835
+ old_text: tool.schema.string().describe(PARAM_DESCRIPTIONS.oldText),
1836
+ new_text: tool.schema.string().describe(PARAM_DESCRIPTIONS.newText),
1837
+ all: tool.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.all)
1838
+ }).strict()
1839
+ }).strict(),
1840
+ tool.schema.object({
1841
+ replace_symbol: tool.schema.object({
1842
+ symbol: tool.schema.string().describe(PARAM_DESCRIPTIONS.symbol),
1843
+ new_body: tool.schema.string().describe(PARAM_DESCRIPTIONS.newBody)
1844
+ }).strict()
1845
+ }).strict()
1846
+ ])).min(1).describe(PARAM_DESCRIPTIONS.edits),
1847
+ language: tool.schema.string().optional().describe(PARAM_DESCRIPTIONS.languageSymbol)
1848
+ }, "edit", async (input, context) => {
1849
+ const filePath = resolvePath(context.directory, input.path);
1850
+ validateEdits(input.edits);
1851
+ return withFileMutationQueue(filePath, async () => {
1852
+ context.abort.throwIfAborted();
1853
+ await authorizeRead(context, filePath);
1854
+ await rejectSymlinkMutation(filePath);
1855
+ const edits = input.edits;
1856
+ const request = JSON.stringify({ edits });
1857
+ const baseArgs = buildEditArgs({
1858
+ path: filePath,
1859
+ language: input.language,
1860
+ requestFromStdin: true
1861
+ });
1862
+ const plan = record(await runReadSeek(context, baseArgs, { stdin: request }));
1863
+ if (typeof plan.before_content !== "string" || typeof plan.content !== "string" || typeof plan.plan_hash !== "string") {
1864
+ throw new Error("readseek edit plan is missing before_content, content, or plan_hash");
1865
+ }
1866
+ const before = plan.before_content;
1867
+ const after = plan.content;
1868
+ const previewStart = Math.max(1, firstChangedLine(before, after) - 3);
1869
+ const previewArgs = readRangeArgs(filePath, previewStart, previewStart + EDIT_RESULT_READ_LIMIT - 1);
1870
+ if (before === after) {
1871
+ return postMutationPreview(context, previewArgs, filePath, false);
1872
+ }
1873
+ await authorizeEdit(context, [filePath], simpleDiff(filePath, before, after), false);
1874
+ context.abort.throwIfAborted();
1875
+ const applied = record(await runReadSeek(context, buildEditArgs({
1876
+ path: filePath,
1877
+ language: input.language,
1878
+ requestFromStdin: true,
1879
+ apply: true,
1880
+ planHash: plan.plan_hash
1881
+ }), {
1882
+ cancelable: false,
1883
+ stdin: request,
1884
+ timeoutMs: DEFAULT_READSEEK_TIMEOUT_MS
1885
+ }));
1886
+ if (applied.applied !== true) {
1887
+ throw new Error("readseek did not confirm that the edit was applied");
1888
+ }
1889
+ return postMutationPreview(context, previewArgs, filePath, true);
1890
+ });
1891
+ }),
1892
+ readseek_write: readseekTool(openCodeToolDescription("write"), {
1893
+ path: tool.schema.string().describe("Path relative to the project directory"),
1894
+ content: tool.schema.string().describe("Complete text file content")
1895
+ }, "write", async (input, context) => {
1896
+ const filePath = resolvePath(context.directory, input.path);
1897
+ await authorizeExternal(context, filePath);
1898
+ return withFileMutationQueue(filePath, async () => {
1899
+ context.abort.throwIfAborted();
1900
+ await rejectSymlinkMutation(filePath);
1901
+ const before = await readTextSnapshot(filePath);
1902
+ const content = input.content;
1903
+ if (content.includes("\x00"))
1904
+ throw new Error("write content must be text");
1905
+ await authorizeEdit(context, [filePath], simpleDiff(filePath, before.content, content), false);
1906
+ context.abort.throwIfAborted();
1907
+ await writeText(filePath, before, content);
1908
+ return postMutationPreview(context, digestArgs(filePath, "content", ["--end", String(DEFAULT_READ_LIMIT)]), filePath, !before.exists || before.content !== content);
1909
+ });
1910
+ })
1911
+ },
1912
+ "experimental.chat.system.transform": async (_input2, output) => {
1913
+ output.system.push(TOOL_POLICY);
1914
+ },
1915
+ "tool.definition": async (input, output) => {
1916
+ const preference = PREFERRED_TOOL_DESCRIPTIONS[input.toolID];
1917
+ if (preference) {
1918
+ output.description = `${preference} ${output.description}`;
1919
+ return;
1920
+ }
1921
+ const fallback = FALLBACK_TOOL_DESCRIPTIONS[input.toolID];
1922
+ if (fallback)
1923
+ output.description = `${fallback} ${output.description}`;
1924
+ },
1925
+ event: async ({ event }) => {
1926
+ if (event.type === "file.edited" || event.type === "file.watcher.updated") {
1927
+ anchors.forget(event.properties.file);
1928
+ return;
1929
+ }
1930
+ if (event.type === "session.deleted")
1931
+ anchors.deleteSession(event.properties.info.id);
1932
+ },
1933
+ "tool.execute.after": async (input, output) => {
1934
+ if (!input.tool.startsWith("readseek_"))
1935
+ return;
1936
+ try {
1937
+ const result = JSON.parse(output.output);
1938
+ if (input.tool === "readseek_rename") {
1939
+ anchors.planRename(input.sessionID, result);
1940
+ if (record(result).applied === true) {
1941
+ for (const filePath of renameFiles(result))
1942
+ anchors.forget(filePath);
1943
+ }
1944
+ }
1945
+ const files = new Set;
1946
+ collectAnchoredFiles(result, files);
1947
+ for (const filePath of files)
1948
+ anchors.mark(input.sessionID, path2.resolve(filePath));
1949
+ } catch {}
1950
+ },
1951
+ "experimental.session.compacting": async (input, output) => {
1952
+ const context = anchors.render(input.sessionID);
1953
+ if (context)
1954
+ output.context.push(context);
1955
+ }
1956
+ };
1957
+ };
1958
+ var opencode_readseek_default = { id: "opencode-readseek", server: ReadSeekPlugin };
1959
+ export {
1960
+ opencode_readseek_default as default,
1961
+ ReadSeekPlugin
1962
+ };