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.
Binary file
@@ -0,0 +1 @@
1
+ npm run validate && npm run test -- --exclude='**/e2e/**'
package/.jscpd.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "threshold": 15,
3
+ "minTokens": 50,
4
+ "minLines": 5,
5
+ "reporters": ["console"],
6
+ "ignore": ["**/fixtures/**", "**/node_modules/**"]
7
+ }
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 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
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 (Recommended)
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.0.0",
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
- "validate": "npm run typecheck && npm run lint && npm run format:check",
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"
@@ -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: ts.Name.Name,
111
- StartLine: fset.Position(d.Pos()).Line,
112
- EndLine: fset.Position(d.End()).Line,
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: name.Name,
167
- Kind: kind,
168
- StartLine: fset.Position(vs.Pos()).Line,
169
- EndLine: fset.Position(vs.End()).Line,
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: 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),
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
- // Add modifiers for full detail
47
- if (level === DetailLevel.Full && symbol.modifiers?.length) {
48
- name = `${symbol.modifiers.join(" ")} ${name}`;
49
- }
50
-
51
- // Add signature for full detail
52
- if (level === DetailLevel.Full && symbol.signature) {
53
- name = `${name}${symbol.signature}`;
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: undefined,
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: undefined,
352
+ imports: [],
336
353
  truncatedInfo: {
337
354
  totalSymbols: total,
338
355
  shownSymbols: symbolsEach * 2,