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.
@@ -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 ReadseekLine {
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 ReadseekWarningSymbol {
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 ReadseekWarning {
19
+ export interface ReadSeekWarning {
20
20
  code: string;
21
21
  message: string;
22
22
  tier?: "camelCase" | "substring";
23
- symbol?: ReadseekWarningSymbol;
24
- otherCandidates?: ReadseekWarningSymbol[];
23
+ symbol?: ReadSeekWarningSymbol;
24
+ otherCandidates?: ReadSeekWarningSymbol[];
25
25
  }
26
26
 
27
- export interface ReadseekError {
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 ReadseekRange {
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 ReadseekEditResult {
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 ReadseekLine} from an already-known hash. Use this when the
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 buildReadseekLine} delegates here after hashing.
61
+ * {@link buildReadSeekLine} delegates here after hashing.
62
62
  */
63
- export function buildReadseekLineWithHash(line: number, hash: string, raw: string): ReadseekLine {
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 buildReadseekLine(line: number, raw: string): ReadseekLine {
74
- return buildReadseekLineWithHash(line, computeLineHash(raw), raw);
73
+ export function buildReadSeekLine(line: number, raw: string): ReadSeekLine {
74
+ return buildReadSeekLineWithHash(line, computeLineHash(raw), raw);
75
75
  }
76
76
 
77
- export function buildReadseekLines(startLine: number, rawLines: string[]): ReadseekLine[] {
78
- return rawLines.map((raw, index) => buildReadseekLine(startLine + index, raw));
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 renderReadseekLine(line: ReadseekLine): string {
81
+ function renderReadSeekLine(line: ReadSeekLine): string {
82
82
  return `${line.anchor}|${line.display}`;
83
83
  }
84
84
 
85
- export function renderReadseekLines(lines: ReadseekLine[]): string {
86
- return lines.map(renderReadseekLine).join("\n");
85
+ export function renderReadSeekLines(lines: ReadSeekLine[]): string {
86
+ return lines.map(renderReadSeekLine).join("\n");
87
87
  }
88
88
 
89
- export function buildReadseekWarning(
89
+ export function buildReadSeekWarning(
90
90
  code: string,
91
91
  message: string,
92
- metadata: Omit<ReadseekWarning, "code" | "message"> = {},
93
- ): ReadseekWarning {
92
+ metadata: Omit<ReadSeekWarning, "code" | "message"> = {},
93
+ ): ReadSeekWarning {
94
94
  return { code, message, ...metadata };
95
95
  }
96
96
 
97
- export function buildReadseekError(
97
+ export function buildReadSeekError(
98
98
  code: string,
99
99
  message: string,
100
100
  hint?: string,
101
101
  details?: unknown,
102
- ): ReadseekError {
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 buildReadseekError} payload.
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: buildReadseekError(code, message, opts.hint, opts.details),
141
+ error: buildReadSeekError(code, message, opts.hint, opts.details),
142
142
  },
143
143
  },
144
144
  };
145
145
  }
146
146
 
147
- export function buildReadseekEditResult(input: {
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
- }): ReadseekEditResult {
157
+ }): ReadSeekEditResult {
158
158
  return {
159
159
  tool: "edit",
160
160
  ok: input.ok ?? true,
@@ -1,6 +1,6 @@
1
- import type { ReadseekLine } from "./readseek-value.js";
1
+ import type { ReadSeekLine } from "./readseek-value.js";
2
2
 
3
- export interface RefsOutputLine extends ReadseekLine {
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
- export interface BuildRefsOutputInput {
13
+ interface BuildRefsOutputInput {
14
14
  name: string;
15
15
  files: RefsOutputFile[];
16
16
  }
17
17
 
18
- export interface RefsOutputResult {
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 { buildReadseekLineWithHash, buildToolErrorResult } from "./readseek-value.js";
7
+ import { buildReadSeekLineWithHash, buildToolErrorResult } from "./readseek-value.js";
8
8
  import { resolveToCwd } from "./path-utils.js";
9
- import { classifyReadseekFailure, readseekRefs, type ReadseekReference } from "./readseek-client.js";
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 { registerReadseekTool } from "./register-tool.js";
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: ReadseekReference): RefsOutputLine {
47
+ function refsLine(reference: ReadSeekReference): RefsOutputLine {
48
48
  return {
49
- ...buildReadseekLineWithHash(reference.line, reference.line_hash, reference.text),
49
+ ...buildReadSeekLineWithHash(reference.line, reference.line_hash, reference.text),
50
50
  enclosingSymbol: reference.enclosingSymbol,
51
51
  };
52
52
  }
53
53
 
54
- function groupReferences(references: ReadseekReference[], cwd: string): RefsOutputFile[] {
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 isReadseekCursorValidationFailure(message: string): boolean {
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 = classifyReadseekFailure(err);
119
- if (p.scope && isReadseekCursorValidationFailure(failure.message)) {
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 = registerReadseekTool(pi, {
127
+ const tool = registerReadSeekTool(pi, {
128
128
  policy: "read-only",
129
129
  pythonName: "refs",
130
130
  defaultExposure: "opt-in",
@@ -1,20 +1,20 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
 
3
- export type ReadseekToolPolicy = "read-only" | "mutating";
3
+ export type ReadSeekToolPolicy = "read-only" | "mutating";
4
4
 
5
- export type ReadseekToolExposure = "safe-by-default" | "opt-in" | "not-safe-by-default";
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 ReadseekToolConfig {
12
- policy: ReadseekToolPolicy;
11
+ export interface ReadSeekToolConfig {
12
+ policy: ReadSeekToolPolicy;
13
13
  pythonName: string;
14
- defaultExposure: ReadseekToolExposure;
14
+ defaultExposure: ReadSeekToolExposure;
15
15
  }
16
16
 
17
- export interface ReadseekToolPtc extends ReadseekToolConfig {
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 registerReadseekTool<T extends ToolSpec>(
31
+ export function registerReadSeekTool<T extends ToolSpec>(
32
32
  pi: ExtensionAPI,
33
- config: ReadseekToolConfig,
33
+ config: ReadSeekToolConfig,
34
34
  tool: T,
35
- ): T & { ptc: ReadseekToolPtc } {
36
- const ptc: ReadseekToolPtc = {
35
+ ): T & { ptc: ReadSeekToolPtc } {
36
+ const ptc: ReadSeekToolPtc = {
37
37
  callable: true,
38
38
  enabled: true,
39
39
  policy: config.policy,
@@ -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
- export interface ReplaceSymbolInput {
6
+ interface ReplaceSymbolInput {
6
7
  filePath: string;
7
8
  content: string;
8
9
  symbol: string;
9
10
  newBody: string;
10
11
  }
11
12
 
12
- export type ReplaceSymbolResult =
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 { ReadseekLine, ReadseekRange } from "./readseek-value.js";
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: ReadseekRange[];
7
- lines: ReadseekLine[];
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: ReadseekRange[];
23
- lines: ReadseekLine[];
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 { buildReadseekLineWithHash, buildToolErrorResult, type ReadseekLine } from "./readseek-value.js";
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 { classifyReadseekFailure, isReadseekAvailable, readseekSearch, type ReadseekHashline, type ReadseekSearchFileOutput } from "./readseek-client.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 { registerReadseekTool } from "./register-tool.js";
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 isReadseekAvailable();
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: ReadseekHashline): ReadseekLine {
74
- return buildReadseekLineWithHash(line.line, line.hash, line.text);
73
+ function readseekLineFromSearch(line: ReadSeekHashline): ReadSeekLine {
74
+ return buildReadSeekLineWithHash(line.line, line.hash, line.text);
75
75
  }
76
76
 
77
- function linesFromSearchResult(result: ReadseekSearchFileOutput, ranges: SgRange[]): ReadseekLine[] {
78
- const lineMap = new Map<number, ReadseekLine>();
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: ReadseekLine[] = [];
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: ReadseekLine[];
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 = classifyReadseekFailure(err);
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 = registerReadseekTool(pi, {
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 attachments.",
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 attachments, so avoid OCR tools unless explicitly needed.",
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
- export interface ToolPromptMetadata {
44
+ interface ToolPromptMetadata {
45
45
  description: string;
46
46
  promptSnippet: string;
47
47
  promptGuidelines: string[];
48
48
  }
49
49
 
50
- export function loadPrompt(promptUrl: URL): string {
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
- export function firstPromptParagraph(prompt: string): string {
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 { buildReadseekError, buildReadseekLine, buildReadseekWarning, type ReadseekLine, type ReadseekWarning } from "./readseek-value.js";
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-cache.js";
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 { registerReadseekTool } from "./register-tool.js";
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: ReadseekLine[];
88
- warnings: ReadseekWarning[];
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
- content: [{ type: "text" as const, text: mapped.message }],
178
- isError: true,
179
- details: {
180
- readseekValue: {
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: ReadseekWarning[] = [];
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(buildReadseekWarning("bare-cr", message));
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(buildReadseekWarning("binary-content", "File content appears to be binary."));
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: ReadseekLine[] = [];
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 = buildReadseekLine(lineNum, rawLines[i]);
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 = registerReadseekTool(pi, {
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 ReadseekWarning entry is preserved on readseekValue.warnings for
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: buildReadseekError(code, warning.message),
363
+ error: buildReadSeekError(code, warning.message),
379
364
  },
380
365
  warnings: result.warnings,
381
366
  },
@@ -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
- }