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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-readseek",
3
- "version": "0.1.1",
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.1.6",
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 anchored edits from fresh `read`, `grep`, `search`, or `write` output; copy `LINE:HASH` anchors exactly.
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/delete one line | 1 |
10
- | `replace_lines` | Replace/delete a contiguous range | 2 |
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` | String replacement escape hatch; one match by default, all with `all: true` | 0 |
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` `new_text`) to `""` to delete the anchored line(s). For an intentionally blank line, use `"\n"` or whitespace content, not `""`.
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
- `replace` is exact-only by default: missing `old_text` fails with `text-not-found`. Wrap old_text/new_text in {replace: ...} a bare top-level `{old_text, new_text}` inside `edits[]` is rejected with guidance. `fuzzy: true` is a narrow fallback that only normalizes whitespace and confusable Unicode (e.g. smart hyphens) after exact matching fails; it is **not approximate or Levenshtein/semantic matching** and will not find renamed or reworded text. When fuzzy matching is used, the response warns that exact text was not found.
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) needed for the task; the example shows all shapes together 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.
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
- ## Optional post-edit verification
36
+ ## Exact and fuzzy replacement
37
+
38
+ `replace` is exact-only by default. Missing `old_text` fails with `text-not-found`.
38
39
 
39
- `postEditVerify: true` opts into post-write persisted-content verification for this one call. It is default off: when omitted or false, successful edits use the normal fast path and do not perform an extra read-back check.
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
- When enabled, `edit` first runs the normal validation and write path. Only after the write succeeds, it reads the file back from disk and compares the persisted content to the exact intended content, including BOM restoration and original line-ending restoration. This is not syntax validation; syntax validation is the separate pre-write `syntaxValidate` / `PI_HASHLINE_SYNTAX_VALIDATE` guard described below.
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 function, class, method, interface, type, enum, or similar symbol. Query symbols like `read symbol:`: `Name`, `Class.method`, or `Name@<line>`.
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
- - Use an exact name, dotted path, or `@<line>`. If `read({symbol})` returned a fuzzy match, confirm the exact symbol before editing.
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/overlapping `replace_symbol` ranges are rejected.
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 a hash mismatch (`hash-mismatch`) and shows nearby current lines. Lines marked `>>>` include updated anchors:
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, check the warning and verify the edit landed in the intended place.
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
- - A whitespace-only warning means formatting changed but behavior probably did not.
73
- - A `replace`-only success may include a reminder to prefer anchored edits next time.
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; the warning is for newly introduced parser errors.
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 `details.diffData` and `details.readseekValue.diffData` in addition to the existing `details.diff` / `readseekValue.diff` string fields. The string fields remain the backward-compatible human-readable fallback.
93
+ Successful `edit` results include:
87
94
 
88
- Successful `edit` results also include `details.patch`: a standard unified diff (`---`/`+++` file headers and `@@` hunk headers) generated from the pre-/post-edit file contents. Use `details.patch` when you need a portable, tool-agnostic patch (e.g. to apply elsewhere); use the compact `details.diff` hashline string for human-readable in-session display. `details.patch` is additive and does not change `details.diff`.
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` is a stable versioned contract:
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, while `diffData.entries` uses expanded remove/add rows so renderers can show inline word changes without breaking hashline output.
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 name. Uses glob patterns by default, respects nested `.gitignore`, includes hidden files, and returns relative paths.
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, inclusive; numbers are bytes, strings accept 1024-based `KB`, `MB`, `GB`, etc. Directories are not removed by 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
- One relative path per line. Directories end with `/`. If results exceed `limit` or 50 KB, output says it was truncated.
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 file-name discovery, `ls` for one directory, and `grep` for file contents. Remember: `pattern` matches basenames, not full paths.
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 return `LINE:HASH` anchors usable directly by `edit`; no follow-up `read` is needed.
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. Output lines are `path:>>LINE:HASH|content`; `>>` marks matches.
6
- - `context: N`: include N lines before/after each match. Context lines use `path: LINE:HASH|content`; nearby ranges are merged/deduped.
7
- - `summary: true`: return per-file match counts only — no line content or anchors. Use first for broad searches, then narrow with `path`/`glob`.
8
- - `scope: "symbol"`: group matches by enclosing symbol. By default returns the full symbol block. `scopeContext: N` windows to ±N lines around each match, clipped to the symbol; `0` returns only match lines. Ignored when `summary: true`.
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` — regex by default; use `literal: true` for exact strings or regex metacharacters.
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, e.g. `'*.ts'` or `'**/*.test.ts'`.
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` — max matches, default 100.
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
- ## Truncation and guidance
22
+ ## Use well
23
23
 
24
- If matches hit `limit`, output appends `[Results truncated at N matches refine pattern or increase limit]`. Large non-summary results may cap displayed matches per file and/or head-truncate by output budget; narrow with `summary`, `path`, `glob`, or a more specific pattern.
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
- Use `grep` for text search. For structural code patterns such as calls, imports, or JSX, prefer `search`.
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. Shows directories first with `/`, then files, sorted alphabetically; dotfiles are included.
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` or `.env*`.
7
+ - `glob` — optional entry-name filter such as `*.ts`, `.env*`, or `test-*`.
8
8
 
