pi-readseek 0.6.4 → 0.6.5

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
@@ -23,8 +23,8 @@ npm install --save-dev @jarkkojs/readseek
23
23
 
24
24
  ## Tools
25
25
 
26
- - **readSeek_read:** reads text with `LINE:HASH` anchors; images and PDFs can
27
- be returned as base64 or analyzed locally.
26
+ - **readSeek_read:** reads text with `LINE:HASH` anchors; when image modes are
27
+ enabled, images and PDFs can be returned as base64 or analyzed locally.
28
28
  - **readSeek_edit:** edits existing text files using fresh `LINE:HASH` anchors.
29
29
  - **readSeek_grep:** searches text and returns edit-ready anchors.
30
30
  - **readSeek_search:** searches code by structural AST pattern.
@@ -32,6 +32,7 @@ npm install --save-dev @jarkkojs/readseek
32
32
  - **readSeek_rename:** plans or applies binding-aware renames.
33
33
  - **readSeek_hover:** identifies the cursor token and enclosing symbol.
34
34
  - **readSeek_def:** finds structural symbol definitions.
35
+ - **readSeek_check:** checks a source file for parser errors and missing syntax.
35
36
  - **readSeek_write:** creates or overwrites whole files and returns anchors.
36
37
 
37
38
  ## Settings
@@ -64,8 +65,8 @@ optional (defaults shown):
64
65
  Valid values are `"read"`, `"edit"`, `"write"`, and `"grep"`. For a
65
66
  readseek-only file surface, use `["read", "edit", "write", "grep"]`.
66
67
  - **imageMode:** `"auto"` exposes `none`, `ocr`, `caption`, and `objects`;
67
- `"on"` omits `none`; `"off"` skips image/PDF files. Omitting `image` also
68
- skips visual files.
68
+ `"on"` omits `none`; `"off"` removes the `image` parameter and always skips
69
+ image/PDF files. Omitting `image` also skips visual files.
69
70
  - **syntaxValidation:** pre-write syntax-regression check in `readSeek_edit`:
70
71
  `"warn"` writes with a warning, `"block"` aborts without writing, `"off"`
71
72
  skips the check.
package/index.ts CHANGED
@@ -2,12 +2,13 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { registerReadTool } from "./src/read.js";
3
3
  import { registerEditTool } from "./src/edit.js";
4
4
  import { registerGrepTool } from "./src/grep.js";
5
- import { registerSgTool } from "./src/sg.js";
5
+ import { registerSearchTool } from "./src/search.js";
6
6
  import { registerRefsTool } from "./src/refs.js";
7
7
  import { registerRenameTool } from "./src/rename.js";
8
8
  import { registerHoverTool } from "./src/hover.js";
9
9
  import { registerWriteTool } from "./src/write.js";
10
10
  import { registerDefTool } from "./src/def.js";
11
+ import { registerCheckTool } from "./src/check.js";
11
12
  import { SessionAnchors } from "./src/session-anchors.js";
12
13
  import { readSeekBinaryAvailability } from "./src/readseek-client.js";
13
14
  import { resolveReadSeekJsonSettings, type ReadSeekSettingsWarning } from "./src/readseek-settings.js";
@@ -37,6 +38,7 @@ const READSEEK_TOOL_ENTRIES: ReadonlyArray<{ builtIn: ReplacedBuiltIn | null; re
37
38
  { builtIn: null, readSeekName: "readSeek_hover" },
38
39
  { builtIn: "write", readSeekName: "readSeek_write" },
39
40
  { builtIn: null, readSeekName: "readSeek_def" },
41
+ { builtIn: null, readSeekName: "readSeek_check" },
40
42
  ];
41
43
 
