pi-readseek 0.3.24 → 0.4.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-readseek",
3
- "version": "0.3.24",
3
+ "version": "0.4.1",
4
4
  "description": "Pi extension for readseek-backed hash-anchored read/edit/grep, structural code maps, structural search, and file exploration",
5
5
  "type": "module",
6
6
  "exports": {
@@ -39,7 +39,7 @@
39
39
  "node": ">=20.0.0"
40
40
  },
41
41
  "dependencies": {
42
- "@jarkkojs/readseek": "^0.3.20",
42
+ "@jarkkojs/readseek": "^0.4.4",
43
43
  "diff": "^8.0.3",
44
44
  "ignore": "^7.0.5",
45
45
  "picomatch": "^4.0.4",
package/src/edit.ts CHANGED
@@ -36,14 +36,6 @@ function pendingPreviewLines(summary: string, preview: PendingDiffPreviewResult
36
36
  return { lines: [summary, headerLine], diffData: expanded ? diffData : undefined, headerLabel: preview.data.headerLabel };
37
37
  }
38
38
 
39
- export function wrapWriteError(err: any, path: string): Error {
40
- const code = err?.code;
41
- if (code === "EACCES" || code === "EPERM") {
42
- return new Error(`Permission denied: ${path}`);
43
- }
44
- return new Error(`Failed to write file: ${path}`);
45
- }
46
-
47
39
 
48
40
  const hashlineEditItemSchema = Type.Union([
49
41
  Type.Object({ set_line: Type.Object({ anchor: Type.String(), new_text: Type.String() }) }, { additionalProperties: true }),
@@ -269,16 +261,16 @@ export async function executeEdit(opts: ExecuteEditOptions): Promise<any> {
269
261
  }
270
262
  }
271
263
  let preAnchorContent = originalNormalized;
272
- // AC 26: reject anchored edits that target a line inside any replace_symbol
264
+ // reject anchored edits that target a line inside any replace_symbol
273
265
  // pre-replace range. Resolve each target against the ORIGINAL content so the
274
266
  // user-provided anchor line numbers (which reference the file as read) are
275
267
  // compared against the pre-replace coordinates.
276
268
  //
277
- // F2: surface replace_symbol symbol-resolution errors (not-found, ambiguous)
278
- // BEFORE the AC 26 overlap check and before any write (C1 preserved).
269
+ // surface replace_symbol symbol-resolution errors (not-found, ambiguous)
270
+ // before the overlap check and before any write.
279
271
  // Error-precedence order: replace_symbol resolution > anchor-overlap > anchored-edit.
280
272
  //
281
- // AC 4: store successful probe results and reuse them in the apply loop so
273
+ // store successful probe results and reuse them in the apply loop so
282
274
  // readseekMapContent is invoked at most once per replace_symbol edit.
283
275
  const replaceSymbolRanges: { start: number; end: number }[] = [];
284
276
  const rsProbeResults: { type: "ok"; content: string; replacement: string; warnings: string[]; range: { start: number; end: number } }[] = [];
@@ -290,7 +282,7 @@ export async function executeEdit(opts: ExecuteEditOptions): Promise<any> {
290
282
  newBody: rs.replace_symbol.new_body,
291
283
  });
292
284
  if (probe.type !== "ok") {
293
- // F2: symbol-resolution errors surface before AC 26 overlap check.
285
+ // symbol-resolution errors surface before the overlap check.
294
286
  return buildEditError(absolutePath, "invalid-edit-variant", probe.message);
295
287
  }
296
288
  rsProbeResults.push(probe);
@@ -349,7 +341,7 @@ export async function executeEdit(opts: ExecuteEditOptions): Promise<any> {
349
341
  }
350
342
  }
351
343
  }
352
- // Apply pass: reuse all probe results (AC 4). The probe pass resolved every
344
+ // Apply pass: reuse all probe results. The probe pass resolved every
353
345
  // replace_symbol against originalNormalized; apply those replacements in
354
346
  // reverse source order so original line ranges stay valid and no second
355
347
  // replaceSymbol/readseekMapContent call is needed.
