pi-readseek 0.4.12 → 0.4.14
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 +38 -6
- 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 +43 -31
- 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 +165 -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 +10 -10
- 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 +12 -12
- package/src/tool-prompt-metadata.ts +5 -5
- package/src/write.ts +18 -33
- package/src/image-detect.ts +0 -47
- package/src/map-cache.ts +0 -82
package/src/readseek-value.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { computeLineHash, escapeControlCharsForDisplay } from "./hashline.js";
|
|
2
2
|
import type { DiffData } from "./diff-data.js";
|
|
3
3
|
|
|
4
|
-
export interface
|
|
4
|
+
export interface ReadSeekLine {
|
|
5
5
|
line: number;
|
|
6
6
|
hash: string;
|
|
7
7
|
anchor: string;
|
|
@@ -9,29 +9,29 @@ export interface ReadseekLine {
|
|
|
9
9
|
display: string;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export interface
|
|
12
|
+
export interface ReadSeekWarningSymbol {
|
|
13
13
|
name: string;
|
|
14
14
|
kind: string;
|
|
15
15
|
startLine: number;
|
|
16
16
|
endLine: number;
|
|
17
17
|
parentName?: string;
|
|
18
18
|
}
|
|
19
|
-
export interface
|
|
19
|
+
export interface ReadSeekWarning {
|
|
20
20
|
code: string;
|
|
21
21
|
message: string;
|
|
22
22
|
tier?: "camelCase" | "substring";
|
|
23
|
-
symbol?:
|
|
24
|
-
otherCandidates?:
|
|
23
|
+
symbol?: ReadSeekWarningSymbol;
|
|
24
|
+
otherCandidates?: ReadSeekWarningSymbol[];
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
export interface
|
|
27
|
+
export interface ReadSeekError {
|
|
28
28
|
code: string;
|
|
29
29
|
message: string;
|
|
30
30
|
hint?: string;
|
|
31
31
|
details?: unknown;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
export interface
|
|
34
|
+
export interface ReadSeekRange {
|
|
35
35
|
startLine: number;
|
|
36
36
|
endLine: number;
|
|
37
37
|
totalLines?: number;
|
|
@@ -42,7 +42,7 @@ export interface SemanticSummary {
|
|
|
42
42
|
difftasticAvailable: boolean;
|
|
43
43
|
movedBlocks?: number;
|
|
44
44
|
}
|
|
45
|
-
export interface
|
|
45
|
+
export interface ReadSeekEditResult {
|
|
46
46
|
tool: "edit";
|
|
47
47
|
ok: boolean;
|
|
48
48
|
path: string;
|
|
@@ -56,11 +56,11 @@ export interface ReadseekEditResult {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
|
-
* Build a {@link
|
|
59
|
+
* Build a {@link ReadSeekLine} from an already-known hash. Use this when the
|
|
60
60
|
* hash is supplied by readseek (search, refs) rather than computed from `raw`;
|
|
61
|
-
* {@link
|
|
61
|
+
* {@link buildReadSeekLine} delegates here after hashing.
|
|
62
62
|
*/
|
|
63
|
-
export function
|
|
63
|
+
export function buildReadSeekLineWithHash(line: number, hash: string, raw: string): ReadSeekLine {
|
|
64
64
|
return {
|
|
65
65
|
line,
|
|
66
66
|
hash,
|
|
@@ -70,36 +70,36 @@ export function buildReadseekLineWithHash(line: number, hash: string, raw: strin
|
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
export function
|
|
74
|
-
return
|
|
73
|
+
export function buildReadSeekLine(line: number, raw: string): ReadSeekLine {
|
|
74
|
+
return buildReadSeekLineWithHash(line, computeLineHash(raw), raw);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
export function
|
|
78
|
-
return rawLines.map((raw, index) =>
|
|
77
|
+
export function buildReadSeekLines(startLine: number, rawLines: string[]): ReadSeekLine[] {
|
|
78
|
+
return rawLines.map((raw, index) => buildReadSeekLine(startLine + index, raw));
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
function
|
|
81
|
+
function renderReadSeekLine(line: ReadSeekLine): string {
|
|
82
82
|
return `${line.anchor}|${line.display}`;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
export function
|
|
86
|
-
return lines.map(
|
|
85
|
+
export function renderReadSeekLines(lines: ReadSeekLine[]): string {
|
|
86
|
+
return lines.map(renderReadSeekLine).join("\n");
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
export function
|
|
89
|
+
export function buildReadSeekWarning(
|
|
90
90
|
code: string,
|
|
91
91
|
message: string,
|
|
92
|
-
metadata: Omit<
|
|
93
|
-
):
|
|
92
|
+
metadata: Omit<ReadSeekWarning, "code" | "message"> = {},
|
|
93
|
+
): ReadSeekWarning {
|
|
94
94
|
return { code, message, ...metadata };
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
export function
|
|
97
|
+
export function buildReadSeekError(
|
|
98
98
|
code: string,
|
|
99
99
|
message: string,
|
|
100
100
|
hint?: string,
|
|
101
101
|
details?: unknown,
|
|
102
|
-
):
|
|
102
|
+
): ReadSeekError {
|
|
103
103
|
return {
|
|
104
104
|
code,
|
|
105
105
|
message,
|
|
@@ -117,7 +117,7 @@ export interface ToolErrorResult {
|
|
|
117
117
|
/**
|
|
118
118
|
* Build the standard failure envelope shared by every read-family tool: a text
|
|
119
119
|
* content block plus a `readseekValue` carrying `ok: false` and a
|
|
120
|
-
* {@link
|
|
120
|
+
* {@link buildReadSeekError} payload.
|
|
121
121
|
*
|
|
122
122
|
* `path` is included only when provided, and `extra` is merged into
|
|
123
123
|
* `readseekValue` so callers can attach tool-specific fields (e.g. write's
|
|
@@ -138,13 +138,13 @@ export function buildToolErrorResult(
|
|
|
138
138
|
...(opts.extra ?? {}),
|
|
139
139
|
ok: false,
|
|
140
140
|
...(opts.path !== undefined ? { path: opts.path } : {}),
|
|
141
|
-
error:
|
|
141
|
+
error: buildReadSeekError(code, message, opts.hint, opts.details),
|
|
142
142
|
},
|
|
143
143
|
},
|
|
144
144
|
};
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
export function
|
|
147
|
+
export function buildReadSeekEditResult(input: {
|
|
148
148
|
ok?: boolean;
|
|
149
149
|
path: string;
|
|
150
150
|
summary: string;
|
|
@@ -154,7 +154,7 @@ export function buildReadseekEditResult(input: {
|
|
|
154
154
|
warnings: string[];
|
|
155
155
|
noopEdits: unknown[];
|
|
156
156
|
semanticSummary?: SemanticSummary;
|
|
157
|
-
}):
|
|
157
|
+
}): ReadSeekEditResult {
|
|
158
158
|
return {
|
|
159
159
|
tool: "edit",
|
|
160
160
|
ok: input.ok ?? true,
|
package/src/refs-output.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ReadSeekLine } from "./readseek-value.js";
|
|
2
2
|
|
|
3
|
-
export interface RefsOutputLine extends
|
|
3
|
+
export interface RefsOutputLine extends ReadSeekLine {
|
|
4
4
|
enclosingSymbol?: string;
|
|
5
5
|
}
|
|
6
6
|
|
|
@@ -10,12 +10,12 @@ export interface RefsOutputFile {
|
|
|
10
10
|
lines: RefsOutputLine[];
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
interface BuildRefsOutputInput {
|
|
14
14
|
name: string;
|
|
15
15
|
files: RefsOutputFile[];
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
interface RefsOutputResult {
|
|
19
19
|
text: string;
|
|
20
20
|
readseekValue: {
|
|
21
21
|
tool: "refs";
|
package/src/refs.ts
CHANGED
|
@@ -4,12 +4,12 @@ import { Type } from "@sinclair/typebox";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
|
|
6
6
|
import { statSearchPathOrError } from "./stat-search-path.js";
|
|
7
|
-
import {
|
|
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) ||
|
|
@@ -115,8 +115,8 @@ export async function executeRefs(opts: ExecuteRefsOptions): Promise<any> {
|
|
|
115
115
|
details: { readseekValue: builtOutput.readseekValue },
|
|
116
116
|
};
|
|
117
117
|
} catch (err: any) {
|
|
118
|
-
const failure =
|
|
119
|
-
if (p.scope &&
|
|
118
|
+
const failure = classifyReadSeekFailure(err);
|
|
119
|
+
if (p.scope && isReadSeekCursorValidationFailure(failure.message)) {
|
|
120
120
|
return buildToolErrorResult("refs", "invalid-parameter", failure.message);
|
|
121
121
|
}
|
|
122
122
|
return buildToolErrorResult("refs", failure.code, failure.message, failure.hint ? { hint: failure.hint } : {});
|
|
@@ -124,7 +124,7 @@ export async function executeRefs(opts: ExecuteRefsOptions): Promise<any> {
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
export function registerRefsTool(pi: ExtensionAPI, options: RefsToolOptions = {}) {
|
|
127
|
-
const tool =
|
|
127
|
+
const tool = registerReadSeekTool(pi, {
|
|
128
128
|
policy: "read-only",
|
|
129
129
|
pythonName: "refs",
|
|
130
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
|
@@ -5,13 +5,13 @@ import { Text } from "@earendil-works/pi-tui";
|
|
|
5
5
|
import { Type } from "@sinclair/typebox";
|
|
6
6
|
|
|
7
7
|
import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
|
|
8
|
-
import {
|
|
8
|
+
import { buildReadSeekLineWithHash, buildToolErrorResult, type ReadSeekLine } from "./readseek-value.js";
|
|
9
9
|
import { resolveToCwd } from "./path-utils.js";
|
|
10
10
|
import { statSearchPathOrError } from "./stat-search-path.js";
|
|
11
|
-
import {
|
|
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++) {
|
|
@@ -140,7 +140,7 @@ export async function executeSg(opts: ExecuteSgOptions): Promise<any> {
|
|
|
140
140
|
displayPath: string;
|
|
141
141
|
path: string;
|
|
142
142
|
ranges: SgRange[];
|
|
143
|
-
lines:
|
|
143
|
+
lines: ReadSeekLine[];
|
|
144
144
|
symbols?: SgEnclosingSymbol[];
|
|
145
145
|
}> = [];
|
|
146
146
|
|
|
@@ -183,13 +183,13 @@ export async function executeSg(opts: ExecuteSgOptions): Promise<any> {
|
|
|
183
183
|
},
|
|
184
184
|
};
|
|
185
185
|
} catch (err: any) {
|
|
186
|
-
const failure =
|
|
186
|
+
const failure = classifyReadSeekFailure(err);
|
|
187
187
|
return buildToolErrorResult("search", failure.code, failure.message, failure.hint ? { hint: failure.hint } : {});
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
export function registerSgTool(pi: ExtensionAPI, options: SgToolOptions = {}) {
|
|
192
|
-
const tool =
|
|
192
|
+
const tool = registerReadSeekTool(pi, {
|
|
193
193
|
policy: "read-only",
|
|
194
194
|
pythonName: "search",
|
|
195
195
|
defaultExposure: "opt-in",
|
|
@@ -3,7 +3,7 @@ import { readFileSync } from "node:fs";
|
|
|
3
3
|
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
|
|
5
5
|
const COMPACT_DESCRIPTIONS: Record<string, string> = {
|
|
6
|
-
"read.md": "Read text files/images by path; text has LINE:HASH anchors, images return
|
|
6
|
+
"read.md": "Read text files/images by path; text has LINE:HASH anchors, images return the attachment plus OCR-extracted text.",
|
|
7
7
|
"edit.md": "Edit existing text files using fresh LINE:HASH anchors from read, grep, search, or write.",
|
|
8
8
|
"grep.md": "Search file contents; non-summary results include LINE:HASH anchors for edits.",
|
|
9
9
|
|
|
@@ -16,7 +16,7 @@ const COMPACT_GUIDELINES: Record<string, string[]> = {
|
|
|
16
16
|
"read.md": [
|
|
17
17
|
"Use read for file contents, images/screenshots, ranges, symbols, and edit anchors.",
|
|
18
18
|
"Use map or symbol mode before pulling large code files into context.",
|
|
19
|
-
"Use read for images; it returns
|
|
19
|
+
"Use read for images; it returns the image attachment plus OCR-extracted text, so you don't need separate OCR tools.",
|
|
20
20
|
],
|
|
21
21
|
"edit.md": [
|
|
22
22
|
"Use edit with fresh LINE:HASH anchors for existing files.",
|
|
@@ -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,
|
|
@@ -224,7 +209,7 @@ export async function executeWrite(opts: {
|
|
|
224
209
|
}
|
|
225
210
|
if (looksLikeBinary(Buffer.from(content, "utf-8"))) {
|
|
226
211
|
warnings.push("File content appears to be binary.");
|
|
227
|
-
readseekWarnings.push(
|
|
212
|
+
readseekWarnings.push(buildReadSeekWarning("binary-content", "File content appears to be binary."));
|
|
228
213
|
return {
|
|
229
214
|
text: `Cannot write ${filePath}\n⚠️ File content appears to be binary — refusing to write.`,
|
|
230
215
|
warnings,
|
|
@@ -257,12 +242,12 @@ export async function executeWrite(opts: {
|
|
|
257
242
|
|
|
258
243
|
// Compute hashlines
|
|
259
244
|
const rawLines = content.split("\n");
|
|
260
|
-
const readseekLines:
|
|
245
|
+
const readseekLines: ReadSeekLine[] = [];
|
|
261
246
|
const displayLines: string[] = [];
|
|
262
247
|
|
|
263
248
|
for (let i = 0; i < rawLines.length; i++) {
|
|
264
249
|
const lineNum = i + 1;
|
|
265
|
-
const readseekLine =
|
|
250
|
+
const readseekLine = buildReadSeekLine(lineNum, rawLines[i]);
|
|
266
251
|
readseekLines.push(readseekLine);
|
|
267
252
|
displayLines.push(formatHashlineDisplay(lineNum, rawLines[i]));
|
|
268
253
|
}
|
|
@@ -325,7 +310,7 @@ export async function executeWrite(opts: {
|
|
|
325
310
|
}
|
|
326
311
|
|
|
327
312
|
export function registerWriteTool(pi: ExtensionAPI, options: WriteToolOptions = {}) {
|
|
328
|
-
const tool =
|
|
313
|
+
const tool = registerReadSeekTool(pi, {
|
|
329
314
|
policy: "mutating",
|
|
330
315
|
pythonName: "write",
|
|
331
316
|
defaultExposure: "not-safe-by-default",
|
|
@@ -363,7 +348,7 @@ export function registerWriteTool(pi: ExtensionAPI, options: WriteToolOptions =
|
|
|
363
348
|
|
|
364
349
|
// Lift binary-content signal into a fatal readseekValue.error envelope so
|
|
365
350
|
// downstream consumers get the same taxonomy shape as every other tool.
|
|
366
|
-
// The existing
|
|
351
|
+
// The existing ReadSeekWarning entry is preserved on readseekValue.warnings for
|
|
367
352
|
// backward compatibility.
|
|
368
353
|
for (const code of ["binary-content", "bare-cr"] as const) {
|
|
369
354
|
const warning = result.readseekValue.warnings.find((w) => w.code === code);
|
|
@@ -375,7 +360,7 @@ export function registerWriteTool(pi: ExtensionAPI, options: WriteToolOptions =
|
|
|
375
360
|
readseekValue: {
|
|
376
361
|
...result.readseekValue,
|
|
377
362
|
ok: false,
|
|
378
|
-
error:
|
|
363
|
+
error: buildReadSeekError(code, warning.message),
|
|
379
364
|
},
|
|
380
365
|
warnings: result.warnings,
|
|
381
366
|
},
|
package/src/image-detect.ts
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
|
|
2
|
-
|
|
3
|
-
function startsWithBytes(buffer: Buffer, bytes: number[]): boolean {
|
|
4
|
-
return buffer.length >= bytes.length && bytes.every((byte, index) => buffer[index] === byte);
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
function startsWithAscii(buffer: Buffer, offset: number, text: string): boolean {
|
|
8
|
-
if (buffer.length < offset + text.length) return false;
|
|
9
|
-
for (let index = 0; index < text.length; index++) {
|
|
10
|
-
if (buffer[offset + index] !== text.charCodeAt(index)) return false;
|
|
11
|
-
}
|
|
12
|
-
return true;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function readUint32BE(buffer: Buffer, offset: number): number {
|
|
16
|
-
return (
|
|
17
|
-
((buffer[offset] ?? 0) * 0x1000000) +
|
|
18
|
-
((buffer[offset + 1] ?? 0) << 16) +
|
|
19
|
-
((buffer[offset + 2] ?? 0) << 8) +
|
|
20
|
-
(buffer[offset + 3] ?? 0)
|
|
21
|
-
);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function isPng(buffer: Buffer): boolean {
|
|
25
|
-
return buffer.length >= 16 && readUint32BE(buffer, PNG_SIGNATURE.length) === 13 && startsWithAscii(buffer, 12, "IHDR");
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function isAnimatedPng(buffer: Buffer): boolean {
|
|
29
|
-
let offset = PNG_SIGNATURE.length;
|
|
30
|
-
while (offset + 8 <= buffer.length) {
|
|
31
|
-
const chunkLength = readUint32BE(buffer, offset);
|
|
32
|
-
const chunkTypeOffset = offset + 4;
|
|
33
|
-
if (startsWithAscii(buffer, chunkTypeOffset, "acTL")) return true;
|
|
34
|
-
if (startsWithAscii(buffer, chunkTypeOffset, "IDAT")) return false;
|
|
35
|
-
const nextOffset = offset + 8 + chunkLength + 4;
|
|
36
|
-
if (nextOffset <= offset || nextOffset > buffer.length) return false;
|
|
37
|
-
offset = nextOffset;
|
|
38
|
-
}
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function isSupportedImageBuffer(buffer: Buffer): boolean {
|
|
43
|
-
if (startsWithBytes(buffer, [0xff, 0xd8, 0xff])) return buffer[3] !== 0xf7;
|
|
44
|
-
if (startsWithBytes(buffer, PNG_SIGNATURE)) return isPng(buffer) && !isAnimatedPng(buffer);
|
|
45
|
-
if (startsWithAscii(buffer, 0, "GIF")) return true;
|
|
46
|
-
return startsWithAscii(buffer, 0, "RIFF") && startsWithAscii(buffer, 8, "WEBP");
|
|
47
|
-
}
|