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 +25 -10
- package/dist/commands/aliases.js +1 -1
- package/dist/commands/bases.js +7 -7
- package/dist/commands/bookmarks.js +7 -7
- package/dist/commands/canvas.js +11 -11
- package/dist/commands/config.js +3 -3
- package/dist/commands/crud.js +47 -31
- package/dist/commands/daily.js +11 -11
- package/dist/commands/files.js +15 -12
- package/dist/commands/graph.js +3 -2
- package/dist/commands/init.js +24 -7
- package/dist/commands/links.js +9 -9
- package/dist/commands/outline.js +3 -3
- package/dist/commands/overview.js +3 -3
- package/dist/commands/properties.js +10 -10
- package/dist/commands/search.js +40 -3
- package/dist/commands/tags.js +2 -2
- package/dist/commands/tasks.js +17 -14
- package/dist/commands/templates.js +15 -15
- package/dist/commands/vault.js +8 -6
- package/dist/commands/wordcount.js +3 -3
- package/dist/main.js +56 -39
- package/dist/utils/config.d.ts +13 -5
- package/dist/utils/config.js +16 -12
- package/dist/utils/files.js +3 -1
- package/dist/utils/search-cache.d.ts +29 -0
- package/dist/utils/search-cache.js +41 -0
- package/dist/utils/vault.d.ts +10 -5
- package/dist/utils/vault.js +46 -10
- package/package.json +1 -1
- package/skills/napkin/SKILL.md +27 -19
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
|
|
98
|
-
napkin append
|
|
99
|
-
napkin prepend
|
|
100
|
-
napkin move
|
|
101
|
-
napkin rename
|
|
102
|
-
napkin delete
|
|
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
|
|
117
|
-
napkin file wordcount
|
|
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
|
|
127
|
-
napkin daily prepend
|
|
141
|
+
napkin daily append "- [ ] Buy groceries"
|
|
142
|
+
napkin daily prepend "## Morning"
|
|
128
143
|
```
|
|
129
144
|
|
|
130
145
|
### Tags — `napkin tag`
|
package/dist/commands/aliases.js
CHANGED
|
@@ -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.
|
|
31
|
+
const result = collectAliases(v.contentPath, opts.file);
|
|
32
32
|
output(opts, {
|
|
33
33
|
json: () => {
|
|
34
34
|
if (opts.total)
|
package/dist/commands/bases.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
53
|
+
const content = fs.readFileSync(path.join(v.contentPath, baseFile), "utf-8");
|
|
54
54
|
const config = parseBaseFile(content);
|
|
55
|
-
const db = await buildDatabase(v.
|
|
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.
|
|
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(
|
|
7
|
-
const configPath = path.join(
|
|
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(
|
|
17
|
-
const configPath = path.join(
|
|
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.
|
|
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.
|
|
75
|
+
const items = readBookmarks(v.obsidianPath);
|
|
76
76
|
items.push(entry);
|
|
77
|
-
writeBookmarks(v.
|
|
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}`),
|
package/dist/commands/canvas.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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}`),
|
package/dist/commands/config.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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) {
|
package/dist/commands/crud.js
CHANGED
|
@@ -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.
|
|
15
|
+
const resolved = resolveFile(v.contentPath, fileRef);
|
|
16
16
|
if (!resolved) {
|
|
17
|
-
fileNotFound(fileRef, suggestFile(v.
|
|
17
|
+
fileNotFound(fileRef, suggestFile(v.contentPath, fileRef));
|
|
18
18
|
process.exit(EXIT_NOT_FOUND);
|
|
19
19
|
}
|
|
20
|
-
const content = fs.readFileSync(path.join(v.
|
|
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.
|
|
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.
|
|
44
|
-
const templateRef = resolveFile(v.
|
|
45
|
-
resolveFile(v.
|
|
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.
|
|
47
|
+
content = fs.readFileSync(path.join(v.contentPath, templateRef), "utf-8");
|
|
48
48
|
}
|
|
49
49
|
else {
|
|
50
|
-
const tmplFiles = listFiles(v.
|
|
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
|
|
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
|
-
|
|
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.
|
|
88
|
+
const resolved = resolveFile(v.contentPath, opts.file);
|
|
76
89
|
if (!resolved) {
|
|
77
|
-
fileNotFound(opts.file, suggestFile(v.
|
|
90
|
+
fileNotFound(opts.file, suggestFile(v.contentPath, opts.file));
|
|
78
91
|
process.exit(EXIT_NOT_FOUND);
|
|
79
92
|
}
|
|
80
|
-
const fullPath = path.join(v.
|
|
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
|
|
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
|
-
|
|
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.
|
|
115
|
+
const resolved = resolveFile(v.contentPath, opts.file);
|
|
100
116
|
if (!resolved) {
|
|
101
|
-
fileNotFound(opts.file, suggestFile(v.
|
|
117
|
+
fileNotFound(opts.file, suggestFile(v.contentPath, opts.file));
|
|
102
118
|
process.exit(EXIT_NOT_FOUND);
|
|
103
119
|
}
|
|
104
|
-
const fullPath = path.join(v.
|
|
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.
|
|
147
|
+
const resolved = resolveFile(v.contentPath, opts.file);
|
|
132
148
|
if (!resolved) {
|
|
133
|
-
fileNotFound(opts.file, suggestFile(v.
|
|
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.
|
|
142
|
-
const destFull = path.join(v.
|
|
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.
|
|
176
|
+
const resolved = resolveFile(v.contentPath, opts.file);
|
|
161
177
|
if (!resolved) {
|
|
162
|
-
fileNotFound(opts.file, suggestFile(v.
|
|
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.
|
|
168
|
-
const destFull = path.join(v.
|
|
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.
|
|
197
|
+
const resolved = resolveFile(v.contentPath, opts.file);
|
|
182
198
|
if (!resolved) {
|
|
183
|
-
fileNotFound(opts.file, suggestFile(v.
|
|
199
|
+
fileNotFound(opts.file, suggestFile(v.contentPath, opts.file));
|
|
184
200
|
process.exit(EXIT_NOT_FOUND);
|
|
185
201
|
}
|
|
186
|
-
const fullPath = path.join(v.
|
|
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.
|
|
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);
|
package/dist/commands/daily.js
CHANGED
|
@@ -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.
|
|
55
|
-
const fullPath = path.join(v.
|
|
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.
|
|
59
|
+
const config = getDailyConfig(v.configPath);
|
|
60
60
|
let content = "";
|
|
61
61
|
if (config.template) {
|
|
62
|
-
const templatePath = path.join(v.
|
|
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.
|
|
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.
|
|
85
|
-
const fullPath = path.join(v.
|
|
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.
|
|
103
|
-
const fullPath = path.join(v.
|
|
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.
|
|
124
|
-
const fullPath = path.join(v.
|
|
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, "");
|
package/dist/commands/files.js
CHANGED
|
@@ -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.
|
|
14
|
+
const resolved = resolveFile(v.contentPath, fileRef);
|
|
15
15
|
if (!resolved) {
|
|
16
|
-
fileNotFound(fileRef, suggestFile(v.
|
|
16
|
+
fileNotFound(fileRef, suggestFile(v.contentPath, fileRef));
|
|
17
17
|
process.exit(EXIT_NOT_FOUND);
|
|
18
18
|
}
|
|
19
|
-
const info = getFileInfo(v.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
76
|
-
const folderCount = listFolders(v.
|
|
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.
|
|
81
|
+
const allFiles = listFiles(v.contentPath, { folder: folderPath });
|
|
79
82
|
for (const f of allFiles) {
|
|
80
|
-
size += fs.statSync(path.join(v.
|
|
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.
|
|
117
|
+
const resolved = resolveFile(v.contentPath, fileRef);
|
|
115
118
|
if (!resolved) {
|
|
116
|
-
fileNotFound(fileRef, suggestFile(v.
|
|
119
|
+
fileNotFound(fileRef, suggestFile(v.contentPath, fileRef));
|
|
117
120
|
process.exit(EXIT_NOT_FOUND);
|
|
118
121
|
}
|
|
119
122
|
const encodedFile = encodeURIComponent(resolved.replace(/\.md$/, ""));
|
package/dist/commands/graph.js
CHANGED
|
@@ -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
|
|
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(
|
|
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");
|