napkin-ai 0.3.0 → 0.4.1

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.
Files changed (56) hide show
  1. package/.pi/extensions/distill/README.md +50 -0
  2. package/.pi/extensions/distill/index.ts +304 -0
  3. package/.pi/extensions/distill/package.json +13 -0
  4. package/.pi/extensions/napkin-context/index.ts +79 -0
  5. package/.pi/extensions/napkin-context/package.json +11 -0
  6. package/README.md +97 -24
  7. package/dist/commands/config.d.ts +13 -0
  8. package/dist/commands/config.js +77 -0
  9. package/dist/commands/crud.js +24 -15
  10. package/dist/commands/daily.js +6 -5
  11. package/dist/commands/files.js +4 -4
  12. package/dist/commands/graph.d.ts +4 -0
  13. package/dist/commands/graph.js +453 -0
  14. package/dist/commands/init.d.ts +2 -5
  15. package/dist/commands/init.js +74 -23
  16. package/dist/commands/links.js +4 -4
  17. package/dist/commands/outline.js +3 -3
  18. package/dist/commands/overview.d.ts +6 -0
  19. package/dist/commands/overview.js +340 -0
  20. package/dist/commands/properties.js +5 -5
  21. package/dist/commands/search.d.ts +3 -1
  22. package/dist/commands/search.js +141 -44
  23. package/dist/commands/tasks.js +5 -5
  24. package/dist/commands/templates.js +11 -8
  25. package/dist/commands/vault.js +1 -3
  26. package/dist/commands/wordcount.js +3 -3
  27. package/dist/main.js +162 -69
  28. package/dist/templates/coding.d.ts +2 -0
  29. package/dist/templates/coding.js +104 -0
  30. package/dist/templates/company.d.ts +2 -0
  31. package/dist/templates/company.js +121 -0
  32. package/dist/templates/index.d.ts +3 -0
  33. package/dist/templates/index.js +12 -0
  34. package/dist/templates/personal.d.ts +2 -0
  35. package/dist/templates/personal.js +91 -0
  36. package/dist/templates/product.d.ts +2 -0
  37. package/dist/templates/product.js +123 -0
  38. package/dist/templates/research.d.ts +2 -0
  39. package/dist/templates/research.js +114 -0
  40. package/dist/templates/types.d.ts +7 -0
  41. package/dist/templates/types.js +1 -0
  42. package/dist/utils/bases.js +1 -2
  43. package/dist/utils/config.d.ts +43 -0
  44. package/dist/utils/config.js +114 -0
  45. package/dist/utils/files.d.ts +5 -0
  46. package/dist/utils/files.js +33 -0
  47. package/dist/utils/output.d.ts +6 -0
  48. package/dist/utils/output.js +17 -0
  49. package/dist/utils/test-helpers.d.ts +7 -1
  50. package/dist/utils/test-helpers.js +20 -14
  51. package/dist/utils/vault.d.ts +3 -2
  52. package/dist/utils/vault.js +6 -12
  53. package/package.json +25 -6
  54. package/skills/napkin/SKILL.md +765 -0
  55. package/dist/commands/onboard.d.ts +0 -2
  56. package/dist/commands/onboard.js +0 -56
