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.
@@ -37,6 +37,33 @@ export function isRendererExpanded(options?: { expanded?: boolean }, context?: {
37
37
  return context?.expanded ?? options?.expanded ?? false;
38
38
  }
39
39
 
40
+ export interface RenderResultContext {
41
+ isPartial: boolean;
42
+ isError: boolean;
43
+ expanded: boolean;
44
+ width: number | undefined;
45
+ cwd: string;
46
+ context: Record<string, any>;
47
+ }
48
+
49
+ /**
50
+ * Resolve the shared render context for a tool's `renderResult`, which may
51
+ * receive its context either as a trailing `rest[0]` argument or folded into
52
+ * `options`. Returns the common flags plus the raw context object for callers
53
+ * that need fields beyond the common set (e.g. `lastComponent`).
54
+ */
55
+ export function resolveRenderResultContext(options: any, rest: any[]): RenderResultContext {
56
+ const context = rest[0] ?? options ?? {};
57
+ return {
58
+ isPartial: context.isPartial ?? options?.isPartial ?? false,
59
+ isError: context.isError ?? false,
60
+ expanded: isRendererExpanded(options, context),
61
+ width: context.width ?? options?.width,
62
+ cwd: context.cwd ?? process.cwd(),
63
+ context,
64
+ };
65
+ }
66
+
40
67
  export function normalizeWidth(width: unknown, fallback = 80): number {
41
68
  return typeof width === "number" && Number.isFinite(width) && width > 0 ? Math.floor(width) : fallback;
42
69
  }
package/src/write.ts CHANGED
@@ -66,11 +66,6 @@ const MAX_BYTES = 50 * 1024;
66
66
  const WRITE_PROMPT_METADATA = defineToolPromptMetadata({
67
67
  promptUrl: new URL("../prompts/write.md", import.meta.url),
68
68
  promptSnippet: "Create or overwrite a complete file and return edit anchors",
69
- promptGuidelines: [
70
- "Use write to create new files or intentionally replace whole files.",
71
- "Use edit instead of write for small changes or appends to existing files.",
72
- "Remember write overwrites existing files without confirmation.",
73
- ],
74
69
  });
75
70
 
76
71
  type WriteDiffFields = {
@@ -1,77 +0,0 @@
1
- import { existsSync as defaultExistsSync, readFileSync } from "node:fs";
2
- import { createRequire } from "node:module";
3
- import { dirname, resolve } from "node:path";
4
-
5
- const require = createRequire(import.meta.url);
6
-
7
- export interface ResolveBundledBinDeps {
8
- resolvePackageJson?: (specifier: string) => string;
9
- readPackageJson?: (packageJsonPath: string) => string;
10
- existsSync?: (candidate: string) => boolean;
11
- platform?: NodeJS.Platform;
12
- }
13
-
14
- function defaultResolvePackageJson(specifier: string): string {
15
- return require.resolve(specifier);
16
- }
17
-
18
- function defaultReadPackageJson(packageJsonPath: string): string {
19
- return readFileSync(packageJsonPath, "utf8");
20
- }
21
-
22
- function binEntryFor(packageJsonText: string, binName: string): string | undefined {
23
- const parsed = JSON.parse(packageJsonText) as { bin?: string | Record<string, string> };
24
- if (typeof parsed.bin === "string") return parsed.bin;
25
- return parsed.bin?.[binName];
26
- }
27
-
28
- function commandCandidates(basePath: string, platform: NodeJS.Platform): string[] {
29
- if (platform !== "win32") return [basePath];
30
- return [`${basePath}.exe`, basePath];
31
- }
32
-
33
- function firstExisting(candidates: string[], existsSync: (candidate: string) => boolean): string | undefined {
34
- return candidates.find((candidate) => existsSync(candidate));
35
- }
36
-
37
- export interface ExecutableCommand {
38
- command: string;
39
- argsPrefix: string[];
40
- }
41
-
42
- export function executableCommand(binaryPath: string, platform: NodeJS.Platform = process.platform): ExecutableCommand {
43
- if (platform === "win32" && /\.js$/i.test(binaryPath)) {
44
- return { command: process.execPath, argsPrefix: [binaryPath] };
45
- }
46
- return { command: binaryPath, argsPrefix: [] };
47
- }
48
-
49
- export function resolveBundledBin(
50
- packageName: string,
51
- binName: string,
52
- fallbackCommand: string,
53
- deps: ResolveBundledBinDeps = {},
54
- ): string {
55
- const resolvePackageJson = deps.resolvePackageJson ?? defaultResolvePackageJson;
56
- const readPackageJson = deps.readPackageJson ?? defaultReadPackageJson;
57
- const existsSync = deps.existsSync ?? defaultExistsSync;
58
- const platform = deps.platform ?? process.platform;
59
-
60
- try {
61
- const packageJsonPath = resolvePackageJson(`${packageName}/package.json`);
62
- const packageDir = dirname(packageJsonPath);
63
- const binEntry = binEntryFor(readPackageJson(packageJsonPath), binName);
64
- if (!binEntry) return fallbackCommand;
65
-
66
-
67
- const packageBinCandidate = firstExisting(
68
- commandCandidates(resolve(packageDir, binEntry), platform),
69
- existsSync,
70
- );
71
- if (packageBinCandidate) return packageBinCandidate;
72
- } catch {
73
- // Fall through to PATH fallback.
74
- }
75
-
76
- return fallbackCommand;
77
- }
@@ -1,89 +0,0 @@
1
- const SIZE_MULTIPLIERS: Record<string, number> = {
2
- "": 1,
3
- B: 1,
4
- K: 1024,
5
- KB: 1024,
6
- M: 1024 * 1024,
7
- MB: 1024 * 1024,
8
- G: 1024 ** 3,
9
- GB: 1024 ** 3,
10
- };
11
-
12
- export function parseSize(field: string, value: number | string): number {
13
- if (typeof value === "number") {
14
- if (!Number.isFinite(value) || value < 0) {
15
- throw new Error(
16
- `Invalid ${field} value: ${value} (expected a non-negative number of bytes)`,
17
- );
18
- }
19
- return value;
20
- }
21
-
22
- const match = /^\s*(\d+(?:\.\d+)?)\s*([a-zA-Z]*)\s*$/.exec(value);
23
- if (!match) {
24
- throw new Error(
25
- `Invalid ${field} value: ${JSON.stringify(value)} ` +
26
- `(expected a number with optional B/K/KB/M/MB/G/GB suffix; units are 1024-based)`,
27
- );
28
- }
29
-
30
- const num = parseFloat(match[1]);
31
- const suffix = match[2].toUpperCase();
32
- const mult = SIZE_MULTIPLIERS[suffix];
33
- if (mult === undefined) {
34
- throw new Error(
35
- `Invalid ${field} value: ${JSON.stringify(value)} ` +
36
- `(unknown unit '${match[2]}'; accepted: B, K, KB, M, MB, G, GB)`,
37
- );
38
- }
39
-
40
- return Math.round(num * mult);
41
- }
42
-
43
- const RELATIVE_UNIT_MS: Record<string, number> = {
44
- m: 60 * 1000,
45
- h: 60 * 60 * 1000,
46
- d: 24 * 60 * 60 * 1000,
47
- };
48
-
49
- const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
50
- const ISO_TS_RE =
51
- /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?(?:Z|[+-]\d{2}:\d{2})$/;
52
-
53
- export function parseRelativeOrIsoDate(
54
- field: string,
55
- value: string,
56
- now: Date = new Date(),
57
- ): Date {
58
- if (typeof value !== "string" || value.length === 0) {
59
- throw new Error(
60
- `Invalid ${field} value: ${JSON.stringify(value)} ` +
61
- `(expected ISO date/timestamp or relative shorthand like '1h', '24h', '7d', '30m')`,
62
- );
63
- }
64
-
65
- const rel = /^\s*(\d+)\s*([mhd])\s*$/.exec(value);
66
- if (rel) {
67
- const n = parseInt(rel[1], 10);
68
- return new Date(now.getTime() - n * RELATIVE_UNIT_MS[rel[2]]);
69
- }
70
-
71
- if (ISO_DATE_RE.test(value)) {
72
- const parsed = new Date(`${value}T00:00:00.000Z`);
73
- if (!Number.isNaN(parsed.getTime())) {
74
- return parsed;
75
- }
76
- }
77
-
78
- if (ISO_TS_RE.test(value)) {
79
- const parsed = new Date(value);
80
- if (!Number.isNaN(parsed.getTime())) {
81
- return parsed;
82
- }
83
- }
84
-
85
- throw new Error(
86
- `Invalid ${field} value: ${JSON.stringify(value)} ` +
87
- `(expected ISO date/timestamp or relative shorthand like '1h', '24h', '7d', '30m')`,
88
- );
89
- }
package/src/find-stat.ts DELETED
@@ -1,36 +0,0 @@
1
- import * as fsPromises from "node:fs/promises";
2
- import type { Stats } from "node:fs";
3
- import { resolve as resolvePath } from "node:path";
4
-
5
- export const DEFAULT_STAT_CONCURRENCY = 32;
6
- export const _testable = {
7
- stat: fsPromises.stat,
8
- };
9
-
10
- export async function statAllWithConcurrency(
11
- relPaths: string[],
12
- baseDir: string,
13
- concurrency: number = DEFAULT_STAT_CONCURRENCY,
14
- ): Promise<(Stats | null)[]> {
15
- const requested = Number.isFinite(concurrency)
16
- ? concurrency
17
- : DEFAULT_STAT_CONCURRENCY;
18
- const limit = Math.max(1, Math.min(requested, DEFAULT_STAT_CONCURRENCY));
19
- const out: (Stats | null)[] = new Array(relPaths.length).fill(null);
20
- let next = 0;
21
-
22
- async function worker() {
23
- while (true) {
24
- const i = next++;
25
- if (i >= relPaths.length) return;
26
- try {
27
- out[i] = await _testable.stat(resolvePath(baseDir, relPaths[i]));
28
- } catch {
29
- out[i] = null;
30
- }
31
- }
32
- }
33
-
34
- await Promise.all(Array.from({ length: Math.min(limit, relPaths.length) }, () => worker()));
35
- return out;
36
- }
@@ -1,230 +0,0 @@
1
- import { homedir } from "node:os";
2
- import { join } from "node:path";
3
- import { createHash, randomBytes } from "node:crypto";
4
- import { open, readFile, writeFile as fsWriteFile, mkdir as fsMkdir, rename, readdir, stat, unlink } from "node:fs/promises";
5
- import xxhashWasm from "xxhash-wasm";
6
- import type { FileMap } from "./readseek/types.js";
7
- import { resolveReadseekJsonSettings } from "./readseek-settings.js";
8
-
9
- /**
10
- * Resolve the on-disk map-cache directory using config precedence:
11
- * 1. `PI_HASHLINE_MAP_CACHE_DIR` (when non-empty) — used verbatim.
12
- * 2. JSON `mapCache.dir` (when configured).
13
- * 3. `$XDG_CACHE_HOME/pi-readseek/maps` (when non-empty).
14
- * 4. `~/.cache/pi-readseek/maps`.
15
- */
16
- function resolveCacheDir(): string {
17
- const explicit = process.env.PI_HASHLINE_MAP_CACHE_DIR;
18
- if (explicit && explicit.length > 0) return explicit;
19
-
20
- const configured = resolveReadseekJsonSettings().settings.mapCache?.dir;
21
- if (configured && configured.length > 0) return configured;
22
-
23
- const xdg = process.env.XDG_CACHE_HOME;
24
- if (xdg && xdg.length > 0) return join(xdg, "pi-readseek/maps");
25
-
26
- return join(homedir(), ".cache/pi-readseek/maps");
27
- }
28
-
29
- export function persistenceEnabled(): boolean {
30
- if (process.env.PI_HASHLINE_NO_PERSIST_MAPS === "1") return false;
31
- const configured = resolveReadseekJsonSettings().settings.mapCache?.enabled;
32
- if (configured !== undefined) return configured;
33
- return true;
34
- }
35
-
36
- /**
37
- * Build a deterministic hex cache key from the five input components.
38
- * Uses SHA-256 truncated to 32 hex chars; collision-safe for our purposes
39
- * and short enough for readable filenames.
40
- */
41
- export function computeKey(
42
- absolutePath: string,
43
- mtimeMs: number,
44
- contentHash: string,
45
- mapperName: string,
46
- mapperVersion: number,
47
- ): string {
48
- return createHash("sha256")
49
- .update(`${absolutePath}\0${mtimeMs}\0${contentHash}\0${mapperName}\0${mapperVersion}`)
50
- .digest("hex")
51
- .slice(0, 32);
52
- }
53
-
54
- const CONTENT_HASH_WINDOW_BYTES = 64 * 1024;
55
-
56
- let xxhashReady: Promise<{ h32Raw: (b: Uint8Array, seed?: number) => number }> | null = null;
57
-
58
- function loadXxhash(): Promise<{ h32Raw: (b: Uint8Array, seed?: number) => number }> {
59
- if (!xxhashReady) {
60
- xxhashReady = xxhashWasm().then((h) => ({ h32Raw: h.h32Raw }));
61
- }
62
-
63
- return xxhashReady;
64
- }
65
-
66
- /**
67
- * xxHash32 hex digest over the first 64 KB of `absPath`. Returns "" if the
68
- * file cannot be opened or read — the caller treats that as a miss.
69
- */
70
- export async function contentHashFor64k(absPath: string): Promise<string> {
71
- try {
72
- const fh = await open(absPath, "r");
73
- try {
74
- const buf = Buffer.alloc(CONTENT_HASH_WINDOW_BYTES);
75
- const { bytesRead } = await fh.read(buf, 0, CONTENT_HASH_WINDOW_BYTES, 0);
76
- const view = buf.subarray(0, bytesRead);
77
- const { h32Raw } = await loadXxhash();
78
- const n = h32Raw(view, 0) >>> 0;
79
- return n.toString(16).padStart(8, "0");
80
- } finally {
81
- await fh.close();
82
- }
83
- } catch {
84
- return "";
85
- }
86
- }
87
-
88
- function keyPath(key: string): string {
89
- return join(resolveCacheDir(), `${key}.json`);
90
- }
91
-
92
- function isRecord(value: unknown): value is Record<string, unknown> {
93
- return typeof value === "object" && value !== null;
94
- }
95
-
96
- function isStringArray(value: unknown): value is string[] {
97
- return Array.isArray(value) && value.every((item) => typeof item === "string");
98
- }
99
-
100
- function isFileSymbolLike(value: unknown): boolean {
101
- if (!isRecord(value)) return false;
102
- if (typeof value.name !== "string") return false;
103
- if (typeof value.kind !== "string") return false;
104
- if (typeof value.startLine !== "number") return false;
105
- if (typeof value.endLine !== "number") return false;
106
- if (value.signature !== undefined && typeof value.signature !== "string") return false;
107
- if (value.children !== undefined && (!Array.isArray(value.children) || !value.children.every(isFileSymbolLike))) {
108
- return false;
109
- }
110
- if (value.modifiers !== undefined && !isStringArray(value.modifiers)) return false;
111
- if (value.docstring !== undefined && typeof value.docstring !== "string") return false;
112
- if (value.isExported !== undefined && typeof value.isExported !== "boolean") return false;
113
- return true;
114
- }
115
-
116
- function isTruncatedInfoLike(value: unknown): boolean {
117
- if (!isRecord(value)) return false;
118
- return (
119
- typeof value.totalSymbols === "number"
120
- && typeof value.shownSymbols === "number"
121
- && typeof value.omittedSymbols === "number"
122
- );
123
- }
124
-
125
- const DETAIL_LEVELS = new Set(["full", "compact", "minimal", "outline", "truncated"]);
126
-
127
- function isFileMap(value: unknown): value is FileMap {
128
- if (!isRecord(value)) return false;
129
- if (typeof value.path !== "string") return false;
130
- if (typeof value.totalLines !== "number") return false;
131
- if (typeof value.totalBytes !== "number") return false;
132
- if (typeof value.language !== "string") return false;
133
- if (!Array.isArray(value.symbols) || !value.symbols.every(isFileSymbolLike)) return false;
134
- if (!isStringArray(value.imports)) return false;
135
- if (typeof value.detailLevel !== "string" || !DETAIL_LEVELS.has(value.detailLevel)) return false;
136
- if (value.truncatedInfo !== undefined && !isTruncatedInfoLike(value.truncatedInfo)) return false;
137
- return true;
138
- }
139
-
140
- /** Read a cached FileMap. Returns null on any failure (missing, corrupt, unreadable). */
141
- export async function readCached(key: string): Promise<FileMap | null> {
142
- if (!persistenceEnabled()) return null;
143
-
144
- try {
145
- const raw = await readFile(keyPath(key), "utf-8");
146
- const parsed: unknown = JSON.parse(raw);
147
- return isFileMap(parsed) ? parsed : null;
148
- } catch {
149
- return null;
150
- }
151
- }
152
-
153
- async function tryWriteCachedRaw(key: string, map: FileMap): Promise<boolean> {
154
- if (!persistenceEnabled()) return false;
155
- const dir = resolveCacheDir();
156
- const target = join(dir, `${key}.json`);
157
- const tmp = join(dir, `${key}.${randomBytes(6).toString("hex")}.tmp`);
158
- try {
159
- await fsMkdir(dir, { recursive: true });
160
- await fsWriteFile(tmp, JSON.stringify(map));
161
- await rename(tmp, target);
162
- return true;
163
- } catch {
164
- try {
165
- await unlink(tmp);
166
- } catch {
167
- // ignore temp-file cleanup failures
168
- }
169
- return false;
170
- }
171
- }
172
-
173
- /** Low-level atomic write. Internal helper. */
174
- async function writeCachedRaw(key: string, map: FileMap): Promise<void> {
175
- await tryWriteCachedRaw(key, map);
176
- }
177
-
178
- const EVICTION_INTERVAL = 20;
179
- const EVICTION_CAP = 5000;
180
- let writeCounter = 0;
181
-
182
-
183
- const CACHE_KEY_FILE = /^[0-9a-f]{32}\.json$/;
184
- async function maybeEvict(): Promise<void> {
185
- try {
186
- const dir = resolveCacheDir();
187
- const names = (await readdir(dir)).filter((n) => CACHE_KEY_FILE.test(n));
188
- if (names.length <= EVICTION_CAP) return;
189
-
190
- const entries: Array<{ path: string; age: number }> = [];
191
- for (const name of names) {
192
- const path = join(dir, name);
193
- try {
194
- const s = await stat(path);
195
- const age = (s.atimeMs ?? s.mtimeMs) || 0;
196
- entries.push({ path, age });
197
- } catch {
198
- // ignore individual stat failures
199
- }
200
- }
201
-
202
- entries.sort((a, b) => a.age - b.age);
203
- const excess = entries.length - EVICTION_CAP;
204
- for (let i = 0; i < excess; i += 1) {
205
- try {
206
- await unlink(entries[i].path);
207
- } catch {
208
- // ignore individual unlink failures
209
- }
210
- }
211
- } catch {
212
- // Swallow eviction errors — never propagate.
213
- }
214
- }
215
-
216
- /**
217
- * Public atomic write. Counts successful invocations so eviction can trigger lazily.
218
- */
219
- export async function writeCached(key: string, map: FileMap): Promise<void> {
220
- if (!(await tryWriteCachedRaw(key, map))) return;
221
- writeCounter += 1;
222
- if (writeCounter % EVICTION_INTERVAL === 0) {
223
- try {
224
- await maybeEvict();
225
- } catch {
226
- // Swallow eviction errors at the call site too.
227
- }
228
- }
229
- }
230
-
@@ -1,29 +0,0 @@
1
- import type { LanguageInfo } from "./types.js";
2
-
3
- const EXTENSION_LANGUAGE_MAP: Record<string, LanguageInfo> = {
4
- ".rs": { id: "rust", name: "Rust" },
5
- ".c": { id: "cpp", name: "C" },
6
- ".cc": { id: "cpp", name: "C++" },
7
- ".cpp": { id: "cpp", name: "C++" },
8
- ".cxx": { id: "cpp", name: "C++" },
9
- ".hpp": { id: "c-header", name: "C/C++ Header" },
10
- ".h": { id: "c-header", name: "C/C++ Header" },
11
- ".hxx": { id: "c-header", name: "C/C++ Header" },
12
- ".java": { id: "java", name: "Java" },
13
- };
14
-
15
- export function detectLanguage(filePath: string): LanguageInfo | null {
16
- const normalized = filePath.toLowerCase();
17
- for (const [ext, language] of Object.entries(EXTENSION_LANGUAGE_MAP)) {
18
- if (normalized.endsWith(ext)) return language;
19
- }
20
- return null;
21
- }
22
-
23
- export function isSupported(filePath: string): boolean {
24
- return detectLanguage(filePath) !== null;
25
- }
26
-
27
- export function getSupportedExtensions(): string[] {
28
- return Object.keys(EXTENSION_LANGUAGE_MAP);
29
- }