stellavault 0.1.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 (294) hide show
  1. package/.env.example +12 -0
  2. package/CLAUDE.md +39 -0
  3. package/CONTRIBUTING.md +65 -0
  4. package/LICENSE +21 -0
  5. package/README.md +182 -0
  6. package/memory/MEMORY.md +25 -0
  7. package/package.json +33 -0
  8. package/packages/cli/bin/ekh.js +2 -0
  9. package/packages/cli/bin/stellavault.js +2 -0
  10. package/packages/cli/dist/commands/brief-cmd.d.ts +2 -0
  11. package/packages/cli/dist/commands/brief-cmd.d.ts.map +1 -0
  12. package/packages/cli/dist/commands/brief-cmd.js +82 -0
  13. package/packages/cli/dist/commands/brief-cmd.js.map +1 -0
  14. package/packages/cli/dist/commands/capture-cmd.d.ts +7 -0
  15. package/packages/cli/dist/commands/capture-cmd.d.ts.map +1 -0
  16. package/packages/cli/dist/commands/capture-cmd.js +31 -0
  17. package/packages/cli/dist/commands/capture-cmd.js.map +1 -0
  18. package/packages/cli/dist/commands/card-cmd.d.ts +4 -0
  19. package/packages/cli/dist/commands/card-cmd.d.ts.map +1 -0
  20. package/packages/cli/dist/commands/card-cmd.js +26 -0
  21. package/packages/cli/dist/commands/card-cmd.js.map +1 -0
  22. package/packages/cli/dist/commands/clip-cmd.d.ts +4 -0
  23. package/packages/cli/dist/commands/clip-cmd.d.ts.map +1 -0
  24. package/packages/cli/dist/commands/clip-cmd.js +151 -0
  25. package/packages/cli/dist/commands/clip-cmd.js.map +1 -0
  26. package/packages/cli/dist/commands/cloud-cmd.d.ts +4 -0
  27. package/packages/cli/dist/commands/cloud-cmd.d.ts.map +1 -0
  28. package/packages/cli/dist/commands/cloud-cmd.js +64 -0
  29. package/packages/cli/dist/commands/cloud-cmd.js.map +1 -0
  30. package/packages/cli/dist/commands/contradictions-cmd.d.ts +2 -0
  31. package/packages/cli/dist/commands/contradictions-cmd.d.ts.map +1 -0
  32. package/packages/cli/dist/commands/contradictions-cmd.js +34 -0
  33. package/packages/cli/dist/commands/contradictions-cmd.js.map +1 -0
  34. package/packages/cli/dist/commands/decay-cmd.d.ts +2 -0
  35. package/packages/cli/dist/commands/decay-cmd.d.ts.map +1 -0
  36. package/packages/cli/dist/commands/decay-cmd.js +48 -0
  37. package/packages/cli/dist/commands/decay-cmd.js.map +1 -0
  38. package/packages/cli/dist/commands/digest-cmd.d.ts +4 -0
  39. package/packages/cli/dist/commands/digest-cmd.d.ts.map +1 -0
  40. package/packages/cli/dist/commands/digest-cmd.js +79 -0
  41. package/packages/cli/dist/commands/digest-cmd.js.map +1 -0
  42. package/packages/cli/dist/commands/duplicates-cmd.d.ts +4 -0
  43. package/packages/cli/dist/commands/duplicates-cmd.d.ts.map +1 -0
  44. package/packages/cli/dist/commands/duplicates-cmd.js +30 -0
  45. package/packages/cli/dist/commands/duplicates-cmd.js.map +1 -0
  46. package/packages/cli/dist/commands/federate-cmd.d.ts +5 -0
  47. package/packages/cli/dist/commands/federate-cmd.d.ts.map +1 -0
  48. package/packages/cli/dist/commands/federate-cmd.js +217 -0
  49. package/packages/cli/dist/commands/federate-cmd.js.map +1 -0
  50. package/packages/cli/dist/commands/gaps-cmd.d.ts +2 -0
  51. package/packages/cli/dist/commands/gaps-cmd.d.ts.map +1 -0
  52. package/packages/cli/dist/commands/gaps-cmd.js +33 -0
  53. package/packages/cli/dist/commands/gaps-cmd.js.map +1 -0
  54. package/packages/cli/dist/commands/graph-cmd.d.ts +2 -0
  55. package/packages/cli/dist/commands/graph-cmd.d.ts.map +1 -0
  56. package/packages/cli/dist/commands/graph-cmd.js +77 -0
  57. package/packages/cli/dist/commands/graph-cmd.js.map +1 -0
  58. package/packages/cli/dist/commands/index-cmd.d.ts +2 -0
  59. package/packages/cli/dist/commands/index-cmd.d.ts.map +1 -0
  60. package/packages/cli/dist/commands/index-cmd.js +57 -0
  61. package/packages/cli/dist/commands/index-cmd.js.map +1 -0
  62. package/packages/cli/dist/commands/init-cmd.d.ts +2 -0
  63. package/packages/cli/dist/commands/init-cmd.d.ts.map +1 -0
  64. package/packages/cli/dist/commands/init-cmd.js +123 -0
  65. package/packages/cli/dist/commands/init-cmd.js.map +1 -0
  66. package/packages/cli/dist/commands/learn-cmd.d.ts +2 -0
  67. package/packages/cli/dist/commands/learn-cmd.d.ts.map +1 -0
  68. package/packages/cli/dist/commands/learn-cmd.js +48 -0
  69. package/packages/cli/dist/commands/learn-cmd.js.map +1 -0
  70. package/packages/cli/dist/commands/pack-cmd.d.ts +15 -0
  71. package/packages/cli/dist/commands/pack-cmd.d.ts.map +1 -0
  72. package/packages/cli/dist/commands/pack-cmd.js +93 -0
  73. package/packages/cli/dist/commands/pack-cmd.js.map +1 -0
  74. package/packages/cli/dist/commands/review-cmd.d.ts +4 -0
  75. package/packages/cli/dist/commands/review-cmd.d.ts.map +1 -0
  76. package/packages/cli/dist/commands/review-cmd.js +107 -0
  77. package/packages/cli/dist/commands/review-cmd.js.map +1 -0
  78. package/packages/cli/dist/commands/search-cmd.d.ts +4 -0
  79. package/packages/cli/dist/commands/search-cmd.d.ts.map +1 -0
  80. package/packages/cli/dist/commands/search-cmd.js +38 -0
  81. package/packages/cli/dist/commands/search-cmd.js.map +1 -0
  82. package/packages/cli/dist/commands/serve-cmd.d.ts +2 -0
  83. package/packages/cli/dist/commands/serve-cmd.d.ts.map +1 -0
  84. package/packages/cli/dist/commands/serve-cmd.js +14 -0
  85. package/packages/cli/dist/commands/serve-cmd.js.map +1 -0
  86. package/packages/cli/dist/commands/status-cmd.d.ts +2 -0
  87. package/packages/cli/dist/commands/status-cmd.d.ts.map +1 -0
  88. package/packages/cli/dist/commands/status-cmd.js +33 -0
  89. package/packages/cli/dist/commands/status-cmd.js.map +1 -0
  90. package/packages/cli/dist/commands/sync-cmd.d.ts +5 -0
  91. package/packages/cli/dist/commands/sync-cmd.d.ts.map +1 -0
  92. package/packages/cli/dist/commands/sync-cmd.js +62 -0
  93. package/packages/cli/dist/commands/sync-cmd.js.map +1 -0
  94. package/packages/cli/dist/commands/vault-cmd.d.ts +10 -0
  95. package/packages/cli/dist/commands/vault-cmd.d.ts.map +1 -0
  96. package/packages/cli/dist/commands/vault-cmd.js +54 -0
  97. package/packages/cli/dist/commands/vault-cmd.js.map +1 -0
  98. package/packages/cli/dist/index.d.ts +2 -0
  99. package/packages/cli/dist/index.d.ts.map +1 -0
  100. package/packages/cli/dist/index.js +156 -0
  101. package/packages/cli/dist/index.js.map +1 -0
  102. package/packages/cli/package.json +24 -0
  103. package/packages/cli/src/commands/brief-cmd.ts +87 -0
  104. package/packages/cli/src/commands/capture-cmd.ts +34 -0
  105. package/packages/cli/src/commands/card-cmd.ts +29 -0
  106. package/packages/cli/src/commands/clip-cmd.ts +172 -0
  107. package/packages/cli/src/commands/cloud-cmd.ts +75 -0
  108. package/packages/cli/src/commands/contradictions-cmd.ts +41 -0
  109. package/packages/cli/src/commands/decay-cmd.ts +57 -0
  110. package/packages/cli/src/commands/digest-cmd.ts +89 -0
  111. package/packages/cli/src/commands/duplicates-cmd.ts +38 -0
  112. package/packages/cli/src/commands/federate-cmd.ts +236 -0
  113. package/packages/cli/src/commands/gaps-cmd.ts +40 -0
  114. package/packages/cli/src/commands/graph-cmd.ts +88 -0
  115. package/packages/cli/src/commands/index-cmd.ts +65 -0
  116. package/packages/cli/src/commands/init-cmd.ts +145 -0
  117. package/packages/cli/src/commands/learn-cmd.ts +56 -0
  118. package/packages/cli/src/commands/pack-cmd.ts +121 -0
  119. package/packages/cli/src/commands/review-cmd.ts +125 -0
  120. package/packages/cli/src/commands/search-cmd.ts +45 -0
  121. package/packages/cli/src/commands/serve-cmd.ts +17 -0
  122. package/packages/cli/src/commands/status-cmd.ts +37 -0
  123. package/packages/cli/src/commands/sync-cmd.ts +68 -0
  124. package/packages/cli/src/commands/vault-cmd.ts +64 -0
  125. package/packages/cli/src/index.ts +187 -0
  126. package/packages/core/package.json +40 -0
  127. package/packages/core/src/api/dashboard.ts +138 -0
  128. package/packages/core/src/api/graph-data.ts +286 -0
  129. package/packages/core/src/api/pwa.ts +82 -0
  130. package/packages/core/src/api/server.ts +660 -0
  131. package/packages/core/src/capture/voice.ts +168 -0
  132. package/packages/core/src/cloud/index.ts +2 -0
  133. package/packages/core/src/cloud/sync.ts +167 -0
  134. package/packages/core/src/config.ts +82 -0
  135. package/packages/core/src/federation/credits.ts +80 -0
  136. package/packages/core/src/federation/hyperswarm.d.ts +19 -0
  137. package/packages/core/src/federation/identity.ts +90 -0
  138. package/packages/core/src/federation/index.ts +8 -0
  139. package/packages/core/src/federation/node.ts +235 -0
  140. package/packages/core/src/federation/privacy.ts +52 -0
  141. package/packages/core/src/federation/reputation.ts +202 -0
  142. package/packages/core/src/federation/search.ts +129 -0
  143. package/packages/core/src/federation/sharing.ts +165 -0
  144. package/packages/core/src/federation/trust.ts +76 -0
  145. package/packages/core/src/federation/types.ts +25 -0
  146. package/packages/core/src/i18n/index.ts +85 -0
  147. package/packages/core/src/index.ts +133 -0
  148. package/packages/core/src/indexer/chunker.ts +180 -0
  149. package/packages/core/src/indexer/embedder.ts +9 -0
  150. package/packages/core/src/indexer/index.ts +113 -0
  151. package/packages/core/src/indexer/local-embedder.ts +35 -0
  152. package/packages/core/src/indexer/scanner.ts +142 -0
  153. package/packages/core/src/indexer/watcher.ts +62 -0
  154. package/packages/core/src/intelligence/contradiction-detector.ts +134 -0
  155. package/packages/core/src/intelligence/decay-engine.ts +229 -0
  156. package/packages/core/src/intelligence/duplicate-detector.ts +71 -0
  157. package/packages/core/src/intelligence/fsrs.ts +79 -0
  158. package/packages/core/src/intelligence/gap-detector.ts +109 -0
  159. package/packages/core/src/intelligence/learning-path.ts +86 -0
  160. package/packages/core/src/intelligence/notifications.ts +106 -0
  161. package/packages/core/src/intelligence/predictive-gaps.ts +94 -0
  162. package/packages/core/src/intelligence/semantic-versioning.ts +97 -0
  163. package/packages/core/src/intelligence/types.ts +28 -0
  164. package/packages/core/src/mcp/custom-tools.ts +97 -0
  165. package/packages/core/src/mcp/index.ts +1 -0
  166. package/packages/core/src/mcp/server.ts +142 -0
  167. package/packages/core/src/mcp/tools/agentic-graph.ts +96 -0
  168. package/packages/core/src/mcp/tools/brief.ts +49 -0
  169. package/packages/core/src/mcp/tools/decay.ts +40 -0
  170. package/packages/core/src/mcp/tools/decision-journal.ts +95 -0
  171. package/packages/core/src/mcp/tools/export.ts +72 -0
  172. package/packages/core/src/mcp/tools/federated-search.ts +43 -0
  173. package/packages/core/src/mcp/tools/generate-claude-md.ts +130 -0
  174. package/packages/core/src/mcp/tools/get-document.ts +26 -0
  175. package/packages/core/src/mcp/tools/get-related.ts +41 -0
  176. package/packages/core/src/mcp/tools/learning-path.ts +52 -0
  177. package/packages/core/src/mcp/tools/list-topics.ts +20 -0
  178. package/packages/core/src/mcp/tools/search.ts +35 -0
  179. package/packages/core/src/mcp/tools/snapshot.ts +98 -0
  180. package/packages/core/src/multi-vault/index.ts +118 -0
  181. package/packages/core/src/pack/creator.ts +127 -0
  182. package/packages/core/src/pack/exporter.ts +21 -0
  183. package/packages/core/src/pack/importer.ts +82 -0
  184. package/packages/core/src/pack/index.ts +5 -0
  185. package/packages/core/src/pack/marketplace.ts +103 -0
  186. package/packages/core/src/pack/pii-masker.ts +38 -0
  187. package/packages/core/src/pack/types.ts +39 -0
  188. package/packages/core/src/plugins/index.ts +100 -0
  189. package/packages/core/src/plugins/webhooks.ts +110 -0
  190. package/packages/core/src/search/bm25.ts +16 -0
  191. package/packages/core/src/search/index.ts +83 -0
  192. package/packages/core/src/search/rrf.ts +31 -0
  193. package/packages/core/src/search/semantic.ts +15 -0
  194. package/packages/core/src/store/index.ts +2 -0
  195. package/packages/core/src/store/sqlite-vec.ts +290 -0
  196. package/packages/core/src/store/types.ts +22 -0
  197. package/packages/core/src/team/index.ts +126 -0
  198. package/packages/core/src/types/chunk.ts +25 -0
  199. package/packages/core/src/types/document.ts +24 -0
  200. package/packages/core/src/types/graph.ts +44 -0
  201. package/packages/core/src/types/index.ts +15 -0
  202. package/packages/core/src/types/search.ts +38 -0
  203. package/packages/core/src/utils/retry.ts +85 -0
  204. package/packages/core/tests/api-card.test.ts +60 -0
  205. package/packages/core/tests/api-routes.test.ts +98 -0
  206. package/packages/core/tests/bm25.test.ts +87 -0
  207. package/packages/core/tests/chunker.test.ts +48 -0
  208. package/packages/core/tests/cluster.test.ts +75 -0
  209. package/packages/core/tests/constellation.test.ts +77 -0
  210. package/packages/core/tests/export-utils.test.ts +97 -0
  211. package/packages/core/tests/fsrs.test.ts +96 -0
  212. package/packages/core/tests/gesture-detector.test.ts +45 -0
  213. package/packages/core/tests/graph-data.test.ts +87 -0
  214. package/packages/core/tests/layout.test.ts +83 -0
  215. package/packages/core/tests/mcp.test.ts +148 -0
  216. package/packages/core/tests/pack.test.ts +127 -0
  217. package/packages/core/tests/pii-masker.test.ts +42 -0
  218. package/packages/core/tests/profile-card.test.ts +62 -0
  219. package/packages/core/tests/rrf.test.ts +29 -0
  220. package/packages/core/tests/search-integration.test.ts +139 -0
  221. package/packages/core/tests/store.test.ts +80 -0
  222. package/packages/graph/click-result.png +0 -0
  223. package/packages/graph/index.html +17 -0
  224. package/packages/graph/package.json +32 -0
  225. package/packages/graph/src/App.tsx +7 -0
  226. package/packages/graph/src/api/client.ts +39 -0
  227. package/packages/graph/src/components/ClusterFilter.tsx +73 -0
  228. package/packages/graph/src/components/ConstellationView.tsx +232 -0
  229. package/packages/graph/src/components/ExportPanel.tsx +177 -0
  230. package/packages/graph/src/components/Graph3D.tsx +230 -0
  231. package/packages/graph/src/components/GraphEdges.tsx +100 -0
  232. package/packages/graph/src/components/GraphNodes.tsx +386 -0
  233. package/packages/graph/src/components/HealthDashboard.tsx +173 -0
  234. package/packages/graph/src/components/Layout.tsx +214 -0
  235. package/packages/graph/src/components/MotionOverlay.tsx +81 -0
  236. package/packages/graph/src/components/MotionToggle.tsx +33 -0
  237. package/packages/graph/src/components/MultiverseView.tsx +286 -0
  238. package/packages/graph/src/components/NodeDetail.tsx +232 -0
  239. package/packages/graph/src/components/PulseParticle.tsx +232 -0
  240. package/packages/graph/src/components/SearchBar.tsx +107 -0
  241. package/packages/graph/src/components/StarField.tsx +197 -0
  242. package/packages/graph/src/components/StatusBar.tsx +53 -0
  243. package/packages/graph/src/components/Timeline.tsx +148 -0
  244. package/packages/graph/src/components/ToolsPanel.tsx +512 -0
  245. package/packages/graph/src/components/Tooltip.tsx +100 -0
  246. package/packages/graph/src/components/TypeFilter.tsx +131 -0
  247. package/packages/graph/src/embed/EmbedGraph.tsx +144 -0
  248. package/packages/graph/src/hooks/useConstellationLOD.ts +76 -0
  249. package/packages/graph/src/hooks/useDecay.ts +37 -0
  250. package/packages/graph/src/hooks/useExport.ts +165 -0
  251. package/packages/graph/src/hooks/useGraph.ts +69 -0
  252. package/packages/graph/src/hooks/useKeyboardNav.ts +122 -0
  253. package/packages/graph/src/hooks/useLayout.ts +45 -0
  254. package/packages/graph/src/hooks/useMotion.ts +120 -0
  255. package/packages/graph/src/hooks/usePulse.ts +58 -0
  256. package/packages/graph/src/hooks/useSearch.ts +71 -0
  257. package/packages/graph/src/lib/constellation.ts +107 -0
  258. package/packages/graph/src/lib/export-utils.ts +48 -0
  259. package/packages/graph/src/lib/gesture-detector.ts +123 -0
  260. package/packages/graph/src/lib/layout.worker.ts +153 -0
  261. package/packages/graph/src/lib/motion-controller.ts +83 -0
  262. package/packages/graph/src/lib/profile-card.ts +122 -0
  263. package/packages/graph/src/main.tsx +4 -0
  264. package/packages/graph/src/stores/graph-store.ts +155 -0
  265. package/packages/graph/success.png +0 -0
  266. package/packages/graph/test-click.mjs +49 -0
  267. package/packages/graph/test-explore.mjs +102 -0
  268. package/packages/graph/test-final.mjs +61 -0
  269. package/packages/graph/test-graph.mjs +139 -0
  270. package/packages/graph/test-hover.mjs +48 -0
  271. package/packages/graph/test-pulse.mjs +68 -0
  272. package/packages/graph/test-screenshot.mjs +56 -0
  273. package/packages/graph/test-v2.mjs +97 -0
  274. package/packages/graph/vite.config.ts +15 -0
  275. package/packages/sync/.env.example +11 -0
  276. package/packages/sync/.sync-state.json +317 -0
  277. package/packages/sync/.upload-state.json +1009 -0
  278. package/packages/sync/create-stella-network-notion.mjs +151 -0
  279. package/packages/sync/create-stellavault-project-notion.mjs +322 -0
  280. package/packages/sync/logs/sync-2026-03-28.log +6 -0
  281. package/packages/sync/logs/sync-2026-03-29.log +12 -0
  282. package/packages/sync/logs/sync-2026-03-30.log +6 -0
  283. package/packages/sync/logs/sync-2026-03-31.log +6 -0
  284. package/packages/sync/logs/sync-2026-04-01.log +6 -0
  285. package/packages/sync/logs/sync-2026-04-02.log +6 -0
  286. package/packages/sync/package-lock.json +373 -0
  287. package/packages/sync/package.json +16 -0
  288. package/packages/sync/run-sync.bat +18 -0
  289. package/packages/sync/run-sync.mjs +46 -0
  290. package/packages/sync/setup-scheduler.mjs +119 -0
  291. package/packages/sync/structured-sync.mjs +187 -0
  292. package/packages/sync/sync-to-obsidian.mjs +264 -0
  293. package/packages/sync/upload-pdca-to-notion.mjs +495 -0
  294. package/tsconfig.base.json +18 -0