@@ -0,0 +1,50 @@
1
+ # napkin-distill
2
+
3
+ Pi extension that automatically distills knowledge from conversations into your napkin vault.
4
+
5
+ ## How it works
6
+
7
+ 1. Runs on a configurable interval (default: 60 minutes)
8
+ 2. Checks if the conversation changed since last distill
9
+ 3. Sends the new conversation to a model
10
+ 4. The model extracts structured notes using your vault's templates
11
+ 5. Notes are written directly into the vault
12
+
13
+ ## Setup
14
+
15
+ Enable distill in your vault config:
16
+
17
+ ```bash
18
+ napkin config set --key distill.enabled --value true
19
+ ```
20
+
21
+ Or edit `.napkin/config.json` directly:
22
+
23
+ ```json
24
+ {
25
+ "distill": {
26
+ "enabled": true,
27
+ "intervalMinutes": 60,
28
+ "model": {
29
+ "provider": "anthropic",
30
+ "id": "claude-sonnet-4-6"
31
+ }
32
+ }
33
+ }
34
+ ```
35
+
36
+ ## Configuration
37
+
38
+ All distill settings live in `.napkin/config.json` under the `distill` key:
39
+
40
+ | Field | Default | Description |
41
+ |-------|---------|-------------|
42
+ | `distill.enabled` | `false` | Enable automatic distillation |
43
+ | `distill.intervalMinutes` | `60` | How often to check for new content |
44
+ | `distill.model.provider` | `"anthropic"` | LLM provider |
45
+ | `distill.model.id` | `"claude-sonnet-4-6"` | Model for distillation |
46
+ | `distill.templates` | `[]` | Which templates to use (empty = all in Templates/) |
47
+
48
+ ## Manual trigger
49
+
50
+ Use `/distill` in pi to manually trigger distillation of the full conversation.
@@ -0,0 +1,304 @@
1
+ import { spawn } from "node:child_process";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
6
+ import { SessionManager } from "@mariozechner/pi-coding-agent";
7
+
8
+ interface DistillConfig {
9
+ enabled: boolean;
10
+ intervalMinutes: number;
11
+ model: { provider: string; id: string };
12
+ }
13
+
14
+ const DEFAULT_CONFIG: DistillConfig = {
15
+ enabled: false,
16
+ intervalMinutes: 60,
17
+ model: { provider: "anthropic", id: "claude-sonnet-4-6" },
18
+ };
19
+
20
+ function loadDistillConfig(vaultPath: string): DistillConfig {
21
+ const configPath = path.join(vaultPath, "config.json");
22
+ if (!fs.existsSync(configPath)) return DEFAULT_CONFIG;
23
+ try {
24
+ const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
25
+ const distill = raw.distill || {};
26
+ return { ...DEFAULT_CONFIG, ...distill };
27
+ } catch {
28
+ return DEFAULT_CONFIG;
29
+ }
30
+ }
31
+
32
+ function findVaultPath(cwd: string): string | null {
33
+ let dir = cwd;
34
+ while (dir !== path.dirname(dir)) {
35
+ const napkinDir = path.join(dir, ".napkin");
36
+ if (fs.existsSync(napkinDir)) {
37
+ return napkinDir;
38
+ }
39
+ dir = path.dirname(dir);
40
+ }
41
+ return null;
42
+ }
43
+
44
+ const DISTILL_PROMPT = `Distill this conversation into the napkin vault.
45
+
46
+ 1. \`napkin overview\` — learn the vault structure and what exists
47
+ 2. \`napkin template list\` and \`napkin template read\` — learn the note formats
48
+ 3. Identify what's worth capturing. The vault structure and templates tell you what kinds of notes belong.
49
+ 4. For each note:
50
+ a. \`napkin search\` for the topic — if a note already covers it, \`napkin append\` instead of creating a duplicate
51
+ b. Create new notes with \`napkin create\`, following the template format
52
+ c. Add \`[[wikilinks]]\` to related notes
53
+
54
+ Be selective. Only capture knowledge useful to someone working on this project later. Skip meta-discussion, tool output, and chatter.`;
55
+
56
+ export default function (pi: ExtensionAPI) {
57
+ let intervalHandle: ReturnType<typeof setInterval> | null = null;
58
+ let countdownHandle: ReturnType<typeof setInterval> | null = null;
59
+ let lastDistillTimestamp = Date.now();
60
+ let lastSessionSize = 0;
61
+ let isRunning = false;
62
+ let activeProcess: ReturnType<typeof spawn> | null = null;
63
+
64
+ pi.on("session_start", async (_event, ctx) => {
65
+ const vaultPath = findVaultPath(ctx.cwd);
66
+ if (!vaultPath) return;
67
+
68
+ const config = loadDistillConfig(vaultPath);
69
+ if (!config.enabled) {
70
+ if (ctx.hasUI) {
71
+ const theme = ctx.ui.theme;
72
+ ctx.ui.setStatus(
73
+ "napkin-distill",
74
+ theme.fg("dim", "distill: off"),
75
+ );
76
+ }
77
+ return;
78
+ }
79
+
80
+ lastDistillTimestamp = Date.now();
81
+ const intervalMs = config.intervalMinutes * 60 * 1000;
82
+
83
+ if (ctx.hasUI) {
84
+ const theme = ctx.ui.theme;
85
+ const updateCountdown = () => {
86
+ if (isRunning) return;
87
+ const remaining = Math.max(0, intervalMs - (Date.now() - lastDistillTimestamp));
88
+ const mins = Math.floor(remaining / 60000);
89
+ const secs = Math.floor((remaining % 60000) / 1000);
90
+ const display = mins > 0 ? `${mins}m${secs.toString().padStart(2, "0")}s` : `${secs}s`;
91
+ ctx.ui.setStatus(
92
+ "napkin-distill",
93
+ theme.fg("dim", `distill: ${display}`),
94
+ );
95
+ };
96
+ updateCountdown();
97
+ countdownHandle = setInterval(updateCountdown, 1000);
98
+ }
99
+
100
+ intervalHandle = setInterval(
101
+ () => {
102
+ if (isRunning) return;
103
+ runDistill(ctx).catch((err) => {
104
+ if (ctx.hasUI) {
105
+ ctx.ui.notify(
106
+ `Distill error: ${err instanceof Error ? err.message : String(err)}`,
107
+ "error",
108
+ );
109
+ }
110
+ });
111
+ },
112
+ intervalMs,
113
+ );
114
+ });
115
+
116
+ pi.on("session_shutdown", async () => {
117
+ if (countdownHandle) {
118
+ clearInterval(countdownHandle);
119
+ countdownHandle = null;
120
+ }
121
+ if (intervalHandle) {
122
+ clearInterval(intervalHandle);
123
+ intervalHandle = null;
124
+ }
125
+ if (activeProcess) {
126
+ activeProcess.kill();
127
+ activeProcess = null;
128
+ }
129
+ });
130
+
131
+ async function runDistill(ctx: {
132
+ sessionManager: any;
133
+ hasUI: boolean;
134
+ ui: any;
135
+ cwd: string;
136
+ }) {
137
+ const vaultPath = findVaultPath(ctx.cwd);
138
+ if (!vaultPath) return;
139
+
140
+ const config = loadDistillConfig(vaultPath);
141
+ const sessionFile = ctx.sessionManager.getSessionFile?.();
142
+ if (!sessionFile) {
143
+ if (ctx.hasUI)
144
+ ctx.ui.notify(
145
+ "Distill: no session file (ephemeral session)",
146
+ "warning",
147
+ );
148
+ return;
149
+ }
150
+
151
+ // Skip if session hasn't changed since last distill
152
+ const currentSize = fs.existsSync(sessionFile)
153
+ ? fs.statSync(sessionFile).size
154
+ : 0;
155
+ if (currentSize > 0 && currentSize === lastSessionSize) {
156
+ lastDistillTimestamp = Date.now();
157
+ return;
158
+ }
159
+
160
+ isRunning = true;
161
+ const startTime = Date.now();
162
+ let timerHandle: ReturnType<typeof setInterval> | null = null;
163
+ const theme = ctx.hasUI ? ctx.ui.theme : null;
164
+
165
+ if (ctx.hasUI && theme) {
166
+ ctx.ui.setStatus(
167
+ "napkin-distill",
168
+ theme.fg("accent", "●") + theme.fg("dim", " distill"),
169
+ );
170
+ timerHandle = setInterval(() => {
171
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
172
+ ctx.ui.setStatus(
173
+ "napkin-distill",
174
+ theme.fg("accent", "●") +
175
+ theme.fg("dim", ` distill ${elapsed}s`),
176
+ );
177
+ }, 1000);
178
+ }
179
+
180
+ // Fork the session to a temp directory so the subprocess
181
+ // inherits the full conversation without modifying the original
182
+ const tmpSessionDir = fs.mkdtempSync(
183
+ path.join(os.tmpdir(), "napkin-distill-"),
184
+ );
185
+ let forkedSessionFile: string | null = null;
186
+
187
+ try {
188
+ // Fork: creates a new session file with the full conversation
189
+ const forkedSm = SessionManager.forkFrom(
190
+ sessionFile,
191
+ ctx.cwd,
192
+ tmpSessionDir,
193
+ );
194
+ forkedSessionFile = forkedSm.getSessionFile();
195
+
196
+ if (!forkedSessionFile) {
197
+ throw new Error("Failed to fork session");
198
+ }
199
+
200
+ // Spawn pi on the forked session
201
+ const args = [
202
+ "--session",
203
+ forkedSessionFile,
204
+ "-p",
205
+ "--model",
206
+ `${config.model.provider}/${config.model.id}`,
207
+ DISTILL_PROMPT,
208
+ ];
209
+
210
+ const exitCode = await new Promise<number>((resolve, reject) => {
211
+ const proc = spawn("pi", args, {
212
+ cwd: ctx.cwd,
213
+ shell: false,
214
+ stdio: ["ignore", "pipe", "pipe"],
215
+ });
216
+ activeProcess = proc;
217
+
218
+ let stderr = "";
219
+ proc.stderr.on("data", (data) => {
220
+ stderr += data.toString();
221
+ });
222
+
223
+ proc.on("error", (err) => {
224
+ activeProcess = null;
225
+ reject(err);
226
+ });
227
+
228
+ proc.on("close", (code) => {
229
+ activeProcess = null;
230
+ if (code !== 0 && stderr.trim()) {
231
+ reject(
232
+ new Error(
233
+ `pi exited with code ${code}: ${stderr.trim().slice(0, 200)}`,
234
+ ),
235
+ );
236
+ } else {
237
+ resolve(code ?? 0);
238
+ }
239
+ });
240
+ });
241
+
242
+ lastDistillTimestamp = Date.now();
243
+ lastSessionSize = currentSize;
244
+
245
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
246
+ if (ctx.hasUI && theme) {
247
+ ctx.ui.setStatus(
248
+ "napkin-distill",
249
+ theme.fg("success", "✓") +
250
+ theme.fg("dim", ` distill ${elapsed}s`),
251
+ );
252
+ ctx.ui.notify(`Distillation complete (${elapsed}s)`, "success");
253
+ }
254
+ } catch (err) {
255
+ if (ctx.hasUI && theme) {
256
+ ctx.ui.setStatus(
257
+ "napkin-distill",
258
+ theme.fg("error", "✗") + theme.fg("dim", " distill"),
259
+ );
260
+ }
261
+ throw err;
262
+ } finally {
263
+ if (timerHandle) clearInterval(timerHandle);
264
+ isRunning = false;
265
+ // Clean up forked session
266
+ fs.rmSync(tmpSessionDir, { recursive: true, force: true });
267
+ }
268
+ }
269
+
270
+ // Manual trigger
271
+ pi.registerCommand("distill", {
272
+ description: "Distill conversation knowledge into the vault",
273
+ handler: async (_args, ctx) => {
274
+ const vaultPath = findVaultPath(ctx.cwd);
275
+ if (!vaultPath) {
276
+ if (ctx.hasUI) ctx.ui.notify("No vault found", "error");
277
+ return;
278
+ }
279
+
280
+ if (isRunning) {
281
+ if (ctx.hasUI) ctx.ui.notify("Distill already running", "warning");
282
+ return;
283
+ }
284
+
285
+ const savedTimestamp = lastDistillTimestamp;
286
+ lastDistillTimestamp = 0;
287
+ lastSessionSize = 0; // bypass size check for manual trigger
288
+ runDistill(ctx)
289
+ .catch((err) => {
290
+ if (ctx.hasUI) {
291
+ ctx.ui.notify(
292
+ `Distill error: ${err instanceof Error ? err.message : String(err)}`,
293
+ "error",
294
+ );
295
+ }
296
+ })
297
+ .finally(() => {
298
+ if (lastDistillTimestamp === 0) {
299
+ lastDistillTimestamp = savedTimestamp;
300
+ }
301
+ });
302
+ },
303
+ });
304
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "napkin-distill",
3
+ "version": "0.1.0",
4
+ "description": "Automatic KB distillation for napkin vaults",
5
+ "peerDependencies": {
6
+ "@mariozechner/pi-ai": "*",
7
+ "@mariozechner/pi-coding-agent": "*",
8
+ "@sinclair/typebox": "*"
9
+ },
10
+ "pi": {
11
+ "extensions": ["./index.ts"]
12
+ }
13
+ }
@@ -0,0 +1,79 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { execSync } from "node:child_process";
4
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
5
+
6
+ function findVaultPath(cwd: string): string | null {
7
+ let dir = cwd;
8
+ while (dir !== path.dirname(dir)) {
9
+ const napkinDir = path.join(dir, ".napkin");
10
+ if (fs.existsSync(napkinDir)) {
11
+ return napkinDir;
12
+ }
13
+ dir = path.dirname(dir);
14
+ }
15
+ return null;
16
+ }
17
+
18
+ function getOverview(vaultPath: string): string | null {
19
+ try {
20
+ const output = execSync(`napkin overview --vault "${vaultPath}"`, {
21
+ encoding: "utf-8",
22
+ timeout: 10000,
23
+ }).trim();
24
+ return output || null;
25
+ } catch {
26
+ // Fallback to reading NAPKIN.md directly
27
+ const napkinPath = path.join(vaultPath, "NAPKIN.md");
28
+ if (!fs.existsSync(napkinPath)) return null;
29
+ return fs.readFileSync(napkinPath, "utf-8").trim();
30
+ }
31
+ }
32
+
33
+ export default function (pi: ExtensionAPI) {
34
+ let hasVault = false;
35
+
36
+ pi.on("session_start", async (_event, ctx) => {
37
+ const vaultPath = findVaultPath(ctx.cwd);
38
+ if (!vaultPath) return;
39
+
40
+ const overview = getOverview(vaultPath);
41
+ hasVault = !!overview;
42
+
43
+ if (overview) {
44
+ // Check if we already injected context in this session
45
+ const alreadyInjected = ctx.sessionManager
46
+ .getEntries()
47
+ .some(
48
+ (e) =>
49
+ e.type === "message" &&
50
+ e.message.role === "custom" &&
51
+ (e.message as any).customType === "napkin-context",
52
+ );
53
+
54
+ if (!alreadyInjected) {
55
+ ctx.sessionManager.appendCustomMessageEntry(
56
+ "napkin-context",
57
+ "## Napkin vault context\n" +
58
+ "You have access to a napkin vault (Obsidian-compatible knowledge base). " +
59
+ "Here is the vault overview. Use `napkin search <query>` to find specific content, " +
60
+ "`napkin read <file>` to open files.\n\n" +
61
+ overview,
62
+ true,
63
+ );
64
+ }
65
+ }
66
+
67
+ if (ctx.hasUI) {
68
+ const theme = ctx.ui.theme;
69
+ if (hasVault) {
70
+ ctx.ui.setStatus("napkin", "🧻" + theme.fg("dim", " napkin"));
71
+ } else {
72
+ ctx.ui.setStatus(
73
+ "napkin",
74
+ theme.fg("dim", "napkin: no NAPKIN.md"),
75
+ );
76
+ }
77
+ }
78
+ });
79
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "napkin-context",
3
+ "version": "0.1.0",
4
+ "description": "Inject napkin vault overview into agent system prompt",
5
+ "peerDependencies": {
6
+ "@mariozechner/pi-coding-agent": "*"
7
+ },
8
+ "pi": {
9
+ "extensions": ["./index.ts"]
10
+ }
11
+ }
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # napkin
2
2
 
