napkin-ai 0.1.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/LICENSE +21 -0
- package/README.md +194 -0
- package/dist/commands/aliases.d.ts +7 -0
- package/dist/commands/aliases.js +51 -0
- package/dist/commands/bases.d.ts +23 -0
- package/dist/commands/bases.js +192 -0
- package/dist/commands/bookmarks.d.ts +15 -0
- package/dist/commands/bookmarks.js +82 -0
- package/dist/commands/canvas.d.ts +49 -0
- package/dist/commands/canvas.js +264 -0
- package/dist/commands/crud.d.ts +40 -0
- package/dist/commands/crud.js +195 -0
- package/dist/commands/daily.d.ts +21 -0
- package/dist/commands/daily.js +142 -0
- package/dist/commands/files.d.ts +23 -0
- package/dist/commands/files.js +130 -0
- package/dist/commands/links.d.ts +26 -0
- package/dist/commands/links.js +149 -0
- package/dist/commands/onboard.d.ts +2 -0
- package/dist/commands/onboard.js +47 -0
- package/dist/commands/outline.d.ts +7 -0
- package/dist/commands/outline.js +46 -0
- package/dist/commands/properties.d.ts +24 -0
- package/dist/commands/properties.js +134 -0
- package/dist/commands/search.d.ts +11 -0
- package/dist/commands/search.js +85 -0
- package/dist/commands/tags.d.ts +13 -0
- package/dist/commands/tags.js +86 -0
- package/dist/commands/tasks.d.ts +22 -0
- package/dist/commands/tasks.js +164 -0
- package/dist/commands/templates.d.ts +16 -0
- package/dist/commands/templates.js +93 -0
- package/dist/commands/vault.d.ts +4 -0
- package/dist/commands/vault.js +51 -0
- package/dist/commands/wordcount.d.ts +7 -0
- package/dist/commands/wordcount.js +43 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +571 -0
- package/dist/utils/bases.d.ts +61 -0
- package/dist/utils/bases.js +662 -0
- package/dist/utils/exit-codes.d.ts +5 -0
- package/dist/utils/exit-codes.js +5 -0
- package/dist/utils/files.d.ts +37 -0
- package/dist/utils/files.js +127 -0
- package/dist/utils/formula.d.ts +28 -0
- package/dist/utils/formula.js +462 -0
- package/dist/utils/frontmatter.d.ts +17 -0
- package/dist/utils/frontmatter.js +34 -0
- package/dist/utils/markdown.d.ts +31 -0
- package/dist/utils/markdown.js +80 -0
- package/dist/utils/output.d.ts +22 -0
- package/dist/utils/output.js +31 -0
- package/dist/utils/test-helpers.d.ts +7 -0
- package/dist/utils/test-helpers.js +30 -0
- package/dist/utils/vault.d.ts +14 -0
- package/dist/utils/vault.js +38 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Michael Liv
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# napkin
|
|
2
|
+
|
|
3
|
+
🧻 Obsidian-compatible CLI for agents.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g napkin
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or build from source:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
git clone https://github.com/Michaelliv/napkin.git
|
|
15
|
+
cd napkin
|
|
16
|
+
bun install
|
|
17
|
+
bun run build:bun
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
Run commands from inside an Obsidian vault (any directory containing `.obsidian/`):
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
cd ~/my-vault
|
|
26
|
+
napkin vault
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or specify the vault path:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
napkin --vault ~/my-vault vault
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Global Flags
|
|
36
|
+
|
|
37
|
+
| Flag | Description |
|
|
38
|
+
|---|---|
|
|
39
|
+
| `--json` | Output as JSON |
|
|
40
|
+
| `-q, --quiet` | Suppress output |
|
|
41
|
+
| `--vault <path>` | Vault path (default: auto-detect from cwd) |
|
|
42
|
+
| `--copy` | Copy output to clipboard |
|
|
43
|
+
|
|
44
|
+
## Commands
|
|
45
|
+
|
|
46
|
+
### Vault
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
napkin vault # Show vault info (name, path, files, folders, size)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Files & Folders
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
napkin file <name> # Show file info
|
|
56
|
+
napkin files # List all files
|
|
57
|
+
napkin files --ext md # Filter by extension
|
|
58
|
+
napkin files --folder Projects # Filter by folder
|
|
59
|
+
napkin files --total # Count files
|
|
60
|
+
napkin folders # List folders
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### File CRUD
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
napkin read <file> # Read file contents
|
|
67
|
+
napkin create --name "Note" --content "Hello"
|
|
68
|
+
napkin create --path "Projects/new" --template "Meeting Note"
|
|
69
|
+
napkin append --file "Note" --content "More text"
|
|
70
|
+
napkin prepend --file "Note" --content "Top line"
|
|
71
|
+
napkin move --file "Note" --to Archive
|
|
72
|
+
napkin rename --file "Note" --name "Renamed"
|
|
73
|
+
napkin delete --file "Note" # Move to .trash
|
|
74
|
+
napkin delete --file "Note" --permanent
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Daily Notes
|
|
78
|
+
|
|
79
|
+
Reads config from `.obsidian/daily-notes.json` (folder, format, template).
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
napkin daily # Create + open today's daily note
|
|
83
|
+
napkin daily:path # Print daily note path
|
|
84
|
+
napkin daily:read # Print daily note contents
|
|
85
|
+
napkin daily:append --content "- [ ] Buy groceries"
|
|
86
|
+
napkin daily:prepend --content "## Morning"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Search
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
napkin search --query "meeting" # Find files matching text
|
|
93
|
+
napkin search --query "TODO" --path Projects
|
|
94
|
+
napkin search --query "bug" --total # Count matches
|
|
95
|
+
napkin search:context --query "TODO" # Grep-style file:line:text output
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Tasks
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
napkin tasks # List all tasks
|
|
102
|
+
napkin tasks --todo # Incomplete only
|
|
103
|
+
napkin tasks --done # Completed only
|
|
104
|
+
napkin tasks --daily # Today's daily note tasks
|
|
105
|
+
napkin tasks --file "Project A" # Tasks in specific file
|
|
106
|
+
napkin tasks --verbose # Group by file with line numbers
|
|
107
|
+
napkin task --file "note" --line 3 # Show task info
|
|
108
|
+
napkin task --file "note" --line 3 --toggle # Toggle ✓/○
|
|
109
|
+
napkin task --file "note" --line 3 --done # Mark done
|
|
110
|
+
napkin task --ref "note.md:3" --todo # Mark todo
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Tags
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
napkin tags # List all tags
|
|
117
|
+
napkin tags --counts # With occurrence counts
|
|
118
|
+
napkin tags --sort count # Sort by frequency
|
|
119
|
+
napkin tag --name "project" # Tag info
|
|
120
|
+
napkin tag --name "project" --verbose # With file list
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Properties (Frontmatter)
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
napkin properties # List all properties in vault
|
|
127
|
+
napkin properties --file "note" # Properties for a file
|
|
128
|
+
napkin properties --counts # With occurrence counts
|
|
129
|
+
napkin property:read --file "note" --name title
|
|
130
|
+
napkin property:set --file "note" --name status --value done
|
|
131
|
+
napkin property:remove --file "note" --name status
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Links
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
napkin backlinks --file "note" # Files linking to this file
|
|
138
|
+
napkin links --file "note" # Outgoing links from file
|
|
139
|
+
napkin unresolved # Broken links in vault
|
|
140
|
+
napkin orphans # Files with no incoming links
|
|
141
|
+
napkin deadends # Files with no outgoing links
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Outline
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
napkin outline --file "note" # Headings (tree format)
|
|
148
|
+
napkin outline --file "note" --format md
|
|
149
|
+
napkin outline --file "note" --format json
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Templates
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
napkin templates # List templates
|
|
156
|
+
napkin template:read --name "Daily Note"
|
|
157
|
+
napkin template:read --name "Meeting" --resolve --title "Standup"
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Word Count
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
napkin wordcount --file "note" # Words + characters
|
|
164
|
+
napkin wordcount --file "note" --words
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Agent Onboarding
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
napkin onboard # Print agent instructions
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## File Resolution
|
|
174
|
+
|
|
175
|
+
Files can be referenced two ways:
|
|
176
|
+
- **By name** (wikilink-style): `--file "Active Projects"` — searches all `.md` files by basename
|
|
177
|
+
- **By path**: `--file "Projects/Active Projects.md"` — exact path from vault root
|
|
178
|
+
|
|
179
|
+
## For AI Agents
|
|
180
|
+
|
|
181
|
+
Every command supports `--json` for structured output. Run `napkin onboard` to get copy-paste instructions for your agent config.
|
|
182
|
+
|
|
183
|
+
## Development
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
bun install
|
|
187
|
+
bun run dev -- vault --json # Run in dev mode
|
|
188
|
+
bun test # Run tests
|
|
189
|
+
bun run check # Lint + format
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
MIT
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { listFiles, resolveFile } from "../utils/files.js";
|
|
4
|
+
import { parseFrontmatter } from "../utils/frontmatter.js";
|
|
5
|
+
import { dim, output } from "../utils/output.js";
|
|
6
|
+
import { findVault } from "../utils/vault.js";
|
|
7
|
+
function collectAliases(vaultPath, fileFilter) {
|
|
8
|
+
const files = fileFilter
|
|
9
|
+
? (() => {
|
|
10
|
+
const r = resolveFile(vaultPath, fileFilter);
|
|
11
|
+
return r ? [r] : [];
|
|
12
|
+
})()
|
|
13
|
+
: listFiles(vaultPath, { ext: "md" });
|
|
14
|
+
const results = [];
|
|
15
|
+
for (const file of files) {
|
|
16
|
+
const content = fs.readFileSync(path.join(vaultPath, file), "utf-8");
|
|
17
|
+
const { properties } = parseFrontmatter(content);
|
|
18
|
+
const aliases = properties.aliases;
|
|
19
|
+
if (Array.isArray(aliases)) {
|
|
20
|
+
for (const a of aliases)
|
|
21
|
+
results.push({ alias: String(a), file });
|
|
22
|
+
}
|
|
23
|
+
else if (typeof aliases === "string" && aliases) {
|
|
24
|
+
results.push({ alias: aliases, file });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return results;
|
|
28
|
+
}
|
|
29
|
+
export async function aliases(opts) {
|
|
30
|
+
const v = findVault(opts.vault);
|
|
31
|
+
const result = collectAliases(v.path, opts.file);
|
|
32
|
+
output(opts, {
|
|
33
|
+
json: () => {
|
|
34
|
+
if (opts.total)
|
|
35
|
+
return { total: result.length };
|
|
36
|
+
if (opts.verbose)
|
|
37
|
+
return { aliases: result };
|
|
38
|
+
return { aliases: result.map((r) => r.alias) };
|
|
39
|
+
},
|
|
40
|
+
human: () => {
|
|
41
|
+
if (opts.total) {
|
|
42
|
+
console.log(result.length);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
for (const r of result) {
|
|
46
|
+
console.log(opts.verbose ? `${r.alias}\t${dim(r.file)}` : r.alias);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type OutputOptions } from "../utils/output.js";
|
|
2
|
+
export declare function bases(opts: OutputOptions & {
|
|
3
|
+
vault?: string;
|
|
4
|
+
}): Promise<void>;
|
|
5
|
+
export declare function baseViews(opts: OutputOptions & {
|
|
6
|
+
vault?: string;
|
|
7
|
+
file?: string;
|
|
8
|
+
path?: string;
|
|
9
|
+
}): Promise<void>;
|
|
10
|
+
export declare function baseQuery(opts: OutputOptions & {
|
|
11
|
+
vault?: string;
|
|
12
|
+
file?: string;
|
|
13
|
+
path?: string;
|
|
14
|
+
view?: string;
|
|
15
|
+
format?: string;
|
|
16
|
+
}): Promise<void>;
|
|
17
|
+
export declare function baseCreate(opts: OutputOptions & {
|
|
18
|
+
vault?: string;
|
|
19
|
+
file?: string;
|
|
20
|
+
path?: string;
|
|
21
|
+
name?: string;
|
|
22
|
+
content?: string;
|
|
23
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { buildDatabase, parseBaseFile, queryBase } from "../utils/bases.js";
|
|
4
|
+
import { EXIT_USER_ERROR } from "../utils/exit-codes.js";
|
|
5
|
+
import { listFiles } from "../utils/files.js";
|
|
6
|
+
import { bold, dim, error, output, success, } from "../utils/output.js";
|
|
7
|
+
import { findVault } from "../utils/vault.js";
|
|
8
|
+
export async function bases(opts) {
|
|
9
|
+
const v = findVault(opts.vault);
|
|
10
|
+
const files = listFiles(v.path).filter((f) => f.endsWith(".base"));
|
|
11
|
+
output(opts, {
|
|
12
|
+
json: () => ({ bases: files }),
|
|
13
|
+
human: () => {
|
|
14
|
+
if (files.length === 0) {
|
|
15
|
+
console.log("No .base files found");
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
for (const f of files)
|
|
19
|
+
console.log(f);
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export async function baseViews(opts) {
|
|
25
|
+
const v = findVault(opts.vault);
|
|
26
|
+
const baseFile = resolveBaseFile(v.path, opts);
|
|
27
|
+
if (!baseFile) {
|
|
28
|
+
error("No base file specified. Use --file or --path");
|
|
29
|
+
process.exit(EXIT_USER_ERROR);
|
|
30
|
+
}
|
|
31
|
+
const content = fs.readFileSync(path.join(v.path, baseFile), "utf-8");
|
|
32
|
+
const config = parseBaseFile(content);
|
|
33
|
+
const views = (config.views || []).map((view) => ({
|
|
34
|
+
name: view.name || "(unnamed)",
|
|
35
|
+
type: view.type,
|
|
36
|
+
}));
|
|
37
|
+
output(opts, {
|
|
38
|
+
json: () => ({ views }),
|
|
39
|
+
human: () => {
|
|
40
|
+
for (const view of views) {
|
|
41
|
+
console.log(`${bold(view.name)} ${dim(view.type)}`);
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
export async function baseQuery(opts) {
|
|
47
|
+
const v = findVault(opts.vault);
|
|
48
|
+
const baseFile = resolveBaseFile(v.path, opts);
|
|
49
|
+
if (!baseFile) {
|
|
50
|
+
error("No base file specified. Use --file or --path");
|
|
51
|
+
process.exit(EXIT_USER_ERROR);
|
|
52
|
+
}
|
|
53
|
+
const content = fs.readFileSync(path.join(v.path, baseFile), "utf-8");
|
|
54
|
+
const config = parseBaseFile(content);
|
|
55
|
+
const db = await buildDatabase(v.path);
|
|
56
|
+
try {
|
|
57
|
+
// Derive thisFile from the base file path
|
|
58
|
+
const thisFile = baseFile
|
|
59
|
+
? {
|
|
60
|
+
name: path.basename(baseFile),
|
|
61
|
+
path: baseFile,
|
|
62
|
+
folder: path.dirname(baseFile),
|
|
63
|
+
}
|
|
64
|
+
: undefined;
|
|
65
|
+
const result = await queryBase(db, config, opts.view, thisFile);
|
|
66
|
+
const fmt = opts.format || "json";
|
|
67
|
+
// Apply displayNames to columns for output
|
|
68
|
+
const displayCols = result.columns.map((c) => result.displayNames?.[c] || c);
|
|
69
|
+
output(opts, {
|
|
70
|
+
json: () => {
|
|
71
|
+
if (fmt === "paths") {
|
|
72
|
+
const pathIdx = result.columns.indexOf("path");
|
|
73
|
+
return { paths: result.rows.map((r) => r[pathIdx]) };
|
|
74
|
+
}
|
|
75
|
+
// Convert to array of objects
|
|
76
|
+
const rows = result.rows.map((row) => {
|
|
77
|
+
const obj = {};
|
|
78
|
+
for (let i = 0; i < result.columns.length; i++) {
|
|
79
|
+
obj[result.columns[i]] = row[i];
|
|
80
|
+
}
|
|
81
|
+
return obj;
|
|
82
|
+
});
|
|
83
|
+
const out = { columns: result.columns, rows };
|
|
84
|
+
if (result.displayNames &&
|
|
85
|
+
Object.keys(result.displayNames).length > 0) {
|
|
86
|
+
out.displayNames = result.displayNames;
|
|
87
|
+
}
|
|
88
|
+
if (result.groups) {
|
|
89
|
+
out.groups = result.groups.map((g) => ({
|
|
90
|
+
key: g.key,
|
|
91
|
+
rows: g.rows.map((row) => {
|
|
92
|
+
const obj = {};
|
|
93
|
+
for (let i = 0; i < result.columns.length; i++) {
|
|
94
|
+
obj[result.columns[i]] = row[i];
|
|
95
|
+
}
|
|
96
|
+
return obj;
|
|
97
|
+
}),
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
if (result.summaries)
|
|
101
|
+
out.summaries = result.summaries;
|
|
102
|
+
return out;
|
|
103
|
+
},
|
|
104
|
+
human: () => {
|
|
105
|
+
if (result.rows.length === 0) {
|
|
106
|
+
console.log("No results");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (fmt === "paths") {
|
|
110
|
+
const pathIdx = result.columns.indexOf("path");
|
|
111
|
+
for (const row of result.rows)
|
|
112
|
+
console.log(row[pathIdx]);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (fmt === "csv" || fmt === "tsv") {
|
|
116
|
+
const sep = fmt === "csv" ? "," : "\t";
|
|
117
|
+
console.log(displayCols.join(sep));
|
|
118
|
+
for (const row of result.rows) {
|
|
119
|
+
console.log(row.map((v) => (v === null ? "" : String(v))).join(sep));
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (fmt === "md") {
|
|
124
|
+
console.log(`| ${displayCols.join(" | ")} |`);
|
|
125
|
+
console.log(`| ${displayCols.map(() => "---").join(" | ")} |`);
|
|
126
|
+
for (const row of result.rows) {
|
|
127
|
+
console.log(`| ${row.map((v) => (v === null ? "" : String(v))).join(" | ")} |`);
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
// Default: table-like
|
|
132
|
+
for (const row of result.rows) {
|
|
133
|
+
const obj = {};
|
|
134
|
+
for (let i = 0; i < result.columns.length; i++) {
|
|
135
|
+
if (row[i] !== null)
|
|
136
|
+
obj[result.columns[i]] = row[i];
|
|
137
|
+
}
|
|
138
|
+
console.log(JSON.stringify(obj));
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
db.close();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
export async function baseCreate(opts) {
|
|
148
|
+
const v = findVault(opts.vault);
|
|
149
|
+
if (!opts.name) {
|
|
150
|
+
error("No name specified. Use --name <name>");
|
|
151
|
+
process.exit(EXIT_USER_ERROR);
|
|
152
|
+
}
|
|
153
|
+
// Create a new note (item in the base)
|
|
154
|
+
const targetPath = opts.path
|
|
155
|
+
? opts.path.endsWith(".md")
|
|
156
|
+
? opts.path
|
|
157
|
+
: `${opts.path}/${opts.name}.md`
|
|
158
|
+
: `${opts.name}.md`;
|
|
159
|
+
const fullPath = path.join(v.path, targetPath);
|
|
160
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
161
|
+
fs.writeFileSync(fullPath, opts.content || "");
|
|
162
|
+
output(opts, {
|
|
163
|
+
json: () => ({ path: targetPath, created: true }),
|
|
164
|
+
human: () => success(`Created ${targetPath}`),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
function resolveBaseFile(vaultPath, opts) {
|
|
168
|
+
if (opts.path) {
|
|
169
|
+
const p = opts.path.endsWith(".base") ? opts.path : `${opts.path}.base`;
|
|
170
|
+
if (fs.existsSync(path.join(vaultPath, p)))
|
|
171
|
+
return p;
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
if (opts.file) {
|
|
175
|
+
// Search for .base file by name
|
|
176
|
+
const allFiles = listFiles(vaultPath).filter((f) => f.endsWith(".base"));
|
|
177
|
+
const target = opts.file.toLowerCase();
|
|
178
|
+
for (const f of allFiles) {
|
|
179
|
+
const basename = path.basename(f, ".base").toLowerCase();
|
|
180
|
+
if (basename === target)
|
|
181
|
+
return f;
|
|
182
|
+
}
|
|
183
|
+
// Try with .base extension
|
|
184
|
+
const withExt = opts.file.endsWith(".base")
|
|
185
|
+
? opts.file
|
|
186
|
+
: `${opts.file}.base`;
|
|
187
|
+
if (fs.existsSync(path.join(vaultPath, withExt)))
|
|
188
|
+
return withExt;
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type OutputOptions } from "../utils/output.js";
|
|
2
|
+
export declare function bookmarks(opts: OutputOptions & {
|
|
3
|
+
vault?: string;
|
|
4
|
+
total?: boolean;
|
|
5
|
+
verbose?: boolean;
|
|
6
|
+
}): Promise<void>;
|
|
7
|
+
export declare function bookmark(opts: OutputOptions & {
|
|
8
|
+
vault?: string;
|
|
9
|
+
file?: string;
|
|
10
|
+
subpath?: string;
|
|
11
|
+
folder?: string;
|
|
12
|
+
search?: string;
|
|
13
|
+
url?: string;
|
|
14
|
+
title?: string;
|
|
15
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { EXIT_USER_ERROR } from "../utils/exit-codes.js";
|
|
4
|
+
import { dim, error, output, success, } from "../utils/output.js";
|
|
5
|
+
import { findVault } from "../utils/vault.js";
|
|
6
|
+
function readBookmarks(vaultPath) {
|
|
7
|
+
const configPath = path.join(vaultPath, ".obsidian", "bookmarks.json");
|
|
8
|
+
try {
|
|
9
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
10
|
+
return JSON.parse(content);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function writeBookmarks(vaultPath, bookmarks) {
|
|
17
|
+
const configPath = path.join(vaultPath, ".obsidian", "bookmarks.json");
|
|
18
|
+
fs.writeFileSync(configPath, JSON.stringify(bookmarks, null, 2));
|
|
19
|
+
}
|
|
20
|
+
function flattenBookmarks(items) {
|
|
21
|
+
const result = [];
|
|
22
|
+
for (const item of items) {
|
|
23
|
+
if (item.type === "group" && item.items) {
|
|
24
|
+
result.push(...flattenBookmarks(item.items));
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
result.push(item);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
export async function bookmarks(opts) {
|
|
33
|
+
const v = findVault(opts.vault);
|
|
34
|
+
const items = readBookmarks(v.path);
|
|
35
|
+
const flat = flattenBookmarks(items);
|
|
36
|
+
output(opts, {
|
|
37
|
+
json: () => (opts.total ? { total: flat.length } : { bookmarks: flat }),
|
|
38
|
+
human: () => {
|
|
39
|
+
if (opts.total) {
|
|
40
|
+
console.log(flat.length);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
for (const b of flat) {
|
|
44
|
+
const label = b.title || b.path || b.query || b.url || "(untitled)";
|
|
45
|
+
console.log(opts.verbose ? `${label}\t${dim(b.type)}` : label);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
export async function bookmark(opts) {
|
|
52
|
+
const v = findVault(opts.vault);
|
|
53
|
+
let entry;
|
|
54
|
+
if (opts.file) {
|
|
55
|
+
entry = {
|
|
56
|
+
type: "file",
|
|
57
|
+
path: opts.file,
|
|
58
|
+
title: opts.title,
|
|
59
|
+
subpath: opts.subpath,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
else if (opts.folder) {
|
|
63
|
+
entry = { type: "folder", path: opts.folder, title: opts.title };
|
|
64
|
+
}
|
|
65
|
+
else if (opts.search) {
|
|
66
|
+
entry = { type: "search", query: opts.search, title: opts.title };
|
|
67
|
+
}
|
|
68
|
+
else if (opts.url) {
|
|
69
|
+
entry = { type: "url", url: opts.url, title: opts.title };
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
error("Specify --file, --folder, --search, or --url to bookmark");
|
|
73
|
+
process.exit(EXIT_USER_ERROR);
|
|
74
|
+
}
|
|
75
|
+
const items = readBookmarks(v.path);
|
|
76
|
+
items.push(entry);
|
|
77
|
+
writeBookmarks(v.path, items);
|
|
78
|
+
output(opts, {
|
|
79
|
+
json: () => ({ added: entry }),
|
|
80
|
+
human: () => success(`Bookmarked ${entry.path || entry.query || entry.url}`),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type OutputOptions } from "../utils/output.js";
|
|
2
|
+
export declare function canvases(opts: OutputOptions & {
|
|
3
|
+
vault?: string;
|
|
4
|
+
total?: boolean;
|
|
5
|
+
}): Promise<void>;
|
|
6
|
+
export declare function canvasRead(opts: OutputOptions & {
|
|
7
|
+
vault?: string;
|
|
8
|
+
file?: string;
|
|
9
|
+
}): Promise<void>;
|
|
10
|
+
export declare function canvasNodes(opts: OutputOptions & {
|
|
11
|
+
vault?: string;
|
|
12
|
+
file?: string;
|
|
13
|
+
type?: string;
|
|
14
|
+
}): Promise<void>;
|
|
15
|
+
export declare function canvasCreate(opts: OutputOptions & {
|
|
16
|
+
vault?: string;
|
|
17
|
+
file?: string;
|
|
18
|
+
path?: string;
|
|
19
|
+
}): Promise<void>;
|
|
20
|
+
export declare function canvasAddNode(opts: OutputOptions & {
|
|
21
|
+
vault?: string;
|
|
22
|
+
file?: string;
|
|
23
|
+
type?: string;
|
|
24
|
+
text?: string;
|
|
25
|
+
noteFile?: string;
|
|
26
|
+
subpath?: string;
|
|
27
|
+
url?: string;
|
|
28
|
+
label?: string;
|
|
29
|
+
x?: string;
|
|
30
|
+
y?: string;
|
|
31
|
+
width?: string;
|
|
32
|
+
height?: string;
|
|
33
|
+
color?: string;
|
|
34
|
+
}): Promise<void>;
|
|
35
|
+
export declare function canvasAddEdge(opts: OutputOptions & {
|
|
36
|
+
vault?: string;
|
|
37
|
+
file?: string;
|
|
38
|
+
from?: string;
|
|
39
|
+
to?: string;
|
|
40
|
+
fromSide?: string;
|
|
41
|
+
toSide?: string;
|
|
42
|
+
label?: string;
|
|
43
|
+
color?: string;
|
|
44
|
+
}): Promise<void>;
|
|
45
|
+
export declare function canvasRemoveNode(opts: OutputOptions & {
|
|
46
|
+
vault?: string;
|
|
47
|
+
file?: string;
|
|
48
|
+
id?: string;
|
|
49
|
+
}): Promise<void>;
|