@aprimediet/codewalker 1.1.0 → 1.3.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/README.md CHANGED
@@ -21,11 +21,25 @@ source files ──→ [ctags / regex] ──→ Symbol[]
21
21
  index.db (SQLite + FTS5, disposable)
22
22
 
23
23
  codewalker_query (compact results)
24
+
25
+ node_modules ──→ [.d.ts extract] ──→ LibSymbol[]
26
+
27
+ renderLibCard() → lib cards (version-pinned)
28
+
29
+ index.db (lib_symbols + FTS5)
30
+
31
+ agent knowledge ──→ codewalker_enrich / codewalker_note
32
+
33
+ cards (enrichment, glossary, decisions)
34
+
35
+ index.db (notes + FTS5, unified query)
24
36
  ```
25
37
 
26
38
  - **Cards are the source of truth** — markdown in `~/.pi/projects/<id>/codewalker/entries/`.
27
39
  - **SQLite + FTS5 is a disposable index** — rebuildable from cards at any time.
28
40
  - **ctags primary, regex fallback** — ctags used when available, regex for TS/JS/Py/Go.
41
+ - **Library layer** — extracts API surface from `node_modules` `.d.ts` files (version-pinned).
42
+ - **Semantic + bridge layer** — agent-driven enrichment, glossary terms, and decision notes.
29
43
  - **Git-anchored** — stale index detected per query.
30
44
 
31
45
  ## Commands
@@ -35,10 +49,21 @@ source files ──→ [ctags / regex] ──→ Symbol[]
35
49
  | `/codewalker scan` | Full (re)build — walks project tree, extracts symbols, writes cards, populates DB |
36
50
  | `/codewalker sync` | Git-anchored incremental — reindexes only changed files |
37
51
  | `/codewalker query <text>` | Search the index (compact results) |
52
+ | `/codewalker enrich <path>` | Select unenriched symbols under `<path>` and write summaries |
53
+ | `/codewalker glossary [query]` | Search glossary terms |
54
+ | `/codewalker decisions [query]` | Search decision notes |
55
+ | `/codewalker libs [--dev]` | Index all direct dependencies from node_modules |
56
+ | `/codewalker lib <pkg> [query]` | Search a specific library's API symbols |
57
+
58
+ ## Tools
38
59
 
39
- ## Tool
60
+ The model can call:
40
61
 
41
- The model can call `codewalker_query` directly. Returns `content` (compact text) + `details` (full rows).
62
+ | Tool | Description |
63
+ |------|-------------|
64
+ | `codewalker_query` | Search code symbols, libraries, and notes with FTS5 |
65
+ | `codewalker_enrich` | Write a one-line semantic summary back to a symbol's card + DB |
66
+ | `codewalker_note` | Write a glossary term or decision note (bridge cards) |
42
67
 
43
68
  ## Install
44
69
 
@@ -56,7 +81,7 @@ pi -e ./node_modules/@aprimediet/codewalker/index.ts
56
81
 
57
82
  ```bash
58
83
  npm install
59
- npm test # vitest — 86+ tests across 12 test files
84
+ npm test # vitest — 216+ tests across 19 test files
60
85
  npm run test:watch # watch mode
