pi-readseek 0.3.15 → 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 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.15",
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.14",
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
- - `PI_HASHLINE_SYNTAX_VALIDATE` can set the default mode.
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
 
@@ -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
- // generateMapFromContent is invoked at most once per replace_symbol edit.
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/generateMapFromContent call is needed.
345
+ // replaceSymbol/readseekMapContent call is needed.
346
346
  const replaceSymbolWarnings: string[] = [];
347
347
  if (rsProbeResults.length > 0) {
348
348
  const lines = originalNormalized.split("\n");
@@ -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 hashline env knobs.
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.PI_HASHLINE_GREP_MAX_LINES,
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.PI_HASHLINE_GREP_MAX_BYTES,
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 { generateMap } from "./readseek/mapper.js";
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 generateMap(absPath);
63
+ const map = await readseekMap(absPath, fileStat.size);
64
64
  rememberInMemory(absPath, { mtimeMs, map });
65
65
  return map;
66
66
  })()
@@ -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
  */
@@ -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", "--path", filePath], { signal: options.signal, stdin: content }),
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
+ }
@@ -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.PI_HASHLINE_EDIT_DIFF_DISPLAY;
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
+ }
@@ -1,4 +1,4 @@
1
- import { generateMapFromContent } from "./readseek/mapper.js";
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 generateMapFromContent(input.filePath, input.content);
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.PI_HASHLINE_SYNTAX_VALIDATE);
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 {
@@ -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
- }