pi-readseek 0.1.0

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.
Files changed (67) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +41 -0
  3. package/index.ts +142 -0
  4. package/package.json +73 -0
  5. package/prompts/edit.md +113 -0
  6. package/prompts/find.md +19 -0
  7. package/prompts/grep.md +26 -0
  8. package/prompts/ls.md +11 -0
  9. package/prompts/read.md +33 -0
  10. package/prompts/sg.md +25 -0
  11. package/prompts/write.md +46 -0
  12. package/src/binary-detect.ts +22 -0
  13. package/src/binary-resolution.ts +77 -0
  14. package/src/coerce-obvious-int.ts +39 -0
  15. package/src/context-application.ts +70 -0
  16. package/src/context-hygiene.ts +503 -0
  17. package/src/diff-data.ts +303 -0
  18. package/src/doom-loop-suggestions.ts +42 -0
  19. package/src/doom-loop.ts +216 -0
  20. package/src/edit-classify.ts +190 -0
  21. package/src/edit-diff.ts +354 -0
  22. package/src/edit-output.ts +107 -0
  23. package/src/edit-render-helpers.ts +141 -0
  24. package/src/edit-syntax-validate.ts +120 -0
  25. package/src/edit.ts +725 -0
  26. package/src/find-parsers.ts +89 -0
  27. package/src/find-stat.ts +36 -0
  28. package/src/find.ts +613 -0
  29. package/src/grep-budget.ts +79 -0
  30. package/src/grep-output.ts +197 -0
  31. package/src/grep-render-helpers.ts +77 -0
  32. package/src/grep-symbol-scope.ts +197 -0
  33. package/src/grep.ts +792 -0
  34. package/src/hashline.ts +747 -0
  35. package/src/ls.ts +293 -0
  36. package/src/map-cache.ts +152 -0
  37. package/src/path-utils.ts +24 -0
  38. package/src/pending-diff-preview.ts +269 -0
  39. package/src/persistent-map-cache.ts +251 -0
  40. package/src/read-local-bundle.ts +87 -0
  41. package/src/read-output.ts +212 -0
  42. package/src/read-render-helpers.ts +104 -0
  43. package/src/read.ts +748 -0
  44. package/src/readseek/constants.ts +21 -0
  45. package/src/readseek/enums.ts +38 -0
  46. package/src/readseek/formatter.ts +431 -0
  47. package/src/readseek/language-detect.ts +29 -0
  48. package/src/readseek/mapper.ts +69 -0
  49. package/src/readseek/parser-errors.ts +22 -0
  50. package/src/readseek/parser-loader.ts +83 -0
  51. package/src/readseek/symbol-error-format.ts +18 -0
  52. package/src/readseek/symbol-lookup.ts +294 -0
  53. package/src/readseek/types.ts +79 -0
  54. package/src/readseek-client.ts +343 -0
  55. package/src/readseek-error-codes.ts +54 -0
  56. package/src/readseek-settings.ts +287 -0
  57. package/src/readseek-value.ts +144 -0
  58. package/src/replace-symbol.ts +74 -0
  59. package/src/runtime.ts +3 -0
  60. package/src/sg-output.ts +88 -0
  61. package/src/sg.ts +308 -0
  62. package/src/syntax-validate-mode.ts +25 -0
  63. package/src/tool-prompt-metadata.ts +76 -0
  64. package/src/tui-diff-component.ts +86 -0
  65. package/src/tui-diff-renderer.ts +92 -0
  66. package/src/tui-render-utils.ts +129 -0
  67. package/src/write.ts +532 -0
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jarkko Sakkinen
4
+ Copyright (c) 2026 Maxwell Newman
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # pi-readseek
2
+
3
+ `pi-readseek` is a pi extension for readseek-backed file reading, hash-anchored
4
+ editing, anchored grep, structural maps, symbol lookup, and structural search.
5
+ It exists to resolve conflicts between overlapping pi file-operation tools by
6
+ exposing one consistent readseek-centered surface.
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ pi install npm:pi-readseek
12
+ ```
13
+
14
+ ## Tools
15
+
16
+ - `read` — reads text files with `LINE:HASH` anchors for later `edit` calls;
17
+ images are returned as attachments. Large or symbol-scoped reads can include
18
+ structural maps powered by `@jarkkojs/readseek`.
19
+ - `edit` — changes existing text files using fresh anchors from `read`, `grep`,
20
+ `search`, or `write`. Use anchored variants such as `set_line`; `new_text`
21
+ must be plain replacement text and never include `LINE:HASH|` prefixes.
22
+ Set `new_text` to `""` to delete a line. Fuzzy replacement is literal
23
+ relocation, not approximate or semantic matching.
24
+ - `grep` — searches text and returns edit-ready `LINE:HASH` anchors without a
25
+ follow-up `read`.
26
+ - `search` — searches code by structural pattern and returns anchored
27
+ matches; use it when syntax matters more than raw text.
28
+ - `write` — creates or overwrites whole files and returns anchors for immediate
29
+ follow-up edits. Create a new file with `write` when there is no existing file
30
+ to edit.
31
+ - `ls` — lists one directory.
32
+ - `find` — recursively discovers files and directories.
33
+
34
+ ## Licensing
35
+
36
+ `pi-readseek` is licensed under `MIT`. See [LICENSE](LICENSE) for more
37
+ information.
38
+
39
+ `readseek` is originally derived from the source code of
40
+ [`pi-hashline-readmap`](https://github.com/coctostan/pi-hashline-readmap).
41
+ The relevant copyrights have been retained in [LICENSE](LICENSE).
package/index.ts ADDED
@@ -0,0 +1,142 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { registerReadTool } from "./src/read.js";
3
+ import { registerEditTool } from "./src/edit.js";
4
+ import { registerGrepTool } from "./src/grep.js";
5
+ import { registerSgTool, isSgAvailable } from "./src/sg.js";
6
+ import { registerWriteTool } from "./src/write.js";
7
+ import { registerLsTool } from "./src/ls.js";
8
+ import { registerFindTool } from "./src/find.js";
9
+ import { applyContextHygieneStaleContext } from "./src/context-application.js";
10
+ import {
11
+ createContextHygieneTracker,
12
+ normalizePathForContextHygiene,
13
+ type ContextHygieneEvent,
14
+ type ContextHygieneMetadata,
15
+ type ContextHygieneReport,
16
+ type ContextHygieneResource,
17
+ type ContextHygieneTracker,
18
+ } from "./src/context-hygiene.js";
19
+ import {
20
+ consumeDoomLoopWarning,
21
+ createDoomLoopState,
22
+ formatDoomLoopMessage,
23
+ recordToolCall,
24
+ } from "./src/doom-loop.js";
25
+
26
+ function isContextHygieneResource(value: unknown): value is ContextHygieneResource {
27
+ if (!value || typeof value !== "object") return false;
28
+ const resource = value as { kind?: unknown; key?: unknown };
29
+ return (resource.kind === "file" || resource.kind === "symbol") && typeof resource.key === "string";
30
+ }
31
+
32
+ function isContextHygieneMetadata(value: unknown): value is ContextHygieneMetadata {
33
+ if (!value || typeof value !== "object") return false;
34
+ const metadata = value as Partial<ContextHygieneMetadata>;
35
+ return (
36
+ metadata.schemaVersion === 1 &&
37
+ typeof metadata.tool === "string" &&
38
+ (metadata.classification === "read-context" ||
39
+ metadata.classification === "search-context" ||
40
+ metadata.classification === "mutation") &&
41
+ Array.isArray(metadata.resources) &&
42
+ metadata.resources.every(isContextHygieneResource)
43
+ );
44
+ }
45
+
46
+ function contextHygieneFromDetails(details: unknown): ContextHygieneMetadata | undefined {
47
+ if (!details || typeof details !== "object") return undefined;
48
+ const metadata = (details as { contextHygiene?: unknown }).contextHygiene;
49
+ return isContextHygieneMetadata(metadata) ? metadata : undefined;
50
+ }
51
+
52
+ function recordContextHygiene(
53
+ tracker: ContextHygieneTracker,
54
+ metadata: ContextHygieneMetadata,
55
+ toolCallId: unknown,
56
+ ): ContextHygieneEvent {
57
+ return tracker.record(metadata, {
58
+ resultId: typeof toolCallId === "string" ? toolCallId : undefined,
59
+ });
60
+ }
61
+
62
+ export default function piReadseekExtension(pi: ExtensionAPI): void {
63
+ const readTurns = new Map<string, number>();
64
+ const doomLoopState = createDoomLoopState();
65
+ const contextHygieneTracker = createContextHygieneTracker();
66
+ const readTurnKey = (absolutePath: string) => normalizePathForContextHygiene(absolutePath);
67
+ const noteRead = (absolutePath: string) => {
68
+ const report = contextHygieneTracker.generateReport();
69
+ const eventId = report.eventCount + 1;
70
+ readTurns.set(readTurnKey(absolutePath), eventId);
71
+ };
72
+ const wasReadInSession = (absolutePath: string) => readTurns.has(readTurnKey(absolutePath));
73
+
74
+ registerReadTool(pi, { onSuccessfulRead: noteRead });
75
+ registerEditTool(pi, { wasReadInSession });
76
+ const sgAvailable = isSgAvailable();
77
+ const searchGuideline = sgAvailable
78
+ ? "Use grep summary for counts; use search for structural code patterns."
79
+ : "Use grep summary for counts; install @jarkkojs/readseek to enable search.";
80
+
81
+ registerGrepTool(pi, { searchGuideline, onFileAnchored: noteRead });
82
+ registerSgTool(pi, { onFileAnchored: noteRead });
83
+ registerWriteTool(pi, { onFileAnchored: noteRead });
84
+ registerLsTool(pi);
85
+ registerFindTool(pi);
86
+
87
+ pi.on("tool_call", (event: any) => {
88
+ recordToolCall(
89
+ doomLoopState,
90
+ event.toolName,
91
+ event.toolCallId,
92
+ (event.input ?? {}) as Record<string, unknown>,
93
+ );
94
+ });
95
+
96
+ const expireStaleReadTurns = (report: ContextHygieneReport) => {
97
+ if (readTurns.size === 0) return;
98
+ for (const candidate of report.staleCandidates) {
99
+ if (!candidate.resourceKey.startsWith("file:")) continue;
100
+ const resourcePath = readTurnKey(candidate.resourceKey.slice("file:".length));
101
+ const recordedEventId = readTurns.get(resourcePath);
102
+ if (recordedEventId === undefined) continue;
103
+ if (recordedEventId < candidate.mutationEventId) {
104
+ readTurns.delete(resourcePath);
105
+ }
106
+ }
107
+ };
108
+
109
+ pi.on("context", (event: any): any => {
110
+ if (!Array.isArray(event.messages)) return undefined;
111
+ const report = contextHygieneTracker.generateReport();
112
+ const messages = applyContextHygieneStaleContext(event.messages, report);
113
+ expireStaleReadTurns(report);
114
+ return { messages };
115
+ });
116
+
117
+ pi.on("tool_result", (event: any) => {
118
+ const contextHygiene = contextHygieneFromDetails(event.details);
119
+ if (contextHygiene) recordContextHygiene(contextHygieneTracker, contextHygiene, event.toolCallId);
120
+
121
+ const doomLoop = consumeDoomLoopWarning(doomLoopState, event.toolCallId);
122
+ if (!doomLoop || !Array.isArray(event.content)) return undefined;
123
+
124
+ const content = [...event.content];
125
+ const prefix = `${formatDoomLoopMessage(doomLoop)}\n\n---\n`;
126
+ const textIndex = content.findIndex((item) => {
127
+ const maybeText = item as { type?: unknown; text?: unknown };
128
+ return maybeText.type === "text" && typeof maybeText.text === "string";
129
+ });
130
+ if (textIndex >= 0) {
131
+ const item = content[textIndex] as { type: "text"; text: string };
132
+ content[textIndex] = { ...item, text: `${prefix}${item.text}` };
133
+ } else {
134
+ content.unshift({ type: "text" as const, text: prefix });
135
+ }
136
+ return {
137
+ content,
138
+ details: event.details,
139
+ isError: event.isError,
140
+ };
141
+ });
142
+ }
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "pi-readseek",
3
+ "version": "0.1.0",
4
+ "description": "Pi extension for readseek-backed hash-anchored read/edit/grep, structural code maps, structural search, and file exploration",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./index.ts"
8
+ },
9
+ "keywords": [
10
+ "pi-package",
11
+ "pi-extension",
12
+ "hashline",
13
+ "code-map",
14
+ "structural-search"
15
+ ],
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/coctostan/pi-readseek.git"
19
+ },
20
+ "author": "Maxwell Newman",
21
+ "license": "MIT",
22
+ "pi": {
23
+ "extensions": [
24
+ "./index.ts"
25
+ ]
26
+ },
27
+ "files": [
28
+ "index.ts",
29
+ "src/",
30
+ "prompts/",
31
+ "LICENSE",
32
+ "README.md"
33
+ ],
34
+ "scripts": {
35
+ "test": "node scripts/vitest-run.mjs",
36
+ "typecheck": "tsc --noEmit"
37
+ },
38
+ "engines": {
39
+ "node": ">=20.0.0"
40
+ },
41
+ "dependencies": {
42
+ "@jarkkojs/readseek": "0.1.5",
43
+ "diff": "^8.0.3",
44
+ "ignore": "^7.0.5",
45
+ "picomatch": "^4.0.4",
46
+ "tree-sitter-wasms": "0.1.13",
47
+ "web-tree-sitter": "0.25.10",
48
+ "xxhash-wasm": "^1.1.0"
49
+ },
50
+ "peerDependencies": {
51
+ "@earendil-works/pi-coding-agent": "*",
52
+ "@earendil-works/pi-tui": "*",
53
+ "@sinclair/typebox": "*"
54
+ },
55
+ "devDependencies": {
56
+ "@earendil-works/pi-ai": "^0.78.0",
57
+ "@earendil-works/pi-coding-agent": "^0.78.0",
58
+ "@earendil-works/pi-tui": "^0.78.0",
59
+ "@sinclair/typebox": "^0.34.48",
60
+ "@types/node": "^25.5.0",
61
+ "jiti": "^2.7.0",
62
+ "typescript": "^5.9.3",
63
+ "vitest": "^4.1.0"
64
+ },
65
+ "directories": {
66
+ "doc": "docs",
67
+ "test": "tests"
68
+ },
69
+ "bugs": {
70
+ "url": "https://github.com/coctostan/pi-readseek/issues"
71
+ },
72
+ "homepage": "https://github.com/coctostan/pi-readseek#readme"
73
+ }
@@ -0,0 +1,113 @@
1
+ Surgically edit existing text files. Prefer hash-verified anchored edits from fresh `read`, `grep`, `search`, or `write` output; copy `LINE:HASH` anchors exactly.
2
+
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
+
5
+ ## Variants
6
+
7
+ | Variant | Use | Anchors |
8
+ |---|---|---|
9
+ | `set_line` | Replace/delete one line | 1 |
10
+ | `replace_lines` | Replace/delete a contiguous range | 2 |
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 |
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.
17
+
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.
19
+
20
+ ## Input shape
21
+
22
+ ```json
23
+ {
24
+ "path": "src/foo.ts",
25
+ "edits": [
26
+ { "set_line": { "anchor": "42:ab1", "new_text": "const x = 2;" } },
27
+ { "replace_lines": { "start_anchor": "50:c3d", "end_anchor": "55:e4f", "new_text": "const y = 3;\nreturn y;" } },
28
+ { "insert_after": { "anchor": "60:f5a", "new_text": "// TODO\n" } },
29
+ { "replace_symbol": { "symbol": "add", "new_body": "export function add(a, b) {\n return a + b;\n}" } },
30
+ { "replace": { "old_text": "value", "new_text": "result", "all": true } }
31
+ ]
32
+ }
33
+ ```
34
+
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.
36
+
37
+ ## Optional post-edit verification
38
+
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
+
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
+
43
+ ## `replace_symbol`
44
+
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
+
47
+ Rules:
48
+ - Use an exact name, dotted path, or `@<line>`. If `read({symbol})` returned a fuzzy match, confirm the exact symbol before editing.
49
+ - Supported for TypeScript, JavaScript, Rust, and Java. For other languages, use anchored edits.
50
+ - `new_body` must not be empty or whitespace-only.
51
+ - Write `new_body` without extra leading indentation; `edit` re-indents it to match the original symbol.
52
+ - 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.
54
+
55
+ ## Stale anchors
56
+
57
+ If anchors no longer match, `edit` fails with a hash mismatch (`hash-mismatch`) and shows nearby current lines. Lines marked `>>>` include updated anchors:
58
+
59
+ ```text
60
+ >>> 41:b34| const renamed = 3;
61
+ ```
62
+
63
+ Copy the updated `LINE:HASH` and retry. If the target moved farther away, re-run `read`, `grep`, `search`, or `write` for fresh anchors.
64
+
65
+ If `edit` auto-relocates an anchor, check the warning and verify the edit landed in the intended place.
66
+
67
+ ## Validation and warnings
68
+
69
+ - All edits are checked before writing; if a hard validation fails, nothing is written.
70
+ - Anchored edits are applied bottom-up so line numbers stay stable.
71
+ - `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
+
75
+ Syntax validation runs before writing when supported:
76
+ - Supported: Rust, C++, C headers, Java.
77
+ - Default `warn`: write succeeds, but warnings include `syntax-regression: lines X-Y`.
78
+ - `block`: aborts without writing.
79
+ - `off`: skips validation.
80
+ - `PI_HASHLINE_SYNTAX_VALIDATE` can set the default mode.
81
+
82
+ Existing syntax errors are tolerated; the warning is for newly introduced parser errors.
83
+
84
+ ## Diff data contract
85
+
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.
87
+
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`.
89
+
90
+ `diffData` is a stable versioned contract:
91
+
92
+ ```ts
93
+ type DiffData = {
94
+ version: 1;
95
+ entries: Array<
96
+ | { kind: "context"; oldLine: number; newLine: number; text: string }
97
+ | { kind: "add"; newLine: number; text: string }
98
+ | { kind: "remove"; oldLine: number; text: string }
99
+ | { kind: "meta"; text: string }
100
+ >;
101
+ stats: { added: number; removed: number; context: number };
102
+ language?: string;
103
+ blockRanges?: Array<{ kind: "add" | "remove"; startLine: number; endLine: number }>;
104
+ inlineDiffs?: Array<{
105
+ removeLineIndex: number;
106
+ addLineIndex: number;
107
+ removeSpans: Array<{ kind: "equal" | "remove" | "add"; text: string }>;
108
+ addSpans: Array<{ kind: "equal" | "remove" | "add"; text: string }>;
109
+ }>;
110
+ };
111
+ ```
112
+
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.
@@ -0,0 +1,19 @@
1
+ Find files recursively by name. Uses glob patterns by default, respects nested `.gitignore`, includes hidden files, and returns relative paths.
2
+
3
+ ## Parameters
4
+
5
+ - `pattern` — required. Glob by default; with `regex: true`, JavaScript regex against each basename.
6
+ - `path` — directory to search, default cwd.
7
+ - `type` — `"file"` default, `"dir"`, or `"any"`.
8
+ - `limit` — max returned entries after filtering/sorting, default 1000.
9
+ - `maxDepth` — non-negative directory depth limit.
10
+ - `sortBy` — `"name"` default, `"mtime"`, or `"size"`; use `reverse: true` for descending/newest/largest first.
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.
13
+
14
+ ## Output and usage
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.
18
+
19
+ Use `find` for recursive file-name discovery, `ls` for one directory, and `grep` for file contents. Remember: `pattern` matches basenames, not full paths.
@@ -0,0 +1,26 @@
1
+ Search file contents. Non-summary results return `LINE:HASH` anchors usable directly by `edit`; no follow-up `read` is needed.
2
+
3
+ ## Modes
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`.
9
+
10
+ ## Parameters
11
+
12
+ - `pattern` — regex by default; use `literal: true` for exact strings or regex metacharacters.
13
+ - `path` — file or directory, default cwd.
14
+ - `glob` — file filter, e.g. `'*.ts'` or `'**/*.test.ts'`.
15
+ - `ignoreCase` — case-insensitive search.
16
+ - `context` — surrounding lines for normal grep.
17
+ - `limit` — max matches, default 100.
18
+ - `summary` — counts only, no anchors.
19
+ - `scope` — only `"symbol"` is supported.
20
+ - `scopeContext` — non-negative context within symbol scope; requires `scope: "symbol"`.
21
+
22
+ ## Truncation and guidance
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.
25
+
26
+ Use `grep` for text search. For structural code patterns such as calls, imports, or JSX, prefer `search`.
package/prompts/ls.md ADDED
@@ -0,0 +1,11 @@
1
+ List one directory. Shows directories first with `/`, then files, sorted alphabetically; dotfiles are included.
2
+
3
+ ## Parameters
4
+
5
+ - `path` — directory to list, default cwd.
6
+ - `limit` — max entries, default 500; must be positive.
7
+ - `glob` — optional entry-name filter such as `*.ts` or `.env*`.
8
+
9
+ ## Usage
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.
@@ -0,0 +1,33 @@
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.
2
+
3
+ ## Parameters
4
+
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`.
9
+
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.
11
+
12
+ ## Symbol examples
13
+
14
+ | Query | Reads |
15
+ |---|---|
16
+ | `{ "symbol": "processEvent" }` | function or top-level symbol |
17
+ | `{ "symbol": "EventEmitter" }` | class/interface/type/enum/etc. |
18
+ | `{ "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 |
21
+
22
+ ## Symbol resolution
23
+
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>`.
25
+
26
+ Result behavior:
27
+ - **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.
32
+
33
+ Hash anchors from symbol and bundled reads are valid for `edit`.
package/prompts/sg.md ADDED
@@ -0,0 +1,25 @@
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.
2
+
3
+ ## Parameters
4
+
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.
7
+ - `path` — file or directory, default cwd.
8
+
9
+ ## Pattern syntax
10
+
11
+ - `$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.
14
+
15
+ ## Examples
16
+
17
+ - `console.log($$$ARGS)` — calls.
18
+ - `import $NAME from '$SOURCE'` — default imports.
19
+ - `export function $NAME($$$PARAMS) { $$$BODY }` — exported functions.
20
+ - `$OBJ.$METHOD($$$ARGS)` — method calls.
21
+ - `<$TAG $$$ATTRS>$$$CHILDREN</$TAG>` — JSX/TSX elements.
22
+
23
+ ## Tips
24
+
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.
@@ -0,0 +1,46 @@
1
+ Write full file content. Creates new files and parent directories, overwrites existing files, and returns `LINE:HASH` anchors for immediate `edit` use.
2
+
3
+ ## Use / avoid
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).
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`.
8
+
9
+ ## Parameters
10
+
11
+ - `path` — relative or absolute file path.
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.
14
+
15
+ ## Output
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`.
18
+
19
+ ## Diff data contract
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.
22
+
23
+ `diffData` is a stable versioned contract:
24
+
25
+ ```ts
26
+ type DiffData = {
27
+ version: 1;
28
+ entries: Array<
29
+ | { kind: "context"; oldLine: number; newLine: number; text: string }
30
+ | { kind: "add"; newLine: number; text: string }
31
+ | { kind: "remove"; oldLine: number; text: string }
32
+ | { kind: "meta"; text: string }
33
+ >;
34
+ stats: { added: number; removed: number; context: number };
35
+ language?: string;
36
+ blockRanges?: Array<{ kind: "add" | "remove"; startLine: number; endLine: number }>;
37
+ inlineDiffs?: Array<{
38
+ removeLineIndex: number;
39
+ addLineIndex: number;
40
+ removeSpans: Array<{ kind: "equal" | "remove" | "add"; text: string }>;
41
+ addSpans: Array<{ kind: "equal" | "remove" | "add"; text: string }>;
42
+ }>;
43
+ };
44
+ ```
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.
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Returns true if the buffer appears to contain binary (non-text) content.
3
+ *
4
+ * Detection strategy:
5
+ * 1. NUL byte (0x00) — universally indicates binary
6
+ * 2. Invalid UTF-8 sequences — Node.js decodes them as U+FFFD replacement
7
+ * characters. To avoid false-positives on valid UTF-8 text that
8
+ * intentionally contains U+FFFD (encoded as EF BF BD), we do a round-trip
9
+ * check: if re-encoding the decoded string produces a different byte
10
+ * sequence, then U+FFFD was introduced by the decoder — not present in
11
+ * the original bytes as valid UTF-8.
12
+ */
13
+ export function looksLikeBinary(buf: Buffer): boolean {
14
+ if (buf.length === 0) return false;
15
+ // Fast path: NUL byte is a reliable binary marker
16
+ if (buf.includes(0)) return true;
17
+ const decoded = buf.toString("utf8");
18
+ if (!decoded.includes("\uFFFD")) return false;
19
+
20
+ // If U+FFFD came from invalid byte sequences, re-encoding will differ.
21
+ return !Buffer.from(decoded, "utf8").equals(buf);
22
+ }
@@ -0,0 +1,77 @@
1
+ import { existsSync as defaultExistsSync, readFileSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import { dirname, resolve } from "node:path";
4
+
5
+ const require = createRequire(import.meta.url);
6
+
7
+ export interface ResolveBundledBinDeps {
8
+ resolvePackageJson?: (specifier: string) => string;
9
+ readPackageJson?: (packageJsonPath: string) => string;
10
+ existsSync?: (candidate: string) => boolean;
11
+ platform?: NodeJS.Platform;
12
+ }
13
+
14
+ function defaultResolvePackageJson(specifier: string): string {
15
+ return require.resolve(specifier);
16
+ }
17
+
18
+ function defaultReadPackageJson(packageJsonPath: string): string {
19
+ return readFileSync(packageJsonPath, "utf8");
20
+ }
21
+
22
+ function binEntryFor(packageJsonText: string, binName: string): string | undefined {
23
+ const parsed = JSON.parse(packageJsonText) as { bin?: string | Record<string, string> };
24
+ if (typeof parsed.bin === "string") return parsed.bin;
25
+ return parsed.bin?.[binName];
26
+ }
27
+
28
+ function commandCandidates(basePath: string, platform: NodeJS.Platform): string[] {
29
+ if (platform !== "win32") return [basePath];
30
+ return [`${basePath}.exe`, basePath];
31
+ }
32
+
33
+ function firstExisting(candidates: string[], existsSync: (candidate: string) => boolean): string | undefined {
34
+ return candidates.find((candidate) => existsSync(candidate));
35
+ }
36
+
37
+ export interface ExecutableCommand {
38
+ command: string;
39
+ argsPrefix: string[];
40
+ }
41
+
42
+ export function executableCommand(binaryPath: string, platform: NodeJS.Platform = process.platform): ExecutableCommand {
43
+ if (platform === "win32" && /\.js$/i.test(binaryPath)) {
44
+ return { command: process.execPath, argsPrefix: [binaryPath] };
45
+ }
46
+ return { command: binaryPath, argsPrefix: [] };
47
+ }
48
+
49
+ export function resolveBundledBin(
50
+ packageName: string,
51
+ binName: string,
52
+ fallbackCommand: string,
53
+ deps: ResolveBundledBinDeps = {},
54
+ ): string {
55
+ const resolvePackageJson = deps.resolvePackageJson ?? defaultResolvePackageJson;
56
+ const readPackageJson = deps.readPackageJson ?? defaultReadPackageJson;
57
+ const existsSync = deps.existsSync ?? defaultExistsSync;
58
+ const platform = deps.platform ?? process.platform;
59
+
60
+ try {
61
+ const packageJsonPath = resolvePackageJson(`${packageName}/package.json`);
62
+ const packageDir = dirname(packageJsonPath);
63
+ const binEntry = binEntryFor(readPackageJson(packageJsonPath), binName);
64
+ if (!binEntry) return fallbackCommand;
65
+
66
+
67
+ const packageBinCandidate = firstExisting(
68
+ commandCandidates(resolve(packageDir, binEntry), platform),
69
+ existsSync,
70
+ );
71
+ if (packageBinCandidate) return packageBinCandidate;
72
+ } catch {
73
+ // Fall through to PATH fallback.
74
+ }
75
+
76
+ return fallbackCommand;
77
+ }