opencode-readseek 0.9.5 → 0.9.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +21 -229
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -22,11 +22,10 @@ var DOCUMENT_NODE_KINDS = [
|
|
|
22
22
|
"structural_section"
|
|
23
23
|
];
|
|
24
24
|
var TOOL_NAMES = [
|
|
25
|
-
"
|
|
25
|
+
"digest",
|
|
26
26
|
"edit",
|
|
27
27
|
"write",
|
|
28
28
|
"grep",
|
|
29
|
-
"digest",
|
|
30
29
|
"view",
|
|
31
30
|
"search",
|
|
32
31
|
"def",
|
|
@@ -103,11 +102,10 @@ var PARAM_DESCRIPTIONS = {
|
|
|
103
102
|
scopeContext: "Context lines within each symbol"
|
|
104
103
|
};
|
|
105
104
|
var TOOL_DESCRIPTIONS = {
|
|
106
|
-
|
|
105
|
+
digest: "Digest source content and images into the native CLI envelope. Returns metadata plus requested facets unchanged from `readseek digest`. Use it before edits to obtain fresh `LINE:HASH` anchors; use for structural maps, diagnostics, and cursor identity.",
|
|
107
106
|
edit: 'Edit existing text files with fresh `LINE:HASH` anchors. Read or search first when no fresh anchors are available. Each `edits[]` item must use exactly one nested object: `{"set_line":{"anchor":"LINE:HASH","new_text":"..."}}`, `{"replace_lines":{"start_anchor":"LINE:HASH","end_anchor":"LINE:HASH","new_text":"..."}}`, `{"insert_after":{"anchor":"LINE:HASH","new_text":"..."}}`, `{"replace_symbol":{"symbol":"name","new_body":"..."}}`, or `{"replace":{"old_text":"...","new_text":"..."}}`.',
|
|
108
107
|
write: "Create or replace a complete text file and return `LINE:HASH` anchors. Use `edit` for small changes to an existing file.",
|
|
109
108
|
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
109
|
view: "Digest a document's structure or selected content. Start with the overview, then narrow by page, node, kind, or depth instead of reading the whole document. PDF is the first supported document format.",
|
|
112
110
|
search: "Search code by AST structure using ast-grep-style patterns. Results include line hashes usable as `LINE:HASH` anchors. Use `grep` for identifiers, strings, comments, configuration, documentation, or other plain text and regex.",
|
|
113
111
|
def: "Go to a symbol definition or declaration by name. Accepts qualified or unqualified names; use this instead of text search for functions, methods, classes, types, and other declarations.",
|
|
@@ -115,7 +113,10 @@ var TOOL_DESCRIPTIONS = {
|
|
|
115
113
|
rename: "Rename a symbol at a source location. Resolvable same-named bindings are preserved; verified edits apply by default. Set `apply: false` to preview the plan before changing files."
|
|
116
114
|
};
|
|
117
115
|
function renderToolDescription(name, aliases = {}) {
|
|
118
|
-
|
|
116
|
+
const desc = TOOL_DESCRIPTIONS[name];
|
|
117
|
+
if (!desc)
|
|
118
|
+
throw new Error(`Missing TOOL_DESCRIPTIONS for ${name}`);
|
|
119
|
+
return TOOL_NAMES.reduce((description, toolName) => description.replaceAll(`\`${toolName}\``, `\`${aliases[toolName] ?? toolName}\``), desc);
|
|
119
120
|
}
|
|
120
121
|
var gitFlagsSchema = {
|
|
121
122
|
cached: {
|
|
@@ -194,25 +195,27 @@ var editItemSchema = {
|
|
|
194
195
|
};
|
|
195
196
|
var TOOL_CONTRACTS = [
|
|
196
197
|
{
|
|
197
|
-
name: "
|
|
198
|
-
title: "
|
|
199
|
-
description: TOOL_DESCRIPTIONS.
|
|
198
|
+
name: "digest",
|
|
199
|
+
title: "Digest source",
|
|
200
|
+
description: TOOL_DESCRIPTIONS.digest,
|
|
200
201
|
inputSchema: {
|
|
201
202
|
type: "object",
|
|
202
203
|
properties: {
|
|
203
204
|
path: { type: "string", description: PARAM_DESCRIPTIONS.path },
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
205
|
+
select: { type: "string", description: PARAM_DESCRIPTIONS.select },
|
|
206
|
+
at: { type: "string", description: PARAM_DESCRIPTIONS.at },
|
|
207
|
+
end: { type: "integer", minimum: 1, description: PARAM_DESCRIPTIONS.end },
|
|
208
|
+
limit: { type: "integer", minimum: 1, description: PARAM_DESCRIPTIONS.limit },
|
|
209
|
+
language: { type: "string", description: PARAM_DESCRIPTIONS.languageOverride },
|
|
210
|
+
visionMode: {
|
|
208
211
|
type: "string",
|
|
209
|
-
enum: [
|
|
210
|
-
description: PARAM_DESCRIPTIONS.
|
|
212
|
+
enum: [...VISION_MODES],
|
|
213
|
+
description: PARAM_DESCRIPTIONS.visionModeDigest
|
|
211
214
|
},
|
|
212
|
-
|
|
215
|
+
visionLevel: {
|
|
213
216
|
type: "string",
|
|
214
|
-
enum: [...
|
|
215
|
-
description: PARAM_DESCRIPTIONS.
|
|
217
|
+
enum: [...VISION_LEVELS],
|
|
218
|
+
description: PARAM_DESCRIPTIONS.visionLevel
|
|
216
219
|
}
|
|
217
220
|
},
|
|
218
221
|
required: ["path"],
|
|
@@ -286,34 +289,6 @@ var TOOL_CONTRACTS = [
|
|
|
286
289
|
additionalProperties: false
|
|
287
290
|
}
|
|
288
291
|
},
|
|
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
292
|
{
|
|
318
293
|
name: "view",
|
|
319
294
|
title: "View document",
|
|
@@ -774,7 +749,7 @@ var MAX_DOCUMENT_OUTPUT_LINES = 2000;
|
|
|
774
749
|
var TOOL_POLICY = [
|
|
775
750
|
"ReadSeek tool policy:",
|
|
776
751
|
"- Prefer readseek_* tools over built-ins when they can do the job.",
|
|
777
|
-
"- Read with
|
|
752
|
+
"- Read with readseek_digest first; readseek_edit needs fresh LINE:HASH anchors.",
|
|
778
753
|
"- Use readseek_grep for text/regex, readseek_search for AST patterns, and readseek_digest/def/refs for symbol navigation.",
|
|
779
754
|
"- Use readseek_edit for existing files, readseek_write for whole-file creation or replacement, and readseek_rename for symbol renames.",
|
|
780
755
|
"- Do not use built-in read, edit, write, grep, or apply_patch when a ReadSeek tool can do the job.",
|
|
@@ -786,7 +761,6 @@ function openCodeToolDescription(name) {
|
|
|
786
761
|
return renderToolDescription(name, OPENCODE_TOOL_ALIASES);
|
|
787
762
|
}
|
|
788
763
|
var PREFERRED_TOOL_DESCRIPTIONS = {
|
|
789
|
-
readseek_read: "Preferred file reader; returns LINE:HASH anchors for durable references.",
|
|
790
764
|
readseek_edit: "Preferred tool for editing existing text files with fresh LINE:HASH anchors.",
|
|
791
765
|
readseek_write: "Preferred tool for creating or replacing a complete text file; returns LINE:HASH anchors.",
|
|
792
766
|
readseek_rename: "Use to rename a symbol with binding-aware, verified edits.",
|
|
@@ -798,7 +772,6 @@ var PREFERRED_TOOL_DESCRIPTIONS = {
|
|
|
798
772
|
readseek_view: "Use to digest document structure or selected content."
|
|
799
773
|
};
|
|
800
774
|
var FALLBACK_TOOL_DESCRIPTIONS = {
|
|
801
|
-
read: "Fallback only. Prefer readseek_read so edits can use LINE:HASH anchors.",
|
|
802
775
|
edit: "Fallback only. Prefer readseek_edit with fresh LINE:HASH anchors.",
|
|
803
776
|
write: "Fallback only. Prefer readseek_write so the result returns LINE:HASH anchors.",
|
|
804
777
|
grep: "Fallback only. Prefer readseek_grep for text/regex search with edit-ready anchors.",
|
|
@@ -816,46 +789,6 @@ function resolveImagePolicy(options) {
|
|
|
816
789
|
return value;
|
|
817
790
|
throw new Error('opencode-readseek imageMode must be "on", "auto", or "off"');
|
|
818
791
|
}
|
|
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
792
|
function digestArgs(filePath, select, extra = []) {
|
|
860
793
|
return [...buildDigestArgs({ path: filePath, select }), ...extra];
|
|
861
794
|
}
|
|
@@ -866,10 +799,6 @@ function requireDigestFacet(envelope, facet) {
|
|
|
866
799
|
}
|
|
867
800
|
return value;
|
|
868
801
|
}
|
|
869
|
-
function digestMetadata(envelope) {
|
|
870
|
-
return record(record(envelope).metadata);
|
|
871
|
-
}
|
|
872
|
-
|
|
873
802
|
class SessionAnchors {
|
|
874
803
|
#pathsBySession = new Map;
|
|
875
804
|
#renamePlans = new Map;
|
|
@@ -1062,12 +991,6 @@ async function authorizeEdit(context, filePaths, diff = "", authorizeExternalPat
|
|
|
1062
991
|
function withAt(args, line, column) {
|
|
1063
992
|
args.push("--at", buildLineLocation(line, column));
|
|
1064
993
|
}
|
|
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
994
|
function readRangeArgs(filePath, start, end) {
|
|
1072
995
|
const extra = [];
|
|
1073
996
|
if (start > 1)
|
|
@@ -1366,43 +1289,6 @@ function boundDocumentOutput(value) {
|
|
|
1366
1289
|
return truncated ? `${output}
|
|
1367
1290
|
[… document view truncated; narrow it with page, node, kind, or depth]` : output;
|
|
1368
1291
|
}
|
|
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
1292
|
function previewDiffLine(prefix, line) {
|
|
1407
1293
|
const value = `${prefix}${line}`;
|
|
1408
1294
|
const maxBytes = 256;
|
|
@@ -1651,100 +1537,6 @@ var ReadSeekPlugin = async (_input, options) => {
|
|
|
1651
1537
|
const imageModes = imagePolicy === "auto" ? ["none", "all", "ocr", "caption", "objects"] : ["all", "ocr", "caption", "objects"];
|
|
1652
1538
|
return {
|
|
1653
1539
|
tool: {
|
|
1654
|
-
readseek_read: readseekTool(imagePolicy === "off" ? `${openCodeToolDescription("read")} Image reads are disabled.` : `${openCodeToolDescription("read")} Available image modes: ${imageModes.join(", ")}.`, {
|
|
1655
|
-
path: tool.schema.string().describe("Path relative to the project directory"),
|
|
1656
|
-
offset: tool.schema.number().int().positive().optional().describe("One-based starting line"),
|
|
1657
|
-
limit: tool.schema.number().int().positive().optional().describe(`Maximum number of lines; defaults to ${DEFAULT_READ_LIMIT}`),
|
|
1658
|
-
language: tool.schema.string().optional().describe("Language override when auto-detection is ambiguous"),
|
|
1659
|
-
...imagePolicy === "off" ? {} : {
|
|
1660
|
-
image: tool.schema.enum(imageModes).optional().describe("Standalone image processing mode")
|
|
1661
|
-
}
|
|
1662
|
-
}, "read", async (input, context) => {
|
|
1663
|
-
const filePath = resolvePath(context.directory, input.path);
|
|
1664
|
-
await authorizeRead(context, filePath);
|
|
1665
|
-
const image = input.image;
|
|
1666
|
-
if (imagePolicy === "off" && image !== undefined)
|
|
1667
|
-
throw new Error("image reads are disabled");
|
|
1668
|
-
if (imagePolicy === "on" && image === "none")
|
|
1669
|
-
throw new Error('image="none" requires imageMode="auto"');
|
|
1670
|
-
if (isLikelyDocumentPath(filePath)) {
|
|
1671
|
-
if (image !== undefined)
|
|
1672
|
-
throw new Error("image does not apply to documents; use readseek_view");
|
|
1673
|
-
if (input.offset !== undefined || input.limit !== undefined) {
|
|
1674
|
-
throw new Error("offset and limit do not apply to documents; use readseek_view");
|
|
1675
|
-
}
|
|
1676
|
-
if (input.language !== undefined) {
|
|
1677
|
-
throw new Error("language does not apply to documents; use readseek_view");
|
|
1678
|
-
}
|
|
1679
|
-
return digestDocumentReceipt(context, filePath);
|
|
1680
|
-
}
|
|
1681
|
-
let detectedText = false;
|
|
1682
|
-
if (image === undefined && isLikelyImagePath(filePath)) {
|
|
1683
|
-
const envelope2 = await runReadSeek(context, digestArgs(filePath, "metadata"));
|
|
1684
|
-
const metadata2 = digestMetadata(envelope2);
|
|
1685
|
-
if (isPdfFile(metadata2))
|
|
1686
|
-
return documentReceipt(filePath, envelope2);
|
|
1687
|
-
if (isVisualFile(metadata2)) {
|
|
1688
|
-
return { file: filePath, skipped: true, reason: "image mode not selected" };
|
|
1689
|
-
}
|
|
1690
|
-
detectedText = true;
|
|
1691
|
-
}
|
|
1692
|
-
const extra = [];
|
|
1693
|
-
const wantsImage = !detectedText && (image !== undefined || isLikelyImagePath(filePath));
|
|
1694
|
-
if (!wantsImage) {
|
|
1695
|
-
const start = input.offset ?? 1;
|
|
1696
|
-
const end = start + (input.limit ?? DEFAULT_READ_LIMIT) - 1;
|
|
1697
|
-
if (start > 1)
|
|
1698
|
-
withAt(extra, start);
|
|
1699
|
-
extra.push("--end", String(end));
|
|
1700
|
-
if (input.language)
|
|
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);
|
|
1711
|
-
}
|
|
1712
|
-
let envelope;
|
|
1713
|
-
try {
|
|
1714
|
-
envelope = await runReadSeek(context, digestArgs(filePath, "content", extra), image !== undefined && image !== "none" ? { timeoutMs: DEFAULT_VISION_TIMEOUT_MS } : {});
|
|
1715
|
-
} catch (error) {
|
|
1716
|
-
if (image !== undefined)
|
|
1717
|
-
throw error;
|
|
1718
|
-
if (isDocumentBoundsError(error)) {
|
|
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
|
-
}
|
|
1729
|
-
}
|
|
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
|
-
}
|
|
1740
|
-
if (visual && (input.offset !== undefined || input.limit !== undefined)) {
|
|
1741
|
-
throw new Error("offset and limit do not apply to image reads");
|
|
1742
|
-
}
|
|
1743
|
-
if (visual && input.language !== undefined) {
|
|
1744
|
-
throw new Error("language does not apply to image reads");
|
|
1745
|
-
}
|
|
1746
|
-
return requireDigestFacet(envelope, "content");
|
|
1747
|
-
}, formatReadResult),
|
|
1748
1540
|
readseek_view: readseekTool(openCodeToolDescription("view"), {
|
|
1749
1541
|
path: tool.schema.string().describe("Document path relative to the project directory"),
|
|
1750
1542
|
node: tool.schema.string().min(1).optional().describe(PARAM_DESCRIPTIONS.node),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-readseek",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.6",
|
|
4
4
|
"description": "OpenCode plugin for anchored file operations and structural code navigation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"node": ">=20.0.0"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@jarkkojs/readseek": "^0.9.
|
|
36
|
+
"@jarkkojs/readseek": "^0.9.6",
|
|
37
37
|
"@opencode-ai/plugin": "^1.17.20"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|