opencode-readseek 0.8.24 → 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.
- package/README.md +20 -17
- package/index.ts +420 -275
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<img alt="OpenCode running opencode-readseek" src="screenshot.png" width="800">
|
|
4
4
|
|
|
5
5
|
`opencode-readseek` adds anchored file operations, structural search, symbol
|
|
6
|
-
navigation, and
|
|
6
|
+
navigation, and document tools to OpenCode.
|
|
7
7
|
|
|
8
8
|
## Install
|
|
9
9
|
|
|
@@ -20,20 +20,18 @@ OpenCode installs the package and its platform-specific
|
|
|
20
20
|
|
|
21
21
|
## Tools
|
|
22
22
|
|
|
23
|
-
| Tool
|
|
24
|
-
|
|
|
25
|
-
| `readseek_read`
|
|
26
|
-
| `readseek_edit`
|
|
27
|
-
| `readseek_write`
|
|
28
|
-
| `readseek_grep`
|
|
29
|
-
| `
|
|
30
|
-
| `readseek_search`
|
|
31
|
-
| `readseek_def`
|
|
32
|
-
| `readseek_refs`
|
|
33
|
-
| `
|
|
34
|
-
| `
|
|
35
|
-
| `readseek_check` | Report parser errors and missing syntax |
|
|
36
|
-
| `readseek_view` | View or narrow a structural PDF index |
|
|
23
|
+
| Tool | Purpose |
|
|
24
|
+
| ------------------ | ----------------------------------------------------------------------- |
|
|
25
|
+
| `readseek_read` | Read text with `LINE:HASH` anchors; read standalone images |
|
|
26
|
+
| `readseek_edit` | Apply hash-verified edits to an existing text file |
|
|
27
|
+
| `readseek_write` | Create or replace a complete text file |
|
|
28
|
+
| `readseek_grep` | Search text or regex and return anchors |
|
|
29
|
+
| `readseek_digest` | Native digest envelope: metadata, content, map, diagnostics, identity |
|
|
30
|
+
| `readseek_search` | Search AST patterns |
|
|
31
|
+
| `readseek_def` | Find symbol definitions |
|
|
32
|
+
| `readseek_refs` | Find identifier usages, optionally scoped to a cursor binding |
|
|
33
|
+
| `readseek_rename` | Apply a binding-aware rename; set `apply: false` for a dry run |
|
|
34
|
+
| `readseek_view` | View or narrow a structural document index |
|
|
37
35
|
|
|
38
36
|
The plugin requests read, grep, external-directory, and edit permissions as
|
|
39
37
|
needed. Mutations invalidate remembered anchors; compaction preserves fresh
|
|
@@ -44,6 +42,9 @@ OpenCode is instructed to prefer ReadSeek over the built-in `read`, `edit`,
|
|
|
44
42
|
`write`, `grep`, and `apply_patch` tools when ReadSeek can do the job. Built-ins
|
|
45
43
|
remain available as fallbacks.
|
|
46
44
|
|
|
45
|
+
Documents such as PDFs are owned by `readseek_view`. Calling `readseek_read` on
|
|
46
|
+
a document returns a cache receipt and directs the caller to `readseek_view`.
|
|
47
|
+
|
|
47
48
|
## Configuration
|
|
48
49
|
|
|
49
50
|
Pass options with OpenCode's plugin tuple syntax:
|
|
@@ -58,9 +59,11 @@ Pass options with OpenCode's plugin tuple syntax:
|
|
|
58
59
|
|
|
59
60
|
- `"auto"` (default): expose `none`, `all`, `ocr`, `caption`, and `objects`.
|
|
60
61
|
- `"on"`: expose local analysis modes and omit `none`.
|
|
61
|
-
- `"off"`: skip image
|
|
62
|
+
- `"off"`: skip image reads.
|
|
62
63
|
|
|
63
|
-
Omitting `image` from a read also skips
|
|
64
|
+
Omitting `image` from a read also skips image files. Document asset analysis
|
|
65
|
+
uses `readseek_view` with optional `visionMode` (`all` | `ocr` | `caption` |
|
|
66
|
+
`objects`) and `visionLevel` (`low` | `medium` | `high`).
|
|
64
67
|
|
|
65
68
|
## Development
|
|
66
69
|
|
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;
|
|
@@ -18,10 +20,10 @@ const TOOL_POLICY = [
|
|
|
18
20
|
"ReadSeek tool policy:",
|
|
19
21
|
"- Prefer readseek_* tools over built-ins when they can do the job.",
|
|
20
22
|
"- Read with readseek_read first; readseek_edit needs fresh LINE:HASH anchors.",
|
|
21
|
-
"- Use readseek_grep for text/regex, readseek_search for AST patterns, and
|
|
23
|
+
"- Use readseek_grep for text/regex, readseek_search for AST patterns, and readseek_digest/def/refs for symbol navigation.",
|
|
22
24
|
"- Use readseek_edit for existing files, readseek_write for whole-file creation or replacement, and readseek_rename for symbol renames.",
|
|
23
25
|
"- Do not use built-in read, edit, write, grep, or apply_patch when a ReadSeek tool can do the job.",
|
|
24
|
-
"- Run
|
|
26
|
+
"- Run readseek_digest with select=diagnostics after source edits for a quick syntax check.",
|
|
25
27
|
].join("\n");
|
|
26
28
|
const PREFERRED_TOOL_DESCRIPTIONS: Record<string, string> = {
|
|
27
29
|
readseek_read: "Preferred file reader; returns LINE:HASH anchors for durable references.",
|
|
@@ -30,12 +32,10 @@ const PREFERRED_TOOL_DESCRIPTIONS: Record<string, string> = {
|
|
|
30
32
|
readseek_rename: "Use to rename a symbol with binding-aware, verified edits.",
|
|
31
33
|
readseek_grep: "Preferred text/regex search; returns edit-ready LINE:HASH anchors.",
|
|
32
34
|
readseek_search: "Use for AST or structural code search with ast-grep-style patterns.",
|
|
33
|
-
|
|
35
|
+
readseek_digest: "Use to digest native facets (metadata, content, map, diagnostics, identity) and return the full envelope.",
|
|
34
36
|
readseek_def: "Use for go-to-definition or declaration lookup by name.",
|
|
35
37
|
readseek_refs: "Use to find identifier-name matches or binding-specific references before rename or delete.",
|
|
36
|
-
|
|
37
|
-
readseek_view: "Use to inspect PDF structure or selected content.",
|
|
38
|
-
readseek_check: "Use for a quick parser syntax check after source edits; unsupported languages are not validated.",
|
|
38
|
+
readseek_view: "Use to digest document structure or selected content.",
|
|
39
39
|
};
|
|
40
40
|
const FALLBACK_TOOL_DESCRIPTIONS: Record<string, string> = {
|
|
41
41
|
read: "Fallback only. Prefer readseek_read so edits can use LINE:HASH anchors.",
|
|
@@ -56,7 +56,7 @@ type RenamePlan = {
|
|
|
56
56
|
files: Set<string>;
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
-
type PresentationKind = "read" | "view" | "
|
|
59
|
+
type PresentationKind = "read" | "view" | "digest" | "search" | "grep" | "def" | "refs" | "rename" | "edit" | "write";
|
|
60
60
|
|
|
61
61
|
type Presentation = {
|
|
62
62
|
title: string;
|
|
@@ -70,7 +70,11 @@ type RenderedToolResult = {
|
|
|
70
70
|
|
|
71
71
|
type ImagePolicy = "on" | "auto" | "off";
|
|
72
72
|
type ImageMode = "none" | "all" | "ocr" | "caption" | "objects";
|
|
73
|
+
type VisionAnalysisMode = Exclude<ImageMode, "none">;
|
|
74
|
+
type VisionLevel = "low" | "medium" | "high";
|
|
73
75
|
type DocumentNodeKind = "artifact" | "footer" | "header" | "heading" | "marginal_label" | "page" | "page_number" | "paragraph" | "section" | "structural_section";
|
|
76
|
+
const VISION_ANALYSIS_MODES: [VisionAnalysisMode, ...VisionAnalysisMode[]] = ["all", "ocr", "caption", "objects"];
|
|
77
|
+
const VISION_LEVELS: [VisionLevel, ...VisionLevel[]] = ["low", "medium", "high"];
|
|
74
78
|
|
|
75
79
|
const DOCUMENT_NODE_KINDS: [DocumentNodeKind, ...DocumentNodeKind[]] = [
|
|
76
80
|
"artifact",
|
|
@@ -95,11 +99,65 @@ function resolveImagePolicy(options: PluginOptions | undefined): ImagePolicy {
|
|
|
95
99
|
function isVisualFile(value: unknown): boolean {
|
|
96
100
|
const output = record(value);
|
|
97
101
|
const type = output.type;
|
|
98
|
-
return typeof output.width === "number"
|
|
102
|
+
return typeof output.width === "number"
|
|
103
|
+
|| (typeof type === "string" && type.startsWith("image/"));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isVisualBoundsError(error: unknown): boolean {
|
|
107
|
+
if (!(error instanceof Error)) return false;
|
|
108
|
+
const message = error.message.replace(/^error:\s*/, "");
|
|
109
|
+
return message.startsWith("--end")
|
|
110
|
+
&& /(?:does|do) not apply to images/.test(message);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isDocumentBoundsError(error: unknown): boolean {
|
|
114
|
+
if (!(error instanceof Error)) return false;
|
|
115
|
+
const message = error.message.replace(/^error:\s*/, "");
|
|
116
|
+
return /(?:does|do) not apply to (?:documents|PDFs)/.test(message)
|
|
117
|
+
|| /document content belongs to view/.test(message)
|
|
118
|
+
|| /source facets do not apply to documents/.test(message);
|
|
99
119
|
}
|
|
100
120
|
|
|
101
121
|
function isPdfFile(value: unknown): boolean {
|
|
102
|
-
|
|
122
|
+
const output = record(value);
|
|
123
|
+
return output.format === "pdf" || output.type === "application/pdf";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function documentReceipt(filePath: string, envelope: unknown): Record<string, unknown> {
|
|
127
|
+
const recordEnvelope = record(envelope);
|
|
128
|
+
return {
|
|
129
|
+
file: filePath,
|
|
130
|
+
use: "readseek_view",
|
|
131
|
+
reason: "document content belongs to readseek_view",
|
|
132
|
+
metadata: digestMetadata(envelope),
|
|
133
|
+
...(recordEnvelope.cache !== undefined ? { cache: recordEnvelope.cache } : {}),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function digestDocumentReceipt(context: ToolContext, filePath: string): Promise<Record<string, unknown>> {
|
|
138
|
+
const cacheDir = await ensureReadSeekCache(context);
|
|
139
|
+
const envelope = await runReadSeek(context, [
|
|
140
|
+
"--readseek-dir",
|
|
141
|
+
cacheDir,
|
|
142
|
+
...digestArgs(filePath, "metadata"),
|
|
143
|
+
]);
|
|
144
|
+
return documentReceipt(filePath, envelope);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function digestArgs(filePath: string, select: string, extra: string[] = []): string[] {
|
|
148
|
+
return ["digest", filePath, "--select", select, ...extra];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function requireDigestFacet(envelope: unknown, facet: "content" | "map" | "diagnostics" | "identity"): unknown {
|
|
152
|
+
const value = record(envelope)[facet];
|
|
153
|
+
if (value === undefined || value === null || typeof value !== "object") {
|
|
154
|
+
throw new Error(`digest returned no ${facet} facet`);
|
|
155
|
+
}
|
|
156
|
+
return value;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function digestMetadata(envelope: unknown): Record<string, unknown> {
|
|
160
|
+
return record(record(envelope).metadata);
|
|
103
161
|
}
|
|
104
162
|
|
|
105
163
|
class SessionAnchors {
|
|
@@ -304,6 +362,37 @@ function optionalFlag(args: string[], enabled: boolean | undefined, flag: string
|
|
|
304
362
|
if (enabled) args.push(flag);
|
|
305
363
|
}
|
|
306
364
|
|
|
365
|
+
function atLocation(line: number, column?: number): string {
|
|
366
|
+
return column === undefined ? `line:${line}` : `line:${line}:${column}`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function withAt(args: string[], line: number, column?: number): void {
|
|
370
|
+
args.push("--at", atLocation(line, column));
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function isLikelyImagePath(filePath: string): boolean {
|
|
374
|
+
return /\.(avif|bmp|gif|heic|heif|ico|jpe?g|png|svg|tiff?|webp)$/i.test(filePath);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function isLikelyDocumentPath(filePath: string): boolean {
|
|
378
|
+
return /\.pdf$/i.test(filePath);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function readRangeArgs(filePath: string, start: number, end: number): string[] {
|
|
382
|
+
const extra: string[] = [];
|
|
383
|
+
if (start > 1) withAt(extra, start);
|
|
384
|
+
extra.push("--end", String(end));
|
|
385
|
+
return digestArgs(filePath, "content", extra);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async function runDigestContent(
|
|
389
|
+
context: ToolContext,
|
|
390
|
+
args: string[],
|
|
391
|
+
options: { cancelable?: boolean; timeoutMs?: number } = {},
|
|
392
|
+
): Promise<unknown> {
|
|
393
|
+
return requireDigestFacet(await runReadSeek(context, args, options), "content");
|
|
394
|
+
}
|
|
395
|
+
|
|
307
396
|
function readSeekBinaryPath(): string {
|
|
308
397
|
const platform = `${process.platform}-${process.arch}`;
|
|
309
398
|
const platformPackage = READSEEK_PLATFORM_PACKAGES[platform];
|
|
@@ -331,7 +420,7 @@ async function readBoundedText(
|
|
|
331
420
|
totalBytes += value.byteLength;
|
|
332
421
|
if (totalBytes > MAX_OUTPUT_BYTES) {
|
|
333
422
|
onOverflow();
|
|
334
|
-
|
|
423
|
+
await reader.cancel().catch(() => undefined);
|
|
335
424
|
throw new Error(`readseek ${streamName} exceeded the ${MAX_OUTPUT_BYTES}-byte output limit`);
|
|
336
425
|
}
|
|
337
426
|
chunks.push(value);
|
|
@@ -352,13 +441,14 @@ async function readBoundedText(
|
|
|
352
441
|
async function runReadSeekRaw(
|
|
353
442
|
context: ToolContext,
|
|
354
443
|
args: string[],
|
|
355
|
-
options: { cancelable?: boolean } = {},
|
|
444
|
+
options: { cancelable?: boolean; stdin?: string; timeoutMs?: number } = {},
|
|
356
445
|
): Promise<string> {
|
|
357
446
|
context.abort.throwIfAborted();
|
|
358
447
|
const cancelable = options.cancelable !== false;
|
|
359
448
|
const spawnOptions = {
|
|
360
449
|
cwd: context.directory,
|
|
361
450
|
stderr: "pipe" as const,
|
|
451
|
+
stdin: options.stdin === undefined ? undefined : Buffer.from(options.stdin),
|
|
362
452
|
stdout: "pipe" as const,
|
|
363
453
|
};
|
|
364
454
|
const child = Bun.spawn(
|
|
@@ -377,11 +467,31 @@ async function runReadSeekRaw(
|
|
|
377
467
|
// Preserve the output-limit error if the process exited concurrently.
|
|
378
468
|
}
|
|
379
469
|
};
|
|
380
|
-
const
|
|
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([
|
|
381
484
|
readBoundedText(child.stdout, "stdout", killForOverflow),
|
|
382
485
|
readBoundedText(child.stderr, "stderr", killForOverflow),
|
|
383
486
|
child.exited,
|
|
384
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;
|
|
385
495
|
if (cancelable) context.abort.throwIfAborted();
|
|
386
496
|
if (exitCode !== 0) throw new Error(stderr.trim() || `readseek exited with status ${exitCode}`);
|
|
387
497
|
return stdout;
|
|
@@ -390,7 +500,7 @@ async function runReadSeekRaw(
|
|
|
390
500
|
async function runReadSeek(
|
|
391
501
|
context: ToolContext,
|
|
392
502
|
args: string[],
|
|
393
|
-
options: { cancelable?: boolean } = {},
|
|
503
|
+
options: { cancelable?: boolean; stdin?: string; timeoutMs?: number } = {},
|
|
394
504
|
): Promise<unknown> {
|
|
395
505
|
const stdout = await runReadSeekRaw(context, args, options);
|
|
396
506
|
try {
|
|
@@ -407,7 +517,10 @@ async function postMutationPreview(
|
|
|
407
517
|
changed: boolean,
|
|
408
518
|
): Promise<unknown> {
|
|
409
519
|
try {
|
|
410
|
-
return await
|
|
520
|
+
return await runDigestContent(context, args, {
|
|
521
|
+
cancelable: false,
|
|
522
|
+
timeoutMs: DEFAULT_READSEEK_TIMEOUT_MS,
|
|
523
|
+
});
|
|
411
524
|
} catch (error) {
|
|
412
525
|
const previewError = error instanceof Error ? error.message : String(error);
|
|
413
526
|
return {
|
|
@@ -471,6 +584,11 @@ type AnchorEdit =
|
|
|
471
584
|
| { replace_lines: { start_anchor: string; end_anchor: string; new_text: string } }
|
|
472
585
|
| { insert_after: { anchor: string; new_text: string } };
|
|
473
586
|
|
|
587
|
+
type EditItem =
|
|
588
|
+
| AnchorEdit
|
|
589
|
+
| { replace: { old_text: string; new_text: string; all?: boolean } }
|
|
590
|
+
| { replace_symbol: { symbol: string; new_body: string } };
|
|
591
|
+
|
|
474
592
|
type TextSnapshot = {
|
|
475
593
|
exists: boolean;
|
|
476
594
|
content: string;
|
|
@@ -496,25 +614,28 @@ async function withFileMutationQueue<T>(filePath: string, operation: () => Promi
|
|
|
496
614
|
}
|
|
497
615
|
}
|
|
498
616
|
|
|
499
|
-
function
|
|
617
|
+
function validateEdits(value: unknown): asserts value is EditItem[] {
|
|
500
618
|
if (!Array.isArray(value) || value.length === 0) throw new Error("edits must contain at least one edit");
|
|
501
|
-
const
|
|
619
|
+
const requiredFields = {
|
|
502
620
|
set_line: ["anchor", "new_text"],
|
|
503
621
|
replace_lines: ["end_anchor", "new_text", "start_anchor"],
|
|
504
622
|
insert_after: ["anchor", "new_text"],
|
|
623
|
+
replace: ["new_text", "old_text"],
|
|
624
|
+
replace_symbol: ["new_body", "symbol"],
|
|
505
625
|
} as const;
|
|
506
626
|
for (const [index, item] of value.entries()) {
|
|
507
627
|
if (!item || typeof item !== "object") throw new Error(`edits[${index}] must be an edit object`);
|
|
508
628
|
const keys = Object.keys(item);
|
|
509
|
-
const variant = keys[0] as keyof typeof
|
|
510
|
-
if (keys.length !== 1 || !Object.hasOwn(
|
|
511
|
-
throw new Error(`edits[${index}] must contain exactly one of: set_line, replace_lines, insert_after`);
|
|
629
|
+
const variant = keys[0] as keyof typeof requiredFields;
|
|
630
|
+
if (keys.length !== 1 || !Object.hasOwn(requiredFields, variant)) {
|
|
631
|
+
throw new Error(`edits[${index}] must contain exactly one of: set_line, replace_lines, insert_after, replace, replace_symbol`);
|
|
512
632
|
}
|
|
513
633
|
const payload = (item as Record<string, unknown>)[variant];
|
|
514
634
|
if (!payload || typeof payload !== "object") throw new Error(`edits[${index}].${variant} must be an object`);
|
|
515
|
-
const expectedFields =
|
|
635
|
+
const expectedFields: readonly string[] = requiredFields[variant];
|
|
636
|
+
const allowedFields = variant === "replace" ? [...expectedFields, "all"] : expectedFields;
|
|
516
637
|
const actualFields = Object.keys(payload).sort();
|
|
517
|
-
if (actualFields.
|
|
638
|
+
if (actualFields.some((field) => !allowedFields.includes(field)) || expectedFields.some((field) => !actualFields.includes(field))) {
|
|
518
639
|
throw new Error(`edits[${index}].${variant} contains invalid fields`);
|
|
519
640
|
}
|
|
520
641
|
const values = payload as Record<string, unknown>;
|
|
@@ -523,6 +644,9 @@ function validateAnchorEdits(value: unknown): asserts value is AnchorEdit[] {
|
|
|
523
644
|
throw new Error(`edits[${index}].${variant}.${field} must be a string`);
|
|
524
645
|
}
|
|
525
646
|
}
|
|
647
|
+
if (variant === "replace" && values.all !== undefined && typeof values.all !== "boolean") {
|
|
648
|
+
throw new Error(`edits[${index}].replace.all must be a boolean`);
|
|
649
|
+
}
|
|
526
650
|
for (const field of expectedFields.filter((field) => field.endsWith("anchor"))) {
|
|
527
651
|
parseAnchor(values[field] as string);
|
|
528
652
|
}
|
|
@@ -537,125 +661,16 @@ function parseAnchor(anchor: string): { line: number; hash: string } {
|
|
|
537
661
|
return { line, hash: match[2].toLowerCase() };
|
|
538
662
|
}
|
|
539
663
|
|
|
540
|
-
function
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
async function verifyAnchors(context: ToolContext, filePath: string, edits: AnchorEdit[]): Promise<void> {
|
|
547
|
-
const refs = new Map<number, string>();
|
|
548
|
-
for (const edit of edits) {
|
|
549
|
-
for (const ref of anchorRefs(edit)) {
|
|
550
|
-
const previous = refs.get(ref.line);
|
|
551
|
-
if (previous !== undefined && previous !== ref.hash) throw new Error(`conflicting hashes for line ${ref.line}`);
|
|
552
|
-
refs.set(ref.line, ref.hash);
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
for (const [line, expected] of refs) {
|
|
556
|
-
const output = record(await runReadSeek(context, ["read", `${filePath}:${line}`, "--end", String(line)]));
|
|
557
|
-
const hashline = items(output.hashlines).map(record)[0];
|
|
558
|
-
const actual = hashline?.hash;
|
|
559
|
-
if (typeof actual !== "string" || actual !== expected) {
|
|
560
|
-
throw new Error(`stale anchor ${line}:${expected}; current hash is ${typeof actual === "string" ? actual : "unavailable"}`);
|
|
561
|
-
}
|
|
664
|
+
function firstChangedLine(before: string, after: string): number {
|
|
665
|
+
const beforeLines = normalizeToLf(before).split("\n");
|
|
666
|
+
const afterLines = normalizeToLf(after).split("\n");
|
|
667
|
+
const count = Math.max(beforeLines.length, afterLines.length);
|
|
668
|
+
for (let index = 0; index < count; index++) {
|
|
669
|
+
if (beforeLines[index] !== afterLines[index]) return index + 1;
|
|
562
670
|
}
|
|
671
|
+
return 1;
|
|
563
672
|
}
|
|
564
673
|
|
|
565
|
-
type EditableLine = {
|
|
566
|
-
text: string;
|
|
567
|
-
ending: "\r\n" | "\n" | "\r" | "";
|
|
568
|
-
};
|
|
569
|
-
|
|
570
|
-
function parseEditableLines(content: string): EditableLine[] {
|
|
571
|
-
const lines: EditableLine[] = [];
|
|
572
|
-
let start = 0;
|
|
573
|
-
for (let index = 0; index < content.length; index++) {
|
|
574
|
-
const character = content[index];
|
|
575
|
-
if (character !== "\r" && character !== "\n") continue;
|
|
576
|
-
const ending = character === "\r" && content[index + 1] === "\n" ? "\r\n" : character;
|
|
577
|
-
lines.push({ text: content.slice(start, index), ending });
|
|
578
|
-
if (ending === "\r\n") index++;
|
|
579
|
-
start = index + 1;
|
|
580
|
-
}
|
|
581
|
-
if (start < content.length || content.length === 0) lines.push({ text: content.slice(start), ending: "" });
|
|
582
|
-
return lines;
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
function detectLineEnding(lines: EditableLine[]): "\r\n" | "\n" | "\r" {
|
|
586
|
-
const counts = new Map<"\r\n" | "\n" | "\r", number>([
|
|
587
|
-
["\r\n", 0],
|
|
588
|
-
["\n", 0],
|
|
589
|
-
["\r", 0],
|
|
590
|
-
]);
|
|
591
|
-
for (const line of lines) {
|
|
592
|
-
if (line.ending !== "") counts.set(line.ending, (counts.get(line.ending) ?? 0) + 1);
|
|
593
|
-
}
|
|
594
|
-
let selected: "\r\n" | "\n" | "\r" = "\n";
|
|
595
|
-
for (const ending of ["\r\n", "\r"] as const) {
|
|
596
|
-
if ((counts.get(ending) ?? 0) > (counts.get(selected) ?? 0)) selected = ending;
|
|
597
|
-
}
|
|
598
|
-
return selected;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
function normalizeToLf(content: string): string {
|
|
602
|
-
return content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
function replacementLines(text: string): string[] {
|
|
606
|
-
if (text === "") return [];
|
|
607
|
-
return normalizeToLf(text).replace(/\n$/, "").split("\n");
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
function applyAnchorEdits(content: string, edits: AnchorEdit[]): string {
|
|
611
|
-
const bom = content.startsWith("\uFEFF") ? "\uFEFF" : "";
|
|
612
|
-
const lines = parseEditableLines(bom ? content.slice(1) : content);
|
|
613
|
-
const newline = detectLineEnding(lines);
|
|
614
|
-
const planned = edits.map((edit) => {
|
|
615
|
-
if ("set_line" in edit) {
|
|
616
|
-
const { line } = parseAnchor(edit.set_line.anchor);
|
|
617
|
-
return { start: line - 1, deleteCount: 1, text: edit.set_line.new_text };
|
|
618
|
-
}
|
|
619
|
-
if ("insert_after" in edit) {
|
|
620
|
-
const { line } = parseAnchor(edit.insert_after.anchor);
|
|
621
|
-
return { start: line, deleteCount: 0, text: edit.insert_after.new_text };
|
|
622
|
-
}
|
|
623
|
-
const start = parseAnchor(edit.replace_lines.start_anchor).line;
|
|
624
|
-
const end = parseAnchor(edit.replace_lines.end_anchor).line;
|
|
625
|
-
if (end < start) throw new Error("replace_lines end anchor precedes start anchor");
|
|
626
|
-
return { start: start - 1, deleteCount: end - start + 1, text: edit.replace_lines.new_text };
|
|
627
|
-
});
|
|
628
|
-
const ascending = [...planned].sort((left, right) => left.start - right.start || left.deleteCount - right.deleteCount);
|
|
629
|
-
for (let index = 1; index < ascending.length; index++) {
|
|
630
|
-
const previous = ascending[index - 1];
|
|
631
|
-
const current = ascending[index];
|
|
632
|
-
const previousEnd = previous.start + Math.max(previous.deleteCount, 1);
|
|
633
|
-
if (current.start < previousEnd) throw new Error("anchored edits overlap or target the same location");
|
|
634
|
-
}
|
|
635
|
-
planned.sort((left, right) => right.start - left.start);
|
|
636
|
-
for (const edit of planned) {
|
|
637
|
-
if (edit.start < 0 || edit.start > lines.length || edit.start + edit.deleteCount > lines.length) {
|
|
638
|
-
throw new Error("anchor line is outside the file");
|
|
639
|
-
}
|
|
640
|
-
const removed = lines.slice(edit.start, edit.start + edit.deleteCount);
|
|
641
|
-
const replacement = replacementLines(edit.text).map((text): EditableLine => ({ text, ending: newline }));
|
|
642
|
-
if (edit.deleteCount === 0 && edit.start === lines.length && replacement.length > 0) {
|
|
643
|
-
const previous = lines.at(-1);
|
|
644
|
-
const terminalEnding = previous?.ending ?? "";
|
|
645
|
-
if (previous?.ending === "") previous.ending = newline;
|
|
646
|
-
replacement[replacement.length - 1].ending = terminalEnding;
|
|
647
|
-
} else if (edit.deleteCount > 0) {
|
|
648
|
-
const terminalEnding = removed.at(-1)?.ending ?? "";
|
|
649
|
-
if (replacement.length > 0) {
|
|
650
|
-
replacement[replacement.length - 1].ending = terminalEnding;
|
|
651
|
-
} else if (edit.start + edit.deleteCount === lines.length && edit.start > 0) {
|
|
652
|
-
lines[edit.start - 1].ending = terminalEnding;
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
lines.splice(edit.start, edit.deleteCount, ...replacement);
|
|
656
|
-
}
|
|
657
|
-
return bom + lines.map((line) => line.text + line.ending).join("");
|
|
658
|
-
}
|
|
659
674
|
|
|
660
675
|
async function readTextSnapshot(filePath: string): Promise<TextSnapshot> {
|
|
661
676
|
try {
|
|
@@ -729,11 +744,19 @@ function renameFiles(value: unknown): string[] {
|
|
|
729
744
|
return files;
|
|
730
745
|
}
|
|
731
746
|
|
|
747
|
+
function normalizeToLf(content: string): string {
|
|
748
|
+
return content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
749
|
+
}
|
|
750
|
+
|
|
732
751
|
function diffLines(content: string): string[] {
|
|
733
752
|
if (content === "") return [];
|
|
734
|
-
const lines =
|
|
753
|
+
const lines = content.split("\n");
|
|
735
754
|
if (lines.at(-1) === "") lines.pop();
|
|
736
|
-
return lines;
|
|
755
|
+
return lines.map((line) => line.replace(/\r/g, "\\r"));
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function hasFinalNewline(content: string): boolean {
|
|
759
|
+
return content === "" || /[\r\n]$/.test(content);
|
|
737
760
|
}
|
|
738
761
|
|
|
739
762
|
function diffRange(start: number, count: number): string {
|
|
@@ -796,23 +819,7 @@ function imageAttachment(image: Record<string, unknown>, filename: string): Tool
|
|
|
796
819
|
|
|
797
820
|
function formatReadResult(value: unknown): RenderedToolResult {
|
|
798
821
|
const output = record(value);
|
|
799
|
-
if (output.
|
|
800
|
-
const attachments: ToolAttachment[] = [];
|
|
801
|
-
const images = items(output.images).map((value, index) => {
|
|
802
|
-
const image = record(value);
|
|
803
|
-
const page = typeof image.page === "number" ? image.page : "unknown";
|
|
804
|
-
const attachment = imageAttachment(image, `pdf-page-${page}-image-${index + 1}`);
|
|
805
|
-
if (attachment) attachments.push(attachment);
|
|
806
|
-
const sanitized = { ...image };
|
|
807
|
-
delete sanitized.data;
|
|
808
|
-
delete sanitized.encoding;
|
|
809
|
-
return sanitized;
|
|
810
|
-
});
|
|
811
|
-
return {
|
|
812
|
-
output: boundDocumentOutput(render({ ...output, images })),
|
|
813
|
-
...(attachments.length > 0 ? { attachments } : {}),
|
|
814
|
-
};
|
|
815
|
-
}
|
|
822
|
+
if (output.use === "readseek_view") return { output: render(value) };
|
|
816
823
|
|
|
817
824
|
const attachment = imageAttachment(output, "image");
|
|
818
825
|
if (!attachment) return { output: render(value) };
|
|
@@ -846,6 +853,8 @@ function simpleDiff(filePath: string, before: string, after: string): string {
|
|
|
846
853
|
if (before === after) return "";
|
|
847
854
|
const oldLines = diffLines(before);
|
|
848
855
|
const newLines = diffLines(after);
|
|
856
|
+
const oldHasFinalNewline = hasFinalNewline(before);
|
|
857
|
+
const newHasFinalNewline = hasFinalNewline(after);
|
|
849
858
|
let prefix = 0;
|
|
850
859
|
while (prefix < oldLines.length && prefix < newLines.length && oldLines[prefix] === newLines[prefix]) prefix++;
|
|
851
860
|
let suffix = 0;
|
|
@@ -874,13 +883,13 @@ function simpleDiff(filePath: string, before: string, after: string): string {
|
|
|
874
883
|
const fullDiff = [...header];
|
|
875
884
|
let fullDiffBytes = Buffer.byteLength(header.join("\n"));
|
|
876
885
|
let complete = true;
|
|
877
|
-
const ranges: Array<[string[], number, number, string]> = [
|
|
878
|
-
[oldLines, oldStart, prefix, " "],
|
|
879
|
-
[oldLines, prefix, oldChangeEnd, "-"],
|
|
880
|
-
[newLines, prefix, newChangeEnd, "+"],
|
|
881
|
-
[newLines, newChangeEnd, newEnd, " "],
|
|
886
|
+
const ranges: Array<[string[], number, number, string, boolean]> = [
|
|
887
|
+
[oldLines, oldStart, prefix, " ", oldHasFinalNewline],
|
|
888
|
+
[oldLines, prefix, oldChangeEnd, "-", oldHasFinalNewline],
|
|
889
|
+
[newLines, prefix, newChangeEnd, "+", newHasFinalNewline],
|
|
890
|
+
[newLines, newChangeEnd, newEnd, " ", newHasFinalNewline],
|
|
882
891
|
];
|
|
883
|
-
outer: for (const [lines, start, end, marker] of ranges) {
|
|
892
|
+
outer: for (const [lines, start, end, marker, hasNewline] of ranges) {
|
|
884
893
|
for (let index = start; index < end; index++) {
|
|
885
894
|
const lineBytes = 2 + Buffer.byteLength(lines[index]);
|
|
886
895
|
if (fullDiffBytes + lineBytes > MAX_PERMISSION_DIFF_BYTES) {
|
|
@@ -889,13 +898,26 @@ function simpleDiff(filePath: string, before: string, after: string): string {
|
|
|
889
898
|
}
|
|
890
899
|
fullDiff.push(`${marker}${lines[index]}`);
|
|
891
900
|
fullDiffBytes += lineBytes;
|
|
901
|
+
if (index === lines.length - 1 && !hasNewline) {
|
|
902
|
+
const eofMarker = "\";
|
|
903
|
+
if (fullDiffBytes + Buffer.byteLength(eofMarker) + 1 > MAX_PERMISSION_DIFF_BYTES) {
|
|
904
|
+
complete = false;
|
|
905
|
+
break outer;
|
|
906
|
+
}
|
|
907
|
+
fullDiff.push(eofMarker);
|
|
908
|
+
fullDiffBytes += Buffer.byteLength(eofMarker) + 1;
|
|
909
|
+
}
|
|
892
910
|
}
|
|
893
911
|
}
|
|
894
912
|
if (complete) return fullDiff.join("\n");
|
|
895
913
|
|
|
914
|
+
const newlineSummary = oldHasFinalNewline === newHasFinalNewline
|
|
915
|
+
? []
|
|
916
|
+
: [` final newline: ${oldHasFinalNewline ? "present" : "missing"} -> ${newHasFinalNewline ? "present" : "missing"}`];
|
|
896
917
|
return [
|
|
897
918
|
...header,
|
|
898
919
|
` diff truncated: ${oldChangeEnd - prefix} removed, ${newChangeEnd - prefix} added lines`,
|
|
920
|
+
...newlineSummary,
|
|
899
921
|
...sampleDiffLines(oldLines, prefix, oldChangeEnd, "-"),
|
|
900
922
|
...sampleDiffLines(newLines, prefix, newChangeEnd, "+"),
|
|
901
923
|
].join("\n");
|
|
@@ -923,8 +945,8 @@ function initialTitle(kind: PresentationKind, args: any): string {
|
|
|
923
945
|
return `Read ${args.path}`;
|
|
924
946
|
case "view":
|
|
925
947
|
return `View ${args.path}`;
|
|
926
|
-
case "
|
|
927
|
-
return `
|
|
948
|
+
case "digest":
|
|
949
|
+
return `Digest ${args.path}`;
|
|
928
950
|
case "search":
|
|
929
951
|
return `Search ${args.pattern}`;
|
|
930
952
|
case "grep":
|
|
@@ -933,16 +955,12 @@ function initialTitle(kind: PresentationKind, args: any): string {
|
|
|
933
955
|
return `Find definition ${args.name}`;
|
|
934
956
|
case "refs":
|
|
935
957
|
return `Find references to ${args.name}`;
|
|
936
|
-
case "hover":
|
|
937
|
-
return `Identify ${args.path}:${args.line}`;
|
|
938
958
|
case "rename":
|
|
939
959
|
return `Rename ${args.path}:${args.line} to ${args.to}`;
|
|
940
960
|
case "edit":
|
|
941
961
|
return `Edit ${args.path}`;
|
|
942
962
|
case "write":
|
|
943
963
|
return `Write ${args.path}`;
|
|
944
|
-
case "check":
|
|
945
|
-
return `Check ${args.path}`;
|
|
946
964
|
}
|
|
947
965
|
}
|
|
948
966
|
|
|
@@ -967,9 +985,37 @@ function resultPresentation(kind: PresentationKind, args: any, value: unknown):
|
|
|
967
985
|
}
|
|
968
986
|
case "view":
|
|
969
987
|
return { title: `Viewed ${args.path}`, metadata: {} };
|
|
970
|
-
case "
|
|
971
|
-
const
|
|
972
|
-
|
|
988
|
+
case "digest": {
|
|
989
|
+
const map = record(output.map);
|
|
990
|
+
const diagnostics = record(output.diagnostics);
|
|
991
|
+
const identity = record(output.identity);
|
|
992
|
+
const content = record(output.content);
|
|
993
|
+
const metadata: Record<string, number> = {};
|
|
994
|
+
const summary: string[] = [];
|
|
995
|
+
if (output.map !== undefined) {
|
|
996
|
+
const symbols = items(map.symbols).length;
|
|
997
|
+
metadata.symbols = symbols;
|
|
998
|
+
summary.push(`${symbols} symbols`);
|
|
999
|
+
}
|
|
1000
|
+
if (output.diagnostics !== undefined) {
|
|
1001
|
+
const errors = typeof diagnostics.error_count === "number" ? diagnostics.error_count : 0;
|
|
1002
|
+
const missing = typeof diagnostics.missing_count === "number" ? diagnostics.missing_count : 0;
|
|
1003
|
+
metadata.error_count = errors;
|
|
1004
|
+
metadata.missing_count = missing;
|
|
1005
|
+
summary.push(`${errors} errors, ${missing} missing`);
|
|
1006
|
+
}
|
|
1007
|
+
if (output.identity !== undefined) {
|
|
1008
|
+
if (typeof identity.line === "number") metadata.line = identity.line;
|
|
1009
|
+
if (typeof identity.column === "number") metadata.column = identity.column;
|
|
1010
|
+
const identifier = record(identity.identifier).text;
|
|
1011
|
+
if (typeof identifier === "string") summary.push(identifier);
|
|
1012
|
+
}
|
|
1013
|
+
if (typeof content.start_line === "number") metadata.start_line = content.start_line;
|
|
1014
|
+
if (typeof content.end_line === "number") metadata.end_line = content.end_line;
|
|
1015
|
+
return {
|
|
1016
|
+
title: summary.length > 0 ? `Digested ${args.path} (${summary.join(", ")})` : `Digested ${args.path}`,
|
|
1017
|
+
metadata,
|
|
1018
|
+
};
|
|
973
1019
|
}
|
|
974
1020
|
case "search": {
|
|
975
1021
|
const results = items(output.results);
|
|
@@ -989,15 +1035,6 @@ function resultPresentation(kind: PresentationKind, args: any, value: unknown):
|
|
|
989
1035
|
const references = items(output.references).length;
|
|
990
1036
|
return { title: `Found ${references} references`, metadata: { references } };
|
|
991
1037
|
}
|
|
992
|
-
case "hover": {
|
|
993
|
-
const identifier = record(output.identifier).text;
|
|
994
|
-
const line = output.line;
|
|
995
|
-
const column = output.column;
|
|
996
|
-
const metadata: Record<string, number> = {};
|
|
997
|
-
if (typeof line === "number") metadata.line = line;
|
|
998
|
-
if (typeof column === "number") metadata.column = column;
|
|
999
|
-
return { title: typeof identifier === "string" ? `Identified ${identifier}` : initialTitle(kind, args), metadata };
|
|
1000
|
-
}
|
|
1001
1038
|
case "rename": {
|
|
1002
1039
|
const oldName = output.old_name;
|
|
1003
1040
|
const newName = output.new_name;
|
|
@@ -1019,14 +1056,6 @@ function resultPresentation(kind: PresentationKind, args: any, value: unknown):
|
|
|
1019
1056
|
const preview = typeof output.preview_error === "string" ? " (preview unavailable)" : "";
|
|
1020
1057
|
return { title: `${kind === "edit" ? "Edited" : "Wrote"} ${args.path}${preview}`, metadata: {} };
|
|
1021
1058
|
}
|
|
1022
|
-
case "check": {
|
|
1023
|
-
const errors = typeof output.error_count === "number" ? output.error_count : 0;
|
|
1024
|
-
const missing = typeof output.missing_count === "number" ? output.missing_count : 0;
|
|
1025
|
-
return {
|
|
1026
|
-
title: `Checked ${args.path} (${errors} errors, ${missing} missing)`,
|
|
1027
|
-
metadata: { error_count: errors, missing_count: missing },
|
|
1028
|
-
};
|
|
1029
|
-
}
|
|
1030
1059
|
}
|
|
1031
1060
|
return { title: initialTitle(kind, args), metadata: {} };
|
|
1032
1061
|
}
|
|
@@ -1079,19 +1108,18 @@ export const ReadSeekPlugin: Plugin = async (_input, options) => {
|
|
|
1079
1108
|
tool: {
|
|
1080
1109
|
readseek_read: readseekTool(
|
|
1081
1110
|
imagePolicy === "off"
|
|
1082
|
-
? "Read text with LINE:HASH anchors. Image
|
|
1083
|
-
: `Read text with LINE:HASH anchors. For an image
|
|
1111
|
+
? "Read text with LINE:HASH anchors. Image reads are disabled. For documents, use readseek_view."
|
|
1112
|
+
: `Read text with LINE:HASH anchors. For an image, select image (${imageModes.join(", ")}); omit it to skip image files. For documents such as PDFs, use readseek_view.`,
|
|
1084
1113
|
{
|
|
1085
1114
|
path: tool.schema.string().describe("Path relative to the project directory"),
|
|
1086
1115
|
offset: tool.schema.number().int().positive().optional().describe("One-based starting line"),
|
|
1087
1116
|
limit: tool.schema.number().int().positive().optional().describe(`Maximum number of lines; defaults to ${DEFAULT_READ_LIMIT}`),
|
|
1088
|
-
page: tool.schema.number().int().positive().optional().describe("One-based PDF page; defaults to 1"),
|
|
1089
1117
|
language: tool.schema.string().optional().describe("Language override when auto-detection is ambiguous"),
|
|
1090
1118
|
...(imagePolicy === "off"
|
|
1091
1119
|
? {}
|
|
1092
1120
|
: {
|
|
1093
1121
|
image: tool.schema.enum(imageModes as [ImageMode, ...ImageMode[]]).optional()
|
|
1094
|
-
.describe("
|
|
1122
|
+
.describe("Standalone image processing mode")
|
|
1095
1123
|
}),
|
|
1096
1124
|
},
|
|
1097
1125
|
"read",
|
|
@@ -1099,47 +1127,123 @@ export const ReadSeekPlugin: Plugin = async (_input, options) => {
|
|
|
1099
1127
|
const filePath = resolvePath(context.directory, input.path as string);
|
|
1100
1128
|
await authorizeRead(context, filePath);
|
|
1101
1129
|
const image = input.image as ImageMode | undefined;
|
|
1102
|
-
if (imagePolicy === "off" && image !== undefined) throw new Error("image
|
|
1130
|
+
if (imagePolicy === "off" && image !== undefined) throw new Error("image reads are disabled");
|
|
1103
1131
|
if (imagePolicy === "on" && image === "none") throw new Error('image="none" requires imageMode="auto"');
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1132
|
+
|
|
1133
|
+
if (isLikelyDocumentPath(filePath)) {
|
|
1134
|
+
if (image !== undefined) throw new Error("image does not apply to documents; use readseek_view");
|
|
1135
|
+
if (input.offset !== undefined || input.limit !== undefined) {
|
|
1136
|
+
throw new Error("offset and limit do not apply to documents; use readseek_view");
|
|
1137
|
+
}
|
|
1138
|
+
if (input.language !== undefined) {
|
|
1139
|
+
throw new Error("language does not apply to documents; use readseek_view");
|
|
1140
|
+
}
|
|
1141
|
+
return digestDocumentReceipt(context, filePath);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// Image paths without an image mode only need metadata (skip without loading content).
|
|
1145
|
+
// Text paths always use one digest --select content spawn.
|
|
1146
|
+
let detectedText = false;
|
|
1147
|
+
if (image === undefined && isLikelyImagePath(filePath)) {
|
|
1148
|
+
const envelope = await runReadSeek(context, digestArgs(filePath, "metadata"));
|
|
1149
|
+
const metadata = digestMetadata(envelope);
|
|
1150
|
+
if (isPdfFile(metadata)) return documentReceipt(filePath, envelope);
|
|
1151
|
+
if (isVisualFile(metadata)) {
|
|
1152
|
+
return { file: filePath, skipped: true, reason: "image mode not selected" };
|
|
1153
|
+
}
|
|
1154
|
+
detectedText = true;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
const extra: string[] = [];
|
|
1158
|
+
const wantsImage = !detectedText
|
|
1159
|
+
&& (image !== undefined || isLikelyImagePath(filePath));
|
|
1160
|
+
if (!wantsImage) {
|
|
1161
|
+
const start = (input.offset as number | undefined) ?? 1;
|
|
1162
|
+
const end = start + ((input.limit as number | undefined) ?? DEFAULT_READ_LIMIT) - 1;
|
|
1163
|
+
if (start > 1) withAt(extra, start);
|
|
1164
|
+
extra.push("--end", String(end));
|
|
1165
|
+
if (input.language) extra.push("--language", input.language as string);
|
|
1166
|
+
} else {
|
|
1167
|
+
if (input.offset !== undefined || input.limit !== undefined) {
|
|
1168
|
+
throw new Error("offset and limit do not apply to image reads");
|
|
1169
|
+
}
|
|
1170
|
+
if (input.language !== undefined) {
|
|
1171
|
+
throw new Error("language does not apply to image reads");
|
|
1172
|
+
}
|
|
1173
|
+
if (image !== undefined) extra.push("--vision-mode", image);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
let envelope: unknown;
|
|
1177
|
+
try {
|
|
1178
|
+
envelope = await runReadSeek(
|
|
1179
|
+
context,
|
|
1180
|
+
digestArgs(filePath, "content", extra),
|
|
1181
|
+
image !== undefined && image !== "none" ? { timeoutMs: DEFAULT_VISION_TIMEOUT_MS } : {},
|
|
1182
|
+
);
|
|
1183
|
+
} catch (error) {
|
|
1184
|
+
// A visual or document file with an uncommon extension rejects text bounds before
|
|
1185
|
+
// an envelope can be returned. Probe metadata only on this cold path.
|
|
1186
|
+
if (image !== undefined) throw error;
|
|
1187
|
+
if (isDocumentBoundsError(error)) {
|
|
1188
|
+
return digestDocumentReceipt(context, filePath);
|
|
1189
|
+
}
|
|
1190
|
+
if (isVisualBoundsError(error)) {
|
|
1191
|
+
const metadataEnvelope = await runReadSeek(context, digestArgs(filePath, "metadata"));
|
|
1192
|
+
if (isPdfFile(digestMetadata(metadataEnvelope))) {
|
|
1193
|
+
return documentReceipt(filePath, metadataEnvelope);
|
|
1194
|
+
}
|
|
1195
|
+
if (isVisualFile(digestMetadata(metadataEnvelope))) {
|
|
1196
|
+
return { file: filePath, skipped: true, reason: "image mode not selected" };
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
throw error;
|
|
1200
|
+
}
|
|
1201
|
+
const metadata = digestMetadata(envelope);
|
|
1202
|
+
if (isPdfFile(metadata) || record(envelope).cache !== undefined) {
|
|
1203
|
+
return documentReceipt(filePath, envelope);
|
|
1204
|
+
}
|
|
1205
|
+
const visual = isVisualFile(metadata);
|
|
1206
|
+
|
|
1107
1207
|
if (visual && image === undefined) {
|
|
1108
1208
|
return { file: filePath, skipped: true, reason: "image mode not selected" };
|
|
1109
1209
|
}
|
|
1110
1210
|
if (visual && (input.offset !== undefined || input.limit !== undefined)) {
|
|
1111
|
-
throw new Error("offset and limit do not apply to image
|
|
1211
|
+
throw new Error("offset and limit do not apply to image reads");
|
|
1112
1212
|
}
|
|
1113
1213
|
if (visual && input.language !== undefined) {
|
|
1114
|
-
throw new Error("language does not apply to image
|
|
1214
|
+
throw new Error("language does not apply to image reads");
|
|
1115
1215
|
}
|
|
1116
|
-
if (!pdf && input.page !== undefined) throw new Error("page applies to PDF reads only");
|
|
1117
1216
|
|
|
1118
|
-
|
|
1119
|
-
if (!visual) {
|
|
1120
|
-
args.push("--end", String((input.offset ?? 1) + ((input.limit as number | undefined) ?? DEFAULT_READ_LIMIT) - 1));
|
|
1121
|
-
}
|
|
1122
|
-
if (input.language) args.push("--language", input.language as string);
|
|
1123
|
-
if (image !== undefined) args.push("--vision-mode", image);
|
|
1124
|
-
if (pdf) args.push("--page", String(input.page ?? 1));
|
|
1125
|
-
return runReadSeek(context, args);
|
|
1217
|
+
return requireDigestFacet(envelope, "content");
|
|
1126
1218
|
},
|
|
1127
1219
|
formatReadResult,
|
|
1128
1220
|
),
|
|
1129
1221
|
readseek_view: readseekTool(
|
|
1130
|
-
"
|
|
1222
|
+
"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.",
|
|
1131
1223
|
{
|
|
1132
|
-
path: tool.schema.string().describe("
|
|
1224
|
+
path: tool.schema.string().describe("Document path relative to the project directory"),
|
|
1133
1225
|
node: tool.schema.string().min(1).optional().describe("Node ID to use as the view root"),
|
|
1134
1226
|
page: tool.schema.number().int().positive().optional().describe("One-based source page"),
|
|
1135
1227
|
kind: tool.schema.enum(DOCUMENT_NODE_KINDS).optional().describe("Node kind filter"),
|
|
1136
1228
|
depth: tool.schema.number().int().nonnegative().optional().describe("Maximum depth below selected roots"),
|
|
1137
1229
|
outline: tool.schema.boolean().optional().describe("Return outline nodes only"),
|
|
1230
|
+
visionMode: tool.schema.enum(VISION_ANALYSIS_MODES).optional()
|
|
1231
|
+
.describe("Analyze selected assets: all, ocr, caption, or objects"),
|
|
1232
|
+
visionLevel: tool.schema.enum(VISION_LEVELS).optional()
|
|
1233
|
+
.describe("Vision inference level: low (default), medium, or high"),
|
|
1138
1234
|
},
|
|
1139
1235
|
"view",
|
|
1140
1236
|
async (input, context) => {
|
|
1141
1237
|
const filePath = resolvePath(context.directory, input.path as string);
|
|
1142
1238
|
await authorizeRead(context, filePath);
|
|
1239
|
+
const visionMode = input.visionMode as VisionAnalysisMode | undefined;
|
|
1240
|
+
const visionLevel = input.visionLevel as VisionLevel | undefined;
|
|
1241
|
+
if (visionLevel !== undefined && visionMode === undefined) {
|
|
1242
|
+
throw new Error("visionLevel requires visionMode");
|
|
1243
|
+
}
|
|
1244
|
+
if (input.outline && (visionMode !== undefined || visionLevel !== undefined)) {
|
|
1245
|
+
throw new Error("vision options cannot be combined with outline");
|
|
1246
|
+
}
|
|
1143
1247
|
const cacheDir = await ensureReadSeekCache(context);
|
|
1144
1248
|
const args = ["--readseek-dir", cacheDir, "view", filePath];
|
|
1145
1249
|
if (input.node) args.push("--node", input.node as string);
|
|
@@ -1147,23 +1251,57 @@ export const ReadSeekPlugin: Plugin = async (_input, options) => {
|
|
|
1147
1251
|
if (input.kind) args.push("--kind", input.kind as string);
|
|
1148
1252
|
if (input.depth !== undefined) args.push("--depth", String(input.depth));
|
|
1149
1253
|
optionalFlag(args, input.outline as boolean | undefined, "--outline");
|
|
1150
|
-
|
|
1254
|
+
if (visionMode !== undefined) args.push("--vision-mode", visionMode);
|
|
1255
|
+
if (visionLevel !== undefined) args.push("--vision-level", visionLevel);
|
|
1256
|
+
return runReadSeekRaw(
|
|
1257
|
+
context,
|
|
1258
|
+
args,
|
|
1259
|
+
visionMode === undefined ? {} : { timeoutMs: DEFAULT_VISION_TIMEOUT_MS },
|
|
1260
|
+
);
|
|
1151
1261
|
},
|
|
1152
1262
|
(result) => ({ output: boundDocumentOutput(String(result)) }),
|
|
1153
1263
|
),
|
|
1154
|
-
|
|
1155
|
-
"
|
|
1264
|
+
readseek_digest: readseekTool(
|
|
1265
|
+
"Digest native facets for a path and return the full envelope unchanged. Facets: metadata, content, map, diagnostics, identity (comma-separated). Metadata is always present; other selected facets appear when available.",
|
|
1156
1266
|
{
|
|
1157
1267
|
path: tool.schema.string().describe("Path relative to the project directory"),
|
|
1268
|
+
select: tool.schema.string().optional()
|
|
1269
|
+
.describe("Comma-separated facets: metadata, content, map, diagnostics, identity; defaults to content"),
|
|
1270
|
+
at: tool.schema.string().optional()
|
|
1271
|
+
.describe("Location: line:N[:COLUMN], hash:HASH[:COLUMN], or symbol:NAME"),
|
|
1272
|
+
end: tool.schema.number().int().positive().optional().describe("Last line to include; content facet only"),
|
|
1273
|
+
limit: tool.schema.number().int().positive().optional().describe("Maximum number of lines; content facet only"),
|
|
1158
1274
|
language: tool.schema.string().optional().describe("Language override when auto-detection is ambiguous"),
|
|
1275
|
+
visionMode: tool.schema.enum(["none", ...VISION_ANALYSIS_MODES] as [ImageMode, ...ImageMode[]]).optional()
|
|
1276
|
+
.describe("Standalone image mode for the content facet: none, all, ocr, caption, or objects"),
|
|
1277
|
+
visionLevel: tool.schema.enum(VISION_LEVELS).optional()
|
|
1278
|
+
.describe("Vision inference level: low (default), medium, or high; content facet only"),
|
|
1159
1279
|
},
|
|
1160
|
-
"
|
|
1280
|
+
"digest",
|
|
1161
1281
|
async (input, context) => {
|
|
1162
1282
|
const filePath = resolvePath(context.directory, input.path as string);
|
|
1163
1283
|
await authorizeRead(context, filePath);
|
|
1164
|
-
const
|
|
1284
|
+
const select = ((input.select as string | undefined) ?? "content").trim() || "content";
|
|
1285
|
+
const visionMode = input.visionMode as ImageMode | undefined;
|
|
1286
|
+
const visionLevel = input.visionLevel as VisionLevel | undefined;
|
|
1287
|
+
if (visionLevel !== undefined && visionMode === undefined) {
|
|
1288
|
+
throw new Error("visionLevel requires visionMode");
|
|
1289
|
+
}
|
|
1290
|
+
const cacheDir = await ensureReadSeekCache(context);
|
|
1291
|
+
const args = ["--readseek-dir", cacheDir, ...digestArgs(filePath, select)];
|
|
1292
|
+
if (input.at) args.push("--at", input.at as string);
|
|
1293
|
+
if (input.end !== undefined) args.push("--end", String(input.end));
|
|
1294
|
+
if (input.limit !== undefined) args.push("--limit", String(input.limit));
|
|
1165
1295
|
if (input.language) args.push("--language", input.language as string);
|
|
1166
|
-
|
|
1296
|
+
if (visionMode !== undefined) args.push("--vision-mode", visionMode);
|
|
1297
|
+
if (visionLevel !== undefined) args.push("--vision-level", visionLevel);
|
|
1298
|
+
return runReadSeek(
|
|
1299
|
+
context,
|
|
1300
|
+
args,
|
|
1301
|
+
visionMode === undefined || visionMode === "none"
|
|
1302
|
+
? {}
|
|
1303
|
+
: { timeoutMs: DEFAULT_VISION_TIMEOUT_MS },
|
|
1304
|
+
);
|
|
1167
1305
|
},
|
|
1168
1306
|
),
|
|
1169
1307
|
readseek_grep: readseekTool(
|
|
@@ -1235,7 +1373,7 @@ export const ReadSeekPlugin: Plugin = async (_input, options) => {
|
|
|
1235
1373
|
}
|
|
1236
1374
|
const linesByNumber = new Map<number, unknown>();
|
|
1237
1375
|
for (const [start, end] of mergedRanges) {
|
|
1238
|
-
const output = record(await
|
|
1376
|
+
const output = record(await runDigestContent(context, readRangeArgs(filePath, start, end)));
|
|
1239
1377
|
for (const hashline of items(output.hashlines)) {
|
|
1240
1378
|
const line = record(hashline).line;
|
|
1241
1379
|
if (typeof line === "number" && !linesByNumber.has(line)) linesByNumber.set(line, hashline);
|
|
@@ -1312,34 +1450,18 @@ export const ReadSeekPlugin: Plugin = async (_input, options) => {
|
|
|
1312
1450
|
throw new Error("line and column require scope");
|
|
1313
1451
|
}
|
|
1314
1452
|
const target = resolvePath(context.directory, (input.path as string | undefined) ?? ".");
|
|
1315
|
-
const args = ["refs", target
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1453
|
+
const args = ["refs", target];
|
|
1454
|
+
if (input.scope) {
|
|
1455
|
+
withAt(args, input.line as number, input.column as number | undefined);
|
|
1456
|
+
} else {
|
|
1457
|
+
args.push(input.name as string);
|
|
1458
|
+
}
|
|
1319
1459
|
if (input.language) args.push("--language", input.language as string);
|
|
1320
1460
|
withSearchFlags(args, input);
|
|
1321
1461
|
await authorizeSearch(context, target, input.name as string);
|
|
1322
1462
|
return runReadSeek(context, args);
|
|
1323
1463
|
},
|
|
1324
1464
|
),
|
|
1325
|
-
readseek_hover: readseekTool(
|
|
1326
|
-
"Inspect the symbol at a source location. This identifies the token and enclosing symbol; use it before definition lookup or binding-specific references.",
|
|
1327
|
-
{
|
|
1328
|
-
path: tool.schema.string().describe("Path relative to the project directory"),
|
|
1329
|
-
line: tool.schema.number().int().positive().describe("One-based cursor line"),
|
|
1330
|
-
column: tool.schema.number().int().positive().optional().describe("One-based cursor byte column for disambiguation"),
|
|
1331
|
-
language: tool.schema.string().optional().describe("Language override when auto-detection is ambiguous"),
|
|
1332
|
-
},
|
|
1333
|
-
"hover",
|
|
1334
|
-
async (input, context) => {
|
|
1335
|
-
const filePath = resolvePath(context.directory, input.path as string);
|
|
1336
|
-
await authorizeRead(context, filePath);
|
|
1337
|
-
const args = ["identify", `${filePath}:${input.line}`];
|
|
1338
|
-
if (input.column !== undefined) args.push("--column", String(input.column));
|
|
1339
|
-
if (input.language) args.push("--language", input.language as string);
|
|
1340
|
-
return runReadSeek(context, args);
|
|
1341
|
-
},
|
|
1342
|
-
),
|
|
1343
1465
|
readseek_rename: readseekTool(
|
|
1344
1466
|
"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.",
|
|
1345
1467
|
{
|
|
@@ -1355,14 +1477,16 @@ export const ReadSeekPlugin: Plugin = async (_input, options) => {
|
|
|
1355
1477
|
const filePath = resolvePath(context.directory, input.path as string);
|
|
1356
1478
|
await authorizeRead(context, filePath);
|
|
1357
1479
|
if (input.workspace) {
|
|
1358
|
-
const identifyArgs =
|
|
1359
|
-
|
|
1360
|
-
const
|
|
1480
|
+
const identifyArgs = digestArgs(filePath, "identity");
|
|
1481
|
+
withAt(identifyArgs, input.line as number, input.column as number | undefined);
|
|
1482
|
+
const envelope = await runReadSeek(context, identifyArgs);
|
|
1483
|
+
const name = identifiedName(requireDigestFacet(envelope, "identity"));
|
|
1361
1484
|
if (!name) throw new Error("readseek could not identify a binding at the rename cursor");
|
|
1362
1485
|
await authorizeSearch(context, context.directory, name);
|
|
1363
1486
|
}
|
|
1364
|
-
const args = ["rename", filePath
|
|
1365
|
-
|
|
1487
|
+
const args = ["rename", filePath];
|
|
1488
|
+
withAt(args, input.line as number, input.column as number | undefined);
|
|
1489
|
+
args.push("--to", input.to as string);
|
|
1366
1490
|
if (input.workspace) args.push("--workspace", context.directory);
|
|
1367
1491
|
const plan = await runReadSeek(context, args);
|
|
1368
1492
|
if (input.apply === false) return plan;
|
|
@@ -1372,11 +1496,14 @@ export const ReadSeekPlugin: Plugin = async (_input, options) => {
|
|
|
1372
1496
|
throw new Error("readseek rename plan did not include a plan hash");
|
|
1373
1497
|
}
|
|
1374
1498
|
await authorizeEdit(context, files);
|
|
1375
|
-
return runReadSeek(context, [...args, "--plan-hash", planHash, "--apply"], {
|
|
1499
|
+
return runReadSeek(context, [...args, "--plan-hash", planHash, "--apply"], {
|
|
1500
|
+
cancelable: false,
|
|
1501
|
+
timeoutMs: DEFAULT_READSEEK_TIMEOUT_MS,
|
|
1502
|
+
});
|
|
1376
1503
|
},
|
|
1377
1504
|
),
|
|
1378
1505
|
readseek_edit: readseekTool(
|
|
1379
|
-
"Edit an existing text file with fresh LINE:HASH anchors
|
|
1506
|
+
"Edit an existing text file with fresh LINE:HASH anchors, exact text, or a mapped symbol. The native engine rejects stale or overlapping edits before writing.",
|
|
1380
1507
|
{
|
|
1381
1508
|
path: tool.schema.string().describe("Path relative to the project directory"),
|
|
1382
1509
|
edits: tool.schema.array(tool.schema.union([
|
|
@@ -1399,31 +1526,64 @@ export const ReadSeekPlugin: Plugin = async (_input, options) => {
|
|
|
1399
1526
|
new_text: tool.schema.string().describe("Text to insert after the anchored line"),
|
|
1400
1527
|
}).strict(),
|
|
1401
1528
|
}).strict(),
|
|
1402
|
-
|
|
1529
|
+
tool.schema.object({
|
|
1530
|
+
replace: tool.schema.object({
|
|
1531
|
+
old_text: tool.schema.string().describe("Exact text to find"),
|
|
1532
|
+
new_text: tool.schema.string().describe("Replacement text"),
|
|
1533
|
+
all: tool.schema.boolean().optional().describe("Replace every exact match"),
|
|
1534
|
+
}).strict(),
|
|
1535
|
+
}).strict(),
|
|
1536
|
+
tool.schema.object({
|
|
1537
|
+
replace_symbol: tool.schema.object({
|
|
1538
|
+
symbol: tool.schema.string().describe("Mapped symbol name"),
|
|
1539
|
+
new_body: tool.schema.string().describe("Complete replacement body for the symbol"),
|
|
1540
|
+
}).strict(),
|
|
1541
|
+
}).strict(),
|
|
1542
|
+
])).min(1).describe("Use set_line, replace_lines, insert_after, replace, or replace_symbol"),
|
|
1543
|
+
language: tool.schema.string().optional().describe("Language override for symbol replacement"),
|
|
1403
1544
|
},
|
|
1404
1545
|
"edit",
|
|
1405
1546
|
async (input, context) => {
|
|
1406
1547
|
const filePath = resolvePath(context.directory, input.path as string);
|
|
1407
|
-
|
|
1548
|
+
validateEdits(input.edits);
|
|
1408
1549
|
return withFileMutationQueue(filePath, async () => {
|
|
1409
1550
|
context.abort.throwIfAborted();
|
|
1410
1551
|
await authorizeRead(context, filePath);
|
|
1411
1552
|
await rejectSymlinkMutation(filePath);
|
|
1412
|
-
const edits = input.edits as
|
|
1413
|
-
const
|
|
1414
|
-
|
|
1415
|
-
|
|
1553
|
+
const edits = input.edits as EditItem[];
|
|
1554
|
+
const request = JSON.stringify({ edits });
|
|
1555
|
+
const baseArgs = ["edit", filePath];
|
|
1556
|
+
if (input.language) baseArgs.push("--language", input.language as string);
|
|
1557
|
+
const plan = record(await runReadSeek(context, baseArgs, { stdin: request }));
|
|
1558
|
+
if (
|
|
1559
|
+
typeof plan.before_content !== "string" ||
|
|
1560
|
+
typeof plan.content !== "string" ||
|
|
1561
|
+
typeof plan.plan_hash !== "string"
|
|
1562
|
+
) {
|
|
1563
|
+
throw new Error("readseek edit plan is missing before_content, content, or plan_hash");
|
|
1564
|
+
}
|
|
1565
|
+
const before = plan.before_content;
|
|
1566
|
+
const after = plan.content;
|
|
1567
|
+
const previewStart = Math.max(1, firstChangedLine(before, after) - 3);
|
|
1568
|
+
const previewArgs = readRangeArgs(filePath, previewStart, previewStart + EDIT_RESULT_READ_LIMIT - 1);
|
|
1569
|
+
if (before === after) {
|
|
1570
|
+
return postMutationPreview(context, previewArgs, filePath, false);
|
|
1571
|
+
}
|
|
1416
1572
|
await authorizeEdit(context, [filePath], simpleDiff(filePath, before, after), false);
|
|
1417
1573
|
context.abort.throwIfAborted();
|
|
1418
|
-
|
|
1419
|
-
const firstEditedLine = Math.min(...edits.flatMap((edit) => anchorRefs(edit).map((ref) => ref.line)));
|
|
1420
|
-
const previewStart = Math.max(1, firstEditedLine - 3);
|
|
1421
|
-
return postMutationPreview(
|
|
1574
|
+
const applied = record(await runReadSeek(
|
|
1422
1575
|
context,
|
|
1423
|
-
["
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1576
|
+
[...baseArgs, "--apply", "--plan-hash", plan.plan_hash],
|
|
1577
|
+
{
|
|
1578
|
+
cancelable: false,
|
|
1579
|
+
stdin: request,
|
|
1580
|
+
timeoutMs: DEFAULT_READSEEK_TIMEOUT_MS,
|
|
1581
|
+
},
|
|
1582
|
+
));
|
|
1583
|
+
if (applied.applied !== true) {
|
|
1584
|
+
throw new Error("readseek did not confirm that the edit was applied");
|
|
1585
|
+
}
|
|
1586
|
+
return postMutationPreview(context, previewArgs, filePath, true);
|
|
1427
1587
|
});
|
|
1428
1588
|
},
|
|
1429
1589
|
),
|
|
@@ -1448,28 +1608,13 @@ export const ReadSeekPlugin: Plugin = async (_input, options) => {
|
|
|
1448
1608
|
await writeText(filePath, before, content);
|
|
1449
1609
|
return postMutationPreview(
|
|
1450
1610
|
context,
|
|
1451
|
-
|
|
1611
|
+
digestArgs(filePath, "content", ["--end", String(DEFAULT_READ_LIMIT)]),
|
|
1452
1612
|
filePath,
|
|
1453
1613
|
!before.exists || before.content !== content,
|
|
1454
1614
|
);
|
|
1455
1615
|
});
|
|
1456
1616
|
},
|
|
1457
1617
|
),
|
|
1458
|
-
readseek_check: readseekTool(
|
|
1459
|
-
"Check a source file for parser errors or missing syntax after edits. Validation requires a supported tree-sitter language; files without a supported parser are not checked. This is not a compiler, type checker, test runner, or linter.",
|
|
1460
|
-
{
|
|
1461
|
-
path: tool.schema.string().describe("Path relative to the project directory"),
|
|
1462
|
-
language: tool.schema.string().optional().describe("Language override when auto-detection is ambiguous"),
|
|
1463
|
-
},
|
|
1464
|
-
"check",
|
|
1465
|
-
async (input, context) => {
|
|
1466
|
-
const filePath = resolvePath(context.directory, input.path as string);
|
|
1467
|
-
await authorizeRead(context, filePath);
|
|
1468
|
-
const args = ["check", filePath];
|
|
1469
|
-
if (input.language) args.push("--language", input.language as string);
|
|
1470
|
-
return runReadSeek(context, args);
|
|
1471
|
-
},
|
|
1472
|
-
),
|
|
1473
1618
|
},
|
|
1474
1619
|
"experimental.chat.system.transform": async (_input, output) => {
|
|
1475
1620
|
output.system.push(TOOL_POLICY);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-readseek",
|
|
3
|
-
"version": "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.
|
|
34
|
+
"@jarkkojs/readseek": "^0.9.1",
|
|
35
35
|
"@opencode-ai/plugin": "^1.17.20"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|