pi-readseek 0.1.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.
Files changed (67) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +41 -0
  3. package/index.ts +142 -0
  4. package/package.json +73 -0
  5. package/prompts/edit.md +113 -0
  6. package/prompts/find.md +19 -0
  7. package/prompts/grep.md +26 -0
  8. package/prompts/ls.md +11 -0
  9. package/prompts/read.md +33 -0
  10. package/prompts/sg.md +25 -0
  11. package/prompts/write.md +46 -0
  12. package/src/binary-detect.ts +22 -0
  13. package/src/binary-resolution.ts +77 -0
  14. package/src/coerce-obvious-int.ts +39 -0
  15. package/src/context-application.ts +70 -0
  16. package/src/context-hygiene.ts +503 -0
  17. package/src/diff-data.ts +303 -0
  18. package/src/doom-loop-suggestions.ts +42 -0
  19. package/src/doom-loop.ts +216 -0
  20. package/src/edit-classify.ts +190 -0
  21. package/src/edit-diff.ts +354 -0
  22. package/src/edit-output.ts +107 -0
  23. package/src/edit-render-helpers.ts +141 -0
  24. package/src/edit-syntax-validate.ts +120 -0
  25. package/src/edit.ts +725 -0
  26. package/src/find-parsers.ts +89 -0
  27. package/src/find-stat.ts +36 -0
  28. package/src/find.ts +613 -0
  29. package/src/grep-budget.ts +79 -0
  30. package/src/grep-output.ts +197 -0
  31. package/src/grep-render-helpers.ts +77 -0
  32. package/src/grep-symbol-scope.ts +197 -0
  33. package/src/grep.ts +792 -0
  34. package/src/hashline.ts +747 -0
  35. package/src/ls.ts +293 -0
  36. package/src/map-cache.ts +152 -0
  37. package/src/path-utils.ts +24 -0
  38. package/src/pending-diff-preview.ts +269 -0
  39. package/src/persistent-map-cache.ts +251 -0
  40. package/src/read-local-bundle.ts +87 -0
  41. package/src/read-output.ts +212 -0
  42. package/src/read-render-helpers.ts +104 -0
  43. package/src/read.ts +748 -0
  44. package/src/readseek/constants.ts +21 -0
  45. package/src/readseek/enums.ts +38 -0
  46. package/src/readseek/formatter.ts +431 -0
  47. package/src/readseek/language-detect.ts +29 -0
  48. package/src/readseek/mapper.ts +69 -0
  49. package/src/readseek/parser-errors.ts +22 -0
  50. package/src/readseek/parser-loader.ts +83 -0
  51. package/src/readseek/symbol-error-format.ts +18 -0
  52. package/src/readseek/symbol-lookup.ts +294 -0
  53. package/src/readseek/types.ts +79 -0
  54. package/src/readseek-client.ts +343 -0
  55. package/src/readseek-error-codes.ts +54 -0
  56. package/src/readseek-settings.ts +287 -0
  57. package/src/readseek-value.ts +144 -0
  58. package/src/replace-symbol.ts +74 -0
  59. package/src/runtime.ts +3 -0
  60. package/src/sg-output.ts +88 -0
  61. package/src/sg.ts +308 -0
  62. package/src/syntax-validate-mode.ts +25 -0
  63. package/src/tool-prompt-metadata.ts +76 -0
  64. package/src/tui-diff-component.ts +86 -0
  65. package/src/tui-diff-renderer.ts +92 -0
  66. package/src/tui-render-utils.ts +129 -0
  67. package/src/write.ts +532 -0
