opencode-readseek 0.12.0 → 0.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -68,9 +68,14 @@ is retained separately in `metadata.readseek`; image attachments are unchanged.
68
68
  references and renames use line or hash cursors. KDL selection/check rules
69
69
  may be inline or `@path`; tool calls cannot supply standard input.
70
70
 
71
- `readseek_grep` accepts exactly one of literal `text` or regex `pattern`.
72
- The adapter also supports `literal`, `ignoreCase`, `glob`, `context`, `limit`,
73
- and per-file `summary`. Native `grep` searches literally.
71
+ `readseek_grep` requires `pattern`: native UTF-8 RE2 regex by default, or exact
72
+ text with `literal: true`. Lookaround and backreferences are unsupported.
73
+ Native ReadSeek handles `ignoreCase`, `glob`, `context`, `summary`, and
74
+ `scope: "symbol"` grouping. `scopeContext` requires symbol scope. The positive
75
+ `limit` defaults to 100 matching lines, not occurrences. `gitScope` accepts
76
+ `tracked`, `untracked`, `ignored`, or `all` (default: tracked plus untracked).
77
+ Source hits carry editable `LINE:HASH` anchors; document hits carry node/page
78
+ selectors and do not mark files as editable.
74
79
 
75
80
  Read fresh `LINE:HASH` anchors before editing. Each edit item contains exactly
76
81
  one nested `set_line`, `replace_lines`, `insert_after`, `replace_symbol`, or
