@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 +28 -3
- package/package.json +1 -1
- package/prompts/codewalker.md +3 -1
- package/skills/codewalker/SKILL.md +118 -28
- package/src/cards.test.ts +123 -1
- package/src/cards.ts +53 -0
- package/src/db.test.ts +405 -3
- package/src/db.ts +402 -29
- package/src/enrich.test.ts +102 -0
- package/src/enrich.ts +107 -0
- package/src/format.test.ts +103 -0
- package/src/format.ts +11 -0
- package/src/index.contract.test.ts +77 -1
- package/src/index.ts +273 -19
- package/src/indexer.heal.test.ts +90 -0
- package/src/indexer.ts +9 -1
- package/src/libs/cards.test.ts +86 -0
- package/src/libs/cards.ts +53 -0
- package/src/libs/dts.test.ts +269 -0
- package/src/libs/dts.ts +213 -0
- package/src/libs/indexer.test.ts +236 -0
- package/src/libs/indexer.ts +291 -0
- package/src/libs/resolve.test.ts +218 -0
- package/src/libs/resolve.ts +120 -0
- package/src/notes-cards.test.ts +99 -0
- package/src/notes-cards.ts +92 -0
- package/src/notes.test.ts +172 -0
- package/src/notes.ts +145 -0
- package/src/project.test.ts +12 -1
- package/src/project.ts +7 -1
- package/src/query.test.ts +148 -1
- package/src/query.ts +28 -8
- package/src/types.ts +44 -0
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
|
-
|
|
60
|
+
The model can call:
|
|
40
61
|
|
|
41
|
-
|
|
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 —
|
|
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
package/prompts/codewalker.md
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
instead of grepping the whole repo.
|
|
22
|
+
## Tools (agent-facing)
|
|
17
23
|
|
|
18
|
-
|
|
19
|
-
```
|
|
20
|
-
/codewalker scan
|
|
21
|
-
```
|
|
22
|
-
(first run) or `/codewalker sync` (incremental update).
|
|
24
|
+
### `codewalker_query` — Search the index
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
124
|
+
## Details
|
|
42
125
|
|
|
43
|
-
|
|
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.
|