@@ -0,0 +1,110 @@
1
+ // Webhook/Event System (F-A17)
2
+ // Emit events on index, decay, gap detection → deliver via HTTP webhook
3
+
4
+ import type { PluginEvent } from './index.js';
5
+
6
+ export interface WebhookConfig {
7
+ url: string;
8
+ events: PluginEvent[];
9
+ secret?: string; // HMAC-SHA256 signing key
10
+ retries?: number;
11
+ }
12
+
13
+ export interface WebhookDelivery {
14
+ id: string;
15
+ event: PluginEvent;
16
+ url: string;
17
+ status: 'pending' | 'success' | 'failed';
18
+ attempts: number;
19
+ lastAttempt?: string;
20
+ response?: { status: number; body: string };
21
+ }
22
+
23
+ export class WebhookManager {
24
+ private configs: WebhookConfig[] = [];
25
+ private deliveries: WebhookDelivery[] = [];
26
+
27
+ register(config: WebhookConfig): void {
28
+ // MED: webhook URL 검증
29
+ try {
30
+ const parsed = new URL(config.url);
31
+ if (!['http:', 'https:'].includes(parsed.protocol)) throw new Error('Invalid protocol');
32
+ const host = parsed.hostname.toLowerCase();
33
+ if (host === 'localhost' || host === '127.0.0.1' || host.startsWith('192.168.') || host.startsWith('10.')) {
34
+ throw new Error('Internal URLs not allowed for webhooks');
35
+ }
36
+ } catch (e) { throw new Error(`Invalid webhook URL: ${e instanceof Error ? e.message : e}`); }
37
+ this.configs.push(config);
38
+ }
39
+
40
+ unregister(url: string): void {
41
+ this.configs = this.configs.filter(c => c.url !== url);
42
+ }
43
+
44
+ async emit(event: PluginEvent, data: unknown): Promise<WebhookDelivery[]> {
45
+ const matching = this.configs.filter(c => c.events.includes(event));
46
+ const results: WebhookDelivery[] = [];
47
+
48
+ for (const config of matching) {
49
+ const delivery = await this.deliver(config, event, data);
50
+ results.push(delivery);
51
+ this.deliveries.push(delivery);
52
+ }
53
+
54
+ // Keep last 100 deliveries
55
+ if (this.deliveries.length > 100) {
56
+ this.deliveries = this.deliveries.slice(-100);
57
+ }
58
+
59
+ return results;
60
+ }
61
+
62
+ private async deliver(config: WebhookConfig, event: PluginEvent, data: unknown): Promise<WebhookDelivery> {
63
+ const id = `wh-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
64
+ const maxRetries = config.retries ?? 3;
65
+ const delivery: WebhookDelivery = { id, event, url: config.url, status: 'pending', attempts: 0 };
66
+
67
+ const payload = JSON.stringify({ event, data, timestamp: new Date().toISOString(), source: 'stellavault' });
68
+
69
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
70
+ delivery.attempts = attempt + 1;
71
+ delivery.lastAttempt = new Date().toISOString();
72
+
73
+ try {
74
+ const headers: Record<string, string> = { 'Content-Type': 'application/json', 'User-Agent': 'stellavault-webhook/1.0' };
75
+
76
+ if (config.secret) {
77
+ const { createHmac } = await import('node:crypto');
78
+ const sig = createHmac('sha256', config.secret).update(payload).digest('hex');
79
+ headers['X-Stellavault-Signature'] = `sha256=${sig}`;
80
+ }
81
+
82
+ const res = await fetch(config.url, { method: 'POST', headers, body: payload, signal: AbortSignal.timeout(10000) });
83
+ delivery.response = { status: res.status, body: (await res.text()).slice(0, 500) };
84
+
85
+ if (res.ok) {
86
+ delivery.status = 'success';
87
+ return delivery;
88
+ }
89
+ } catch (err) {
90
+ delivery.response = { status: 0, body: err instanceof Error ? err.message : String(err) };
91
+ }
92
+
93
+ // Exponential backoff
94
+ if (attempt < maxRetries) {
95
+ await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt)));
96
+ }
97
+ }
98
+
99
+ delivery.status = 'failed';
100
+ return delivery;
101
+ }
102
+
103
+ getRecentDeliveries(limit = 20): WebhookDelivery[] {
104
+ return this.deliveries.slice(-limit).reverse();
105
+ }
106
+
107
+ listWebhooks(): WebhookConfig[] {
108
+ return [...this.configs];
109
+ }
110
+ }
@@ -0,0 +1,16 @@
1
+ // Design Ref: §6.2 — BM25 키워드 검색 (FTS5)
2
+
3
+ import type { VectorStore } from '../store/types.js';
4
+ import type { ScoredChunk } from '../types/chunk.js';
5
+
6
+ export async function searchBm25(
7
+ store: VectorStore,
8
+ query: string,
9
+ limit: number,
10
+ ): Promise<ScoredChunk[]> {
11
+ // FTS5 쿼리 전처리: 특수문자 제거, 공백으로 OR 검색
12
+ const sanitized = query.replace(/[^\w\s가-힣]/g, ' ').trim();
13
+ if (!sanitized) return [];
14
+ const ftsQuery = sanitized.split(/\s+/).join(' OR ');
15
+ return store.searchKeyword(ftsQuery, limit);
16
+ }
@@ -0,0 +1,83 @@
1
+ // Design Ref: §4.2 — createSearchEngine
2
+
3
+ import type { Embedder } from '../indexer/embedder.js';
4
+ import type { VectorStore } from '../store/types.js';
5
+ import type { SearchResult, SearchOptions } from '../types/search.js';
6
+ import { searchBm25 } from './bm25.js';
7
+ import { searchSemantic } from './semantic.js';
8
+ import { rrfFusion } from './rrf.js';
9
+
10
+ export { rrfFusion } from './rrf.js';
11
+
12
+ export interface SearchEngine {
13
+ search(options: SearchOptions): Promise<SearchResult[]>;
14
+ }
15
+
16
+ export function createSearchEngine(deps: {
17
+ store: VectorStore;
18
+ embedder: Embedder;
19
+ rrfK?: number;
20
+ }): SearchEngine {
21
+ const { store, embedder, rrfK = 60 } = deps;
22
+ const FETCH_LIMIT = 30; // 각 검색에서 가져올 후보 수
23
+
24
+ return {
25
+ async search(options: SearchOptions): Promise<SearchResult[]> {
26
+ const { query, limit = 10, threshold = 0.0, tags } = options;
27
+
28
+ // 병렬로 BM25 + Semantic 검색
29
+ const [bm25Results, semanticResults] = await Promise.all([
30
+ searchBm25(store, query, FETCH_LIMIT),
31
+ searchSemantic(store, embedder, query, FETCH_LIMIT),
32
+ ]);
33
+
34
+ // RRF Fusion
35
+ const fused = rrfFusion(semanticResults, bm25Results, rrfK, limit * 2);
36
+
37
+ // 청크+문서 조회 + 필터링
38
+ const results: SearchResult[] = [];
39
+ for (const scored of fused) {
40
+ if (scored.score < threshold) continue;
41
+
42
+ const chunk = await store.getChunk(scored.chunkId);
43
+ if (!chunk) continue;
44
+
45
+ const document = await store.getDocument(chunk.documentId);
46
+ if (!document) continue;
47
+
48
+ // 태그 필터
49
+ if (tags && tags.length > 0) {
50
+ const docTags = new Set(document.tags);
51
+ if (!tags.some(t => docTags.has(t))) continue;
52
+ }
53
+
54
+ results.push({
55
+ chunk,
56
+ document,
57
+ score: scored.score,
58
+ highlights: extractHighlights(chunk.content, query),
59
+ });
60
+
61
+ if (results.length >= limit) break;
62
+ }
63
+
64
+ return results;
65
+ },
66
+ };
67
+ }
68
+
69
+ function extractHighlights(content: string, query: string): string[] {
70
+ const words = query.toLowerCase().split(/\s+/).filter(w => w.length > 1);
71
+ const lines = content.split('\n');
72
+ const highlights: string[] = [];
73
+
74
+ for (const line of lines) {
75
+ const lower = line.toLowerCase();
76
+ if (words.some(w => lower.includes(w))) {
77
+ highlights.push(line.trim());
78
+ if (highlights.length >= 3) break;
79
+ }
80
+ }
81
+
82
+ return highlights;
83
+ }
@@ -0,0 +1,31 @@
1
+ // Design Ref: §6.2 — Reciprocal Rank Fusion (k=60)
2
+
3
+ import type { ScoredChunk } from '../types/chunk.js';
4
+
5
+ /**
6
+ * RRF: 두 랭킹 리스트를 통합하여 최종 점수를 산출합니다.
7
+ * score(d) = Σ 1/(k + rank_i) for each ranker i
8
+ */
9
+ export function rrfFusion(
10
+ listA: ScoredChunk[],
11
+ listB: ScoredChunk[],
12
+ k: number = 60,
13
+ limit: number = 10,
14
+ ): ScoredChunk[] {
15
+ const scores = new Map<string, number>();
16
+
17
+ for (let i = 0; i < listA.length; i++) {
18
+ const id = listA[i].chunkId;
19
+ scores.set(id, (scores.get(id) ?? 0) + 1 / (k + i + 1));
20
+ }
21
+
22
+ for (let i = 0; i < listB.length; i++) {
23
+ const id = listB[i].chunkId;
24
+ scores.set(id, (scores.get(id) ?? 0) + 1 / (k + i + 1));
25
+ }
26
+
27
+ return [...scores.entries()]
28
+ .sort((a, b) => b[1] - a[1])
29
+ .slice(0, limit)
30
+ .map(([chunkId, score]) => ({ chunkId, score }));
31
+ }
@@ -0,0 +1,15 @@
1
+ // Design Ref: §6.2 — 시맨틱 벡터 검색
2
+
3
+ import type { Embedder } from '../indexer/embedder.js';
4
+ import type { VectorStore } from '../store/types.js';
5
+ import type { ScoredChunk } from '../types/chunk.js';
6
+
7
+ export async function searchSemantic(
8
+ store: VectorStore,
9
+ embedder: Embedder,
10
+ query: string,
11
+ limit: number,
12
+ ): Promise<ScoredChunk[]> {
13
+ const embedding = await embedder.embed(query);
14
+ return store.searchSemantic(embedding, limit);
15
+ }
@@ -0,0 +1,2 @@
1
+ export type { VectorStore } from './types.js';
2
+ export { createSqliteVecStore, upsertDocument } from './sqlite-vec.js';
@@ -0,0 +1,290 @@
1
+ // Design Ref: §3.3 — SQLite-vec 스키마
2
+ // Design Ref: §3.2 — VectorStore 인터페이스 구현
3
+
4
+ import Database from 'better-sqlite3';
5
+ import * as sqliteVec from 'sqlite-vec';
6
+ import { mkdirSync } from 'node:fs';
7
+ import { dirname } from 'node:path';
8
+ import type { VectorStore } from './types.js';
9
+ import type { Chunk, ScoredChunk, Document, TopicInfo, StoreStats } from '../types/index.js';
10
+
11
+ export function createSqliteVecStore(dbPath: string, dimensions: number = 384): VectorStore {
12
+ let db: Database.Database;
13
+
14
+ return {
15
+ async initialize() {
16
+ mkdirSync(dirname(dbPath), { recursive: true });
17
+ db = new Database(dbPath);
18
+ sqliteVec.load(db);
19
+ db.pragma('journal_mode = WAL');
20
+ db.pragma('foreign_keys = ON');
21
+ createTables(db, dimensions);
22
+ },
23
+
24
+ async upsertDocument(doc: Document) {
25
+ db.prepare(`
26
+ INSERT OR REPLACE INTO documents (id, file_path, title, content, frontmatter, tags, last_modified, content_hash, indexed_at, source, type)
27
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
28
+ `).run(
29
+ doc.id, doc.filePath, doc.title, doc.content,
30
+ JSON.stringify(doc.frontmatter), JSON.stringify(doc.tags),
31
+ doc.lastModified, doc.contentHash, new Date().toISOString(),
32
+ doc.source ?? 'local', doc.type ?? 'note'
33
+ );
34
+ },
35
+
36
+ async upsertChunks(chunks: Chunk[]) {
37
+ if (chunks.length === 0) return;
38
+ const docId = chunks[0].documentId;
39
+
40
+ const tx = db.transaction(() => {
41
+ // 기존 청크 삭제 (문서 단위 교체)
42
+ // 새로 삽입할 chunk ID들도 미리 삭제 (다른 문서에서 온 중복 방지)
43
+ const newChunkIds = chunks.map(c => c.id);
44
+ db.prepare('DELETE FROM chunk_embeddings WHERE chunk_id IN (SELECT id FROM chunks WHERE document_id = ?)').run(docId);
45
+ db.prepare('DELETE FROM chunks WHERE document_id = ?').run(docId);
46
+ for (const cid of newChunkIds) {
47
+ db.prepare('DELETE FROM chunk_embeddings WHERE chunk_id = ?').run(cid);
48
+ db.prepare('DELETE FROM chunks WHERE id = ?').run(cid);
49
+ }
50
+
51
+ const insertChunk = db.prepare(`
52
+ INSERT INTO chunks (id, document_id, content, heading, start_line, end_line, token_count)
53
+ VALUES (?, ?, ?, ?, ?, ?, ?)
54
+ `);
55
+ const insertEmbedding = db.prepare(`
56
+ INSERT INTO chunk_embeddings (chunk_id, embedding)
57
+ VALUES (?, ?)
58
+ `);
59
+
60
+ for (const chunk of chunks) {
61
+ insertChunk.run(
62
+ chunk.id, chunk.documentId, chunk.content,
63
+ chunk.heading, chunk.startLine, chunk.endLine, chunk.tokenCount
64
+ );
65
+ if (chunk.embedding) {
66
+ insertEmbedding.run(chunk.id, float32Buffer(chunk.embedding));
67
+ }
68
+ }
69
+ });
70
+ tx();
71
+ },
72
+
73
+ async deleteByDocumentId(documentId: string) {
74
+ const tx = db.transaction(() => {
75
+ db.prepare('DELETE FROM chunk_embeddings WHERE chunk_id IN (SELECT id FROM chunks WHERE document_id = ?)').run(documentId);
76
+ db.prepare('DELETE FROM chunks WHERE document_id = ?').run(documentId);
77
+ db.prepare('DELETE FROM documents WHERE id = ?').run(documentId);
78
+ });
79
+ tx();
80
+ },
81
+
82
+ async searchSemantic(embedding: number[], limit: number): Promise<ScoredChunk[]> {
83
+ const rows = db.prepare(`
84
+ SELECT chunk_id, distance
85
+ FROM chunk_embeddings
86
+ WHERE embedding MATCH ?
87
+ ORDER BY distance
88
+ LIMIT ?
89
+ `).all(float32Buffer(embedding), limit) as Array<{ chunk_id: string; distance: number }>;
90
+
91
+ return rows.map(r => ({
92
+ chunkId: r.chunk_id,
93
+ score: 1 / (1 + r.distance), // distance → similarity score
94
+ }));
95
+ },
96
+
97
+ async searchKeyword(query: string, limit: number): Promise<ScoredChunk[]> {
98
+ const rows = db.prepare(`
99
+ SELECT c.id as chunk_id, rank
100
+ FROM chunks_fts f
101
+ JOIN chunks c ON c.rowid = f.rowid
102
+ WHERE chunks_fts MATCH ?
103
+ ORDER BY rank
104
+ LIMIT ?
105
+ `).all(query, limit) as Array<{ chunk_id: string; rank: number }>;
106
+
107
+ return rows.map(r => ({
108
+ chunkId: r.chunk_id,
109
+ score: -r.rank, // FTS5 rank is negative (lower = better)
110
+ }));
111
+ },
112
+
113
+ async getDocument(documentId: string): Promise<Document | null> {
114
+ const row = db.prepare('SELECT * FROM documents WHERE id = ?').get(documentId) as any;
115
+ if (!row) return null;
116
+ return rowToDocument(row);
117
+ },
118
+
119
+ async getChunk(chunkId: string): Promise<Chunk | null> {
120
+ const row = db.prepare('SELECT * FROM chunks WHERE id = ?').get(chunkId) as any;
121
+ if (!row) return null;
122
+ return rowToChunk(row);
123
+ },
124
+
125
+ async getAllDocuments(): Promise<Document[]> {
126
+ const rows = db.prepare('SELECT * FROM documents ORDER BY last_modified DESC').all() as any[];
127
+ return rows.map(rowToDocument);
128
+ },
129
+
130
+ async getTopics(): Promise<TopicInfo[]> {
131
+ const rows = db.prepare(`
132
+ SELECT je.value as tag, COUNT(DISTINCT d.id) as count
133
+ FROM documents d, json_each(d.tags) je
134
+ GROUP BY je.value
135
+ ORDER BY count DESC
136
+ `).all() as Array<{ tag: string; count: number }>;
137
+
138
+ return rows.map(r => ({
139
+ topic: r.tag,
140
+ count: r.count,
141
+ recentDocuments: [],
142
+ }));
143
+ },
144
+
145
+ async getStats(): Promise<StoreStats> {
146
+ const docCount = (db.prepare('SELECT COUNT(*) as c FROM documents').get() as any).c;
147
+ const chunkCount = (db.prepare('SELECT COUNT(*) as c FROM chunks').get() as any).c;
148
+ const lastRow = db.prepare('SELECT indexed_at FROM documents ORDER BY indexed_at DESC LIMIT 1').get() as any;
149
+ return {
150
+ documentCount: docCount,
151
+ chunkCount: chunkCount,
152
+ dbSizeBytes: 0, // 나중에 파일 크기 조회
153
+ lastIndexed: lastRow?.indexed_at ?? null,
154
+ };
155
+ },
156
+
157
+ async getDocumentEmbeddings(): Promise<Map<string, number[]>> {
158
+ // 각 문서의 첫 청크 임베딩을 문서 대표 벡터로 사용
159
+ const rows = db.prepare(`
160
+ SELECT c.document_id, ce.embedding
161
+ FROM chunks c
162
+ JOIN chunk_embeddings ce ON ce.chunk_id = c.id
163
+ WHERE c.id IN (
164
+ SELECT MIN(id) FROM chunks GROUP BY document_id
165
+ )
166
+ `).all() as Array<{ document_id: string; embedding: Buffer }>;
167
+
168
+ const result = new Map<string, number[]>();
169
+ for (const row of rows) {
170
+ result.set(row.document_id, bufferToFloat32(row.embedding));
171
+ }
172
+ return result;
173
+ },
174
+
175
+ async close() {
176
+ db.close();
177
+ },
178
+
179
+ getDb() {
180
+ return db;
181
+ },
182
+ };
183
+ }
184
+
185
+ function bufferToFloat32(buf: Buffer): number[] {
186
+ const float32 = new Float32Array(buf.buffer, buf.byteOffset, buf.byteLength / 4);
187
+ return Array.from(float32);
188
+ }
189
+
190
+ function createTables(db: Database.Database, dimensions: number = 384) {
191
+ // 기존 DB 마이그레이션: source/type 컬럼 추가
192
+ try {
193
+ db.exec(`ALTER TABLE documents ADD COLUMN source TEXT DEFAULT 'local'`);
194
+ } catch { /* 이미 존재 */ }
195
+ try {
196
+ db.exec(`ALTER TABLE documents ADD COLUMN type TEXT DEFAULT 'note'`);
197
+ } catch { /* 이미 존재 */ }
198
+
199
+ db.exec(`
200
+ CREATE TABLE IF NOT EXISTS documents (
201
+ id TEXT PRIMARY KEY,
202
+ file_path TEXT NOT NULL UNIQUE,
203
+ title TEXT NOT NULL,
204
+ content TEXT NOT NULL,
205
+ frontmatter TEXT,
206
+ tags TEXT DEFAULT '[]',
207
+ last_modified TEXT NOT NULL,
208
+ content_hash TEXT NOT NULL,
209
+ indexed_at TEXT NOT NULL,
210
+ source TEXT DEFAULT 'local',
211
+ type TEXT DEFAULT 'note'
212
+ );
213
+
214
+ CREATE TABLE IF NOT EXISTS chunks (
215
+ id TEXT PRIMARY KEY,
216
+ document_id TEXT NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
217
+ content TEXT NOT NULL,
218
+ heading TEXT,
219
+ start_line INTEGER,
220
+ end_line INTEGER,
221
+ token_count INTEGER
222
+ );
223
+
224
+ CREATE VIRTUAL TABLE IF NOT EXISTS chunk_embeddings USING vec0(
225
+ chunk_id TEXT PRIMARY KEY,
226
+ embedding FLOAT[${dimensions}]
227
+ );
228
+
229
+ CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(
230
+ content,
231
+ heading,
232
+ content='chunks',
233
+ content_rowid='rowid'
234
+ );
235
+
236
+ CREATE INDEX IF NOT EXISTS idx_chunks_document_id ON chunks(document_id);
237
+ CREATE INDEX IF NOT EXISTS idx_documents_content_hash ON documents(content_hash);
238
+ `);
239
+
240
+ // FTS5 트리거: chunks INSERT/DELETE 시 자동 동기화
241
+ db.exec(`
242
+ CREATE TRIGGER IF NOT EXISTS chunks_ai AFTER INSERT ON chunks BEGIN
243
+ INSERT INTO chunks_fts(rowid, content, heading) VALUES (new.rowid, new.content, new.heading);
244
+ END;
245
+ CREATE TRIGGER IF NOT EXISTS chunks_ad AFTER DELETE ON chunks BEGIN
246
+ INSERT INTO chunks_fts(chunks_fts, rowid, content, heading) VALUES('delete', old.rowid, old.content, old.heading);
247
+ END;
248
+ `);
249
+ }
250
+
251
+ // Document 헬퍼: upsertDocument는 indexer에서 호출
252
+ export function upsertDocument(db: Database.Database, doc: Document) {
253
+ db.prepare(`
254
+ INSERT OR REPLACE INTO documents (id, file_path, title, content, frontmatter, tags, last_modified, content_hash, indexed_at)
255
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
256
+ `).run(
257
+ doc.id, doc.filePath, doc.title, doc.content,
258
+ JSON.stringify(doc.frontmatter), JSON.stringify(doc.tags),
259
+ doc.lastModified, doc.contentHash, new Date().toISOString()
260
+ );
261
+ }
262
+
263
+ function rowToDocument(row: any): Document {
264
+ return {
265
+ id: row.id,
266
+ filePath: row.file_path,
267
+ title: row.title,
268
+ content: row.content,
269
+ frontmatter: JSON.parse(row.frontmatter || '{}'),
270
+ tags: JSON.parse(row.tags || '[]'),
271
+ lastModified: row.last_modified,
272
+ contentHash: row.content_hash,
273
+ };
274
+ }
275
+
276
+ function rowToChunk(row: any): Chunk {
277
+ return {
278
+ id: row.id,
279
+ documentId: row.document_id,
280
+ content: row.content,
281
+ heading: row.heading ?? '',
282
+ startLine: row.start_line,
283
+ endLine: row.end_line,
284
+ tokenCount: row.token_count,
285
+ };
286
+ }
287
+
288
+ function float32Buffer(arr: number[]): Buffer {
289
+ return Buffer.from(new Float32Array(arr).buffer);
290
+ }
@@ -0,0 +1,22 @@
1
+ // Design Ref: §3.2 — 교체 가능 설계 (VectorStore 인터페이스)
2
+
3
+ import type { Chunk, ScoredChunk, Document, TopicInfo, StoreStats } from '../types/index.js';
4
+
5
+ export interface VectorStore {
6
+ initialize(): Promise<void>;
7
+ upsertDocument(doc: Document): Promise<void>;
8
+ upsertChunks(chunks: Chunk[]): Promise<void>;
9
+ deleteByDocumentId(documentId: string): Promise<void>;
10
+ searchSemantic(embedding: number[], limit: number): Promise<ScoredChunk[]>;
11
+ searchKeyword(query: string, limit: number): Promise<ScoredChunk[]>;
12
+ getDocument(documentId: string): Promise<Document | null>;
13
+ getChunk(chunkId: string): Promise<Chunk | null>;
14
+ getAllDocuments(): Promise<Document[]>;
15
+ getTopics(): Promise<TopicInfo[]>;
16
+ getStats(): Promise<StoreStats>;
17
+ /** 각 문서의 첫 청크 임베딩 반환 (graph용) */
18
+ getDocumentEmbeddings(): Promise<Map<string, number[]>>;
19
+ close(): Promise<void>;
20
+ /** 내부 DB 인스턴스 접근 (Intelligence Layer용) */
21
+ getDb(): unknown;
22
+ }