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