pi-readseek 0.4.11 → 0.4.13
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/index.ts +1 -1
- package/package.json +2 -2
- package/src/edit-output.ts +3 -3
- package/src/edit-syntax-validate.ts +5 -5
- package/src/edit.ts +56 -17
- package/src/file-map.ts +17 -0
- package/src/grep-budget.ts +3 -3
- package/src/grep-output.ts +8 -8
- package/src/grep-render-helpers.ts +3 -3
- package/src/grep-symbol-scope.ts +4 -4
- package/src/grep.ts +7 -7
- package/src/hashline.ts +5 -5
- package/src/pending-diff-preview.ts +3 -3
- package/src/read-output.ts +21 -21
- package/src/read-render-helpers.ts +2 -2
- package/src/read.ts +17 -17
- package/src/readseek/constants.ts +0 -4
- package/src/readseek/formatter.ts +9 -74
- package/src/readseek/symbol-lookup.ts +1 -43
- package/src/readseek/types.ts +0 -20
- package/src/readseek-client.ts +68 -69
- package/src/readseek-settings.ts +17 -17
- package/src/readseek-value.ts +27 -27
- package/src/refs-output.ts +4 -4
- package/src/refs.ts +13 -30
- package/src/register-tool.ts +10 -10
- package/src/replace-symbol.ts +4 -3
- package/src/sg-output.ts +5 -5
- package/src/sg.ts +16 -29
- package/src/stat-search-path.ts +34 -0
- package/src/tool-prompt-metadata.ts +3 -3
- package/src/write.ts +32 -48
- package/src/map-cache.ts +0 -82
package/src/refs.ts
CHANGED
|
@@ -2,14 +2,14 @@ import type { ExtensionAPI, ToolRenderResultOptions } from "@earendil-works/pi-c
|
|
|
2
2
|
import { Text } from "@earendil-works/pi-tui";
|
|
3
3
|
import { Type } from "@sinclair/typebox";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { stat as fsStat } from "node:fs/promises";
|
|
6
5
|
import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
|
|
7
|
-
import {
|
|
6
|
+
import { statSearchPathOrError } from "./stat-search-path.js";
|
|
7
|
+
import { buildReadSeekLineWithHash, buildToolErrorResult } from "./readseek-value.js";
|
|
8
8
|
import { resolveToCwd } from "./path-utils.js";
|
|
9
|
-
import {
|
|
9
|
+
import { classifyReadSeekFailure, readseekRefs, type ReadSeekReference } from "./readseek-client.js";
|
|
10
10
|
import { buildRefsOutput, type RefsOutputFile, type RefsOutputLine } from "./refs-output.js";
|
|
11
11
|
import type { FileAnchoredCallback } from "./tool-types.js";
|
|
12
|
-
import {
|
|
12
|
+
import { registerReadSeekTool } from "./register-tool.js";
|
|
13
13
|
|
|
14
14
|
import { clampLineToWidth, renderAnchoredFilesResult, renderToolLabel } from "./tui-render-utils.js";
|
|
15
15
|
|
|
@@ -44,14 +44,14 @@ export interface ExecuteRefsOptions {
|
|
|
44
44
|
onFileAnchored?: FileAnchoredCallback;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
function refsLine(reference:
|
|
47
|
+
function refsLine(reference: ReadSeekReference): RefsOutputLine {
|
|
48
48
|
return {
|
|
49
|
-
...
|
|
49
|
+
...buildReadSeekLineWithHash(reference.line, reference.line_hash, reference.text),
|
|
50
50
|
enclosingSymbol: reference.enclosingSymbol,
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
function groupReferences(references:
|
|
54
|
+
function groupReferences(references: ReadSeekReference[], cwd: string): RefsOutputFile[] {
|
|
55
55
|
const files = new Map<string, RefsOutputFile>();
|
|
56
56
|
const seen = new Set<string>();
|
|
57
57
|
for (const reference of references) {
|
|
@@ -69,7 +69,7 @@ function groupReferences(references: ReadseekReference[], cwd: string): RefsOutp
|
|
|
69
69
|
return [...files.values()];
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
function
|
|
72
|
+
function isReadSeekCursorValidationFailure(message: string): boolean {
|
|
73
73
|
return (
|
|
74
74
|
/line and column must be greater than zero/i.test(message) ||
|
|
75
75
|
/line \d+ not found/i.test(message) ||
|
|
@@ -91,25 +91,8 @@ export async function executeRefs(opts: ExecuteRefsOptions): Promise<any> {
|
|
|
91
91
|
}
|
|
92
92
|
const searchPath = resolveToCwd(p.path ?? ".", cwd);
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
} catch (err: any) {
|
|
97
|
-
if (err?.code === "ENOENT") {
|
|
98
|
-
return buildToolErrorResult("refs", "path-not-found", `Error: path '${p.path ?? "."}' does not exist`, {
|
|
99
|
-
path: p.path ?? searchPath,
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
if (err?.code === "EACCES" || err?.code === "EPERM") {
|
|
103
|
-
return buildToolErrorResult("refs", "permission-denied", `Error: permission denied for path '${p.path ?? "."}'`, {
|
|
104
|
-
path: p.path ?? searchPath,
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
const message = `Error: could not access path '${p.path ?? "."}': ${err?.message ?? String(err)}`;
|
|
108
|
-
return buildToolErrorResult("refs", "fs-error", message, {
|
|
109
|
-
path: p.path ?? searchPath,
|
|
110
|
-
details: { fsCode: err?.code, fsMessage: err?.message },
|
|
111
|
-
});
|
|
112
|
-
}
|
|
94
|
+
const statResult = await statSearchPathOrError("refs", p.path, searchPath);
|
|
95
|
+
if (!statResult.ok) return statResult.error;
|
|
113
96
|
|
|
114
97
|
try {
|
|
115
98
|
const references = await readseekRefs(searchPath, p.name, {
|
|
@@ -132,8 +115,8 @@ export async function executeRefs(opts: ExecuteRefsOptions): Promise<any> {
|
|
|
132
115
|
details: { readseekValue: builtOutput.readseekValue },
|
|
133
116
|
};
|
|
134
117
|
} catch (err: any) {
|
|
135
|
-
const failure =
|
|
136
|
-
if (p.scope &&
|
|
118
|
+
const failure = classifyReadSeekFailure(err);
|
|
119
|
+
if (p.scope && isReadSeekCursorValidationFailure(failure.message)) {
|
|
137
120
|
return buildToolErrorResult("refs", "invalid-parameter", failure.message);
|
|
138
121
|
}
|
|
139
122
|
return buildToolErrorResult("refs", failure.code, failure.message, failure.hint ? { hint: failure.hint } : {});
|
|
@@ -141,7 +124,7 @@ export async function executeRefs(opts: ExecuteRefsOptions): Promise<any> {
|
|
|
141
124
|
}
|
|
142
125
|
|
|
143
126
|
export function registerRefsTool(pi: ExtensionAPI, options: RefsToolOptions = {}) {
|
|
144
|
-
const tool =
|
|
127
|
+
const tool = registerReadSeekTool(pi, {
|
|
145
128
|
policy: "read-only",
|
|
146
129
|
pythonName: "refs",
|
|
147
130
|
defaultExposure: "opt-in",
|
package/src/register-tool.ts
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
|
|
3
|
-
export type
|
|
3
|
+
export type ReadSeekToolPolicy = "read-only" | "mutating";
|
|
4
4
|
|
|
5
|
-
export type
|
|
5
|
+
export type ReadSeekToolExposure = "safe-by-default" | "opt-in" | "not-safe-by-default";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Per-tool fields of the pi tool config (`ptc`) that genuinely differ between
|
|
9
9
|
* readseek tools. The remaining fields are constant or derived.
|
|
10
10
|
*/
|
|
11
|
-
export interface
|
|
12
|
-
policy:
|
|
11
|
+
export interface ReadSeekToolConfig {
|
|
12
|
+
policy: ReadSeekToolPolicy;
|
|
13
13
|
pythonName: string;
|
|
14
|
-
defaultExposure:
|
|
14
|
+
defaultExposure: ReadSeekToolExposure;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export interface
|
|
17
|
+
export interface ReadSeekToolPtc extends ReadSeekToolConfig {
|
|
18
18
|
callable: true;
|
|
19
19
|
enabled: true;
|
|
20
20
|
readOnly: boolean;
|
|
@@ -28,12 +28,12 @@ type ToolSpec = Parameters<ExtensionAPI["registerTool"]>[0];
|
|
|
28
28
|
* `callable` and `enabled` are always true and `readOnly` is derived from
|
|
29
29
|
* `policy`, so callers supply only the fields that vary between tools.
|
|
30
30
|
*/
|
|
31
|
-
export function
|
|
31
|
+
export function registerReadSeekTool<T extends ToolSpec>(
|
|
32
32
|
pi: ExtensionAPI,
|
|
33
|
-
config:
|
|
33
|
+
config: ReadSeekToolConfig,
|
|
34
34
|
tool: T,
|
|
35
|
-
): T & { ptc:
|
|
36
|
-
const ptc:
|
|
35
|
+
): T & { ptc: ReadSeekToolPtc } {
|
|
36
|
+
const ptc: ReadSeekToolPtc = {
|
|
37
37
|
callable: true,
|
|
38
38
|
enabled: true,
|
|
39
39
|
policy: config.policy,
|
package/src/replace-symbol.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { readseekMapContent } from "./readseek-client.js";
|
|
2
2
|
import { findSymbol } from "./readseek/symbol-lookup.js";
|
|
3
3
|
import { formatAmbiguous, formatNotFound } from "./readseek/symbol-error-format.js";
|
|
4
|
+
import { normalizeToLF } from "./edit-diff.js";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
interface ReplaceSymbolInput {
|
|
6
7
|
filePath: string;
|
|
7
8
|
content: string;
|
|
8
9
|
symbol: string;
|
|
9
10
|
newBody: string;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
type ReplaceSymbolResult =
|
|
13
14
|
| { type: "ok"; content: string; replacement: string; warnings: string[]; range: { start: number; end: number } }
|
|
14
15
|
| { type: "not-found"; message: string }
|
|
15
16
|
| { type: "ambiguous"; message: string }
|
|
@@ -50,7 +51,7 @@ export async function replaceSymbol(input: ReplaceSymbolInput): Promise<ReplaceS
|
|
|
50
51
|
const lines = input.content.split("\n");
|
|
51
52
|
const sigLine = lines[sym.startLine - 1] ?? "";
|
|
52
53
|
const indent = detectIndent(sigLine);
|
|
53
|
-
const reindented = reindent(dedent(input.newBody), indent);
|
|
54
|
+
const reindented = reindent(dedent(normalizeToLF(input.newBody)), indent);
|
|
54
55
|
const warnings: string[] = [];
|
|
55
56
|
const leaf = input.symbol.replace(/@\d+$/, "").split(".").pop() ?? "";
|
|
56
57
|
const declNameRe =
|
package/src/sg-output.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ReadSeekLine, ReadSeekRange } from "./readseek-value.js";
|
|
2
2
|
|
|
3
3
|
export interface SgOutputFile {
|
|
4
4
|
displayPath: string;
|
|
5
5
|
path: string;
|
|
6
|
-
ranges:
|
|
7
|
-
lines:
|
|
6
|
+
ranges: ReadSeekRange[];
|
|
7
|
+
lines: ReadSeekLine[];
|
|
8
8
|
symbols?: Array<{ name: string; kind?: string }>;
|
|
9
9
|
}
|
|
10
10
|
|
|
@@ -19,8 +19,8 @@ export interface SgOutputResult {
|
|
|
19
19
|
tool: "search";
|
|
20
20
|
files: Array<{
|
|
21
21
|
path: string;
|
|
22
|
-
ranges:
|
|
23
|
-
lines:
|
|
22
|
+
ranges: ReadSeekRange[];
|
|
23
|
+
lines: ReadSeekLine[];
|
|
24
24
|
}>;
|
|
25
25
|
};
|
|
26
26
|
}
|
package/src/sg.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { stat as fsStat } from "node:fs/promises";
|
|
3
2
|
|
|
4
3
|
import type { ExtensionAPI, ToolRenderResultOptions } from "@earendil-works/pi-coding-agent";
|
|
5
4
|
import { Text } from "@earendil-works/pi-tui";
|
|
6
5
|
import { Type } from "@sinclair/typebox";
|
|
7
6
|
|
|
8
7
|
import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
|
|
9
|
-
import {
|
|
8
|
+
import { buildReadSeekLineWithHash, buildToolErrorResult, type ReadSeekLine } from "./readseek-value.js";
|
|
10
9
|
import { resolveToCwd } from "./path-utils.js";
|
|
11
|
-
import {
|
|
10
|
+
import { statSearchPathOrError } from "./stat-search-path.js";
|
|
11
|
+
import { classifyReadSeekFailure, isReadSeekAvailable, readseekSearch, type ReadSeekHashline, type ReadSeekSearchFileOutput } from "./readseek-client.js";
|
|
12
12
|
import { buildSgOutput } from "./sg-output.js";
|
|
13
13
|
import type { FileAnchoredCallback } from "./tool-types.js";
|
|
14
|
-
import {
|
|
14
|
+
import { registerReadSeekTool } from "./register-tool.js";
|
|
15
15
|
|
|
16
16
|
import { clampLineToWidth, renderAnchoredFilesResult, renderToolLabel } from "./tui-render-utils.js";
|
|
17
17
|
|
|
@@ -53,7 +53,7 @@ const SG_PROMPT_METADATA = defineToolPromptMetadata({
|
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
export function isSgAvailable(): boolean {
|
|
56
|
-
return
|
|
56
|
+
return isReadSeekAvailable();
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
interface SgToolOptions {
|
|
@@ -70,19 +70,19 @@ export interface ExecuteSgOptions {
|
|
|
70
70
|
onFileAnchored?: FileAnchoredCallback;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
function readseekLineFromSearch(line:
|
|
74
|
-
return
|
|
73
|
+
function readseekLineFromSearch(line: ReadSeekHashline): ReadSeekLine {
|
|
74
|
+
return buildReadSeekLineWithHash(line.line, line.hash, line.text);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
function linesFromSearchResult(result:
|
|
78
|
-
const lineMap = new Map<number,
|
|
77
|
+
function linesFromSearchResult(result: ReadSeekSearchFileOutput, ranges: SgRange[]): ReadSeekLine[] {
|
|
78
|
+
const lineMap = new Map<number, ReadSeekLine>();
|
|
79
79
|
for (const match of result.matches) {
|
|
80
80
|
for (const line of match.hashlines) {
|
|
81
81
|
lineMap.set(line.line, readseekLineFromSearch(line));
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
const lines:
|
|
85
|
+
const lines: ReadSeekLine[] = [];
|
|
86
86
|
const seen = new Set<number>();
|
|
87
87
|
for (const range of ranges) {
|
|
88
88
|
for (let line = range.startLine; line <= range.endLine; line++) {
|
|
@@ -112,23 +112,10 @@ export async function executeSg(opts: ExecuteSgOptions): Promise<any> {
|
|
|
112
112
|
return buildToolErrorResult("search", "invalid-parameter", message);
|
|
113
113
|
}
|
|
114
114
|
const searchPath = resolveToCwd(p.path ?? ".", cwd);
|
|
115
|
-
let searchPathIsFile = false;
|
|
116
115
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
} catch (err: any) {
|
|
121
|
-
if (err?.code === "ENOENT") {
|
|
122
|
-
const message = `Error: path '${p.path ?? "."}' does not exist`;
|
|
123
|
-
return buildToolErrorResult("search", "path-not-found", message, { path: p.path ?? searchPath });
|
|
124
|
-
}
|
|
125
|
-
if (err?.code === "EACCES" || err?.code === "EPERM") {
|
|
126
|
-
const message = `Error: permission denied for path '${p.path ?? "."}'`;
|
|
127
|
-
return buildToolErrorResult("search", "permission-denied", message, { path: p.path ?? searchPath });
|
|
128
|
-
}
|
|
129
|
-
const message = `Error: could not access path '${p.path ?? "."}': ${err?.message ?? String(err)}`;
|
|
130
|
-
return buildToolErrorResult("search", "fs-error", message, { path: p.path ?? searchPath, details: { fsCode: err?.code, fsMessage: err?.message } });
|
|
131
|
-
}
|
|
116
|
+
const statResult = await statSearchPathOrError("search", p.path, searchPath);
|
|
117
|
+
if (!statResult.ok) return statResult.error;
|
|
118
|
+
const searchPathIsFile = statResult.stats.isFile();
|
|
132
119
|
|
|
133
120
|
try {
|
|
134
121
|
const effectiveLang = readseekLanguageForPath(p.lang, searchPath, searchPathIsFile);
|
|
@@ -153,7 +140,7 @@ export async function executeSg(opts: ExecuteSgOptions): Promise<any> {
|
|
|
153
140
|
displayPath: string;
|
|
154
141
|
path: string;
|
|
155
142
|
ranges: SgRange[];
|
|
156
|
-
lines:
|
|
143
|
+
lines: ReadSeekLine[];
|
|
157
144
|
symbols?: SgEnclosingSymbol[];
|
|
158
145
|
}> = [];
|
|
159
146
|
|
|
@@ -196,13 +183,13 @@ export async function executeSg(opts: ExecuteSgOptions): Promise<any> {
|
|
|
196
183
|
},
|
|
197
184
|
};
|
|
198
185
|
} catch (err: any) {
|
|
199
|
-
const failure =
|
|
186
|
+
const failure = classifyReadSeekFailure(err);
|
|
200
187
|
return buildToolErrorResult("search", failure.code, failure.message, failure.hint ? { hint: failure.hint } : {});
|
|
201
188
|
}
|
|
202
189
|
}
|
|
203
190
|
|
|
204
191
|
export function registerSgTool(pi: ExtensionAPI, options: SgToolOptions = {}) {
|
|
205
|
-
const tool =
|
|
192
|
+
const tool = registerReadSeekTool(pi, {
|
|
206
193
|
policy: "read-only",
|
|
207
194
|
pythonName: "search",
|
|
208
195
|
defaultExposure: "opt-in",
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { stat as fsStat } from "node:fs/promises";
|
|
2
|
+
import type { Stats } from "node:fs";
|
|
3
|
+
|
|
4
|
+
import { buildToolErrorResult, type ToolErrorResult } from "./readseek-value.js";
|
|
5
|
+
|
|
6
|
+
export type StatSearchPathResult =
|
|
7
|
+
| { ok: true; stats: Stats }
|
|
8
|
+
| { ok: false; error: ToolErrorResult };
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Stat a tool's search path, mapping access failures into the shared readseek
|
|
12
|
+
* error taxonomy. Used by refs and search, which reject a missing or
|
|
13
|
+
* unreadable path with identical codes and messages.
|
|
14
|
+
*/
|
|
15
|
+
export async function statSearchPathOrError(
|
|
16
|
+
tool: string,
|
|
17
|
+
rawPath: string | undefined,
|
|
18
|
+
searchPath: string,
|
|
19
|
+
): Promise<StatSearchPathResult> {
|
|
20
|
+
try {
|
|
21
|
+
return { ok: true, stats: await fsStat(searchPath) };
|
|
22
|
+
} catch (err: any) {
|
|
23
|
+
const display = rawPath ?? ".";
|
|
24
|
+
const path = rawPath ?? searchPath;
|
|
25
|
+
if (err?.code === "ENOENT") {
|
|
26
|
+
return { ok: false, error: buildToolErrorResult(tool, "path-not-found", `Error: path '${display}' does not exist`, { path }) };
|
|
27
|
+
}
|
|
28
|
+
if (err?.code === "EACCES" || err?.code === "EPERM") {
|
|
29
|
+
return { ok: false, error: buildToolErrorResult(tool, "permission-denied", `Error: permission denied for path '${display}'`, { path }) };
|
|
30
|
+
}
|
|
31
|
+
const message = `Error: could not access path '${display}': ${err?.message ?? String(err)}`;
|
|
32
|
+
return { ok: false, error: buildToolErrorResult(tool, "fs-error", message, { path, details: { fsCode: err?.code, fsMessage: err?.message } }) };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -41,20 +41,20 @@ const COMPACT_GUIDELINES: Record<string, string[]> = {
|
|
|
41
41
|
],
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
interface ToolPromptMetadata {
|
|
45
45
|
description: string;
|
|
46
46
|
promptSnippet: string;
|
|
47
47
|
promptGuidelines: string[];
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
function loadPrompt(promptUrl: URL): string {
|
|
51
51
|
return readFileSync(promptUrl, "utf-8")
|
|
52
52
|
.replaceAll("{{DEFAULT_MAX_LINES}}", String(DEFAULT_MAX_LINES))
|
|
53
53
|
.replaceAll("{{DEFAULT_MAX_BYTES}}", formatSize(DEFAULT_MAX_BYTES))
|
|
54
54
|
.trim();
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
function firstPromptParagraph(prompt: string): string {
|
|
58
58
|
return prompt.split(/\n\s*\n/, 1)[0]?.trim() ?? prompt;
|
|
59
59
|
}
|
|
60
60
|
|
package/src/write.ts
CHANGED
|
@@ -7,9 +7,9 @@ import { Type } from "@sinclair/typebox";
|
|
|
7
7
|
|
|
8
8
|
import { resolveToCwd } from "./path-utils.js";
|
|
9
9
|
import { ensureHashInit, formatHashlineDisplay } from "./hashline.js";
|
|
10
|
-
import {
|
|
10
|
+
import { buildReadSeekError, buildReadSeekLine, buildReadSeekWarning, buildToolErrorResult, type ReadSeekLine, type ReadSeekWarning } from "./readseek-value.js";
|
|
11
11
|
import { looksLikeBinary } from "./binary-detect.js";
|
|
12
|
-
import { getOrGenerateMap } from "./map
|
|
12
|
+
import { getOrGenerateMap } from "./file-map.js";
|
|
13
13
|
import { formatFileMapWithBudget } from "./readseek/formatter.js";
|
|
14
14
|
|
|
15
15
|
import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
|
|
@@ -19,7 +19,7 @@ import { buildDiffData, type DiffData } from "./diff-data.js";
|
|
|
19
19
|
import { clampLineToWidth, clampLinesToWidth, isRendererExpanded, linkToolPath, renderErrorResult, renderToolLabel, summaryLine } from "./tui-render-utils.js";
|
|
20
20
|
import { DiffPreviewComponent } from "./tui-diff-component.js";
|
|
21
21
|
import type { FileAnchoredCallback } from "./tool-types.js";
|
|
22
|
-
import {
|
|
22
|
+
import { registerReadSeekTool } from "./register-tool.js";
|
|
23
23
|
|
|
24
24
|
const WRITE_PENDING_PREVIEW_STATE_KEY = "hashline-write-pending-preview";
|
|
25
25
|
|
|
@@ -84,8 +84,8 @@ export interface WriteResult extends WriteDiffFields {
|
|
|
84
84
|
readseekValue: {
|
|
85
85
|
tool: "write";
|
|
86
86
|
path: string;
|
|
87
|
-
lines:
|
|
88
|
-
warnings:
|
|
87
|
+
lines: ReadSeekLine[];
|
|
88
|
+
warnings: ReadSeekWarning[];
|
|
89
89
|
diff?: string;
|
|
90
90
|
diffData?: DiffData;
|
|
91
91
|
map?: { appended: boolean };
|
|
@@ -173,26 +173,11 @@ function mapFsWriteError(err: any, path: string): MappedFsError {
|
|
|
173
173
|
|
|
174
174
|
function buildWriteFsErrorResult(err: any, absolutePath: string) {
|
|
175
175
|
const mapped = mapFsWriteError(err, absolutePath);
|
|
176
|
-
return {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
details: {
|
|
180
|
-
|
|
181
|
-
tool: "write" as const,
|
|
182
|
-
path: absolutePath,
|
|
183
|
-
lines: [] as ReadseekLine[],
|
|
184
|
-
warnings: [] as ReadseekWarning[],
|
|
185
|
-
ok: false,
|
|
186
|
-
error: buildReadseekError(
|
|
187
|
-
mapped.code,
|
|
188
|
-
mapped.message,
|
|
189
|
-
undefined,
|
|
190
|
-
mapped.includeMeta ? { fsCode: err?.code, fsMessage: err?.message } : undefined,
|
|
191
|
-
),
|
|
192
|
-
},
|
|
193
|
-
warnings: [] as string[],
|
|
194
|
-
},
|
|
195
|
-
};
|
|
176
|
+
return buildToolErrorResult("write", mapped.code, mapped.message, {
|
|
177
|
+
path: absolutePath,
|
|
178
|
+
extra: { lines: [], warnings: [] },
|
|
179
|
+
details: mapped.includeMeta ? { fsCode: err?.code, fsMessage: err?.message } : undefined,
|
|
180
|
+
});
|
|
196
181
|
}
|
|
197
182
|
|
|
198
183
|
export async function executeWrite(opts: {
|
|
@@ -205,12 +190,12 @@ export async function executeWrite(opts: {
|
|
|
205
190
|
|
|
206
191
|
const { path: filePath, content, map: requestMap, cwd } = opts;
|
|
207
192
|
const warnings: string[] = [];
|
|
208
|
-
const readseekWarnings:
|
|
193
|
+
const readseekWarnings: ReadSeekWarning[] = [];
|
|
209
194
|
|
|
210
195
|
if (hasBareCarriageReturn(content)) {
|
|
211
196
|
const message = "File content contains bare CR (\\r) line endings; write refuses to emit anchors that read/edit would normalize differently.";
|
|
212
197
|
warnings.push(message);
|
|
213
|
-
readseekWarnings.push(
|
|
198
|
+
readseekWarnings.push(buildReadSeekWarning("bare-cr", message));
|
|
214
199
|
return {
|
|
215
200
|
text: `Cannot write ${filePath}\n⚠️ ${message}`,
|
|
216
201
|
warnings,
|
|
@@ -222,6 +207,21 @@ export async function executeWrite(opts: {
|
|
|
222
207
|
},
|
|
223
208
|
};
|
|
224
209
|
}
|
|
210
|
+
if (looksLikeBinary(Buffer.from(content, "utf-8"))) {
|
|
211
|
+
warnings.push("File content appears to be binary.");
|
|
212
|
+
readseekWarnings.push(buildReadSeekWarning("binary-content", "File content appears to be binary."));
|
|
213
|
+
return {
|
|
214
|
+
text: `Cannot write ${filePath}\n⚠️ File content appears to be binary — refusing to write.`,
|
|
215
|
+
warnings,
|
|
216
|
+
readseekValue: {
|
|
217
|
+
tool: "write",
|
|
218
|
+
path: filePath,
|
|
219
|
+
lines: [],
|
|
220
|
+
warnings: readseekWarnings,
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
225
|
const previousContent = await readPreviousTextForDiff(filePath);
|
|
226
226
|
const existedBeforeWrite = await access(filePath).then(() => true, () => false);
|
|
227
227
|
|
|
@@ -240,30 +240,14 @@ export async function executeWrite(opts: {
|
|
|
240
240
|
throw err;
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
-
// Binary detection
|
|
244
|
-
if (looksLikeBinary(Buffer.from(content, "utf-8"))) {
|
|
245
|
-
warnings.push("File content appears to be binary.");
|
|
246
|
-
readseekWarnings.push(buildReadseekWarning("binary-content", "File content appears to be binary."));
|
|
247
|
-
return {
|
|
248
|
-
text: `Wrote ${filePath}\n⚠️ File content appears to be binary — hashlines not generated.`,
|
|
249
|
-
warnings,
|
|
250
|
-
readseekValue: {
|
|
251
|
-
tool: "write",
|
|
252
|
-
path: filePath,
|
|
253
|
-
lines: [],
|
|
254
|
-
warnings: readseekWarnings,
|
|
255
|
-
},
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
|
|
259
243
|
// Compute hashlines
|
|
260
244
|
const rawLines = content.split("\n");
|
|
261
|
-
const readseekLines:
|
|
245
|
+
const readseekLines: ReadSeekLine[] = [];
|
|
262
246
|
const displayLines: string[] = [];
|
|
263
247
|
|
|
264
248
|
for (let i = 0; i < rawLines.length; i++) {
|
|
265
249
|
const lineNum = i + 1;
|
|
266
|
-
const readseekLine =
|
|
250
|
+
const readseekLine = buildReadSeekLine(lineNum, rawLines[i]);
|
|
267
251
|
readseekLines.push(readseekLine);
|
|
268
252
|
displayLines.push(formatHashlineDisplay(lineNum, rawLines[i]));
|
|
269
253
|
}
|
|
@@ -326,7 +310,7 @@ export async function executeWrite(opts: {
|
|
|
326
310
|
}
|
|
327
311
|
|
|
328
312
|
export function registerWriteTool(pi: ExtensionAPI, options: WriteToolOptions = {}) {
|
|
329
|
-
const tool =
|
|
313
|
+
const tool = registerReadSeekTool(pi, {
|
|
330
314
|
policy: "mutating",
|
|
331
315
|
pythonName: "write",
|
|
332
316
|
defaultExposure: "not-safe-by-default",
|
|
@@ -364,7 +348,7 @@ export function registerWriteTool(pi: ExtensionAPI, options: WriteToolOptions =
|
|
|
364
348
|
|
|
365
349
|
// Lift binary-content signal into a fatal readseekValue.error envelope so
|
|
366
350
|
// downstream consumers get the same taxonomy shape as every other tool.
|
|
367
|
-
// The existing
|
|
351
|
+
// The existing ReadSeekWarning entry is preserved on readseekValue.warnings for
|
|
368
352
|
// backward compatibility.
|
|
369
353
|
for (const code of ["binary-content", "bare-cr"] as const) {
|
|
370
354
|
const warning = result.readseekValue.warnings.find((w) => w.code === code);
|
|
@@ -376,7 +360,7 @@ export function registerWriteTool(pi: ExtensionAPI, options: WriteToolOptions =
|
|
|
376
360
|
readseekValue: {
|
|
377
361
|
...result.readseekValue,
|
|
378
362
|
ok: false,
|
|
379
|
-
error:
|
|
363
|
+
error: buildReadSeekError(code, warning.message),
|
|
380
364
|
},
|
|
381
365
|
warnings: result.warnings,
|
|
382
366
|
},
|
package/src/map-cache.ts
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { stat } from "node:fs/promises";
|
|
2
|
-
import type { FileMap } from "./readseek/types.js";
|
|
3
|
-
import { readseekMap } from "./readseek-client.js";
|
|
4
|
-
|
|
5
|
-
interface CacheEntry {
|
|
6
|
-
mtimeMs: number;
|
|
7
|
-
map: FileMap | null;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export const MAP_CACHE_MAX_SIZE = 500;
|
|
11
|
-
|
|
12
|
-
interface MapCacheGlobalState {
|
|
13
|
-
cache: Map<string, CacheEntry>;
|
|
14
|
-
inflight: Map<string, Promise<FileMap | null>>;
|
|
15
|
-
maxSize: number;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const MAP_CACHE_STATE_KEY = Symbol.for("pi-readseek.mapCacheState.v1");
|
|
19
|
-
|
|
20
|
-
type MapCacheGlobalSlots = typeof globalThis & Record<symbol, MapCacheGlobalState | undefined>;
|
|
21
|
-
|
|
22
|
-
function getMapCacheState(): MapCacheGlobalState {
|
|
23
|
-
const globalObject = globalThis as MapCacheGlobalSlots;
|
|
24
|
-
const state = globalObject[MAP_CACHE_STATE_KEY] ?? {
|
|
25
|
-
cache: new Map<string, CacheEntry>(),
|
|
26
|
-
inflight: new Map<string, Promise<FileMap | null>>(),
|
|
27
|
-
maxSize: MAP_CACHE_MAX_SIZE,
|
|
28
|
-
};
|
|
29
|
-
globalObject[MAP_CACHE_STATE_KEY] = state;
|
|
30
|
-
return state;
|
|
31
|
-
}
|
|
32
|
-
// Move an existing entry to the most-recently-used position (Map insertion-order tail).
|
|
33
|
-
function touchMapEntry<K, V>(map: Map<K, V>, key: K, value: V): void {
|
|
34
|
-
map.delete(key);
|
|
35
|
-
map.set(key, value);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function rememberInMemory(absPath: string, entry: CacheEntry): void {
|
|
39
|
-
const state = getMapCacheState();
|
|
40
|
-
touchMapEntry(state.cache, absPath, entry);
|
|
41
|
-
if (state.cache.size > state.maxSize) {
|
|
42
|
-
const oldestKey = state.cache.keys().next().value;
|
|
43
|
-
if (oldestKey !== undefined) state.cache.delete(oldestKey);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Get or generate a structural file map, with mtime-based in-memory caching.
|
|
49
|
-
* Readseek already caches map results in `.readseek/maps/` with its own binary
|
|
50
|
-
* format. The in-memory layer here avoids redundant process spawns within a
|
|
51
|
-
* single session.
|
|
52
|
-
*
|
|
53
|
-
* Returns null on any failure — never throws.
|
|
54
|
-
*/
|
|
55
|
-
export async function getOrGenerateMap(absPath: string): Promise<FileMap | null> {
|
|
56
|
-
try {
|
|
57
|
-
const fileStat = await stat(absPath);
|
|
58
|
-
const { mtimeMs } = fileStat;
|
|
59
|
-
const state = getMapCacheState();
|
|
60
|
-
const cached = state.cache.get(absPath);
|
|
61
|
-
if (cached && cached.mtimeMs === mtimeMs) {
|
|
62
|
-
touchMapEntry(state.cache, absPath, cached);
|
|
63
|
-
return cached.map;
|
|
64
|
-
}
|
|
65
|
-
const inflight = state.inflight.get(absPath);
|
|
66
|
-
if (inflight) return inflight;
|
|
67
|
-
|
|
68
|
-
const generation = (async () => {
|
|
69
|
-
const map = await readseekMap(absPath, fileStat.size);
|
|
70
|
-
rememberInMemory(absPath, { mtimeMs, map });
|
|
71
|
-
return map;
|
|
72
|
-
})()
|
|
73
|
-
.catch(() => null)
|
|
74
|
-
.finally(() => {
|
|
75
|
-
state.inflight.delete(absPath);
|
|
76
|
-
});
|
|
77
|
-
state.inflight.set(absPath, generation);
|
|
78
|
-
return generation;
|
|
79
|
-
} catch {
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
}
|