pi-readseek 0.1.1 → 0.2.1
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/package.json +2 -2
- package/prompts/edit.md +33 -24
- package/prompts/find.md +4 -5
- package/prompts/grep.md +13 -11
- package/prompts/ls.md +3 -3
- package/prompts/read.md +22 -14
- package/prompts/sg.md +21 -6
- package/prompts/write.md +7 -7
- package/src/context-hygiene.ts +9 -0
- package/src/read.ts +10 -2
- package/src/readseek/mapper.ts +2 -2
- package/src/readseek-client.ts +71 -36
- package/src/sg.ts +30 -2
- package/src/tool-prompt-metadata.ts +10 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-readseek",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
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.
|
|
42
|
+
"@jarkkojs/readseek": "0.2.4",
|
|
43
43
|
"diff": "^8.0.3",
|
|
44
44
|
"ignore": "^7.0.5",
|
|
45
45
|
"picomatch": "^4.0.4",
|
package/prompts/edit.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Surgically edit existing text files. Prefer hash-verified
|
|
1
|
+
Surgically edit existing text files. Prefer hash-verified anchors from fresh `read`, `grep`, `search`, or `write` output; copy `LINE:HASH` anchors exactly.
|
|
2
2
|
|
|
3
3
|
`edit` requires the target file to have been anchored earlier in the current session. If you get `file-not-read`, run `read`, `grep`, `search`, or `write` first.
|
|
4
4
|
|
|
@@ -6,16 +6,15 @@ Surgically edit existing text files. Prefer hash-verified anchored edits from fr
|
|
|
6
6
|
|
|
7
7
|
| Variant | Use | Anchors |
|
|
8
8
|
|---|---|---|
|
|
9
|
-
| `set_line` | Replace
|
|
10
|
-
| `replace_lines` | Replace
|
|
9
|
+
| `set_line` | Replace or delete one line | 1 |
|
|
10
|
+
| `replace_lines` | Replace or delete one contiguous range | 2 |
|
|
11
11
|
| `insert_after` | Insert after an existing line | 1 |
|
|
12
|
-
| `replace_symbol` | Replace one function/class/method/etc. | 0 (`symbol`) |
|
|
13
|
-
| `replace` |
|
|
12
|
+
| `replace_symbol` | Replace one function/class/method/interface/type/enum/etc. | 0 (`symbol`) |
|
|
13
|
+
| `replace` | Exact string replacement escape hatch; one match by default, all with `all: true` | 0 |
|
|
14
14
|
|
|
15
|
-
Set `new_text` (or `replace_lines
|
|
16
|
-
Prefer `set_line`, `replace_lines`, and `insert_after`: they verify the file still matches the anchored content. Use `replace` only when anchors are impractical, such as repeated text across many unrelated lines.
|
|
15
|
+
Set `new_text` (or `replace_lines.new_text`) to `""` to delete anchored line(s). For an intentionally blank line, use `"\n"` or whitespace content, not `""`.
|
|
17
16
|
|
|
18
|
-
`
|
|
17
|
+
Prefer `set_line`, `replace_lines`, and `insert_after`: they verify that the file still matches the anchored content. Use `replace` only when anchors are impractical, such as repeated text across many unrelated lines.
|
|
19
18
|
|
|
20
19
|
## Input shape
|
|
21
20
|
|
|
@@ -32,29 +31,32 @@ Prefer `set_line`, `replace_lines`, and `insert_after`: they verify the file sti
|
|
|
32
31
|
}
|
|
33
32
|
```
|
|
34
33
|
|
|
35
|
-
Use only the variant(s)
|
|
34
|
+
Use only the needed variant(s); the example shows all shapes for reference. Each `edits[]` entry must contain exactly one variant key. `new_text` / `new_body` is plain file content — no hash prefixes or diff markers.
|
|
36
35
|
|
|
37
|
-
##
|
|
36
|
+
## Exact and fuzzy replacement
|
|
37
|
+
|
|
38
|
+
`replace` is exact-only by default. Missing `old_text` fails with `text-not-found`.
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
Wrap string replacements as `{ "replace": { "old_text": "...", "new_text": "..." } }`; a bare top-level `{ old_text, new_text }` inside `edits[]` is rejected with guidance.
|
|
40
41
|
|
|
41
|
-
|
|
42
|
+
`fuzzy: true` is a narrow fallback after exact matching fails. It normalizes whitespace and confusable Unicode such as smart hyphens; it is **not** approximate, Levenshtein, or semantic matching. Fuzzy successes return a warning.
|
|
42
43
|
|
|
43
44
|
## `replace_symbol`
|
|
44
45
|
|
|
45
|
-
Use `replace_symbol` to replace one
|
|
46
|
+
Use `replace_symbol` when you want to replace one whole mapped symbol without line anchors. Query symbols like `read({ symbol })`: `Name`, `Class.method`, or `Name@<line>`.
|
|
46
47
|
|
|
47
48
|
Rules:
|
|
48
|
-
|
|
49
|
+
|
|
50
|
+
- Use an exact name, dotted path, or `@<line>`. If `read({ symbol })` returned a fuzzy match, confirm the exact symbol first.
|
|
49
51
|
- Supported for TypeScript, JavaScript, Rust, and Java. For other languages, use anchored edits.
|
|
50
52
|
- `new_body` must not be empty or whitespace-only.
|
|
51
53
|
- Write `new_body` without extra leading indentation; `edit` re-indents it to match the original symbol.
|
|
52
54
|
- If `new_body` appears to declare a different symbol name, the edit still applies but returns a `name-mismatch` warning.
|
|
53
|
-
- Do not combine `replace_symbol` with anchored edits that touch the same lines. Duplicate
|
|
55
|
+
- Do not combine `replace_symbol` with anchored edits that touch the same lines. Duplicate or overlapping `replace_symbol` ranges are rejected.
|
|
54
56
|
|
|
55
57
|
## Stale anchors
|
|
56
58
|
|
|
57
|
-
If anchors no longer match, `edit` fails with
|
|
59
|
+
If anchors no longer match, `edit` fails with `hash-mismatch` and shows nearby current lines. Lines marked `>>>` include updated anchors:
|
|
58
60
|
|
|
59
61
|
```text
|
|
60
62
|
>>> 41:b34| const renamed = 3;
|
|
@@ -62,32 +64,39 @@ If anchors no longer match, `edit` fails with a hash mismatch (`hash-mismatch`)
|
|
|
62
64
|
|
|
63
65
|
Copy the updated `LINE:HASH` and retry. If the target moved farther away, re-run `read`, `grep`, `search`, or `write` for fresh anchors.
|
|
64
66
|
|
|
65
|
-
If `edit` auto-relocates an anchor,
|
|
67
|
+
If `edit` auto-relocates an anchor, read the warning and verify that the edit landed in the intended place.
|
|
66
68
|
|
|
67
69
|
## Validation and warnings
|
|
68
70
|
|
|
69
71
|
- All edits are checked before writing; if a hard validation fails, nothing is written.
|
|
70
72
|
- Anchored edits are applied bottom-up so line numbers stay stable.
|
|
71
73
|
- `no-op` means the requested edit matched the current file already or produced identical content.
|
|
72
|
-
-
|
|
73
|
-
- A `replace`-only success may
|
|
74
|
+
- Whitespace-only warnings mean formatting changed but behavior probably did not.
|
|
75
|
+
- A `replace`-only success may remind you to prefer anchored edits next time.
|
|
74
76
|
|
|
75
77
|
Syntax validation runs before writing when supported:
|
|
78
|
+
|
|
76
79
|
- Supported: Rust, C++, C headers, Java.
|
|
77
80
|
- Default `warn`: write succeeds, but warnings include `syntax-regression: lines X-Y`.
|
|
78
81
|
- `block`: aborts without writing.
|
|
79
82
|
- `off`: skips validation.
|
|
80
83
|
- `PI_HASHLINE_SYNTAX_VALIDATE` can set the default mode.
|
|
81
84
|
|
|
82
|
-
Existing syntax errors are tolerated;
|
|
85
|
+
Existing syntax errors are tolerated; warnings are for newly introduced parser errors.
|
|
86
|
+
|
|
87
|
+
## Optional post-edit verification
|
|
88
|
+
|
|
89
|
+
`postEditVerify: true` opts into read-back verification for this call. It is off by default. When enabled, `edit` writes normally, then reads the file back and compares persisted content to the intended content, including BOM and original line endings. This is not syntax validation.
|
|
83
90
|
|
|
84
91
|
## Diff data contract
|
|
85
92
|
|
|
86
|
-
Successful `edit` results include
|
|
93
|
+
Successful `edit` results include:
|
|
87
94
|
|
|
88
|
-
|
|
95
|
+
- `details.diff` and `details.readseekValue.diff`: compact human-readable hashline diff strings.
|
|
96
|
+
- `details.patch`: standard unified diff with file and hunk headers.
|
|
97
|
+
- `details.diffData` and `details.readseekValue.diffData`: stable structured diff data.
|
|
89
98
|
|
|
90
|
-
`diffData`
|
|
99
|
+
`diffData` shape:
|
|
91
100
|
|
|
92
101
|
```ts
|
|
93
102
|
type DiffData = {
|
|
@@ -110,4 +119,4 @@ type DiffData = {
|
|
|
110
119
|
};
|
|
111
120
|
```
|
|
112
121
|
|
|
113
|
-
For compact one-line hashline diffs, `details.diff` remains compact
|
|
122
|
+
For compact one-line hashline diffs, `details.diff` remains compact while `diffData.entries` uses expanded remove/add rows so renderers can show inline word changes without breaking hashline output.
|
package/prompts/find.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Find files recursively by
|
|
1
|
+
Find files or directories recursively by basename. Uses glob patterns by default, respects nested `.gitignore`, includes hidden entries, and returns relative paths.
|
|
2
2
|
|
|
3
3
|
## Parameters
|
|
4
4
|
|
|
@@ -9,11 +9,10 @@ Find files recursively by name. Uses glob patterns by default, respects nested `
|
|
|
9
9
|
- `maxDepth` — non-negative directory depth limit.
|
|
10
10
|
- `sortBy` — `"name"` default, `"mtime"`, or `"size"`; use `reverse: true` for descending/newest/largest first.
|
|
11
11
|
- `modifiedSince` — keep entries modified strictly after an ISO date/time or relative age like `30m`, `1h`, `24h`, `7d`.
|
|
12
|
-
- `minSize` / `maxSize` — file-size filters
|
|
12
|
+
- `minSize` / `maxSize` — inclusive file-size filters; numbers are bytes, strings accept 1024-based `KB`, `MB`, `GB`, etc. Directories are not removed by size filters.
|
|
13
13
|
|
|
14
14
|
## Output and usage
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
Filtering and sorting happen before `limit`, so queries like largest/newest files work as expected.
|
|
16
|
+
Output is one relative path per line. Directories end with `/`. Filtering and sorting happen before `limit`, so newest/largest queries work as expected.
|
|
18
17
|
|
|
19
|
-
Use `find` for recursive
|
|
18
|
+
Use `find` for recursive name discovery, `ls` for one directory, `grep` or `search` for contents, and `read` for file content. Remember: `pattern` matches basenames, not full paths.
|
package/prompts/grep.md
CHANGED
|
@@ -1,26 +1,28 @@
|
|
|
1
|
-
Search file contents. Non-summary results
|
|
1
|
+
Search file contents. Non-summary results include edit-ready `LINE:HASH` anchors, so you usually do not need a follow-up `read` before `edit`.
|
|
2
2
|
|
|
3
3
|
## Modes
|
|
4
4
|
|
|
5
|
-
- Default: matching lines only.
|
|
6
|
-
- `context: N`: include N lines before
|
|
7
|
-
- `summary: true`: return per-file match counts only
|
|
8
|
-
- `scope: "symbol"`: group matches by enclosing symbol. By default returns
|
|
5
|
+
- Default: matching lines only. Match rows look like `path:>>LINE:HASH|content`.
|
|
6
|
+
- `context: N`: include N lines before and after each match. Context rows use `path: LINE:HASH|content`; nearby ranges are merged and deduped.
|
|
7
|
+
- `summary: true`: return per-file match counts only. Use this first for broad searches, then narrow with `path`, `glob`, or a stricter pattern.
|
|
8
|
+
- `scope: "symbol"`: group matches by enclosing symbol. By default returns full symbol blocks. `scopeContext: N` clips each match to ±N lines inside the symbol; `0` returns only match lines. Ignored with `summary: true`.
|
|
9
9
|
|
|
10
10
|
## Parameters
|
|
11
11
|
|
|
12
|
-
- `pattern` —
|
|
12
|
+
- `pattern` — regular expression by default; set `literal: true` for exact text or regex metacharacters.
|
|
13
13
|
- `path` — file or directory, default cwd.
|
|
14
|
-
- `glob` — file filter
|
|
14
|
+
- `glob` — file-name filter such as `*.ts` or `**/*.test.ts`.
|
|
15
15
|
- `ignoreCase` — case-insensitive search.
|
|
16
16
|
- `context` — surrounding lines for normal grep.
|
|
17
|
-
- `limit` —
|
|
17
|
+
- `limit` — maximum matches, default 100.
|
|
18
18
|
- `summary` — counts only, no anchors.
|
|
19
19
|
- `scope` — only `"symbol"` is supported.
|
|
20
20
|
- `scopeContext` — non-negative context within symbol scope; requires `scope: "symbol"`.
|
|
21
21
|
|
|
22
|
-
##
|
|
22
|
+
## Use well
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
Use `grep` for text: identifiers, strings, config keys, error messages, comments, or docs. Use `literal: true` unless you want regex behavior.
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
For code shape — calls, imports, declarations, JSX, object literals, control flow — prefer `search`, which parses AST patterns.
|
|
27
|
+
|
|
28
|
+
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/ls.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
List one directory.
|
|
1
|
+
List one directory. Output is directories first (with `/`), then files, sorted alphabetically; dotfiles are included.
|
|
2
2
|
|
|
3
3
|
## Parameters
|
|
4
4
|
|
|
5
5
|
- `path` — directory to list, default cwd.
|
|
6
6
|
- `limit` — max entries, default 500; must be positive.
|
|
7
|
-
- `glob` — optional entry-name filter such as `*.ts
|
|
7
|
+
- `glob` — optional entry-name filter such as `*.ts`, `.env*`, or `test-*`.
|
|
8
8
|
|
|
9
9
|
## Usage
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Use `ls` to inspect one known directory. Use `find` for recursive discovery, `grep` or `search` for contents, and `read` for file content. If output exceeds `limit` or 50 KB, narrow with `glob` or switch to `find`.
|
package/prompts/read.md
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
|
-
Read
|
|
1
|
+
Read files through readseek. Text output uses `LINE:HASH|content` anchors that can be copied directly into `edit`; supported images return attachments instead of anchors. Default cap: {{DEFAULT_MAX_LINES}} lines or {{DEFAULT_MAX_BYTES}}.
|
|
2
|
+
|
|
3
|
+
## Choose the right read
|
|
4
|
+
|
|
5
|
+
- Normal read: inspect a whole small file or a targeted `offset` / `limit` range.
|
|
6
|
+
- `map: true`: append a structural map for navigation before reading more code.
|
|
7
|
+
- `symbol: "Name"`: read one function, class, method, interface, type, enum, or similar symbol.
|
|
8
|
+
- `bundle: "local"`: with `symbol`, include direct same-file local support when readseek can identify it.
|
|
2
9
|
|
|
3
10
|
## Parameters
|
|
4
11
|
|
|
5
|
-
- `
|
|
6
|
-
- `
|
|
7
|
-
- `
|
|
8
|
-
- `
|
|
12
|
+
- `path` — file path.
|
|
13
|
+
- `offset` / `limit` — positive line numbers; `offset` is 1-indexed.
|
|
14
|
+
- `map` — append the full-file structural map; cannot combine with `symbol` or `bundle`.
|
|
15
|
+
- `symbol` — symbol query; supports `Class.method`, package-relative Java names, and `Name@<line>` disambiguation; cannot combine with `offset` / `limit`.
|
|
16
|
+
- `bundle` — only `"local"`; requires `symbol` and cannot combine with `map`.
|
|
9
17
|
|
|
10
|
-
When a full-file read is truncated, a
|
|
18
|
+
When a full-file read is truncated, readseek appends a structural map automatically when available. Use map line ranges for follow-up `read({ offset, limit })` calls.
|
|
11
19
|
|
|
12
20
|
## Symbol examples
|
|
13
21
|
|
|
@@ -16,18 +24,18 @@ When a full-file read is truncated, a readseek structural map is appended automa
|
|
|
16
24
|
| `{ "symbol": "processEvent" }` | function or top-level symbol |
|
|
17
25
|
| `{ "symbol": "EventEmitter" }` | class/interface/type/enum/etc. |
|
|
18
26
|
| `{ "symbol": "EventEmitter.emit" }` | child method/member |
|
|
19
|
-
| `{ "symbol": "Foo.bar@42" }` |
|
|
20
|
-
| `{ "symbol": "handleRequest", "bundle": "local" }` | symbol plus direct
|
|
27
|
+
| `{ "symbol": "Foo.bar@42" }` | overload/definition near line 42 |
|
|
28
|
+
| `{ "symbol": "handleRequest", "bundle": "local" }` | symbol plus direct same-file support |
|
|
21
29
|
|
|
22
30
|
## Symbol resolution
|
|
23
31
|
|
|
24
|
-
`@<line>` only applies as a trailing suffix like `Foo.bar@42`; names such as `foo@bar` are ordinary queries. Resolution order: containing range → nearest symbol starting at/after the requested line → nearest symbol above it.
|
|
32
|
+
`@<line>` only applies as a trailing suffix like `Foo.bar@42`; names such as `foo@bar` are ordinary queries. Resolution order: containing range → nearest symbol starting at/after the requested line → nearest symbol above it.
|
|
25
33
|
|
|
26
34
|
Result behavior:
|
|
35
|
+
|
|
27
36
|
- **Found**: returns only the symbol range with `[Symbol: name (kind), lines X-Y of Z]`.
|
|
28
|
-
- **Ambiguous**:
|
|
29
|
-
- **Fuzzy**: returns the best camelCase/substring match with a warning
|
|
30
|
-
- **Not found**: falls back to normal read with a warning
|
|
31
|
-
- **Unmappable**: falls back to normal read with a warning.
|
|
37
|
+
- **Ambiguous**: lists candidates and retry hints such as `name@<startLine>`.
|
|
38
|
+
- **Fuzzy**: returns the best camelCase/substring match with a warning; verify before editing from those anchors.
|
|
39
|
+
- **Not found** or **unmappable**: falls back to normal read with a warning and, when available, symbol suggestions.
|
|
32
40
|
|
|
33
|
-
Hash anchors from symbol and bundled reads are valid for `edit
|
|
41
|
+
Hash anchors from normal, symbol, and bundled reads are valid for `edit` until the file changes.
|
package/prompts/sg.md
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
|
-
|
|
1
|
+
Search code with readseek AST patterns. Use it when text search is too broad or brittle and the query depends on syntax: calls, imports, declarations, JSX, object fields, control flow, and similar code shapes. Results are grouped by file with edit-ready hashline anchors.
|
|
2
2
|
|
|
3
3
|
## Parameters
|
|
4
4
|
|
|
5
5
|
- `pattern` — ast-grep-style pattern to match.
|
|
6
|
-
- `lang` — language hint
|
|
6
|
+
- `lang` — language hint; set it when syntax is ambiguous, extensionless, generated, or TSX/JSX-like.
|
|
7
7
|
- `path` — file or directory, default cwd.
|
|
8
|
+
- `cached` — in a Git repository, search tracked/indexed files.
|
|
9
|
+
- `others` — in a Git repository, search untracked files.
|
|
10
|
+
- `ignored` — with `others`, include ignored untracked files.
|
|
8
11
|
|
|
9
12
|
## Pattern syntax
|
|
10
13
|
|
|
11
14
|
- `$NAME` matches one AST node.
|
|
12
|
-
- `$_` matches any one node.
|
|
13
|
-
- `$$$ARGS` matches zero or more nodes
|
|
15
|
+
- `$_` matches any one AST node when you do not need to reuse it.
|
|
16
|
+
- `$$$ARGS` matches zero or more sibling nodes. Use it for function args, body statements, object fields, JSX children, etc.
|
|
17
|
+
- Reusing a metavariable name requires every occurrence to match the same source text.
|
|
18
|
+
|
|
19
|
+
Patterns are parsed as code, not text. Formatting is mostly ignored, but syntax must be valid for the selected language. Include punctuation that the language grammar requires.
|
|
14
20
|
|
|
15
21
|
## Examples
|
|
16
22
|
|
|
@@ -19,7 +25,16 @@ AST-aware structural code search. Use when text search is too broad or brittle a
|
|
|
19
25
|
- `export function $NAME($$$PARAMS) { $$$BODY }` — exported functions.
|
|
20
26
|
- `$OBJ.$METHOD($$$ARGS)` — method calls.
|
|
21
27
|
- `<$TAG $$$ATTRS>$$$CHILDREN</$TAG>` — JSX/TSX elements.
|
|
28
|
+
- `if ($COND) { $$$BODY }` — control-flow blocks.
|
|
29
|
+
|
|
30
|
+
## Languages
|
|
31
|
+
|
|
32
|
+
Useful `lang` values include `typescript`, `tsx`, `javascript`, `jsx`, `rust`, `python`, `go`, `java`, `c`, `cpp`, `csharp`, `ruby`, `php`, `lua`, `bash`, `json`, `yaml`, `toml`, `markdown`, `dockerfile`, `nix`, and `zig`. readseek 0.2.3 also accepts languages such as `assembly`, `css`, `gdscript`, `html`, `just`, `kconfig`, `latex`, `make`, `meson`, `perl`, `puppet`, `riscv`, `sql`, `swift`, `typst`, `xml`, and `unknown`.
|
|
33
|
+
|
|
34
|
+
`unknown` forces text-only handling and is not useful for parser-backed search.
|
|
35
|
+
|
|
36
|
+
## Git selection
|
|
22
37
|
|
|
23
|
-
|
|
38
|
+
When searching a directory inside a Git repository, readseek defaults to tracked/indexed files plus untracked non-ignored files. Use `cached`, `others`, and `ignored` to narrow or expand that selection. `ignored` requires `others`.
|
|
24
39
|
|
|
25
|
-
|
|
40
|
+
Use `grep` for plain text and `search` for structure.
|
package/prompts/write.md
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
|
|
1
|
+
Create or overwrite a whole file and return `LINE:HASH` anchors for immediate follow-up `edit` calls.
|
|
2
2
|
|
|
3
3
|
## Use / avoid
|
|
4
4
|
|
|
5
|
-
Use `write`
|
|
5
|
+
Use `write` for new files, generated files, or intentional full-file replacement. For small changes or appends to an existing file, read or search first and use `edit` (`insert_after` for appends).
|
|
6
6
|
|
|
7
|
-
Existing files are overwritten without confirmation. Binary-looking content
|
|
7
|
+
Existing files are overwritten without confirmation. Binary-looking content can be written, but hashlines are not generated, so there are no anchors for `edit`.
|
|
8
8
|
|
|
9
9
|
## Parameters
|
|
10
10
|
|
|
11
11
|
- `path` — relative or absolute file path.
|
|
12
12
|
- `content` — complete file contents.
|
|
13
|
-
- `map` — optional; append a structural map when possible. Map
|
|
13
|
+
- `map` — optional; append a structural map when possible. Map generation is best-effort and does not make the write fail.
|
|
14
14
|
|
|
15
15
|
## Output
|
|
16
16
|
|
|
17
|
-
Successful text writes return `LINE:HASH|content
|
|
17
|
+
Successful text writes return `LINE:HASH|content`. Display hashlines escape control characters for safe rendering. Visible output is capped at 2000 lines or 50 KB, but full anchors remain available in `readseekValue`.
|
|
18
18
|
|
|
19
19
|
## Diff data contract
|
|
20
20
|
|
|
21
|
-
Successful text `write` results include additive final `details.diff`, `details.readseekValue.diff`, `details.diffData`, and `details.readseekValue.diffData` fields.
|
|
21
|
+
Successful text `write` results include additive final `details.diff`, `details.readseekValue.diff`, `details.diffData`, and `details.readseekValue.diffData` fields. String diff fields remain the backward-compatible human-readable fallback.
|
|
22
22
|
|
|
23
23
|
`diffData` is a stable versioned contract:
|
|
24
24
|
|
|
@@ -43,4 +43,4 @@ type DiffData = {
|
|
|
43
43
|
};
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
For compact one-line hashline diffs, `details.diff` remains compact
|
|
46
|
+
For compact one-line hashline diffs, `details.diff` remains compact while `diffData.entries` uses expanded remove/add rows so renderers can show inline word changes without breaking hashline output.
|
package/src/context-hygiene.ts
CHANGED
|
@@ -48,6 +48,9 @@ export interface ContextHygieneSearchRehydrateInput {
|
|
|
48
48
|
pattern: string;
|
|
49
49
|
lang?: string;
|
|
50
50
|
path?: string;
|
|
51
|
+
cached?: true;
|
|
52
|
+
others?: true;
|
|
53
|
+
ignored?: true;
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
export interface ContextHygieneReadRehydrateDescriptor {
|
|
@@ -200,6 +203,9 @@ export interface BuildSearchRehydrateDescriptorInput {
|
|
|
200
203
|
pattern: string;
|
|
201
204
|
lang?: string;
|
|
202
205
|
path?: string;
|
|
206
|
+
cached?: boolean;
|
|
207
|
+
others?: boolean;
|
|
208
|
+
ignored?: boolean;
|
|
203
209
|
}
|
|
204
210
|
|
|
205
211
|
export function buildSearchRehydrateDescriptor(
|
|
@@ -208,6 +214,9 @@ export function buildSearchRehydrateDescriptor(
|
|
|
208
214
|
const descriptorInput: ContextHygieneSearchRehydrateInput = { pattern: input.pattern };
|
|
209
215
|
if (input.lang !== undefined) descriptorInput.lang = input.lang;
|
|
210
216
|
if (input.path !== undefined) descriptorInput.path = input.path;
|
|
217
|
+
if (input.cached === true) descriptorInput.cached = true;
|
|
218
|
+
if (input.others === true) descriptorInput.others = true;
|
|
219
|
+
if (input.ignored === true) descriptorInput.ignored = true;
|
|
211
220
|
return { tool: "search", input: descriptorInput };
|
|
212
221
|
}
|
|
213
222
|
|
package/src/read.ts
CHANGED
|
@@ -52,6 +52,12 @@ interface ReadToolOptions {
|
|
|
52
52
|
onSuccessfulRead?: (absolutePath: string) => void;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
function splitReadseekLines(text: string): string[] {
|
|
56
|
+
if (text.length === 0) return [];
|
|
57
|
+
const withoutTrailingTerminator = text.endsWith("\n") ? text.slice(0, -1) : text;
|
|
58
|
+
return withoutTrailingTerminator.split("\n");
|
|
59
|
+
}
|
|
60
|
+
|
|
55
61
|
const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
|
|
56
62
|
|
|
57
63
|
function startsWithBytes(buffer: Buffer, bytes: number[]): boolean {
|
|
@@ -386,7 +392,7 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
|
|
|
386
392
|
const hasBinaryContent = looksLikeBinary(rawBuffer);
|
|
387
393
|
throwIfAborted(signal);
|
|
388
394
|
const normalized = normalizeToLF(stripBom(rawBuffer.toString("utf-8")).text);
|
|
389
|
-
const allLines = normalized
|
|
395
|
+
const allLines = splitReadseekLines(normalized);
|
|
390
396
|
const total = allLines.length;
|
|
391
397
|
const structuredWarnings: ReadseekWarning[] = [];
|
|
392
398
|
let startLine = p.offset !== undefined ? p.offset : 1;
|
|
@@ -538,7 +544,9 @@ export function registerReadTool(pi: ExtensionAPI, options: ReadToolOptions = {}
|
|
|
538
544
|
throwIfAborted(signal);
|
|
539
545
|
let readseekOutput: Awaited<ReturnType<typeof readseekRead>>;
|
|
540
546
|
try {
|
|
541
|
-
readseekOutput =
|
|
547
|
+
readseekOutput = total === 0
|
|
548
|
+
? await readseekRead(absolutePath)
|
|
549
|
+
: await readseekRead(absolutePath, startLine, endIdx);
|
|
542
550
|
} catch (err: any) {
|
|
543
551
|
const detail = err?.message ? ` — ${err.message}` : "";
|
|
544
552
|
const message = `readseek failed while reading ${rawPath}${detail}`;
|
package/src/readseek/mapper.ts
CHANGED
|
@@ -38,7 +38,7 @@ export async function generateMapWithIdentity(
|
|
|
38
38
|
throwIfAborted(options.signal);
|
|
39
39
|
const fileStat = await stat(filePath);
|
|
40
40
|
throwIfAborted(options.signal);
|
|
41
|
-
const map = await readseekMap(filePath, fileStat.size);
|
|
41
|
+
const map = await readseekMap(filePath, fileStat.size, { signal: options.signal });
|
|
42
42
|
throwIfAborted(options.signal);
|
|
43
43
|
return { map, ...READSEEK_MAPPER_IDENTITY };
|
|
44
44
|
}
|
|
@@ -56,7 +56,7 @@ export async function generateMapFromContent(
|
|
|
56
56
|
options: MapOptions = {},
|
|
57
57
|
): Promise<FileMap | null> {
|
|
58
58
|
throwIfAborted(options.signal);
|
|
59
|
-
const map = await readseekMapContent(filePath, content);
|
|
59
|
+
const map = await readseekMapContent(filePath, content, { signal: options.signal });
|
|
60
60
|
throwIfAborted(options.signal);
|
|
61
61
|
return map;
|
|
62
62
|
}
|
package/src/readseek-client.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
1
|
+
import { spawn, type StdioOptions } from "node:child_process";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
-
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
3
|
import path from "node:path";
|
|
6
4
|
|
|
7
5
|
import { DetailLevel } from "./readseek/enums.js";
|
|
@@ -72,6 +70,14 @@ interface ReadseekSearchOutput {
|
|
|
72
70
|
results: ReadseekSearchFileOutput[];
|
|
73
71
|
}
|
|
74
72
|
|
|
73
|
+
export interface ReadseekSearchOptions {
|
|
74
|
+
language?: string;
|
|
75
|
+
cached?: boolean;
|
|
76
|
+
others?: boolean;
|
|
77
|
+
ignored?: boolean;
|
|
78
|
+
signal?: AbortSignal;
|
|
79
|
+
}
|
|
80
|
+
|
|
75
81
|
function normalizeLanguage(language: string): string {
|
|
76
82
|
return language === "java" ? "Java" : language;
|
|
77
83
|
}
|
|
@@ -148,14 +154,29 @@ export function isReadseekAvailable(): boolean {
|
|
|
148
154
|
}
|
|
149
155
|
}
|
|
150
156
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
157
|
+
interface RunReadseekOptions {
|
|
158
|
+
signal?: AbortSignal;
|
|
159
|
+
stdin?: string;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function runReadseek(args: string[], options: RunReadseekOptions = {}): Promise<unknown> {
|
|
163
|
+
const stdout = await new Promise<string>((resolve, reject) => {
|
|
164
|
+
const stdin = options.stdin;
|
|
165
|
+
const stdio: StdioOptions = [stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"];
|
|
166
|
+
const child = spawn(readseekBinaryPath(), args, { stdio, signal: options.signal });
|
|
167
|
+
const childStdout = child.stdout;
|
|
168
|
+
const childStderr = child.stderr;
|
|
169
|
+
const childStdin = child.stdin;
|
|
170
|
+
if (!childStdout || !childStderr) {
|
|
171
|
+
child.kill();
|
|
172
|
+
reject(new Error("readseek stdio streams are unavailable"));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
154
175
|
const stdoutChunks: Buffer[] = [];
|
|
155
176
|
const stderrChunks: Buffer[] = [];
|
|
156
177
|
let stdoutBytes = 0;
|
|
157
178
|
|
|
158
|
-
|
|
179
|
+
childStdout.on("data", (chunk: Buffer) => {
|
|
159
180
|
stdoutBytes += chunk.length;
|
|
160
181
|
if (stdoutBytes > 32 * 1024 * 1024) {
|
|
161
182
|
child.kill();
|
|
@@ -164,16 +185,26 @@ async function runReadseek(args: string[], options: { signal?: AbortSignal } = {
|
|
|
164
185
|
}
|
|
165
186
|
stdoutChunks.push(chunk);
|
|
166
187
|
});
|
|
167
|
-
|
|
188
|
+
childStderr.on("data", (chunk: Buffer) => stderrChunks.push(chunk));
|
|
168
189
|
child.on("error", (error: any) => reject(error));
|
|
190
|
+
if (stdin !== undefined) {
|
|
191
|
+
if (!childStdin) {
|
|
192
|
+
child.kill();
|
|
193
|
+
reject(new Error("readseek stdin stream is unavailable"));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
childStdin.on("error", (error: any) => {
|
|
197
|
+
if (error?.code !== "EPIPE") reject(error);
|
|
198
|
+
});
|
|
199
|
+
childStdin.end(stdin, "utf-8");
|
|
200
|
+
}
|
|
169
201
|
child.on("close", (code) => {
|
|
170
202
|
const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
|
|
171
203
|
const stderr = Buffer.concat(stderrChunks).toString("utf-8").trim();
|
|
172
|
-
if (code === 0) resolve(
|
|
204
|
+
if (code === 0) resolve(stdout);
|
|
173
205
|
else reject(new Error((stderr || `readseek exited with status ${code}`).replace(/^error:\s*/i, "")));
|
|
174
206
|
});
|
|
175
207
|
});
|
|
176
|
-
void stderr;
|
|
177
208
|
return JSON.parse(stdout) as unknown;
|
|
178
209
|
}
|
|
179
210
|
|
|
@@ -293,19 +324,14 @@ function parseSearchOutput(value: unknown): ReadseekSearchOutput {
|
|
|
293
324
|
};
|
|
294
325
|
}
|
|
295
326
|
|
|
296
|
-
export async function readseekRead(filePath: string, startLine
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
String(startLine),
|
|
302
|
-
"--end",
|
|
303
|
-
String(endLine),
|
|
304
|
-
]));
|
|
327
|
+
export async function readseekRead(filePath: string, startLine?: number, endLine?: number): Promise<ReadseekReadOutput> {
|
|
328
|
+
const args = ["read", filePath];
|
|
329
|
+
if (startLine !== undefined) args.push("--start", String(startLine));
|
|
330
|
+
if (endLine !== undefined) args.push("--end", String(endLine));
|
|
331
|
+
return parseReadOutput(await runReadseek(args));
|
|
305
332
|
}
|
|
306
333
|
|
|
307
|
-
|
|
308
|
-
const output = parseMapOutput(await runReadseek(["map", filePath]));
|
|
334
|
+
function fileMapFromReadseekOutput(output: ReadseekMapOutput, filePath: string, totalBytes: number): FileMap | null {
|
|
309
335
|
if (output.language === "unknown" && output.symbols.length === 0) return null;
|
|
310
336
|
return {
|
|
311
337
|
path: filePath,
|
|
@@ -318,26 +344,35 @@ export async function readseekMap(filePath: string, totalBytes: number): Promise
|
|
|
318
344
|
};
|
|
319
345
|
}
|
|
320
346
|
|
|
347
|
+
export async function readseekMap(
|
|
348
|
+
filePath: string,
|
|
349
|
+
totalBytes: number,
|
|
350
|
+
options: { signal?: AbortSignal } = {},
|
|
351
|
+
): Promise<FileMap | null> {
|
|
352
|
+
const output = parseMapOutput(await runReadseek(["map", filePath], { signal: options.signal }));
|
|
353
|
+
return fileMapFromReadseekOutput(output, filePath, totalBytes);
|
|
354
|
+
}
|
|
355
|
+
|
|
321
356
|
export async function readseekSearch(
|
|
322
357
|
target: string,
|
|
323
358
|
pattern: string,
|
|
324
|
-
|
|
325
|
-
signal?: AbortSignal,
|
|
359
|
+
options: ReadseekSearchOptions = {},
|
|
326
360
|
): Promise<ReadseekSearchFileOutput[]> {
|
|
327
361
|
const args = ["search", target, pattern];
|
|
328
|
-
if (language) args.push("--language", language);
|
|
329
|
-
|
|
362
|
+
if (options.language) args.push("--language", options.language);
|
|
363
|
+
if (options.cached) args.push("--cached");
|
|
364
|
+
if (options.others) args.push("--others");
|
|
365
|
+
if (options.ignored) args.push("--ignored");
|
|
366
|
+
return parseSearchOutput(await runReadseek(args, { signal: options.signal })).results;
|
|
330
367
|
}
|
|
331
368
|
|
|
332
|
-
export async function readseekMapContent(
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
await rm(tempDir, { recursive: true, force: true });
|
|
342
|
-
}
|
|
369
|
+
export async function readseekMapContent(
|
|
370
|
+
filePath: string,
|
|
371
|
+
content: string,
|
|
372
|
+
options: { signal?: AbortSignal } = {},
|
|
373
|
+
): Promise<FileMap | null> {
|
|
374
|
+
const output = parseMapOutput(
|
|
375
|
+
await runReadseek(["map", "--stdin", "--path", filePath], { signal: options.signal, stdin: content }),
|
|
376
|
+
);
|
|
377
|
+
return fileMapFromReadseekOutput(output, filePath, Buffer.byteLength(content, "utf8"));
|
|
343
378
|
}
|
package/src/sg.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { buildSgOutput } from "./sg-output.js";
|
|
|
12
12
|
import { buildSearchRehydrateDescriptor } from "./context-hygiene.js";
|
|
13
13
|
import { clampLineToWidth, clampLinesToWidth, isRendererExpanded, renderToolLabel, summaryLine } from "./tui-render-utils.js";
|
|
14
14
|
|
|
15
|
-
type SgParams = { pattern: string; lang?: string; path?: string };
|
|
15
|
+
type SgParams = { pattern: string; lang?: string; path?: string; cached?: boolean; others?: boolean; ignored?: boolean };
|
|
16
16
|
|
|
17
17
|
export interface SgRange {
|
|
18
18
|
startLine: number;
|
|
@@ -113,15 +113,35 @@ export function registerSgTool(pi: ExtensionAPI, options: SgToolOptions = {}) {
|
|
|
113
113
|
pattern: Type.String({ description: "AST pattern" }),
|
|
114
114
|
lang: Type.Optional(Type.String({ description: "Language hint" })),
|
|
115
115
|
path: Type.Optional(Type.String({ description: "Search path" })),
|
|
116
|
+
cached: Type.Optional(Type.Boolean({ description: "In a Git repository, search tracked/indexed files" })),
|
|
117
|
+
others: Type.Optional(Type.Boolean({ description: "In a Git repository, search untracked files" })),
|
|
118
|
+
ignored: Type.Optional(Type.Boolean({ description: "With others=true, include ignored untracked files" })),
|
|
116
119
|
}),
|
|
117
120
|
ptc: toolConfig,
|
|
118
121
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
119
122
|
await ensureHashInit();
|
|
120
123
|
const p = params as SgParams;
|
|
124
|
+
if (p.ignored && !p.others) {
|
|
125
|
+
const message = "Error: search parameter 'ignored' requires 'others'";
|
|
126
|
+
return {
|
|
127
|
+
content: [{ type: "text", text: message }],
|
|
128
|
+
isError: true,
|
|
129
|
+
details: {
|
|
130
|
+
readseekValue: {
|
|
131
|
+
tool: "search",
|
|
132
|
+
ok: false,
|
|
133
|
+
error: buildReadseekError("invalid-parameter", message),
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
121
138
|
const rehydrate = buildSearchRehydrateDescriptor({
|
|
122
139
|
pattern: p.pattern,
|
|
123
140
|
lang: p.lang,
|
|
124
141
|
path: p.path,
|
|
142
|
+
cached: p.cached,
|
|
143
|
+
others: p.others,
|
|
144
|
+
ignored: p.ignored,
|
|
125
145
|
});
|
|
126
146
|
|
|
127
147
|
const searchPath = resolveToCwd(p.path ?? ".", ctx.cwd);
|
|
@@ -178,7 +198,13 @@ export function registerSgTool(pi: ExtensionAPI, options: SgToolOptions = {}) {
|
|
|
178
198
|
|
|
179
199
|
try {
|
|
180
200
|
const effectiveLang = readseekLanguageForPath(p.lang, searchPath, searchPathIsFile);
|
|
181
|
-
const results = await readseekSearch(searchPath, p.pattern,
|
|
201
|
+
const results = await readseekSearch(searchPath, p.pattern, {
|
|
202
|
+
language: effectiveLang,
|
|
203
|
+
cached: p.cached,
|
|
204
|
+
others: p.others,
|
|
205
|
+
ignored: p.ignored,
|
|
206
|
+
signal,
|
|
207
|
+
});
|
|
182
208
|
if (results.length === 0) {
|
|
183
209
|
const emptyOutput = buildSgOutput({ pattern: p.pattern, files: [], rehydrate });
|
|
184
210
|
return {
|
|
@@ -262,6 +288,8 @@ export function registerSgTool(pi: ExtensionAPI, options: SgToolOptions = {}) {
|
|
|
262
288
|
let text = `${renderToolLabel(theme, "search")} ${theme.fg("accent", `/${args.pattern}/`)}`;
|
|
263
289
|
text += theme.fg("dim", ` in ${args.path ?? "."}`);
|
|
264
290
|
if (args.lang) text += theme.fg("dim", ` (${args.lang})`);
|
|
291
|
+
const flags = [args.cached && "cached", args.others && "others", args.ignored && "ignored"].filter(Boolean);
|
|
292
|
+
if (flags.length > 0) text += theme.fg("dim", ` [${flags.join(",")}]`);
|
|
265
293
|
return new Text(clampLineToWidth(text, context.width), 0, 0);
|
|
266
294
|
},
|
|
267
295
|
renderResult(result: any, options: ToolRenderResultOptions, theme: any, ...rest: any[]) {
|
|
@@ -5,37 +5,39 @@ const COMPACT_DESCRIPTIONS: Record<string, string> = {
|
|
|
5
5
|
"read.md": "Read text files/images by path; text has LINE:HASH anchors, images return attachments.",
|
|
6
6
|
"edit.md": "Edit existing text files using fresh LINE:HASH anchors from read, grep, search, or write.",
|
|
7
7
|
"grep.md": "Search file contents; non-summary results include LINE:HASH anchors for edits.",
|
|
8
|
-
"find.md": "Find files by glob, respecting .gitignore.",
|
|
9
|
-
"ls.md": "List one directory.",
|
|
10
|
-
"write.md": "Create or overwrite a file and return anchors.",
|
|
8
|
+
"find.md": "Find files recursively by basename glob, respecting .gitignore.",
|
|
9
|
+
"ls.md": "List one directory with directories first and dotfiles included.",
|
|
10
|
+
"write.md": "Create or overwrite a complete file and return anchors.",
|
|
11
11
|
"sg.md": "Search code by AST pattern and return anchored matches.",
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
const COMPACT_GUIDELINES: Record<string, string[]> = {
|
|
15
15
|
"read.md": [
|
|
16
16
|
"Use read for file contents, images/screenshots, ranges, symbols, and edit anchors.",
|
|
17
|
+
"Use map or symbol mode before pulling large code files into context.",
|
|
17
18
|
"Use read for images; it returns attachments, so avoid OCR tools unless explicitly needed.",
|
|
18
19
|
],
|
|
19
20
|
"edit.md": [
|
|
20
21
|
"Use edit with fresh LINE:HASH anchors for existing files.",
|
|
21
|
-
"
|
|
22
|
+
"Prefer set_line, replace_lines, and insert_after; use replace only when anchors are impractical.",
|
|
22
23
|
],
|
|
23
24
|
"grep.md": [
|
|
24
25
|
"Use grep for text search and edit-ready matching anchors.",
|
|
25
|
-
"Use grep summary mode
|
|
26
|
+
"Use grep summary mode for broad count/file discovery before narrowing.",
|
|
26
27
|
],
|
|
27
28
|
"find.md": [
|
|
28
|
-
"Use find for recursive file discovery by glob.",
|
|
29
|
+
"Use find for recursive file discovery by basename glob.",
|
|
29
30
|
],
|
|
30
31
|
"ls.md": [
|
|
31
|
-
"Use ls to
|
|
32
|
+
"Use ls to inspect one directory; use find for recursion.",
|
|
32
33
|
],
|
|
33
34
|
"write.md": [
|
|
34
35
|
"Use write to create files or intentionally overwrite whole files.",
|
|
35
|
-
"Use edit rather than write for small changes to existing files.",
|
|
36
|
+
"Use edit rather than write for small changes or appends to existing files.",
|
|
36
37
|
],
|
|
37
38
|
"sg.md": [
|
|
38
39
|
"Use search for AST-shaped code patterns.",
|
|
40
|
+
"Use grep instead of search for plain text.",
|
|
39
41
|
],
|
|
40
42
|
};
|
|
41
43
|
|