pi-read-map 1.0.0 → 1.2.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.
- package/.codemap/cache.db +0 -0
- package/.husky/pre-commit +1 -0
- package/.jscpd.json +7 -0
- package/AGENTS.md +158 -0
- package/CHANGELOG.md +49 -0
- package/README.md +10 -6
- package/demo.md +108 -0
- package/knip.json +11 -0
- package/package.json +11 -3
- package/scripts/go_outline.go +43 -11
- package/scripts/python_outline.py +16 -0
- package/src/.codemap/cache.db +0 -0
- package/src/formatter.ts +29 -12
- package/src/index.ts +124 -2
- package/src/language-detect.ts +6 -0
- package/src/mapper.ts +4 -0
- package/src/mappers/c.ts +7 -0
- package/src/mappers/clojure.ts +613 -0
- package/src/mappers/cpp.ts +58 -0
- package/src/mappers/csv.ts +1 -0
- package/src/mappers/ctags.ts +33 -4
- package/src/mappers/fallback.ts +1 -0
- package/src/mappers/go.ts +11 -4
- package/src/mappers/json.ts +1 -0
- package/src/mappers/jsonl.ts +468 -68
- package/src/mappers/markdown.ts +1 -0
- package/src/mappers/python.ts +12 -4
- package/src/mappers/rust.ts +46 -0
- package/src/mappers/sql.ts +1 -0
- package/src/mappers/toml.ts +1 -0
- package/src/mappers/typescript.ts +36 -1
- package/src/mappers/yaml.ts +1 -0
- package/src/types.ts +6 -2
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
npm run validate && npm run test -- --exclude='**/e2e/**'
|
package/.jscpd.json
ADDED
package/AGENTS.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
> Last updated: 2026-02-14
|
|
4
|
+
|
|
5
|
+
Pi extension that augments the built-in `read` tool with structural file maps for large files (>2,000 lines or >50 KB). Intercepts `read` calls, generates symbol maps via language-specific parsers, and sends them as separate `file-map` messages after the tool result.
|
|
6
|
+
|
|
7
|
+
## Commands (verified 2026-02-14)
|
|
8
|
+
|
|
9
|
+
| Command | Purpose | ~Time |
|
|
10
|
+
|---------|---------|-------|
|
|
11
|
+
| `npm run test` | Unit + integration tests (vitest) | ~2s |
|
|
12
|
+
| `npm run test:integration` | Integration tests only | ~1s |
|
|
13
|
+
| `npm run test:e2e` | E2E tests (requires pi + tmux) | ~60s |
|
|
14
|
+
| `npm run bench` | Benchmarks | ~5s |
|
|
15
|
+
| `npm run typecheck` | `tsc --noEmit` | ~2s |
|
|
16
|
+
| `npm run lint` | oxlint (via npx) | ~1s |
|
|
17
|
+
| `npm run lint:fix` | Auto-fix lint issues | ~1s |
|
|
18
|
+
| `npm run format` | Format with oxfmt | ~1s |
|
|
19
|
+
| `npm run format:check` | Check formatting | ~1s |
|
|
20
|
+
| `npm run validate` | typecheck + lint + format:check | ~4s |
|
|
21
|
+
|
|
22
|
+
## File Map
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
src/
|
|
26
|
+
├── index.ts → Extension entry: tool registration, caching, message rendering
|
|
27
|
+
├── mapper.ts → Dispatcher: routes files to language mappers, fallback chain
|
|
28
|
+
├── formatter.ts → Budget-aware formatting with progressive detail reduction
|
|
29
|
+
├── language-detect.ts → File extension → language mapping
|
|
30
|
+
├── types.ts → FileMap, FileSymbol, MapOptions, FileMapMessageDetails
|
|
31
|
+
├── enums.ts → SymbolKind (21 kinds), DetailLevel (5 levels)
|
|
32
|
+
├── constants.ts → THRESHOLDS: lines, bytes, budget tiers
|
|
33
|
+
└── mappers/ → One mapper per language (17 total)
|
|
34
|
+
├── typescript.ts → ts-morph (handles TS + JS)
|
|
35
|
+
├── rust.ts → tree-sitter-rust
|
|
36
|
+
├── cpp.ts → tree-sitter-cpp (C++ and .h files)
|
|
37
|
+
├── clojure.ts → tree-sitter-clojure (.clj, .cljs, .cljc, .edn)
|
|
38
|
+
├── python.ts → subprocess: scripts/python_outline.py
|
|
39
|
+
├── go.ts → subprocess: scripts/go_outline.go
|
|
40
|
+
├── json.ts → subprocess: jq
|
|
41
|
+
├── c.ts → Regex patterns
|
|
42
|
+
├── sql.ts → Regex
|
|
43
|
+
├── markdown.ts → Regex
|
|
44
|
+
├── yaml.ts → Regex
|
|
45
|
+
├── toml.ts → Regex
|
|
46
|
+
├── csv.ts → In-process streaming
|
|
47
|
+
├── jsonl.ts → In-process streaming
|
|
48
|
+
├── ctags.ts → universal-ctags fallback
|
|
49
|
+
└── fallback.ts → Grep-based final fallback
|
|
50
|
+
|
|
51
|
+
scripts/
|
|
52
|
+
├── python_outline.py → Python AST extraction (called by python mapper)
|
|
53
|
+
├── go_outline.go → Go AST extraction (compiled on first use)
|
|
54
|
+
└── go_outline → Compiled Go binary
|
|
55
|
+
|
|
56
|
+
tests/
|
|
57
|
+
├── unit/ → Mapper tests, formatter tests, language detection
|
|
58
|
+
├── integration/ → Dispatch, caching, budget enforcement, map messages
|
|
59
|
+
├── e2e/ → Real pi sessions via tmux (vitest.e2e.config.ts)
|
|
60
|
+
├── fixtures/ → Sample files per language
|
|
61
|
+
├── benchmarks/ → Mapper performance benchmarks
|
|
62
|
+
└── helpers/ → Test utilities (pi-runner, constants, tree-sitter)
|
|
63
|
+
|
|
64
|
+
docs/
|
|
65
|
+
├── plans/ → Implementation plans (phased)
|
|
66
|
+
├── handoffs/ → Session handoff notes
|
|
67
|
+
├── reviews/ → Phase review documents
|
|
68
|
+
└── todo/ → Outstanding work items
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Architecture
|
|
72
|
+
|
|
73
|
+
Dispatch chain: `index.ts` → `mapper.ts` → language mapper → ctags → fallback.
|
|
74
|
+
|
|
75
|
+
Budget enforcement in `formatter.ts` uses progressive detail reduction:
|
|
76
|
+
- 10 KB: full detail (signatures, modifiers)
|
|
77
|
+
- 15 KB: compact (drop signatures)
|
|
78
|
+
- 20 KB: minimal (names + line ranges only)
|
|
79
|
+
- 50 KB: outline (top-level symbols only)
|
|
80
|
+
- 100 KB: truncated (first/last 50 symbols, hard cap)
|
|
81
|
+
|
|
82
|
+
Maps are cached in-memory by `(filePath, mtime)`. Delivered as custom `file-map` messages via `pi.sendMessage()` after the `tool_result` event.
|
|
83
|
+
|
|
84
|
+
## Golden Samples
|
|
85
|
+
|
|
86
|
+
| For | Reference | Key patterns |
|
|
87
|
+
|-----|-----------|--------------|
|
|
88
|
+
| New mapper | `src/mappers/csv.ts` | Simple, clean, regex-free in-process parsing |
|
|
89
|
+
| Complex mapper | `src/mappers/typescript.ts` | ts-morph AST walk, nested symbols, modifiers |
|
|
90
|
+
| Tree-sitter mapper | `src/mappers/clojure.ts` | tree-sitter AST walk, reader conditionals, platform modifiers |
|
|
91
|
+
| Subprocess mapper | `src/mappers/python.ts` | Calls external script, parses JSON output |
|
|
92
|
+
| Unit test | `tests/unit/mappers/csv.test.ts` | Fixture-based, edge cases, null returns |
|
|
93
|
+
| Integration test | `tests/integration/budget-enforcement.test.ts` | Tests progressive detail reduction |
|
|
94
|
+
|
|
95
|
+
## Heuristics
|
|
96
|
+
|
|
97
|
+
| When | Do |
|
|
98
|
+
|------|-----|
|
|
99
|
+
| Adding a new language | Create `src/mappers/<lang>.ts`, add to `MAPPERS` in `mapper.ts`, add extension in `language-detect.ts`, add unit test |
|
|
100
|
+
| Mapper returns too many symbols | Rely on `formatter.ts` budget system, don't filter in mappers |
|
|
101
|
+
| Mapper can't parse a file | Return `null` — the dispatch chain falls through to ctags then fallback |
|
|
102
|
+
| Adding a new SymbolKind | Add to `enums.ts`, update formatter if display differs |
|
|
103
|
+
| Testing mappers | Use fixture files in `tests/fixtures/`, never mock file reads |
|
|
104
|
+
| E2E tests | Require pi installed + tmux; use `tests/helpers/pi-runner.ts` |
|
|
105
|
+
|
|
106
|
+
## Boundaries
|
|
107
|
+
|
|
108
|
+
**Always:**
|
|
109
|
+
- Return `FileMap` or `null` from mappers (never throw)
|
|
110
|
+
- Include `startLine` and `endLine` for every symbol (1-indexed)
|
|
111
|
+
- Run `npm run validate` before committing
|
|
112
|
+
- Use existing `FileSymbol` interface for all symbol data
|
|
113
|
+
|
|
114
|
+
**Ask first:**
|
|
115
|
+
- Changing budget thresholds in `constants.ts`
|
|
116
|
+
- Adding new `DetailLevel` variants
|
|
117
|
+
- Modifying the tool description in `index.ts`
|
|
118
|
+
- Changes to the `tool_result` event handler
|
|
119
|
+
|
|
120
|
+
**Never:**
|
|
121
|
+
- Filter symbols in mappers based on budget (formatter handles this)
|
|
122
|
+
- Add runtime dependencies without discussing (binary size matters for pi extensions)
|
|
123
|
+
- Use `any` types
|
|
124
|
+
- Disable lint rules
|
|
125
|
+
|
|
126
|
+
## Codebase State
|
|
127
|
+
|
|
128
|
+
- `oxlint` installed as devDependency; `npm run lint` exits cleanly (0 errors, 0 warnings)
|
|
129
|
+
- `tree-sitter` pinned to 0.22.4 due to peer dependency conflicts (see `docs/todo/upgrade-tree-sitter-0.26.md`)
|
|
130
|
+
- `tree-sitter-clojure` pinned to commit SHA from `github:ghoseb/tree-sitter-clojure` (third-party fork)
|
|
131
|
+
- Go outline script auto-compiles on first use; compiled binary checked in at `scripts/go_outline`
|
|
132
|
+
- Phase 1-5 of implementation plan complete; remaining TODOs in `docs/todo/`
|
|
133
|
+
|
|
134
|
+
| Docstrings / JSDoc | `FileSymbol.docstring?: string` | First-line summary of doc comments |
|
|
135
|
+
| Exported flag | `FileSymbol.isExported?: boolean` | Whether symbol is part of public API |
|
|
136
|
+
| Required imports | `FileMap.imports: string[]` | Always an array, never undefined |
|
|
137
|
+
|
|
138
|
+
## Terminology
|
|
139
|
+
|
|
140
|
+
| Docstring | First line of a JSDoc / doc comment on a symbol |
|
|
141
|
+
| isExported | Boolean flag: true if symbol is part of the module's public API |
|
|
142
|
+
| Term | Means |
|
|
143
|
+
|------|-------|
|
|
144
|
+
| Map | Structural outline of a file's symbols with line ranges |
|
|
145
|
+
| Mapper | Language-specific parser that produces a `FileMap` |
|
|
146
|
+
| Budget | Maximum byte size for formatted map output |
|
|
147
|
+
| Detail level | How much information each symbol carries (full → truncated) |
|
|
148
|
+
| Fallback chain | mapper → ctags → grep when parsers fail |
|
|
149
|
+
| Pending map | Map waiting to be sent after `tool_result` event fires |
|
|
150
|
+
|
|
151
|
+
## Tech Stack
|
|
152
|
+
|
|
153
|
+
- **Runtime:** Node.js (ES2022 modules)
|
|
154
|
+
- **Language:** TypeScript (strict, `noUncheckedIndexedAccess`)
|
|
155
|
+
- **Testing:** Vitest (unit/integration: 10s timeout, e2e: 60s timeout)
|
|
156
|
+
- **Linting:** oxlint + oxfmt
|
|
157
|
+
- **Parsing:** ts-morph, tree-sitter (rust, cpp, clojure), regex, subprocess (Python/Go/jq)
|
|
158
|
+
- **Framework:** pi extension API (`@mariozechner/pi-coding-agent`)
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [1.2.0] - 2026-02-14
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **Clojure mapper** — tree-sitter-based parser for `.clj`, `.cljs`, `.cljc`, and `.edn` files. Extracts `ns`, `defn`, `defn-`, `def`, `defonce`, `defmacro`, `defmulti`, `defmethod`, `defprotocol`, `defrecord`, and `deftype` forms with docstrings, signatures, modifiers, and protocol method children. Supports reader conditionals (`#?`) with per-platform annotations. Contributed by [Baishampayan Ghose](https://github.com/ghoseb). ([#2](https://github.com/Whamp/pi-read-map/pull/2))
|
|
10
|
+
- Clojure demo asset (`clojure/core.clj` from the official Clojure repo)
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Tree-sitter tests use `describe.runIf` for conditional execution
|
|
15
|
+
|
|
16
|
+
## [1.1.0] - 2026-02-10
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- Docstring extraction (`FileSymbol.docstring`) across all mappers
|
|
21
|
+
- Export flag (`FileSymbol.isExported`) across all mappers
|
|
22
|
+
- Required imports (`FileMap.imports`) across all mappers
|
|
23
|
+
- Skipped read recovery — detects reads cancelled by steering queue and re-issues them
|
|
24
|
+
- JSONL session-aware maps for pi session files
|
|
25
|
+
- Directory read handling with EISDIR error and `ls` fallback
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- Symbol duplication in file map output
|
|
30
|
+
- oxlint warnings and errors resolved across codebase
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- Test helpers refactored; `models.ts` renamed to `constants.ts`
|
|
35
|
+
|
|
36
|
+
## [1.0.0] - 2026-02-09
|
|
37
|
+
|
|
38
|
+
Initial release.
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- Structural file maps for large files (>2,000 lines or >50 KB)
|
|
43
|
+
- 14 language mappers: TypeScript, JavaScript, Python, Go, Rust, C, C++, SQL, JSON, JSONL, YAML, TOML, CSV, Markdown
|
|
44
|
+
- Budget-aware formatting with progressive detail reduction (10 KB full → 100 KB truncated)
|
|
45
|
+
- In-memory caching by file path and modification time
|
|
46
|
+
- Fallback chain: language mapper → universal-ctags → grep
|
|
47
|
+
- Custom `file-map` messages delivered after `tool_result` events
|
|
48
|
+
- E2E test infrastructure via tmux
|
|
49
|
+
- Demo assets from 10 major open-source projects
|
package/README.md
CHANGED
|
@@ -23,22 +23,20 @@ https://github.com/user-attachments/assets/4408f37b-b669-453f-a588-336a5332ae90
|
|
|
23
23
|
## What It Does
|
|
24
24
|
|
|
25
25
|
- **Generates structural maps** showing symbols, classes, functions, and their exact line ranges
|
|
26
|
-
- **Supports
|
|
27
|
-
- **
|
|
28
|
-
- **Enforces
|
|
26
|
+
- **Supports 17 languages** through specialized parsers: TypeScript, JavaScript, Python, Go, Rust, C, C++, Clojure, ClojureScript, SQL, JSON, JSONL, YAML, TOML, CSV, Markdown, EDN
|
|
27
|
+
- **Extracts structural outlines** — functions, classes, and their line ranges — typically under 1% of file size
|
|
28
|
+
- **Enforces budgets** through progressive detail reduction (10 KB full → 15 KB compact → 20 KB minimal → 50 KB outline → 100 KB hard cap)
|
|
29
29
|
- **Caches maps** in memory by file path and modification time for instant re-reads
|
|
30
30
|
- **Falls back** from language-specific parsers to ctags to grep heuristics
|
|
31
31
|
|
|
32
32
|
## Installation
|
|
33
33
|
|
|
34
|
-
### From Git
|
|
34
|
+
### From Git
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
37
|
# Global install
|
|
38
38
|
pi install https://github.com/Whamp/pi-read-map
|
|
39
39
|
|
|
40
|
-
# Project-local install (adds to .pi/settings.json)
|
|
41
|
-
pi install https://github.com/Whamp/pi-read-map -l
|
|
42
40
|
```
|
|
43
41
|
|
|
44
42
|
### From npm
|
|
@@ -132,6 +130,7 @@ src/
|
|
|
132
130
|
├── go.ts # Go AST via subprocess
|
|
133
131
|
├── rust.ts # tree-sitter
|
|
134
132
|
├── cpp.ts # tree-sitter for C/C++
|
|
133
|
+
├── clojure.ts # tree-sitter for Clojure/ClojureScript/EDN
|
|
135
134
|
├── c.ts # Regex patterns
|
|
136
135
|
├── sql.ts # Regex
|
|
137
136
|
├── json.ts # jq subprocess
|
|
@@ -175,6 +174,7 @@ The extension intercepts `read` calls and decides:
|
|
|
175
174
|
- `tree-sitter` - Parser framework
|
|
176
175
|
- `tree-sitter-cpp` - C/C++ parsing
|
|
177
176
|
- `tree-sitter-rust` - Rust parsing
|
|
177
|
+
- `tree-sitter-clojure` - Clojure parsing
|
|
178
178
|
|
|
179
179
|
**System tools (optional):**
|
|
180
180
|
- `python3` - Python mapper
|
|
@@ -186,6 +186,10 @@ The extension intercepts `read` calls and decides:
|
|
|
186
186
|
|
|
187
187
|
This project was inspired by and built upon the foundation of [codemap](https://github.com/kcosr/codemap) by [kcosr](https://github.com/kcosr). Check out the original project for the ideas that made this possible.
|
|
188
188
|
|
|
189
|
+
### Contributors
|
|
190
|
+
|
|
191
|
+
- [Baishampayan Ghose](https://github.com/ghoseb) — Clojure tree-sitter mapper and [tree-sitter-clojure](https://github.com/ghoseb/tree-sitter-clojure) grammar
|
|
192
|
+
|
|
189
193
|
## License
|
|
190
194
|
|
|
191
195
|
MIT
|
package/demo.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Skipped Read Recovery
|
|
2
|
+
|
|
3
|
+
*2026-02-12T16:06:08Z*
|
|
4
|
+
|
|
5
|
+
When pi-read-map generates a file map for a large file, it sends the map via `pi.sendMessage()`. During streaming, this defaults to `deliverAs: "steer"`, which pushes the message into the agent's steering queue. After each tool execution, the agent loop checks the steering queue — if non-empty, it skips all remaining tool calls with 'Skipped due to queued user message.'
|
|
6
|
+
|
|
7
|
+
This means: if the agent requests 3 parallel reads and the first file triggers a map, the other 2 reads are cancelled.
|
|
8
|
+
|
|
9
|
+
**Skipped Read Recovery** detects these cancelled reads at `turn_end` and sends a `followUp` message instructing the agent to re-issue them.
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
1. A `turn_end` event handler inspects all tool results for the turn
|
|
14
|
+
2. It identifies read results containing 'Skipped due to queued user message.'
|
|
15
|
+
3. It extracts the file paths from the assistant message's `toolCall` entries
|
|
16
|
+
4. It sends a `read-recovery` followUp message listing the interrupted paths
|
|
17
|
+
5. The followUp triggers a new turn where the agent re-reads the skipped files
|
|
18
|
+
|
|
19
|
+
## Files Changed
|
|
20
|
+
|
|
21
|
+
| File | Change |
|
|
22
|
+
|------|--------|
|
|
23
|
+
| `src/index.ts` | Added `turn_end` handler and `read-recovery` message renderer |
|
|
24
|
+
| `tests/integration/skipped-read-recovery.test.ts` | 9 new tests covering detection, edge cases, and rendering |
|
|
25
|
+
|
|
26
|
+
## Tests
|
|
27
|
+
|
|
28
|
+
The new test suite covers:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
cd /home/will/projects/pi-read-map && npx vitest run tests/integration/skipped-read-recovery.test.ts 2>&1 | tail -20
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```output
|
|
35
|
+
|
|
36
|
+
RUN v3.2.4 /home/will/projects/pi-read-map
|
|
37
|
+
|
|
38
|
+
✓ tests/integration/skipped-read-recovery.test.ts (9 tests) 6ms
|
|
39
|
+
|
|
40
|
+
Test Files 1 passed (1)
|
|
41
|
+
Tests 9 passed (9)
|
|
42
|
+
Start at 08:06:28
|
|
43
|
+
Duration 845ms (transform 139ms, setup 0ms, collect 625ms, tests 6ms, environment 0ms, prepare 61ms)
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Full Validation
|
|
48
|
+
|
|
49
|
+
Typecheck, lint, format, and dead-code detection all pass:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
cd /home/will/projects/pi-read-map && npm run validate 2>&1
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```output
|
|
56
|
+
|
|
57
|
+
> pi-read-map@1.0.0 validate
|
|
58
|
+
> npm run typecheck && npm run lint && npm run format:check && npm run dead-code
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
> pi-read-map@1.0.0 typecheck
|
|
62
|
+
> tsc --noEmit
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
> pi-read-map@1.0.0 lint
|
|
66
|
+
> oxlint -c .oxlintrc.json src tests
|
|
67
|
+
|
|
68
|
+
Found 0 warnings and 0 errors.
|
|
69
|
+
Finished in 214ms on 61 files with 427 rules using 16 threads.
|
|
70
|
+
|
|
71
|
+
> pi-read-map@1.0.0 format:check
|
|
72
|
+
> oxfmt --config .oxfmtrc.jsonc src tests --check
|
|
73
|
+
|
|
74
|
+
Checking formatting...
|
|
75
|
+
|
|
76
|
+
All matched files use the correct format.
|
|
77
|
+
Finished in 556ms on 73 files using 16 threads.
|
|
78
|
+
|
|
79
|
+
> pi-read-map@1.0.0 dead-code
|
|
80
|
+
> knip
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Full Test Suite
|
|
85
|
+
|
|
86
|
+
All 197 tests pass (27 test files), including the 9 new recovery tests:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
cd /home/will/projects/pi-read-map && npm run test 2>&1 | grep -E "(Test Files|Tests|✓ tests/integration)"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
```output
|
|
93
|
+
✓ tests/integration/mapper-dispatch.test.ts (8 tests) 627ms
|
|
94
|
+
✓ tests/integration/budget-enforcement.test.ts (8 tests) 693ms
|
|
95
|
+
✓ tests/integration/directory-read.test.ts (4 tests) 14ms
|
|
96
|
+
✓ tests/integration/cache-behavior.test.ts (4 tests) 262ms
|
|
97
|
+
✓ tests/integration/skipped-read-recovery.test.ts (9 tests) 7ms
|
|
98
|
+
✓ tests/integration/separate-map-message.test.ts (6 tests) 9ms
|
|
99
|
+
✓ tests/integration/extension-load.test.ts (6 tests) 6ms
|
|
100
|
+
Test Files 27 passed (27)
|
|
101
|
+
Tests 197 passed (197)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Implementation Detail
|
|
105
|
+
|
|
106
|
+
The recovery handler in `src/index.ts` narrows the `AgentMessage` union to `AssistantMessage` (via `role === "assistant"` check) and then matches skipped `toolCallId`s against the `toolCall` entries in the assistant content to extract file paths. The recovery message is delivered as a `followUp`, which triggers a new agent turn without entering the steering queue.
|
|
107
|
+
|
|
108
|
+
A custom `read-recovery` message renderer shows a compact summary in collapsed view (e.g., 'Recovery: 2 interrupted read(s) being re-issued') and the full path list when expanded.
|
package/knip.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://unpkg.com/knip@latest/schema.json",
|
|
3
|
+
"entry": ["src/index.ts"],
|
|
4
|
+
"project": ["src/**/*.ts"],
|
|
5
|
+
"ignore": ["scripts/**", "tests/fixtures/**"],
|
|
6
|
+
"ignoreDependencies": [
|
|
7
|
+
"@factory/eslint-plugin",
|
|
8
|
+
"ultracite",
|
|
9
|
+
"@mariozechner/pi-tui"
|
|
10
|
+
]
|
|
11
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-read-map",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Pi extension that adds structural file maps for large files",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"pi": {
|
|
@@ -14,12 +14,15 @@
|
|
|
14
14
|
"lint:fix": "oxlint -c .oxlintrc.json src tests --fix",
|
|
15
15
|
"format": "oxfmt --config .oxfmtrc.jsonc src tests",
|
|
16
16
|
"format:check": "oxfmt --config .oxfmtrc.jsonc src tests --check",
|
|
17
|
-
"
|
|
17
|
+
"dead-code": "knip",
|
|
18
|
+
"duplicates": "jscpd src --min-tokens 50 --min-lines 5",
|
|
19
|
+
"validate": "npm run typecheck && npm run lint && npm run format:check && npm run dead-code",
|
|
18
20
|
"test": "vitest run",
|
|
19
21
|
"test:watch": "vitest",
|
|
20
22
|
"test:integration": "vitest run tests/integration/",
|
|
21
23
|
"test:e2e": "vitest run --config vitest.e2e.config.ts",
|
|
22
|
-
"bench": "vitest bench"
|
|
24
|
+
"bench": "vitest bench",
|
|
25
|
+
"prepare": "husky"
|
|
23
26
|
},
|
|
24
27
|
"keywords": [
|
|
25
28
|
"pi-package",
|
|
@@ -41,6 +44,7 @@
|
|
|
41
44
|
"license": "MIT",
|
|
42
45
|
"dependencies": {
|
|
43
46
|
"tree-sitter": "0.22.4",
|
|
47
|
+
"tree-sitter-clojure": "github:ghoseb/tree-sitter-clojure#78928e6",
|
|
44
48
|
"tree-sitter-cpp": "0.23.4",
|
|
45
49
|
"tree-sitter-rust": "0.23.3",
|
|
46
50
|
"ts-morph": "27.0.2"
|
|
@@ -50,7 +54,11 @@
|
|
|
50
54
|
"@mariozechner/pi-coding-agent": "^0.52.9",
|
|
51
55
|
"@sinclair/typebox": "^0.34.0",
|
|
52
56
|
"@types/node": "^22.0.0",
|
|
57
|
+
"husky": "^9.1.7",
|
|
58
|
+
"jscpd": "^4.0.8",
|
|
59
|
+
"knip": "^5.83.1",
|
|
53
60
|
"oxfmt": "^0.28.0",
|
|
61
|
+
"oxlint": "^1.46.0",
|
|
54
62
|
"typescript": "^5.7.0",
|
|
55
63
|
"ultracite": "^7.1.5",
|
|
56
64
|
"vitest": "^3.0.0"
|
package/scripts/go_outline.go
CHANGED
|
@@ -18,6 +18,8 @@ type Symbol struct {
|
|
|
18
18
|
Signature string `json:"signature,omitempty"`
|
|
19
19
|
Modifiers []string `json:"modifiers,omitempty"`
|
|
20
20
|
Children []Symbol `json:"children,omitempty"`
|
|
21
|
+
Docstring string `json:"docstring,omitempty"`
|
|
22
|
+
IsExported bool `json:"isExported"`
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
type OutlineResult struct {
|
|
@@ -106,10 +108,16 @@ func extractSymbols(fset *token.FileSet, file *ast.File) []Symbol {
|
|
|
106
108
|
case token.TYPE:
|
|
107
109
|
for _, spec := range d.Specs {
|
|
108
110
|
ts := spec.(*ast.TypeSpec)
|
|
111
|
+
doc := ts.Doc
|
|
112
|
+
if doc == nil {
|
|
113
|
+
doc = d.Doc
|
|
114
|
+
}
|
|
109
115
|
sym := Symbol{
|
|
110
|
-
Name:
|
|
111
|
-
StartLine:
|
|
112
|
-
EndLine:
|
|
116
|
+
Name: ts.Name.Name,
|
|
117
|
+
StartLine: fset.Position(d.Pos()).Line,
|
|
118
|
+
EndLine: fset.Position(d.End()).Line,
|
|
119
|
+
Docstring: getDocstringFirstLine(doc),
|
|
120
|
+
IsExported: isExportedName(ts.Name.Name),
|
|
113
121
|
}
|
|
114
122
|
switch t := ts.Type.(type) {
|
|
115
123
|
case *ast.StructType:
|
|
@@ -163,10 +171,12 @@ func extractSymbols(fset *token.FileSet, file *ast.File) []Symbol {
|
|
|
163
171
|
continue
|
|
164
172
|
}
|
|
165
173
|
sym := Symbol{
|
|
166
|
-
Name:
|
|
167
|
-
Kind:
|
|
168
|
-
StartLine:
|
|
169
|
-
EndLine:
|
|
174
|
+
Name: name.Name,
|
|
175
|
+
Kind: kind,
|
|
176
|
+
StartLine: fset.Position(vs.Pos()).Line,
|
|
177
|
+
EndLine: fset.Position(vs.End()).Line,
|
|
178
|
+
Docstring: getDocstringFirstLine(d.Doc),
|
|
179
|
+
IsExported: isExportedName(name.Name),
|
|
170
180
|
}
|
|
171
181
|
if vs.Type != nil {
|
|
172
182
|
sym.Signature = formatType(vs.Type)
|
|
@@ -177,10 +187,12 @@ func extractSymbols(fset *token.FileSet, file *ast.File) []Symbol {
|
|
|
177
187
|
}
|
|
178
188
|
case *ast.FuncDecl:
|
|
179
189
|
sym := Symbol{
|
|
180
|
-
Name:
|
|
181
|
-
StartLine:
|
|
182
|
-
EndLine:
|
|
183
|
-
Signature:
|
|
190
|
+
Name: d.Name.Name,
|
|
191
|
+
StartLine: fset.Position(d.Pos()).Line,
|
|
192
|
+
EndLine: fset.Position(d.End()).Line,
|
|
193
|
+
Signature: formatParams(d.Type.Params) + formatResults(d.Type.Results),
|
|
194
|
+
Docstring: getDocstringFirstLine(d.Doc),
|
|
195
|
+
IsExported: isExportedName(d.Name.Name),
|
|
184
196
|
}
|
|
185
197
|
if d.Recv != nil && len(d.Recv.List) > 0 {
|
|
186
198
|
sym.Kind = "method"
|
|
@@ -199,6 +211,26 @@ func extractSymbols(fset *token.FileSet, file *ast.File) []Symbol {
|
|
|
199
211
|
return symbols
|
|
200
212
|
}
|
|
201
213
|
|
|
214
|
+
func getDocstringFirstLine(doc *ast.CommentGroup) string {
|
|
215
|
+
if doc == nil {
|
|
216
|
+
return ""
|
|
217
|
+
}
|
|
218
|
+
text := doc.Text()
|
|
219
|
+
if text == "" {
|
|
220
|
+
return ""
|
|
221
|
+
}
|
|
222
|
+
lines := strings.SplitN(text, "\n", 2)
|
|
223
|
+
return strings.TrimSpace(lines[0])
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
func isExportedName(name string) bool {
|
|
227
|
+
if len(name) == 0 {
|
|
228
|
+
return false
|
|
229
|
+
}
|
|
230
|
+
r := []rune(name)
|
|
231
|
+
return r[0] >= 'A' && r[0] <= 'Z'
|
|
232
|
+
}
|
|
233
|
+
|
|
202
234
|
func extractImports(file *ast.File) []string {
|
|
203
235
|
var imports []string
|
|
204
236
|
for _, imp := range file.Imports {
|
|
@@ -82,6 +82,18 @@ def get_end_line(node: ast.AST) -> int:
|
|
|
82
82
|
return getattr(node, 'lineno', 0)
|
|
83
83
|
|
|
84
84
|
|
|
85
|
+
def get_docstring_first_line(node: ast.AST) -> str | None:
|
|
86
|
+
"""Extract the first line of a docstring from a class or function."""
|
|
87
|
+
try:
|
|
88
|
+
doc = ast.get_docstring(node)
|
|
89
|
+
except TypeError:
|
|
90
|
+
return None
|
|
91
|
+
if not doc:
|
|
92
|
+
return None
|
|
93
|
+
first_line = doc.split('\n')[0].strip()
|
|
94
|
+
return first_line if first_line else None
|
|
95
|
+
|
|
96
|
+
|
|
85
97
|
def extract_symbols(node: ast.AST, parent_end: int | None = None) -> list[dict]:
|
|
86
98
|
"""Recursively extract symbols from AST."""
|
|
87
99
|
symbols = []
|
|
@@ -113,6 +125,8 @@ def extract_symbols(node: ast.AST, parent_end: int | None = None) -> list[dict]:
|
|
|
113
125
|
"endLine": end_line,
|
|
114
126
|
"modifiers": modifiers,
|
|
115
127
|
"children": children if children else None,
|
|
128
|
+
"docstring": get_docstring_first_line(node),
|
|
129
|
+
"is_exported": not node.name.startswith("_"),
|
|
116
130
|
})
|
|
117
131
|
|
|
118
132
|
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
@@ -132,6 +146,8 @@ def extract_symbols(node: ast.AST, parent_end: int | None = None) -> list[dict]:
|
|
|
132
146
|
"endLine": end_line,
|
|
133
147
|
"signature": get_signature(node),
|
|
134
148
|
"modifiers": modifiers if modifiers else None,
|
|
149
|
+
"docstring": get_docstring_first_line(node),
|
|
150
|
+
"is_exported": not node.name.startswith("_"),
|
|
135
151
|
})
|
|
136
152
|
|
|
137
153
|
elif isinstance(node, ast.Assign):
|
|
Binary file
|
package/src/formatter.ts
CHANGED
|
@@ -43,14 +43,23 @@ function formatSymbol(
|
|
|
43
43
|
|
|
44
44
|
let { name } = symbol;
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
46
|
+
if (level === DetailLevel.Full) {
|
|
47
|
+
if (symbol.signature) {
|
|
48
|
+
// Check whether the signature already contains the symbol name.
|
|
49
|
+
// Full-declaration signatures (e.g. Rust "pub fn foo(x: i32) -> bool")
|
|
50
|
+
// include the name; partial signatures (e.g. Python "(x, y) -> None")
|
|
51
|
+
// do not and should be appended.
|
|
52
|
+
if (symbol.signature.includes(name)) {
|
|
53
|
+
name = symbol.signature;
|
|
54
|
+
} else {
|
|
55
|
+
if (symbol.modifiers?.length) {
|
|
56
|
+
name = `${symbol.modifiers.join(" ")} ${name}`;
|
|
57
|
+
}
|
|
58
|
+
name = `${name}${symbol.signature}`;
|
|
59
|
+
}
|
|
60
|
+
} else if (symbol.modifiers?.length) {
|
|
61
|
+
name = `${symbol.modifiers.join(" ")} ${name}`;
|
|
62
|
+
}
|
|
54
63
|
}
|
|
55
64
|
|
|
56
65
|
// Format based on kind
|
|
@@ -79,6 +88,11 @@ function formatSymbol(
|
|
|
79
88
|
}
|
|
80
89
|
}
|
|
81
90
|
|
|
91
|
+
// Append docstring at Full detail level
|
|
92
|
+
if (level === DetailLevel.Full && symbol.docstring) {
|
|
93
|
+
formatted += ` — ${symbol.docstring}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
82
96
|
return formatted;
|
|
83
97
|
}
|
|
84
98
|
|
|
@@ -155,7 +169,6 @@ export function formatFileMap(map: FileMap, level?: DetailLevel): string {
|
|
|
155
169
|
if (
|
|
156
170
|
effectiveLevel !== DetailLevel.Outline &&
|
|
157
171
|
effectiveLevel !== DetailLevel.Truncated &&
|
|
158
|
-
map.imports?.length &&
|
|
159
172
|
map.imports.length > 0
|
|
160
173
|
) {
|
|
161
174
|
const importList =
|
|
@@ -245,7 +258,7 @@ export function reduceToLevel(map: FileMap, level: DetailLevel): FileMap {
|
|
|
245
258
|
return {
|
|
246
259
|
...map,
|
|
247
260
|
detailLevel: DetailLevel.Outline,
|
|
248
|
-
imports:
|
|
261
|
+
imports: [],
|
|
249
262
|
symbols: map.symbols.map((s) => ({
|
|
250
263
|
name: s.name,
|
|
251
264
|
kind: s.kind,
|
|
@@ -256,7 +269,7 @@ export function reduceToLevel(map: FileMap, level: DetailLevel): FileMap {
|
|
|
256
269
|
}
|
|
257
270
|
|
|
258
271
|
if (level === DetailLevel.Minimal) {
|
|
259
|
-
// Remove signatures but keep children flattened
|
|
272
|
+
// Remove signatures and docstrings but keep children flattened
|
|
260
273
|
return {
|
|
261
274
|
...map,
|
|
262
275
|
detailLevel: DetailLevel.Minimal,
|
|
@@ -265,11 +278,13 @@ export function reduceToLevel(map: FileMap, level: DetailLevel): FileMap {
|
|
|
265
278
|
kind: s.kind,
|
|
266
279
|
startLine: s.startLine,
|
|
267
280
|
endLine: s.endLine,
|
|
281
|
+
isExported: s.isExported,
|
|
268
282
|
children: s.children?.map((c) => ({
|
|
269
283
|
name: c.name,
|
|
270
284
|
kind: c.kind,
|
|
271
285
|
startLine: c.startLine,
|
|
272
286
|
endLine: c.endLine,
|
|
287
|
+
isExported: c.isExported,
|
|
273
288
|
})),
|
|
274
289
|
})),
|
|
275
290
|
};
|
|
@@ -294,6 +309,8 @@ function stripSignatures(symbol: FileSymbol): FileSymbol {
|
|
|
294
309
|
startLine: symbol.startLine,
|
|
295
310
|
endLine: symbol.endLine,
|
|
296
311
|
modifiers: symbol.modifiers,
|
|
312
|
+
docstring: symbol.docstring,
|
|
313
|
+
isExported: symbol.isExported,
|
|
297
314
|
children: symbol.children?.map(stripSignatures),
|
|
298
315
|
};
|
|
299
316
|
}
|
|
@@ -332,7 +349,7 @@ export function reduceToTruncated(
|
|
|
332
349
|
...map,
|
|
333
350
|
symbols: [...firstSymbols, ...lastSymbols],
|
|
334
351
|
detailLevel: DetailLevel.Truncated,
|
|
335
|
-
imports:
|
|
352
|
+
imports: [],
|
|
336
353
|
truncatedInfo: {
|
|
337
354
|
totalSymbols: total,
|
|
338
355
|
shownSymbols: symbolsEach * 2,
|