@tekmidian/pai 0.5.7 → 0.6.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 (137) hide show
  1. package/ARCHITECTURE.md +72 -1
  2. package/README.md +87 -1
  3. package/dist/{auto-route-BG6I_4B1.mjs → auto-route-C-DrW6BL.mjs} +3 -3
  4. package/dist/{auto-route-BG6I_4B1.mjs.map → auto-route-C-DrW6BL.mjs.map} +1 -1
  5. package/dist/cli/index.mjs +1482 -1628
  6. package/dist/cli/index.mjs.map +1 -1
  7. package/dist/clusters-JIDQW65f.mjs +201 -0
  8. package/dist/clusters-JIDQW65f.mjs.map +1 -0
  9. package/dist/{config-Cf92lGX_.mjs → config-BuhHWyOK.mjs} +21 -6
  10. package/dist/config-BuhHWyOK.mjs.map +1 -0
  11. package/dist/daemon/index.mjs +11 -8
  12. package/dist/daemon/index.mjs.map +1 -1
  13. package/dist/{daemon-2ND5WO2j.mjs → daemon-D3hYb5_C.mjs} +669 -218
  14. package/dist/daemon-D3hYb5_C.mjs.map +1 -0
  15. package/dist/daemon-mcp/index.mjs +4597 -4
  16. package/dist/daemon-mcp/index.mjs.map +1 -1
  17. package/dist/db-DdUperSl.mjs +110 -0
  18. package/dist/db-DdUperSl.mjs.map +1 -0
  19. package/dist/{detect-BU3Nx_2L.mjs → detect-CdaA48EI.mjs} +1 -1
  20. package/dist/{detect-BU3Nx_2L.mjs.map → detect-CdaA48EI.mjs.map} +1 -1
  21. package/dist/{detector-Bp-2SM3x.mjs → detector-jGBuYQJM.mjs} +2 -2
  22. package/dist/{detector-Bp-2SM3x.mjs.map → detector-jGBuYQJM.mjs.map} +1 -1
  23. package/dist/{factory-Bzcy70G9.mjs → factory-Ygqe_bVZ.mjs} +7 -5
  24. package/dist/{factory-Bzcy70G9.mjs.map → factory-Ygqe_bVZ.mjs.map} +1 -1
  25. package/dist/helpers-BEST-4Gx.mjs +420 -0
  26. package/dist/helpers-BEST-4Gx.mjs.map +1 -0
  27. package/dist/hooks/capture-all-events.mjs +2 -2
  28. package/dist/hooks/capture-all-events.mjs.map +3 -3
  29. package/dist/hooks/capture-session-summary.mjs +38 -0
  30. package/dist/hooks/capture-session-summary.mjs.map +3 -3
  31. package/dist/hooks/cleanup-session-files.mjs +6 -12
  32. package/dist/hooks/cleanup-session-files.mjs.map +4 -4
  33. package/dist/hooks/context-compression-hook.mjs +93 -104
  34. package/dist/hooks/context-compression-hook.mjs.map +4 -4
  35. package/dist/hooks/initialize-session.mjs +14 -11
  36. package/dist/hooks/initialize-session.mjs.map +4 -4
  37. package/dist/hooks/inject-observations.mjs +220 -0
  38. package/dist/hooks/inject-observations.mjs.map +7 -0
  39. package/dist/hooks/load-core-context.mjs +2 -2
  40. package/dist/hooks/load-core-context.mjs.map +3 -3
  41. package/dist/hooks/load-project-context.mjs +90 -91
  42. package/dist/hooks/load-project-context.mjs.map +4 -4
  43. package/dist/hooks/observe.mjs +354 -0
  44. package/dist/hooks/observe.mjs.map +7 -0
  45. package/dist/hooks/stop-hook.mjs +94 -107
  46. package/dist/hooks/stop-hook.mjs.map +4 -4
  47. package/dist/hooks/sync-todo-to-md.mjs +31 -33
  48. package/dist/hooks/sync-todo-to-md.mjs.map +4 -4
  49. package/dist/index.d.mts +30 -7
  50. package/dist/index.d.mts.map +1 -1
  51. package/dist/index.mjs +5 -8
  52. package/dist/indexer-D53l5d1U.mjs +1 -0
  53. package/dist/{indexer-backend-CIMXedqk.mjs → indexer-backend-jcJFsmB4.mjs} +37 -127
  54. package/dist/indexer-backend-jcJFsmB4.mjs.map +1 -0
  55. package/dist/{ipc-client-Bjg_a1dc.mjs → ipc-client-CoyUHPod.mjs} +2 -7
  56. package/dist/{ipc-client-Bjg_a1dc.mjs.map → ipc-client-CoyUHPod.mjs.map} +1 -1
  57. package/dist/latent-ideas-bTJo6Omd.mjs +191 -0
  58. package/dist/latent-ideas-bTJo6Omd.mjs.map +1 -0
  59. package/dist/neighborhood-BYYbEkUJ.mjs +135 -0
  60. package/dist/neighborhood-BYYbEkUJ.mjs.map +1 -0
  61. package/dist/note-context-BK24bX8Y.mjs +126 -0
  62. package/dist/note-context-BK24bX8Y.mjs.map +1 -0
  63. package/dist/postgres-CKf-EDtS.mjs +846 -0
  64. package/dist/postgres-CKf-EDtS.mjs.map +1 -0
  65. package/dist/{reranker-D7bRAHi6.mjs → reranker-CMNZcfVx.mjs} +1 -1
  66. package/dist/{reranker-D7bRAHi6.mjs.map → reranker-CMNZcfVx.mjs.map} +1 -1
  67. package/dist/{search-_oHfguA5.mjs → search-DC1qhkKn.mjs} +2 -58
  68. package/dist/search-DC1qhkKn.mjs.map +1 -0
  69. package/dist/{sqlite-WWBq7_2C.mjs → sqlite-l-s9xPjY.mjs} +160 -3
  70. package/dist/sqlite-l-s9xPjY.mjs.map +1 -0
  71. package/dist/state-C6_vqz7w.mjs +102 -0
  72. package/dist/state-C6_vqz7w.mjs.map +1 -0
  73. package/dist/stop-words-BaMEGVeY.mjs +326 -0
  74. package/dist/stop-words-BaMEGVeY.mjs.map +1 -0
  75. package/dist/{indexer-CMPOiY1r.mjs → sync-BOsnEj2-.mjs} +14 -216
  76. package/dist/sync-BOsnEj2-.mjs.map +1 -0
  77. package/dist/themes-BvYF0W8T.mjs +148 -0
  78. package/dist/themes-BvYF0W8T.mjs.map +1 -0
  79. package/dist/{tools-DV_lsiCc.mjs → tools-DcaJlYDN.mjs} +162 -273
  80. package/dist/tools-DcaJlYDN.mjs.map +1 -0
  81. package/dist/trace-CRx9lPuc.mjs +137 -0
  82. package/dist/trace-CRx9lPuc.mjs.map +1 -0
  83. package/dist/{vault-indexer-k-kUlaZ-.mjs → vault-indexer-Bi2cRmn7.mjs} +134 -132
  84. package/dist/vault-indexer-Bi2cRmn7.mjs.map +1 -0
  85. package/dist/zettelkasten-cdajbnPr.mjs +708 -0
  86. package/dist/zettelkasten-cdajbnPr.mjs.map +1 -0
  87. package/package.json +1 -2
  88. package/src/hooks/ts/lib/project-utils/index.ts +50 -0
  89. package/src/hooks/ts/lib/project-utils/notify.ts +75 -0
  90. package/src/hooks/ts/lib/project-utils/paths.ts +218 -0
  91. package/src/hooks/ts/lib/project-utils/session-notes.ts +363 -0
  92. package/src/hooks/ts/lib/project-utils/todo.ts +178 -0
  93. package/src/hooks/ts/lib/project-utils/tokens.ts +39 -0
  94. package/src/hooks/ts/lib/project-utils.ts +40 -1018
  95. package/src/hooks/ts/post-tool-use/observe.ts +327 -0
  96. package/src/hooks/ts/session-end/capture-session-summary.ts +41 -0
  97. package/src/hooks/ts/session-start/inject-observations.ts +254 -0
  98. package/dist/chunker-CbnBe0s0.mjs +0 -191
  99. package/dist/chunker-CbnBe0s0.mjs.map +0 -1
  100. package/dist/config-Cf92lGX_.mjs.map +0 -1
  101. package/dist/daemon-2ND5WO2j.mjs.map +0 -1
  102. package/dist/db-Dp8VXIMR.mjs +0 -212
  103. package/dist/db-Dp8VXIMR.mjs.map +0 -1
  104. package/dist/indexer-CMPOiY1r.mjs.map +0 -1
  105. package/dist/indexer-backend-CIMXedqk.mjs.map +0 -1
  106. package/dist/mcp/index.d.mts +0 -1
  107. package/dist/mcp/index.mjs +0 -500
  108. package/dist/mcp/index.mjs.map +0 -1
  109. package/dist/postgres-FXrHDPcE.mjs +0 -358
  110. package/dist/postgres-FXrHDPcE.mjs.map +0 -1
  111. package/dist/schemas-BFIgGntb.mjs +0 -3405
  112. package/dist/schemas-BFIgGntb.mjs.map +0 -1
  113. package/dist/search-_oHfguA5.mjs.map +0 -1
  114. package/dist/sqlite-WWBq7_2C.mjs.map +0 -1
  115. package/dist/tools-DV_lsiCc.mjs.map +0 -1
  116. package/dist/vault-indexer-k-kUlaZ-.mjs.map +0 -1
  117. package/dist/zettelkasten-e-a4rW_6.mjs +0 -901
  118. package/dist/zettelkasten-e-a4rW_6.mjs.map +0 -1
  119. package/templates/README.md +0 -181
  120. package/templates/skills/CORE/Aesthetic.md +0 -333
  121. package/templates/skills/CORE/CONSTITUTION.md +0 -1502
  122. package/templates/skills/CORE/HistorySystem.md +0 -427
  123. package/templates/skills/CORE/HookSystem.md +0 -1082
  124. package/templates/skills/CORE/Prompting.md +0 -509
  125. package/templates/skills/CORE/ProsodyAgentTemplate.md +0 -53
  126. package/templates/skills/CORE/ProsodyGuide.md +0 -416
  127. package/templates/skills/CORE/SKILL.md +0 -741
  128. package/templates/skills/CORE/SkillSystem.md +0 -213
  129. package/templates/skills/CORE/TerminalTabs.md +0 -119
  130. package/templates/skills/CORE/VOICE.md +0 -106
  131. package/templates/skills/createskill-skill.template.md +0 -78
  132. package/templates/skills/history-system.template.md +0 -371
  133. package/templates/skills/hook-system.template.md +0 -913
  134. package/templates/skills/sessions-skill.template.md +0 -102
  135. package/templates/skills/skill-system.template.md +0 -214
  136. package/templates/skills/terminal-tabs.template.md +0 -120
  137. package/templates/templates.md +0 -20
