opencode-readseek 0.9.5 → 0.9.7
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/dist/index.js +824 -933
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
// index.ts
|
|
2
|
-
import
|
|
2
|
+
import path7 from "node:path";
|
|
3
|
+
|
|
4
|
+
// src/client.ts
|
|
3
5
|
import { createRequire as createRequire2 } from "node:module";
|
|
4
6
|
import { homedir } from "node:os";
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
+
import path3 from "node:path";
|
|
8
|
+
import { stat } from "node:fs/promises";
|
|
7
9
|
|
|
8
10
|
// ../readseek-api/src/contract.ts
|
|
9
11
|
var VISION_MODES = ["none", "all", "ocr", "caption", "objects"];
|
|
@@ -22,11 +24,10 @@ var DOCUMENT_NODE_KINDS = [
|
|
|
22
24
|
"structural_section"
|
|
23
25
|
];
|
|
24
26
|
var TOOL_NAMES = [
|
|
25
|
-
"
|
|
27
|
+
"digest",
|
|
26
28
|
"edit",
|
|
27
29
|
"write",
|
|
28
30
|
"grep",
|
|
29
|
-
"digest",
|
|
30
31
|
"view",
|
|
31
32
|
"search",
|
|
32
33
|
"def",
|
|
@@ -35,6 +36,8 @@ var TOOL_NAMES = [
|
|
|
35
36
|
];
|
|
36
37
|
var PARAM_DESCRIPTIONS = {
|
|
37
38
|
path: "Absolute or relative file path",
|
|
39
|
+
projectPath: "Path relative to the project directory",
|
|
40
|
+
projectSearchPath: "File or directory to search; defaults to the project directory",
|
|
38
41
|
searchPath: "File or directory to search; defaults to the current directory",
|
|
39
42
|
language: "Language override when auto-detection is ambiguous",
|
|
40
43
|
languageOverride: "Language override",
|
|
@@ -88,6 +91,7 @@ var PARAM_DESCRIPTIONS = {
|
|
|
88
91
|
symbol: "Mapped symbol name",
|
|
89
92
|
newBody: "Complete replacement body for the symbol",
|
|
90
93
|
apply: "When supported by the host, apply the verified plan instead of previewing only",
|
|
94
|
+
applyDefaultTrue: "Apply the verified edits; defaults to true",
|
|
91
95
|
planHash: "Require the apply plan to match this dry-run plan hash",
|
|
92
96
|
workspace: "Rename across the project",
|
|
93
97
|
to: "New symbol name",
|
|
@@ -103,11 +107,10 @@ var PARAM_DESCRIPTIONS = {
|
|
|
103
107
|
scopeContext: "Context lines within each symbol"
|
|
104
108
|
};
|
|
105
109
|
var TOOL_DESCRIPTIONS = {
|
|
106
|
-
|
|
110
|
+
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.",
|
|
107
111
|
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":"..."}}`.',
|
|
108
112
|
write: "Create or replace a complete text file and return `LINE:HASH` anchors. Use `edit` for small changes to an existing file.",
|
|
109
113
|
grep: "Search text or regex in files and return edit-ready `LINE:HASH` anchors. Use for identifiers, strings, configuration, errors, comments, and documentation.",
|
|
110
|
-
digest: "Digest source content and images into the native CLI envelope. Returns metadata plus requested facets unchanged from `readseek digest`; use for structural maps, diagnostics, and cursor identity.",
|
|
111
114
|
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.",
|
|
112
115
|
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.",
|
|
113
116
|
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.",
|
|
@@ -115,7 +118,10 @@ var TOOL_DESCRIPTIONS = {
|
|
|
115
118
|
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."
|
|
116
119
|
};
|
|
117
120
|
function renderToolDescription(name, aliases = {}) {
|
|
118
|
-
|
|
121
|
+
const desc = TOOL_DESCRIPTIONS[name];
|
|
122
|
+
if (!desc)
|
|
123
|
+
throw new Error(`Missing TOOL_DESCRIPTIONS for ${name}`);
|
|
124
|
+
return TOOL_NAMES.reduce((description, toolName) => description.replaceAll(`\`${toolName}\``, `\`${aliases[toolName] ?? toolName}\``), desc);
|
|
119
125
|
}
|
|
120
126
|
var gitFlagsSchema = {
|
|
121
127
|
cached: {
|
|
@@ -194,25 +200,27 @@ var editItemSchema = {
|
|
|
194
200
|
};
|
|
195
201
|
var TOOL_CONTRACTS = [
|
|
196
202
|
{
|
|
197
|
-
name: "
|
|
198
|
-
title: "
|
|
199
|
-
description: TOOL_DESCRIPTIONS.
|
|
203
|
+
name: "digest",
|
|
204
|
+
title: "Digest source",
|
|
205
|
+
description: TOOL_DESCRIPTIONS.digest,
|
|
200
206
|
inputSchema: {
|
|
201
207
|
type: "object",
|
|
202
208
|
properties: {
|
|
203
209
|
path: { type: "string", description: PARAM_DESCRIPTIONS.path },
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
210
|
+
select: { type: "string", description: PARAM_DESCRIPTIONS.select },
|
|
211
|
+
at: { type: "string", description: PARAM_DESCRIPTIONS.at },
|
|
212
|
+
end: { type: "integer", minimum: 1, description: PARAM_DESCRIPTIONS.end },
|
|
213
|
+
limit: { type: "integer", minimum: 1, description: PARAM_DESCRIPTIONS.limit },
|
|
214
|
+
language: { type: "string", description: PARAM_DESCRIPTIONS.languageOverride },
|
|
215
|
+
visionMode: {
|
|
208
216
|
type: "string",
|
|
209
|
-
enum: [
|
|
210
|
-
description: PARAM_DESCRIPTIONS.
|
|
217
|
+
enum: [...VISION_MODES],
|
|
218
|
+
description: PARAM_DESCRIPTIONS.visionModeDigest
|
|
211
219
|
},
|
|
212
|
-
|
|
220
|
+
visionLevel: {
|
|
213
221
|
type: "string",
|
|
214
|
-
enum: [...
|
|
215
|
-
description: PARAM_DESCRIPTIONS.
|
|
222
|
+
enum: [...VISION_LEVELS],
|
|
223
|
+
description: PARAM_DESCRIPTIONS.visionLevel
|
|
216
224
|
}
|
|
217
225
|
},
|
|
218
226
|
required: ["path"],
|
|
@@ -286,34 +294,6 @@ var TOOL_CONTRACTS = [
|
|
|
286
294
|
additionalProperties: false
|
|
287
295
|
}
|
|
288
296
|
},
|
|
289
|
-
{
|
|
290
|
-
name: "digest",
|
|
291
|
-
title: "Digest source",
|
|
292
|
-
description: TOOL_DESCRIPTIONS.digest,
|
|
293
|
-
inputSchema: {
|
|
294
|
-
type: "object",
|
|
295
|
-
properties: {
|
|
296
|
-
path: { type: "string", description: PARAM_DESCRIPTIONS.path },
|
|
297
|
-
select: { type: "string", description: PARAM_DESCRIPTIONS.select },
|
|
298
|
-
at: { type: "string", description: PARAM_DESCRIPTIONS.at },
|
|
299
|
-
end: { type: "integer", minimum: 1, description: PARAM_DESCRIPTIONS.end },
|
|
300
|
-
limit: { type: "integer", minimum: 1, description: PARAM_DESCRIPTIONS.limit },
|
|
301
|
-
language: { type: "string", description: PARAM_DESCRIPTIONS.languageOverride },
|
|
302
|
-
visionMode: {
|
|
303
|
-
type: "string",
|
|
304
|
-
enum: [...VISION_MODES],
|
|
305
|
-
description: PARAM_DESCRIPTIONS.visionModeDigest
|
|
306
|
-
},
|
|
307
|
-
visionLevel: {
|
|
308
|
-
type: "string",
|
|
309
|
-
enum: [...VISION_LEVELS],
|
|
310
|
-
description: PARAM_DESCRIPTIONS.visionLevel
|
|
311
|
-
}
|
|
312
|
-
},
|
|
313
|
-
required: ["path"],
|
|
314
|
-
additionalProperties: false
|
|
315
|
-
}
|
|
316
|
-
},
|
|
317
297
|
{
|
|
318
298
|
name: "view",
|
|
319
299
|
title: "View document",
|
|
@@ -416,6 +396,88 @@ var TOOL_CONTRACTS = [
|
|
|
416
396
|
}
|
|
417
397
|
}
|
|
418
398
|
];
|
|
399
|
+
// ../readseek-api/src/policy.ts
|
|
400
|
+
var BUILTIN_FALLBACKS = {
|
|
401
|
+
read: "digest",
|
|
402
|
+
edit: "edit",
|
|
403
|
+
write: "write",
|
|
404
|
+
grep: "grep",
|
|
405
|
+
apply_patch: "edit"
|
|
406
|
+
};
|
|
407
|
+
var PREFERENCE_LEADS = {
|
|
408
|
+
digest: "Preferred for reading source with anchors and facets.",
|
|
409
|
+
edit: "Preferred for editing existing text files with fresh LINE:HASH anchors.",
|
|
410
|
+
write: "Preferred for creating or replacing a complete text file; returns LINE:HASH anchors.",
|
|
411
|
+
grep: "Preferred text/regex search; returns edit-ready LINE:HASH anchors.",
|
|
412
|
+
search: "Preferred for AST or structural code search with ast-grep-style patterns.",
|
|
413
|
+
def: "Preferred for go-to-definition or declaration lookup by name.",
|
|
414
|
+
refs: "Preferred for finding identifier-name matches or binding-specific references before rename or delete.",
|
|
415
|
+
rename: "Preferred for renaming a symbol with binding-aware, verified edits.",
|
|
416
|
+
view: "Preferred for digesting document structure or selected content."
|
|
417
|
+
};
|
|
418
|
+
var FALLBACK_TEMPLATES = {
|
|
419
|
+
read: "Fallback only. Prefer `digest` with fresh LINE:HASH anchors.",
|
|
420
|
+
edit: "Fallback only. Prefer `edit` with fresh LINE:HASH anchors.",
|
|
421
|
+
write: "Fallback only. Prefer `write` so the result returns LINE:HASH anchors.",
|
|
422
|
+
grep: "Fallback only. Prefer `grep` for text/regex search with edit-ready anchors.",
|
|
423
|
+
apply_patch: "Fallback only. Prefer `edit` for existing text files."
|
|
424
|
+
};
|
|
425
|
+
function resolveToolName(name, aliases = {}) {
|
|
426
|
+
return aliases[name] ?? name;
|
|
427
|
+
}
|
|
428
|
+
function rewriteBacktickedToolNames(text, aliases = {}) {
|
|
429
|
+
return TOOL_NAMES.reduce((rewritten, toolName) => rewritten.replaceAll(`\`${toolName}\``, `\`${resolveToolName(toolName, aliases)}\``), text);
|
|
430
|
+
}
|
|
431
|
+
function renderPreferredToolDescription(name, aliases = {}) {
|
|
432
|
+
return rewriteBacktickedToolNames(PREFERENCE_LEADS[name], aliases);
|
|
433
|
+
}
|
|
434
|
+
function renderFallbackToolDescription(builtIn, aliases = {}) {
|
|
435
|
+
const template = FALLBACK_TEMPLATES[builtIn];
|
|
436
|
+
if (!template)
|
|
437
|
+
return;
|
|
438
|
+
return rewriteBacktickedToolNames(template, aliases);
|
|
439
|
+
}
|
|
440
|
+
function renderToolRoutingPolicy(options = {}) {
|
|
441
|
+
const aliases = options.aliases ?? {};
|
|
442
|
+
const digest = resolveToolName("digest", aliases);
|
|
443
|
+
const edit = resolveToolName("edit", aliases);
|
|
444
|
+
const write = resolveToolName("write", aliases);
|
|
445
|
+
const grep = resolveToolName("grep", aliases);
|
|
446
|
+
const search = resolveToolName("search", aliases);
|
|
447
|
+
const def = resolveToolName("def", aliases);
|
|
448
|
+
const refs = resolveToolName("refs", aliases);
|
|
449
|
+
const rename = resolveToolName("rename", aliases);
|
|
450
|
+
const view = resolveToolName("view", aliases);
|
|
451
|
+
if (options.style === "server") {
|
|
452
|
+
return [
|
|
453
|
+
"Structural code navigation, document viewing, and hash-verified edits with readseek.",
|
|
454
|
+
"For structural questions, prefer these tools to text Grep/Bash: definitions, references, AST search, multi-facet digest (content/map/diagnostics/identity), and document views.",
|
|
455
|
+
`Use ${edit} for LINE:HASH-anchored mutations (preview with apply=false; apply defaults to true and re-checks plan_hash).`,
|
|
456
|
+
"Paths resolve against CLAUDE_PROJECT_DIR. Native Read/Grep/Edit hooks provide passive context;",
|
|
457
|
+
"call MCP tools for active navigation, document questions, or anchored edits."
|
|
458
|
+
].join(" ");
|
|
459
|
+
}
|
|
460
|
+
const title = options.title ?? "ReadSeek tool policy:";
|
|
461
|
+
const bullets = [];
|
|
462
|
+
if (options.preferOverBuiltIns) {
|
|
463
|
+
bullets.push("- Prefer ReadSeek tools over built-ins when they can do the job.");
|
|
464
|
+
}
|
|
465
|
+
bullets.push(`- Read with ${digest} first; ${edit} needs fresh LINE:HASH anchors.`, `- Use ${grep} for text/regex, ${search} for AST patterns, and ${digest}/${def}/${refs} for symbol navigation.`, `- Use ${edit} for existing files, ${write} for whole-file creation or replacement, and ${rename} for symbol renames.`, `- Use ${view} for document structure; run ${digest} with select diagnostics after source edits for a quick syntax check.`);
|
|
466
|
+
if (options.demoteBuiltIns && options.demoteBuiltIns.length > 0) {
|
|
467
|
+
bullets.push(`- Do not use built-in ${options.demoteBuiltIns.join(", ")} when a ReadSeek tool can do the job.`);
|
|
468
|
+
}
|
|
469
|
+
return [title, ...bullets].join(`
|
|
470
|
+
`);
|
|
471
|
+
}
|
|
472
|
+
function preferredToolDescriptionMap(aliases = {}) {
|
|
473
|
+
return Object.fromEntries(TOOL_NAMES.map((name) => [resolveToolName(name, aliases), renderPreferredToolDescription(name, aliases)]));
|
|
474
|
+
}
|
|
475
|
+
function fallbackToolDescriptionMap(builtIns = Object.keys(BUILTIN_FALLBACKS), aliases = {}) {
|
|
476
|
+
return Object.fromEntries(builtIns.flatMap((builtIn) => {
|
|
477
|
+
const description = renderFallbackToolDescription(builtIn, aliases);
|
|
478
|
+
return description ? [[builtIn, description]] : [];
|
|
479
|
+
}));
|
|
480
|
+
}
|
|
419
481
|
// ../readseek-api/src/validation.ts
|
|
420
482
|
function validateGitSelection(input) {
|
|
421
483
|
if (input.ignored && !input.others) {
|
|
@@ -764,50 +826,103 @@ async function runReadSeekBunText(binaryPath, args, options = {}) {
|
|
|
764
826
|
async function runReadSeekBunJson(binaryPath, args, options = {}) {
|
|
765
827
|
return parseSuccessfulJson(await runReadSeekBun(binaryPath, args, options));
|
|
766
828
|
}
|
|
767
|
-
//
|
|
768
|
-
var resolveFromHost = createRequire2(import.meta.url).resolve;
|
|
829
|
+
// src/constants.ts
|
|
769
830
|
var DEFAULT_VISION_TIMEOUT_MS = 30 * 60 * 1000;
|
|
770
831
|
var DEFAULT_READ_LIMIT = 2000;
|
|
771
832
|
var EDIT_RESULT_READ_LIMIT = 40;
|
|
772
833
|
var MAX_DOCUMENT_OUTPUT_BYTES = 256 * 1024;
|
|
773
834
|
var MAX_DOCUMENT_OUTPUT_LINES = 2000;
|
|
774
|
-
var
|
|
775
|
-
"ReadSeek tool policy:",
|
|
776
|
-
"- Prefer readseek_* tools over built-ins when they can do the job.",
|
|
777
|
-
"- Read with readseek_read first; readseek_edit needs fresh LINE:HASH anchors.",
|
|
778
|
-
"- Use readseek_grep for text/regex, readseek_search for AST patterns, and readseek_digest/def/refs for symbol navigation.",
|
|
779
|
-
"- Use readseek_edit for existing files, readseek_write for whole-file creation or replacement, and readseek_rename for symbol renames.",
|
|
780
|
-
"- Do not use built-in read, edit, write, grep, or apply_patch when a ReadSeek tool can do the job.",
|
|
781
|
-
"- Run readseek_digest with select=diagnostics after source edits for a quick syntax check."
|
|
782
|
-
].join(`
|
|
783
|
-
`);
|
|
784
|
-
var OPENCODE_TOOL_ALIASES = Object.fromEntries(TOOL_NAMES.map((name) => [name, `readseek_${name}`]));
|
|
785
|
-
function openCodeToolDescription(name) {
|
|
786
|
-
return renderToolDescription(name, OPENCODE_TOOL_ALIASES);
|
|
787
|
-
}
|
|
788
|
-
var PREFERRED_TOOL_DESCRIPTIONS = {
|
|
789
|
-
readseek_read: "Preferred file reader; returns LINE:HASH anchors for durable references.",
|
|
790
|
-
readseek_edit: "Preferred tool for editing existing text files with fresh LINE:HASH anchors.",
|
|
791
|
-
readseek_write: "Preferred tool for creating or replacing a complete text file; returns LINE:HASH anchors.",
|
|
792
|
-
readseek_rename: "Use to rename a symbol with binding-aware, verified edits.",
|
|
793
|
-
readseek_grep: "Preferred text/regex search; returns edit-ready LINE:HASH anchors.",
|
|
794
|
-
readseek_search: "Use for AST or structural code search with ast-grep-style patterns.",
|
|
795
|
-
readseek_digest: "Use to digest native facets (metadata, content, map, diagnostics, identity) and return the full envelope.",
|
|
796
|
-
readseek_def: "Use for go-to-definition or declaration lookup by name.",
|
|
797
|
-
readseek_refs: "Use to find identifier-name matches or binding-specific references before rename or delete.",
|
|
798
|
-
readseek_view: "Use to digest document structure or selected content."
|
|
799
|
-
};
|
|
800
|
-
var FALLBACK_TOOL_DESCRIPTIONS = {
|
|
801
|
-
read: "Fallback only. Prefer readseek_read so edits can use LINE:HASH anchors.",
|
|
802
|
-
edit: "Fallback only. Prefer readseek_edit with fresh LINE:HASH anchors.",
|
|
803
|
-
write: "Fallback only. Prefer readseek_write so the result returns LINE:HASH anchors.",
|
|
804
|
-
grep: "Fallback only. Prefer readseek_grep for text/regex search with edit-ready anchors.",
|
|
805
|
-
apply_patch: "Fallback only. Prefer readseek_edit for existing text files."
|
|
806
|
-
};
|
|
835
|
+
var MAX_PERMISSION_DIFF_BYTES = 32 * 1024;
|
|
807
836
|
var IMAGE_MODES = [...VISION_MODES];
|
|
808
837
|
var VISION_ANALYSIS_MODE_VALUES = [...VISION_ANALYSIS_MODES];
|
|
809
838
|
var VISION_LEVEL_VALUES = [...VISION_LEVELS];
|
|
810
839
|
var DOCUMENT_NODE_KIND_VALUES = [...DOCUMENT_NODE_KINDS];
|
|
840
|
+
|
|
841
|
+
// src/util.ts
|
|
842
|
+
import path2 from "node:path";
|
|
843
|
+
function render(value) {
|
|
844
|
+
return JSON.stringify(value, null, 2);
|
|
845
|
+
}
|
|
846
|
+
function record(value) {
|
|
847
|
+
return value && typeof value === "object" ? value : {};
|
|
848
|
+
}
|
|
849
|
+
function items(value) {
|
|
850
|
+
return Array.isArray(value) ? value : [];
|
|
851
|
+
}
|
|
852
|
+
function identifiedName(value) {
|
|
853
|
+
if (!value || typeof value !== "object")
|
|
854
|
+
return;
|
|
855
|
+
const identifier = value.identifier;
|
|
856
|
+
if (!identifier || typeof identifier !== "object")
|
|
857
|
+
return;
|
|
858
|
+
const text = identifier.text;
|
|
859
|
+
return typeof text === "string" ? text : undefined;
|
|
860
|
+
}
|
|
861
|
+
function collectAnchoredFiles(value, files) {
|
|
862
|
+
if (Array.isArray(value)) {
|
|
863
|
+
for (const item of value)
|
|
864
|
+
collectAnchoredFiles(item, files);
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
if (!value || typeof value !== "object")
|
|
868
|
+
return;
|
|
869
|
+
const output = value;
|
|
870
|
+
const hashlines = output.hashlines;
|
|
871
|
+
if (typeof output.file === "string" && Array.isArray(hashlines) && hashlines.some((line) => typeof record(line).line === "number" && typeof record(line).hash === "string"))
|
|
872
|
+
files.add(output.file);
|
|
873
|
+
for (const item of Object.values(output))
|
|
874
|
+
collectAnchoredFiles(item, files);
|
|
875
|
+
}
|
|
876
|
+
function renameFiles(value) {
|
|
877
|
+
const output = record(value);
|
|
878
|
+
const files = [];
|
|
879
|
+
if (typeof output.file === "string" && items(output.edits).length > 0)
|
|
880
|
+
files.push(path2.resolve(output.file));
|
|
881
|
+
for (const other of items(output.others)) {
|
|
882
|
+
const item = record(other);
|
|
883
|
+
if (typeof item.file === "string" && items(item.edits).length > 0)
|
|
884
|
+
files.push(path2.resolve(item.file));
|
|
885
|
+
}
|
|
886
|
+
return files;
|
|
887
|
+
}
|
|
888
|
+
function normalizeToLf(content) {
|
|
889
|
+
return content.replace(/\r\n/g, `
|
|
890
|
+
`).replace(/\r/g, `
|
|
891
|
+
`);
|
|
892
|
+
}
|
|
893
|
+
function diffLines(content) {
|
|
894
|
+
if (content === "")
|
|
895
|
+
return [];
|
|
896
|
+
const lines = content.split(`
|
|
897
|
+
`);
|
|
898
|
+
if (lines.at(-1) === "")
|
|
899
|
+
lines.pop();
|
|
900
|
+
return lines.map((line) => line.replace(/\r/g, "\\r"));
|
|
901
|
+
}
|
|
902
|
+
function hasFinalNewline(content) {
|
|
903
|
+
return content === "" || /[\r\n]$/.test(content);
|
|
904
|
+
}
|
|
905
|
+
function diffRange(start, count) {
|
|
906
|
+
return count === 0 ? `${start},0` : `${start + 1},${count}`;
|
|
907
|
+
}
|
|
908
|
+
function utf8Prefix(value, maxBytes) {
|
|
909
|
+
const bytes = Buffer.from(value, "utf8");
|
|
910
|
+
if (bytes.length <= maxBytes)
|
|
911
|
+
return value;
|
|
912
|
+
let end = maxBytes;
|
|
913
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
914
|
+
while (end > 0) {
|
|
915
|
+
try {
|
|
916
|
+
return decoder.decode(bytes.subarray(0, end));
|
|
917
|
+
} catch {
|
|
918
|
+
end--;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
return "";
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// src/client.ts
|
|
925
|
+
var resolveFromHost = createRequire2(import.meta.url).resolve;
|
|
811
926
|
function resolveImagePolicy(options) {
|
|
812
927
|
const value = options?.imageMode;
|
|
813
928
|
if (value === undefined)
|
|
@@ -816,46 +931,6 @@ function resolveImagePolicy(options) {
|
|
|
816
931
|
return value;
|
|
817
932
|
throw new Error('opencode-readseek imageMode must be "on", "auto", or "off"');
|
|
818
933
|
}
|
|
819
|
-
function isVisualFile(value) {
|
|
820
|
-
const output = record(value);
|
|
821
|
-
const type = output.type;
|
|
822
|
-
return typeof output.width === "number" || typeof type === "string" && type.startsWith("image/");
|
|
823
|
-
}
|
|
824
|
-
function isVisualBoundsError(error) {
|
|
825
|
-
if (!(error instanceof Error))
|
|
826
|
-
return false;
|
|
827
|
-
const message = error.message.replace(/^error:\s*/, "");
|
|
828
|
-
return message.startsWith("--end") && /(?:does|do) not apply to images/.test(message);
|
|
829
|
-
}
|
|
830
|
-
function isDocumentBoundsError(error) {
|
|
831
|
-
if (!(error instanceof Error))
|
|
832
|
-
return false;
|
|
833
|
-
const message = error.message.replace(/^error:\s*/, "");
|
|
834
|
-
return /(?:does|do) not apply to (?:documents|PDFs)/.test(message) || /document content belongs to view/.test(message) || /source facets do not apply to documents/.test(message);
|
|
835
|
-
}
|
|
836
|
-
function isPdfFile(value) {
|
|
837
|
-
const output = record(value);
|
|
838
|
-
return output.format === "pdf" || output.type === "application/pdf";
|
|
839
|
-
}
|
|
840
|
-
function documentReceipt(filePath, envelope) {
|
|
841
|
-
const recordEnvelope = record(envelope);
|
|
842
|
-
return {
|
|
843
|
-
file: filePath,
|
|
844
|
-
use: "readseek_view",
|
|
845
|
-
reason: "document content belongs to readseek_view",
|
|
846
|
-
metadata: digestMetadata(envelope),
|
|
847
|
-
...recordEnvelope.cache !== undefined ? { cache: recordEnvelope.cache } : {}
|
|
848
|
-
};
|
|
849
|
-
}
|
|
850
|
-
async function digestDocumentReceipt(context, filePath) {
|
|
851
|
-
const cacheDir = await ensureReadSeekCache(context);
|
|
852
|
-
const envelope = await runReadSeek(context, [
|
|
853
|
-
"--readseek-dir",
|
|
854
|
-
cacheDir,
|
|
855
|
-
...digestArgs(filePath, "metadata")
|
|
856
|
-
]);
|
|
857
|
-
return documentReceipt(filePath, envelope);
|
|
858
|
-
}
|
|
859
934
|
function digestArgs(filePath, select, extra = []) {
|
|
860
935
|
return [...buildDigestArgs({ path: filePath, select }), ...extra];
|
|
861
936
|
}
|
|
@@ -866,232 +941,30 @@ function requireDigestFacet(envelope, facet) {
|
|
|
866
941
|
}
|
|
867
942
|
return value;
|
|
868
943
|
}
|
|
869
|
-
function
|
|
870
|
-
|
|
944
|
+
function withAt(args, line, column) {
|
|
945
|
+
args.push("--at", buildLineLocation(line, column));
|
|
871
946
|
}
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
if (!paths) {
|
|
879
|
-
paths = new Set;
|
|
880
|
-
this.#pathsBySession.set(sessionID, paths);
|
|
881
|
-
}
|
|
882
|
-
paths.add(filePath);
|
|
883
|
-
}
|
|
884
|
-
forget(filePath) {
|
|
885
|
-
const absolutePath = path2.resolve(filePath);
|
|
886
|
-
for (const paths of this.#pathsBySession.values())
|
|
887
|
-
paths.delete(absolutePath);
|
|
888
|
-
for (const [sessionID, plan] of this.#renamePlans) {
|
|
889
|
-
if (plan.files.has(absolutePath))
|
|
890
|
-
this.#renamePlans.delete(sessionID);
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
deleteSession(sessionID) {
|
|
894
|
-
this.#pathsBySession.delete(sessionID);
|
|
895
|
-
this.#renamePlans.delete(sessionID);
|
|
896
|
-
}
|
|
897
|
-
planRename(sessionID, output) {
|
|
898
|
-
if (!output || typeof output !== "object")
|
|
899
|
-
return;
|
|
900
|
-
const record = output;
|
|
901
|
-
if (record.applied === true) {
|
|
902
|
-
this.#renamePlans.delete(sessionID);
|
|
903
|
-
return;
|
|
904
|
-
}
|
|
905
|
-
const { file, old_name: oldName, new_name: newName, others } = record;
|
|
906
|
-
if (typeof file !== "string" || typeof oldName !== "string" || typeof newName !== "string")
|
|
907
|
-
return;
|
|
908
|
-
const files = new Set([path2.resolve(file)]);
|
|
909
|
-
if (Array.isArray(others)) {
|
|
910
|
-
for (const item of others) {
|
|
911
|
-
if (!item || typeof item !== "object")
|
|
912
|
-
continue;
|
|
913
|
-
const otherFile = item.file;
|
|
914
|
-
if (typeof otherFile === "string")
|
|
915
|
-
files.add(path2.resolve(otherFile));
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
this.#renamePlans.set(sessionID, { summary: `${oldName} -> ${newName}`, files });
|
|
919
|
-
}
|
|
920
|
-
render(sessionID) {
|
|
921
|
-
const paths = this.#pathsBySession.get(sessionID);
|
|
922
|
-
const sections = [];
|
|
923
|
-
if (paths?.size) {
|
|
924
|
-
sections.push(`## ReadSeek Anchors
|
|
925
|
-
The following files have fresh LINE:HASH anchors:
|
|
926
|
-
${[...paths].sort().map((filePath) => `- ${filePath}`).join(`
|
|
927
|
-
`)}`);
|
|
928
|
-
}
|
|
929
|
-
const renamePlan = this.#renamePlans.get(sessionID);
|
|
930
|
-
if (renamePlan)
|
|
931
|
-
sections.push(`## Pending ReadSeek Rename Plan
|
|
932
|
-
- ${renamePlan.summary}`);
|
|
933
|
-
return sections.length === 0 ? undefined : sections.join(`
|
|
934
|
-
|
|
935
|
-
`);
|
|
936
|
-
}
|
|
947
|
+
function readRangeArgs(filePath, start, end) {
|
|
948
|
+
const extra = [];
|
|
949
|
+
if (start > 1)
|
|
950
|
+
withAt(extra, start);
|
|
951
|
+
extra.push("--end", String(end));
|
|
952
|
+
return digestArgs(filePath, "content", extra);
|
|
937
953
|
}
|
|
938
|
-
function
|
|
939
|
-
return
|
|
954
|
+
function readSeekBinaryPath() {
|
|
955
|
+
return resolveReadSeekBinaryFromPackages(resolveFromHost);
|
|
940
956
|
}
|
|
941
|
-
function
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
let restart = false;
|
|
953
|
-
for (let index = 0;index < parts.length; index++) {
|
|
954
|
-
const candidate = path2.join(resolved, parts[index]);
|
|
955
|
-
const info = await lstat(candidate);
|
|
956
|
-
if (!info.isSymbolicLink()) {
|
|
957
|
-
resolved = candidate;
|
|
958
|
-
continue;
|
|
959
|
-
}
|
|
960
|
-
symlinks++;
|
|
961
|
-
if (symlinks > 40) {
|
|
962
|
-
const error = new Error(`too many symbolic links: ${filePath}`);
|
|
963
|
-
error.code = "ELOOP";
|
|
964
|
-
throw error;
|
|
965
|
-
}
|
|
966
|
-
const target = await readlink(candidate);
|
|
967
|
-
unresolved = path2.resolve(path2.dirname(candidate), target, ...parts.slice(index + 1));
|
|
968
|
-
restart = true;
|
|
969
|
-
break;
|
|
970
|
-
}
|
|
971
|
-
if (!restart)
|
|
972
|
-
return resolved;
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
async function portableRealpath(filePath) {
|
|
976
|
-
try {
|
|
977
|
-
return await realpath(filePath);
|
|
978
|
-
} catch (error) {
|
|
979
|
-
if (error.code !== "EACCES")
|
|
980
|
-
throw error;
|
|
981
|
-
return resolveSymlinks(filePath);
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
async function canonicalPath(filePath) {
|
|
985
|
-
const missing = [];
|
|
986
|
-
let existing = filePath;
|
|
987
|
-
while (true) {
|
|
988
|
-
try {
|
|
989
|
-
return path2.join(await portableRealpath(existing), ...missing.reverse());
|
|
990
|
-
} catch (error) {
|
|
991
|
-
if (error.code !== "ENOENT")
|
|
992
|
-
throw error;
|
|
993
|
-
const parent = path2.dirname(existing);
|
|
994
|
-
if (parent === existing)
|
|
995
|
-
throw error;
|
|
996
|
-
missing.push(path2.basename(existing));
|
|
997
|
-
existing = parent;
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
async function authorizeExternal(context, filePath) {
|
|
1002
|
-
const [accessPath, directory, worktree] = await Promise.all([
|
|
1003
|
-
canonicalPath(filePath),
|
|
1004
|
-
canonicalPath(context.directory),
|
|
1005
|
-
context.worktree === "/" ? Promise.resolve("/") : canonicalPath(context.worktree)
|
|
1006
|
-
]);
|
|
1007
|
-
if (containsPath(directory, accessPath) || worktree !== "/" && containsPath(worktree, accessPath)) {
|
|
1008
|
-
return;
|
|
1009
|
-
}
|
|
1010
|
-
const info = await stat(accessPath).catch(() => {
|
|
1011
|
-
return;
|
|
1012
|
-
});
|
|
1013
|
-
const parentDir = info?.isDirectory() ? accessPath : path2.dirname(accessPath);
|
|
1014
|
-
const pattern = path2.join(parentDir, "*").replaceAll("\\", "/");
|
|
1015
|
-
await context.ask({
|
|
1016
|
-
permission: "external_directory",
|
|
1017
|
-
patterns: [pattern],
|
|
1018
|
-
always: [pattern],
|
|
1019
|
-
metadata: { filepath: filePath, parentDir }
|
|
1020
|
-
});
|
|
1021
|
-
}
|
|
1022
|
-
async function rejectSymlinkMutation(filePath) {
|
|
1023
|
-
const info = await lstat(filePath).catch((error) => {
|
|
1024
|
-
if (error.code === "ENOENT")
|
|
1025
|
-
return;
|
|
1026
|
-
throw error;
|
|
1027
|
-
});
|
|
1028
|
-
if (info?.isSymbolicLink())
|
|
1029
|
-
throw new Error(`refusing to modify symbolic link: ${filePath}`);
|
|
1030
|
-
}
|
|
1031
|
-
async function authorizeRead(context, filePath) {
|
|
1032
|
-
await authorizeExternal(context, filePath);
|
|
1033
|
-
await context.ask({
|
|
1034
|
-
permission: "read",
|
|
1035
|
-
patterns: [path2.relative(context.worktree, filePath).replaceAll("\\", "/")],
|
|
1036
|
-
always: ["*"],
|
|
1037
|
-
metadata: {}
|
|
1038
|
-
});
|
|
1039
|
-
}
|
|
1040
|
-
async function authorizeSearch(context, filePath, pattern) {
|
|
1041
|
-
await context.ask({
|
|
1042
|
-
permission: "grep",
|
|
1043
|
-
patterns: [pattern],
|
|
1044
|
-
always: ["*"],
|
|
1045
|
-
metadata: { pattern, path: filePath }
|
|
1046
|
-
});
|
|
1047
|
-
await authorizeExternal(context, filePath);
|
|
1048
|
-
}
|
|
1049
|
-
async function authorizeEdit(context, filePaths, diff = "", authorizeExternalPaths = true) {
|
|
1050
|
-
if (authorizeExternalPaths) {
|
|
1051
|
-
for (const filePath of filePaths)
|
|
1052
|
-
await authorizeExternal(context, filePath);
|
|
1053
|
-
}
|
|
1054
|
-
const patterns = filePaths.map((filePath) => path2.relative(context.worktree, filePath).replaceAll("\\", "/"));
|
|
1055
|
-
await context.ask({
|
|
1056
|
-
permission: "edit",
|
|
1057
|
-
patterns,
|
|
1058
|
-
always: ["*"],
|
|
1059
|
-
metadata: { filepath: filePaths.join(", "), diff }
|
|
1060
|
-
});
|
|
1061
|
-
}
|
|
1062
|
-
function withAt(args, line, column) {
|
|
1063
|
-
args.push("--at", buildLineLocation(line, column));
|
|
1064
|
-
}
|
|
1065
|
-
function isLikelyImagePath(filePath) {
|
|
1066
|
-
return /\.(avif|bmp|gif|heic|heif|ico|jpe?g|png|svg|tiff?|webp)$/i.test(filePath);
|
|
1067
|
-
}
|
|
1068
|
-
function isLikelyDocumentPath(filePath) {
|
|
1069
|
-
return /\.pdf$/i.test(filePath);
|
|
1070
|
-
}
|
|
1071
|
-
function readRangeArgs(filePath, start, end) {
|
|
1072
|
-
const extra = [];
|
|
1073
|
-
if (start > 1)
|
|
1074
|
-
withAt(extra, start);
|
|
1075
|
-
extra.push("--end", String(end));
|
|
1076
|
-
return digestArgs(filePath, "content", extra);
|
|
1077
|
-
}
|
|
1078
|
-
async function runDigestContent(context, args, options = {}) {
|
|
1079
|
-
return requireDigestFacet(await runReadSeek(context, args, options), "content");
|
|
1080
|
-
}
|
|
1081
|
-
function readSeekBinaryPath() {
|
|
1082
|
-
return resolveReadSeekBinaryFromPackages(resolveFromHost);
|
|
1083
|
-
}
|
|
1084
|
-
async function preserveAbortReason(signal, cancelable, invoke) {
|
|
1085
|
-
try {
|
|
1086
|
-
const value = await invoke();
|
|
1087
|
-
if (cancelable)
|
|
1088
|
-
signal.throwIfAborted();
|
|
1089
|
-
return value;
|
|
1090
|
-
} catch (error) {
|
|
1091
|
-
if (cancelable)
|
|
1092
|
-
signal.throwIfAborted();
|
|
1093
|
-
throw error;
|
|
1094
|
-
}
|
|
957
|
+
async function preserveAbortReason(signal, cancelable, invoke) {
|
|
958
|
+
try {
|
|
959
|
+
const value = await invoke();
|
|
960
|
+
if (cancelable)
|
|
961
|
+
signal.throwIfAborted();
|
|
962
|
+
return value;
|
|
963
|
+
} catch (error) {
|
|
964
|
+
if (cancelable)
|
|
965
|
+
signal.throwIfAborted();
|
|
966
|
+
throw error;
|
|
967
|
+
}
|
|
1095
968
|
}
|
|
1096
969
|
async function runReadSeekRaw(context, args, options = {}) {
|
|
1097
970
|
context.abort.throwIfAborted();
|
|
@@ -1113,6 +986,9 @@ async function runReadSeek(context, args, options = {}) {
|
|
|
1113
986
|
timeoutMs: options.timeoutMs
|
|
1114
987
|
}));
|
|
1115
988
|
}
|
|
989
|
+
async function runDigestContent(context, args, options = {}) {
|
|
990
|
+
return requireDigestFacet(await runReadSeek(context, args, options), "content");
|
|
991
|
+
}
|
|
1116
992
|
async function postMutationPreview(context, args, filePath, changed) {
|
|
1117
993
|
try {
|
|
1118
994
|
return await runDigestContent(context, args, {
|
|
@@ -1132,8 +1008,8 @@ async function postMutationPreview(context, args, filePath, changed) {
|
|
|
1132
1008
|
var readSeekCacheInit;
|
|
1133
1009
|
function readSeekCacheDir() {
|
|
1134
1010
|
const xdgCacheHome = process.env.XDG_CACHE_HOME?.trim();
|
|
1135
|
-
const cacheHome = xdgCacheHome ||
|
|
1136
|
-
return
|
|
1011
|
+
const cacheHome = xdgCacheHome || path3.join(homedir(), ".cache");
|
|
1012
|
+
return path3.join(cacheHome, "opencode", "readseek");
|
|
1137
1013
|
}
|
|
1138
1014
|
async function ensureReadSeekCache(context) {
|
|
1139
1015
|
const cacheDir = readSeekCacheDir();
|
|
@@ -1151,28 +1027,109 @@ async function ensureReadSeekCache(context) {
|
|
|
1151
1027
|
});
|
|
1152
1028
|
return readSeekCacheInit;
|
|
1153
1029
|
}
|
|
1154
|
-
function
|
|
1155
|
-
|
|
1030
|
+
function boundDocumentOutput(value) {
|
|
1031
|
+
const lines = value.split(`
|
|
1032
|
+
`);
|
|
1033
|
+
let output = lines.length > MAX_DOCUMENT_OUTPUT_LINES ? lines.slice(0, MAX_DOCUMENT_OUTPUT_LINES).join(`
|
|
1034
|
+
`) : value;
|
|
1035
|
+
const truncated = output !== value || Buffer.byteLength(output, "utf8") > MAX_DOCUMENT_OUTPUT_BYTES;
|
|
1036
|
+
output = utf8Prefix(output, MAX_DOCUMENT_OUTPUT_BYTES);
|
|
1037
|
+
return truncated ? `${output}
|
|
1038
|
+
[… document view truncated; narrow it with page, node, kind, or depth]` : output;
|
|
1156
1039
|
}
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1040
|
+
|
|
1041
|
+
// src/policy.ts
|
|
1042
|
+
var OPENCODE_TOOL_ALIASES = Object.fromEntries(TOOL_NAMES.map((name) => [name, `readseek_${name}`]));
|
|
1043
|
+
var TOOL_POLICY = renderToolRoutingPolicy({
|
|
1044
|
+
aliases: OPENCODE_TOOL_ALIASES,
|
|
1045
|
+
preferOverBuiltIns: true,
|
|
1046
|
+
demoteBuiltIns: ["read", "edit", "write", "grep", "apply_patch"]
|
|
1047
|
+
});
|
|
1048
|
+
var PREFERRED_TOOL_DESCRIPTIONS = preferredToolDescriptionMap(OPENCODE_TOOL_ALIASES);
|
|
1049
|
+
var FALLBACK_TOOL_DESCRIPTIONS = fallbackToolDescriptionMap(["edit", "write", "grep", "apply_patch"], OPENCODE_TOOL_ALIASES);
|
|
1050
|
+
function openCodeToolDescription(name) {
|
|
1051
|
+
return renderToolDescription(name, OPENCODE_TOOL_ALIASES);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// src/session-anchors.ts
|
|
1055
|
+
import path4 from "node:path";
|
|
1056
|
+
|
|
1057
|
+
class SessionAnchors {
|
|
1058
|
+
#pathsBySession = new Map;
|
|
1059
|
+
#renamePlans = new Map;
|
|
1060
|
+
mark(sessionID, filePath) {
|
|
1061
|
+
let paths = this.#pathsBySession.get(sessionID);
|
|
1062
|
+
if (!paths) {
|
|
1063
|
+
paths = new Set;
|
|
1064
|
+
this.#pathsBySession.set(sessionID, paths);
|
|
1065
|
+
}
|
|
1066
|
+
paths.add(filePath);
|
|
1067
|
+
}
|
|
1068
|
+
forget(filePath) {
|
|
1069
|
+
const absolutePath = path4.resolve(filePath);
|
|
1070
|
+
for (const paths of this.#pathsBySession.values())
|
|
1071
|
+
paths.delete(absolutePath);
|
|
1072
|
+
for (const [sessionID, plan] of this.#renamePlans) {
|
|
1073
|
+
if (plan.files.has(absolutePath))
|
|
1074
|
+
this.#renamePlans.delete(sessionID);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
deleteSession(sessionID) {
|
|
1078
|
+
this.#pathsBySession.delete(sessionID);
|
|
1079
|
+
this.#renamePlans.delete(sessionID);
|
|
1080
|
+
}
|
|
1081
|
+
planRename(sessionID, output) {
|
|
1082
|
+
if (!output || typeof output !== "object")
|
|
1083
|
+
return;
|
|
1084
|
+
const record2 = output;
|
|
1085
|
+
if (record2.applied === true) {
|
|
1086
|
+
this.#renamePlans.delete(sessionID);
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
const { file, old_name: oldName, new_name: newName, others } = record2;
|
|
1090
|
+
if (typeof file !== "string" || typeof oldName !== "string" || typeof newName !== "string")
|
|
1091
|
+
return;
|
|
1092
|
+
const files = new Set([path4.resolve(file)]);
|
|
1093
|
+
if (Array.isArray(others)) {
|
|
1094
|
+
for (const item of others) {
|
|
1095
|
+
if (!item || typeof item !== "object")
|
|
1096
|
+
continue;
|
|
1097
|
+
const otherFile = item.file;
|
|
1098
|
+
if (typeof otherFile === "string")
|
|
1099
|
+
files.add(path4.resolve(otherFile));
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
this.#renamePlans.set(sessionID, { summary: `${oldName} -> ${newName}`, files });
|
|
1103
|
+
}
|
|
1104
|
+
render(sessionID) {
|
|
1105
|
+
const paths = this.#pathsBySession.get(sessionID);
|
|
1106
|
+
const sections = [];
|
|
1107
|
+
if (paths?.size) {
|
|
1108
|
+
sections.push(`## ReadSeek Anchors
|
|
1109
|
+
The following files have fresh LINE:HASH anchors:
|
|
1110
|
+
${[...paths].sort().map((filePath) => `- ${filePath}`).join(`
|
|
1111
|
+
`)}`);
|
|
1112
|
+
}
|
|
1113
|
+
const renamePlan = this.#renamePlans.get(sessionID);
|
|
1114
|
+
if (renamePlan)
|
|
1115
|
+
sections.push(`## Pending ReadSeek Rename Plan
|
|
1116
|
+
- ${renamePlan.summary}`);
|
|
1117
|
+
return sections.length === 0 ? undefined : sections.join(`
|
|
1118
|
+
|
|
1119
|
+
`);
|
|
1162
1120
|
}
|
|
1163
|
-
if (!value || typeof value !== "object")
|
|
1164
|
-
return;
|
|
1165
|
-
const output = value;
|
|
1166
|
-
const hashlines = output.hashlines;
|
|
1167
|
-
if (typeof output.file === "string" && Array.isArray(hashlines) && hashlines.some((line) => typeof record(line).line === "number" && typeof record(line).hash === "string"))
|
|
1168
|
-
files.add(output.file);
|
|
1169
|
-
for (const item of Object.values(output))
|
|
1170
|
-
collectAnchoredFiles(item, files);
|
|
1171
1121
|
}
|
|
1122
|
+
|
|
1123
|
+
// src/tools.ts
|
|
1124
|
+
import { readFile as readFile2, stat as stat3 } from "node:fs/promises";
|
|
1125
|
+
import { tool as tool2 } from "@opencode-ai/plugin";
|
|
1126
|
+
|
|
1127
|
+
// src/edits.ts
|
|
1128
|
+
import { lstat, mkdir, open, readFile } from "node:fs/promises";
|
|
1129
|
+
import path5 from "node:path";
|
|
1172
1130
|
var fileMutationTails = new Map;
|
|
1173
|
-
var MAX_PERMISSION_DIFF_BYTES = 32 * 1024;
|
|
1174
1131
|
async function withFileMutationQueue(filePath, operation) {
|
|
1175
|
-
const key =
|
|
1132
|
+
const key = path5.resolve(filePath);
|
|
1176
1133
|
const previous = fileMutationTails.get(key);
|
|
1177
1134
|
let release = () => {};
|
|
1178
1135
|
const current = new Promise((resolve) => {
|
|
@@ -1189,6 +1146,15 @@ async function withFileMutationQueue(filePath, operation) {
|
|
|
1189
1146
|
fileMutationTails.delete(key);
|
|
1190
1147
|
}
|
|
1191
1148
|
}
|
|
1149
|
+
function parseAnchor(anchor) {
|
|
1150
|
+
const match = /^(\d+):([0-9a-fA-F]{3})$/.exec(anchor.trim());
|
|
1151
|
+
if (!match)
|
|
1152
|
+
throw new Error(`invalid LINE:HASH anchor: ${anchor}`);
|
|
1153
|
+
const line = Number(match[1]);
|
|
1154
|
+
if (line === 0)
|
|
1155
|
+
throw new Error(`anchor line must be greater than zero: ${anchor}`);
|
|
1156
|
+
return { line, hash: match[2].toLowerCase() };
|
|
1157
|
+
}
|
|
1192
1158
|
function validateEdits(value) {
|
|
1193
1159
|
if (!Array.isArray(value) || value.length === 0)
|
|
1194
1160
|
throw new Error("edits must contain at least one edit");
|
|
@@ -1230,15 +1196,6 @@ function validateEdits(value) {
|
|
|
1230
1196
|
}
|
|
1231
1197
|
}
|
|
1232
1198
|
}
|
|
1233
|
-
function parseAnchor(anchor) {
|
|
1234
|
-
const match = /^(\d+):([0-9a-fA-F]{3})$/.exec(anchor.trim());
|
|
1235
|
-
if (!match)
|
|
1236
|
-
throw new Error(`invalid LINE:HASH anchor: ${anchor}`);
|
|
1237
|
-
const line = Number(match[1]);
|
|
1238
|
-
if (line === 0)
|
|
1239
|
-
throw new Error(`anchor line must be greater than zero: ${anchor}`);
|
|
1240
|
-
return { line, hash: match[2].toLowerCase() };
|
|
1241
|
-
}
|
|
1242
1199
|
function firstChangedLine(before, after) {
|
|
1243
1200
|
const beforeLines = normalizeToLf(before).split(`
|
|
1244
1201
|
`);
|
|
@@ -1272,7 +1229,7 @@ async function writeHandle(handle, content) {
|
|
|
1272
1229
|
}
|
|
1273
1230
|
}
|
|
1274
1231
|
async function writeText(filePath, expected, content) {
|
|
1275
|
-
await mkdir(
|
|
1232
|
+
await mkdir(path5.dirname(filePath), { recursive: true });
|
|
1276
1233
|
if (!expected.exists) {
|
|
1277
1234
|
let handle2;
|
|
1278
1235
|
try {
|
|
@@ -1309,100 +1266,6 @@ async function writeText(filePath, expected, content) {
|
|
|
1309
1266
|
await handle.close();
|
|
1310
1267
|
}
|
|
1311
1268
|
}
|
|
1312
|
-
function renameFiles(value) {
|
|
1313
|
-
const output = record(value);
|
|
1314
|
-
const files = [];
|
|
1315
|
-
if (typeof output.file === "string" && items(output.edits).length > 0)
|
|
1316
|
-
files.push(path2.resolve(output.file));
|
|
1317
|
-
for (const other of items(output.others)) {
|
|
1318
|
-
const item = record(other);
|
|
1319
|
-
if (typeof item.file === "string" && items(item.edits).length > 0)
|
|
1320
|
-
files.push(path2.resolve(item.file));
|
|
1321
|
-
}
|
|
1322
|
-
return files;
|
|
1323
|
-
}
|
|
1324
|
-
function normalizeToLf(content) {
|
|
1325
|
-
return content.replace(/\r\n/g, `
|
|
1326
|
-
`).replace(/\r/g, `
|
|
1327
|
-
`);
|
|
1328
|
-
}
|
|
1329
|
-
function diffLines(content) {
|
|
1330
|
-
if (content === "")
|
|
1331
|
-
return [];
|
|
1332
|
-
const lines = content.split(`
|
|
1333
|
-
`);
|
|
1334
|
-
if (lines.at(-1) === "")
|
|
1335
|
-
lines.pop();
|
|
1336
|
-
return lines.map((line) => line.replace(/\r/g, "\\r"));
|
|
1337
|
-
}
|
|
1338
|
-
function hasFinalNewline(content) {
|
|
1339
|
-
return content === "" || /[\r\n]$/.test(content);
|
|
1340
|
-
}
|
|
1341
|
-
function diffRange(start, count) {
|
|
1342
|
-
return count === 0 ? `${start},0` : `${start + 1},${count}`;
|
|
1343
|
-
}
|
|
1344
|
-
function utf8Prefix(value, maxBytes) {
|
|
1345
|
-
const bytes = Buffer.from(value, "utf8");
|
|
1346
|
-
if (bytes.length <= maxBytes)
|
|
1347
|
-
return value;
|
|
1348
|
-
let end = maxBytes;
|
|
1349
|
-
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
1350
|
-
while (end > 0) {
|
|
1351
|
-
try {
|
|
1352
|
-
return decoder.decode(bytes.subarray(0, end));
|
|
1353
|
-
} catch {
|
|
1354
|
-
end--;
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1357
|
-
return "";
|
|
1358
|
-
}
|
|
1359
|
-
function boundDocumentOutput(value) {
|
|
1360
|
-
const lines = value.split(`
|
|
1361
|
-
`);
|
|
1362
|
-
let output = lines.length > MAX_DOCUMENT_OUTPUT_LINES ? lines.slice(0, MAX_DOCUMENT_OUTPUT_LINES).join(`
|
|
1363
|
-
`) : value;
|
|
1364
|
-
const truncated = output !== value || Buffer.byteLength(output, "utf8") > MAX_DOCUMENT_OUTPUT_BYTES;
|
|
1365
|
-
output = utf8Prefix(output, MAX_DOCUMENT_OUTPUT_BYTES);
|
|
1366
|
-
return truncated ? `${output}
|
|
1367
|
-
[… document view truncated; narrow it with page, node, kind, or depth]` : output;
|
|
1368
|
-
}
|
|
1369
|
-
function attachmentExtension(mime) {
|
|
1370
|
-
switch (mime) {
|
|
1371
|
-
case "image/gif":
|
|
1372
|
-
return "gif";
|
|
1373
|
-
case "image/jpeg":
|
|
1374
|
-
return "jpg";
|
|
1375
|
-
case "image/png":
|
|
1376
|
-
return "png";
|
|
1377
|
-
case "image/webp":
|
|
1378
|
-
return "webp";
|
|
1379
|
-
default:
|
|
1380
|
-
return "bin";
|
|
1381
|
-
}
|
|
1382
|
-
}
|
|
1383
|
-
function imageAttachment(image, filename) {
|
|
1384
|
-
if (image.encoding !== "base64" || typeof image.data !== "string" || typeof image.mime !== "string") {
|
|
1385
|
-
return;
|
|
1386
|
-
}
|
|
1387
|
-
return {
|
|
1388
|
-
type: "file",
|
|
1389
|
-
mime: image.mime,
|
|
1390
|
-
url: `data:${image.mime};base64,${image.data}`,
|
|
1391
|
-
filename: `${filename}.${attachmentExtension(image.mime)}`
|
|
1392
|
-
};
|
|
1393
|
-
}
|
|
1394
|
-
function formatReadResult(value) {
|
|
1395
|
-
const output = record(value);
|
|
1396
|
-
if (output.use === "readseek_view")
|
|
1397
|
-
return { output: render(value) };
|
|
1398
|
-
const attachment = imageAttachment(output, "image");
|
|
1399
|
-
if (!attachment)
|
|
1400
|
-
return { output: render(value) };
|
|
1401
|
-
const sanitized = { ...output };
|
|
1402
|
-
delete sanitized.data;
|
|
1403
|
-
delete sanitized.encoding;
|
|
1404
|
-
return { output: render(sanitized), attachments: [attachment] };
|
|
1405
|
-
}
|
|
1406
1269
|
function previewDiffLine(prefix, line) {
|
|
1407
1270
|
const value = `${prefix}${line}`;
|
|
1408
1271
|
const maxBytes = 256;
|
|
@@ -1495,21 +1358,137 @@ function simpleDiff(filePath, before, after) {
|
|
|
1495
1358
|
].join(`
|
|
1496
1359
|
`);
|
|
1497
1360
|
}
|
|
1498
|
-
|
|
1499
|
-
|
|
1361
|
+
|
|
1362
|
+
// src/paths.ts
|
|
1363
|
+
import { lstat as lstat2, readlink, realpath, stat as stat2 } from "node:fs/promises";
|
|
1364
|
+
import path6 from "node:path";
|
|
1365
|
+
function resolvePath(directory, filePath) {
|
|
1366
|
+
return path6.resolve(directory, filePath);
|
|
1367
|
+
}
|
|
1368
|
+
function containsPath(directory, filePath) {
|
|
1369
|
+
const relative = path6.relative(directory, filePath);
|
|
1370
|
+
return relative === "" || !relative.startsWith(`..${path6.sep}`) && relative !== ".." && !path6.isAbsolute(relative);
|
|
1371
|
+
}
|
|
1372
|
+
async function resolveSymlinks(filePath) {
|
|
1373
|
+
let unresolved = path6.resolve(filePath);
|
|
1374
|
+
let symlinks = 0;
|
|
1375
|
+
while (true) {
|
|
1376
|
+
const parsed = path6.parse(unresolved);
|
|
1377
|
+
const parts = unresolved.slice(parsed.root.length).split(path6.sep).filter(Boolean);
|
|
1378
|
+
let resolved = parsed.root;
|
|
1379
|
+
let restart = false;
|
|
1380
|
+
for (let index = 0;index < parts.length; index++) {
|
|
1381
|
+
const candidate = path6.join(resolved, parts[index]);
|
|
1382
|
+
const info = await lstat2(candidate);
|
|
1383
|
+
if (!info.isSymbolicLink()) {
|
|
1384
|
+
resolved = candidate;
|
|
1385
|
+
continue;
|
|
1386
|
+
}
|
|
1387
|
+
symlinks++;
|
|
1388
|
+
if (symlinks > 40) {
|
|
1389
|
+
const error = new Error(`too many symbolic links: ${filePath}`);
|
|
1390
|
+
error.code = "ELOOP";
|
|
1391
|
+
throw error;
|
|
1392
|
+
}
|
|
1393
|
+
const target = await readlink(candidate);
|
|
1394
|
+
unresolved = path6.resolve(path6.dirname(candidate), target, ...parts.slice(index + 1));
|
|
1395
|
+
restart = true;
|
|
1396
|
+
break;
|
|
1397
|
+
}
|
|
1398
|
+
if (!restart)
|
|
1399
|
+
return resolved;
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
async function portableRealpath(filePath) {
|
|
1403
|
+
try {
|
|
1404
|
+
return await realpath(filePath);
|
|
1405
|
+
} catch (error) {
|
|
1406
|
+
if (error.code !== "EACCES")
|
|
1407
|
+
throw error;
|
|
1408
|
+
return resolveSymlinks(filePath);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
async function canonicalPath(filePath) {
|
|
1412
|
+
const missing = [];
|
|
1413
|
+
let existing = filePath;
|
|
1414
|
+
while (true) {
|
|
1415
|
+
try {
|
|
1416
|
+
return path6.join(await portableRealpath(existing), ...missing.reverse());
|
|
1417
|
+
} catch (error) {
|
|
1418
|
+
if (error.code !== "ENOENT")
|
|
1419
|
+
throw error;
|
|
1420
|
+
const parent = path6.dirname(existing);
|
|
1421
|
+
if (parent === existing)
|
|
1422
|
+
throw error;
|
|
1423
|
+
missing.push(path6.basename(existing));
|
|
1424
|
+
existing = parent;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
async function authorizeExternal(context, filePath) {
|
|
1429
|
+
const [accessPath, directory, worktree] = await Promise.all([
|
|
1430
|
+
canonicalPath(filePath),
|
|
1431
|
+
canonicalPath(context.directory),
|
|
1432
|
+
context.worktree === "/" ? Promise.resolve("/") : canonicalPath(context.worktree)
|
|
1433
|
+
]);
|
|
1434
|
+
if (containsPath(directory, accessPath) || worktree !== "/" && containsPath(worktree, accessPath)) {
|
|
1500
1435
|
return;
|
|
1501
|
-
|
|
1502
|
-
|
|
1436
|
+
}
|
|
1437
|
+
const info = await stat2(accessPath).catch(() => {
|
|
1503
1438
|
return;
|
|
1504
|
-
|
|
1505
|
-
|
|
1439
|
+
});
|
|
1440
|
+
const parentDir = info?.isDirectory() ? accessPath : path6.dirname(accessPath);
|
|
1441
|
+
const pattern = path6.join(parentDir, "*").replaceAll("\\", "/");
|
|
1442
|
+
await context.ask({
|
|
1443
|
+
permission: "external_directory",
|
|
1444
|
+
patterns: [pattern],
|
|
1445
|
+
always: [pattern],
|
|
1446
|
+
metadata: { filepath: filePath, parentDir }
|
|
1447
|
+
});
|
|
1506
1448
|
}
|
|
1507
|
-
function
|
|
1508
|
-
|
|
1449
|
+
async function rejectSymlinkMutation(filePath) {
|
|
1450
|
+
const info = await lstat2(filePath).catch((error) => {
|
|
1451
|
+
if (error.code === "ENOENT")
|
|
1452
|
+
return;
|
|
1453
|
+
throw error;
|
|
1454
|
+
});
|
|
1455
|
+
if (info?.isSymbolicLink())
|
|
1456
|
+
throw new Error(`refusing to modify symbolic link: ${filePath}`);
|
|
1509
1457
|
}
|
|
1510
|
-
function
|
|
1511
|
-
|
|
1458
|
+
async function authorizeRead(context, filePath) {
|
|
1459
|
+
await authorizeExternal(context, filePath);
|
|
1460
|
+
await context.ask({
|
|
1461
|
+
permission: "read",
|
|
1462
|
+
patterns: [path6.relative(context.worktree, filePath).replaceAll("\\", "/")],
|
|
1463
|
+
always: ["*"],
|
|
1464
|
+
metadata: {}
|
|
1465
|
+
});
|
|
1512
1466
|
}
|
|
1467
|
+
async function authorizeSearch(context, filePath, pattern) {
|
|
1468
|
+
await context.ask({
|
|
1469
|
+
permission: "grep",
|
|
1470
|
+
patterns: [pattern],
|
|
1471
|
+
always: ["*"],
|
|
1472
|
+
metadata: { pattern, path: filePath }
|
|
1473
|
+
});
|
|
1474
|
+
await authorizeExternal(context, filePath);
|
|
1475
|
+
}
|
|
1476
|
+
async function authorizeEdit(context, filePaths, diff = "", authorizeExternalPaths = true) {
|
|
1477
|
+
if (authorizeExternalPaths) {
|
|
1478
|
+
for (const filePath of filePaths)
|
|
1479
|
+
await authorizeExternal(context, filePath);
|
|
1480
|
+
}
|
|
1481
|
+
const patterns = filePaths.map((filePath) => path6.relative(context.worktree, filePath).replaceAll("\\", "/"));
|
|
1482
|
+
await context.ask({
|
|
1483
|
+
permission: "edit",
|
|
1484
|
+
patterns,
|
|
1485
|
+
always: ["*"],
|
|
1486
|
+
metadata: { filepath: filePaths.join(", "), diff }
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
// src/presentation.ts
|
|
1491
|
+
import { tool } from "@opencode-ai/plugin";
|
|
1513
1492
|
function initialTitle(kind, args) {
|
|
1514
1493
|
switch (kind) {
|
|
1515
1494
|
case "read":
|
|
@@ -1645,478 +1624,390 @@ function readseekTool(description, args, kind, execute, formatResult = (result)
|
|
|
1645
1624
|
}
|
|
1646
1625
|
});
|
|
1647
1626
|
}
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
const imageModes = imagePolicy === "auto" ? ["none", "all", "ocr", "caption", "objects"] : ["all", "ocr", "caption", "objects"];
|
|
1627
|
+
|
|
1628
|
+
// src/tools.ts
|
|
1629
|
+
function createReadSeekTools(imagePolicy) {
|
|
1652
1630
|
return {
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1631
|
+
readseek_view: readseekTool(openCodeToolDescription("view"), {
|
|
1632
|
+
path: tool2.schema.string().describe(PARAM_DESCRIPTIONS.projectPath),
|
|
1633
|
+
node: tool2.schema.string().min(1).optional().describe(PARAM_DESCRIPTIONS.node),
|
|
1634
|
+
page: tool2.schema.number().int().positive().optional().describe(PARAM_DESCRIPTIONS.page),
|
|
1635
|
+
kind: tool2.schema.enum(DOCUMENT_NODE_KIND_VALUES).optional().describe(PARAM_DESCRIPTIONS.kind),
|
|
1636
|
+
depth: tool2.schema.number().int().nonnegative().optional().describe(PARAM_DESCRIPTIONS.depth),
|
|
1637
|
+
outline: tool2.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.outline),
|
|
1638
|
+
visionMode: tool2.schema.enum(VISION_ANALYSIS_MODE_VALUES).optional().describe(PARAM_DESCRIPTIONS.visionModeView),
|
|
1639
|
+
visionLevel: tool2.schema.enum(VISION_LEVEL_VALUES).optional().describe(PARAM_DESCRIPTIONS.visionLevel)
|
|
1640
|
+
}, "view", async (input, context) => {
|
|
1641
|
+
const filePath = resolvePath(context.directory, input.path);
|
|
1642
|
+
await authorizeRead(context, filePath);
|
|
1643
|
+
const visionMode = input.visionMode;
|
|
1644
|
+
const visionLevel = input.visionLevel;
|
|
1645
|
+
const vision = validateVisionLevelVsMode({
|
|
1646
|
+
visionMode,
|
|
1647
|
+
visionLevel,
|
|
1648
|
+
outline: input.outline
|
|
1649
|
+
});
|
|
1650
|
+
if (!vision.ok)
|
|
1651
|
+
throw new Error(vision.error);
|
|
1652
|
+
const cacheDir = await ensureReadSeekCache(context);
|
|
1653
|
+
const args = buildViewArgs({
|
|
1654
|
+
path: filePath,
|
|
1655
|
+
node: input.node,
|
|
1656
|
+
page: input.page,
|
|
1657
|
+
kind: input.kind,
|
|
1658
|
+
depth: input.depth,
|
|
1659
|
+
outline: input.outline,
|
|
1660
|
+
visionMode,
|
|
1661
|
+
visionLevel,
|
|
1662
|
+
readseekDir: cacheDir
|
|
1663
|
+
});
|
|
1664
|
+
return runReadSeekRaw(context, args, visionMode === undefined ? {} : { timeoutMs: DEFAULT_VISION_TIMEOUT_MS });
|
|
1665
|
+
}, (result) => ({ output: boundDocumentOutput(String(result)) })),
|
|
1666
|
+
readseek_digest: readseekTool(openCodeToolDescription("digest"), {
|
|
1667
|
+
path: tool2.schema.string().describe(PARAM_DESCRIPTIONS.projectPath),
|
|
1668
|
+
select: tool2.schema.string().optional().describe(PARAM_DESCRIPTIONS.selectComma),
|
|
1669
|
+
at: tool2.schema.string().optional().describe(PARAM_DESCRIPTIONS.at),
|
|
1670
|
+
end: tool2.schema.number().int().positive().optional().describe(PARAM_DESCRIPTIONS.end),
|
|
1671
|
+
limit: tool2.schema.number().int().positive().optional().describe(PARAM_DESCRIPTIONS.limit),
|
|
1672
|
+
language: tool2.schema.string().optional().describe(PARAM_DESCRIPTIONS.languageOverride),
|
|
1673
|
+
visionMode: tool2.schema.enum(IMAGE_MODES).optional().describe(PARAM_DESCRIPTIONS.visionModeDigest),
|
|
1674
|
+
visionLevel: tool2.schema.enum(VISION_LEVEL_VALUES).optional().describe(PARAM_DESCRIPTIONS.visionLevel)
|
|
1675
|
+
}, "digest", async (input, context) => {
|
|
1676
|
+
const filePath = resolvePath(context.directory, input.path);
|
|
1677
|
+
await authorizeRead(context, filePath);
|
|
1678
|
+
const select = (input.select ?? "content").trim() || "content";
|
|
1679
|
+
const visionMode = input.visionMode;
|
|
1680
|
+
const visionLevel = input.visionLevel;
|
|
1681
|
+
const vision = validateVisionLevelVsMode({ visionMode, visionLevel });
|
|
1682
|
+
if (!vision.ok)
|
|
1683
|
+
throw new Error(vision.error);
|
|
1684
|
+
const cacheDir = await ensureReadSeekCache(context);
|
|
1685
|
+
const args = buildDigestArgs({
|
|
1686
|
+
path: filePath,
|
|
1687
|
+
select,
|
|
1688
|
+
at: input.at,
|
|
1689
|
+
end: input.end,
|
|
1690
|
+
limit: input.limit,
|
|
1691
|
+
language: input.language,
|
|
1692
|
+
visionMode,
|
|
1693
|
+
visionLevel,
|
|
1694
|
+
readseekDir: cacheDir
|
|
1695
|
+
});
|
|
1696
|
+
return runReadSeek(context, args, visionMode === undefined || visionMode === "none" ? {} : { timeoutMs: DEFAULT_VISION_TIMEOUT_MS });
|
|
1697
|
+
}),
|
|
1698
|
+
readseek_grep: readseekTool(openCodeToolDescription("grep"), {
|
|
1699
|
+
pattern: tool2.schema.string().describe(PARAM_DESCRIPTIONS.grepPattern),
|
|
1700
|
+
path: tool2.schema.string().optional().describe(PARAM_DESCRIPTIONS.projectSearchPath),
|
|
1701
|
+
glob: tool2.schema.string().optional().describe(PARAM_DESCRIPTIONS.glob),
|
|
1702
|
+
literal: tool2.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.literal),
|
|
1703
|
+
ignoreCase: tool2.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.ignoreCase),
|
|
1704
|
+
context: tool2.schema.number().int().nonnegative().optional().describe(PARAM_DESCRIPTIONS.context),
|
|
1705
|
+
limit: tool2.schema.number().int().positive().optional().describe(PARAM_DESCRIPTIONS.matchLimit)
|
|
1706
|
+
}, "grep", async (input, context) => {
|
|
1707
|
+
const target = resolvePath(context.directory, input.path ?? ".");
|
|
1708
|
+
const pattern = input.pattern;
|
|
1709
|
+
await authorizeSearch(context, target, pattern);
|
|
1710
|
+
let expression;
|
|
1711
|
+
try {
|
|
1712
|
+
const source = input.literal ? pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : pattern;
|
|
1713
|
+
expression = new RegExp(source, input.ignoreCase ? "i" : "");
|
|
1714
|
+
} catch (error) {
|
|
1715
|
+
throw new Error(`invalid regular expression: ${error instanceof Error ? error.message : String(error)}`);
|
|
1716
|
+
}
|
|
1717
|
+
const info = await stat3(target);
|
|
1718
|
+
const files = [];
|
|
1719
|
+
if (info.isFile())
|
|
1720
|
+
files.push(target);
|
|
1721
|
+
else if (info.isDirectory()) {
|
|
1722
|
+
const glob = new Bun.Glob(input.glob ?? "**/*");
|
|
1723
|
+
for await (const filePath of glob.scan({ cwd: target, absolute: true, onlyFiles: true, followSymlinks: false })) {
|
|
1724
|
+
files.push(filePath);
|
|
1680
1725
|
}
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1726
|
+
} else
|
|
1727
|
+
throw new Error(`grep target is not a file or directory: ${target}`);
|
|
1728
|
+
const maxMatches = input.limit ?? 100;
|
|
1729
|
+
const contextLines = input.context ?? 0;
|
|
1730
|
+
const results = [];
|
|
1731
|
+
let totalMatches = 0;
|
|
1732
|
+
let truncated = false;
|
|
1733
|
+
for (const filePath of files.sort()) {
|
|
1734
|
+
context.abort.throwIfAborted();
|
|
1735
|
+
if (truncated)
|
|
1736
|
+
break;
|
|
1737
|
+
const buffer = await readFile2(filePath);
|
|
1738
|
+
if (buffer.includes(0))
|
|
1739
|
+
continue;
|
|
1740
|
+
const lines = buffer.toString("utf8").replace(/\r\n/g, `
|
|
1741
|
+
`).split(`
|
|
1742
|
+
`);
|
|
1743
|
+
const matchLines = new Set;
|
|
1744
|
+
const ranges = [];
|
|
1745
|
+
for (let index = 0;index < lines.length; index++) {
|
|
1746
|
+
expression.lastIndex = 0;
|
|
1747
|
+
if (!expression.test(lines[index] ?? ""))
|
|
1748
|
+
continue;
|
|
1749
|
+
if (totalMatches === maxMatches) {
|
|
1750
|
+
truncated = true;
|
|
1751
|
+
break;
|
|
1689
1752
|
}
|
|
1690
|
-
|
|
1753
|
+
const line = index + 1;
|
|
1754
|
+
matchLines.add(line);
|
|
1755
|
+
ranges.push([Math.max(1, line - contextLines), Math.min(lines.length, line + contextLines)]);
|
|
1756
|
+
totalMatches++;
|
|
1691
1757
|
}
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
const
|
|
1697
|
-
if (start
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
extra.push("--language", input.language);
|
|
1702
|
-
} else {
|
|
1703
|
-
if (input.offset !== undefined || input.limit !== undefined) {
|
|
1704
|
-
throw new Error("offset and limit do not apply to image reads");
|
|
1705
|
-
}
|
|
1706
|
-
if (input.language !== undefined) {
|
|
1707
|
-
throw new Error("language does not apply to image reads");
|
|
1708
|
-
}
|
|
1709
|
-
if (image !== undefined)
|
|
1710
|
-
extra.push("--vision-mode", image);
|
|
1758
|
+
if (ranges.length === 0)
|
|
1759
|
+
continue;
|
|
1760
|
+
const mergedRanges = [];
|
|
1761
|
+
for (const [start, end] of ranges) {
|
|
1762
|
+
const previous = mergedRanges.at(-1);
|
|
1763
|
+
if (previous && start <= previous[1] + 1)
|
|
1764
|
+
previous[1] = Math.max(previous[1], end);
|
|
1765
|
+
else
|
|
1766
|
+
mergedRanges.push([start, end]);
|
|
1711
1767
|
}
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
return digestDocumentReceipt(context, filePath);
|
|
1720
|
-
}
|
|
1721
|
-
if (isVisualBoundsError(error)) {
|
|
1722
|
-
const metadataEnvelope = await runReadSeek(context, digestArgs(filePath, "metadata"));
|
|
1723
|
-
if (isPdfFile(digestMetadata(metadataEnvelope))) {
|
|
1724
|
-
return documentReceipt(filePath, metadataEnvelope);
|
|
1725
|
-
}
|
|
1726
|
-
if (isVisualFile(digestMetadata(metadataEnvelope))) {
|
|
1727
|
-
return { file: filePath, skipped: true, reason: "image mode not selected" };
|
|
1728
|
-
}
|
|
1768
|
+
const linesByNumber = new Map;
|
|
1769
|
+
for (const [start, end] of mergedRanges) {
|
|
1770
|
+
const output = record(await runDigestContent(context, readRangeArgs(filePath, start, end)));
|
|
1771
|
+
for (const hashline of items(output.hashlines)) {
|
|
1772
|
+
const line = record(hashline).line;
|
|
1773
|
+
if (typeof line === "number" && !linesByNumber.has(line))
|
|
1774
|
+
linesByNumber.set(line, hashline);
|
|
1729
1775
|
}
|
|
1730
|
-
throw error;
|
|
1731
|
-
}
|
|
1732
|
-
const metadata = digestMetadata(envelope);
|
|
1733
|
-
if (isPdfFile(metadata) || record(envelope).cache !== undefined) {
|
|
1734
|
-
return documentReceipt(filePath, envelope);
|
|
1735
|
-
}
|
|
1736
|
-
const visual = isVisualFile(metadata);
|
|
1737
|
-
if (visual && image === undefined) {
|
|
1738
|
-
return { file: filePath, skipped: true, reason: "image mode not selected" };
|
|
1739
1776
|
}
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
throw new Error("language does not apply to image reads");
|
|
1745
|
-
}
|
|
1746
|
-
return requireDigestFacet(envelope, "content");
|
|
1747
|
-
}, formatReadResult),
|
|
1748
|
-
readseek_view: readseekTool(openCodeToolDescription("view"), {
|
|
1749
|
-
path: tool.schema.string().describe("Document path relative to the project directory"),
|
|
1750
|
-
node: tool.schema.string().min(1).optional().describe(PARAM_DESCRIPTIONS.node),
|
|
1751
|
-
page: tool.schema.number().int().positive().optional().describe(PARAM_DESCRIPTIONS.page),
|
|
1752
|
-
kind: tool.schema.enum(DOCUMENT_NODE_KIND_VALUES).optional().describe(PARAM_DESCRIPTIONS.kind),
|
|
1753
|
-
depth: tool.schema.number().int().nonnegative().optional().describe(PARAM_DESCRIPTIONS.depth),
|
|
1754
|
-
outline: tool.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.outline),
|
|
1755
|
-
visionMode: tool.schema.enum(VISION_ANALYSIS_MODE_VALUES).optional().describe(PARAM_DESCRIPTIONS.visionModeView),
|
|
1756
|
-
visionLevel: tool.schema.enum(VISION_LEVEL_VALUES).optional().describe(PARAM_DESCRIPTIONS.visionLevel)
|
|
1757
|
-
}, "view", async (input, context) => {
|
|
1758
|
-
const filePath = resolvePath(context.directory, input.path);
|
|
1759
|
-
await authorizeRead(context, filePath);
|
|
1760
|
-
const visionMode = input.visionMode;
|
|
1761
|
-
const visionLevel = input.visionLevel;
|
|
1762
|
-
const vision = validateVisionLevelVsMode({
|
|
1763
|
-
visionMode,
|
|
1764
|
-
visionLevel,
|
|
1765
|
-
outline: input.outline
|
|
1766
|
-
});
|
|
1767
|
-
if (!vision.ok)
|
|
1768
|
-
throw new Error(vision.error);
|
|
1769
|
-
const cacheDir = await ensureReadSeekCache(context);
|
|
1770
|
-
const args = buildViewArgs({
|
|
1771
|
-
path: filePath,
|
|
1772
|
-
node: input.node,
|
|
1773
|
-
page: input.page,
|
|
1774
|
-
kind: input.kind,
|
|
1775
|
-
depth: input.depth,
|
|
1776
|
-
outline: input.outline,
|
|
1777
|
-
visionMode,
|
|
1778
|
-
visionLevel,
|
|
1779
|
-
readseekDir: cacheDir
|
|
1777
|
+
const hashlines = [...linesByNumber.values()];
|
|
1778
|
+
const matches = hashlines.filter((hashline) => {
|
|
1779
|
+
const line = record(hashline).line;
|
|
1780
|
+
return typeof line === "number" && matchLines.has(line);
|
|
1780
1781
|
});
|
|
1781
|
-
|
|
1782
|
-
}
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1782
|
+
results.push({ file: filePath, matches, hashlines });
|
|
1783
|
+
}
|
|
1784
|
+
return { results, truncated };
|
|
1785
|
+
}),
|
|
1786
|
+
readseek_search: readseekTool(openCodeToolDescription("search"), {
|
|
1787
|
+
pattern: tool2.schema.string().describe(PARAM_DESCRIPTIONS.pattern),
|
|
1788
|
+
path: tool2.schema.string().optional().describe(PARAM_DESCRIPTIONS.projectSearchPath),
|
|
1789
|
+
language: tool2.schema.string().optional().describe(PARAM_DESCRIPTIONS.language),
|
|
1790
|
+
cached: tool2.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.cached),
|
|
1791
|
+
others: tool2.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.others),
|
|
1792
|
+
ignored: tool2.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.ignored)
|
|
1793
|
+
}, "search", async (input, context) => {
|
|
1794
|
+
const target = resolvePath(context.directory, input.path ?? ".");
|
|
1795
|
+
const args = buildSearchArgs({
|
|
1796
|
+
path: target,
|
|
1797
|
+
pattern: input.pattern,
|
|
1798
|
+
language: input.language,
|
|
1799
|
+
cached: input.cached,
|
|
1800
|
+
others: input.others,
|
|
1801
|
+
ignored: input.ignored
|
|
1802
|
+
});
|
|
1803
|
+
await authorizeSearch(context, target, input.pattern);
|
|
1804
|
+
return runReadSeek(context, args);
|
|
1805
|
+
}),
|
|
1806
|
+
readseek_def: readseekTool(openCodeToolDescription("def"), {
|
|
1807
|
+
name: tool2.schema.string().describe(PARAM_DESCRIPTIONS.name),
|
|
1808
|
+
path: tool2.schema.string().optional().describe(PARAM_DESCRIPTIONS.projectSearchPath),
|
|
1809
|
+
language: tool2.schema.string().optional().describe(PARAM_DESCRIPTIONS.language),
|
|
1810
|
+
cached: tool2.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.cached),
|
|
1811
|
+
others: tool2.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.others),
|
|
1812
|
+
ignored: tool2.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.ignored)
|
|
1813
|
+
}, "def", async (input, context) => {
|
|
1814
|
+
const target = resolvePath(context.directory, input.path ?? ".");
|
|
1815
|
+
const args = buildDefArgs({
|
|
1816
|
+
path: target,
|
|
1817
|
+
name: input.name,
|
|
1818
|
+
language: input.language,
|
|
1819
|
+
format: "plain",
|
|
1820
|
+
cached: input.cached,
|
|
1821
|
+
others: input.others,
|
|
1822
|
+
ignored: input.ignored
|
|
1823
|
+
});
|
|
1824
|
+
await authorizeSearch(context, target, input.name);
|
|
1825
|
+
return runReadSeek(context, args);
|
|
1826
|
+
}),
|
|
1827
|
+
readseek_refs: readseekTool(openCodeToolDescription("refs"), {
|
|
1828
|
+
name: tool2.schema.string().optional().describe(PARAM_DESCRIPTIONS.refsName),
|
|
1829
|
+
path: tool2.schema.string().optional().describe(PARAM_DESCRIPTIONS.projectSearchPath),
|
|
1830
|
+
language: tool2.schema.string().optional().describe(PARAM_DESCRIPTIONS.language),
|
|
1831
|
+
scope: tool2.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.scope),
|
|
1832
|
+
line: tool2.schema.number().int().positive().optional().describe(PARAM_DESCRIPTIONS.line),
|
|
1833
|
+
column: tool2.schema.number().int().positive().optional().describe(PARAM_DESCRIPTIONS.column),
|
|
1834
|
+
cached: tool2.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.cached),
|
|
1835
|
+
others: tool2.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.others),
|
|
1836
|
+
ignored: tool2.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.ignored)
|
|
1837
|
+
}, "refs", async (input, context) => {
|
|
1838
|
+
const target = resolvePath(context.directory, input.path ?? ".");
|
|
1839
|
+
const name = input.name;
|
|
1840
|
+
const args = buildRefsArgs({
|
|
1841
|
+
path: target,
|
|
1842
|
+
name,
|
|
1843
|
+
scope: input.scope,
|
|
1844
|
+
line: input.line,
|
|
1845
|
+
column: input.column,
|
|
1846
|
+
language: input.language,
|
|
1847
|
+
cached: input.cached,
|
|
1848
|
+
others: input.others,
|
|
1849
|
+
ignored: input.ignored
|
|
1850
|
+
});
|
|
1851
|
+
await authorizeSearch(context, target, name ?? "*");
|
|
1852
|
+
return runReadSeek(context, args);
|
|
1853
|
+
}),
|
|
1854
|
+
readseek_rename: readseekTool(openCodeToolDescription("rename"), {
|
|
1855
|
+
path: tool2.schema.string().describe(PARAM_DESCRIPTIONS.projectPath),
|
|
1856
|
+
line: tool2.schema.number().int().positive().describe(PARAM_DESCRIPTIONS.renameLine),
|
|
1857
|
+
column: tool2.schema.number().int().positive().optional().describe(PARAM_DESCRIPTIONS.column),
|
|
1858
|
+
to: tool2.schema.string().min(1).describe(PARAM_DESCRIPTIONS.to),
|
|
1859
|
+
workspace: tool2.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.workspace),
|
|
1860
|
+
apply: tool2.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.applyDefaultTrue)
|
|
1861
|
+
}, "rename", async (input, context) => {
|
|
1862
|
+
const filePath = resolvePath(context.directory, input.path);
|
|
1863
|
+
await authorizeRead(context, filePath);
|
|
1864
|
+
if (input.workspace) {
|
|
1865
|
+
const identifyArgs = digestArgs(filePath, "identity");
|
|
1866
|
+
withAt(identifyArgs, input.line, input.column);
|
|
1867
|
+
const envelope = await runReadSeek(context, identifyArgs);
|
|
1868
|
+
const name = identifiedName(requireDigestFacet(envelope, "identity"));
|
|
1869
|
+
if (!name)
|
|
1870
|
+
throw new Error("readseek could not identify a binding at the rename cursor");
|
|
1871
|
+
await authorizeSearch(context, context.directory, name);
|
|
1872
|
+
}
|
|
1873
|
+
const args = buildRenameArgs({
|
|
1874
|
+
path: filePath,
|
|
1875
|
+
to: input.to,
|
|
1876
|
+
line: input.line,
|
|
1877
|
+
column: input.column,
|
|
1878
|
+
workspace: input.workspace ? context.directory : undefined
|
|
1879
|
+
});
|
|
1880
|
+
const plan = await runReadSeek(context, args);
|
|
1881
|
+
if (input.apply === false)
|
|
1882
|
+
return plan;
|
|
1883
|
+
const files = renameFiles(plan);
|
|
1884
|
+
const planHash = record(plan).plan_hash;
|
|
1885
|
+
if (typeof planHash !== "string" || planHash.length === 0) {
|
|
1886
|
+
throw new Error("readseek rename plan did not include a plan hash");
|
|
1887
|
+
}
|
|
1888
|
+
await authorizeEdit(context, files);
|
|
1889
|
+
return runReadSeek(context, buildRenameArgs({
|
|
1890
|
+
path: filePath,
|
|
1891
|
+
to: input.to,
|
|
1892
|
+
line: input.line,
|
|
1893
|
+
column: input.column,
|
|
1894
|
+
workspace: input.workspace ? context.directory : undefined,
|
|
1895
|
+
apply: true,
|
|
1896
|
+
planHash
|
|
1897
|
+
}), {
|
|
1898
|
+
cancelable: false,
|
|
1899
|
+
timeoutMs: DEFAULT_READSEEK_TIMEOUT_MS
|
|
1900
|
+
});
|
|
1901
|
+
}),
|
|
1902
|
+
readseek_edit: readseekTool(openCodeToolDescription("edit"), {
|
|
1903
|
+
path: tool2.schema.string().describe(PARAM_DESCRIPTIONS.projectPath),
|
|
1904
|
+
edits: tool2.schema.array(tool2.schema.union([
|
|
1905
|
+
tool2.schema.object({
|
|
1906
|
+
set_line: tool2.schema.object({
|
|
1907
|
+
anchor: tool2.schema.string().describe(PARAM_DESCRIPTIONS.anchor),
|
|
1908
|
+
new_text: tool2.schema.string().describe(PARAM_DESCRIPTIONS.newTextLine)
|
|
1909
|
+
}).strict()
|
|
1910
|
+
}).strict(),
|
|
1911
|
+
tool2.schema.object({
|
|
1912
|
+
replace_lines: tool2.schema.object({
|
|
1913
|
+
start_anchor: tool2.schema.string().describe(PARAM_DESCRIPTIONS.startAnchor),
|
|
1914
|
+
end_anchor: tool2.schema.string().describe(PARAM_DESCRIPTIONS.endAnchor),
|
|
1915
|
+
new_text: tool2.schema.string().describe(PARAM_DESCRIPTIONS.newTextRange)
|
|
1916
|
+
}).strict()
|
|
1917
|
+
}).strict(),
|
|
1918
|
+
tool2.schema.object({
|
|
1919
|
+
insert_after: tool2.schema.object({
|
|
1920
|
+
anchor: tool2.schema.string().describe(PARAM_DESCRIPTIONS.insertAnchor),
|
|
1921
|
+
new_text: tool2.schema.string().describe(PARAM_DESCRIPTIONS.newTextInsert)
|
|
1922
|
+
}).strict()
|
|
1923
|
+
}).strict(),
|
|
1924
|
+
tool2.schema.object({
|
|
1925
|
+
replace: tool2.schema.object({
|
|
1926
|
+
old_text: tool2.schema.string().describe(PARAM_DESCRIPTIONS.oldText),
|
|
1927
|
+
new_text: tool2.schema.string().describe(PARAM_DESCRIPTIONS.newText),
|
|
1928
|
+
all: tool2.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.all)
|
|
1929
|
+
}).strict()
|
|
1930
|
+
}).strict(),
|
|
1931
|
+
tool2.schema.object({
|
|
1932
|
+
replace_symbol: tool2.schema.object({
|
|
1933
|
+
symbol: tool2.schema.string().describe(PARAM_DESCRIPTIONS.symbol),
|
|
1934
|
+
new_body: tool2.schema.string().describe(PARAM_DESCRIPTIONS.newBody)
|
|
1935
|
+
}).strict()
|
|
1936
|
+
}).strict()
|
|
1937
|
+
])).min(1).describe(PARAM_DESCRIPTIONS.edits),
|
|
1938
|
+
language: tool2.schema.string().optional().describe(PARAM_DESCRIPTIONS.languageSymbol)
|
|
1939
|
+
}, "edit", async (input, context) => {
|
|
1940
|
+
const filePath = resolvePath(context.directory, input.path);
|
|
1941
|
+
validateEdits(input.edits);
|
|
1942
|
+
return withFileMutationQueue(filePath, async () => {
|
|
1943
|
+
context.abort.throwIfAborted();
|
|
1794
1944
|
await authorizeRead(context, filePath);
|
|
1795
|
-
|
|
1796
|
-
const
|
|
1797
|
-
const
|
|
1798
|
-
const
|
|
1799
|
-
if (!vision.ok)
|
|
1800
|
-
throw new Error(vision.error);
|
|
1801
|
-
const cacheDir = await ensureReadSeekCache(context);
|
|
1802
|
-
const args = buildDigestArgs({
|
|
1945
|
+
await rejectSymlinkMutation(filePath);
|
|
1946
|
+
const edits = input.edits;
|
|
1947
|
+
const request = JSON.stringify({ edits });
|
|
1948
|
+
const baseArgs = buildEditArgs({
|
|
1803
1949
|
path: filePath,
|
|
1804
|
-
select,
|
|
1805
|
-
at: input.at,
|
|
1806
|
-
end: input.end,
|
|
1807
|
-
limit: input.limit,
|
|
1808
1950
|
language: input.language,
|
|
1809
|
-
|
|
1810
|
-
visionLevel,
|
|
1811
|
-
readseekDir: cacheDir
|
|
1951
|
+
requestFromStdin: true
|
|
1812
1952
|
});
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
pattern: tool.schema.string().describe("Text or regular expression to search for; set literal for exact text"),
|
|
1817
|
-
path: tool.schema.string().optional().describe("File or directory to search; defaults to the project directory"),
|
|
1818
|
-
glob: tool.schema.string().optional().describe("File-name glob, such as **/*.ts"),
|
|
1819
|
-
literal: tool.schema.boolean().optional().describe("Treat pattern as literal text"),
|
|
1820
|
-
ignoreCase: tool.schema.boolean().optional().describe("Ignore case"),
|
|
1821
|
-
context: tool.schema.number().int().nonnegative().optional().describe("Surrounding lines for each match"),
|
|
1822
|
-
limit: tool.schema.number().int().positive().optional().describe("Maximum matching lines; defaults to 100")
|
|
1823
|
-
}, "grep", async (input, context) => {
|
|
1824
|
-
const target = resolvePath(context.directory, input.path ?? ".");
|
|
1825
|
-
const pattern = input.pattern;
|
|
1826
|
-
await authorizeSearch(context, target, pattern);
|
|
1827
|
-
let expression;
|
|
1828
|
-
try {
|
|
1829
|
-
const source = input.literal ? pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : pattern;
|
|
1830
|
-
expression = new RegExp(source, input.ignoreCase ? "i" : "");
|
|
1831
|
-
} catch (error) {
|
|
1832
|
-
throw new Error(`invalid regular expression: ${error instanceof Error ? error.message : String(error)}`);
|
|
1953
|
+
const plan = record(await runReadSeek(context, baseArgs, { stdin: request }));
|
|
1954
|
+
if (typeof plan.before_content !== "string" || typeof plan.content !== "string" || typeof plan.plan_hash !== "string") {
|
|
1955
|
+
throw new Error("readseek edit plan is missing before_content, content, or plan_hash");
|
|
1833
1956
|
}
|
|
1834
|
-
const
|
|
1835
|
-
const
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
for await (const filePath of glob.scan({ cwd: target, absolute: true, onlyFiles: true, followSymlinks: false })) {
|
|
1841
|
-
files.push(filePath);
|
|
1842
|
-
}
|
|
1843
|
-
} else
|
|
1844
|
-
throw new Error(`grep target is not a file or directory: ${target}`);
|
|
1845
|
-
const maxMatches = input.limit ?? 100;
|
|
1846
|
-
const contextLines = input.context ?? 0;
|
|
1847
|
-
const results = [];
|
|
1848
|
-
let totalMatches = 0;
|
|
1849
|
-
let truncated = false;
|
|
1850
|
-
for (const filePath of files.sort()) {
|
|
1851
|
-
context.abort.throwIfAborted();
|
|
1852
|
-
if (truncated)
|
|
1853
|
-
break;
|
|
1854
|
-
const buffer = await readFile(filePath);
|
|
1855
|
-
if (buffer.includes(0))
|
|
1856
|
-
continue;
|
|
1857
|
-
const lines = buffer.toString("utf8").replace(/\r\n/g, `
|
|
1858
|
-
`).split(`
|
|
1859
|
-
`);
|
|
1860
|
-
const matchLines = new Set;
|
|
1861
|
-
const ranges = [];
|
|
1862
|
-
for (let index = 0;index < lines.length; index++) {
|
|
1863
|
-
expression.lastIndex = 0;
|
|
1864
|
-
if (!expression.test(lines[index] ?? ""))
|
|
1865
|
-
continue;
|
|
1866
|
-
if (totalMatches === maxMatches) {
|
|
1867
|
-
truncated = true;
|
|
1868
|
-
break;
|
|
1869
|
-
}
|
|
1870
|
-
const line = index + 1;
|
|
1871
|
-
matchLines.add(line);
|
|
1872
|
-
ranges.push([Math.max(1, line - contextLines), Math.min(lines.length, line + contextLines)]);
|
|
1873
|
-
totalMatches++;
|
|
1874
|
-
}
|
|
1875
|
-
if (ranges.length === 0)
|
|
1876
|
-
continue;
|
|
1877
|
-
const mergedRanges = [];
|
|
1878
|
-
for (const [start, end] of ranges) {
|
|
1879
|
-
const previous = mergedRanges.at(-1);
|
|
1880
|
-
if (previous && start <= previous[1] + 1)
|
|
1881
|
-
previous[1] = Math.max(previous[1], end);
|
|
1882
|
-
else
|
|
1883
|
-
mergedRanges.push([start, end]);
|
|
1884
|
-
}
|
|
1885
|
-
const linesByNumber = new Map;
|
|
1886
|
-
for (const [start, end] of mergedRanges) {
|
|
1887
|
-
const output = record(await runDigestContent(context, readRangeArgs(filePath, start, end)));
|
|
1888
|
-
for (const hashline of items(output.hashlines)) {
|
|
1889
|
-
const line = record(hashline).line;
|
|
1890
|
-
if (typeof line === "number" && !linesByNumber.has(line))
|
|
1891
|
-
linesByNumber.set(line, hashline);
|
|
1892
|
-
}
|
|
1893
|
-
}
|
|
1894
|
-
const hashlines = [...linesByNumber.values()];
|
|
1895
|
-
const matches = hashlines.filter((hashline) => {
|
|
1896
|
-
const line = record(hashline).line;
|
|
1897
|
-
return typeof line === "number" && matchLines.has(line);
|
|
1898
|
-
});
|
|
1899
|
-
results.push({ file: filePath, matches, hashlines });
|
|
1957
|
+
const before = plan.before_content;
|
|
1958
|
+
const after = plan.content;
|
|
1959
|
+
const previewStart = Math.max(1, firstChangedLine(before, after) - 3);
|
|
1960
|
+
const previewArgs = readRangeArgs(filePath, previewStart, previewStart + EDIT_RESULT_READ_LIMIT - 1);
|
|
1961
|
+
if (before === after) {
|
|
1962
|
+
return postMutationPreview(context, previewArgs, filePath, false);
|
|
1900
1963
|
}
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
pattern: tool.schema.string().describe(PARAM_DESCRIPTIONS.pattern),
|
|
1905
|
-
path: tool.schema.string().optional().describe("File or directory to search; defaults to the project directory"),
|
|
1906
|
-
language: tool.schema.string().optional().describe(PARAM_DESCRIPTIONS.language),
|
|
1907
|
-
cached: tool.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.cached),
|
|
1908
|
-
others: tool.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.others),
|
|
1909
|
-
ignored: tool.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.ignored)
|
|
1910
|
-
}, "search", async (input, context) => {
|
|
1911
|
-
const target = resolvePath(context.directory, input.path ?? ".");
|
|
1912
|
-
const args = buildSearchArgs({
|
|
1913
|
-
path: target,
|
|
1914
|
-
pattern: input.pattern,
|
|
1915
|
-
language: input.language,
|
|
1916
|
-
cached: input.cached,
|
|
1917
|
-
others: input.others,
|
|
1918
|
-
ignored: input.ignored
|
|
1919
|
-
});
|
|
1920
|
-
await authorizeSearch(context, target, input.pattern);
|
|
1921
|
-
return runReadSeek(context, args);
|
|
1922
|
-
}),
|
|
1923
|
-
readseek_def: readseekTool(openCodeToolDescription("def"), {
|
|
1924
|
-
name: tool.schema.string().describe(PARAM_DESCRIPTIONS.name),
|
|
1925
|
-
path: tool.schema.string().optional().describe("File or directory to search; defaults to the project directory"),
|
|
1926
|
-
language: tool.schema.string().optional().describe(PARAM_DESCRIPTIONS.language),
|
|
1927
|
-
cached: tool.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.cached),
|
|
1928
|
-
others: tool.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.others),
|
|
1929
|
-
ignored: tool.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.ignored)
|
|
1930
|
-
}, "def", async (input, context) => {
|
|
1931
|
-
const target = resolvePath(context.directory, input.path ?? ".");
|
|
1932
|
-
const args = buildDefArgs({
|
|
1933
|
-
path: target,
|
|
1934
|
-
name: input.name,
|
|
1935
|
-
language: input.language,
|
|
1936
|
-
format: "plain",
|
|
1937
|
-
cached: input.cached,
|
|
1938
|
-
others: input.others,
|
|
1939
|
-
ignored: input.ignored
|
|
1940
|
-
});
|
|
1941
|
-
await authorizeSearch(context, target, input.name);
|
|
1942
|
-
return runReadSeek(context, args);
|
|
1943
|
-
}),
|
|
1944
|
-
readseek_refs: readseekTool(openCodeToolDescription("refs"), {
|
|
1945
|
-
name: tool.schema.string().optional().describe(PARAM_DESCRIPTIONS.refsName),
|
|
1946
|
-
path: tool.schema.string().optional().describe("File or directory to search; defaults to the project directory"),
|
|
1947
|
-
language: tool.schema.string().optional().describe(PARAM_DESCRIPTIONS.language),
|
|
1948
|
-
scope: tool.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.scope),
|
|
1949
|
-
line: tool.schema.number().int().positive().optional().describe(PARAM_DESCRIPTIONS.line),
|
|
1950
|
-
column: tool.schema.number().int().positive().optional().describe(PARAM_DESCRIPTIONS.column),
|
|
1951
|
-
cached: tool.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.cached),
|
|
1952
|
-
others: tool.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.others),
|
|
1953
|
-
ignored: tool.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.ignored)
|
|
1954
|
-
}, "refs", async (input, context) => {
|
|
1955
|
-
const target = resolvePath(context.directory, input.path ?? ".");
|
|
1956
|
-
const name = input.name;
|
|
1957
|
-
const args = buildRefsArgs({
|
|
1958
|
-
path: target,
|
|
1959
|
-
name,
|
|
1960
|
-
scope: input.scope,
|
|
1961
|
-
line: input.line,
|
|
1962
|
-
column: input.column,
|
|
1963
|
-
language: input.language,
|
|
1964
|
-
cached: input.cached,
|
|
1965
|
-
others: input.others,
|
|
1966
|
-
ignored: input.ignored
|
|
1967
|
-
});
|
|
1968
|
-
await authorizeSearch(context, target, name ?? "*");
|
|
1969
|
-
return runReadSeek(context, args);
|
|
1970
|
-
}),
|
|
1971
|
-
readseek_rename: readseekTool(openCodeToolDescription("rename"), {
|
|
1972
|
-
path: tool.schema.string().describe("Path relative to the project directory"),
|
|
1973
|
-
line: tool.schema.number().int().positive().describe(PARAM_DESCRIPTIONS.renameLine),
|
|
1974
|
-
column: tool.schema.number().int().positive().optional().describe(PARAM_DESCRIPTIONS.column),
|
|
1975
|
-
to: tool.schema.string().min(1).describe(PARAM_DESCRIPTIONS.to),
|
|
1976
|
-
workspace: tool.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.workspace),
|
|
1977
|
-
apply: tool.schema.boolean().optional().describe("Apply the verified edits; defaults to true")
|
|
1978
|
-
}, "rename", async (input, context) => {
|
|
1979
|
-
const filePath = resolvePath(context.directory, input.path);
|
|
1980
|
-
await authorizeRead(context, filePath);
|
|
1981
|
-
if (input.workspace) {
|
|
1982
|
-
const identifyArgs = digestArgs(filePath, "identity");
|
|
1983
|
-
withAt(identifyArgs, input.line, input.column);
|
|
1984
|
-
const envelope = await runReadSeek(context, identifyArgs);
|
|
1985
|
-
const name = identifiedName(requireDigestFacet(envelope, "identity"));
|
|
1986
|
-
if (!name)
|
|
1987
|
-
throw new Error("readseek could not identify a binding at the rename cursor");
|
|
1988
|
-
await authorizeSearch(context, context.directory, name);
|
|
1989
|
-
}
|
|
1990
|
-
const args = buildRenameArgs({
|
|
1964
|
+
await authorizeEdit(context, [filePath], simpleDiff(filePath, before, after), false);
|
|
1965
|
+
context.abort.throwIfAborted();
|
|
1966
|
+
const applied = record(await runReadSeek(context, buildEditArgs({
|
|
1991
1967
|
path: filePath,
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
column: input.column,
|
|
1995
|
-
workspace: input.workspace ? context.directory : undefined
|
|
1996
|
-
});
|
|
1997
|
-
const plan = await runReadSeek(context, args);
|
|
1998
|
-
if (input.apply === false)
|
|
1999
|
-
return plan;
|
|
2000
|
-
const files = renameFiles(plan);
|
|
2001
|
-
const planHash = record(plan).plan_hash;
|
|
2002
|
-
if (typeof planHash !== "string" || planHash.length === 0) {
|
|
2003
|
-
throw new Error("readseek rename plan did not include a plan hash");
|
|
2004
|
-
}
|
|
2005
|
-
await authorizeEdit(context, files);
|
|
2006
|
-
return runReadSeek(context, buildRenameArgs({
|
|
2007
|
-
path: filePath,
|
|
2008
|
-
to: input.to,
|
|
2009
|
-
line: input.line,
|
|
2010
|
-
column: input.column,
|
|
2011
|
-
workspace: input.workspace ? context.directory : undefined,
|
|
1968
|
+
language: input.language,
|
|
1969
|
+
requestFromStdin: true,
|
|
2012
1970
|
apply: true,
|
|
2013
|
-
planHash
|
|
1971
|
+
planHash: plan.plan_hash
|
|
2014
1972
|
}), {
|
|
2015
1973
|
cancelable: false,
|
|
1974
|
+
stdin: request,
|
|
2016
1975
|
timeoutMs: DEFAULT_READSEEK_TIMEOUT_MS
|
|
2017
|
-
});
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
}).strict()
|
|
2053
|
-
}).strict()
|
|
2054
|
-
])).min(1).describe(PARAM_DESCRIPTIONS.edits),
|
|
2055
|
-
language: tool.schema.string().optional().describe(PARAM_DESCRIPTIONS.languageSymbol)
|
|
2056
|
-
}, "edit", async (input, context) => {
|
|
2057
|
-
const filePath = resolvePath(context.directory, input.path);
|
|
2058
|
-
validateEdits(input.edits);
|
|
2059
|
-
return withFileMutationQueue(filePath, async () => {
|
|
2060
|
-
context.abort.throwIfAborted();
|
|
2061
|
-
await authorizeRead(context, filePath);
|
|
2062
|
-
await rejectSymlinkMutation(filePath);
|
|
2063
|
-
const edits = input.edits;
|
|
2064
|
-
const request = JSON.stringify({ edits });
|
|
2065
|
-
const baseArgs = buildEditArgs({
|
|
2066
|
-
path: filePath,
|
|
2067
|
-
language: input.language,
|
|
2068
|
-
requestFromStdin: true
|
|
2069
|
-
});
|
|
2070
|
-
const plan = record(await runReadSeek(context, baseArgs, { stdin: request }));
|
|
2071
|
-
if (typeof plan.before_content !== "string" || typeof plan.content !== "string" || typeof plan.plan_hash !== "string") {
|
|
2072
|
-
throw new Error("readseek edit plan is missing before_content, content, or plan_hash");
|
|
2073
|
-
}
|
|
2074
|
-
const before = plan.before_content;
|
|
2075
|
-
const after = plan.content;
|
|
2076
|
-
const previewStart = Math.max(1, firstChangedLine(before, after) - 3);
|
|
2077
|
-
const previewArgs = readRangeArgs(filePath, previewStart, previewStart + EDIT_RESULT_READ_LIMIT - 1);
|
|
2078
|
-
if (before === after) {
|
|
2079
|
-
return postMutationPreview(context, previewArgs, filePath, false);
|
|
2080
|
-
}
|
|
2081
|
-
await authorizeEdit(context, [filePath], simpleDiff(filePath, before, after), false);
|
|
2082
|
-
context.abort.throwIfAborted();
|
|
2083
|
-
const applied = record(await runReadSeek(context, buildEditArgs({
|
|
2084
|
-
path: filePath,
|
|
2085
|
-
language: input.language,
|
|
2086
|
-
requestFromStdin: true,
|
|
2087
|
-
apply: true,
|
|
2088
|
-
planHash: plan.plan_hash
|
|
2089
|
-
}), {
|
|
2090
|
-
cancelable: false,
|
|
2091
|
-
stdin: request,
|
|
2092
|
-
timeoutMs: DEFAULT_READSEEK_TIMEOUT_MS
|
|
2093
|
-
}));
|
|
2094
|
-
if (applied.applied !== true) {
|
|
2095
|
-
throw new Error("readseek did not confirm that the edit was applied");
|
|
2096
|
-
}
|
|
2097
|
-
return postMutationPreview(context, previewArgs, filePath, true);
|
|
2098
|
-
});
|
|
2099
|
-
}),
|
|
2100
|
-
readseek_write: readseekTool(openCodeToolDescription("write"), {
|
|
2101
|
-
path: tool.schema.string().describe("Path relative to the project directory"),
|
|
2102
|
-
content: tool.schema.string().describe("Complete text file content")
|
|
2103
|
-
}, "write", async (input, context) => {
|
|
2104
|
-
const filePath = resolvePath(context.directory, input.path);
|
|
2105
|
-
await authorizeExternal(context, filePath);
|
|
2106
|
-
return withFileMutationQueue(filePath, async () => {
|
|
2107
|
-
context.abort.throwIfAborted();
|
|
2108
|
-
await rejectSymlinkMutation(filePath);
|
|
2109
|
-
const before = await readTextSnapshot(filePath);
|
|
2110
|
-
const content = input.content;
|
|
2111
|
-
if (content.includes("\x00"))
|
|
2112
|
-
throw new Error("write content must be text");
|
|
2113
|
-
await authorizeEdit(context, [filePath], simpleDiff(filePath, before.content, content), false);
|
|
2114
|
-
context.abort.throwIfAborted();
|
|
2115
|
-
await writeText(filePath, before, content);
|
|
2116
|
-
return postMutationPreview(context, digestArgs(filePath, "content", ["--end", String(DEFAULT_READ_LIMIT)]), filePath, !before.exists || before.content !== content);
|
|
2117
|
-
});
|
|
2118
|
-
})
|
|
2119
|
-
},
|
|
1976
|
+
}));
|
|
1977
|
+
if (applied.applied !== true) {
|
|
1978
|
+
throw new Error("readseek did not confirm that the edit was applied");
|
|
1979
|
+
}
|
|
1980
|
+
return postMutationPreview(context, previewArgs, filePath, true);
|
|
1981
|
+
});
|
|
1982
|
+
}),
|
|
1983
|
+
readseek_write: readseekTool(openCodeToolDescription("write"), {
|
|
1984
|
+
path: tool2.schema.string().describe(PARAM_DESCRIPTIONS.projectPath),
|
|
1985
|
+
content: tool2.schema.string().describe(PARAM_DESCRIPTIONS.content)
|
|
1986
|
+
}, "write", async (input, context) => {
|
|
1987
|
+
const filePath = resolvePath(context.directory, input.path);
|
|
1988
|
+
await authorizeExternal(context, filePath);
|
|
1989
|
+
return withFileMutationQueue(filePath, async () => {
|
|
1990
|
+
context.abort.throwIfAborted();
|
|
1991
|
+
await rejectSymlinkMutation(filePath);
|
|
1992
|
+
const before = await readTextSnapshot(filePath);
|
|
1993
|
+
const content = input.content;
|
|
1994
|
+
if (content.includes("\x00"))
|
|
1995
|
+
throw new Error("write content must be text");
|
|
1996
|
+
await authorizeEdit(context, [filePath], simpleDiff(filePath, before.content, content), false);
|
|
1997
|
+
context.abort.throwIfAborted();
|
|
1998
|
+
await writeText(filePath, before, content);
|
|
1999
|
+
return postMutationPreview(context, digestArgs(filePath, "content", ["--end", String(DEFAULT_READ_LIMIT)]), filePath, !before.exists || before.content !== content);
|
|
2000
|
+
});
|
|
2001
|
+
})
|
|
2002
|
+
};
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
// index.ts
|
|
2006
|
+
var ReadSeekPlugin = async (_input, options) => {
|
|
2007
|
+
const anchors = new SessionAnchors;
|
|
2008
|
+
const imagePolicy = resolveImagePolicy(options);
|
|
2009
|
+
return {
|
|
2010
|
+
tool: createReadSeekTools(imagePolicy),
|
|
2120
2011
|
"experimental.chat.system.transform": async (_input2, output) => {
|
|
2121
2012
|
output.system.push(TOOL_POLICY);
|
|
2122
2013
|
},
|
|
@@ -2153,7 +2044,7 @@ var ReadSeekPlugin = async (_input, options) => {
|
|
|
2153
2044
|
const files = new Set;
|
|
2154
2045
|
collectAnchoredFiles(result, files);
|
|
2155
2046
|
for (const filePath of files)
|
|
2156
|
-
anchors.mark(input.sessionID,
|
|
2047
|
+
anchors.mark(input.sessionID, path7.resolve(filePath));
|
|
2157
2048
|
} catch {}
|
|
2158
2049
|
},
|
|
2159
2050
|
"experimental.session.compacting": async (input, output) => {
|