pi-readseek 0.3.20 → 0.3.21

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/index.ts CHANGED
@@ -5,22 +5,22 @@ import { registerGrepTool } from "./src/grep.js";
5
5
  import { registerSgTool, isSgAvailable } from "./src/sg.js";
6
6
  import { registerRefsTool } from "./src/refs.js";
7
7
  import { registerWriteTool } from "./src/write.js";
8
+ import { SessionAnchors } from "./src/session-anchors.js";
9
+
8
10
  export default function piReadseekExtension(pi: ExtensionAPI): void {
9
- const readPaths = new Set<string>();
10
- const noteRead = (absolutePath: string) => {
11
- readPaths.add(absolutePath);
12
- };
13
- const wasReadInSession = (absolutePath: string) => readPaths.has(absolutePath);
11
+ const sessionAnchors = new SessionAnchors();
12
+ const markAnchored = (absolutePath: string) => sessionAnchors.markAnchored(absolutePath);
13
+ const hasFreshAnchors = (absolutePath: string) => sessionAnchors.hasFreshAnchors(absolutePath);
14
14
 
15
- registerReadTool(pi, { onSuccessfulRead: noteRead });
16
- registerEditTool(pi, { wasReadInSession });
15
+ registerReadTool(pi, { onSuccessfulRead: markAnchored });
16
+ registerEditTool(pi, { wasReadInSession: hasFreshAnchors });
17
17
  const sgAvailable = isSgAvailable();
18
18
  const searchGuideline = sgAvailable
19
19
  ? "Use grep summary for counts; use search for structural code patterns."
20
20
  : "Use grep summary for counts; install @jarkkojs/readseek to enable search.";
21
21
 
22
- registerGrepTool(pi, { searchGuideline, onFileAnchored: noteRead });
23
- registerSgTool(pi, { onFileAnchored: noteRead });
24
- registerRefsTool(pi, { onFileAnchored: noteRead });
25
- registerWriteTool(pi, { onFileAnchored: noteRead });
22
+ registerGrepTool(pi, { searchGuideline, onFileAnchored: markAnchored });
23
+ registerSgTool(pi, { onFileAnchored: markAnchored });
24
+ registerRefsTool(pi, { onFileAnchored: markAnchored });
25
+ registerWriteTool(pi, { onFileAnchored: markAnchored });
26
26
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-readseek",
3
- "version": "0.3.20",
3
+ "version": "0.3.21",
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": {
package/src/edit.ts CHANGED
@@ -22,6 +22,7 @@ import { buildEditPreviewKey, buildPendingEditPreviewData, resolvePendingDiffPre
22
22
  import { buildDiffData, type DiffBlockRange } from "./diff-data.js";
23
23
  import { clampLineToWidth, clampLinesToWidth, linkToolPath, resolveRenderResultContext, summaryLine } from "./tui-render-utils.js";
24
24
  import { DiffPreviewComponent } from "./tui-diff-component.js";
25
+ import type { FreshAnchorsPredicate } from "./tool-types.js";
25
26
 
26
27
  import { resolveEditDiffDisplay } from "./readseek-settings.js";
27
28
 
@@ -134,7 +135,7 @@ function mapEditFileError(err: any, filePath: string, displayPath: string, phase
134
135
  }
135
136
 
136
137
  export interface EditToolOptions {
137
- wasReadInSession?: (absolutePath: string) => boolean;
138
+ wasReadInSession?: FreshAnchorsPredicate;
138
139
  syntaxValidate?: SyntaxValidateOptions["syntaxValidate"];
139
140
  }
140
141
 
@@ -142,7 +143,7 @@ export interface ExecuteEditOptions {
142
143
  params: unknown;
143
144
  signal: AbortSignal | undefined;
144
145
  cwd: string;
145
- wasReadInSession?: (absolutePath: string) => boolean;
146
+ wasReadInSession?: FreshAnchorsPredicate;
146
147
  syntaxValidate?: SyntaxValidateOptions["syntaxValidate"];
147
148
  }
148
149
 
package/src/grep.ts CHANGED
@@ -18,6 +18,7 @@ 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
20
  import { clampLineToWidth, clampLinesToWidth, linkToolPath, renderToolLabel, resolveRenderResultContext, summaryLine } from "./tui-render-utils.js";
21
+ import type { FileAnchoredCallback } from "./tool-types.js";
21
22
 
22
23
  const GREP_PROMPT_METADATA = defineToolPromptMetadata({
23
24
  promptUrl: new URL("../prompts/grep.md", import.meta.url),
@@ -180,7 +181,7 @@ function escapeForRegex(s: string): string {
180
181
 
181
182
  interface GrepToolOptions {
182
183
  searchGuideline?: string;
183
- onFileAnchored?: (absolutePath: string) => void;
184
+ onFileAnchored?: FileAnchoredCallback;
184
185
  }
185
186
 
186
187
  export interface ExecuteGrepOptions {
@@ -189,7 +190,7 @@ export interface ExecuteGrepOptions {
189
190
  signal: AbortSignal | undefined;
190
191
  onUpdate: any;
191
192
  cwd: string;
192
- onFileAnchored?: (absolutePath: string) => void;
193
+ onFileAnchored?: FileAnchoredCallback;
193
194
  }
194
195
 
195
196
  export async function executeGrep(opts: ExecuteGrepOptions): Promise<any> {
package/src/read.ts CHANGED
@@ -27,6 +27,7 @@ import { readseekRead } from "./readseek-client.js";
27
27
  import { Text } from "@earendil-works/pi-tui";
28
28
  import { formatReadCallText, formatReadResultText } from "./read-render-helpers.js";
29
29
  import { clampLineToWidth, clampLinesToWidth, linkToolPath, renderToolLabel, resolveRenderResultContext, summaryLine, wrapReadHashlinesForWidth } from "./tui-render-utils.js";
30
+ import type { FileAnchoredCallback } from "./tool-types.js";
30
31
 
31
32
  const READ_PROMPT_METADATA = defineToolPromptMetadata({
32
33
  promptUrl: new URL("../prompts/read.md", import.meta.url),
@@ -43,7 +44,7 @@ interface ReadParams {
43
44
  }
44
45
 
45
46
  interface ReadToolOptions {
46
- onSuccessfulRead?: (absolutePath: string) => void;
47
+ onSuccessfulRead?: FileAnchoredCallback;
47
48
  }
48
49
 
49
50
  export interface ExecuteReadOptions {
@@ -52,7 +53,7 @@ export interface ExecuteReadOptions {
52
53
  signal: AbortSignal | undefined;
53
54
  onUpdate: any;
54
55
  cwd: string;
55
- onSuccessfulRead?: (absolutePath: string) => void;
56
+ onSuccessfulRead?: FileAnchoredCallback;
56
57
  }
57
58
 
58
59
  export async function executeRead(opts: ExecuteReadOptions): Promise<AgentToolResult<any>> {
package/src/refs.ts CHANGED
@@ -8,6 +8,7 @@ import { buildReadseekLineWithHash, buildToolErrorResult } from "./readseek-valu
8
8
  import { resolveToCwd } from "./path-utils.js";
9
9
  import { isReadseekAvailable, readseekRefs, type ReadseekReference } from "./readseek-client.js";
10
10
  import { buildRefsOutput, type RefsOutputFile, type RefsOutputLine } from "./refs-output.js";
11
+ import type { FileAnchoredCallback } from "./tool-types.js";
11
12
 
12
13
  import { clampLineToWidth, renderAnchoredFilesResult, renderToolLabel } from "./tui-render-utils.js";
13
14
 
@@ -33,7 +34,17 @@ export function isRefsAvailable(): boolean {
33
34
  }
34
35
 
35
36
  interface RefsToolOptions {
36
- onFileAnchored?: (absolutePath: string) => void;
37
+ onFileAnchored?: FileAnchoredCallback;
38
+ }
39
+
40
+ /**
41
+ * Inputs for executing the references tool without registering it.
42
+ */
43
+ export interface ExecuteRefsOptions {
44
+ params: unknown;
45
+ signal: AbortSignal | undefined;
46
+ cwd: string;
47
+ onFileAnchored?: FileAnchoredCallback;
37
48
  }
38
49
 
39
50
  function refsLine(reference: ReadseekReference): RefsOutputLine {
@@ -61,6 +72,72 @@ function groupReferences(references: ReadseekReference[], cwd: string): RefsOutp
61
72
  return [...files.values()];
62
73
  }
63
74
 
75
+ /**
76
+ * Executes identifier reference lookup and returns readseek-anchored matches.
77
+ */
78
+ export async function executeRefs(opts: ExecuteRefsOptions): Promise<any> {
79
+ const { params, signal, cwd, onFileAnchored } = opts;
80
+ const p = params as RefsParams;
81
+ if (p.ignored && !p.others) {
82
+ return buildToolErrorResult("refs", "invalid-parameter", "Error: refs parameter 'ignored' requires 'others'");
83
+ }
84
+ if (p.scope && p.line === undefined) {
85
+ return buildToolErrorResult("refs", "invalid-parameter", "Error: refs parameter 'scope' requires 'line'");
86
+ }
87
+ const searchPath = resolveToCwd(p.path ?? ".", cwd);
88
+
89
+ try {
90
+ await fsStat(searchPath);
91
+ } catch (err: any) {
92
+ if (err?.code === "ENOENT") {
93
+ return buildToolErrorResult("refs", "path-not-found", `Error: path '${p.path ?? "."}' does not exist`, {
94
+ path: p.path ?? searchPath,
95
+ });
96
+ }
97
+ if (err?.code === "EACCES" || err?.code === "EPERM") {
98
+ return buildToolErrorResult("refs", "permission-denied", `Error: permission denied for path '${p.path ?? "."}'`, {
99
+ path: p.path ?? searchPath,
100
+ });
101
+ }
102
+ const message = `Error: could not access path '${p.path ?? "."}': ${err?.message ?? String(err)}`;
103
+ return buildToolErrorResult("refs", "fs-error", message, {
104
+ path: p.path ?? searchPath,
105
+ details: { fsCode: err?.code, fsMessage: err?.message },
106
+ });
107
+ }
108
+
109
+ try {
110
+ const references = await readseekRefs(searchPath, p.name, {
111
+ scope: p.scope,
112
+ line: p.line,
113
+ column: p.column,
114
+ language: p.lang,
115
+ cached: p.cached,
116
+ others: p.others,
117
+ ignored: p.ignored,
118
+ signal,
119
+ });
120
+ const files = groupReferences(references, cwd);
121
+ const builtOutput = buildRefsOutput({ name: p.name, files });
122
+ for (const file of files) {
123
+ onFileAnchored?.(file.path);
124
+ }
125
+ return {
126
+ content: [{ type: "text", text: builtOutput.text }],
127
+ details: { readseekValue: builtOutput.readseekValue },
128
+ };
129
+ } catch (err: any) {
130
+ const message = String(err?.message || err);
131
+ const missingReadseek = err?.code === "ENOENT" || /Cannot find package|Cannot find module|no such file/i.test(message);
132
+ return buildToolErrorResult(
133
+ "refs",
134
+ missingReadseek ? "readseek-not-installed" : "readseek-execution-error",
135
+ message,
136
+ missingReadseek ? { hint: "Run npm install to install @jarkkojs/readseek." } : {},
137
+ );
138
+ }
139
+ }
140
+
64
141
  export function registerRefsTool(pi: ExtensionAPI, options: RefsToolOptions = {}) {
65
142
  const toolConfig = {
66
143
  callable: true,
@@ -90,65 +167,7 @@ export function registerRefsTool(pi: ExtensionAPI, options: RefsToolOptions = {}
90
167
  }),
91
168
  ptc: toolConfig,
92
169
  async execute(_toolCallId, params, signal, _onUpdate, ctx) {
93
- const p = params as RefsParams;
94
- if (p.ignored && !p.others) {
95
- return buildToolErrorResult("refs", "invalid-parameter", "Error: refs parameter 'ignored' requires 'others'");
96
- }
97
- if (p.scope && p.line === undefined) {
98
- return buildToolErrorResult("refs", "invalid-parameter", "Error: refs parameter 'scope' requires 'line'");
99
- }
100
- const searchPath = resolveToCwd(p.path ?? ".", ctx.cwd);
101
-
102
- try {
103
- await fsStat(searchPath);
104
- } catch (err: any) {
105
- if (err?.code === "ENOENT") {
106
- return buildToolErrorResult("refs", "path-not-found", `Error: path '${p.path ?? "."}' does not exist`, {
107
- path: p.path ?? searchPath,
108
- });
109
- }
110
- if (err?.code === "EACCES" || err?.code === "EPERM") {
111
- return buildToolErrorResult("refs", "permission-denied", `Error: permission denied for path '${p.path ?? "."}'`, {
112
- path: p.path ?? searchPath,
113
- });
114
- }
115
- const message = `Error: could not access path '${p.path ?? "."}': ${err?.message ?? String(err)}`;
116
- return buildToolErrorResult("refs", "fs-error", message, {
117
- path: p.path ?? searchPath,
118
- details: { fsCode: err?.code, fsMessage: err?.message },
119
- });
120
- }
121
-
122
- try {
123
- const references = await readseekRefs(searchPath, p.name, {
124
- scope: p.scope,
125
- line: p.line,
126
- column: p.column,
127
- language: p.lang,
128
- cached: p.cached,
129
- others: p.others,
130
- ignored: p.ignored,
131
- signal,
132
- });
133
- const files = groupReferences(references, ctx.cwd);
134
- const builtOutput = buildRefsOutput({ name: p.name, files });
135
- for (const file of files) {
136
- options.onFileAnchored?.(file.path);
137
- }
138
- return {
139
- content: [{ type: "text", text: builtOutput.text }],
140
- details: { readseekValue: builtOutput.readseekValue },
141
- };
142
- } catch (err: any) {
143
- const message = String(err?.message || err);
144
- const missingReadseek = err?.code === "ENOENT" || /Cannot find package|Cannot find module|no such file/i.test(message);
145
- return buildToolErrorResult(
146
- "refs",
147
- missingReadseek ? "readseek-not-installed" : "readseek-execution-error",
148
- message,
149
- missingReadseek ? { hint: "Run npm install to install @jarkkojs/readseek." } : {},
150
- );
151
- }
170
+ return executeRefs({ params, signal, cwd: ctx.cwd, onFileAnchored: options.onFileAnchored });
152
171
  },
153
172
  renderCall(args: any, theme: any, ...rest: any[]) {
154
173
  const context = rest[0] ?? {};
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Tracks files that have fresh hashline anchors in the current extension session.
3
+ */
4
+ export class SessionAnchors {
5
+ readonly #paths = new Set<string>();
6
+
7
+ /**
8
+ * Records that a file has produced anchors usable by later edit calls.
9
+ */
10
+ markAnchored(absolutePath: string): void {
11
+ this.#paths.add(absolutePath);
12
+ }
13
+
14
+ /**
15
+ * Returns whether a file has fresh anchors in the current session.
16
+ */
17
+ hasFreshAnchors(absolutePath: string): boolean {
18
+ return this.#paths.has(absolutePath);
19
+ }
20
+ }
package/src/sg.ts CHANGED
@@ -8,6 +8,7 @@ import { buildReadseekLineWithHash, buildToolErrorResult, type ReadseekLine } fr
8
8
  import { resolveToCwd } from "./path-utils.js";
9
9
  import { isReadseekAvailable, readseekSearch, type ReadseekHashline, type ReadseekSearchFileOutput } from "./readseek-client.js";
10
10
  import { buildSgOutput } from "./sg-output.js";
11
+ import type { FileAnchoredCallback } from "./tool-types.js";
11
12
 
12
13
  import { clampLineToWidth, renderAnchoredFilesResult, renderToolLabel } from "./tui-render-utils.js";
13
14
 
@@ -53,7 +54,17 @@ export function isSgAvailable(): boolean {
53
54
  }
54
55
 
55
56
  interface SgToolOptions {
56
- onFileAnchored?: (absolutePath: string) => void;
57
+ onFileAnchored?: FileAnchoredCallback;
58
+ }
59
+
60
+ /**
61
+ * Inputs for executing the structural search tool without registering it.
62
+ */
63
+ export interface ExecuteSgOptions {
64
+ params: unknown;
65
+ signal: AbortSignal | undefined;
66
+ cwd: string;
67
+ onFileAnchored?: FileAnchoredCallback;
57
68
  }
58
69
 
59
70
  function readseekLineFromSearch(line: ReadseekHashline): ReadseekLine {
@@ -87,6 +98,112 @@ function readseekLanguageForPath(language: string | undefined, searchPath: strin
87
98
  return language;
88
99
  }
89
100
 
101
+ /**
102
+ * Executes structural search and returns readseek-anchored matches.
103
+ */
104
+ export async function executeSg(opts: ExecuteSgOptions): Promise<any> {
105
+ const { params, signal, cwd, onFileAnchored } = opts;
106
+ const p = params as SgParams;
107
+ if (p.ignored && !p.others) {
108
+ const message = "Error: search parameter 'ignored' requires 'others'";
109
+ return buildToolErrorResult("search", "invalid-parameter", message);
110
+ }
111
+ const searchPath = resolveToCwd(p.path ?? ".", cwd);
112
+ let searchPathIsFile = false;
113
+
114
+ try {
115
+ const stat = await fsStat(searchPath);
116
+ searchPathIsFile = stat.isFile();
117
+ } catch (err: any) {
118
+ if (err?.code === "ENOENT") {
119
+ const message = `Error: path '${p.path ?? "."}' does not exist`;
120
+ return buildToolErrorResult("search", "path-not-found", message, { path: p.path ?? searchPath });
121
+ }
122
+ if (err?.code === "EACCES" || err?.code === "EPERM") {
123
+ const message = `Error: permission denied for path '${p.path ?? "."}'`;
124
+ return buildToolErrorResult("search", "permission-denied", message, { path: p.path ?? searchPath });
125
+ }
126
+ const message = `Error: could not access path '${p.path ?? "."}': ${err?.message ?? String(err)}`;
127
+ return buildToolErrorResult("search", "fs-error", message, { path: p.path ?? searchPath, details: { fsCode: err?.code, fsMessage: err?.message } });
128
+ }
129
+
130
+ try {
131
+ const effectiveLang = readseekLanguageForPath(p.lang, searchPath, searchPathIsFile);
132
+ const results = await readseekSearch(searchPath, p.pattern, {
133
+ language: effectiveLang,
134
+ cached: p.cached,
135
+ others: p.others,
136
+ ignored: p.ignored,
137
+ signal,
138
+ });
139
+ if (results.length === 0) {
140
+ const emptyOutput = buildSgOutput({ pattern: p.pattern, files: [] });
141
+ return {
142
+ content: [{ type: "text", text: emptyOutput.text }],
143
+ details: {
144
+ readseekValue: emptyOutput.readseekValue,
145
+ },
146
+ };
147
+ }
148
+
149
+ const readseekFiles: Array<{
150
+ displayPath: string;
151
+ path: string;
152
+ ranges: SgRange[];
153
+ lines: ReadseekLine[];
154
+ symbols?: SgEnclosingSymbol[];
155
+ }> = [];
156
+
157
+ for (const result of results) {
158
+ const abs = path.isAbsolute(result.file) ? result.file : path.resolve(cwd, result.file);
159
+ const display = path.relative(cwd, abs) || abs;
160
+ const ranges = result.matches.map((match) => ({ startLine: match.start_line, endLine: match.end_line }));
161
+ const mergedRanges = mergeRanges(ranges);
162
+ const lines = linesFromSearchResult(result, mergedRanges);
163
+ if (lines.length === 0) continue;
164
+ readseekFiles.push({
165
+ displayPath: display,
166
+ path: abs,
167
+ ranges: mergedRanges.map((range) => ({ ...range })),
168
+ lines,
169
+ });
170
+ }
171
+
172
+ if (readseekFiles.length === 0) {
173
+ const emptyOutput = buildSgOutput({ pattern: p.pattern, files: [] });
174
+ return {
175
+ content: [{ type: "text", text: emptyOutput.text }],
176
+ details: {
177
+ readseekValue: emptyOutput.readseekValue,
178
+ },
179
+ };
180
+ }
181
+
182
+ const builtOutput = buildSgOutput({
183
+ pattern: p.pattern,
184
+ files: readseekFiles,
185
+ });
186
+ for (const readseekFile of readseekFiles) {
187
+ onFileAnchored?.(readseekFile.path);
188
+ }
189
+ return {
190
+ content: [{ type: "text", text: builtOutput.text }],
191
+ details: {
192
+ readseekValue: builtOutput.readseekValue,
193
+ },
194
+ };
195
+ } catch (err: any) {
196
+ const message = String(err?.message || err);
197
+ const missingReadseek = err?.code === "ENOENT" || /Cannot find package|Cannot find module|no such file/i.test(message);
198
+ return buildToolErrorResult(
199
+ "search",
200
+ missingReadseek ? "readseek-not-installed" : "readseek-execution-error",
201
+ message,
202
+ missingReadseek ? { hint: "Run npm install to install @jarkkojs/readseek." } : {},
203
+ );
204
+ }
205
+ }
206
+
90
207
  export function registerSgTool(pi: ExtensionAPI, options: SgToolOptions = {}) {
91
208
  const toolConfig = {
92
209
  callable: true,
@@ -113,105 +230,7 @@ export function registerSgTool(pi: ExtensionAPI, options: SgToolOptions = {}) {
113
230
  }),
114
231
  ptc: toolConfig,
115
232
  async execute(_toolCallId, params, signal, _onUpdate, ctx) {
116
- const p = params as SgParams;
117
- if (p.ignored && !p.others) {
118
- const message = "Error: search parameter 'ignored' requires 'others'";
119
- return buildToolErrorResult("search", "invalid-parameter", message);
120
- }
121
- const searchPath = resolveToCwd(p.path ?? ".", ctx.cwd);
122
- let searchPathIsFile = false;
123
-
124
- try {
125
- const stat = await fsStat(searchPath);
126
- searchPathIsFile = stat.isFile();
127
- } catch (err: any) {
128
- if (err?.code === "ENOENT") {
129
- const message = `Error: path '${p.path ?? "."}' does not exist`;
130
- return buildToolErrorResult("search", "path-not-found", message, { path: p.path ?? searchPath });
131
- }
132
- if (err?.code === "EACCES" || err?.code === "EPERM") {
133
- const message = `Error: permission denied for path '${p.path ?? "."}'`;
134
- return buildToolErrorResult("search", "permission-denied", message, { path: p.path ?? searchPath });
135
- }
136
- const message = `Error: could not access path '${p.path ?? "."}': ${err?.message ?? String(err)}`;
137
- return buildToolErrorResult("search", "fs-error", message, { path: p.path ?? searchPath, details: { fsCode: err?.code, fsMessage: err?.message } });
138
- }
139
-
140
- try {
141
- const effectiveLang = readseekLanguageForPath(p.lang, searchPath, searchPathIsFile);
142
- const results = await readseekSearch(searchPath, p.pattern, {
143
- language: effectiveLang,
144
- cached: p.cached,
145
- others: p.others,
146
- ignored: p.ignored,
147
- signal,
148
- });
149
- if (results.length === 0) {
150
- const emptyOutput = buildSgOutput({ pattern: p.pattern, files: [] });
151
- return {
152
- content: [{ type: "text", text: emptyOutput.text }],
153
- details: {
154
- readseekValue: emptyOutput.readseekValue,
155
- },
156
- };
157
- }
158
-
159
- const readseekFiles: Array<{
160
- displayPath: string;
161
- path: string;
162
- ranges: SgRange[];
163
- lines: ReadseekLine[];
164
- symbols?: SgEnclosingSymbol[];
165
- }> = [];
166
-
167
- for (const result of results) {
168
- const abs = path.isAbsolute(result.file) ? result.file : path.resolve(ctx.cwd, result.file);
169
- const display = path.relative(ctx.cwd, abs) || abs;
170
- const ranges = result.matches.map((match) => ({ startLine: match.start_line, endLine: match.end_line }));
171
- const mergedRanges = mergeRanges(ranges);
172
- const lines = linesFromSearchResult(result, mergedRanges);
173
- if (lines.length === 0) continue;
174
- readseekFiles.push({
175
- displayPath: display,
176
- path: abs,
177
- ranges: mergedRanges.map((range) => ({ ...range })),
178
- lines,
179
- });
180
- }
181
-
182
- if (readseekFiles.length === 0) {
183
- const emptyOutput = buildSgOutput({ pattern: p.pattern, files: [] });
184
- return {
185
- content: [{ type: "text", text: emptyOutput.text }],
186
- details: {
187
- readseekValue: emptyOutput.readseekValue,
188
- },
189
- };
190
- }
191
-
192
- const builtOutput = buildSgOutput({
193
- pattern: p.pattern,
194
- files: readseekFiles,
195
- });
196
- for (const readseekFile of readseekFiles) {
197
- options.onFileAnchored?.(readseekFile.path);
198
- }
199
- return {
200
- content: [{ type: "text", text: builtOutput.text }],
201
- details: {
202
- readseekValue: builtOutput.readseekValue,
203
- },
204
- };
205
- } catch (err: any) {
206
- const message = String(err?.message || err);
207
- const missingReadseek = err?.code === "ENOENT" || /Cannot find package|Cannot find module|no such file/i.test(message);
208
- return buildToolErrorResult(
209
- "search",
210
- missingReadseek ? "readseek-not-installed" : "readseek-execution-error",
211
- message,
212
- missingReadseek ? { hint: "Run npm install to install @jarkkojs/readseek." } : {},
213
- );
214
- }
233
+ return executeSg({ params, signal, cwd: ctx.cwd, onFileAnchored: options.onFileAnchored });
215
234
  },
216
235
  renderCall(args: any, theme: any, ...rest: any[]) {
217
236
  const context = rest[0] ?? {};
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Callback used when a tool returns fresh anchors for a file path.
3
+ */
4
+ export type FileAnchoredCallback = (absolutePath: string) => void;
5
+
6
+ /**
7
+ * Predicate used by mutating tools to check whether a file has session-fresh anchors.
8
+ */
9
+ export type FreshAnchorsPredicate = (absolutePath: string) => boolean;
package/src/write.ts CHANGED
@@ -16,6 +16,7 @@ import { generateCompactOrFullDiff, normalizeToLF, hasBareCarriageReturn } from
16
16
  import { buildDiffData, type DiffData } from "./diff-data.js";
17
17
  import { clampLineToWidth, clampLinesToWidth, isRendererExpanded, linkToolPath, renderToolLabel, summaryLine } from "./tui-render-utils.js";
18
18
  import { DiffPreviewComponent } from "./tui-diff-component.js";
19
+ import type { FileAnchoredCallback } from "./tool-types.js";
19
20
 
20
21
  const WRITE_PENDING_PREVIEW_STATE_KEY = "hashline-write-pending-preview";
21
22
 
@@ -113,7 +114,7 @@ function generateWriteDiff(previousContent: string, nextContent: string): { diff
113
114
  }
114
115
 
115
116
  export interface WriteToolOptions {
116
- onFileAnchored?: (absolutePath: string) => void;
117
+ onFileAnchored?: FileAnchoredCallback;
117
118
  }
118
119
 
119
120
  type MappedFsError = {