pi-readseek 0.9.5 → 0.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.ts CHANGED
@@ -1,12 +1,9 @@
1
- // src/read.ts
1
+ // src/edit.ts
2
2
  import { readFile as fsReadFile } from "node:fs/promises";
3
- import {
4
- truncateHead as truncateHead2,
5
- DEFAULT_MAX_BYTES as DEFAULT_MAX_BYTES2,
6
- DEFAULT_MAX_LINES as DEFAULT_MAX_LINES2
7
- } from "@earendil-works/pi-coding-agent";
3
+ import { createPatch } from "diff";
4
+ import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
8
5
  import { Type as Type2 } from "@sinclair/typebox";
9
- import { Text as Text2 } from "@earendil-works/pi-tui";
6
+ import { Text as Text3 } from "@earendil-works/pi-tui";
10
7
 
11
8
  // ../readseek-api/src/contract.ts
12
9
  var DIGEST_FACETS = [
@@ -32,11 +29,10 @@ var DOCUMENT_NODE_KINDS = [
32
29
  "structural_section"
33
30
  ];
34
31
  var TOOL_NAMES = [
35
- "read",
32
+ "digest",
36
33
  "edit",
37
34
  "write",
38
35
  "grep",
39
- "digest",
40
36
  "view",
41
37
  "search",
42
38
  "def",
@@ -45,6 +41,8 @@ var TOOL_NAMES = [
45
41
  ];
46
42
  var PARAM_DESCRIPTIONS = {
47
43
  path: "Absolute or relative file path",
44
+ projectPath: "Path relative to the project directory",
45
+ projectSearchPath: "File or directory to search; defaults to the project directory",
48
46
  searchPath: "File or directory to search; defaults to the current directory",
49
47
  language: "Language override when auto-detection is ambiguous",
50
48
  languageOverride: "Language override",
@@ -98,6 +96,7 @@ var PARAM_DESCRIPTIONS = {
98
96
  symbol: "Mapped symbol name",
99
97
  newBody: "Complete replacement body for the symbol",
100
98
  apply: "When supported by the host, apply the verified plan instead of previewing only",
99
+ applyDefaultTrue: "Apply the verified edits; defaults to true",
101
100
  planHash: "Require the apply plan to match this dry-run plan hash",
102
101
  workspace: "Rename across the project",
103
102
  to: "New symbol name",
@@ -113,11 +112,10 @@ var PARAM_DESCRIPTIONS = {
113
112
  scopeContext: "Context lines within each symbol"
114
113
  };
115
114
  var TOOL_DESCRIPTIONS = {
116
- read: "Read anchored text by range or symbol. Standalone images require an explicit mode. Use it before edits to obtain fresh `LINE:HASH` anchors; use `view` for documents and `digest` with the map facet for structural outlines.",
115
+ digest: "Digest source content and images into the native CLI envelope. Returns metadata plus requested facets unchanged from `readseek digest`. Use it before edits to obtain fresh `LINE:HASH` anchors; use for structural maps, diagnostics, and cursor identity.",
117
116
  edit: 'Edit existing text files with fresh `LINE:HASH` anchors. Read or search first when no fresh anchors are available. Each `edits[]` item must use exactly one nested object: `{"set_line":{"anchor":"LINE:HASH","new_text":"..."}}`, `{"replace_lines":{"start_anchor":"LINE:HASH","end_anchor":"LINE:HASH","new_text":"..."}}`, `{"insert_after":{"anchor":"LINE:HASH","new_text":"..."}}`, `{"replace_symbol":{"symbol":"name","new_body":"..."}}`, or `{"replace":{"old_text":"...","new_text":"..."}}`.',
118
117
  write: "Create or replace a complete text file and return `LINE:HASH` anchors. Use `edit` for small changes to an existing file.",
119
118
  grep: "Search text or regex in files and return edit-ready `LINE:HASH` anchors. Use for identifiers, strings, configuration, errors, comments, and documentation.",
120
- digest: "Digest source content and images into the native CLI envelope. Returns metadata plus requested facets unchanged from `readseek digest`; use for structural maps, diagnostics, and cursor identity.",
121
119
  view: "Digest a document's structure or selected content. Start with the overview, then narrow by page, node, kind, or depth instead of reading the whole document. PDF is the first supported document format.",
122
120
  search: "Search code by AST structure using ast-grep-style patterns. Results include line hashes usable as `LINE:HASH` anchors. Use `grep` for identifiers, strings, comments, configuration, documentation, or other plain text and regex.",
123
121
  def: "Go to a symbol definition or declaration by name. Accepts qualified or unqualified names; use this instead of text search for functions, methods, classes, types, and other declarations.",
@@ -125,7 +123,10 @@ var TOOL_DESCRIPTIONS = {
125
123
  rename: "Rename a symbol at a source location. Resolvable same-named bindings are preserved; verified edits apply by default. Set `apply: false` to preview the plan before changing files."
126
124
  };
127
125
  function renderToolDescription(name, aliases = {}) {
128
- return TOOL_NAMES.reduce((description, toolName) => description.replaceAll(`\`${toolName}\``, `\`${aliases[toolName] ?? toolName}\``), TOOL_DESCRIPTIONS[name]);
126
+ const desc = TOOL_DESCRIPTIONS[name];
127
+ if (!desc)
128
+ throw new Error(`Missing TOOL_DESCRIPTIONS for ${name}`);
129
+ return TOOL_NAMES.reduce((description, toolName) => description.replaceAll(`\`${toolName}\``, `\`${aliases[toolName] ?? toolName}\``), desc);
129
130
  }
130
131
  var gitFlagsSchema = {
131
132
  cached: {
@@ -204,25 +205,27 @@ var editItemSchema = {
204
205
  };
205
206
  var TOOL_CONTRACTS = [
206
207
  {
207
- name: "read",
208
- title: "Read anchored text",
209
- description: TOOL_DESCRIPTIONS.read,
208
+ name: "digest",
209
+ title: "Digest source",
210
+ description: TOOL_DESCRIPTIONS.digest,
210
211
  inputSchema: {
211
212
  type: "object",
212
213
  properties: {
213
214
  path: { type: "string", description: PARAM_DESCRIPTIONS.path },
214
- offset: { type: "integer", minimum: 1, description: PARAM_DESCRIPTIONS.offset },
215
- limit: { type: "integer", minimum: 1, description: PARAM_DESCRIPTIONS.readLimit },
216
- symbol: { type: "string", description: PARAM_DESCRIPTIONS.symbolRead },
217
- bundle: {
215
+ select: { type: "string", description: PARAM_DESCRIPTIONS.select },
216
+ at: { type: "string", description: PARAM_DESCRIPTIONS.at },
217
+ end: { type: "integer", minimum: 1, description: PARAM_DESCRIPTIONS.end },
218
+ limit: { type: "integer", minimum: 1, description: PARAM_DESCRIPTIONS.limit },
219
+ language: { type: "string", description: PARAM_DESCRIPTIONS.languageOverride },
220
+ visionMode: {
218
221
  type: "string",
219
- enum: ["local"],
220
- description: PARAM_DESCRIPTIONS.bundle
222
+ enum: [...VISION_MODES],
223
+ description: PARAM_DESCRIPTIONS.visionModeDigest
221
224
  },
222
- image: {
225
+ visionLevel: {
223
226
  type: "string",
224
- enum: [...VISION_MODES],
225
- description: PARAM_DESCRIPTIONS.image
227
+ enum: [...VISION_LEVELS],
228
+ description: PARAM_DESCRIPTIONS.visionLevel
226
229
  }
227
230
  },
228
231
  required: ["path"],
@@ -296,34 +299,6 @@ var TOOL_CONTRACTS = [
296
299
  additionalProperties: false
297
300
  }
298
301
  },
299
- {
300
- name: "digest",
301
- title: "Digest source",
302
- description: TOOL_DESCRIPTIONS.digest,
303
- inputSchema: {
304
- type: "object",
305
- properties: {
306
- path: { type: "string", description: PARAM_DESCRIPTIONS.path },
307
- select: { type: "string", description: PARAM_DESCRIPTIONS.select },
308
- at: { type: "string", description: PARAM_DESCRIPTIONS.at },
309
- end: { type: "integer", minimum: 1, description: PARAM_DESCRIPTIONS.end },
310
- limit: { type: "integer", minimum: 1, description: PARAM_DESCRIPTIONS.limit },
311
- language: { type: "string", description: PARAM_DESCRIPTIONS.languageOverride },
312
- visionMode: {
313
- type: "string",
314
- enum: [...VISION_MODES],
315
- description: PARAM_DESCRIPTIONS.visionModeDigest
316
- },
317
- visionLevel: {
318
- type: "string",
319
- enum: [...VISION_LEVELS],
320
- description: PARAM_DESCRIPTIONS.visionLevel
321
- }
322
- },
323
- required: ["path"],
324
- additionalProperties: false
325
- }
326
- },
327
302
  {
328
303
  name: "view",
329
304
  title: "View document",
@@ -426,6 +401,90 @@ var TOOL_CONTRACTS = [
426
401
  }
427
402
  }
428
403
  ];
404
+ // ../readseek-api/src/policy.ts
405
+ var REPLACEABLE_TOOLS = [
406
+ {
407
+ tool: "digest",
408
+ builtIn: "read",
409
+ benefit: "it returns LINE:HASH anchors for safe edits."
410
+ },
411
+ {
412
+ tool: "edit",
413
+ builtIn: "edit",
414
+ benefit: "it verifies fresh LINE:HASH anchors."
415
+ },
416
+ {
417
+ tool: "grep",
418
+ builtIn: "grep",
419
+ benefit: "it returns LINE:HASH anchors."
420
+ },
421
+ {
422
+ tool: "write",
423
+ builtIn: "write",
424
+ benefit: "it returns LINE:HASH anchors."
425
+ }
426
+ ];
427
+ var TOOL_COMPACT_GUIDELINES = {
428
+ digest: [
429
+ "Use `digest` for map, diagnostics, and identity facets instead of separate tools."
430
+ ],
431
+ edit: [
432
+ "Prefer set_line, replace_lines, and insert_after; use replace only when anchors are impractical."
433
+ ],
434
+ grep: [
435
+ "Use `grep` with summary first for broad searches, then narrow by path, glob, or pattern."
436
+ ],
437
+ write: [
438
+ "Use anchored edits rather than `write` for small changes or appends."
439
+ ],
440
+ search: [
441
+ "Use `search` for AST patterns; use `grep` for plain text."
442
+ ],
443
+ refs: [
444
+ "Use `refs` before changing a symbol; add scope plus line/column to follow one binding."
445
+ ]
446
+ };
447
+ function resolveToolName(name, aliases = {}) {
448
+ return aliases[name] ?? name;
449
+ }
450
+ function rewriteBacktickedToolNames(text, aliases = {}) {
451
+ return TOOL_NAMES.reduce((rewritten, toolName) => rewritten.replaceAll(`\`${toolName}\``, `\`${resolveToolName(toolName, aliases)}\``), text);
452
+ }
453
+ function renderCompactGuidelines(name, aliases = {}) {
454
+ return (TOOL_COMPACT_GUIDELINES[name] ?? []).map((guideline) => rewriteBacktickedToolNames(guideline, aliases));
455
+ }
456
+ function renderToolRoutingPolicy(options = {}) {
457
+ const aliases = options.aliases ?? {};
458
+ const digest = resolveToolName("digest", aliases);
459
+ const edit = resolveToolName("edit", aliases);
460
+ const write = resolveToolName("write", aliases);
461
+ const grep = resolveToolName("grep", aliases);
462
+ const search = resolveToolName("search", aliases);
463
+ const def = resolveToolName("def", aliases);
464
+ const refs = resolveToolName("refs", aliases);
465
+ const rename = resolveToolName("rename", aliases);
466
+ const view = resolveToolName("view", aliases);
467
+ if (options.style === "server") {
468
+ return [
469
+ "Structural code navigation, document viewing, and hash-verified edits with readseek.",
470
+ "For structural questions, prefer these tools to text Grep/Bash: definitions, references, AST search, multi-facet digest (content/map/diagnostics/identity), and document views.",
471
+ `Use ${edit} for LINE:HASH-anchored mutations (preview with apply=false; apply defaults to true and re-checks plan_hash).`,
472
+ "Paths resolve against CLAUDE_PROJECT_DIR. Native Read/Grep/Edit hooks provide passive context;",
473
+ "call MCP tools for active navigation, document questions, or anchored edits."
474
+ ].join(" ");
475
+ }
476
+ const title = options.title ?? "ReadSeek tool policy:";
477
+ const bullets = [];
478
+ if (options.preferOverBuiltIns) {
479
+ bullets.push("- Prefer ReadSeek tools over built-ins when they can do the job.");
480
+ }
481
+ bullets.push(`- Read with ${digest} first; ${edit} needs fresh LINE:HASH anchors.`, `- Use ${grep} for text/regex, ${search} for AST patterns, and ${digest}/${def}/${refs} for symbol navigation.`, `- Use ${edit} for existing files, ${write} for whole-file creation or replacement, and ${rename} for symbol renames.`, `- Use ${view} for document structure; run ${digest} with select diagnostics after source edits for a quick syntax check.`);
482
+ if (options.demoteBuiltIns && options.demoteBuiltIns.length > 0) {
483
+ bullets.push(`- Do not use built-in ${options.demoteBuiltIns.join(", ")} when a ReadSeek tool can do the job.`);
484
+ }
485
+ return [title, ...bullets].join(`
486
+ `);
487
+ }
429
488
  // ../readseek-api/src/validation.ts
430
489
  function validateGitSelection(input) {
431
490
  if (input.ignored && !input.others) {
@@ -796,51 +855,7 @@ async function runReadSeekNodeJson(binaryPath, args, options = {}) {
796
855
  return parseSuccessfulJson(await runReadSeekNode(binaryPath, args, options));
797
856
  }
798
857
  // src/tool-prompt-metadata.ts
799
- var REPLACEABLE_TOOL_GUIDELINES = {
800
- "read.md": {
801
- readSeekName: "readSeek_read",
802
- builtInName: "read",
803
- benefit: "it returns LINE:HASH anchors for safe edits."
804
- },
805
- "edit.md": {
806
- readSeekName: "readSeek_edit",
807
- builtInName: "edit",
808
- benefit: "it verifies fresh LINE:HASH anchors."
809
- },
810
- "grep.md": {
811
- readSeekName: "readSeek_grep",
812
- builtInName: "grep",
813
- benefit: "it returns LINE:HASH anchors."
814
- },
815
- "write.md": {
816
- readSeekName: "readSeek_write",
817
- builtInName: "write",
818
- benefit: "it returns LINE:HASH anchors."
819
- }
820
- };
821
- var COMPACT_GUIDELINES = {
822
- "read.md": [
823
- "Use readSeek_read with symbol or offset/limit to inspect large files without reading them in full."
824
- ],
825
- "digest.md": [
826
- "Use readSeek_digest for map, diagnostics, and identity facets instead of separate tools."
827
- ],
828
- "edit.md": [
829
- "Prefer set_line, replace_lines, and insert_after; use replace only when anchors are impractical."
830
- ],
831
- "grep.md": [
832
- "Use readSeek_grep with summary first for broad searches, then narrow by path, glob, or pattern."
833
- ],
834
- "write.md": [
835
- "Use anchored edits rather than readSeek_write for small changes or appends."
836
- ],
837
- "search.md": [
838
- "Use readSeek_search for AST patterns; use readSeek_grep for plain text."
839
- ],
840
- "refs.md": [
841
- "Use readSeek_refs before changing a symbol; add scope plus line/column to follow one binding."
842
- ]
843
- };
858
+ var REPLACEABLE_BY_TOOL = new Map(REPLACEABLE_TOOLS.map((entry) => [entry.tool, entry]));
844
859
  function toolNameFromPrompt(fileName) {
845
860
  const name = fileName.replace(/\.md$/, "");
846
861
  if (!TOOL_NAMES.includes(name))
@@ -850,28 +865,32 @@ function toolNameFromPrompt(fileName) {
850
865
  function promptFileName(promptUrl) {
851
866
  return promptUrl.pathname.split("/").pop() ?? "";
852
867
  }
853
- function rewriteToolAliases(value, toolAliases) {
868
+ function rewriteRegisteredToolNames(value, toolAliases) {
854
869
  if (!toolAliases)
855
870
  return value;
856
871
  return Object.entries(toolAliases).reduce((rewritten, [canonicalName, registeredName]) => rewritten.replaceAll(canonicalName, registeredName), value);
857
872
  }
858
- function defineToolPromptMetadata(options) {
859
- const fileName = promptFileName(options.promptUrl);
860
- const toolName = toolNameFromPrompt(fileName);
861
- const replaceable = REPLACEABLE_TOOL_GUIDELINES[fileName];
862
- const registeredName = options.registeredName ?? replaceable?.readSeekName;
863
- const preferenceGuideline = replaceable && registeredName ? registeredName === replaceable.readSeekName ? `Prefer ${registeredName} over ${replaceable.builtInName} when both are available; ${replaceable.benefit}` : `Use ${registeredName}; ${replaceable.benefit}` : undefined;
864
- const aliases = Object.fromEntries(TOOL_NAMES.map((name) => {
873
+ function descriptionAliases(toolName, registeredName, toolAliases) {
874
+ return Object.fromEntries(TOOL_NAMES.map((name) => {
865
875
  const readSeekName = `readSeek_${name}`;
866
- const alias = name === toolName && registeredName ? registeredName : options.toolAliases?.[readSeekName] ?? readSeekName;
876
+ const alias = name === toolName && registeredName ? registeredName : toolAliases?.[readSeekName] ?? readSeekName;
867
877
  return [name, alias];
868
878
  }));
879
+ }
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);
869
888
  return {
870
889
  description: renderToolDescription(toolName, aliases),
871
- promptSnippet: rewriteToolAliases(options.promptSnippet, options.toolAliases),
890
+ promptSnippet: rewriteRegisteredToolNames(options.promptSnippet, options.toolAliases),
872
891
  promptGuidelines: [
873
892
  ...preferenceGuideline ? [preferenceGuideline] : [],
874
- ...(COMPACT_GUIDELINES[fileName] ?? []).map((guideline) => rewriteToolAliases(registeredName && replaceable ? guideline.replaceAll(replaceable.readSeekName, registeredName) : guideline, options.toolAliases))
893
+ ...renderCompactGuidelines(toolName, aliases)
875
894
  ]
876
895
  };
877
896
  }
@@ -1068,6 +1087,146 @@ function generateCompactOrFullDiff(oldContent, newContent, contextLines = 4) {
1068
1087
  return generateDiffString(oldContent, newContent, contextLines);
1069
1088
  }
1070
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, " ");
1096
+ }
1097
+ function normalizeAtPrefix(filePath) {
1098
+ return filePath.startsWith("@") ? filePath.slice(1) : filePath;
1099
+ }
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;
1107
+ }
1108
+ function resolveToCwd(filePath, cwd) {
1109
+ const expanded = expandPath(filePath);
1110
+ return isAbsolute(expanded) ? expanded : resolvePath(cwd, expanded);
1111
+ }
1112
+
1113
+ // src/runtime.ts
1114
+ function throwIfAborted(signal) {
1115
+ if (signal?.aborted)
1116
+ throw new Error("Operation aborted");
1117
+ }
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, "");
1124
+ }
1125
+ function formatFsError(err, domain) {
1126
+ const errno = typeof err.code === "string" ? err.code : "EIO";
1127
+ return {
1128
+ code: errno,
1129
+ message: `${domain}: ${errno}: ${strerror(err)}`
1130
+ };
1131
+ }
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
1142
+ };
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
+ }
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 };
1173
+ }
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 };
1182
+ }
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}`);
1188
+ }
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;
1219
+ }
1220
+ }
1221
+ return {
1222
+ diffStats,
1223
+ noOp: isNoOp,
1224
+ warningsBadge,
1225
+ errorText: showErrorText,
1226
+ semanticBadge
1227
+ };
1228
+ }
1229
+
1071
1230
  // src/readseek-value.ts
1072
1231
  function buildReadSeekLineWithHash(line, hash, raw) {
1073
1232
  return {
@@ -1081,13 +1240,6 @@ function buildReadSeekLineWithHash(line, hash, raw) {
1081
1240
  function buildReadSeekLine(line, raw) {
1082
1241
  return buildReadSeekLineWithHash(line, computeLineHash(raw), raw);
1083
1242
  }
1084
- function buildReadSeekLines(startLine, rawLines) {
1085
- return rawLines.map((raw, index) => buildReadSeekLine(startLine + index, raw));
1086
- }
1087
- function renderReadSeekLines(lines) {
1088
- return lines.map((line) => `${line.anchor}|${line.display}`).join(`
1089
- `);
1090
- }
1091
1243
  function formatAnchoredFileBlocks(files, lineSuffix) {
1092
1244
  const blocks = [];
1093
1245
  for (const file of files) {
@@ -1140,63 +1292,150 @@ function buildReadSeekEditResult(input) {
1140
1292
  };
1141
1293
  }
1142
1294
 
1143
- // src/binary-detect.ts
1144
- function looksLikeBinary(buf) {
1145
- if (buf.length === 0)
1146
- return false;
1147
- if (buf.includes(0))
1148
- return true;
1149
- const decoded = buf.toString("utf8");
1150
- if (!decoded.includes("�"))
1151
- return false;
1152
- return !Buffer.from(decoded, "utf8").equals(buf);
1153
- }
1154
-
1155
- // src/path-utils.ts
1156
- import { homedir } from "node:os";
1157
- import { isAbsolute, resolve as resolvePath } from "node:path";
1158
- var UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
1159
- function normalizeUnicodeSpaces(str) {
1160
- return str.replace(UNICODE_SPACES, " ");
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";
1161
1299
  }
1162
- function normalizeAtPrefix(filePath) {
1163
- return filePath.startsWith("@") ? filePath.slice(1) : filePath;
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 };
1164
1309
  }
1165
- function expandPath(filePath) {
1166
- const normalized = normalizeUnicodeSpaces(normalizeAtPrefix(filePath));
1167
- if (normalized === "~")
1168
- return homedir();
1169
- if (normalized.startsWith("~/"))
1170
- return homedir() + normalized.slice(1);
1171
- return normalized;
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})`;
1172
1321
  }
1173
- function resolveToCwd(filePath, cwd) {
1174
- const expanded = expandPath(filePath);
1175
- return isAbsolute(expanded) ? expanded : resolvePath(cwd, expanded);
1322
+ function extractNewTextValues(edits) {
1323
+ const values = [];
1324
+ for (const edit of edits ?? []) {
1325
+ if (!edit || typeof edit !== "object")
1326
+ 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
+ }
1333
+ }
1334
+ return values;
1176
1335
  }
1177
-
1178
- // src/runtime.ts
1179
- function throwIfAborted(signal) {
1180
- if (signal?.aborted)
1181
- throw new Error("Operation aborted");
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.";
1182
1342
  }
1183
-
1184
- // src/fs-error.ts
1185
- var SYSLOG_PATH_SUFFIX = /,\s+[a-z]+(\s+'.*')?$/;
1186
- function strerror(err) {
1187
- const raw = String(err?.message ?? String(err));
1188
- return raw.replace(SYSLOG_PATH_SUFFIX, "");
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.]";
1189
1352
  }
1190
- function formatFsError(err, domain) {
1191
- const errno = typeof err.code === "string" ? err.code : "EIO";
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}`;
1192
1371
  return {
1193
- code: errno,
1194
- message: `${domain}: ${errno}: ${strerror(err)}`
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
+ })
1195
1384
  };
1196
1385
  }
1197
1386
 
1198
- // src/file-map.ts
1199
- import { stat } from "node:fs/promises";
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
+ }
1393
+ const oldLines = oldContent.split(`
1394
+ `);
1395
+ const newLines = newContent.split(`
1396
+ `);
1397
+ if (oldLines.length === newLines.length) {
1398
+ let hasWhitespaceChange2 = false;
1399
+ let hasSemanticChange2 = false;
1400
+ 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;
1407
+ }
1408
+ }
1409
+ if (hasSemanticChange2 && hasWhitespaceChange2)
1410
+ return { classification: "mixed" };
1411
+ if (hasWhitespaceChange2)
1412
+ return { classification: "whitespace-only" };
1413
+ return { classification: "semantic" };
1414
+ }
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;
1430
+ }
1431
+ }
1432
+ }
1433
+ if (hasSemanticChange && hasWhitespaceChange)
1434
+ return { classification: "mixed" };
1435
+ if (hasWhitespaceChange)
1436
+ return { classification: "whitespace-only" };
1437
+ return { classification: "semantic" };
1438
+ }
1200
1439
 
1201
1440
  // src/readseek-client.ts
1202
1441
  import { existsSync, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
@@ -1236,7 +1475,7 @@ import { readFileSync, statSync } from "node:fs";
1236
1475
  import { homedir as homedir2 } from "node:os";
1237
1476
  import { join } from "node:path";
1238
1477
  var READSEEK_TOOL_REPLACEMENTS = {
1239
- read: "readSeek_read",
1478
+ read: "readSeek_digest",
1240
1479
  edit: "readSeek_edit",
1241
1480
  grep: "readSeek_grep",
1242
1481
  write: "readSeek_write"
@@ -1434,9 +1673,6 @@ function resolveReadSeekJsonSettings() {
1434
1673
  warnings: [...globalResult.warnings, ...projectResult.warnings]
1435
1674
  };
1436
1675
  }
1437
- function resolveReadSeekImageMode() {
1438
- return resolveReadSeekJsonSettings().settings.imageMode ?? "auto";
1439
- }
1440
1676
  function resolveReadSeekSyntaxValidation() {
1441
1677
  return resolveReadSeekJsonSettings().settings.syntaxValidation;
1442
1678
  }
@@ -1665,23 +1901,6 @@ function requireBoolean(value, field) {
1665
1901
  throw new Error(`invalid readseek ${field}: expected boolean`);
1666
1902
  return value;
1667
1903
  }
1668
- function parseReadOutput(value) {
1669
- if (!value || typeof value !== "object")
1670
- throw new Error("invalid readseek read output");
1671
- const output = value;
1672
- const hashlines = output.hashlines;
1673
- if (!Array.isArray(hashlines))
1674
- throw new Error("invalid readseek hashlines");
1675
- return {
1676
- file: requireString(output.file, "file"),
1677
- language: requireString(output.language, "language"),
1678
- line_count: requireNumber(output.line_count, "line_count"),
1679
- file_hash: requireString(output.file_hash, "file_hash"),
1680
- start_line: requireNumber(output.start_line, "start_line"),
1681
- end_line: requireNumber(output.end_line, "end_line"),
1682
- hashlines: hashlines.map((line) => parseHashline(line, "hashline"))
1683
- };
1684
- }
1685
1904
  function parseMapOutput(value) {
1686
1905
  if (!value || typeof value !== "object")
1687
1906
  throw new Error("invalid readseek map output");
@@ -1948,32 +2167,6 @@ function parseIdentifyOutput(value) {
1948
2167
  } : undefined
1949
2168
  };
1950
2169
  }
1951
- function parseImageContent(value) {
1952
- const detection = parseDetectOutput(value);
1953
- if (detection.kind !== "image")
1954
- throw new Error("invalid readseek image content");
1955
- return detection;
1956
- }
1957
- function parseTextContent(value) {
1958
- if (!value || typeof value !== "object")
1959
- throw new Error("invalid readseek content");
1960
- const output = value;
1961
- if (output.hashlines !== undefined && output.start_line === undefined) {
1962
- const hashlines = parseSearchHashlines(output.hashlines, "content.hashlines");
1963
- const first = hashlines[0];
1964
- const last = hashlines[hashlines.length - 1];
1965
- return {
1966
- file: requireString(output.file, "file"),
1967
- language: requireString(output.language, "language"),
1968
- line_count: requireNumber(output.line_count, "line_count"),
1969
- file_hash: requireString(output.file_hash, "file_hash"),
1970
- start_line: first?.line ?? 1,
1971
- end_line: last?.line ?? 0,
1972
- hashlines
1973
- };
1974
- }
1975
- return parseReadOutput(value);
1976
- }
1977
2170
  function parseDigestEnvelope(value) {
1978
2171
  if (!value || typeof value !== "object")
1979
2172
  throw new Error("invalid readseek digest output");
@@ -2023,11 +2216,6 @@ async function readSeekDigestNative(target, options = {}) {
2023
2216
  async function readSeekDigest(target, options = {}) {
2024
2217
  return parseDigestEnvelope(await runDigestRaw(target, options));
2025
2218
  }
2026
- function requireDigestContent(envelope, field) {
2027
- if (envelope.content === undefined)
2028
- throw new Error(`invalid readseek ${field}: missing content`);
2029
- return envelope.content;
2030
- }
2031
2219
  function fileMapFromReadSeekOutput(output, filePath, totalBytes) {
2032
2220
  if (output.language === "unknown" && output.symbols.length === 0)
2033
2221
  return null;
@@ -2082,70 +2270,6 @@ async function readSeekCheck(filePath, content, options = {}) {
2082
2270
  throw new Error("invalid readseek diagnostics: missing diagnostics facet");
2083
2271
  return envelope.diagnostics;
2084
2272
  }
2085
- async function readSeekDetect(filePath, options = {}) {
2086
- const envelope = await readSeekDigest(filePath, { select: "metadata", signal: options.signal });
2087
- return envelope.metadata;
2088
- }
2089
- async function readSeekDigestContent(filePath, options = {}) {
2090
- const envelope = await readSeekDigest(filePath, {
2091
- select: "content",
2092
- at: options.startLine !== undefined ? buildLineLocation(options.startLine) : undefined,
2093
- end: options.endLine,
2094
- visionMode: options.visionMode,
2095
- signal: options.signal,
2096
- vision: options.vision
2097
- });
2098
- if (envelope.metadata.kind === "pdf") {
2099
- return { metadata: envelope.metadata, cache: envelope.cache };
2100
- }
2101
- const content = requireDigestContent(envelope, "content");
2102
- if (envelope.metadata.kind === "image") {
2103
- return { metadata: envelope.metadata, image: parseImageContent(content) };
2104
- }
2105
- return { metadata: envelope.metadata, text: parseTextContent(content) };
2106
- }
2107
- async function readSeekImage(filePath, modes, options = {}) {
2108
- const requestedModes = modes.length > 1 ? ["all"] : modes;
2109
- const results = await Promise.allSettled(requestedModes.map(async (mode) => {
2110
- const digested = await readSeekDigestContent(filePath, {
2111
- visionMode: mode,
2112
- signal: options.signal,
2113
- vision: true
2114
- });
2115
- if (!digested.image)
2116
- throw new Error(`readseek returned no image analysis for ${filePath}`);
2117
- return digested.image;
2118
- }));
2119
- let merged;
2120
- for (const result of results) {
2121
- if (result.status !== "fulfilled")
2122
- continue;
2123
- const detection = result.value;
2124
- merged = merged === undefined ? detection : {
2125
- ...merged,
2126
- ocr: detection.ocr ?? merged.ocr,
2127
- caption: detection.caption ?? merged.caption,
2128
- objects: detection.objects ?? merged.objects
2129
- };
2130
- }
2131
- if (merged !== undefined)
2132
- return merged;
2133
- const failure = results.find((result) => result.status === "rejected");
2134
- if (failure?.status === "rejected")
2135
- throw failure.reason;
2136
- throw new Error(`readseek returned no image analysis for ${filePath}`);
2137
- }
2138
- async function readSeekPreparedImage(filePath, options = {}) {
2139
- const digested = await readSeekDigestContent(filePath, {
2140
- visionMode: "none",
2141
- signal: options.signal
2142
- });
2143
- const output = digested.image;
2144
- if (!output || output.encoding !== "base64" || output.data === undefined || output.mime === undefined) {
2145
- throw new Error(`readseek returned no prepared image for ${filePath}`);
2146
- }
2147
- return { mime: output.mime, encoding: output.encoding, data: output.data };
2148
- }
2149
2273
  function parseEditOutput(value) {
2150
2274
  if (!value || typeof value !== "object")
2151
2275
  throw new Error("invalid readseek edit output");
@@ -2296,1774 +2420,45 @@ async function readSeekDef(target, options) {
2296
2420
  return parseDefCompact(await runReadSeek(args, { signal: options.signal }));
2297
2421
  }
2298
2422
 
2299
- // src/file-map.ts
2300
- async function getOrGenerateMap(absPath) {
2423
+ // src/edit-syntax-validate.ts
2424
+ function dedupeSortLines(diagnostics) {
2425
+ const seen = new Set;
2426
+ const out = [];
2427
+ for (const diagnostic of diagnostics) {
2428
+ const key = diagnostic.start_line === diagnostic.end_line ? String(diagnostic.start_line) : `${diagnostic.start_line}-${diagnostic.end_line}`;
2429
+ if (!seen.has(key)) {
2430
+ seen.add(key);
2431
+ out.push({ key, start: diagnostic.start_line });
2432
+ }
2433
+ }
2434
+ out.sort((a, b) => a.start - b.start);
2435
+ return out.map((o) => o.key);
2436
+ }
2437
+ var EMPTY = { errorCount: 0, missingCount: 0, diagnostics: [] };
2438
+ async function validateSyntaxRegression(input, options = {}) {
2439
+ let before;
2440
+ let after;
2301
2441
  try {
2302
- const { size } = await stat(absPath);
2303
- return await readSeekMap(absPath, size);
2442
+ before = input.before === undefined ? EMPTY : await readSeekCheck(input.filePath, input.before, { signal: options.signal });
2443
+ after = await readSeekCheck(input.filePath, input.after, { signal: options.signal });
2304
2444
  } catch {
2305
2445
  return null;
2306
2446
  }
2447
+ const newErrorCount = Math.max(0, after.errorCount - before.errorCount);
2448
+ const newMissingCount = Math.max(0, after.missingCount - before.missingCount);
2449
+ if (newErrorCount === 0 && newMissingCount === 0)
2450
+ return null;
2451
+ return {
2452
+ errorLines: dedupeSortLines(after.diagnostics),
2453
+ newErrorCount,
2454
+ newMissingCount
2455
+ };
2307
2456
  }
2308
2457
 
2309
- // src/readseek/formatter.ts
2310
- import { basename } from "node:path";
2311
-
2312
- // src/readseek/constants.ts
2313
- var THRESHOLDS = {
2314
- MAX_MAP_BYTES: 25 * 1024,
2315
- FULL_TARGET_BYTES: 10 * 1024,
2316
- COMPACT_TARGET_BYTES: 20 * 1024,
2317
- MAX_OUTLINE_BYTES: 50 * 1024,
2318
- MAX_TRUNCATED_BYTES: 100 * 1024,
2319
- TRUNCATED_SYMBOLS_EACH: 50
2320
- };
2321
-
2322
- // src/readseek/formatter.ts
2323
- var BOX_LINE = "───────────────────────────────────────";
2324
- function formatSize(bytes) {
2325
- if (bytes < 1024) {
2326
- return `${bytes} B`;
2327
- }
2328
- if (bytes < 1024 * 1024) {
2329
- return `${(bytes / 1024).toFixed(1)} KB`;
2330
- }
2331
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
2332
- }
2333
- function formatNumber(n) {
2334
- return n.toLocaleString("en-US");
2335
- }
2336
- function formatSymbol(symbol, indent = 0) {
2337
- const prefix = " ".repeat(indent);
2338
- const lineRange = symbol.startLine === symbol.endLine ? `[${symbol.startLine}]` : `[${symbol.startLine}-${symbol.endLine}]`;
2339
- const { name } = symbol;
2340
- let formatted;
2341
- switch (symbol.kind) {
2342
- case "class":
2343
- case "interface":
2344
- case "struct":
2345
- case "enum":
2346
- case "type": {
2347
- formatted = `${prefix}${symbol.kind} ${name}: ${lineRange}`;
2348
- break;
2349
- }
2350
- case "function":
2351
- case "method": {
2352
- formatted = `${prefix}${name}: ${lineRange}`;
2353
- break;
2354
- }
2355
- case "variable":
2356
- case "constant": {
2357
- formatted = `${prefix}${name} = ... ${lineRange}`;
2358
- break;
2359
- }
2360
- default: {
2361
- formatted = `${prefix}${name}: ${lineRange}`;
2362
- }
2363
- }
2364
- return formatted;
2365
- }
2366
- function formatSymbols(symbols, level, indent = 0) {
2367
- const lines = [];
2368
- for (const symbol of symbols) {
2369
- lines.push(formatSymbol(symbol, indent));
2370
- if (level !== "outline" /* Outline */ && level !== "truncated" /* Truncated */ && symbol.children?.length) {
2371
- if (level === "minimal" /* Minimal */) {
2372
- for (const child of symbol.children) {
2373
- lines.push(formatSymbol(child, indent + 1));
2374
- }
2375
- } else {
2376
- lines.push(...formatSymbols(symbol.children, level, indent + 1));
2377
- }
2378
- }
2379
- }
2380
- return lines;
2381
- }
2382
- function formatFileMap(map, level) {
2383
- const effectiveLevel = level ?? map.detailLevel;
2384
- const fileName = basename(map.path);
2385
- const lines = [
2386
- "",
2387
- BOX_LINE,
2388
- `File Map: ${fileName}`,
2389
- `${formatNumber(map.totalLines)} lines │ ${formatSize(map.totalBytes)} │ ${map.language}`,
2390
- BOX_LINE,
2391
- ""
2392
- ];
2393
- if (map.truncatedInfo) {
2394
- const { shownSymbols, totalSymbols } = map.truncatedInfo;
2395
- lines.push(`[Map ≤${formatSize(THRESHOLDS.MAX_TRUNCATED_BYTES)} | ${shownSymbols} of ${formatNumber(totalSymbols)} symbols]`);
2396
- lines.push("");
2397
- } else if (effectiveLevel === "outline" /* Outline */) {
2398
- lines.push(`[Map ≤${formatSize(THRESHOLDS.MAX_OUTLINE_BYTES)} | outline]`);
2399
- lines.push("");
2400
- } else if (effectiveLevel === "minimal" /* Minimal */) {
2401
- lines.push(`[Map ≤${formatSize(THRESHOLDS.MAX_MAP_BYTES)} | minimal]`);
2402
- lines.push("");
2403
- } else if (effectiveLevel === "compact" /* Compact */) {
2404
- lines.push(`[Map ≤${formatSize(THRESHOLDS.COMPACT_TARGET_BYTES)} | compact]`);
2405
- lines.push("");
2406
- }
2407
- if (map.truncatedInfo) {
2408
- const half = Math.floor(map.symbols.length / 2);
2409
- const firstSymbols = map.symbols.slice(0, half);
2410
- const lastSymbols = map.symbols.slice(half);
2411
- const firstLines = formatSymbols(firstSymbols, effectiveLevel);
2412
- lines.push(...firstLines);
2413
- lines.push("");
2414
- lines.push(` ─ ─ ─ ${formatNumber(map.truncatedInfo.omittedSymbols)} more symbols ─ ─ ─`);
2415
- lines.push("");
2416
- const lastLines = formatSymbols(lastSymbols, effectiveLevel);
2417
- lines.push(...lastLines);
2418
- } else {
2419
- const symbolLines = formatSymbols(map.symbols, effectiveLevel);
2420
- lines.push(...symbolLines);
2421
- }
2422
- lines.push("");
2423
- lines.push(BOX_LINE);
2424
- if (map.truncatedInfo) {
2425
- const firstShown = map.symbols.slice(0, Math.floor(map.symbols.length / 2));
2426
- const lastShown = map.symbols.slice(Math.floor(map.symbols.length / 2));
2427
- const lastFirst = firstShown.at(-1);
2428
- const firstLast = lastShown.at(0);
2429
- if (lastFirst && firstLast) {
2430
- const omitStart = lastFirst.endLine + 1;
2431
- const omitEnd = firstLast.startLine - 1;
2432
- lines.push(`Omitted symbols are in lines ${formatNumber(omitStart)}-${formatNumber(omitEnd)}.`);
2433
- }
2434
- lines.push("Use read(path, offset=LINE, limit=N) to view specific sections.");
2435
- } else {
2436
- lines.push("Use read(path, offset=LINE, limit=N) for targeted reads.");
2437
- }
2438
- lines.push(BOX_LINE);
2439
- return lines.join(`
2440
- `);
2441
- }
2442
- function reduceToLevel(map, level) {
2443
- if (level === "outline" /* Outline */) {
2444
- return {
2445
- ...map,
2446
- detailLevel: "outline" /* Outline */,
2447
- symbols: map.symbols.map((s) => ({
2448
- name: s.name,
2449
- kind: s.kind,
2450
- startLine: s.startLine,
2451
- endLine: s.endLine
2452
- }))
2453
- };
2454
- }
2455
- if (level === "minimal" /* Minimal */) {
2456
- return {
2457
- ...map,
2458
- detailLevel: "minimal" /* Minimal */,
2459
- symbols: map.symbols.map((s) => ({
2460
- name: s.name,
2461
- kind: s.kind,
2462
- startLine: s.startLine,
2463
- endLine: s.endLine,
2464
- children: s.children?.map((c) => ({
2465
- name: c.name,
2466
- kind: c.kind,
2467
- startLine: c.startLine,
2468
- endLine: c.endLine
2469
- }))
2470
- }))
2471
- };
2472
- }
2473
- if (level === "compact" /* Compact */) {
2474
- return { ...map, detailLevel: "compact" /* Compact */ };
2475
- }
2476
- return { ...map, detailLevel: "full" /* Full */ };
2477
- }
2478
- function reduceToTruncated(map, symbolsEach = THRESHOLDS.TRUNCATED_SYMBOLS_EACH) {
2479
- const { symbols } = map;
2480
- const total = symbols.length;
2481
- if (total <= symbolsEach * 2) {
2482
- return reduceToLevel(map, "outline" /* Outline */);
2483
- }
2484
- const firstSymbols = symbols.slice(0, symbolsEach).map((s) => ({
2485
- name: s.name,
2486
- kind: s.kind,
2487
- startLine: s.startLine,
2488
- endLine: s.endLine
2489
- }));
2490
- const lastSymbols = symbols.slice(-symbolsEach).map((s) => ({
2491
- name: s.name,
2492
- kind: s.kind,
2493
- startLine: s.startLine,
2494
- endLine: s.endLine
2495
- }));
2496
- return {
2497
- ...map,
2498
- symbols: [...firstSymbols, ...lastSymbols],
2499
- detailLevel: "truncated" /* Truncated */,
2500
- truncatedInfo: {
2501
- totalSymbols: total,
2502
- shownSymbols: symbolsEach * 2,
2503
- omittedSymbols: total - symbolsEach * 2
2504
- }
2505
- };
2506
- }
2507
- function formatFileMapWithBudget(map, maxBytes = THRESHOLDS.MAX_TRUNCATED_BYTES) {
2508
- const tiers = [
2509
- { level: "full" /* Full */, budget: THRESHOLDS.FULL_TARGET_BYTES },
2510
- { level: "compact" /* Compact */, budget: THRESHOLDS.COMPACT_TARGET_BYTES },
2511
- { level: "minimal" /* Minimal */, budget: THRESHOLDS.MAX_MAP_BYTES },
2512
- { level: "outline" /* Outline */, budget: THRESHOLDS.MAX_OUTLINE_BYTES }
2513
- ];
2514
- for (const { level, budget } of tiers) {
2515
- const reduced = reduceToLevel(map, level);
2516
- const formatted = formatFileMap(reduced, level);
2517
- const size = Buffer.byteLength(formatted, "utf8");
2518
- if (size <= budget && size <= maxBytes) {
2519
- return formatted;
2520
- }
2521
- }
2522
- const outline = reduceToLevel(map, "outline" /* Outline */);
2523
- const outlineFormatted = formatFileMap(outline, "outline" /* Outline */);
2524
- const outlineSize = Buffer.byteLength(outlineFormatted, "utf8");
2525
- if (outlineSize <= maxBytes) {
2526
- return outlineFormatted;
2527
- }
2528
- const totalSymbols = map.symbols.length;
2529
- const minSymbols = 10;
2530
- const maxSymbolsEach = Math.floor(totalSymbols / 2);
2531
- let low = minSymbols;
2532
- let high = maxSymbolsEach;
2533
- let bestResult = null;
2534
- while (low <= high) {
2535
- const mid = Math.floor((low + high) / 2);
2536
- const truncated = reduceToTruncated(map, mid);
2537
- const formatted = formatFileMap(truncated, "truncated" /* Truncated */);
2538
- const size = Buffer.byteLength(formatted, "utf8");
2539
- if (size <= maxBytes) {
2540
- bestResult = formatted;
2541
- low = mid + 1;
2542
- } else {
2543
- high = mid - 1;
2544
- }
2545
- }
2546
- if (bestResult) {
2547
- return bestResult;
2548
- }
2549
- const minimal = reduceToTruncated(map, minSymbols);
2550
- return formatFileMap(minimal, "truncated" /* Truncated */);
2551
- }
2552
-
2553
- // src/readseek/symbol-tree.ts
2554
- function traverseSymbolTree(symbols, visit) {
2555
- const result = [];
2556
- const walk = (nodes, parentName) => {
2557
- for (const symbol of nodes) {
2558
- result.push(visit(symbol, parentName));
2559
- if (symbol.children?.length)
2560
- walk(symbol.children, symbol.name);
2561
- }
2562
- };
2563
- walk(symbols);
2564
- return result;
2565
- }
2566
-
2567
- // src/readseek/symbol-lookup.ts
2568
- function toMatch(symbol, parentName) {
2569
- return {
2570
- name: symbol.name,
2571
- kind: symbol.kind,
2572
- startLine: symbol.startLine,
2573
- endLine: symbol.endLine,
2574
- ...parentName ? { parentName } : {}
2575
- };
2576
- }
2577
- function toMatches(candidates) {
2578
- return candidates.map((candidate) => toMatch(candidate.symbol, candidate.parentName));
2579
- }
2580
- function flattenSymbols(symbols) {
2581
- return traverseSymbolTree(symbols, (symbol, parentName) => ({ symbol, parentName }));
2582
- }
2583
- function resolveDotPath(symbols, parts) {
2584
- let candidates = symbols.filter((symbol) => symbol.name === parts[0]).map((symbol) => ({ symbol }));
2585
- for (let i = 1;i < parts.length; i++) {
2586
- const nextCandidates = [];
2587
- for (const candidate of candidates) {
2588
- for (const child of candidate.symbol.children ?? []) {
2589
- if (child.name === parts[i]) {
2590
- nextCandidates.push({ symbol: child, parentName: candidate.symbol.name });
2591
- }
2592
- }
2593
- }
2594
- candidates = nextCandidates;
2595
- }
2596
- return candidates;
2597
- }
2598
- var JAVA_TOP_LEVEL_TYPE_KINDS = new Set(["class", "interface", "enum", "module"]);
2599
- function preferJavaTopLevelType(map, candidates) {
2600
- if (map.language !== "Java")
2601
- return;
2602
- const topLevelTypes = candidates.filter((candidate) => !candidate.parentName && JAVA_TOP_LEVEL_TYPE_KINDS.has(candidate.symbol.kind));
2603
- return topLevelTypes.length === 1 ? topLevelTypes[0] : undefined;
2604
- }
2605
- function resolveDotPathSuffix(symbols, parts) {
2606
- for (let start = 1;start < parts.length; start++) {
2607
- const candidates = resolveDotPath(symbols, parts.slice(start));
2608
- if (candidates.length > 0)
2609
- return candidates;
2610
- }
2611
- return [];
2612
- }
2613
- function findSymbol(map, query) {
2614
- const q = query.trim();
2615
- if (!q)
2616
- return { type: "not-found" };
2617
- if (map.symbols.length === 0)
2618
- return { type: "not-found" };
2619
- const allSymbols = flattenSymbols(map.symbols);
2620
- const atLineMatch = /^(.+?)@(\d+)$/.exec(q);
2621
- if (atLineMatch) {
2622
- const namePart = atLineMatch[1];
2623
- const lineNum = Number.parseInt(atLineMatch[2], 10);
2624
- let pool;
2625
- if (namePart.includes(".")) {
2626
- const parts = namePart.split(".").map((p) => p.trim());
2627
- pool = parts.every((p) => p.length > 0) ? resolveDotPath(map.symbols, parts) : [];
2628
- if (pool.length === 0) {
2629
- pool = allSymbols.filter((c) => c.symbol.name === namePart);
2630
- }
2631
- } else {
2632
- pool = allSymbols.filter((c) => c.symbol.name === namePart);
2633
- }
2634
- pool = pool.filter((c) => Number.isFinite(c.symbol.startLine) && c.symbol.startLine > 0);
2635
- if (pool.length === 0) {
2636
- const leaf = namePart.split(".").pop() ?? namePart;
2637
- const sameLeaf = allSymbols.filter((c) => c.symbol.name === leaf && Number.isFinite(c.symbol.startLine) && c.symbol.startLine > 0);
2638
- if (sameLeaf.length > 0) {
2639
- const list = sameLeaf.slice(0, 5).map((c) => `${c.parentName ? c.parentName + "." : ""}${c.symbol.name}@${c.symbol.startLine}`).join(", ");
2640
- return {
2641
- type: "not-found",
2642
- message: `${q} not found. Candidates: ${list}`,
2643
- candidates: toMatches(sameLeaf.slice(0, 5))
2644
- };
2645
- }
2646
- }
2647
- if (pool.length > 0) {
2648
- const containing = pool.filter((c) => c.symbol.startLine <= lineNum && c.symbol.endLine >= lineNum);
2649
- if (containing.length > 0) {
2650
- return { type: "found", symbol: toMatch(containing[0].symbol, containing[0].parentName) };
2651
- }
2652
- const below = pool.filter((c) => c.symbol.startLine >= lineNum).sort((a, b) => a.symbol.startLine - b.symbol.startLine);
2653
- if (below.length) {
2654
- return { type: "found", symbol: toMatch(below[0].symbol, below[0].parentName) };
2655
- }
2656
- const above = pool.filter((c) => c.symbol.startLine < lineNum).sort((a, b) => b.symbol.startLine - a.symbol.startLine);
2657
- if (above.length) {
2658
- return { type: "found", symbol: toMatch(above[0].symbol, above[0].parentName) };
2659
- }
2660
- }
2661
- }
2662
- const exact = allSymbols.filter((c) => c.symbol.name === q);
2663
- if (exact.length === 1)
2664
- return { type: "found", symbol: toMatch(exact[0].symbol, exact[0].parentName) };
2665
- if (exact.length > 1) {
2666
- const preferred = preferJavaTopLevelType(map, exact);
2667
- if (preferred)
2668
- return { type: "found", symbol: toMatch(preferred.symbol, preferred.parentName) };
2669
- return { type: "ambiguous", candidates: toMatches(exact.slice(0, 5)) };
2670
- }
2671
- if (q.includes(".")) {
2672
- const parts = q.split(".").map((p) => p.trim());
2673
- if (parts.length >= 2 && parts.every((p) => p.length > 0)) {
2674
- const candidates = resolveDotPath(map.symbols, parts);
2675
- if (candidates.length === 1) {
2676
- return { type: "found", symbol: toMatch(candidates[0].symbol, candidates[0].parentName) };
2677
- }
2678
- if (candidates.length > 1) {
2679
- return {
2680
- type: "ambiguous",
2681
- candidates: toMatches(candidates.slice(0, 5))
2682
- };
2683
- }
2684
- const suffixCandidates = resolveDotPathSuffix(map.symbols, parts);
2685
- if (suffixCandidates.length === 1) {
2686
- return { type: "found", symbol: toMatch(suffixCandidates[0].symbol, suffixCandidates[0].parentName) };
2687
- }
2688
- if (suffixCandidates.length > 1) {
2689
- return {
2690
- type: "ambiguous",
2691
- candidates: toMatches(suffixCandidates.slice(0, 5))
2692
- };
2693
- }
2694
- }
2695
- }
2696
- const qLower = q.toLowerCase();
2697
- const ci = allSymbols.filter((c) => c.symbol.name.toLowerCase() === qLower);
2698
- if (ci.length === 1)
2699
- return { type: "found", symbol: toMatch(ci[0].symbol, ci[0].parentName) };
2700
- if (ci.length > 1)
2701
- return { type: "ambiguous", candidates: toMatches(ci.slice(0, 5)) };
2702
- const prefix = allSymbols.filter((c) => c.symbol.name.toLowerCase().startsWith(qLower));
2703
- if (prefix.length === 1)
2704
- return { type: "found", symbol: toMatch(prefix[0].symbol, prefix[0].parentName) };
2705
- if (prefix.length > 1)
2706
- return { type: "ambiguous", candidates: toMatches(prefix.slice(0, 5)) };
2707
- const camelCase = allSymbols.filter((c) => {
2708
- const words = c.symbol.name.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase().split(/[\s_]+/);
2709
- return words.some((w) => w === qLower);
2710
- });
2711
- const partial = allSymbols.filter((c) => c.symbol.name.toLowerCase().includes(qLower));
2712
- const buildOtherCandidates = (chosen) => {
2713
- const pool = [];
2714
- const seen = new Set;
2715
- seen.add(chosen.symbol);
2716
- for (const c of [...camelCase, ...partial]) {
2717
- if (seen.has(c.symbol))
2718
- continue;
2719
- seen.add(c.symbol);
2720
- pool.push(c);
2721
- }
2722
- return toMatches(pool.slice(0, 4));
2723
- };
2724
- if (camelCase.length === 1) {
2725
- return {
2726
- type: "fuzzy",
2727
- symbol: toMatch(camelCase[0].symbol, camelCase[0].parentName),
2728
- tier: "camelCase",
2729
- otherCandidates: buildOtherCandidates(camelCase[0])
2730
- };
2731
- }
2732
- if (camelCase.length > 1)
2733
- return { type: "ambiguous", candidates: toMatches(camelCase.slice(0, 5)) };
2734
- if (partial.length === 1) {
2735
- return {
2736
- type: "fuzzy",
2737
- symbol: toMatch(partial[0].symbol, partial[0].parentName),
2738
- tier: "substring",
2739
- otherCandidates: buildOtherCandidates(partial[0])
2740
- };
2741
- }
2742
- if (partial.length > 1)
2743
- return { type: "ambiguous", candidates: toMatches(partial.slice(0, 5)) };
2744
- return { type: "not-found" };
2745
- }
2746
-
2747
- // src/readseek/symbol-error-format.ts
2748
- function formatAmbiguous(query, candidates) {
2749
- const lines = candidates.map((c) => `- ${c.name} (${c.kind}) — lines ${c.startLine}-${c.endLine}`);
2750
- const hints = candidates.map((c) => `${query}@${c.startLine}`).join(" or ");
2751
- return [
2752
- `Symbol '${query}' is ambiguous.`,
2753
- "Matches:",
2754
- ...lines,
2755
- `Use ${hints} to select by start line.`
2756
- ].join(`
2757
- `);
2758
- }
2759
- function formatNotFound(query, map) {
2760
- const available = map.symbols.slice(0, 20).map((s) => s.name).join(", ");
2761
- return `[Warning: symbol '${query}' not found. Available symbols: ${available}]`;
2762
- }
2763
-
2764
- // src/read-output.ts
2765
- import {
2766
- DEFAULT_MAX_BYTES,
2767
- DEFAULT_MAX_LINES,
2768
- formatSize as formatSize2,
2769
- truncateHead
2770
- } from "@earendil-works/pi-coding-agent";
2771
- function buildReadOutput(input) {
2772
- const selectedLines = input.selectedLines ?? input.lines?.map((line) => line.raw) ?? [];
2773
- const lines = input.lines ?? buildReadSeekLines(input.startLine, selectedLines);
2774
- const warnings = input.warnings ?? [];
2775
- const renderedLines = renderReadSeekLines(lines);
2776
- const truncated = truncateHead(renderedLines, {
2777
- maxLines: DEFAULT_MAX_LINES,
2778
- maxBytes: DEFAULT_MAX_BYTES
2779
- });
2780
- let text = input.truncation ? truncated.content : renderedLines;
2781
- if (input.truncation) {
2782
- text += `
2783
-
2784
- [Output truncated: showing ${input.truncation.outputLines} of ${input.totalLines} lines (${formatSize2(input.truncation.outputBytes)} of ${formatSize2(input.truncation.totalBytes)}). Use offset=${input.startLine + input.truncation.outputLines} to continue.]`;
2785
- } else if (input.continuation) {
2786
- text += `
2787
-
2788
- [Showing lines ${input.startLine}-${input.endLine} of ${input.totalLines}. Use offset=${input.continuation.nextOffset} to continue.]`;
2789
- }
2790
- if (input.bundle?.applied) {
2791
- const supportBlocks = input.bundle.localSupport.map((item) => {
2792
- const supportLines = buildReadSeekLines(item.symbol.startLine, item.lines);
2793
- return renderReadSeekLines(supportLines);
2794
- });
2795
- text = [
2796
- "## Requested symbol",
2797
- text,
2798
- "",
2799
- "## Local support",
2800
- ...supportBlocks
2801
- ].join(`
2802
- `);
2803
- }
2804
- if (input.map?.appended && input.map.text) {
2805
- text += `
2806
-
2807
- ${input.map.text}`;
2808
- }
2809
- if (input.symbol) {
2810
- const parentInfo = input.symbol.parentName ? ` in ${input.symbol.parentName}` : "";
2811
- text = `[Symbol: ${input.symbol.name} (${input.symbol.kind})${parentInfo}, lines ${input.symbol.startLine}-${input.symbol.endLine} of ${input.totalLines}]
2812
-
2813
- ${text}`;
2814
- }
2815
- if (warnings.length) {
2816
- text = `${warnings.map((warning) => warning.message).join(`
2817
-
2818
- `)}
2819
-
2820
- ${text}`;
2821
- }
2822
- const readSeekValue = {
2823
- tool: "read",
2824
- path: input.path,
2825
- range: {
2826
- startLine: input.startLine,
2827
- endLine: input.endLine,
2828
- totalLines: input.totalLines
2829
- },
2830
- warnings,
2831
- truncation: input.truncation ?? null,
2832
- symbol: input.symbol ?? null,
2833
- map: {
2834
- requested: input.map?.requested ?? false,
2835
- appended: input.map?.appended ?? false
2836
- },
2837
- lines
2838
- };
2839
- if (input.bundle) {
2840
- readSeekValue.bundle = {
2841
- mode: input.bundle.mode,
2842
- applied: input.bundle.applied,
2843
- localSupport: input.bundle.localSupport.map((item) => {
2844
- const supportLines = buildReadSeekLines(item.symbol.startLine, item.lines);
2845
- return {
2846
- name: item.symbol.name,
2847
- kind: item.symbol.kind,
2848
- parentName: item.symbol.parentName,
2849
- startLine: item.symbol.startLine,
2850
- endLine: item.symbol.endLine,
2851
- lineAnchors: supportLines.map((line) => line.anchor)
2852
- };
2853
- }),
2854
- warnings: input.bundle.warnings ?? []
2855
- };
2856
- }
2857
- return {
2858
- text,
2859
- lines,
2860
- readSeekValue
2861
- };
2862
- }
2863
-
2864
- // src/read-local-bundle.ts
2865
- var CONTAINER_KINDS = new Set([
2866
- "class" /* Class */,
2867
- "module" /* Module */,
2868
- "namespace" /* Namespace */
2869
- ]);
2870
- function buildLocalBundle(fileMap, requested, allLines) {
2871
- const requestedText = allLines.slice(requested.startLine - 1, requested.endLine).join(`
2872
- `);
2873
- const identifiers = new Set(requestedText.match(/\b[A-Za-z_][A-Za-z0-9_]*\b/g) ?? []);
2874
- const candidates = traverseSymbolTree(fileMap.symbols, (symbol, parentName) => ({ symbol, parentName })).filter(({ symbol }) => {
2875
- if (CONTAINER_KINDS.has(symbol.kind))
2876
- return false;
2877
- return !(symbol.name === requested.name && symbol.startLine === requested.startLine && symbol.endLine === requested.endLine);
2878
- });
2879
- const symbolsByName = new Map;
2880
- for (const candidate of candidates) {
2881
- const bucket = symbolsByName.get(candidate.symbol.name) ?? [];
2882
- bucket.push(candidate);
2883
- symbolsByName.set(candidate.symbol.name, bucket);
2884
- }
2885
- const support = [];
2886
- for (const identifier of identifiers) {
2887
- const matches = symbolsByName.get(identifier) ?? [];
2888
- if (matches.length === 0)
2889
- continue;
2890
- if (matches.length > 1)
2891
- return null;
2892
- const match = matches[0];
2893
- support.push({
2894
- symbol: {
2895
- name: match.symbol.name,
2896
- kind: match.symbol.kind,
2897
- startLine: match.symbol.startLine,
2898
- endLine: match.symbol.endLine,
2899
- ...match.parentName ? { parentName: match.parentName } : {}
2900
- },
2901
- lines: allLines.slice(match.symbol.startLine - 1, match.symbol.endLine)
2902
- });
2903
- }
2904
- support.sort((a, b) => {
2905
- if (a.symbol.startLine !== b.symbol.startLine)
2906
- return a.symbol.startLine - b.symbol.startLine;
2907
- return a.symbol.name.localeCompare(b.symbol.name);
2908
- });
2909
- return {
2910
- requested,
2911
- support
2912
- };
2913
- }
2914
-
2915
- // src/coerce-obvious-int.ts
2916
- var BASE10_INT_RE = /^-?\d+$/;
2917
- var MIN_SAFE_INTEGER = BigInt(Number.MIN_SAFE_INTEGER);
2918
- var MAX_SAFE_INTEGER = BigInt(Number.MAX_SAFE_INTEGER);
2919
- function formatReceived(value) {
2920
- if (typeof value === "number" || typeof value === "bigint")
2921
- return String(value);
2922
- return JSON.stringify(value) ?? String(value);
2923
- }
2924
- function invalidInteger(value, name) {
2925
- return {
2926
- ok: false,
2927
- message: `Invalid ${name}: expected a safe base-10 integer, received ${formatReceived(value)}.`
2928
- };
2929
- }
2930
- function coerceObviousBase10Int(value, name) {
2931
- if (value === undefined || typeof value === "string" && value.trim() === "") {
2932
- return { ok: true, value: undefined };
2933
- }
2934
- if (typeof value === "number") {
2935
- if (Number.isSafeInteger(value)) {
2936
- return { ok: true, value };
2937
- }
2938
- return invalidInteger(value, name);
2939
- }
2940
- if (typeof value === "string" && BASE10_INT_RE.test(value)) {
2941
- const parsed = BigInt(value);
2942
- if (parsed >= MIN_SAFE_INTEGER && parsed <= MAX_SAFE_INTEGER) {
2943
- return { ok: true, value: Number(parsed) };
2944
- }
2945
- }
2946
- return invalidInteger(value, name);
2947
- }
2948
-
2949
- // src/read-render-helpers.ts
2950
- function formatReadCallText(args) {
2951
- const rawPath = typeof args?.path === "string" ? args.path : null;
2952
- if (typeof args?.symbol === "string" && args.symbol) {
2953
- return { path: rawPath, suffix: `→ symbol ${args.symbol}` };
2954
- }
2955
- if (typeof args?.offset === "number") {
2956
- const offset = args.offset;
2957
- const limit = typeof args?.limit === "number" ? args.limit : undefined;
2958
- if (offset < 1) {
2959
- return { path: rawPath, suffix: undefined };
2960
- }
2961
- if (limit !== undefined) {
2962
- if (limit < 1) {
2963
- return { path: rawPath, suffix: undefined };
2964
- }
2965
- return { path: rawPath, suffix: `lines ${offset}-${offset + limit - 1}` };
2966
- }
2967
- return { path: rawPath, suffix: `from line ${offset}` };
2968
- }
2969
- return { path: rawPath, suffix: undefined };
2970
- }
2971
- function formatReadResultText(input) {
2972
- const { range, truncation, symbol, map, warnings } = input;
2973
- if (input.isError && input.errorText) {
2974
- return {
2975
- summary: "",
2976
- symbolBadge: undefined,
2977
- badges: [],
2978
- truncated: false,
2979
- errorText: input.errorText
2980
- };
2981
- }
2982
- const lineCount = range.endLine - range.startLine + 1;
2983
- const isFullFile = range.startLine === 1 && range.endLine === range.totalLines;
2984
- const isTruncated = !!truncation;
2985
- let summary;
2986
- if (isTruncated) {
2987
- summary = `✓ ${truncation.outputLines} of ${truncation.totalLines} lines (truncated)`;
2988
- } else if (isFullFile) {
2989
- summary = `✓ ${lineCount} lines`;
2990
- } else {
2991
- summary = `✓ ${lineCount} lines (${range.startLine}-${range.endLine} of ${range.totalLines})`;
2992
- }
2993
- const symbolBadge = symbol ? `${symbol.kind} ${symbol.name}` : undefined;
2994
- const badges = [];
2995
- if (map.appended)
2996
- badges.push("\uD83D\uDCD0 map");
2997
- for (const w of warnings) {
2998
- if (w.code === "binary-content")
2999
- badges.push("⚠ binary");
3000
- if (w.code === "bare-cr")
3001
- badges.push("⚠ bare CR");
3002
- if (w.code === "fuzzy-symbol-match")
3003
- badges.push("⚠ fuzzy match");
3004
- }
3005
- return {
3006
- summary,
3007
- symbolBadge,
3008
- badges,
3009
- truncated: isTruncated,
3010
- errorText: undefined
3011
- };
3012
- }
3013
-
3014
- // src/tui-render-utils.ts
3015
- import { relative } from "node:path";
3016
- import { pathToFileURL } from "node:url";
3017
- import { getCapabilities, hyperlink, Text, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
3018
- var SUMMARY_PREFIX = "↳";
3019
- var EXPAND_HINT = " • Ctrl+O to expand";
3020
- function renderToolLabel(theme, label) {
3021
- const boldFn = typeof theme.bold === "function" ? theme.bold.bind(theme) : (text) => text;
3022
- return theme.fg("toolTitle", boldFn(label));
3023
- }
3024
- function linkToolPath(styledText, rawPath, cwd) {
3025
- try {
3026
- if (!getCapabilities().hyperlinks)
3027
- return styledText;
3028
- const absolutePath = resolveToCwd(rawPath, cwd);
3029
- return hyperlink(styledText, pathToFileURL(absolutePath).href);
3030
- } catch {
3031
- return styledText;
3032
- }
3033
- }
3034
- function linkedPathLine(theme, rawPath, displayPath, cwd, suffix, width) {
3035
- const pathWidth = width === undefined ? undefined : Math.max(1, width - 2 - visibleWidth(suffix));
3036
- const visiblePath = pathWidth === undefined ? displayPath : truncateToWidth(displayPath, pathWidth);
3037
- const linkedPath = linkToolPath(theme.fg("dim", visiblePath), rawPath, cwd);
3038
- return ` ${linkedPath}${theme.fg("dim", suffix)}`;
3039
- }
3040
- function appendExpandHint(text, hidden) {
3041
- return hidden ? `${text}${EXPAND_HINT}` : text;
3042
- }
3043
- function summaryLine(summary, options = {}) {
3044
- const prefix = options.theme ? options.theme.fg(options.style ?? "dim", SUMMARY_PREFIX) : SUMMARY_PREFIX;
3045
- return appendExpandHint(`${prefix} ${summary}`, !!options.hidden);
3046
- }
3047
- function isRendererExpanded(options, context, defaultExpanded = false) {
3048
- return context?.expanded ?? options?.expanded ?? defaultExpanded;
3049
- }
3050
- function resolveRenderResultContext(options, rest, defaultExpanded = false) {
3051
- const context = rest[0] ?? options ?? {};
3052
- return {
3053
- isPartial: context.isPartial ?? options?.isPartial ?? false,
3054
- isError: context.isError ?? false,
3055
- expanded: isRendererExpanded(options, context, defaultExpanded),
3056
- width: context.width ?? options?.width,
3057
- cwd: context.cwd ?? process.cwd(),
3058
- context
3059
- };
3060
- }
3061
- function normalizeWidth(width, fallback = 80) {
3062
- return typeof width === "number" && Number.isFinite(width) && width > 0 ? Math.floor(width) : fallback;
3063
- }
3064
- function clampLineToWidth(line, width) {
3065
- if (width === undefined || width === null)
3066
- return line;
3067
- const normalized = normalizeWidth(width);
3068
- return visibleWidth(line) <= normalized ? line : truncateToWidth(line, normalized);
3069
- }
3070
- function clampLinesToWidth(lines, width) {
3071
- if (width === undefined || width === null)
3072
- return lines;
3073
- return lines.map((line) => clampLineToWidth(line, width));
3074
- }
3075
- function wrapWithHangingIndent(prefix, content, width, options = {}) {
3076
- const tint = options.tint ?? ((text) => text);
3077
- if (width === undefined || width === null)
3078
- return [tint(prefix + content)];
3079
- const normalized = normalizeWidth(width);
3080
- const combined = prefix + content;
3081
- if (visibleWidth(combined) <= normalized)
3082
- return [tint(combined)];
3083
- const prefixWidth = visibleWidth(prefix);
3084
- const contentWidth = Math.max(1, normalized - prefixWidth);
3085
- const wrapped = wrapTextWithAnsi(content, contentWidth);
3086
- if (wrapped.length === 0)
3087
- return [tint(clampLineToWidth(prefix, normalized))];
3088
- const indent = " ".repeat(prefixWidth);
3089
- return wrapped.map((line, index) => tint(clampLineToWidth(index === 0 ? prefix + line : indent + line, normalized)));
3090
- }
3091
- var wrappedHashlinesCache = new WeakMap;
3092
- function renderPendingResult(pendingLabel, width, theme) {
3093
- return new Text(clampLinesToWidth([summaryLine(pendingLabel, { theme, style: "muted" })], width).join(`
3094
- `), 0, 0);
3095
- }
3096
- function renderErrorResult(textContent, options) {
3097
- const firstLine = textContent.split(`
3098
- `)[0] || (options.fallback ?? "Error");
3099
- const body = options.expanded && textContent ? textContent : firstLine;
3100
- return new Text(clampLinesToWidth(summaryLine(body, { theme: options.theme, style: "error" }).split(`
3101
- `), options.width).join(`
3102
- `), 0, 0);
3103
- }
3104
- function renderReadSeekSearchCall(args, theme, rest, opts) {
3105
- const context = rest[0] ?? {};
3106
- let text = `${renderToolLabel(theme, opts.label)} ${theme.fg("accent", opts.accent)}`;
3107
- text += theme.fg("dim", ` in ${args.path ?? "."}`);
3108
- const language = args.language ?? args.lang;
3109
- if (language)
3110
- text += theme.fg("dim", ` (${language})`);
3111
- const flags = opts.flags.filter(Boolean);
3112
- if (flags.length > 0)
3113
- text += theme.fg("dim", ` [${flags.join(",")}]`);
3114
- return new Text(clampLineToWidth(text, context.width), 0, 0);
3115
- }
3116
- function renderAnchoredFilesResult(result, options, theme, rest, labels) {
3117
- const { isPartial, isError, expanded, cwd, width } = resolveRenderResultContext(options, rest);
3118
- if (isPartial)
3119
- return renderPendingResult(labels.pendingLabel, width, theme);
3120
- const content = result.content?.[0];
3121
- const textContent = content?.type === "text" ? content.text : "";
3122
- if (isError || result.isError)
3123
- return renderErrorResult(textContent, { expanded, width, theme });
3124
- const readSeekValue = result.details?.readSeekValue;
3125
- const files = readSeekValue?.files ?? [];
3126
- if (files.length === 0)
3127
- return new Text(summaryLine(labels.emptyLabel, { theme, style: "dim" }), 0, 0);
3128
- const fileCount = files.length;
3129
- const total = files.reduce((sum, f) => sum + f.lines.length, 0);
3130
- const unitWord = total === 1 ? labels.unitSingular : labels.unitPlural;
3131
- const fileWord = fileCount === 1 ? "file" : "files";
3132
- let text = summaryLine(`${total} ${unitWord} in ${fileCount} ${fileWord}`, {
3133
- hidden: !expanded,
3134
- theme,
3135
- style: "success"
3136
- });
3137
- if (expanded) {
3138
- for (const file of files.slice(0, 20)) {
3139
- const display = relative(cwd, file.path) || file.path;
3140
- text += `
3141
- ${linkedPathLine(theme, file.path, display, cwd, ` (${file.lines.length})`, width)}`;
3142
- }
3143
- if (files.length > 20)
3144
- text += `
3145
- ` + theme.fg("muted", ` … and ${files.length - 20} more files`);
3146
- }
3147
- return new Text(clampLinesToWidth(text.split(`
3148
- `), width).join(`
3149
- `), 0, 0);
3150
- }
3151
-
3152
- // src/tui-source-render.ts
3153
- import { getLanguageFromPath, highlightCode } from "@earendil-works/pi-coding-agent";
3154
- import { wrapTextWithAnsi as wrapTextWithAnsi2 } from "@earendil-works/pi-tui";
3155
- var READ_ANCHOR_RE = /^(\d+:[0-9a-zA-Z]{1,16})\|(.*)$/;
3156
- var GREP_ANCHOR_RE = /^(.*?):(>>| )(\d+):([0-9a-zA-Z]{1,16})\|(.*)$/;
3157
- var SOURCE_THEME_STYLES = [
3158
- "mdCodeBlock",
3159
- "muted",
3160
- "syntaxComment",
3161
- "syntaxKeyword",
3162
- "syntaxFunction",
3163
- "syntaxVariable",
3164
- "syntaxString",
3165
- "syntaxNumber",
3166
- "syntaxType",
3167
- "syntaxOperator",
3168
- "syntaxPunctuation",
3169
- "toolDiffAdded",
3170
- "toolDiffRemoved"
3171
- ];
3172
- function wrapDisplayLines(lines, width) {
3173
- if (width === undefined || width === null)
3174
- return lines;
3175
- const normalized = normalizeWidth(width);
3176
- return lines.flatMap((line) => {
3177
- const wrapped = wrapTextWithAnsi2(line, normalized);
3178
- return wrapped.length > 0 ? wrapped : [""];
3179
- });
3180
- }
3181
- function renderSourceBlock(lines, path3) {
3182
- return highlightCode(lines.join(`
3183
- `), getLanguageFromPath(path3));
3184
- }
3185
- function renderReadSourceForDisplay(text, path3, anchors, width) {
3186
- const output = [];
3187
- let sourceLines = [];
3188
- const flushSourceLines = () => {
3189
- if (sourceLines.length === 0)
3190
- return;
3191
- output.push(...wrapDisplayLines(renderSourceBlock(sourceLines, path3), width));
3192
- sourceLines = [];
3193
- };
3194
- for (const line of text.split(`
3195
- `)) {
3196
- const match = line.match(READ_ANCHOR_RE);
3197
- if (match && anchors.has(match[1])) {
3198
- sourceLines.push(match[2] ?? "");
3199
- continue;
3200
- }
3201
- flushSourceLines();
3202
- output.push(...wrapDisplayLines([line], width));
3203
- }
3204
- flushSourceLines();
3205
- return output.join(`
3206
- `);
3207
- }
3208
- var readSourceCache = new WeakMap;
3209
- function renderReadSourceForDisplayCached(cacheKey, text, path3, anchors, width, theme) {
3210
- const themeKey = SOURCE_THEME_STYLES.map((style) => theme.fg(style, "")).join("");
3211
- const anchorKey = [...anchors].join(`
3212
- `);
3213
- const cached = readSourceCache.get(cacheKey);
3214
- if (cached && cached.text === text && cached.path === path3 && cached.width === width && cached.theme === themeKey && cached.anchors === anchorKey) {
3215
- return cached.rendered;
3216
- }
3217
- const rendered = renderReadSourceForDisplay(text, path3, anchors, width);
3218
- readSourceCache.set(cacheKey, { text, path: path3, width, theme: themeKey, anchors: anchorKey, rendered });
3219
- return rendered;
3220
- }
3221
- function renderGrepSourceForDisplay(text, anchors, renderPath = (path3) => path3) {
3222
- return text.split(`
3223
- `).map((line) => {
3224
- const match = line.match(GREP_ANCHOR_RE);
3225
- if (!match)
3226
- return line;
3227
- const anchor = `${match[3]}:${match[4]}`;
3228
- if (!anchors.has(anchor))
3229
- return line;
3230
- const separator = match[2] === ">>" ? ":" : "-";
3231
- return `${renderPath(match[1])}${separator}${match[3]}${separator}${match[5] ?? ""}`;
3232
- }).join(`
3233
- `);
3234
- }
3235
-
3236
- // src/register-tool.ts
3237
- import { Type } from "@sinclair/typebox";
3238
- function optionalIntOrString(description) {
3239
- return Type.Optional(Type.Union([Type.Number({ description }), Type.String({ description })]));
3240
- }
3241
- function filePathParam() {
3242
- return Type.String({ description: "Absolute or relative file path" });
3243
- }
3244
- function registerReadSeekTool(pi, tool) {
3245
- pi.registerTool(tool);
3246
- return tool;
3247
- }
3248
-
3249
- // src/read.ts
3250
- function hasReadAnchors(result) {
3251
- const details = result.details;
3252
- if (!details || typeof details !== "object")
3253
- return false;
3254
- const readSeekValue = details.readSeekValue;
3255
- if (!readSeekValue || typeof readSeekValue !== "object")
3256
- return false;
3257
- const lines = readSeekValue.lines;
3258
- return Array.isArray(lines) && lines.length > 0;
3259
- }
3260
- function formatImageAnalysis(detection, mode) {
3261
- if (detection.kind !== "image")
3262
- return;
3263
- const sections = [];
3264
- const ocr = detection.ocr?.trim();
3265
- if (ocr)
3266
- sections.push(`OCR text from image:
3267
- ${ocr}`);
3268
- const caption = detection.caption?.trim();
3269
- if (caption)
3270
- sections.push(`Image caption:
3271
- ${caption}`);
3272
- if (detection.objects?.length) {
3273
- const lines = detection.objects.map((object) => `- ${object.label} [${object.bbox.join(", ")}]`);
3274
- sections.push(`Detected objects:
3275
- ${lines.join(`
3276
- `)}`);
3277
- }
3278
- if (sections.length > 0)
3279
- return sections.join(`
3280
-
3281
- `);
3282
- if (mode === "ocr")
3283
- return "No OCR text detected in image.";
3284
- if (mode === "objects")
3285
- return "No objects detected in image.";
3286
- if (mode === "all")
3287
- return "No image analysis returned.";
3288
- return "Image caption returned no text.";
3289
- }
3290
- function documentRedirectMessage(path3, cache) {
3291
- const cacheNote = cache ? ` Index ${cache.state} (hash ${cache.content_hash}).` : "";
3292
- return `Document content is not available via readSeek_read for ${path3}.${cacheNote} Use readSeek_view to digest structure or selected content.`;
3293
- }
3294
- function skippedImageFile(path3) {
3295
- return {
3296
- content: [{ type: "text", text: `[Skipped image: ${path3}; no image mode selected]` }],
3297
- details: {}
3298
- };
3299
- }
3300
- async function executeRead(opts) {
3301
- const { params, signal, cwd, onSuccessfulRead } = opts;
3302
- await ensureHashInit();
3303
- const rawParams = params;
3304
- const offset = coerceObviousBase10Int(rawParams.offset, "offset");
3305
- if (!offset.ok) {
3306
- return buildToolErrorResult("read", "invalid-offset", offset.message, { path: rawParams.path });
3307
- }
3308
- const limit = coerceObviousBase10Int(rawParams.limit, "limit");
3309
- if (!limit.ok) {
3310
- return buildToolErrorResult("read", "invalid-limit", limit.message, { path: rawParams.path });
3311
- }
3312
- if (limit.value !== undefined && limit.value < 1) {
3313
- const message = `Invalid limit: expected a positive integer, received ${limit.value}.`;
3314
- return buildToolErrorResult("read", "invalid-limit", message, { path: rawParams.path });
3315
- }
3316
- if (offset.value !== undefined && offset.value < 1) {
3317
- const message = `Invalid offset: expected a positive integer, received ${offset.value}.`;
3318
- return buildToolErrorResult("read", "invalid-offset", message, { path: rawParams.path });
3319
- }
3320
- const rawSymbol = typeof rawParams.symbol === "string" && rawParams.symbol.trim().length > 0 ? rawParams.symbol.trim() : undefined;
3321
- const rawBundle = typeof rawParams.bundle === "string" ? rawParams.bundle.trim() : undefined;
3322
- const p = {
3323
- ...rawParams,
3324
- offset: offset.value,
3325
- limit: limit.value,
3326
- symbol: rawSymbol,
3327
- bundle: rawBundle
3328
- };
3329
- const rawPath = p.path.replace(/^@/, "");
3330
- const absolutePath = resolveToCwd(rawPath, cwd);
3331
- const succeed = (result) => {
3332
- const isError = result.isError;
3333
- if (!isError && hasReadAnchors(result)) {
3334
- onSuccessfulRead?.(absolutePath);
3335
- }
3336
- return result;
3337
- };
3338
- throwIfAborted(signal);
3339
- if (p.bundle !== undefined && p.bundle !== "local") {
3340
- const message = `Invalid bundle: expected "local", received ${JSON.stringify(p.bundle)}.`;
3341
- return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
3342
- }
3343
- if (p.symbol && (p.offset !== undefined || p.limit !== undefined)) {
3344
- const message = "Cannot combine symbol with offset/limit. Use one or the other.";
3345
- return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
3346
- }
3347
- if (p.bundle && !p.symbol) {
3348
- const message = 'Cannot use bundle without symbol. Use read({ path, symbol, bundle: "local" }).';
3349
- return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
3350
- }
3351
- throwIfAborted(signal);
3352
- const ext = rawPath.split(".").pop()?.toLowerCase() ?? "";
3353
- let rawBuffer;
3354
- try {
3355
- rawBuffer = await fsReadFile(absolutePath);
3356
- } catch (err) {
3357
- const { code, message } = formatFsError(err, "read-error");
3358
- return buildToolErrorResult("read", code, message, {
3359
- path: rawParams.path
3360
- });
3361
- }
3362
- const hasBinaryContent = looksLikeBinary(rawBuffer);
3363
- const hasPdfHeader = rawBuffer.subarray(0, 1024).includes(Buffer.from("%PDF-"));
3364
- const maybeVisual = hasBinaryContent || ext === "pdf" || hasPdfHeader;
3365
- let earlyDigest;
3366
- if (maybeVisual) {
3367
- let detection;
3368
- try {
3369
- earlyDigest = await readSeekDigestContent(absolutePath, {
3370
- ...ext === "pdf" || hasPdfHeader ? {} : { visionMode: "none" },
3371
- signal
3372
- });
3373
- detection = earlyDigest.metadata;
3374
- } catch {
3375
- try {
3376
- detection = await readSeekDetect(absolutePath, { signal });
3377
- } catch {
3378
- return buildToolErrorResult("read", "read-error", `Visual file detection failed for ${absolutePath}.`, {
3379
- path: rawParams.path
3380
- });
3381
- }
3382
- }
3383
- if (detection.kind === "pdf") {
3384
- return buildToolErrorResult("read", "document-use-view", documentRedirectMessage(rawParams.path, earlyDigest?.cache), { path: rawParams.path });
3385
- }
3386
- if (detection.kind === "image") {
3387
- if (p.offset !== undefined || p.limit !== undefined) {
3388
- const message = "Cannot combine offset/limit with image reads.";
3389
- return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
3390
- }
3391
- const imageMode = resolveReadSeekImageMode();
3392
- const useNativeVision = imageMode === "auto" && opts.modelSupportsImages === true;
3393
- if (imageMode === "on" && p.image === "none") {
3394
- return buildToolErrorResult("read", "invalid-params-combo", 'image="none" is only available when imageMode is "auto".', {
3395
- path: rawParams.path
3396
- });
3397
- }
3398
- if (imageMode === "off" || p.image === undefined) {
3399
- return skippedImageFile(rawParams.path);
3400
- }
3401
- if (useNativeVision || p.image === "none") {
3402
- try {
3403
- const prepared = earlyDigest?.image && earlyDigest.image.encoding === "base64" && earlyDigest.image.data !== undefined && earlyDigest.image.mime !== undefined ? { mime: earlyDigest.image.mime, encoding: earlyDigest.image.encoding, data: earlyDigest.image.data } : await readSeekPreparedImage(absolutePath, { signal });
3404
- return succeed({
3405
- content: [{ type: "image", data: prepared.data, mimeType: prepared.mime }],
3406
- details: {}
3407
- });
3408
- } catch (error) {
3409
- const message = error instanceof Error ? error.message : String(error);
3410
- return buildToolErrorResult("read", "read-error", `Image preprocessing unavailable: ${message}`, {
3411
- path: rawParams.path
3412
- });
3413
- }
3414
- }
3415
- try {
3416
- const analysis = await readSeekImage(absolutePath, [p.image], { signal });
3417
- const imageAnalysis = formatImageAnalysis(analysis, p.image);
3418
- if (imageAnalysis) {
3419
- return succeed({
3420
- content: [{ type: "text", text: imageAnalysis }],
3421
- details: {}
3422
- });
3423
- }
3424
- } catch (error) {
3425
- const message = error instanceof Error ? error.message : String(error);
3426
- return buildToolErrorResult("read", "read-error", `Image analysis unavailable: ${message}`, {
3427
- path: rawParams.path
3428
- });
3429
- }
3430
- return buildToolErrorResult("read", "read-error", "Image analysis returned no content.", {
3431
- path: rawParams.path
3432
- });
3433
- }
3434
- }
3435
- throwIfAborted(signal);
3436
- const rawText = rawBuffer.toString("utf-8");
3437
- const normalized = normalizeToLF(stripBom(rawText).text);
3438
- const allLines = splitReadSeekLines(normalized);
3439
- const total = allLines.length;
3440
- const structuredWarnings = [];
3441
- let startLine = p.offset !== undefined ? p.offset : 1;
3442
- let endIdx = p.limit !== undefined ? Math.min(startLine - 1 + p.limit, total) : total;
3443
- if (p.offset !== undefined && startLine > total) {
3444
- const message = `offset ${p.offset} is past end of file (${total} lines)`;
3445
- return buildToolErrorResult("read", "offset-past-end", message, { path: rawParams.path });
3446
- }
3447
- let symbolMatch;
3448
- let symbolFileMap = null;
3449
- let symbolWarning;
3450
- let bundleMetadata = null;
3451
- if (p.symbol) {
3452
- symbolFileMap = await getOrGenerateMap(absolutePath);
3453
- if (!symbolFileMap) {
3454
- const extLabel = ext || "unknown";
3455
- symbolWarning = `[Warning: symbol lookup not available for .${extLabel} files — showing full file]
3456
-
3457
- `;
3458
- } else {
3459
- const lookup = findSymbol(symbolFileMap, p.symbol);
3460
- if (lookup.type === "ambiguous") {
3461
- return succeed({
3462
- content: [
3463
- {
3464
- type: "text",
3465
- text: formatAmbiguous(p.symbol, lookup.candidates)
3466
- }
3467
- ],
3468
- isError: false,
3469
- details: {}
3470
- });
3471
- }
3472
- if (lookup.type === "not-found") {
3473
- symbolWarning = `${formatNotFound(p.symbol, symbolFileMap)}
3474
-
3475
- `;
3476
- }
3477
- if (lookup.type === "found") {
3478
- startLine = Math.max(1, lookup.symbol.startLine);
3479
- endIdx = Math.min(total, lookup.symbol.endLine);
3480
- symbolMatch = lookup.symbol;
3481
- }
3482
- if (lookup.type === "fuzzy") {
3483
- startLine = Math.max(1, lookup.symbol.startLine);
3484
- endIdx = Math.min(total, lookup.symbol.endLine);
3485
- symbolMatch = lookup.symbol;
3486
- const tierLabel = lookup.tier === "camelCase" ? "camelCase word boundary" : "substring";
3487
- const otherNames = lookup.otherCandidates.map((c) => `\`${c.name}\``).join(", ");
3488
- const confirmHint = `read({ symbol: "${lookup.symbol.name}" }) or ${lookup.symbol.name}@${lookup.symbol.startLine} to select by start line`;
3489
- const lines = [
3490
- `[Symbol '${p.symbol}' not exact-matched. Closest match: \`${lookup.symbol.name}\` (${lookup.symbol.kind}, lines ${lookup.symbol.startLine}-${lookup.symbol.endLine}) via ${tierLabel}.`
3491
- ];
3492
- if (otherNames)
3493
- lines.push(` Other candidates: ${otherNames}.`);
3494
- lines.push(` To confirm: ${confirmHint}.]`);
3495
- const bannerText = lines.join(`
3496
- `);
3497
- structuredWarnings.push(buildReadSeekWarning("fuzzy-symbol-match", bannerText, {
3498
- tier: lookup.tier,
3499
- symbol: lookup.symbol,
3500
- otherCandidates: lookup.otherCandidates
3501
- }));
3502
- }
3503
- }
3504
- }
3505
- if (p.bundle === "local") {
3506
- if (!symbolFileMap) {
3507
- const extLabel = ext || "unknown";
3508
- const warning = buildReadSeekWarning("bundle-unmappable", `[Warning: local bundle unavailable because symbol mapping is not available for .${extLabel} files — showing plain symbol read]`);
3509
- structuredWarnings.push(warning);
3510
- bundleMetadata = {
3511
- mode: "local",
3512
- applied: false,
3513
- localSupport: [],
3514
- warnings: [warning]
3515
- };
3516
- } else if (!symbolMatch) {
3517
- bundleMetadata = {
3518
- mode: "local",
3519
- applied: false,
3520
- localSupport: [],
3521
- warnings: []
3522
- };
3523
- } else {
3524
- const bundle = buildLocalBundle(symbolFileMap, symbolMatch, allLines);
3525
- if (!bundle) {
3526
- const warning = buildReadSeekWarning("bundle-context-unavailable", `[Warning: local bundle context could not be determined for symbol '${symbolMatch.name}' — showing plain symbol read]`);
3527
- structuredWarnings.push(warning);
3528
- bundleMetadata = {
3529
- mode: "local",
3530
- applied: false,
3531
- localSupport: [],
3532
- warnings: [warning]
3533
- };
3534
- } else {
3535
- bundleMetadata = {
3536
- mode: "local",
3537
- applied: true,
3538
- localSupport: bundle.support.map((item) => ({
3539
- symbol: {
3540
- query: item.symbol.name,
3541
- name: item.symbol.name,
3542
- kind: item.symbol.kind,
3543
- parentName: item.symbol.parentName,
3544
- startLine: item.symbol.startLine,
3545
- endLine: item.symbol.endLine
3546
- },
3547
- lines: item.lines
3548
- })),
3549
- warnings: []
3550
- };
3551
- }
3552
- }
3553
- }
3554
- throwIfAborted(signal);
3555
- let readSeekOutput;
3556
- try {
3557
- const digested = total === 0 ? await readSeekDigestContent(absolutePath, { signal }) : await readSeekDigestContent(absolutePath, { startLine, endLine: endIdx, signal });
3558
- if (!digested.text) {
3559
- throw new Error(`readseek returned no text content for ${absolutePath}`);
3560
- }
3561
- readSeekOutput = digested.text;
3562
- } catch (err) {
3563
- const detail = err?.message ? ` — ${err.message}` : "";
3564
- const message = `readseek failed while reading ${rawPath}${detail}`;
3565
- return buildToolErrorResult("read", "readseek-error", message, { path: rawParams.path, hint: "Ensure @jarkkojs/readseek and its npm platform package are installed.", details: { message: err?.message } });
3566
- }
3567
- const expectedLineCount = Math.max(0, endIdx - startLine + 1);
3568
- const invalidLine = readSeekOutput.hashlines.find((line, index) => line.line !== startLine + index);
3569
- if (readSeekOutput.hashlines.length !== expectedLineCount || invalidLine) {
3570
- const message = invalidLine ? `readseek returned non-sequential line ${invalidLine.line} for requested range ${startLine}-${endIdx}` : `readseek returned ${readSeekOutput.hashlines.length} lines for requested range ${startLine}-${endIdx} (${expectedLineCount} expected)`;
3571
- return buildToolErrorResult("read", "readseek-output-mismatch", message, { path: rawParams.path });
3572
- }
3573
- const readSeekLines = readSeekOutput.hashlines.map((line) => ({
3574
- line: line.line,
3575
- hash: line.hash,
3576
- anchor: `${line.line}:${line.hash}`,
3577
- raw: line.text,
3578
- display: escapeControlCharsForDisplay(line.text)
3579
- }));
3580
- const selected = readSeekLines.map((line) => line.raw);
3581
- const formatted = renderReadSeekLines(readSeekLines);
3582
- const truncation = truncateHead2(formatted, { maxLines: DEFAULT_MAX_LINES2, maxBytes: DEFAULT_MAX_BYTES2 });
3583
- const shouldAppendMap = !!truncation.truncated && !p.offset && !p.limit && !symbolMatch;
3584
- let appendedMap = false;
3585
- let mapText = null;
3586
- if (shouldAppendMap) {
3587
- try {
3588
- const fileMap = await getOrGenerateMap(absolutePath);
3589
- if (fileMap) {
3590
- const formattedMap = formatFileMapWithBudget(fileMap);
3591
- mapText = formattedMap;
3592
- appendedMap = true;
3593
- }
3594
- } catch {}
3595
- }
3596
- let currentBuffer;
3597
- try {
3598
- currentBuffer = await fsReadFile(absolutePath);
3599
- } catch (err) {
3600
- const { code, message } = formatFsError(err, "read-error");
3601
- return buildToolErrorResult("read", code, message, { path: rawParams.path });
3602
- }
3603
- if (!currentBuffer.equals(rawBuffer)) {
3604
- return buildToolErrorResult("read", "file-changed-during-read", `File changed while it was being read: ${rawPath}. Re-read it to obtain a consistent snapshot.`, { path: rawParams.path });
3605
- }
3606
- if (symbolWarning) {
3607
- structuredWarnings.push(buildReadSeekWarning("symbol-warning", symbolWarning.trim()));
3608
- }
3609
- if (hasBinaryContent) {
3610
- const warning = "[Warning: file appears to be binary — output may be garbled]";
3611
- structuredWarnings.push(buildReadSeekWarning("binary-content", warning));
3612
- }
3613
- if (hasBareCarriageReturn(rawText)) {
3614
- const warning = "[Warning: file contains bare CR (\\r) line endings — line numbering may be inconsistent with grep and other tools]";
3615
- structuredWarnings.push(buildReadSeekWarning("bare-cr", warning));
3616
- }
3617
- const readOutput = buildReadOutput({
3618
- path: absolutePath,
3619
- startLine,
3620
- endLine: endIdx,
3621
- totalLines: total,
3622
- selectedLines: selected,
3623
- lines: readSeekLines,
3624
- warnings: structuredWarnings,
3625
- truncation: truncation.truncated ? {
3626
- outputLines: truncation.outputLines,
3627
- totalLines: total,
3628
- outputBytes: truncation.outputBytes,
3629
- totalBytes: truncation.totalBytes
3630
- } : null,
3631
- continuation: !truncation.truncated && endIdx < total ? { nextOffset: endIdx + 1 } : null,
3632
- symbol: symbolMatch ? {
3633
- query: p.symbol ?? symbolMatch.name,
3634
- name: symbolMatch.name,
3635
- kind: symbolMatch.kind,
3636
- parentName: symbolMatch.parentName,
3637
- startLine: symbolMatch.startLine,
3638
- endLine: symbolMatch.endLine
3639
- } : null,
3640
- map: {
3641
- requested: false,
3642
- appended: appendedMap,
3643
- text: mapText
3644
- },
3645
- ...bundleMetadata ? { bundle: bundleMetadata } : {}
3646
- });
3647
- return succeed({
3648
- content: [{ type: "text", text: readOutput.text }],
3649
- details: {
3650
- truncation: truncation.truncated ? truncation : undefined,
3651
- readSeekValue: readOutput.readSeekValue
3652
- }
3653
- });
3654
- }
3655
- function splitReadSeekLines(text) {
3656
- if (text.length === 0)
3657
- return [];
3658
- const withoutTrailingTerminator = text.endsWith(`
3659
- `) ? text.slice(0, -1) : text;
3660
- return withoutTrailingTerminator.split(`
3661
- `);
3662
- }
3663
- function registerReadTool(pi, options = {}) {
3664
- const name = options.name ?? "readSeek_read";
3665
- const imageMode = resolveReadSeekImageMode();
3666
- const imageModes = imageMode === "auto" ? ["none", "all", "ocr", "caption", "objects"] : ["all", "ocr", "caption", "objects"];
3667
- const promptMetadata = defineToolPromptMetadata({
3668
- promptUrl: new URL("../prompts/read.md", import.meta.url),
3669
- promptSnippet: imageMode === "off" ? "Read anchored text or symbols" : "Read anchored text, symbols, or images",
3670
- registeredName: name
3671
- });
3672
- const tool = registerReadSeekTool(pi, {
3673
- name,
3674
- label: "Read",
3675
- description: imageMode === "off" ? "Read anchored text by range or symbol. Images and documents are disabled; use readSeek_view for documents." : promptMetadata.description,
3676
- promptSnippet: promptMetadata.promptSnippet,
3677
- promptGuidelines: [
3678
- ...promptMetadata.promptGuidelines,
3679
- ...imageMode === "auto" ? ["Image-capable Pi models receive prepared images directly. For standalone images, image=none skips local analysis; other modes use ReadSeek when native vision is unavailable."] : []
3680
- ],
3681
- parameters: Type2.Object({
3682
- path: filePathParam(),
3683
- offset: optionalIntOrString("One-based starting line"),
3684
- limit: optionalIntOrString("Maximum lines to return"),
3685
- symbol: Type2.Optional(Type2.String({ description: "Symbol name to read" })),
3686
- bundle: Type2.Optional(Type2.Literal("local", {
3687
- description: "Include related symbols from the same file; requires symbol"
3688
- })),
3689
- ...imageMode === "off" ? {} : {
3690
- image: Type2.Optional(Type2.Union(imageModes.map((mode) => Type2.Literal(mode)), {
3691
- description: `Standalone image mode: ${imageModes.join(", ")}. Required for standalone images.${imageMode === "auto" ? " For standalone images, none skips local analysis." : ""}`
3692
- }))
3693
- }
3694
- }),
3695
- async execute(toolCallId, params, signal, onUpdate, ctx) {
3696
- return executeRead({
3697
- toolCallId,
3698
- params,
3699
- signal,
3700
- onUpdate,
3701
- cwd: ctx.cwd,
3702
- onSuccessfulRead: options.onSuccessfulRead,
3703
- modelSupportsImages: ctx.model?.input.includes("image") ?? false
3704
- });
3705
- },
3706
- renderCall(args, theme, ...rest) {
3707
- const context = rest[0] ?? {};
3708
- const cwd = context.cwd ?? process.cwd();
3709
- const { path: filePath, suffix } = formatReadCallText(args);
3710
- const rangeSuffix = typeof args?.offset === "number" && typeof args?.limit === "number" && args.offset > 0 && args.limit > 0 ? `:${args.offset}-${args.offset + args.limit - 1}` : "";
3711
- let text = renderToolLabel(theme, "read");
3712
- if (filePath) {
3713
- text += ` ${linkToolPath(theme.fg("accent", `${filePath}${rangeSuffix}`), filePath, cwd)}`;
3714
- } else {
3715
- text += ` ${theme.fg("toolOutput", "...")}`;
3716
- }
3717
- if (!rangeSuffix && suffix)
3718
- text += ` ${theme.fg("dim", suffix)}`;
3719
- return new Text2(clampLineToWidth(text, context.width), 0, 0);
3720
- },
3721
- renderResult(result, options2, theme, ...rest) {
3722
- const { isPartial, isError, expanded, width, context } = resolveRenderResultContext(options2, rest, resolveReadSeekToolDisplayMode("read") === "expanded");
3723
- if (isPartial)
3724
- return renderPendingResult("pending read", width, theme);
3725
- const content = result.content?.[0];
3726
- const textContent = content?.type === "text" ? content.text : "";
3727
- if (isError || result.isError) {
3728
- const firstLine = textContent.split(`
3729
- `)[0] || "Error";
3730
- const errorText = expanded ? textContent || firstLine : firstLine;
3731
- return new Text2(clampLinesToWidth([summaryLine(errorText, { theme, style: "error" })], width).join(`
3732
- `), 0, 0);
3733
- }
3734
- const readSeekValue = result.details?.readSeekValue;
3735
- if (!readSeekValue) {
3736
- const lines = textContent.split(`
3737
- `).filter(Boolean).length;
3738
- let text2 = summaryLine(`loaded ${lines} ${lines === 1 ? "line" : "lines"}`, {
3739
- hidden: !!textContent && !expanded,
3740
- theme,
3741
- style: "success"
3742
- });
3743
- if (expanded && textContent) {
3744
- text2 += `
3745
- ${textContent}`;
3746
- }
3747
- return new Text2(clampLinesToWidth(text2.split(`
3748
- `), width).join(`
3749
- `), 0, 0);
3750
- }
3751
- const info = formatReadResultText({ range: readSeekValue.range, truncation: readSeekValue.truncation, symbol: readSeekValue.symbol, map: readSeekValue.map, warnings: readSeekValue.warnings });
3752
- const visibleLines = info.truncated && readSeekValue.truncation ? readSeekValue.truncation.outputLines : readSeekValue.range.endLine - readSeekValue.range.startLine + 1;
3753
- const loadedWord = visibleLines === 1 ? "line" : "lines";
3754
- const summaryParts = [info.truncated ? `loaded ${visibleLines} of ${readSeekValue.truncation?.totalLines ?? readSeekValue.range.totalLines} ${loadedWord} (truncated)` : `loaded ${visibleLines} ${loadedWord}`];
3755
- if (info.symbolBadge)
3756
- summaryParts.push(info.symbolBadge);
3757
- for (const badge of info.badges)
3758
- summaryParts.push(badge);
3759
- const summary = summaryParts.join(" • ");
3760
- let text = summaryLine(summary, { hidden: !!textContent && !expanded, theme, style: "success" });
3761
- if (expanded && textContent) {
3762
- const anchors = new Set(readSeekValue.lines?.map((line) => line.anchor) ?? []);
3763
- for (const item of readSeekValue.bundle?.localSupport ?? []) {
3764
- for (const anchor of item.lineAnchors)
3765
- anchors.add(anchor);
3766
- }
3767
- const sourcePath = readSeekValue.path ?? context.args?.path ?? "";
3768
- text += `
3769
- ` + renderReadSourceForDisplayCached(content, textContent, sourcePath, anchors, width, theme);
3770
- }
3771
- return new Text2(clampLinesToWidth(text.split(`
3772
- `), width).join(`
3773
- `), 0, 0);
3774
- }
3775
- });
3776
- return tool;
3777
- }
3778
-
3779
- // src/edit.ts
3780
- import { readFile as fsReadFile2 } from "node:fs/promises";
3781
- import { createPatch } from "diff";
3782
- import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
3783
- import { Type as Type3 } from "@sinclair/typebox";
3784
- import { Text as Text4 } from "@earendil-works/pi-tui";
3785
-
3786
- // src/edit-render-helpers.ts
3787
- var VARIANT_KEYS = ["set_line", "replace_lines", "insert_after", "replace"];
3788
- function countEditTypes(edits) {
3789
- const counts = {
3790
- set_line: 0,
3791
- replace_lines: 0,
3792
- insert_after: 0,
3793
- replace: 0,
3794
- total: 0
3795
- };
3796
- if (!edits)
3797
- return counts;
3798
- for (const edit of edits) {
3799
- counts.total++;
3800
- if (edit && typeof edit === "object") {
3801
- for (const key of VARIANT_KEYS) {
3802
- if (key in edit) {
3803
- counts[key]++;
3804
- break;
3805
- }
3806
- }
3807
- }
3808
- }
3809
- return counts;
3810
- }
3811
- function parseDiffStats(diff) {
3812
- if (!diff)
3813
- return { added: 0, removed: 0 };
3814
- let added = 0;
3815
- let removed = 0;
3816
- for (const line of diff.split(`
3817
- `)) {
3818
- if (line.startsWith("+++") || line.startsWith("---"))
3819
- continue;
3820
- if (line.startsWith("+"))
3821
- added++;
3822
- else if (line.startsWith("-"))
3823
- removed++;
3824
- }
3825
- return { added, removed };
3826
- }
3827
- function formatEditCallText(args, argsComplete) {
3828
- const rawPath = typeof args?.path === "string" ? args.path : null;
3829
- if (!argsComplete) {
3830
- return { path: rawPath, suffix: undefined };
3831
- }
3832
- const edits = args?.edits;
3833
- if (!Array.isArray(edits) || edits.length === 0) {
3834
- return { path: rawPath, suffix: undefined };
3835
- }
3836
- const counts = countEditTypes(edits);
3837
- const parts = [];
3838
- for (const key of VARIANT_KEYS) {
3839
- if (counts[key] > 0) {
3840
- parts.push(`${counts[key]} ${key}`);
3841
- }
3842
- }
3843
- const word = counts.total === 1 ? "edit" : "edits";
3844
- const suffix = `${counts.total} ${word} (${parts.join(", ")})`;
3845
- return { path: rawPath, suffix };
3846
- }
3847
- function formatEditResultText(input) {
3848
- const { isError, diff, warnings, noopEdits, errorText } = input;
3849
- const isNoOp = isError && (Array.isArray(noopEdits) && noopEdits.length > 0 || errorText.includes("No changes made"));
3850
- const stats = parseDiffStats(diff);
3851
- const hasDiffStats = stats.added > 0 || stats.removed > 0;
3852
- const diffStats = hasDiffStats ? `+${stats.added} / -${stats.removed}` : undefined;
3853
- let warningsBadge;
3854
- if (warnings.length === 1) {
3855
- warningsBadge = "⚠ 1 warning";
3856
- } else if (warnings.length > 1) {
3857
- warningsBadge = `⚠ ${warnings.length} warnings`;
3858
- }
3859
- const showErrorText = isError ? errorText : undefined;
3860
- let semanticBadge;
3861
- if (!isError && !isNoOp && input.semanticClassification) {
3862
- switch (input.semanticClassification) {
3863
- case "whitespace-only":
3864
- semanticBadge = "ws-only";
3865
- break;
3866
- case "semantic":
3867
- semanticBadge = "✓ semantic";
3868
- break;
3869
- case "mixed":
3870
- semanticBadge = "mixed";
3871
- break;
3872
- }
3873
- }
3874
- return {
3875
- diffStats,
3876
- noOp: isNoOp,
3877
- warningsBadge,
3878
- errorText: showErrorText,
3879
- semanticBadge
3880
- };
3881
- }
3882
-
3883
- // src/edit-output.ts
3884
- var EDIT_OPERATION_NAMES = ["set_line", "replace_lines", "insert_after", "replace"];
3885
- function hasNewText(value) {
3886
- return typeof value === "object" && value !== null && typeof value.new_text === "string";
3887
- }
3888
- function getVisibleDiffStats(diff) {
3889
- const stats = parseDiffStats(diff);
3890
- if (stats.added > 0 || stats.removed > 0)
3891
- return stats;
3892
- if (!diff.includes("→"))
3893
- return stats;
3894
- if (diff.includes("→ [deleted]"))
3895
- return { added: 0, removed: 1 };
3896
- return { added: 1, removed: 1 };
3897
- }
3898
- function buildVisibleSummary(displayPath, diff, edits) {
3899
- let stats = getVisibleDiffStats(diff);
3900
- const counts = countEditTypes(edits);
3901
- const editCount = counts.total || 1;
3902
- if (counts.total > 0 && counts.insert_after === counts.total && stats.removed > 0 && stats.added === stats.removed + counts.insert_after) {
3903
- stats = { added: counts.insert_after, removed: 0 };
3904
- }
3905
- const changeWord = editCount === 1 ? "change" : "changes";
3906
- const changedLineCount = Math.max(stats.added, stats.removed);
3907
- const lineWord = changedLineCount === 1 ? "line" : "lines";
3908
- return `Edited ${displayPath} (${editCount} ${changeWord}, +${stats.added} -${stats.removed} ${lineWord})`;
3909
- }
3910
- function extractNewTextValues(edits) {
3911
- const values = [];
3912
- for (const edit of edits ?? []) {
3913
- if (!edit || typeof edit !== "object")
3914
- continue;
3915
- const operations = edit;
3916
- for (const operationName of EDIT_OPERATION_NAMES) {
3917
- const operation = operations[operationName];
3918
- if (hasNewText(operation))
3919
- values.push(operation.new_text);
3920
- }
3921
- }
3922
- return values;
3923
- }
3924
- function formatWhitespaceOnlyWarning(semanticSummary, edits) {
3925
- if (semanticSummary?.classification !== "whitespace-only")
3926
- return;
3927
- if (!extractNewTextValues(edits).some((text) => /\S/.test(text)))
3928
- return;
3929
- return "⚠ Edit classified as whitespace-only — if you intended a behavior change, re-read to verify.";
3930
- }
3931
- function formatReplaceHint(edits, noopEdits) {
3932
- if ((noopEdits ?? []).length > 0)
3933
- return;
3934
- const counts = countEditTypes(edits);
3935
- if (counts.replace === 0)
3936
- return;
3937
- if (counts.replace !== counts.total)
3938
- return;
3939
- return "[info: this edit used replace (unverified). For safer future edits, prefer set_line/replace_lines with an anchor from readSeek_read/readSeek_grep/readSeek_search.]";
3940
- }
3941
- function buildEditOutput(input) {
3942
- const summary = `Updated ${input.displayPath}`;
3943
- const visibleSummary = buildVisibleSummary(input.displayPath, input.diff, input.edits);
3944
- const semanticWarning = formatWhitespaceOnlyWarning(input.semanticSummary, input.edits);
3945
- const warningText = input.warnings.length ? `
3946
-
3947
- Warnings:
3948
- ${input.warnings.join(`
3949
- `)}` : "";
3950
- const replaceHint = formatReplaceHint(input.edits, input.noopEdits);
3951
- let text = visibleSummary;
3952
- if (semanticWarning)
3953
- text += `
3954
- ${semanticWarning}`;
3955
- text += warningText;
3956
- if (replaceHint)
3957
- text += `
3958
- ${replaceHint}`;
3959
- return {
3960
- text,
3961
- patch: input.patch ?? "",
3962
- readSeekValue: buildReadSeekEditResult({
3963
- path: input.path,
3964
- summary,
3965
- diff: input.diff,
3966
- ...input.diffData ? { diffData: input.diffData } : {},
3967
- firstChangedLine: input.firstChangedLine,
3968
- warnings: input.warnings,
3969
- noopEdits: input.noopEdits,
3970
- ...input.semanticSummary ? { semanticSummary: input.semanticSummary } : {}
3971
- })
3972
- };
3973
- }
3974
-
3975
- // src/edit-classify.ts
3976
- import * as Diff2 from "diff";
3977
- function classifyEdit(oldContent, newContent) {
3978
- if (oldContent === newContent) {
3979
- return { classification: "no-op" };
3980
- }
3981
- const oldLines = oldContent.split(`
3982
- `);
3983
- const newLines = newContent.split(`
3984
- `);
3985
- if (oldLines.length === newLines.length) {
3986
- let hasWhitespaceChange2 = false;
3987
- let hasSemanticChange2 = false;
3988
- for (let i = 0;i < oldLines.length; i++) {
3989
- if (oldLines[i] === newLines[i])
3990
- continue;
3991
- if (oldLines[i].trim() === newLines[i].trim()) {
3992
- hasWhitespaceChange2 = true;
3993
- } else {
3994
- hasSemanticChange2 = true;
3995
- }
3996
- }
3997
- if (hasSemanticChange2 && hasWhitespaceChange2)
3998
- return { classification: "mixed" };
3999
- if (hasWhitespaceChange2)
4000
- return { classification: "whitespace-only" };
4001
- return { classification: "semantic" };
4002
- }
4003
- const parts = Diff2.diffLines(oldContent, newContent);
4004
- let hasWhitespaceChange = false;
4005
- let hasSemanticChange = false;
4006
- for (const part of parts) {
4007
- if (!part.added && !part.removed)
4008
- continue;
4009
- const lines = part.value.split(`
4010
- `);
4011
- if (lines[lines.length - 1] === "")
4012
- lines.pop();
4013
- for (const line of lines) {
4014
- if (line.trim() === "") {
4015
- hasWhitespaceChange = true;
4016
- } else {
4017
- hasSemanticChange = true;
4018
- }
4019
- }
4020
- }
4021
- if (hasSemanticChange && hasWhitespaceChange)
4022
- return { classification: "mixed" };
4023
- if (hasWhitespaceChange)
4024
- return { classification: "whitespace-only" };
4025
- return { classification: "semantic" };
4026
- }
4027
-
4028
- // src/edit-syntax-validate.ts
4029
- function dedupeSortLines(diagnostics) {
4030
- const seen = new Set;
4031
- const out = [];
4032
- for (const diagnostic of diagnostics) {
4033
- const key = diagnostic.start_line === diagnostic.end_line ? String(diagnostic.start_line) : `${diagnostic.start_line}-${diagnostic.end_line}`;
4034
- if (!seen.has(key)) {
4035
- seen.add(key);
4036
- out.push({ key, start: diagnostic.start_line });
4037
- }
4038
- }
4039
- out.sort((a, b) => a.start - b.start);
4040
- return out.map((o) => o.key);
4041
- }
4042
- var EMPTY = { errorCount: 0, missingCount: 0, diagnostics: [] };
4043
- async function validateSyntaxRegression(input, options = {}) {
4044
- let before;
4045
- let after;
4046
- try {
4047
- before = input.before === undefined ? EMPTY : await readSeekCheck(input.filePath, input.before, { signal: options.signal });
4048
- after = await readSeekCheck(input.filePath, input.after, { signal: options.signal });
4049
- } catch {
4050
- return null;
4051
- }
4052
- const newErrorCount = Math.max(0, after.errorCount - before.errorCount);
4053
- const newMissingCount = Math.max(0, after.missingCount - before.missingCount);
4054
- if (newErrorCount === 0 && newMissingCount === 0)
4055
- return null;
4056
- return {
4057
- errorLines: dedupeSortLines(after.diagnostics),
4058
- newErrorCount,
4059
- newMissingCount
4060
- };
4061
- }
4062
-
4063
- // src/syntax-validate-mode.ts
4064
- var DEFAULT = "warn";
4065
- function resolveSyntaxValidateMode(opts) {
4066
- return opts.syntaxValidate ?? resolveReadSeekSyntaxValidation() ?? DEFAULT;
2458
+ // src/syntax-validate-mode.ts
2459
+ var DEFAULT = "warn";
2460
+ function resolveSyntaxValidateMode(opts) {
2461
+ return opts.syntaxValidate ?? resolveReadSeekSyntaxValidation() ?? DEFAULT;
4067
2462
  }
4068
2463
 
4069
2464
  // src/diff-data.ts
@@ -4251,66 +2646,204 @@ function buildInlineSpans(removeText, addText) {
4251
2646
  return;
4252
2647
  return { removeSpans, addSpans };
4253
2648
  }
4254
- function buildInlineDiffs(entries) {
4255
- const inlineDiffs = [];
4256
- let remainingPairs = MAX_INLINE_DIFF_PAIRS;
4257
- for (let index = 0;index < entries.length; ) {
4258
- if (entries[index]?.kind !== "remove") {
4259
- index++;
4260
- continue;
4261
- }
4262
- const removeStart = index;
4263
- while (entries[index]?.kind === "remove")
4264
- index++;
4265
- const addStart = index;
4266
- while (entries[index]?.kind === "add")
4267
- index++;
4268
- const removeCount = addStart - removeStart;
4269
- const addCount = index - addStart;
4270
- if (removeCount === 0 || addCount === 0 || removeCount !== addCount)
4271
- continue;
4272
- for (let offset = 0;offset < removeCount; offset++) {
4273
- if (remainingPairs <= 0)
4274
- break;
4275
- remainingPairs--;
4276
- const removeIndex = removeStart + offset;
4277
- const addIndex = addStart + offset;
4278
- const removeEntry = entries[removeIndex];
4279
- const addEntry = entries[addIndex];
4280
- if (removeEntry?.kind !== "remove" || addEntry?.kind !== "add")
4281
- continue;
4282
- const spans = buildInlineSpans(removeEntry.text, addEntry.text);
4283
- if (!spans)
4284
- continue;
4285
- inlineDiffs.push({
4286
- removeLineIndex: removeIndex,
4287
- addLineIndex: addIndex,
4288
- removeSpans: spans.removeSpans,
4289
- addSpans: spans.addSpans
4290
- });
2649
+ function buildInlineDiffs(entries) {
2650
+ const inlineDiffs = [];
2651
+ let remainingPairs = MAX_INLINE_DIFF_PAIRS;
2652
+ for (let index = 0;index < entries.length; ) {
2653
+ if (entries[index]?.kind !== "remove") {
2654
+ index++;
2655
+ continue;
2656
+ }
2657
+ const removeStart = index;
2658
+ while (entries[index]?.kind === "remove")
2659
+ index++;
2660
+ const addStart = index;
2661
+ while (entries[index]?.kind === "add")
2662
+ index++;
2663
+ const removeCount = addStart - removeStart;
2664
+ const addCount = index - addStart;
2665
+ if (removeCount === 0 || addCount === 0 || removeCount !== addCount)
2666
+ continue;
2667
+ for (let offset = 0;offset < removeCount; offset++) {
2668
+ if (remainingPairs <= 0)
2669
+ break;
2670
+ remainingPairs--;
2671
+ const removeIndex = removeStart + offset;
2672
+ const addIndex = addStart + offset;
2673
+ const removeEntry = entries[removeIndex];
2674
+ const addEntry = entries[addIndex];
2675
+ if (removeEntry?.kind !== "remove" || addEntry?.kind !== "add")
2676
+ continue;
2677
+ const spans = buildInlineSpans(removeEntry.text, addEntry.text);
2678
+ if (!spans)
2679
+ continue;
2680
+ inlineDiffs.push({
2681
+ removeLineIndex: removeIndex,
2682
+ addLineIndex: addIndex,
2683
+ removeSpans: spans.removeSpans,
2684
+ addSpans: spans.addSpans
2685
+ });
2686
+ }
2687
+ }
2688
+ return inlineDiffs.length ? inlineDiffs : undefined;
2689
+ }
2690
+ function finalizeDiffData(path3, entries, blockRanges) {
2691
+ const language = inferLanguage(path3);
2692
+ const inlineDiffs = buildInlineDiffs(entries);
2693
+ return {
2694
+ version: 1,
2695
+ entries,
2696
+ stats: buildStats(entries),
2697
+ ...language ? { language } : {},
2698
+ ...blockRanges?.length ? { blockRanges: [...blockRanges] } : {},
2699
+ ...inlineDiffs ? { inlineDiffs } : {}
2700
+ };
2701
+ }
2702
+ function buildDiffData(input) {
2703
+ const entries = parseCompactDiffEntries(input.diff, input.oldContent, input.newContent) ?? parseFullDiffEntries(input.diff);
2704
+ return finalizeDiffData(input.path, entries, input.blockRanges);
2705
+ }
2706
+
2707
+ // src/tui-render-utils.ts
2708
+ import { relative } from "node:path";
2709
+ import { pathToFileURL } from "node:url";
2710
+ import { getCapabilities, hyperlink, Text, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
2711
+ var SUMMARY_PREFIX = "↳";
2712
+ var EXPAND_HINT = " • Ctrl+O to expand";
2713
+ function renderToolLabel(theme, label) {
2714
+ const boldFn = typeof theme.bold === "function" ? theme.bold.bind(theme) : (text) => text;
2715
+ return theme.fg("toolTitle", boldFn(label));
2716
+ }
2717
+ function linkToolPath(styledText, rawPath, cwd) {
2718
+ try {
2719
+ if (!getCapabilities().hyperlinks)
2720
+ return styledText;
2721
+ const absolutePath = resolveToCwd(rawPath, cwd);
2722
+ return hyperlink(styledText, pathToFileURL(absolutePath).href);
2723
+ } catch {
2724
+ return styledText;
2725
+ }
2726
+ }
2727
+ function linkedPathLine(theme, rawPath, displayPath, cwd, suffix, width) {
2728
+ const pathWidth = width === undefined ? undefined : Math.max(1, width - 2 - visibleWidth(suffix));
2729
+ const visiblePath = pathWidth === undefined ? displayPath : truncateToWidth(displayPath, pathWidth);
2730
+ const linkedPath = linkToolPath(theme.fg("dim", visiblePath), rawPath, cwd);
2731
+ return ` ${linkedPath}${theme.fg("dim", suffix)}`;
2732
+ }
2733
+ function appendExpandHint(text, hidden) {
2734
+ return hidden ? `${text}${EXPAND_HINT}` : text;
2735
+ }
2736
+ function summaryLine(summary, options = {}) {
2737
+ const prefix = options.theme ? options.theme.fg(options.style ?? "dim", SUMMARY_PREFIX) : SUMMARY_PREFIX;
2738
+ return appendExpandHint(`${prefix} ${summary}`, !!options.hidden);
2739
+ }
2740
+ function isRendererExpanded(options, context, defaultExpanded = false) {
2741
+ return context?.expanded ?? options?.expanded ?? defaultExpanded;
2742
+ }
2743
+ function resolveRenderResultContext(options, rest, defaultExpanded = false) {
2744
+ const context = rest[0] ?? options ?? {};
2745
+ return {
2746
+ isPartial: context.isPartial ?? options?.isPartial ?? false,
2747
+ isError: context.isError ?? false,
2748
+ expanded: isRendererExpanded(options, context, defaultExpanded),
2749
+ width: context.width ?? options?.width,
2750
+ cwd: context.cwd ?? process.cwd(),
2751
+ context
2752
+ };
2753
+ }
2754
+ function normalizeWidth(width, fallback = 80) {
2755
+ return typeof width === "number" && Number.isFinite(width) && width > 0 ? Math.floor(width) : fallback;
2756
+ }
2757
+ function clampLineToWidth(line, width) {
2758
+ if (width === undefined || width === null)
2759
+ return line;
2760
+ const normalized = normalizeWidth(width);
2761
+ return visibleWidth(line) <= normalized ? line : truncateToWidth(line, normalized);
2762
+ }
2763
+ function clampLinesToWidth(lines, width) {
2764
+ if (width === undefined || width === null)
2765
+ return lines;
2766
+ return lines.map((line) => clampLineToWidth(line, width));
2767
+ }
2768
+ function wrapWithHangingIndent(prefix, content, width, options = {}) {
2769
+ const tint = options.tint ?? ((text) => text);
2770
+ if (width === undefined || width === null)
2771
+ return [tint(prefix + content)];
2772
+ const normalized = normalizeWidth(width);
2773
+ const combined = prefix + content;
2774
+ if (visibleWidth(combined) <= normalized)
2775
+ return [tint(combined)];
2776
+ const prefixWidth = visibleWidth(prefix);
2777
+ const contentWidth = Math.max(1, normalized - prefixWidth);
2778
+ const wrapped = wrapTextWithAnsi(content, contentWidth);
2779
+ if (wrapped.length === 0)
2780
+ return [tint(clampLineToWidth(prefix, normalized))];
2781
+ const indent = " ".repeat(prefixWidth);
2782
+ return wrapped.map((line, index) => tint(clampLineToWidth(index === 0 ? prefix + line : indent + line, normalized)));
2783
+ }
2784
+ var wrappedHashlinesCache = new WeakMap;
2785
+ function renderPendingResult(pendingLabel, width, theme) {
2786
+ return new Text(clampLinesToWidth([summaryLine(pendingLabel, { theme, style: "muted" })], width).join(`
2787
+ `), 0, 0);
2788
+ }
2789
+ function renderErrorResult(textContent, options) {
2790
+ const firstLine = textContent.split(`
2791
+ `)[0] || (options.fallback ?? "Error");
2792
+ const body = options.expanded && textContent ? textContent : firstLine;
2793
+ return new Text(clampLinesToWidth(summaryLine(body, { theme: options.theme, style: "error" }).split(`
2794
+ `), options.width).join(`
2795
+ `), 0, 0);
2796
+ }
2797
+ function renderReadSeekSearchCall(args, theme, rest, opts) {
2798
+ const context = rest[0] ?? {};
2799
+ let text = `${renderToolLabel(theme, opts.label)} ${theme.fg("accent", opts.accent)}`;
2800
+ text += theme.fg("dim", ` in ${args.path ?? "."}`);
2801
+ const language = args.language ?? args.lang;
2802
+ if (language)
2803
+ text += theme.fg("dim", ` (${language})`);
2804
+ const flags = opts.flags.filter(Boolean);
2805
+ if (flags.length > 0)
2806
+ text += theme.fg("dim", ` [${flags.join(",")}]`);
2807
+ return new Text(clampLineToWidth(text, context.width), 0, 0);
2808
+ }
2809
+ function renderAnchoredFilesResult(result, options, theme, rest, labels) {
2810
+ const { isPartial, isError, expanded, cwd, width } = resolveRenderResultContext(options, rest);
2811
+ if (isPartial)
2812
+ return renderPendingResult(labels.pendingLabel, width, theme);
2813
+ const content = result.content?.[0];
2814
+ const textContent = content?.type === "text" ? content.text : "";
2815
+ if (isError || result.isError)
2816
+ return renderErrorResult(textContent, { expanded, width, theme });
2817
+ const readSeekValue = result.details?.readSeekValue;
2818
+ const files = readSeekValue?.files ?? [];
2819
+ if (files.length === 0)
2820
+ return new Text(summaryLine(labels.emptyLabel, { theme, style: "dim" }), 0, 0);
2821
+ const fileCount = files.length;
2822
+ const total = files.reduce((sum, f) => sum + f.lines.length, 0);
2823
+ const unitWord = total === 1 ? labels.unitSingular : labels.unitPlural;
2824
+ const fileWord = fileCount === 1 ? "file" : "files";
2825
+ let text = summaryLine(`${total} ${unitWord} in ${fileCount} ${fileWord}`, {
2826
+ hidden: !expanded,
2827
+ theme,
2828
+ style: "success"
2829
+ });
2830
+ if (expanded) {
2831
+ for (const file of files.slice(0, 20)) {
2832
+ const display = relative(cwd, file.path) || file.path;
2833
+ text += `
2834
+ ${linkedPathLine(theme, file.path, display, cwd, ` (${file.lines.length})`, width)}`;
4291
2835
  }
2836
+ if (files.length > 20)
2837
+ text += `
2838
+ ` + theme.fg("muted", ` … and ${files.length - 20} more files`);
4292
2839
  }
4293
- return inlineDiffs.length ? inlineDiffs : undefined;
4294
- }
4295
- function finalizeDiffData(path3, entries, blockRanges) {
4296
- const language = inferLanguage(path3);
4297
- const inlineDiffs = buildInlineDiffs(entries);
4298
- return {
4299
- version: 1,
4300
- entries,
4301
- stats: buildStats(entries),
4302
- ...language ? { language } : {},
4303
- ...blockRanges?.length ? { blockRanges: [...blockRanges] } : {},
4304
- ...inlineDiffs ? { inlineDiffs } : {}
4305
- };
4306
- }
4307
- function buildDiffData(input) {
4308
- const entries = parseCompactDiffEntries(input.diff, input.oldContent, input.newContent) ?? parseFullDiffEntries(input.diff);
4309
- return finalizeDiffData(input.path, entries, input.blockRanges);
2840
+ return new Text(clampLinesToWidth(text.split(`
2841
+ `), width).join(`
2842
+ `), 0, 0);
4310
2843
  }
4311
2844
 
4312
2845
  // src/tui-diff-component.ts
4313
- import { Text as Text3, visibleWidth as visibleWidth3 } from "@earendil-works/pi-tui";
2846
+ import { Text as Text2, visibleWidth as visibleWidth3 } from "@earendil-works/pi-tui";
4314
2847
 
4315
2848
  // src/tui-diff-renderer.ts
4316
2849
  import { visibleWidth as visibleWidth2 } from "@earendil-works/pi-tui";
@@ -4478,50 +3011,63 @@ function upsertDiffComponent(lastComponent, options) {
4478
3011
  return component;
4479
3012
  }
4480
3013
  function upsertTextComponent(lastComponent, text) {
4481
- const component = lastComponent && !(lastComponent instanceof DiffPreviewComponent) ? lastComponent : new Text3("", 0, 0);
3014
+ const component = lastComponent && !(lastComponent instanceof DiffPreviewComponent) ? lastComponent : new Text2("", 0, 0);
4482
3015
  component.setText(text);
4483
3016
  return component;
4484
3017
  }
4485
3018
 
3019
+ // src/register-tool.ts
3020
+ import { Type } from "@sinclair/typebox";
3021
+ function optionalIntOrString(description) {
3022
+ return Type.Optional(Type.Union([Type.Number({ description }), Type.String({ description })]));
3023
+ }
3024
+ function filePathParam() {
3025
+ return Type.String({ description: PARAM_DESCRIPTIONS.path });
3026
+ }
3027
+ function registerReadSeekTool(pi, tool) {
3028
+ pi.registerTool(tool);
3029
+ return tool;
3030
+ }
3031
+
4486
3032
  // src/edit.ts
4487
- var hashlineEditItemSchema = Type3.Object({
4488
- set_line: Type3.Optional(Type3.Object({
4489
- anchor: Type3.String({ description: "Fresh LINE:HASH anchor for the line to replace" }),
4490
- new_text: Type3.String({ description: "Replacement text; use an empty string to delete the line" })
4491
- }, { additionalProperties: false, description: 'Replace one line: {"set_line":{"anchor":"LINE:HASH","new_text":"..."}}' })),
4492
- replace_lines: Type3.Optional(Type3.Object({
4493
- start_anchor: Type3.String({ description: "Fresh LINE:HASH anchor for the first line in the range" }),
4494
- end_anchor: Type3.String({ description: "Fresh LINE:HASH anchor for the last line in the range" }),
4495
- new_text: Type3.String({ description: "Replacement text; use an empty string to delete the range" })
4496
- }, { additionalProperties: false, description: 'Replace a range: {"replace_lines":{"start_anchor":"LINE:HASH","end_anchor":"LINE:HASH","new_text":"..."}}' })),
4497
- insert_after: Type3.Optional(Type3.Object({
4498
- anchor: Type3.String({ description: "Fresh LINE:HASH anchor for the line after which to insert" }),
4499
- new_text: Type3.String({ description: "Text to insert after the anchored line" })
4500
- }, { additionalProperties: false, description: 'Insert text: {"insert_after":{"anchor":"LINE:HASH","new_text":"..."}}' })),
4501
- replace: Type3.Optional(Type3.Object({
4502
- old_text: Type3.String({ description: "Exact text to find" }),
4503
- new_text: Type3.String({ description: "Replacement text" }),
4504
- all: Type3.Optional(Type3.Boolean({ description: "Replace every exact match" }))
4505
- }, { additionalProperties: false, description: 'Replace text: {"replace":{"old_text":"...","new_text":"..."}}' })),
4506
- replace_symbol: Type3.Optional(Type3.Object({
4507
- symbol: Type3.String({ description: "Mapped symbol name" }),
4508
- new_body: Type3.String({ description: "Complete replacement body for the symbol" })
4509
- }, { additionalProperties: false, description: 'Replace a symbol: {"replace_symbol":{"symbol":"name","new_body":"..."}}' }))
3033
+ var hashlineEditItemSchema = Type2.Object({
3034
+ set_line: Type2.Optional(Type2.Object({
3035
+ anchor: Type2.String({ description: PARAM_DESCRIPTIONS.anchor }),
3036
+ new_text: Type2.String({ description: PARAM_DESCRIPTIONS.newTextLine })
3037
+ }, { additionalProperties: false, description: PARAM_DESCRIPTIONS.setLine })),
3038
+ replace_lines: Type2.Optional(Type2.Object({
3039
+ start_anchor: Type2.String({ description: PARAM_DESCRIPTIONS.startAnchor }),
3040
+ end_anchor: Type2.String({ description: PARAM_DESCRIPTIONS.endAnchor }),
3041
+ new_text: Type2.String({ description: PARAM_DESCRIPTIONS.newTextRange })
3042
+ }, { additionalProperties: false, description: PARAM_DESCRIPTIONS.replaceLines })),
3043
+ insert_after: Type2.Optional(Type2.Object({
3044
+ anchor: Type2.String({ description: PARAM_DESCRIPTIONS.insertAnchor }),
3045
+ new_text: Type2.String({ description: PARAM_DESCRIPTIONS.newTextInsert })
3046
+ }, { additionalProperties: false, description: PARAM_DESCRIPTIONS.insertAfter })),
3047
+ replace: Type2.Optional(Type2.Object({
3048
+ old_text: Type2.String({ description: PARAM_DESCRIPTIONS.oldText }),
3049
+ new_text: Type2.String({ description: PARAM_DESCRIPTIONS.newText }),
3050
+ all: Type2.Optional(Type2.Boolean({ description: PARAM_DESCRIPTIONS.all }))
3051
+ }, { additionalProperties: false, description: PARAM_DESCRIPTIONS.replace })),
3052
+ replace_symbol: Type2.Optional(Type2.Object({
3053
+ symbol: Type2.String({ description: PARAM_DESCRIPTIONS.symbol }),
3054
+ new_body: Type2.String({ description: PARAM_DESCRIPTIONS.newBody })
3055
+ }, { additionalProperties: false, description: PARAM_DESCRIPTIONS.replaceSymbol }))
4510
3056
  }, {
4511
3057
  additionalProperties: false,
4512
3058
  minProperties: 1,
4513
3059
  maxProperties: 1,
4514
- description: "Exactly one nested edit variant"
3060
+ description: PARAM_DESCRIPTIONS.editVariant
4515
3061
  });
4516
- var hashlineEditSchema = Type3.Object({
3062
+ var hashlineEditSchema = Type2.Object({
4517
3063
  path: filePathParam(),
4518
- language: Type3.Optional(Type3.String({ description: "Language override for symbol replacement" })),
4519
- edits: Type3.Array(hashlineEditItemSchema, {
3064
+ language: Type2.Optional(Type2.String({ description: PARAM_DESCRIPTIONS.languageSymbol })),
3065
+ edits: Type2.Array(hashlineEditItemSchema, {
4520
3066
  minItems: 1,
4521
- description: "Use set_line, replace_lines, insert_after, replace_symbol, or replace"
3067
+ description: PARAM_DESCRIPTIONS.edits
4522
3068
  }),
4523
- postEditVerify: Type3.Optional(Type3.Boolean({
4524
- description: "Read back and verify persisted content"
3069
+ postEditVerify: Type2.Optional(Type2.Boolean({
3070
+ description: PARAM_DESCRIPTIONS.postEditVerify
4525
3071
  }))
4526
3072
  }, { additionalProperties: false });
4527
3073
  function buildEditError(path3, code, message, hint, errorDetails) {
@@ -4556,7 +3102,7 @@ async function executeEdit(opts) {
4556
3102
  return await withFileMutationQueue(absolutePath, async () => {
4557
3103
  throwIfAborted(signal);
4558
3104
  if (wasReadInSession && !wasReadInSession(absolutePath)) {
4559
- const hint = `Call readSeek_read(${JSON.stringify(rawPath)}) first, or use readSeek_grep, readSeek_search, or readSeek_write to produce fresh anchors for this file.`;
3105
+ const hint = `Call readSeek_digest(${JSON.stringify(rawPath)}) first, or use readSeek_grep, readSeek_search, or readSeek_write to produce fresh anchors for this file.`;
4560
3106
  return buildEditError(absolutePath, "file-not-read", `You must get fresh anchors for ${absolutePath} before editing it. ${hint}`, hint);
4561
3107
  }
4562
3108
  const edits = parsed.edits ?? [];
@@ -4627,7 +3173,7 @@ async function executeEdit(opts) {
4627
3173
  if (parsed.postEditVerify === true) {
4628
3174
  let verifiedContent;
4629
3175
  try {
4630
- verifiedContent = await fsReadFile2(absolutePath, "utf-8");
3176
+ verifiedContent = await fsReadFile(absolutePath, "utf-8");
4631
3177
  } catch (err) {
4632
3178
  return buildEditError(absolutePath, "post-edit-verification-read-failed", `Edit write completed but post-edit verification failed: could not read ${path3} after writing.`, undefined, { fsCode: err?.code, fsMessage: err?.message });
4633
3179
  }
@@ -4766,7 +3312,7 @@ ${theme.fg("error", info.errorText)}`;
4766
3312
  `), diffData, theme, expanded: true });
4767
3313
  }
4768
3314
  }
4769
- return new Text4(clampLinesToWidth(text.split(`
3315
+ return new Text3(clampLinesToWidth(text.split(`
4770
3316
  `), width).join(`
4771
3317
  `), 0, 0);
4772
3318
  }
@@ -4775,22 +3321,34 @@ ${theme.fg("error", info.errorText)}`;
4775
3321
  }
4776
3322
 
4777
3323
  // src/grep.ts
4778
- import { readFile as fsReadFile3, stat as fsStat } from "node:fs/promises";
3324
+ import { readFile as fsReadFile2, stat as fsStat } from "node:fs/promises";
4779
3325
  import path3 from "node:path";
4780
3326
  import { createGrepTool } from "@earendil-works/pi-coding-agent";
4781
- import { Type as Type5 } from "@sinclair/typebox";
4782
- import { Text as Text5 } from "@earendil-works/pi-tui";
3327
+ import { Type as Type4 } from "@sinclair/typebox";
3328
+ import { Text as Text4 } from "@earendil-works/pi-tui";
3329
+
3330
+ // src/binary-detect.ts
3331
+ function looksLikeBinary(buf) {
3332
+ if (buf.length === 0)
3333
+ return false;
3334
+ if (buf.includes(0))
3335
+ return true;
3336
+ const decoded = buf.toString("utf8");
3337
+ if (!decoded.includes("�"))
3338
+ return false;
3339
+ return !Buffer.from(decoded, "utf8").equals(buf);
3340
+ }
4783
3341
 
4784
3342
  // src/grep-output.ts
4785
3343
  import {
4786
- formatSize as formatSize3,
4787
- truncateHead as truncateHead3
3344
+ formatSize,
3345
+ truncateHead
4788
3346
  } from "@earendil-works/pi-coding-agent";
4789
3347
 
4790
3348
  // src/grep-budget.ts
4791
- import { DEFAULT_MAX_BYTES as DEFAULT_MAX_BYTES3, DEFAULT_MAX_LINES as DEFAULT_MAX_LINES3 } from "@earendil-works/pi-coding-agent";
4792
- var GREP_OUTPUT_DEFAULT_MAX_LINES = DEFAULT_MAX_LINES3;
4793
- var GREP_OUTPUT_DEFAULT_MAX_BYTES = Math.min(DEFAULT_MAX_BYTES3, 50 * 1024);
3349
+ import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES } from "@earendil-works/pi-coding-agent";
3350
+ var GREP_OUTPUT_DEFAULT_MAX_LINES = DEFAULT_MAX_LINES;
3351
+ var GREP_OUTPUT_DEFAULT_MAX_BYTES = Math.min(DEFAULT_MAX_BYTES, 50 * 1024);
4794
3352
  function resolveDimension(jsonValue, ceiling) {
4795
3353
  if (jsonValue !== undefined)
4796
3354
  return Math.min(jsonValue, ceiling);
@@ -4876,14 +3434,14 @@ ${passthroughLines.join(`
4876
3434
  ${text}`;
4877
3435
  }
4878
3436
  const budget = resolveGrepOutputBudget();
4879
- const truncated = truncateHead3(text, {
3437
+ const truncated = truncateHead(text, {
4880
3438
  maxLines: budget.maxLines,
4881
3439
  maxBytes: budget.maxBytes
4882
3440
  });
4883
3441
  if (truncated.truncated) {
4884
3442
  text = `${truncated.content}
4885
3443
 
4886
- [Output truncated: showing ${truncated.outputLines} of ${truncated.totalLines} lines (${formatSize3(truncated.outputBytes)} of ${formatSize3(truncated.totalBytes)}). Refine pattern or increase limit.]`;
3444
+ [Output truncated: showing ${truncated.outputLines} of ${truncated.totalLines} lines (${formatSize(truncated.outputBytes)} of ${formatSize(truncated.totalBytes)}). Refine pattern or increase limit.]`;
4887
3445
  }
4888
3446
  const readSeekValue = {
4889
3447
  tool: "grep",
@@ -4905,6 +3463,31 @@ ${text}`;
4905
3463
  };
4906
3464
  }
4907
3465
 
3466
+ // src/file-map.ts
3467
+ import { stat } from "node:fs/promises";
3468
+ async function getOrGenerateMap(absPath) {
3469
+ try {
3470
+ const { size } = await stat(absPath);
3471
+ return await readSeekMap(absPath, size);
3472
+ } catch {
3473
+ return null;
3474
+ }
3475
+ }
3476
+
3477
+ // src/readseek/symbol-tree.ts
3478
+ function traverseSymbolTree(symbols, visit) {
3479
+ const result = [];
3480
+ const walk = (nodes, parentName) => {
3481
+ for (const symbol of nodes) {
3482
+ result.push(visit(symbol, parentName));
3483
+ if (symbol.children?.length)
3484
+ walk(symbol.children, symbol.name);
3485
+ }
3486
+ };
3487
+ walk(symbols);
3488
+ return result;
3489
+ }
3490
+
4908
3491
  // src/grep-symbol-scope.ts
4909
3492
  function findEnclosingSymbol(map, lineNumber) {
4910
3493
  const candidates = traverseSymbolTree(map.symbols, (symbol, parentName) => ({
@@ -5112,19 +3695,73 @@ function formatGrepResultText(input) {
5112
3695
  };
5113
3696
  }
5114
3697
 
3698
+ // src/coerce-obvious-int.ts
3699
+ var BASE10_INT_RE = /^-?\d+$/;
3700
+ var MIN_SAFE_INTEGER = BigInt(Number.MIN_SAFE_INTEGER);
3701
+ var MAX_SAFE_INTEGER = BigInt(Number.MAX_SAFE_INTEGER);
3702
+ function formatReceived(value) {
3703
+ if (typeof value === "number" || typeof value === "bigint")
3704
+ return String(value);
3705
+ return JSON.stringify(value) ?? String(value);
3706
+ }
3707
+ function invalidInteger(value, name) {
3708
+ return {
3709
+ ok: false,
3710
+ message: `Invalid ${name}: expected a safe base-10 integer, received ${formatReceived(value)}.`
3711
+ };
3712
+ }
3713
+ function coerceObviousBase10Int(value, name) {
3714
+ if (value === undefined || typeof value === "string" && value.trim() === "") {
3715
+ return { ok: true, value: undefined };
3716
+ }
3717
+ if (typeof value === "number") {
3718
+ if (Number.isSafeInteger(value)) {
3719
+ return { ok: true, value };
3720
+ }
3721
+ return invalidInteger(value, name);
3722
+ }
3723
+ if (typeof value === "string" && BASE10_INT_RE.test(value)) {
3724
+ const parsed = BigInt(value);
3725
+ if (parsed >= MIN_SAFE_INTEGER && parsed <= MAX_SAFE_INTEGER) {
3726
+ return { ok: true, value: Number(parsed) };
3727
+ }
3728
+ }
3729
+ return invalidInteger(value, name);
3730
+ }
3731
+
3732
+ // src/tui-source-render.ts
3733
+ import { getLanguageFromPath, highlightCode } from "@earendil-works/pi-coding-agent";
3734
+ import { wrapTextWithAnsi as wrapTextWithAnsi2 } from "@earendil-works/pi-tui";
3735
+ var GREP_ANCHOR_RE = /^(.*?):(>>| )(\d+):([0-9a-zA-Z]{1,16})\|(.*)$/;
3736
+ var readSourceCache = new WeakMap;
3737
+ function renderGrepSourceForDisplay(text, anchors, renderPath = (path3) => path3) {
3738
+ return text.split(`
3739
+ `).map((line) => {
3740
+ const match = line.match(GREP_ANCHOR_RE);
3741
+ if (!match)
3742
+ return line;
3743
+ const anchor = `${match[3]}:${match[4]}`;
3744
+ if (!anchors.has(anchor))
3745
+ return line;
3746
+ const separator = match[2] === ">>" ? ":" : "-";
3747
+ return `${renderPath(match[1])}${separator}${match[3]}${separator}${match[5] ?? ""}`;
3748
+ }).join(`
3749
+ `);
3750
+ }
3751
+
5115
3752
  // src/readseek-params.ts
5116
- import { Type as Type4 } from "@sinclair/typebox";
3753
+ import { Type as Type3 } from "@sinclair/typebox";
5117
3754
  function searchPathParam() {
5118
- return Type4.Optional(Type4.String({ description: PARAM_DESCRIPTIONS.searchPath }));
3755
+ return Type3.Optional(Type3.String({ description: PARAM_DESCRIPTIONS.searchPath }));
5119
3756
  }
5120
3757
  function languageParam() {
5121
- return Type4.Optional(Type4.String({ description: PARAM_DESCRIPTIONS.language }));
3758
+ return Type3.Optional(Type3.String({ description: PARAM_DESCRIPTIONS.language }));
5122
3759
  }
5123
3760
  function readSeekGitSearchParams() {
5124
3761
  return {
5125
- cached: Type4.Optional(Type4.Boolean({ description: PARAM_DESCRIPTIONS.cached })),
5126
- others: Type4.Optional(Type4.Boolean({ description: PARAM_DESCRIPTIONS.others })),
5127
- ignored: Type4.Optional(Type4.Boolean({ description: PARAM_DESCRIPTIONS.ignored }))
3762
+ cached: Type3.Optional(Type3.Boolean({ description: PARAM_DESCRIPTIONS.cached })),
3763
+ others: Type3.Optional(Type3.Boolean({ description: PARAM_DESCRIPTIONS.others })),
3764
+ ignored: Type3.Optional(Type3.Boolean({ description: PARAM_DESCRIPTIONS.ignored }))
5128
3765
  };
5129
3766
  }
5130
3767
  function validateIgnoredRequiresOthers(tool, params) {
@@ -5136,19 +3773,19 @@ function validateIgnoredRequiresOthers(tool, params) {
5136
3773
  }
5137
3774
 
5138
3775
  // src/grep.ts
5139
- var grepSchema = Type5.Object({
5140
- pattern: Type5.String({ description: "Regex pattern; set literal for exact text" }),
3776
+ var grepSchema = Type4.Object({
3777
+ pattern: Type4.String({ description: PARAM_DESCRIPTIONS.grepPattern }),
5141
3778
  path: searchPathParam(),
5142
- glob: Type5.Optional(Type5.String({ description: "File-name glob, such as *.ts" })),
5143
- ignoreCase: Type5.Optional(Type5.Boolean({ description: "Ignore case" })),
5144
- literal: Type5.Optional(Type5.Boolean({ description: "Treat pattern literally" })),
5145
- context: optionalIntOrString("Surrounding lines for each match"),
5146
- limit: optionalIntOrString("Maximum matches to return"),
5147
- summary: Type5.Optional(Type5.Boolean({ description: "Return per-file counts" })),
5148
- scope: Type5.Optional(Type5.Literal("symbol", {
5149
- description: "Group matches by enclosing symbol"
3779
+ glob: Type4.Optional(Type4.String({ description: PARAM_DESCRIPTIONS.glob })),
3780
+ ignoreCase: Type4.Optional(Type4.Boolean({ description: PARAM_DESCRIPTIONS.ignoreCase })),
3781
+ literal: Type4.Optional(Type4.Boolean({ description: PARAM_DESCRIPTIONS.literal })),
3782
+ context: optionalIntOrString(PARAM_DESCRIPTIONS.context),
3783
+ limit: optionalIntOrString(PARAM_DESCRIPTIONS.matchLimit),
3784
+ summary: Type4.Optional(Type4.Boolean({ description: PARAM_DESCRIPTIONS.summary })),
3785
+ scope: Type4.Optional(Type4.Literal("symbol", {
3786
+ description: PARAM_DESCRIPTIONS.grepScope
5150
3787
  })),
5151
- scopeContext: optionalIntOrString("Context lines within each symbol")
3788
+ scopeContext: optionalIntOrString(PARAM_DESCRIPTIONS.scopeContext)
5152
3789
  });
5153
3790
  var MATCH_LINE_RE = /^(.*?):(\d+): (.*)$/;
5154
3791
  var CONTEXT_LINE_RE = /^(.*?)-(\d+)- (.*)$/;
@@ -5272,7 +3909,7 @@ async function executeGrep(opts) {
5272
3909
  }
5273
3910
  if (!searchPathIsDirectory) {
5274
3911
  try {
5275
- const buf = await fsReadFile3(searchPath);
3912
+ const buf = await fsReadFile2(searchPath);
5276
3913
  if (looksLikeBinary(buf)) {
5277
3914
  const warning = `[Warning: '${p.path ?? searchPath}' appears to be a binary file — grep skips binary files by default. Use a hex tool or the read tool to inspect it.]`;
5278
3915
  return {
@@ -5298,7 +3935,7 @@ async function executeGrep(opts) {
5298
3935
  if (fileCache.has(absolutePath))
5299
3936
  return fileCache.get(absolutePath);
5300
3937
  try {
5301
- const rawBuffer = await fsReadFile3(absolutePath);
3938
+ const rawBuffer = await fsReadFile2(absolutePath);
5302
3939
  if (looksLikeBinary(rawBuffer)) {
5303
3940
  fileCache.set(absolutePath, undefined);
5304
3941
  return;
@@ -5545,7 +4182,7 @@ function registerGrepTool(pi, options = {}) {
5545
4182
  text += theme.fg("dim", ` in ${suffix}`);
5546
4183
  }
5547
4184
  }
5548
- return new Text5(clampLineToWidth(text, context.width), 0, 0);
4185
+ return new Text4(clampLineToWidth(text, context.width), 0, 0);
5549
4186
  },
5550
4187
  renderResult(result, options2, theme, ...rest) {
5551
4188
  const { isPartial, isError, expanded, cwd, width } = resolveRenderResultContext(options2, rest, resolveReadSeekToolDisplayMode("grep") === "expanded");
@@ -5570,7 +4207,7 @@ function registerGrepTool(pi, options = {}) {
5570
4207
  hasBinaryWarning
5571
4208
  });
5572
4209
  if (info.noMatches && !hasBinaryWarning)
5573
- return new Text5(summaryLine("no matches", { theme, style: "dim" }), 0, 0);
4210
+ return new Text4(summaryLine("no matches", { theme, style: "dim" }), 0, 0);
5574
4211
  const matchCount = readSeekValue?.totalMatches ?? 0;
5575
4212
  const matchWord = matchCount === 1 ? "match" : "matches";
5576
4213
  let text = summaryLine(`${matchCount} ${matchWord} returned`, {
@@ -5585,7 +4222,7 @@ function registerGrepTool(pi, options = {}) {
5585
4222
  text += `
5586
4223
  ` + renderGrepSourceForDisplay(textContent, anchors, (displayPath) => linkToolPath(theme.fg("dim", displayPath), displayPath, cwd));
5587
4224
  }
5588
- return new Text5(clampLinesToWidth(text.split(`
4225
+ return new Text4(clampLinesToWidth(text.split(`
5589
4226
  `), width).join(`
5590
4227
  `), 0, 0);
5591
4228
  }
@@ -5595,7 +4232,7 @@ function registerGrepTool(pi, options = {}) {
5595
4232
 
5596
4233
  // src/search.ts
5597
4234
  import path4 from "node:path";
5598
- import { Type as Type6 } from "@sinclair/typebox";
4235
+ import { Type as Type5 } from "@sinclair/typebox";
5599
4236
 
5600
4237
  // src/stat-search-path.ts
5601
4238
  import { stat as fsStat2 } from "node:fs/promises";
@@ -5762,8 +4399,8 @@ function registerSearchTool(pi, options = {}) {
5762
4399
  description: SEARCH_PROMPT_METADATA.description,
5763
4400
  promptSnippet: SEARCH_PROMPT_METADATA.promptSnippet,
5764
4401
  promptGuidelines: SEARCH_PROMPT_METADATA.promptGuidelines,
5765
- parameters: Type6.Object({
5766
- pattern: Type6.String({ description: PARAM_DESCRIPTIONS.pattern }),
4402
+ parameters: Type5.Object({
4403
+ pattern: Type5.String({ description: PARAM_DESCRIPTIONS.pattern }),
5767
4404
  language: languageParam(),
5768
4405
  path: searchPathParam(),
5769
4406
  ...readSeekGitSearchParams()
@@ -5791,7 +4428,7 @@ function registerSearchTool(pi, options = {}) {
5791
4428
  }
5792
4429
 
5793
4430
  // src/refs.ts
5794
- import { Type as Type7 } from "@sinclair/typebox";
4431
+ import { Type as Type6 } from "@sinclair/typebox";
5795
4432
  import path5 from "node:path";
5796
4433
 
5797
4434
  // src/refs-output.ts
@@ -5910,13 +4547,13 @@ function registerRefsTool(pi, options = {}) {
5910
4547
  description: REFS_PROMPT_METADATA.description,
5911
4548
  promptSnippet: REFS_PROMPT_METADATA.promptSnippet,
5912
4549
  promptGuidelines: REFS_PROMPT_METADATA.promptGuidelines,
5913
- parameters: Type7.Object({
5914
- name: Type7.Optional(Type7.String({ description: PARAM_DESCRIPTIONS.refsName })),
4550
+ parameters: Type6.Object({
4551
+ name: Type6.Optional(Type6.String({ description: PARAM_DESCRIPTIONS.refsName })),
5915
4552
  path: searchPathParam(),
5916
4553
  language: languageParam(),
5917
- scope: Type7.Optional(Type7.Boolean({ description: PARAM_DESCRIPTIONS.scope })),
5918
- line: Type7.Optional(Type7.Number({ description: PARAM_DESCRIPTIONS.line })),
5919
- column: Type7.Optional(Type7.Number({ description: PARAM_DESCRIPTIONS.column })),
4554
+ scope: Type6.Optional(Type6.Boolean({ description: PARAM_DESCRIPTIONS.scope })),
4555
+ line: Type6.Optional(Type6.Number({ description: PARAM_DESCRIPTIONS.line })),
4556
+ column: Type6.Optional(Type6.Number({ description: PARAM_DESCRIPTIONS.column })),
5920
4557
  ...readSeekGitSearchParams()
5921
4558
  }),
5922
4559
  async execute(_toolCallId, params, signal, _onUpdate, ctx) {
@@ -5943,20 +4580,20 @@ function registerRefsTool(pi, options = {}) {
5943
4580
  }
5944
4581
 
5945
4582
  // src/rename.ts
5946
- import { Type as Type8 } from "@sinclair/typebox";
4583
+ import { Type as Type7 } from "@sinclair/typebox";
5947
4584
  import path6 from "node:path";
5948
- import { Text as Text6 } from "@earendil-works/pi-tui";
4585
+ import { Text as Text5 } from "@earendil-works/pi-tui";
5949
4586
  var RENAME_PROMPT_METADATA = defineToolPromptMetadata({
5950
4587
  promptUrl: new URL("../prompts/rename.md", import.meta.url),
5951
4588
  promptSnippet: "Rename a symbol at a source location with verified edits"
5952
4589
  });
5953
- var renameSchema = Type8.Object({
4590
+ var renameSchema = Type7.Object({
5954
4591
  path: filePathParam(),
5955
- line: Type8.Integer({ minimum: 1, description: "One-based cursor line of the symbol to rename" }),
5956
- column: Type8.Optional(Type8.Integer({ minimum: 1, description: "One-based cursor byte column for disambiguation" })),
5957
- to: Type8.String({ description: "New symbol name" }),
5958
- workspace: Type8.Optional(Type8.Boolean({ description: "Rename across the project" })),
5959
- apply: Type8.Optional(Type8.Boolean({ description: "Apply the verified edits; defaults to true" }))
4592
+ line: Type7.Integer({ minimum: 1, description: PARAM_DESCRIPTIONS.renameLine }),
4593
+ column: Type7.Optional(Type7.Integer({ minimum: 1, description: PARAM_DESCRIPTIONS.column })),
4594
+ to: Type7.String({ description: PARAM_DESCRIPTIONS.to }),
4595
+ workspace: Type7.Optional(Type7.Boolean({ description: PARAM_DESCRIPTIONS.workspace })),
4596
+ apply: Type7.Optional(Type7.Boolean({ description: PARAM_DESCRIPTIONS.applyDefaultTrue }))
5960
4597
  });
5961
4598
  async function executeRename(opts) {
5962
4599
  const { params, signal, cwd, onFileMutated } = opts;
@@ -6046,7 +4683,7 @@ function registerRenameTool(pi, options = {}) {
6046
4683
  text += ` ${linkToolPath(theme.fg("accent", displayPath), displayPath, cwd)}`;
6047
4684
  if (args?.to)
6048
4685
  text += theme.fg("dim", ` → ${args.to}`);
6049
- return new Text6(clampLineToWidth(text, context.width), 0, 0);
4686
+ return new Text5(clampLineToWidth(text, context.width), 0, 0);
6050
4687
  },
6051
4688
  renderResult(result, options2, theme, ...rest) {
6052
4689
  const { isPartial, isError, expanded, cwd, width } = resolveRenderResultContext(options2, rest);
@@ -6079,7 +4716,7 @@ ${linkedPathLine(theme, file.file, display, cwd, ` (${file.edits.length} edits)`
6079
4716
  }
6080
4717
  const lines = clampLinesToWidth(text.split(`
6081
4718
  `), width);
6082
- return new Text6(lines.join(`
4719
+ return new Text5(lines.join(`
6083
4720
  `), 0, 0);
6084
4721
  }
6085
4722
  });
@@ -6088,9 +4725,9 @@ ${linkedPathLine(theme, file.file, display, cwd, ` (${file.edits.length} edits)`
6088
4725
  // src/write.ts
6089
4726
  import { mkdir, readFile, writeFile } from "node:fs/promises";
6090
4727
  import { dirname as dirname2, relative as relative3 } from "node:path";
6091
- import { withFileMutationQueue as withFileMutationQueue2, truncateHead as truncateHead4 } from "@earendil-works/pi-coding-agent";
6092
- import { Text as Text7 } from "@earendil-works/pi-tui";
6093
- import { Type as Type9 } from "@sinclair/typebox";
4728
+ import { withFileMutationQueue as withFileMutationQueue2, truncateHead as truncateHead2 } from "@earendil-works/pi-coding-agent";
4729
+ import { Text as Text6 } from "@earendil-works/pi-tui";
4730
+ import { Type as Type8 } from "@sinclair/typebox";
6094
4731
 
6095
4732
  // src/pending-diff-preview.ts
6096
4733
  import { existsSync as existsSync2, readFileSync as readFileSync3, realpathSync, statSync as statSync3 } from "node:fs";
@@ -6295,7 +4932,7 @@ async function executeWrite(opts) {
6295
4932
  const warnings = [];
6296
4933
  const readSeekWarnings = [];
6297
4934
  if (hasBareCarriageReturn(content)) {
6298
- const message = "File content contains bare CR (\\r) line endings; readSeek_write refuses to emit anchors that readSeek_read/readSeek_edit would normalize differently.";
4935
+ const message = "File content contains bare CR (\\r) line endings; readSeek_write refuses to emit anchors that readSeek_digest/readSeek_edit would normalize differently.";
6299
4936
  warnings.push(message);
6300
4937
  readSeekWarnings.push(buildReadSeekWarning("bare-cr", message));
6301
4938
  return buildToolErrorResult("write", "bare-cr", `Cannot write ${filePath}
@@ -6336,7 +4973,7 @@ async function executeWrite(opts) {
6336
4973
  }
6337
4974
  const fullText = displayLines.join(`
6338
4975
  `);
6339
- const truncated = truncateHead4(fullText);
4976
+ const truncated = truncateHead2(fullText);
6340
4977
  let text = truncated.content;
6341
4978
  if (truncated.truncated) {
6342
4979
  if (truncated.truncatedBy === "lines") {
@@ -6386,9 +5023,9 @@ function registerWriteTool(pi, options = {}) {
6386
5023
  description: promptMetadata.description,
6387
5024
  promptSnippet: promptMetadata.promptSnippet,
6388
5025
  promptGuidelines: promptMetadata.promptGuidelines,
6389
- parameters: Type9.Object({
5026
+ parameters: Type8.Object({
6390
5027
  path: filePathParam(),
6391
- content: Type9.String({ description: "Complete text file content" })
5028
+ content: Type8.String({ description: PARAM_DESCRIPTIONS.content })
6392
5029
  }),
6393
5030
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
6394
5031
  const cwd = ctx?.cwd ?? process.cwd();
@@ -6469,7 +5106,7 @@ function registerWriteTool(pi, options = {}) {
6469
5106
  `);
6470
5107
  lines.push(...formatContentPreviewLines(content, theme));
6471
5108
  }
6472
- return new Text7(clampLinesToWidth(lines, width).join(`
5109
+ return new Text6(clampLinesToWidth(lines, width).join(`
6473
5110
  `), 0, 0);
6474
5111
  }
6475
5112
  const hasExpandableDiff = !!diffData;
@@ -6478,7 +5115,7 @@ function registerWriteTool(pi, options = {}) {
6478
5115
  return upsertDiffComponent(context.lastComponent, { prefixLines: text.split(`
6479
5116
  `), diffData, theme, expanded: true });
6480
5117
  }
6481
- return new Text7(clampLinesToWidth(text.split(`
5118
+ return new Text6(clampLinesToWidth(text.split(`
6482
5119
  `), width).join(`
6483
5120
  `), 0, 0);
6484
5121
  }
@@ -6487,7 +5124,7 @@ function registerWriteTool(pi, options = {}) {
6487
5124
  }
6488
5125
 
6489
5126
  // src/def.ts
6490
- import { Type as Type10 } from "@sinclair/typebox";
5127
+ import { Type as Type9 } from "@sinclair/typebox";
6491
5128
  import path7 from "node:path";
6492
5129
  var DEF_PROMPT_METADATA = defineToolPromptMetadata({
6493
5130
  promptUrl: new URL("../prompts/def.md", import.meta.url),
@@ -6563,8 +5200,8 @@ function registerDefTool(pi, options = {}) {
6563
5200
  description: DEF_PROMPT_METADATA.description,
6564
5201
  promptSnippet: DEF_PROMPT_METADATA.promptSnippet,
6565
5202
  promptGuidelines: DEF_PROMPT_METADATA.promptGuidelines,
6566
- parameters: Type10.Object({
6567
- name: Type10.String({ description: PARAM_DESCRIPTIONS.name }),
5203
+ parameters: Type9.Object({
5204
+ name: Type9.String({ description: PARAM_DESCRIPTIONS.name }),
6568
5205
  path: searchPathParam(),
6569
5206
  language: languageParam(),
6570
5207
  ...readSeekGitSearchParams()
@@ -6591,33 +5228,29 @@ function registerDefTool(pi, options = {}) {
6591
5228
  }
6592
5229
 
6593
5230
  // src/digest.ts
6594
- import { Text as Text8 } from "@earendil-works/pi-tui";
6595
- import { Type as Type11 } from "@sinclair/typebox";
5231
+ import { Text as Text7 } from "@earendil-works/pi-tui";
5232
+ import { Type as Type10 } from "@sinclair/typebox";
6596
5233
  var DIGEST_FACETS2 = DIGEST_FACETS;
6597
- var DIGEST_PROMPT_METADATA = defineToolPromptMetadata({
6598
- promptUrl: new URL("../prompts/digest.md", import.meta.url),
6599
- promptSnippet: "Digest source facets as a native CLI envelope"
6600
- });
6601
- var facetSchema = Type11.Union(DIGEST_FACETS2.map((facet) => Type11.Literal(facet)));
6602
- var digestSchema = Type11.Object({
5234
+ var facetSchema = Type10.Union(DIGEST_FACETS2.map((facet) => Type10.Literal(facet)));
5235
+ var digestSchema = Type10.Object({
6603
5236
  path: filePathParam(),
6604
- select: Type11.Optional(Type11.Union([
5237
+ select: Type10.Optional(Type10.Union([
6605
5238
  facetSchema,
6606
- Type11.Array(facetSchema, { minItems: 1 }),
6607
- Type11.String({
5239
+ Type10.Array(facetSchema, { minItems: 1 }),
5240
+ Type10.String({
6608
5241
  description: PARAM_DESCRIPTIONS.selectComma
6609
5242
  })
6610
5243
  ], {
6611
5244
  description: PARAM_DESCRIPTIONS.select
6612
5245
  })),
6613
- at: Type11.Optional(Type11.String({
5246
+ at: Type10.Optional(Type10.String({
6614
5247
  description: PARAM_DESCRIPTIONS.at
6615
5248
  })),
6616
5249
  end: optionalIntOrString(PARAM_DESCRIPTIONS.end),
6617
5250
  limit: optionalIntOrString(PARAM_DESCRIPTIONS.limit),
6618
- language: Type11.Optional(Type11.String({ description: PARAM_DESCRIPTIONS.languageOverride })),
6619
- visionMode: Type11.Optional(Type11.Union(VISION_MODES.map((mode) => Type11.Literal(mode)), { description: PARAM_DESCRIPTIONS.visionModeDigest })),
6620
- visionLevel: Type11.Optional(Type11.Union(VISION_LEVELS.map((level) => Type11.Literal(level)), { description: PARAM_DESCRIPTIONS.visionLevel }))
5251
+ language: Type10.Optional(Type10.String({ description: PARAM_DESCRIPTIONS.languageOverride })),
5252
+ visionMode: Type10.Optional(Type10.Union(VISION_MODES.map((mode) => Type10.Literal(mode)), { description: PARAM_DESCRIPTIONS.visionModeDigest })),
5253
+ visionLevel: Type10.Optional(Type10.Union(VISION_LEVELS.map((level) => Type10.Literal(level)), { description: PARAM_DESCRIPTIONS.visionLevel }))
6621
5254
  });
6622
5255
  function hasDigestAnchors(value) {
6623
5256
  if (!value || typeof value !== "object")
@@ -6743,12 +5376,19 @@ async function executeDigest(opts) {
6743
5376
  }
6744
5377
  }
6745
5378
  function registerDigestTool(pi, options = {}) {
5379
+ const promptMetadata = defineToolPromptMetadata({
5380
+ promptUrl: new URL("../prompts/digest.md", import.meta.url),
5381
+ promptSnippet: "Digest source facets as a native CLI envelope",
5382
+ registeredName: options.name ?? "readSeek_digest",
5383
+ toolAliases: options.toolAliases
5384
+ });
6746
5385
  return registerReadSeekTool(pi, {
6747
- name: "readSeek_digest",
5386
+ name: options.name ?? "readSeek_digest",
6748
5387
  label: "Digest",
6749
- description: DIGEST_PROMPT_METADATA.description,
6750
- promptSnippet: DIGEST_PROMPT_METADATA.promptSnippet,
6751
- promptGuidelines: DIGEST_PROMPT_METADATA.promptGuidelines,
5388
+ description: promptMetadata.description,
5389
+ promptSnippet: promptMetadata.promptSnippet,
5390
+ toolAliases: options.toolAliases,
5391
+ promptGuidelines: promptMetadata.promptGuidelines,
6752
5392
  parameters: digestSchema,
6753
5393
  async execute(_toolCallId, params, signal, _onUpdate, ctx) {
6754
5394
  return executeDigest({ params, signal, cwd: ctx.cwd, onFileAnchored: options.onFileAnchored });
@@ -6765,7 +5405,7 @@ function registerDigestTool(pi, options = {}) {
6765
5405
  }
6766
5406
  if (typeof args?.at === "string" && args.at)
6767
5407
  text += theme.fg("dim", ` @ ${args.at}`);
6768
- return new Text8(clampLineToWidth(text, context.width), 0, 0);
5408
+ return new Text7(clampLineToWidth(text, context.width), 0, 0);
6769
5409
  },
6770
5410
  renderResult(result, options2, theme, ...rest) {
6771
5411
  const { isPartial, isError, expanded, width } = resolveRenderResultContext(options2, rest);
@@ -6782,7 +5422,7 @@ function registerDigestTool(pi, options = {}) {
6782
5422
  if (expanded && textContent)
6783
5423
  text += `
6784
5424
  ${textContent}`;
6785
- return new Text8(clampLinesToWidth(text.split(`
5425
+ return new Text7(clampLinesToWidth(text.split(`
6786
5426
  `), width).join(`
6787
5427
  `), 0, 0);
6788
5428
  }
@@ -6791,12 +5431,12 @@ ${textContent}`;
6791
5431
 
6792
5432
  // src/view.ts
6793
5433
  import {
6794
- DEFAULT_MAX_BYTES as DEFAULT_MAX_BYTES4,
6795
- DEFAULT_MAX_LINES as DEFAULT_MAX_LINES4,
6796
- truncateHead as truncateHead5
5434
+ DEFAULT_MAX_BYTES as DEFAULT_MAX_BYTES2,
5435
+ DEFAULT_MAX_LINES as DEFAULT_MAX_LINES2,
5436
+ truncateHead as truncateHead3
6797
5437
  } from "@earendil-works/pi-coding-agent";
6798
- import { Text as Text9 } from "@earendil-works/pi-tui";
6799
- import { Type as Type12 } from "@sinclair/typebox";
5438
+ import { Text as Text8 } from "@earendil-works/pi-tui";
5439
+ import { Type as Type11 } from "@sinclair/typebox";
6800
5440
  var NODE_KINDS = DOCUMENT_NODE_KINDS;
6801
5441
  var VIEW_PROMPT_METADATA = defineToolPromptMetadata({
6802
5442
  promptUrl: new URL("../prompts/view.md", import.meta.url),
@@ -6843,9 +5483,9 @@ async function executeView(opts) {
6843
5483
  visionLevel: input.visionLevel,
6844
5484
  signal
6845
5485
  });
6846
- const truncation = truncateHead5(output, {
6847
- maxLines: DEFAULT_MAX_LINES4,
6848
- maxBytes: DEFAULT_MAX_BYTES4
5486
+ const truncation = truncateHead3(output, {
5487
+ maxLines: DEFAULT_MAX_LINES2,
5488
+ maxBytes: DEFAULT_MAX_BYTES2
6849
5489
  });
6850
5490
  const notice = truncation.truncated ? `
6851
5491
  [… document view truncated; narrow it with page, node, kind, or depth]` : "";
@@ -6871,17 +5511,17 @@ function registerViewTool(pi) {
6871
5511
  description: VIEW_PROMPT_METADATA.description,
6872
5512
  promptSnippet: VIEW_PROMPT_METADATA.promptSnippet,
6873
5513
  promptGuidelines: VIEW_PROMPT_METADATA.promptGuidelines,
6874
- parameters: Type12.Object({
5514
+ parameters: Type11.Object({
6875
5515
  path: filePathParam(),
6876
- node: Type12.Optional(Type12.String({ description: PARAM_DESCRIPTIONS.node })),
5516
+ node: Type11.Optional(Type11.String({ description: PARAM_DESCRIPTIONS.node })),
6877
5517
  page: optionalIntOrString(PARAM_DESCRIPTIONS.page),
6878
- kind: Type12.Optional(Type12.Union(NODE_KINDS.map((kind) => Type12.Literal(kind)), {
5518
+ kind: Type11.Optional(Type11.Union(NODE_KINDS.map((kind) => Type11.Literal(kind)), {
6879
5519
  description: PARAM_DESCRIPTIONS.kind
6880
5520
  })),
6881
5521
  depth: optionalIntOrString(PARAM_DESCRIPTIONS.depth),
6882
- visionMode: Type12.Optional(Type12.Union(VISION_ANALYSIS_MODES.map((mode) => Type12.Literal(mode)), { description: PARAM_DESCRIPTIONS.visionModeView })),
6883
- visionLevel: Type12.Optional(Type12.Union(VISION_LEVELS.map((level) => Type12.Literal(level)), { description: PARAM_DESCRIPTIONS.visionLevel })),
6884
- outline: Type12.Optional(Type12.Boolean({ description: PARAM_DESCRIPTIONS.outline }))
5522
+ visionMode: Type11.Optional(Type11.Union(VISION_ANALYSIS_MODES.map((mode) => Type11.Literal(mode)), { description: PARAM_DESCRIPTIONS.visionModeView })),
5523
+ visionLevel: Type11.Optional(Type11.Union(VISION_LEVELS.map((level) => Type11.Literal(level)), { description: PARAM_DESCRIPTIONS.visionLevel })),
5524
+ outline: Type11.Optional(Type11.Boolean({ description: PARAM_DESCRIPTIONS.outline }))
6885
5525
  }),
6886
5526
  async execute(_toolCallId, params, signal, _onUpdate, ctx) {
6887
5527
  return executeView({ params, signal, cwd: ctx.cwd });
@@ -6891,7 +5531,7 @@ function registerViewTool(pi) {
6891
5531
  const cwd = context.cwd ?? process.cwd();
6892
5532
  const displayPath = typeof args?.path === "string" ? args.path : "?";
6893
5533
  const text = `${theme.fg("toolTitle", theme.bold("view"))} ${linkToolPath(theme.fg("accent", displayPath), displayPath, cwd)}`;
6894
- return new Text9(clampLineToWidth(text, context.width), 0, 0);
5534
+ return new Text8(clampLineToWidth(text, context.width), 0, 0);
6895
5535
  },
6896
5536
  renderResult(result, options, theme, ...rest) {
6897
5537
  const { isPartial, isError, expanded, width } = resolveRenderResultContext(options, rest);
@@ -6911,7 +5551,7 @@ function registerViewTool(pi) {
6911
5551
  if (expanded && textContent)
6912
5552
  text += `
6913
5553
  ${textContent}`;
6914
- return new Text9(clampLinesToWidth(text.split(`
5554
+ return new Text8(clampLinesToWidth(text.split(`
6915
5555
  `), width).join(`
6916
5556
  `), 0, 0);
6917
5557
  }
@@ -6937,7 +5577,6 @@ class SessionAnchors {
6937
5577
 
6938
5578
  // index.ts
6939
5579
  var READSEEK_TOOL_ENTRIES = [
6940
- { builtIn: "read", readSeekName: "readSeek_read" },
6941
5580
  { builtIn: "edit", readSeekName: "readSeek_edit" },
6942
5581
  { builtIn: "grep", readSeekName: "readSeek_grep" },
6943
5582
  { builtIn: null, readSeekName: "readSeek_search" },
@@ -6945,20 +5584,17 @@ var READSEEK_TOOL_ENTRIES = [
6945
5584
  { builtIn: null, readSeekName: "readSeek_rename" },
6946
5585
  { builtIn: "write", readSeekName: "readSeek_write" },
6947
5586
  { builtIn: null, readSeekName: "readSeek_def" },
6948
- { builtIn: null, readSeekName: "readSeek_digest" },
5587
+ { builtIn: "read", readSeekName: "readSeek_digest" },
6949
5588
  { builtIn: null, readSeekName: "readSeek_view" }
6950
5589
  ];
6951
5590
  function formatSettingsWarning(warning) {
6952
5591
  return `${warning.message} (${warning.source})`;
6953
5592
  }
6954
- function editingPolicy(readName, editName, writeName) {
6955
- return [
6956
- "ReadSeek editing policy:",
6957
- `- Read first with ${readName} when preparing to edit; ${editName} needs fresh LINE:HASH anchors.`,
6958
- `- Use ${editName} for existing files, ${writeName} for whole-file creation or replacement, and readSeek_rename for symbol renames.`,
6959
- "- Run readSeek_digest with select diagnostics after source edits for a quick syntax check."
6960
- ].join(`
6961
- `);
5593
+ function toolRoutingPolicy(aliases) {
5594
+ return renderToolRoutingPolicy({
5595
+ aliases,
5596
+ title: "ReadSeek tool policy:"
5597
+ });
6962
5598
  }
6963
5599
  function piReadSeekExtension(pi) {
6964
5600
  const sessionAnchors = new SessionAnchors;
@@ -6969,24 +5605,27 @@ function piReadSeekExtension(pi) {
6969
5605
  const replacedBuiltIns = new Set(settings.overrideTools ?? []);
6970
5606
  const binaryAvailable = readSeekBinaryAvailability().available;
6971
5607
  const swap = (builtIn, readSeekName) => binaryAvailable && replacedBuiltIns.has(builtIn) ? builtIn : readSeekName;
6972
- const readName = swap("read", "readSeek_read");
5608
+ const readName = swap("read", "readSeek_digest");
6973
5609
  const editName = swap("edit", "readSeek_edit");
6974
5610
  const writeName = swap("write", "readSeek_write");
6975
5611
  const grepName = swap("grep", "readSeek_grep");
6976
5612
  const toolAliases = {
6977
- readSeek_read: readName,
5613
+ readSeek_digest: readName,
6978
5614
  readSeek_edit: editName,
6979
5615
  readSeek_grep: grepName,
6980
5616
  readSeek_write: writeName
6981
5617
  };
6982
- registerReadTool(pi, { onSuccessfulRead: markAnchored, name: readName });
5618
+ const policyAliases = Object.fromEntries(TOOL_NAMES.map((name) => {
5619
+ const readSeekName = `readSeek_${name}`;
5620
+ return [name, toolAliases[readSeekName] ?? readSeekName];
5621
+ }));
6983
5622
  registerEditTool(pi, { wasReadInSession: hasFreshAnchors, onFileMutated: forgetAnchors, name: editName, toolAliases });
6984
5623
  registerGrepTool(pi, { onFileAnchored: markAnchored, name: grepName });
6985
5624
  registerSearchTool(pi, { onFileAnchored: markAnchored });
6986
5625
  registerRefsTool(pi, { onFileAnchored: markAnchored });
6987
5626
  registerRenameTool(pi, { onFileMutated: forgetAnchors });
6988
5627
  registerDefTool(pi, { onFileAnchored: markAnchored });
6989
- registerDigestTool(pi, { onFileAnchored: markAnchored });
5628
+ registerDigestTool(pi, { onFileAnchored: markAnchored, name: readName, toolAliases });
6990
5629
  registerViewTool(pi);
6991
5630
  registerWriteTool(pi, { onFileAnchored: markAnchored, name: writeName });
6992
5631
  pi.on("before_agent_start", (event) => {
@@ -6994,7 +5633,7 @@ function piReadSeekExtension(pi) {
6994
5633
  return;
6995
5634
  return { systemPrompt: `${event.systemPrompt}
6996
5635
 
6997
- ${editingPolicy(readName, editName, writeName)}` };
5636
+ ${toolRoutingPolicy(policyAliases)}` };
6998
5637
  });
6999
5638
  pi.on("session_start", (_event, ctx) => {
7000
5639
  sessionAnchors.clear();