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 +2 -2
- package/src/edit.ts +7 -15
- package/src/readseek/formatter.ts +0 -16
- package/src/readseek/symbol-lookup.ts +1 -1
- package/src/readseek-client.ts +14 -20
- package/src/refs.ts +11 -0
- package/src/tui-render-utils.ts +0 -9
- package/src/write.ts +16 -30
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-readseek",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
//
|
|
278
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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);
|
package/src/readseek-client.ts
CHANGED
|
@@ -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("--
|
|
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
|
}
|
package/src/tui-render-utils.ts
CHANGED
|
@@ -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
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
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 {
|