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,126 @@
1
+ // Team Vault (F-A06) — Bearer 토큰 인증 + RBAC
2
+ // MCP Streamable HTTP에 인증 레이어 추가
3
+
4
+ import { randomBytes, createHash } from 'node:crypto';
5
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+ import { homedir } from 'node:os';
8
+
9
+ export type TeamRole = 'admin' | 'editor' | 'viewer';
10
+
11
+ export interface TeamMember {
12
+ token: string; // Bearer token (SHA256 hash stored)
13
+ tokenHash: string; // stored version
14
+ displayName: string;
15
+ role: TeamRole;
16
+ createdAt: string;
17
+ lastAccess?: string;
18
+ }
19
+
20
+ export interface TeamConfig {
21
+ teamName: string;
22
+ members: TeamMember[];
23
+ }
24
+
25
+ const TEAM_DIR = join(homedir(), '.stellavault', 'team');
26
+ const TEAM_FILE = join(TEAM_DIR, 'team.json');
27
+
28
+ export function loadTeamConfig(): TeamConfig {
29
+ if (existsSync(TEAM_FILE)) {
30
+ return JSON.parse(readFileSync(TEAM_FILE, 'utf-8'));
31
+ }
32
+ return { teamName: 'My Team', members: [] };
33
+ }
34
+
35
+ function saveTeamConfig(config: TeamConfig): void {
36
+ mkdirSync(TEAM_DIR, { recursive: true });
37
+ writeFileSync(TEAM_FILE, JSON.stringify(config, null, 2), 'utf-8');
38
+ }
39
+
40
+ // 토큰 생성
41
+ export function generateToken(): string {
42
+ return `sv_${randomBytes(24).toString('hex')}`;
43
+ }
44
+
45
+ function hashToken(token: string): string {
46
+ return createHash('sha256').update(token).digest('hex');
47
+ }
48
+
49
+ // 멤버 초대 → 토큰 반환
50
+ export function inviteMember(displayName: string, role: TeamRole = 'viewer'): { token: string; member: TeamMember } {
51
+ const config = loadTeamConfig();
52
+ const token = generateToken();
53
+ const member: TeamMember = {
54
+ token: '', // 클리어 토큰은 저장 안 함
55
+ tokenHash: hashToken(token),
56
+ displayName,
57
+ role,
58
+ createdAt: new Date().toISOString(),
59
+ };
60
+ config.members.push(member);
61
+ saveTeamConfig(config);
62
+ return { token, member }; // 토큰은 이때만 보여줌
63
+ }
64
+
65
+ // 토큰으로 멤버 인증
66
+ export function authenticateMember(token: string): TeamMember | null {
67
+ const config = loadTeamConfig();
68
+ const hash = hashToken(token);
69
+ const member = config.members.find(m => m.tokenHash === hash);
70
+ if (member) {
71
+ member.lastAccess = new Date().toISOString();
72
+ saveTeamConfig(config);
73
+ }
74
+ return member ?? null;
75
+ }
76
+
77
+ // RBAC 권한 확인
78
+ export function hasPermission(member: TeamMember, action: 'read' | 'write' | 'admin'): boolean {
79
+ switch (action) {
80
+ case 'read': return true; // 모든 역할 읽기 가능
81
+ case 'write': return member.role === 'admin' || member.role === 'editor';
82
+ case 'admin': return member.role === 'admin';
83
+ }
84
+ }
85
+
86
+ // 멤버 목록
87
+ export function listMembers(): TeamMember[] {
88
+ return loadTeamConfig().members;
89
+ }
90
+
91
+ // 멤버 제거
92
+ export function removeMember(displayName: string): boolean {
93
+ const config = loadTeamConfig();
94
+ const before = config.members.length;
95
+ config.members = config.members.filter(m => m.displayName !== displayName);
96
+ if (config.members.length < before) {
97
+ saveTeamConfig(config);
98
+ return true;
99
+ }
100
+ return false;
101
+ }
102
+
103
+ // Express 미들웨어: Bearer 토큰 인증
104
+ export function createAuthMiddleware() {
105
+ return (req: any, res: any, next: any) => {
106
+ const authHeader = req.headers.authorization;
107
+ if (!authHeader?.startsWith('Bearer ')) {
108
+ // HIGH-01: localhost는 editor 권한만 (admin은 토큰 필수)
109
+ const ip = req.ip || req.connection?.remoteAddress;
110
+ if (ip === '127.0.0.1' || ip === '::1' || ip === '::ffff:127.0.0.1') {
111
+ req.teamMember = { displayName: 'local', role: 'editor' } as TeamMember;
112
+ return next();
113
+ }
114
+ return res.status(401).json({ error: 'Bearer token required' });
115
+ }
116
+
117
+ const token = authHeader.slice(7);
118
+ const member = authenticateMember(token);
119
+ if (!member) {
120
+ return res.status(403).json({ error: 'Invalid token' });
121
+ }
122
+
123
+ req.teamMember = member;
124
+ next();
125
+ };
126
+ }
@@ -0,0 +1,25 @@
1
+ // Design Ref: §3.1 — Core Types (Chunk)
2
+
3
+ export interface Chunk {
4
+ /** document.id + "#" + chunkIndex */
5
+ id: string;
6
+ /** 소속 문서 ID */
7
+ documentId: string;
8
+ /** 청크 텍스트 */
9
+ content: string;
10
+ /** 소속 heading (## 제목) */
11
+ heading: string;
12
+ /** 원문 시작 줄 번호 */
13
+ startLine: number;
14
+ /** 원문 끝 줄 번호 */
15
+ endLine: number;
16
+ /** 토큰 수 */
17
+ tokenCount: number;
18
+ /** 768차원 벡터 (로딩 시 선택적) */
19
+ embedding?: number[];
20
+ }
21
+
22
+ export interface ScoredChunk {
23
+ chunkId: string;
24
+ score: number;
25
+ }
@@ -0,0 +1,24 @@
1
+ // Design Ref: §3.1 — Core Types (Document)
2
+
3
+ export interface Document {
4
+ /** SHA-256 hash of file path */
5
+ id: string;
6
+ /** vault 기준 상대 경로 */
7
+ filePath: string;
8
+ /** frontmatter title 또는 첫 heading */
9
+ title: string;
10
+ /** 전체 마크다운 텍스트 */
11
+ content: string;
12
+ /** YAML frontmatter */
13
+ frontmatter: Record<string, unknown>;
14
+ /** #태그 목록 */
15
+ tags: string[];
16
+ /** ISO 8601 */
17
+ lastModified: string;
18
+ /** SHA-256 of content (증분 인덱싱용) */
19
+ contentHash: string;
20
+ /** 출처: local | notion | clip | bridge | pack */
21
+ source?: string;
22
+ /** 노트 유형: note | clip | sync | bridge | decision | snapshot */
23
+ type?: string;
24
+ }
@@ -0,0 +1,44 @@
1
+ // Design Ref: §3.1 — Graph Data Types
2
+
3
+ export interface GraphNode {
4
+ id: string;
5
+ label: string;
6
+ filePath: string;
7
+ tags: string[];
8
+ clusterId: number;
9
+ position?: [number, number, number];
10
+ size: number;
11
+ source?: string;
12
+ type?: string;
13
+ lastModified?: string;
14
+ }
15
+
16
+ export interface GraphEdge {
17
+ source: string;
18
+ target: string;
19
+ weight: number;
20
+ }
21
+
22
+ export interface Cluster {
23
+ id: number;
24
+ label: string;
25
+ color: string;
26
+ nodeCount: number;
27
+ }
28
+
29
+ export interface GraphData {
30
+ nodes: GraphNode[];
31
+ edges: GraphEdge[];
32
+ clusters: Cluster[];
33
+ stats: {
34
+ nodeCount: number;
35
+ edgeCount: number;
36
+ clusterCount: number;
37
+ };
38
+ }
39
+
40
+ export interface GraphResponse {
41
+ data: GraphData;
42
+ generatedAt: string;
43
+ cacheKey: string;
44
+ }
@@ -0,0 +1,15 @@
1
+ export type { Document } from './document.js';
2
+ export type { Chunk, ScoredChunk } from './chunk.js';
3
+ export type {
4
+ SearchResult,
5
+ SearchOptions,
6
+ TopicInfo,
7
+ StoreStats,
8
+ } from './search.js';
9
+ export type {
10
+ GraphNode,
11
+ GraphEdge,
12
+ Cluster,
13
+ GraphData,
14
+ GraphResponse,
15
+ } from './graph.js';
@@ -0,0 +1,38 @@
1
+ // Design Ref: §3.1 — Core Types (Search)
2
+
3
+ import type { Chunk } from './chunk.js';
4
+ import type { Document } from './document.js';
5
+
6
+ export interface SearchResult {
7
+ chunk: Chunk;
8
+ document: Document;
9
+ /** RRF 통합 점수 (0~1) */
10
+ score: number;
11
+ /** 매칭 부분 하이라이트 */
12
+ highlights: string[];
13
+ }
14
+
15
+ export interface SearchOptions {
16
+ query: string;
17
+ /** default: 10 */
18
+ limit?: number;
19
+ /** minimum score, default: 0.1 */
20
+ threshold?: number;
21
+ /** 태그 필터 */
22
+ tags?: string[];
23
+ /** 날짜 범위 필터 */
24
+ dateRange?: { from?: string; to?: string };
25
+ }
26
+
27
+ export interface TopicInfo {
28
+ topic: string;
29
+ count: number;
30
+ recentDocuments: Array<{ id: string; title: string }>;
31
+ }
32
+
33
+ export interface StoreStats {
34
+ documentCount: number;
35
+ chunkCount: number;
36
+ dbSizeBytes: number;
37
+ lastIndexed: string | null;
38
+ }
@@ -0,0 +1,85 @@
1
+ // Error Recovery System (F-A02) — retry with exponential backoff + meaningful messages
2
+
3
+ export interface RetryOptions {
4
+ maxRetries?: number;
5
+ baseDelayMs?: number;
6
+ maxDelayMs?: number;
7
+ onRetry?: (error: Error, attempt: number, maxRetries: number) => void;
8
+ }
9
+
10
+ const DEFAULT_OPTIONS: Required<Pick<RetryOptions, 'maxRetries' | 'baseDelayMs' | 'maxDelayMs'>> = {
11
+ maxRetries: 3,
12
+ baseDelayMs: 500,
13
+ maxDelayMs: 10000,
14
+ };
15
+
16
+ export async function withRetry<T>(
17
+ fn: () => Promise<T>,
18
+ options: RetryOptions = {},
19
+ ): Promise<T> {
20
+ const { maxRetries, baseDelayMs, maxDelayMs } = { ...DEFAULT_OPTIONS, ...options };
21
+
22
+ let lastError: Error | undefined;
23
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
24
+ try {
25
+ return await fn();
26
+ } catch (err) {
27
+ lastError = err instanceof Error ? err : new Error(String(err));
28
+ if (attempt >= maxRetries) break;
29
+
30
+ const delay = Math.min(baseDelayMs * Math.pow(2, attempt), maxDelayMs);
31
+ options.onRetry?.(lastError, attempt + 1, maxRetries);
32
+ await new Promise((r) => setTimeout(r, delay));
33
+ }
34
+ }
35
+
36
+ throw lastError!;
37
+ }
38
+
39
+ export class StellavaultError extends Error {
40
+ constructor(
41
+ message: string,
42
+ public readonly code: string,
43
+ public readonly suggestion?: string,
44
+ public readonly cause?: Error,
45
+ ) {
46
+ super(message);
47
+ this.name = 'StellavaultError';
48
+ }
49
+
50
+ format(): string {
51
+ const lines = [`Error [${this.code}]: ${this.message}`];
52
+ if (this.suggestion) lines.push(` Fix: ${this.suggestion}`);
53
+ if (this.cause) lines.push(` Cause: ${this.cause.message}`);
54
+ return lines.join('\n');
55
+ }
56
+ }
57
+
58
+ export function wrapError(err: unknown, code: string, suggestion?: string): StellavaultError {
59
+ const cause = err instanceof Error ? err : new Error(String(err));
60
+ return new StellavaultError(cause.message, code, suggestion, cause);
61
+ }
62
+
63
+ // Common error factories
64
+ export const errors = {
65
+ vaultNotFound: (path: string) =>
66
+ new StellavaultError(`Vault not found: ${path}`, 'VAULT_NOT_FOUND', 'Check the path exists and contains .md files'),
67
+
68
+ dbInitFailed: (err: unknown) =>
69
+ wrapError(err, 'DB_INIT_FAILED', 'Delete ~/.stellavault/index.db and re-index'),
70
+
71
+ embedderFailed: (err: unknown) =>
72
+ wrapError(err, 'EMBEDDER_FAILED', 'Check disk space and try again. The model downloads on first run (~80MB)'),
73
+
74
+ indexingFailed: (file: string, err: unknown) =>
75
+ wrapError(err, 'INDEX_FAILED', `Skipping "${file}". Re-run indexing to retry failed files`),
76
+
77
+ searchFailed: (err: unknown) =>
78
+ wrapError(err, 'SEARCH_FAILED', 'Re-index your vault: stellavault index <path>'),
79
+
80
+ configInvalid: (field: string) =>
81
+ new StellavaultError(`Invalid config: ${field}`, 'CONFIG_INVALID', 'Check ~/.stellavault.json format'),
82
+
83
+ apiServerFailed: (port: number, err: unknown) =>
84
+ wrapError(err, 'API_SERVER_FAILED', `Port ${port} may be in use. Try: stellavault graph --port ${port + 1}`),
85
+ };
@@ -0,0 +1,60 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
+ import { createSqliteVecStore } from '../src/store/sqlite-vec.js';
3
+ import { createSearchEngine } from '../src/search/index.js';
4
+ import { createApiServer } from '../src/api/server.js';
5
+ import type { VectorStore } from '../src/store/types.js';
6
+
7
+ const DIMS = 4;
8
+ const PORT = 13334;
9
+ let store: VectorStore;
10
+
11
+ beforeAll(async () => {
12
+ store = createSqliteVecStore(':memory:', DIMS);
13
+ await store.initialize();
14
+ await store.upsertDocument({
15
+ id: 'doc1', filePath: 'test.md', title: 'Test',
16
+ content: 'Content', frontmatter: {}, tags: ['test'],
17
+ lastModified: '2026-01-01', contentHash: 'h1',
18
+ });
19
+ await store.upsertChunks([{
20
+ id: 'doc1#0', documentId: 'doc1', content: 'Content',
21
+ heading: 'Test', startLine: 1, endLine: 1, tokenCount: 2,
22
+ embedding: [1, 0, 0, 0],
23
+ }]);
24
+
25
+ const embedder = {
26
+ dimensions: DIMS, modelName: 'test',
27
+ initialize: async () => {},
28
+ embed: async () => [0.5, 0.5, 0.5, 0.5],
29
+ embedBatch: async (t: string[]) => t.map(() => [0.5, 0.5, 0.5, 0.5]),
30
+ };
31
+ const searchEngine = createSearchEngine({ store, embedder });
32
+ const server = createApiServer({ store, searchEngine, port: PORT });
33
+ await server.start();
34
+ });
35
+
36
+ afterAll(async () => { await store.close(); });
37
+
38
+ describe('GET /api/profile-card', () => {
39
+ it('SVG 반환', async () => {
40
+ const res = await fetch(`http://127.0.0.1:${PORT}/api/profile-card`);
41
+ expect(res.ok).toBe(true);
42
+ expect(res.headers.get('content-type')).toContain('image/svg+xml');
43
+ const text = await res.text();
44
+ expect(text).toContain('<svg');
45
+ expect(text).toContain('</svg>');
46
+ });
47
+
48
+ it('문서 수 포함', async () => {
49
+ const res = await fetch(`http://127.0.0.1:${PORT}/api/profile-card`);
50
+ const text = await res.text();
51
+ expect(text).toContain('1 docs');
52
+ });
53
+
54
+ it('mode=folder 지원', async () => {
55
+ const res = await fetch(`http://127.0.0.1:${PORT}/api/profile-card?mode=folder`);
56
+ expect(res.ok).toBe(true);
57
+ const text = await res.text();
58
+ expect(text).toContain('<svg');
59
+ });
60
+ });
@@ -0,0 +1,98 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
+ import { createSqliteVecStore } from '../src/store/sqlite-vec.js';
3
+ import { createSearchEngine } from '../src/search/index.js';
4
+ import { createApiServer } from '../src/api/server.js';
5
+ import type { VectorStore } from '../src/store/types.js';
6
+ import type { Embedder } from '../src/indexer/embedder.js';
7
+
8
+ const DIMS = 4;
9
+ let store: VectorStore;
10
+ let server: ReturnType<typeof createApiServer>;
11
+ const PORT = 13333; // 테스트용 포트
12
+
13
+ function mockEmbedder(): Embedder {
14
+ return {
15
+ dimensions: DIMS, modelName: 'test',
16
+ initialize: async () => {},
17
+ embed: async () => [0.5, 0.5, 0.5, 0.5],
18
+ embedBatch: async (texts) => texts.map(() => [0.5, 0.5, 0.5, 0.5]),
19
+ };
20
+ }
21
+
22
+ beforeAll(async () => {
23
+ store = createSqliteVecStore(':memory:', DIMS);
24
+ await store.initialize();
25
+
26
+ await store.upsertDocument({
27
+ id: 'doc1', filePath: 'test.md', title: 'Test Doc',
28
+ content: 'OAuth authentication patterns', frontmatter: {}, tags: ['auth'],
29
+ lastModified: '2026-01-01', contentHash: 'h1',
30
+ });
31
+ await store.upsertChunks([{
32
+ id: 'doc1#0', documentId: 'doc1', content: 'OAuth authentication patterns',
33
+ heading: 'Auth', startLine: 1, endLine: 1, tokenCount: 3,
34
+ embedding: [1, 0, 0, 0],
35
+ }]);
36
+
37
+ const embedder = mockEmbedder();
38
+ const searchEngine = createSearchEngine({ store, embedder });
39
+ server = createApiServer({ store, searchEngine, port: PORT });
40
+ await server.start();
41
+ });
42
+
43
+ afterAll(async () => { await store.close(); });
44
+
45
+ describe('API Routes', () => {
46
+ it('GET /api/stats', async () => {
47
+ const res = await fetch(`http://127.0.0.1:${PORT}/api/stats`);
48
+ expect(res.ok).toBe(true);
49
+ const data = await res.json();
50
+ expect(data.documentCount).toBe(1);
51
+ expect(data.chunkCount).toBe(1);
52
+ });
53
+
54
+ it('GET /api/graph', async () => {
55
+ const res = await fetch(`http://127.0.0.1:${PORT}/api/graph`);
56
+ expect(res.ok).toBe(true);
57
+ const data = await res.json();
58
+ expect(data.data.nodes.length).toBe(1);
59
+ expect(data.data.stats.nodeCount).toBe(1);
60
+ });
61
+
62
+ it('GET /api/graph?mode=folder', async () => {
63
+ const res = await fetch(`http://127.0.0.1:${PORT}/api/graph?mode=folder`);
64
+ expect(res.ok).toBe(true);
65
+ const data = await res.json();
66
+ expect(data.data.clusters.length).toBeGreaterThan(0);
67
+ });
68
+
69
+ it('GET /api/search?q=OAuth', async () => {
70
+ const res = await fetch(`http://127.0.0.1:${PORT}/api/search?q=OAuth`);
71
+ expect(res.ok).toBe(true);
72
+ const data = await res.json();
73
+ expect(data.query).toBe('OAuth');
74
+ expect(data.results.length).toBeGreaterThan(0);
75
+ expect(data.results[0].documentId).toBe('doc1');
76
+ });
77
+
78
+ it('GET /api/search 빈 쿼리', async () => {
79
+ const res = await fetch(`http://127.0.0.1:${PORT}/api/search?q=`);
80
+ expect(res.ok).toBe(true);
81
+ const data = await res.json();
82
+ expect(data.results).toEqual([]);
83
+ });
84
+
85
+ it('GET /api/document/:id 존재', async () => {
86
+ const res = await fetch(`http://127.0.0.1:${PORT}/api/document/doc1`);
87
+ expect(res.ok).toBe(true);
88
+ const data = await res.json();
89
+ expect(data.title).toBe('Test Doc');
90
+ expect(data.content).toContain('OAuth');
91
+ expect(data.related).toBeDefined();
92
+ });
93
+
94
+ it('GET /api/document/:id 미존재 → 404', async () => {
95
+ const res = await fetch(`http://127.0.0.1:${PORT}/api/document/nonexistent`);
96
+ expect(res.status).toBe(404);
97
+ });
98
+ });
@@ -0,0 +1,87 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { searchBm25 } from '../src/search/bm25.js';
3
+ import type { VectorStore } from '../src/store/types.js';
4
+ import type { ScoredChunk } from '../src/types/chunk.js';
5
+
6
+ // FTS5 검색을 시뮬레이션하는 mock store
7
+ function createMockStore(keywordResults: ScoredChunk[] = []): VectorStore {
8
+ return {
9
+ searchKeyword: async (_query: string, _limit: number) => keywordResults,
10
+ // 사용하지 않는 메서드 stub
11
+ initialize: async () => {},
12
+ close: async () => {},
13
+ upsertDocument: async () => {},
14
+ upsertChunks: async () => {},
15
+ deleteByDocumentId: async () => {},
16
+ getDocument: async () => null,
17
+ getChunk: async () => null,
18
+ searchSemantic: async () => [],
19
+ getTopics: async () => [],
20
+ getStats: async () => ({ documentCount: 0, chunkCount: 0, dbSizeBytes: 0, lastIndexed: null }),
21
+ getAllDocumentHashes: async () => new Map(),
22
+ } as VectorStore;
23
+ }
24
+
25
+ describe('searchBm25', () => {
26
+ it('빈 쿼리는 빈 결과 반환', async () => {
27
+ const store = createMockStore();
28
+ const results = await searchBm25(store, '', 10);
29
+ expect(results).toEqual([]);
30
+ });
31
+
32
+ it('특수문자만 있는 쿼리는 빈 결과 반환', async () => {
33
+ const store = createMockStore();
34
+ const results = await searchBm25(store, '!@#$%^&*()', 10);
35
+ expect(results).toEqual([]);
36
+ });
37
+
38
+ it('일반 영문 쿼리 OR 조합 생성', async () => {
39
+ let capturedQuery = '';
40
+ const store = createMockStore();
41
+ store.searchKeyword = async (query: string, _limit: number) => {
42
+ capturedQuery = query;
43
+ return [];
44
+ };
45
+
46
+ await searchBm25(store, 'React state management', 10);
47
+ expect(capturedQuery).toBe('React OR state OR management');
48
+ });
49
+
50
+ it('한국어 쿼리 처리', async () => {
51
+ let capturedQuery = '';
52
+ const store = createMockStore();
53
+ store.searchKeyword = async (query: string, _limit: number) => {
54
+ capturedQuery = query;
55
+ return [];
56
+ };
57
+
58
+ await searchBm25(store, '리액트 상태관리', 10);
59
+ expect(capturedQuery).toBe('리액트 OR 상태관리');
60
+ });
61
+
62
+ it('특수문자 제거 후 정상 검색', async () => {
63
+ let capturedQuery = '';
64
+ const store = createMockStore();
65
+ store.searchKeyword = async (query: string, _limit: number) => {
66
+ capturedQuery = query;
67
+ return [];
68
+ };
69
+
70
+ await searchBm25(store, 'OAuth2.0 인증/설계', 10);
71
+ // 특수문자(., /) 제거 후 단어 추출
72
+ expect(capturedQuery).toContain('OR');
73
+ expect(capturedQuery).not.toContain('.');
74
+ expect(capturedQuery).not.toContain('/');
75
+ });
76
+
77
+ it('결과를 그대로 반환', async () => {
78
+ const mockResults: ScoredChunk[] = [
79
+ { chunkId: 'doc1#0', score: 0.9 },
80
+ { chunkId: 'doc2#1', score: 0.5 },
81
+ ];
82
+ const store = createMockStore(mockResults);
83
+ const results = await searchBm25(store, 'test query', 10);
84
+ expect(results).toEqual(mockResults);
85
+ expect(results.length).toBe(2);
86
+ });
87
+ });
@@ -0,0 +1,48 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { chunkDocument, estimateTokens } from '../src/indexer/chunker.js';
3
+
4
+ describe('estimateTokens', () => {
5
+ it('영문 토큰 추정', () => {
6
+ expect(estimateTokens('hello world')).toBe(3);
7
+ });
8
+
9
+ it('한국어 토큰 추정', () => {
10
+ expect(estimateTokens('안녕하세요')).toBe(3);
11
+ });
12
+
13
+ it('빈 문자열', () => {
14
+ expect(estimateTokens('')).toBe(0);
15
+ });
16
+ });
17
+
18
+ describe('chunkDocument', () => {
19
+ it('짧은 문서는 1개 청크', () => {
20
+ const chunks = chunkDocument('doc1', '# Title\n\nShort content.');
21
+ expect(chunks.length).toBe(1);
22
+ expect(chunks[0].documentId).toBe('doc1');
23
+ expect(chunks[0].id).toBe('doc1#0');
24
+ });
25
+
26
+ it('heading으로 분할', () => {
27
+ const md = '# Title\n\nIntro.\n\n## A\n\n' + 'Content A. '.repeat(50) + '\n\n## B\n\n' + 'Content B. '.repeat(50);
28
+ const chunks = chunkDocument('doc2', md);
29
+ expect(chunks.length).toBeGreaterThanOrEqual(2);
30
+ });
31
+
32
+ it('빈 문서는 빈 배열', () => {
33
+ const chunks = chunkDocument('doc3', '');
34
+ expect(chunks).toEqual([]);
35
+ });
36
+
37
+ it('heading 없는 문서도 처리', () => {
38
+ const chunks = chunkDocument('doc4', 'Just plain text without headings.');
39
+ expect(chunks.length).toBe(1);
40
+ });
41
+
42
+ it('짧은 청크 병합', () => {
43
+ const md = '## A\n\nHi\n\n## B\n\nBye';
44
+ const chunks = chunkDocument('doc5', md, { minTokens: 50 });
45
+ // 둘 다 50 토큰 미만이므로 병합됨
46
+ expect(chunks.length).toBe(1);
47
+ });
48
+ });