pi-readseek 0.3.8 → 0.3.10

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.8",
3
+ "version": "0.3.10",
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.0",
42
+ "@jarkkojs/readseek": "^0.3.8",
43
43
  "diff": "^8.0.3",
44
44
  "ignore": "^7.0.5",
45
45
  "picomatch": "^4.0.4",
@@ -1,6 +1,5 @@
1
1
  import type { Node as SyntaxNode, Parser as WasmParser, Tree } from "web-tree-sitter";
2
- import { detectLanguage } from "./readseek/language-detect.js";
3
- import { getWasmParser } from "./readseek/parser-loader.js";
2
+ import { getWasmParser, type WasmLanguageId } from "./readseek/parser-loader.js";
4
3
  import { reportParserError } from "./readseek/parser-errors.js";
5
4
 
6
5
  export interface ValidateInput {
@@ -75,15 +74,31 @@ function dedupeSortLines(
75
74
  return out.map((o) => o.key);
76
75
  }
77
76
 
77
+ const EXTENSION_TO_LANGUAGE: Record<string, WasmLanguageId> = {
78
+ ".rs": "rust",
79
+ ".c": "cpp",
80
+ ".cc": "cpp",
81
+ ".cpp": "cpp",
82
+ ".cxx": "cpp",
83
+ ".h": "c-header",
84
+ ".hpp": "c-header",
85
+ ".hxx": "c-header",
86
+ ".java": "java",
87
+ };
88
+
89
+ function detectWasmLang(filePath: string): WasmLanguageId | null {
90
+ for (const [ext, langId] of Object.entries(EXTENSION_TO_LANGUAGE)) {
91
+ if (filePath.toLowerCase().endsWith(ext)) return langId;
92
+ }
93
+ return null;
94
+ }
95
+
78
96
  export async function validateSyntaxRegression(
79
- input: ValidateInput,
97
+ input: ValidateInput,
80
98
  ): Promise<ValidateResult | null> {
81
- const lang = detectLanguage(input.filePath);
82
- if (!lang) return null;
83
- if (lang.id !== "rust" && lang.id !== "cpp" && lang.id !== "c-header" && lang.id !== "java") {
84
- return null;
85
- }
86
- const parser = await getWasmParser(lang.id);
99
+ const langId = detectWasmLang(input.filePath);
100
+ if (!langId) return null;
101
+ const parser = await getWasmParser(langId);
87
102
  if (!parser) return null;
88
103
 
89
104
  try {
@@ -109,12 +124,12 @@ export async function validateSyntaxRegression(
109
124
  ...afterStats.missing,
110
125
  ]);
111
126
  return { errorLines, newErrorCount, newMissingCount };
112
- } catch (err) {
113
- reportParserError(`wasm:syntax-validate:${lang.id}:${err instanceof Error ? err.message : String(err)}`, err, {
114
- context: `tree-sitter syntax validation failed for ${lang.id}`,
115
- });
116
- return null;
117
- } finally {
118
- parser.delete();
127
+ } catch (err) {
128
+ reportParserError(`wasm:syntax-validate:${langId}:${err instanceof Error ? err.message : String(err)}`, err, {
129
+ context: `tree-sitter syntax validation failed for ${langId}`,
130
+ });
131
+ return null;
132
+ } finally {
133
+ parser.delete();
119
134
  }
120
135
  }
package/src/edit.ts CHANGED
@@ -19,7 +19,7 @@ import { resolveSyntaxValidateMode, type SyntaxValidateOptions } from "./syntax-
19
19
  import { replaceSymbol } from "./replace-symbol.js";
20
20
  import { buildEditPreviewKey, buildPendingEditPreviewData, resolvePendingDiffPreview, type PendingDiffPreviewResult } from "./pending-diff-preview.js";
21
21
  import { buildDiffData, type DiffBlockRange } from "./diff-data.js";
22
- import { clampLineToWidth, clampLinesToWidth, isRendererExpanded, linkToolPath, summaryLine } from "./tui-render-utils.js";
22
+ import { clampLineToWidth, clampLinesToWidth, linkToolPath, resolveRenderResultContext, summaryLine } from "./tui-render-utils.js";
23
23
  import { DiffPreviewComponent } from "./tui-diff-component.js";
24
24
 
25
25
  import { resolveEditDiffDisplay } from "./readseek-settings.js";