@@ -0,0 +1,18 @@
1
+ import type { FileMap } from "./types.js";
2
+ import type { SymbolMatch } from "./symbol-lookup.js";
3
+
4
+ export function formatAmbiguous(query: string, candidates: SymbolMatch[]): string {
5
+ const lines = candidates.map((c) => `- ${c.name} (${c.kind}) — lines ${c.startLine}-${c.endLine}`);
6
+ const hints = candidates.map((c) => `${query}@${c.startLine}`).join(" or ");
7
+ return [
8
+ `Symbol '${query}' is ambiguous.`,
9
+ "Matches:",
10
+ ...lines,
11
+ `Use ${hints} to select by start line.`,
12
+ ].join("\n");
13
+ }
14
+
15
+ export function formatNotFound(query: string, map: FileMap): string {
16
+ const available = map.symbols.slice(0, 20).map((s) => s.name).join(", ");
17
+ return `[Warning: symbol '${query}' not found. Available symbols: ${available}]`;
18
+ }
@@ -0,0 +1,294 @@
1
+ import type { FileMap, FileSymbol } from "./types.js";
2
+ import type { SymbolKind } from "./enums.js";
3
+
4
+ export interface SymbolMatch {
5
+ name: string;
6
+ kind: SymbolKind;
7
+ startLine: number;
8
+ endLine: number;
9
+ parentName?: string;
10
+ }
11
+
12
+ export type SymbolLookupResult =
13
+ | { type: "found"; symbol: SymbolMatch }
14
+ | { type: "ambiguous"; candidates: SymbolMatch[] }
15
+ | {
16
+ type: "fuzzy";
17
+ symbol: SymbolMatch;
18
+ tier: "camelCase" | "substring";
19
+ otherCandidates: SymbolMatch[];
20
+ }
21
+ | { type: "not-found"; message?: string; candidates?: SymbolMatch[] };
22
+
23
+ function toMatch(symbol: FileSymbol, parentName?: string): SymbolMatch {
24
+ return {
25
+ name: symbol.name,
26
+ kind: symbol.kind,
27
+ startLine: symbol.startLine,
28
+ endLine: symbol.endLine,
29
+ ...(parentName ? { parentName } : {}),
30
+ };
31
+ }
32
+
33
+ interface SymbolCandidate {
34
+ symbol: FileSymbol;
35
+ parentName?: string;
36
+ }
37
+
38
+ function toMatches(candidates: SymbolCandidate[]): SymbolMatch[] {
39
+ return candidates.map((candidate) => toMatch(candidate.symbol, candidate.parentName));
40
+ }
41
+
42
+ function flattenSymbols(symbols: FileSymbol[]): SymbolCandidate[] {
43
+ const flattened: SymbolCandidate[] = [];
44
+
45
+ const visit = (symbol: FileSymbol, parentName?: string): void => {
46
+ flattened.push({ symbol, parentName });
47
+ for (const child of symbol.children ?? []) {
48
+ visit(child, symbol.name);
49
+ }
50
+ };
51
+
52
+ for (const symbol of symbols) {
53
+ visit(symbol);
54
+ }
55
+
56
+ return flattened;
57
+ }
58
+
59
+ function resolveDotPath(symbols: FileSymbol[], parts: string[]): SymbolCandidate[] {
60
+ let candidates: SymbolCandidate[] = symbols
61
+ .filter((symbol) => symbol.name === parts[0])
62
+ .map((symbol) => ({ symbol }));
63
+
64
+ for (let i = 1; i < parts.length; i++) {
65
+ const nextCandidates: SymbolCandidate[] = [];
66
+
67
+ for (const candidate of candidates) {
68
+ for (const child of candidate.symbol.children ?? []) {
69
+ if (child.name === parts[i]) {
70
+ nextCandidates.push({ symbol: child, parentName: candidate.symbol.name });
71
+ }
72
+ }
73
+ }
74
+
75
+ candidates = nextCandidates;
76
+ }
77
+
78
+ return candidates;
79
+ }
80
+
81
+ const JAVA_TOP_LEVEL_TYPE_KINDS = new Set(["class", "interface", "enum", "module"]);
82
+
83
+ function preferJavaTopLevelType(map: FileMap, candidates: SymbolCandidate[]): SymbolCandidate | undefined {
84
+ if (map.language !== "Java") return undefined;
85
+ const topLevelTypes = candidates.filter(
86
+ (candidate) => !candidate.parentName && JAVA_TOP_LEVEL_TYPE_KINDS.has(candidate.symbol.kind),
87
+ );
88
+ return topLevelTypes.length === 1 ? topLevelTypes[0] : undefined;
89
+ }
90
+
91
+ function javaPackageName(map: FileMap): string | undefined {
92
+ if (map.language !== "Java") return undefined;
93
+ for (const entry of map.imports) {
94
+ const match = /^package\s+([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\s*;?$/.exec(entry);
95
+ if (match) return match[1];
96
+ }
97
+ return undefined;
98
+ }
99
+
100
+ function stripJavaPackagePrefix(map: FileMap, query: string): string | undefined {
101
+ const packageName = javaPackageName(map);
102
+ if (!packageName) return undefined;
103
+ const prefix = `${packageName}.`;
104
+ return query.startsWith(prefix) ? query.slice(prefix.length) : undefined;
105
+ }
106
+
107
+ function resolveDotPathSuffix(symbols: FileSymbol[], parts: string[]): SymbolCandidate[] {
108
+ for (let start = 1; start < parts.length; start++) {
109
+ const candidates = resolveDotPath(symbols, parts.slice(start));
110
+ if (candidates.length > 0) return candidates;
111
+ }
112
+ return [];
113
+ }
114
+
115
+ export function findSymbol(map: FileMap, query: string): SymbolLookupResult {
116
+ const q = query.trim();
117
+ if (!q) return { type: "not-found" };
118
+ if (map.symbols.length === 0) return { type: "not-found" };
119
+
120
+ const allSymbols = flattenSymbols(map.symbols);
121
+ const javaRelativeQuery = stripJavaPackagePrefix(map, q);
122
+
123
+ const atLineMatch = /^(.+?)@(\d+)$/.exec(q);
124
+ if (atLineMatch) {
125
+ const namePart = atLineMatch[1];
126
+ const lineNum = Number.parseInt(atLineMatch[2], 10);
127
+ let pool: SymbolCandidate[];
128
+ if (namePart.includes(".")) {
129
+ const parts = namePart.split(".").map((p) => p.trim());
130
+ pool = parts.every((p) => p.length > 0)
131
+ ? resolveDotPath(map.symbols, parts)
132
+ : [];
133
+ if (pool.length === 0) {
134
+ pool = allSymbols.filter((c) => c.symbol.name === namePart);
135
+ }
136
+ } else {
137
+ pool = allSymbols.filter((c) => c.symbol.name === namePart);
138
+ }
139
+ // AC 16 + AC 17: drop candidates without usable startLine. If that empties
140
+ // the pool entirely, surface any same-leaf-name decls elsewhere in the file
141
+ // so the user can see real candidate lines.
142
+ pool = pool.filter((c) => Number.isFinite(c.symbol.startLine) && c.symbol.startLine > 0);
143
+ if (pool.length === 0) {
144
+ const leaf = namePart.split(".").pop() ?? namePart;
145
+ const sameLeaf = allSymbols.filter(
146
+ (c) => c.symbol.name === leaf
147
+ && Number.isFinite(c.symbol.startLine)
148
+ && c.symbol.startLine > 0,
149
+ );
150
+ if (sameLeaf.length > 0) {
151
+ const list = sameLeaf
152
+ .slice(0, 5)
153
+ .map((c) => `${c.parentName ? c.parentName + "." : ""}${c.symbol.name}@${c.symbol.startLine}`)
154
+ .join(", ");
155
+ return {
156
+ type: "not-found",
157
+ message: `${q} not found. Candidates: ${list}`,
158
+ candidates: toMatches(sameLeaf.slice(0, 5)),
159
+ };
160
+ }
161
+ }
162
+ if (pool.length > 0) {
163
+ const containing = pool.filter(
164
+ (c) => c.symbol.startLine <= lineNum && c.symbol.endLine >= lineNum,
165
+ );
166
+ if (containing.length > 0) {
167
+ return { type: "found", symbol: toMatch(containing[0].symbol, containing[0].parentName) };
168
+ }
169
+ const below = pool
170
+ .filter((c) => c.symbol.startLine >= lineNum)
171
+ .sort((a, b) => a.symbol.startLine - b.symbol.startLine);
172
+ if (below.length) {
173
+ return { type: "found", symbol: toMatch(below[0].symbol, below[0].parentName) };
174
+ }
175
+ const above = pool
176
+ .filter((c) => c.symbol.startLine < lineNum)
177
+ .sort((a, b) => b.symbol.startLine - a.symbol.startLine);
178
+ if (above.length) {
179
+ return { type: "found", symbol: toMatch(above[0].symbol, above[0].parentName) };
180
+ }
181
+ }
182
+ }
183
+
184
+ const exact = allSymbols.filter((c) => c.symbol.name === q);
185
+ if (exact.length === 1) return { type: "found", symbol: toMatch(exact[0].symbol, exact[0].parentName) };
186
+ if (exact.length > 1) {
187
+ const preferred = preferJavaTopLevelType(map, exact);
188
+ if (preferred) return { type: "found", symbol: toMatch(preferred.symbol, preferred.parentName) };
189
+ return { type: "ambiguous", candidates: toMatches(exact.slice(0, 5)) };
190
+ }
191
+
192
+ if (javaRelativeQuery) {
193
+ const javaExact = allSymbols.filter((c) => c.symbol.name === javaRelativeQuery);
194
+ if (javaExact.length === 1) return { type: "found", symbol: toMatch(javaExact[0].symbol, javaExact[0].parentName) };
195
+ if (javaExact.length > 1) {
196
+ const preferred = preferJavaTopLevelType(map, javaExact);
197
+ if (preferred) return { type: "found", symbol: toMatch(preferred.symbol, preferred.parentName) };
198
+ return { type: "ambiguous", candidates: toMatches(javaExact.slice(0, 5)) };
199
+ }
200
+ }
201
+
202
+ if (q.includes(".")) {
203
+ const parts = q.split(".").map((p) => p.trim());
204
+ if (parts.length >= 2 && parts.every((p) => p.length > 0)) {
205
+ const candidates = resolveDotPath(map.symbols, parts);
206
+ if (candidates.length === 1) {
207
+ return { type: "found", symbol: toMatch(candidates[0].symbol, candidates[0].parentName) };
208
+ }
209
+ if (candidates.length > 1) {
210
+ return {
211
+ type: "ambiguous",
212
+ candidates: toMatches(candidates.slice(0, 5)),
213
+ };
214
+ }
215
+ const javaParts = javaRelativeQuery?.includes(".")
216
+ ? javaRelativeQuery.split(".").map((p) => p.trim())
217
+ : [];
218
+ if (javaParts.length >= 2 && javaParts.every((p) => p.length > 0)) {
219
+ const javaCandidates = resolveDotPath(map.symbols, javaParts);
220
+ if (javaCandidates.length === 1) {
221
+ return { type: "found", symbol: toMatch(javaCandidates[0].symbol, javaCandidates[0].parentName) };
222
+ }
223
+ if (javaCandidates.length > 1) {
224
+ return {
225
+ type: "ambiguous",
226
+ candidates: toMatches(javaCandidates.slice(0, 5)),
227
+ };
228
+ }
229
+ }
230
+ const suffixCandidates = resolveDotPathSuffix(map.symbols, parts);
231
+ if (suffixCandidates.length === 1) {
232
+ return { type: "found", symbol: toMatch(suffixCandidates[0].symbol, suffixCandidates[0].parentName) };
233
+ }
234
+ if (suffixCandidates.length > 1) {
235
+ return {
236
+ type: "ambiguous",
237
+ candidates: toMatches(suffixCandidates.slice(0, 5)),
238
+ };
239
+ }
240
+ }
241
+ }
242
+
243
+ const qLower = q.toLowerCase();
244
+
245
+ // Tier 1: case-insensitive exact
246
+ const ci = allSymbols.filter((c) => c.symbol.name.toLowerCase() === qLower);
247
+ if (ci.length === 1) return { type: "found", symbol: toMatch(ci[0].symbol, ci[0].parentName) };
248
+ if (ci.length > 1) return { type: "ambiguous", candidates: toMatches(ci.slice(0, 5)) };
249
+
250
+ // Tier 2: prefix match
251
+ const prefix = allSymbols.filter((c) => c.symbol.name.toLowerCase().startsWith(qLower));
252
+ if (prefix.length === 1) return { type: "found", symbol: toMatch(prefix[0].symbol, prefix[0].parentName) };
253
+ if (prefix.length > 1) return { type: "ambiguous", candidates: toMatches(prefix.slice(0, 5)) };
254
+
255
+ // Tier 3: camelCase word boundary match
256
+ const camelCase = allSymbols.filter((c) => {
257
+ const words = c.symbol.name.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase().split(/[\s_]+/);
258
+ return words.some((w) => w === qLower);
259
+ });
260
+
261
+ // Tier 4: substring match
262
+ const partial = allSymbols.filter((c) => c.symbol.name.toLowerCase().includes(qLower));
263
+ const buildOtherCandidates = (chosen: SymbolCandidate): SymbolMatch[] => {
264
+ const pool: SymbolCandidate[] = [];
265
+ const seen = new Set<FileSymbol>();
266
+ seen.add(chosen.symbol);
267
+ for (const c of [...camelCase, ...partial]) {
268
+ if (seen.has(c.symbol)) continue;
269
+ seen.add(c.symbol);
270
+ pool.push(c);
271
+ }
272
+ return toMatches(pool.slice(0, 4));
273
+ };
274
+ if (camelCase.length === 1) {
275
+ return {
276
+ type: "fuzzy",
277
+ symbol: toMatch(camelCase[0].symbol, camelCase[0].parentName),
278
+ tier: "camelCase",
279
+ otherCandidates: buildOtherCandidates(camelCase[0]),
280
+ };
281
+ }
282
+ if (camelCase.length > 1) return { type: "ambiguous", candidates: toMatches(camelCase.slice(0, 5)) };
283
+ if (partial.length === 1) {
284
+ return {
285
+ type: "fuzzy",
286
+ symbol: toMatch(partial[0].symbol, partial[0].parentName),
287
+ tier: "substring",
288
+ otherCandidates: buildOtherCandidates(partial[0]),
289
+ };
290
+ }
291
+ if (partial.length > 1) return { type: "ambiguous", candidates: toMatches(partial.slice(0, 5)) };
292
+
293
+ return { type: "not-found" };
294
+ }
@@ -0,0 +1,79 @@
1
+ import type { DetailLevel, SymbolKind } from "./enums.js";
2
+
3
+ /**
4
+ * Represents a symbol extracted from a file.
5
+ */
6
+ export interface FileSymbol {
7
+ /** Symbol name (e.g., class name, function name) */
8
+ name: string;
9
+ /** Symbol kind */
10
+ kind: SymbolKind;
11
+ /** Starting line number (1-indexed) */
12
+ startLine: number;
13
+ /** Ending line number (1-indexed) */
14
+ endLine: number;
15
+ /** Optional signature (for functions/methods) */
16
+ signature?: string;
17
+ /** Child symbols (for nested structures like methods in classes) */
18
+ children?: FileSymbol[];
19
+ /** Additional modifiers (async, static, etc.) */
20
+ modifiers?: string[];
21
+ /** First line of JSDoc / docstring (if present) */
22
+ docstring?: string;
23
+ /** Whether this symbol is exported from its module */
24
+ isExported?: boolean;
25
+ }
26
+
27
+ /**
28
+ * Information about truncated symbol display.
29
+ */
30
+ export interface TruncatedInfo {
31
+ /** Total number of symbols in the original file */
32
+ totalSymbols: number;
33
+ /** Number of symbols shown in the truncated view */
34
+ shownSymbols: number;
35
+ /** Number of symbols omitted from the truncated view */
36
+ omittedSymbols: number;
37
+ }
38
+
39
+ /**
40
+ * Result of generating a file map.
41
+ */
42
+ export interface FileMap {
43
+ /** Absolute file path */
44
+ path: string;
45
+ /** Total line count */
46
+ totalLines: number;
47
+ /** Total file size in bytes */
48
+ totalBytes: number;
49
+ /** Detected language */
50
+ language: string;
51
+ /** Extracted symbols */
52
+ symbols: FileSymbol[];
53
+ /** Module imports / dependencies */
54
+ imports: string[];
55
+ /** Detail level used for this map */
56
+ detailLevel: DetailLevel;
57
+ /** Truncation metadata (present when symbols are truncated) */
58
+ truncatedInfo?: TruncatedInfo;
59
+ }
60
+
61
+ /**
62
+ * Map generation options.
63
+ */
64
+ export interface MapOptions {
65
+ /** Maximum map size in bytes (default: 20KB) */
66
+ maxBytes?: number;
67
+ /** Abort signal */
68
+ signal?: AbortSignal;
69
+ }
70
+
71
+ /**
72
+ * Language information.
73
+ */
74
+ export interface LanguageInfo {
75
+ /** Language identifier */
76
+ id: string;
77
+ /** Display name */
78
+ name: string;
79
+ }