42
44
  function formatSettingsWarning(warning: ReadSeekSettingsWarning): string {
@@ -47,6 +49,7 @@ export default function piReadSeekExtension(pi: ExtensionAPI): void {
47
49
  const sessionAnchors = new SessionAnchors();
48
50
  const markAnchored = (absolutePath: string) => sessionAnchors.markAnchored(absolutePath);
49
51
  const hasFreshAnchors = (absolutePath: string) => sessionAnchors.hasFreshAnchors(absolutePath);
52
+ const forgetAnchors = (absolutePath: string) => sessionAnchors.forget(absolutePath);
50
53
 
51
54
  // Resolve replacedTools at load time so the readSeek implementation is
52
55
  // registered under the built-in name (e.g. "edit") when replaced. Extension
@@ -76,16 +79,18 @@ export default function piReadSeekExtension(pi: ExtensionAPI): void {
76
79
  };
77
80
 
78
81
  registerReadTool(pi, { onSuccessfulRead: markAnchored, name: readName });
79
- registerEditTool(pi, { wasReadInSession: hasFreshAnchors, name: editName, toolAliases });
82
+ registerEditTool(pi, { wasReadInSession: hasFreshAnchors, onFileMutated: forgetAnchors, name: editName, toolAliases });
80
83
  registerGrepTool(pi, { onFileAnchored: markAnchored, name: grepName });
81
- registerSgTool(pi, { onFileAnchored: markAnchored });
84
+ registerSearchTool(pi, { onFileAnchored: markAnchored });
82
85
  registerRefsTool(pi, { onFileAnchored: markAnchored });
83
- registerRenameTool(pi);
86
+ registerRenameTool(pi, { onFileMutated: forgetAnchors });
84
87
  registerHoverTool(pi);
85
88
  registerDefTool(pi, { onFileAnchored: markAnchored });
89
+ registerCheckTool(pi);
86
90
  registerWriteTool(pi, { onFileAnchored: markAnchored, name: writeName });
87
91
 
88
92
  pi.on("session_start", (_event, ctx) => {
93
+ sessionAnchors.clear();
89
94
  const { warnings } = resolveReadSeekJsonSettings();
90
95
  const problems = warnings.map(formatSettingsWarning);
91
96
 
@@ -121,4 +126,4 @@ export default function piReadSeekExtension(pi: ExtensionAPI): void {
121
126
  pi.setActiveTools([...new Set(activeTools)]);
122
127
  });
123
128
 
124
- }
129
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-readseek",
3
- "version": "0.6.4",
3
+ "version": "0.6.5",
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.6.4",
42
+ "@jarkkojs/readseek": "^0.6.5",
43
43
  "diff": "^8.0.3",
44
44
  "xxhash-wasm": "^1.1.0"
45
45
  },
@@ -0,0 +1,9 @@
1
+ Check a source file for parser errors and missing syntax. Use after edits for quick syntax validation, not as a compiler or linter.
2
+
3
+ ## Parameters
4
+
5
+ - `path` - source file to check.
6
+
7
+ ## Output
8
+
9
+ Returns parser error and missing-node counts with affected line ranges.
package/prompts/hover.md CHANGED
@@ -1,4 +1,4 @@
1
- Identify the token and enclosing symbol at a cursor. Use before rename or definition lookup, or to identify a line's enclosing symbol; unsaved editor content is included.
1
+ Identify the token and enclosing symbol at a cursor. Use before rename or definition lookup, or to identify a line's enclosing symbol. The file is read from disk.
2
2
 
3
3
  ## Parameters
4
4
 
package/prompts/read.md CHANGED
@@ -14,9 +14,9 @@ Read anchored text by range, map, or symbol, and process images or PDFs with an
14
14
  - `map` — full-file map; incompatible with `symbol` or `bundle`.
15
15
  - `symbol` — `Name`, `Class.method`, or `Name@<line>`; incompatible with `offset` / `limit`.
16
16
  - `bundle` — only `"local"`; requires `symbol` and excludes `map`.
17
- - `image` — an exposed image/PDF mode.
17
+ - `image` — an exposed image/PDF mode; unavailable when `imageMode` is `"off"`.
18
18
 
19
- Default cap: {{DEFAULT_MAX_LINES}} lines or {{DEFAULT_MAX_BYTES}}. Omitting `image` skips images and PDFs.
19
+ Default cap: {{DEFAULT_MAX_LINES}} lines or {{DEFAULT_MAX_BYTES}}. Omitting `image` skips images and PDFs; when `imageMode` is `"off"`, visual files are always skipped.
20
20
 
21
21
  Truncated full-file reads append a map when available. Use its ranges for follow-up reads.
22
22
 
package/prompts/write.md CHANGED
@@ -2,8 +2,8 @@ Create or replace a complete file and return `LINE:HASH` anchors. Use for new or
2
2
 
3
3
  ## Use / avoid
4
4
 
5
- Existing files are overwritten without confirmation. Binary-looking content gets
6
- no anchors.
5
+ Existing files are overwritten without confirmation. Binary-looking content is
6
+ rejected without writing.
7
7
 
8
8
  ## Parameters
9
9
 
@@ -13,6 +13,7 @@ no anchors.
13
13
 
14
14
  ## Output
15
15
 
16
- Text writes return `LINE:HASH|content`. Visible output is capped at 2000 lines or
17
- 50 KB; full anchors remain in `readSeekValue`. Results also include a compact
16
+ Text writes return `LINE:HASH|content`. Visible output is capped at
17
+ {{DEFAULT_MAX_LINES}} lines or {{DEFAULT_MAX_BYTES}}; full anchors remain in
18
+ `readSeekValue`. Results also include a compact
18
19
  diff, unified patch, and structured `details.diffData`.
package/src/check.ts ADDED
@@ -0,0 +1,92 @@
1
+ import { readFile } from "node:fs/promises";
2
+
3
+ import type { ExtensionAPI, ToolRenderResultOptions } from "@earendil-works/pi-coding-agent";
4
+ import { Text } from "@earendil-works/pi-tui";
5
+ import { Type } from "@sinclair/typebox";
6
+
7
+ import { classifyReadSeekFailure, readSeekCheck, type ReadSeekCheckOutput } from "./readseek-client.js";
8
+ import { buildToolErrorResult } from "./readseek-value.js";
9
+ import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
10
+ import { formatFsError } from "./fs-error.js";
11
+ import { resolveToCwd } from "./path-utils.js";
12
+ import { filePathParam, registerReadSeekTool } from "./register-tool.js";
13
+ import { clampLineToWidth, clampLinesToWidth, linkToolPath, renderErrorResult, renderPendingResult, resolveRenderResultContext, summaryLine } from "./tui-render-utils.js";
14
+
15
+ const CHECK_PROMPT_METADATA = defineToolPromptMetadata({
16
+ promptUrl: new URL("../prompts/check.md", import.meta.url),
17
+ promptSnippet: "Check a source file for parser errors and missing syntax",
18
+ });
19
+
20
+ export async function executeCheck(opts: {
21
+ params: unknown;
22
+ signal: AbortSignal | undefined;
23
+ cwd: string;
24
+ }): Promise<any> {
25
+ const { params, signal, cwd } = opts;
26
+ const p = params as { path: string };
27
+ const filePath = resolveToCwd(p.path, cwd);
28
+
29
+ let content: string;
30
+ try {
31
+ content = await readFile(filePath, "utf-8");
32
+ } catch (err: any) {
33
+ const { code, message } = formatFsError(err, "check-error");
34
+ return buildToolErrorResult("check", code, message, { path: p.path });
35
+ }
36
+
37
+ try {
38
+ const output = await readSeekCheck(filePath, content, { signal });
39
+ const lines = output.diagnostics.map((diagnostic) =>
40
+ `${diagnostic.kind}: lines ${diagnostic.start_line}-${diagnostic.end_line}`,
41
+ );
42
+ const summary = output.errorCount === 0 && output.missingCount === 0
43
+ ? "No syntax errors found."
44
+ : `${output.errorCount} error(s), ${output.missingCount} missing syntax node(s)`;
45
+ return {
46
+ content: [{ type: "text", text: [summary, ...lines].join("\n") }],
47
+ details: {
48
+ readSeekValue: { tool: "check", ok: true, path: filePath, output },
49
+ },
50
+ };
51
+ } catch (err: any) {
52
+ const failure = classifyReadSeekFailure(err);
53
+ return buildToolErrorResult("check", failure.code, failure.message, failure.hint ? { hint: failure.hint } : {});
54
+ }
55
+ }
56
+
57
+ export function registerCheckTool(pi: ExtensionAPI) {
58
+ return registerReadSeekTool(pi, {
59
+ name: "readSeek_check",
60
+ label: "Check",
61
+ description: CHECK_PROMPT_METADATA.description,
62
+ promptSnippet: CHECK_PROMPT_METADATA.promptSnippet,
63
+ promptGuidelines: CHECK_PROMPT_METADATA.promptGuidelines,
64
+ parameters: Type.Object({ path: filePathParam() }),
65
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
66
+ return executeCheck({ params, signal, cwd: ctx.cwd });
67
+ },
68
+ renderCall(args: any, theme: any, ...rest: any[]) {
69
+ const context = rest[0] ?? {};
70
+ const cwd = context.cwd ?? process.cwd();
71
+ const displayPath = typeof args?.path === "string" ? args.path : "?";
72
+ const text = `${theme.fg("toolTitle", theme.bold("check"))} ${linkToolPath(theme.fg("accent", displayPath), displayPath, cwd)}`;
73
+ return new Text(clampLineToWidth(text, context.width), 0, 0);
74
+ },
75
+ renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
76
+ const { isPartial, isError, expanded, width } = resolveRenderResultContext(options, rest);
77
+ if (isPartial) return renderPendingResult("pending check", width, theme);
78
+ const textContent = result.content?.[0]?.type === "text" ? result.content[0].text : "";
79
+ if (isError || result.isError) {
80
+ return renderErrorResult(textContent, { expanded, width, fallback: "check failed", theme });
81
+ }
82
+ const output = (result.details as any)?.readSeekValue?.output as ReadSeekCheckOutput | undefined;
83
+ const issueCount = (output?.errorCount ?? 0) + (output?.missingCount ?? 0);
84
+ let text = summaryLine(issueCount === 0 ? "syntax valid" : `${issueCount} syntax issue(s)`, {
85
+ theme,
86
+ style: issueCount === 0 ? "success" : "warning",
87
+ });
88
+ if (expanded && textContent) text += `\n${textContent}`;
89
+ return new Text(clampLinesToWidth(text.split("\n"), width).join("\n"), 0, 0);
90
+ },
91
+ });
92
+ }
package/src/def.ts CHANGED
@@ -7,7 +7,7 @@ import { buildReadSeekLineWithHash, buildToolErrorResult } from "./readseek-valu
7
7
  import { resolveToCwd } from "./path-utils.js";