3
- 🧻 Obsidian-compatible CLI for agents.
3
+ 🧻 Knowledge system for AI agents. Local-first, file-based, progressively disclosed.
4
4
 
5
- Every great idea started on a napkin. This one reads your Obsidian vault.
5
+ Every great idea started on a napkin.
6
6
 
7
7
  ## Install
8
8
 
@@ -10,21 +10,75 @@ Every great idea started on a napkin. This one reads your Obsidian vault.
10
10
  npm install -g napkin-ai
11
11
  ```
12
12
 
13
- ## Usage
13
+ As a pi package (includes extensions + skills):
14
14
 
15
- Run from inside an Obsidian vault (any directory containing `.obsidian/`):
15
+ ```bash
16
+ pi install npm:napkin-ai
17
+ ```
18
+
19
+ ## Quick Start
20
+
21
+ ```bash
22
+ # Initialize a vault with a template
23
+ napkin init --template coding
24
+
25
+ # See what's in it
26
+ napkin overview
27
+
28
+ # Search for something
29
+ napkin search "authentication"
30
+
31
+ # Read a file
32
+ napkin read "Architecture"
33
+ ```
34
+
35
+ ## Vault Structure
36
+
37
+ `.napkin/` is the vault root — all content lives inside it:
38
+
39
+ ```
40
+ my-project/
41
+ .napkin/ # The vault
42
+ NAPKIN.md # Context note (Level 0)
43
+ config.json # Unified config (syncs to .obsidian/)
44
+ decisions/ # Template-defined directories
45
+ architecture/
46
+ Templates/ # Note templates
47
+ .obsidian/ # Obsidian compatibility (auto-generated)
48
+ src/ # Your project (not in vault)
49
+ ```
50
+
51
+ ## Progressive Disclosure
52
+
53
+ napkin is designed as a memory system for AI agents. Instead of dumping the full vault into context, it reveals information gradually:
54
+
55
+ | Level | Command | Tokens | What it does |
56
+ |-------|---------|--------|-------------|
57
+ | 0 | `NAPKIN.md` | ~200 | Project context note |
58
+ | 1 | `napkin overview` | ~1-2k | L0 + vault map with TF-IDF keywords |
59
+ | 2 | `napkin search <query>` | ~2-5k | Ranked results with snippets |
60
+ | 3 | `napkin read <file>` | ~5-20k | Full file content |
61
+
62
+ ## Templates
63
+
64
+ Scaffold a vault with a domain-specific structure:
16
65
 
17
66
  ```bash
