smoking-mirror 1.0.0 → 1.2.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.
Files changed (3) hide show
  1. package/README.md +339 -151
  2. package/dist/index.js +1629 -7
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,42 +1,277 @@
1
1
  # smoking-mirror
2
2
 
3
- > Obsidian vault intelligence for Claude Code - graph queries, wikilink suggestions, and vault health
3
+ > Your vault's secrets stay yours. Claude sees only what it needs.
4
4
 
5
5
  [![npm version](https://badge.fury.io/js/smoking-mirror.svg)](https://www.npmjs.com/package/smoking-mirror)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
7
 
8
- ## What is smoking-mirror?
8
+ ---
9
9
 
10
- "Smoking mirror" (tezcatl in Nahuatl) is the literal translation of "obsidian". This MCP server acts as a reflective surface that reveals the hidden structure of your Obsidian vault to Claude Code.
10
+ ## The Problem with AI + Your Notes
11
11
 
12
- **First-to-market** features:
13
- - **Graph Intelligence** - Backlinks, forward links, orphan detection, hub analysis
14
- - **Wikilink Services** - Auto-suggest entities, validate links
15
- - **Vault Health** - Broken link detection, comprehensive statistics
16
- - **Frontmatter Queries** - Search notes by metadata, tags, folders
12
+ ```
13
+ ┌─────────────────────────────────────────────────────────────────┐
14
+ │ THE OLD WAY │
15
+ │ │
16
+ │ "Claude, find notes about Project X" │
17
+ │ │
18
+ │ ┌──────────┐ ENTIRE VAULT ┌──────────┐ │
19
+ │ │ Claude │ ◄──────────────────── │ 1,500 │ │
20
+ │ │ Code │ (~50MB of text) │ Notes │ │
21
+ │ └──────────┘ └──────────┘ │
22
+ │ │
23
+ │ • Your private journals? Sent. │
24
+ │ • Financial notes? Sent. │
25
+ │ • That embarrassing poetry? Definitely sent. │
26
+ │ • Token bill? Astronomical. │
27
+ │ │
28
+ └─────────────────────────────────────────────────────────────────┘
29
+ ```
30
+
31
+ ```
32
+ ┌─────────────────────────────────────────────────────────────────┐
33
+ │ THE SMOKING-MIRROR WAY │
34
+ │ │
35
+ │ "Claude, find notes about Project X" │
36
+ │ │
37
+ │ ┌──────────┐ STRUCTURED QUERY ┌──────────┐ │
38
+ │ │ Claude │ ◄───────────────────► │ smoking │ │
39
+ │ │ Code │ { backlinks: [...], │ -mirror │ │
40
+ │ └──────────┘ tags: [...] } └────┬─────┘ │
41
+ │ │ │
42
+ │ ONLY searches │ │
43
+ │ the index! ▼ │
44
+ │ ┌──────────┐ │
45
+ │ │ 1,500 │ │
46
+ │ │ Notes │ │
47
+ │ └──────────┘ │
48
+ │ │
49
+ │ • Your secrets? Never leave your machine. │
50
+ │ • Claude sees? Filenames, links, tags. That's it. │
51
+ │ • Token usage? Minimal. Blazing fast. │
52
+ │ │
53
+ └─────────────────────────────────────────────────────────────────┘
54
+ ```
55
+
56
+ ---
57
+
58
+ ## What IS smoking-mirror?
59
+
60
+ "Smoking mirror" (tezcatl in Nahuatl) is the literal translation of **obsidian**. This MCP server acts as a reflective surface that reveals the *structure* of your vault—without exposing the *content*.
61
+
62
+ **47 tools** that answer questions like:
63
+ - "What links to this note?" → **Backlinks returned, content untouched**
64
+ - "Find orphan notes" → **Paths only, privacy intact**
65
+ - "Search by #tag or frontmatter" → **Metadata queries, not content dumps**
66
+
67
+ ---
68
+
69
+ ## Privacy Architecture
70
+
71
+ ```
72
+ YOUR MACHINE │ CLOUD
73
+ ────────────────────────────────────── │ ────────────────
74
+
75
+ ┌─────────────────┐ │
76
+ │ Obsidian │ │
77
+ │ Vault │ │
78
+ │ ┌───────────┐ │ │
79
+ │ │ 📓 Notes │ │ NEVER LEAVES │
80
+ │ │ 📊 Data │ │ ───────────► │ ❌ Blocked
81
+ │ │ 📝 Journal│ │ │
82
+ │ └───────────┘ │ │
83
+ └────────┬────────┘ │
84
+ │ │
85
+ │ Parse locally │
86
+ ▼ │
87
+ ┌─────────────────┐ │
88
+ │ smoking-mirror │ │
89
+ │ ┌───────────┐ │ │
90
+ │ │ Index │ │ │
91
+ │ │ • links │ │ │
92
+ │ │ • tags │ │ │
93
+ │ │ • paths │ │ │
94
+ │ └───────────┘ │ │
95
+ └────────┬────────┘ │
96
+ │ │
97
+ │ Structured responses only │
98
+ ▼ │
99
+ ┌─────────────────┐ API calls ┌─────────────────┐
100
+ │ Claude Code │ ───────────────► │ Claude AI │
101
+ │ │ (metadata only) │ │
102
+ │ "Find hubs" │ ◄─────────────── │ (processes │
103
+ │ │ { paths, counts }│ structure) │
104
+ └─────────────────┘ └─────────────────┘
105
+ ```
106
+
107
+ **What Claude receives:**
108
+ - File paths and names
109
+ - Link relationships (A → B)
110
+ - Tag lists
111
+ - Frontmatter keys/values
112
+ - Word counts, modification dates
113
+
114
+ **What Claude NEVER receives:**
115
+ - Your actual note content (unless you explicitly Read it)
116
+ - Personal journals
117
+ - Private thoughts
118
+ - Sensitive data
119
+
120
+ ---
121
+
122
+ ## Speed Demon
123
+
124
+ Forget waiting for full-vault searches. smoking-mirror pre-indexes everything.
125
+
126
+ ```
127
+ ┌────────────────────────────────────────────────────────────────┐
128
+ │ PERFORMANCE BENCHMARKS │
129
+ ├────────────────┬───────────────┬───────────────┬───────────────┤
130
+ │ Vault Size │ Index Build │ Query Time │ Memory │
131
+ ├────────────────┼───────────────┼───────────────┼───────────────┤
132
+ │ 100 notes │ <200ms │ <10ms │ ~20MB │
133
+ │ 500 notes │ <500ms │ <10ms │ ~30MB │
134
+ │ 1,500 notes │ <2s │ <10ms │ ~50MB │
135
+ │ 5,000 notes │ <5s │ <10ms │ ~100MB │
136
+ └────────────────┴───────────────┴───────────────┴───────────────┘
137
+
138
+
139
+ Queries are INSTANT because
140
+ they hit an in-memory index,
141
+ not your filesystem.
142
+ ```
143
+
144
+ ---
145
+
146
+ ## Token Economy
147
+
148
+ Every character Claude reads costs you tokens. Here's the math:
149
+
150
+ ```
151
+ ┌─────────────────────────────────────────────────────────────────┐
152
+ │ TOKEN COMPARISON │
153
+ │ │
154
+ │ Traditional approach: "Read all notes with #project tag" │
155
+ │ ───────────────────────────────────────────────────────── │
156
+ │ 📄 50 notes × ~2,000 tokens each = 100,000 tokens │
157
+ │ 💰 Cost: ~$0.30 per query (Claude pricing) │
158
+ │ │
159
+ │ smoking-mirror: search_notes({ has_tag: "project" }) │
160
+ │ ───────────────────────────────────────────────────────── │
161
+ │ 📊 Returns: paths, titles, metadata │
162
+ │ 🎯 ~500 tokens total │
163
+ │ 💰 Cost: ~$0.0015 per query │
164
+ │ │
165
+ │ ═══════════════════════════════════════════════════════════ │
166
+ │ SAVINGS: 200x fewer tokens per query │
167
+ │ ═══════════════════════════════════════════════════════════ │
168
+ │ │
169
+ └─────────────────────────────────────────────────────────────────┘
170
+ ```
171
+
172
+ Then Claude can surgically `Read` only the 2-3 notes it actually needs.
173
+
174
+ ---
175
+
176
+ ## 47 Tools at Your Service
177
+
178
+ ### Graph Intelligence
179
+ | Tool | What it does |
180
+ |------|--------------|
181
+ | `get_backlinks` | Who's linking to this note? |
182
+ | `get_forward_links` | What does this note link to? |
183
+ | `find_orphan_notes` | Notes nobody loves (no incoming links) |
184
+ | `find_hub_notes` | Your vault's superstars (highly connected) |
185
+
186
+ ### Wikilink Services
187
+ | Tool | What it does |
188
+ |------|--------------|
189
+ | `suggest_wikilinks` | "You mentioned 'John'—want to link to [[John Smith]]?" |
190
+ | `validate_links` | Find broken links before they embarrass you |
191
+ | `find_broken_links` | [[Dead Note]] → nowhere |
192
+ | `get_unlinked_mentions` | "John" mentioned 47 times, linked 3 times |
193
+
194
+ ### Vault Health
195
+ | Tool | What it does |
196
+ |------|--------------|
197
+ | `get_vault_stats` | The big picture: notes, links, tags, orphans |
198
+ | `get_folder_structure` | Your vault's anatomy |
199
+ | `get_activity_summary` | What's been happening lately? |
200
+
201
+ ### Smart Search
202
+ | Tool | What it does |
203
+ |------|--------------|
204
+ | `search_notes` | Query by frontmatter, tags, folders—Dataview vibes |
205
+ | `get_recent_notes` | Modified in the last N days |
206
+ | `get_stale_notes` | Important notes gathering dust |
207
+ | `get_notes_modified_on` | What happened on 2024-01-15? |
208
+ | `get_notes_in_range` | Activity between two dates |
209
+
210
+ ### Deep Graph Analysis
211
+ | Tool | What it does |
212
+ |------|--------------|
213
+ | `get_link_path` | How do these two notes connect? (A → B → C → D) |
214
+ | `get_common_neighbors` | What do these notes have in common? |
215
+ | `find_bidirectional_links` | Mutual admirers (A ↔ B) |
216
+ | `find_dead_ends` | Popular notes that link nowhere |
217
+ | `find_sources` | Link-givers, not link-receivers |
218
+ | `get_connection_strength` | How related are these notes, really? |
219
+
220
+ ### Structure Analysis
221
+ | Tool | What it does |
222
+ |------|--------------|
223
+ | `get_note_structure` | Full heading tree and sections |
224
+ | `get_headings` | Just the headings, quick |
225
+ | `get_section_content` | Extract a specific section |
226
+ | `find_sections` | Find all "## References" across the vault |
227
+
228
+ ### Task Management
229
+ | Tool | What it does |
230
+ |------|--------------|
231
+ | `get_all_tasks` | Every `- [ ]` in your vault |
232
+ | `get_tasks_from_note` | Tasks in a specific note |
233
+ | `get_tasks_with_due_dates` | What's due? |
234
+
235
+ ### Frontmatter Intelligence
236
+ | Tool | What it does |
237
+ |------|--------------|
238
+ | `get_frontmatter_schema` | What fields exist across your vault? |
239
+ | `get_field_values` | All unique values for `status` field |
240
+ | `find_frontmatter_inconsistencies` | `status: "done"` vs `status: true` chaos |
241
+
242
+ ### Temporal Analysis
243
+ | Tool | What it does |
244
+ |------|--------------|
245
+ | `get_contemporaneous_notes` | Edited around the same time |
246
+ | `get_note_metadata` | Quick stats without reading content |
247
+
248
+ ### System
249
+ | Tool | What it does |
250
+ |------|--------------|
251
+ | `refresh_index` | Rebuilt after Obsidian changes |
252
+ | `get_all_entities` | Every linkable thing (titles + aliases) |
253
+
254
+ ---
17
255
 
18
256
  ## Installation
19
257
 
20
258
  ### Quick Install (Claude Code CLI)
21
259
 
22
260
  **macOS / Linux:**
23
-
24
261
  ```bash
25
262
  claude mcp add smoking-mirror -e OBSIDIAN_VAULT_PATH=/path/to/your/vault -- npx -y smoking-mirror
26
263
  ```
27
264
 
28
265
  **Windows:**
29
-
30
266
  ```bash
31
267
  claude mcp add smoking-mirror -e OBSIDIAN_VAULT_PATH=C:\path\to\your\vault -- cmd /c npx -y smoking-mirror
32
268
  ```
33
269
 
34
270
  ### Manual Configuration
35
271
 
36
- Alternatively, add to your `.mcp.json` file:
272
+ Add to your `.mcp.json`:
37
273
 
38
274
  **Windows:**
39
-
40
275
  ```json
41
276
  {
42
277
  "mcpServers": {
@@ -52,7 +287,6 @@ Alternatively, add to your `.mcp.json` file:
52
287
  ```
53
288
 
54
289
  **macOS / Linux:**
55
-
56
290
  ```json
57
291
  {
58
292
  "mcpServers": {
@@ -67,193 +301,147 @@ Alternatively, add to your `.mcp.json` file:
67
301
  }
68
302
  ```
69
303
 
70
- ### Verify Installation
71
-
72
- After adding, verify with:
304
+ ### Verify
73
305
 
74
306
  ```bash
75
307
  claude mcp list
308
+ # Should show: smoking-mirror ✓
76
309
  ```
77
310
 
78
- You should see `smoking-mirror` in the list of configured servers.
79
-
80
- ## Tools
81
-
82
- ### Graph Intelligence
83
-
84
- #### `get_backlinks`
85
- Find all notes that link TO a specific note.
86
-
87
- ```
88
- Input: { path: "projects/My Project.md" }
89
- Output: [{ source: "daily/2024-01-15.md", line: 42, context: "Working on [[My Project]] today" }]
90
- ```
311
+ ---
91
312
 
92
- #### `get_forward_links`
93
- Find all notes that a specific note links TO.
313
+ ## Real-World Example
94
314
 
95
315
  ```
96
- Input: { path: "projects/My Project.md" }
97
- Output: [{ target: "Team Members", exists: true, resolved_path: "people/Team Members.md" }]
98
- ```
316
+ You: "What are my most connected project notes?"
99
317
 
100
- #### `find_orphan_notes`
101
- Find notes with no incoming links (potential cleanup candidates).
318
+ Claude uses: find_hub_notes({ min_links: 10 })
102
319
 
103
- ```
104
- Input: { folder: "archive" } // optional folder filter
105
- Output: [{ path: "archive/old-idea.md", title: "old-idea", modified: "2024-01-01" }]
106
- ```
107
-
108
- #### `find_hub_notes`
109
- Find highly connected notes (potential MOCs or index notes).
110
-
111
- ```
112
- Input: { min_links: 10 } // minimum total connections
113
- Output: [{ path: "MOC.md", backlink_count: 45, forward_link_count: 23, total_connections: 68 }]
114
- ```
115
-
116
- ### Wikilink Services
117
-
118
- #### `suggest_wikilinks`
119
- Analyze text and suggest where wikilinks could be added based on existing notes.
120
-
121
- ```
122
- Input: { text: "Meeting with John about the API project" }
123
- Output: [
124
- { entity: "John", start: 13, end: 17, target: "people/John Smith.md" },
125
- { entity: "API project", start: 28, end: 39, target: "projects/API Project.md" }
126
- ]
127
- ```
128
-
129
- #### `validate_links`
130
- Check wikilinks in a note (or all notes) and report broken links.
320
+ Response (what Claude actually sees):
321
+ {
322
+ "hubs": [
323
+ { "path": "projects/Main Project.md", "connections": 47 },
324
+ { "path": "areas/Work.md", "connections": 34 },
325
+ { "path": "resources/API Docs.md", "connections": 28 }
326
+ ]
327
+ }
131
328
 
329
+ Your actual note content? Still safely on your disk.
330
+ Claude knows the STRUCTURE, not the SECRETS.
132
331
  ```
133
- Input: { path: "projects/My Project.md" } // optional, validates all if omitted
134
- Output: [{ source: "projects/My Project.md", target: "Missing Note", line: 15, exists: false }]
135
- ```
136
-
137
- ### Vault Health
138
-
139
- #### `get_vault_stats`
140
- Get comprehensive statistics about your vault.
141
332
 
142
- ```
143
- Output: {
144
- total_notes: 1444,
145
- total_links: 20373,
146
- total_tags: 431,
147
- orphan_notes: 971,
148
- broken_links: 8565,
149
- average_links_per_note: 14.11,
150
- most_linked_notes: [...],
151
- top_tags: [...],
152
- folders: [...]
153
- }
154
- ```
333
+ ---
155
334
 
156
- #### `find_broken_links`
157
- Find all wikilinks that point to non-existent notes.
335
+ ## Architecture
158
336
 
159
337
  ```
160
- Input: { folder: "projects" } // optional folder filter
161
- Output: [{ source: "projects/Old.md", target: "Deleted Note", line: 5 }]
338
+ ┌────────────────────────────────┐
339
+ │ smoking-mirror │
340
+ │ │
341
+ ┌──────────┐ │ ┌──────────────────────────┐ │
342
+ │ │ │ │ VaultIndex │ │
343
+ │ Obsidian │ scan │ │ │ │
344
+ │ Vault │───────►│ │ notes: Map<path, Note> │ │
345
+ │ │ │ │ backlinks: Map<→Set> │ │
346
+ │ 📁 .md │ │ │ entities: Map<name→path>│ │
347
+ │ files │ │ │ tags: Map<tag→Set> │ │
348
+ │ │ │ │ │ │
349
+ └──────────┘ │ └──────────────────────────┘ │
350
+ │ │ │
351
+ │ ▼ │
352
+ │ ┌──────────────────────────┐ │
353
+ │ │ 47 MCP Tools │ │
354
+ │ │ │ │
355
+ │ │ Queries return │ │
356
+ │ │ STRUCTURE not CONTENT │ │
357
+ │ │ │ │
358
+ │ └──────────────────────────┘ │
359
+ │ │
360
+ └────────────────────────────────┘
361
+
362
+ │ MCP Protocol
363
+
364
+ ┌──────────────┐
365
+ │ Claude Code │
366
+ └──────────────┘
162
367
  ```
163
368
 
164
- ### Query
369
+ **Key Design Decisions:**
370
+ - **File-first**: Parses markdown directly, no database
371
+ - **Works offline**: No connection to Obsidian app needed
372
+ - **Eager loading**: Full index on startup (fine for <5000 notes)
373
+ - **In-memory graph**: Lightning-fast queries
374
+ - **Privacy by design**: Content stays local
165
375
 
166
- #### `search_notes`
167
- Search notes by frontmatter fields, tags, folders, or title. Covers ~80% of Dataview use cases.
168
-
169
- ```
170
- Input: {
171
- has_tag: "project",
172
- where: { status: "active" },
173
- folder: "work",
174
- sort_by: "modified",
175
- limit: 10
176
- }
177
- Output: [{ path: "work/Current.md", title: "Current", frontmatter: {...}, tags: [...] }]
178
- ```
376
+ ---
179
377
 
180
- **Parameters:**
181
- - `where` - Frontmatter key-value filters (e.g., `{ type: "book", rating: 5 }`)
182
- - `has_tag` - Filter by single tag
183
- - `has_any_tag` - Filter by any of these tags
184
- - `has_all_tags` - Filter by all of these tags
185
- - `folder` - Limit to notes in this folder
186
- - `title_contains` - Filter by title substring
187
- - `sort_by` - Sort by "modified", "created", or "title"
188
- - `order` - "asc" or "desc"
189
- - `limit` - Maximum results to return
378
+ ## Error Handling
190
379
 
191
- ## Architecture
380
+ | Situation | Behavior |
381
+ |-----------|----------|
382
+ | Malformed YAML | Gracefully skipped, file treated as content |
383
+ | Binary files | Detected and skipped |
384
+ | Empty files | Indexed with no links/tags |
385
+ | Large files (>10MB) | Skipped with warning |
386
+ | Missing files | Graceful degradation |
192
387
 
193
- - **File-first**: Parses markdown directly, no database required
194
- - **Works offline**: No connection to Obsidian needed
195
- - **Fast startup**: <2s for 1500+ notes
196
- - **Memory efficient**: ~50MB for large vaults
197
- - **Parallel parsing**: Processes files in batches for speed
388
+ ---
198
389
 
199
- ### Performance
390
+ ## Why not Dataview?
200
391
 
201
- | Vault Size | Startup Time | Memory |
202
- |------------|--------------|--------|
203
- | 100 notes | <200ms | ~20MB |
204
- | 500 notes | <500ms | ~30MB |
205
- | 1500 notes | <2s | ~50MB |
206
- | 5000 notes | <5s | ~100MB |
392
+ We wanted to use `obsidian-dataview` as a library, but it requires Obsidian's internal `CachedMetadata` API and cannot run standalone. See [this discussion](https://github.com/blacksmithgu/obsidian-dataview/discussions/1811).
207
393
 
208
- ### Error Handling
394
+ Instead, smoking-mirror:
395
+ - Parses markdown directly using `gray-matter`
396
+ - Builds its own in-memory graph index
397
+ - Provides ~80% of Dataview functionality with simpler syntax
398
+ - And does it all **without exposing your content to AI**
209
399
 
210
- - Malformed YAML frontmatter: Gracefully skipped, file treated as content
211
- - Binary files: Detected and skipped
212
- - Empty files: Indexed with no links/tags
213
- - Large files (>10MB): Skipped with warning
214
- - Timeout protection: 5-minute default for vault indexing
400
+ ---
215
401
 
216
402
  ## Development
217
403
 
218
404
  ```bash
219
- # Install dependencies
220
405
  bun install
221
-
222
- # Run in development mode
223
406
  OBSIDIAN_VAULT_PATH=/path/to/vault bun run dev
224
-
225
- # Build for distribution
226
407
  bun run build
227
-
228
- # Test with MCP inspector
229
408
  bun run inspect
230
-
231
- # Run tests
232
409
  bun test
233
410
  ```
234
411
 
235
- ## Why not Dataview?
236
-
237
- We initially planned to use `obsidian-dataview` as a library, but discovered it requires Obsidian's internal `CachedMetadata` API and cannot run standalone. See [this discussion](https://github.com/blacksmithgu/obsidian-dataview/discussions/1811).
238
-
239
- Instead, smoking-mirror:
240
- - Parses markdown files directly using `gray-matter`
241
- - Builds its own in-memory graph index
242
- - Provides ~80% of Dataview functionality with simpler syntax
412
+ ---
243
413
 
244
414
  ## Roadmap
245
415
 
246
- - [ ] MarkdownDB integration for SQL-like queries
247
416
  - [ ] Watch mode for incremental updates
248
417
  - [ ] `rename_with_links` - Safe note renaming with reference updates
249
- - [ ] Obsidian REST API integration for live Dataview queries
418
+ - [ ] MarkdownDB integration for SQL-like queries
419
+ - [ ] Semantic search via local embeddings
420
+
421
+ ---
250
422
 
251
423
  ## License
252
424
 
253
425
  MIT - Ben Cassie
254
426
 
427
+ ---
428
+
429
+ ## The Philosophy
430
+
431
+ > Your notes are yours. Your thoughts are private. Your vault is sacred.
432
+ >
433
+ > AI should help you navigate your knowledge—not consume it.
434
+ >
435
+ > smoking-mirror gives Claude the **map**, not the **territory**.
436
+
437
+ ---
438
+
255
439
  ## Related
256
440
 
257
441
  - [Model Context Protocol](https://modelcontextprotocol.io/)
258
442
  - [Claude Code](https://claude.ai/code)
259
443
  - [Obsidian](https://obsidian.md/)
444
+
445
+ ---
446
+
447
+ *"tezcatl" — the obsidian mirror that reveals truth without taking it*