obsidian-vault-cli 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. obsidian_vault_cli-0.2.0/PKG-INFO +586 -0
  2. obsidian_vault_cli-0.2.0/SPEC.md +560 -0
  3. obsidian_vault_cli-0.2.0/obsidian_vault_cli.egg-info/PKG-INFO +586 -0
  4. obsidian_vault_cli-0.2.0/obsidian_vault_cli.egg-info/SOURCES.txt +36 -0
  5. obsidian_vault_cli-0.2.0/obsidian_vault_cli.egg-info/dependency_links.txt +1 -0
  6. obsidian_vault_cli-0.2.0/obsidian_vault_cli.egg-info/entry_points.txt +2 -0
  7. obsidian_vault_cli-0.2.0/obsidian_vault_cli.egg-info/requires.txt +8 -0
  8. obsidian_vault_cli-0.2.0/obsidian_vault_cli.egg-info/top_level.txt +1 -0
  9. obsidian_vault_cli-0.2.0/pyproject.toml +47 -0
  10. obsidian_vault_cli-0.2.0/setup.cfg +4 -0
  11. obsidian_vault_cli-0.2.0/tests/test_cli.py +795 -0
  12. obsidian_vault_cli-0.2.0/tests/test_client.py +1123 -0
  13. obsidian_vault_cli-0.2.0/tests/test_config.py +286 -0
  14. obsidian_vault_cli-0.2.0/tests/test_frontmatter.py +317 -0
  15. obsidian_vault_cli-0.2.0/tests/test_index.py +432 -0
  16. obsidian_vault_cli-0.2.0/tests/test_safety.py +805 -0
  17. obsidian_vault_cli-0.2.0/tests/test_wikilinks.py +182 -0
  18. obsidian_vault_cli-0.2.0/vault_cli/__init__.py +3 -0
  19. obsidian_vault_cli-0.2.0/vault_cli/__main__.py +5 -0
  20. obsidian_vault_cli-0.2.0/vault_cli/cli/__init__.py +1 -0
  21. obsidian_vault_cli-0.2.0/vault_cli/cli/config_cmd.py +115 -0
  22. obsidian_vault_cli-0.2.0/vault_cli/cli/crud.py +385 -0
  23. obsidian_vault_cli-0.2.0/vault_cli/cli/errors.py +65 -0
  24. obsidian_vault_cli-0.2.0/vault_cli/cli/files.py +62 -0
  25. obsidian_vault_cli-0.2.0/vault_cli/cli/graph.py +101 -0
  26. obsidian_vault_cli-0.2.0/vault_cli/cli/helpers.py +91 -0
  27. obsidian_vault_cli-0.2.0/vault_cli/cli/main.py +78 -0
  28. obsidian_vault_cli-0.2.0/vault_cli/cli/properties.py +166 -0
  29. obsidian_vault_cli-0.2.0/vault_cli/cli/search.py +64 -0
  30. obsidian_vault_cli-0.2.0/vault_cli/cli/tags.py +70 -0
  31. obsidian_vault_cli-0.2.0/vault_cli/cli/templates.py +56 -0
  32. obsidian_vault_cli-0.2.0/vault_cli/core/__init__.py +1 -0
  33. obsidian_vault_cli-0.2.0/vault_cli/core/client.py +247 -0
  34. obsidian_vault_cli-0.2.0/vault_cli/core/config.py +107 -0
  35. obsidian_vault_cli-0.2.0/vault_cli/core/frontmatter.py +59 -0
  36. obsidian_vault_cli-0.2.0/vault_cli/core/index.py +158 -0
  37. obsidian_vault_cli-0.2.0/vault_cli/core/output.py +25 -0
  38. obsidian_vault_cli-0.2.0/vault_cli/core/wikilinks.py +41 -0
