obsidian-zk 0.1.3 → 0.2.1

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 (50) hide show
  1. package/README.md +204 -42
  2. package/dist/db/index.d.ts +5 -1
  3. package/dist/db/index.d.ts.map +1 -1
  4. package/dist/db/index.js +143 -28
  5. package/dist/db/index.js.map +1 -1
  6. package/dist/server/server.d.ts.map +1 -1
  7. package/dist/server/server.js +85 -9
  8. package/dist/server/server.js.map +1 -1
  9. package/dist/tools/analysis.d.ts.map +1 -1
  10. package/dist/tools/analysis.js +16 -0
  11. package/dist/tools/analysis.js.map +1 -1
  12. package/dist/tools/backlinks.d.ts +30 -0
  13. package/dist/tools/backlinks.d.ts.map +1 -0
  14. package/dist/tools/backlinks.js +45 -0
  15. package/dist/tools/backlinks.js.map +1 -0
  16. package/dist/tools/capture.d.ts.map +1 -1
  17. package/dist/tools/capture.js +5 -4
  18. package/dist/tools/capture.js.map +1 -1
  19. package/dist/tools/literature.d.ts.map +1 -1
  20. package/dist/tools/literature.js +5 -4
  21. package/dist/tools/literature.js.map +1 -1
  22. package/dist/tools/manage.d.ts +19 -1
  23. package/dist/tools/manage.d.ts.map +1 -1
  24. package/dist/tools/manage.js +41 -10
  25. package/dist/tools/manage.js.map +1 -1
  26. package/dist/tools/moc.d.ts +15 -0
  27. package/dist/tools/moc.d.ts.map +1 -0
  28. package/dist/tools/moc.js +61 -0
  29. package/dist/tools/moc.js.map +1 -0
  30. package/dist/tools/permanent.d.ts +1 -0
  31. package/dist/tools/permanent.d.ts.map +1 -1
  32. package/dist/tools/permanent.js +17 -5
  33. package/dist/tools/permanent.js.map +1 -1
  34. package/dist/tools/project.d.ts +16 -0
  35. package/dist/tools/project.d.ts.map +1 -0
  36. package/dist/tools/project.js +49 -0
  37. package/dist/tools/project.js.map +1 -0
  38. package/dist/tools/search.d.ts.map +1 -1
  39. package/dist/tools/search.js +0 -3
  40. package/dist/tools/search.js.map +1 -1
  41. package/dist/vault/writer.d.ts +1 -0
  42. package/dist/vault/writer.d.ts.map +1 -1
  43. package/dist/vault/writer.js +11 -0
  44. package/dist/vault/writer.js.map +1 -1
  45. package/package.json +1 -1
  46. package/templates/claude/skills/zk-daily/SKILL.md +6 -3
  47. package/templates/claude/skills/zk-finalize/SKILL.md +26 -0
  48. package/templates/claude/skills/zk-moc/SKILL.md +25 -0
  49. package/templates/claude/skills/zk-project/SKILL.md +23 -0
  50. package/templates/claude/skills/zk-promote/SKILL.md +18 -7
