napkin-ai 0.4.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -59,6 +59,20 @@ napkin is designed as a memory system for agents. Instead of dumping the full va
59
59
  | 2 | `napkin search <query>` | ~2-5k | Ranked results with snippets |
60
60
  | 3 | `napkin read <file>` | ~5-20k | Full file content |
61
61
 
62
+ ## Benchmarks
63
+
64
+ napkin includes agentic retrieval benchmarks in `bench/`. The headline result is [LongMemEval](https://arxiv.org/abs/2410.10813) (ICLR 2025), which tests long-term conversational memory across 500 questions.
65
+
66
+ | Dataset | Sessions | pi + napkin | Best prior system | GPT-4o full context |
67
+ |---------|----------|-------------|-------------------|---------------------|
68
+ | Oracle | 1-6 | **92.0%** | 92.4% | 92.4% |
69
+ | S | ~40 | **91.0%** | 86% | 64% |
70
+ | M | ~500 | **83.0%** | 72% | n/a |
71
+
72
+ Zero preprocessing. No embeddings, no graphs, no summaries. Just BM25 search on markdown files.
73
+
74
+ See [`bench/README.md`](bench/README.md) for details and usage.
75
+
62
76
  ## Templates
63
77
 
64
78
  Scaffold a vault with a domain-specific structure:
@@ -94,14 +108,15 @@ napkin init --list # List available templates
94
108
  napkin vault # Vault info
95
109
  napkin overview # Vault map with keywords
96
110
  napkin read <file> # Read file contents
97
- napkin create --name "Note" --content "Hello"
98
- napkin append --file "Note" --content "More text"
99
- napkin prepend --file "Note" --content "Top line"
100
- napkin move --file "Note" --to Archive
101
- napkin rename --file "Note" --name "Renamed"
102
- napkin delete --file "Note" # Move to .trash
111
+ napkin create "Note" "Hello" # Create with content
112
+ napkin append "Note" "More text" # Append to file
113
+ napkin prepend "Note" "Top line" # Prepend after frontmatter
114
+ napkin move "Note" Archive # Move to folder
115
+ napkin rename "Note" "Renamed" # Rename file
116
+ napkin delete "Note" # Move to .trash
103
117
  napkin search "meeting" # Ranked search with snippets
104
118
  napkin search "TODO" --no-snippets # Files only
119
+ echo "piped content" | napkin append "Note" # Stdin support
105
120
  ```
106
121
 
107
122
  ### Files & folders — `napkin file`
@@ -113,8 +128,8 @@ napkin file list --ext md # Filter by extension
113
128
  napkin file list --folder Projects # Filter by folder
114
129
  napkin file folder <path> # Folder info
115
130
  napkin file folders # List all folders
116
- napkin file outline --file "note" # Heading tree
117
- napkin file wordcount --file "note" # Word + character count
131
+ napkin file outline "note" # Heading tree
132
+ napkin file wordcount "note" # Word + character count
118
133
  ```
119
134
 
120
135
  ### Daily notes — `napkin daily`
@@ -123,8 +138,8 @@ napkin file wordcount --file "note" # Word + character count
123
138
  napkin daily today # Create today's daily note
124
139
  napkin daily path # Print daily note path
125
140
  napkin daily read # Print daily note contents
126
- napkin daily append --content "- [ ] Buy groceries"
127
- napkin daily prepend --content "## Morning"
141
+ napkin daily append "- [ ] Buy groceries"
142
+ napkin daily prepend "## Morning"
128
143
  ```
129
144
 
130
145
  ### Tags — `napkin tag`
@@ -28,7 +28,7 @@ function collectAliases(vaultPath, fileFilter) {
28
28
  }
29
29
  export async function aliases(opts) {
30
30
  const v = findVault(opts.vault);
31
- const result = collectAliases(v.path, opts.file);
31
+ const result = collectAliases(v.contentPath, opts.file);
32
32
  output(opts, {
33
33
  json: () => {
34
34
  if (opts.total)
@@ -7,7 +7,7 @@ import { bold, dim, error, output, success, } from "../utils/output.js";
7
7
  import { findVault } from "../utils/vault.js";
8
8
  export async function bases(opts) {
9
9
  const v = findVault(opts.vault);
10
- const files = listFiles(v.path).filter((f) => f.endsWith(".base"));
10
+ const files = listFiles(v.contentPath).filter((f) => f.endsWith(".base"));
11
11
  output(opts, {
12
12
  json: () => ({ bases: files }),
13
13
  human: () => {
@@ -23,12 +23,12 @@ export async function bases(opts) {
23
23
  }
24
24
  export async function baseViews(opts) {
25
25
  const v = findVault(opts.vault);
26
- const baseFile = resolveBaseFile(v.path, opts);
26
+ const baseFile = resolveBaseFile(v.contentPath, opts);
27
27
  if (!baseFile) {
28
28
  error("No base file specified. Use --file or --path");
29
29
  process.exit(EXIT_USER_ERROR);
30
30
  }
31
- const content = fs.readFileSync(path.join(v.path, baseFile), "utf-8");
31
+ const content = fs.readFileSync(path.join(v.contentPath, baseFile), "utf-8");
32
32
  const config = parseBaseFile(content);
33
33
  const views = (config.views || []).map((view) => ({
34
34
  name: view.name || "(unnamed)",
@@ -45,14 +45,14 @@ export async function baseViews(opts) {
45
45
  }
46
46
  export async function baseQuery(opts) {
47
47
  const v = findVault(opts.vault);
48
- const baseFile = resolveBaseFile(v.path, opts);
48
+ const baseFile = resolveBaseFile(v.contentPath, opts);
49
49
  if (!baseFile) {
50
50
  error("No base file specified. Use --file or --path");
51
51
  process.exit(EXIT_USER_ERROR);
52
52
  }
53
- const content = fs.readFileSync(path.join(v.path, baseFile), "utf-8");
53
+ const content = fs.readFileSync(path.join(v.contentPath, baseFile), "utf-8");
54
54
  const config = parseBaseFile(content);
55
- const db = await buildDatabase(v.path);
55
+ const db = await buildDatabase(v.contentPath);
56
56
  try {
57
57
  // Derive thisFile from the base file path
58
58
  const thisFile = baseFile
@@ -156,7 +156,7 @@ export async function baseCreate(opts) {
156
156
  ? opts.path
157
157
  : `${opts.path}/${opts.name}.md`
158
158
  : `${opts.name}.md`;
159
- const fullPath = path.join(v.path, targetPath);
159
+ const fullPath = path.join(v.contentPath, targetPath);
160
160
  fs.mkdirSync(path.dirname(fullPath), { recursive: true });
161
161
  fs.writeFileSync(fullPath, opts.content || "");
162
162
  output(opts, {
@@ -3,8 +3,8 @@ import * as path from "node:path";
3
3
  import { EXIT_USER_ERROR } from "../utils/exit-codes.js";
4
4
  import { dim, error, output, success, } from "../utils/output.js";
5
5
  import { findVault } from "../utils/vault.js";
6
- function readBookmarks(vaultPath) {
7
- const configPath = path.join(vaultPath, ".obsidian", "bookmarks.json");
6
+ function readBookmarks(obsidianPath) {
7
+ const configPath = path.join(obsidianPath, "bookmarks.json");
8
8
  try {
9
9
  const content = fs.readFileSync(configPath, "utf-8");
10
10
  return JSON.parse(content);
@@ -13,8 +13,8 @@ function readBookmarks(vaultPath) {
13
13
  return [];
14
14
  }
15
15
  }
16
- function writeBookmarks(vaultPath, bookmarks) {
17
- const configPath = path.join(vaultPath, ".obsidian", "bookmarks.json");
16
+ function writeBookmarks(obsidianPath, bookmarks) {
17
+ const configPath = path.join(obsidianPath, "bookmarks.json");
18
18
  fs.writeFileSync(configPath, JSON.stringify(bookmarks, null, 2));
19
19
  }
20
20
  function flattenBookmarks(items) {
@@ -31,7 +31,7 @@ function flattenBookmarks(items) {
31
31
  }
32
32
  export async function bookmarks(opts) {
33
33
  const v = findVault(opts.vault);
34
- const items = readBookmarks(v.path);
34
+ const items = readBookmarks(v.obsidianPath);
35
35
  const flat = flattenBookmarks(items);
36
36
  output(opts, {
37
37
  json: () => (opts.total ? { total: flat.length } : { bookmarks: flat }),
@@ -72,9 +72,9 @@ export async function bookmark(opts) {
72
72
  error("Specify --file, --folder, --search, or --url to bookmark");
73
73
  process.exit(EXIT_USER_ERROR);
74
74
  }
75
- const items = readBookmarks(v.path);
75
+ const items = readBookmarks(v.obsidianPath);
76
76
  items.push(entry);
77
- writeBookmarks(v.path, items);
77
+ writeBookmarks(v.obsidianPath, items);
78
78
  output(opts, {
79
79
  json: () => ({ added: entry }),
80
80
  human: () => success(`Bookmarked ${entry.path || entry.query || entry.url}`),
@@ -36,7 +36,7 @@ function writeCanvas(vaultPath, filePath, canvas) {
36
36
  }
37
37
  export async function canvases(opts) {
38
38
  const v = findVault(opts.vault);
39
- const files = listFiles(v.path).filter((f) => f.endsWith(".canvas"));
39
+ const files = listFiles(v.contentPath).filter((f) => f.endsWith(".canvas"));
40
40
  output(opts, {
41
41
  json: () => (opts.total ? { total: files.length } : { canvases: files }),
42
42
  human: () => {
@@ -59,7 +59,7 @@ export async function canvasRead(opts) {
59
59
  error("No file specified. Use --file <name>");
60
60
  process.exit(EXIT_USER_ERROR);
61
61
  }
62
- const { canvas, filePath } = readCanvas(v.path, opts.file);
62
+ const { canvas, filePath } = readCanvas(v.contentPath, opts.file);
63
63
  output(opts, {
64
64
  json: () => ({ path: filePath, ...canvas }),
65
65
  human: () => {
@@ -94,7 +94,7 @@ export async function canvasNodes(opts) {
94
94
  error("No file specified. Use --file <name>");
95
95
  process.exit(EXIT_USER_ERROR);
96
96
  }
97
- const { canvas } = readCanvas(v.path, opts.file);
97
+ const { canvas } = readCanvas(v.contentPath, opts.file);
98
98
  let nodes = canvas.nodes;
99
99
  if (opts.type) {
100
100
  nodes = nodes.filter((n) => n.type === opts.type);
@@ -125,14 +125,14 @@ export async function canvasCreate(opts) {
125
125
  ? opts.file
126
126
  : `${opts.file}.canvas`;
127
127
  const targetPath = opts.path ? `${opts.path}/${fileName}` : fileName;
128
- const fullPath = path.join(v.path, targetPath);
128
+ const fullPath = path.join(v.contentPath, targetPath);
129
129
  if (fs.existsSync(fullPath)) {
130
130
  error(`Canvas already exists: ${targetPath}`);
131
131
  process.exit(EXIT_USER_ERROR);
132
132
  }
133
133
  fs.mkdirSync(path.dirname(fullPath), { recursive: true });
134
134
  const canvas = { nodes: [], edges: [] };
135
- writeCanvas(v.path, targetPath, canvas);
135
+ writeCanvas(v.contentPath, targetPath, canvas);
136
136
  output(opts, {
137
137
  json: () => ({ path: targetPath, created: true }),
138
138
  human: () => success(`Created ${targetPath}`),
@@ -149,7 +149,7 @@ export async function canvasAddNode(opts) {
149
149
  error("Invalid node type. Use: text, file, link, or group");
150
150
  process.exit(EXIT_USER_ERROR);
151
151
  }
152
- const { canvas, filePath } = readCanvas(v.path, opts.file);
152
+ const { canvas, filePath } = readCanvas(v.contentPath, opts.file);
153
153
  // Auto-position: find rightmost node and place next to it
154
154
  const maxX = canvas.nodes.reduce((max, n) => Math.max(max, n.x + n.width), 0);
155
155
  const node = {
@@ -186,7 +186,7 @@ export async function canvasAddNode(opts) {
186
186
  if (opts.color)
187
187
  node.color = opts.color;
188
188
  canvas.nodes.push(node);
189
- writeCanvas(v.path, filePath, canvas);
189
+ writeCanvas(v.contentPath, filePath, canvas);
190
190
  output(opts, {
191
191
  json: () => ({ id: node.id, type: node.type, added: true }),
192
192
  human: () => success(`Added ${node.type} node ${node.id}`),
@@ -202,7 +202,7 @@ export async function canvasAddEdge(opts) {
202
202
  error("Both --from and --to node IDs required");
203
203
  process.exit(EXIT_USER_ERROR);
204
204
  }
205
- const { canvas, filePath } = readCanvas(v.path, opts.file);
205
+ const { canvas, filePath } = readCanvas(v.contentPath, opts.file);
206
206
  // Validate node IDs exist (match by full ID or prefix)
207
207
  const findNode = (ref) => canvas.nodes.find((n) => n.id === ref || n.id.startsWith(ref));
208
208
  const fromNode = findNode(opts.from);
@@ -229,7 +229,7 @@ export async function canvasAddEdge(opts) {
229
229
  if (opts.color)
230
230
  edge.color = opts.color;
231
231
  canvas.edges.push(edge);
232
- writeCanvas(v.path, filePath, canvas);
232
+ writeCanvas(v.contentPath, filePath, canvas);
233
233
  output(opts, {
234
234
  json: () => ({
235
235
  id: edge.id,
@@ -246,7 +246,7 @@ export async function canvasRemoveNode(opts) {
246
246
  error("Both --file and --id required");
247
247
  process.exit(EXIT_USER_ERROR);
248
248
  }
249
- const { canvas, filePath } = readCanvas(v.path, opts.file);
249
+ const { canvas, filePath } = readCanvas(v.contentPath, opts.file);
250
250
  const id = opts.id;
251
251
  const node = canvas.nodes.find((n) => n.id === id || n.id.startsWith(id));
252
252
  if (!node) {
@@ -256,7 +256,7 @@ export async function canvasRemoveNode(opts) {
256
256
  // Remove node and any connected edges
257
257
  canvas.nodes = canvas.nodes.filter((n) => n.id !== node.id);
258
258
  canvas.edges = canvas.edges.filter((e) => e.fromNode !== node.id && e.toNode !== node.id);
259
- writeCanvas(v.path, filePath, canvas);
259
+ writeCanvas(v.contentPath, filePath, canvas);
260
260
  output(opts, {
261
261
  json: () => ({ id: node.id, removed: true }),
262
262
  human: () => success(`Removed node ${node.id}`),
@@ -4,7 +4,7 @@ import { bold, dim, error, output, } from "../utils/output.js";
4
4
  import { findVault } from "../utils/vault.js";
5
5
  export async function configShow(opts) {
6
6
  const v = findVault(opts.vault);
7
- const config = loadConfig(v.path);
7
+ const config = loadConfig(v.configPath);
8
8
  output(opts, {
9
9
  json: () => config,
10
10
  human: () => {
@@ -35,7 +35,7 @@ export async function configSet(opts) {
35
35
  parsed = opts.value;
36
36
  }
37
37
  current[parts[parts.length - 1]] = parsed;
38
- const updated = updateConfig(v.path, obj);
38
+ const updated = updateConfig(v.configPath, obj);
39
39
  output(opts, {
40
40
  json: () => updated,
41
41
  human: () => {
@@ -49,7 +49,7 @@ export async function configGet(opts) {
49
49
  error("Usage: napkin config get --key <path>");
50
50
  process.exit(EXIT_USER_ERROR);
51
51
  }
52
- const config = loadConfig(v.path);
52
+ const config = loadConfig(v.configPath);
53
53
  const parts = opts.key.split(".");
54
54
  let value = config;
55
55
  for (const part of parts) {
@@ -12,12 +12,12 @@ export async function read(fileRef, opts) {
12
12
  error("No file specified. Usage: napkin read <file>");
13
13
  process.exit(EXIT_USER_ERROR);
14
14
  }
15
- const resolved = resolveFile(v.path, fileRef);
15
+ const resolved = resolveFile(v.contentPath, fileRef);
16
16
  if (!resolved) {
17
- fileNotFound(fileRef, suggestFile(v.path, fileRef));
17
+ fileNotFound(fileRef, suggestFile(v.contentPath, fileRef));
18
18
  process.exit(EXIT_NOT_FOUND);
19
19
  }
20
- const content = fs.readFileSync(path.join(v.path, resolved), "utf-8");
20
+ const content = fs.readFileSync(path.join(v.contentPath, resolved), "utf-8");
21
21
  output(opts, {
22
22
  json: () => ({ path: resolved, content }),
23
23
  human: () => console.log(content),
@@ -33,21 +33,21 @@ export async function create(opts) {
33
33
  const name = opts.name || "Untitled";
34
34
  targetPath = `${name}.md`;
35
35
  }
36
- const fullPath = path.join(v.path, targetPath);
36
+ const fullPath = path.join(v.contentPath, targetPath);
37
37
  if (fs.existsSync(fullPath) && !opts.overwrite) {
38
38
  error(`File already exists: ${targetPath}. Use --overwrite to replace.`);
39
39
  process.exit(EXIT_USER_ERROR);
40
40
  }
41
41
  let content = opts.content || "";
42
42
  if (opts.template) {
43
- const config = loadConfig(v.path);
44
- const templateRef = resolveFile(v.path, opts.template) ||
45
- resolveFile(v.path, `${config.templates.folder}/${opts.template}`);
43
+ const config = loadConfig(v.configPath);
44
+ const templateRef = resolveFile(v.contentPath, opts.template) ||
45
+ resolveFile(v.contentPath, `${config.templates.folder}/${opts.template}`);
46
46
  if (templateRef) {
47
- content = fs.readFileSync(path.join(v.path, templateRef), "utf-8");
47
+ content = fs.readFileSync(path.join(v.contentPath, templateRef), "utf-8");
48
48
  }
49
49
  else {
50
- const tmplFiles = listFiles(v.path, {
50
+ const tmplFiles = listFiles(v.contentPath, {
51
51
  folder: config.templates.folder,
52
52
  ext: "md",
53
53
  }).map((f) => path.basename(f, ".md"));
@@ -62,22 +62,35 @@ export async function create(opts) {
62
62
  human: () => success(`Created ${targetPath}`),
63
63
  });
64
64
  }
65
+ async function readStdin() {
66
+ if (process.stdin.isTTY)
67
+ return undefined;
68
+ const chunks = [];
69
+ for await (const chunk of process.stdin) {
70
+ chunks.push(chunk);
71
+ }
72
+ const result = Buffer.concat(chunks).toString("utf-8").trimEnd();
73
+ return result || undefined;
74
+ }
65
75
  export async function append(opts) {
66
76
  const v = findVault(opts.vault);
67
77
  if (!opts.file) {
68
- error("No file specified. Use --file <name>");
78
+ error("No file specified. Use: napkin append <file> [content]");
69
79
  process.exit(EXIT_USER_ERROR);
70
80
  }
71
81
  if (!opts.content) {
72
- error("No content specified. Use --content <text>");
82
+ opts.content = await readStdin();
83
+ }
84
+ if (!opts.content) {
85
+ error("No content specified. Use: napkin append <file> <content>");
73
86
  process.exit(EXIT_USER_ERROR);
74
87
  }
75
- const resolved = resolveFile(v.path, opts.file);
88
+ const resolved = resolveFile(v.contentPath, opts.file);
76
89
  if (!resolved) {
77
- fileNotFound(opts.file, suggestFile(v.path, opts.file));
90
+ fileNotFound(opts.file, suggestFile(v.contentPath, opts.file));
78
91
  process.exit(EXIT_NOT_FOUND);
79
92
  }
80
- const fullPath = path.join(v.path, resolved);
93
+ const fullPath = path.join(v.contentPath, resolved);
81
94
  const existing = fs.readFileSync(fullPath, "utf-8");
82
95
  const separator = opts.inline ? "" : "\n";
83
96
  fs.writeFileSync(fullPath, existing + separator + opts.content);
@@ -89,19 +102,22 @@ export async function append(opts) {
89
102
  export async function prepend(opts) {
90
103
  const v = findVault(opts.vault);
91
104
  if (!opts.file) {
92
- error("No file specified. Use --file <name>");
105
+ error("No file specified. Use: napkin prepend <file> [content]");
93
106
  process.exit(EXIT_USER_ERROR);
94
107
  }
95
108
  if (!opts.content) {
96
- error("No content specified. Use --content <text>");
109
+ opts.content = await readStdin();
110
+ }
111
+ if (!opts.content) {
112
+ error("No content specified. Use: napkin prepend <file> <content>");
97
113
  process.exit(EXIT_USER_ERROR);
98
114
  }
99
- const resolved = resolveFile(v.path, opts.file);
115
+ const resolved = resolveFile(v.contentPath, opts.file);
100
116
  if (!resolved) {
101
- fileNotFound(opts.file, suggestFile(v.path, opts.file));
117
+ fileNotFound(opts.file, suggestFile(v.contentPath, opts.file));
102
118
  process.exit(EXIT_NOT_FOUND);
103
119
  }
104
- const fullPath = path.join(v.path, resolved);
120
+ const fullPath = path.join(v.contentPath, resolved);
105
121
  const existing = fs.readFileSync(fullPath, "utf-8");
106
122
  const separator = opts.inline ? "" : "\n";
107
123
  // Insert after frontmatter if present
@@ -128,9 +144,9 @@ export async function move(opts) {
128
144
  error("No destination specified. Use --to <path>");
129
145
  process.exit(EXIT_USER_ERROR);
130
146
  }
131
- const resolved = resolveFile(v.path, opts.file);
147
+ const resolved = resolveFile(v.contentPath, opts.file);
132
148
  if (!resolved) {
133
- fileNotFound(opts.file, suggestFile(v.path, opts.file));
149
+ fileNotFound(opts.file, suggestFile(v.contentPath, opts.file));
134
150
  process.exit(EXIT_NOT_FOUND);
135
151
  }
136
152
  let destPath = opts.to;
@@ -138,8 +154,8 @@ export async function move(opts) {
138
154
  if (!destPath.endsWith(".md")) {
139
155
  destPath = path.join(destPath, path.basename(resolved));
140
156
  }
141
- const srcFull = path.join(v.path, resolved);
142
- const destFull = path.join(v.path, destPath);
157
+ const srcFull = path.join(v.contentPath, resolved);
158
+ const destFull = path.join(v.contentPath, destPath);
143
159
  fs.mkdirSync(path.dirname(destFull), { recursive: true });
144
160
  fs.renameSync(srcFull, destFull);
145
161
  output(opts, {
@@ -157,15 +173,15 @@ export async function rename(opts) {
157
173
  error("No new name specified. Use --name <name>");
158
174
  process.exit(EXIT_USER_ERROR);
159
175
  }
160
- const resolved = resolveFile(v.path, opts.file);
176
+ const resolved = resolveFile(v.contentPath, opts.file);
161
177
  if (!resolved) {
162
- fileNotFound(opts.file, suggestFile(v.path, opts.file));
178
+ fileNotFound(opts.file, suggestFile(v.contentPath, opts.file));
163
179
  process.exit(EXIT_NOT_FOUND);
164
180
  }
165
181
  const newName = opts.name.endsWith(".md") ? opts.name : `${opts.name}.md`;
166
182
  const destPath = path.join(path.dirname(resolved), newName);
167
- const srcFull = path.join(v.path, resolved);
168
- const destFull = path.join(v.path, destPath);
183
+ const srcFull = path.join(v.contentPath, resolved);
184
+ const destFull = path.join(v.contentPath, destPath);
169
185
  fs.renameSync(srcFull, destFull);
170
186
  output(opts, {
171
187
  json: () => ({ from: resolved, to: destPath }),
@@ -178,17 +194,17 @@ export async function del(opts) {
178
194
  error("No file specified. Use --file <name>");
179
195
  process.exit(EXIT_USER_ERROR);
180
196
  }
181
- const resolved = resolveFile(v.path, opts.file);
197
+ const resolved = resolveFile(v.contentPath, opts.file);
182
198
  if (!resolved) {
183
- fileNotFound(opts.file, suggestFile(v.path, opts.file));
199
+ fileNotFound(opts.file, suggestFile(v.contentPath, opts.file));
184
200
  process.exit(EXIT_NOT_FOUND);
185
201
  }
186
- const fullPath = path.join(v.path, resolved);
202
+ const fullPath = path.join(v.contentPath, resolved);
187
203
  if (opts.permanent) {
188
204
  fs.unlinkSync(fullPath);
189
205
  }
190
206
  else {
191
- const trashDir = path.join(v.path, ".trash");
207
+ const trashDir = path.join(v.contentPath, ".trash");
192
208
  fs.mkdirSync(trashDir, { recursive: true });
193
209
  const trashPath = path.join(trashDir, path.basename(resolved));
194
210
  fs.renameSync(fullPath, trashPath);
@@ -51,15 +51,15 @@ export function getDailyPath(vaultPath, date) {
51
51
  }
52
52
  export async function daily(opts) {
53
53
  const v = findVault(opts.vault);
54
- const dailyPath = getDailyPath(v.path);
55
- const fullPath = path.join(v.path, dailyPath);
54
+ const dailyPath = getDailyPath(v.configPath);
55
+ const fullPath = path.join(v.contentPath, dailyPath);
56
56
  // Create if doesn't exist
57
57
  if (!fs.existsSync(fullPath)) {
58
58
  fs.mkdirSync(path.dirname(fullPath), { recursive: true });
59
- const config = getDailyConfig(v.path);
59
+ const config = getDailyConfig(v.configPath);
60
60
  let content = "";
61
61
  if (config.template) {
62
- const templatePath = path.join(v.path, `${config.template}.md`);
62
+ const templatePath = path.join(v.contentPath, `${config.template}.md`);
63
63
  if (fs.existsSync(templatePath)) {
64
64
  content = fs.readFileSync(templatePath, "utf-8");
65
65
  }
@@ -73,7 +73,7 @@ export async function daily(opts) {
73
73
  }
74
74
  export async function dailyPath(opts) {
75
75
  const v = findVault(opts.vault);
76
- const dp = getDailyPath(v.path);
76
+ const dp = getDailyPath(v.configPath);
77
77
  output(opts, {
78
78
  json: () => ({ path: dp }),
79
79
  human: () => console.log(dp),
@@ -81,8 +81,8 @@ export async function dailyPath(opts) {
81
81
  }
82
82
  export async function dailyRead(opts) {
83
83
  const v = findVault(opts.vault);
84
- const dp = getDailyPath(v.path);
85
- const fullPath = path.join(v.path, dp);
84
+ const dp = getDailyPath(v.configPath);
85
+ const fullPath = path.join(v.contentPath, dp);
86
86
  if (!fs.existsSync(fullPath)) {
87
87
  error(`Daily note not found: ${dp}`);
88
88
  process.exit(EXIT_NOT_FOUND);
@@ -99,8 +99,8 @@ export async function dailyAppend(opts) {
99
99
  error("No content specified. Use --content <text>");
100
100
  process.exit(EXIT_USER_ERROR);
101
101
  }
102
- const dp = getDailyPath(v.path);
103
- const fullPath = path.join(v.path, dp);
102
+ const dp = getDailyPath(v.configPath);
103
+ const fullPath = path.join(v.contentPath, dp);
104
104
  // Create if doesn't exist
105
105
  if (!fs.existsSync(fullPath)) {
106
106
  fs.mkdirSync(path.dirname(fullPath), { recursive: true });
@@ -120,8 +120,8 @@ export async function dailyPrepend(opts) {
120
120
  error("No content specified. Use --content <text>");
121
121
  process.exit(EXIT_USER_ERROR);
122
122
  }
123
- const dp = getDailyPath(v.path);
124
- const fullPath = path.join(v.path, dp);
123
+ const dp = getDailyPath(v.configPath);
124
+ const fullPath = path.join(v.contentPath, dp);
125
125
  if (!fs.existsSync(fullPath)) {
126
126
  fs.mkdirSync(path.dirname(fullPath), { recursive: true });
127
127
  fs.writeFileSync(fullPath, "");
@@ -11,12 +11,12 @@ export async function file(fileRef, opts) {
11
11
  error("No file specified. Usage: obsidian-cli file <name>");
12
12
  process.exit(EXIT_NOT_FOUND);
13
13
  }
14
- const resolved = resolveFile(v.path, fileRef);
14
+ const resolved = resolveFile(v.contentPath, fileRef);
15
15
  if (!resolved) {
16
- fileNotFound(fileRef, suggestFile(v.path, fileRef));
16
+ fileNotFound(fileRef, suggestFile(v.contentPath, fileRef));
17
17
  process.exit(EXIT_NOT_FOUND);
18
18
  }
19
- const info = getFileInfo(v.path, resolved);
19
+ const info = getFileInfo(v.contentPath, resolved);
20
20
  output(opts, {
21
21
  json: () => info,
22
22
  human: () => {
@@ -31,7 +31,10 @@ export async function file(fileRef, opts) {
31
31
  }
32
32
  export async function files(opts) {
33
33
  const v = findVault(opts.vault);
34
- const result = listFiles(v.path, { folder: opts.folder, ext: opts.ext });
34
+ const result = listFiles(v.contentPath, {
35
+ folder: opts.folder,
36
+ ext: opts.ext,
37
+ });
35
38
  output(opts, {
36
39
  json: () => (opts.total ? { total: result.length } : { files: result }),
37
40
  human: () => {
@@ -47,7 +50,7 @@ export async function files(opts) {
47
50
  }
48
51
  export async function folders(opts) {
49
52
  const v = findVault(opts.vault);
50
- const result = listFolders(v.path, opts.folder);
53
+ const result = listFolders(v.contentPath, opts.folder);
51
54
  output(opts, {
52
55
  json: () => (opts.total ? { total: result.length } : { folders: result }),
53
56
  human: () => {
@@ -67,17 +70,17 @@ export async function folder(folderPath, opts) {
67
70
  error("No folder specified. Usage: obsidian-cli folder <path>");
68
71
  process.exit(EXIT_NOT_FOUND);
69
72
  }
70
- const fullPath = path.join(v.path, folderPath);
73
+ const fullPath = path.join(v.contentPath, folderPath);
71
74
  if (!fs.existsSync(fullPath) || !fs.statSync(fullPath).isDirectory()) {
72
75
  error(`Folder not found: ${folderPath}`);
73
76
  process.exit(EXIT_NOT_FOUND);
74
77
  }
75
- const fileCount = listFiles(v.path, { folder: folderPath }).length;
76
- const folderCount = listFolders(v.path, folderPath).length;
78
+ const fileCount = listFiles(v.contentPath, { folder: folderPath }).length;
79
+ const folderCount = listFolders(v.contentPath, folderPath).length;
77
80
  let size = 0;
78
- const allFiles = listFiles(v.path, { folder: folderPath });
81
+ const allFiles = listFiles(v.contentPath, { folder: folderPath });
79
82
  for (const f of allFiles) {
80
- size += fs.statSync(path.join(v.path, f)).size;
83
+ size += fs.statSync(path.join(v.contentPath, f)).size;
81
84
  }
82
85
  if (opts.info) {
83
86
  const val = opts.info === "files"
@@ -111,9 +114,9 @@ export async function open(fileRef, opts) {
111
114
  const vaultName = encodeURIComponent(v.name);
112
115
  let uri;
113
116
  if (fileRef) {
114
- const resolved = resolveFile(v.path, fileRef);
117
+ const resolved = resolveFile(v.contentPath, fileRef);
115
118
  if (!resolved) {
116
- fileNotFound(fileRef, suggestFile(v.path, fileRef));
119
+ fileNotFound(fileRef, suggestFile(v.contentPath, fileRef));
117
120
  process.exit(EXIT_NOT_FOUND);
118
121
  }
119
122
  const encodedFile = encodeURIComponent(resolved.replace(/\.md$/, ""));
@@ -427,13 +427,14 @@ async function openInBrowser(html) {
427
427
  }
428
428
  export async function graph(_args, options) {
429
429
  const { findVault } = await import("../utils/vault.js");
430
- const vault = options.vault ? options.vault : findVault(process.cwd())?.path;
430
+ const vaultInfo = findVault(options.vault || process.cwd());
431
+ const vault = vaultInfo?.contentPath;
431
432
  if (!vault) {
432
433
  error("No vault found. Run napkin init or use --vault <path>");
433
434
  process.exit(1);
434
435
  }
435
436
  const { loadConfig } = await import("../utils/config.js");
436
- const config = loadConfig(vault);
437
+ const config = loadConfig(vaultInfo.configPath);
437
438
  const renderer = config.graph?.renderer ?? "auto";
438
439
  const { nodes, links } = buildGraphData(vault);
439
440
  const graphDataB64 = Buffer.from(JSON.stringify({ nodes, links })).toString("base64");