@@ -0,0 +1,586 @@
1
+ Metadata-Version: 2.4
2
+ Name: obsidian-vault-cli
3
+ Version: 0.2.0
4
+ Summary: Obsidian vault CLI via CouchDB/LiveSync — read, write, search, graph traversal, tags, properties
5
+ License-Expression: MIT
6
+ Project-URL: Repository, https://github.com/HarKro753/obsctl
7
+ Project-URL: Documentation, https://github.com/HarKro753/obsctl/tree/main/packages/cli
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Software Development :: Libraries
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: requests>=2.28.0
20
+ Requires-Dist: python-frontmatter>=1.0.0
21
+ Requires-Dist: click>=8.0.0
22
+ Requires-Dist: PyYAML>=6.0
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=7.0; extra == "dev"
25
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
26
+
27
+ # obsidian-vault-cli — Product Spec v2.0
28
+
29
+ **Status:** Draft
30
+ **Date:** 2026-03-03
31
+ **Authors:** Harro Krog
32
+
33
+ ---
34
+
35
+ ## Vision
36
+
37
+ A Python CLI that gives agents (and humans) full Obsidian CLI-equivalent access to an Obsidian vault synced via Self-hosted LiveSync (CouchDB) — **without requiring Obsidian to be installed or running**.
38
+
39
+ The Obsidian CLI (v1.12+) requires a running Obsidian desktop app with a Catalyst license. This tool replaces it entirely by talking directly to the CouchDB database that LiveSync uses for sync.
40
+
41
+ **No AI. No curation. No inbox processing.** This is a pure vault access tool: read, write, search, traverse the knowledge graph, manage properties and tags. The kind of foundation an agent needs to operate on a vault as a knowledge graph.
42
+
43
+ ---
44
+
45
+ ## Problem
46
+
47
+ When an AI agent (OpenClaw, Claude Code, etc.) needs to interact with an Obsidian vault synced via LiveSync, it currently has to:
48
+
49
+ 1. **Read notes** via raw `curl` calls with URL-encoded paths and HTTP basic auth
50
+ 2. **Write notes** by generating temporary `.mjs` scripts, importing a Node.js VaultClient class as an ESM module, running them with `node`, then verifying via separate `curl` calls
51
+ 3. **Search** — not possible (only path matching via `_all_docs`)
52
+ 4. **Backlinks / links / tags / properties** — not possible at all
53
+
54
+ This tool replaces all of that with simple CLI commands:
55
+
56
+ ```bash
57
+ vault read file="north star"
58
+ vault create name="New Idea" content="..." folder="References"
59
+ vault search query="agent loop" --context
60
+ vault backlinks file="closedclaw"
61
+ vault tags --counts --sort=count
62
+ vault property:set name="status" value="active" file="self-hosting"
63
+ vault links file="elevenstoic"
64
+ vault templates
65
+ ```
66
+
67
+ ---
68
+
69
+ ## Package Identity
70
+
71
+ - **Language:** Python 3.10+
72
+ - **CLI name:** `vault` (via `pyproject.toml` entry point)
73
+ - **Package:** `obsidian-vault-cli`
74
+ - **Repository:** `github.com/HarKro753/obsidian-curator-real` (to be renamed)
75
+ - **License:** MIT
76
+
77
+ ---
78
+
79
+ ## Architecture
80
+
81
+ Two layers:
82
+
83
+ ```
84
+ ┌──────────────────────────────────────────────────┐
85
+ │ CLI │ vault read / write / search / backlinks / tags
86
+ │ │ Mirrors Obsidian CLI syntax (key=value params)
87
+ ├──────────────────────────────────────────────────┤
88
+ │ VaultClient (Python) │ CouchDB CRUD, LiveSync document format,
89
+ │ │ in-memory graph index, frontmatter parsing
90
+ └──────────────────────────────────────────────────┘
91
+
92
+
93
+ CouchDB / LiveSync
94
+ ```
95
+
96
+ ### VaultClient (Core Library)
97
+
98
+ The programmatic API. Handles all CouchDB interaction and LiveSync document format details.
99
+
100
+ ```python
101
+ from vault_cli import VaultClient
102
+
103
+ client = VaultClient(
104
+ host="obsidian.harrokrog.com",
105
+ port=443,
106
+ database="obsidian",
107
+ username="admin",
108
+ password="changeme",
109
+ protocol="https"
110
+ )
111
+
112
+ # CRUD
113
+ note = client.read_note("north star.md")
114
+ client.write_note("References/New Person.md", content)
115
+ client.delete_note("old-note.md")
116
+ client.move_note("draft.md", "References/final.md")
117
+
118
+ # Search
119
+ results = client.search_content("agent loop")
120
+ results = client.search_content("agent loop", path="References")
121
+
122
+ # Graph
123
+ backlinks = client.get_backlinks("closedclaw.md")
124
+ links = client.get_links("closedclaw.md")
125
+ orphans = client.get_orphans()
126
+ unresolved = client.get_unresolved()
127
+
128
+ # Tags & Properties
129
+ tags = client.get_all_tags()
130
+ props = client.get_properties("north star.md")
131
+ client.set_property("north star.md", "status", "active")
132
+ ```
133
+
134
+ ### CLI
135
+
136
+ Wraps the VaultClient for terminal use. Mirrors Obsidian CLI syntax where possible.
137
+
138
+ ```bash
139
+ vault read file="north star"
140
+ vault create name="New Idea" content="# Idea" folder="References"
141
+ vault write path="References/Person.md" content="---\n..."
142
+ vault append file="north star" content="\n## New section"
143
+ vault prepend file="north star" content="**Updated 2026-03-03**"
144
+ vault delete file="old note"
145
+ vault move file="draft" to="References/final.md"
146
+ vault rename file="draft" name="Final Version"
147
+
148
+ vault files
149
+ vault files folder="References"
150
+ vault files ext=md
151
+ vault files --total
152
+ vault folders
153
+
154
+ vault search query="agent loop"
155
+ vault search query="TODO" path="References" limit=5
156
+ vault search query="agent" --context
157
+ vault search query="agent" --total
158
+
159
+ vault backlinks file="closedclaw"
160
+ vault backlinks file="closedclaw" --counts
161
+ vault links file="closedclaw"
162
+ vault unresolved
163
+ vault orphans
164
+
165
+ vault tags
166
+ vault tags --counts
167
+ vault tags --sort=count
168
+ vault tag name="ai" --verbose
169
+
170
+ vault properties file="north star"
171
+ vault property:read name="status" file="north star"
172
+ vault property:set name="status" value="active" file="north star"
173
+ vault property:set name="tags" value="ai" type=list file="north star"
174
+ vault property:remove name="status" file="north star"
175
+
176
+ vault templates
177
+ vault template:read name="people template"
178
+
179
+ vault ping # test CouchDB connectivity
180
+ vault config show # show current config
181
+ vault config set vault.host obsidian.harrokrog.com
182
+ ```
183
+
184
+ ---
185
+
186
+ ## Configuration
187
+
188
+ Config file at `~/.vault-cli/config.json` (global) or `.vault-cli.json` (project-local). Local overrides global.
189
+
190
+ ```json
191
+ {
192
+ "vault": {
193
+ "host": "localhost",
194
+ "port": 5984,
195
+ "database": "obsidian",
196
+ "username": "",
197
+ "password": "",
198
+ "protocol": "http"
199
+ },
200
+ "templates_folder": "Templates",
201
+ "output_format": "text"
202
+ }
203
+ ```
204
+
205
+ That's it. No AI config, no structure presets, no tidy rules. Just the CouchDB connection and a couple of preferences.
206
+
207
+ ### Environment Variable Override
208
+
209
+ Every config value can be set via env var:
210
+
211
+ ```bash
212
+ export VAULT_HOST=obsidian.harrokrog.com
213
+ export VAULT_PORT=443
214
+ export VAULT_DATABASE=obsidian
215
+ export VAULT_USERNAME=admin
216
+ export VAULT_PASSWORD=changeme
217
+ export VAULT_PROTOCOL=https
218
+ ```
219
+
220
+ Env vars take precedence over config file.
221
+
222
+ ---
223
+
224
+ ## LiveSync Document Format
225
+
226
+ LiveSync stores notes in CouchDB as two document types:
227
+
228
+ ### Metadata Document
229
+
230
+ `_id` = note path (lowercased). Contains note metadata and references to content chunks.
231
+
232
+ ```json
233
+ {
234
+ "_id": "references/some person.md",
235
+ "_rev": "1-abc123",
236
+ "path": "References/Some Person.md",
237
+ "children": ["h:a1b2c3d4e5f6"],
238
+ "ctime": 1709500000000,
239
+ "mtime": 1709500000000,
240
+ "size": 1234,
241
+ "type": "plain",
242
+ "eden": {}
243
+ }
244
+ ```
245
+
246
+ ### Leaf/Chunk Document
247
+
248
+ `_id` = `h:` + first 12 chars of SHA-256 hash of content. Contains the actual note content. Content-addressed and shared across notes.
249
+
250
+ ```json
251
+ {
252
+ "_id": "h:a1b2c3d4e5f6",
253
+ "type": "leaf",
254
+ "data": "---\ncategories:\n - \"[[People]]\"\n---\n\n# Some Person\n..."
255
+ }
256
+ ```
257
+
258
+ Large notes are split into multiple chunks (50KB each). The metadata document's `children` array lists them in order.
259
+
260
+ ### Deletion
261
+
262
+ Soft-delete: set `deleted: true`, clear `children`, clear `data`. Do NOT use CouchDB `db.destroy()`.
263
+
264
+ ### ⚠️ Known CouchDB/LiveSync Footgun
265
+
266
+ If a parent doc has `deleted: true` and a write comes in without removing that flag, LiveSync treats it as deleted. Always check for `deleted: true` on any doc before writing to it and strip the flag explicitly.
267
+
268
+ ---
269
+
270
+ ## Safety & Error Handling (v0.2.0)
271
+
272
+ Agent-operated tools must fail loudly and prevent data loss by default. These rules apply to all destructive commands.
273
+
274
+ ### Write safety (`vault write`)
275
+
276
+ `vault write` **will not silently overwrite** an existing note without explicit opt-in.
277
+
278
+ **Default behaviour (no flags):**
279
+ ```
280
+ $ vault write --path "References/Dijkstra Algorithm.md" --content "..."
281
+ Note already exists (2,341 chars, last modified 2026-03-03).
282
+ Use --force to overwrite, or --diff to preview changes.
283
+ Aborted.
284
+ ```
285
+
286
+ **`--force` flag:** Skip the guard, overwrite unconditionally. Use in scripts.
287
+
288
+ **`--diff` flag:** Print a unified diff of what would change, then exit. No write.
289
+
290
+ **`deleted: true` detection:** Before writing, if the existing doc has `deleted: true`, print:
291
+ ```
292
+ Warning: note exists but is marked deleted in CouchDB.
293
+ Writing will restore it. Continue? [y/N]
294
+ ```
295
+
296
+ ### Create safety (`vault create`)
297
+
298
+ If a note with the same path already exists:
299
+ ```
300
+ Note already exists: References/Dijkstra Algorithm.md
301
+ Use `vault write --path "..." --force` to overwrite.
302
+ Aborted.
303
+ ```
304
+
305
+ ### Delete safety (`vault delete`)
306
+
307
+ Delete requires explicit confirmation.
308
+
309
+ **Interactive (TTY):** Prompts `Delete "note name"? [y/N]` — defaults to No.
310
+
311
+ **Non-interactive (piped/scripted):** Requires `--yes` flag explicitly.
312
+
313
+ ```bash
314
+ vault delete --file "old note" --yes # scripted, no prompt
315
+ vault delete --file "old note" # prompts in TTY, errors in scripts
316
+ ```
317
+
318
+ ### Property safety (`vault property:set`)
319
+
320
+ Before writing, reads the current value and shows what will change:
321
+ ```
322
+ $ vault property:set --name tags --value math --file "Mathe I"
323
+ Current: tags = ['evergreen', 'engineering']
324
+ New: tags = ['evergreen', 'engineering', 'math']
325
+ Apply? [Y/n]
326
+ ```
327
+
328
+ Add `--yes` to skip the prompt in scripts.
329
+
330
+ ### Error messages
331
+
332
+ All HTTP errors include:
333
+ - The operation attempted (`write`, `delete`, `property:set`)
334
+ - The note path
335
+ - The CouchDB error body (not just the status code)
336
+ - A human hint where applicable
337
+
338
+ **Examples:**
339
+
340
+ ```
341
+ Error writing "References/Mathe I.md":
342
+ CouchDB 409 Conflict — document update conflict.
343
+ Hint: The doc was modified externally. Re-read and retry.
344
+
345
+ Error connecting to CouchDB at https://obsidian.harrokrog.com:
346
+ Connection refused. Is the server running?
347
+ Check: vault ping
348
+
349
+ Error reading "References/Unknown.md":
350
+ Note not found. Use `vault files` to browse, or `vault search query="Unknown"` to find it.
351
+ ```
352
+
353
+ ### `--dry-run` global flag
354
+
355
+ Any mutating command accepts `--dry-run`: prints what would happen without doing it. Safe for agents to validate before committing.
356
+
357
+ ```bash
358
+ vault write --path "X.md" --content "..." --dry-run
359
+ # → Would write 1,234 chars to X.md (currently 980 chars)
360
+
361
+ vault delete --file "old note" --dry-run
362
+ # → Would soft-delete: old note.md
363
+ ```
364
+
365
+ ---
366
+
367
+ ## Feature Breakdown
368
+
369
+ ### Tier 1: Core CRUD (must have)
370
+
371
+ | Command | Description | Maps to Obsidian CLI |
372
+ |---------|-------------|---------------------|
373
+ | `vault read file="X"` | Read note content | `obsidian read file="X"` |
374
+ | `vault read path="X"` | Read by exact path | `obsidian read path="X"` |
375
+ | `vault create name="X" content="Y"` | Create new note | `obsidian create name="X" content="Y"` |
376
+ | `vault create name="X" template="Y"` | Create from template | `obsidian create name="X" template="Y"` |
377
+ | `vault write path="X" content="Y"` | Write/overwrite note | — |
378
+ | `vault append file="X" content="Y"` | Append to note | `obsidian append` |
379
+ | `vault prepend file="X" content="Y"` | Prepend to note | `obsidian prepend` |
380
+ | `vault delete file="X"` | Soft-delete | `obsidian delete` |
381
+ | `vault move file="X" to="Y"` | Move/rename path | `obsidian move` |
382
+ | `vault rename file="X" name="Y"` | Rename (keep folder) | `obsidian rename` |
383
+ | `vault files` | List all files | `obsidian files` |
384
+ | `vault files folder="X"` | List in folder | `obsidian files folder="X"` |
385
+ | `vault files --total` | Count files | `obsidian files total` |
386
+ | `vault folders` | List all folders | `obsidian folders` |
387
+ | `vault search query="X"` | Content search | `obsidian search` |
388
+ | `vault search query="X" --context` | Search with line context | `obsidian search:context` |
389
+
390
+ ### Tier 2: Graph Traversal (essential for agents)
391
+
392
+ | Command | Description | Maps to Obsidian CLI |
393
+ |---------|-------------|---------------------|
394
+ | `vault backlinks file="X"` | Incoming `[[wikilinks]]` | `obsidian backlinks` |
395
+ | `vault backlinks file="X" --counts` | With counts | `obsidian backlinks counts` |
396
+ | `vault links file="X"` | Outgoing `[[wikilinks]]` | `obsidian links` |
397
+ | `vault unresolved` | Links pointing to non-existent notes | `obsidian unresolved` |
398
+ | `vault orphans` | Notes with zero incoming links | `obsidian orphans` |
399
+
400
+ ### Tier 3: Tags & Properties
401
+
402
+ | Command | Description | Maps to Obsidian CLI |
403
+ |---------|-------------|---------------------|
404
+ | `vault tags` | List all tags | `obsidian tags` |
405
+ | `vault tags --counts` | With counts | `obsidian tags counts` |
406
+ | `vault tags --sort=count` | Sorted by frequency | `obsidian tags sort=count` |
407
+ | `vault tag name="X"` | Notes with tag | `obsidian tag name="X"` |
408
+ | `vault tag name="X" --verbose` | With file list | `obsidian tag name="X" verbose` |
409
+ | `vault properties file="X"` | All properties of note | `obsidian properties file="X"` |
410
+ | `vault property:read name="X" file="Y"` | Read one property | `obsidian property:read` |
411
+ | `vault property:set name="X" value="Y" file="Z"` | Set property | `obsidian property:set` |
412
+ | `vault property:remove name="X" file="Y"` | Remove property | `obsidian property:remove` |
413
+
414
+ ### Tier 4: Templates
415
+
416
+ | Command | Description | Maps to Obsidian CLI |
417
+ |---------|-------------|---------------------|
418
+ | `vault templates` | List available templates | `obsidian templates` |
419
+ | `vault template:read name="X"` | Read template content | `obsidian template:read` |
420
+
421
+ ### Tier 5: Utility
422
+
423
+ | Command | Description |
424
+ |---------|-------------|
425
+ | `vault ping` | Test CouchDB connectivity |
426
+ | `vault config show` | Show resolved config |
427
+ | `vault config set <key> <value>` | Update config |
428
+ | `vault --version` | Show version |
429
+ | `vault --help` | Show help |
430
+
431
+ ---
432
+
433
+ ## Implementation Details
434
+
435
+ ### In-Memory Index (for graph operations)
436
+
437
+ Graph commands (`backlinks`, `links`, `unresolved`, `orphans`, `tags`) need to scan all notes. Strategy:
438
+
439
+ 1. Load all notes from CouchDB in a single `_all_docs?include_docs=true` call
440
+ 2. Fetch content chunks for each note
441
+ 3. Build in-memory index: path -> content, wikilink graph, tag map
442
+ 4. Run the requested operation
443
+ 5. Discard index when process exits
444
+
445
+ No persistent cache. No background sync. Every command invocation is stateless.
446
+
447
+ **Performance target:** < 5 seconds for a vault with 500 notes. Acceptable — agent commands are not interactive.
448
+
449
+ **Optimization:** For CRUD commands (read, write, create, delete, move), skip the index entirely. Only graph/tag/search commands trigger the full vault load.
450
+
451
+ ### Wikilink Parsing
452
+
453
+ Extract `[[wikilinks]]` from both frontmatter and body text using regex:
454
+
455
+ ```python
456
+ import re
457
+ WIKILINK_RE = re.compile(r'\[\[([^\]|]+?)(?:\|[^\]]+?)?\]\]')
458
+ ```
459
+
460
+ This captures `[[Note Name]]` and `[[Note Name|Display]]`, extracting just the note name.
461
+
462
+ ### Frontmatter Parsing
463
+
464
+ Use `python-frontmatter` library for reliable YAML parsing. Do not roll custom YAML parsing.
465
+
466
+ ```python
467
+ import frontmatter
468
+
469
+ post = frontmatter.loads(content)
470
+ props = dict(post.metadata) # frontmatter as dict
471
+ body = post.content # body without frontmatter
472
+ ```
473
+
474
+ ### File Resolution
475
+
476
+ The `file=` parameter resolves like an Obsidian wikilink — name only, case-insensitive, no extension required:
477
+
478
+ 1. Try exact path match (case-insensitive)
479
+ 2. Try `{name}.md` (case-insensitive)
480
+ 3. Try basename match across all folders (case-insensitive)
481
+ 4. Return first match, or error if ambiguous
482
+
483
+ The `path=` parameter is an exact path from vault root.
484
+
485
+ ### Output Formats
486
+
487
+ - **Default (text):** Human-readable, one item per line
488
+ - **`--json`:** Structured JSON output (for agents parsing output programmatically)
489
+
490
+ ---
491
+
492
+ ## Technical Decisions
493
+
494
+ 1. **Python 3.10+** — available on every VM and server. Simple HTTP requests to CouchDB. Rich ecosystem for markdown/YAML parsing.
495
+ 2. **`requests`** library for CouchDB HTTP — simple, reliable, no CouchDB-specific ORM needed
496
+ 3. **`python-frontmatter`** for YAML frontmatter — battle-tested, handles edge cases
497
+ 4. **`click`** for CLI — clean decorator syntax, automatic `--help`, subcommand support
498
+ 5. **No AI dependencies** — no torch, no transformers, no API keys. Pure vault access.
499
+ 6. **Minimal dependencies:** `requests`, `python-frontmatter`, `click`, `PyYAML`
500
+ 7. **Stateless** — no daemon, no background process, no persistent cache. Each command is a fresh invocation.
501
+ 8. **`pip install`** — standard Python packaging via `pyproject.toml`. No npm, no Node.js.
502
+
503
+ ---
504
+
505
+ ## Repository Structure
506
+
507
+ ```
508
+ obsidian-vault-cli/
509
+ ├── vault_cli/
510
+ │ ├── __init__.py
511
+ │ ├── __main__.py # python -m vault_cli
512
+ │ ├── client.py # VaultClient — CouchDB CRUD + LiveSync format
513
+ │ ├── index.py # In-memory vault index (graph, tags, search)
514
+ │ ├── frontmatter.py # Frontmatter parsing/writing helpers
515
+ │ ├── wikilinks.py # Wikilink extraction and resolution
516
+ │ ├── config.py # Config loader (file + env vars)
517
+ │ ├── cli/
518
+ │ │ ├── __init__.py
519
+ │ │ ├── main.py # Click CLI entry point + command groups
520
+ │ │ ├── crud.py # read, create, write, append, prepend, delete, move, rename
521
+ │ │ ├── files.py # files, folders
522
+ │ │ ├── search.py # search, search:context
523
+ │ │ ├── graph.py # backlinks, links, unresolved, orphans
524
+ │ │ ├── tags.py # tags, tag
525
+ │ │ ├── properties.py # properties, property:read/set/remove
526
+ │ │ ├── templates.py # templates, template:read
527
+ │ │ └── config_cmd.py # config show/set, ping
528
+ │ └── output.py # Output formatting (text, JSON)
529
+ ├── tests/
530
+ │ ├── test_client.py
531
+ │ ├── test_index.py
532
+ │ ├── test_wikilinks.py
533
+ │ ├── test_frontmatter.py
534
+ │ └── test_cli.py
535
+ ├── pyproject.toml
536
+ ├── requirements.txt
537
+ ├── LICENSE
538
+ ├── README.md
539
+ └── SPEC.md # This file
540
+ ```
541
+
542
+ ---
543
+
544
+ ## What This Project Is NOT
545
+
546
+ - **Not an AI curation tool.** No inbox processing, no auto-tagging, no AI-powered filing.
547
+ - **Not a sync tool.** Does not sync files to/from disk. Use LiveSync for that.
548
+ - **Not a replacement for Obsidian.** Does not render markdown, manage plugins, or provide a UI.
549
+ - **Not a CouchDB admin tool.** Does not manage databases, users, or replication.
550
+
551
+ ---
552
+
553
+ ## Implementation Plan
554
+
555
+ ### Phase 1: Core (VaultClient + CRUD CLI) ✅
556
+
557
+ 1. `vault_cli/client.py` — VaultClient class
558
+ 2. `vault_cli/config.py` — Config loader
559
+ 3. `vault_cli/cli/crud.py` — `read`, `create`, `write`, `append`, `prepend`, `delete`, `move`, `rename`
560
+ 4. `vault_cli/cli/files.py` — `files`, `folders`
561
+ 5. `vault ping` + `vault config show/set`
562
+
563
+ ### Phase 2: Search + Graph ✅
564
+
565
+ 1. `vault_cli/index.py` — In-memory vault index
566
+ 2. `vault_cli/wikilinks.py` — Wikilink regex extraction
567
+ 3. `vault_cli/cli/search.py` — `search`, `search --context`
568
+ 4. `vault_cli/cli/graph.py` — `backlinks`, `links`, `unresolved`, `orphans`
569
+
570
+ ### Phase 3: Tags, Properties, Templates ✅
571
+
572
+ 1. `vault_cli/frontmatter.py` — Frontmatter helpers
573
+ 2. `vault_cli/cli/tags.py` — `tags`, `tag`
574
+ 3. `vault_cli/cli/properties.py` — `properties`, `property:read/set/remove`
575
+ 4. `vault_cli/cli/templates.py` — `templates`, `template:read`
576
+
577
+ ### Phase 4: Safety & Error Handling (v0.2.0) 🔜
578
+
579
+ 1. `write --force` guard — refuse silent overwrites by default
580
+ 2. `create` existence check
581
+ 3. `delete --yes` / interactive confirmation
582
+ 4. `property:set` read-before-write + diff preview
583
+ 5. `--dry-run` global flag
584
+ 6. `deleted: true` detection on write
585
+ 7. Wrapped HTTP error messages with context + hints
586
+ 8. Bump version to `0.2.0`