18
- cd ~/my-vault
19
- napkin vault
67
+ napkin init --template coding # decisions/, architecture/, guides/, changelog/
68
+ napkin init --template company # people/, projects/, runbooks/, infrastructure/
69
+ napkin init --template product # features/, roadmap/, research/, specs/, releases/
70
+ napkin init --template personal # people/, projects/, areas/, references/
71
+ napkin init --template research # papers/, concepts/, questions/, experiments/
20
72
  ```
21
73
 
22
- Or specify the vault path:
74
+ Each template includes directory structure, `_about.md` files, Obsidian note templates, and a `NAPKIN.md` skeleton.
23
75
 
24
76
  ```bash
25
- napkin --vault ~/my-vault vault
77
+ napkin init --list # List available templates
26
78
  ```
27
79
 
80
+ ## Commands
81
+
28
82
  ### Global flags
29
83
 
30
84
  | Flag | Description |
@@ -34,12 +88,11 @@ napkin --vault ~/my-vault vault
34
88
  | `--vault <path>` | Vault path (default: auto-detect from cwd) |
35
89
  | `--copy` | Copy output to clipboard |
36
90
 
37
- ## Commands
38
-
39
91
  ### Core
40
92
 
41
93
  ```bash
42
94
  napkin vault # Vault info
