pi-readseek 0.9.8 → 0.9.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +5 -5
  2. package/dist/index.ts +1563 -1413
  3. package/package.json +5 -5
package/dist/index.ts CHANGED
@@ -403,11 +403,6 @@ var TOOL_CONTRACTS = [
403
403
  ];
404
404
  // ../readseek-api/src/policy.ts
405
405
  var REPLACEABLE_TOOLS = [
406
- {
407
- tool: "digest",
408
- builtIn: "read",
409
- benefit: "it returns LINE:HASH anchors for safe edits."
410
- },
411
406
  {
412
407
  tool: "edit",
413
408
  builtIn: "edit",
@@ -661,6 +656,7 @@ function buildRenameArgs(input) {
661
656
  return appendGitSelectionArgs(args, input);
662
657
  }
663
658
  // ../readseek-api/src/platform.ts
659
+ import { accessSync, constants as fsConstants } from "node:fs";
664
660
  import { createRequire } from "node:module";
665
661
  import path from "node:path";
666
662
  var READSEEK_PLATFORM_PACKAGES = {
@@ -694,6 +690,19 @@ function resolveReadSeekBinaryFromPackages(resolver = resolveFromPackage, option
694
690
  });
695
691
  return path.join(path.dirname(packageJsonPath), "bin", readSeekBinaryName(platform));
696
692
  }
693
+ function resolveReadSeekBinaryFromPath(pathEnv = process.env.PATH) {
694
+ const name = readSeekBinaryName();
695
+ for (const dir of (pathEnv ?? "").split(path.delimiter)) {
696
+ if (!dir)
697
+ continue;
698
+ const candidate = path.join(dir, name);
699
+ try {
700
+ accessSync(candidate, fsConstants.X_OK);
701
+ return candidate;
702
+ } catch {}
703
+ }
704
+ return null;
705
+ }
697
706
  // ../readseek-api/src/json.ts
698
707
  var READSEEK_USAGE_HINT = /\n\s*Run readseek(?: [\w-]+)* --help for more information\.?\s*$/;
699
708
  function cleanReadSeekErrorText(stderr) {
@@ -854,1367 +863,1695 @@ async function runReadSeekNodeText(binaryPath, args, options = {}) {
854
863
  async function runReadSeekNodeJson(binaryPath, args, options = {}) {
855
864
  return parseSuccessfulJson(await runReadSeekNode(binaryPath, args, options));
856
865
  }
857
- // src/tool-prompt-metadata.ts
858
- var REPLACEABLE_BY_TOOL = new Map(REPLACEABLE_TOOLS.map((entry) => [entry.tool, entry]));
859
- function toolNameFromPrompt(fileName) {
860
- const name = fileName.replace(/\.md$/, "");
861
- if (!TOOL_NAMES.includes(name))
862
- throw new Error(`unknown ReadSeek tool prompt: ${fileName}`);
863
- return name;
866
+ // ../readseek-api/src/client.ts
867
+ function classifyReadSeekFailure(err) {
868
+ const failure = err;
869
+ const message = String(failure?.message ?? err);
870
+ const missing = failure?.code === "ENOENT" || /Cannot find package|Cannot find module|no such file/i.test(message);
871
+ if (missing) {
872
+ return { code: "readseek-not-installed", message, hint: "Run npm install to install @jarkkojs/readseek." };
873
+ }
874
+ return { code: "readseek-execution-error", message };
864
875
  }
865
- function promptFileName(promptUrl) {
866
- return promptUrl.pathname.split("/").pop() ?? "";
876
+ function requireNumber(value, field) {
877
+ if (typeof value !== "number" || !Number.isSafeInteger(value))
878
+ throw new Error(`invalid readseek ${field}: expected safe integer`);
879
+ return value;
867
880
  }
868
- function rewriteRegisteredToolNames(value, toolAliases) {
869
- if (!toolAliases)
870
- return value;
871
- return Object.entries(toolAliases).reduce((rewritten, [canonicalName, registeredName]) => rewritten.replaceAll(canonicalName, registeredName), value);
881
+ function requireString(value, field) {
882
+ if (typeof value !== "string")
883
+ throw new Error(`invalid readseek ${field}`);
884
+ return value;
872
885
  }
873
- function descriptionAliases(toolName, registeredName, toolAliases) {
874
- return Object.fromEntries(TOOL_NAMES.map((name) => {
875
- const readSeekName = `readSeek_${name}`;
876
- const alias = name === toolName && registeredName ? registeredName : toolAliases?.[readSeekName] ?? readSeekName;
877
- return [name, alias];
878
- }));
886
+ function requireBoolean(value, field) {
887
+ if (typeof value !== "boolean")
888
+ throw new Error(`invalid readseek ${field}: expected boolean`);
889
+ return value;
879
890
  }
880
- function defineToolPromptMetadata(options) {
881
- const fileName = promptFileName(options.promptUrl);
882
- const toolName = toolNameFromPrompt(fileName);
883
- const replaceable = REPLACEABLE_BY_TOOL.get(toolName);
884
- const defaultReadSeekName = `readSeek_${toolName}`;
885
- const registeredName = options.registeredName ?? (replaceable ? defaultReadSeekName : undefined);
886
- const preferenceGuideline = replaceable && registeredName ? registeredName === defaultReadSeekName ? `Prefer ${registeredName} over ${replaceable.builtIn} when both are available; ${replaceable.benefit}` : `Use ${registeredName}; ${replaceable.benefit}` : undefined;
887
- const aliases = descriptionAliases(toolName, registeredName, options.toolAliases);
891
+ function optionalString(value, field) {
892
+ if (value === undefined || value === null)
893
+ return;
894
+ return requireString(value, field);
895
+ }
896
+ function parseHashline(value, field) {
897
+ if (!value || typeof value !== "object")
898
+ throw new Error(`invalid readseek ${field}`);
899
+ const item = value;
888
900
  return {
889
- description: renderToolDescription(toolName, aliases),
890
- promptSnippet: rewriteRegisteredToolNames(options.promptSnippet, options.toolAliases),
891
- promptGuidelines: [
892
- ...preferenceGuideline ? [preferenceGuideline] : [],
893
- ...renderCompactGuidelines(toolName, aliases)
894
- ]
901
+ line: requireNumber(item.line, `${field}.line`),
902
+ hash: requireString(item.hash, `${field}.hash`),
903
+ text: requireString(item.text, `${field}.text`)
895
904
  };
896
905
  }
897
-
898
- // src/edit-diff.ts
899
- import * as Diff from "diff";
900
-
901
- // src/hashline.ts
902
- import xxhashWasm from "xxhash-wasm";
903
- var HASH_LEN = 3;
904
- var RADIX = 16;
905
- var HASH_MOD = RADIX ** HASH_LEN;
906
- var DICT = Array.from({ length: HASH_MOD }, (_, i) => i.toString(RADIX).padStart(HASH_LEN, "0"));
907
- var HASHLINE_STATE_KEY = Symbol.for("pi-readseek.hashlineState.v1");
908
- function getHashlineState() {
909
- const globalObject = globalThis;
910
- const state = globalObject[HASHLINE_STATE_KEY] ?? {
911
- h32Fn: null,
912
- initPromise: null
913
- };
914
- globalObject[HASHLINE_STATE_KEY] = state;
915
- return state;
906
+ function parseSearchHashlines(value, field) {
907
+ if (!Array.isArray(value))
908
+ throw new Error(`invalid readseek ${field}`);
909
+ return value.map((line) => parseHashline(line, field));
916
910
  }
917
- async function ensureHashInit() {
918
- const state = getHashlineState();
919
- if (state.h32Fn)
920
- return;
921
- if (!state.initPromise) {
922
- state.initPromise = xxhashWasm().then((hasher) => {
923
- state.h32Fn = hasher.h32;
924
- });
925
- }
926
- await state.initPromise;
911
+ function parseReadOutput(value) {
912
+ if (!value || typeof value !== "object")
913
+ throw new Error("invalid readseek read output");
914
+ const output = value;
915
+ const hashlines = output.hashlines;
916
+ if (!Array.isArray(hashlines))
917
+ throw new Error("invalid readseek hashlines");
918
+ return {
919
+ file: requireString(output.file, "file"),
920
+ language: requireString(output.language, "language"),
921
+ line_count: requireNumber(output.line_count, "line_count"),
922
+ file_hash: requireString(output.file_hash, "file_hash"),
923
+ start_line: requireNumber(output.start_line, "start_line"),
924
+ end_line: requireNumber(output.end_line, "end_line"),
925
+ hashlines: hashlines.map((line) => parseHashline(line, "hashline"))
926
+ };
927
927
  }
928
- function xxh32(input) {
929
- const state = getHashlineState();
930
- if (!state.h32Fn)
931
- throw new Error("Hash not initialized — call ensureHashInit() first");
932
- return state.h32Fn(input, 0) >>> 0;
928
+ function parseMapOutput(value) {
929
+ if (!value || typeof value !== "object")
930
+ throw new Error("invalid readseek map output");
931
+ const output = value;
932
+ const symbols = output.symbols;
933
+ if (!Array.isArray(symbols))
934
+ throw new Error("invalid readseek symbols");
935
+ return {
936
+ file: requireString(output.file, "file"),
937
+ language: requireString(output.language, "language"),
938
+ line_count: requireNumber(output.line_count, "line_count"),
939
+ file_hash: requireString(output.file_hash, "file_hash"),
940
+ symbols: symbols.map((symbol) => {
941
+ if (!symbol || typeof symbol !== "object")
942
+ throw new Error("invalid readseek symbol");
943
+ const item = symbol;
944
+ return {
945
+ kind: requireString(item.kind, "symbol.kind"),
946
+ name: requireString(item.name, "symbol.name"),
947
+ qualified_name: requireString(item.qualified_name, "symbol.qualified_name"),
948
+ start_line: requireNumber(item.start_line, "symbol.start_line"),
949
+ end_line: requireNumber(item.end_line, "symbol.end_line"),
950
+ start_hash: requireString(item.start_hash, "symbol.start_hash"),
951
+ end_hash: requireString(item.end_hash, "symbol.end_hash")
952
+ };
953
+ })
954
+ };
933
955
  }
934
- function computeLineHash(line) {
935
- if (line.endsWith("\r"))
936
- line = line.slice(0, -1);
937
- line = line.replace(/\s+/g, "");
938
- return DICT[xxh32(line) % HASH_MOD];
956
+ function parseSearchOutput(value) {
957
+ if (!value || typeof value !== "object")
958
+ throw new Error("invalid readseek search output");
959
+ const output = value;
960
+ if (!Array.isArray(output.results))
961
+ throw new Error("invalid readseek search results");
962
+ return output.results.map((result) => {
963
+ if (!result || typeof result !== "object")
964
+ throw new Error("invalid readseek search result");
965
+ const file = result;
966
+ if (!Array.isArray(file.matches))
967
+ throw new Error("invalid readseek search matches");
968
+ return {
969
+ file: requireString(file.file, "search.file"),
970
+ language: requireString(file.language, "search.language"),
971
+ file_hash: requireString(file.file_hash, "search.file_hash"),
972
+ matches: file.matches.map((match) => {
973
+ if (!match || typeof match !== "object")
974
+ throw new Error("invalid readseek search match");
975
+ const item = match;
976
+ if (!Array.isArray(item.captures))
977
+ throw new Error("invalid readseek search captures");
978
+ return {
979
+ pattern_index: item.pattern_index === undefined ? 0 : requireNumber(item.pattern_index, "search.match.pattern_index"),
980
+ start_line: requireNumber(item.start_line, "search.match.start_line"),
981
+ end_line: requireNumber(item.end_line, "search.match.end_line"),
982
+ start_hash: requireString(item.start_hash, "search.match.start_hash"),
983
+ end_hash: requireString(item.end_hash, "search.match.end_hash"),
984
+ hashlines: parseSearchHashlines(item.hashlines, "search.match.hashlines"),
985
+ captures: item.captures.map((capture) => {
986
+ if (!capture || typeof capture !== "object")
987
+ throw new Error("invalid readseek search capture");
988
+ const captureItem = capture;
989
+ return {
990
+ name: requireString(captureItem.name, "search.capture.name"),
991
+ start_line: requireNumber(captureItem.start_line, "search.capture.start_line"),
992
+ end_line: requireNumber(captureItem.end_line, "search.capture.end_line"),
993
+ start_hash: requireString(captureItem.start_hash, "search.capture.start_hash"),
994
+ end_hash: requireString(captureItem.end_hash, "search.capture.end_hash"),
995
+ hashlines: parseSearchHashlines(captureItem.hashlines, "search.capture.hashlines")
996
+ };
997
+ })
998
+ };
999
+ })
1000
+ };
1001
+ });
939
1002
  }
940
- var DISPLAY_CONTROL_CHAR_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f]/g;
941
- function escapeControlCharsForDisplay(text) {
942
- return text.replace(DISPLAY_CONTROL_CHAR_RE, (ch) => {
943
- return `\\u${ch.charCodeAt(0).toString(16).padStart(4, "0")}`;
1003
+ function parseRefsOutput(value) {
1004
+ if (!value || typeof value !== "object")
1005
+ throw new Error("invalid readseek refs output");
1006
+ const output = value;
1007
+ if (!Array.isArray(output.references))
1008
+ throw new Error("invalid readseek references");
1009
+ return output.references.map((reference) => {
1010
+ if (!reference || typeof reference !== "object")
1011
+ throw new Error("invalid readseek reference");
1012
+ const item = reference;
1013
+ const symbol = item.symbol;
1014
+ const enclosing = symbol && typeof symbol === "object" ? optionalString(symbol.qualified_name, "reference.symbol.qualified_name") : undefined;
1015
+ return {
1016
+ file: requireString(item.file, "reference.file"),
1017
+ line: requireNumber(item.line, "reference.line"),
1018
+ column: requireNumber(item.column, "reference.column"),
1019
+ line_hash: requireString(item.line_hash, "reference.line_hash"),
1020
+ text: requireString(item.text, "reference.text"),
1021
+ enclosingSymbol: enclosing
1022
+ };
944
1023
  });
945
1024
  }
946
- function formatHashlineDisplay(lineNumber, content) {
947
- return `${lineNumber}:${computeLineHash(content)}|${escapeControlCharsForDisplay(content)}`;
1025
+ function parseDiagnosticKind(value) {
1026
+ if (value === "error" || value === "missing")
1027
+ return value;
1028
+ throw new Error("invalid readseek diagnostic.kind");
948
1029
  }
949
-
950
- // src/edit-diff.ts
951
- function normalizeToLF(text) {
952
- return text.replace(/\r\n/g, `
953
- `).replace(/\r/g, `
954
- `);
1030
+ function parseCheckOutput(value) {
1031
+ if (!value || typeof value !== "object")
1032
+ throw new Error("invalid readseek check output");
1033
+ const output = value;
1034
+ if (!Array.isArray(output.diagnostics))
1035
+ throw new Error("invalid readseek diagnostics");
1036
+ return {
1037
+ errorCount: requireNumber(output.error_count, "error_count"),
1038
+ missingCount: requireNumber(output.missing_count, "missing_count"),
1039
+ diagnostics: output.diagnostics.map((diagnostic) => {
1040
+ if (!diagnostic || typeof diagnostic !== "object")
1041
+ throw new Error("invalid readseek diagnostic");
1042
+ const item = diagnostic;
1043
+ return {
1044
+ kind: parseDiagnosticKind(item.kind),
1045
+ start_line: requireNumber(item.start_line, "diagnostic.start_line"),
1046
+ end_line: requireNumber(item.end_line, "diagnostic.end_line")
1047
+ };
1048
+ })
1049
+ };
955
1050
  }
956
- function stripBom(content) {
957
- return content.startsWith("\uFEFF") ? { bom: "\uFEFF", text: content.slice(1) } : { bom: "", text: content };
1051
+ function parseDetectedObjects(value) {
1052
+ if (value === undefined || value === null)
1053
+ return;
1054
+ if (!Array.isArray(value))
1055
+ throw new Error("invalid readseek detect objects");
1056
+ return value.map((object) => {
1057
+ if (!object || typeof object !== "object")
1058
+ throw new Error("invalid readseek detect object");
1059
+ const item = object;
1060
+ const bbox = item.bbox;
1061
+ if (!Array.isArray(bbox) || bbox.length !== 4)
1062
+ throw new Error("invalid readseek detect object.bbox");
1063
+ return {
1064
+ label: requireString(item.label, "object.label"),
1065
+ bbox: bbox.map((n, i) => requireNumber(n, `object.bbox[${i}]`))
1066
+ };
1067
+ });
958
1068
  }
959
- function hasBareCarriageReturn(content) {
960
- return content.replace(/\r\n/g, "").includes("\r");
1069
+ function parseImageEncoding(value) {
1070
+ if (value === "base64")
1071
+ return value;
1072
+ throw new Error("invalid image encoding");
961
1073
  }
962
- function generateDiffString(oldContent, newContent, contextLines = 4) {
963
- const parts = Diff.diffLines(oldContent, newContent);
964
- const output = [];
965
- const maxLineNum = Math.max(oldContent.split(`
966
- `).length, newContent.split(`
967
- `).length);
968
- const lineNumWidth = String(maxLineNum).length;
969
- let oldLineNum = 1;
970
- let newLineNum = 1;
971
- let lastWasChange = false;
972
- let firstChangedLine;
973
- for (let i = 0;i < parts.length; i++) {
974
- const part = parts[i];
975
- const raw = part.value.split(`
976
- `);
977
- if (raw[raw.length - 1] === "")
978
- raw.pop();
979
- if (part.added || part.removed) {
980
- if (firstChangedLine === undefined)
981
- firstChangedLine = newLineNum;
982
- for (const line of raw) {
983
- if (part.added) {
984
- output.push(`+${String(newLineNum).padStart(lineNumWidth, " ")} ${line}`);
985
- newLineNum++;
986
- } else {
987
- output.push(`-${String(oldLineNum).padStart(lineNumWidth, " ")} ${line}`);
988
- oldLineNum++;
989
- }
990
- }
991
- lastWasChange = true;
992
- continue;
993
- }
994
- const nextPartIsChange = i < parts.length - 1 && (parts[i + 1].added || parts[i + 1].removed);
995
- if (lastWasChange || nextPartIsChange) {
996
- let linesToShow = raw;
997
- let skipStart = 0;
998
- let skipEnd = 0;
999
- if (!lastWasChange) {
1000
- skipStart = Math.max(0, raw.length - contextLines);
1001
- linesToShow = raw.slice(skipStart);
1002
- }
1003
- if (!nextPartIsChange && linesToShow.length > contextLines) {
1004
- skipEnd = linesToShow.length - contextLines;
1005
- linesToShow = linesToShow.slice(0, contextLines);
1006
- }
1007
- if (skipStart > 0) {
1008
- output.push(` ${"".padStart(lineNumWidth, " ")} ...`);
1009
- oldLineNum += skipStart;
1010
- newLineNum += skipStart;
1011
- }
1012
- for (const line of linesToShow) {
1013
- output.push(` ${String(oldLineNum).padStart(lineNumWidth, " ")} ${line}`);
1014
- oldLineNum++;
1015
- newLineNum++;
1016
- }
1017
- if (skipEnd > 0) {
1018
- output.push(` ${"".padStart(lineNumWidth, " ")} ...`);
1019
- oldLineNum += skipEnd;
1020
- newLineNum += skipEnd;
1021
- }
1022
- } else {
1023
- oldLineNum += raw.length;
1024
- newLineNum += raw.length;
1025
- }
1026
- lastWasChange = false;
1074
+ function parseDetectOutput(value) {
1075
+ if (!value || typeof value !== "object")
1076
+ throw new Error("invalid readseek detect output");
1077
+ const output = value;
1078
+ const type = requireString(output.type, "type");
1079
+ const file = requireString(output.file, "file");
1080
+ const mime = optionalString(output.mime, "mime");
1081
+ if (type === "application/pdf") {
1082
+ if (output.format !== "pdf")
1083
+ throw new Error("invalid readseek PDF format");
1084
+ return {
1085
+ kind: "pdf",
1086
+ type,
1087
+ file,
1088
+ mime,
1089
+ format: output.format,
1090
+ pages: requireNumber(output.pages, "pages")
1091
+ };
1027
1092
  }
1028
- return { diff: output.join(`
1029
- `), firstChangedLine };
1030
- }
1031
- function generateCompactOrFullDiff(oldContent, newContent, contextLines = 4) {
1032
- if (oldContent === newContent)
1033
- return { diff: "", firstChangedLine: undefined };
1034
- const oldLines = oldContent.split(`
1035
- `);
1036
- const newLines = newContent.split(`
1037
- `);
1038
- if (oldLines.length === newLines.length) {
1039
- let changedIndex = -1;
1040
- let changeCount = 0;
1041
- for (let i = 0;i < oldLines.length; i++) {
1042
- if (oldLines[i] !== newLines[i]) {
1043
- changedIndex = i;
1044
- changeCount++;
1045
- if (changeCount > 1)
1046
- break;
1047
- }
1048
- }
1049
- if (changeCount === 1 && changedIndex >= 0) {
1050
- const lineNum = changedIndex + 1;
1051
- const oldLine = oldLines[changedIndex] ?? "";
1052
- const newLine = newLines[changedIndex] ?? "";
1053
- const oldHash = computeLineHash(oldLine);
1054
- const newHash = computeLineHash(newLine);
1055
- return {
1056
- diff: `${lineNum}:${oldHash}|${oldLine} → ${lineNum}:${newHash}|${newLine}`,
1057
- firstChangedLine: lineNum
1058
- };
1059
- }
1093
+ if (output.width !== undefined || output.height !== undefined) {
1094
+ return {
1095
+ kind: "image",
1096
+ type,
1097
+ file,
1098
+ mime,
1099
+ format: requireString(output.format, "format"),
1100
+ width: requireNumber(output.width, "width"),
1101
+ height: requireNumber(output.height, "height"),
1102
+ animated: requireBoolean(output.animated, "animated"),
1103
+ encoding: output.encoding === undefined ? undefined : parseImageEncoding(output.encoding),
1104
+ data: output.data === undefined ? undefined : requireString(output.data, "data"),
1105
+ ocr: optionalString(output.ocr, "ocr"),
1106
+ caption: optionalString(output.caption, "caption"),
1107
+ objects: parseDetectedObjects(output.objects)
1108
+ };
1060
1109
  }
1061
- if (oldLines.length === newLines.length + 1) {
1062
- let deletedIndex = -1;
1063
- let j = 0;
1064
- let failed = false;
1065
- for (let i = 0;i < oldLines.length; i++) {
1066
- if (j < newLines.length && oldLines[i] === newLines[j]) {
1067
- j++;
1068
- continue;
1069
- }
1070
- if (deletedIndex === -1) {
1071
- deletedIndex = i;
1072
- continue;
1073
- }
1074
- failed = true;
1075
- break;
1076
- }
1077
- if (!failed && deletedIndex !== -1 && j === newLines.length) {
1078
- const lineNum = deletedIndex + 1;
1079
- const oldLine = oldLines[deletedIndex] ?? "";
1080
- const oldHash = computeLineHash(oldLine);
1081
- return {
1082
- diff: `${lineNum}:${oldHash}|${oldLine} → [deleted]`,
1083
- firstChangedLine: lineNum
1084
- };
1085
- }
1110
+ if (output.language !== undefined) {
1111
+ return {
1112
+ kind: "source",
1113
+ type,
1114
+ file,
1115
+ language: requireString(output.language, "language"),
1116
+ engine: optionalString(output.engine, "engine"),
1117
+ supported: requireBoolean(output.supported, "supported"),
1118
+ mime,
1119
+ syntax: optionalString(output.syntax, "syntax")
1120
+ };
1086
1121
  }
1087
- return generateDiffString(oldContent, newContent, contextLines);
1088
- }
1089
-
1090
- // src/path-utils.ts
1091
- import { homedir } from "node:os";
1092
- import { isAbsolute, resolve as resolvePath } from "node:path";
1093
- var UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
1094
- function normalizeUnicodeSpaces(str) {
1095
- return str.replace(UNICODE_SPACES, " ");
1122
+ return { kind: "text", type, file, mime };
1096
1123
  }
1097
- function normalizeAtPrefix(filePath) {
1098
- return filePath.startsWith("@") ? filePath.slice(1) : filePath;
1124
+ function parseDocumentCache(value) {
1125
+ if (!value || typeof value !== "object")
1126
+ throw new Error("invalid readseek document cache");
1127
+ const cache = value;
1128
+ const state = requireString(cache.state, "cache.state");
1129
+ if (state !== "built" && state !== "reused")
1130
+ throw new Error("invalid readseek cache.state");
1131
+ return {
1132
+ content_hash: requireString(cache.content_hash, "cache.content_hash"),
1133
+ state
1134
+ };
1099
1135
  }
1100
- function expandPath(filePath) {
1101
- const normalized = normalizeUnicodeSpaces(normalizeAtPrefix(filePath));
1102
- if (normalized === "~")
1103
- return homedir();
1104
- if (normalized.startsWith("~/"))
1105
- return homedir() + normalized.slice(1);
1106
- return normalized;
1136
+ function parseIdentifyOutput(value) {
1137
+ if (!value || typeof value !== "object")
1138
+ throw new Error("invalid readseek identify output");
1139
+ const output = value;
1140
+ const identifier = output.identifier;
1141
+ const symbol = output.symbol;
1142
+ return {
1143
+ file: requireString(output.file, "file"),
1144
+ language: requireString(output.language, "language"),
1145
+ engine: optionalString(output.engine, "engine"),
1146
+ line_count: requireNumber(output.line_count, "line_count"),
1147
+ file_hash: requireString(output.file_hash, "file_hash"),
1148
+ line: requireNumber(output.line, "line"),
1149
+ column: requireNumber(output.column, "column"),
1150
+ line_hash: requireString(output.line_hash, "line_hash"),
1151
+ identifier: identifier && typeof identifier === "object" ? {
1152
+ text: requireString(identifier.text, "identifier.text"),
1153
+ start_column: requireNumber(identifier.start_column, "identifier.start_column"),
1154
+ end_column: requireNumber(identifier.end_column, "identifier.end_column"),
1155
+ start_byte: requireNumber(identifier.start_byte, "identifier.start_byte"),
1156
+ end_byte: requireNumber(identifier.end_byte, "identifier.end_byte")
1157
+ } : undefined,
1158
+ symbol: symbol && typeof symbol === "object" ? {
1159
+ name: requireString(symbol.name, "symbol.name"),
1160
+ kind: requireString(symbol.kind, "symbol.kind"),
1161
+ qualified_name: requireString(symbol.qualified_name, "symbol.qualified_name"),
1162
+ start_line: requireNumber(symbol.start_line, "symbol.start_line"),
1163
+ end_line: requireNumber(symbol.end_line, "symbol.end_line")
1164
+ } : undefined
1165
+ };
1107
1166
  }
1108
- function resolveToCwd(filePath, cwd) {
1109
- const expanded = expandPath(filePath);
1110
- return isAbsolute(expanded) ? expanded : resolvePath(cwd, expanded);
1167
+ function parseImageContent(value) {
1168
+ const detection = parseDetectOutput(value);
1169
+ if (detection.kind !== "image")
1170
+ throw new Error("invalid readseek image content");
1171
+ return detection;
1111
1172
  }
1112
-
1113
- // src/runtime.ts
1114
- function throwIfAborted(signal) {
1115
- if (signal?.aborted)
1116
- throw new Error("Operation aborted");
1173
+ function parseTextContent(value) {
1174
+ if (!value || typeof value !== "object")
1175
+ throw new Error("invalid readseek content");
1176
+ const output = value;
1177
+ if (output.hashlines !== undefined && output.start_line === undefined) {
1178
+ const hashlines = parseSearchHashlines(output.hashlines, "content.hashlines");
1179
+ const first = hashlines[0];
1180
+ const last = hashlines[hashlines.length - 1];
1181
+ return {
1182
+ file: requireString(output.file, "file"),
1183
+ language: requireString(output.language, "language"),
1184
+ line_count: requireNumber(output.line_count, "line_count"),
1185
+ file_hash: requireString(output.file_hash, "file_hash"),
1186
+ start_line: first?.line ?? 1,
1187
+ end_line: last?.line ?? 0,
1188
+ hashlines
1189
+ };
1190
+ }
1191
+ return parseReadOutput(value);
1117
1192
  }
1118
-
1119
- // src/fs-error.ts
1120
- var SYSLOG_PATH_SUFFIX = /,\s+[a-z]+(\s+'.*')?$/;
1121
- function strerror(err) {
1122
- const raw = String(err?.message ?? String(err));
1123
- return raw.replace(SYSLOG_PATH_SUFFIX, "");
1193
+ function parseDigestEnvelope(value) {
1194
+ if (!value || typeof value !== "object")
1195
+ throw new Error("invalid readseek digest output");
1196
+ const output = value;
1197
+ if (output.metadata === undefined)
1198
+ throw new Error("invalid readseek digest metadata");
1199
+ const result = {
1200
+ metadata: parseDetectOutput(output.metadata)
1201
+ };
1202
+ if (output.cache !== undefined)
1203
+ result.cache = parseDocumentCache(output.cache);
1204
+ if (output.content !== undefined)
1205
+ result.content = output.content;
1206
+ if (output.map !== undefined)
1207
+ result.map = parseMapOutput(output.map);
1208
+ if (output.diagnostics !== undefined)
1209
+ result.diagnostics = parseCheckOutput(output.diagnostics);
1210
+ if (output.identity !== undefined)
1211
+ result.identity = parseIdentifyOutput(output.identity);
1212
+ return result;
1124
1213
  }
1125
- function formatFsError(err, domain) {
1126
- const errno = typeof err.code === "string" ? err.code : "EIO";
1214
+ function parseEditOutput(value) {
1215
+ if (!value || typeof value !== "object")
1216
+ throw new Error("invalid readseek edit output");
1217
+ const output = value;
1127
1218
  return {
1128
- code: errno,
1129
- message: `${domain}: ${errno}: ${strerror(err)}`
1219
+ file: requireString(output.file, "file"),
1220
+ before_hash: requireString(output.before_hash, "before_hash"),
1221
+ file_hash: requireString(output.file_hash, "file_hash"),
1222
+ plan_hash: requireString(output.plan_hash, "plan_hash"),
1223
+ applied: requireBoolean(output.applied, "applied"),
1224
+ changed: requireBoolean(output.changed, "changed"),
1225
+ edit_count: requireNumber(output.edit_count, "edit_count"),
1226
+ before_content: optionalString(output.before_content, "before_content"),
1227
+ content: optionalString(output.content, "content")
1130
1228
  };
1131
1229
  }
1132
-
1133
- // src/edit-render-helpers.ts
1134
- var VARIANT_KEYS = ["set_line", "replace_lines", "insert_after", "replace"];
1135
- function countEditTypes(edits) {
1136
- const counts = {
1137
- set_line: 0,
1138
- replace_lines: 0,
1139
- insert_after: 0,
1140
- replace: 0,
1141
- total: 0
1230
+ function parseRenameConflicts(value, field) {
1231
+ if (value === undefined)
1232
+ return [];
1233
+ if (!Array.isArray(value))
1234
+ throw new Error(`invalid readseek ${field}`);
1235
+ return value.map((item) => {
1236
+ if (!item || typeof item !== "object")
1237
+ throw new Error(`invalid readseek ${field} entry`);
1238
+ const c = item;
1239
+ return {
1240
+ line: requireNumber(c.line, `${field}.line`),
1241
+ column: requireNumber(c.column, `${field}.column`),
1242
+ reason: requireString(c.reason, `${field}.reason`)
1243
+ };
1244
+ });
1245
+ }
1246
+ function parseRenameEdits(value, field) {
1247
+ if (value === undefined)
1248
+ return [];
1249
+ if (!Array.isArray(value))
1250
+ throw new Error(`invalid readseek ${field} entry`);
1251
+ return value.map((item) => {
1252
+ if (!item || typeof item !== "object")
1253
+ throw new Error(`invalid readseek ${field} entry`);
1254
+ const e = item;
1255
+ return {
1256
+ line: requireNumber(e.line, `${field}.line`),
1257
+ start_column: requireNumber(e.start_column, `${field}.start_column`),
1258
+ end_column: requireNumber(e.end_column, `${field}.end_column`),
1259
+ start_byte: requireNumber(e.start_byte, `${field}.start_byte`),
1260
+ end_byte: requireNumber(e.end_byte, `${field}.end_byte`),
1261
+ occurrence: requireString(e.occurrence, `${field}.occurrence`),
1262
+ line_hash: requireString(e.line_hash, `${field}.line_hash`),
1263
+ text: requireString(e.text, `${field}.text`)
1264
+ };
1265
+ });
1266
+ }
1267
+ function parseRenameOutput(value) {
1268
+ if (!value || typeof value !== "object")
1269
+ throw new Error("invalid readseek rename output");
1270
+ const output = value;
1271
+ const others = output.others;
1272
+ if (others !== undefined && !Array.isArray(others))
1273
+ throw new Error("invalid readseek others");
1274
+ return {
1275
+ file: requireString(output.file, "file"),
1276
+ language: requireString(output.language, "language"),
1277
+ engine: optionalString(output.engine, "engine"),
1278
+ file_hash: requireString(output.file_hash, "file_hash"),
1279
+ old_name: requireString(output.old_name, "old_name"),
1280
+ new_name: requireString(output.new_name, "new_name"),
1281
+ applied: requireBoolean(output.applied, "applied"),
1282
+ conflicts: parseRenameConflicts(output.conflicts, "conflicts"),
1283
+ edits: parseRenameEdits(output.edits, "edits"),
1284
+ others: others?.map((entry) => {
1285
+ if (!entry || typeof entry !== "object")
1286
+ throw new Error("invalid readseek other");
1287
+ const o = entry;
1288
+ return {
1289
+ file: requireString(o.file, "other.file"),
1290
+ language: requireString(o.language, "other.language"),
1291
+ engine: optionalString(o.engine, "other.engine"),
1292
+ file_hash: requireString(o.file_hash, "other.file_hash"),
1293
+ conflicts: parseRenameConflicts(o.conflicts, "other.conflicts"),
1294
+ edits: parseRenameEdits(o.edits, "other.edits")
1295
+ };
1296
+ }) ?? []
1142
1297
  };
1143
- if (!edits)
1144
- return counts;
1145
- for (const edit of edits) {
1146
- counts.total++;
1147
- if (edit && typeof edit === "object") {
1148
- for (const key of VARIANT_KEYS) {
1149
- if (key in edit) {
1150
- counts[key]++;
1151
- break;
1152
- }
1153
- }
1154
- }
1155
- }
1156
- return counts;
1157
1298
  }
1158
- function parseDiffStats(diff) {
1159
- if (!diff)
1160
- return { added: 0, removed: 0 };
1161
- let added = 0;
1162
- let removed = 0;
1163
- for (const line of diff.split(`
1164
- `)) {
1165
- if (line.startsWith("+++") || line.startsWith("---"))
1166
- continue;
1167
- if (line.startsWith("+"))
1168
- added++;
1169
- else if (line.startsWith("-"))
1170
- removed++;
1171
- }
1172
- return { added, removed };
1299
+ function parseDefCompact(value) {
1300
+ if (!value || typeof value !== "object")
1301
+ throw new Error("invalid readseek def output");
1302
+ const output = value;
1303
+ const locations = output.locations;
1304
+ if (!Array.isArray(locations))
1305
+ throw new Error("invalid readseek def locations");
1306
+ return locations.map((loc) => {
1307
+ if (!loc || typeof loc !== "object")
1308
+ throw new Error("invalid readseek def location");
1309
+ const item = loc;
1310
+ return {
1311
+ file: requireString(item.file, "location.file"),
1312
+ line: requireNumber(item.line, "location.line"),
1313
+ column: requireNumber(item.column, "location.column"),
1314
+ line_hash: requireString(item.line_hash, "location.line_hash"),
1315
+ text: requireString(item.text, "location.text"),
1316
+ kind: optionalString(item.kind, "location.kind"),
1317
+ name: optionalString(item.name, "location.name"),
1318
+ qualified_name: optionalString(item.qualified_name, "location.qualified_name")
1319
+ };
1320
+ });
1173
1321
  }
1174
- function formatEditCallText(args, argsComplete) {
1175
- const rawPath = typeof args?.path === "string" ? args.path : null;
1176
- if (!argsComplete) {
1177
- return { path: rawPath, suffix: undefined };
1178
- }
1179
- const edits = args?.edits;
1180
- if (!Array.isArray(edits) || edits.length === 0) {
1181
- return { path: rawPath, suffix: undefined };
1322
+ function createReadSeekClient(runner) {
1323
+ function digestArgs(target, options) {
1324
+ return buildDigestArgs({
1325
+ path: target,
1326
+ select: options.select ?? "content",
1327
+ at: options.at,
1328
+ end: options.end,
1329
+ limit: options.limit,
1330
+ language: options.language,
1331
+ visionMode: options.visionMode,
1332
+ visionLevel: options.visionLevel,
1333
+ stdinName: options.stdin !== undefined && target === "-" ? options.stdinName ?? target : undefined
1334
+ });
1182
1335
  }
1183
- const counts = countEditTypes(edits);
1184
- const parts = [];
1185
- for (const key of VARIANT_KEYS) {
1186
- if (counts[key] > 0) {
1187
- parts.push(`${counts[key]} ${key}`);
1336
+ async function runDigestRaw(target, options = {}) {
1337
+ const args = digestArgs(target, options);
1338
+ const runOptions = {
1339
+ signal: options.signal,
1340
+ stdin: options.stdin,
1341
+ timeoutMs: options.timeoutMs,
1342
+ vision: options.vision
1343
+ };
1344
+ return runner.json(args, runOptions);
1345
+ }
1346
+ async function digest(target, options = {}) {
1347
+ return parseDigestEnvelope(await runDigestRaw(target, options));
1348
+ }
1349
+ function requireDigestContent(envelope, field) {
1350
+ if (envelope.content === undefined)
1351
+ throw new Error(`invalid readseek ${field}: missing content`);
1352
+ return envelope.content;
1353
+ }
1354
+ async function digestContent(filePath, options = {}) {
1355
+ const envelope = await digest(filePath, {
1356
+ select: "content",
1357
+ at: options.startLine !== undefined ? buildLineLocation(options.startLine) : undefined,
1358
+ end: options.endLine,
1359
+ visionMode: options.visionMode,
1360
+ signal: options.signal,
1361
+ vision: options.vision
1362
+ });
1363
+ if (envelope.metadata.kind === "pdf") {
1364
+ return { metadata: envelope.metadata, cache: envelope.cache };
1188
1365
  }
1189
- }
1190
- const word = counts.total === 1 ? "edit" : "edits";
1191
- const suffix = `${counts.total} ${word} (${parts.join(", ")})`;
1192
- return { path: rawPath, suffix };
1193
- }
1194
- function formatEditResultText(input) {
1195
- const { isError, diff, warnings, noopEdits, errorText } = input;
1196
- const isNoOp = isError && (Array.isArray(noopEdits) && noopEdits.length > 0 || errorText.includes("No changes made"));
1197
- const stats = parseDiffStats(diff);
1198
- const hasDiffStats = stats.added > 0 || stats.removed > 0;
1199
- const diffStats = hasDiffStats ? `+${stats.added} / -${stats.removed}` : undefined;
1200
- let warningsBadge;
1201
- if (warnings.length === 1) {
1202
- warningsBadge = "⚠ 1 warning";
1203
- } else if (warnings.length > 1) {
1204
- warningsBadge = `⚠ ${warnings.length} warnings`;
1205
- }
1206
- const showErrorText = isError ? errorText : undefined;
1207
- let semanticBadge;
1208
- if (!isError && !isNoOp && input.semanticClassification) {
1209
- switch (input.semanticClassification) {
1210
- case "whitespace-only":
1211
- semanticBadge = "ws-only";
1212
- break;
1213
- case "semantic":
1214
- semanticBadge = "✓ semantic";
1215
- break;
1216
- case "mixed":
1217
- semanticBadge = "mixed";
1218
- break;
1366
+ const content = requireDigestContent(envelope, "content");
1367
+ if (envelope.metadata.kind === "image") {
1368
+ return { metadata: envelope.metadata, image: parseImageContent(content) };
1219
1369
  }
1370
+ return { metadata: envelope.metadata, text: parseTextContent(content) };
1220
1371
  }
1221
1372
  return {
1222
- diffStats,
1223
- noOp: isNoOp,
1224
- warningsBadge,
1225
- errorText: showErrorText,
1226
- semanticBadge
1373
+ async view(filePath, options = {}) {
1374
+ const args = buildViewArgs({
1375
+ path: filePath,
1376
+ node: options.node,
1377
+ page: options.page,
1378
+ kind: options.kind,
1379
+ depth: options.depth,
1380
+ outline: options.outline,
1381
+ visionMode: options.visionMode,
1382
+ visionLevel: options.visionLevel
1383
+ });
1384
+ return runner.text(args, {
1385
+ signal: options.signal,
1386
+ vision: options.visionMode !== undefined
1387
+ });
1388
+ },
1389
+ digestRaw(target, options = {}) {
1390
+ return runDigestRaw(target, options);
1391
+ },
1392
+ digest,
1393
+ digestContent,
1394
+ async image(filePath, modes, options = {}) {
1395
+ const requestedModes = modes.length > 1 ? ["all"] : modes;
1396
+ const results = await Promise.allSettled(requestedModes.map(async (mode) => {
1397
+ const digested = await digestContent(filePath, {
1398
+ visionMode: mode,
1399
+ signal: options.signal,
1400
+ vision: true
1401
+ });
1402
+ if (!digested.image)
1403
+ throw new Error(`readseek returned no image analysis for ${filePath}`);
1404
+ return digested.image;
1405
+ }));
1406
+ let merged;
1407
+ for (const result of results) {
1408
+ if (result.status !== "fulfilled")
1409
+ continue;
1410
+ const detection = result.value;
1411
+ merged = merged === undefined ? detection : {
1412
+ ...merged,
1413
+ ocr: detection.ocr ?? merged.ocr,
1414
+ caption: detection.caption ?? merged.caption,
1415
+ objects: detection.objects ?? merged.objects
1416
+ };
1417
+ }
1418
+ if (merged !== undefined)
1419
+ return merged;
1420
+ const failure = results.find((result) => result.status === "rejected");
1421
+ if (failure?.status === "rejected")
1422
+ throw failure.reason;
1423
+ throw new Error(`readseek returned no image analysis for ${filePath}`);
1424
+ },
1425
+ async preparedImage(filePath, options = {}) {
1426
+ const digested = await digestContent(filePath, {
1427
+ visionMode: "none",
1428
+ signal: options.signal
1429
+ });
1430
+ const output = digested.image;
1431
+ if (!output || output.encoding !== "base64" || output.data === undefined || output.mime === undefined) {
1432
+ throw new Error(`readseek returned no prepared image for ${filePath}`);
1433
+ }
1434
+ return { mime: output.mime, encoding: output.encoding, data: output.data };
1435
+ },
1436
+ async search(target, pattern, options = {}) {
1437
+ const args = buildSearchArgs({
1438
+ path: target,
1439
+ pattern,
1440
+ language: options.language,
1441
+ cached: options.cached,
1442
+ others: options.others,
1443
+ ignored: options.ignored
1444
+ });
1445
+ return parseSearchOutput(await runner.json(args, { signal: options.signal }));
1446
+ },
1447
+ async refs(target, options = {}) {
1448
+ const args = buildRefsArgs({
1449
+ path: target,
1450
+ name: options.name,
1451
+ scope: options.scope,
1452
+ line: options.line,
1453
+ column: options.column,
1454
+ language: options.language,
1455
+ cached: options.cached,
1456
+ others: options.others,
1457
+ ignored: options.ignored
1458
+ });
1459
+ return parseRefsOutput(await runner.json(args, { signal: options.signal }));
1460
+ },
1461
+ async def(target, options) {
1462
+ const args = buildDefArgs({
1463
+ path: target,
1464
+ name: options.name,
1465
+ format: "plain",
1466
+ language: options.language,
1467
+ cached: options.cached,
1468
+ others: options.others,
1469
+ ignored: options.ignored
1470
+ });
1471
+ return parseDefCompact(await runner.json(args, { signal: options.signal }));
1472
+ },
1473
+ async check(filePath, content, options = {}) {
1474
+ const envelope = await digest("-", {
1475
+ select: "diagnostics",
1476
+ stdin: content,
1477
+ stdinName: filePath,
1478
+ signal: options.signal
1479
+ });
1480
+ if (!envelope.diagnostics)
1481
+ throw new Error("invalid readseek diagnostics: missing diagnostics facet");
1482
+ return envelope.diagnostics;
1483
+ },
1484
+ async identify(filePath, content, options = {}) {
1485
+ const envelope = await digest("-", {
1486
+ select: "identity",
1487
+ stdin: content,
1488
+ stdinName: filePath,
1489
+ at: options.line !== undefined ? buildLineLocation(options.line, options.column) : undefined,
1490
+ language: options.language,
1491
+ signal: options.signal
1492
+ });
1493
+ if (!envelope.identity)
1494
+ throw new Error("invalid readseek identity: missing identity facet");
1495
+ return envelope.identity;
1496
+ },
1497
+ async detect(filePath, options = {}) {
1498
+ const envelope = await digest(filePath, { select: "metadata", signal: options.signal });
1499
+ return envelope.metadata;
1500
+ },
1501
+ async edit(filePath, edits, options = {}) {
1502
+ const args = buildEditArgs({
1503
+ path: filePath,
1504
+ apply: options.apply,
1505
+ planHash: options.planHash,
1506
+ language: options.language
1507
+ });
1508
+ return parseEditOutput(await runner.json(args, {
1509
+ signal: options.signal,
1510
+ stdin: JSON.stringify({ edits }),
1511
+ timeoutMs: options.timeoutMs,
1512
+ cancelable: options.cancelable
1513
+ }));
1514
+ },
1515
+ async rename(filePath, options) {
1516
+ const args = buildRenameArgs({
1517
+ path: filePath,
1518
+ to: options.to,
1519
+ line: options.line,
1520
+ column: options.column,
1521
+ workspace: options.workspace,
1522
+ apply: options.apply,
1523
+ language: options.language,
1524
+ cached: options.cached,
1525
+ others: options.others,
1526
+ ignored: options.ignored
1527
+ });
1528
+ return parseRenameOutput(await runner.json(args, { signal: options.signal }));
1529
+ }
1530
+ };
1531
+ }
1532
+ // src/tool-prompt-metadata.ts
1533
+ var REPLACEABLE_BY_TOOL = new Map(REPLACEABLE_TOOLS.map((entry) => [entry.tool, entry]));
1534
+ function toolNameFromPrompt(fileName) {
1535
+ const name = fileName.replace(/\.md$/, "");
1536
+ if (!TOOL_NAMES.includes(name))
1537
+ throw new Error(`unknown ReadSeek tool prompt: ${fileName}`);
1538
+ return name;
1539
+ }
1540
+ function promptFileName(promptUrl) {
1541
+ return promptUrl.pathname.split("/").pop() ?? "";
1542
+ }
1543
+ function rewriteRegisteredToolNames(value, toolAliases) {
1544
+ if (!toolAliases)
1545
+ return value;
1546
+ return Object.entries(toolAliases).reduce((rewritten, [canonicalName, registeredName]) => rewritten.replaceAll(canonicalName, registeredName), value);
1547
+ }
1548
+ function descriptionAliases(toolName, registeredName, toolAliases) {
1549
+ return Object.fromEntries(TOOL_NAMES.map((name) => {
1550
+ const readSeekName = `readSeek_${name}`;
1551
+ const alias = name === toolName && registeredName ? registeredName : toolAliases?.[readSeekName] ?? readSeekName;
1552
+ return [name, alias];
1553
+ }));
1554
+ }
1555
+ function defineToolPromptMetadata(options) {
1556
+ const fileName = promptFileName(options.promptUrl);
1557
+ const toolName = toolNameFromPrompt(fileName);
1558
+ const replaceable = REPLACEABLE_BY_TOOL.get(toolName);
1559
+ const defaultReadSeekName = `readSeek_${toolName}`;
1560
+ const registeredName = options.registeredName ?? (replaceable ? defaultReadSeekName : undefined);
1561
+ const preferenceGuideline = replaceable && registeredName ? registeredName === defaultReadSeekName ? `Prefer ${registeredName} over ${replaceable.builtIn} when both are available; ${replaceable.benefit}` : `Use ${registeredName}; ${replaceable.benefit}` : undefined;
1562
+ const aliases = descriptionAliases(toolName, registeredName, options.toolAliases);
1563
+ return {
1564
+ description: renderToolDescription(toolName, aliases),
1565
+ promptSnippet: rewriteRegisteredToolNames(options.promptSnippet, options.toolAliases),
1566
+ promptGuidelines: [
1567
+ ...preferenceGuideline ? [preferenceGuideline] : [],
1568
+ ...renderCompactGuidelines(toolName, aliases)
1569
+ ]
1227
1570
  };
1228
1571
  }
1229
1572
 
1230
- // src/readseek-value.ts
1231
- function buildReadSeekLineWithHash(line, hash, raw) {
1232
- return {
1233
- line,
1234
- hash,
1235
- anchor: `${line}:${hash}`,
1236
- raw,
1237
- display: escapeControlCharsForDisplay(raw)
1573
+ // src/edit-diff.ts
1574
+ import * as Diff from "diff";
1575
+
1576
+ // src/hashline.ts
1577
+ import xxhashWasm from "xxhash-wasm";
1578
+ var HASH_LEN = 3;
1579
+ var RADIX = 16;
1580
+ var HASH_MOD = RADIX ** HASH_LEN;
1581
+ var DICT = Array.from({ length: HASH_MOD }, (_, i) => i.toString(RADIX).padStart(HASH_LEN, "0"));
1582
+ var HASHLINE_STATE_KEY = Symbol.for("pi-readseek.hashlineState.v1");
1583
+ function getHashlineState() {
1584
+ const globalObject = globalThis;
1585
+ const state = globalObject[HASHLINE_STATE_KEY] ?? {
1586
+ h32Fn: null,
1587
+ initPromise: null
1238
1588
  };
1589
+ globalObject[HASHLINE_STATE_KEY] = state;
1590
+ return state;
1239
1591
  }
1240
- function buildReadSeekLine(line, raw) {
1241
- return buildReadSeekLineWithHash(line, computeLineHash(raw), raw);
1242
- }
1243
- function formatAnchoredFileBlocks(files, lineSuffix) {
1244
- const blocks = [];
1245
- for (const file of files) {
1246
- blocks.push(`--- ${file.displayPath} ---`);
1247
- for (const line of file.lines) {
1248
- blocks.push(`>>${line.anchor}|${line.display}${lineSuffix?.(line) ?? ""}`);
1249
- }
1592
+ async function ensureHashInit() {
1593
+ const state = getHashlineState();
1594
+ if (state.h32Fn)
1595
+ return;
1596
+ if (!state.initPromise) {
1597
+ state.initPromise = xxhashWasm().then((hasher) => {
1598
+ state.h32Fn = hasher.h32;
1599
+ });
1250
1600
  }
1251
- return blocks.join(`
1252
- `);
1601
+ await state.initPromise;
1253
1602
  }
1254
- function buildReadSeekWarning(code, message, metadata = {}) {
1255
- return { code, message, ...metadata };
1603
+ function xxh32(input) {
1604
+ const state = getHashlineState();
1605
+ if (!state.h32Fn)
1606
+ throw new Error("Hash not initialized — call ensureHashInit() first");
1607
+ return state.h32Fn(input, 0) >>> 0;
1256
1608
  }
1257
- function buildReadSeekError(code, message, hint, details) {
1258
- return {
1259
- code,
1260
- message,
1261
- ...hint !== undefined ? { hint } : {},
1262
- ...details !== undefined ? { details } : {}
1263
- };
1609
+ function computeLineHash(line) {
1610
+ if (line.endsWith("\r"))
1611
+ line = line.slice(0, -1);
1612
+ line = line.replace(/\s+/g, "");
1613
+ return DICT[xxh32(line) % HASH_MOD];
1264
1614
  }
1265
- function buildToolErrorResult(tool, code, message, opts = {}) {
1266
- return {
1267
- content: [{ type: "text", text: message }],
1268
- isError: true,
1269
- details: {
1270
- readSeekValue: {
1271
- tool,
1272
- ...opts.extra ?? {},
1273
- ok: false,
1274
- ...opts.path !== undefined ? { path: opts.path } : {},
1275
- error: buildReadSeekError(code, message, opts.hint, opts.details)
1276
- }
1277
- }
1278
- };
1615
+ var DISPLAY_CONTROL_CHAR_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f]/g;
1616
+ function escapeControlCharsForDisplay(text) {
1617
+ return text.replace(DISPLAY_CONTROL_CHAR_RE, (ch) => {
1618
+ return `\\u${ch.charCodeAt(0).toString(16).padStart(4, "0")}`;
1619
+ });
1279
1620
  }
1280
- function buildReadSeekEditResult(input) {
1281
- return {
1282
- tool: "edit",
1283
- ok: input.ok ?? true,
1284
- path: input.path,
1285
- summary: input.summary,
1286
- diff: input.diff,
1287
- ...input.diffData ? { diffData: input.diffData } : {},
1288
- firstChangedLine: input.firstChangedLine,
1289
- warnings: [...input.warnings],
1290
- noopEdits: [...input.noopEdits],
1291
- ...input.semanticSummary ? { semanticSummary: input.semanticSummary } : {}
1292
- };
1621
+ function formatHashlineDisplay(lineNumber, content) {
1622
+ return `${lineNumber}:${computeLineHash(content)}|${escapeControlCharsForDisplay(content)}`;
1293
1623
  }
1294
1624
 
1295
- // src/edit-output.ts
1296
- var EDIT_OPERATION_NAMES = ["set_line", "replace_lines", "insert_after", "replace"];
1297
- function hasNewText(value) {
1298
- return typeof value === "object" && value !== null && typeof value.new_text === "string";
1625
+ // src/edit-diff.ts
1626
+ function normalizeToLF(text) {
1627
+ return text.replace(/\r\n/g, `
1628
+ `).replace(/\r/g, `
1629
+ `);
1299
1630
  }
1300
- function getVisibleDiffStats(diff) {
1301
- const stats = parseDiffStats(diff);
1302
- if (stats.added > 0 || stats.removed > 0)
1303
- return stats;
1304
- if (!diff.includes("→"))
1305
- return stats;
1306
- if (diff.includes("→ [deleted]"))
1307
- return { added: 0, removed: 1 };
1308
- return { added: 1, removed: 1 };
1631
+ function stripBom(content) {
1632
+ return content.startsWith("\uFEFF") ? { bom: "\uFEFF", text: content.slice(1) } : { bom: "", text: content };
1309
1633
  }
1310
- function buildVisibleSummary(displayPath, diff, edits) {
1311
- let stats = getVisibleDiffStats(diff);
1312
- const counts = countEditTypes(edits);
1313
- const editCount = counts.total || 1;
1314
- if (counts.total > 0 && counts.insert_after === counts.total && stats.removed > 0 && stats.added === stats.removed + counts.insert_after) {
1315
- stats = { added: counts.insert_after, removed: 0 };
1316
- }
1317
- const changeWord = editCount === 1 ? "change" : "changes";
1318
- const changedLineCount = Math.max(stats.added, stats.removed);
1319
- const lineWord = changedLineCount === 1 ? "line" : "lines";
1320
- return `Edited ${displayPath} (${editCount} ${changeWord}, +${stats.added} -${stats.removed} ${lineWord})`;
1634
+ function hasBareCarriageReturn(content) {
1635
+ return content.replace(/\r\n/g, "").includes("\r");
1321
1636
  }
1322
- function extractNewTextValues(edits) {
1323
- const values = [];
1324
- for (const edit of edits ?? []) {
1325
- if (!edit || typeof edit !== "object")
1637
+ function generateDiffString(oldContent, newContent, contextLines = 4) {
1638
+ const parts = Diff.diffLines(oldContent, newContent);
1639
+ const output = [];
1640
+ const maxLineNum = Math.max(oldContent.split(`
1641
+ `).length, newContent.split(`
1642
+ `).length);
1643
+ const lineNumWidth = String(maxLineNum).length;
1644
+ let oldLineNum = 1;
1645
+ let newLineNum = 1;
1646
+ let lastWasChange = false;
1647
+ let firstChangedLine;
1648
+ for (let i = 0;i < parts.length; i++) {
1649
+ const part = parts[i];
1650
+ const raw = part.value.split(`
1651
+ `);
1652
+ if (raw[raw.length - 1] === "")
1653
+ raw.pop();
1654
+ if (part.added || part.removed) {
1655
+ if (firstChangedLine === undefined)
1656
+ firstChangedLine = newLineNum;
1657
+ for (const line of raw) {
1658
+ if (part.added) {
1659
+ output.push(`+${String(newLineNum).padStart(lineNumWidth, " ")} ${line}`);
1660
+ newLineNum++;
1661
+ } else {
1662
+ output.push(`-${String(oldLineNum).padStart(lineNumWidth, " ")} ${line}`);
1663
+ oldLineNum++;
1664
+ }
1665
+ }
1666
+ lastWasChange = true;
1326
1667
  continue;
1327
- const operations = edit;
1328
- for (const operationName of EDIT_OPERATION_NAMES) {
1329
- const operation = operations[operationName];
1330
- if (hasNewText(operation))
1331
- values.push(operation.new_text);
1332
1668
  }
1669
+ const nextPartIsChange = i < parts.length - 1 && (parts[i + 1].added || parts[i + 1].removed);
1670
+ if (lastWasChange || nextPartIsChange) {
1671
+ let linesToShow = raw;
1672
+ let skipStart = 0;
1673
+ let skipEnd = 0;
1674
+ if (!lastWasChange) {
1675
+ skipStart = Math.max(0, raw.length - contextLines);
1676
+ linesToShow = raw.slice(skipStart);
1677
+ }
1678
+ if (!nextPartIsChange && linesToShow.length > contextLines) {
1679
+ skipEnd = linesToShow.length - contextLines;
1680
+ linesToShow = linesToShow.slice(0, contextLines);
1681
+ }
1682
+ if (skipStart > 0) {
1683
+ output.push(` ${"".padStart(lineNumWidth, " ")} ...`);
1684
+ oldLineNum += skipStart;
1685
+ newLineNum += skipStart;
1686
+ }
1687
+ for (const line of linesToShow) {
1688
+ output.push(` ${String(oldLineNum).padStart(lineNumWidth, " ")} ${line}`);
1689
+ oldLineNum++;
1690
+ newLineNum++;
1691
+ }
1692
+ if (skipEnd > 0) {
1693
+ output.push(` ${"".padStart(lineNumWidth, " ")} ...`);
1694
+ oldLineNum += skipEnd;
1695
+ newLineNum += skipEnd;
1696
+ }
1697
+ } else {
1698
+ oldLineNum += raw.length;
1699
+ newLineNum += raw.length;
1700
+ }
1701
+ lastWasChange = false;
1333
1702
  }
1334
- return values;
1335
- }
1336
- function formatWhitespaceOnlyWarning(semanticSummary, edits) {
1337
- if (semanticSummary?.classification !== "whitespace-only")
1338
- return;
1339
- if (!extractNewTextValues(edits).some((text) => /\S/.test(text)))
1340
- return;
1341
- return "⚠ Edit classified as whitespace-only — if you intended a behavior change, re-read to verify.";
1342
- }
1343
- function formatReplaceHint(edits, noopEdits) {
1344
- if ((noopEdits ?? []).length > 0)
1345
- return;
1346
- const counts = countEditTypes(edits);
1347
- if (counts.replace === 0)
1348
- return;
1349
- if (counts.replace !== counts.total)
1350
- return;
1351
- return "[info: this edit used replace (unverified). For safer future edits, prefer set_line/replace_lines with an anchor from readSeek_digest/readSeek_grep/readSeek_search.]";
1352
- }
1353
- function buildEditOutput(input) {
1354
- const summary = `Updated ${input.displayPath}`;
1355
- const visibleSummary = buildVisibleSummary(input.displayPath, input.diff, input.edits);
1356
- const semanticWarning = formatWhitespaceOnlyWarning(input.semanticSummary, input.edits);
1357
- const warningText = input.warnings.length ? `
1358
-
1359
- Warnings:
1360
- ${input.warnings.join(`
1361
- `)}` : "";
1362
- const replaceHint = formatReplaceHint(input.edits, input.noopEdits);
1363
- let text = visibleSummary;
1364
- if (semanticWarning)
1365
- text += `
1366
- ${semanticWarning}`;
1367
- text += warningText;
1368
- if (replaceHint)
1369
- text += `
1370
- ${replaceHint}`;
1371
- return {
1372
- text,
1373
- patch: input.patch ?? "",
1374
- readSeekValue: buildReadSeekEditResult({
1375
- path: input.path,
1376
- summary,
1377
- diff: input.diff,
1378
- ...input.diffData ? { diffData: input.diffData } : {},
1379
- firstChangedLine: input.firstChangedLine,
1380
- warnings: input.warnings,
1381
- noopEdits: input.noopEdits,
1382
- ...input.semanticSummary ? { semanticSummary: input.semanticSummary } : {}
1383
- })
1384
- };
1385
- }
1386
-
1387
- // src/edit-classify.ts
1388
- import * as Diff2 from "diff";
1389
- function classifyEdit(oldContent, newContent) {
1390
- if (oldContent === newContent) {
1391
- return { classification: "no-op" };
1392
- }
1703
+ return { diff: output.join(`
1704
+ `), firstChangedLine };
1705
+ }
1706
+ function generateCompactOrFullDiff(oldContent, newContent, contextLines = 4) {
1707
+ if (oldContent === newContent)
1708
+ return { diff: "", firstChangedLine: undefined };
1393
1709
  const oldLines = oldContent.split(`
1394
1710
  `);
1395
1711
  const newLines = newContent.split(`
1396
1712
  `);
1397
1713
  if (oldLines.length === newLines.length) {
1398
- let hasWhitespaceChange2 = false;
1399
- let hasSemanticChange2 = false;
1714
+ let changedIndex = -1;
1715
+ let changeCount = 0;
1400
1716
  for (let i = 0;i < oldLines.length; i++) {
1401
- if (oldLines[i] === newLines[i])
1402
- continue;
1403
- if (oldLines[i].trim() === newLines[i].trim()) {
1404
- hasWhitespaceChange2 = true;
1405
- } else {
1406
- hasSemanticChange2 = true;
1717
+ if (oldLines[i] !== newLines[i]) {
1718
+ changedIndex = i;
1719
+ changeCount++;
1720
+ if (changeCount > 1)
1721
+ break;
1407
1722
  }
1408
1723
  }
1409
- if (hasSemanticChange2 && hasWhitespaceChange2)
1410
- return { classification: "mixed" };
1411
- if (hasWhitespaceChange2)
1412
- return { classification: "whitespace-only" };
1413
- return { classification: "semantic" };
1724
+ if (changeCount === 1 && changedIndex >= 0) {
1725
+ const lineNum = changedIndex + 1;
1726
+ const oldLine = oldLines[changedIndex] ?? "";
1727
+ const newLine = newLines[changedIndex] ?? "";
1728
+ const oldHash = computeLineHash(oldLine);
1729
+ const newHash = computeLineHash(newLine);
1730
+ return {
1731
+ diff: `${lineNum}:${oldHash}|${oldLine} → ${lineNum}:${newHash}|${newLine}`,
1732
+ firstChangedLine: lineNum
1733
+ };
1734
+ }
1414
1735
  }
1415
- const parts = Diff2.diffLines(oldContent, newContent);
1416
- let hasWhitespaceChange = false;
1417
- let hasSemanticChange = false;
1418
- for (const part of parts) {
1419
- if (!part.added && !part.removed)
1420
- continue;
1421
- const lines = part.value.split(`
1422
- `);
1423
- if (lines[lines.length - 1] === "")
1424
- lines.pop();
1425
- for (const line of lines) {
1426
- if (line.trim() === "") {
1427
- hasWhitespaceChange = true;
1428
- } else {
1429
- hasSemanticChange = true;
1736
+ if (oldLines.length === newLines.length + 1) {
1737
+ let deletedIndex = -1;
1738
+ let j = 0;
1739
+ let failed = false;
1740
+ for (let i = 0;i < oldLines.length; i++) {
1741
+ if (j < newLines.length && oldLines[i] === newLines[j]) {
1742
+ j++;
1743
+ continue;
1744
+ }
1745
+ if (deletedIndex === -1) {
1746
+ deletedIndex = i;
1747
+ continue;
1430
1748
  }
1749
+ failed = true;
1750
+ break;
1751
+ }
1752
+ if (!failed && deletedIndex !== -1 && j === newLines.length) {
1753
+ const lineNum = deletedIndex + 1;
1754
+ const oldLine = oldLines[deletedIndex] ?? "";
1755
+ const oldHash = computeLineHash(oldLine);
1756
+ return {
1757
+ diff: `${lineNum}:${oldHash}|${oldLine} → [deleted]`,
1758
+ firstChangedLine: lineNum
1759
+ };
1431
1760
  }
1432
1761
  }
1433
- if (hasSemanticChange && hasWhitespaceChange)
1434
- return { classification: "mixed" };
1435
- if (hasWhitespaceChange)
1436
- return { classification: "whitespace-only" };
1437
- return { classification: "semantic" };
1762
+ return generateDiffString(oldContent, newContent, contextLines);
1438
1763
  }
1439
1764
 
1440
- // src/readseek-client.ts
1441
- import { existsSync, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
1442
- import { createRequire as createRequire2 } from "node:module";
1443
- import { homedir as homedir3 } from "node:os";
1444
- import path2 from "node:path";
1445
-
1446
- // src/readseek/enums.ts
1447
- var SymbolKind;
1448
- ((SymbolKind2) => {
1449
- SymbolKind2["Class"] = "class";
1450
- SymbolKind2["Function"] = "function";
1451
- SymbolKind2["Method"] = "method";
1452
- SymbolKind2["Variable"] = "variable";
1453
- SymbolKind2["Constant"] = "constant";
1454
- SymbolKind2["Interface"] = "interface";
1455
- SymbolKind2["Type"] = "type";
1456
- SymbolKind2["Enum"] = "enum";
1457
- SymbolKind2["Struct"] = "struct";
1458
- SymbolKind2["Import"] = "import";
1459
- SymbolKind2["Module"] = "module";
1460
- SymbolKind2["Namespace"] = "namespace";
1461
- SymbolKind2["Property"] = "property";
1462
- SymbolKind2["Heading"] = "heading";
1463
- SymbolKind2["Table"] = "table";
1464
- SymbolKind2["View"] = "view";
1465
- SymbolKind2["Procedure"] = "procedure";
1466
- SymbolKind2["Trigger"] = "trigger";
1467
- SymbolKind2["Index"] = "index";
1468
- SymbolKind2["Schema"] = "schema";
1469
- SymbolKind2["Signal"] = "signal";
1470
- SymbolKind2["Unknown"] = "unknown";
1471
- })(SymbolKind ||= {});
1472
-
1473
- // src/readseek-settings.ts
1474
- import { readFileSync, statSync } from "node:fs";
1475
- import { homedir as homedir2 } from "node:os";
1476
- import { join } from "node:path";
1477
- var READSEEK_TOOL_REPLACEMENTS = {
1478
- read: "readSeek_digest",
1479
- edit: "readSeek_edit",
1480
- grep: "readSeek_grep",
1481
- write: "readSeek_write"
1482
- };
1483
- var READSEEK_KEYS = ["overrideTools", "imageMode", "syntaxValidation", "timeoutMs", "grep", "display"];
1484
- var READSEEK_GREP_KEYS = ["maxLines", "maxBytes"];
1485
- var READSEEK_DISPLAY_KEYS = ["read", "grep", "edit", "write"];
1486
- var DEFAULT_DISPLAY_MODES = {
1487
- read: "compact",
1488
- grep: "compact",
1489
- edit: "expanded",
1490
- write: "expanded"
1491
- };
1492
- function globalSettingsPath() {
1493
- return join(homedir2(), ".pi/agent/settings.json");
1494
- }
1495
- function projectSettingsPath() {
1496
- return join(process.cwd(), ".pi/settings.json");
1765
+ // src/path-utils.ts
1766
+ import { homedir } from "node:os";
1767
+ import { isAbsolute, resolve as resolvePath } from "node:path";
1768
+ var UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
1769
+ function normalizeUnicodeSpaces(str) {
1770
+ return str.replace(UNICODE_SPACES, " ");
1497
1771
  }
1498
- function isRecord(value) {
1499
- return typeof value === "object" && value !== null && !Array.isArray(value);
1772
+ function normalizeAtPrefix(filePath) {
1773
+ return filePath.startsWith("@") ? filePath.slice(1) : filePath;
1500
1774
  }
1501
- function invalid(source, path2) {
1502
- return { source, path: path2, message: `Invalid readseek setting at ${path2}` };
1775
+ function expandPath(filePath) {
1776
+ const normalized = normalizeUnicodeSpaces(normalizeAtPrefix(filePath));
1777
+ if (normalized === "~")
1778
+ return homedir();
1779
+ if (normalized.startsWith("~/"))
1780
+ return homedir() + normalized.slice(1);
1781
+ return normalized;
1503
1782
  }
1504
- function warnUnknownKeys(raw, known, prefix, source, warnings) {
1505
- for (const key of Object.keys(raw)) {
1506
- if (known.includes(key))
1507
- continue;
1508
- const path2 = `${prefix}.${key}`;
1509
- warnings.push({ source, path: path2, message: `Unknown readseek setting at ${path2}` });
1510
- }
1783
+ function resolveToCwd(filePath, cwd) {
1784
+ const expanded = expandPath(filePath);
1785
+ return isAbsolute(expanded) ? expanded : resolvePath(cwd, expanded);
1511
1786
  }
1512
- function readPositive(raw, key, path2, source, warnings) {
1513
- if (!(key in raw))
1514
- return;
1515
- const val = raw[key];
1516
- if (typeof val === "number" && Number.isSafeInteger(val) && val > 0)
1517
- return val;
1518
- warnings.push(invalid(source, path2));
1519
- return;
1787
+
1788
+ // src/runtime.ts
1789
+ function throwIfAborted(signal) {
1790
+ if (signal?.aborted)
1791
+ throw new Error("Operation aborted");
1520
1792
  }
1521
- function readEnum(raw, key, values, path2, source, warnings) {
1522
- if (!(key in raw))
1523
- return;
1524
- const val = raw[key];
1525
- if (typeof val === "string" && values.includes(val))
1526
- return val;
1527
- warnings.push(invalid(source, path2));
1528
- return;
1793
+
1794
+ // src/fs-error.ts
1795
+ var SYSLOG_PATH_SUFFIX = /,\s+[a-z]+(\s+'.*')?$/;
1796
+ function strerror(err) {
1797
+ const raw = String(err?.message ?? String(err));
1798
+ return raw.replace(SYSLOG_PATH_SUFFIX, "");
1529
1799
  }
1530
- function readImageMode(raw, source, warnings) {
1531
- if (!("imageMode" in raw))
1532
- return;
1533
- const val = raw.imageMode;
1534
- if (val === "on" || val === "off" || val === "auto")
1535
- return val;
1536
- warnings.push(invalid(source, "readseek.imageMode"));
1537
- return;
1800
+ function formatFsError(err, domain) {
1801
+ const errno = typeof err.code === "string" ? err.code : "EIO";
1802
+ return {
1803
+ code: errno,
1804
+ message: `${domain}: ${errno}: ${strerror(err)}`
1805
+ };
1538
1806
  }
1539
- function readOverrideTools(raw, source, warnings) {
1540
- if (!("overrideTools" in raw))
1541
- return;
1542
- const value = raw.overrideTools;
1543
- if (!Array.isArray(value)) {
1544
- warnings.push(invalid(source, "readseek.overrideTools"));
1545
- return;
1546
- }
1547
- const tools = [];
1548
- value.forEach((tool, index) => {
1549
- if (typeof tool === "string" && Object.hasOwn(READSEEK_TOOL_REPLACEMENTS, tool)) {
1550
- tools.push(tool);
1551
- } else {
1552
- warnings.push(invalid(source, `readseek.overrideTools[${index}]`));
1807
+
1808
+ // src/edit-render-helpers.ts
1809
+ var VARIANT_KEYS = ["set_line", "replace_lines", "insert_after", "replace"];
1810
+ function countEditTypes(edits) {
1811
+ const counts = {
1812
+ set_line: 0,
1813
+ replace_lines: 0,
1814
+ insert_after: 0,
1815
+ replace: 0,
1816
+ total: 0
1817
+ };
1818
+ if (!edits)
1819
+ return counts;
1820
+ for (const edit of edits) {
1821
+ counts.total++;
1822
+ if (edit && typeof edit === "object") {
1823
+ for (const key of VARIANT_KEYS) {
1824
+ if (key in edit) {
1825
+ counts[key]++;
1826
+ break;
1827
+ }
1828
+ }
1553
1829
  }
1554
- });
1555
- return tools;
1556
- }
1557
- function readDisplaySettings(raw, source, warnings) {
1558
- if (!("display" in raw))
1559
- return;
1560
- if (!isRecord(raw.display)) {
1561
- warnings.push(invalid(source, "readseek.display"));
1562
- return;
1563
1830
  }
1564
- warnUnknownKeys(raw.display, READSEEK_DISPLAY_KEYS, "readseek.display", source, warnings);
1565
- const display = {};
1566
- for (const tool of READSEEK_DISPLAY_KEYS) {
1567
- const mode = readEnum(raw.display, tool, ["compact", "expanded"], `readseek.display.${tool}`, source, warnings);
1568
- if (mode !== undefined)
1569
- display[tool] = mode;
1831
+ return counts;
1832
+ }
1833
+ function parseDiffStats(diff) {
1834
+ if (!diff)
1835
+ return { added: 0, removed: 0 };
1836
+ let added = 0;
1837
+ let removed = 0;
1838
+ for (const line of diff.split(`
1839
+ `)) {
1840
+ if (line.startsWith("+++") || line.startsWith("---"))
1841
+ continue;
1842
+ if (line.startsWith("+"))
1843
+ added++;
1844
+ else if (line.startsWith("-"))
1845
+ removed++;
1570
1846
  }
1571
- return Object.keys(display).length > 0 ? display : undefined;
1847
+ return { added, removed };
1572
1848
  }
1573
- function validateSettings(raw, source) {
1574
- const settings = {};
1575
- const warnings = [];
1576
- if (!isRecord(raw)) {
1577
- warnings.push({ source, message: "Invalid readseek settings: expected a JSON object" });
1578
- return { settings, warnings };
1849
+ function formatEditCallText(args, argsComplete) {
1850
+ const rawPath = typeof args?.path === "string" ? args.path : null;
1851
+ if (!argsComplete) {
1852
+ return { path: rawPath, suffix: undefined };
1579
1853
  }
1580
- if (!("readseek" in raw)) {
1581
- const misplaced = READSEEK_KEYS.filter((key) => (key in raw));
1582
- if (misplaced.length > 0) {
1583
- warnings.push({
1584
- source,
1585
- path: misplaced[0],
1586
- message: `Readseek setting at top level: move ${misplaced.join(", ")} under "readseek"`
1587
- });
1854
+ const edits = args?.edits;
1855
+ if (!Array.isArray(edits) || edits.length === 0) {
1856
+ return { path: rawPath, suffix: undefined };
1857
+ }
1858
+ const counts = countEditTypes(edits);
1859
+ const parts = [];
1860
+ for (const key of VARIANT_KEYS) {
1861
+ if (counts[key] > 0) {
1862
+ parts.push(`${counts[key]} ${key}`);
1588
1863
  }
1589
- return { settings, warnings };
1590
1864
  }
1591
- if (!isRecord(raw.readseek)) {
1592
- warnings.push(invalid(source, "readseek"));
1593
- return { settings, warnings };
1865
+ const word = counts.total === 1 ? "edit" : "edits";
1866
+ const suffix = `${counts.total} ${word} (${parts.join(", ")})`;
1867
+ return { path: rawPath, suffix };
1868
+ }
1869
+ function formatEditResultText(input) {
1870
+ const { isError, diff, warnings, noopEdits, errorText } = input;
1871
+ const isNoOp = isError && (Array.isArray(noopEdits) && noopEdits.length > 0 || errorText.includes("No changes made"));
1872
+ const stats = parseDiffStats(diff);
1873
+ const hasDiffStats = stats.added > 0 || stats.removed > 0;
1874
+ const diffStats = hasDiffStats ? `+${stats.added} / -${stats.removed}` : undefined;
1875
+ let warningsBadge;
1876
+ if (warnings.length === 1) {
1877
+ warningsBadge = "⚠ 1 warning";
1878
+ } else if (warnings.length > 1) {
1879
+ warningsBadge = `⚠ ${warnings.length} warnings`;
1594
1880
  }
1595
- const section = raw.readseek;
1596
- warnUnknownKeys(section, READSEEK_KEYS, "readseek", source, warnings);
1597
- const overrideTools = readOverrideTools(section, source, warnings);
1598
- if (overrideTools !== undefined)
1599
- settings.overrideTools = overrideTools;
1600
- const imageMode = readImageMode(section, source, warnings);
1601
- if (imageMode !== undefined)
1602
- settings.imageMode = imageMode;
1603
- const syntaxValidation = readEnum(section, "syntaxValidation", ["warn", "block", "off"], "readseek.syntaxValidation", source, warnings);
1604
- if (syntaxValidation !== undefined)
1605
- settings.syntaxValidation = syntaxValidation;
1606
- const timeoutMs = readPositive(section, "timeoutMs", "readseek.timeoutMs", source, warnings);
1607
- if (timeoutMs !== undefined)
1608
- settings.timeoutMs = timeoutMs;
1609
- if ("grep" in section) {
1610
- if (isRecord(section.grep)) {
1611
- warnUnknownKeys(section.grep, READSEEK_GREP_KEYS, "readseek.grep", source, warnings);
1612
- const grep = {};
1613
- const maxLines = readPositive(section.grep, "maxLines", "readseek.grep.maxLines", source, warnings);
1614
- if (maxLines !== undefined)
1615
- grep.maxLines = maxLines;
1616
- const maxBytes = readPositive(section.grep, "maxBytes", "readseek.grep.maxBytes", source, warnings);
1617
- if (maxBytes !== undefined)
1618
- grep.maxBytes = maxBytes;
1619
- if (Object.keys(grep).length > 0)
1620
- settings.grep = grep;
1621
- } else {
1622
- warnings.push(invalid(source, "readseek.grep"));
1881
+ const showErrorText = isError ? errorText : undefined;
1882
+ let semanticBadge;
1883
+ if (!isError && !isNoOp && input.semanticClassification) {
1884
+ switch (input.semanticClassification) {
1885
+ case "whitespace-only":
1886
+ semanticBadge = "ws-only";
1887
+ break;
1888
+ case "semantic":
1889
+ semanticBadge = "✓ semantic";
1890
+ break;
1891
+ case "mixed":
1892
+ semanticBadge = "mixed";
1893
+ break;
1623
1894
  }
1624
1895
  }
1625
- const display = readDisplaySettings(section, source, warnings);
1626
- if (display !== undefined)
1627
- settings.display = display;
1628
- return { settings, warnings };
1896
+ return {
1897
+ diffStats,
1898
+ noOp: isNoOp,
1899
+ warningsBadge,
1900
+ errorText: showErrorText,
1901
+ semanticBadge
1902
+ };
1629
1903
  }
1630
- var settingsFileCache = new Map;
1631
- function statFileOrNull(path2) {
1632
- try {
1633
- return statSync(path2);
1634
- } catch {
1635
- return null;
1636
- }
1904
+
1905
+ // src/readseek-value.ts
1906
+ function buildReadSeekLineWithHash(line, hash, raw) {
1907
+ return {
1908
+ line,
1909
+ hash,
1910
+ anchor: `${line}:${hash}`,
1911
+ raw,
1912
+ display: escapeControlCharsForDisplay(raw)
1913
+ };
1637
1914
  }
1638
- function readSettingsFile(path2) {
1639
- const stats = statFileOrNull(path2);
1640
- if (!stats) {
1641
- settingsFileCache.delete(path2);
1642
- return { settings: {}, warnings: [] };
1643
- }
1644
- const cached = settingsFileCache.get(path2);
1645
- if (cached && cached.mtimeMs === stats.mtimeMs && cached.size === stats.size)
1646
- return cached.result;
1647
- let result;
1648
- try {
1649
- const text = readFileSync(path2, "utf8");
1650
- result = validateSettings(JSON.parse(text), path2);
1651
- } catch (error) {
1652
- const message = error instanceof Error ? error.message : String(error);
1653
- result = { settings: {}, warnings: [{ source: path2, message: `Invalid JSON: ${message}` }] };
1915
+ function buildReadSeekLine(line, raw) {
1916
+ return buildReadSeekLineWithHash(line, computeLineHash(raw), raw);
1917
+ }
1918
+ function formatAnchoredFileBlocks(files, lineSuffix) {
1919
+ const blocks = [];
1920
+ for (const file of files) {
1921
+ blocks.push(`--- ${file.displayPath} ---`);
1922
+ for (const line of file.lines) {
1923
+ blocks.push(`>>${line.anchor}|${line.display}${lineSuffix?.(line) ?? ""}`);
1924
+ }
1654
1925
  }
1655
- settingsFileCache.set(path2, { mtimeMs: stats.mtimeMs, size: stats.size, result });
1656
- return result;
1926
+ return blocks.join(`
1927
+ `);
1657
1928
  }
1658
- function mergeSettings(base, override) {
1659
- const settings = { ...base, ...override };
1660
- const grep = { ...base.grep ?? {}, ...override.grep ?? {} };
1661
- if (Object.keys(grep).length > 0)
1662
- settings.grep = grep;
1663
- const display = { ...base.display ?? {}, ...override.display ?? {} };
1664
- if (Object.keys(display).length > 0)
1665
- settings.display = display;
1666
- return settings;
1929
+ function buildReadSeekWarning(code, message, metadata = {}) {
1930
+ return { code, message, ...metadata };
1667
1931
  }
1668
- function resolveReadSeekJsonSettings() {
1669
- const globalResult = readSettingsFile(globalSettingsPath());
1670
- const projectResult = readSettingsFile(projectSettingsPath());
1932
+ function buildReadSeekError(code, message, hint, details) {
1671
1933
  return {
1672
- settings: mergeSettings(globalResult.settings, projectResult.settings),
1673
- warnings: [...globalResult.warnings, ...projectResult.warnings]
1934
+ code,
1935
+ message,
1936
+ ...hint !== undefined ? { hint } : {},
1937
+ ...details !== undefined ? { details } : {}
1674
1938
  };
1675
1939
  }
1676
- function resolveReadSeekSyntaxValidation() {
1677
- return resolveReadSeekJsonSettings().settings.syntaxValidation;
1678
- }
1679
- function resolveReadSeekTimeoutMs() {
1680
- return resolveReadSeekJsonSettings().settings.timeoutMs;
1940
+ function buildToolErrorResult(tool, code, message, opts = {}) {
1941
+ return {
1942
+ content: [{ type: "text", text: message }],
1943
+ isError: true,
1944
+ details: {
1945
+ readSeekValue: {
1946
+ tool,
1947
+ ...opts.extra ?? {},
1948
+ ok: false,
1949
+ ...opts.path !== undefined ? { path: opts.path } : {},
1950
+ error: buildReadSeekError(code, message, opts.hint, opts.details)
1951
+ }
1952
+ }
1953
+ };
1681
1954
  }
1682
- function resolveReadSeekToolDisplayMode(tool) {
1683
- return resolveReadSeekJsonSettings().settings.display?.[tool] ?? DEFAULT_DISPLAY_MODES[tool];
1955
+ function buildReadSeekEditResult(input) {
1956
+ return {
1957
+ tool: "edit",
1958
+ ok: input.ok ?? true,
1959
+ path: input.path,
1960
+ summary: input.summary,
1961
+ diff: input.diff,
1962
+ ...input.diffData ? { diffData: input.diffData } : {},
1963
+ firstChangedLine: input.firstChangedLine,
1964
+ warnings: [...input.warnings],
1965
+ noopEdits: [...input.noopEdits],
1966
+ ...input.semanticSummary ? { semanticSummary: input.semanticSummary } : {}
1967
+ };
1684
1968
  }
1685
1969
 
1686
- // src/readseek-client.ts
1687
- var resolveFromHost = createRequire2(import.meta.url).resolve;
1688
- function normalizeLanguage(language) {
1689
- return language === "java" ? "Java" : language;
1690
- }
1691
- function normalizeKind(kind) {
1692
- if (kind === "constructor")
1693
- return "method" /* Method */;
1694
- if (Object.values(SymbolKind).includes(kind))
1695
- return kind;
1696
- return "unknown" /* Unknown */;
1697
- }
1698
- function parentQualifiedNameFor(qualifiedName) {
1699
- const lastDot = qualifiedName.lastIndexOf(".");
1700
- return lastDot === -1 ? "" : qualifiedName.slice(0, lastDot);
1970
+ // src/edit-output.ts
1971
+ var EDIT_OPERATION_NAMES = ["set_line", "replace_lines", "insert_after", "replace"];
1972
+ function hasNewText(value) {
1973
+ return typeof value === "object" && value !== null && typeof value.new_text === "string";
1701
1974
  }
1702
- function symbolsFromReadSeek(symbols) {
1703
- const symbolsByQualifiedName = new Map;
1704
- const entries = [];
1705
- for (const symbol of symbols) {
1706
- const parentQualifiedName = parentQualifiedNameFor(symbol.qualified_name);
1707
- const fileSymbol = {
1708
- name: symbol.name,
1709
- kind: normalizeKind(symbol.kind),
1710
- startLine: symbol.start_line,
1711
- endLine: symbol.end_line
1712
- };
1713
- const bucket = symbolsByQualifiedName.get(symbol.qualified_name);
1714
- if (bucket)
1715
- bucket.push(fileSymbol);
1716
- else
1717
- symbolsByQualifiedName.set(symbol.qualified_name, [fileSymbol]);
1718
- entries.push({ parentQualifiedName, symbol: fileSymbol });
1975
+ function getVisibleDiffStats(diff) {
1976
+ const stats = parseDiffStats(diff);
1977
+ if (stats.added > 0 || stats.removed > 0)
1978
+ return stats;
1979
+ if (!diff.includes("→"))
1980
+ return stats;
1981
+ if (diff.includes("→ [deleted]"))
1982
+ return { added: 0, removed: 1 };
1983
+ return { added: 1, removed: 1 };
1984
+ }
1985
+ function buildVisibleSummary(displayPath, diff, edits) {
1986
+ let stats = getVisibleDiffStats(diff);
1987
+ const counts = countEditTypes(edits);
1988
+ const editCount = counts.total || 1;
1989
+ if (counts.total > 0 && counts.insert_after === counts.total && stats.removed > 0 && stats.added === stats.removed + counts.insert_after) {
1990
+ stats = { added: counts.insert_after, removed: 0 };
1719
1991
  }
1720
- const roots = [];
1721
- for (const entry of entries) {
1722
- const parent = entry.parentQualifiedName ? symbolsByQualifiedName.get(entry.parentQualifiedName)?.[0] : undefined;
1723
- if (!parent) {
1724
- roots.push(entry.symbol);
1992
+ const changeWord = editCount === 1 ? "change" : "changes";
1993
+ const changedLineCount = Math.max(stats.added, stats.removed);
1994
+ const lineWord = changedLineCount === 1 ? "line" : "lines";
1995
+ return `Edited ${displayPath} (${editCount} ${changeWord}, +${stats.added} -${stats.removed} ${lineWord})`;
1996
+ }
1997
+ function extractNewTextValues(edits) {
1998
+ const values = [];
1999
+ for (const edit of edits ?? []) {
2000
+ if (!edit || typeof edit !== "object")
1725
2001
  continue;
2002
+ const operations = edit;
2003
+ for (const operationName of EDIT_OPERATION_NAMES) {
2004
+ const operation = operations[operationName];
2005
+ if (hasNewText(operation))
2006
+ values.push(operation.new_text);
1726
2007
  }
1727
- parent.children ??= [];
1728
- parent.children.push(entry.symbol);
1729
2008
  }
1730
- return roots;
2009
+ return values;
1731
2010
  }
1732
- var READSEEK_REPO_PACKAGE_NAMES = new Set(["@jarkkojs/readseek", "readseek"]);
1733
- var defaultReadSeekDirInit;
1734
- function readSeekBinaryPath() {
1735
- if (process.env.READSEEK_BIN)
1736
- return process.env.READSEEK_BIN;
1737
- return resolveReadSeekBinaryFromPackages(resolveFromHost);
2011
+ function formatWhitespaceOnlyWarning(semanticSummary, edits) {
2012
+ if (semanticSummary?.classification !== "whitespace-only")
2013
+ return;
2014
+ if (!extractNewTextValues(edits).some((text) => /\S/.test(text)))
2015
+ return;
2016
+ return "⚠ Edit classified as whitespace-only — if you intended a behavior change, re-read to verify.";
1738
2017
  }
1739
- function readSeekBinaryAvailability() {
1740
- try {
1741
- readSeekBinaryPath();
1742
- return { available: true };
1743
- } catch (err) {
1744
- return { available: false, reason: classifyReadSeekFailure(err).message };
1745
- }
2018
+ function formatReplaceHint(edits, noopEdits) {
2019
+ if ((noopEdits ?? []).length > 0)
2020
+ return;
2021
+ const counts = countEditTypes(edits);
2022
+ if (counts.replace === 0)
2023
+ return;
2024
+ if (counts.replace !== counts.total)
2025
+ return;
2026
+ return "[info: this edit used replace (unverified). For safer future edits, prefer set_line/replace_lines with an anchor from readSeek_digest/readSeek_grep/readSeek_search.]";
1746
2027
  }
1747
- function classifyReadSeekFailure(err) {
1748
- const failure = err;
1749
- const message = String(failure?.message ?? err);
1750
- const missing = failure?.code === "ENOENT" || /Cannot find package|Cannot find module|no such file/i.test(message);
1751
- if (missing) {
1752
- return { code: "readseek-not-installed", message, hint: "Run npm install to install @jarkkojs/readseek." };
1753
- }
1754
- return { code: "readseek-execution-error", message };
2028
+ function buildEditOutput(input) {
2029
+ const summary = `Updated ${input.displayPath}`;
2030
+ const visibleSummary = buildVisibleSummary(input.displayPath, input.diff, input.edits);
2031
+ const semanticWarning = formatWhitespaceOnlyWarning(input.semanticSummary, input.edits);
2032
+ const warningText = input.warnings.length ? `
2033
+
2034
+ Warnings:
2035
+ ${input.warnings.join(`
2036
+ `)}` : "";
2037
+ const replaceHint = formatReplaceHint(input.edits, input.noopEdits);
2038
+ let text = visibleSummary;
2039
+ if (semanticWarning)
2040
+ text += `
2041
+ ${semanticWarning}`;
2042
+ text += warningText;
2043
+ if (replaceHint)
2044
+ text += `
2045
+ ${replaceHint}`;
2046
+ return {
2047
+ text,
2048
+ patch: input.patch ?? "",
2049
+ readSeekValue: buildReadSeekEditResult({
2050
+ path: input.path,
2051
+ summary,
2052
+ diff: input.diff,
2053
+ ...input.diffData ? { diffData: input.diffData } : {},
2054
+ firstChangedLine: input.firstChangedLine,
2055
+ warnings: input.warnings,
2056
+ noopEdits: input.noopEdits,
2057
+ ...input.semanticSummary ? { semanticSummary: input.semanticSummary } : {}
2058
+ })
2059
+ };
1755
2060
  }
1756
- function directoryExists(dirPath) {
1757
- try {
1758
- return statSync2(dirPath).isDirectory();
1759
- } catch {
1760
- return false;
2061
+
2062
+ // src/edit-classify.ts
2063
+ import * as Diff2 from "diff";
2064
+ function classifyEdit(oldContent, newContent) {
2065
+ if (oldContent === newContent) {
2066
+ return { classification: "no-op" };
1761
2067
  }
1762
- }
1763
- var ownRepositoryByDir = new Map;
1764
- function isOwnReadSeekRepository(cwd = process.cwd()) {
1765
- const start = path2.resolve(cwd);
1766
- const cached = ownRepositoryByDir.get(start);
1767
- if (cached !== undefined)
1768
- return cached;
1769
- let dir = start;
1770
- let result = false;
1771
- while (true) {
1772
- const packageJsonPath = path2.join(dir, "package.json");
1773
- if (existsSync(packageJsonPath)) {
1774
- try {
1775
- const packageJson = JSON.parse(readFileSync2(packageJsonPath, "utf8"));
1776
- if (typeof packageJson.name === "string" && READSEEK_REPO_PACKAGE_NAMES.has(packageJson.name)) {
1777
- result = true;
1778
- break;
1779
- }
1780
- } catch {}
2068
+ const oldLines = oldContent.split(`
2069
+ `);
2070
+ const newLines = newContent.split(`
2071
+ `);
2072
+ if (oldLines.length === newLines.length) {
2073
+ let hasWhitespaceChange2 = false;
2074
+ let hasSemanticChange2 = false;
2075
+ for (let i = 0;i < oldLines.length; i++) {
2076
+ if (oldLines[i] === newLines[i])
2077
+ continue;
2078
+ if (oldLines[i].trim() === newLines[i].trim()) {
2079
+ hasWhitespaceChange2 = true;
2080
+ } else {
2081
+ hasSemanticChange2 = true;
2082
+ }
1781
2083
  }
1782
- const parent = path2.dirname(dir);
1783
- if (parent === dir)
1784
- break;
1785
- dir = parent;
2084
+ if (hasSemanticChange2 && hasWhitespaceChange2)
2085
+ return { classification: "mixed" };
2086
+ if (hasWhitespaceChange2)
2087
+ return { classification: "whitespace-only" };
2088
+ return { classification: "semantic" };
1786
2089
  }
1787
- ownRepositoryByDir.set(start, result);
1788
- return result;
2090
+ const parts = Diff2.diffLines(oldContent, newContent);
2091
+ let hasWhitespaceChange = false;
2092
+ let hasSemanticChange = false;
2093
+ for (const part of parts) {
2094
+ if (!part.added && !part.removed)
2095
+ continue;
2096
+ const lines = part.value.split(`
2097
+ `);
2098
+ if (lines[lines.length - 1] === "")
2099
+ lines.pop();
2100
+ for (const line of lines) {
2101
+ if (line.trim() === "") {
2102
+ hasWhitespaceChange = true;
2103
+ } else {
2104
+ hasSemanticChange = true;
2105
+ }
2106
+ }
2107
+ }
2108
+ if (hasSemanticChange && hasWhitespaceChange)
2109
+ return { classification: "mixed" };
2110
+ if (hasWhitespaceChange)
2111
+ return { classification: "whitespace-only" };
2112
+ return { classification: "semantic" };
2113
+ }
2114
+
2115
+ // src/readseek-client.ts
2116
+ import { existsSync, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
2117
+ import { createRequire as createRequire2 } from "node:module";
2118
+ import { homedir as homedir3 } from "node:os";
2119
+ import path2 from "node:path";
2120
+
2121
+ // src/readseek/enums.ts
2122
+ var SymbolKind;
2123
+ ((SymbolKind2) => {
2124
+ SymbolKind2["Class"] = "class";
2125
+ SymbolKind2["Function"] = "function";
2126
+ SymbolKind2["Method"] = "method";
2127
+ SymbolKind2["Variable"] = "variable";
2128
+ SymbolKind2["Constant"] = "constant";
2129
+ SymbolKind2["Interface"] = "interface";
2130
+ SymbolKind2["Type"] = "type";
2131
+ SymbolKind2["Enum"] = "enum";
2132
+ SymbolKind2["Struct"] = "struct";
2133
+ SymbolKind2["Import"] = "import";
2134
+ SymbolKind2["Module"] = "module";
2135
+ SymbolKind2["Namespace"] = "namespace";
2136
+ SymbolKind2["Property"] = "property";
2137
+ SymbolKind2["Heading"] = "heading";
2138
+ SymbolKind2["Table"] = "table";
2139
+ SymbolKind2["View"] = "view";
2140
+ SymbolKind2["Procedure"] = "procedure";
2141
+ SymbolKind2["Trigger"] = "trigger";
2142
+ SymbolKind2["Index"] = "index";
2143
+ SymbolKind2["Schema"] = "schema";
2144
+ SymbolKind2["Signal"] = "signal";
2145
+ SymbolKind2["Unknown"] = "unknown";
2146
+ })(SymbolKind ||= {});
2147
+
2148
+ // src/readseek-settings.ts
2149
+ import { readFileSync, statSync } from "node:fs";
2150
+ import { homedir as homedir2 } from "node:os";
2151
+ import { join } from "node:path";
2152
+ var READSEEK_TOOL_REPLACEMENTS = {
2153
+ edit: "readSeek_edit",
2154
+ grep: "readSeek_grep",
2155
+ write: "readSeek_write"
2156
+ };
2157
+ var READSEEK_KEYS = ["overrideTools", "imageMode", "syntaxValidation", "timeoutMs", "grep", "display"];
2158
+ var READSEEK_GREP_KEYS = ["maxLines", "maxBytes"];
2159
+ var READSEEK_DISPLAY_KEYS = ["grep", "edit", "write"];
2160
+ var DEFAULT_DISPLAY_MODES = {
2161
+ grep: "compact",
2162
+ edit: "expanded",
2163
+ write: "expanded"
2164
+ };
2165
+ function globalSettingsPath() {
2166
+ return join(homedir2(), ".pi/agent/settings.json");
2167
+ }
2168
+ function projectSettingsPath() {
2169
+ return join(process.cwd(), ".pi/settings.json");
1789
2170
  }
1790
- function defaultReadSeekDir() {
1791
- const home = homedir3();
1792
- return home ? path2.join(home, ".pi", "readseek") : null;
2171
+ function isRecord(value) {
2172
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1793
2173
  }
1794
- var DEFAULT_READSEEK_VISION_TIMEOUT_MS = 30 * 60000;
1795
- function readSeekTimeoutMs() {
1796
- return resolveReadSeekTimeoutMs() ?? DEFAULT_READSEEK_TIMEOUT_MS;
2174
+ function invalid(source, path2) {
2175
+ return { source, path: path2, message: `Invalid readseek setting at ${path2}` };
1797
2176
  }
1798
- async function spawnReadSeekRaw(args, options = {}) {
1799
- return runReadSeekNodeText(readSeekBinaryPath(), args, {
1800
- signal: options.signal,
1801
- stdin: options.stdin,
1802
- timeoutMs: options.timeoutMs ?? readSeekTimeoutMs(),
1803
- cancelable: options.cancelable
1804
- });
2177
+ function warnUnknownKeys(raw, known, prefix, source, warnings) {
2178
+ for (const key of Object.keys(raw)) {
2179
+ if (known.includes(key))
2180
+ continue;
2181
+ const path2 = `${prefix}.${key}`;
2182
+ warnings.push({ source, path: path2, message: `Unknown readseek setting at ${path2}` });
2183
+ }
1805
2184
  }
1806
- async function ensureDefaultReadSeekDir() {
1807
- const dir = defaultReadSeekDir();
1808
- if (!dir)
1809
- return null;
1810
- if (directoryExists(dir))
1811
- return dir;
1812
- defaultReadSeekDirInit ??= spawnReadSeekRaw(["--readseek-dir", dir, "init"]).then(() => directoryExists(dir) ? dir : null).catch(() => null).finally(() => {
1813
- defaultReadSeekDirInit = undefined;
1814
- });
1815
- return defaultReadSeekDirInit;
2185
+ function readPositive(raw, key, path2, source, warnings) {
2186
+ if (!(key in raw))
2187
+ return;
2188
+ const val = raw[key];
2189
+ if (typeof val === "number" && Number.isSafeInteger(val) && val > 0)
2190
+ return val;
2191
+ warnings.push(invalid(source, path2));
2192
+ return;
1816
2193
  }
1817
- async function readSeekInvocationArgs(args) {
1818
- if (isOwnReadSeekRepository())
1819
- return args;
1820
- const readSeekDir = await ensureDefaultReadSeekDir();
1821
- return readSeekDir ? ["--readseek-dir", readSeekDir, ...args] : args;
2194
+ function readEnum(raw, key, values, path2, source, warnings) {
2195
+ if (!(key in raw))
2196
+ return;
2197
+ const val = raw[key];
2198
+ if (typeof val === "string" && values.includes(val))
2199
+ return val;
2200
+ warnings.push(invalid(source, path2));
2201
+ return;
1822
2202
  }
1823
- async function runReadSeekRaw(args, options = {}) {
1824
- return spawnReadSeekRaw(await readSeekInvocationArgs(args), options);
2203
+ function readImageMode(raw, source, warnings) {
2204
+ if (!("imageMode" in raw))
2205
+ return;
2206
+ const val = raw.imageMode;
2207
+ if (val === "on" || val === "off" || val === "auto")
2208
+ return val;
2209
+ warnings.push(invalid(source, "readseek.imageMode"));
2210
+ return;
1825
2211
  }
1826
- async function runReadSeek(args, options = {}) {
1827
- return runReadSeekNodeJson(readSeekBinaryPath(), await readSeekInvocationArgs(args), {
1828
- signal: options.signal,
1829
- stdin: options.stdin,
1830
- timeoutMs: options.timeoutMs ?? readSeekTimeoutMs(),
1831
- cancelable: options.cancelable
2212
+ function readOverrideTools(raw, source, warnings) {
2213
+ if (!("overrideTools" in raw))
2214
+ return;
2215
+ const value = raw.overrideTools;
2216
+ if (!Array.isArray(value)) {
2217
+ warnings.push(invalid(source, "readseek.overrideTools"));
2218
+ return;
2219
+ }
2220
+ const tools = [];
2221
+ value.forEach((tool, index) => {
2222
+ if (typeof tool === "string" && Object.hasOwn(READSEEK_TOOL_REPLACEMENTS, tool)) {
2223
+ tools.push(tool);
2224
+ } else {
2225
+ warnings.push(invalid(source, `readseek.overrideTools[${index}]`));
2226
+ }
1832
2227
  });
2228
+ return tools;
1833
2229
  }
1834
- async function readSeekView(filePath, options = {}) {
1835
- const args = buildViewArgs({
1836
- path: filePath,
1837
- node: options.node,
1838
- page: options.page,
1839
- kind: options.kind,
1840
- depth: options.depth,
1841
- outline: options.outline,
1842
- visionMode: options.visionMode,
1843
- visionLevel: options.visionLevel
1844
- });
1845
- const runOptions = { signal: options.signal };
1846
- return options.visionMode !== undefined ? runReadSeekVisionRaw(args, runOptions) : runReadSeekRaw(args, runOptions);
2230
+ function readDisplaySettings(raw, source, warnings) {
2231
+ if (!("display" in raw))
2232
+ return;
2233
+ if (!isRecord(raw.display)) {
2234
+ warnings.push(invalid(source, "readseek.display"));
2235
+ return;
2236
+ }
2237
+ warnUnknownKeys(raw.display, READSEEK_DISPLAY_KEYS, "readseek.display", source, warnings);
2238
+ const display = {};
2239
+ for (const tool of READSEEK_DISPLAY_KEYS) {
2240
+ const mode = readEnum(raw.display, tool, ["compact", "expanded"], `readseek.display.${tool}`, source, warnings);
2241
+ if (mode !== undefined)
2242
+ display[tool] = mode;
2243
+ }
2244
+ return Object.keys(display).length > 0 ? display : undefined;
1847
2245
  }
1848
- var visionInvocationTail = Promise.resolve();
1849
- async function runReadSeekVisionRaw(args, options = {}) {
1850
- let release;
1851
- const gate = new Promise((resolve) => {
1852
- release = resolve;
1853
- });
1854
- const predecessor = visionInvocationTail;
1855
- visionInvocationTail = predecessor.then(() => gate);
1856
- const signal = options.signal;
1857
- try {
1858
- if (signal) {
1859
- signal.throwIfAborted();
1860
- await new Promise((resolve, reject) => {
1861
- const onAbort = () => {
1862
- signal.removeEventListener("abort", onAbort);
1863
- reject(signal.reason);
1864
- };
1865
- signal.addEventListener("abort", onAbort, { once: true });
1866
- predecessor.then(() => {
1867
- signal.removeEventListener("abort", onAbort);
1868
- resolve();
1869
- }, (error) => {
1870
- signal.removeEventListener("abort", onAbort);
1871
- reject(error);
1872
- });
2246
+ function validateSettings(raw, source) {
2247
+ const settings = {};
2248
+ const warnings = [];
2249
+ if (!isRecord(raw)) {
2250
+ warnings.push({ source, message: "Invalid readseek settings: expected a JSON object" });
2251
+ return { settings, warnings };
2252
+ }
2253
+ if (!("readseek" in raw)) {
2254
+ const misplaced = READSEEK_KEYS.filter((key) => (key in raw));
2255
+ if (misplaced.length > 0) {
2256
+ warnings.push({
2257
+ source,
2258
+ path: misplaced[0],
2259
+ message: `Readseek setting at top level: move ${misplaced.join(", ")} under "readseek"`
1873
2260
  });
1874
- } else {
1875
- await predecessor;
1876
2261
  }
1877
- signal?.throwIfAborted();
1878
- return await runReadSeekRaw(args, {
1879
- ...options,
1880
- timeoutMs: options.timeoutMs ?? resolveReadSeekTimeoutMs() ?? DEFAULT_READSEEK_VISION_TIMEOUT_MS
1881
- });
1882
- } finally {
1883
- release();
2262
+ return { settings, warnings };
1884
2263
  }
2264
+ if (!isRecord(raw.readseek)) {
2265
+ warnings.push(invalid(source, "readseek"));
2266
+ return { settings, warnings };
2267
+ }
2268
+ const section = raw.readseek;
2269
+ warnUnknownKeys(section, READSEEK_KEYS, "readseek", source, warnings);
2270
+ const overrideTools = readOverrideTools(section, source, warnings);
2271
+ if (overrideTools !== undefined)
2272
+ settings.overrideTools = overrideTools;
2273
+ const imageMode = readImageMode(section, source, warnings);
2274
+ if (imageMode !== undefined)
2275
+ settings.imageMode = imageMode;
2276
+ const syntaxValidation = readEnum(section, "syntaxValidation", ["warn", "block", "off"], "readseek.syntaxValidation", source, warnings);
2277
+ if (syntaxValidation !== undefined)
2278
+ settings.syntaxValidation = syntaxValidation;
2279
+ const timeoutMs = readPositive(section, "timeoutMs", "readseek.timeoutMs", source, warnings);
2280
+ if (timeoutMs !== undefined)
2281
+ settings.timeoutMs = timeoutMs;
2282
+ if ("grep" in section) {
2283
+ if (isRecord(section.grep)) {
2284
+ warnUnknownKeys(section.grep, READSEEK_GREP_KEYS, "readseek.grep", source, warnings);
2285
+ const grep = {};
2286
+ const maxLines = readPositive(section.grep, "maxLines", "readseek.grep.maxLines", source, warnings);
2287
+ if (maxLines !== undefined)
2288
+ grep.maxLines = maxLines;
2289
+ const maxBytes = readPositive(section.grep, "maxBytes", "readseek.grep.maxBytes", source, warnings);
2290
+ if (maxBytes !== undefined)
2291
+ grep.maxBytes = maxBytes;
2292
+ if (Object.keys(grep).length > 0)
2293
+ settings.grep = grep;
2294
+ } else {
2295
+ warnings.push(invalid(source, "readseek.grep"));
2296
+ }
2297
+ }
2298
+ const display = readDisplaySettings(section, source, warnings);
2299
+ if (display !== undefined)
2300
+ settings.display = display;
2301
+ return { settings, warnings };
1885
2302
  }
1886
- async function runReadSeekVision(args, options = {}) {
1887
- return JSON.parse(await runReadSeekVisionRaw(args, options));
1888
- }
1889
- function requireNumber(value, field) {
1890
- if (typeof value !== "number" || !Number.isSafeInteger(value))
1891
- throw new Error(`invalid readseek ${field}: expected safe integer`);
1892
- return value;
1893
- }
1894
- function requireString(value, field) {
1895
- if (typeof value !== "string")
1896
- throw new Error(`invalid readseek ${field}`);
1897
- return value;
1898
- }
1899
- function requireBoolean(value, field) {
1900
- if (typeof value !== "boolean")
1901
- throw new Error(`invalid readseek ${field}: expected boolean`);
1902
- return value;
1903
- }
1904
- function parseMapOutput(value) {
1905
- if (!value || typeof value !== "object")
1906
- throw new Error("invalid readseek map output");
1907
- const output = value;
1908
- const symbols = output.symbols;
1909
- if (!Array.isArray(symbols))
1910
- throw new Error("invalid readseek symbols");
1911
- return {
1912
- file: requireString(output.file, "file"),
1913
- language: requireString(output.language, "language"),
1914
- line_count: requireNumber(output.line_count, "line_count"),
1915
- file_hash: requireString(output.file_hash, "file_hash"),
1916
- symbols: symbols.map((symbol) => {
1917
- if (!symbol || typeof symbol !== "object")
1918
- throw new Error("invalid readseek symbol");
1919
- const item = symbol;
1920
- return {
1921
- kind: requireString(item.kind, "symbol.kind"),
1922
- name: requireString(item.name, "symbol.name"),
1923
- qualified_name: requireString(item.qualified_name, "symbol.qualified_name"),
1924
- start_line: requireNumber(item.start_line, "symbol.start_line"),
1925
- end_line: requireNumber(item.end_line, "symbol.end_line"),
1926
- start_hash: requireString(item.start_hash, "symbol.start_hash"),
1927
- end_hash: requireString(item.end_hash, "symbol.end_hash")
1928
- };
1929
- })
1930
- };
1931
- }
1932
- function parseHashline(value, field) {
1933
- if (!value || typeof value !== "object")
1934
- throw new Error(`invalid readseek ${field}`);
1935
- const item = value;
1936
- return {
1937
- line: requireNumber(item.line, `${field}.line`),
1938
- hash: requireString(item.hash, `${field}.hash`),
1939
- text: requireString(item.text, `${field}.text`)
1940
- };
1941
- }
1942
- function parseSearchHashlines(value, field) {
1943
- if (!Array.isArray(value))
1944
- throw new Error(`invalid readseek ${field}`);
1945
- return value.map((line) => parseHashline(line, field));
1946
- }
1947
- function parseSearchOutput(value) {
1948
- if (!value || typeof value !== "object")
1949
- throw new Error("invalid readseek search output");
1950
- const output = value;
1951
- if (!Array.isArray(output.results))
1952
- throw new Error("invalid readseek search results");
1953
- return {
1954
- results: output.results.map((result) => {
1955
- if (!result || typeof result !== "object")
1956
- throw new Error("invalid readseek search result");
1957
- const file = result;
1958
- if (!Array.isArray(file.matches))
1959
- throw new Error("invalid readseek search matches");
1960
- return {
1961
- file: requireString(file.file, "search.file"),
1962
- language: requireString(file.language, "search.language"),
1963
- file_hash: requireString(file.file_hash, "search.file_hash"),
1964
- matches: file.matches.map((match) => {
1965
- if (!match || typeof match !== "object")
1966
- throw new Error("invalid readseek search match");
1967
- const item = match;
1968
- if (!Array.isArray(item.captures))
1969
- throw new Error("invalid readseek search captures");
1970
- return {
1971
- pattern_index: item.pattern_index === undefined ? 0 : requireNumber(item.pattern_index, "search.match.pattern_index"),
1972
- start_line: requireNumber(item.start_line, "search.match.start_line"),
1973
- end_line: requireNumber(item.end_line, "search.match.end_line"),
1974
- start_hash: requireString(item.start_hash, "search.match.start_hash"),
1975
- end_hash: requireString(item.end_hash, "search.match.end_hash"),
1976
- hashlines: parseSearchHashlines(item.hashlines, "search.match.hashlines"),
1977
- captures: item.captures.map((capture) => {
1978
- if (!capture || typeof capture !== "object")
1979
- throw new Error("invalid readseek search capture");
1980
- const captureItem = capture;
1981
- return {
1982
- name: requireString(captureItem.name, "search.capture.name"),
1983
- start_line: requireNumber(captureItem.start_line, "search.capture.start_line"),
1984
- end_line: requireNumber(captureItem.end_line, "search.capture.end_line"),
1985
- start_hash: requireString(captureItem.start_hash, "search.capture.start_hash"),
1986
- end_hash: requireString(captureItem.end_hash, "search.capture.end_hash"),
1987
- hashlines: parseSearchHashlines(captureItem.hashlines, "search.capture.hashlines")
1988
- };
1989
- })
1990
- };
1991
- })
1992
- };
1993
- })
1994
- };
1995
- }
1996
- function optionalString(value, field) {
1997
- if (value === undefined || value === null)
1998
- return;
1999
- return requireString(value, field);
2303
+ var settingsFileCache = new Map;
2304
+ function statFileOrNull(path2) {
2305
+ try {
2306
+ return statSync(path2);
2307
+ } catch {
2308
+ return null;
2309
+ }
2000
2310
  }
2001
- function parseRefsOutput(value) {
2002
- if (!value || typeof value !== "object")
2003
- throw new Error("invalid readseek refs output");
2004
- const output = value;
2005
- if (!Array.isArray(output.references))
2006
- throw new Error("invalid readseek references");
2007
- return {
2008
- references: output.references.map((reference) => {
2009
- if (!reference || typeof reference !== "object")
2010
- throw new Error("invalid readseek reference");
2011
- const item = reference;
2012
- const symbol = item.symbol;
2013
- const enclosing = symbol && typeof symbol === "object" ? optionalString(symbol.qualified_name, "reference.symbol.qualified_name") : undefined;
2014
- return {
2015
- file: requireString(item.file, "reference.file"),
2016
- line: requireNumber(item.line, "reference.line"),
2017
- column: requireNumber(item.column, "reference.column"),
2018
- line_hash: requireString(item.line_hash, "reference.line_hash"),
2019
- text: requireString(item.text, "reference.text"),
2020
- enclosingSymbol: enclosing
2021
- };
2022
- })
2023
- };
2311
+ function readSettingsFile(path2) {
2312
+ const stats = statFileOrNull(path2);
2313
+ if (!stats) {
2314
+ settingsFileCache.delete(path2);
2315
+ return { settings: {}, warnings: [] };
2316
+ }
2317
+ const cached = settingsFileCache.get(path2);
2318
+ if (cached && cached.mtimeMs === stats.mtimeMs && cached.size === stats.size)
2319
+ return cached.result;
2320
+ let result;
2321
+ try {
2322
+ const text = readFileSync(path2, "utf8");
2323
+ result = validateSettings(JSON.parse(text), path2);
2324
+ } catch (error) {
2325
+ const message = error instanceof Error ? error.message : String(error);
2326
+ result = { settings: {}, warnings: [{ source: path2, message: `Invalid JSON: ${message}` }] };
2327
+ }
2328
+ settingsFileCache.set(path2, { mtimeMs: stats.mtimeMs, size: stats.size, result });
2329
+ return result;
2024
2330
  }
2025
- function parseDiagnosticKind(value) {
2026
- if (value === "error" || value === "missing")
2027
- return value;
2028
- throw new Error("invalid readseek diagnostic.kind");
2331
+ function mergeSettings(base, override) {
2332
+ const settings = { ...base, ...override };
2333
+ const grep = { ...base.grep ?? {}, ...override.grep ?? {} };
2334
+ if (Object.keys(grep).length > 0)
2335
+ settings.grep = grep;
2336
+ const display = { ...base.display ?? {}, ...override.display ?? {} };
2337
+ if (Object.keys(display).length > 0)
2338
+ settings.display = display;
2339
+ return settings;
2029
2340
  }
2030
- function parseCheckOutput(value) {
2031
- if (!value || typeof value !== "object")
2032
- throw new Error("invalid readseek check output");
2033
- const output = value;
2034
- if (!Array.isArray(output.diagnostics))
2035
- throw new Error("invalid readseek diagnostics");
2341
+ function resolveReadSeekJsonSettings() {
2342
+ const globalResult = readSettingsFile(globalSettingsPath());
2343
+ const projectResult = readSettingsFile(projectSettingsPath());
2036
2344
  return {
2037
- errorCount: requireNumber(output.error_count, "error_count"),
2038
- missingCount: requireNumber(output.missing_count, "missing_count"),
2039
- diagnostics: output.diagnostics.map((diagnostic) => {
2040
- if (!diagnostic || typeof diagnostic !== "object")
2041
- throw new Error("invalid readseek diagnostic");
2042
- const item = diagnostic;
2043
- return {
2044
- kind: parseDiagnosticKind(item.kind),
2045
- start_line: requireNumber(item.start_line, "diagnostic.start_line"),
2046
- end_line: requireNumber(item.end_line, "diagnostic.end_line")
2047
- };
2048
- })
2345
+ settings: mergeSettings(globalResult.settings, projectResult.settings),
2346
+ warnings: [...globalResult.warnings, ...projectResult.warnings]
2049
2347
  };
2050
2348
  }
2051
- function parseOcrText(value) {
2052
- return optionalString(value, "ocr");
2349
+ function resolveReadSeekSyntaxValidation() {
2350
+ return resolveReadSeekJsonSettings().settings.syntaxValidation;
2053
2351
  }
2054
- function parseDetectedObjects(value) {
2055
- if (value === undefined || value === null)
2056
- return;
2057
- if (!Array.isArray(value))
2058
- throw new Error("invalid readseek detect objects");
2059
- return value.map((object) => {
2060
- if (!object || typeof object !== "object")
2061
- throw new Error("invalid readseek detect object");
2062
- const item = object;
2063
- const bbox = item.bbox;
2064
- if (!Array.isArray(bbox) || bbox.length !== 4)
2065
- throw new Error("invalid readseek detect object.bbox");
2066
- return {
2067
- label: requireString(item.label, "object.label"),
2068
- bbox: bbox.map((n, i) => requireNumber(n, `object.bbox[${i}]`))
2069
- };
2070
- });
2352
+ function resolveReadSeekTimeoutMs() {
2353
+ return resolveReadSeekJsonSettings().settings.timeoutMs;
2071
2354
  }
2072
- function parseImageEncoding(value) {
2073
- if (value === "base64")
2074
- return value;
2075
- throw new Error("invalid image encoding");
2355
+ function resolveReadSeekToolDisplayMode(tool) {
2356
+ return resolveReadSeekJsonSettings().settings.display?.[tool] ?? DEFAULT_DISPLAY_MODES[tool];
2076
2357
  }
2077
- function parseDetectOutput(value) {
2078
- if (!value || typeof value !== "object")
2079
- throw new Error("invalid readseek detect output");
2080
- const output = value;
2081
- const type = requireString(output.type, "type");
2082
- const file = requireString(output.file, "file");
2083
- const mime = optionalString(output.mime, "mime");
2084
- if (type === "application/pdf") {
2085
- if (output.format !== "pdf")
2086
- throw new Error("invalid readseek PDF format");
2087
- return {
2088
- kind: "pdf",
2089
- type,
2090
- file,
2091
- mime,
2092
- format: output.format,
2093
- pages: requireNumber(output.pages, "pages")
2094
- };
2095
- }
2096
- if (output.width !== undefined || output.height !== undefined) {
2097
- return {
2098
- kind: "image",
2099
- type,
2100
- file,
2101
- mime,
2102
- format: requireString(output.format, "format"),
2103
- width: requireNumber(output.width, "width"),
2104
- height: requireNumber(output.height, "height"),
2105
- animated: requireBoolean(output.animated, "animated"),
2106
- encoding: output.encoding === undefined ? undefined : parseImageEncoding(output.encoding),
2107
- data: output.data === undefined ? undefined : requireString(output.data, "data"),
2108
- ocr: parseOcrText(output.ocr),
2109
- caption: optionalString(output.caption, "caption"),
2110
- objects: parseDetectedObjects(output.objects)
2111
- };
2112
- }
2113
- if (output.language !== undefined) {
2114
- return {
2115
- kind: "source",
2116
- type,
2117
- file,
2118
- language: requireString(output.language, "language"),
2119
- engine: optionalString(output.engine, "engine"),
2120
- supported: requireBoolean(output.supported, "supported"),
2121
- mime,
2122
- syntax: optionalString(output.syntax, "syntax")
2123
- };
2124
- }
2125
- return { kind: "text", type, file, mime };
2358
+
2359
+ // src/readseek-client.ts
2360
+ var resolveFromHost = createRequire2(import.meta.url).resolve;
2361
+ function readSeekBinaryPath() {
2362
+ return resolveReadSeekBinaryFromPath() ?? resolveReadSeekBinaryFromPackages(resolveFromHost);
2126
2363
  }
2127
- function parseDocumentCache(value) {
2128
- if (!value || typeof value !== "object")
2129
- throw new Error("invalid readseek document cache");
2130
- const cache = value;
2131
- const state = requireString(cache.state, "cache.state");
2132
- if (state !== "built" && state !== "reused")
2133
- throw new Error("invalid readseek cache.state");
2134
- return {
2135
- content_hash: requireString(cache.content_hash, "cache.content_hash"),
2136
- state
2137
- };
2364
+ function readSeekBinaryAvailability() {
2365
+ try {
2366
+ readSeekBinaryPath();
2367
+ return { available: true };
2368
+ } catch (err) {
2369
+ return { available: false, reason: classifyReadSeekFailure(err).message };
2370
+ }
2138
2371
  }
2139
- function parseIdentifyOutput(value) {
2140
- if (!value || typeof value !== "object")
2141
- throw new Error("invalid readseek identify output");
2142
- const output = value;
2143
- const identifier = output.identifier;
2144
- const symbol = output.symbol;
2145
- return {
2146
- file: requireString(output.file, "file"),
2147
- language: requireString(output.language, "language"),
2148
- engine: optionalString(output.engine, "engine"),
2149
- line_count: requireNumber(output.line_count, "line_count"),
2150
- file_hash: requireString(output.file_hash, "file_hash"),
2151
- line: requireNumber(output.line, "line"),
2152
- column: requireNumber(output.column, "column"),
2153
- line_hash: requireString(output.line_hash, "line_hash"),
2154
- identifier: identifier && typeof identifier === "object" ? {
2155
- text: requireString(identifier.text, "identifier.text"),
2156
- start_column: requireNumber(identifier.start_column, "identifier.start_column"),
2157
- end_column: requireNumber(identifier.end_column, "identifier.end_column"),
2158
- start_byte: requireNumber(identifier.start_byte, "identifier.start_byte"),
2159
- end_byte: requireNumber(identifier.end_byte, "identifier.end_byte")
2160
- } : undefined,
2161
- symbol: symbol && typeof symbol === "object" ? {
2162
- name: requireString(symbol.name, "symbol.name"),
2163
- kind: requireString(symbol.kind, "symbol.kind"),
2164
- qualified_name: requireString(symbol.qualified_name, "symbol.qualified_name"),
2165
- start_line: requireNumber(symbol.start_line, "symbol.start_line"),
2166
- end_line: requireNumber(symbol.end_line, "symbol.end_line")
2167
- } : undefined
2168
- };
2372
+ var READSEEK_REPO_PACKAGE_NAMES = new Set(["@jarkkojs/readseek", "readseek"]);
2373
+ var defaultReadSeekDirInit;
2374
+ function directoryExists(dirPath) {
2375
+ try {
2376
+ return statSync2(dirPath).isDirectory();
2377
+ } catch {
2378
+ return false;
2379
+ }
2169
2380
  }
2170
- function parseDigestEnvelope(value) {
2171
- if (!value || typeof value !== "object")
2172
- throw new Error("invalid readseek digest output");
2173
- const output = value;
2174
- if (output.metadata === undefined)
2175
- throw new Error("invalid readseek digest metadata");
2176
- const result = {
2177
- metadata: parseDetectOutput(output.metadata)
2178
- };
2179
- if (output.cache !== undefined)
2180
- result.cache = parseDocumentCache(output.cache);
2181
- if (output.content !== undefined)
2182
- result.content = output.content;
2183
- if (output.map !== undefined)
2184
- result.map = parseMapOutput(output.map);
2185
- if (output.diagnostics !== undefined)
2186
- result.diagnostics = parseCheckOutput(output.diagnostics);
2187
- if (output.identity !== undefined)
2188
- result.identity = parseIdentifyOutput(output.identity);
2381
+ var ownRepositoryByDir = new Map;
2382
+ function isOwnReadSeekRepository(cwd = process.cwd()) {
2383
+ const start = path2.resolve(cwd);
2384
+ const cached = ownRepositoryByDir.get(start);
2385
+ if (cached !== undefined)
2386
+ return cached;
2387
+ let dir = start;
2388
+ let result = false;
2389
+ while (true) {
2390
+ const packageJsonPath = path2.join(dir, "package.json");
2391
+ if (existsSync(packageJsonPath)) {
2392
+ try {
2393
+ const packageJson = JSON.parse(readFileSync2(packageJsonPath, "utf8"));
2394
+ if (typeof packageJson.name === "string" && READSEEK_REPO_PACKAGE_NAMES.has(packageJson.name)) {
2395
+ result = true;
2396
+ break;
2397
+ }
2398
+ } catch {}
2399
+ }
2400
+ const parent = path2.dirname(dir);
2401
+ if (parent === dir)
2402
+ break;
2403
+ dir = parent;
2404
+ }
2405
+ ownRepositoryByDir.set(start, result);
2189
2406
  return result;
2190
2407
  }
2191
- function digestArgs(target, options) {
2192
- return buildDigestArgs({
2193
- path: target,
2194
- select: options.select ?? "content",
2195
- at: options.at,
2196
- end: options.end,
2197
- limit: options.limit,
2198
- language: options.language,
2199
- visionMode: options.visionMode,
2200
- visionLevel: options.visionLevel,
2201
- stdinName: options.stdin !== undefined && target === "-" ? options.stdinName ?? target : undefined
2408
+ function defaultReadSeekDir() {
2409
+ const home = homedir3();
2410
+ return home ? path2.join(home, ".pi", "readseek") : null;
2411
+ }
2412
+ var DEFAULT_READSEEK_VISION_TIMEOUT_MS = 30 * 60000;
2413
+ function readSeekTimeoutMs() {
2414
+ return resolveReadSeekTimeoutMs() ?? DEFAULT_READSEEK_TIMEOUT_MS;
2415
+ }
2416
+ async function spawnReadSeekRaw(args, options = {}) {
2417
+ return runReadSeekNodeText(readSeekBinaryPath(), args, {
2418
+ signal: options.signal,
2419
+ stdin: options.stdin,
2420
+ timeoutMs: options.timeoutMs ?? readSeekTimeoutMs(),
2421
+ cancelable: options.cancelable
2422
+ });
2423
+ }
2424
+ async function ensureDefaultReadSeekDir() {
2425
+ const dir = defaultReadSeekDir();
2426
+ if (!dir)
2427
+ return null;
2428
+ if (directoryExists(dir))
2429
+ return dir;
2430
+ defaultReadSeekDirInit ??= spawnReadSeekRaw(["--readseek-dir", dir, "init"]).then(() => directoryExists(dir) ? dir : null).catch(() => null).finally(() => {
2431
+ defaultReadSeekDirInit = undefined;
2202
2432
  });
2433
+ return defaultReadSeekDirInit;
2434
+ }
2435
+ async function readSeekInvocationArgs(args) {
2436
+ if (isOwnReadSeekRepository())
2437
+ return [...args];
2438
+ const readSeekDir = await ensureDefaultReadSeekDir();
2439
+ return readSeekDir ? ["--readseek-dir", readSeekDir, ...args] : [...args];
2440
+ }
2441
+ async function runReadSeekRaw(args, options = {}) {
2442
+ return spawnReadSeekRaw(await readSeekInvocationArgs(args), options);
2203
2443
  }
2204
- async function runDigestRaw(target, options = {}) {
2205
- const args = digestArgs(target, options);
2206
- const runOptions = {
2444
+ async function runReadSeek(args, options = {}) {
2445
+ return runReadSeekNodeJson(readSeekBinaryPath(), await readSeekInvocationArgs(args), {
2207
2446
  signal: options.signal,
2208
2447
  stdin: options.stdin,
2209
- timeoutMs: options.timeoutMs
2210
- };
2211
- return options.vision ? await runReadSeekVision(args, runOptions) : await runReadSeek(args, runOptions);
2448
+ timeoutMs: options.timeoutMs ?? readSeekTimeoutMs(),
2449
+ cancelable: options.cancelable
2450
+ });
2451
+ }
2452
+ var visionInvocationTail = Promise.resolve();
2453
+ async function runReadSeekVisionRaw(args, options = {}) {
2454
+ let release;
2455
+ const gate = new Promise((resolve) => {
2456
+ release = resolve;
2457
+ });
2458
+ const predecessor = visionInvocationTail;
2459
+ visionInvocationTail = predecessor.then(() => gate);
2460
+ const signal = options.signal;
2461
+ try {
2462
+ if (signal) {
2463
+ signal.throwIfAborted();
2464
+ await new Promise((resolve, reject) => {
2465
+ const onAbort = () => {
2466
+ signal.removeEventListener("abort", onAbort);
2467
+ reject(signal.reason);
2468
+ };
2469
+ signal.addEventListener("abort", onAbort, { once: true });
2470
+ predecessor.then(() => {
2471
+ signal.removeEventListener("abort", onAbort);
2472
+ resolve();
2473
+ }, (error) => {
2474
+ signal.removeEventListener("abort", onAbort);
2475
+ reject(error);
2476
+ });
2477
+ });
2478
+ } else {
2479
+ await predecessor;
2480
+ }
2481
+ signal?.throwIfAborted();
2482
+ return await runReadSeekRaw(args, {
2483
+ ...options,
2484
+ timeoutMs: options.timeoutMs ?? resolveReadSeekTimeoutMs() ?? DEFAULT_READSEEK_VISION_TIMEOUT_MS
2485
+ });
2486
+ } finally {
2487
+ release();
2488
+ }
2489
+ }
2490
+ async function runReadSeekVision(args, options = {}) {
2491
+ return JSON.parse(await runReadSeekVisionRaw(args, options));
2492
+ }
2493
+ var runner = {
2494
+ text: (args, options = {}) => options.vision ? runReadSeekVisionRaw(args, options) : runReadSeekRaw(args, options),
2495
+ json: (args, options = {}) => options.vision ? runReadSeekVision(args, options) : runReadSeek(args, options)
2496
+ };
2497
+ var client = createReadSeekClient(runner);
2498
+ var readSeekView = client.view;
2499
+ var readSeekDigestNative = client.digestRaw;
2500
+ var readSeekDigest = client.digest;
2501
+ var readSeekDigestContent = client.digestContent;
2502
+ var readSeekImage = client.image;
2503
+ var readSeekPreparedImage = client.preparedImage;
2504
+ var readSeekSearch = client.search;
2505
+ var readSeekRefs = client.refs;
2506
+ var readSeekDef = client.def;
2507
+ var readSeekCheck = client.check;
2508
+ var readSeekIdentify = client.identify;
2509
+ var readSeekDetect = client.detect;
2510
+ var readSeekEdit = client.edit;
2511
+ var readSeekRename = client.rename;
2512
+ function normalizeLanguage(language) {
2513
+ return language === "java" ? "Java" : language;
2514
+ }
2515
+ function normalizeKind(kind) {
2516
+ if (kind === "constructor")
2517
+ return "method" /* Method */;
2518
+ if (Object.values(SymbolKind).includes(kind))
2519
+ return kind;
2520
+ return "unknown" /* Unknown */;
2212
2521
  }
2213
- async function readSeekDigestNative(target, options = {}) {
2214
- return runDigestRaw(target, options);
2522
+ function parentQualifiedNameFor(qualifiedName) {
2523
+ const lastDot = qualifiedName.lastIndexOf(".");
2524
+ return lastDot === -1 ? "" : qualifiedName.slice(0, lastDot);
2215
2525
  }
2216
- async function readSeekDigest(target, options = {}) {
2217
- return parseDigestEnvelope(await runDigestRaw(target, options));
2526
+ function symbolsFromReadSeek(symbols) {
2527
+ const symbolsByQualifiedName = new Map;
2528
+ const entries = [];
2529
+ for (const symbol of symbols) {
2530
+ const parentQualifiedName = parentQualifiedNameFor(symbol.qualified_name);
2531
+ const fileSymbol = {
2532
+ name: symbol.name,
2533
+ kind: normalizeKind(symbol.kind),
2534
+ startLine: symbol.start_line,
2535
+ endLine: symbol.end_line
2536
+ };
2537
+ const bucket = symbolsByQualifiedName.get(symbol.qualified_name);
2538
+ if (bucket)
2539
+ bucket.push(fileSymbol);
2540
+ else
2541
+ symbolsByQualifiedName.set(symbol.qualified_name, [fileSymbol]);
2542
+ entries.push({ parentQualifiedName, symbol: fileSymbol });
2543
+ }
2544
+ const roots = [];
2545
+ for (const entry of entries) {
2546
+ const parent = entry.parentQualifiedName ? symbolsByQualifiedName.get(entry.parentQualifiedName)?.[0] : undefined;
2547
+ if (!parent) {
2548
+ roots.push(entry.symbol);
2549
+ continue;
2550
+ }
2551
+ parent.children ??= [];
2552
+ parent.children.push(entry.symbol);
2553
+ }
2554
+ return roots;
2218
2555
  }
2219
2556
  function fileMapFromReadSeekOutput(output, filePath, totalBytes) {
2220
2557
  if (output.language === "unknown" && output.symbols.length === 0)
@@ -2229,196 +2566,11 @@ function fileMapFromReadSeekOutput(output, filePath, totalBytes) {
2229
2566
  };
2230
2567
  }
2231
2568
  async function readSeekMap(filePath, totalBytes, options = {}) {
2232
- const envelope = await readSeekDigest(filePath, { select: "map", signal: options.signal });
2569
+ const envelope = await client.digest(filePath, { select: "map", signal: options.signal });
2233
2570
  if (!envelope.map)
2234
2571
  return null;
2235
2572
  return fileMapFromReadSeekOutput(envelope.map, filePath, totalBytes);
2236
2573
  }
2237
- async function readSeekSearch(target, pattern, options = {}) {
2238
- const args = buildSearchArgs({
2239
- path: target,
2240
- pattern,
2241
- language: options.language,
2242
- cached: options.cached,
2243
- others: options.others,
2244
- ignored: options.ignored
2245
- });
2246
- return parseSearchOutput(await runReadSeek(args, { signal: options.signal })).results;
2247
- }
2248
- async function readSeekRefs(target, options = {}) {
2249
- const args = buildRefsArgs({
2250
- path: target,
2251
- name: options.name,
2252
- scope: options.scope,
2253
- line: options.line,
2254
- column: options.column,
2255
- language: options.language,
2256
- cached: options.cached,
2257
- others: options.others,
2258
- ignored: options.ignored
2259
- });
2260
- return parseRefsOutput(await runReadSeek(args, { signal: options.signal })).references;
2261
- }
2262
- async function readSeekCheck(filePath, content, options = {}) {
2263
- const envelope = await readSeekDigest("-", {
2264
- select: "diagnostics",
2265
- stdin: content,
2266
- stdinName: filePath,
2267
- signal: options.signal
2268
- });
2269
- if (!envelope.diagnostics)
2270
- throw new Error("invalid readseek diagnostics: missing diagnostics facet");
2271
- return envelope.diagnostics;
2272
- }
2273
- function parseEditOutput(value) {
2274
- if (!value || typeof value !== "object")
2275
- throw new Error("invalid readseek edit output");
2276
- const output = value;
2277
- return {
2278
- file: requireString(output.file, "file"),
2279
- before_hash: requireString(output.before_hash, "before_hash"),
2280
- file_hash: requireString(output.file_hash, "file_hash"),
2281
- plan_hash: requireString(output.plan_hash, "plan_hash"),
2282
- applied: requireBoolean(output.applied, "applied"),
2283
- changed: requireBoolean(output.changed, "changed"),
2284
- edit_count: requireNumber(output.edit_count, "edit_count"),
2285
- before_content: optionalString(output.before_content, "before_content"),
2286
- content: optionalString(output.content, "content")
2287
- };
2288
- }
2289
- async function readSeekEdit(filePath, edits, options = {}) {
2290
- const args = buildEditArgs({
2291
- path: filePath,
2292
- apply: options.apply,
2293
- planHash: options.planHash,
2294
- language: options.language
2295
- });
2296
- return parseEditOutput(await runReadSeek(args, {
2297
- signal: options.signal,
2298
- stdin: JSON.stringify({ edits }),
2299
- timeoutMs: options.timeoutMs,
2300
- cancelable: options.cancelable
2301
- }));
2302
- }
2303
- function parseRenameConflicts(value, field) {
2304
- if (value === undefined)
2305
- return [];
2306
- if (!Array.isArray(value))
2307
- throw new Error(`invalid readseek ${field}`);
2308
- return value.map((item) => {
2309
- if (!item || typeof item !== "object")
2310
- throw new Error(`invalid readseek ${field} entry`);
2311
- const c = item;
2312
- return {
2313
- line: requireNumber(c.line, `${field}.line`),
2314
- column: requireNumber(c.column, `${field}.column`),
2315
- reason: requireString(c.reason, `${field}.reason`)
2316
- };
2317
- });
2318
- }
2319
- function parseRenameEdits(value, field) {
2320
- if (value === undefined)
2321
- return [];
2322
- if (!Array.isArray(value))
2323
- throw new Error(`invalid readseek ${field}`);
2324
- return value.map((item) => {
2325
- if (!item || typeof item !== "object")
2326
- throw new Error(`invalid readseek ${field} entry`);
2327
- const e = item;
2328
- return {
2329
- line: requireNumber(e.line, `${field}.line`),
2330
- start_column: requireNumber(e.start_column, `${field}.start_column`),
2331
- end_column: requireNumber(e.end_column, `${field}.end_column`),
2332
- start_byte: requireNumber(e.start_byte, `${field}.start_byte`),
2333
- end_byte: requireNumber(e.end_byte, `${field}.end_byte`),
2334
- occurrence: requireString(e.occurrence, `${field}.occurrence`),
2335
- line_hash: requireString(e.line_hash, `${field}.line_hash`),
2336
- text: requireString(e.text, `${field}.text`)
2337
- };
2338
- });
2339
- }
2340
- function parseRenameOutput(value) {
2341
- if (!value || typeof value !== "object")
2342
- throw new Error("invalid readseek rename output");
2343
- const output = value;
2344
- const others = output.others;
2345
- if (others !== undefined && !Array.isArray(others))
2346
- throw new Error("invalid readseek others");
2347
- return {
2348
- file: requireString(output.file, "file"),
2349
- language: requireString(output.language, "language"),
2350
- engine: optionalString(output.engine, "engine"),
2351
- file_hash: requireString(output.file_hash, "file_hash"),
2352
- old_name: requireString(output.old_name, "old_name"),
2353
- new_name: requireString(output.new_name, "new_name"),
2354
- applied: requireBoolean(output.applied, "applied"),
2355
- conflicts: parseRenameConflicts(output.conflicts, "conflicts"),
2356
- edits: parseRenameEdits(output.edits, "edits"),
2357
- others: others?.map((entry) => {
2358
- if (!entry || typeof entry !== "object")
2359
- throw new Error("invalid readseek other");
2360
- const o = entry;
2361
- return {
2362
- file: requireString(o.file, "other.file"),
2363
- language: requireString(o.language, "other.language"),
2364
- engine: optionalString(o.engine, "other.engine"),
2365
- file_hash: requireString(o.file_hash, "other.file_hash"),
2366
- conflicts: parseRenameConflicts(o.conflicts, "other.conflicts"),
2367
- edits: parseRenameEdits(o.edits, "other.edits")
2368
- };
2369
- }) ?? []
2370
- };
2371
- }
2372
- async function readSeekRename(filePath, options) {
2373
- const args = buildRenameArgs({
2374
- path: filePath,
2375
- to: options.to,
2376
- line: options.line,
2377
- column: options.column,
2378
- workspace: options.workspace,
2379
- apply: options.apply,
2380
- language: options.language,
2381
- cached: options.cached,
2382
- others: options.others,
2383
- ignored: options.ignored
2384
- });
2385
- return parseRenameOutput(await runReadSeek(args, { signal: options.signal }));
2386
- }
2387
- function parseDefCompact(value) {
2388
- if (!value || typeof value !== "object")
2389
- throw new Error("invalid readseek def output");
2390
- const output = value;
2391
- const locations = output.locations;
2392
- if (!Array.isArray(locations))
2393
- throw new Error("invalid readseek def locations");
2394
- return locations.map((loc) => {
2395
- if (!loc || typeof loc !== "object")
2396
- throw new Error("invalid readseek def location");
2397
- const item = loc;
2398
- return {
2399
- file: requireString(item.file, "location.file"),
2400
- line: requireNumber(item.line, "location.line"),
2401
- column: requireNumber(item.column, "location.column"),
2402
- line_hash: requireString(item.line_hash, "location.line_hash"),
2403
- text: requireString(item.text, "location.text"),
2404
- kind: optionalString(item.kind, "location.kind"),
2405
- name: optionalString(item.name, "location.name"),
2406
- qualified_name: optionalString(item.qualified_name, "location.qualified_name")
2407
- };
2408
- });
2409
- }
2410
- async function readSeekDef(target, options) {
2411
- const args = buildDefArgs({
2412
- path: target,
2413
- name: options.name,
2414
- format: "plain",
2415
- language: options.language,
2416
- cached: options.cached,
2417
- others: options.others,
2418
- ignored: options.ignored
2419
- });
2420
- return parseDefCompact(await runReadSeek(args, { signal: options.signal }));
2421
- }
2422
2574
 
2423
2575
  // src/edit-syntax-validate.ts
2424
2576
  function dedupeSortLines(diagnostics) {
@@ -5584,7 +5736,7 @@ var READSEEK_TOOL_ENTRIES = [
5584
5736
  { builtIn: null, readSeekName: "readSeek_rename" },
5585
5737
  { builtIn: "write", readSeekName: "readSeek_write" },
5586
5738
  { builtIn: null, readSeekName: "readSeek_def" },
5587
- { builtIn: "read", readSeekName: "readSeek_digest" },
5739
+ { builtIn: null, readSeekName: "readSeek_digest" },
5588
5740
  { builtIn: null, readSeekName: "readSeek_view" }
5589
5741
  ];
5590
5742
  function formatSettingsWarning(warning) {
@@ -5605,12 +5757,10 @@ function piReadSeekExtension(pi) {
5605
5757
  const replacedBuiltIns = new Set(settings.overrideTools ?? []);
5606
5758
  const binaryAvailable = readSeekBinaryAvailability().available;
5607
5759
  const swap = (builtIn, readSeekName) => binaryAvailable && replacedBuiltIns.has(builtIn) ? builtIn : readSeekName;
5608
- const readName = swap("read", "readSeek_digest");
5609
5760
  const editName = swap("edit", "readSeek_edit");
5610
5761
  const writeName = swap("write", "readSeek_write");
5611
5762
  const grepName = swap("grep", "readSeek_grep");
5612
5763
  const toolAliases = {
5613
- readSeek_digest: readName,
5614
5764
  readSeek_edit: editName,
5615
5765
  readSeek_grep: grepName,
5616
5766
  readSeek_write: writeName
@@ -5625,7 +5775,7 @@ function piReadSeekExtension(pi) {
5625
5775
  registerRefsTool(pi, { onFileAnchored: markAnchored });
5626
5776
  registerRenameTool(pi, { onFileMutated: forgetAnchors });
5627
5777
  registerDefTool(pi, { onFileAnchored: markAnchored });
5628
- registerDigestTool(pi, { onFileAnchored: markAnchored, name: readName, toolAliases });
5778
+ registerDigestTool(pi, { onFileAnchored: markAnchored, name: "readSeek_digest", toolAliases });
5629
5779
  registerViewTool(pi);
5630
5780
  registerWriteTool(pi, { onFileAnchored: markAnchored, name: writeName });
5631
5781
  pi.on("before_agent_start", (event) => {