package/dist/index.js CHANGED
@@ -43,7 +43,7 @@ var PARAM_DESCRIPTIONS = {
43
43
  pattern: "AST pattern (ast-grep style), such as console.log($$$ARGS)",
44
44
  patternContext: "Surrounding code context for sub-statement patterns",
45
45
  selector: "Target AST node kind to extract from pattern context",
46
- grepPattern: "Regex pattern; set literal for exact text",
46
+ grepPattern: "Native regex pattern; set literal for exact text. Empty regex matches every line; empty literal is invalid.",
47
47
  name: "Qualified or unqualified symbol name",
48
48
  refsName: "Identifier to find references for",
49
49
  gitScope: "In a Git repository, which files to search: tracked, untracked, ignored (untracked plus ignored), or all. Defaults to tracked plus untracked.",
@@ -60,7 +60,6 @@ var PARAM_DESCRIPTIONS = {
60
60
  kind: "Restrict structure to this node kind",
61
61
  outline: "Show outline of document hierarchy or source symbols",
62
62
  noAnchors: "Omit source line anchors",
63
- text: "Literal native text query; adapter grep also supports regex via pattern",
64
63
  edits: "Use set_line, replace_lines, insert_after, replace_symbol, or replace",
65
64
  editVariant: "Exactly one nested edit variant",
66
65
  setLine: 'Replace one line: {"set_line":{"anchor":"LINE:HASH","new_text":"..."}}',
@@ -92,9 +91,10 @@ var PARAM_DESCRIPTIONS = {
92
91
  literal: "Treat pattern literally",
93
92
  context: "Surrounding lines for each match",
94
93
  matchLimit: "Maximum matches to return",
94
+ grepLimit: "Maximum matching lines to return; positive integer, defaults to 100",
95
95
  summary: "Return per-file counts",
96
96
  grepScope: "Group matches by enclosing symbol",
97
- scopeContext: "Context lines within each symbol"
97
+ scopeContext: "Non-negative context lines within each symbol; requires scope: symbol"
98
98
  };
99
99
  var TOOL_DESCRIPTIONS = {
100
100
  show: "Read source, document content, or image descriptors without inference. Obtain fresh `LINE:HASH` anchors before edits; use at with a column for cursor context. Document results include typed nodes and cached asset paths.",
@@ -107,7 +107,7 @@ var TOOL_DESCRIPTIONS = {
107
107
  objects: "Explicitly infer object labels and bounding boxes using an installed model.",
108
108
  ocr: "Explicitly recognize text from pixels using an installed model; reading never silently invokes OCR.",
109
109
  search: "Find AST nodes by pattern or KDL selection rule. Returns edit-ready anchors. Use `grep` for text and regex.",
110
- grep: "Search text or regex and return edit-ready `LINE:HASH` anchors. Native CLI text is literal; adapters preserve regex, glob, context, summary, and symbol grouping.",
110
+ grep: "Search with native regex or literal matching. Returns matching-line counts, source LINE:HASH anchors, and document node-local locations. Native glob, context, summary, limits, Git scope, and symbol grouping apply; default limit is 100 matching lines.",
111
111
  def: "Find a symbol definition or declaration by qualified or unqualified name.",
112
112
  refs: "Find identifier-name matches with name, or binding-scoped references with at. Use before renaming or deleting a symbol.",
113
113
  edit: "Edit existing files with fresh `LINE:HASH` anchors. Each edits item contains exactly one set_line, replace_lines, insert_after, replace_symbol, or replace object. Preview before applying a verified plan.",
@@ -215,7 +215,7 @@ var TOOL_CONTRACTS = [
215
215
  tool("objects", "Detect objects", { ...path, ...at, ...stdin, ...level }, ["path"]),
216
216
  tool("ocr", "Recognize text", { ...path, ...at, ...stdin, ...level }, ["path"]),
217
217
  tool("search", "Search AST nodes", { ...path, ...lang, ...stdin, ...limit, ...gitScopeSchema, ...noAnchors, pattern: string(PARAM_DESCRIPTIONS.pattern), context: string(PARAM_DESCRIPTIONS.patternContext), selector: string(PARAM_DESCRIPTIONS.selector), rule: string(PARAM_DESCRIPTIONS.rule) }, []),
218
- tool("grep", "Search text", { ...path, ...limit, ...gitScopeSchema, text: string(PARAM_DESCRIPTIONS.text), pattern: string(PARAM_DESCRIPTIONS.grepPattern), glob: string(PARAM_DESCRIPTIONS.glob), ignoreCase: { type: "boolean", description: PARAM_DESCRIPTIONS.ignoreCase }, literal: { type: "boolean", description: PARAM_DESCRIPTIONS.literal }, context: integer(PARAM_DESCRIPTIONS.context, 0), summary: { type: "boolean", description: PARAM_DESCRIPTIONS.summary }, scope: { type: "string", enum: ["symbol"], description: PARAM_DESCRIPTIONS.grepScope }, scopeContext: integer(PARAM_DESCRIPTIONS.scopeContext, 0) }, []),
218
+ tool("grep", "Search text", { path: string(PARAM_DESCRIPTIONS.searchPath), limit: { ...integer(PARAM_DESCRIPTIONS.grepLimit), default: 100 }, ...gitScopeSchema, pattern: string(PARAM_DESCRIPTIONS.grepPattern), glob: string(PARAM_DESCRIPTIONS.glob), ignoreCase: { type: "boolean", description: PARAM_DESCRIPTIONS.ignoreCase }, literal: { type: "boolean", description: PARAM_DESCRIPTIONS.literal }, context: integer(PARAM_DESCRIPTIONS.context, 0), summary: { type: "boolean", description: PARAM_DESCRIPTIONS.summary }, scope: { type: "string", enum: ["symbol"], description: PARAM_DESCRIPTIONS.grepScope }, scopeContext: integer(PARAM_DESCRIPTIONS.scopeContext, 0) }, ["pattern"]),
219
219
  tool("def", "Find definitions", { ...path, ...lang, ...gitScopeSchema, ...noAnchors, name: string(PARAM_DESCRIPTIONS.name) }, ["name"]),
220
220
  tool("refs", "Find references", { ...path, ...at, ...lang, ...gitScopeSchema, ...noAnchors, name: string(PARAM_DESCRIPTIONS.refsName) }, []),
221
221
  tool("edit", "Hash-verified edit", { ...path, ...lang, ...gitScopeSchema, ...apply, edits: { type: "array", minItems: 1, items: editItemSchema, description: PARAM_DESCRIPTIONS.edits }, postEditVerify: { type: "boolean", description: PARAM_DESCRIPTIONS.postEditVerify } }, ["path", "edits"]),
@@ -234,7 +234,7 @@ var PREFERENCE_LEADS = {
234
234
  show: "Preferred for reading source with anchors.",
235
235
  edit: "Preferred for editing existing text files with fresh LINE:HASH anchors.",
236
236
  write: "Preferred for creating or replacing a complete text file; returns LINE:HASH anchors.",
237
- grep: "Preferred text/regex search; returns edit-ready LINE:HASH anchors.",
237
+ grep: "Preferred native text/regex search; returns edit-ready source LINE:HASH anchors and document node-local locations.",
238
238
  search: "Preferred for AST or structural code search with ast-grep-style patterns.",
239
239
  def: "Preferred for go-to-definition or declaration lookup by name.",
240
240
  refs: "Preferred for finding identifier-name matches or binding-specific references before `rename` or delete.",
@@ -245,7 +245,7 @@ var FALLBACK_TEMPLATES = {
245
245
  read: "Prefer `show` with fresh LINE:HASH anchors.",
246
246
  edit: "Prefer `edit` with fresh LINE:HASH anchors.",
247
247
  write: "Prefer `write` so the result returns LINE:HASH anchors.",
248
- grep: "Prefer `grep` for text/regex search with edit-ready anchors.",
248
+ grep: "Prefer `grep` for native text/regex search with source anchors and document locations.",
249
249
  apply_patch: "Prefer `edit` for existing text files."
250
250
  };
251
251
  function resolveToolName(name, aliases = {}) {
@@ -316,6 +316,59 @@ function validateReferencesSelection(input) {
316
316
  return { ok: false, error: "exactly one of name or at is required" };
317
317
  return { ok: true };
318
318
  }
319
+ function validateGrepParams(input) {
320
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
321
+ return { ok: false, error: "grep params must be an object" };
322
+ }
323
+ const params = input;
324
+ const allowed = [
325
+ "pattern",
326
+ "path",
327
+ "glob",
328
+ "ignoreCase",
329
+ "literal",
330
+ "context",
331
+ "limit",
332
+ "summary",
333
+ "scope",
334
+ "scopeContext",
335
+ "gitScope",
336
+ "signal",
337
+ "stdin",
338
+ "timeoutMs",
339
+ "cancelable"
340
+ ];
341
+ if (Object.keys(params).some((key) => !allowed.includes(key)))
342
+ return { ok: false, error: "unknown grep parameter" };
343
+ if (typeof params.pattern !== "string")
344
+ return { ok: false, error: "grep requires a pattern string" };
345
+ if (params.literal === true && params.pattern.length === 0)
346
+ return { ok: false, error: "literal pattern must not be empty" };
347
+ for (const key of ["path", "glob"]) {
348
+ if (params[key] !== undefined && (typeof params[key] !== "string" || params[key].length === 0)) {
349
+ return { ok: false, error: `${key} must be a non-empty string` };
350
+ }
351
+ }
352
+ for (const key of ["ignoreCase", "literal", "summary"]) {
353
+ if (params[key] !== undefined && typeof params[key] !== "boolean")
354
+ return { ok: false, error: `${key} must be a boolean` };
355
+ }
356
+ for (const key of ["context", "limit", "scopeContext"]) {
357
+ const value = params[key];
358
+ const minimum = key === "limit" ? 1 : 0;
359
+ if (value !== undefined && (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum)) {
360
+ return { ok: false, error: `${key} must be a ${minimum === 1 ? "positive" : "non-negative"} integer` };
361
+ }
362
+ }
363
+ if (params.scope !== undefined && params.scope !== "symbol")
364
+ return { ok: false, error: "scope must be symbol" };
365
+ if (params.scopeContext !== undefined && params.scope !== "symbol")
366
+ return { ok: false, error: "scopeContext requires scope" };
367
+ if (params.gitScope !== undefined && !GIT_SCOPES.includes(params.gitScope)) {
368
+ return { ok: false, error: "invalid gitScope" };
369
+ }
370
+ return { ok: true };
371
+ }
319
372
  var VARIANT_FIELDS = {
320
373
  set_line: ["anchor", "new_text"],
321
374
  replace_lines: ["start_anchor", "end_anchor", "new_text"],
@@ -645,11 +698,29 @@ function buildSearchArgs(input) {
645
698
  return finish(args);
646
699
  }
647
700
  function buildGrepArgs(input) {
648
- const args = start("grep", input);
649
- args.push("--text", input.text);
650
- limit2(args, input.limit);
701
+ const validated = validateGrepParams(input);
702
+ if (!validated.ok)
703
+ throw new Error(validated.error);
704
+ const args = ["grep", "--kdl", `--pattern=${input.pattern}`];
705
+ const needsPathTerminator = input.path !== undefined && input.path.startsWith("-") && input.path !== "-";
706
+ if (input.path !== undefined && !needsPathTerminator)
707
+ args.push(input.path);
708
+ if (input.literal)
709
+ args.push("--literal");
710
+ if (input.ignoreCase)
711
+ args.push("--ignore-case");
712
+ if (input.glob !== undefined)
713
+ args.push(`--glob=${input.glob}`);
714
+ pushOptionalNumber(args, "--context", input.context);
715
+ limit2(args, input.limit ?? 100);
716
+ if (input.summary)
717
+ args.push("--summary");
718
+ pushOptionalString(args, "--scope", input.scope);
719
+ pushOptionalNumber(args, "--scope-context", input.scopeContext);
651
720
  appendGitScopeArgs(args, input);
652
- return finish(args);
721
+ if (input.path !== undefined && needsPathTerminator)
722
+ args.push("--", input.path);
723
+ return args;
653
724
  }
654
725
  function buildDefArgs(input) {
655
726
  const args = start("def", input);
@@ -704,6 +775,17 @@ function buildRenameArgs(input) {
704
775
  // ../readseek-api/src/client-output.ts
705
776
  import { Type } from "@sinclair/typebox";
706
777
  import { AssertError, Value } from "@sinclair/typebox/value";
778
+ var OPERATIONS = ["Clone", "Clean", "Default", "Assert"];
779
+ function parse(schema, value, what) {
780
+ try {
781
+ return Value.Parse(OPERATIONS, schema, value);
782
+ } catch (error) {
783
+ if (!(error instanceof AssertError))
784
+ throw error;
785
+ const detail = error.error;
786
+ throw new Error(`invalid readseek ${what}${detail?.path ?? ""}: ${detail?.message ?? "invalid value"}`);
787
+ }
788
+ }
707
789
  var Int = Type.Integer({ minimum: Number.MIN_SAFE_INTEGER, maximum: Number.MAX_SAFE_INTEGER });
708
790
  var Str = Type.String();
709
791
  var Bool = Type.Boolean();
@@ -938,6 +1020,82 @@ var DefEnvelope = Type.Object({
938
1020
  qualified_name: OptStr
939
1021
  }))
940
1022
  });
1023
+ function parseReadOutput(value) {
1024
+ return parse(ReadOutput, value, "read output");
1025
+ }
1026
+ function parseStructureOutput(value) {
1027
+ return parse(StructureOutput, value, "structure output");
1028
+ }
1029
+ function parseSearchOutput(value) {
1030
+ const envelope = parse(SearchEnvelope, value, "search output");
1031
+ const results = envelope.results;
1032
+ if (envelope.returned_matches !== undefined)
1033
+ results.returned_matches = envelope.returned_matches;
1034
+ if (envelope.truncated !== undefined)
1035
+ results.truncated = envelope.truncated;
1036
+ if (envelope.byte_basis !== undefined)
1037
+ results.byte_basis = envelope.byte_basis;
1038
+ return results;
1039
+ }
1040
+ function parseRefsOutput(value) {
1041
+ return parse(RefsEnvelope, value, "refs output").references.map(({ symbol, ...reference }) => ({
1042
+ ...reference,
1043
+ enclosingSymbol: symbol?.qualified_name
1044
+ }));
1045
+ }
1046
+ function parseCheckOutput(value) {
1047
+ const output = parse(CheckEnvelope, value, "check output");
1048
+ return {
1049
+ file_hash: output.file_hash,
1050
+ errorCount: output.error_count,
1051
+ missingCount: output.missing_count,
1052
+ diagnostics: output.diagnostics
1053
+ };
1054
+ }
1055
+ function parseDetectOutput(value) {
1056
+ const output = value ?? {};
1057
+ if (output.type === "application/pdf") {
1058
+ return { kind: "pdf", ...parse(PdfDetectionInput, value, "detect output") };
1059
+ }
1060
+ if (output.width !== undefined || output.height !== undefined) {
1061
+ return { kind: "image", ...parse(ImageDetectionInput, value, "detect output") };
1062
+ }
1063
+ if (output.language !== undefined) {
1064
+ return { kind: "source", ...parse(SourceDetectionInput, value, "detect output") };
1065
+ }
1066
+ return { kind: "text", ...parse(TextDetectionInput, value, "detect output") };
1067
+ }
1068
+ function parseTextContent(value) {
1069
+ const output = value ?? {};
1070
+ if (output.hashlines === undefined || output.start_line !== undefined)
1071
+ return parseReadOutput(value);
1072
+ const spanned = parse(UnspannedContent, value, "content");
1073
+ return {
1074
+ ...spanned,
1075
+ start_line: spanned.hashlines[0]?.line ?? 1,
1076
+ end_line: spanned.hashlines[spanned.hashlines.length - 1]?.line ?? 0
1077
+ };
1078
+ }
1079
+ function parseCursorOutput(value) {
1080
+ return parse(CursorEnvelope, value, "cursor output");
1081
+ }
1082
+ function parsePdfMetadata(value) {
1083
+ if (!value || typeof value !== "object")
1084
+ return;
1085
+ if (value.format !== "pdf")
1086
+ return;
1087
+ const document = parse(PdfMetadata, value, "inspect metadata");
1088
+ return { kind: "pdf", type: "application/pdf", file: document.source, format: "pdf", pages: document.pages };
1089
+ }
1090
+ function parseEditOutput(value) {
1091
+ return parse(EditOutputSchema, value, "edit output");
1092
+ }
1093
+ function parseRenameOutput(value) {
1094
+ return parse(RenameOutputSchema, value, "rename output");
1095
+ }
1096
+ function parseDefOutput(value) {
1097
+ return parse(DefEnvelope, value, "def output").locations;
1098
+ }
941
1099
  var Bbox = Type.Object({ x: Type.Number(), y: Type.Number(), width: Type.Number(), height: Type.Number() });
942
1100
  var Position = { page: OptInt, destination: OptStr, bbox: Type.Optional(Bbox) };
943
1101
  var Provenance = { inferred: Type.Optional(Bool), model: OptStr, level: OptStr, task: OptStr };
@@ -994,25 +1152,41 @@ var DocumentOutput = Type.Object({
994
1152
  assets: Type.Array(DocumentAsset, { default: [] }),
995
1153
  vision: Type.Array(DocumentVision, { default: [] })
996
1154
  });
997
- var TextHit = Type.Object({
998
- line: Int,
999
- column: Int,
1155
+ var GrepCount = Type.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER });
1156
+ var GrepPosition = Type.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER });
1157
+ var GrepAnchor = Type.String({ pattern: "^[1-9][0-9]*:[0-9a-f]{6}$" });
1158
+ var GrepLineSchema = Type.Object({
1159
+ line: GrepPosition,
1160
+ hash: Type.String({ pattern: "^[0-9a-f]{6}$" }),
1000
1161
  text: Str,
1001
- start_line: Int,
1002
- end_line: Int,
1003
- start_byte: Int,
1004
- end_byte: Int,
1005
- hashlines: Hashlines
1162
+ kind: Type.Union([Type.Literal("match"), Type.Literal("context")])
1006
1163
  });
1007
- var DocumentHit = Type.Object({ node_id: Str, kind: Str, snippet: Str, page: OptInt });
1008
- var TextSearchOutput = Type.Object({
1009
- text: Str,
1010
- mode: Type.Literal("literal"),
1011
- total_matches: Int,
1012
- returned_matches: Int,
1164
+ var GrepHitSchema = Type.Object({ line: GrepPosition, column: GrepPosition, start_byte: GrepCount, end_byte: GrepCount, text: Str, anchor: GrepAnchor });
1165
+ var GrepSymbolSchema = Type.Object({ name: Str, kind: Str, start_line: GrepPosition, end_line: GrepPosition, lines: Type.Array(GrepLineSchema) });
1166
+ var GrepSourceResultSchema = Type.Object({
1167
+ file: Str,
1168
+ file_hash: Str,
1169
+ language: Str,
1170
+ total_matches: GrepCount,
1171
+ returned_matches: GrepCount,
1172
+ hits: Type.Array(GrepHitSchema),
1173
+ lines: Type.Array(GrepLineSchema),
1174
+ symbols: Type.Array(GrepSymbolSchema)
1175
+ });
1176
+ var GrepDocumentHitSchema = Type.Object({ node_id: Str, kind: Str, snippet: Str, line: GrepPosition, page: Type.Optional(GrepPosition) });
1177
+ var GrepDocumentResultSchema = Type.Object({ file: Str, content_hash: Str, total_matches: GrepCount, returned_matches: GrepCount, hits: Type.Array(GrepDocumentHitSchema) });
1178
+ var GrepOutputSchema = Type.Object({
1179
+ pattern: Str,
1180
+ mode: Type.Union([Type.Literal("regex"), Type.Literal("literal")]),
1181
+ ignore_case: Bool,
1182
+ summary: Bool,
1183
+ total_matches: GrepCount,
1184
+ returned_matches: GrepCount,
1013
1185
  truncated: Bool,
1014
- results: Type.Array(Type.Object({ file: Str, file_hash: Str, matches: Type.Array(TextHit) })),
1015
- documents: Type.Array(Type.Object({ file: Str, content_hash: Str, hits: Type.Array(DocumentHit) }))
1186
+ count_unit: Type.Literal("lines"),
1187
+ scope: Type.Optional(Type.Literal("symbol")),
1188
+ results: Type.Array(GrepSourceResultSchema),
1189
+ documents: Type.Array(GrepDocumentResultSchema)
1016
1190
  });
1017
1191
  var RuleFinding = Type.Object({
1018
1192
  name: Str,
@@ -1033,7 +1207,72 @@ var RulesOutput = Type.Object({
1033
1207
  violation_count: Int,
1034
1208
  diagnostics: Type.Array(RuleFinding)
1035
1209
  });
1210
+ function parseDocumentOutput(value) {
1211
+ return parse(DocumentOutput, value, "document output");
1212
+ }
1213
+ function parseContentOutput(value) {
1214
+ const output = value;
1215
+ if (output?.format !== undefined)
1216
+ return parseDocumentOutput(value);
1217
+ if (!output || output.metadata === undefined || output.content === undefined)
1218
+ throw new Error("invalid readseek content: missing metadata or content");
1219
+ const metadata = parseDetectOutput(output.metadata);
1220
+ return {
1221
+ metadata,
1222
+ content: metadata.kind === "image" ? parse(ImageDescriptor, output.content, "image descriptor") : parseTextContent(output.content),
1223
+ ...output.cursor !== undefined ? { cursor: parseCursorOutput(output.cursor) } : {}
1224
+ };
1225
+ }
1226
+ function parseStructureEnvelope(value) {
1227
+ const output = value;
1228
+ if (output?.format !== undefined)
1229
+ return parseDocumentOutput(value);
1230
+ if (!output || output.structure === undefined)
1231
+ throw new Error("invalid readseek structure: missing structure");
1232
+ return { ...output.metadata !== undefined ? { metadata: parseDetectOutput(output.metadata) } : {}, structure: parseStructureOutput(output.structure) };
1233
+ }
1234
+ function parseGrepOutput(value) {
1235
+ const output = parse(GrepOutputSchema, value, "grep output");
1236
+ const invalid = (message) => {
1237
+ throw new Error(`invalid readseek grep output: ${message}`);
1238
+ };
1239
+ const groups = [...output.results, ...output.documents];
1240
+ if (output.returned_matches > output.total_matches || groups.reduce((sum, group) => sum + group.total_matches, 0) !== output.total_matches || groups.reduce((sum, group) => sum + group.returned_matches, 0) !== output.returned_matches)
1241
+ invalid("inconsistent counts");
1242
+ for (const group of groups) {
1243
+ if (group.total_matches === 0 || group.returned_matches > group.total_matches)
1244
+ invalid("inconsistent file counts");
1245
+ if (output.summary ? group.hits.length !== 0 : group.hits.length !== group.returned_matches)
1246
+ invalid("inconsistent hit count");
1247
+ }
1248
+ for (const result of output.results) {
1249
+ if (output.summary && (result.lines.length > 0 || result.symbols.length > 0))
1250
+ invalid("summary contains display entries");
1251
+ if (output.scope !== "symbol" && result.symbols.length > 0)
1252
+ invalid("symbols require symbol scope");
1253
+ for (const hit of result.hits) {
1254
+ if (hit.end_byte < hit.start_byte || Number(hit.anchor.split(":")[0]) !== hit.line)
1255
+ invalid("invalid hit span or anchor");
1256
+ }
1257
+ for (const symbol of result.symbols) {
1258
+ if (symbol.end_line < symbol.start_line || symbol.lines.some((line) => line.line < symbol.start_line || line.line > symbol.end_line))
1259
+ invalid("invalid symbol range");
1260
+ }
1261
+ }
1262
+ return output;
1263
+ }
1264
+ function parseRulesOutput(value) {
1265
+ return parse(RulesOutput, value, "rules output");
1266
+ }
1036
1267
  var SourceAssetsOutput = Type.Object({ file: OptStr, metadata: Type.Optional(Detection), assets: Type.Array(DocumentAsset, { default: [] }) });
1268
+ function parseAssetsOutput(value) {
1269
+ const raw = value;
1270
+ if (raw?.format !== undefined)
1271
+ return parseDocumentOutput(value);
1272
+ if (!raw || raw.file === undefined && raw.metadata === undefined)
1273
+ throw new Error("invalid readseek assets output: missing file or metadata");
1274
+ return parse(SourceAssetsOutput, { ...raw, ...raw.metadata !== undefined ? { metadata: parseDetectOutput(raw.metadata) } : {} }, "assets output");
1275
+ }
1037
1276
 
1038
1277
  // ../readseek-api/src/edit-plan.ts
1039
1278
  import { Document, Node, format } from "@bgotink/kdl";
@@ -1075,6 +1314,81 @@ function serializeEditPlan(edits) {
1075
1314
  throw new Error(validation.error);
1076
1315
  return format(new Document([node("plan", {}, validation.edits.map(editNode))]));
1077
1316
  }
1317
+
1318
+ // ../readseek-api/src/client.ts
1319
+ function runOptions(options, vision = false) {
1320
+ return {
1321
+ signal: options.signal,
1322
+ stdin: options.stdin,
1323
+ timeoutMs: options.timeoutMs,
1324
+ cancelable: options.cancelable,
1325
+ ...vision ? { vision: true } : {}
1326
+ };
1327
+ }
1328
+ function createReadSeekClient(runner) {
1329
+ return {
1330
+ async show(path, options = {}) {
1331
+ const value = await runner.kdl(buildShowArgs({ ...options, path }), runOptions(options));
1332
+ return parseContentOutput(value);
1333
+ },
1334
+ async tree(path, options = {}) {
1335
+ const value = await runner.kdl(buildTreeArgs({ ...options, path }), runOptions(options));
1336
+ return parseStructureEnvelope(value);
1337
+ },
1338
+ async meta(path, options = {}) {
1339
+ const value = await runner.kdl(buildMetaArgs({ ...options, path }), runOptions(options));
1340
+ return parsePdfMetadata(value) ?? parseDetectOutput(value?.metadata);
1341
+ },
1342
+ async assets(path, options = {}) {
1343
+ const value = await runner.kdl(buildAssetsArgs({ ...options, path }), runOptions(options));
1344
+ return parseAssetsOutput(value);
1345
+ },
1346
+ async syntax(path, options = {}) {
1347
+ const value = await runner.kdl(buildSyntaxArgs({ ...options, path }), runOptions(options));
1348
+ return parseCheckOutput(value?.diagnostics ?? value);
1349
+ },
1350
+ async check(path, options) {
1351
+ const value = await runner.kdl(buildCheckArgs({ ...options, path }), runOptions(options));
1352
+ return parseRulesOutput(value);
1353
+ },
1354
+ async caption(path, options = {}) {
1355
+ const value = await runner.kdl(buildCaptionArgs({ ...options, path }), runOptions(options, true));
1356
+ return parseContentOutput(value);
1357
+ },
1358
+ async objects(path, options = {}) {
1359
+ const value = await runner.kdl(buildObjectsArgs({ ...options, path }), runOptions(options, true));
1360
+ return parseContentOutput(value);
1361
+ },
1362
+ async ocr(path, options = {}) {
1363
+ const value = await runner.kdl(buildOcrArgs({ ...options, path }), runOptions(options, true));
1364
+ return parseContentOutput(value);
1365
+ },
1366
+ async search(path, options) {
1367
+ const value = await runner.kdl(buildSearchArgs({ ...options, path }), runOptions(options));
1368
+ return parseSearchOutput(value);
1369
+ },
1370
+ async grep(params) {
1371
+ const value = await runner.kdl(buildGrepArgs(params), runOptions(params));
1372
+ return parseGrepOutput(value);
1373
+ },
1374
+ async def(path, options) {
1375
+ const value = await runner.kdl(buildDefArgs({ ...options, path }), runOptions(options));
1376
+ return parseDefOutput(value);
1377
+ },
1378
+ async refs(path, options) {
1379
+ const value = await runner.kdl(buildRefsArgs({ ...options, path }), runOptions(options));
1380
+ return parseRefsOutput(value);
1381
+ },
1382
+ async edit(path, edits, options = {}) {
1383
+ const stdin = serializeEditPlan(edits);
1384
+ return parseEditOutput(await runner.kdl(buildEditArgs({ ...options, path, requestFromStdin: true }), runOptions({ ...options, stdin })));
1385
+ },
1386
+ async rename(path, options) {
1387
+ const value = await runner.kdl(buildRenameArgs({ ...options, path }), runOptions(options));
1388
+ return parseRenameOutput(value);
1389
+ }
1390
+ };
1391
+ }
1078
1392
  // ../readseek-api/src/show-presentation.ts
1079
1393
  function isShowTool(name) {
1080
1394
  return name === "show" || name === "tree" || name === "meta" || name === "assets";
@@ -1290,6 +1604,31 @@ function formatShowResult(presentation) {
1290
1604
 
1291
1605
  `);
1292
1606
  }
1607
+ // ../readseek-api/src/grep-presentation.ts
1608
+ function renderGrepOutput(output) {
1609
+ const rendered = [`${output.returned_matches} of ${output.total_matches} matching lines${output.truncated ? " (truncated)" : ""}`];
1610
+ const displayLine = (line) => `${line.line}:${line.hash}|${line.kind === "match" ? ">>" : " "}${line.text}`;
1611
+ for (const result of output.results) {
1612
+ rendered.push(`${result.file}: ${result.total_matches} matching lines (${result.returned_matches} returned)`);
1613
+ if (output.summary)
1614
+ continue;
1615
+ rendered.push(...result.lines.map(displayLine));
1616
+ for (const symbol of result.symbols) {
1617
+ rendered.push(` ${symbol.kind} ${symbol.name} (${symbol.start_line}-${symbol.end_line})`);
1618
+ rendered.push(...symbol.lines.map(displayLine));
1619
+ }
1620
+ }
1621
+ for (const document of output.documents) {
1622
+ rendered.push(`${document.file}: ${document.total_matches} matching lines (${document.returned_matches} returned)`);
1623
+ if (output.summary)
1624
+ continue;
1625
+ for (const hit of document.hits) {
1626
+ rendered.push(` node:${hit.node_id}${hit.page === undefined ? "" : ` page:${hit.page}`} line:${hit.line} (${hit.kind}): ${hit.snippet}`);
1627
+ }
1628
+ }
1629
+ return rendered.join(`
1630
+ `);
1631
+ }
1293
1632
  // ../readseek-api/src/platform.ts
1294
1633
  import { accessSync, constants as fsConstants, existsSync, statSync } from "node:fs";
1295
1634
  import { createRequire } from "node:module";
@@ -1391,7 +1730,7 @@ function cleanReadSeekErrorText(stderr) {
1391
1730
  return stderr.replace(READSEEK_USAGE_HINT, "").replace(/^error:\s*/i, "").replace(/\s*\n\s*/g, " ").trim();
1392
1731
  }
1393
1732
  // ../readseek-api/src/kdl.ts
1394
- import { parse } from "@bgotink/kdl";
1733
+ import { parse as parse2 } from "@bgotink/kdl";
1395
1734
  function convertNode(node) {
1396
1735
  if (node.getTag() !== null)
1397
1736
  throw new Error("KDL node annotations are not supported");
@@ -1419,7 +1758,7 @@ function convertNode(node) {
1419
1758
  };
1420
1759
  }
1421
1760
  function parseKdlDocument(text) {
1422
- const document = parse(text);
1761
+ const document = parse2(text);
1423
1762
  if (document.nodes.length !== 1)
1424
1763
  throw new Error("KDL document must contain exactly one root node");
1425
1764
  return convertNode(document.nodes[0]);
@@ -1553,6 +1892,46 @@ function search(root) {
1553
1892
  }))
1554
1893
  };
1555
1894
  }
1895
+ function grepChildren(node, allowed) {
1896
+ if (node.children.some((child) => !allowed.includes(child.name))) {
1897
+ throw new Error(`invalid readseek KDL grep: unexpected child of ${node.name}`);
1898
+ }
1899
+ }
1900
+ function grepLine(node) {
1901
+ grepChildren(node, []);
1902
+ const parsed = line(node);
1903
+ if (requireProperty(node, "number") !== parsed.line)
1904
+ throw new Error("invalid readseek KDL grep line number");
1905
+ return { ...parsed, kind: requireProperty(node, "kind") };
1906
+ }
1907
+ function grep(root) {
1908
+ grepChildren(root, ["match", "document"]);
1909
+ return parseGrepOutput({
1910
+ ...properties(root),
1911
+ results: children(root, "match").map((match) => {
1912
+ grepChildren(match, ["hit", "line", "symbol"]);
1913
+ return {
1914
+ ...properties(match),
1915
+ hits: children(match, "hit").map((hit) => {
1916
+ grepChildren(hit, []);
1917
+ return properties(hit);
1918
+ }),
1919
+ lines: children(match, "line").map(grepLine),
1920
+ symbols: children(match, "symbol").map((symbol) => {
1921
+ grepChildren(symbol, ["line"]);
1922
+ return { ...properties(symbol), lines: children(symbol, "line").map(grepLine) };
1923
+ })
1924
+ };
1925
+ }),
1926
+ documents: children(root, "document").map((document) => {
1927
+ grepChildren(document, ["hit"]);
1928
+ return { ...properties(document), hits: children(document, "hit").map((hit) => {
1929
+ grepChildren(hit, []);
1930
+ return properties(hit);
1931
+ }) };
1932
+ })
1933
+ });
1934
+ }
1556
1935
  function references(root) {
1557
1936
  const parsed = children(root, "reference").map((reference) => {
1558
1937
  const sourceLine = child(reference, "line");
@@ -1668,7 +2047,7 @@ function parseReadSeekKdl(text) {
1668
2047
  case "document":
1669
2048
  return root.properties.has("format") ? pdfDocument(root) : sourceDocument(root);
1670
2049
  case "search":
1671
- return search(root);
2050
+ return ["mode", "ignore_case", "summary", "count_unit"].some((key) => root.properties.has(key)) ? grep(root) : search(root);
1672
2051
  case "check":
1673
2052
  return diagnostics(root);
1674
2053
  case "refs":
@@ -1867,8 +2246,8 @@ function collectAnchoredFiles(value, files) {
1867
2246
  if (!value || typeof value !== "object")
1868
2247
  return;
1869
2248
  const output = value;
1870
- const hashlines = output.hashlines;
1871
- if (typeof output.file === "string" && Array.isArray(hashlines) && hashlines.some((line) => typeof record2(line).line === "number" && typeof record2(line).hash === "string"))
2249
+ const lineGroups = [output.hashlines, output.lines, ...items(output.symbols).map((symbol) => record2(symbol).lines)];
2250
+ if (typeof output.file === "string" && lineGroups.some((lines) => items(lines).some((line) => typeof record2(line).line === "number" && typeof record2(line).hash === "string")))
1872
2251
  files.add(output.file);
1873
2252
  for (const item of Object.values(output))
1874
2253
  collectAnchoredFiles(item, files);
@@ -1981,6 +2360,12 @@ async function runReadSeek(context, args, options = {}) {
1981
2360
  timeoutMs: options.timeoutMs
1982
2361
  }));
1983
2362
  }
2363
+ function readSeekClient(context) {
2364
+ return createReadSeekClient({
2365
+ text: (args, options) => runReadSeekRaw(context, [...args], options),
2366
+ kdl: (args, options) => runReadSeek(context, [...args], options)
2367
+ });
2368
+ }
1984
2369
  async function runContent(context, args, options = {}) {
1985
2370
  return requireContent(await runReadSeek(context, args, options));
1986
2371
  }
@@ -2104,7 +2489,6 @@ ${[...paths].sort().map((filePath) => `- ${filePath}`).join(`
2104
2489
  }
2105
2490
 
2106
2491
  // src/tools.ts
2107
- import { readFile as readFile2, stat as stat3 } from "node:fs/promises";
2108
2492
  import { tool as tool3 } from "@opencode-ai/plugin";
2109
2493
 
2110
2494
  // src/constants.ts
@@ -2448,7 +2832,7 @@ function initialTitle(kind, args) {
2448
2832
  case "search":
2449
2833
  return `Find nodes ${args.pattern ?? args.context ?? args.rule}`;
2450
2834
  case "grep":
2451
- return `Find text ${args.pattern ?? args.text}`;
2835
+ return `Find text ${args.pattern}`;
2452
2836
  case "def":
2453
2837
  return `Find definition ${args.name}`;
2454
2838
  case "refs":
@@ -2480,12 +2864,9 @@ function resultPresentation(kind, args, value) {
2480
2864
  return { title: `Found ${matches} matches`, metadata: { results: results.length, matches } };
2481
2865
  }
2482
2866
  case "grep": {
2483
- const results = items(output.results);
2484
- const matches = results.reduce((total, result) => {
2485
- const entry = record2(result);
2486
- return total + (typeof entry.count === "number" ? entry.count : items(entry.matches).length);
2487
- }, 0);
2488
- return { title: `Found ${matches} matches`, metadata: { results: results.length, matches } };
2867
+ const results = items(output.results).length + items(output.documents).length;
2868
+ const matches = output.total_matches;
2869
+ return { title: `Found ${matches} matches`, metadata: { results, matches } };
2489
2870
  }
2490
2871
  case "def": {
2491
2872
  const locations = items(output.locations).length;
@@ -2543,8 +2924,8 @@ function readseekTool(description, args, kind, execute, imagePolicy) {
2543
2924
  return {
2544
2925
  ...attachments.length > 0 ? { attachments } : {},
2545
2926
  title: presentation.title,
2546
- output: isShowTool(kind) ? formatShowResult(presentShowResult(kind, displayResult, args)) : render(displayResult),
2547
- metadata: { ...presentation.metadata, ...isShowTool(kind) ? { readseek: result } : {} }
2927
+ output: isShowTool(kind) ? formatShowResult(presentShowResult(kind, displayResult, args)) : kind === "grep" ? renderGrepOutput(result) : render(displayResult),
2928
+ metadata: { ...presentation.metadata, ...isShowTool(kind) || kind === "grep" ? { readseek: result } : {} }
2548
2929
  };
2549
2930
  }
2550
2931
  });
@@ -2555,104 +2936,35 @@ function createReadSeekTools(imagePolicy) {
2555
2936
  return {
2556
2937
  ...createAccessTools(imagePolicy),
2557
2938
  readseek_grep: readseekTool(openCodeToolDescription("grep"), {
2558
- pattern: tool3.schema.string().optional().describe(PARAM_DESCRIPTIONS.grepPattern),
2559
- text: tool3.schema.string().optional().describe(PARAM_DESCRIPTIONS.text),
2939
+ pattern: tool3.schema.string().describe(PARAM_DESCRIPTIONS.grepPattern),
2560
2940
  summary: tool3.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.summary),
2561
2941
  path: tool3.schema.string().optional().describe(PARAM_DESCRIPTIONS.projectSearchPath),
2562
2942
  glob: tool3.schema.string().optional().describe(PARAM_DESCRIPTIONS.glob),
2563
2943
  literal: tool3.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.literal),
2564
2944
  ignoreCase: tool3.schema.boolean().optional().describe(PARAM_DESCRIPTIONS.ignoreCase),
2565
2945
  context: tool3.schema.number().int().nonnegative().optional().describe(PARAM_DESCRIPTIONS.context),
2566
- limit: tool3.schema.number().int().positive().optional().describe(PARAM_DESCRIPTIONS.matchLimit)
2946
+ limit: tool3.schema.number().int().positive().optional().describe(PARAM_DESCRIPTIONS.matchLimit),
2947
+ scope: tool3.schema.literal("symbol").optional().describe(PARAM_DESCRIPTIONS.grepScope),
2948
+ scopeContext: tool3.schema.number().int().nonnegative().optional().describe(PARAM_DESCRIPTIONS.scopeContext),
2949
+ gitScope: tool3.schema.enum(GIT_SCOPES).optional().describe(PARAM_DESCRIPTIONS.gitScope)
2567
2950
  }, "grep", async (input, context) => {
2568
2951
  const target = resolvePath(context.directory, input.path ?? ".");
2569
- if (input.pattern === undefined === (input.text === undefined)) {
2570
- throw new Error("exactly one of pattern or text is required");
2571
- }
2572
- const pattern = input.pattern ?? input.text;
2952
+ const pattern = input.pattern;
2573
2953
  await authorizeSearch(context, target, pattern);
2574
- if (input.text !== undefined && !input.glob && !input.ignoreCase && !input.context && !input.summary) {
2575
- return runReadSeek(context, buildGrepArgs({ path: target, text: pattern, limit: input.limit }));
2576
- }
2577
- let expression;
2578
- try {
2579
- const source = input.literal || input.text !== undefined ? pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : pattern;
2580
- expression = new RegExp(source, input.ignoreCase ? "i" : "");
2581
- } catch (error) {
2582
- throw new Error(`invalid regular expression: ${error instanceof Error ? error.message : String(error)}`);
2583
- }
2584
- const info = await stat3(target);
2585
- const files = [];
2586
- if (info.isFile())
2587
- files.push(target);
2588
- else if (info.isDirectory()) {
2589
- const glob = new Bun.Glob(input.glob ?? "**/*");
2590
- for await (const filePath of glob.scan({ cwd: target, absolute: true, onlyFiles: true, followSymlinks: false })) {
2591
- files.push(filePath);
2592
- }
2593
- } else
2594
- throw new Error(`grep target is not a file or directory: ${target}`);
2595
- const maxMatches = input.limit ?? 100;
2596
- const contextLines = input.context ?? 0;
2597
- const results = [];
2598
- let totalMatches = 0;
2599
- let truncated = false;
2600
- for (const filePath of files.sort()) {
2601
- context.abort.throwIfAborted();
2602
- if (truncated)
2603
- break;
2604
- const buffer = await readFile2(filePath);
2605
- if (buffer.includes(0))
2606
- continue;
2607
- const lines = buffer.toString("utf8").replace(/\r\n/g, `
2608
- `).split(`
2609
- `);
2610
- const matchLines = new Set;
2611
- const ranges = [];
2612
- for (let index = 0;index < lines.length; index++) {
2613
- expression.lastIndex = 0;
2614
- if (!expression.test(lines[index] ?? ""))
2615
- continue;
2616
- if (totalMatches === maxMatches) {
2617
- truncated = true;
2618
- break;
2619
- }
2620
- const line = index + 1;
2621
- matchLines.add(line);
2622
- ranges.push([Math.max(1, line - contextLines), Math.min(lines.length, line + contextLines)]);
2623
- totalMatches++;
2624
- }
2625
- if (ranges.length === 0)
2626
- continue;
2627
- if (input.summary) {
2628
- results.push({ file: filePath, count: matchLines.size });
2629
- continue;
2630
- }
2631
- const mergedRanges = [];
2632
- for (const [start, end] of ranges) {
2633
- const previous = mergedRanges.at(-1);
2634
- if (previous && start <= previous[1] + 1)
2635
- previous[1] = Math.max(previous[1], end);
2636
- else
2637
- mergedRanges.push([start, end]);
2638
- }
2639
- const linesByNumber = new Map;
2640
- for (const [start, end] of mergedRanges) {
2641
- const output = record2(await runContent(context, readRangeArgs(filePath, start, end)));
2642
- for (const hashline of items(output.hashlines)) {
2643
- const line = record2(hashline).line;
2644
- if (typeof line === "number" && !linesByNumber.has(line))
2645
- linesByNumber.set(line, hashline);
2646
- }
2647
- }
2648
- const hashlines = [...linesByNumber.values()];
2649
- const matches = hashlines.filter((hashline) => {
2650
- const line = record2(hashline).line;
2651
- return typeof line === "number" && matchLines.has(line);
2652
- });
2653
- results.push({ file: filePath, matches, hashlines });
2654
- }
2655
- return { results, truncated };
2954
+ return readSeekClient(context).grep({
2955
+ pattern,
2956
+ path: target,
2957
+ glob: input.glob,
2958
+ ignoreCase: input.ignoreCase,
2959
+ literal: input.literal,
2960
+ context: input.context,
2961
+ limit: input.limit,
2962
+ summary: input.summary,
2963
+ scope: input.scope,
2964
+ scopeContext: input.scopeContext,
2965
+ gitScope: input.gitScope,
2966
+ signal: context.abort
2967
+ });
2656
2968
  }),
2657
2969
  readseek_search: readseekTool(openCodeToolDescription("search"), {
2658
2970
  pattern: tool3.schema.string().optional().describe(PARAM_DESCRIPTIONS.pattern),
@@ -2951,7 +3263,7 @@ readseek edit file.ts @edits.kdl --kdl
2951
3263
  readseek rename file.ts --at line:10:3 nextName --kdl
2952
3264
  \`\`\`
2953
3265
 
2954
- Native \`grep\` is literal, not regex. Reference selection requires either a symbol \`name\` or \`--at\`; rename requires a line or hash cursor. Preview mutations first, then use \`--apply --plan-hash HASH\` with the returned plan hash. Edit requests are KDL: for example, \`set_line anchor="10:HASH" new_text="replacement"\`.
3266
+ Native \`grep\` uses UTF-8 RE2 regex by default; add \`--literal\` for exact text. Its positive \`--limit\` defaults to 100 matching lines, not occurrences. Reference selection requires either a symbol \`name\` or \`--at\`; rename requires a line or hash cursor. Preview mutations first, then use \`--apply --plan-hash HASH\` with the returned plan hash. Edit requests are KDL: for example, \`set_line anchor="10:HASH" new_text="replacement"\`.
2955
3267
 
2956
3268
  Syntax and rule checks return exit 0 when clean, 1 for findings, and 2 on failure. Exit 1 is not a transport failure. Syntax checks are parser checks, not type checking.
2957
3269
 
package/dist/v2.js CHANGED
@@ -26,7 +26,7 @@ readseek edit file.ts @edits.kdl --kdl
26
26
  readseek rename file.ts --at line:10:3 nextName --kdl
27
27
  \`\`\`
28
28
 
29
- Native \`grep\` is literal, not regex. Reference selection requires either a symbol \`name\` or \`--at\`; rename requires a line or hash cursor. Preview mutations first, then use \`--apply --plan-hash HASH\` with the returned plan hash. Edit requests are KDL: for example, \`set_line anchor="10:HASH" new_text="replacement"\`.
29
+ Native \`grep\` uses UTF-8 RE2 regex by default; add \`--literal\` for exact text. Its positive \`--limit\` defaults to 100 matching lines, not occurrences. Reference selection requires either a symbol \`name\` or \`--at\`; rename requires a line or hash cursor. Preview mutations first, then use \`--apply --plan-hash HASH\` with the returned plan hash. Edit requests are KDL: for example, \`set_line anchor="10:HASH" new_text="replacement"\`.
30
30
 
31
31
  Syntax and rule checks return exit 0 when clean, 1 for findings, and 2 on failure. Exit 1 is not a transport failure. Syntax checks are parser checks, not type checking.
32
32
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-readseek",
3
- "version": "0.12.0",
3
+ "version": "0.12.2",
4
4
  "description": "OpenCode plugin for anchored file operations and structural code navigation",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -33,8 +33,8 @@
33
33
  "node": ">=20.0.0"
34
34
  },
35
35
  "dependencies": {
36
- "@jarkkojs/readseek": "^0.12.0",
37
- "@jarkkojs/readseek-api": "^0.12.0",
36
+ "@jarkkojs/readseek": "^0.12.2",
37
+ "@jarkkojs/readseek-api": "^0.12.2",
38
38
  "@opencode-ai/plugin": "^1.18.18"
39
39
  },
40
40
  "devDependencies": {