9
9
  ## Usage
10
10
 
11
- Output is one entry per line. Use `ls` to inspect a single directory, `find` for recursive discovery, and `read` for file contents. If output exceeds `limit` or 50 KB, it says so.
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 text files with `LINE:HASH|content` anchors usable by `edit`. Default cap: {{DEFAULT_MAX_LINES}} lines or {{DEFAULT_MAX_BYTES}}. Images return attachments, not edit anchors.
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
- - `offset` / `limit` positive line numbers for targeted reads; `offset` is 1-indexed.
6
- - `map: true` — append a full-file structural map even for small files. May combine with `offset` / `limit`; cannot combine with `symbol` or `bundle`.
7
- - `symbol: "Name"` — read one symbol range by name, with hash anchors. Supports `ClassName.method`, Java package-relative names, and `Name@<line>` disambiguation. Cannot combine with `offset` / `limit`.
8
- - `bundle: "local"` — with `symbol`, also include direct same-file local support when available. Cannot combine with `map`.
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 readseek structural map is appended automatically when available. Use that map's line ranges for follow-up `read({ offset, limit })`. Map and symbol coverage depends on readseek language support.
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" }` | specific overload/definition near line 42 |
20
- | `{ "symbol": "handleRequest", "bundle": "local" }` | symbol plus direct local support |
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. If unresolved but same-name candidates exist, the response lists retry hints like `name@<startLine>`.
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**: returns candidate names/kinds/ranges; retry with dot notation or `@<line>`.
29
- - **Fuzzy**: returns the best camelCase/substring match with a warning banner and confirmation hint. Verify before editing from fuzzy-match anchors.
30
- - **Not found**: falls back to normal read with a warning listing available symbols.
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
- AST-aware structural code search. Use when text search is too broad or brittle and you need code shape, such as calls, imports, declarations, or JSX. Returns matches grouped by file with edit-ready hashline anchors.
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 such as `typescript`, `tsx`, `javascript`, `jsx`, `rust`, or `python`; set it when syntax is ambiguous.
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; use `$$$` for variable-length args, body statements, object fields, JSX children, etc.
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
- ## Tips
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
- Patterns are parsed as code, not text: formatting is mostly ignored, but syntax must be valid for `lang`. Include semicolons in languages that require them. Use `grep` for plain text and `search` for structure.
40
+ Use `grep` for plain text and `search` for structure.
package/prompts/write.md CHANGED
@@ -1,24 +1,24 @@
1
- Write full file content. Creates new files and parent directories, overwrites existing files, and returns `LINE:HASH` anchors for immediate `edit` use.
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` to create a file or intentionally replace a whole file. For small changes or appends, `read` first and use `edit` (`insert_after` for appends).
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 is written, but hashlines are not generated, so there are no anchors to feed into `edit`.
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 append is best-effort and write still succeeds if map generation fails.
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`; 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`.
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. The string fields remain the backward-compatible human-readable fallback.
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, while `diffData.entries` uses expanded remove/add rows so renderers can show inline word changes without breaking hashline output.
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.
@@ -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.split("\n");
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 = await readseekRead(absolutePath, startLine, endIdx);
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}`;
@@ -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
  }
@@ -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
- async function runReadseek(args: string[], options: { signal?: AbortSignal } = {}): Promise<unknown> {
152
- const { stdout, stderr } = await new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
153
- const child = spawn(readseekBinaryPath(), args, { stdio: ["ignore", "pipe", "pipe"], signal: options.signal });
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
- child.stdout.on("data", (chunk: Buffer) => {
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
- child.stderr.on("data", (chunk: Buffer) => stderrChunks.push(chunk));
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({ stdout, stderr });
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: number, endLine: number): Promise<ReadseekReadOutput> {
297
- return parseReadOutput(await runReadseek([
298
- "read",
299
- filePath,
300
- "--start",
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
- export async function readseekMap(filePath: string, totalBytes: number): Promise<FileMap | null> {
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
- language?: string,
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
- return parseSearchOutput(await runReadseek(args, { signal })).results;
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(filePath: string, content: string): Promise<FileMap | null> {
333
- const tempDir = await mkdtemp(path.join(tmpdir(), "readseek-map-"));
334
- const tempPath = path.join(tempDir, path.basename(filePath) || `content${path.extname(filePath)}` || "content");
335
- try {
336
- await writeFile(tempPath, content, "utf8");
337
- const map = await readseekMap(tempPath, Buffer.byteLength(content, "utf8"));
338
- if (!map) return null;
339
- return { ...map, path: filePath };
340
- } finally {
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, effectiveLang, signal);
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
- "Use edit replace only when anchored edits are impractical.",
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 when only file counts are needed.",
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 list one directory, optionally with a glob filter.",
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