smoking-mirror 1.1.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.
- package/README.md +344 -204
- package/dist/index.js +1180 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,42 +1,277 @@
|
|
|
1
1
|
# smoking-mirror
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Your vault's secrets stay yours. Claude sees only what it needs.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/smoking-mirror)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## The Problem with AI + Your Notes
|
|
11
|
+
|
|
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
|
|
9
177
|
|
|
10
|
-
|
|
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 |
|
|
11
193
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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,241 +301,147 @@ Alternatively, add to your `.mcp.json` file:
|
|
|
67
301
|
}
|
|
68
302
|
```
|
|
69
303
|
|
|
70
|
-
### Verify
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
## Tools
|
|
311
|
+
---
|
|
81
312
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
#### `get_backlinks`
|
|
85
|
-
Find all notes that link TO a specific note.
|
|
313
|
+
## Real-World Example
|
|
86
314
|
|
|
87
315
|
```
|
|
88
|
-
|
|
89
|
-
Output: [{ source: "daily/2024-01-15.md", line: 42, context: "Working on [[My Project]] today" }]
|
|
90
|
-
```
|
|
316
|
+
You: "What are my most connected project notes?"
|
|
91
317
|
|
|
92
|
-
|
|
93
|
-
Find all notes that a specific note links TO.
|
|
318
|
+
Claude uses: find_hub_notes({ min_links: 10 })
|
|
94
319
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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.
|
|
131
|
-
|
|
132
|
-
```
|
|
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
|
-
|
|
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: [...]
|
|
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
|
+
]
|
|
153
327
|
}
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
#### `find_broken_links`
|
|
157
|
-
Find all wikilinks that point to non-existent notes.
|
|
158
|
-
|
|
159
|
-
```
|
|
160
|
-
Input: { folder: "projects" } // optional folder filter
|
|
161
|
-
Output: [{ source: "projects/Old.md", target: "Deleted Note", line: 5 }]
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
### Query
|
|
165
|
-
|
|
166
|
-
#### `search_notes`
|
|
167
|
-
Search notes by frontmatter fields, tags, folders, or title. Covers ~80% of Dataview use cases.
|
|
168
328
|
|
|
329
|
+
Your actual note content? Still safely on your disk.
|
|
330
|
+
Claude knows the STRUCTURE, not the SECRETS.
|
|
169
331
|
```
|
|
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
|
-
```
|
|
179
|
-
|
|
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
|
|
190
332
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
#### `refresh_index`
|
|
194
|
-
Rebuild the vault index without restarting the server.
|
|
195
|
-
|
|
196
|
-
```
|
|
197
|
-
Output: { success: true, notes_count: 1444, entities_count: 3421, duration_ms: 1823 }
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
#### `get_all_entities`
|
|
201
|
-
Get all linkable entities (note titles and aliases).
|
|
202
|
-
|
|
203
|
-
```
|
|
204
|
-
Input: { include_aliases: true, limit: 100 }
|
|
205
|
-
Output: { entity_count: 100, entities: [{ name: "John Smith", path: "people/John Smith.md", is_alias: false }] }
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
#### `get_recent_notes`
|
|
209
|
-
Get notes modified within the last N days.
|
|
210
|
-
|
|
211
|
-
```
|
|
212
|
-
Input: { days: 7, limit: 50 }
|
|
213
|
-
Output: { count: 23, days: 7, notes: [{ path: "daily/2024-01-15.md", title: "2024-01-15", modified: "...", tags: [...] }] }
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
#### `get_unlinked_mentions`
|
|
217
|
-
Find mentions of an entity that aren't linked (linking opportunities).
|
|
218
|
-
|
|
219
|
-
```
|
|
220
|
-
Input: { entity: "John Smith", limit: 50 }
|
|
221
|
-
Output: { entity: "John Smith", mention_count: 12, mentions: [{ path: "meetings/standup.md", line: 5, context: "Talked to John Smith about..." }] }
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
#### `get_note_metadata`
|
|
225
|
-
Get metadata about a note without reading full content.
|
|
226
|
-
|
|
227
|
-
```
|
|
228
|
-
Input: { path: "projects/API.md", include_word_count: true }
|
|
229
|
-
Output: { path: "...", title: "API", frontmatter: {...}, tags: [...], outlink_count: 15, backlink_count: 8, word_count: 2340 }
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
#### `get_folder_structure`
|
|
233
|
-
Get the folder structure of the vault with note counts.
|
|
234
|
-
|
|
235
|
-
```
|
|
236
|
-
Output: { folder_count: 12, folders: [{ path: "daily", note_count: 365, subfolder_count: 0 }] }
|
|
237
|
-
```
|
|
333
|
+
---
|
|
238
334
|
|
|
239
335
|
## Architecture
|
|
240
336
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
-
|
|
244
|
-
|
|
245
|
-
|
|
337
|
+
```
|
|
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
|
+
└──────────────┘
|
|
367
|
+
```
|
|
368
|
+
|
|
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
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## Error Handling
|
|
379
|
+
|
|
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 |
|
|
387
|
+
|
|
388
|
+
---
|
|
246
389
|
|
|
247
|
-
|
|
390
|
+
## Why not Dataview?
|
|
248
391
|
|
|
249
|
-
|
|
250
|
-
|------------|--------------|--------|
|
|
251
|
-
| 100 notes | <200ms | ~20MB |
|
|
252
|
-
| 500 notes | <500ms | ~30MB |
|
|
253
|
-
| 1500 notes | <2s | ~50MB |
|
|
254
|
-
| 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).
|
|
255
393
|
|
|
256
|
-
|
|
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**
|
|
257
399
|
|
|
258
|
-
|
|
259
|
-
- Binary files: Detected and skipped
|
|
260
|
-
- Empty files: Indexed with no links/tags
|
|
261
|
-
- Large files (>10MB): Skipped with warning
|
|
262
|
-
- Timeout protection: 5-minute default for vault indexing
|
|
400
|
+
---
|
|
263
401
|
|
|
264
402
|
## Development
|
|
265
403
|
|
|
266
404
|
```bash
|
|
267
|
-
# Install dependencies
|
|
268
405
|
bun install
|
|
269
|
-
|
|
270
|
-
# Run in development mode
|
|
271
406
|
OBSIDIAN_VAULT_PATH=/path/to/vault bun run dev
|
|
272
|
-
|
|
273
|
-
# Build for distribution
|
|
274
407
|
bun run build
|
|
275
|
-
|
|
276
|
-
# Test with MCP inspector
|
|
277
408
|
bun run inspect
|
|
278
|
-
|
|
279
|
-
# Run tests
|
|
280
409
|
bun test
|
|
281
410
|
```
|
|
282
411
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
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).
|
|
286
|
-
|
|
287
|
-
Instead, smoking-mirror:
|
|
288
|
-
- Parses markdown files directly using `gray-matter`
|
|
289
|
-
- Builds its own in-memory graph index
|
|
290
|
-
- Provides ~80% of Dataview functionality with simpler syntax
|
|
412
|
+
---
|
|
291
413
|
|
|
292
414
|
## Roadmap
|
|
293
415
|
|
|
294
|
-
- [ ] MarkdownDB integration for SQL-like queries
|
|
295
416
|
- [ ] Watch mode for incremental updates
|
|
296
417
|
- [ ] `rename_with_links` - Safe note renaming with reference updates
|
|
297
|
-
- [ ]
|
|
418
|
+
- [ ] MarkdownDB integration for SQL-like queries
|
|
419
|
+
- [ ] Semantic search via local embeddings
|
|
420
|
+
|
|
421
|
+
---
|
|
298
422
|
|
|
299
423
|
## License
|
|
300
424
|
|
|
301
425
|
MIT - Ben Cassie
|
|
302
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
|
+
|
|
303
439
|
## Related
|
|
304
440
|
|
|
305
441
|
- [Model Context Protocol](https://modelcontextprotocol.io/)
|
|
306
442
|
- [Claude Code](https://claude.ai/code)
|
|
307
443
|
- [Obsidian](https://obsidian.md/)
|
|
444
|
+
|
|
445
|
+
---
|
|
446
|
+
|
|
447
|
+
*"tezcatl" — the obsidian mirror that reveals truth without taking it*
|