pi-readseek 0.6.3 → 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 +5 -4
- package/index.ts +10 -5
- package/package.json +2 -2
- package/prompts/check.md +9 -0
- package/prompts/def.md +2 -6
- package/prompts/edit.md +3 -3
- package/prompts/grep.md +1 -7
- package/prompts/hover.md +1 -6
- package/prompts/read.md +4 -2
- package/prompts/refs.md +1 -4
- package/prompts/rename.md +1 -11
- package/prompts/{sg.md → search.md} +1 -5
- package/prompts/write.md +6 -8
- package/src/check.ts +92 -0
- package/src/def.ts +4 -2
- package/src/edit.ts +15 -9
- package/src/grep.ts +7 -7
- package/src/hashline.ts +2 -1
- package/src/hover.ts +6 -3
- package/src/read.ts +7 -5
- package/src/readseek-params.ts +2 -2
- package/src/refs.ts +4 -4
- package/src/register-tool.ts +1 -1
- package/src/rename.ts +21 -9
- package/src/{sg-output.ts → search-output.ts} +5 -5
- package/src/{sg.ts → search.ts} +25 -25
- package/src/session-anchors.ts +14 -0
- package/src/tool-prompt-metadata.ts +4 -17
- package/src/tool-types.ts +3 -0
- package/src/write.ts +2 -2
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;
|
|
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"`
|
|
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 {
|
|
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
|
-
|
|
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.
|
|
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.
|
|
42
|
+
"@jarkkojs/readseek": "^0.6.5",
|
|
43
43
|
"diff": "^8.0.3",
|
|
44
44
|
"xxhash-wasm": "^1.1.0"
|
|
45
45
|
},
|
package/prompts/check.md
ADDED
|
@@ -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/def.md
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
Find
|
|
2
|
-
directory.
|
|
1
|
+
Find symbol declarations by qualified or unqualified name. Use instead of text search when locating where a function, class, type, or other symbol is defined.
|
|
3
2
|
|
|
4
3
|
## Parameters
|
|
5
4
|
|
|
@@ -8,7 +7,4 @@ directory.
|
|
|
8
7
|
- `lang` — language override.
|
|
9
8
|
- `cached`, `others`, `ignored` — Git file selection; `ignored` requires `others`.
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
- After `readSeek_hover`, use its qualified symbol name.
|
|
14
|
-
- When asked where a function, class, or type is defined.
|
|
10
|
+
After `readSeek_hover`, use its qualified symbol name.
|
package/prompts/edit.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Edit existing text files with fresh `LINE:HASH` anchors
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
Edit existing text files safely with fresh `LINE:HASH` anchors; on `file-not-read`, read or search the file first.
|
|
2
|
+
|
|
3
|
+
Anchors come from `readSeek_read`, `readSeek_grep`, `readSeek_search`, or `readSeek_write`.
|
|
4
4
|
|
|
5
5
|
## Variants
|
|
6
6
|
|
package/prompts/grep.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Search
|
|
1
|
+
Search plain text or regex in files and return edit-ready `LINE:HASH` anchors. Use for identifiers, strings, configuration, errors, comments, or documentation.
|
|
2
2
|
|
|
3
3
|
## Modes
|
|
4
4
|
|
|
@@ -19,10 +19,4 @@ Search file contents. Non-summary results include edit-ready `LINE:HASH` anchors
|
|
|
19
19
|
- `scope` — only `"symbol"` is supported.
|
|
20
20
|
- `scopeContext` — non-negative context within symbol scope; requires `scope: "symbol"`.
|
|
21
21
|
|
|
22
|
-
## Use well
|
|
23
|
-
|
|
24
|
-
Use `readSeek_grep` for text: identifiers, strings, config keys, error messages, comments, or docs. Use `literal: true` unless you want regex behavior.
|
|
25
|
-
|
|
26
|
-
For code shape — calls, imports, declarations, JSX, object literals, control flow — prefer `readSeek_search`, which parses AST patterns.
|
|
27
|
-
|
|
28
22
|
If output says results were truncated at `limit` or by display budget, narrow before editing. Good narrowing order: `summary` → `path`/`glob` → stricter pattern → `scope: "symbol"` or `context`.
|
package/prompts/hover.md
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
Identify the
|
|
2
|
-
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.
|
|
3
2
|
|
|
4
3
|
## Parameters
|
|
5
4
|
|
|
6
5
|
- `path`, `line` — required file path and one-based cursor line.
|
|
7
6
|
- `column` — optional one-based cursor byte column.
|
|
8
|
-
|
|
9
|
-
## When to use
|
|
10
|
-
|
|
11
|
-
- Before rename or go-to-definition, or to identify a line's enclosing symbol.
|
package/prompts/read.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Read
|
|
1
|
+
Read anchored text by range, map, or symbol, and process images or PDFs with an explicit mode.
|
|
2
2
|
|
|
3
3
|
## Choose the right read
|
|
4
4
|
|
|
@@ -14,7 +14,9 @@ Read files through readseek. Text results use `LINE:HASH|content` anchors for `r
|
|
|
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
|
+
|
|
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.
|
|
18
20
|
|
|
19
21
|
Truncated full-file reads append a map when available. Use its ranges for follow-up reads.
|
|
20
22
|
|
package/prompts/refs.md
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
Find identifier
|
|
2
|
-
text search is too broad. Results include edit-ready anchors and enclosing symbols.
|
|
1
|
+
Find identifier usages with enclosing symbols and edit-ready anchors. Use before renaming, deleting, or changing a symbol; add a cursor scope to exclude same-named bindings.
|
|
3
2
|
|
|
4
3
|
## Parameters
|
|
5
4
|
|
|
@@ -23,5 +22,3 @@ shadows.
|
|
|
23
22
|
|
|
24
23
|
In Git repositories, directory search includes tracked/indexed and untracked
|
|
25
24
|
non-ignored files. `ignored` requires `others`.
|
|
26
|
-
|
|
27
|
-
Use `readSeek_grep` for plain text, `readSeek_search` for code shape, and `readSeek_refs` for identifier usage.
|
package/prompts/rename.md
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
Rename
|
|
2
|
-
only occurrences of that declaration are changed.
|
|
1
|
+
Rename the symbol at a cursor without changing same-named bindings. Use for symbol renames; use search and edit for broader refactors.
|
|
3
2
|
|
|
4
3
|
## Parameters
|
|
5
4
|
|
|
@@ -7,12 +6,3 @@ only occurrences of that declaration are changed.
|
|
|
7
6
|
- `column` — optional one-based cursor byte column.
|
|
8
7
|
- `workspace` — expand across the project; local shadows in other files are excluded.
|
|
9
8
|
- `apply` — default `true`; set `false` to return only the verified plan.
|
|
10
|
-
|
|
11
|
-
## When to use
|
|
12
|
-
|
|
13
|
-
- Use when the user requests a symbol rename and the cursor is known.
|
|
14
|
-
|
|
15
|
-
## When not to use
|
|
16
|
-
|
|
17
|
-
- Use `readSeek_grep`/`readSeek_search` for broad replacement and
|
|
18
|
-
`readSeek_edit` for other refactors.
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
Search code with AST patterns
|
|
2
|
-
imports, declarations, JSX, object fields, and control flow. Results include
|
|
3
|
-
edit-ready anchors.
|
|
1
|
+
Search syntax-aware code shapes with AST patterns and return edit-ready anchors. Use for calls, imports, declarations, JSX, object fields, or control flow; use `readSeek_grep` for plain text.
|
|
4
2
|
|
|
5
3
|
## Parameters
|
|
6
4
|
|
|
@@ -35,5 +33,3 @@ punctuation must be valid for the selected language.
|
|
|
35
33
|
In Git repositories, directory search includes tracked/indexed and untracked
|
|
36
34
|
non-ignored files. Use `cached`, `others`, and `ignored` to choose; `ignored`
|
|
37
35
|
requires `others`.
|
|
38
|
-
|
|
39
|
-
Use `readSeek_grep` for plain text and `readSeek_search` for structure.
|
package/prompts/write.md
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
Create or replace a
|
|
1
|
+
Create or replace a complete file and return `LINE:HASH` anchors. Use for new or fully generated files; use anchored edits for small changes.
|
|
2
2
|
|
|
3
3
|
## Use / avoid
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
Existing files are overwritten without confirmation. Binary-looking content gets
|
|
9
|
-
no anchors.
|
|
5
|
+
Existing files are overwritten without confirmation. Binary-looking content is
|
|
6
|
+
rejected without writing.
|
|
10
7
|
|
|
11
8
|
## Parameters
|
|
12
9
|
|
|
@@ -16,6 +13,7 @@ no anchors.
|
|
|
16
13
|
|
|
17
14
|
## Output
|
|
18
15
|
|
|
19
|
-
Text writes return `LINE:HASH|content`. Visible output is capped at
|
|
20
|
-
|
|
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
|
|
21
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";
|
|
@@ -15,7 +15,7 @@ import type { FileAnchoredCallback } from "./tool-types.js";
|
|
|
15
15
|
|
|
16
16
|
const DEF_PROMPT_METADATA = defineToolPromptMetadata({
|
|
17
17
|
promptUrl: new URL("../prompts/def.md", import.meta.url),
|
|
18
|
-
promptSnippet: "Find
|
|
18
|
+
promptSnippet: "Find where a symbol is defined",
|
|
19
19
|
});
|
|
20
20
|
|
|
21
21
|
type DefParams = {
|
|
@@ -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,26 +40,28 @@ 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:
|
|
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:
|
|
46
|
+
{ additionalProperties: false },
|
|
47
47
|
),
|
|
48
|
-
Type.Object({ insert_after: Type.Object({ anchor: Type.String(), new_text: Type.String() }) }, { additionalProperties:
|
|
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:
|
|
51
|
+
{ additionalProperties: false },
|
|
52
52
|
),
|
|
53
53
|
Type.Object(
|
|
54
54
|
{ replace_symbol: Type.Object({ symbol: Type.String(), new_body: Type.String() }) },
|
|
55
|
-
{ additionalProperties:
|
|
55
|
+
{ additionalProperties: false },
|
|
56
56
|
),
|
|
57
57
|
]);
|
|
58
58
|
|
|
59
59
|
const hashlineEditSchema = Type.Object(
|
|
60
60
|
{
|
|
61
61
|
path: filePathParam(),
|
|
62
|
-
edits: Type.Optional(Type.Array(hashlineEditItemSchema, {
|
|
62
|
+
edits: Type.Optional(Type.Array(hashlineEditItemSchema, {
|
|
63
|
+
description: "Edits: set_line, replace_lines, insert_after, replace_symbol, or replace",
|
|
64
|
+
})),
|
|
63
65
|
postEditVerify: Type.Optional(Type.Boolean({
|
|
64
66
|
description: "Verify persisted content after write",
|
|
65
67
|
})),
|
|
@@ -105,6 +107,7 @@ function mapEditFileError(err: any, filePath: string, _displayPath: string, _pha
|
|
|
105
107
|
|
|
106
108
|
export interface EditToolOptions {
|
|
107
109
|
wasReadInSession?: FreshAnchorsPredicate;
|
|
110
|
+
onFileMutated?: FileMutatedCallback;
|
|
108
111
|
syntaxValidate?: SyntaxValidateOptions["syntaxValidate"];
|
|
109
112
|
name?: string;
|
|
110
113
|
toolAliases?: Readonly<Record<string, string>>;
|
|
@@ -115,11 +118,12 @@ export interface ExecuteEditOptions {
|
|
|
115
118
|
signal: AbortSignal | undefined;
|
|
116
119
|
cwd: string;
|
|
117
120
|
wasReadInSession?: FreshAnchorsPredicate;
|
|
121
|
+
onFileMutated?: FileMutatedCallback;
|
|
118
122
|
syntaxValidate?: SyntaxValidateOptions["syntaxValidate"];
|
|
119
123
|
}
|
|
120
124
|
|
|
121
125
|
export async function executeEdit(opts: ExecuteEditOptions): Promise<any> {
|
|
122
|
-
const { params, signal, cwd, wasReadInSession, syntaxValidate } = opts;
|
|
126
|
+
const { params, signal, cwd, wasReadInSession, onFileMutated, syntaxValidate } = opts;
|
|
123
127
|
await ensureHashInit();
|
|
124
128
|
const parsed = params as HashlineParams;
|
|
125
129
|
const input = params as Record<string, unknown>;
|
|
@@ -467,6 +471,7 @@ export async function executeEdit(opts: ExecuteEditOptions): Promise<any> {
|
|
|
467
471
|
} catch (err: any) {
|
|
468
472
|
return mapEditFileError(err, absolutePath, path, "write");
|
|
469
473
|
}
|
|
474
|
+
onFileMutated?.(absolutePath);
|
|
470
475
|
|
|
471
476
|
if (input.postEditVerify === true) {
|
|
472
477
|
let verifiedContent: string;
|
|
@@ -565,7 +570,7 @@ export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}
|
|
|
565
570
|
const name = options.name ?? "readSeek_edit";
|
|
566
571
|
const promptMetadata = defineToolPromptMetadata({
|
|
567
572
|
promptUrl: new URL("../prompts/edit.md", import.meta.url),
|
|
568
|
-
promptSnippet: "
|
|
573
|
+
promptSnippet: "Safely edit with fresh LINE:HASH anchors",
|
|
569
574
|
registeredName: name,
|
|
570
575
|
toolAliases: options.toolAliases,
|
|
571
576
|
});
|
|
@@ -583,6 +588,7 @@ export function registerEditTool(pi: ExtensionAPI, options: EditToolOptions = {}
|
|
|
583
588
|
signal,
|
|
584
589
|
cwd: ctx.cwd,
|
|
585
590
|
wasReadInSession: options.wasReadInSession,
|
|
591
|
+
onFileMutated: options.onFileMutated,
|
|
586
592
|
syntaxValidate: options.syntaxValidate,
|
|
587
593
|
});
|
|
588
594
|
},
|
package/src/grep.ts
CHANGED
|
@@ -26,20 +26,20 @@ import { searchPathParam } from "./readseek-params.js";
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
const grepSchema = Type.Object({
|
|
29
|
-
pattern: Type.String({ description: "
|
|
29
|
+
pattern: Type.String({ description: "Regex pattern; use literal for exact text" }),
|
|
30
30
|
path: searchPathParam(),
|
|
31
|
-
glob: Type.Optional(Type.String({ description: "
|
|
31
|
+
glob: Type.Optional(Type.String({ description: "File-name glob, such as *.ts" })),
|
|
32
32
|
ignoreCase: Type.Optional(Type.Boolean({ description: "Ignore case" })),
|
|
33
33
|
literal: Type.Optional(Type.Boolean({ description: "Treat pattern literally" })),
|
|
34
|
-
context: optionalIntOrString("
|
|
35
|
-
limit: optionalIntOrString("
|
|
34
|
+
context: optionalIntOrString("Surrounding lines for each match"),
|
|
35
|
+
limit: optionalIntOrString("Maximum matches to return"),
|
|
36
36
|
summary: Type.Optional(Type.Boolean({ description: "Return per-file counts" })),
|
|
37
37
|
scope: Type.Optional(
|
|
38
38
|
Type.Literal("symbol", {
|
|
39
|
-
description: "
|
|
39
|
+
description: "Group matches by enclosing symbol",
|
|
40
40
|
}),
|
|
41
41
|
),
|
|
42
|
-
scopeContext: optionalIntOrString("
|
|
42
|
+
scopeContext: optionalIntOrString("Context lines within each symbol"),
|
|
43
43
|
});
|
|
44
44
|
|
|
45
45
|
interface GrepParams {
|
|
@@ -513,7 +513,7 @@ export function registerGrepTool(pi: ExtensionAPI, options: GrepToolOptions = {}
|
|
|
513
513
|
const name = options.name ?? "readSeek_grep";
|
|
514
514
|
const promptMetadata = defineToolPromptMetadata({
|
|
515
515
|
promptUrl: new URL("../prompts/grep.md", import.meta.url),
|
|
516
|
-
promptSnippet: "Search
|
|
516
|
+
promptSnippet: "Search plain text or regex with edit-ready anchors",
|
|
517
517
|
registeredName: name,
|
|
518
518
|
});
|
|
519
519
|
const tool = registerReadSeekTool(pi, {
|
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
|
@@ -15,13 +15,13 @@ import { clampLineToWidth, clampLinesToWidth, linkToolPath, renderErrorResult, r
|
|
|
15
15
|
|
|
16
16
|
const HOVER_PROMPT_METADATA = defineToolPromptMetadata({
|
|
17
17
|
promptUrl: new URL("../prompts/hover.md", import.meta.url),
|
|
18
|
-
promptSnippet: "Identify
|
|
18
|
+
promptSnippet: "Identify the token and enclosing symbol at a cursor",
|
|
19
19
|
});
|
|
20
20
|
|
|
21
21
|
const hoverSchema = Type.Object({
|
|
22
22
|
path: filePathParam(),
|
|
23
|
-
line: Type.
|
|
24
|
-
column: Type.Optional(Type.
|
|
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
|
@@ -524,29 +524,31 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
|
|
|
524
524
|
const imageModes = imageMode === "auto" ? ["none", "ocr", "caption", "objects"] as const : ["ocr", "caption", "objects"] as const;
|
|
525
525
|
const promptMetadata = defineToolPromptMetadata({
|
|
526
526
|
promptUrl: new URL("../prompts/read.md", import.meta.url),
|
|
527
|
-
promptSnippet: "Read
|
|
527
|
+
promptSnippet: "Read anchored text, symbols, maps, images, or PDFs",
|
|
528
528
|
registeredName: name,
|
|
529
529
|
});
|
|
530
530
|
const tool = registerReadSeekTool(pi, {
|
|
531
531
|
name,
|
|
532
532
|
label: "Read",
|
|
533
|
-
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
|
-
? "
|
|
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({
|
|
542
544
|
path: filePathParam(),
|
|
543
545
|
offset: optionalIntOrString("Start line (1-indexed)"),
|
|
544
|
-
limit: optionalIntOrString("
|
|
546
|
+
limit: optionalIntOrString("Maximum lines to return"),
|
|
545
547
|
symbol: Type.Optional(Type.String({ description: "Symbol name to read" })),
|
|
546
548
|
map: mapParam(),
|
|
547
549
|
bundle: Type.Optional(
|
|
548
550
|
Type.Literal("local", {
|
|
549
|
-
description: "Include same
|
|
551
|
+
description: "Include related symbols from the same file; requires symbol",
|
|
550
552
|
}),
|
|
551
553
|
),
|
|
552
554
|
...(imageMode === "off"
|
package/src/readseek-params.ts
CHANGED
|
@@ -4,12 +4,12 @@ import { buildToolErrorResult, type ToolErrorResult } from "./readseek-value.js"
|
|
|
4
4
|
|
|
5
5
|
/** Optional search path shared by the readseek search tools. */
|
|
6
6
|
export function searchPathParam() {
|
|
7
|
-
return Type.Optional(Type.String({ description: "
|
|
7
|
+
return Type.Optional(Type.String({ description: "File or directory to search" }));
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
/** Optional language hint shared by the readseek search tools. */
|
|
11
11
|
export function langParam() {
|
|
12
|
-
return Type.Optional(Type.String({ description: "Language
|
|
12
|
+
return Type.Optional(Type.String({ description: "Language override when auto-detection is ambiguous" }));
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/**
|
package/src/refs.ts
CHANGED
|
@@ -27,7 +27,7 @@ type RefsParams = {
|
|
|
27
27
|
|
|
28
28
|
const REFS_PROMPT_METADATA = defineToolPromptMetadata({
|
|
29
29
|
promptUrl: new URL("../prompts/refs.md", import.meta.url),
|
|
30
|
-
promptSnippet: "Find identifier
|
|
30
|
+
promptSnippet: "Find usages of an identifier or cursor binding",
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
interface RefsToolOptions {
|
|
@@ -133,9 +133,9 @@ export function registerRefsTool(pi: ExtensionAPI, options: RefsToolOptions = {}
|
|
|
133
133
|
name: Type.String({ description: "Identifier to find references for" }),
|
|
134
134
|
path: searchPathParam(),
|
|
135
135
|
lang: langParam(),
|
|
136
|
-
scope: Type.Optional(Type.Boolean({ description: "Restrict to the binding
|
|
137
|
-
line: Type.Optional(Type.Number({ description: "
|
|
138
|
-
column: Type.Optional(Type.Number({ description: "
|
|
136
|
+
scope: Type.Optional(Type.Boolean({ description: "Restrict results to the binding at the cursor" })),
|
|
137
|
+
line: Type.Optional(Type.Number({ description: "Cursor line (1-indexed), required with scope" })),
|
|
138
|
+
column: Type.Optional(Type.Number({ description: "Cursor byte column (1-indexed) for disambiguation" })),
|
|
139
139
|
...readSeekGitSearchParams(),
|
|
140
140
|
}),
|
|
141
141
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
package/src/register-tool.ts
CHANGED
|
@@ -12,7 +12,7 @@ export function optionalIntOrString(description: string) {
|
|
|
12
12
|
|
|
13
13
|
/** Required file-path parameter shared by the read, edit, and write tools. */
|
|
14
14
|
export function filePathParam() {
|
|
15
|
-
return Type.String({ description: "
|
|
15
|
+
return Type.String({ description: "Absolute or relative file path" });
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/** Optional structural-map toggle shared by the read and write tools. */
|
package/src/rename.ts
CHANGED
|
@@ -8,21 +8,22 @@ 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
|
|
|
14
15
|
const RENAME_PROMPT_METADATA = defineToolPromptMetadata({
|
|
15
16
|
promptUrl: new URL("../prompts/rename.md", import.meta.url),
|
|
16
|
-
promptSnippet: "Rename a
|
|
17
|
+
promptSnippet: "Rename the symbol at a cursor without touching shadows",
|
|
17
18
|
});
|
|
18
19
|
|
|
19
20
|
const renameSchema = Type.Object({
|
|
20
21
|
path: filePathParam(),
|
|
21
|
-
line: Type.
|
|
22
|
-
column: Type.Optional(Type.
|
|
23
|
-
to: Type.String({ description: "New name
|
|
24
|
-
workspace: Type.Optional(Type.Boolean({ description: "
|
|
25
|
-
apply: Type.Optional(Type.Boolean({ description: "
|
|
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" })),
|
|
24
|
+
to: Type.String({ description: "New symbol name" }),
|
|
25
|
+
workspace: Type.Optional(Type.Boolean({ description: "Rename across the project" })),
|
|
26
|
+
apply: Type.Optional(Type.Boolean({ description: "Apply the verified edits; default true" })),
|
|
26
27
|
});
|
|
27
28
|
|
|
28
29
|
interface RenameParams {
|
|
@@ -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
|
|
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
|
|
11
|
+
export interface BuildSearchOutputInput {
|
|
12
12
|
pattern: string;
|
|
13
|
-
files:
|
|
13
|
+
files: SearchOutputFile[];
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export interface
|
|
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
|
|
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}`,
|
package/src/{sg.ts → search.ts}
RENAMED
|
@@ -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 {
|
|
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
|
|
18
|
+
type SearchParams = { pattern: string; lang?: string; path?: string; cached?: boolean; others?: boolean; ignored?: boolean };
|
|
19
19
|
|
|
20
|
-
export interface
|
|
20
|
+
export interface SearchRange {
|
|
21
21
|
startLine: number;
|
|
22
22
|
endLine: number;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
export interface
|
|
25
|
+
export interface SearchEnclosingSymbol {
|
|
26
26
|
name: string;
|
|
27
27
|
kind: string;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
export function mergeRanges(ranges:
|
|
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:
|
|
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
|
|
51
|
-
promptUrl: new URL("../prompts/
|
|
52
|
-
promptSnippet: "Search code
|
|
50
|
+
const SEARCH_PROMPT_METADATA = defineToolPromptMetadata({
|
|
51
|
+
promptUrl: new URL("../prompts/search.md", import.meta.url),
|
|
52
|
+
promptSnippet: "Search syntax-aware code shapes with AST patterns",
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
-
interface
|
|
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
|
|
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:
|
|
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
|
|
103
|
+
export async function executeSearch(opts: ExecuteSearchOptions): Promise<any> {
|
|
104
104
|
const { params, signal, cwd, onFileAnchored } = opts;
|
|
105
|
-
const p = params as
|
|
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 =
|
|
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:
|
|
136
|
+
ranges: SearchRange[];
|
|
137
137
|
lines: ReadSeekLine[];
|
|
138
|
-
symbols?:
|
|
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 =
|
|
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 =
|
|
166
|
+
const builtOutput = buildSearchOutput({
|
|
167
167
|
pattern: p.pattern,
|
|
168
168
|
files: readSeekFiles,
|
|
169
169
|
});
|
|
@@ -182,21 +182,21 @@ export async function executeSg(opts: ExecuteSgOptions): Promise<any> {
|
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
export function
|
|
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:
|
|
190
|
-
promptSnippet:
|
|
191
|
-
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
|
-
pattern: Type.String({ description: "
|
|
193
|
+
pattern: Type.String({ description: "ast-grep pattern, such as console.log($$$ARGS)" }),
|
|
194
194
|
lang: langParam(),
|
|
195
195
|
path: searchPathParam(),
|
|
196
196
|
...readSeekGitSearchParams(),
|
|
197
197
|
}),
|
|
198
198
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
199
|
-
return
|
|
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, {
|
package/src/session-anchors.ts
CHANGED
|
@@ -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
|
*/
|
|
@@ -2,16 +2,6 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
|
|
3
3
|
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
|
|
5
|
-
const COMPACT_DESCRIPTIONS: Record<string, string> = {
|
|
6
|
-
"read.md": "Read text files, images, or PDFs by path; text has LINE:HASH anchors and visual files require an explicit image mode.",
|
|
7
|
-
"edit.md": "Edit existing text files using fresh LINE:HASH anchors from readSeek_read, readSeek_grep, readSeek_search, or readSeek_write.",
|
|
8
|
-
"grep.md": "Search file contents; non-summary results include LINE:HASH anchors for edits.",
|
|
9
|
-
|
|
10
|
-
"write.md": "Create or overwrite a complete file and return anchors.",
|
|
11
|
-
"sg.md": "Search code by AST pattern and return anchored matches.",
|
|
12
|
-
"refs.md": "Find references to an identifier and return anchored usages with enclosing symbols.",
|
|
13
|
-
};
|
|
14
|
-
|
|
15
5
|
const REPLACEABLE_TOOL_GUIDELINES: Record<string, { readSeekName: string; builtInName: string; benefit: string }> = {
|
|
16
6
|
"read.md": {
|
|
17
7
|
readSeekName: "readSeek_read",
|
|
@@ -37,8 +27,7 @@ const REPLACEABLE_TOOL_GUIDELINES: Record<string, { readSeekName: string; builtI
|
|
|
37
27
|
|
|
38
28
|
const COMPACT_GUIDELINES: Record<string, string[]> = {
|
|
39
29
|
"read.md": [
|
|
40
|
-
"Use readSeek_read map or symbol mode
|
|
41
|
-
"For images and PDFs, explicitly choose one of the image modes exposed by readSeek_read.",
|
|
30
|
+
"Use readSeek_read map or symbol mode to inspect large code files without reading them in full.",
|
|
42
31
|
],
|
|
43
32
|
"edit.md": [
|
|
44
33
|
"With readSeek_edit, prefer set_line, replace_lines, and insert_after; use replace only when anchors are impractical.",
|
|
@@ -49,9 +38,8 @@ const COMPACT_GUIDELINES: Record<string, string[]> = {
|
|
|
49
38
|
"write.md": [
|
|
50
39
|
"Use anchored edits rather than readSeek_write for small changes or appends to existing files.",
|
|
51
40
|
],
|
|
52
|
-
"
|
|
53
|
-
"Use readSeek_search for
|
|
54
|
-
"Use readSeek_grep instead of readSeek_search for plain text.",
|
|
41
|
+
"search.md": [
|
|
42
|
+
"Use readSeek_search for syntax-aware code shapes; use readSeek_grep for plain text.",
|
|
55
43
|
],
|
|
56
44
|
"refs.md": [
|
|
57
45
|
"Use readSeek_refs to find every usage of an identifier before renaming or deleting it.",
|
|
@@ -96,7 +84,6 @@ export function defineToolPromptMetadata(options: {
|
|
|
96
84
|
}): ToolPromptMetadata {
|
|
97
85
|
const prompt = loadPrompt(options.promptUrl);
|
|
98
86
|
const fileName = promptFileName(options.promptUrl);
|
|
99
|
-
const compactDescription = COMPACT_DESCRIPTIONS[fileName];
|
|
100
87
|
const replaceable = REPLACEABLE_TOOL_GUIDELINES[fileName];
|
|
101
88
|
const registeredName = options.registeredName ?? replaceable?.readSeekName;
|
|
102
89
|
const preferenceGuideline = replaceable && registeredName
|
|
@@ -105,7 +92,7 @@ export function defineToolPromptMetadata(options: {
|
|
|
105
92
|
: `Use ${registeredName}; ${replaceable.benefit}`
|
|
106
93
|
: undefined;
|
|
107
94
|
return {
|
|
108
|
-
description: rewriteToolAliases(
|
|
95
|
+
description: rewriteToolAliases(firstPromptParagraph(prompt), options.toolAliases),
|
|
109
96
|
promptSnippet: rewriteToolAliases(options.promptSnippet, options.toolAliases),
|
|
110
97
|
promptGuidelines: [
|
|
111
98
|
...(preferenceGuideline ? [preferenceGuideline] : []),
|
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;
|
package/src/write.ts
CHANGED
|
@@ -241,7 +241,7 @@ export function registerWriteTool(pi: ExtensionAPI, options: WriteToolOptions =
|
|
|
241
241
|
const name = options.name ?? "readSeek_write";
|
|
242
242
|
const promptMetadata = defineToolPromptMetadata({
|
|
243
243
|
promptUrl: new URL("../prompts/write.md", import.meta.url),
|
|
244
|
-
promptSnippet: "Create or
|
|
244
|
+
promptSnippet: "Create or replace a complete file with edit anchors",
|
|
245
245
|
registeredName: name,
|
|
246
246
|
});
|
|
247
247
|
const tool = registerReadSeekTool(pi, {
|
|
@@ -252,7 +252,7 @@ export function registerWriteTool(pi: ExtensionAPI, options: WriteToolOptions =
|
|
|
252
252
|
promptGuidelines: promptMetadata.promptGuidelines,
|
|
253
253
|
parameters: Type.Object({
|
|
254
254
|
path: filePathParam(),
|
|
255
|
-
content: Type.String({ description: "
|
|
255
|
+
content: Type.String({ description: "Complete file content" }),
|
|
256
256
|
map: mapParam(),
|
|
257
257
|
}),
|
|
258
258
|
async execute(_toolCallId: string, params: { path: string; content: string; map?: boolean }, _signal: AbortSignal | undefined, _onUpdate: any, ctx: any): Promise<any> {
|