pi-readseek 0.4.29 → 0.5.0

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,25 +1,15 @@
1
+ import { resolveReadSeekSyntaxValidation } from "./readseek-settings.js";
2
+
1
3
  export type SyntaxValidateMode = "warn" | "block" | "off";
2
4
 
3
5
  export interface SyntaxValidateOptions {
4
6
  syntaxValidate?: SyntaxValidateMode;
5
7
  }
6
8
 
7
- const VALID = new Set<SyntaxValidateMode>(["warn", "block", "off"]);
8
9
  const DEFAULT: SyntaxValidateMode = "warn";
9
10
 
10
- function coerce(value: unknown): SyntaxValidateMode | undefined {
11
- if (typeof value !== "string") return undefined;
12
- return VALID.has(value as SyntaxValidateMode)
13
- ? (value as SyntaxValidateMode)
14
- : undefined;
15
- }
16
-
17
11
  export function resolveSyntaxValidateMode(
18
12
  opts: SyntaxValidateOptions,
19
13
  ): SyntaxValidateMode {
20
- const fromOpt = coerce(opts.syntaxValidate);
21
- if (fromOpt) return fromOpt;
22
- const fromEnv = coerce(process.env.READSEEK_SYNTAX_VALIDATE);
23
- if (fromEnv) return fromEnv;
24
- return DEFAULT;
14
+ return opts.syntaxValidate ?? resolveReadSeekSyntaxValidation() ?? DEFAULT;
25
15
  }
@@ -4,7 +4,7 @@ import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize } from "@earendil-work
4
4
 