8
8
  import { statSearchPathOrError } from "./stat-search-path.js";
9
9
  import { classifyReadSeekFailure, readSeekDef } from "./readseek-client.js";
10
- import { searchPathParam, langParam, readSeekGitSearchParams } from "./readseek-params.js";
10
+ import { searchPathParam, langParam, readSeekGitSearchParams, validateIgnoredRequiresOthers } from "./readseek-params.js";
11
11
  import { registerReadSeekTool } from "./register-tool.js";
12
12
 
13
13
  import { renderAnchoredFilesResult, renderReadSeekSearchCall } from "./tui-render-utils.js";
@@ -41,6 +41,8 @@ export interface ExecuteDefOptions {
41
41
  export async function executeDef(opts: ExecuteDefOptions): Promise<any> {
42
42
  const { params, signal, cwd, onFileAnchored } = opts;
43
43
  const p = params as DefParams;
44
+ const ignoredError = validateIgnoredRequiresOthers("def", p);
45
+ if (ignoredError) return ignoredError;
44
46
 
45
47
  if (!p.name || !p.name.trim()) {
46
48
  return buildToolErrorResult("def", "invalid-parameter", "readSeek_def requires 'name'");
package/src/edit.ts CHANGED
@@ -26,7 +26,7 @@ import { buildEditPreviewKey, buildPendingEditPreviewData, resolvePendingDiffPre
26
26
  import { buildDiffData, type DiffBlockRange } from "./diff-data.js";
27
27
  import { clampLineToWidth, clampLinesToWidth, linkToolPath, renderPendingResult, resolveRenderResultContext, summaryLine } from "./tui-render-utils.js";
28
28
  import { upsertDiffComponent, upsertTextComponent } from "./tui-diff-component.js";
29
- import type { FreshAnchorsPredicate } from "./tool-types.js";
29
+ import type { FileMutatedCallback, FreshAnchorsPredicate } from "./tool-types.js";
30
30
  import { filePathParam, registerReadSeekTool } from "./register-tool.js";
31
31
 
32
32
  const EDIT_PENDING_PREVIEW_STATE_KEY = "hashline-edit-pending-preview";
@@ -40,19 +40,19 @@ function pendingPreviewLines(summary: string, preview: PendingDiffPreviewResult
40
40
 
41
41
 
42
42
  const hashlineEditItemSchema = Type.Union([
43
- Type.Object({ set_line: Type.Object({ anchor: Type.String(), new_text: Type.String() }) }, { additionalProperties: true }),
43
+ Type.Object({ set_line: Type.Object({ anchor: Type.String(), new_text: Type.String() }) }, { additionalProperties: false }),
44
44
  Type.Object(
45
45
  { replace_lines: Type.Object({ start_anchor: Type.String(), end_anchor: Type.String(), new_text: Type.String() }) },
46
- { additionalProperties: true },
46
+ { additionalProperties: false },
47
47
  ),
48
- Type.Object({ insert_after: Type.Object({ anchor: Type.String(), new_text: Type.String() }) }, { additionalProperties: true }),
48
+ Type.Object({ insert_after: Type.Object({ anchor: Type.String(), new_text: Type.String() }) }, { additionalProperties: false }),
49
49
  Type.Object(
50
50
  { replace: Type.Object({ old_text: Type.String(), new_text: Type.String(), all: Type.Optional(Type.Boolean()), fuzzy: Type.Optional(Type.Boolean()) }) },
51
- { additionalProperties: true },
51
+ { additionalProperties: false },
52
52
  ),
53
53
  Type.Object(
54
54
  { replace_symbol: Type.Object({ symbol: Type.String(), new_body: Type.String() }) },
55
- { additionalProperties: true },
55
+ { additionalProperties: false },
56
56
  ),
57
57
  ]);
58
58
 
@@ -107,6 +107,7 @@ function mapEditFileError(err: any, filePath: string, _displayPath: string, _pha
107
107
 
108
108
  export interface EditToolOptions {
109
109
  wasReadInSession?: FreshAnchorsPredicate;
110
+ onFileMutated?: FileMutatedCallback;
110
111
  syntaxValidate?: SyntaxValidateOptions["syntaxValidate"];
111
112
  name?: string;
112
113
  toolAliases?: Readonly<Record<string, string>>;
@@ -117,11 +118,12 @@ export interface ExecuteEditOptions {
117
118
  signal: AbortSignal | undefined;
118
119
  cwd: string;
119
120
  wasReadInSession?: FreshAnchorsPredicate;
121
+ onFileMutated?: FileMutatedCallback;
120
122
  syntaxValidate?: SyntaxValidateOptions["syntaxValidate"];
121
123
  }
122
124
 
123
125
  export async function executeEdit(opts: ExecuteEditOptions): Promise<any> {
124
- const { params, signal, cwd, wasReadInSession, syntaxValidate } = opts;
126
+ const { params, signal, cwd, wasReadInSession, onFileMutated, syntaxValidate } = opts;
125
127
  await ensureHashInit();
126
128
  const parsed = params as HashlineParams;
127
129
  const input = params as Record<string, unknown>;
@@ -469,6 +471,7 @@ export async function executeEdit(opts: ExecuteEditOptions): Promise<any> {
469
471
  } catch (err: any) {
470
472
  return mapEditFileError(err, absolutePath, path, "write");
471
473
  }
474
+ onFileMutated?.(absolutePath);
472
475
 
473
476
  if (input.postEditVerify === true) {
474
477
  let verifiedContent: string;
@@ -585,6 +588,7 @@ export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}
585
588
  signal,
586
589
  cwd: ctx.cwd,
587
590
  wasReadInSession: options.wasReadInSession,
591
+ onFileMutated: options.onFileMutated,
588
592
  syntaxValidate: options.syntaxValidate,
589
593
  });
590
594
  },
package/src/hashline.ts CHANGED
@@ -15,7 +15,8 @@ export type HashlineEditItem =
15
15
  | { set_line: { anchor: string; new_text: string } }
16
16
  | { replace_lines: { start_anchor: string; end_anchor: string; new_text: string } }
17
17
  | { insert_after: { anchor: string; new_text: string } }
18
- | { replace: { old_text: string; new_text: string; all?: boolean } };
18
+ | { replace: { old_text: string; new_text: string; all?: boolean; fuzzy?: boolean } }
19
+ | { replace_symbol: { symbol: string; new_body: string } };
19
20
 
20
21
  interface HashMismatch {
21
22
  line: number;
package/src/hover.ts CHANGED
@@ -20,8 +20,8 @@ const HOVER_PROMPT_METADATA = defineToolPromptMetadata({
20
20
 
21
21
  const hoverSchema = Type.Object({
22
22
  path: filePathParam(),
23
- line: Type.Number({ description: "One-based cursor line" }),
24
- column: Type.Optional(Type.Number({ description: "One-based cursor byte column" })),
23
+ line: Type.Integer({ minimum: 1, description: "One-based cursor line" }),
24
+ column: Type.Optional(Type.Integer({ minimum: 1, description: "One-based cursor byte column" })),
25
25
  });
26
26
 
27
27
  interface HoverParams {
@@ -43,6 +43,9 @@ export async function executeHover(opts: ExecuteHoverOptions): Promise<any> {
43
43
  if (!Number.isSafeInteger(p.line) || p.line < 1) {
44
44
  return buildToolErrorResult("hover", "invalid-parameter", "hover parameter 'line' must be a positive integer");
45
45
  }
46
+ if (p.column !== undefined && (!Number.isSafeInteger(p.column) || p.column < 1)) {
47
+ return buildToolErrorResult("hover", "invalid-parameter", "hover parameter 'column' must be a positive integer");
48
+ }
46
49
 
47
50
  const filePath = resolveToCwd(p.path, cwd);
48
51
 
package/src/read.ts CHANGED
@@ -530,12 +530,14 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
530
530
  const tool = registerReadSeekTool(pi, {
531
531
  name,
532
532
  label: "Read",
533
- description: promptMetadata.description,
533
+ description: imageMode === "off"
534
+ ? 'Read anchored text by range, map, or symbol; image and PDF files are skipped because imageMode is "off".'
535
+ : promptMetadata.description,
534
536
  promptSnippet: promptMetadata.promptSnippet,
535
537
  promptGuidelines: [
536
538
  ...promptMetadata.promptGuidelines,
537
539
  imageMode === "off"
538
- ? "Image and PDF reads are disabled."
540
+ ? "The image parameter is unavailable; image and PDF files are always skipped."
539
541
  : `For an image or PDF, explicitly choose image: ${imageModes.join(", ")}; omitting image skips the file.`,
540
542
  ],
541
543
  parameters: Type.Object({
package/src/rename.ts CHANGED
@@ -8,6 +8,7 @@ import { buildToolErrorResult } from "./readseek-value.js";
8
8
  import { resolveToCwd } from "./path-utils.js";
9
9
  import { classifyReadSeekFailure, readSeekRename, type RenameOutput } from "./readseek-client.js";
10
10
  import { filePathParam, registerReadSeekTool } from "./register-tool.js";
11
+ import type { FileMutatedCallback } from "./tool-types.js";
11
12
 
12
13
  import { clampLineToWidth, clampLinesToWidth, linkedPathLine, linkToolPath, renderErrorResult, renderPendingResult, resolveRenderResultContext, summaryLine } from "./tui-render-utils.js";
13
14
 
@@ -18,8 +19,8 @@ const RENAME_PROMPT_METADATA = defineToolPromptMetadata({
18
19
 
19
20
  const renameSchema = Type.Object({
20
21
  path: filePathParam(),
21
- line: Type.Number({ description: "One-based line of the symbol to rename" }),
22
- column: Type.Optional(Type.Number({ description: "One-based byte column for disambiguation" })),
22
+ line: Type.Integer({ minimum: 1, description: "One-based line of the symbol to rename" }),
23
+ column: Type.Optional(Type.Integer({ minimum: 1, description: "One-based byte column for disambiguation" })),
23
24
  to: Type.String({ description: "New symbol name" }),
24
25
  workspace: Type.Optional(Type.Boolean({ description: "Rename across the project" })),
25
26
  apply: Type.Optional(Type.Boolean({ description: "Apply the verified edits; default true" })),
@@ -38,10 +39,11 @@ export interface ExecuteRenameOptions {
38
39
  params: unknown;
39
40
  signal: AbortSignal | undefined;
40
41
  cwd: string;
42
+ onFileMutated?: FileMutatedCallback;
41
43
  }
42
44
 
43
45
  export async function executeRename(opts: ExecuteRenameOptions): Promise<any> {
44
- const { params, signal, cwd } = opts;
46
+ const { params, signal, cwd, onFileMutated } = opts;
45
47
  const p = params as RenameParams;
46
48
 
47
49
  if (!p.to.trim()) {
@@ -50,6 +52,9 @@ export async function executeRename(opts: ExecuteRenameOptions): Promise<any> {
50
52
  if (!Number.isSafeInteger(p.line) || p.line < 1) {
51
53
  return buildToolErrorResult("rename", "invalid-parameter", "rename parameter 'line' must be a positive integer");
52
54
  }
55
+ if (p.column !== undefined && (!Number.isSafeInteger(p.column) || p.column < 1)) {
56
+ return buildToolErrorResult("rename", "invalid-parameter", "rename parameter 'column' must be a positive integer");
57
+ }
53
58
 
54
59
  const filePath = resolveToCwd(p.path, cwd);
55
60
 
@@ -62,6 +67,13 @@ export async function executeRename(opts: ExecuteRenameOptions): Promise<any> {
62
67
  apply: p.apply ?? true,
63
68
  signal,
64
69
  });
70
+ if (output.applied) {
71
+ for (const file of [output, ...output.others]) {
72
+ if (file.edits.length === 0) continue;
73
+ const absoluteFile = path.isAbsolute(file.file) ? file.file : path.resolve(cwd, file.file);
74
+ onFileMutated?.(absoluteFile);
75
+ }
76
+ }
65
77
 
66
78
  const files: string[] = [output.file];
67
79
  for (const other of output.others) {
@@ -106,7 +118,7 @@ export async function executeRename(opts: ExecuteRenameOptions): Promise<any> {
106
118
  }
107
119
  }
108
120
 
109
- export function registerRenameTool(pi: ExtensionAPI) {
121
+ export function registerRenameTool(pi: ExtensionAPI, options: { onFileMutated?: FileMutatedCallback } = {}) {
110
122
  registerReadSeekTool(pi, {
111
123
  name: "readSeek_rename",
112
124
  label: "Rename",
@@ -115,7 +127,7 @@ export function registerRenameTool(pi: ExtensionAPI) {
115
127
  promptGuidelines: RENAME_PROMPT_METADATA.promptGuidelines,
116
128
  parameters: renameSchema,
117
129
  async execute(_toolCallId, params, signal, _onUpdate, ctx) {
118
- return executeRename({ params, signal, cwd: ctx.cwd });
130
+ return executeRename({ params, signal, cwd: ctx.cwd, onFileMutated: options.onFileMutated });
119
131
  },
120
132
  renderCall(args: any, theme: any, ...rest: any[]) {
121
133
  const context = rest[0] ?? {};
@@ -1,6 +1,6 @@
1
1
  import { formatAnchoredFileBlocks, type ReadSeekLine, type ReadSeekRange } from "./readseek-value.js";
2
2
 
3
- export interface SgOutputFile {
3
+ export interface SearchOutputFile {
4
4
  displayPath: string;
5
5
  path: string;
6
6
  ranges: ReadSeekRange[];
@@ -8,12 +8,12 @@ export interface SgOutputFile {
8
8
  symbols?: Array<{ name: string; kind?: string }>;
9
9
  }
10
10
 
11
- export interface BuildSgOutputInput {
11
+ export interface BuildSearchOutputInput {
12
12
  pattern: string;
13
- files: SgOutputFile[];
13
+ files: SearchOutputFile[];
14
14
  }
15
15
 
16
- export interface SgOutputResult {
16
+ export interface SearchOutputResult {
17
17
  text: string;
18
18
  readSeekValue: {
19
19
  tool: "search";
@@ -25,7 +25,7 @@ export interface SgOutputResult {
25
25
  };
26
26
  }
27
27
 
28
- export function buildSgOutput(input: BuildSgOutputInput): SgOutputResult {
28
+ export function buildSearchOutput(input: BuildSearchOutputInput): SearchOutputResult {
29
29
  if (input.files.length === 0) {
30
30
  return {
31
31
  text: `No matches found for pattern: ${input.pattern}`,
@@ -8,31 +8,31 @@ import { buildReadSeekLineWithHash, buildToolErrorResult, type ReadSeekLine } fr
8
8
  import { resolveToCwd } from "./path-utils.js";
9
9
  import { statSearchPathOrError } from "./stat-search-path.js";
10
10
  import { classifyReadSeekFailure, readSeekSearch, type ReadSeekHashline, type ReadSeekSearchFileOutput } from "./readseek-client.js";
11
- import { buildSgOutput } from "./sg-output.js";
11
+ import { buildSearchOutput } from "./search-output.js";
12
12
  import type { FileAnchoredCallback } from "./tool-types.js";
13
13
  import { langParam, readSeekGitSearchParams, searchPathParam, validateIgnoredRequiresOthers } from "./readseek-params.js";
14
14
  import { registerReadSeekTool } from "./register-tool.js";
15
15
 
16
16
  import { renderAnchoredFilesResult, renderReadSeekSearchCall } from "./tui-render-utils.js";
17
17
 
18
- type SgParams = { pattern: string; lang?: string; path?: string; cached?: boolean; others?: boolean; ignored?: boolean };
18
+ type SearchParams = { pattern: string; lang?: string; path?: string; cached?: boolean; others?: boolean; ignored?: boolean };
19
19
 
20
- export interface SgRange {
20
+ export interface SearchRange {
21
21
  startLine: number;
22
22
  endLine: number;
23
23
  }
24
24
 
25
- export interface SgEnclosingSymbol {
25
+ export interface SearchEnclosingSymbol {
26
26
  name: string;
27
27
  kind: string;
28
28
  }
29
29
 
30
- export function mergeRanges(ranges: SgRange[]): SgRange[] {
30
+ export function mergeRanges(ranges: SearchRange[]): SearchRange[] {
31
31
  if (ranges.length === 0) return [];
32
32
  if (ranges.length === 1) return [{ ...ranges[0] }];
33
33
 
34
34
  const sorted = [...ranges].sort((a, b) => a.startLine - b.startLine);
35
- const merged: SgRange[] = [{ ...sorted[0] }];
35
+ const merged: SearchRange[] = [{ ...sorted[0] }];
36
36
 
37
37
  for (let i = 1; i < sorted.length; i++) {
38
38
  const current = sorted[i];
@@ -47,19 +47,19 @@ export function mergeRanges(ranges: SgRange[]): SgRange[] {
47
47
  return merged;
48
48
  }
49
49
 
50
- const SG_PROMPT_METADATA = defineToolPromptMetadata({
51
- promptUrl: new URL("../prompts/sg.md", import.meta.url),
50
+ const SEARCH_PROMPT_METADATA = defineToolPromptMetadata({
51
+ promptUrl: new URL("../prompts/search.md", import.meta.url),
52
52
  promptSnippet: "Search syntax-aware code shapes with AST patterns",
53
53
  });
54
54
 
55
- interface SgToolOptions {
55
+ interface SearchToolOptions {
56
56
  onFileAnchored?: FileAnchoredCallback;
57
57
  }
58
58
 
59
59
  /**
60
60
  * Inputs for executing the structural search tool without registering it.
61
61
  */
62
- export interface ExecuteSgOptions {
62
+ export interface ExecuteSearchOptions {
63
63
  params: unknown;
64
64
  signal: AbortSignal | undefined;
65
65
  cwd: string;
@@ -70,7 +70,7 @@ function readSeekLineFromSearch(line: ReadSeekHashline): ReadSeekLine {
70
70
  return buildReadSeekLineWithHash(line.line, line.hash, line.text);
71
71
  }
72
72
 
73
- function linesFromSearchResult(result: ReadSeekSearchFileOutput, ranges: SgRange[]): ReadSeekLine[] {
73
+ function linesFromSearchResult(result: ReadSeekSearchFileOutput, ranges: SearchRange[]): ReadSeekLine[] {
74
74
  const lineMap = new Map<number, ReadSeekLine>();
75
75
  for (const match of result.matches) {
76
76
  for (const line of match.hashlines) {
@@ -100,9 +100,9 @@ function readSeekLanguageForPath(language: string | undefined, searchPath: strin
100
100
  /**
101
101
  * Executes structural search and returns readseek-anchored matches.
102
102
  */
103
- export async function executeSg(opts: ExecuteSgOptions): Promise<any> {
103
+ export async function executeSearch(opts: ExecuteSearchOptions): Promise<any> {
104
104
  const { params, signal, cwd, onFileAnchored } = opts;
105
- const p = params as SgParams;
105
+ const p = params as SearchParams;
106
106
  const ignoredError = validateIgnoredRequiresOthers("search", p);
107
107
  if (ignoredError) return ignoredError;
108
108
  const searchPath = resolveToCwd(p.path ?? ".", cwd);
@@ -121,7 +121,7 @@ export async function executeSg(opts: ExecuteSgOptions): Promise<any> {
121
121
  signal,
122
122
  });
123
123
  if (results.length === 0) {
124
- const emptyOutput = buildSgOutput({ pattern: p.pattern, files: [] });
124
+ const emptyOutput = buildSearchOutput({ pattern: p.pattern, files: [] });
125
125
  return {
126
126
  content: [{ type: "text", text: emptyOutput.text }],
127
127
  details: {
@@ -133,9 +133,9 @@ export async function executeSg(opts: ExecuteSgOptions): Promise<any> {
133
133
  const readSeekFiles: Array<{
134
134
  displayPath: string;
135
135
  path: string;
136
- ranges: SgRange[];
136
+ ranges: SearchRange[];
137
137
  lines: ReadSeekLine[];
138
- symbols?: SgEnclosingSymbol[];
138
+ symbols?: SearchEnclosingSymbol[];
139
139
  }> = [];
140
140
 
141
141
  for (const result of results) {
@@ -154,7 +154,7 @@ export async function executeSg(opts: ExecuteSgOptions): Promise<any> {
154
154
  }
155
155
 
156
156
  if (readSeekFiles.length === 0) {
157
- const emptyOutput = buildSgOutput({ pattern: p.pattern, files: [] });
157
+ const emptyOutput = buildSearchOutput({ pattern: p.pattern, files: [] });
158
158
  return {
159
159
  content: [{ type: "text", text: emptyOutput.text }],
160
160
  details: {
@@ -163,7 +163,7 @@ export async function executeSg(opts: ExecuteSgOptions): Promise<any> {
163
163
  };
164
164
  }
165
165
 
166
- const builtOutput = buildSgOutput({
166
+ const builtOutput = buildSearchOutput({
167
167
  pattern: p.pattern,
168
168
  files: readSeekFiles,
169
169
  });
@@ -182,13 +182,13 @@ export async function executeSg(opts: ExecuteSgOptions): Promise<any> {
182
182
  }
183
183
  }
184
184
 
185
- export function registerSgTool(pi: ExtensionAPI, options: SgToolOptions = {}) {
185
+ export function registerSearchTool(pi: ExtensionAPI, options: SearchToolOptions = {}) {
186
186
  const tool = registerReadSeekTool(pi, {
187
187
  name: "readSeek_search",
188
188
  label: "Structural Search",
189
- description: SG_PROMPT_METADATA.description,
190
- promptSnippet: SG_PROMPT_METADATA.promptSnippet,
191
- promptGuidelines: SG_PROMPT_METADATA.promptGuidelines,
189
+ description: SEARCH_PROMPT_METADATA.description,
190
+ promptSnippet: SEARCH_PROMPT_METADATA.promptSnippet,
191
+ promptGuidelines: SEARCH_PROMPT_METADATA.promptGuidelines,
192
192
  parameters: Type.Object({
193
193
  pattern: Type.String({ description: "ast-grep pattern, such as console.log($$$ARGS)" }),
194
194
  lang: langParam(),
@@ -196,7 +196,7 @@ export function registerSgTool(pi: ExtensionAPI, options: SgToolOptions = {}) {
196
196
  ...readSeekGitSearchParams(),
197
197
  }),
198
198
  async execute(_toolCallId, params, signal, _onUpdate, ctx) {
199
- return executeSg({ params, signal, cwd: ctx.cwd, onFileAnchored: options.onFileAnchored });
199
+ return executeSearch({ params, signal, cwd: ctx.cwd, onFileAnchored: options.onFileAnchored });
200
200
  },
201
201
  renderCall(args: any, theme: any, ...rest: any[]) {
202
202
  return renderReadSeekSearchCall(args, theme, rest, {
@@ -11,6 +11,20 @@ export class SessionAnchors {
11
11
  this.#paths.add(absolutePath);
12
12
  }
13
13
 
14
+ /**
15
+ * Forgets anchors after a file mutation that did not return current anchors.
16
+ */
17
+ forget(absolutePath: string): void {
18
+ this.#paths.delete(absolutePath);
19
+ }
20
+
21
+ /**
22
+ * Forgets every anchor, such as when resetting extension session state.
23
+ */
24
+ clear(): void {
25
+ this.#paths.clear();
26
+ }
27
+
14
28
  /**
15
29
  * Returns whether a file has fresh anchors in the current session.
16
30
  */
@@ -38,7 +38,7 @@ const COMPACT_GUIDELINES: Record<string, string[]> = {
38
38
  "write.md": [
39
39
  "Use anchored edits rather than readSeek_write for small changes or appends to existing files.",
40
40
  ],
41
- "sg.md": [
41
+ "search.md": [
42
42
  "Use readSeek_search for syntax-aware code shapes; use readSeek_grep for plain text.",
43
43
  ],
44
44
  "refs.md": [
package/src/tool-types.ts CHANGED
@@ -7,3 +7,6 @@ export type FileAnchoredCallback = (absolutePath: string) => void;
7
7
  * Predicate used by mutating tools to check whether a file has session-fresh anchors.
8
8
  */
9
9
  export type FreshAnchorsPredicate = (absolutePath: string) => boolean;
10
+
11
+ /** Callback used after a tool persists a file mutation. */
12
+ export type FileMutatedCallback = (absolutePath: string) => void;
File without changes