@aprimediet/codewalker 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/README.md +44 -50
- package/index.ts +6 -42
- package/package.json +20 -39
- package/prompts/codewalker.md +7 -0
- package/skills/codewalker/SKILL.md +43 -0
- package/src/cards.test.ts +88 -0
- package/src/cards.ts +87 -0
- package/src/db.test.ts +343 -0
- package/src/db.ts +363 -0
- package/src/extract/ctags-parse.test.ts +108 -0
- package/src/extract/ctags-parse.ts +112 -0
- package/src/extract/ctags.ts +51 -0
- package/src/extract/docs.test.ts +81 -0
- package/src/extract/docs.ts +169 -0
- package/src/extract/regex.test.ts +202 -0
- package/src/extract/regex.ts +192 -0
- package/src/format.test.ts +123 -0
- package/src/format.ts +69 -0
- package/src/git.test.ts +75 -0
- package/src/git.ts +62 -0
- package/src/index.contract.test.ts +145 -0
- package/src/index.ts +173 -0
- package/src/indexer.test.ts +138 -0
- package/src/indexer.ts +352 -0
- 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/project.test.ts +115 -0
- package/src/project.ts +206 -0
- package/src/query.test.ts +169 -0
- package/src/query.ts +89 -0
- package/src/sync.test.ts +116 -0
- package/src/types.ts +117 -0
- package/vitest.config.ts +28 -0
- package/LICENSE +0 -21
- package/agents.ts +0 -126
- package/compat.ts +0 -217
- package/detect.ts +0 -188
- package/docs/PRD.md +0 -78
- package/prd.ts +0 -106
- package/skills/learn-this/SKILL.md +0 -325
package/README.md
CHANGED
|
@@ -1,71 +1,65 @@
|
|
|
1
1
|
# @aprimediet/codewalker
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Queryable, token-economical project & code index for the pi coding agent.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## What
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Instead of blindly scanning files (glob → grep → read) on every request — which floods the
|
|
8
|
+
context window with irrelevant bytes — codewalker builds a **mechanical code index** once,
|
|
9
|
+
out of band, and lets the agent query it with compact, ranked results.
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
| Phase 1 | Detect primary language, frameworks, infrastructure, and package manager from manifest files |
|
|
12
|
-
| Phase 2 | Find or gather project goals and non-goals (interactively, one question at a time) |
|
|
13
|
-
| Phase 3 | Document key entry points, external services, exposed interfaces, and technical constraints |
|
|
14
|
-
| Phase 4 | Scan recent commit history and search for technical debt markers (TODO/FIXME/HACK/BUG) |
|
|
15
|
-
| Phase 5 | Detect missing test directories, broken env files, invalid configs, and TypeScript strictness issues |
|
|
16
|
-
| Phase 6 | Generate docs, split by audience: `docs/PRD.md` (product), `AGENTS.md` (engineering), `CLAUDE.md` (pointer to AGENTS.md) — each with user confirmation |
|
|
17
|
-
| Phase 7 | Detect active `@aprimediet/minion` and `@aprimediet/memory` integrations via the shared `.pi/<id>.md` marker |
|
|
18
|
-
| Phase 8 | Compile full project intelligence document with all sections |
|
|
19
|
-
| Phase 9 | Store summary to memory (if active) or save to local file |
|
|
11
|
+
**Token economy is the north star.** Pay the file scan once, outside the LLM context. Query
|
|
12
|
+
cheaply, many times.
|
|
20
13
|
|
|
21
|
-
##
|
|
14
|
+
## Architecture
|
|
22
15
|
|
|
23
|
-
```bash
|
|
24
|
-
pi install npm:@aprimediet/codewalker
|
|
25
|
-
pi list
|
|
26
16
|
```
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
17
|
+
source files ──→ [ctags / regex] ──→ Symbol[]
|
|
18
|
+
↓
|
|
19
|
+
renderCard() → .md files (source of truth)
|
|
20
|
+
↓
|
|
21
|
+
index.db (SQLite + FTS5, disposable)
|
|
22
|
+
↓
|
|
23
|
+
codewalker_query (compact results)
|
|
32
24
|
```
|
|
33
25
|
|
|
34
|
-
|
|
26
|
+
- **Cards are the source of truth** — markdown in `~/.pi/projects/<id>/codewalker/entries/`.
|
|
27
|
+
- **SQLite + FTS5 is a disposable index** — rebuildable from cards at any time.
|
|
28
|
+
- **ctags primary, regex fallback** — ctags used when available, regex for TS/JS/Py/Go.
|
|
29
|
+
- **Git-anchored** — stale index detected per query.
|
|
35
30
|
|
|
36
|
-
##
|
|
31
|
+
## Commands
|
|
37
32
|
|
|
38
|
-
|
|
33
|
+
| Command | Description |
|
|
34
|
+
|---------|-------------|
|
|
35
|
+
| `/codewalker scan` | Full (re)build — walks project tree, extracts symbols, writes cards, populates DB |
|
|
36
|
+
| `/codewalker sync` | Git-anchored incremental — reindexes only changed files |
|
|
37
|
+
| `/codewalker query <text>` | Search the index (compact results) |
|
|
39
38
|
|
|
40
|
-
|
|
41
|
-
- Reads the `.pi/<project-id>.md` marker file from the current working directory
|
|
42
|
-
- Checks `~/.pi/projects/<id>/tasks/` for open kanban cards (backlog, todo, in_progress, blocked, review)
|
|
43
|
-
- Counts and reports open task count during the `/learn-this` summary
|
|
39
|
+
## Tool
|
|
44
40
|
|
|
45
|
-
|
|
46
|
-
- Reads the same `.pi/<project-id>.md` marker file
|
|
47
|
-
- Checks `~/.pi/projects/<id>/memory/` for active memory (MEMORY.md + entries/)
|
|
48
|
-
- Counts memory entries and reads the index during Phase 7
|
|
49
|
-
- In Phase 9, if memory is active, calls `memory_write` with scope `"project"` to store the full intelligence snapshot
|
|
41
|
+
The model can call `codewalker_query` directly. Returns `content` (compact text) + `details` (full rows).
|
|
50
42
|
|
|
51
|
-
|
|
43
|
+
## Install
|
|
52
44
|
|
|
53
|
-
|
|
45
|
+
```bash
|
|
46
|
+
# In the pi extensions directory:
|
|
47
|
+
npm install @aprimediet/codewalker
|
|
48
|
+
```
|
|
54
49
|
|
|
50
|
+
Then load the extension:
|
|
51
|
+
```
|
|
52
|
+
pi -e ./node_modules/@aprimediet/codewalker/index.ts
|
|
55
53
|
```
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
└── skills/
|
|
64
|
-
└── learn-this/
|
|
65
|
-
└── SKILL.md # 9-phase intelligence gathering workflow + checklist
|
|
54
|
+
|
|
55
|
+
## Development
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npm install
|
|
59
|
+
npm test # vitest — 86+ tests across 12 test files
|
|
60
|
+
npm run test:watch # watch mode
|
|
66
61
|
```
|
|
67
62
|
|
|
68
|
-
|
|
69
|
-
that triggers the agent to invoke the `learn-this` skill and run all 9 phases.
|
|
63
|
+
## License
|
|
70
64
|
|
|
71
|
-
|
|
65
|
+
MIT
|
package/index.ts
CHANGED
|
@@ -1,43 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @aprimediet/codewalker — extension entry point.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports the factory from src/ so pi finds the extension at root level.
|
|
5
|
+
*/
|
|
6
|
+
export { default } from "./src/index.ts";
|
|
3
7
|
|
|
4
|
-
export default function codewalkExtension(pi: ExtensionAPI): void {
|
|
5
|
-
pi.registerCommand("learn-this", {
|
|
6
|
-
description: "Analyze this project: tech stack, goals, status, issues, then generate PRD + AGENTS.md/CLAUDE.md.",
|
|
7
|
-
handler: async (_args, ctx: ExtensionContext) => {
|
|
8
|
-
const compat = probeCompat(ctx.cwd);
|
|
9
|
-
|
|
10
|
-
const minionLine = compat.minionActive
|
|
11
|
-
? `minion: active (project ${compat.projectId}, ${compat.openTaskCount} open tasks)`
|
|
12
|
-
: "minion: not detected";
|
|
13
|
-
|
|
14
|
-
const memoryLine = compat.memoryActive
|
|
15
|
-
? `memory: active (${compat.memoryEntries} entries)`
|
|
16
|
-
: "memory: not detected";
|
|
17
|
-
|
|
18
|
-
// Surface the probe result to the user immediately (TUI only).
|
|
19
|
-
if (ctx.hasUI) {
|
|
20
|
-
ctx.ui.notify(
|
|
21
|
-
["codewalker: starting /learn-this", ` ${minionLine}`, ` ${memoryLine}`].join("\n"),
|
|
22
|
-
"info",
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Actually kick off the workflow: send a user message that triggers an agent turn.
|
|
27
|
-
// The agent invokes the learn-this skill and runs all 9 phases. We hand it the
|
|
28
|
-
// integration facts up front so Phase 7 (and Phase 9 storage) start from real data.
|
|
29
|
-
const directive = [
|
|
30
|
-
"Run the /learn-this project intelligence workflow on the current working directory now.",
|
|
31
|
-
"Invoke the `learn-this` skill and complete every phase and checklist item in order.",
|
|
32
|
-
"Do not stop after detection — gather goals interactively (one question at a time) where they are missing, generate the docs (docs/PRD.md for humans, AGENTS.md + CLAUDE.md for coding agents, with the audience-correct content split), and finish with the summary and memory storage steps.",
|
|
33
|
-
"",
|
|
34
|
-
"Integration status detected by codewalker (use these facts in Phase 7 and Phase 9):",
|
|
35
|
-
`- ${minionLine}`,
|
|
36
|
-
`- ${memoryLine}`,
|
|
37
|
-
compat.projectId ? `- project id: ${compat.projectId}` : "- project id: none (no .pi marker found)",
|
|
38
|
-
].join("\n");
|
|
39
|
-
|
|
40
|
-
pi.sendUserMessage(directive, { deliverAs: "followUp" });
|
|
41
|
-
},
|
|
42
|
-
});
|
|
43
|
-
}
|
package/package.json
CHANGED
|
@@ -1,47 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aprimediet/codewalker",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "
|
|
6
|
-
"keywords": [
|
|
7
|
-
"pi-package",
|
|
8
|
-
"pi-extension",
|
|
9
|
-
"codewalker",
|
|
10
|
-
"project-intelligence",
|
|
11
|
-
"prd",
|
|
12
|
-
"agents-md"
|
|
13
|
-
],
|
|
14
|
-
"license": "MIT",
|
|
15
|
-
"author": {
|
|
16
|
-
"name": "aprimediet",
|
|
17
|
-
"url": "https://github.com/aprimediet"
|
|
18
|
-
},
|
|
19
|
-
"repository": {
|
|
20
|
-
"type": "git",
|
|
21
|
-
"url": "git+https://github.com/aprimediet/codewalker.git"
|
|
22
|
-
},
|
|
23
|
-
"bugs": {
|
|
24
|
-
"url": "https://github.com/aprimediet/codewalker/issues"
|
|
25
|
-
},
|
|
26
|
-
"homepage": "https://github.com/aprimediet/codewalker#readme",
|
|
27
|
-
"engines": {
|
|
28
|
-
"node": ">=18"
|
|
29
|
-
},
|
|
30
|
-
"publishConfig": {
|
|
31
|
-
"access": "public"
|
|
32
|
-
},
|
|
5
|
+
"description": "Queryable, token-economical project & code index for the pi coding agent.",
|
|
6
|
+
"keywords": ["pi-package"],
|
|
33
7
|
"pi": {
|
|
34
8
|
"extensions": ["./index.ts"],
|
|
35
|
-
"skills": ["./skills"]
|
|
9
|
+
"skills": ["./skills"],
|
|
10
|
+
"prompts": ["./prompts"]
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"test": "vitest run",
|
|
14
|
+
"test:watch": "vitest"
|
|
36
15
|
},
|
|
37
|
-
"files": [
|
|
38
|
-
"*.ts",
|
|
39
|
-
"skills/**",
|
|
40
|
-
"README.md",
|
|
41
|
-
"LICENSE",
|
|
42
|
-
"docs/**"
|
|
43
|
-
],
|
|
16
|
+
"files": ["*.ts", "src/**", "skills/**", "prompts/**", "README.md"],
|
|
44
17
|
"peerDependencies": {
|
|
45
|
-
"@earendil-works/pi-coding-agent": "*"
|
|
46
|
-
|
|
18
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
19
|
+
"@earendil-works/pi-agent-core": "*",
|
|
20
|
+
"@earendil-works/pi-ai": "*",
|
|
21
|
+
"@earendil-works/pi-tui": "*",
|
|
22
|
+
"typebox": "*"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": { "better-sqlite3": "^11.0.0" },
|
|
25
|
+
"devDependencies": { "vitest": "^1.6.0", "@types/better-sqlite3": "^7.6.0" },
|
|
26
|
+
"engines": { "node": ">=20" },
|
|
27
|
+
"publishConfig": { "access": "public" }
|
|
47
28
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
You have access to the codewalker code index for this project. Before reading or grepping
|
|
2
|
+
files to find symbols (functions, consts, classes, types), use `codewalker_query` to
|
|
3
|
+
look them up. The query returns compact facts — name, kind, file:line, and a one-line
|
|
4
|
+
summary.
|
|
5
|
+
|
|
6
|
+
- If the index is stale (shown in the result), run `/codewalker sync`.
|
|
7
|
+
- For a full index, run `/codewalker scan`.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Codewalker — Queryable Code Index
|
|
2
|
+
|
|
3
|
+
**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.
|
|
6
|
+
|
|
7
|
+
## Workflow
|
|
8
|
+
|
|
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`.
|
|
14
|
+
|
|
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.
|
|
17
|
+
|
|
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).
|
|
23
|
+
|
|
24
|
+
4. **When results include a staleness warning** (`indexed @abc, HEAD @def`), run
|
|
25
|
+
`/codewalker sync` before trusting the results.
|
|
26
|
+
|
|
27
|
+
## Why
|
|
28
|
+
|
|
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.
|
|
32
|
+
|
|
33
|
+
## Commands
|
|
34
|
+
|
|
35
|
+
| Command | Purpose |
|
|
36
|
+
|---------|---------|
|
|
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 |
|
|
40
|
+
|
|
41
|
+
## Tool
|
|
42
|
+
|
|
43
|
+
The model can also call `codewalker_query` directly (same behavior as the command).
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { renderCard, parseCard, cardHead } from './cards.ts';
|
|
3
|
+
import type { Symbol } from './types.ts';
|
|
4
|
+
|
|
5
|
+
function makeSymbol(overrides: Partial<Symbol> = {}): Symbol {
|
|
6
|
+
return {
|
|
7
|
+
name: 'probeCompat',
|
|
8
|
+
kind: 'function',
|
|
9
|
+
file_path: '/root/src/compat.ts',
|
|
10
|
+
line_start: 201,
|
|
11
|
+
line_end: 243,
|
|
12
|
+
signature: '(cwd: string) => CompatResult',
|
|
13
|
+
doc: 'Detect whether minion & memory are active for the project at cwd.',
|
|
14
|
+
summary: '',
|
|
15
|
+
card_path: '',
|
|
16
|
+
...overrides,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('renderCard', () => {
|
|
21
|
+
it('renders a symbol as a markdown card with frontmatter head and body', () => {
|
|
22
|
+
const sym = makeSymbol();
|
|
23
|
+
const md = renderCard(sym);
|
|
24
|
+
|
|
25
|
+
// Head (frontmatter)
|
|
26
|
+
expect(md).toContain('---');
|
|
27
|
+
expect(md).toContain('name: probeCompat');
|
|
28
|
+
expect(md).toContain('kind: function');
|
|
29
|
+
expect(md).toContain('signature: (cwd: string) => CompatResult');
|
|
30
|
+
expect(md).toContain('location: compat.ts:201-243');
|
|
31
|
+
expect(md).toContain('summary: Detect whether minion & memory are active for the project at cwd.');
|
|
32
|
+
|
|
33
|
+
// Body
|
|
34
|
+
expect(md).toContain('# probeCompat');
|
|
35
|
+
expect(md).toContain(sym.doc);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('uses relative path (basename only) for location', () => {
|
|
39
|
+
const sym = makeSymbol({ file_path: '/very/deep/src/util/helper.ts', line_start: 5, line_end: 10 });
|
|
40
|
+
const md = renderCard(sym);
|
|
41
|
+
expect(md).toContain('location: helper.ts:5-10');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('handles empty doc gracefully', () => {
|
|
45
|
+
const sym = makeSymbol({ doc: '' });
|
|
46
|
+
const md = renderCard(sym);
|
|
47
|
+
expect(md).toContain('name: probeCompat');
|
|
48
|
+
expect(md).toContain('# probeCompat');
|
|
49
|
+
// Body may be empty
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('parseCard', () => {
|
|
54
|
+
it('round-trips renderCard → parseCard preserving head fields', () => {
|
|
55
|
+
const sym = makeSymbol();
|
|
56
|
+
const md = renderCard(sym);
|
|
57
|
+
const parsed = parseCard(md);
|
|
58
|
+
expect(parsed).not.toBeNull();
|
|
59
|
+
expect(parsed!.head.name).toBe('probeCompat');
|
|
60
|
+
expect(parsed!.head.kind).toBe('function');
|
|
61
|
+
expect(parsed!.head.signature).toBe('(cwd: string) => CompatResult');
|
|
62
|
+
expect(parsed!.head.location).toBe('compat.ts:201-243');
|
|
63
|
+
expect(parsed!.head.summary).toContain('minion');
|
|
64
|
+
// Body contains the doc text
|
|
65
|
+
expect(parsed!.body).toContain(sym.doc);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('returns null for invalid markdown', () => {
|
|
69
|
+
expect(parseCard('not frontmatter')).toBeNull();
|
|
70
|
+
expect(parseCard('')).toBeNull();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('cardHead', () => {
|
|
75
|
+
it('returns only the frontmatter head from a card', () => {
|
|
76
|
+
const sym = makeSymbol();
|
|
77
|
+
const md = renderCard(sym);
|
|
78
|
+
const head = cardHead(md);
|
|
79
|
+
expect(head).not.toBeNull();
|
|
80
|
+
expect(head!.name).toBe('probeCompat');
|
|
81
|
+
expect(head!.kind).toBe('function');
|
|
82
|
+
expect(head!.location).toBe('compat.ts:201-243');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('returns null for invalid input', () => {
|
|
86
|
+
expect(cardHead('no frontmatter here')).toBeNull();
|
|
87
|
+
});
|
|
88
|
+
});
|
package/src/cards.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown card rendering and parsing for codewalker.
|
|
3
|
+
*
|
|
4
|
+
* A card has a frontmatter head (agent-cheap, compact) and a body (human-rich, verbose).
|
|
5
|
+
* The head is what `query` returns; the full card is only expanded on demand.
|
|
6
|
+
*
|
|
7
|
+
* PURE module — no I/O.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Symbol, CardHead } from "./types.ts";
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Render a Symbol into a markdown card string with frontmatter head + body.
|
|
15
|
+
*/
|
|
16
|
+
export function renderCard(symbol: Symbol): string {
|
|
17
|
+
const location = `${path.basename(symbol.file_path)}:${symbol.line_start}-${symbol.line_end}`;
|
|
18
|
+
const name = symbol.name;
|
|
19
|
+
const summary = symbol.summary || symbol.doc.split("\n")[0]?.trim() || "";
|
|
20
|
+
|
|
21
|
+
const frontmatter = [
|
|
22
|
+
"---",
|
|
23
|
+
`name: ${name}`,
|
|
24
|
+
`kind: ${symbol.kind}`,
|
|
25
|
+
`signature: ${symbol.signature || ""}`,
|
|
26
|
+
`location: ${location}`,
|
|
27
|
+
`summary: ${summary}`,
|
|
28
|
+
"---",
|
|
29
|
+
].join("\n");
|
|
30
|
+
|
|
31
|
+
const body = symbol.doc
|
|
32
|
+
? [`# ${name}`, "", symbol.doc].join("\n")
|
|
33
|
+
: `# ${name}`;
|
|
34
|
+
|
|
35
|
+
return `${frontmatter}\n\n${body}\n`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Parse a markdown card string into head + body.
|
|
40
|
+
* Returns null if the card is invalid.
|
|
41
|
+
*/
|
|
42
|
+
export function parseCard(text: string): { head: CardHead; body: string } | null {
|
|
43
|
+
const trimmed = text.trim();
|
|
44
|
+
if (!trimmed.startsWith("---")) return null;
|
|
45
|
+
|
|
46
|
+
// Find the closing ---
|
|
47
|
+
const endOfFm = trimmed.indexOf("\n---", 3);
|
|
48
|
+
if (endOfFm === -1) return null;
|
|
49
|
+
|
|
50
|
+
const fmRaw = trimmed.slice(3, endOfFm).trim();
|
|
51
|
+
const body = trimmed.slice(endOfFm + 4).trim();
|
|
52
|
+
|
|
53
|
+
// Parse frontmatter lines
|
|
54
|
+
const fm: Record<string, string> = {};
|
|
55
|
+
for (const line of fmRaw.split("\n")) {
|
|
56
|
+
const sep = line.indexOf(":");
|
|
57
|
+
if (sep > 0) {
|
|
58
|
+
const key = line.slice(0, sep).trim();
|
|
59
|
+
const value = line.slice(sep + 1).trim();
|
|
60
|
+
fm[key] = value;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!fm["name"]) return null;
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
head: {
|
|
68
|
+
name: fm["name"] ?? "",
|
|
69
|
+
kind: fm["kind"] ?? "",
|
|
70
|
+
signature: fm["signature"] ?? "",
|
|
71
|
+
location: fm["location"] ?? "",
|
|
72
|
+
tags: (fm["tags"] ?? "").split(",").map((t) => t.trim()).filter(Boolean),
|
|
73
|
+
summary: fm["summary"] ?? "",
|
|
74
|
+
},
|
|
75
|
+
body,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Extract only the frontmatter head from a card — the compact, agent-cheap view.
|
|
81
|
+
* Returns null if the card is invalid.
|
|
82
|
+
*/
|
|
83
|
+
export function cardHead(text: string): CardHead | null {
|
|
84
|
+
const parsed = parseCard(text);
|
|
85
|
+
if (!parsed) return null;
|
|
86
|
+
return parsed.head;
|
|
87
|
+
}
|