95
+ napkin overview # Vault map with keywords
43
96
  napkin read <file> # Read file contents
44
97
  napkin create --name "Note" --content "Hello"
45
98
  napkin append --file "Note" --content "More text"
@@ -47,8 +100,8 @@ napkin prepend --file "Note" --content "Top line"
47
100
  napkin move --file "Note" --to Archive
48
101
  napkin rename --file "Note" --name "Renamed"
49
102
  napkin delete --file "Note" # Move to .trash
50
- napkin search "meeting" # Full-text search
51
- napkin search "TODO" --context # Grep-style output
103
+ napkin search "meeting" # Ranked search with snippets
104
+ napkin search "TODO" --no-snippets # Files only
52
105
  ```
53
106
 
54
107
  ### Files & folders — `napkin file`
@@ -60,6 +113,8 @@ napkin file list --ext md # Filter by extension
60
113
  napkin file list --folder Projects # Filter by folder
61
114
  napkin file folder <path> # Folder info
62
115
  napkin file folders # List all folders
116
+ napkin file outline --file "note" # Heading tree
117
+ napkin file wordcount --file "note" # Word + character count
63
118
  ```
64
119
 
65
120
  ### Daily notes — `napkin daily`
@@ -114,8 +169,6 @@ napkin link deadends # No outgoing links
114
169
 