@@ -0,0 +1,708 @@
1
+ import { a as generateEmbedding, n as cosineSimilarity, r as deserializeEmbedding } from "./embeddings-DGRAPAYb.mjs";
2
+ import { t as zettelThemes } from "./themes-BvYF0W8T.mjs";
3
+ import { basename, dirname } from "node:path";
4
+
5
+ //#region src/zettelkasten/explore.ts
6
+ function classifyEdge(source, target) {
7
+ return dirname(source) === dirname(target) ? "sequential" : "associative";
8
+ }
9
+ async function resolveStart(backend, startNote) {
10
+ const files = await backend.getVaultFilesByPaths([startNote]);
11
+ if (files.length > 0) return files[0].vaultPath;
12
+ const alias = await backend.getVaultAlias(startNote);
13
+ if (!alias) return null;
14
+ const canonical = await backend.getVaultFilesByPaths([alias.canonicalPath]);
15
+ return canonical.length > 0 ? canonical[0].vaultPath : null;
16
+ }
17
+ async function getForwardNeighbors(backend, path) {
18
+ return (await backend.getLinksFromSource(path)).filter((l) => l.targetPath !== null).map((l) => l.targetPath);
19
+ }
20
+ async function getBackwardNeighbors(backend, path) {
21
+ return (await backend.getLinksToTarget(path)).map((l) => l.sourcePath);
22
+ }
23
+ async function getFileInfo(backend, path) {
24
+ const [files, health] = await Promise.all([backend.getVaultFilesByPaths([path]), backend.getVaultHealth(path)]);
25
+ return {
26
+ title: files[0]?.title ?? null,
27
+ inbound: health?.inboundCount ?? 0,
28
+ outbound: health?.outboundCount ?? 0
29
+ };
30
+ }
31
+ /**
32
+ * Traverse the Zettelkasten link graph using BFS, following chains of thought
33
+ * from a starting note up to a configurable depth.
34
+ */
35
+ async function zettelExplore(backend, opts) {
36
+ const depth = Math.min(Math.max(opts.depth ?? 3, 1), 10);
37
+ const direction = opts.direction ?? "both";
38
+ const mode = opts.mode ?? "all";
39
+ const root = await resolveStart(backend, opts.startNote);
40
+ if (!root) return {
41
+ root: opts.startNote,
42
+ nodes: [],
43
+ edges: [],
44
+ branchingPoints: [],
45
+ maxDepthReached: false
46
+ };
47
+ const visited = new Set([root]);
48
+ const nodes = [];
49
+ const edges = [];
50
+ let maxDepthReached = false;
51
+ const queue = [{
52
+ path: root,
53
+ depth: 0
54
+ }];
55
+ while (queue.length > 0) {
56
+ const current = queue.shift();
57
+ if (current.depth >= depth) {
58
+ maxDepthReached = true;
59
+ continue;
60
+ }
61
+ const neighbors = [];
62
+ if (direction === "forward" || direction === "both") for (const n of await getForwardNeighbors(backend, current.path)) neighbors.push({
63
+ neighbor: n,
64
+ from: current.path,
65
+ to: n
66
+ });
67
+ if (direction === "backward" || direction === "both") for (const n of await getBackwardNeighbors(backend, current.path)) neighbors.push({
68
+ neighbor: n,
69
+ from: n,
70
+ to: current.path
71
+ });
72
+ for (const { neighbor, from, to } of neighbors) {
73
+ const edgeType = classifyEdge(from, to);
74
+ if (mode !== "all" && edgeType !== mode) continue;
75
+ if (!edges.some((e) => e.from === from && e.to === to)) edges.push({
76
+ from,
77
+ to,
78
+ type: edgeType
79
+ });
80
+ if (!visited.has(neighbor)) {
81
+ visited.add(neighbor);
82
+ const info = await getFileInfo(backend, neighbor);
83
+ nodes.push({
84
+ path: neighbor,
85
+ title: info.title,
86
+ depth: current.depth + 1,
87
+ linkType: edgeType,
88
+ inbound: info.inbound,
89
+ outbound: info.outbound
90
+ });
91
+ queue.push({
92
+ path: neighbor,
93
+ depth: current.depth + 1
94
+ });
95
+ }
96
+ }
97
+ }
98
+ const branchingPoints = nodes.filter((n) => n.outbound > 2).map((n) => n.path);
99
+ if ((await getFileInfo(backend, root)).outbound > 2) branchingPoints.unshift(root);
100
+ return {
101
+ root,
102
+ nodes,
103
+ edges,
104
+ branchingPoints,
105
+ maxDepthReached
106
+ };
107
+ }
108
+
109
+ //#endregion
110
+ //#region src/zettelkasten/surprise.ts
111
+ const MAX_CHUNKS$1 = 5e3;
112
+ const BFS_HOP_CAP = 20;
113
+ async function getFileEmbeddings(backend, projectId) {
114
+ const rows = await backend.getChunksWithEmbeddings(projectId, MAX_CHUNKS$1);
115
+ const byPath = /* @__PURE__ */ new Map();
116
+ for (const row of rows) {
117
+ const vec = deserializeEmbedding(row.embedding);
118
+ const entry = byPath.get(row.path);
119
+ if (!entry) byPath.set(row.path, {
120
+ sum: new Float32Array(vec),
121
+ count: 1,
122
+ text: row.text
123
+ });
124
+ else {
125
+ for (let i = 0; i < vec.length; i++) entry.sum[i] += vec[i];
126
+ entry.count++;
127
+ }
128
+ }
129
+ const result = /* @__PURE__ */ new Map();
130
+ for (const [path, { sum, count, text }] of byPath) {
131
+ const avg = new Float32Array(sum.length);
132
+ for (let i = 0; i < sum.length; i++) avg[i] = sum[i] / count;
133
+ result.set(path, {
134
+ embedding: avg,
135
+ text
136
+ });
137
+ }
138
+ return result;
139
+ }
140
+ async function getReferenceEmbedding(backend, projectId, path) {
141
+ const rows = await backend.getChunksForPath(projectId, path);
142
+ if (rows.length === 0) return {
143
+ embedding: new Float32Array(0),
144
+ found: false
145
+ };
146
+ const embRows = rows.filter((r) => r.embedding !== null);
147
+ if (embRows.length === 0) return {
148
+ embedding: new Float32Array(0),
149
+ found: false
150
+ };
151
+ const dim = deserializeEmbedding(embRows[0].embedding).length;
152
+ const sum = new Float32Array(dim);
153
+ for (const row of embRows) {
154
+ const vec = deserializeEmbedding(row.embedding);
155
+ for (let i = 0; i < dim; i++) sum[i] += vec[i];
156
+ }
157
+ const avg = new Float32Array(dim);
158
+ for (let i = 0; i < dim; i++) avg[i] = sum[i] / embRows.length;
159
+ return {
160
+ embedding: avg,
161
+ found: true
162
+ };
163
+ }
164
+ async function bfsGraphDistance(backend, source, target) {
165
+ if (source === target) return 0;
166
+ const visited = new Set([source]);
167
+ const queue = [{
168
+ path: source,
169
+ hops: 0
170
+ }];
171
+ while (queue.length > 0) {
172
+ const { path, hops } = queue.shift();
173
+ if (hops >= BFS_HOP_CAP) continue;
174
+ const [forwardLinks, backwardLinks] = await Promise.all([backend.getLinksFromSource(path), backend.getLinksToTarget(path)]);
175
+ const neighbors = [...forwardLinks.filter((l) => l.targetPath !== null).map((l) => l.targetPath), ...backwardLinks.map((l) => l.sourcePath)];
176
+ for (const neighbor of neighbors) {
177
+ if (neighbor === target) return hops + 1;
178
+ if (!visited.has(neighbor)) {
179
+ visited.add(neighbor);
180
+ queue.push({
181
+ path: neighbor,
182
+ hops: hops + 1
183
+ });
184
+ }
185
+ }
186
+ }
187
+ return Infinity;
188
+ }
189
+ function getBestChunkText(chunkRows, refEmbedding) {
190
+ const rows = chunkRows.filter((r) => r.embedding !== null);
191
+ if (rows.length === 0) return "";
192
+ let bestText = rows[0].text;
193
+ let bestSim = -Infinity;
194
+ for (const row of rows) {
195
+ const sim = cosineSimilarity(refEmbedding, deserializeEmbedding(row.embedding));
196
+ if (sim > bestSim) {
197
+ bestSim = sim;
198
+ bestText = row.text;
199
+ }
200
+ }
201
+ return bestText.trim().slice(0, 200);
202
+ }
203
+ /**
204
+ * Find notes that are semantically similar to a reference note but graph-distant —
205
+ * revealing surprising conceptual connections across unrelated areas of the Zettelkasten.
206
+ */
207
+ async function zettelSurprise(backend, opts) {
208
+ const limit = opts.limit ?? 10;
209
+ const minSimilarity = opts.minSimilarity ?? .3;
210
+ const minGraphDistance = opts.minGraphDistance ?? 3;
211
+ let { embedding: refEmbedding, found } = await getReferenceEmbedding(backend, opts.vaultProjectId, opts.referencePath);
212
+ if (!found) refEmbedding = await generateEmbedding((await backend.getVaultFilesByPaths([opts.referencePath]))[0]?.title ?? opts.referencePath, true);
213
+ const allFileEmbeddings = await getFileEmbeddings(backend, opts.vaultProjectId);
214
+ allFileEmbeddings.delete(opts.referencePath);
215
+ const semanticCandidates = [];
216
+ for (const [path, { embedding }] of allFileEmbeddings) {
217
+ const sim = cosineSimilarity(refEmbedding, embedding);
218
+ if (sim >= minSimilarity) semanticCandidates.push({
219
+ path,
220
+ sim
221
+ });
222
+ }
223
+ const results = [];
224
+ for (const { path, sim } of semanticCandidates) {
225
+ const graphDistance = await bfsGraphDistance(backend, opts.referencePath, path);
226
+ const effectiveDistance = isFinite(graphDistance) ? graphDistance : BFS_HOP_CAP;
227
+ if (effectiveDistance < minGraphDistance) continue;
228
+ const files = await backend.getVaultFilesByPaths([path]);
229
+ const chunkRows = await backend.getChunksForPath(opts.vaultProjectId, path, 20);
230
+ const surpriseScore = sim * Math.log2(effectiveDistance + 1);
231
+ const sharedSnippet = getBestChunkText(chunkRows, refEmbedding);
232
+ results.push({
233
+ path,
234
+ title: files[0]?.title ?? null,
235
+ cosineSimilarity: sim,
236
+ graphDistance: isFinite(graphDistance) ? graphDistance : Infinity,
237
+ surpriseScore,
238
+ sharedSnippet
239
+ });
240
+ }
241
+ results.sort((a, b) => b.surpriseScore - a.surpriseScore);
242
+ return results.slice(0, limit);
243
+ }
244
+
245
+ //#endregion
246
+ //#region src/zettelkasten/converse.ts
247
+ /** Extract the top-level folder from a vault path (first path segment). */
248
+ function extractDomain(vaultPath) {
249
+ const slash = vaultPath.indexOf("/");
250
+ return slash === -1 ? vaultPath : vaultPath.slice(0, slash);
251
+ }
252
+ /**
253
+ * Expand one level of graph neighbors for a set of paths.
254
+ * Returns all outbound and inbound neighbor paths (excluding already-visited).
255
+ */
256
+ async function expandNeighbors(backend, paths) {
257
+ if (paths.size === 0) return [];
258
+ const pathList = Array.from(paths);
259
+ const [forwardLinks, backwardLinks] = await Promise.all([backend.getVaultLinksFromPaths(pathList), Promise.all(pathList.map((p) => backend.getLinksToTarget(p)))]);
260
+ const neighbors = [];
261
+ for (const link of forwardLinks) if (link.targetPath) neighbors.push(link.targetPath);
262
+ for (const linkList of backwardLinks) for (const link of linkList) neighbors.push(link.sourcePath);
263
+ return neighbors;
264
+ }
265
+ /**
266
+ * Hybrid search combining keyword + semantic results using the StorageBackend.
267
+ */
268
+ async function hybridSearch(backend, query, queryEmbedding, opts) {
269
+ const maxResults = opts.maxResults ?? 10;
270
+ const kw = .5;
271
+ const sw = .5;
272
+ const [keywordResults, semanticResults] = await Promise.all([backend.searchKeyword(query, {
273
+ ...opts,
274
+ maxResults: 50
275
+ }), backend.searchSemantic(queryEmbedding, {
276
+ ...opts,
277
+ maxResults: 50
278
+ })]);
279
+ if (keywordResults.length === 0 && semanticResults.length === 0) return [];
280
+ const keyFor = (r) => `${r.projectId}:${r.path}:${r.startLine}:${r.endLine}`;
281
+ function minMaxNormalize(scores) {
282
+ const min = Math.min(...scores);
283
+ const range = Math.max(...scores) - min;
284
+ if (range === 0) return scores.map(() => 1);
285
+ return scores.map((s) => (s - min) / range);
286
+ }
287
+ const kwNorm = minMaxNormalize(keywordResults.map((r) => r.score));
288
+ const semNorm = minMaxNormalize(semanticResults.map((r) => r.score));
289
+ const combined = /* @__PURE__ */ new Map();
290
+ for (let i = 0; i < keywordResults.length; i++) {
291
+ const r = keywordResults[i];
292
+ const k = keyFor(r);
293
+ combined.set(k, {
294
+ ...r,
295
+ combinedScore: kw * kwNorm[i]
296
+ });
297
+ }
298
+ for (let i = 0; i < semanticResults.length; i++) {
299
+ const r = semanticResults[i];
300
+ const k = keyFor(r);
301
+ const existing = combined.get(k);
302
+ if (existing) existing.combinedScore += sw * semNorm[i];
303
+ else combined.set(k, {
304
+ ...r,
305
+ combinedScore: sw * semNorm[i]
306
+ });
307
+ }
308
+ return Array.from(combined.values()).sort((a, b) => b.combinedScore - a.combinedScore).slice(0, maxResults).map((r) => ({
309
+ ...r,
310
+ score: r.combinedScore
311
+ }));
312
+ }
313
+ /**
314
+ * Let the vault "talk back" — find notes relevant to a question, expand
315
+ * through the link graph, identify cross-domain connections, and return a
316
+ * structured result including a synthesis prompt for an AI to generate insights.
317
+ */
318
+ async function zettelConverse(backend, opts) {
319
+ const depth = Math.max(opts.depth ?? 2, 0);
320
+ const limit = Math.max(opts.limit ?? 15, 1);
321
+ const candidateLimit = 20;
322
+ const queryEmbedding = await generateEmbedding(opts.question, true);
323
+ const searchResults = await hybridSearch(backend, opts.question, queryEmbedding, {
324
+ projectIds: [opts.vaultProjectId],
325
+ maxResults: candidateLimit
326
+ });
327
+ const searchHits = /* @__PURE__ */ new Map();
328
+ for (const r of searchResults) {
329
+ const existing = searchHits.get(r.path);
330
+ if (!existing || r.score > existing.score) searchHits.set(r.path, {
331
+ score: r.score,
332
+ snippet: r.snippet
333
+ });
334
+ }
335
+ const allPaths = new Set(searchHits.keys());
336
+ let frontier = new Set(searchHits.keys());
337
+ for (let d = 0; d < depth; d++) {
338
+ const neighbors = await expandNeighbors(backend, frontier);
339
+ const newFrontier = /* @__PURE__ */ new Set();
340
+ for (const n of neighbors) if (!allPaths.has(n)) {
341
+ allPaths.add(n);
342
+ newFrontier.add(n);
343
+ }
344
+ if (newFrontier.size === 0) break;
345
+ frontier = newFrontier;
346
+ }
347
+ const searchRanked = Array.from(searchHits.entries()).sort((a, b) => b[1].score - a[1].score).map(([path, info]) => ({
348
+ path,
349
+ ...info,
350
+ isSearchResult: true
351
+ }));
352
+ const neighborPaths = Array.from(allPaths).filter((p) => !searchHits.has(p));
353
+ const neighborHealthRows = await Promise.all(neighborPaths.map((p) => backend.getVaultHealth(p)));
354
+ const neighborRanked = neighborPaths.map((path, idx) => ({
355
+ path,
356
+ score: 0,
357
+ snippet: "",
358
+ inbound: neighborHealthRows[idx]?.inboundCount ?? 0,
359
+ isSearchResult: false
360
+ })).sort((a, b) => b.inbound - a.inbound);
361
+ const budgetForNeighbors = Math.max(limit - searchRanked.length, 0);
362
+ const selectedNeighbors = neighborRanked.slice(0, budgetForNeighbors);
363
+ const selectedSearchPaths = searchRanked.slice(0, limit);
364
+ const selectedPaths = new Set([...selectedSearchPaths.map((r) => r.path), ...selectedNeighbors.map((r) => r.path)]);
365
+ const allSelectedPaths = Array.from(selectedPaths);
366
+ const fileRows = await backend.getVaultFilesByPaths(allSelectedPaths);
367
+ const titleMap = new Map(fileRows.map((f) => [f.vaultPath, f.title]));
368
+ const relevantNotes = [];
369
+ for (const r of selectedSearchPaths) {
370
+ if (!selectedPaths.has(r.path)) continue;
371
+ relevantNotes.push({
372
+ path: r.path,
373
+ title: titleMap.get(r.path) ?? null,
374
+ snippet: r.snippet,
375
+ score: r.score,
376
+ domain: extractDomain(r.path)
377
+ });
378
+ }
379
+ for (const r of selectedNeighbors) relevantNotes.push({
380
+ path: r.path,
381
+ title: titleMap.get(r.path) ?? null,
382
+ snippet: r.snippet,
383
+ score: 0,
384
+ domain: extractDomain(r.path)
385
+ });
386
+ let connections = [];
387
+ if (selectedPaths.size > 0) {
388
+ const pathList = Array.from(selectedPaths);
389
+ const pathSet = new Set(pathList);
390
+ const linkRows = await backend.getVaultLinksFromPaths(pathList);
391
+ const edgeCounts = /* @__PURE__ */ new Map();
392
+ for (const link of linkRows) if (link.targetPath && pathSet.has(link.targetPath)) {
393
+ const key = `${link.sourcePath}|||${link.targetPath}`;
394
+ edgeCounts.set(key, (edgeCounts.get(key) ?? 0) + 1);
395
+ }
396
+ for (const [key, cnt] of edgeCounts) {
397
+ const [sourcePath, targetPath] = key.split("|||");
398
+ connections.push({
399
+ fromPath: sourcePath,
400
+ toPath: targetPath,
401
+ fromDomain: extractDomain(sourcePath),
402
+ toDomain: extractDomain(targetPath),
403
+ strength: cnt
404
+ });
405
+ }
406
+ }
407
+ const domainSet = new Set(relevantNotes.map((n) => n.domain));
408
+ const domains = Array.from(domainSet).sort();
409
+ const crossDomainConnections = connections.filter((c) => c.fromDomain !== c.toDomain);
410
+ const notesSummary = relevantNotes.map((n, i) => {
411
+ const title = n.title ? `"${n.title}"` : "(untitled)";
412
+ const domain = n.domain;
413
+ const scoreLabel = n.score > 0 ? ` [relevance: ${n.score.toFixed(3)}]` : " [context]";
414
+ const snippet = n.snippet.trim().slice(0, 300);
415
+ return `${i + 1}. [${domain}] ${title}${scoreLabel}\n Path: ${n.path}\n "${snippet}"`;
416
+ }).join("\n\n");
417
+ const connectionSummary = crossDomainConnections.length > 0 ? crossDomainConnections.map((c) => `- "${c.fromPath}" (${c.fromDomain}) → "${c.toPath}" (${c.toDomain}) [strength: ${c.strength}]`).join("\n") : "(no cross-domain connections found)";
418
+ const domainList = domains.join(", ");
419
+ return {
420
+ relevantNotes,
421
+ connections: crossDomainConnections,
422
+ domains,
423
+ synthesisPrompt: `You are a Zettelkasten research assistant. The vault has surfaced the following notes in response to this question:
424
+
425
+ QUESTION: ${opts.question}
426
+
427
+ ---
428
+
429
+ RELEVANT NOTES (${relevantNotes.length} notes across ${domains.length} domain(s): ${domainList}):
430
+
431
+ ${notesSummary}
432
+
433
+ ---
434
+
435
+ CROSS-DOMAIN CONNECTIONS (links bridging different knowledge areas):
436
+
437
+ ${connectionSummary}
438
+
439
+ ---
440
+
441
+ SYNTHESIS TASK:
442
+
443
+ Based on these notes and the connections between them, please:
444
+
445
+ 1. Identify the key insights that emerge in direct response to the question.
446
+ 2. Highlight any unexpected connections between notes from different domains (${domainList}).
447
+ 3. Point out tensions, contradictions, or open questions the vault raises but does not resolve.
448
+ 4. Suggest what is notably absent — what the vault does NOT yet contain that would strengthen the understanding of this topic.
449
+ 5. Propose 2-3 new notes that would meaningfully extend this knowledge cluster.
450
+
451
+ Think like a scholar who has deeply internalized these ideas and is now synthesizing them for the first time.`
452
+ };
453
+ }
454
+
455
+ //#endregion
456
+ //#region src/zettelkasten/health.ts
457
+ function countComponents(nodes, edges) {
458
+ if (nodes.length === 0) return 0;
459
+ const parent = /* @__PURE__ */ new Map();
460
+ const rank = /* @__PURE__ */ new Map();
461
+ for (const n of nodes) {
462
+ parent.set(n, n);
463
+ rank.set(n, 0);
464
+ }
465
+ function find(x) {
466
+ let root = x;
467
+ while (parent.get(root) !== root) root = parent.get(root);
468
+ let current = x;
469
+ while (current !== root) {
470
+ const next = parent.get(current);
471
+ parent.set(current, root);
472
+ current = next;
473
+ }
474
+ return root;
475
+ }
476
+ function union(a, b) {
477
+ const ra = find(a);
478
+ const rb = find(b);
479
+ if (ra === rb) return;
480
+ const rankA = rank.get(ra) ?? 0;
481
+ const rankB = rank.get(rb) ?? 0;
482
+ if (rankA < rankB) parent.set(ra, rb);
483
+ else if (rankA > rankB) parent.set(rb, ra);
484
+ else {
485
+ parent.set(rb, ra);
486
+ rank.set(ra, rankA + 1);
487
+ }
488
+ }
489
+ for (const { source, target } of edges) if (parent.has(source) && parent.has(target)) union(source, target);
490
+ const roots = /* @__PURE__ */ new Set();
491
+ for (const n of nodes) roots.add(find(n));
492
+ return roots.size;
493
+ }
494
+ /**
495
+ * Audit the structural health of the Zettelkasten vault using graph metrics.
496
+ */
497
+ async function zettelHealth(backend, opts) {
498
+ const options = opts ?? {};
499
+ const scope = options.scope ?? "full";
500
+ const include = options.include ?? [
501
+ "dead_links",
502
+ "orphans",
503
+ "disconnected",
504
+ "low_connectivity"
505
+ ];
506
+ const computedAt = Date.now();
507
+ let totalFiles = 0;
508
+ if (scope === "full") totalFiles = await backend.countVaultFiles();
509
+ else if (scope === "project") {
510
+ const prefix = options.projectPath ?? "";
511
+ totalFiles = await backend.countVaultFilesWithPrefix(prefix);
512
+ } else {
513
+ const cutoff = computedAt - (options.recentDays ?? 30) * 864e5;
514
+ totalFiles = await backend.countVaultFilesAfter(cutoff);
515
+ }
516
+ let totalLinks = 0;
517
+ if (scope === "full") totalLinks = (await backend.getVaultLinkGraph()).length;
518
+ else if (scope === "project") {
519
+ const prefix = options.projectPath ?? "";
520
+ totalLinks = await backend.countVaultLinksWithPrefix(prefix);
521
+ } else {
522
+ const cutoff = computedAt - (options.recentDays ?? 30) * 864e5;
523
+ totalLinks = await backend.countVaultLinksAfter(cutoff);
524
+ }
525
+ let deadLinks = [];
526
+ if (include.includes("dead_links")) if (scope === "full") deadLinks = await backend.getDeadLinksWithLineNumbers();
527
+ else if (scope === "project") {
528
+ const prefix = options.projectPath ?? "";
529
+ deadLinks = await backend.getDeadLinksWithPrefix(prefix);
530
+ } else {
531
+ const cutoff = computedAt - (options.recentDays ?? 30) * 864e5;
532
+ deadLinks = await backend.getDeadLinksAfter(cutoff);
533
+ }
534
+ let orphans = [];
535
+ if (include.includes("orphans")) if (scope === "full") orphans = (await backend.getOrphans()).map((r) => r.vaultPath);
536
+ else if (scope === "project") {
537
+ const prefix = options.projectPath ?? "";
538
+ orphans = await backend.getOrphansWithPrefix(prefix);
539
+ } else {
540
+ const cutoff = computedAt - (options.recentDays ?? 30) * 864e5;
541
+ orphans = await backend.getOrphansAfter(cutoff);
542
+ }
543
+ let disconnectedClusters = 1;
544
+ if (include.includes("disconnected")) {
545
+ let allNodes;
546
+ let allEdges;
547
+ if (scope === "full") [allNodes, allEdges] = await Promise.all([backend.getAllVaultFilePaths(), backend.getVaultLinkEdges()]);
548
+ else if (scope === "project") {
549
+ const prefix = options.projectPath ?? "";
550
+ [allNodes, allEdges] = await Promise.all([backend.getVaultFilePathsWithPrefix(prefix), backend.getVaultLinkEdgesWithPrefix(prefix)]);
551
+ } else {
552
+ const cutoff = computedAt - (options.recentDays ?? 30) * 864e5;
553
+ [allNodes, allEdges] = await Promise.all([backend.getVaultFilePathsAfter(cutoff), backend.getVaultLinkEdgesAfter(cutoff)]);
554
+ }
555
+ disconnectedClusters = countComponents(allNodes, allEdges);
556
+ }
557
+ let lowConnectivity = [];
558
+ if (include.includes("low_connectivity")) if (scope === "full") lowConnectivity = await backend.getLowConnectivity();
559
+ else if (scope === "project") {
560
+ const prefix = options.projectPath ?? "";
561
+ lowConnectivity = await backend.getLowConnectivityWithPrefix(prefix);
562
+ } else {
563
+ const cutoff = computedAt - (options.recentDays ?? 30) * 864e5;
564
+ lowConnectivity = await backend.getLowConnectivityAfter(cutoff);
565
+ }
566
+ const deadRatio = totalLinks > 0 ? deadLinks.length / totalLinks : 0;
567
+ const orphanRatio = totalFiles > 0 ? orphans.length / totalFiles : 0;
568
+ const lowConnRatio = totalFiles > 0 ? lowConnectivity.length / totalFiles : 0;
569
+ const healthScore = Math.round(100 * (1 - deadRatio) * (1 - orphanRatio * .5) * (1 - lowConnRatio * .3));
570
+ return {
571
+ totalFiles,
572
+ totalLinks,
573
+ deadLinks,
574
+ orphans,
575
+ disconnectedClusters,
576
+ lowConnectivity,
577
+ healthScore,
578
+ computedAt
579
+ };
580
+ }
581
+
582
+ //#endregion
583
+ //#region src/zettelkasten/suggest.ts
584
+ const MAX_CHUNKS = 5e3;
585
+ const SEMANTIC_WEIGHT = .5;
586
+ const TAG_WEIGHT = .2;
587
+ const NEIGHBOR_WEIGHT = .3;
588
+ function extractTagsFromChunkTexts(texts) {
589
+ const tags = /* @__PURE__ */ new Set();
590
+ for (const text of texts) {
591
+ const match = text.match(/^tags:\s*\n((?:[ \t]*-[ \t]*.+\n?)*)/m);
592
+ if (!match) continue;
593
+ const lines = match[1].split("\n");
594
+ for (const line of lines) {
595
+ const tagMatch = line.match(/^[ \t]*-[ \t]*(.+)/);
596
+ if (tagMatch) {
597
+ const tag = tagMatch[1].trim().toLowerCase();
598
+ if (tag) tags.add(tag);
599
+ }
600
+ }
601
+ }
602
+ return tags;
603
+ }
604
+ function jaccardSimilarity(a, b) {
605
+ if (a.size === 0 && b.size === 0) return 0;
606
+ let intersection = 0;
607
+ for (const tag of a) if (b.has(tag)) intersection++;
608
+ const union = a.size + b.size - intersection;
609
+ return union === 0 ? 0 : intersection / union;
610
+ }
611
+ function buildReason(semanticScore, tagScore, neighborScore, neighborCount) {
612
+ const signals = [
613
+ {
614
+ label: `Semantically similar (${semanticScore.toFixed(2)})`,
615
+ value: semanticScore * SEMANTIC_WEIGHT
616
+ },
617
+ {
618
+ label: `Shared tags (${tagScore.toFixed(2)} Jaccard)`,
619
+ value: tagScore * TAG_WEIGHT
620
+ },
621
+ {
622
+ label: `Linked by ${neighborCount} mutual connection${neighborCount !== 1 ? "s" : ""}`,
623
+ value: neighborScore * NEIGHBOR_WEIGHT
624
+ }
625
+ ];
626
+ signals.sort((a, b) => b.value - a.value);
627
+ return signals[0].label;
628
+ }
629
+ function suggestedWikilink(vaultPath) {
630
+ const base = basename(vaultPath);
631
+ return `[[${base.endsWith(".md") ? base.slice(0, -3) : base}]]`;
632
+ }
633
+ /**
634
+ * Proactively find notes worth linking to a given note, combining semantic similarity,
635
+ * shared tags, and graph-neighborhood signals into a ranked list of suggestions.
636
+ */
637
+ async function zettelSuggest(backend, opts) {
638
+ const limit = opts.limit ?? 5;
639
+ const excludeLinked = opts.excludeLinked ?? true;
640
+ const outboundLinks = await backend.getLinksFromSource(opts.notePath);
641
+ const linkedPaths = new Set(outboundLinks.filter((l) => l.targetPath !== null).map((l) => l.targetPath));
642
+ const chunkRows = await backend.getChunksWithEmbeddings(opts.vaultProjectId, MAX_CHUNKS);
643
+ const byPath = /* @__PURE__ */ new Map();
644
+ for (const row of chunkRows) {
645
+ const vec = deserializeEmbedding(row.embedding);
646
+ const entry = byPath.get(row.path);
647
+ if (!entry) byPath.set(row.path, {
648
+ sum: new Float32Array(vec),
649
+ count: 1
650
+ });
651
+ else {
652
+ for (let i = 0; i < vec.length; i++) entry.sum[i] += vec[i];
653
+ entry.count++;
654
+ }
655
+ }
656
+ const allEmbeddings = /* @__PURE__ */ new Map();
657
+ for (const [path, { sum, count }] of byPath) {
658
+ const avg = new Float32Array(sum.length);
659
+ for (let i = 0; i < sum.length; i++) avg[i] = sum[i] / count;
660
+ allEmbeddings.set(path, avg);
661
+ }
662
+ allEmbeddings.delete(opts.notePath);
663
+ const sourceEmbedding = allEmbeddings.get(opts.notePath) ?? null;
664
+ const sourceTags = extractTagsFromChunkTexts((await backend.getChunksForPath(opts.vaultProjectId, opts.notePath, 5)).map((r) => r.text));
665
+ const directTargets = (await backend.getLinksFromSource(opts.notePath)).filter((l) => l.targetPath !== null).map((l) => l.targetPath);
666
+ const friendLinkCounts = /* @__PURE__ */ new Map();
667
+ for (const target of directTargets) {
668
+ const friendLinks = await backend.getLinksFromSource(target);
669
+ for (const link of friendLinks) if (link.targetPath && link.targetPath !== opts.notePath) friendLinkCounts.set(link.targetPath, (friendLinkCounts.get(link.targetPath) ?? 0) + 1);
670
+ }
671
+ const maxFriendLinks = Math.max(1, ...friendLinkCounts.values());
672
+ const allFiles = await backend.getAllVaultFiles();
673
+ const suggestions = [];
674
+ for (const fileRow of allFiles) {
675
+ const vault_path = fileRow.vaultPath;
676
+ const title = fileRow.title;
677
+ if (vault_path === opts.notePath) continue;
678
+ if (excludeLinked && linkedPaths.has(vault_path)) continue;
679
+ let semanticScore = 0;
680
+ if (sourceEmbedding) {
681
+ const candidateEmbedding = allEmbeddings.get(vault_path);
682
+ if (candidateEmbedding) semanticScore = Math.max(0, cosineSimilarity(sourceEmbedding, candidateEmbedding));
683
+ }
684
+ let tagScore = 0;
685
+ if (allEmbeddings.has(vault_path)) tagScore = jaccardSimilarity(sourceTags, extractTagsFromChunkTexts((await backend.getChunksForPath(opts.vaultProjectId, vault_path, 5)).map((r) => r.text)));
686
+ const friendCount = friendLinkCounts.get(vault_path) ?? 0;
687
+ const neighborScore = friendCount / maxFriendLinks;
688
+ const score = SEMANTIC_WEIGHT * semanticScore + TAG_WEIGHT * tagScore + NEIGHBOR_WEIGHT * neighborScore;
689
+ if (score <= 0) continue;
690
+ const reason = buildReason(semanticScore, tagScore, neighborScore, friendCount);
691
+ suggestions.push({
692
+ path: vault_path,
693
+ title,
694
+ score,
695
+ semanticScore,
696
+ tagScore,
697
+ neighborScore,
698
+ reason,
699
+ suggestedWikilink: suggestedWikilink(vault_path)
700
+ });
701
+ }
702
+ suggestions.sort((a, b) => b.score - a.score);
703
+ return suggestions.slice(0, limit);
704
+ }
705
+
706
+ //#endregion
707
+ export { zettelConverse, zettelExplore, zettelHealth, zettelSuggest, zettelSurprise, zettelThemes };
708
+ //# sourceMappingURL=zettelkasten-cdajbnPr.mjs.map