5
5
  const COMPACT_DESCRIPTIONS: Record<string, string> = {
6
6
  "read.md": "Read text files/images by path; text has LINE:HASH anchors, images return the attachment plus OCR-extracted text.",
7
- "edit.md": "Edit existing text files using fresh LINE:HASH anchors from read, grep, search, or write.",
7
+ "edit.md": "Edit existing text files using fresh LINE:HASH anchors from readSeek_read, readSeek_grep, readSeek_search, or readSeek_write.",
8
8
  "grep.md": "Search file contents; non-summary results include LINE:HASH anchors for edits.",
9
9
 
10
10
  "write.md": "Create or overwrite a complete file and return anchors.",
@@ -14,30 +14,30 @@ const COMPACT_DESCRIPTIONS: Record<string, string> = {
14
14
 
15
15
  const COMPACT_GUIDELINES: Record<string, string[]> = {
16
16
  "read.md": [
17
- "Use read for file contents, images/screenshots, ranges, symbols, and edit anchors.",
17
+ "Use readSeek_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 the image attachment plus OCR-extracted text, so you don't need separate OCR tools.",
19
+ "Use readSeek_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
- "Use edit with fresh LINE:HASH anchors for existing files.",
22
+ "Use readSeek_edit with fresh LINE:HASH anchors for existing files.",
23
23
  "Prefer set_line, replace_lines, and insert_after; use replace only when anchors are impractical.",
24
24
  ],
25
25
  "grep.md": [
26
- "Use grep for text search and edit-ready matching anchors.",
27
- "Use grep summary mode for broad count/file discovery before narrowing.",
26
+ "Use readSeek_grep for text search and edit-ready matching anchors.",
27
+ "Use readSeek_grep summary mode for broad count/file discovery before narrowing.",
28
28
  ],
29
29
 
30
30
  "write.md": [
31
- "Use write to create files or intentionally overwrite whole files.",
32
- "Use edit rather than write for small changes or appends to existing files.",
31
+ "Use readSeek_write to create files or intentionally overwrite whole files.",
32
+ "Use readSeek_edit rather than readSeek_write for small changes or appends to existing files.",
33
33
  ],
34
34
  "sg.md": [
35
- "Use search for AST-shaped code patterns.",
36
- "Use grep instead of search for plain text.",
35
+ "Use readSeek_search for AST-shaped code patterns.",
36
+ "Use readSeek_grep instead of readSeek_search for plain text.",
37
37
  ],
38
38
  "refs.md": [
39
- "Use refs to find every usage of an identifier before renaming or deleting it.",
40
- "Use refs with scope plus line/column to follow a specific binding instead of every same-named identifier.",
39
+ "Use readSeek_refs to find every usage of an identifier before renaming or deleting it.",
40
+ "Use readSeek_refs with scope plus line/column to follow a specific binding instead of every same-named identifier.",
41
41
  ],
42
42
  };
43
43
 
@@ -238,10 +238,10 @@ export function renderAnchoredFilesResult(
238
238
  const textContent = content?.type === "text" ? content.text : "";
239
239
  if (isError || result.isError) return renderErrorResult(textContent, { expanded, width });
240
240
 
241
- const readseekValue = (result.details as any)?.readseekValue as
241
+ const readSeekValue = (result.details as any)?.readSeekValue as
242
242
  | { files: Array<{ path: string; lines: any[] }> }
243
243
  | undefined;
244
- const files = readseekValue?.files ?? [];
244
+ const files = readSeekValue?.files ?? [];
245
245
  if (files.length === 0) return new Text(summaryLine(labels.emptyLabel), 0, 0);
246
246
 
247
247
  const fileCount = files.length;
package/src/write.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  import { access, mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import { dirname, relative } from "node:path";
3
3
 
4
- import { withFileMutationQueue, type ExtensionAPI, type ToolRenderResultOptions } from "@earendil-works/pi-coding-agent";
4
+ import { withFileMutationQueue, truncateHead, type ExtensionAPI, type ToolRenderResultOptions } from "@earendil-works/pi-coding-agent";
5
5
  import { Text } from "@earendil-works/pi-tui";
6
6
  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, buildToolErrorResult, type ReadSeekLine, type ReadSeekWarning } from "./readseek-value.js";
10
+ import { buildReadSeekLine, buildReadSeekWarning, buildToolErrorResult, type ReadSeekLine, type ReadSeekWarning, type ToolErrorResult } from "./readseek-value.js";
11
11
  import { looksLikeBinary } from "./binary-detect.js";
12
12
  import { formatFsError } from "./fs-error.js";
13
13
  import { getOrGenerateMap } from "./file-map.js";
@@ -66,8 +66,6 @@ function pendingWritePreviewParts(summary: string, preview: PendingDiffPreviewRe
66
66
  return { lines: [summary, headerLine], diffData: expanded ? diffData : undefined };
67
67
  }
68
68
 
69
- const MAX_LINES = 2000;
70
- const MAX_BYTES = 50 * 1024;
71
69
  const WRITE_PROMPT_METADATA = defineToolPromptMetadata({
72
70
  promptUrl: new URL("../prompts/write.md", import.meta.url),
73
71
  promptSnippet: "Create or overwrite a complete file and return edit anchors",
@@ -82,7 +80,7 @@ export interface WriteResult extends WriteDiffFields {
82
80
  text: string;
83
81
  warnings: string[];
84
82
  writeState?: "created" | "overwritten";
85
- readseekValue: {
83
+ readSeekValue: {
86
84
  tool: "write";
87
85
  path: string;
88
86
  lines: ReadSeekLine[];
@@ -93,6 +91,13 @@ export interface WriteResult extends WriteDiffFields {
93
91
  };
94
92
  }
95
93
 
94
+
95
+ type ExecuteWriteResult = WriteResult | ToolErrorResult;
96
+
97
+ function isToolErrorResult(result: ExecuteWriteResult): result is ToolErrorResult {
98
+ return "isError" in result;
99
+ }
100
+
96
101
  async function readPreviousTextForDiff(filePath: string): Promise<string> {
97
102
  try {
98
103
  const previous = await readFile(filePath);
@@ -133,41 +138,30 @@ export async function executeWrite(opts: {
133
138
  content: string;
134
139
  map?: boolean;
135
140
  cwd?: string;
136
- }): Promise<WriteResult> {
141
+ }): Promise<ExecuteWriteResult> {
137
142
  await ensureHashInit();
138
143
 
139
144
  const { path: filePath, content, map: requestMap, cwd } = opts;
140
145
  const warnings: string[] = [];
141
- const readseekWarnings: ReadSeekWarning[] = [];
146
+ const readSeekWarnings: ReadSeekWarning[] = [];
142
147
 
143
148
  if (hasBareCarriageReturn(content)) {
144
- const message = "File content contains bare CR (\\r) line endings; write refuses to emit anchors that read/edit would normalize differently.";
149
+ const message = "File content contains bare CR (\\r) line endings; readSeek_write refuses to emit anchors that readSeek_read/readSeek_edit would normalize differently.";
145
150
  warnings.push(message);
146
- readseekWarnings.push(buildReadSeekWarning("bare-cr", message));
147
- return {
148
- text: `Cannot write ${filePath}\n⚠️ ${message}`,
149
- warnings,
150
- readseekValue: {
151
- tool: "write",
152
- path: filePath,
153
- lines: [],
154
- warnings: readseekWarnings,
155
- },
156
- };
151
+ readSeekWarnings.push(buildReadSeekWarning("bare-cr", message));
152
+ return buildToolErrorResult("write", "bare-cr", `Cannot write ${filePath}\n⚠️ ${message}`, {
153
+ path: filePath,
154
+ extra: { lines: [], warnings: readSeekWarnings },
155
+ });
157
156
  }
158
157
  if (looksLikeBinary(Buffer.from(content, "utf-8"))) {
159
- warnings.push("File content appears to be binary.");
160
- readseekWarnings.push(buildReadSeekWarning("binary-content", "File content appears to be binary."));
161
- return {
162
- text: `Cannot write ${filePath}\n⚠️ File content appears to be binary — refusing to write.`,
163
- warnings,
164
- readseekValue: {
165
- tool: "write",
166
- path: filePath,
167
- lines: [],
168
- warnings: readseekWarnings,
169
- },
170
- };
158
+ const message = "File content appears to be binary.";
159
+ warnings.push(message);
160
+ readSeekWarnings.push(buildReadSeekWarning("binary-content", message));
161
+ return buildToolErrorResult("write", "binary-content", `Cannot write ${filePath}\n⚠️ ${message} — refusing to write.`, {
162
+ path: filePath,
163
+ extra: { lines: [], warnings: readSeekWarnings },
164
+ });
171
165
  }
172
166
 
173
167
  const previousContent = await readPreviousTextForDiff(filePath);
@@ -179,25 +173,25 @@ export async function executeWrite(opts: {
179
173
  await writeFile(filePath, content, "utf-8");
180
174
  // Compute hashlines
181
175
  const rawLines = content.split("\n");
182
- const readseekLines: ReadSeekLine[] = [];
176
+ const readSeekLines: ReadSeekLine[] = [];
183
177
  const displayLines: string[] = [];
184
178
 
185
179
  for (let i = 0; i < rawLines.length; i++) {
186
180
  const lineNum = i + 1;
187
- const readseekLine = buildReadSeekLine(lineNum, rawLines[i]);
188
- readseekLines.push(readseekLine);
181
+ const readSeekLine = buildReadSeekLine(lineNum, rawLines[i]);
182
+ readSeekLines.push(readSeekLine);
189
183
  displayLines.push(formatHashlineDisplay(lineNum, rawLines[i]));
190
184
  }
191
185
 
192
- let text = displayLines.join("\n");
193
- if (rawLines.length > MAX_LINES) {
194
- text = displayLines.slice(0, MAX_LINES).join("\n");
195
- text += `\n[… ${rawLines.length - MAX_LINES} more lines not shown — full anchors in readseekValue]`;
196
- }
197
- const bytes = Buffer.byteLength(text, "utf8");
198
- if (bytes > MAX_BYTES) {
199
- text = Buffer.from(text, "utf8").subarray(0, MAX_BYTES).toString("utf8");
200
- text += "\n[… output truncated at 50 KB — full anchors in readseekValue]";
186
+ const fullText = displayLines.join("\n");
187
+ const truncated = truncateHead(fullText);
188
+ let text = truncated.content;
189
+ if (truncated.truncated) {
190
+ if (truncated.truncatedBy === "lines") {
191
+ text += `\n[… ${truncated.totalLines - truncated.outputLines} more lines not shown — full anchors in readSeekValue]`;
192
+ } else {
193
+ text += "\n[… output truncated at 50 KB — full anchors in readSeekValue]";
194
+ }
201
195
  }
202
196
 
203
197
  // Optional structural map
@@ -234,11 +228,11 @@ export async function executeWrite(opts: {
234
228
  writeState: existedBeforeWrite ? "overwritten" : "created",
235
229
  diff: diffResult.diff,
236
230
  diffData,
237
- readseekValue: {
231
+ readSeekValue: {
238
232
  tool: "write",
239
233
  path: displayPath,
240
- lines: readseekLines,
241
- warnings: readseekWarnings,
234
+ lines: readSeekLines,
235
+ warnings: readSeekWarnings,
242
236
  diff: diffResult.diff,
243
237
  diffData,
244
238
  ...(requestMap ? { map: { appended: mapAppended } } : {}),
@@ -248,11 +242,7 @@ export async function executeWrite(opts: {
248
242
 
249
243
  export function registerWriteTool(pi: ExtensionAPI, options: WriteToolOptions = {}) {
250
244
  const tool = registerReadSeekTool(pi, {
251
- policy: "mutating",
252
- pythonName: "write",
253
- defaultExposure: "not-safe-by-default",
254
- }, {
255
- name: "write",
245
+ name: "readSeek_write",
256
246
  label: "write",
257
247
  description: WRITE_PROMPT_METADATA.description,
258
248
  promptSnippet: WRITE_PROMPT_METADATA.promptSnippet,
@@ -267,7 +257,7 @@ export function registerWriteTool(pi: ExtensionAPI, options: WriteToolOptions =
267
257
  const absolutePath = resolveToCwd(params.path, cwd);
268
258
  try {
269
259
  return await withFileMutationQueue(absolutePath, async () => {
270
- let result: WriteResult;
260
+ let result: ExecuteWriteResult;
271
261
  try {
272
262
  result = await executeWrite({
273
263
  path: absolutePath,
@@ -279,30 +269,10 @@ export function registerWriteTool(pi: ExtensionAPI, options: WriteToolOptions =
279
269
  return buildWriteFsErrorResult(err, absolutePath);
280
270
  }
281
271
 
282
- if (result.readseekValue.lines.length > 0) {
283
- options.onFileAnchored?.(absolutePath);
284
- }
272
+ if (isToolErrorResult(result)) return result;
285
273
 
286
- // Lift binary-content signal into a fatal readseekValue.error envelope so
287
- // downstream consumers get the same taxonomy shape as every other tool.
288
- // The existing ReadSeekWarning entry is preserved on readseekValue.warnings for
289
- // backward compatibility.
290
- for (const code of ["binary-content", "bare-cr"] as const) {
291
- const warning = result.readseekValue.warnings.find((w) => w.code === code);
292
- if (warning) {
293
- return {
294
- content: [{ type: "text" as const, text: result.text }],
295
- isError: true,
296
- details: {
297
- readseekValue: {
298
- ...result.readseekValue,
299
- ok: false,
300
- error: buildReadSeekError(code, warning.message),
301
- },
302
- warnings: result.warnings,
303
- },
304
- };
305
- }
274
+ if (result.readSeekValue.lines.length > 0) {
275
+ options.onFileAnchored?.(absolutePath);
306
276
  }
307
277
 
308
278
  return {
@@ -311,7 +281,7 @@ export function registerWriteTool(pi: ExtensionAPI, options: WriteToolOptions =
311
281
  ...(result.diff !== undefined ? { diff: result.diff } : {}),
312
282
  ...(result.diffData !== undefined ? { diffData: result.diffData } : {}),
313
283
  ...(result.writeState ? { writeState: result.writeState } : {}),
314
- readseekValue: result.readseekValue,
284
+ readSeekValue: result.readSeekValue,
315
285
  warnings: result.warnings,
316
286
  },
317
287
  };
@@ -352,7 +322,7 @@ export function registerWriteTool(pi: ExtensionAPI, options: WriteToolOptions =
352
322
  if (isPartial) return renderPendingResult("pending write", width);
353
323
  const details = result.details ?? {};
354
324
  const output = result.content?.[0]?.type === "text" ? result.content[0].text : "";
355
- if (result.isError || details.readseekValue?.ok === false) {
325
+ if (result.isError || details.readSeekValue?.ok === false) {
356
326
  return renderErrorResult(output, { expanded, width, fallback: "write failed" });
357
327
  }
358
328
  const diffData = details.diffData;
@@ -361,12 +331,12 @@ export function registerWriteTool(pi: ExtensionAPI, options: WriteToolOptions =
361
331
  // instead of a diff body — every line is an add, so the gutter, line
362
332
  // numbers, and red/green tinting are noise.
363
333
  if (state === "created") {
364
- const readseekLines = (details.readseekValue?.lines ?? []) as Array<{ raw: string }>;
365
- const hasContent = readseekLines.length > 0;
334
+ const readSeekLines = (details.readSeekValue?.lines ?? []) as Array<{ raw: string }>;
335
+ const hasContent = readSeekLines.length > 0;
366
336
  const header = summaryLine(state, { hidden: hasContent && !expanded });
367
337
  const lines = header.split("\n");
368
338
  if (expanded && hasContent) {
369
- const content = readseekLines.map((l) => l.raw).join("\n");
339
+ const content = readSeekLines.map((l) => l.raw).join("\n");
370
340
  lines.push(...formatContentPreviewLines(content, theme));
371
341
  }
372
342
  return new Text(clampLinesToWidth(lines, width).join("\n"), 0, 0);