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.
- package/.pi/extensions/distill/README.md +50 -0
- package/.pi/extensions/distill/index.ts +304 -0
- package/.pi/extensions/distill/package.json +13 -0
- package/.pi/extensions/napkin-context/index.ts +79 -0
- package/.pi/extensions/napkin-context/package.json +11 -0
- package/README.md +97 -24
- package/dist/commands/config.d.ts +13 -0
- package/dist/commands/config.js +77 -0
- package/dist/commands/crud.js +24 -15
- package/dist/commands/daily.js +6 -5
- package/dist/commands/files.js +4 -4
- package/dist/commands/graph.d.ts +4 -0
- package/dist/commands/graph.js +453 -0
- package/dist/commands/init.d.ts +2 -5
- package/dist/commands/init.js +74 -23
- package/dist/commands/links.js +4 -4
- package/dist/commands/outline.js +3 -3
- package/dist/commands/overview.d.ts +6 -0
- package/dist/commands/overview.js +340 -0
- package/dist/commands/properties.js +5 -5
- package/dist/commands/search.d.ts +3 -1
- package/dist/commands/search.js +141 -44
- package/dist/commands/tasks.js +5 -5
- package/dist/commands/templates.js +11 -8
- package/dist/commands/vault.js +1 -3
- package/dist/commands/wordcount.js +3 -3
- package/dist/main.js +162 -69
- package/dist/templates/coding.d.ts +2 -0
- package/dist/templates/coding.js +104 -0
- package/dist/templates/company.d.ts +2 -0
- package/dist/templates/company.js +121 -0
- package/dist/templates/index.d.ts +3 -0
- package/dist/templates/index.js +12 -0
- package/dist/templates/personal.d.ts +2 -0
- package/dist/templates/personal.js +91 -0
- package/dist/templates/product.d.ts +2 -0
- package/dist/templates/product.js +123 -0
- package/dist/templates/research.d.ts +2 -0
- package/dist/templates/research.js +114 -0
- package/dist/templates/types.d.ts +7 -0
- package/dist/templates/types.js +1 -0
- package/dist/utils/bases.js +1 -2
- package/dist/utils/config.d.ts +43 -0
- package/dist/utils/config.js +114 -0
- package/dist/utils/files.d.ts +5 -0
- package/dist/utils/files.js +33 -0
- package/dist/utils/output.d.ts +6 -0
- package/dist/utils/output.js +17 -0
- package/dist/utils/test-helpers.d.ts +7 -1
- package/dist/utils/test-helpers.js +20 -14
- package/dist/utils/vault.d.ts +3 -2
- package/dist/utils/vault.js +6 -12
- package/package.json +25 -6
- package/skills/napkin/SKILL.md +765 -0
- package/dist/commands/onboard.d.ts +0 -2
- 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
|
+
}
|
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# napkin
|
|
2
2
|
|
|
3
|
-
🧻
|
|
3
|
+
🧻 Knowledge system for AI agents. Local-first, file-based, progressively disclosed.
|
|
4
4
|
|
|
5
|
-
Every great idea started on a napkin.
|
|
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
|
-
|
|
13
|
+
As a pi package (includes extensions + skills):
|
|
14
14
|
|
|
15
|
-
|
|
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
|
-
|
|
19
|
-
napkin
|
|
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
|
-
|
|
74
|
+
Each template includes directory structure, `_about.md` files, Obsidian note templates, and a `NAPKIN.md` skeleton.
|
|
23
75
|
|
|
24
76
|
```bash
|
|
25
|
-
napkin --
|
|
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" #
|
|
51
|
-
napkin search "TODO" --
|
|
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
|
-
###
|
|
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
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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>;
|