package/README.md CHANGED
@@ -6,12 +6,14 @@ Works with [Claude Code](https://claude.ai/code) — exposes tools and prompts v
6
6
 
7
7
  ## What it does
8
8
 
9
- - **CRUD** for fleeting, literature, and permanent notes with proper frontmatter
9
+ - **CRUD** for fleeting, literature, permanent, MOC, and project notes with proper frontmatter
10
10
  - **Luhmann numbering** — auto-generates `zk_id` with alternating numbers/letters (`1 → 1a → 1a1 → 1a1a`)
11
- - **Connection search** — finds related notes by shared tags, keywords, link proximity
12
- - **Vault analysis** — unprocessed notes, orphans, emerging theme clusters
13
- - **MCP prompts** — guided workflows as slash commands (`/zk:capture`, `/zk:promote`, etc.)
14
- - **SQLite index** — fast metadata queries without scanning files every time
11
+ - **Connection scoring** — finds related notes by shared tags, keywords, Luhmann proximity, MOC overlap
12
+ - **Backlinks** — incoming/outgoing link tracking with path-based resolution
13
+ - **Vault analysis** — unprocessed notes with age warnings, orphans, emerging theme clusters
14
+ - **Quality pipeline** — promote fleeting/literature permanent, finalize with quality checks
15
+ - **MCP skills** — guided workflows as slash commands (`/zk:capture`, `/zk:promote`, `/zk:moc`, etc.)
16
+ - **SQLite index** — incremental indexing with single-note updates, path-based link storage
15
17
 
16
18
  No embeddings — Claude judges semantic relevance directly in context.
17
19
 
@@ -24,8 +26,9 @@ npx obsidian-zk init
24
26
  ```
25
27
 
26
28
  Interactive wizard:
29
+
27
30
  - Detects or asks for vault path
28
- - Creates folder structure (`1-Fleeting/`, `2-Literature/`, `3-Permanent/`, etc.)
31
+ - Creates folder structure (`1-Fleeting/`, `2-Literature/`, `3-Permanent/`, `4-MOC/`, `5-Projects/`)
29
32
  - Copies note templates, skills, and agents into `.claude/`
30
33
  - Creates `CLAUDE.md` with project instructions
31
34
  - Sets up SQLite database in `.zk/`
@@ -42,17 +45,22 @@ claude
42
45
 
43
46
  The MCP server starts automatically. Use prompts:
44
47
 
45
- | Command | What it does |
46
- |---------|-------------|
47
- | `/zk:capture` | Quick fleeting note from a thought |
48
- | `/zk:literature` | Literature note from pasted source |
49
- | `/zk:permanent` | Atomic permanent note with auto Luhmann ID |
50
- | `/zk:promote` | Convert fleeting permanent |
51
- | `/zk:manage` | Edit/archive/delete by Luhmann number |
52
- | `/zk:review` | Vault health report |
53
- | `/zk:daily` | Morning briefing |
54
- | `/zk:connect` | Find and create connections for a note |
55
- | `/zk:reflect` | Deep reflection on vault themes |
48
+
49
+ | Command | What it does |
50
+ | ---------------- | ---------------------------------------------------- |
51
+ | `/zk:capture` | Quick fleeting note from a thought |
52
+ | `/zk:literature` | Literature note from pasted source |
53
+ | `/zk:permanent` | Atomic permanent note with auto Luhmann ID |
54
+ | `/zk:promote` | Convert fleeting/literature permanent (single/batch) |
55
+ | `/zk:manage` | Edit/archive/delete by Luhmann number |
56
+ | `/zk:moc` | Create Map of Content — auto-pulls notes by tag |
57
+ | `/zk:project` | Create project note with tasks and deadlines |
58
+ | `/zk:finalize` | Quality-check and finalize permanent notes |
59
+ | `/zk:review` | Vault health report |
60
+ | `/zk:daily` | Morning briefing with age warnings |
61
+ | `/zk:connect` | Find and create connections for a note |
62
+ | `/zk:reflect` | Deep reflection on vault themes |
63
+
56
64
 
57
65
  ### 3. Update
58
66
 
@@ -69,42 +77,52 @@ Syncs skills/agents, runs DB migrations.
69
77
  ```
70
78
  Claude Code ←→ MCP Server (obsidian-zk serve) ←→ SQLite DB + Vault files
71
79
 
72
- ├── Tools (zk_capture, zk_permanent, zk_find_connections, ...)
73
- └── Prompts (/zk:capture, /zk:promote, /zk:review, ...)
80
+ ├── Tools (zk_capture, zk_permanent, zk_moc, zk_backlinks, ...)
81
+ └── Skills (/zk:capture, /zk:promote, /zk:finalize, ...)
74
82
  ```
75
83
 
76
84
  ### Architecture
77
85
 
78
- | Component | Choice |
79
- |-----------|--------|
80
- | MCP SDK | `@modelcontextprotocol/sdk` (stdio transport) |
81
- | Database | `better-sqlite3` — single `.zk/zettelkasten.db` file |
82
- | Semantic search | Claude itself no embeddings infra needed |
83
- | Vault I/O | Node.js `fs`direct file read/write |
86
+
87
+ | Component | Choice |
88
+ | --------------- | ---------------------------------------------------- |
89
+ | MCP SDK | `@modelcontextprotocol/sdk` (stdio transport) |
90
+ | Database | `better-sqlite3`single `.zk/zettelkasten.db` file |
91
+ | Semantic search | Claude itselfno embeddings infra needed |
92
+ | Vault I/O | Node.js `fs` — direct file read/write |
93
+
84
94
 
85
95
  ### MCP Tools
86
96
 
87
97
  **CRUD:**
98
+
88
99
  - `zk_capture` — create fleeting note
89
100
  - `zk_literature` — create literature note
90
101
  - `zk_permanent` — create permanent note with Luhmann ID
91
- - `zk_manage` — edit/archive/delete by ID
92
- - `zk_promote` — mark fleeting as processed
102
+ - `zk_manage` — edit frontmatter + body sections, archive, delete by ID
103
+ - `zk_promote` — mark fleeting/literature as processed, extract key ideas
104
+ - `zk_moc` — create Map of Content, auto-pull notes by tags
105
+ - `zk_project` — create project note with tasks/deadlines
93
106
 
94
107
  **Search & connections:**
95
- - `zk_find_connections` — candidates by tags + links + keywords
108
+
109
+ - `zk_find_connections` — candidates scored by tags, keywords, Luhmann proximity, MOC overlap
110
+ - `zk_backlinks` — incoming/outgoing links for a note (by path, title, or ID)
96
111
  - `zk_cluster_detect` — emerging themes without MOCs
97
112
 
98
113
  **Analysis:**
114
+
99
115
  - `zk_list` — filter notes by type/status/folder
100
- - `zk_unprocessed` — notes needing processing
116
+ - `zk_unprocessed` — notes needing processing with age and urgency tiers
101
117
  - `zk_orphans` — notes with no incoming links
118
+ - `zk_finalize` — quality-check permanent notes (connections, claim, evidence, confidence)
102
119
  - `zk_next_id` — next Luhmann ID
103
120
  - `zk_find_by_id` — resolve ID → path
104
121
  - `zk_list_ids` — all numbered notes
105
122
  - `zk_review` — full vault health report
106
123
 
107
124
  **Index:**
125
+
108
126
  - `zk_reindex` — full vault re-scan
109
127
  - `zk_status` — DB stats
110
128
 
@@ -124,19 +142,21 @@ CREATE TABLE links (
124
142
  );
125
143
  ```
126
144
 
127
- Incremental indexing compares `content_hash`, only updates changed files.
145
+ Links use **path-based resolution** — wikilink titles resolved to file paths during indexing. Incremental indexing via `content_hash`; single-note `indexNote()` for CRUD ops, full `reindex()` for vault scans.
128
146
 
129
147
  ## Zettelkasten method
130
148
 
131
149
  ### Note types
132
150
 
133
- | Folder | Type | Atomic? | Lifecycle |
134
- |--------|------|---------|-----------|
135
- | `1-Fleeting/` | Raw thoughts | No | unprocessed → processed |
136
- | `2-Literature/` | Source summaries | Partial | unprocessed → processed |
137
- | `3-Permanent/` | One idea per note | **Yes** | draftfinalized |
138
- | `4-MOC/` | Topic indexes | No | |
139
- | `5-Projects/` | Active goals | No | activecompleted |
151
+
152
+ | Folder | Type | Atomic? | Lifecycle |
153
+ | --------------- | ----------------- | ------- | ----------------------- |
154
+ | `1-Fleeting/` | Raw thoughts | No | unprocessed → processed |
155
+ | `2-Literature/` | Source summaries | Partial | unprocessedprocessed |
156
+ | `3-Permanent/` | One idea per note | **Yes** | draft → finalized |
157
+ | `4-MOC/` | Topic indexes | No | draftactive |
158
+ | `5-Projects/` | Active goals | No | active → completed |
159
+
140
160
 
141
161
  ### Luhmann numbering
142
162
 
@@ -153,19 +173,151 @@ Incremental indexing — compares `content_hash`, only updates changed files.
153
173
  - **Розширює** (Extends) — builds upon another idea
154
174
  - **Пов'язано** (Related) — topically connected
155
175
 
176
+ ### Connection scoring
177
+
178
+ Notes are scored for connection relevance using multiple signals:
179
+ - Shared tags: +2 per tag
180
+ - Keyword overlap: +1 (4+ chars), +2 (6+ chars)
181
+ - Luhmann proximity: +3 (siblings), +1 (cousins)
182
+ - Shared MOC: +2
183
+ - Threshold: score ≥ 2 to appear as candidate
184
+
156
185
  ### Workflow
157
186
 
158
187
  ```
159
188
  Thought → /zk:capture → Fleeting note
160
189
  Source → /zk:literature → Literature note
161
190
 
162
- /zk:promote (or /zk:permanent)
191
+ /zk:promote (fleeting or literature)
163
192
 
164
193
  Permanent note (auto zk_id) ←→ connections
165
194
 
195
+ /zk:finalize (quality check)
196
+
166
197
  MOC (when 3+ related notes cluster)
167
198
  ```
168
199
 
200
+ ## Use cases
201
+
202
+ ### Research & learning
203
+
204
+ **Reading a book or paper:**
205
+
206
+ ```
207
+ > /zk:literature
208
+ > Here are my highlights from "Thinking, Fast and Slow" by Kahneman...
209
+ ```
210
+
211
+ 1. `/zk:literature` — paste highlights, Claude creates a structured literature note with source metadata
212
+ 2. `/zk:promote` — extract atomic ideas into permanent notes, each with its own Luhmann ID
213
+ 3. `/zk:connect` — Claude finds related notes in your vault and suggests typed connections
214
+
215
+ **Exploring a new topic:**
216
+
217
+ ```
218
+ > /zk:capture
219
+ > Just learned that mitochondrial DNA is inherited only from the mother.
220
+ > This might connect to my notes on epigenetics.
221
+ ```
222
+
223
+ 1. `/zk:capture` several fleeting notes as you read/watch/listen
224
+ 2. `/zk:daily` — review what's accumulated, see age warnings for stale notes
225
+ 3. `/zk:promote` in batch — convert the best ideas into permanent notes
226
+ 4. `/zk:moc` — once 3+ notes cluster around a theme, create a Map of Content
227
+
228
+ ### Writing & thinking
229
+
230
+ **Preparing an article or essay:**
231
+
232
+ ```
233
+ > /zk:reflect
234
+ > I'm writing a blog post about decision-making under uncertainty.
235
+ > What themes in my vault are relevant?
236
+ ```
237
+
238
+ 1. `/zk:reflect` — Claude analyzes vault themes and surfaces unexpected connections
239
+ 2. `/zk:review` — find orphaned notes and weak spots in your knowledge graph
240
+ 3. Browse MOCs to build an outline from existing permanent notes
241
+ 4. `/zk:connect` — strengthen the argument by finding supporting/contradicting notes
242
+
243
+ **Developing an argument:**
244
+
245
+ ```
246
+ > /zk:permanent
247
+ > Claim: distributed teams outperform co-located ones when
248
+ > async communication norms are explicit.
249
+ ```
250
+
251
+ 1. `/zk:permanent` — write your claim as an atomic note
252
+ 2. `/zk:connect` — Claude scores candidates and suggests supporting/contradicting links
253
+ 3. Follow the Luhmann tree (1 → 1a → 1a1) to build a chain of reasoning
254
+ 4. `/zk:finalize` — quality check: does the note have evidence, confidence level, enough connections?
255
+
256
+ ### Daily knowledge work
257
+
258
+ **Morning routine:**
259
+
260
+ ```
261
+ > /zk:daily
262
+ ```
263
+
264
+ Claude shows unprocessed notes grouped by urgency, suggests which to promote first.
265
+
266
+ 1. `/zk:daily` — see unprocessed notes with urgency tiers, aging warnings
267
+ 2. `/zk:promote` — process the oldest fleeting notes first
268
+ 3. `/zk:connect` on newly promoted notes — integrate them into the graph
269
+
270
+ **After a meeting or conversation:**
271
+
272
+ ```
273
+ > /zk:capture
274
+ > Meeting with Alex: they suggested using event sourcing instead of CRUD
275
+ > for the audit log. Interesting tradeoff — immutability vs query complexity.
276
+ ```
277
+
278
+ 1. `/zk:capture` — dump raw thoughts quickly (title + body, no structure needed)
279
+ 2. Come back later, `/zk:promote` — Claude helps extract the key insight into a permanent note
280
+
281
+ ### Project management
282
+
283
+ **Tracking a goal:**
284
+
285
+ ```
286
+ > /zk:project
287
+ > Project: "Q2 API redesign". Deadline: June 30.
288
+ > Key tasks: schema migration, client SDK update, docs rewrite.
289
+ > Link to my notes on REST vs GraphQL tradeoffs.
290
+ ```
291
+
292
+ 1. `/zk:project` — create a project note with tasks, deadlines, and linked permanent notes
293
+ 2. Link relevant permanent notes as the knowledge base for the project
294
+ 3. `/zk:review` — monitor project health alongside vault health
295
+
296
+ ### Vault maintenance
297
+
298
+ **Weekly cleanup:**
299
+
300
+ ```
301
+ > /zk:review
302
+ ```
303
+
304
+ Claude reports: 12 unprocessed notes (3 urgent), 5 orphans, 2 emerging clusters without MOCs.
305
+
306
+ 1. `/zk:review` — full vault health report (orphans, unprocessed count, connection density)
307
+ 2. `/zk:finalize` — batch quality-check draft permanent notes
308
+ 3. `/zk:moc` — create MOCs for emerging clusters detected by `/zk:reflect`
309
+
310
+ **Onboarding to an existing vault:**
311
+
312
+ ```
313
+ > /zk:reflect
314
+ > I just inherited this vault. What are the main themes and how do they connect?
315
+ ```
316
+
317
+ 1. `/zk:review` — get a high-level picture of vault state
318
+ 2. `/zk:reflect` — understand major themes and how they connect
319
+ 3. Browse MOCs and Luhmann trees to navigate the knowledge structure
320
+
169
321
  ## Development
170
322
 
171
323
  ```bash
@@ -202,14 +354,24 @@ src/
202
354
  ├── vault/
203
355
  │ ├── parser.ts # Frontmatter, wikilinks, body
204
356
  │ ├── scanner.ts # File discovery
205
- │ └── writer.ts # Note creation/editing
357
+ │ └── writer.ts # Note creation/editing + section updates
206
358
  ├── db/
207
- │ ├── schema.ts # SQLite schema
208
- │ └── index.ts # DB connection + indexing
359
+ │ ├── schema.ts # SQLite schema + migrations
360
+ │ └── index.ts # DB connection, indexing, connection scoring
209
361
  ├── tools/ # MCP tool implementations
362
+ │ ├── capture.ts # Fleeting notes
363
+ │ ├── literature.ts # Literature notes
364
+ │ ├── permanent.ts # Permanent notes (from fleeting or literature)
365
+ │ ├── manage.ts # Edit/archive/delete/finalize
366
+ │ ├── moc.ts # Map of Content creation
367
+ │ ├── project.ts # Project notes
368
+ │ ├── backlinks.ts # Incoming/outgoing link queries
369
+ │ ├── search.ts # Connection search + cluster detection
370
+ │ ├── analysis.ts # Unprocessed, orphans, review
371
+ │ └── index-mgmt.ts # Reindex, status
210
372
  └── luhmann.ts # ID generation + sorting
211
373
  templates/ # Copied to user vault on init
212
- ├── claude/skills/ # 9 SKILL.md files
374
+ ├── claude/skills/ # 12 SKILL.md files
213
375
  ├── claude/agents/ # zk-analyzer agent
214
376
  ├── vault-folders/ # Default folder structure + note templates
215
377
  └── CLAUDE.md.template # Project instructions
@@ -6,6 +6,9 @@ export declare class ZkDatabase {
6
6
  db: Database.Database;
7
7
  vaultRoot: string;
8
8
  constructor(vaultRoot: string);
9
+ resolveWikilink(title: string): string | null;
10
+ indexNote(relPath: string): void;
11
+ removeNote(relPath: string): void;
9
12
  reindex(): {
10
13
  added: number;
11
14
  updated: number;
@@ -28,7 +31,8 @@ export declare class ZkDatabase {
28
31
  getUnprocessed(type?: string): any[];
29
32
  getOrphans(folder?: string): any[];
30
33
  getLinksFrom(relPath: string): any[];
31
- getLinksTo(title: string): any[];
34
+ getLinksTo(path: string): any[];
35
+ shareMoc(pathA: string, pathB: string): boolean;
32
36
  findConnections(notePath: string): {
33
37
  path: string;
34
38
  title: string;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/db/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AAQtC,qBAAa,UAAU;IACrB,EAAE,EAAE,QAAQ,CAAC,QAAQ,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;gBAEN,SAAS,EAAE,MAAM;IAY7B,OAAO,IAAI;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE;IA8E9D,aAAa,CAAC,OAAO,EAAE,MAAM,GACkD,GAAG;IAGlF,WAAW,CAAC,IAAI,EAAE,MAAM,GACqD,GAAG;IAGhF,cAAc,CAAC,IAAI,EAAE,MAAM,GACiD,GAAG,EAAE;IAGjF,gBAAgB,CAAC,MAAM,EAAE,MAAM,GACiD,GAAG,EAAE;IAGrF,gBAAgB,CAAC,MAAM,EAAE,MAAM,GACiD,GAAG,EAAE;IAGrF,SAAS,CAAC,OAAO,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAOtB,GAAG,EAAE;IAGrD,WAAW,IAAI,GAAG,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAO3D,cAAc,CAAC,IAAI,CAAC,EAAE,MAAM,GAKoB,GAAG,EAAE;IAGrD,UAAU,CAAC,MAAM,CAAC,EAAE,MAAM,GAUsB,GAAG,EAAE;IAGrD,YAAY,CAAC,OAAO,EAAE,MAAM,GACqD,GAAG,EAAE;IAGtF,UAAU,CAAC,KAAK,EAAE,MAAM,GACuD,GAAG,EAAE;IAGpF,eAAe,CAAC,QAAQ,EAAE,MAAM;cAQJ,MAAM;eAAS,MAAM;eAAS,MAAM;iBAAW,MAAM,EAAE;iBAAW,MAAM;cAAQ,MAAM;;IA4BlH,QAAQ;;;;;;;IASR,KAAK;CAGN"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/db/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AAiDtC,qBAAa,UAAU;IACrB,EAAE,EAAE,QAAQ,CAAC,QAAQ,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;gBAEN,SAAS,EAAE,MAAM;IAY7B,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAK7C,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IA4ChC,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAMjC,OAAO,IAAI;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE;IAoF9D,aAAa,CAAC,OAAO,EAAE,MAAM,GACkD,GAAG;IAGlF,WAAW,CAAC,IAAI,EAAE,MAAM,GACqD,GAAG;IAGhF,cAAc,CAAC,IAAI,EAAE,MAAM,GACiD,GAAG,EAAE;IAGjF,gBAAgB,CAAC,MAAM,EAAE,MAAM,GACiD,GAAG,EAAE;IAGrF,gBAAgB,CAAC,MAAM,EAAE,MAAM,GACiD,GAAG,EAAE;IAGrF,SAAS,CAAC,OAAO,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAOtB,GAAG,EAAE;IAGrD,WAAW,IAAI,GAAG,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAO3D,cAAc,CAAC,IAAI,CAAC,EAAE,MAAM,GAKoB,GAAG,EAAE;IAGrD,UAAU,CAAC,MAAM,CAAC,EAAE,MAAM,GASsB,GAAG,EAAE;IAGrD,YAAY,CAAC,OAAO,EAAE,MAAM,GACqD,GAAG,EAAE;IAGtF,UAAU,CAAC,IAAI,EAAE,MAAM,GACuD,GAAG,EAAE;IAGnF,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO;IAU/C,eAAe,CAAC,QAAQ,EAAE,MAAM;cAOJ,MAAM;eAAS,MAAM;eAAS,MAAM;iBAAW,MAAM,EAAE;iBAAW,MAAM;cAAQ,MAAM;;IA4ClH,QAAQ;;;;;;;IASR,KAAK;CAGN"}
package/dist/db/index.js CHANGED
@@ -8,6 +8,52 @@ import { join, dirname, basename } from "node:path";
8
8
  import { CREATE_TABLES, SCHEMA_VERSION } from "./schema.js";
9
9
  import { scanVault } from "../vault/scanner.js";
10
10
  import { parseFrontmatter, getBody, getTags, getWikilinks } from "../vault/parser.js";
11
+ function extractSummary(fm, body, type) {
12
+ if (type === "permanent" && fm.claim) {
13
+ return String(fm.claim);
14
+ }
15
+ if (type === "literature") {
16
+ const lines = body.split("\n");
17
+ let para = "";
18
+ for (const line of lines) {
19
+ const trimmed = line.trim();
20
+ if (!trimmed || trimmed.startsWith("#")) {
21
+ if (para)
22
+ return para.slice(0, 500);
23
+ continue;
24
+ }
25
+ para += (para ? " " : "") + trimmed;
26
+ }
27
+ if (para)
28
+ return para.slice(0, 500);
29
+ }
30
+ if (type === "fleeting") {
31
+ const match = body.match(/##\s+Thought[^\n]*\n([\s\S]*?)(?=\n##|$)/);
32
+ if (match)
33
+ return match[1].trim().slice(0, 500);
34
+ }
35
+ return body.trim().slice(0, 500);
36
+ }
37
+ function luhmannProximity(idA, idB) {
38
+ if (!idA || !idB)
39
+ return 0;
40
+ const partsA = idA.match(/(\d+|[a-z]+)/g) ?? [];
41
+ const partsB = idB.match(/(\d+|[a-z]+)/g) ?? [];
42
+ let common = 0;
43
+ for (let i = 0; i < Math.min(partsA.length, partsB.length); i++) {
44
+ if (partsA[i] === partsB[i])
45
+ common++;
46
+ else
47
+ break;
48
+ }
49
+ if (common === 0)
50
+ return 0;
51
+ // Siblings: share all but last segment
52
+ if (common >= Math.max(partsA.length, partsB.length) - 1)
53
+ return 3;
54
+ // Cousins: share at least one segment
55
+ return 1;
56
+ }
11
57
  export class ZkDatabase {
12
58
  db;
13
59
  vaultRoot;
@@ -18,33 +64,71 @@ export class ZkDatabase {
18
64
  mkdirSync(dbDir, { recursive: true });
19
65
  const dbPath = join(dbDir, "zettelkasten.db");
20
66
  this.db = new Database(dbPath);
21
- // WAL mode: allows concurrent reads during writes, better for indexing while serving queries
22
67
  this.db.pragma("journal_mode = WAL");
23
68
  this.db.exec(CREATE_TABLES);
24
69
  this.db.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)").run("schema_version", String(SCHEMA_VERSION));
25
70
  }
71
+ resolveWikilink(title) {
72
+ const row = this.db.prepare("SELECT path FROM notes WHERE title = ?").get(title);
73
+ return row?.path ?? null;
74
+ }
75
+ indexNote(relPath) {
76
+ const fullPath = join(this.vaultRoot, relPath);
77
+ let content;
78
+ try {
79
+ content = readFileSync(fullPath, "utf-8");
80
+ }
81
+ catch {
82
+ return;
83
+ }
84
+ const hash = createHash("md5").update(content).digest("hex");
85
+ const fm = parseFrontmatter(fullPath);
86
+ const body = getBody(fullPath);
87
+ const title = basename(relPath, ".md");
88
+ const folder = dirname(relPath).split("/")[0] || "";
89
+ const tags = getTags(fm);
90
+ const type = fm.type;
91
+ const summary = extractSummary(fm, body, type);
92
+ this.db.prepare(`
93
+ INSERT OR REPLACE INTO notes (path, title, zk_id, type, status, folder, tags, summary, created, modified, content_hash)
94
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
95
+ `).run(relPath, title, fm.zk_id || null, type || null, fm.status || null, folder, JSON.stringify(tags), summary, fm.date || null, fm.last_modified || null, hash);
96
+ // Rebuild links for this note
97
+ this.db.prepare("DELETE FROM links WHERE source = ?").run(relPath);
98
+ const insertLink = this.db.prepare("INSERT OR REPLACE INTO links (source, target, link_type) VALUES (?, ?, ?)");
99
+ const wikilinks = getWikilinks(fullPath);
100
+ for (const target of wikilinks) {
101
+ const targetPath = this.resolveWikilink(target) ?? target;
102
+ insertLink.run(relPath, targetPath, null);
103
+ }
104
+ this.db.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)").run("last_index", new Date().toISOString());
105
+ }
106
+ removeNote(relPath) {
107
+ this.db.prepare("DELETE FROM notes WHERE path = ?").run(relPath);
108
+ this.db.prepare("DELETE FROM links WHERE source = ?").run(relPath);
109
+ this.db.prepare("DELETE FROM links WHERE target = ?").run(relPath);
110
+ }
26
111
  reindex() {
27
112
  const notes = scanVault(this.vaultRoot);
28
113
  const currentPaths = new Set(notes.map((n) => n.relPath));
29
114
  let added = 0, updated = 0, removed = 0;
30
- // Remove deleted notes
115
+ // Remove deleted notes + their links
31
116
  const dbPaths = this.db.prepare("SELECT path FROM notes").all();
32
- const removePaths = dbPaths.filter((r) => !currentPaths.has(r.path));
33
117
  const deleteNote = this.db.prepare("DELETE FROM notes WHERE path = ?");
34
118
  const deleteLinks = this.db.prepare("DELETE FROM links WHERE source = ?");
35
- for (const { path } of removePaths) {
119
+ for (const { path } of dbPaths.filter((r) => !currentPaths.has(r.path))) {
36
120
  deleteNote.run(path);
37
121
  deleteLinks.run(path);
38
122
  removed++;
39
123
  }
40
- // Upsert notes
124
+ // Pass 1: upsert all notes
41
125
  const getHash = this.db.prepare("SELECT content_hash FROM notes WHERE path = ?");
42
126
  const upsert = this.db.prepare(`
43
127
  INSERT OR REPLACE INTO notes (path, title, zk_id, type, status, folder, tags, summary, created, modified, content_hash)
44
128
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
45
129
  `);
46
- const insertLink = this.db.prepare("INSERT OR REPLACE INTO links (source, target, link_type) VALUES (?, ?, ?)");
47
- const transaction = this.db.transaction(() => {
130
+ const changedPaths = new Set();
131
+ const upsertTx = this.db.transaction(() => {
48
132
  for (const note of notes) {
49
133
  const fullPath = join(this.vaultRoot, note.relPath);
50
134
  let content;
@@ -54,31 +138,41 @@ export class ZkDatabase {
54
138
  catch {
55
139
  continue;
56
140
  }
57
- // Incremental indexing: skip files whose content hash hasn't changed
58
141
  const hash = createHash("md5").update(content).digest("hex");
59
142
  const existing = getHash.get(note.relPath);
60
143
  if (existing?.content_hash === hash)
61
144
  continue;
145
+ changedPaths.add(note.relPath);
62
146
  const fm = parseFrontmatter(fullPath);
63
147
  const body = getBody(fullPath);
64
148
  const title = basename(note.relPath, ".md");
65
149
  const folder = dirname(note.relPath).split("/")[0] || "";
66
150
  const tags = getTags(fm);
67
- const summary = body.trim().slice(0, 200);
68
- upsert.run(note.relPath, title, fm.zk_id || null, fm.type || null, fm.status || null, folder, JSON.stringify(tags), summary, fm.date || null, fm.last_modified || null, hash);
69
- // Fully delete+reinsert links simplest way to handle removed wikilinks
70
- deleteLinks.run(note.relPath);
71
- const wikilinks = getWikilinks(fullPath);
72
- for (const target of wikilinks) {
73
- insertLink.run(note.relPath, target, null);
74
- }
151
+ const type = fm.type;
152
+ const summary = extractSummary(fm, body, type);
153
+ upsert.run(note.relPath, title, fm.zk_id || null, type || null, fm.status || null, folder, JSON.stringify(tags), summary, fm.date || null, fm.last_modified || null, hash);
75
154
  if (existing)
76
155
  updated++;
77
156
  else
78
157
  added++;
79
158
  }
80
159
  });
81
- transaction();
160
+ upsertTx();
161
+ // Pass 2: rebuild links for changed notes (all notes now in DB for resolution)
162
+ const insertLink = this.db.prepare("INSERT OR REPLACE INTO links (source, target, link_type) VALUES (?, ?, ?)");
163
+ const deleteSrcLinks = this.db.prepare("DELETE FROM links WHERE source = ?");
164
+ const linkTx = this.db.transaction(() => {
165
+ for (const relPath of changedPaths) {
166
+ deleteSrcLinks.run(relPath);
167
+ const fullPath = join(this.vaultRoot, relPath);
168
+ const wikilinks = getWikilinks(fullPath);
169
+ for (const target of wikilinks) {
170
+ const targetPath = this.resolveWikilink(target) ?? target;
171
+ insertLink.run(relPath, targetPath, null);
172
+ }
173
+ }
174
+ });
175
+ linkTx();
82
176
  this.db.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)").run("last_index", new Date().toISOString());
83
177
  return { added, updated, removed };
84
178
  }
@@ -133,10 +227,9 @@ export class ZkDatabase {
133
227
  return this.db.prepare(sql).all(...params);
134
228
  }
135
229
  getOrphans(folder) {
136
- // LEFT JOIN + NULL: notes where no link row matched = no incoming links
137
230
  let sql = `
138
231
  SELECT n.* FROM notes n
139
- LEFT JOIN links l ON l.target = n.title
232
+ LEFT JOIN links l ON l.target = n.path
140
233
  WHERE l.target IS NULL
141
234
  `;
142
235
  const params = [];
@@ -150,34 +243,56 @@ export class ZkDatabase {
150
243
  getLinksFrom(relPath) {
151
244
  return this.db.prepare("SELECT * FROM links WHERE source = ?").all(relPath);
152
245
  }
153
- getLinksTo(title) {
154
- return this.db.prepare("SELECT * FROM links WHERE target = ?").all(title);
246
+ getLinksTo(path) {
247
+ return this.db.prepare("SELECT * FROM links WHERE target = ?").all(path);
248
+ }
249
+ shareMoc(pathA, pathB) {
250
+ const row = this.db.prepare(`
251
+ SELECT COUNT(*) as c FROM links l1
252
+ JOIN links l2 ON l1.source = l2.source
253
+ JOIN notes n ON l1.source = n.path AND n.type = 'moc'
254
+ WHERE l1.target = ? AND l2.target = ?
255
+ `).get(pathA, pathB);
256
+ return row.c > 0;
155
257
  }
156
258
  findConnections(notePath) {
157
259
  const note = this.getNoteByPath(notePath);
158
260
  if (!note)
159
261
  return [];
160
262
  const noteTags = JSON.parse(note.tags || "[]");
161
- const noteTitle = note.title;
162
263
  const existingLinks = new Set(this.getLinksFrom(notePath).map((l) => l.target));
163
264
  const allNotes = this.db.prepare("SELECT * FROM notes WHERE path != ?").all(notePath);
164
265
  const candidates = [];
165
266
  for (const other of allNotes) {
166
- if (existingLinks.has(other.title))
267
+ if (existingLinks.has(other.path))
167
268
  continue;
168
269
  let score = 0;
169
270
  const reasons = [];
170
- // Scoring: shared tags worth 2pts each, keyword overlap 1pt each.
271
+ // Shared tags: 2pts each
171
272
  const otherTags = JSON.parse(other.tags || "[]");
172
273
  const shared = noteTags.filter((t) => otherTags.includes(t));
173
274
  score += shared.length * 2;
174
275
  reasons.push(...shared.map((t) => `tag:${t}`));
175
- // 4-char minimum filters stopwords; Ukrainian+Latin regex covers both languages
276
+ // Keyword overlap: 6+ chars +2, shorter +1
176
277
  const noteWords = new Set((note.summary || "").toLowerCase().match(/[а-яієїґa-z]{4,}/g) ?? []);
177
278
  const otherWords = (other.summary || "").toLowerCase().match(/[а-яієїґa-z]{4,}/g) ?? [];
178
- const kwMatches = otherWords.filter((w) => noteWords.has(w)).length;
179
- score += kwMatches;
180
- // Threshold ≥2 filters noise (single keyword match alone not enough)
279
+ for (const w of otherWords) {
280
+ if (noteWords.has(w)) {
281
+ score += w.length >= 6 ? 2 : 1;
282
+ reasons.push(`kw:${w}`);
283
+ }
284
+ }
285
+ // Luhmann proximity
286
+ const proximity = luhmannProximity(note.zk_id, other.zk_id);
287
+ if (proximity > 0) {
288
+ score += proximity;
289
+ reasons.push(proximity >= 3 ? "luhmann:sibling" : "luhmann:cousin");
290
+ }
291
+ // MOC boost: both notes referenced by same MOC
292
+ if (score >= 1 && this.shareMoc(notePath, other.path)) {
293
+ score += 2;
294
+ reasons.push("shared-moc");
295
+ }
181
296
  if (score >= 2) {
182
297
  candidates.push({ path: other.path, title: other.title, score, reasons, summary: other.summary || "", type: other.type || "" });
183
298
  }