61
86
  ```
62
87
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aprimediet/codewalker",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "type": "module",
5
5
  "description": "Queryable, token-economical project & code index for the pi coding agent.",
6
6
  "keywords": ["pi-package"],
@@ -1,7 +1,9 @@
1
1
  You have access to the codewalker code index for this project. Before reading or grepping
2
2
  files to find symbols (functions, consts, classes, types), use `codewalker_query` to
3
3
  look them up. The query returns compact facts — name, kind, file:line, and a one-line
4
- summary.
4
+ summary. Use `source='all'` to also surface glossary terms and decision notes.
5
5
 
6
6
  - If the index is stale (shown in the result), run `/codewalker sync`.
7
7
  - For a full index, run `/codewalker scan`.
8
+ - After reading an unfamiliar symbol, call `codewalker_enrich` to cache a summary.
9
+ - When you discover a design decision or domain term, write it with `codewalker_note`.
@@ -1,43 +1,133 @@
1
- # Codewalker — Queryable Code Index
1
+ ---
2
+ name: codewalker
3
+ description: >
4
+ Queryable code index + knowledge base for understanding codebases. Finds symbol
5
+ definitions, function summaries, const/class/type/interface lookups, library API
6
+ searches, glossary terms, and design decisions — before blindly grepping or reading
7
+ files. Use FIRST when exploring unfamiliar code.
8
+ ---
9
+
10
+ # Codewalker — Queryable Code Index + Knowledge Base
2
11
 
3
12
  **Use this skill when:** you need to understand a codebase — find where a symbol is defined,
4
- understand what a function does, or check if a const/class/type exists — BEFORE editing
5
- files or grepping through the repo.
13
+ understand what a function does, check if a const/class/type/interface exists, search
14
+ library APIs, look up glossary terms or past design decisions — **before** blindly
15
+ grepping or reading files.
6
16
 
7
- ## Workflow
17
+ Codewalker is a token-economical project index that surfaces compact facts instead of
18
+ pulling whole files into your LLM context.
8
19
 
9
- 1. **Always query first** before editing unfamiliar code:
10
- ```
11
- /codewalker query "<symbol-name or concept>"
12
- ```
13
- This returns compact facts: `name · kind · file:line · one-line summary`.
20
+ ---
14
21
 
15
- 2. **If the query returns relevant hits**, use `file:line` to read only the span you need
16
- instead of grepping the whole repo.
22
+ ## Tools (agent-facing)
17
23
 
18
- 3. **If the query returns no hits**, the index may be stale or missing. Run:
19
- ```
20
- /codewalker scan
21
- ```
22
- (first run) or `/codewalker sync` (incremental update).
24
+ ### `codewalker_query` Search the index
23
25
 
24
- 4. **When results include a staleness warning** (`indexed @abc, HEAD @def`), run
25
- `/codewalker sync` before trusting the results.
26
+ Use this **first** before reading or grepping files. Returns one-line-per-hit:
27
+ `name · kind · file:line · one-line-summary`
26
28
 
27
- ## Why
29
+ Parameters:
30
+ - `query` — symbol name or concept keywords
31
+ - `kind` — optional filter: `function|const|class|type|method|enum|interface|glossary|decision`
32
+ - `limit` — max hits (default 10)
33
+ - `source` — scope: `code` (default, source symbols), `libs` (library APIs), `notes` (glossary + decisions), `all` (everything)
34
+
35
+ ### `codewalker_enrich` — Write a semantic summary
36
+
37
+ Call this **after** reading a symbol's source span. Caches a one-line (≤120 char)
38
+ plain-English summary so future queries surface meaning, not just names.
39
+
40
+ Parameters:
41
+ - `card` — `card_path` of the symbol (from the enrich worklist or query results)
42
+ - `summary` — one-line description of what it does
28
43
 
29
- The index is built out-of-band (mechanical ctags/regex pass) so you never pay the file-scan
30
- cost inside the LLM context. Queries return compact, ranked facts — tens of tokens instead
31
- of thousands.
44
+ ### `codewalker_note` Save domain knowledge
32
45
 
33
- ## Commands
46
+ Write a glossary term or design decision note. Persists as a markdown card + FTS index
47
+ so future queries find it.
48
+
49
+ Parameters:
50
+ - `type` — `glossary` | `decision`
51
+ - `title` — glossary term or decision title
52
+ - `body` — definition or rationale
53
+ - `tags` — optional comma-separated tags
54
+ - `related` — optional comma-separated symbol names or `file:line` refs
55
+
56
+ ---
57
+
58
+ ## Commands (human-facing)
34
59
 
35
60
  | Command | Purpose |
36
61
  |---------|---------|
37
- | `/codewalker scan` | Full (re)build of the code index |
38
- | `/codewalker sync` | Git-anchored incremental update |
39
- | `/codewalker query <text>` | Search symbols by name or keyword |
62
+ | `/codewalker scan` | Full (re)build of the code index from scratch |
63
+ | `/codewalker sync` | Git-anchored incremental update (fast) |
64
+ | `/codewalker query <text>` | Search code symbols by name or keyword |
65
+ | `/codewalker enrich <path> [--max=N]` | List unenriched symbols under `path` for annotation |
66
+ | `/codewalker glossary [query]` | Search glossary terms |
67
+ | `/codewalker decisions [query]` | Search decision notes |
68
+ | `/codewalker libs [--dev]` | Index all direct npm dependencies (--dev includes devDeps) |
69
+ | `/codewalker lib <pkg> [query]` | Search a specific library's exported API symbols |
70
+ | `/codewalker help` | Show this help |
71
+
72
+ ---
73
+
74
+ ## Workflow
75
+
76
+ ### 1. Before editing unfamiliar code
77
+ ```
78
+ /codewalker query "<symbol-name>"
79
+ ```
80
+ Or call `codewalker_query` directly from agent conversation.
81
+ If hits are relevant, use the `file:line` to read only the span you need.
82
+
83
+ ### 2. If the index is stale
84
+ Results include a staleness warning like:
85
+ `⚠ Index is stale (3 file(s) changed since last index): indexed @abc1234, HEAD @def5678`
86
+ → Run `/codewalker sync` first, then query again.
87
+
88
+ ### 3. First time in a project
89
+ ```
90
+ /codewalker scan
91
+ ```
92
+ This does a full build (ctags primary, regex fallback) and sets up the SQLite+FTS5 database.
93
+
94
+ ### 4. Annotating symbols for future clarity
95
+ After reading a symbol you didn't understand, call `codewalker_enrich` with a summary.
96
+ This builds up the codebase knowledge map over time.
97
+
98
+ ### 5. Capturing domain knowledge
99
+ When you discover a project-specific concept or learn why a decision was made:
100
+ ```
101
+ codewalker_note(type="glossary", title="term", body="definition")
102
+ codewalker_note(type="decision", title="why X", body="rationale")
103
+ ```
104
+ These become searchable via `codewalker_query` with `source='notes'` or `source='all'`.
105
+
106
+ ### 6. Exploring library APIs
107
+ ```
108
+ /codewalker libs # index dependencies
109
+ /codewalker lib express # search express exports
110
+ /codewalker lib lodash get # search lodash for 'get'
111
+ ```
112
+
113
+ ---
114
+
115
+ ## Why
116
+
117
+ The index is built out-of-band (mechanical ctags/regex pass) so you never pay the
118
+ file-scan cost inside the LLM context. Queries return compact, ranked facts — tens of
119
+ tokens instead of thousands. The note system (glossary + decisions) captures conceptual
120
+ knowledge your future self will thank you for.
121
+
122
+ ---
40
123
 
41
- ## Tool
124
+ ## Details
42
125
 
43
- The model can also call `codewalker_query` directly (same behavior as the command).
126
+ - **Staleness detection**: every query result includes git-anchored staleness info
127
+ comparing the indexed commit against HEAD
128
+ - **FTS5 ranking**: results use bm25 relevance scoring; `code` → `libs` → `notes` order
129
+ when using `source='all'`
130
+ - **Cards as source of truth**: every symbol and note is stored as a markdown card file.
131
+ The DB is rebuilt from cards on `scan` — cards are the durable artifact
132
+ - **Source filter**: use `source='libs'` to search only library APIs, `source='notes'`
133
+ for glossary/decisions, `source='all'` for everything at once
package/src/cards.test.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { renderCard, parseCard, cardHead } from './cards.ts';
2
+ import { renderCard, parseCard, cardHead, updateCardSummary } from './cards.ts';
3
3
  import type { Symbol } from './types.ts';
4
4
 
5
5
  function makeSymbol(overrides: Partial<Symbol> = {}): Symbol {
@@ -71,6 +71,128 @@ describe('parseCard', () => {
71
71
  });
72
72
  });
73
73
 
74
+ describe('updateCardSummary', () => {
75
+ it('replaces frontmatter summary line and appends ## What it does section', () => {
76
+ const card = `---
77
+ name: probeCompat
78
+ kind: function
79
+ signature: (cwd: string) => CompatResult
80
+ location: compat.ts:201-243
81
+ summary: Old summary
82
+ ---
83
+
84
+ # probeCompat
85
+
86
+ Some body text here.
87
+ `;
88
+
89
+ const updated = updateCardSummary(card, 'Detect whether minion & memory are active.');
90
+
91
+ // Frontmatter summary updated
92
+ expect(updated).toContain('summary: Detect whether minion & memory are active.');
93
+ expect(updated).not.toContain('summary: Old summary');
94
+
95
+ // ## What it does section added
96
+ expect(updated).toContain('## What it does');
97
+ expect(updated).toContain('Detect whether minion & memory are active.');
98
+ });
99
+
100
+ it('is idempotent — second apply does not stack duplicates', () => {
101
+ const card = `---
102
+ name: probeCompat
103
+ kind: function
104
+ location: compat.ts:201-243
105
+ summary: Old
106
+ ---
107
+
108
+ # probeCompat
109
+ `;
110
+
111
+ const once = updateCardSummary(card, 'First summary.');
112
+ const twice = updateCardSummary(once, 'Second summary.');
113
+
114
+ // Has the new summary
115
+ expect(twice).toContain('summary: Second summary.');
116
+ expect(twice).not.toContain('summary: First summary.');
117
+
118
+ // Only one ## What it does section
119
+ const matches = twice.match(/## What it does/g);
120
+ expect(matches).toHaveLength(1);
121
+
122
+ // Only one summary in frontmatter
123
+ const summaryMatches = twice.match(/^summary:/gm);
124
+ expect(summaryMatches).toHaveLength(1);
125
+ });
126
+
127
+ it('handles empty summary in input', () => {
128
+ const card = `---
129
+ name: foo
130
+ kind: function
131
+ location: foo.ts:1-10
132
+ summary:
133
+ ---
134
+
135
+ # foo
136
+ `;
137
+
138
+ const updated = updateCardSummary(card, 'New summary.');
139
+ expect(updated).toContain('summary: New summary.');
140
+ expect(updated).toContain('## What it does');
141
+ });
142
+
143
+ it('handles card with no existing body', () => {
144
+ const card = `---
145
+ name: bar
146
+ kind: const
147
+ location: bar.ts:5-5
148
+ summary:
149
+ ---
150
+ `;
151
+
152
+ const updated = updateCardSummary(card, 'Just a constant.');
153
+ expect(updated).toContain('summary: Just a constant.');
154
+ expect(updated).toContain('## What it does');
155
+ });
156
+
157
+ it('does not break frontmatter for cards with tags field', () => {
158
+ const card = `---
159
+ name: myFunc
160
+ kind: function
161
+ location: a.ts:1-10
162
+ tags: alpha, beta
163
+ summary:
164
+ ---
165
+
166
+ # myFunc
167
+ `;
168
+
169
+ const updated = updateCardSummary(card, 'A function that does something.');
170
+ const parsed = parseCard(updated);
171
+ expect(parsed).not.toBeNull();
172
+ expect(parsed!.head.name).toBe('myFunc');
173
+ expect(parsed!.head.summary).toBe('A function that does something.');
174
+ expect(parsed!.head.tags).toEqual(['alpha', 'beta']);
175
+ });
176
+
177
+ it('round-trips through parseCard', () => {
178
+ const card = `---
179
+ name: probeCompat
180
+ kind: function
181
+ location: compat.ts:201-243
182
+ summary: Old
183
+ ---
184
+
185
+ # probeCompat
186
+ `;
187
+
188
+ const updated = updateCardSummary(card, 'A function to detect integrations.');
189
+ const parsed = parseCard(updated);
190
+ expect(parsed).not.toBeNull();
191
+ expect(parsed!.head.summary).toBe('A function to detect integrations.');
192
+ expect(parsed!.body).toContain('A function to detect integrations.');
193
+ });
194
+ });
195
+
74
196
  describe('cardHead', () => {
75
197
  it('returns only the frontmatter head from a card', () => {
76
198
  const sym = makeSymbol();
package/src/cards.ts CHANGED
@@ -76,6 +76,59 @@ export function parseCard(text: string): { head: CardHead; body: string } | null
76
76
  };
77
77
  }
78
78
 
79
+ /**
80
+ * Pure: rewrite a card's frontmatter summary: line and upsert a ## What it does body section.
81
+ * Idempotent — enriching twice yields the same card (replaces the section, doesn't stack duplicates).
82
+ */
83
+ export function updateCardSummary(cardText: string, summary: string): string {
84
+ const trimmed = cardText.trim();
85
+
86
+ // Split into frontmatter and body
87
+ const endOfFm = trimmed.indexOf("\n---", 3);
88
+ if (endOfFm === -1) return cardText; // invalid card, return as-is
89
+
90
+ const fmRaw = trimmed.slice(3, endOfFm).trim();
91
+ const bodyRaw = trimmed.slice(endOfFm + 4).trim();
92
+
93
+ // Rebuild frontmatter, replacing summary: line
94
+ const fmLines = fmRaw.split("\n");
95
+ const newFmLines: string[] = [];
96
+ let summaryReplaced = false;
97
+
98
+ for (const line of fmLines) {
99
+ const sep = line.indexOf(":");
100
+ if (sep > 0) {
101
+ const key = line.slice(0, sep).trim();
102
+ if (key === "summary") {
103
+ newFmLines.push(`summary: ${summary}`);
104
+ summaryReplaced = true;
105
+ continue;
106
+ }
107
+ }
108
+ newFmLines.push(line);
109
+ }
110
+
111
+ if (!summaryReplaced) {
112
+ newFmLines.push(`summary: ${summary}`);
113
+ }
114
+
115
+ const newFrontmatter = `---\n${newFmLines.join("\n")}\n---`;
116
+
117
+ // Build body — replace existing ## What it does section if present
118
+ let body = bodyRaw;
119
+ const whatItDoesRegex = /## What it does[\s\S]*?(?=\n## |$)/;
120
+ const whatItDoesSection = `## What it does\n\n${summary}`;
121
+
122
+ if (whatItDoesRegex.test(body)) {
123
+ body = body.replace(whatItDoesRegex, whatItDoesSection);
124
+ } else {
125
+ // Append after existing body (or replace empty body)
126
+ body = body ? `${body}\n\n${whatItDoesSection}` : whatItDoesSection;
127
+ }
128
+
129
+ return `${newFrontmatter}\n\n${body}\n`;
130
+ }
131
+
79
132
  /**
80
133
  * Extract only the frontmatter head from a card — the compact, agent-cheap view.
81
134
  * Returns null if the card is invalid.