pi-readseek 0.3.14 → 0.3.16
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/README.md +3 -0
- package/index.ts +2 -0
- package/package.json +2 -2
- package/prompts/edit.md +1 -1
- package/prompts/refs.md +23 -0
- package/src/edit.ts +2 -2
- package/src/grep-budget.ts +3 -3
- package/src/map-cache.ts +2 -2
- package/src/readseek/types.ts +0 -10
- package/src/readseek-client.ts +71 -1
- package/src/readseek-settings.ts +1 -1
- package/src/refs-output.ts +56 -0
- package/src/refs.ts +202 -0
- package/src/replace-symbol.ts +2 -2
- package/src/syntax-validate-mode.ts +1 -1
- package/src/tool-prompt-metadata.ts +5 -0
- package/src/readseek/mapper.ts +0 -28
package/README.md
CHANGED
|
@@ -34,6 +34,9 @@ npm install --save-dev @jarkkojs/readseek
|
|
|
34
34
|
follow-up `read`.
|
|
35
35
|
- **search** — searches code by structural pattern (AST) and returns anchored
|
|
36
36
|
matches. Use when syntax matters more than raw text.
|
|
37
|
+
- **refs** — finds binding-accurate references to an identifier and returns
|
|
38
|
+
anchored usages with their enclosing symbols. Use before renaming or deleting
|
|
39
|
+
a symbol.
|
|
37
40
|
- **write** — creates or overwrites whole files and returns anchors for
|
|
38
41
|
immediate follow-up edits.
|
|
39
42
|
|
package/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { registerReadTool } from "./src/read.js";
|
|
|
3
3
|
import { registerEditTool } from "./src/edit.js";
|
|
4
4
|
import { registerGrepTool } from "./src/grep.js";
|
|
5
5
|
import { registerSgTool, isSgAvailable } from "./src/sg.js";
|
|
6
|
+
import { registerRefsTool } from "./src/refs.js";
|
|
6
7
|
import { registerWriteTool } from "./src/write.js";
|
|
7
8
|
export default function piReadseekExtension(pi: ExtensionAPI): void {
|
|
8
9
|
const readPaths = new Set<string>();
|
|
@@ -20,5 +21,6 @@ export default function piReadseekExtension(pi: ExtensionAPI): void {
|
|
|
20
21
|
|
|
21
22
|
registerGrepTool(pi, { searchGuideline, onFileAnchored: noteRead });
|
|
22
23
|
registerSgTool(pi, { onFileAnchored: noteRead });
|
|
24
|
+
registerRefsTool(pi, { onFileAnchored: noteRead });
|
|
23
25
|
registerWriteTool(pi, { onFileAnchored: noteRead });
|
|
24
26
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-readseek",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.16",
|
|
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.
|
|
42
|
+
"@jarkkojs/readseek": "^0.3.16",
|
|
43
43
|
"diff": "^8.0.3",
|
|
44
44
|
"ignore": "^7.0.5",
|
|
45
45
|
"picomatch": "^4.0.4",
|
package/prompts/edit.md
CHANGED
|
@@ -80,7 +80,7 @@ Syntax validation runs before writing when supported:
|
|
|
80
80
|
- Default `warn`: write succeeds, but warnings include `syntax-regression: lines X-Y`.
|
|
81
81
|
- `block`: aborts without writing.
|
|
82
82
|
- `off`: skips validation.
|
|
83
|
-
- `
|
|
83
|
+
- `READSEEK_SYNTAX_VALIDATE` can set the default mode.
|
|
84
84
|
|
|
85
85
|
Existing syntax errors are tolerated; warnings are for newly introduced parser errors.
|
|
86
86
|
|
package/prompts/refs.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Find binding-accurate references to an identifier with readseek. Use it when you need every place a name is used — before renaming, deleting, or changing a symbol — and plain text search would be too broad. Results are grouped by file with edit-ready hashline anchors and the enclosing symbol for each reference.
|
|
2
|
+
|
|
3
|
+
## Parameters
|
|
4
|
+
|
|
5
|
+
- `name` — identifier to find references for.
|
|
6
|
+
- `path` — file or directory, default cwd.
|
|
7
|
+
- `lang` — language hint; set it when syntax is ambiguous, extensionless, or generated.
|
|
8
|
+
- `scope` — restrict results to the binding under `line`/`column` in a single file. Requires `line`.
|
|
9
|
+
- `line` — one-based cursor line, used with `scope`.
|
|
10
|
+
- `column` — one-based cursor byte column, used with `scope`.
|
|
11
|
+
- `cached` — in a Git repository, search tracked/indexed files.
|
|
12
|
+
- `others` — in a Git repository, search untracked files.
|
|
13
|
+
- `ignored` — with `others`, include ignored untracked files.
|
|
14
|
+
|
|
15
|
+
## Scope
|
|
16
|
+
|
|
17
|
+
Without `scope`, references match by identifier name across the target. With `scope` plus `line` (and optionally `column`), results are limited to the specific binding under that cursor in a single file, so shadowed or unrelated same-named identifiers are excluded.
|
|
18
|
+
|
|
19
|
+
## Git selection
|
|
20
|
+
|
|
21
|
+
When searching a directory inside a Git repository, readseek defaults to tracked/indexed files plus untracked non-ignored files. Use `cached`, `others`, and `ignored` to narrow or expand that selection. `ignored` requires `others`.
|
|
22
|
+
|
|
23
|
+
Use `grep` for plain text, `search` for code shape, and `refs` for identifier usage.
|
package/src/edit.ts
CHANGED
|
@@ -269,7 +269,7 @@ export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}
|
|
|
269
269
|
// Error-precedence order: replace_symbol resolution > anchor-overlap > anchored-edit.
|
|
270
270
|
//
|
|
271
271
|
// AC 4: store successful probe results and reuse them in the apply loop so
|
|
272
|
-
//
|
|
272
|
+
// readseekMapContent is invoked at most once per replace_symbol edit.
|
|
273
273
|
const replaceSymbolRanges: { start: number; end: number }[] = [];
|
|
274
274
|
const rsProbeResults: { type: "ok"; content: string; replacement: string; warnings: string[]; range: { start: number; end: number } }[] = [];
|
|
275
275
|
for (const rs of replaceSymbolEdits) {
|
|
@@ -342,7 +342,7 @@ export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}
|
|
|
342
342
|
// Apply pass: reuse all probe results (AC 4). The probe pass resolved every
|
|
343
343
|
// replace_symbol against originalNormalized; apply those replacements in
|
|
344
344
|
// reverse source order so original line ranges stay valid and no second
|
|
345
|
-
// replaceSymbol/
|
|
345
|
+
// replaceSymbol/readseekMapContent call is needed.
|
|
346
346
|
const replaceSymbolWarnings: string[] = [];
|
|
347
347
|
if (rsProbeResults.length > 0) {
|
|
348
348
|
const lines = originalNormalized.split("\n");
|
package/src/grep-budget.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { resolveReadseekJsonSettings } from "./readseek-settings.js";
|
|
|
4
4
|
const POSITIVE_BASE10_INT = /^[1-9][0-9]*$/;
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Strict positive base-10 integer parser used by
|
|
7
|
+
* Strict positive base-10 integer parser used by readseek env knobs.
|
|
8
8
|
*
|
|
9
9
|
* Accepts: trimmed strings matching /^[1-9][0-9]*$/ that parse to a finite
|
|
10
10
|
* positive integer.
|
|
@@ -66,12 +66,12 @@ export function resolveGrepOutputBudget(): GrepOutputBudget {
|
|
|
66
66
|
const settings = resolveReadseekJsonSettings().settings.grep;
|
|
67
67
|
return {
|
|
68
68
|
maxLines: resolveDimension(
|
|
69
|
-
process.env.
|
|
69
|
+
process.env.READSEEK_GREP_MAX_LINES,
|
|
70
70
|
settings?.maxLines,
|
|
71
71
|
GREP_OUTPUT_DEFAULT_MAX_LINES,
|
|
72
72
|
),
|
|
73
73
|
maxBytes: resolveDimension(
|
|
74
|
-
process.env.
|
|
74
|
+
process.env.READSEEK_GREP_MAX_BYTES,
|
|
75
75
|
settings?.maxBytes,
|
|
76
76
|
GREP_OUTPUT_DEFAULT_MAX_BYTES,
|
|
77
77
|
),
|
package/src/map-cache.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { stat } from "node:fs/promises";
|
|
2
2
|
import type { FileMap } from "./readseek/types.js";
|
|
3
|
-
import {
|
|
3
|
+
import { readseekMap } from "./readseek-client.js";
|
|
4
4
|
|
|
5
5
|
interface CacheEntry {
|
|
6
6
|
mtimeMs: number;
|
|
@@ -60,7 +60,7 @@ export async function getOrGenerateMap(absPath: string): Promise<FileMap | null>
|
|
|
60
60
|
if (inflight) return inflight;
|
|
61
61
|
|
|
62
62
|
const generation = (async () => {
|
|
63
|
-
const map = await
|
|
63
|
+
const map = await readseekMap(absPath, fileStat.size);
|
|
64
64
|
rememberInMemory(absPath, { mtimeMs, map });
|
|
65
65
|
return map;
|
|
66
66
|
})()
|
package/src/readseek/types.ts
CHANGED
|
@@ -58,16 +58,6 @@ export interface FileMap {
|
|
|
58
58
|
truncatedInfo?: TruncatedInfo;
|
|
59
59
|
}
|
|
60
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
61
|
/**
|
|
72
62
|
* Language information.
|
|
73
63
|
*/
|
package/src/readseek-client.ts
CHANGED
|
@@ -70,6 +70,30 @@ interface ReadseekSearchOutput {
|
|
|
70
70
|
results: ReadseekSearchFileOutput[];
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
export interface ReadseekReference {
|
|
74
|
+
file: string;
|
|
75
|
+
line: number;
|
|
76
|
+
column: number;
|
|
77
|
+
line_hash: string;
|
|
78
|
+
text: string;
|
|
79
|
+
enclosingSymbol?: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface ReadseekRefsOutput {
|
|
83
|
+
references: ReadseekReference[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface ReadseekRefsOptions {
|
|
87
|
+
scope?: boolean;
|
|
88
|
+
line?: number;
|
|
89
|
+
column?: number;
|
|
90
|
+
language?: string;
|
|
91
|
+
cached?: boolean;
|
|
92
|
+
others?: boolean;
|
|
93
|
+
ignored?: boolean;
|
|
94
|
+
signal?: AbortSignal;
|
|
95
|
+
}
|
|
96
|
+
|
|
73
97
|
export interface ReadseekSearchOptions {
|
|
74
98
|
language?: string;
|
|
75
99
|
cached?: boolean;
|
|
@@ -398,7 +422,53 @@ export async function readseekMapContent(
|
|
|
398
422
|
options: { signal?: AbortSignal } = {},
|
|
399
423
|
): Promise<FileMap | null> {
|
|
400
424
|
const output = parseMapOutput(
|
|
401
|
-
await runReadseek(["map", "--stdin",
|
|
425
|
+
await runReadseek(["map", "--stdin", filePath], { signal: options.signal, stdin: content }),
|
|
402
426
|
);
|
|
403
427
|
return fileMapFromReadseekOutput(output, filePath, Buffer.byteLength(content, "utf8"));
|
|
404
428
|
}
|
|
429
|
+
|
|
430
|
+
function optionalString(value: unknown, field: string): string | undefined {
|
|
431
|
+
if (value === undefined || value === null) return undefined;
|
|
432
|
+
return requireString(value, field);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function parseRefsOutput(value: unknown): ReadseekRefsOutput {
|
|
436
|
+
if (!value || typeof value !== "object") throw new Error("invalid readseek refs output");
|
|
437
|
+
const output = value as Record<string, unknown>;
|
|
438
|
+
if (!Array.isArray(output.references)) throw new Error("invalid readseek references");
|
|
439
|
+
return {
|
|
440
|
+
references: output.references.map((reference) => {
|
|
441
|
+
if (!reference || typeof reference !== "object") throw new Error("invalid readseek reference");
|
|
442
|
+
const item = reference as Record<string, unknown>;
|
|
443
|
+
const symbol = item.symbol;
|
|
444
|
+
const enclosing =
|
|
445
|
+
symbol && typeof symbol === "object"
|
|
446
|
+
? optionalString((symbol as Record<string, unknown>).qualified_name, "reference.symbol.qualified_name")
|
|
447
|
+
: undefined;
|
|
448
|
+
return {
|
|
449
|
+
file: requireString(item.file, "reference.file"),
|
|
450
|
+
line: requireNumber(item.line, "reference.line"),
|
|
451
|
+
column: requireNumber(item.column, "reference.column"),
|
|
452
|
+
line_hash: requireString(item.line_hash, "reference.line_hash"),
|
|
453
|
+
text: requireString(item.text, "reference.text"),
|
|
454
|
+
enclosingSymbol: enclosing,
|
|
455
|
+
};
|
|
456
|
+
}),
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export async function readseekRefs(
|
|
461
|
+
target: string,
|
|
462
|
+
name: string,
|
|
463
|
+
options: ReadseekRefsOptions = {},
|
|
464
|
+
): Promise<ReadseekReference[]> {
|
|
465
|
+
const args = ["refs", target, name];
|
|
466
|
+
if (options.scope) args.push("--scope");
|
|
467
|
+
if (options.line !== undefined) args.push("--line", String(options.line));
|
|
468
|
+
if (options.column !== undefined) args.push("--column", String(options.column));
|
|
469
|
+
if (options.language) args.push("--language", options.language);
|
|
470
|
+
if (options.cached) args.push("--cached");
|
|
471
|
+
if (options.others) args.push("--others");
|
|
472
|
+
if (options.ignored) args.push("--ignored");
|
|
473
|
+
return parseRefsOutput(await runReadseek(args, { signal: options.signal })).references;
|
|
474
|
+
}
|
package/src/readseek-settings.ts
CHANGED
|
@@ -121,7 +121,7 @@ export function resolveReadseekJsonSettings(): ReadseekSettingsResult {
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
export function resolveEditDiffDisplay(env: NodeJS.ProcessEnv = process.env): "collapsed" | "expanded" {
|
|
124
|
-
const raw = env.
|
|
124
|
+
const raw = env.READSEEK_EDIT_DIFF_DISPLAY;
|
|
125
125
|
if (typeof raw === "string") {
|
|
126
126
|
const normalized = raw.trim().toLowerCase();
|
|
127
127
|
if (normalized === "expanded" || normalized === "collapsed") return normalized;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ReadseekLine } from "./readseek-value.js";
|
|
2
|
+
|
|
3
|
+
export interface RefsOutputLine extends ReadseekLine {
|
|
4
|
+
enclosingSymbol?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface RefsOutputFile {
|
|
8
|
+
displayPath: string;
|
|
9
|
+
path: string;
|
|
10
|
+
lines: RefsOutputLine[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface BuildRefsOutputInput {
|
|
14
|
+
name: string;
|
|
15
|
+
files: RefsOutputFile[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface RefsOutputResult {
|
|
19
|
+
text: string;
|
|
20
|
+
readseekValue: {
|
|
21
|
+
tool: "refs";
|
|
22
|
+
files: Array<{
|
|
23
|
+
path: string;
|
|
24
|
+
lines: RefsOutputLine[];
|
|
25
|
+
}>;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function buildRefsOutput(input: BuildRefsOutputInput): RefsOutputResult {
|
|
30
|
+
if (input.files.length === 0) {
|
|
31
|
+
return {
|
|
32
|
+
text: `No references found for: ${input.name}`,
|
|
33
|
+
readseekValue: { tool: "refs", files: [] },
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const blocks: string[] = [];
|
|
38
|
+
for (const file of input.files) {
|
|
39
|
+
blocks.push(`--- ${file.displayPath} ---`);
|
|
40
|
+
for (const line of file.lines) {
|
|
41
|
+
const suffix = line.enclosingSymbol ? ` (in ${line.enclosingSymbol})` : "";
|
|
42
|
+
blocks.push(`>>${line.anchor}|${line.display}${suffix}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
text: blocks.join("\n"),
|
|
48
|
+
readseekValue: {
|
|
49
|
+
tool: "refs",
|
|
50
|
+
files: input.files.map((file) => ({
|
|
51
|
+
path: file.path,
|
|
52
|
+
lines: file.lines.map((line) => ({ ...line })),
|
|
53
|
+
})),
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
package/src/refs.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type { ExtensionAPI, ToolRenderResultOptions } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
3
|
+
import { Type } from "@sinclair/typebox";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { stat as fsStat } from "node:fs/promises";
|
|
6
|
+
import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
|
|
7
|
+
import { escapeControlCharsForDisplay } from "./hashline.js";
|
|
8
|
+
import { buildToolErrorResult } from "./readseek-value.js";
|
|
9
|
+
import { resolveToCwd } from "./path-utils.js";
|
|
10
|
+
import { isReadseekAvailable, readseekRefs, type ReadseekReference } from "./readseek-client.js";
|
|
11
|
+
import { buildRefsOutput, type RefsOutputFile, type RefsOutputLine } from "./refs-output.js";
|
|
12
|
+
|
|
13
|
+
import { clampLineToWidth, clampLinesToWidth, renderToolLabel, resolveRenderResultContext, summaryLine } from "./tui-render-utils.js";
|
|
14
|
+
|
|
15
|
+
type RefsParams = {
|
|
16
|
+
name: string;
|
|
17
|
+
path?: string;
|
|
18
|
+
lang?: string;
|
|
19
|
+
scope?: boolean;
|
|
20
|
+
line?: number;
|
|
21
|
+
column?: number;
|
|
22
|
+
cached?: boolean;
|
|
23
|
+
others?: boolean;
|
|
24
|
+
ignored?: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const REFS_PROMPT_METADATA = defineToolPromptMetadata({
|
|
28
|
+
promptUrl: new URL("../prompts/refs.md", import.meta.url),
|
|
29
|
+
promptSnippet: "Find references to an identifier with readseek and return edit-ready anchors",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export function isRefsAvailable(): boolean {
|
|
33
|
+
return isReadseekAvailable();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface RefsToolOptions {
|
|
37
|
+
onFileAnchored?: (absolutePath: string) => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function refsLine(reference: ReadseekReference): RefsOutputLine {
|
|
41
|
+
return {
|
|
42
|
+
line: reference.line,
|
|
43
|
+
hash: reference.line_hash,
|
|
44
|
+
anchor: `${reference.line}:${reference.line_hash}`,
|
|
45
|
+
raw: reference.text,
|
|
46
|
+
display: escapeControlCharsForDisplay(reference.text),
|
|
47
|
+
enclosingSymbol: reference.enclosingSymbol,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function groupReferences(references: ReadseekReference[], cwd: string): RefsOutputFile[] {
|
|
52
|
+
const files = new Map<string, RefsOutputFile>();
|
|
53
|
+
const seen = new Set<string>();
|
|
54
|
+
for (const reference of references) {
|
|
55
|
+
const abs = path.isAbsolute(reference.file) ? reference.file : path.resolve(cwd, reference.file);
|
|
56
|
+
const dedupeKey = `${abs}:${reference.line}:${reference.column}`;
|
|
57
|
+
if (seen.has(dedupeKey)) continue;
|
|
58
|
+
seen.add(dedupeKey);
|
|
59
|
+
let file = files.get(abs);
|
|
60
|
+
if (!file) {
|
|
61
|
+
file = { displayPath: path.relative(cwd, abs) || abs, path: abs, lines: [] };
|
|
62
|
+
files.set(abs, file);
|
|
63
|
+
}
|
|
64
|
+
file.lines.push(refsLine(reference));
|
|
65
|
+
}
|
|
66
|
+
return [...files.values()];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function registerRefsTool(pi: ExtensionAPI, options: RefsToolOptions = {}) {
|
|
70
|
+
const toolConfig = {
|
|
71
|
+
callable: true,
|
|
72
|
+
enabled: true,
|
|
73
|
+
policy: "read-only" as const,
|
|
74
|
+
readOnly: true,
|
|
75
|
+
pythonName: "refs",
|
|
76
|
+
defaultExposure: "opt-in" as const,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const tool = {
|
|
80
|
+
name: "refs",
|
|
81
|
+
label: "References",
|
|
82
|
+
description: REFS_PROMPT_METADATA.description,
|
|
83
|
+
promptSnippet: REFS_PROMPT_METADATA.promptSnippet,
|
|
84
|
+
promptGuidelines: REFS_PROMPT_METADATA.promptGuidelines,
|
|
85
|
+
parameters: Type.Object({
|
|
86
|
+
name: Type.String({ description: "Identifier to find references for" }),
|
|
87
|
+
path: Type.Optional(Type.String({ description: "Search path" })),
|
|
88
|
+
lang: Type.Optional(Type.String({ description: "Language hint" })),
|
|
89
|
+
scope: Type.Optional(Type.Boolean({ description: "Restrict to the binding under line/column (single file)" })),
|
|
90
|
+
line: Type.Optional(Type.Number({ description: "One-based cursor line, used with scope" })),
|
|
91
|
+
column: Type.Optional(Type.Number({ description: "One-based cursor byte column, used with scope" })),
|
|
92
|
+
cached: Type.Optional(Type.Boolean({ description: "In a Git repository, search tracked/indexed files" })),
|
|
93
|
+
others: Type.Optional(Type.Boolean({ description: "In a Git repository, search untracked files" })),
|
|
94
|
+
ignored: Type.Optional(Type.Boolean({ description: "With others=true, include ignored untracked files" })),
|
|
95
|
+
}),
|
|
96
|
+
ptc: toolConfig,
|
|
97
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
98
|
+
const p = params as RefsParams;
|
|
99
|
+
if (p.ignored && !p.others) {
|
|
100
|
+
return buildToolErrorResult("refs", "invalid-parameter", "Error: refs parameter 'ignored' requires 'others'");
|
|
101
|
+
}
|
|
102
|
+
if (p.scope && p.line === undefined) {
|
|
103
|
+
return buildToolErrorResult("refs", "invalid-parameter", "Error: refs parameter 'scope' requires 'line'");
|
|
104
|
+
}
|
|
105
|
+
const searchPath = resolveToCwd(p.path ?? ".", ctx.cwd);
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
await fsStat(searchPath);
|
|
109
|
+
} catch (err: any) {
|
|
110
|
+
if (err?.code === "ENOENT") {
|
|
111
|
+
return buildToolErrorResult("refs", "path-not-found", `Error: path '${p.path ?? "."}' does not exist`, {
|
|
112
|
+
path: p.path ?? searchPath,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
if (err?.code === "EACCES" || err?.code === "EPERM") {
|
|
116
|
+
return buildToolErrorResult("refs", "permission-denied", `Error: permission denied for path '${p.path ?? "."}'`, {
|
|
117
|
+
path: p.path ?? searchPath,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
const message = `Error: could not access path '${p.path ?? "."}': ${err?.message ?? String(err)}`;
|
|
121
|
+
return buildToolErrorResult("refs", "fs-error", message, {
|
|
122
|
+
path: p.path ?? searchPath,
|
|
123
|
+
details: { fsCode: err?.code, fsMessage: err?.message },
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const references = await readseekRefs(searchPath, p.name, {
|
|
129
|
+
scope: p.scope,
|
|
130
|
+
line: p.line,
|
|
131
|
+
column: p.column,
|
|
132
|
+
language: p.lang,
|
|
133
|
+
cached: p.cached,
|
|
134
|
+
others: p.others,
|
|
135
|
+
ignored: p.ignored,
|
|
136
|
+
signal,
|
|
137
|
+
});
|
|
138
|
+
const files = groupReferences(references, ctx.cwd);
|
|
139
|
+
const builtOutput = buildRefsOutput({ name: p.name, files });
|
|
140
|
+
for (const file of files) {
|
|
141
|
+
options.onFileAnchored?.(file.path);
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
content: [{ type: "text", text: builtOutput.text }],
|
|
145
|
+
details: { readseekValue: builtOutput.readseekValue },
|
|
146
|
+
};
|
|
147
|
+
} catch (err: any) {
|
|
148
|
+
const message = String(err?.message || err);
|
|
149
|
+
const missingReadseek = err?.code === "ENOENT" || /Cannot find package|Cannot find module|no such file/i.test(message);
|
|
150
|
+
return buildToolErrorResult(
|
|
151
|
+
"refs",
|
|
152
|
+
missingReadseek ? "readseek-not-installed" : "readseek-execution-error",
|
|
153
|
+
message,
|
|
154
|
+
missingReadseek ? { hint: "Run npm install to install @jarkkojs/readseek." } : {},
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
renderCall(args: any, theme: any, ...rest: any[]) {
|
|
159
|
+
const context = rest[0] ?? {};
|
|
160
|
+
let text = `${renderToolLabel(theme, "refs")} ${theme.fg("accent", args.name)}`;
|
|
161
|
+
text += theme.fg("dim", ` in ${args.path ?? "."}`);
|
|
162
|
+
if (args.lang) text += theme.fg("dim", ` (${args.lang})`);
|
|
163
|
+
const flags = [args.scope && "scope", args.cached && "cached", args.others && "others", args.ignored && "ignored"].filter(Boolean);
|
|
164
|
+
if (flags.length > 0) text += theme.fg("dim", ` [${flags.join(",")}]`);
|
|
165
|
+
return new Text(clampLineToWidth(text, context.width), 0, 0);
|
|
166
|
+
},
|
|
167
|
+
renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
|
|
168
|
+
const { isPartial, isError, expanded, cwd, width } = resolveRenderResultContext(options, rest);
|
|
169
|
+
|
|
170
|
+
if (isPartial) return new Text(clampLinesToWidth([summaryLine("pending refs")], width).join("\n"), 0, 0);
|
|
171
|
+
|
|
172
|
+
const content = result.content?.[0];
|
|
173
|
+
const textContent = content?.type === "text" ? content.text : "";
|
|
174
|
+
if (isError || result.isError) {
|
|
175
|
+
const firstLine = textContent.split("\n")[0] || "Error";
|
|
176
|
+
const body = expanded && textContent ? textContent : firstLine;
|
|
177
|
+
return new Text(clampLinesToWidth(summaryLine(body).split("\n"), width).join("\n"), 0, 0);
|
|
178
|
+
}
|
|
179
|
+
const readseekValue = (result.details as any)?.readseekValue as
|
|
180
|
+
| { tool: "refs"; files: Array<{ path: string; lines: any[] }> }
|
|
181
|
+
| undefined;
|
|
182
|
+
const files = readseekValue?.files ?? [];
|
|
183
|
+
if (files.length === 0) return new Text(summaryLine("no references"), 0, 0);
|
|
184
|
+
const fileCount = files.length;
|
|
185
|
+
const totalRefs = files.reduce((sum: number, f: any) => sum + f.lines.length, 0);
|
|
186
|
+
const refWord = totalRefs === 1 ? "reference" : "references";
|
|
187
|
+
const fileWord = fileCount === 1 ? "file" : "files";
|
|
188
|
+
let text = summaryLine(`${totalRefs} ${refWord} in ${fileCount} ${fileWord}`, { hidden: !expanded });
|
|
189
|
+
if (expanded) {
|
|
190
|
+
for (const file of files.slice(0, 20)) {
|
|
191
|
+
const display = path.relative(cwd, file.path) || file.path;
|
|
192
|
+
text += "\n" + theme.fg("dim", ` ${display} (${file.lines.length})`);
|
|
193
|
+
}
|
|
194
|
+
if (files.length > 20) text += "\n" + theme.fg("muted", ` … and ${files.length - 20} more files`);
|
|
195
|
+
}
|
|
196
|
+
return new Text(clampLinesToWidth(text.split("\n"), width).join("\n"), 0, 0);
|
|
197
|
+
},
|
|
198
|
+
} satisfies Parameters<ExtensionAPI["registerTool"]>[0] & { ptc: typeof toolConfig };
|
|
199
|
+
|
|
200
|
+
pi.registerTool(tool);
|
|
201
|
+
return tool;
|
|
202
|
+
}
|
package/src/replace-symbol.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readseekMapContent } from "./readseek-client.js";
|
|
2
2
|
import { findSymbol } from "./readseek/symbol-lookup.js";
|
|
3
3
|
import { formatAmbiguous, formatNotFound } from "./readseek/symbol-error-format.js";
|
|
4
4
|
|
|
@@ -32,7 +32,7 @@ function reindent(text: string, indent: string): string {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
export async function replaceSymbol(input: ReplaceSymbolInput): Promise<ReplaceSymbolResult> {
|
|
35
|
-
const map = await
|
|
35
|
+
const map = await readseekMapContent(input.filePath, input.content);
|
|
36
36
|
if (!map) {
|
|
37
37
|
return {
|
|
38
38
|
type: "unsupported",
|
|
@@ -19,7 +19,7 @@ export function resolveSyntaxValidateMode(
|
|
|
19
19
|
): SyntaxValidateMode {
|
|
20
20
|
const fromOpt = coerce(opts.syntaxValidate);
|
|
21
21
|
if (fromOpt) return fromOpt;
|
|
22
|
-
const fromEnv = coerce(process.env.
|
|
22
|
+
const fromEnv = coerce(process.env.READSEEK_SYNTAX_VALIDATE);
|
|
23
23
|
if (fromEnv) return fromEnv;
|
|
24
24
|
return DEFAULT;
|
|
25
25
|
}
|
|
@@ -8,6 +8,7 @@ const COMPACT_DESCRIPTIONS: Record<string, string> = {
|
|
|
8
8
|
|
|
9
9
|
"write.md": "Create or overwrite a complete file and return anchors.",
|
|
10
10
|
"sg.md": "Search code by AST pattern and return anchored matches.",
|
|
11
|
+
"refs.md": "Find references to an identifier and return anchored usages with enclosing symbols.",
|
|
11
12
|
};
|
|
12
13
|
|
|
13
14
|
const COMPACT_GUIDELINES: Record<string, string[]> = {
|
|
@@ -33,6 +34,10 @@ const COMPACT_GUIDELINES: Record<string, string[]> = {
|
|
|
33
34
|
"Use search for AST-shaped code patterns.",
|
|
34
35
|
"Use grep instead of search for plain text.",
|
|
35
36
|
],
|
|
37
|
+
"refs.md": [
|
|
38
|
+
"Use refs to find every usage of an identifier before renaming or deleting it.",
|
|
39
|
+
"Use refs with scope plus line/column to follow a specific binding instead of every same-named identifier.",
|
|
40
|
+
],
|
|
36
41
|
};
|
|
37
42
|
|
|
38
43
|
export interface ToolPromptMetadata {
|
package/src/readseek/mapper.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { stat } from "node:fs/promises";
|
|
2
|
-
|
|
3
|
-
import { readseekMap, readseekMapContent } from "../readseek-client.js";
|
|
4
|
-
import { throwIfAborted } from "../runtime.js";
|
|
5
|
-
import type { FileMap, MapOptions } from "./types.js";
|
|
6
|
-
|
|
7
|
-
export async function generateMap(
|
|
8
|
-
filePath: string,
|
|
9
|
-
options: MapOptions = {},
|
|
10
|
-
): Promise<FileMap | null> {
|
|
11
|
-
throwIfAborted(options.signal);
|
|
12
|
-
const fileStat = await stat(filePath);
|
|
13
|
-
throwIfAborted(options.signal);
|
|
14
|
-
const map = await readseekMap(filePath, fileStat.size, { signal: options.signal });
|
|
15
|
-
throwIfAborted(options.signal);
|
|
16
|
-
return map;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export async function generateMapFromContent(
|
|
20
|
-
filePath: string,
|
|
21
|
-
content: string,
|
|
22
|
-
options: MapOptions = {},
|
|
23
|
-
): Promise<FileMap | null> {
|
|
24
|
-
throwIfAborted(options.signal);
|
|
25
|
-
const map = await readseekMapContent(filePath, content, { signal: options.signal });
|
|
26
|
-
throwIfAborted(options.signal);
|
|
27
|
-
return map;
|
|
28
|
-
}
|