@@ -445,7 +437,7 @@ export async function executeEdit(opts: ExecuteEditOptions): Promise<any> {
445
437
  if (regression) {
446
438
  const lines = regression.errorLines.join(", ");
447
439
  const message = `syntax-regression: lines ${lines}`;
448
- // Task 7 (AC 12): block mode aborts with syntax-regression code; file is left untouched.
440
+ // block mode aborts with syntax-regression code; file is left untouched.
449
441
  if (syntaxMode === "block") {
450
442
  return buildEditError(absolutePath, "syntax-regression", message);
451
443
  }
@@ -233,22 +233,6 @@ export function formatFileMap(map: FileMap, level?: DetailLevel): string {
233
233
  return lines.join("\n");
234
234
  }
235
235
 
236
- /**
237
- * Get the appropriate detail level for a map based on size.
238
- */
239
- export function getDetailLevelForSize(currentSize: number): DetailLevel {
240
- if (currentSize <= THRESHOLDS.FULL_TARGET_BYTES) {
241
- return DetailLevel.Full;
242
- }
243
- if (currentSize <= THRESHOLDS.COMPACT_TARGET_BYTES) {
244
- return DetailLevel.Compact;
245
- }
246
- if (currentSize <= THRESHOLDS.MAX_MAP_BYTES) {
247
- return DetailLevel.Minimal;
248
- }
249
- return DetailLevel.Outline;
250
- }
251
-
252
236
  /**
253
237
  * Reduce detail level of a file map.
254
238
  */
@@ -136,7 +136,7 @@ export function findSymbol(map: FileMap, query: string): SymbolLookupResult {
136
136
  } else {
137
137
  pool = allSymbols.filter((c) => c.symbol.name === namePart);
138
138
  }
139
- // AC 16 + AC 17: drop candidates without usable startLine. If that empties
139
+ // drop candidates without usable startLine. If that empties
140
140
  // the pool entirely, surface any same-leaf-name decls elsewhere in the file
141
141
  // so the user can see real candidate lines.
142
142
  pool = pool.filter((c) => Number.isFinite(c.symbol.startLine) && c.symbol.startLine > 0);
@@ -375,15 +375,7 @@ function parseReadOutput(value: unknown): ReadseekReadOutput {
375
375
  file_hash: requireString(output.file_hash, "file_hash"),
376
376
  start_line: requireNumber(output.start_line, "start_line"),
377
377
  end_line: requireNumber(output.end_line, "end_line"),
378
- hashlines: hashlines.map((line) => {
379
- if (!line || typeof line !== "object") throw new Error("invalid readseek hashline");
380
- const item = line as Record<string, unknown>;
381
- return {
382
- line: requireNumber(item.line, "hashline.line"),
383
- hash: requireString(item.hash, "hashline.hash"),
384
- text: requireString(item.text, "hashline.text"),
385
- };
386
- }),
378
+ hashlines: hashlines.map((line) => parseHashline(line, "hashline")),
387
379
  };
388
380
  }
389
381
 
@@ -413,17 +405,19 @@ function parseMapOutput(value: unknown): ReadseekMapOutput {
413
405
  };
414
406
  }
415
407
 
408
+ function parseHashline(value: unknown, field: string): ReadseekHashline {
409
+ if (!value || typeof value !== "object") throw new Error(`invalid readseek ${field}`);
410
+ const item = value as Record<string, unknown>;
411
+ return {
412
+ line: requireNumber(item.line, `${field}.line`),
413
+ hash: requireString(item.hash, `${field}.hash`),
414
+ text: requireString(item.text, `${field}.text`),
415
+ };
416
+ }
417
+
416
418
  function parseSearchHashlines(value: unknown, field: string): ReadseekHashline[] {
417
419
  if (!Array.isArray(value)) throw new Error(`invalid readseek ${field}`);
418
- return value.map((line) => {
419
- if (!line || typeof line !== "object") throw new Error(`invalid readseek ${field}`);
420
- const item = line as Record<string, unknown>;
421
- return {
422
- line: requireNumber(item.line, `${field}.line`),
423
- hash: requireString(item.hash, `${field}.hash`),
424
- text: requireString(item.text, `${field}.text`),
425
- };
426
- });
420
+ return value.map((line) => parseHashline(line, field));
427
421
  }
428
422
 
