pi-read-map 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Will Hampson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,191 @@
1
+ # pi-read-map
2
+
3
+ ![pi-read-map banner](banner.png)
4
+
5
+ This pi extension augments the built-in `read` tool with structural file maps. When you open a file larger than 2,000 lines or 50 KB, the extension generates a map of every symbol and its line range. You navigate large codebases precisely instead of scanning sequentially.
6
+
7
+ ## Why This Exists
8
+
9
+ **The problem:** pi sees only the first 2,000 lines of a 50,000-line source file. Ask "how does the type checker handle unions?" and the model either hallucinates or burns tokens re-reading until it finds the answer.
10
+
11
+ **The trade-off:** `pi-read-map` spends ~2,000–10,000 tokens upfront to generate a map of the entire file. The extension triggers only for files exceeding the truncation limit (>2,000 lines and >50 KB); smaller files pass through unchanged.
12
+
13
+ **The payoff:** The map stays in context. Ask "show me the merge implementation," "compare error handling in these three functions," or "what symbols exist after line 40,000?" without re-reading. The investment pays for itself when you analyze a large file beyond a single summary.
14
+
15
+ ## Demo
16
+
17
+ See `pi-read-map` in action analyzing the TypeScript compiler's 54,000-line type checker:
18
+
19
+
20
+ https://github.com/user-attachments/assets/4408f37b-b669-453f-a588-336a5332ae90
21
+
22
+
23
+ ## What It Does
24
+
25
+ - **Generates structural maps** showing symbols, classes, functions, and their exact line ranges
26
+ - **Supports 16 languages** through specialized parsers: TypeScript, JavaScript, Python, Go, Rust, C, C++, SQL, JSON, JSONL, YAML, TOML, CSV, Markdown
27
+ - **Compresses aggressively** to ~3-5% of original file size (a 400 KB file yields an ~18 KB map)
28
+ - **Enforces a 20 KB budget** through progressive detail reduction
29
+ - **Caches maps** in memory by file path and modification time for instant re-reads
30
+ - **Falls back** from language-specific parsers to ctags to grep heuristics
31
+
32
+ ## Installation
33
+
34
+ ### From Git (Recommended)
35
+
36
+ ```bash
37
+ # Global install
38
+ pi install https://github.com/Whamp/pi-read-map
39
+
40
+ # Project-local install (adds to .pi/settings.json)
41
+ pi install https://github.com/Whamp/pi-read-map -l
42
+ ```
43
+
44
+ ### From npm
45
+
46
+ ```bash
47
+ # Global install
48
+ pi install npm:pi-read-map
49
+
50
+ # Project-local install
51
+ pi install npm:pi-read-map -l
52
+ ```
53
+
54
+ ### From Local Directory
55
+
56
+ ```bash
57
+ # Clone and install globally
58
+ pi install ./path/to/pi-read-map
59
+
60
+ # Or project-local
61
+ pi install ./path/to/pi-read-map -l
62
+ ```
63
+
64
+ ### One-off Test
65
+
66
+ Try the extension without installing:
67
+
68
+ ```bash
69
+ pi -e https://github.com/Whamp/pi-read-map
70
+ pi -e npm:pi-read-map
71
+ pi -e ./path/to/pi-read-map
72
+ ```
73
+
74
+ ## Verification
75
+
76
+ Start pi or run `/reload`. Then read a large file:
77
+
78
+ ```
79
+ read path/to/large-file.ts
80
+ ```
81
+
82
+ Output includes the truncated content followed by:
83
+
84
+ ```
85
+ [Truncated: showing first 2000 lines of 10,247 (50 KB of 412 KB)]
86
+ ───────────────────────────────────────
87
+ File Map: path/to/large-file.ts
88
+ 10,247 lines │ 412 KB │ TypeScript
89
+ ───────────────────────────────────────
90
+
91
+ class ProcessorConfig: [18-32]
92
+ class BatchProcessor: [34-890]
93
+ constructor(config: ProcessorConfig): [40-65]
94
+ async run(items: List<Item>): [67-180]
95
+ ...
96
+
97
+ ───────────────────────────────────────
98
+ Use read(path, offset=LINE, limit=N) for targeted reads.
99
+ ───────────────────────────────────────
100
+ ```
101
+
102
+ ## Development
103
+
104
+ ```bash
105
+ npm run typecheck # Type checking
106
+ npm run lint # Linting with oxlint
107
+ npm run lint:fix # Auto-fix lint issues
108
+ npm run format # Format with oxfmt
109
+ npm run format:check # Check formatting
110
+ npm run validate # Run all checks
111
+ npm run test # Unit tests
112
+ npm run test:watch # Watch mode
113
+ npm run test:integration # Integration tests
114
+ npm run test:e2e # End-to-end tests
115
+ npm run bench # Benchmarks
116
+ ```
117
+
118
+ ## Project Structure
119
+
120
+ ```
121
+ src/
122
+ ├── index.ts # Extension entry: tool registration, caching, messages
123
+ ├── mapper.ts # Dispatcher: routes files to language mappers
124
+ ├── formatter.ts # Budget-aware formatting with detail reduction
125
+ ├── language-detect.ts # Maps file extensions to languages
126
+ ├── types.ts # Shared interfaces (FileMap, FileSymbol)
127
+ ├── enums.ts # SymbolKind, DetailLevel
128
+ ├── constants.ts # Thresholds (2,000 lines, 50 KB, 20 KB budget)
129
+ └── mappers/ # Language-specific parsers
130
+ ├── typescript.ts # ts-morph for TS/JS
131
+ ├── python.ts # Python AST via subprocess
132
+ ├── go.ts # Go AST via subprocess
133
+ ├── rust.ts # tree-sitter
134
+ ├── cpp.ts # tree-sitter for C/C++
135
+ ├── c.ts # Regex patterns
136
+ ├── sql.ts # Regex
137
+ ├── json.ts # jq subprocess
138
+ ├── jsonl.ts # Streaming parser
139
+ ├── yaml.ts # Regex
140
+ ├── toml.ts # Regex
141
+ ├── csv.ts # In-process parser
142
+ ├── markdown.ts # Regex
143
+ ├── ctags.ts # universal-ctags fallback
144
+ └── fallback.ts # Grep-based final fallback
145
+
146
+ scripts/
147
+ ├── python_outline.py # Python AST extraction
148
+ └── go_outline.go # Go AST extraction (compiles on first use)
149
+
150
+ tests/
151
+ ├── unit/ # Mapper and utility tests
152
+ ├── integration/ # Dispatcher, caching, budget enforcement
153
+ ├── e2e/ # Real pi sessions via tmux
154
+ └── fixtures/ # Sample files per language
155
+ ```
156
+
157
+ ## How It Works
158
+
159
+ The extension intercepts `read` calls and decides:
160
+
161
+ 1. **Small files** (≤2,000 lines, ≤50 KB): Delegate to built-in read tool
162
+ 2. **Targeted reads** (offset or limit provided): Delegate to built-in read tool
163
+ 3. **Large files:**
164
+ - Call built-in read for the first chunk
165
+ - Detect language from file extension
166
+ - Dispatch to a mapper (language-specific → ctags → grep fallback)
167
+ - Format with budget enforcement
168
+ - Cache the map
169
+ - Send as a separate `file-map` message after `tool_result`
170
+
171
+ ## Dependencies
172
+
173
+ **npm packages:**
174
+ - `ts-morph` - TypeScript AST analysis
175
+ - `tree-sitter` - Parser framework
176
+ - `tree-sitter-cpp` - C/C++ parsing
177
+ - `tree-sitter-rust` - Rust parsing
178
+
179
+ **System tools (optional):**
180
+ - `python3` - Python mapper
181
+ - `go` - Go mapper
182
+ - `jq` - JSON mapper
183
+ - `universal-ctags` - Language fallback
184
+
185
+ ## Acknowledgments
186
+
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
+
189
+ ## License
190
+
191
+ MIT
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "pi-read-map",
3
+ "version": "1.0.0",
4
+ "description": "Pi extension that adds structural file maps for large files",
5
+ "type": "module",
6
+ "pi": {
7
+ "extensions": [
8
+ "./src/index.ts"
9
+ ]
10
+ },
11
+ "scripts": {
12
+ "typecheck": "tsc --noEmit",
13
+ "lint": "oxlint -c .oxlintrc.json src tests",
14
+ "lint:fix": "oxlint -c .oxlintrc.json src tests --fix",
15
+ "format": "oxfmt --config .oxfmtrc.jsonc src tests",
16
+ "format:check": "oxfmt --config .oxfmtrc.jsonc src tests --check",
17
+ "validate": "npm run typecheck && npm run lint && npm run format:check",
18
+ "test": "vitest run",
19
+ "test:watch": "vitest",
20
+ "test:integration": "vitest run tests/integration/",
21
+ "test:e2e": "vitest run --config vitest.e2e.config.ts",
22
+ "bench": "vitest bench"
23
+ },
24
+ "keywords": [
25
+ "pi-package",
26
+ "pi",
27
+ "extension",
28
+ "read",
29
+ "file-map",
30
+ "code-navigation"
31
+ ],
32
+ "author": "Will Hampson",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/Whamp/pi-read-map"
36
+ },
37
+ "homepage": "https://github.com/Whamp/pi-read-map#readme",
38
+ "bugs": {
39
+ "url": "https://github.com/Whamp/pi-read-map/issues"
40
+ },
41
+ "license": "MIT",
42
+ "dependencies": {
43
+ "tree-sitter": "0.22.4",
44
+ "tree-sitter-cpp": "0.23.4",
45
+ "tree-sitter-rust": "0.23.3",
46
+ "ts-morph": "27.0.2"
47
+ },
48
+ "devDependencies": {
49
+ "@factory/eslint-plugin": "^0.1.0",
50
+ "@mariozechner/pi-coding-agent": "^0.52.9",
51
+ "@sinclair/typebox": "^0.34.0",
52
+ "@types/node": "^22.0.0",
53
+ "oxfmt": "^0.28.0",
54
+ "typescript": "^5.7.0",
55
+ "ultracite": "^7.1.5",
56
+ "vitest": "^3.0.0"
57
+ },
58
+ "overrides": {
59
+ "tree-sitter": "$tree-sitter"
60
+ }
61
+ }
@@ -0,0 +1,239 @@
1
+ package main
2
+
3
+ import (
4
+ "encoding/json"
5
+ "fmt"
6
+ "go/ast"
7
+ "go/parser"
8
+ "go/token"
9
+ "os"
10
+ "strings"
11
+ )
12
+
13
+ type Symbol struct {
14
+ Name string `json:"name"`
15
+ Kind string `json:"kind"`
16
+ StartLine int `json:"startLine"`
17
+ EndLine int `json:"endLine"`
18
+ Signature string `json:"signature,omitempty"`
19
+ Modifiers []string `json:"modifiers,omitempty"`
20
+ Children []Symbol `json:"children,omitempty"`
21
+ }
22
+
23
+ type OutlineResult struct {
24
+ Package string `json:"package"`
25
+ Imports []string `json:"imports,omitempty"`
26
+ Symbols []Symbol `json:"symbols"`
27
+ Error string `json:"error,omitempty"`
28
+ }
29
+
30
+ func formatType(expr ast.Expr) string {
31
+ if expr == nil {
32
+ return ""
33
+ }
34
+ switch t := expr.(type) {
35
+ case *ast.Ident:
36
+ return t.Name
37
+ case *ast.SelectorExpr:
38
+ return formatType(t.X) + "." + t.Sel.Name
39
+ case *ast.StarExpr:
40
+ return "*" + formatType(t.X)
41
+ case *ast.ArrayType:
42
+ return "[]" + formatType(t.Elt)
43
+ case *ast.MapType:
44
+ return "map[" + formatType(t.Key) + "]" + formatType(t.Value)
45
+ case *ast.InterfaceType:
46
+ return "interface{}"
47
+ case *ast.StructType:
48
+ return "struct{}"
49
+ case *ast.FuncType:
50
+ return "func(...)"
51
+ case *ast.ChanType:
52
+ return "chan " + formatType(t.Value)
53
+ case *ast.Ellipsis:
54
+ return "..." + formatType(t.Elt)
55
+ default:
56
+ return "?"
57
+ }
58
+ }
59
+
60
+ func formatParams(fields *ast.FieldList) string {
61
+ if fields == nil || len(fields.List) == 0 {
62
+ return "()"
63
+ }
64
+ var parts []string
65
+ for _, f := range fields.List {
66
+ typeStr := formatType(f.Type)
67
+ if len(f.Names) == 0 {
68
+ parts = append(parts, typeStr)
69
+ } else {
70
+ for _, name := range f.Names {
71
+ parts = append(parts, name.Name+" "+typeStr)
72
+ }
73
+ }
74
+ }
75
+ return "(" + strings.Join(parts, ", ") + ")"
76
+ }
77
+
78
+ func formatResults(fields *ast.FieldList) string {
79
+ if fields == nil || len(fields.List) == 0 {
80
+ return ""
81
+ }
82
+ if len(fields.List) == 1 && len(fields.List[0].Names) == 0 {
83
+ return " " + formatType(fields.List[0].Type)
84
+ }
85
+ var parts []string
86
+ for _, f := range fields.List {
87
+ typeStr := formatType(f.Type)
88
+ if len(f.Names) == 0 {
89
+ parts = append(parts, typeStr)
90
+ } else {
91
+ for _, name := range f.Names {
92
+ parts = append(parts, name.Name+" "+typeStr)
93
+ }
94
+ }
95
+ }
96
+ return " (" + strings.Join(parts, ", ") + ")"
97
+ }
98
+
99
+ func extractSymbols(fset *token.FileSet, file *ast.File) []Symbol {
100
+ var symbols []Symbol
101
+
102
+ for _, decl := range file.Decls {
103
+ switch d := decl.(type) {
104
+ case *ast.GenDecl:
105
+ switch d.Tok {
106
+ case token.TYPE:
107
+ for _, spec := range d.Specs {
108
+ ts := spec.(*ast.TypeSpec)
109
+ sym := Symbol{
110
+ Name: ts.Name.Name,
111
+ StartLine: fset.Position(d.Pos()).Line,
112
+ EndLine: fset.Position(d.End()).Line,
113
+ }
114
+ switch t := ts.Type.(type) {
115
+ case *ast.StructType:
116
+ sym.Kind = "struct"
117
+ // Extract struct fields as children
118
+ if t.Fields != nil {
119
+ for _, field := range t.Fields.List {
120
+ for _, name := range field.Names {
121
+ sym.Children = append(sym.Children, Symbol{
122
+ Name: name.Name,
123
+ Kind: "field",
124
+ StartLine: fset.Position(field.Pos()).Line,
125
+ EndLine: fset.Position(field.End()).Line,
126
+ Signature: formatType(field.Type),
127
+ })
128
+ }
129
+ }
130
+ }
131
+ case *ast.InterfaceType:
132
+ sym.Kind = "interface"
133
+ // Extract interface methods as children
134
+ if t.Methods != nil {
135
+ for _, method := range t.Methods.List {
136
+ for _, name := range method.Names {
137
+ if ft, ok := method.Type.(*ast.FuncType); ok {
138
+ sym.Children = append(sym.Children, Symbol{
139
+ Name: name.Name,
140
+ Kind: "method",
141
+ StartLine: fset.Position(method.Pos()).Line,
142
+ EndLine: fset.Position(method.End()).Line,
143
+ Signature: formatParams(ft.Params) + formatResults(ft.Results),
144
+ })
145
+ }
146
+ }
147
+ }
148
+ }
149
+ default:
150
+ sym.Kind = "type"
151
+ }
152
+ symbols = append(symbols, sym)
153
+ }
154
+ case token.CONST, token.VAR:
155
+ kind := "variable"
156
+ if d.Tok == token.CONST {
157
+ kind = "constant"
158
+ }
159
+ for _, spec := range d.Specs {
160
+ vs := spec.(*ast.ValueSpec)
161
+ for _, name := range vs.Names {
162
+ if name.Name == "_" {
163
+ continue
164
+ }
165
+ sym := Symbol{
166
+ Name: name.Name,
167
+ Kind: kind,
168
+ StartLine: fset.Position(vs.Pos()).Line,
169
+ EndLine: fset.Position(vs.End()).Line,
170
+ }
171
+ if vs.Type != nil {
172
+ sym.Signature = formatType(vs.Type)
173
+ }
174
+ symbols = append(symbols, sym)
175
+ }
176
+ }
177
+ }
178
+ case *ast.FuncDecl:
179
+ sym := Symbol{
180
+ Name: d.Name.Name,
181
+ StartLine: fset.Position(d.Pos()).Line,
182
+ EndLine: fset.Position(d.End()).Line,
183
+ Signature: formatParams(d.Type.Params) + formatResults(d.Type.Results),
184
+ }
185
+ if d.Recv != nil && len(d.Recv.List) > 0 {
186
+ sym.Kind = "method"
187
+ // Add receiver info
188
+ recv := d.Recv.List[0]
189
+ recvType := formatType(recv.Type)
190
+ sym.Signature = "(" + recvType + ") " + sym.Name + sym.Signature
191
+ sym.Name = recvType + "." + d.Name.Name
192
+ } else {
193
+ sym.Kind = "function"
194
+ }
195
+ symbols = append(symbols, sym)
196
+ }
197
+ }
198
+
199
+ return symbols
200
+ }
201
+
202
+ func extractImports(file *ast.File) []string {
203
+ var imports []string
204
+ for _, imp := range file.Imports {
205
+ path := strings.Trim(imp.Path.Value, `"`)
206
+ if imp.Name != nil && imp.Name.Name != "." && imp.Name.Name != "_" {
207
+ imports = append(imports, imp.Name.Name+" "+path)
208
+ } else {
209
+ imports = append(imports, path)
210
+ }
211
+ }
212
+ return imports
213
+ }
214
+
215
+ func main() {
216
+ if len(os.Args) < 2 {
217
+ result := OutlineResult{Error: "usage: go_outline <file.go>"}
218
+ json.NewEncoder(os.Stdout).Encode(result)
219
+ os.Exit(1)
220
+ }
221
+
222
+ filePath := os.Args[1]
223
+
224
+ fset := token.NewFileSet()
225
+ file, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments)
226
+ if err != nil {
227
+ result := OutlineResult{Error: fmt.Sprintf("parse error: %v", err)}
228
+ json.NewEncoder(os.Stdout).Encode(result)
229
+ os.Exit(1)
230
+ }
231
+
232
+ result := OutlineResult{
233
+ Package: file.Name.Name,
234
+ Imports: extractImports(file),
235
+ Symbols: extractSymbols(fset, file),
236
+ }
237
+
238
+ json.NewEncoder(os.Stdout).Encode(result)
239
+ }