115
170
  ### Bases — `napkin base`
116
171
 
117
- Query vault files using Obsidian Bases `.base` files.
118
-
119
172
  ```bash
120
173
  napkin base list # List .base files
121
174
  napkin base views --file "projects" # List views
@@ -126,8 +179,6 @@ napkin base create --file "projects" --name "New Item"
126
179
 
127
180
  ### Canvas — `napkin canvas`
128
181
 
129
- Read and write JSON Canvas files (`.canvas`).
130
-
131
182
  ```bash
132
183
  napkin canvas list # List .canvas files
133
184
  napkin canvas read --file "Board" # Dump canvas
@@ -141,7 +192,7 @@ napkin canvas remove-node --file "Board" --id abc1
141
192
  ### Templates — `napkin template`
142
193
 
143
194
  ```bash
144
- napkin template list # List templates
195
+ napkin template list # List note templates
145
196
  napkin template read --name "Daily Note"
146
197
  napkin template insert --file "note" --name "Template"
147
198
  ```
@@ -153,23 +204,45 @@ napkin bookmark list # List bookmarks
153
204
  napkin bookmark add --file "note" # Bookmark a file
154
205
  ```
155
206
 
156
- ### Other
207
+ ### Config — `napkin config`
208
+
209
+ ```bash
210
+ napkin config show # Show full config
211
+ napkin config get --key search.limit # Get a value
212
+ napkin config set --key search.limit --value 50
213
+ ```
214
+
215
+ See [docs/configuration.md](docs/configuration.md) for all config options.
216
+
217
+ ### Graph — `napkin graph`
157
218
 
158
219
  ```bash