@@ -86,11 +86,6 @@ type HashlineParams = Static<typeof hashlineEditSchema>;
86
86
  const EDIT_PROMPT_METADATA = defineToolPromptMetadata({
87
87
  promptUrl: new URL("../prompts/edit.md", import.meta.url),
88
88
  promptSnippet: "Edit files using hash-verified anchors from read/grep/search/write",
89
- promptGuidelines: [
90
- "Use edit for changes to existing files; read or search first and copy fresh LINE:HASH anchors.",
91
- "Prefer edit anchored set_line, replace_lines, and insert_after over shell rewrites.",
92
- "Use edit replace only when anchored edits are impractical.",
93
- ],
94
89
  });
95
90
 
96
91
  function buildEditError(
@@ -644,13 +639,9 @@ export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}
644
639
  return textComponent;
645
640
  },
646
641
  renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
647
- const context: { isPartial?: boolean; isError?: boolean; expanded?: boolean; lastComponent?: any; width?: number } =
648
- rest[0] ?? options ?? {};
649
- const isPartial = context.isPartial ?? (options as any)?.isPartial ?? false;
650
- const isError = context.isError ?? false;
642
+ const { isPartial, isError, expanded: baseExpanded, width, context } = resolveRenderResultContext(options, rest);
651
643
 
652
644
  if (isPartial) {
653
- const width = (context as any).width ?? (options as any)?.width;
654
645
  return new Text(clampLinesToWidth([summaryLine("pending edit")], width).join("\n"), 0, 0);
655
646
  }
656
647
 
@@ -678,8 +669,7 @@ export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}
678
669
  semanticClassification: semanticClassification as any,
679
670
  });
680
671
 
681
- const expanded = isRendererExpanded(options as any, context as any) || resolveEditDiffDisplay() === "expanded";
682
- const width = (context as any).width ?? (options as any)?.width;
672
+ const expanded = baseExpanded || resolveEditDiffDisplay() === "expanded";
683
673
  const diffData = (details as any).diffData;
684
674
  const stats = diffData?.stats ?? { added: 0, removed: 0 };
685
675
  let text = "";
package/src/grep.ts CHANGED
@@ -7,7 +7,7 @@ import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
7
7
  import { normalizeToLF, stripBom, hasBareCarriageReturn } from "./edit-diff.js";
8
8
  import { looksLikeBinary } from "./binary-detect.js";
9
9
  import { ensureHashInit, formatHashlineDisplay, escapeControlCharsForDisplay } from "./hashline.js";
10
- import { buildReadseekError, buildReadseekLine } from "./readseek-value.js";
10
+ import { buildReadseekLine, buildToolErrorResult } from "./readseek-value.js";
11
11
  import { buildGrepOutput } from "./grep-output.js";
12
12
 
13
13
  import { getOrGenerateMap } from "./map-cache.js";
@@ -17,16 +17,11 @@ import { throwIfAborted } from "./runtime.js";
17
17
  import { Text } from "@earendil-works/pi-tui";
18
18
  import { formatGrepCallText, formatGrepResultText } from "./grep-render-helpers.js";
19
19
  import { coerceObviousBase10Int } from "./coerce-obvious-int.js";
20
- import { clampLineToWidth, clampLinesToWidth, isRendererExpanded, linkToolPath, renderToolLabel, summaryLine } from "./tui-render-utils.js";
20
+ import { clampLineToWidth, clampLinesToWidth, linkToolPath, renderToolLabel, resolveRenderResultContext, summaryLine } from "./tui-render-utils.js";
21
21
 
22
22
  const GREP_PROMPT_METADATA = defineToolPromptMetadata({
23
23
  promptUrl: new URL("../prompts/grep.md", import.meta.url),
24
24
  promptSnippet: "Search file contents and return edit-ready hashline anchors",
25
- promptGuidelines: [
26
- "Use grep for text search across files instead of bash grep or rg.",
27
- "Use grep summary mode when you only need matching files or counts.",
28
- "Use search instead of grep when the query depends on code structure.",
29
- ],
30
25
  });
31
26
 