429
423
  function parseSearchOutput(value: unknown): ReadseekSearchOutput {
@@ -444,7 +438,7 @@ function parseSearchOutput(value: unknown): ReadseekSearchOutput {
444
438
  const item = match as Record<string, unknown>;
445
439
  if (!Array.isArray(item.captures)) throw new Error("invalid readseek search captures");
446
440
  return {
447
- pattern_index: requireNumber(item.pattern_index, "search.match.pattern_index"),
441
+ pattern_index: item.pattern_index === undefined ? 0 : requireNumber(item.pattern_index, "search.match.pattern_index"),
448
442
  start_line: requireNumber(item.start_line, "search.match.start_line"),
449
443
  end_line: requireNumber(item.end_line, "search.match.end_line"),
450
444
  start_hash: requireString(item.start_hash, "search.match.start_hash"),
@@ -471,7 +465,7 @@ function parseSearchOutput(value: unknown): ReadseekSearchOutput {
471
465
 
472
466
  export async function readseekRead(filePath: string, startLine?: number, endLine?: number): Promise<ReadseekReadOutput> {
473
467
  const args = ["read", filePath];
474
- if (startLine !== undefined) args.push("--offset", String(startLine));
468
+ if (startLine !== undefined) args.push("--start", String(startLine));
475
469
  if (endLine !== undefined) args.push("--end", String(endLine));
476
470
  return parseReadOutput(await runReadseek(args));
477
471
  }
package/src/refs.ts CHANGED
@@ -69,6 +69,14 @@ function groupReferences(references: ReadseekReference[], cwd: string): RefsOutp
69
69
  return [...files.values()];
70
70
  }
71
71
 
72
+ function isReadseekCursorValidationFailure(message: string): boolean {
73
+ return (
74
+ /line and column must be greater than zero/i.test(message) ||
75
+ /line \d+ not found/i.test(message) ||
76
+ /column \d+ exceeds maximum column \d+ for line \d+/i.test(message)
77
+ );
78
+ }
79
+
72
80
  /**
73
81
  * Executes identifier reference lookup and returns readseek-anchored matches.
74
82
  */
@@ -125,6 +133,9 @@ export async function executeRefs(opts: ExecuteRefsOptions): Promise<any> {
125
133
  };
126
134
  } catch (err: any) {
127
135
  const failure = classifyReadseekFailure(err);
136
+ if (p.scope && isReadseekCursorValidationFailure(failure.message)) {
137
+ return buildToolErrorResult("refs", "invalid-parameter", failure.message);
138
+ }
128
139
  return buildToolErrorResult("refs", failure.code, failure.message, failure.hint ? { hint: failure.hint } : {});
129
140
  }
130
141
  }
@@ -80,15 +80,6 @@ export function clampLinesToWidth(lines: string[], width: number | undefined): s
80
80
  return lines.map((line) => clampLineToWidth(line, width));
81
81
  }
82
82
 
83
- export function wrapLinesToWidth(lines: string[], width: number | undefined): string[] {
84
- if (width === undefined || width === null) return lines;
85
- const normalized = normalizeWidth(width);
86
- return lines.flatMap((line) => {
87
- if (visibleWidth(line) <= normalized) return [line];
88
- return wrapTextWithAnsi(line, normalized).map((wrapped) => clampLineToWidth(wrapped, normalized));
89
- });
90
- }
91
-
92
83
  export interface WrapWithHangingIndentOptions {
93
84
  /** Optional transform applied to each produced line (e.g. theme tinting). */
94
85
  tint?: (text: string) => string;
package/src/write.ts CHANGED
@@ -365,37 +365,23 @@ export function registerWriteTool(pi: ExtensionAPI, options: WriteToolOptions =
365
365
  // Lift binary-content signal into a fatal readseekValue.error envelope so
366
366
  // downstream consumers get the same taxonomy shape as every other tool.
367
367
  // The existing ReadseekWarning entry is preserved on readseekValue.warnings for
368
- // backward compatibility (see AC 12 — warnings namespace alignment).
369
- const binaryWarning = result.readseekValue.warnings.find((w) => w.code === "binary-content");
370
- if (binaryWarning) {
371
- return {
372
- content: [{ type: "text" as const, text: result.text }],
373
- isError: true,
374
- details: {
375
- readseekValue: {
376
- ...result.readseekValue,
377
- ok: false,
378
- error: buildReadseekError("binary-content", binaryWarning.message),
368
+ // backward compatibility.
369
+ for (const code of ["binary-content", "bare-cr"] as const) {
370
+ const warning = result.readseekValue.warnings.find((w) => w.code === code);
371
+ if (warning) {
372
+ return {
373
+ content: [{ type: "text" as const, text: result.text }],
374
+ isError: true,
375
+ details: {
376
+ readseekValue: {
377
+ ...result.readseekValue,
378
+ ok: false,
379
+ error: buildReadseekError(code, warning.message),
380
+ },
381
+ warnings: result.warnings,
379
382
  },
380
- warnings: result.warnings,
381
- },
382
- };
383
- }
384
-
385
- const bareCrWarning = result.readseekValue.warnings.find((w) => w.code === "bare-cr");
386
- if (bareCrWarning) {
387
- return {
388
- content: [{ type: "text" as const, text: result.text }],
389
- isError: true,
390
- details: {
391
- readseekValue: {
392
- ...result.readseekValue,
393
- ok: false,
394
- error: buildReadseekError("bare-cr", bareCrWarning.message),
395
- },
396
- warnings: result.warnings,
397
- },
398
- };
383
+ };
384
+ }
399
385
  }
400
386
 
401
387
  return {