159
- napkin outline --file "note" # Heading tree
160
- napkin wordcount --file "note" # Word + character count
161
- napkin onboard # Agent instructions for CLAUDE.md
220
+ napkin graph # Interactive vault graph
162
221
  ```
163
222
 
164
- ## File resolution
223
+ Force-directed graph of vault notes and wikilinks. Click nodes to read content in a sidebar. On macOS, opens in a native window (Glimpse). On other platforms, opens in the browser. Configure with `graph.renderer` in config.
224
+
225
+ ## File Resolution
165
226
 
166
227
  Files can be referenced two ways:
167
228
  - **By name** (wikilink-style): `--file "Active Projects"` — searches all `.md` files by basename
168
229
  - **By path**: `--file "Projects/Active Projects.md"` — exact path from vault root
169
230
 
170
- ## For AI agents
231
+ ## Pi Extensions
232
+
233
+ napkin ships as a pi package with two extensions:
234
+
235
+ ### napkin-context
236
+ Injects the vault overview (Level 0 + Level 1) into the agent's system prompt on session start. The agent gets NAPKIN.md and the vault map with keywords for free.
237
+
238
+ ### napkin-distill
239
+ Forks the current session and spawns a sub-agent to distill knowledge into the vault. The sub-agent inherits the full conversation, uses napkin tools to read templates and create structured notes. Runs in the background.
240
+
241
+ ```bash
242
+ napkin config set --key distill.enabled --value true # Enable auto-distill
243
+ ```
171
244
 
172
- Every command supports `--json` for structured output. Run `napkin onboard` to get copy-paste instructions for your agent config.
245
+ Or trigger manually in pi: `/distill`
173
246
 
174
247
  ## Development
175
248
 
@@ -0,0 +1,13 @@
1
+ import { type OutputOptions } from "../utils/output.js";
2
+ export declare function configShow(opts: OutputOptions & {
3
+ vault?: string;
4
+ }): Promise<void>;
5
+ export declare function configSet(opts: OutputOptions & {
6
+ vault?: string;
7
+ key?: string;
8
+ value?: string;
9
+ }): Promise<void>;
10
+ export declare function configGet(opts: OutputOptions & {
11
+ vault?: string;
12
+ key?: string;
13
+ }): Promise<void>;