pi-readseek 0.9.5 → 0.9.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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",
@@ -113,11 +109,10 @@ var PARAM_DESCRIPTIONS = {
113
109
  scopeContext: "Context lines within each symbol"
114
110
  };
115
111
  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.",
112
+ 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
113
  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
114
  write: "Create or replace a complete text file and return `LINE:HASH` anchors. Use `edit` for small changes to an existing file.",
119
115
  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
116
  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
117
  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
118
  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 +120,10 @@ var TOOL_DESCRIPTIONS = {
125
120
  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
121
  };
127
122
  function renderToolDescription(name, aliases = {}) {
128
- return TOOL_NAMES.reduce((description, toolName) => description.replaceAll(`\`${toolName}\``, `\`${aliases[toolName] ?? toolName}\``), TOOL_DESCRIPTIONS[name]);
123
+ const desc = TOOL_DESCRIPTIONS[name];
124
+ if (!desc)
125
+ throw new Error(`Missing TOOL_DESCRIPTIONS for ${name}`);
126
+ return TOOL_NAMES.reduce((description, toolName) => description.replaceAll(`\`${toolName}\``, `\`${aliases[toolName] ?? toolName}\``), desc);
129
127
  }
130
128
  var gitFlagsSchema = {
131
129
  cached: {
@@ -204,25 +202,27 @@ var editItemSchema = {
204
202
  };
205
203
  var TOOL_CONTRACTS = [
206
204
  {
207
- name: "read",
208
- title: "Read anchored text",
209
- description: TOOL_DESCRIPTIONS.read,
205
+ name: "digest",
206
+ title: "Digest source",
207
+ description: TOOL_DESCRIPTIONS.digest,
210
208
  inputSchema: {
211
209
  type: "object",
212
210
  properties: {
213
211
  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: {
212
+ select: { type: "string", description: PARAM_DESCRIPTIONS.select },
213
+ at: { type: "string", description: PARAM_DESCRIPTIONS.at },
214
+ end: { type: "integer", minimum: 1, description: PARAM_DESCRIPTIONS.end },
215
+ limit: { type: "integer", minimum: 1, description: PARAM_DESCRIPTIONS.limit },
216
+ language: { type: "string", description: PARAM_DESCRIPTIONS.languageOverride },
217
+ visionMode: {
218
218
  type: "string",
219
- enum: ["local"],
220
- description: PARAM_DESCRIPTIONS.bundle
219
+ enum: [...VISION_MODES],
220
+ description: PARAM_DESCRIPTIONS.visionModeDigest
221
221
  },
222
- image: {
222
+ visionLevel: {
223
223
  type: "string",
224
- enum: [...VISION_MODES],
225
- description: PARAM_DESCRIPTIONS.image
224
+ enum: [...VISION_LEVELS],
225
+ description: PARAM_DESCRIPTIONS.visionLevel
226
226
  }
227
227
  },
228
228
  required: ["path"],
@@ -296,34 +296,6 @@ var TOOL_CONTRACTS = [
296
296
  additionalProperties: false
297
297
  }
298
298
  },
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
299
  {
328
300
  name: "view",
329
301
  title: "View document",
@@ -797,8 +769,8 @@ async function runReadSeekNodeJson(binaryPath, args, options = {}) {
797
769
  }
798
770
  // src/tool-prompt-metadata.ts
799
771
  var REPLACEABLE_TOOL_GUIDELINES = {
800
- "read.md": {
801
- readSeekName: "readSeek_read",
772
+ "digest.md": {
773
+ readSeekName: "readSeek_digest",
802
774
  builtInName: "read",
803
775
  benefit: "it returns LINE:HASH anchors for safe edits."
804
776
  },
@@ -819,9 +791,6 @@ var REPLACEABLE_TOOL_GUIDELINES = {
819
791
  }
820
792
  };
821
793
  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
794
  "digest.md": [
826
795
  "Use readSeek_digest for map, diagnostics, and identity facets instead of separate tools."
827
796
  ],
@@ -1068,6 +1037,146 @@ function generateCompactOrFullDiff(oldContent, newContent, contextLines = 4) {
1068
1037
  return generateDiffString(oldContent, newContent, contextLines);
1069
1038
  }
1070
1039
 
1040
+ // src/path-utils.ts
1041
+ import { homedir } from "node:os";
1042
+ import { isAbsolute, resolve as resolvePath } from "node:path";
1043
+ var UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
1044
+ function normalizeUnicodeSpaces(str) {
1045
+ return str.replace(UNICODE_SPACES, " ");
1046
+ }
1047
+ function normalizeAtPrefix(filePath) {
1048
+ return filePath.startsWith("@") ? filePath.slice(1) : filePath;
1049
+ }
1050
+ function expandPath(filePath) {
1051
+ const normalized = normalizeUnicodeSpaces(normalizeAtPrefix(filePath));
1052
+ if (normalized === "~")
1053
+ return homedir();
1054
+ if (normalized.startsWith("~/"))
1055
+ return homedir() + normalized.slice(1);
1056
+ return normalized;
1057
+ }
1058
+ function resolveToCwd(filePath, cwd) {
1059
+ const expanded = expandPath(filePath);
1060
+ return isAbsolute(expanded) ? expanded : resolvePath(cwd, expanded);
1061
+ }
1062
+
1063
+ // src/runtime.ts
1064
+ function throwIfAborted(signal) {
1065
+ if (signal?.aborted)
1066
+ throw new Error("Operation aborted");
1067
+ }
1068
+
1069
+ // src/fs-error.ts
1070
+ var SYSLOG_PATH_SUFFIX = /,\s+[a-z]+(\s+'.*')?$/;
1071
+ function strerror(err) {
1072
+ const raw = String(err?.message ?? String(err));
1073
+ return raw.replace(SYSLOG_PATH_SUFFIX, "");
1074
+ }
1075
+ function formatFsError(err, domain) {
1076
+ const errno = typeof err.code === "string" ? err.code : "EIO";
1077
+ return {
1078
+ code: errno,
1079
+ message: `${domain}: ${errno}: ${strerror(err)}`
1080
+ };
1081
+ }
1082
+
1083
+ // src/edit-render-helpers.ts
1084
+ var VARIANT_KEYS = ["set_line", "replace_lines", "insert_after", "replace"];
1085
+ function countEditTypes(edits) {
1086
+ const counts = {
1087
+ set_line: 0,
1088
+ replace_lines: 0,
1089
+ insert_after: 0,
1090
+ replace: 0,
1091
+ total: 0
1092
+ };
1093
+ if (!edits)
1094
+ return counts;
1095
+ for (const edit of edits) {
1096
+ counts.total++;
1097
+ if (edit && typeof edit === "object") {
1098
+ for (const key of VARIANT_KEYS) {
1099
+ if (key in edit) {
1100
+ counts[key]++;
1101
+ break;
1102
+ }
1103
+ }
1104
+ }
1105
+ }
1106
+ return counts;
1107
+ }
1108
+ function parseDiffStats(diff) {
1109
+ if (!diff)
1110
+ return { added: 0, removed: 0 };
1111
+ let added = 0;
1112
+ let removed = 0;
1113
+ for (const line of diff.split(`
1114
+ `)) {
1115
+ if (line.startsWith("+++") || line.startsWith("---"))
1116
+ continue;
1117
+ if (line.startsWith("+"))
1118
+ added++;
1119
+ else if (line.startsWith("-"))
1120
+ removed++;
1121
+ }
1122
+ return { added, removed };
1123
+ }
1124
+ function formatEditCallText(args, argsComplete) {
1125
+ const rawPath = typeof args?.path === "string" ? args.path : null;
1126
+ if (!argsComplete) {
1127
+ return { path: rawPath, suffix: undefined };
1128
+ }
1129
+ const edits = args?.edits;
1130
+ if (!Array.isArray(edits) || edits.length === 0) {
1131
+ return { path: rawPath, suffix: undefined };
1132
+ }
1133
+ const counts = countEditTypes(edits);
1134
+ const parts = [];
1135
+ for (const key of VARIANT_KEYS) {
1136
+ if (counts[key] > 0) {
1137
+ parts.push(`${counts[key]} ${key}`);
1138
+ }
1139
+ }
1140
+ const word = counts.total === 1 ? "edit" : "edits";
1141
+ const suffix = `${counts.total} ${word} (${parts.join(", ")})`;
1142
+ return { path: rawPath, suffix };
1143
+ }
1144
+ function formatEditResultText(input) {
1145
+ const { isError, diff, warnings, noopEdits, errorText } = input;
1146
+ const isNoOp = isError && (Array.isArray(noopEdits) && noopEdits.length > 0 || errorText.includes("No changes made"));
1147
+ const stats = parseDiffStats(diff);
1148
+ const hasDiffStats = stats.added > 0 || stats.removed > 0;
1149
+ const diffStats = hasDiffStats ? `+${stats.added} / -${stats.removed}` : undefined;
1150
+ let warningsBadge;
1151
+ if (warnings.length === 1) {
1152
+ warningsBadge = "⚠ 1 warning";
1153
+ } else if (warnings.length > 1) {
1154
+ warningsBadge = `⚠ ${warnings.length} warnings`;
1155
+ }
1156
+ const showErrorText = isError ? errorText : undefined;
1157
+ let semanticBadge;
1158
+ if (!isError && !isNoOp && input.semanticClassification) {
1159
+ switch (input.semanticClassification) {
1160
+ case "whitespace-only":
1161
+ semanticBadge = "ws-only";
1162
+ break;
1163
+ case "semantic":
1164
+ semanticBadge = "✓ semantic";
1165
+ break;
1166
+ case "mixed":
1167
+ semanticBadge = "mixed";
1168
+ break;
1169
+ }
1170
+ }
1171
+ return {
1172
+ diffStats,
1173
+ noOp: isNoOp,
1174
+ warningsBadge,
1175
+ errorText: showErrorText,
1176
+ semanticBadge
1177
+ };
1178
+ }
1179
+
1071
1180
  // src/readseek-value.ts
1072
1181
  function buildReadSeekLineWithHash(line, hash, raw) {
1073
1182
  return {
@@ -1081,13 +1190,6 @@ function buildReadSeekLineWithHash(line, hash, raw) {
1081
1190
  function buildReadSeekLine(line, raw) {
1082
1191
  return buildReadSeekLineWithHash(line, computeLineHash(raw), raw);
1083
1192
  }
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
1193
  function formatAnchoredFileBlocks(files, lineSuffix) {
1092
1194
  const blocks = [];
1093
1195
  for (const file of files) {
@@ -1140,63 +1242,150 @@ function buildReadSeekEditResult(input) {
1140
1242
  };
1141
1243
  }
1142
1244
 
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, " ");
1161
- }
1162
- function normalizeAtPrefix(filePath) {
1163
- return filePath.startsWith("@") ? filePath.slice(1) : filePath;
1164
- }
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;
1245
+ // src/edit-output.ts
1246
+ var EDIT_OPERATION_NAMES = ["set_line", "replace_lines", "insert_after", "replace"];
1247
+ function hasNewText(value) {
1248
+ return typeof value === "object" && value !== null && typeof value.new_text === "string";
1172
1249
  }
1173
- function resolveToCwd(filePath, cwd) {
1174
- const expanded = expandPath(filePath);
1175
- return isAbsolute(expanded) ? expanded : resolvePath(cwd, expanded);
1250
+ function getVisibleDiffStats(diff) {
1251
+ const stats = parseDiffStats(diff);
1252
+ if (stats.added > 0 || stats.removed > 0)
1253
+ return stats;
1254
+ if (!diff.includes("→"))
1255
+ return stats;
1256
+ if (diff.includes("→ [deleted]"))
1257
+ return { added: 0, removed: 1 };
1258
+ return { added: 1, removed: 1 };
1176
1259
  }
1177
-
1178
- // src/runtime.ts
1179
- function throwIfAborted(signal) {
1180
- if (signal?.aborted)
1181
- throw new Error("Operation aborted");
1260
+ function buildVisibleSummary(displayPath, diff, edits) {
1261
+ let stats = getVisibleDiffStats(diff);
1262
+ const counts = countEditTypes(edits);
1263
+ const editCount = counts.total || 1;
1264
+ if (counts.total > 0 && counts.insert_after === counts.total && stats.removed > 0 && stats.added === stats.removed + counts.insert_after) {
1265
+ stats = { added: counts.insert_after, removed: 0 };
1266
+ }
1267
+ const changeWord = editCount === 1 ? "change" : "changes";
1268
+ const changedLineCount = Math.max(stats.added, stats.removed);
1269
+ const lineWord = changedLineCount === 1 ? "line" : "lines";
1270
+ return `Edited ${displayPath} (${editCount} ${changeWord}, +${stats.added} -${stats.removed} ${lineWord})`;
1182
1271
  }
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, "");
1272
+ function extractNewTextValues(edits) {
1273
+ const values = [];
1274
+ for (const edit of edits ?? []) {
1275
+ if (!edit || typeof edit !== "object")
1276
+ continue;
1277
+ const operations = edit;
1278
+ for (const operationName of EDIT_OPERATION_NAMES) {
1279
+ const operation = operations[operationName];
1280
+ if (hasNewText(operation))
1281
+ values.push(operation.new_text);
1282
+ }
1283
+ }
1284
+ return values;
1189
1285
  }
1190
- function formatFsError(err, domain) {
1191
- const errno = typeof err.code === "string" ? err.code : "EIO";
1286
+ function formatWhitespaceOnlyWarning(semanticSummary, edits) {
1287
+ if (semanticSummary?.classification !== "whitespace-only")
1288
+ return;
1289
+ if (!extractNewTextValues(edits).some((text) => /\S/.test(text)))
1290
+ return;
1291
+ return "⚠ Edit classified as whitespace-only — if you intended a behavior change, re-read to verify.";
1292
+ }
1293
+ function formatReplaceHint(edits, noopEdits) {
1294
+ if ((noopEdits ?? []).length > 0)
1295
+ return;
1296
+ const counts = countEditTypes(edits);
1297
+ if (counts.replace === 0)
1298
+ return;
1299
+ if (counts.replace !== counts.total)
1300
+ return;
1301
+ 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.]";
1302
+ }
1303
+ function buildEditOutput(input) {
1304
+ const summary = `Updated ${input.displayPath}`;
1305
+ const visibleSummary = buildVisibleSummary(input.displayPath, input.diff, input.edits);
1306
+ const semanticWarning = formatWhitespaceOnlyWarning(input.semanticSummary, input.edits);
1307
+ const warningText = input.warnings.length ? `
1308
+
1309
+ Warnings:
1310
+ ${input.warnings.join(`
1311
+ `)}` : "";
1312
+ const replaceHint = formatReplaceHint(input.edits, input.noopEdits);
1313
+ let text = visibleSummary;
1314
+ if (semanticWarning)
1315
+ text += `
1316
+ ${semanticWarning}`;
1317
+ text += warningText;
1318
+ if (replaceHint)
1319
+ text += `
1320
+ ${replaceHint}`;
1192
1321
  return {
1193
- code: errno,
1194
- message: `${domain}: ${errno}: ${strerror(err)}`
1322
+ text,
1323
+ patch: input.patch ?? "",
1324
+ readSeekValue: buildReadSeekEditResult({
1325
+ path: input.path,
1326
+ summary,
1327
+ diff: input.diff,
1328
+ ...input.diffData ? { diffData: input.diffData } : {},
1329
+ firstChangedLine: input.firstChangedLine,
1330
+ warnings: input.warnings,
1331
+ noopEdits: input.noopEdits,
1332
+ ...input.semanticSummary ? { semanticSummary: input.semanticSummary } : {}
1333
+ })
1195
1334
  };
1196
1335
  }
1197
1336
 
1198
- // src/file-map.ts
1199
- import { stat } from "node:fs/promises";
1337
+ // src/edit-classify.ts
1338
+ import * as Diff2 from "diff";
1339
+ function classifyEdit(oldContent, newContent) {
1340
+ if (oldContent === newContent) {
1341
+ return { classification: "no-op" };
1342
+ }
1343
+ const oldLines = oldContent.split(`
1344
+ `);
1345
+ const newLines = newContent.split(`
1346
+ `);
1347
+ if (oldLines.length === newLines.length) {
1348
+ let hasWhitespaceChange2 = false;
1349
+ let hasSemanticChange2 = false;
1350
+ for (let i = 0;i < oldLines.length; i++) {
1351
+ if (oldLines[i] === newLines[i])
1352
+ continue;
1353
+ if (oldLines[i].trim() === newLines[i].trim()) {
1354
+ hasWhitespaceChange2 = true;
1355
+ } else {
1356
+ hasSemanticChange2 = true;
1357
+ }
1358
+ }
1359
+ if (hasSemanticChange2 && hasWhitespaceChange2)
1360
+ return { classification: "mixed" };
1361
+ if (hasWhitespaceChange2)
1362
+ return { classification: "whitespace-only" };
1363
+ return { classification: "semantic" };
1364
+ }
1365
+ const parts = Diff2.diffLines(oldContent, newContent);
1366
+ let hasWhitespaceChange = false;
1367
+ let hasSemanticChange = false;
1368
+ for (const part of parts) {
1369
+ if (!part.added && !part.removed)
1370
+ continue;
1371
+ const lines = part.value.split(`
1372
+ `);
1373
+ if (lines[lines.length - 1] === "")
1374
+ lines.pop();
1375
+ for (const line of lines) {
1376
+ if (line.trim() === "") {
1377
+ hasWhitespaceChange = true;
1378
+ } else {
1379
+ hasSemanticChange = true;
1380
+ }
1381
+ }
1382
+ }
1383
+ if (hasSemanticChange && hasWhitespaceChange)
1384
+ return { classification: "mixed" };
1385
+ if (hasWhitespaceChange)
1386
+ return { classification: "whitespace-only" };
1387
+ return { classification: "semantic" };
1388
+ }
1200
1389
 
1201
1390
  // src/readseek-client.ts
1202
1391
  import { existsSync, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
@@ -1236,7 +1425,7 @@ import { readFileSync, statSync } from "node:fs";
1236
1425
  import { homedir as homedir2 } from "node:os";
1237
1426
  import { join } from "node:path";
1238
1427
  var READSEEK_TOOL_REPLACEMENTS = {
1239
- read: "readSeek_read",
1428
+ read: "readSeek_digest",
1240
1429
  edit: "readSeek_edit",
1241
1430
  grep: "readSeek_grep",
1242
1431
  write: "readSeek_write"
@@ -1434,9 +1623,6 @@ function resolveReadSeekJsonSettings() {
1434
1623
  warnings: [...globalResult.warnings, ...projectResult.warnings]
1435
1624
  };
1436
1625
  }
1437
- function resolveReadSeekImageMode() {
1438
- return resolveReadSeekJsonSettings().settings.imageMode ?? "auto";
1439
- }
1440
1626
  function resolveReadSeekSyntaxValidation() {
1441
1627
  return resolveReadSeekJsonSettings().settings.syntaxValidation;
1442
1628
  }
@@ -1665,23 +1851,6 @@ function requireBoolean(value, field) {
1665
1851
  throw new Error(`invalid readseek ${field}: expected boolean`);
1666
1852
  return value;
1667
1853
  }
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
1854
  function parseMapOutput(value) {
1686
1855
  if (!value || typeof value !== "object")
1687
1856
  throw new Error("invalid readseek map output");
@@ -1948,32 +2117,6 @@ function parseIdentifyOutput(value) {
1948
2117
  } : undefined
1949
2118
  };
1950
2119
  }
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
2120
  function parseDigestEnvelope(value) {
1978
2121
  if (!value || typeof value !== "object")
1979
2122
  throw new Error("invalid readseek digest output");
@@ -2023,11 +2166,6 @@ async function readSeekDigestNative(target, options = {}) {
2023
2166
  async function readSeekDigest(target, options = {}) {
2024
2167
  return parseDigestEnvelope(await runDigestRaw(target, options));
2025
2168
  }
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
2169
  function fileMapFromReadSeekOutput(output, filePath, totalBytes) {
2032
2170
  if (output.language === "unknown" && output.symbols.length === 0)
2033
2171
  return null;
@@ -2082,70 +2220,6 @@ async function readSeekCheck(filePath, content, options = {}) {
2082
2220
  throw new Error("invalid readseek diagnostics: missing diagnostics facet");
2083
2221
  return envelope.diagnostics;
2084
2222
  }
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
2223
  function parseEditOutput(value) {
2150
2224
  if (!value || typeof value !== "object")
2151
2225
  throw new Error("invalid readseek edit output");
@@ -2296,1831 +2370,102 @@ async function readSeekDef(target, options) {
2296
2370
  return parseDefCompact(await runReadSeek(args, { signal: options.signal }));
2297
2371
  }
2298
2372
 
2299
- // src/file-map.ts
2300
- async function getOrGenerateMap(absPath) {
2373
+ // src/edit-syntax-validate.ts
2374
+ function dedupeSortLines(diagnostics) {
2375
+ const seen = new Set;
2376
+ const out = [];
2377
+ for (const diagnostic of diagnostics) {
2378
+ const key = diagnostic.start_line === diagnostic.end_line ? String(diagnostic.start_line) : `${diagnostic.start_line}-${diagnostic.end_line}`;
2379
+ if (!seen.has(key)) {
2380
+ seen.add(key);
2381
+ out.push({ key, start: diagnostic.start_line });
2382
+ }
2383
+ }
2384
+ out.sort((a, b) => a.start - b.start);
2385
+ return out.map((o) => o.key);
2386
+ }
2387
+ var EMPTY = { errorCount: 0, missingCount: 0, diagnostics: [] };
2388
+ async function validateSyntaxRegression(input, options = {}) {
2389
+ let before;
2390
+ let after;
2301
2391
  try {
2302
- const { size } = await stat(absPath);
2303
- return await readSeekMap(absPath, size);
2392
+ before = input.before === undefined ? EMPTY : await readSeekCheck(input.filePath, input.before, { signal: options.signal });
2393
+ after = await readSeekCheck(input.filePath, input.after, { signal: options.signal });
2304
2394
  } catch {
2305
2395
  return null;
2306
2396
  }
2397
+ const newErrorCount = Math.max(0, after.errorCount - before.errorCount);
2398
+ const newMissingCount = Math.max(0, after.missingCount - before.missingCount);
2399
+ if (newErrorCount === 0 && newMissingCount === 0)
2400
+ return null;
2401
+ return {
2402
+ errorLines: dedupeSortLines(after.diagnostics),
2403
+ newErrorCount,
2404
+ newMissingCount
2405
+ };
2307
2406
  }
2308
2407
 
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
- };
2408
+ // src/syntax-validate-mode.ts
2409
+ var DEFAULT = "warn";
2410
+ function resolveSyntaxValidateMode(opts) {
2411
+ return opts.syntaxValidate ?? resolveReadSeekSyntaxValidation() ?? DEFAULT;
2412
+ }
2321
2413
 
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;
2414
+ // src/diff-data.ts
2415
+ var MAX_INLINE_DIFF_LINE_LENGTH = 4096;
2416
+ var MAX_INLINE_DIFF_TOKENS = 512;
2417
+ var MAX_INLINE_DIFF_CELLS = 200000;
2418
+ var MAX_INLINE_DIFF_PAIRS = 200;
2419
+ var INLINE_SIMILARITY_THRESHOLD = 0.35;
2420
+ var INLINE_TOKEN_PATTERN = /([A-Za-z_$][\w$]*|\d+|\s+|[^A-Za-z_$\w\s]+)/gu;
2421
+ var LANGUAGE_BY_EXTENSION = new Map([
2422
+ [".ts", "typescript"],
2423
+ [".tsx", "typescript"],
2424
+ [".js", "javascript"],
2425
+ [".jsx", "javascript"],
2426
+ [".py", "python"],
2427
+ [".rs", "rust"],
2428
+ [".java", "java"]
2429
+ ]);
2430
+ function inferLanguage(path3) {
2431
+ const extensionMatch = path3.match(/\.[^.\/\\]+$/);
2432
+ if (!extensionMatch)
2433
+ return;
2434
+ return LANGUAGE_BY_EXTENSION.get(extensionMatch[0].toLowerCase());
2435
+ }
2436
+ function parseFullDiffEntries(diff) {
2437
+ const entries = [];
2438
+ let nextOldLine = 1;
2439
+ let nextNewLine = 1;
2440
+ for (const line of diff ? diff.split(`
2441
+ `) : []) {
2442
+ const removeMatch = line.match(/^-\s*(\d+) (.*)$/);
2443
+ if (removeMatch) {
2444
+ const oldLine = Number(removeMatch[1]);
2445
+ entries.push({ kind: "remove", oldLine, text: removeMatch[2] ?? "" });
2446
+ nextOldLine = oldLine + 1;
2447
+ continue;
2354
2448
  }
2355
- case "variable":
2356
- case "constant": {
2357
- formatted = `${prefix}${name} = ... ${lineRange}`;
2358
- break;
2449
+ const addMatch = line.match(/^\+\s*(\d+) (.*)$/);
2450
+ if (addMatch) {
2451
+ const newLine = Number(addMatch[1]);
2452
+ entries.push({ kind: "add", newLine, text: addMatch[2] ?? "" });
2453
+ nextNewLine = newLine + 1;
2454
+ continue;
2359
2455
  }
2360
- default: {
2361
- formatted = `${prefix}${name}: ${lineRange}`;
2456
+ const contextMatch = line.match(/^ \s*(\d+) (.*)$/);
2457
+ if (contextMatch) {
2458
+ const oldLine = Number(contextMatch[1]);
2459
+ const lineDelta = nextNewLine - nextOldLine;
2460
+ const newLine = oldLine + lineDelta;
2461
+ entries.push({ kind: "context", oldLine, newLine, text: contextMatch[2] ?? "" });
2462
+ nextOldLine = oldLine + 1;
2463
+ nextNewLine = newLine + 1;
2464
+ continue;
2362
2465
  }
2466
+ entries.push({ kind: "meta", text: line });
2363
2467
  }
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;
4067
- }
4068
-
4069
- // src/diff-data.ts
4070
- var MAX_INLINE_DIFF_LINE_LENGTH = 4096;
4071
- var MAX_INLINE_DIFF_TOKENS = 512;
4072
- var MAX_INLINE_DIFF_CELLS = 200000;
4073
- var MAX_INLINE_DIFF_PAIRS = 200;
4074
- var INLINE_SIMILARITY_THRESHOLD = 0.35;
4075
- var INLINE_TOKEN_PATTERN = /([A-Za-z_$][\w$]*|\d+|\s+|[^A-Za-z_$\w\s]+)/gu;
4076
- var LANGUAGE_BY_EXTENSION = new Map([
4077
- [".ts", "typescript"],
4078
- [".tsx", "typescript"],
4079
- [".js", "javascript"],
4080
- [".jsx", "javascript"],
4081
- [".py", "python"],
4082
- [".rs", "rust"],
4083
- [".java", "java"]
4084
- ]);
4085
- function inferLanguage(path3) {
4086
- const extensionMatch = path3.match(/\.[^.\/\\]+$/);
4087
- if (!extensionMatch)
4088
- return;
4089
- return LANGUAGE_BY_EXTENSION.get(extensionMatch[0].toLowerCase());
4090
- }
4091
- function parseFullDiffEntries(diff) {
4092
- const entries = [];
4093
- let nextOldLine = 1;
4094
- let nextNewLine = 1;
4095
- for (const line of diff ? diff.split(`
4096
- `) : []) {
4097
- const removeMatch = line.match(/^-\s*(\d+) (.*)$/);
4098
- if (removeMatch) {
4099
- const oldLine = Number(removeMatch[1]);
4100
- entries.push({ kind: "remove", oldLine, text: removeMatch[2] ?? "" });
4101
- nextOldLine = oldLine + 1;
4102
- continue;
4103
- }
4104
- const addMatch = line.match(/^\+\s*(\d+) (.*)$/);
4105
- if (addMatch) {
4106
- const newLine = Number(addMatch[1]);
4107
- entries.push({ kind: "add", newLine, text: addMatch[2] ?? "" });
4108
- nextNewLine = newLine + 1;
4109
- continue;
4110
- }
4111
- const contextMatch = line.match(/^ \s*(\d+) (.*)$/);
4112
- if (contextMatch) {
4113
- const oldLine = Number(contextMatch[1]);
4114
- const lineDelta = nextNewLine - nextOldLine;
4115
- const newLine = oldLine + lineDelta;
4116
- entries.push({ kind: "context", oldLine, newLine, text: contextMatch[2] ?? "" });
4117
- nextOldLine = oldLine + 1;
4118
- nextNewLine = newLine + 1;
4119
- continue;
4120
- }
4121
- entries.push({ kind: "meta", text: line });
4122
- }
4123
- return entries;
2468
+ return entries;
4124
2469
  }
4125
2470
  function parseCompactDiffEntries(diff, oldContent, newContent) {
4126
2471
  const oldLines = oldContent.split(`
@@ -4251,66 +2596,204 @@ function buildInlineSpans(removeText, addText) {
4251
2596
  return;
4252
2597
  return { removeSpans, addSpans };
4253
2598
  }
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
- });
2599
+ function buildInlineDiffs(entries) {
2600
+ const inlineDiffs = [];
2601
+ let remainingPairs = MAX_INLINE_DIFF_PAIRS;
2602
+ for (let index = 0;index < entries.length; ) {
2603
+ if (entries[index]?.kind !== "remove") {
2604
+ index++;
2605
+ continue;
2606
+ }
2607
+ const removeStart = index;
2608
+ while (entries[index]?.kind === "remove")
2609
+ index++;
2610
+ const addStart = index;
2611
+ while (entries[index]?.kind === "add")
2612
+ index++;
2613
+ const removeCount = addStart - removeStart;
2614
+ const addCount = index - addStart;
2615
+ if (removeCount === 0 || addCount === 0 || removeCount !== addCount)
2616
+ continue;
2617
+ for (let offset = 0;offset < removeCount; offset++) {
2618
+ if (remainingPairs <= 0)
2619
+ break;
2620
+ remainingPairs--;
2621
+ const removeIndex = removeStart + offset;
2622
+ const addIndex = addStart + offset;
2623
+ const removeEntry = entries[removeIndex];
2624
+ const addEntry = entries[addIndex];
2625
+ if (removeEntry?.kind !== "remove" || addEntry?.kind !== "add")
2626
+ continue;
2627
+ const spans = buildInlineSpans(removeEntry.text, addEntry.text);
2628
+ if (!spans)
2629
+ continue;
2630
+ inlineDiffs.push({
2631
+ removeLineIndex: removeIndex,
2632
+ addLineIndex: addIndex,
2633
+ removeSpans: spans.removeSpans,
2634
+ addSpans: spans.addSpans
2635
+ });
2636
+ }
2637
+ }
2638
+ return inlineDiffs.length ? inlineDiffs : undefined;
2639
+ }
2640
+ function finalizeDiffData(path3, entries, blockRanges) {
2641
+ const language = inferLanguage(path3);
2642
+ const inlineDiffs = buildInlineDiffs(entries);
2643
+ return {
2644
+ version: 1,
2645
+ entries,
2646
+ stats: buildStats(entries),
2647
+ ...language ? { language } : {},
2648
+ ...blockRanges?.length ? { blockRanges: [...blockRanges] } : {},
2649
+ ...inlineDiffs ? { inlineDiffs } : {}
2650
+ };
2651
+ }
2652
+ function buildDiffData(input) {
2653
+ const entries = parseCompactDiffEntries(input.diff, input.oldContent, input.newContent) ?? parseFullDiffEntries(input.diff);
2654
+ return finalizeDiffData(input.path, entries, input.blockRanges);
2655
+ }
2656
+
2657
+ // src/tui-render-utils.ts
2658
+ import { relative } from "node:path";
2659
+ import { pathToFileURL } from "node:url";
2660
+ import { getCapabilities, hyperlink, Text, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
2661
+ var SUMMARY_PREFIX = "↳";
2662
+ var EXPAND_HINT = " • Ctrl+O to expand";
2663
+ function renderToolLabel(theme, label) {
2664
+ const boldFn = typeof theme.bold === "function" ? theme.bold.bind(theme) : (text) => text;
2665
+ return theme.fg("toolTitle", boldFn(label));
2666
+ }
2667
+ function linkToolPath(styledText, rawPath, cwd) {
2668
+ try {
2669
+ if (!getCapabilities().hyperlinks)
2670
+ return styledText;
2671
+ const absolutePath = resolveToCwd(rawPath, cwd);
2672
+ return hyperlink(styledText, pathToFileURL(absolutePath).href);
2673
+ } catch {
2674
+ return styledText;
2675
+ }
2676
+ }
2677
+ function linkedPathLine(theme, rawPath, displayPath, cwd, suffix, width) {
2678
+ const pathWidth = width === undefined ? undefined : Math.max(1, width - 2 - visibleWidth(suffix));
2679
+ const visiblePath = pathWidth === undefined ? displayPath : truncateToWidth(displayPath, pathWidth);
2680
+ const linkedPath = linkToolPath(theme.fg("dim", visiblePath), rawPath, cwd);
2681
+ return ` ${linkedPath}${theme.fg("dim", suffix)}`;
2682
+ }
2683
+ function appendExpandHint(text, hidden) {
2684
+ return hidden ? `${text}${EXPAND_HINT}` : text;
2685
+ }
2686
+ function summaryLine(summary, options = {}) {
2687
+ const prefix = options.theme ? options.theme.fg(options.style ?? "dim", SUMMARY_PREFIX) : SUMMARY_PREFIX;
2688
+ return appendExpandHint(`${prefix} ${summary}`, !!options.hidden);
2689
+ }
2690
+ function isRendererExpanded(options, context, defaultExpanded = false) {
2691
+ return context?.expanded ?? options?.expanded ?? defaultExpanded;
2692
+ }
2693
+ function resolveRenderResultContext(options, rest, defaultExpanded = false) {
2694
+ const context = rest[0] ?? options ?? {};
2695
+ return {
2696
+ isPartial: context.isPartial ?? options?.isPartial ?? false,
2697
+ isError: context.isError ?? false,
2698
+ expanded: isRendererExpanded(options, context, defaultExpanded),
2699
+ width: context.width ?? options?.width,
2700
+ cwd: context.cwd ?? process.cwd(),
2701
+ context
2702
+ };
2703
+ }
2704
+ function normalizeWidth(width, fallback = 80) {
2705
+ return typeof width === "number" && Number.isFinite(width) && width > 0 ? Math.floor(width) : fallback;
2706
+ }
2707
+ function clampLineToWidth(line, width) {
2708
+ if (width === undefined || width === null)
2709
+ return line;
2710
+ const normalized = normalizeWidth(width);
2711
+ return visibleWidth(line) <= normalized ? line : truncateToWidth(line, normalized);
2712
+ }
2713
+ function clampLinesToWidth(lines, width) {
2714
+ if (width === undefined || width === null)
2715
+ return lines;
2716
+ return lines.map((line) => clampLineToWidth(line, width));
2717
+ }
2718
+ function wrapWithHangingIndent(prefix, content, width, options = {}) {
2719
+ const tint = options.tint ?? ((text) => text);
2720
+ if (width === undefined || width === null)
2721
+ return [tint(prefix + content)];
2722
+ const normalized = normalizeWidth(width);
2723
+ const combined = prefix + content;
2724
+ if (visibleWidth(combined) <= normalized)
2725
+ return [tint(combined)];
2726
+ const prefixWidth = visibleWidth(prefix);
2727
+ const contentWidth = Math.max(1, normalized - prefixWidth);
2728
+ const wrapped = wrapTextWithAnsi(content, contentWidth);
2729
+ if (wrapped.length === 0)
2730
+ return [tint(clampLineToWidth(prefix, normalized))];
2731
+ const indent = " ".repeat(prefixWidth);
2732
+ return wrapped.map((line, index) => tint(clampLineToWidth(index === 0 ? prefix + line : indent + line, normalized)));
2733
+ }
2734
+ var wrappedHashlinesCache = new WeakMap;
2735
+ function renderPendingResult(pendingLabel, width, theme) {
2736
+ return new Text(clampLinesToWidth([summaryLine(pendingLabel, { theme, style: "muted" })], width).join(`
2737
+ `), 0, 0);
2738
+ }
2739
+ function renderErrorResult(textContent, options) {
2740
+ const firstLine = textContent.split(`
2741
+ `)[0] || (options.fallback ?? "Error");
2742
+ const body = options.expanded && textContent ? textContent : firstLine;
2743
+ return new Text(clampLinesToWidth(summaryLine(body, { theme: options.theme, style: "error" }).split(`
2744
+ `), options.width).join(`
2745
+ `), 0, 0);
2746
+ }
2747
+ function renderReadSeekSearchCall(args, theme, rest, opts) {
2748
+ const context = rest[0] ?? {};
2749
+ let text = `${renderToolLabel(theme, opts.label)} ${theme.fg("accent", opts.accent)}`;
2750
+ text += theme.fg("dim", ` in ${args.path ?? "."}`);
2751
+ const language = args.language ?? args.lang;
2752
+ if (language)
2753
+ text += theme.fg("dim", ` (${language})`);
2754
+ const flags = opts.flags.filter(Boolean);
2755
+ if (flags.length > 0)
2756
+ text += theme.fg("dim", ` [${flags.join(",")}]`);
2757
+ return new Text(clampLineToWidth(text, context.width), 0, 0);
2758
+ }
2759
+ function renderAnchoredFilesResult(result, options, theme, rest, labels) {
2760
+ const { isPartial, isError, expanded, cwd, width } = resolveRenderResultContext(options, rest);
2761
+ if (isPartial)
2762
+ return renderPendingResult(labels.pendingLabel, width, theme);
2763
+ const content = result.content?.[0];
2764
+ const textContent = content?.type === "text" ? content.text : "";
2765
+ if (isError || result.isError)
2766
+ return renderErrorResult(textContent, { expanded, width, theme });
2767
+ const readSeekValue = result.details?.readSeekValue;
2768
+ const files = readSeekValue?.files ?? [];
2769
+ if (files.length === 0)
2770
+ return new Text(summaryLine(labels.emptyLabel, { theme, style: "dim" }), 0, 0);
2771
+ const fileCount = files.length;
2772
+ const total = files.reduce((sum, f) => sum + f.lines.length, 0);
2773
+ const unitWord = total === 1 ? labels.unitSingular : labels.unitPlural;
2774
+ const fileWord = fileCount === 1 ? "file" : "files";
2775
+ let text = summaryLine(`${total} ${unitWord} in ${fileCount} ${fileWord}`, {
2776
+ hidden: !expanded,
2777
+ theme,
2778
+ style: "success"
2779
+ });
2780
+ if (expanded) {
2781
+ for (const file of files.slice(0, 20)) {
2782
+ const display = relative(cwd, file.path) || file.path;
2783
+ text += `
2784
+ ${linkedPathLine(theme, file.path, display, cwd, ` (${file.lines.length})`, width)}`;
4291
2785
  }
2786
+ if (files.length > 20)
2787
+ text += `
2788
+ ` + theme.fg("muted", ` … and ${files.length - 20} more files`);
4292
2789
  }
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);
2790
+ return new Text(clampLinesToWidth(text.split(`
2791
+ `), width).join(`
2792
+ `), 0, 0);
4310
2793
  }
4311
2794
 
4312
2795
  // src/tui-diff-component.ts
4313
- import { Text as Text3, visibleWidth as visibleWidth3 } from "@earendil-works/pi-tui";
2796
+ import { Text as Text2, visibleWidth as visibleWidth3 } from "@earendil-works/pi-tui";
4314
2797
 
4315
2798
  // src/tui-diff-renderer.ts
4316
2799
  import { visibleWidth as visibleWidth2 } from "@earendil-works/pi-tui";
@@ -4478,34 +2961,47 @@ function upsertDiffComponent(lastComponent, options) {
4478
2961
  return component;
4479
2962
  }
4480
2963
  function upsertTextComponent(lastComponent, text) {
4481
- const component = lastComponent && !(lastComponent instanceof DiffPreviewComponent) ? lastComponent : new Text3("", 0, 0);
2964
+ const component = lastComponent && !(lastComponent instanceof DiffPreviewComponent) ? lastComponent : new Text2("", 0, 0);
4482
2965
  component.setText(text);
4483
2966
  return component;
4484
2967
  }
4485
2968
 
2969
+ // src/register-tool.ts
2970
+ import { Type } from "@sinclair/typebox";
2971
+ function optionalIntOrString(description) {
2972
+ return Type.Optional(Type.Union([Type.Number({ description }), Type.String({ description })]));
2973
+ }
2974
+ function filePathParam() {
2975
+ return Type.String({ description: "Absolute or relative file path" });
2976
+ }
2977
+ function registerReadSeekTool(pi, tool) {
2978
+ pi.registerTool(tool);
2979
+ return tool;
2980
+ }
2981
+
4486
2982
  // 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" })
2983
+ var hashlineEditItemSchema = Type2.Object({
2984
+ set_line: Type2.Optional(Type2.Object({
2985
+ anchor: Type2.String({ description: "Fresh LINE:HASH anchor for the line to replace" }),
2986
+ new_text: Type2.String({ description: "Replacement text; use an empty string to delete the line" })
4491
2987
  }, { 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" })
2988
+ replace_lines: Type2.Optional(Type2.Object({
2989
+ start_anchor: Type2.String({ description: "Fresh LINE:HASH anchor for the first line in the range" }),
2990
+ end_anchor: Type2.String({ description: "Fresh LINE:HASH anchor for the last line in the range" }),
2991
+ new_text: Type2.String({ description: "Replacement text; use an empty string to delete the range" })
4496
2992
  }, { 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" })
2993
+ insert_after: Type2.Optional(Type2.Object({
2994
+ anchor: Type2.String({ description: "Fresh LINE:HASH anchor for the line after which to insert" }),
2995
+ new_text: Type2.String({ description: "Text to insert after the anchored line" })
4500
2996
  }, { 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" }))
2997
+ replace: Type2.Optional(Type2.Object({
2998
+ old_text: Type2.String({ description: "Exact text to find" }),
2999
+ new_text: Type2.String({ description: "Replacement text" }),
3000
+ all: Type2.Optional(Type2.Boolean({ description: "Replace every exact match" }))
4505
3001
  }, { 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" })
3002
+ replace_symbol: Type2.Optional(Type2.Object({
3003
+ symbol: Type2.String({ description: "Mapped symbol name" }),
3004
+ new_body: Type2.String({ description: "Complete replacement body for the symbol" })
4509
3005
  }, { additionalProperties: false, description: 'Replace a symbol: {"replace_symbol":{"symbol":"name","new_body":"..."}}' }))
4510
3006
  }, {
4511
3007
  additionalProperties: false,
@@ -4513,14 +3009,14 @@ var hashlineEditItemSchema = Type3.Object({
4513
3009
  maxProperties: 1,
4514
3010
  description: "Exactly one nested edit variant"
4515
3011
  });
4516
- var hashlineEditSchema = Type3.Object({
3012
+ var hashlineEditSchema = Type2.Object({
4517
3013
  path: filePathParam(),
4518
- language: Type3.Optional(Type3.String({ description: "Language override for symbol replacement" })),
4519
- edits: Type3.Array(hashlineEditItemSchema, {
3014
+ language: Type2.Optional(Type2.String({ description: "Language override for symbol replacement" })),
3015
+ edits: Type2.Array(hashlineEditItemSchema, {
4520
3016
  minItems: 1,
4521
3017
  description: "Use set_line, replace_lines, insert_after, replace_symbol, or replace"
4522
3018
  }),
4523
- postEditVerify: Type3.Optional(Type3.Boolean({
3019
+ postEditVerify: Type2.Optional(Type2.Boolean({
4524
3020
  description: "Read back and verify persisted content"
4525
3021
  }))
4526
3022
  }, { additionalProperties: false });
@@ -4556,7 +3052,7 @@ async function executeEdit(opts) {
4556
3052
  return await withFileMutationQueue(absolutePath, async () => {
4557
3053
  throwIfAborted(signal);
4558
3054
  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.`;
3055
+ 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
3056
  return buildEditError(absolutePath, "file-not-read", `You must get fresh anchors for ${absolutePath} before editing it. ${hint}`, hint);
4561
3057
  }
4562
3058
  const edits = parsed.edits ?? [];
@@ -4627,7 +3123,7 @@ async function executeEdit(opts) {
4627
3123
  if (parsed.postEditVerify === true) {
4628
3124
  let verifiedContent;
4629
3125
  try {
4630
- verifiedContent = await fsReadFile2(absolutePath, "utf-8");
3126
+ verifiedContent = await fsReadFile(absolutePath, "utf-8");
4631
3127
  } catch (err) {
4632
3128
  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
3129
  }
@@ -4766,7 +3262,7 @@ ${theme.fg("error", info.errorText)}`;
4766
3262
  `), diffData, theme, expanded: true });
4767
3263
  }
4768
3264
  }
4769
- return new Text4(clampLinesToWidth(text.split(`
3265
+ return new Text3(clampLinesToWidth(text.split(`
4770
3266
  `), width).join(`
4771
3267
  `), 0, 0);
4772
3268
  }
@@ -4775,22 +3271,34 @@ ${theme.fg("error", info.errorText)}`;
4775
3271
  }
4776
3272
 
4777
3273
  // src/grep.ts
4778
- import { readFile as fsReadFile3, stat as fsStat } from "node:fs/promises";
3274
+ import { readFile as fsReadFile2, stat as fsStat } from "node:fs/promises";
4779
3275
  import path3 from "node:path";
4780
3276
  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";
3277
+ import { Type as Type4 } from "@sinclair/typebox";
3278
+ import { Text as Text4 } from "@earendil-works/pi-tui";
3279
+
3280
+ // src/binary-detect.ts
3281
+ function looksLikeBinary(buf) {
3282
+ if (buf.length === 0)
3283
+ return false;
3284
+ if (buf.includes(0))
3285
+ return true;
3286
+ const decoded = buf.toString("utf8");
3287
+ if (!decoded.includes("�"))
3288
+ return false;
3289
+ return !Buffer.from(decoded, "utf8").equals(buf);
3290
+ }
4783
3291
 
4784
3292
  // src/grep-output.ts
4785
3293
  import {
4786
- formatSize as formatSize3,
4787
- truncateHead as truncateHead3
3294
+ formatSize,
3295
+ truncateHead
4788
3296
  } from "@earendil-works/pi-coding-agent";
4789
3297
 
4790
3298
  // 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);
3299
+ import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES } from "@earendil-works/pi-coding-agent";
3300
+ var GREP_OUTPUT_DEFAULT_MAX_LINES = DEFAULT_MAX_LINES;
3301
+ var GREP_OUTPUT_DEFAULT_MAX_BYTES = Math.min(DEFAULT_MAX_BYTES, 50 * 1024);
4794
3302
  function resolveDimension(jsonValue, ceiling) {
4795
3303
  if (jsonValue !== undefined)
4796
3304
  return Math.min(jsonValue, ceiling);
@@ -4876,14 +3384,14 @@ ${passthroughLines.join(`
4876
3384
  ${text}`;
4877
3385
  }
4878
3386
  const budget = resolveGrepOutputBudget();
4879
- const truncated = truncateHead3(text, {
3387
+ const truncated = truncateHead(text, {
4880
3388
  maxLines: budget.maxLines,
4881
3389
  maxBytes: budget.maxBytes
4882
3390
  });
4883
3391
  if (truncated.truncated) {
4884
3392
  text = `${truncated.content}
4885
3393
 
4886
- [Output truncated: showing ${truncated.outputLines} of ${truncated.totalLines} lines (${formatSize3(truncated.outputBytes)} of ${formatSize3(truncated.totalBytes)}). Refine pattern or increase limit.]`;
3394
+ [Output truncated: showing ${truncated.outputLines} of ${truncated.totalLines} lines (${formatSize(truncated.outputBytes)} of ${formatSize(truncated.totalBytes)}). Refine pattern or increase limit.]`;
4887
3395
  }
4888
3396
  const readSeekValue = {
4889
3397
  tool: "grep",
@@ -4905,6 +3413,31 @@ ${text}`;
4905
3413
  };
4906
3414
  }
4907
3415
 
3416
+ // src/file-map.ts
3417
+ import { stat } from "node:fs/promises";
3418
+ async function getOrGenerateMap(absPath) {
3419
+ try {
3420
+ const { size } = await stat(absPath);
3421
+ return await readSeekMap(absPath, size);
3422
+ } catch {
3423
+ return null;
3424
+ }
3425
+ }
3426
+
3427
+ // src/readseek/symbol-tree.ts
3428
+ function traverseSymbolTree(symbols, visit) {
3429
+ const result = [];
3430
+ const walk = (nodes, parentName) => {
3431
+ for (const symbol of nodes) {
3432
+ result.push(visit(symbol, parentName));
3433
+ if (symbol.children?.length)
3434
+ walk(symbol.children, symbol.name);
3435
+ }
3436
+ };
3437
+ walk(symbols);
3438
+ return result;
3439
+ }
3440
+
4908
3441
  // src/grep-symbol-scope.ts
4909
3442
  function findEnclosingSymbol(map, lineNumber) {
4910
3443
  const candidates = traverseSymbolTree(map.symbols, (symbol, parentName) => ({
@@ -5112,19 +3645,73 @@ function formatGrepResultText(input) {
5112
3645
  };
5113
3646
  }
5114
3647
 
3648
+ // src/coerce-obvious-int.ts
3649
+ var BASE10_INT_RE = /^-?\d+$/;
3650
+ var MIN_SAFE_INTEGER = BigInt(Number.MIN_SAFE_INTEGER);
3651
+ var MAX_SAFE_INTEGER = BigInt(Number.MAX_SAFE_INTEGER);
3652
+ function formatReceived(value) {
3653
+ if (typeof value === "number" || typeof value === "bigint")
3654
+ return String(value);
3655
+ return JSON.stringify(value) ?? String(value);
3656
+ }
3657
+ function invalidInteger(value, name) {
3658
+ return {
3659
+ ok: false,
3660
+ message: `Invalid ${name}: expected a safe base-10 integer, received ${formatReceived(value)}.`
3661
+ };
3662
+ }
3663
+ function coerceObviousBase10Int(value, name) {
3664
+ if (value === undefined || typeof value === "string" && value.trim() === "") {
3665
+ return { ok: true, value: undefined };
3666
+ }
3667
+ if (typeof value === "number") {
3668
+ if (Number.isSafeInteger(value)) {
3669
+ return { ok: true, value };
3670
+ }
3671
+ return invalidInteger(value, name);
3672
+ }
3673
+ if (typeof value === "string" && BASE10_INT_RE.test(value)) {
3674
+ const parsed = BigInt(value);
3675
+ if (parsed >= MIN_SAFE_INTEGER && parsed <= MAX_SAFE_INTEGER) {
3676
+ return { ok: true, value: Number(parsed) };
3677
+ }
3678
+ }
3679
+ return invalidInteger(value, name);
3680
+ }
3681
+
3682
+ // src/tui-source-render.ts
3683
+ import { getLanguageFromPath, highlightCode } from "@earendil-works/pi-coding-agent";
3684
+ import { wrapTextWithAnsi as wrapTextWithAnsi2 } from "@earendil-works/pi-tui";
3685
+ var GREP_ANCHOR_RE = /^(.*?):(>>| )(\d+):([0-9a-zA-Z]{1,16})\|(.*)$/;
3686
+ var readSourceCache = new WeakMap;
3687
+ function renderGrepSourceForDisplay(text, anchors, renderPath = (path3) => path3) {
3688
+ return text.split(`
3689
+ `).map((line) => {
3690
+ const match = line.match(GREP_ANCHOR_RE);
3691
+ if (!match)
3692
+ return line;
3693
+ const anchor = `${match[3]}:${match[4]}`;
3694
+ if (!anchors.has(anchor))
3695
+ return line;
3696
+ const separator = match[2] === ">>" ? ":" : "-";
3697
+ return `${renderPath(match[1])}${separator}${match[3]}${separator}${match[5] ?? ""}`;
3698
+ }).join(`
3699
+ `);
3700
+ }
3701
+
5115
3702
  // src/readseek-params.ts
5116
- import { Type as Type4 } from "@sinclair/typebox";
3703
+ import { Type as Type3 } from "@sinclair/typebox";
5117
3704
  function searchPathParam() {
5118
- return Type4.Optional(Type4.String({ description: PARAM_DESCRIPTIONS.searchPath }));
3705
+ return Type3.Optional(Type3.String({ description: PARAM_DESCRIPTIONS.searchPath }));
5119
3706
  }
5120
3707
  function languageParam() {
5121
- return Type4.Optional(Type4.String({ description: PARAM_DESCRIPTIONS.language }));
3708
+ return Type3.Optional(Type3.String({ description: PARAM_DESCRIPTIONS.language }));
5122
3709
  }
5123
3710
  function readSeekGitSearchParams() {
5124
3711
  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 }))
3712
+ cached: Type3.Optional(Type3.Boolean({ description: PARAM_DESCRIPTIONS.cached })),
3713
+ others: Type3.Optional(Type3.Boolean({ description: PARAM_DESCRIPTIONS.others })),
3714
+ ignored: Type3.Optional(Type3.Boolean({ description: PARAM_DESCRIPTIONS.ignored }))
5128
3715
  };
5129
3716
  }
5130
3717
  function validateIgnoredRequiresOthers(tool, params) {
@@ -5136,16 +3723,16 @@ function validateIgnoredRequiresOthers(tool, params) {
5136
3723
  }
5137
3724
 
5138
3725
  // src/grep.ts
5139
- var grepSchema = Type5.Object({
5140
- pattern: Type5.String({ description: "Regex pattern; set literal for exact text" }),
3726
+ var grepSchema = Type4.Object({
3727
+ pattern: Type4.String({ description: "Regex pattern; set literal for exact text" }),
5141
3728
  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" })),
3729
+ glob: Type4.Optional(Type4.String({ description: "File-name glob, such as *.ts" })),
3730
+ ignoreCase: Type4.Optional(Type4.Boolean({ description: "Ignore case" })),
3731
+ literal: Type4.Optional(Type4.Boolean({ description: "Treat pattern literally" })),
5145
3732
  context: optionalIntOrString("Surrounding lines for each match"),
5146
3733
  limit: optionalIntOrString("Maximum matches to return"),
5147
- summary: Type5.Optional(Type5.Boolean({ description: "Return per-file counts" })),
5148
- scope: Type5.Optional(Type5.Literal("symbol", {
3734
+ summary: Type4.Optional(Type4.Boolean({ description: "Return per-file counts" })),
3735
+ scope: Type4.Optional(Type4.Literal("symbol", {
5149
3736
  description: "Group matches by enclosing symbol"
5150
3737
  })),
5151
3738
  scopeContext: optionalIntOrString("Context lines within each symbol")
@@ -5272,7 +3859,7 @@ async function executeGrep(opts) {
5272
3859
  }
5273
3860
  if (!searchPathIsDirectory) {
5274
3861
  try {
5275
- const buf = await fsReadFile3(searchPath);
3862
+ const buf = await fsReadFile2(searchPath);
5276
3863
  if (looksLikeBinary(buf)) {
5277
3864
  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
3865
  return {
@@ -5298,7 +3885,7 @@ async function executeGrep(opts) {
5298
3885
  if (fileCache.has(absolutePath))
5299
3886
  return fileCache.get(absolutePath);
5300
3887
  try {
5301
- const rawBuffer = await fsReadFile3(absolutePath);
3888
+ const rawBuffer = await fsReadFile2(absolutePath);
5302
3889
  if (looksLikeBinary(rawBuffer)) {
5303
3890
  fileCache.set(absolutePath, undefined);
5304
3891
  return;
@@ -5545,7 +4132,7 @@ function registerGrepTool(pi, options = {}) {
5545
4132
  text += theme.fg("dim", ` in ${suffix}`);
5546
4133
  }
5547
4134
  }
5548
- return new Text5(clampLineToWidth(text, context.width), 0, 0);
4135
+ return new Text4(clampLineToWidth(text, context.width), 0, 0);
5549
4136
  },
5550
4137
  renderResult(result, options2, theme, ...rest) {
5551
4138
  const { isPartial, isError, expanded, cwd, width } = resolveRenderResultContext(options2, rest, resolveReadSeekToolDisplayMode("grep") === "expanded");
@@ -5570,7 +4157,7 @@ function registerGrepTool(pi, options = {}) {
5570
4157
  hasBinaryWarning
5571
4158
  });
5572
4159
  if (info.noMatches && !hasBinaryWarning)
5573
- return new Text5(summaryLine("no matches", { theme, style: "dim" }), 0, 0);
4160
+ return new Text4(summaryLine("no matches", { theme, style: "dim" }), 0, 0);
5574
4161
  const matchCount = readSeekValue?.totalMatches ?? 0;
5575
4162
  const matchWord = matchCount === 1 ? "match" : "matches";
5576
4163
  let text = summaryLine(`${matchCount} ${matchWord} returned`, {
@@ -5585,7 +4172,7 @@ function registerGrepTool(pi, options = {}) {
5585
4172
  text += `
5586
4173
  ` + renderGrepSourceForDisplay(textContent, anchors, (displayPath) => linkToolPath(theme.fg("dim", displayPath), displayPath, cwd));
5587
4174
  }
5588
- return new Text5(clampLinesToWidth(text.split(`
4175
+ return new Text4(clampLinesToWidth(text.split(`
5589
4176
  `), width).join(`
5590
4177
  `), 0, 0);
5591
4178
  }
@@ -5595,7 +4182,7 @@ function registerGrepTool(pi, options = {}) {
5595
4182
 
5596
4183
  // src/search.ts
5597
4184
  import path4 from "node:path";
5598
- import { Type as Type6 } from "@sinclair/typebox";
4185
+ import { Type as Type5 } from "@sinclair/typebox";
5599
4186
 
5600
4187
  // src/stat-search-path.ts
5601
4188
  import { stat as fsStat2 } from "node:fs/promises";
@@ -5762,8 +4349,8 @@ function registerSearchTool(pi, options = {}) {
5762
4349
  description: SEARCH_PROMPT_METADATA.description,
5763
4350
  promptSnippet: SEARCH_PROMPT_METADATA.promptSnippet,
5764
4351
  promptGuidelines: SEARCH_PROMPT_METADATA.promptGuidelines,
5765
- parameters: Type6.Object({
5766
- pattern: Type6.String({ description: PARAM_DESCRIPTIONS.pattern }),
4352
+ parameters: Type5.Object({
4353
+ pattern: Type5.String({ description: PARAM_DESCRIPTIONS.pattern }),
5767
4354
  language: languageParam(),
5768
4355
  path: searchPathParam(),
5769
4356
  ...readSeekGitSearchParams()
@@ -5791,7 +4378,7 @@ function registerSearchTool(pi, options = {}) {
5791
4378
  }
5792
4379
 
5793
4380
  // src/refs.ts
5794
- import { Type as Type7 } from "@sinclair/typebox";
4381
+ import { Type as Type6 } from "@sinclair/typebox";
5795
4382
  import path5 from "node:path";
5796
4383
 
5797
4384
  // src/refs-output.ts
@@ -5910,13 +4497,13 @@ function registerRefsTool(pi, options = {}) {
5910
4497
  description: REFS_PROMPT_METADATA.description,
5911
4498
  promptSnippet: REFS_PROMPT_METADATA.promptSnippet,
5912
4499
  promptGuidelines: REFS_PROMPT_METADATA.promptGuidelines,
5913
- parameters: Type7.Object({
5914
- name: Type7.Optional(Type7.String({ description: PARAM_DESCRIPTIONS.refsName })),
4500
+ parameters: Type6.Object({
4501
+ name: Type6.Optional(Type6.String({ description: PARAM_DESCRIPTIONS.refsName })),
5915
4502
  path: searchPathParam(),
5916
4503
  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 })),
4504
+ scope: Type6.Optional(Type6.Boolean({ description: PARAM_DESCRIPTIONS.scope })),
4505
+ line: Type6.Optional(Type6.Number({ description: PARAM_DESCRIPTIONS.line })),
4506
+ column: Type6.Optional(Type6.Number({ description: PARAM_DESCRIPTIONS.column })),
5920
4507
  ...readSeekGitSearchParams()
5921
4508
  }),
5922
4509
  async execute(_toolCallId, params, signal, _onUpdate, ctx) {
@@ -5943,20 +4530,20 @@ function registerRefsTool(pi, options = {}) {
5943
4530
  }
5944
4531
 
5945
4532
  // src/rename.ts
5946
- import { Type as Type8 } from "@sinclair/typebox";
4533
+ import { Type as Type7 } from "@sinclair/typebox";
5947
4534
  import path6 from "node:path";
5948
- import { Text as Text6 } from "@earendil-works/pi-tui";
4535
+ import { Text as Text5 } from "@earendil-works/pi-tui";
5949
4536
  var RENAME_PROMPT_METADATA = defineToolPromptMetadata({
5950
4537
  promptUrl: new URL("../prompts/rename.md", import.meta.url),
5951
4538
  promptSnippet: "Rename a symbol at a source location with verified edits"
5952
4539
  });
5953
- var renameSchema = Type8.Object({
4540
+ var renameSchema = Type7.Object({
5954
4541
  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" }))
4542
+ line: Type7.Integer({ minimum: 1, description: "One-based cursor line of the symbol to rename" }),
4543
+ column: Type7.Optional(Type7.Integer({ minimum: 1, description: "One-based cursor byte column for disambiguation" })),
4544
+ to: Type7.String({ description: "New symbol name" }),
4545
+ workspace: Type7.Optional(Type7.Boolean({ description: "Rename across the project" })),
4546
+ apply: Type7.Optional(Type7.Boolean({ description: "Apply the verified edits; defaults to true" }))
5960
4547
  });
5961
4548
  async function executeRename(opts) {
5962
4549
  const { params, signal, cwd, onFileMutated } = opts;
@@ -6046,7 +4633,7 @@ function registerRenameTool(pi, options = {}) {
6046
4633
  text += ` ${linkToolPath(theme.fg("accent", displayPath), displayPath, cwd)}`;
6047
4634
  if (args?.to)
6048
4635
  text += theme.fg("dim", ` → ${args.to}`);
6049
- return new Text6(clampLineToWidth(text, context.width), 0, 0);
4636
+ return new Text5(clampLineToWidth(text, context.width), 0, 0);
6050
4637
  },
6051
4638
  renderResult(result, options2, theme, ...rest) {
6052
4639
  const { isPartial, isError, expanded, cwd, width } = resolveRenderResultContext(options2, rest);
@@ -6079,7 +4666,7 @@ ${linkedPathLine(theme, file.file, display, cwd, ` (${file.edits.length} edits)`
6079
4666
  }
6080
4667
  const lines = clampLinesToWidth(text.split(`
6081
4668
  `), width);
6082
- return new Text6(lines.join(`
4669
+ return new Text5(lines.join(`
6083
4670
  `), 0, 0);
6084
4671
  }
6085
4672
  });
@@ -6088,9 +4675,9 @@ ${linkedPathLine(theme, file.file, display, cwd, ` (${file.edits.length} edits)`
6088
4675
  // src/write.ts
6089
4676
  import { mkdir, readFile, writeFile } from "node:fs/promises";
6090
4677
  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";
4678
+ import { withFileMutationQueue as withFileMutationQueue2, truncateHead as truncateHead2 } from "@earendil-works/pi-coding-agent";
4679
+ import { Text as Text6 } from "@earendil-works/pi-tui";
4680
+ import { Type as Type8 } from "@sinclair/typebox";
6094
4681
 
6095
4682
  // src/pending-diff-preview.ts
6096
4683
  import { existsSync as existsSync2, readFileSync as readFileSync3, realpathSync, statSync as statSync3 } from "node:fs";
@@ -6295,7 +4882,7 @@ async function executeWrite(opts) {
6295
4882
  const warnings = [];
6296
4883
  const readSeekWarnings = [];
6297
4884
  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.";
4885
+ 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
4886
  warnings.push(message);
6300
4887
  readSeekWarnings.push(buildReadSeekWarning("bare-cr", message));
6301
4888
  return buildToolErrorResult("write", "bare-cr", `Cannot write ${filePath}
@@ -6336,7 +4923,7 @@ async function executeWrite(opts) {
6336
4923
  }
6337
4924
  const fullText = displayLines.join(`
6338
4925
  `);
6339
- const truncated = truncateHead4(fullText);
4926
+ const truncated = truncateHead2(fullText);
6340
4927
  let text = truncated.content;
6341
4928
  if (truncated.truncated) {
6342
4929
  if (truncated.truncatedBy === "lines") {
@@ -6386,9 +4973,9 @@ function registerWriteTool(pi, options = {}) {
6386
4973
  description: promptMetadata.description,
6387
4974
  promptSnippet: promptMetadata.promptSnippet,
6388
4975
  promptGuidelines: promptMetadata.promptGuidelines,
6389
- parameters: Type9.Object({
4976
+ parameters: Type8.Object({
6390
4977
  path: filePathParam(),
6391
- content: Type9.String({ description: "Complete text file content" })
4978
+ content: Type8.String({ description: "Complete text file content" })
6392
4979
  }),
6393
4980
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
6394
4981
  const cwd = ctx?.cwd ?? process.cwd();
@@ -6469,7 +5056,7 @@ function registerWriteTool(pi, options = {}) {
6469
5056
  `);
6470
5057
  lines.push(...formatContentPreviewLines(content, theme));
6471
5058
  }
6472
- return new Text7(clampLinesToWidth(lines, width).join(`
5059
+ return new Text6(clampLinesToWidth(lines, width).join(`
6473
5060
  `), 0, 0);
6474
5061
  }
6475
5062
  const hasExpandableDiff = !!diffData;
@@ -6478,7 +5065,7 @@ function registerWriteTool(pi, options = {}) {
6478
5065
  return upsertDiffComponent(context.lastComponent, { prefixLines: text.split(`
6479
5066
  `), diffData, theme, expanded: true });
6480
5067
  }
6481
- return new Text7(clampLinesToWidth(text.split(`
5068
+ return new Text6(clampLinesToWidth(text.split(`
6482
5069
  `), width).join(`
6483
5070
  `), 0, 0);
6484
5071
  }
@@ -6487,7 +5074,7 @@ function registerWriteTool(pi, options = {}) {
6487
5074
  }
6488
5075
 
6489
5076
  // src/def.ts
6490
- import { Type as Type10 } from "@sinclair/typebox";
5077
+ import { Type as Type9 } from "@sinclair/typebox";
6491
5078
  import path7 from "node:path";
6492
5079
  var DEF_PROMPT_METADATA = defineToolPromptMetadata({
6493
5080
  promptUrl: new URL("../prompts/def.md", import.meta.url),
@@ -6563,8 +5150,8 @@ function registerDefTool(pi, options = {}) {
6563
5150
  description: DEF_PROMPT_METADATA.description,
6564
5151
  promptSnippet: DEF_PROMPT_METADATA.promptSnippet,
6565
5152
  promptGuidelines: DEF_PROMPT_METADATA.promptGuidelines,
6566
- parameters: Type10.Object({
6567
- name: Type10.String({ description: PARAM_DESCRIPTIONS.name }),
5153
+ parameters: Type9.Object({
5154
+ name: Type9.String({ description: PARAM_DESCRIPTIONS.name }),
6568
5155
  path: searchPathParam(),
6569
5156
  language: languageParam(),
6570
5157
  ...readSeekGitSearchParams()
@@ -6591,33 +5178,29 @@ function registerDefTool(pi, options = {}) {
6591
5178
  }
6592
5179
 
6593
5180
  // src/digest.ts
6594
- import { Text as Text8 } from "@earendil-works/pi-tui";
6595
- import { Type as Type11 } from "@sinclair/typebox";
5181
+ import { Text as Text7 } from "@earendil-works/pi-tui";
5182
+ import { Type as Type10 } from "@sinclair/typebox";
6596
5183
  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({
5184
+ var facetSchema = Type10.Union(DIGEST_FACETS2.map((facet) => Type10.Literal(facet)));
5185
+ var digestSchema = Type10.Object({
6603
5186
  path: filePathParam(),
6604
- select: Type11.Optional(Type11.Union([
5187
+ select: Type10.Optional(Type10.Union([
6605
5188
  facetSchema,
6606
- Type11.Array(facetSchema, { minItems: 1 }),
6607
- Type11.String({
5189
+ Type10.Array(facetSchema, { minItems: 1 }),
5190
+ Type10.String({
6608
5191
  description: PARAM_DESCRIPTIONS.selectComma
6609
5192
  })
6610
5193
  ], {
6611
5194
  description: PARAM_DESCRIPTIONS.select
6612
5195
  })),
6613
- at: Type11.Optional(Type11.String({
5196
+ at: Type10.Optional(Type10.String({
6614
5197
  description: PARAM_DESCRIPTIONS.at
6615
5198
  })),
6616
5199
  end: optionalIntOrString(PARAM_DESCRIPTIONS.end),
6617
5200
  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 }))
5201
+ language: Type10.Optional(Type10.String({ description: PARAM_DESCRIPTIONS.languageOverride })),
5202
+ visionMode: Type10.Optional(Type10.Union(VISION_MODES.map((mode) => Type10.Literal(mode)), { description: PARAM_DESCRIPTIONS.visionModeDigest })),
5203
+ visionLevel: Type10.Optional(Type10.Union(VISION_LEVELS.map((level) => Type10.Literal(level)), { description: PARAM_DESCRIPTIONS.visionLevel }))
6621
5204
  });
6622
5205
  function hasDigestAnchors(value) {
6623
5206
  if (!value || typeof value !== "object")
@@ -6743,12 +5326,19 @@ async function executeDigest(opts) {
6743
5326
  }
6744
5327
  }
6745
5328
  function registerDigestTool(pi, options = {}) {
5329
+ const promptMetadata = defineToolPromptMetadata({
5330
+ promptUrl: new URL("../prompts/digest.md", import.meta.url),
5331
+ promptSnippet: "Digest source facets as a native CLI envelope",
5332
+ registeredName: options.name ?? "readSeek_digest",
5333
+ toolAliases: options.toolAliases
5334
+ });
6746
5335
  return registerReadSeekTool(pi, {
6747
- name: "readSeek_digest",
5336
+ name: options.name ?? "readSeek_digest",
6748
5337
  label: "Digest",
6749
- description: DIGEST_PROMPT_METADATA.description,
6750
- promptSnippet: DIGEST_PROMPT_METADATA.promptSnippet,
6751
- promptGuidelines: DIGEST_PROMPT_METADATA.promptGuidelines,
5338
+ description: promptMetadata.description,
5339
+ promptSnippet: promptMetadata.promptSnippet,
5340
+ toolAliases: options.toolAliases,
5341
+ promptGuidelines: promptMetadata.promptGuidelines,
6752
5342
  parameters: digestSchema,
6753
5343
  async execute(_toolCallId, params, signal, _onUpdate, ctx) {
6754
5344
  return executeDigest({ params, signal, cwd: ctx.cwd, onFileAnchored: options.onFileAnchored });
@@ -6765,7 +5355,7 @@ function registerDigestTool(pi, options = {}) {
6765
5355
  }
6766
5356
  if (typeof args?.at === "string" && args.at)
6767
5357
  text += theme.fg("dim", ` @ ${args.at}`);
6768
- return new Text8(clampLineToWidth(text, context.width), 0, 0);
5358
+ return new Text7(clampLineToWidth(text, context.width), 0, 0);
6769
5359
  },
6770
5360
  renderResult(result, options2, theme, ...rest) {
6771
5361
  const { isPartial, isError, expanded, width } = resolveRenderResultContext(options2, rest);
@@ -6782,7 +5372,7 @@ function registerDigestTool(pi, options = {}) {
6782
5372
  if (expanded && textContent)
6783
5373
  text += `
6784
5374
  ${textContent}`;
6785
- return new Text8(clampLinesToWidth(text.split(`
5375
+ return new Text7(clampLinesToWidth(text.split(`
6786
5376
  `), width).join(`
6787
5377
  `), 0, 0);
6788
5378
  }
@@ -6791,12 +5381,12 @@ ${textContent}`;
6791
5381
 
6792
5382
  // src/view.ts
6793
5383
  import {
6794
- DEFAULT_MAX_BYTES as DEFAULT_MAX_BYTES4,
6795
- DEFAULT_MAX_LINES as DEFAULT_MAX_LINES4,
6796
- truncateHead as truncateHead5
5384
+ DEFAULT_MAX_BYTES as DEFAULT_MAX_BYTES2,
5385
+ DEFAULT_MAX_LINES as DEFAULT_MAX_LINES2,
5386
+ truncateHead as truncateHead3
6797
5387
  } from "@earendil-works/pi-coding-agent";
6798
- import { Text as Text9 } from "@earendil-works/pi-tui";
6799
- import { Type as Type12 } from "@sinclair/typebox";
5388
+ import { Text as Text8 } from "@earendil-works/pi-tui";
5389
+ import { Type as Type11 } from "@sinclair/typebox";
6800
5390
  var NODE_KINDS = DOCUMENT_NODE_KINDS;
6801
5391
  var VIEW_PROMPT_METADATA = defineToolPromptMetadata({
6802
5392
  promptUrl: new URL("../prompts/view.md", import.meta.url),
@@ -6843,9 +5433,9 @@ async function executeView(opts) {
6843
5433
  visionLevel: input.visionLevel,
6844
5434
  signal
6845
5435
  });
6846
- const truncation = truncateHead5(output, {
6847
- maxLines: DEFAULT_MAX_LINES4,
6848
- maxBytes: DEFAULT_MAX_BYTES4
5436
+ const truncation = truncateHead3(output, {
5437
+ maxLines: DEFAULT_MAX_LINES2,
5438
+ maxBytes: DEFAULT_MAX_BYTES2
6849
5439
  });
6850
5440
  const notice = truncation.truncated ? `
6851
5441
  [… document view truncated; narrow it with page, node, kind, or depth]` : "";
@@ -6871,17 +5461,17 @@ function registerViewTool(pi) {
6871
5461
  description: VIEW_PROMPT_METADATA.description,
6872
5462
  promptSnippet: VIEW_PROMPT_METADATA.promptSnippet,
6873
5463
  promptGuidelines: VIEW_PROMPT_METADATA.promptGuidelines,
6874
- parameters: Type12.Object({
5464
+ parameters: Type11.Object({
6875
5465
  path: filePathParam(),
6876
- node: Type12.Optional(Type12.String({ description: PARAM_DESCRIPTIONS.node })),
5466
+ node: Type11.Optional(Type11.String({ description: PARAM_DESCRIPTIONS.node })),
6877
5467
  page: optionalIntOrString(PARAM_DESCRIPTIONS.page),
6878
- kind: Type12.Optional(Type12.Union(NODE_KINDS.map((kind) => Type12.Literal(kind)), {
5468
+ kind: Type11.Optional(Type11.Union(NODE_KINDS.map((kind) => Type11.Literal(kind)), {
6879
5469
  description: PARAM_DESCRIPTIONS.kind
6880
5470
  })),
6881
5471
  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 }))
5472
+ visionMode: Type11.Optional(Type11.Union(VISION_ANALYSIS_MODES.map((mode) => Type11.Literal(mode)), { description: PARAM_DESCRIPTIONS.visionModeView })),
5473
+ visionLevel: Type11.Optional(Type11.Union(VISION_LEVELS.map((level) => Type11.Literal(level)), { description: PARAM_DESCRIPTIONS.visionLevel })),
5474
+ outline: Type11.Optional(Type11.Boolean({ description: PARAM_DESCRIPTIONS.outline }))
6885
5475
  }),
6886
5476
  async execute(_toolCallId, params, signal, _onUpdate, ctx) {
6887
5477
  return executeView({ params, signal, cwd: ctx.cwd });
@@ -6891,7 +5481,7 @@ function registerViewTool(pi) {
6891
5481
  const cwd = context.cwd ?? process.cwd();
6892
5482
  const displayPath = typeof args?.path === "string" ? args.path : "?";
6893
5483
  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);
5484
+ return new Text8(clampLineToWidth(text, context.width), 0, 0);
6895
5485
  },
6896
5486
  renderResult(result, options, theme, ...rest) {
6897
5487
  const { isPartial, isError, expanded, width } = resolveRenderResultContext(options, rest);
@@ -6911,7 +5501,7 @@ function registerViewTool(pi) {
6911
5501
  if (expanded && textContent)
6912
5502
  text += `
6913
5503
  ${textContent}`;
6914
- return new Text9(clampLinesToWidth(text.split(`
5504
+ return new Text8(clampLinesToWidth(text.split(`
6915
5505
  `), width).join(`
6916
5506
  `), 0, 0);
6917
5507
  }
@@ -6937,7 +5527,6 @@ class SessionAnchors {
6937
5527
 
6938
5528
  // index.ts
6939
5529
  var READSEEK_TOOL_ENTRIES = [
6940
- { builtIn: "read", readSeekName: "readSeek_read" },
6941
5530
  { builtIn: "edit", readSeekName: "readSeek_edit" },
6942
5531
  { builtIn: "grep", readSeekName: "readSeek_grep" },
6943
5532
  { builtIn: null, readSeekName: "readSeek_search" },
@@ -6945,7 +5534,7 @@ var READSEEK_TOOL_ENTRIES = [
6945
5534
  { builtIn: null, readSeekName: "readSeek_rename" },
6946
5535
  { builtIn: "write", readSeekName: "readSeek_write" },
6947
5536
  { builtIn: null, readSeekName: "readSeek_def" },
6948
- { builtIn: null, readSeekName: "readSeek_digest" },
5537
+ { builtIn: "read", readSeekName: "readSeek_digest" },
6949
5538
  { builtIn: null, readSeekName: "readSeek_view" }
6950
5539
  ];
6951
5540
  function formatSettingsWarning(warning) {
@@ -6956,7 +5545,7 @@ function editingPolicy(readName, editName, writeName) {
6956
5545
  "ReadSeek editing policy:",
6957
5546
  `- Read first with ${readName} when preparing to edit; ${editName} needs fresh LINE:HASH anchors.`,
6958
5547
  `- 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."
5548
+ `- Run ${readName} with select diagnostics after source edits for a quick syntax check.`
6960
5549
  ].join(`
6961
5550
  `);
6962
5551
  }
@@ -6969,24 +5558,23 @@ function piReadSeekExtension(pi) {
6969
5558
  const replacedBuiltIns = new Set(settings.overrideTools ?? []);
6970
5559
  const binaryAvailable = readSeekBinaryAvailability().available;
6971
5560
  const swap = (builtIn, readSeekName) => binaryAvailable && replacedBuiltIns.has(builtIn) ? builtIn : readSeekName;
6972
- const readName = swap("read", "readSeek_read");
5561
+ const readName = swap("read", "readSeek_digest");
6973
5562
  const editName = swap("edit", "readSeek_edit");
6974
5563
  const writeName = swap("write", "readSeek_write");
6975
5564
  const grepName = swap("grep", "readSeek_grep");
6976
5565
  const toolAliases = {
6977
- readSeek_read: readName,
5566
+ readSeek_digest: readName,
6978
5567
  readSeek_edit: editName,
6979
5568
  readSeek_grep: grepName,
6980
5569
  readSeek_write: writeName
6981
5570
  };
6982
- registerReadTool(pi, { onSuccessfulRead: markAnchored, name: readName });
6983
5571
  registerEditTool(pi, { wasReadInSession: hasFreshAnchors, onFileMutated: forgetAnchors, name: editName, toolAliases });
6984
5572
  registerGrepTool(pi, { onFileAnchored: markAnchored, name: grepName });
6985
5573
  registerSearchTool(pi, { onFileAnchored: markAnchored });
6986
5574
  registerRefsTool(pi, { onFileAnchored: markAnchored });
6987
5575
  registerRenameTool(pi, { onFileMutated: forgetAnchors });
6988
5576
  registerDefTool(pi, { onFileAnchored: markAnchored });
6989
- registerDigestTool(pi, { onFileAnchored: markAnchored });
5577
+ registerDigestTool(pi, { onFileAnchored: markAnchored, name: readName, toolAliases });
6990
5578
  registerViewTool(pi);
6991
5579
  registerWriteTool(pi, { onFileAnchored: markAnchored, name: writeName });
6992
5580
  pi.on("before_agent_start", (event) => {