32
27
  const grepSchema = Type.Object({
@@ -314,76 +309,23 @@ export function registerGrepTool(pi: ExtensionAPI, options: GrepToolOptions = {}
314
309
  const rawParams = params as GrepParams;
315
310
  const context = coerceObviousBase10Int(rawParams.context, "context");
316
311
  if (!context.ok) {
317
- return {
318
- content: [{ type: "text", text: context.message }],
319
- isError: true,
320
- details: {
321
- readseekValue: {
322
- tool: "grep",
323
- ok: false,
324
- error: buildReadseekError("invalid-params-combo", context.message),
325
- },
326
- },
327
- };
312
+ return buildToolErrorResult("grep", "invalid-params-combo", context.message);
328
313
  }
329
314
  const limit = coerceObviousBase10Int(rawParams.limit, "limit");
330
315
  if (!limit.ok) {
331
- return {
332
- content: [{ type: "text", text: limit.message }],
333
- isError: true,
334
- details: {
335
- readseekValue: {
336
- tool: "grep",
337
- ok: false,
338
- error: buildReadseekError("invalid-limit", limit.message),
339
- },
340
- },
341
- };
316
+ return buildToolErrorResult("grep", "invalid-limit", limit.message);
342
317
  }
343
318
  const scopeContext = coerceObviousBase10Int(rawParams.scopeContext, "scopeContext");
344
319
  if (!scopeContext.ok) {
345
- return {
346
- content: [{ type: "text", text: scopeContext.message }],
347
- isError: true,
348
- details: {
349
- readseekValue: {
350
- tool: "grep",
351
- ok: false,
352
- error: buildReadseekError("invalid-params-combo", scopeContext.message),
353
- },
354
- },
355
- };
320
+ return buildToolErrorResult("grep", "invalid-params-combo", scopeContext.message);
356
321
  }
357
322
  if (scopeContext.value !== undefined && rawParams.scope !== "symbol") {
358
323
  const message = 'Invalid scopeContext: requires scope: "symbol". For normal surrounding-line context outside symbol scope, use the `context` parameter.';
359
- return {
360
- content: [{
361
- type: "text",
362
- text: message,
363
- }],
364
- isError: true,
365
- details: {
366
- readseekValue: {
367
- tool: "grep",
368
- ok: false,
369
- error: buildReadseekError("invalid-params-combo", message),
370
- },
371
- },
372
- };
324
+ return buildToolErrorResult("grep", "invalid-params-combo", message);
373
325
  }
374
326
  if (scopeContext.value !== undefined && scopeContext.value < 0) {
375
327
  const message = `Invalid scopeContext: expected a non-negative integer, received ${scopeContext.value}.`;
376
- return {
377
- content: [{ type: "text", text: message }],
378
- isError: true,
379
- details: {
380
- readseekValue: {
381
- tool: "grep",
382
- ok: false,
383
- error: buildReadseekError("invalid-params-combo", message),
384
- },
385
- },
386
- };
328
+ return buildToolErrorResult("grep", "invalid-params-combo", message);
387
329
  }
388
330
  const p: GrepParams = {
389
331
  ...rawParams,
@@ -716,13 +658,7 @@ if (p.scope === "symbol" && !summary) {
716
658
  return new Text(clampLineToWidth(text, context.width), 0, 0);
717
659
  },
718
660
  renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
719
- const context: { isPartial?: boolean; isError?: boolean; expanded?: boolean; cwd?: string; width?: number } =
720
- rest[0] ?? options ?? {};
721
- const isPartial = context.isPartial ?? (options as any)?.isPartial ?? false;
722
- const isError = context.isError ?? false;
723
- const expanded = isRendererExpanded(options as any, context as any);
724
- const cwd = context.cwd ?? process.cwd();
725
- const width = (context as any).width ?? (options as any)?.width;
661
+ const { isPartial, isError, expanded, cwd, width } = resolveRenderResultContext(options, rest);
726
662
 
727
663
  if (isPartial) return new Text(clampLinesToWidth([summaryLine("pending search")], width).join("\n"), 0, 0);
728
664
 
@@ -0,0 +1,47 @@
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
+ }
package/src/map-cache.ts CHANGED
@@ -1,18 +1,12 @@
1
1
  import { stat } from "node:fs/promises";
2
2
  import type { FileMap } from "./readseek/types.js";
3
- import { generateMap, generateMapWithIdentity, READSEEK_MAPPER_IDENTITY } from "./readseek/mapper.js";
4
- import {
5
- computeKey,
6
- contentHashFor64k,
7
- readCached,
8
- writeCached,
9
- persistenceEnabled,
10
- } from "./persistent-map-cache.js";
3
+ import { generateMap } from "./readseek/mapper.js";
4
+
11
5
  interface CacheEntry {
12
6
  mtimeMs: number;
13
- contentHash: string;
14
7
  map: FileMap | null;
15
8
  }
9
+
16
10
  export const MAP_CACHE_MAX_SIZE = 500;
17
11
 
18
12
  interface MapCacheGlobalState {
@@ -41,21 +35,12 @@ function rememberInMemory(absPath: string, entry: CacheEntry): void {
41
35
  }
42
36
  }
43
37
 
44
- async function stableContentHash(
45
- absPath: string,
46
- mtimeMs: number,
47
- expectedHash: string,
48
- ): Promise<string | null> {
49
- if (!expectedHash) return null;
50
- const currentStat = await stat(absPath);
51
- if (currentStat.mtimeMs !== mtimeMs) return null;
52
- const currentHash = await contentHashFor64k(absPath);
53
- if (!currentHash || currentHash !== expectedHash) return null;
54
- return currentHash;
55
- }
56
-
57
38
  /**
58
- * Get or generate a structural file map, with mtime-based caching.
39
+ * Get or generate a structural file map, with mtime-based in-memory caching.
40
+ * Readseek already caches map results in `.readseek/maps/` with its own binary
41
+ * format. The in-memory layer here avoids redundant process spawns within a
42
+ * single session.
43
+ *
59
44
  * Returns null on any failure — never throws.
60
45
  */
61
46
  export async function getOrGenerateMap(absPath: string): Promise<FileMap | null> {
@@ -65,72 +50,14 @@ export async function getOrGenerateMap(absPath: string): Promise<FileMap | null>
65
50
  const state = getMapCacheState();
66
51
  const cached = state.cache.get(absPath);
67
52
  if (cached && cached.mtimeMs === mtimeMs) {
68
- const currentHash = await contentHashFor64k(absPath);
69
- if (currentHash && currentHash === cached.contentHash) {
70
- state.cache.delete(absPath);
71
- state.cache.set(absPath, cached);
72
- return cached.map;
73
- }
74
- }
75
- if (!persistenceEnabled()) {
76
- const map = await generateMap(absPath);
77
- const hash = await contentHashFor64k(absPath);
78
- rememberInMemory(absPath, { mtimeMs, contentHash: hash, map });
79
- return map;
80
- }
81
-
82
- let preContentHash = "";
83
- try {
84
- preContentHash = await contentHashFor64k(absPath);
85
- if (preContentHash) {
86
- const key = computeKey(
87
- absPath,
88
- mtimeMs,
89
- preContentHash,
90
- READSEEK_MAPPER_IDENTITY.mapperName,
91
- READSEEK_MAPPER_IDENTITY.mapperVersion,
92
- );
93
- const fromDisk = await readCached(key);
94
- if (fromDisk) {
95
- rememberInMemory(absPath, { mtimeMs, contentHash: preContentHash, map: fromDisk });
96
- return fromDisk;
97
- }
98
- }
99
- } catch {
100
- // fall through to regeneration on a disk-cache miss
101
- }
102
- const { map, mapperName, mapperVersion } = await generateMapWithIdentity(absPath);
103
- const persistentIdentity = { mapperName, mapperVersion };
104
- let stableHash: string | null = null;
105
- let shouldRemember = true;
106
- if (preContentHash) {
107
- try {
108
- stableHash = await stableContentHash(absPath, mtimeMs, preContentHash);
109
- shouldRemember = stableHash !== null;
110
- } catch {
111
- shouldRemember = false;
112
- }
113
- }
114
- if (shouldRemember) {
115
- const hash = stableHash ?? preContentHash ?? "";
116
- rememberInMemory(absPath, { mtimeMs, contentHash: hash, map });
117
- }
118
- if (map && stableHash) {
119
- try {
120
- const key = computeKey(
121
- absPath,
122
- mtimeMs,
123
- stableHash,
124
- persistentIdentity.mapperName,
125
- persistentIdentity.mapperVersion,
126
- );
127
- await writeCached(key, map);
128
- } catch {
129
- // never fail the caller on a cache-write miss
130
- }
53
+ state.cache.delete(absPath);
54
+ state.cache.set(absPath, cached);
55
+ return cached.map;
131
56
  }
57
+ const map = await generateMap(absPath);
58
+ rememberInMemory(absPath, { mtimeMs, map });
132
59
  return map;
133
60
  } catch {
134
61
  return null;
135
62
  }
136
- }
63
+ }