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.
- package/.env.example +12 -0
- package/CLAUDE.md +39 -0
- package/CONTRIBUTING.md +65 -0
- package/LICENSE +21 -0
- package/README.md +182 -0
- package/memory/MEMORY.md +25 -0
- package/package.json +33 -0
- package/packages/cli/bin/ekh.js +2 -0
- package/packages/cli/bin/stellavault.js +2 -0
- package/packages/cli/dist/commands/brief-cmd.d.ts +2 -0
- package/packages/cli/dist/commands/brief-cmd.d.ts.map +1 -0
- package/packages/cli/dist/commands/brief-cmd.js +82 -0
- package/packages/cli/dist/commands/brief-cmd.js.map +1 -0
- package/packages/cli/dist/commands/capture-cmd.d.ts +7 -0
- package/packages/cli/dist/commands/capture-cmd.d.ts.map +1 -0
- package/packages/cli/dist/commands/capture-cmd.js +31 -0
- package/packages/cli/dist/commands/capture-cmd.js.map +1 -0
- package/packages/cli/dist/commands/card-cmd.d.ts +4 -0
- package/packages/cli/dist/commands/card-cmd.d.ts.map +1 -0
- package/packages/cli/dist/commands/card-cmd.js +26 -0
- package/packages/cli/dist/commands/card-cmd.js.map +1 -0
- package/packages/cli/dist/commands/clip-cmd.d.ts +4 -0
- package/packages/cli/dist/commands/clip-cmd.d.ts.map +1 -0
- package/packages/cli/dist/commands/clip-cmd.js +151 -0
- package/packages/cli/dist/commands/clip-cmd.js.map +1 -0
- package/packages/cli/dist/commands/cloud-cmd.d.ts +4 -0
- package/packages/cli/dist/commands/cloud-cmd.d.ts.map +1 -0
- package/packages/cli/dist/commands/cloud-cmd.js +64 -0
- package/packages/cli/dist/commands/cloud-cmd.js.map +1 -0
- package/packages/cli/dist/commands/contradictions-cmd.d.ts +2 -0
- package/packages/cli/dist/commands/contradictions-cmd.d.ts.map +1 -0
- package/packages/cli/dist/commands/contradictions-cmd.js +34 -0
- package/packages/cli/dist/commands/contradictions-cmd.js.map +1 -0
- package/packages/cli/dist/commands/decay-cmd.d.ts +2 -0
- package/packages/cli/dist/commands/decay-cmd.d.ts.map +1 -0
- package/packages/cli/dist/commands/decay-cmd.js +48 -0
- package/packages/cli/dist/commands/decay-cmd.js.map +1 -0
- package/packages/cli/dist/commands/digest-cmd.d.ts +4 -0
- package/packages/cli/dist/commands/digest-cmd.d.ts.map +1 -0
- package/packages/cli/dist/commands/digest-cmd.js +79 -0
- package/packages/cli/dist/commands/digest-cmd.js.map +1 -0
- package/packages/cli/dist/commands/duplicates-cmd.d.ts +4 -0
- package/packages/cli/dist/commands/duplicates-cmd.d.ts.map +1 -0
- package/packages/cli/dist/commands/duplicates-cmd.js +30 -0
- package/packages/cli/dist/commands/duplicates-cmd.js.map +1 -0
- package/packages/cli/dist/commands/federate-cmd.d.ts +5 -0
- package/packages/cli/dist/commands/federate-cmd.d.ts.map +1 -0
- package/packages/cli/dist/commands/federate-cmd.js +217 -0
- package/packages/cli/dist/commands/federate-cmd.js.map +1 -0
- package/packages/cli/dist/commands/gaps-cmd.d.ts +2 -0
- package/packages/cli/dist/commands/gaps-cmd.d.ts.map +1 -0
- package/packages/cli/dist/commands/gaps-cmd.js +33 -0
- package/packages/cli/dist/commands/gaps-cmd.js.map +1 -0
- package/packages/cli/dist/commands/graph-cmd.d.ts +2 -0
- package/packages/cli/dist/commands/graph-cmd.d.ts.map +1 -0
- package/packages/cli/dist/commands/graph-cmd.js +77 -0
- package/packages/cli/dist/commands/graph-cmd.js.map +1 -0
- package/packages/cli/dist/commands/index-cmd.d.ts +2 -0
- package/packages/cli/dist/commands/index-cmd.d.ts.map +1 -0
- package/packages/cli/dist/commands/index-cmd.js +57 -0
- package/packages/cli/dist/commands/index-cmd.js.map +1 -0
- package/packages/cli/dist/commands/init-cmd.d.ts +2 -0
- package/packages/cli/dist/commands/init-cmd.d.ts.map +1 -0
- package/packages/cli/dist/commands/init-cmd.js +123 -0
- package/packages/cli/dist/commands/init-cmd.js.map +1 -0
- package/packages/cli/dist/commands/learn-cmd.d.ts +2 -0
- package/packages/cli/dist/commands/learn-cmd.d.ts.map +1 -0
- package/packages/cli/dist/commands/learn-cmd.js +48 -0
- package/packages/cli/dist/commands/learn-cmd.js.map +1 -0
- package/packages/cli/dist/commands/pack-cmd.d.ts +15 -0
- package/packages/cli/dist/commands/pack-cmd.d.ts.map +1 -0
- package/packages/cli/dist/commands/pack-cmd.js +93 -0
- package/packages/cli/dist/commands/pack-cmd.js.map +1 -0
- package/packages/cli/dist/commands/review-cmd.d.ts +4 -0
- package/packages/cli/dist/commands/review-cmd.d.ts.map +1 -0
- package/packages/cli/dist/commands/review-cmd.js +107 -0
- package/packages/cli/dist/commands/review-cmd.js.map +1 -0
- package/packages/cli/dist/commands/search-cmd.d.ts +4 -0
- package/packages/cli/dist/commands/search-cmd.d.ts.map +1 -0
- package/packages/cli/dist/commands/search-cmd.js +38 -0
- package/packages/cli/dist/commands/search-cmd.js.map +1 -0
- package/packages/cli/dist/commands/serve-cmd.d.ts +2 -0
- package/packages/cli/dist/commands/serve-cmd.d.ts.map +1 -0
- package/packages/cli/dist/commands/serve-cmd.js +14 -0
- package/packages/cli/dist/commands/serve-cmd.js.map +1 -0
- package/packages/cli/dist/commands/status-cmd.d.ts +2 -0
- package/packages/cli/dist/commands/status-cmd.d.ts.map +1 -0
- package/packages/cli/dist/commands/status-cmd.js +33 -0
- package/packages/cli/dist/commands/status-cmd.js.map +1 -0
- package/packages/cli/dist/commands/sync-cmd.d.ts +5 -0
- package/packages/cli/dist/commands/sync-cmd.d.ts.map +1 -0
- package/packages/cli/dist/commands/sync-cmd.js +62 -0
- package/packages/cli/dist/commands/sync-cmd.js.map +1 -0
- package/packages/cli/dist/commands/vault-cmd.d.ts +10 -0
- package/packages/cli/dist/commands/vault-cmd.d.ts.map +1 -0
- package/packages/cli/dist/commands/vault-cmd.js +54 -0
- package/packages/cli/dist/commands/vault-cmd.js.map +1 -0
- package/packages/cli/dist/index.d.ts +2 -0
- package/packages/cli/dist/index.d.ts.map +1 -0
- package/packages/cli/dist/index.js +156 -0
- package/packages/cli/dist/index.js.map +1 -0
- package/packages/cli/package.json +24 -0
- package/packages/cli/src/commands/brief-cmd.ts +87 -0
- package/packages/cli/src/commands/capture-cmd.ts +34 -0
- package/packages/cli/src/commands/card-cmd.ts +29 -0
- package/packages/cli/src/commands/clip-cmd.ts +172 -0
- package/packages/cli/src/commands/cloud-cmd.ts +75 -0
- package/packages/cli/src/commands/contradictions-cmd.ts +41 -0
- package/packages/cli/src/commands/decay-cmd.ts +57 -0
- package/packages/cli/src/commands/digest-cmd.ts +89 -0
- package/packages/cli/src/commands/duplicates-cmd.ts +38 -0
- package/packages/cli/src/commands/federate-cmd.ts +236 -0
- package/packages/cli/src/commands/gaps-cmd.ts +40 -0
- package/packages/cli/src/commands/graph-cmd.ts +88 -0
- package/packages/cli/src/commands/index-cmd.ts +65 -0
- package/packages/cli/src/commands/init-cmd.ts +145 -0
- package/packages/cli/src/commands/learn-cmd.ts +56 -0
- package/packages/cli/src/commands/pack-cmd.ts +121 -0
- package/packages/cli/src/commands/review-cmd.ts +125 -0
- package/packages/cli/src/commands/search-cmd.ts +45 -0
- package/packages/cli/src/commands/serve-cmd.ts +17 -0
- package/packages/cli/src/commands/status-cmd.ts +37 -0
- package/packages/cli/src/commands/sync-cmd.ts +68 -0
- package/packages/cli/src/commands/vault-cmd.ts +64 -0
- package/packages/cli/src/index.ts +187 -0
- package/packages/core/package.json +40 -0
- package/packages/core/src/api/dashboard.ts +138 -0
- package/packages/core/src/api/graph-data.ts +286 -0
- package/packages/core/src/api/pwa.ts +82 -0
- package/packages/core/src/api/server.ts +660 -0
- package/packages/core/src/capture/voice.ts +168 -0
- package/packages/core/src/cloud/index.ts +2 -0
- package/packages/core/src/cloud/sync.ts +167 -0
- package/packages/core/src/config.ts +82 -0
- package/packages/core/src/federation/credits.ts +80 -0
- package/packages/core/src/federation/hyperswarm.d.ts +19 -0
- package/packages/core/src/federation/identity.ts +90 -0
- package/packages/core/src/federation/index.ts +8 -0
- package/packages/core/src/federation/node.ts +235 -0
- package/packages/core/src/federation/privacy.ts +52 -0
- package/packages/core/src/federation/reputation.ts +202 -0
- package/packages/core/src/federation/search.ts +129 -0
- package/packages/core/src/federation/sharing.ts +165 -0
- package/packages/core/src/federation/trust.ts +76 -0
- package/packages/core/src/federation/types.ts +25 -0
- package/packages/core/src/i18n/index.ts +85 -0
- package/packages/core/src/index.ts +133 -0
- package/packages/core/src/indexer/chunker.ts +180 -0
- package/packages/core/src/indexer/embedder.ts +9 -0
- package/packages/core/src/indexer/index.ts +113 -0
- package/packages/core/src/indexer/local-embedder.ts +35 -0
- package/packages/core/src/indexer/scanner.ts +142 -0
- package/packages/core/src/indexer/watcher.ts +62 -0
- package/packages/core/src/intelligence/contradiction-detector.ts +134 -0
- package/packages/core/src/intelligence/decay-engine.ts +229 -0
- package/packages/core/src/intelligence/duplicate-detector.ts +71 -0
- package/packages/core/src/intelligence/fsrs.ts +79 -0
- package/packages/core/src/intelligence/gap-detector.ts +109 -0
- package/packages/core/src/intelligence/learning-path.ts +86 -0
- package/packages/core/src/intelligence/notifications.ts +106 -0
- package/packages/core/src/intelligence/predictive-gaps.ts +94 -0
- package/packages/core/src/intelligence/semantic-versioning.ts +97 -0
- package/packages/core/src/intelligence/types.ts +28 -0
- package/packages/core/src/mcp/custom-tools.ts +97 -0
- package/packages/core/src/mcp/index.ts +1 -0
- package/packages/core/src/mcp/server.ts +142 -0
- package/packages/core/src/mcp/tools/agentic-graph.ts +96 -0
- package/packages/core/src/mcp/tools/brief.ts +49 -0
- package/packages/core/src/mcp/tools/decay.ts +40 -0
- package/packages/core/src/mcp/tools/decision-journal.ts +95 -0
- package/packages/core/src/mcp/tools/export.ts +72 -0
- package/packages/core/src/mcp/tools/federated-search.ts +43 -0
- package/packages/core/src/mcp/tools/generate-claude-md.ts +130 -0
- package/packages/core/src/mcp/tools/get-document.ts +26 -0
- package/packages/core/src/mcp/tools/get-related.ts +41 -0
- package/packages/core/src/mcp/tools/learning-path.ts +52 -0
- package/packages/core/src/mcp/tools/list-topics.ts +20 -0
- package/packages/core/src/mcp/tools/search.ts +35 -0
- package/packages/core/src/mcp/tools/snapshot.ts +98 -0
- package/packages/core/src/multi-vault/index.ts +118 -0
- package/packages/core/src/pack/creator.ts +127 -0
- package/packages/core/src/pack/exporter.ts +21 -0
- package/packages/core/src/pack/importer.ts +82 -0
- package/packages/core/src/pack/index.ts +5 -0
- package/packages/core/src/pack/marketplace.ts +103 -0
- package/packages/core/src/pack/pii-masker.ts +38 -0
- package/packages/core/src/pack/types.ts +39 -0
- package/packages/core/src/plugins/index.ts +100 -0
- package/packages/core/src/plugins/webhooks.ts +110 -0
- package/packages/core/src/search/bm25.ts +16 -0
- package/packages/core/src/search/index.ts +83 -0
- package/packages/core/src/search/rrf.ts +31 -0
- package/packages/core/src/search/semantic.ts +15 -0
- package/packages/core/src/store/index.ts +2 -0
- package/packages/core/src/store/sqlite-vec.ts +290 -0
- package/packages/core/src/store/types.ts +22 -0
- package/packages/core/src/team/index.ts +126 -0
- package/packages/core/src/types/chunk.ts +25 -0
- package/packages/core/src/types/document.ts +24 -0
- package/packages/core/src/types/graph.ts +44 -0
- package/packages/core/src/types/index.ts +15 -0
- package/packages/core/src/types/search.ts +38 -0
- package/packages/core/src/utils/retry.ts +85 -0
- package/packages/core/tests/api-card.test.ts +60 -0
- package/packages/core/tests/api-routes.test.ts +98 -0
- package/packages/core/tests/bm25.test.ts +87 -0
- package/packages/core/tests/chunker.test.ts +48 -0
- package/packages/core/tests/cluster.test.ts +75 -0
- package/packages/core/tests/constellation.test.ts +77 -0
- package/packages/core/tests/export-utils.test.ts +97 -0
- package/packages/core/tests/fsrs.test.ts +96 -0
- package/packages/core/tests/gesture-detector.test.ts +45 -0
- package/packages/core/tests/graph-data.test.ts +87 -0
- package/packages/core/tests/layout.test.ts +83 -0
- package/packages/core/tests/mcp.test.ts +148 -0
- package/packages/core/tests/pack.test.ts +127 -0
- package/packages/core/tests/pii-masker.test.ts +42 -0
- package/packages/core/tests/profile-card.test.ts +62 -0
- package/packages/core/tests/rrf.test.ts +29 -0
- package/packages/core/tests/search-integration.test.ts +139 -0
- package/packages/core/tests/store.test.ts +80 -0
- package/packages/graph/click-result.png +0 -0
- package/packages/graph/index.html +17 -0
- package/packages/graph/package.json +32 -0
- package/packages/graph/src/App.tsx +7 -0
- package/packages/graph/src/api/client.ts +39 -0
- package/packages/graph/src/components/ClusterFilter.tsx +73 -0
- package/packages/graph/src/components/ConstellationView.tsx +232 -0
- package/packages/graph/src/components/ExportPanel.tsx +177 -0
- package/packages/graph/src/components/Graph3D.tsx +230 -0
- package/packages/graph/src/components/GraphEdges.tsx +100 -0
- package/packages/graph/src/components/GraphNodes.tsx +386 -0
- package/packages/graph/src/components/HealthDashboard.tsx +173 -0
- package/packages/graph/src/components/Layout.tsx +214 -0
- package/packages/graph/src/components/MotionOverlay.tsx +81 -0
- package/packages/graph/src/components/MotionToggle.tsx +33 -0
- package/packages/graph/src/components/MultiverseView.tsx +286 -0
- package/packages/graph/src/components/NodeDetail.tsx +232 -0
- package/packages/graph/src/components/PulseParticle.tsx +232 -0
- package/packages/graph/src/components/SearchBar.tsx +107 -0
- package/packages/graph/src/components/StarField.tsx +197 -0
- package/packages/graph/src/components/StatusBar.tsx +53 -0
- package/packages/graph/src/components/Timeline.tsx +148 -0
- package/packages/graph/src/components/ToolsPanel.tsx +512 -0
- package/packages/graph/src/components/Tooltip.tsx +100 -0
- package/packages/graph/src/components/TypeFilter.tsx +131 -0
- package/packages/graph/src/embed/EmbedGraph.tsx +144 -0
- package/packages/graph/src/hooks/useConstellationLOD.ts +76 -0
- package/packages/graph/src/hooks/useDecay.ts +37 -0
- package/packages/graph/src/hooks/useExport.ts +165 -0
- package/packages/graph/src/hooks/useGraph.ts +69 -0
- package/packages/graph/src/hooks/useKeyboardNav.ts +122 -0
- package/packages/graph/src/hooks/useLayout.ts +45 -0
- package/packages/graph/src/hooks/useMotion.ts +120 -0
- package/packages/graph/src/hooks/usePulse.ts +58 -0
- package/packages/graph/src/hooks/useSearch.ts +71 -0
- package/packages/graph/src/lib/constellation.ts +107 -0
- package/packages/graph/src/lib/export-utils.ts +48 -0
- package/packages/graph/src/lib/gesture-detector.ts +123 -0
- package/packages/graph/src/lib/layout.worker.ts +153 -0
- package/packages/graph/src/lib/motion-controller.ts +83 -0
- package/packages/graph/src/lib/profile-card.ts +122 -0
- package/packages/graph/src/main.tsx +4 -0
- package/packages/graph/src/stores/graph-store.ts +155 -0
- package/packages/graph/success.png +0 -0
- package/packages/graph/test-click.mjs +49 -0
- package/packages/graph/test-explore.mjs +102 -0
- package/packages/graph/test-final.mjs +61 -0
- package/packages/graph/test-graph.mjs +139 -0
- package/packages/graph/test-hover.mjs +48 -0
- package/packages/graph/test-pulse.mjs +68 -0
- package/packages/graph/test-screenshot.mjs +56 -0
- package/packages/graph/test-v2.mjs +97 -0
- package/packages/graph/vite.config.ts +15 -0
- package/packages/sync/.env.example +11 -0
- package/packages/sync/.sync-state.json +317 -0
- package/packages/sync/.upload-state.json +1009 -0
- package/packages/sync/create-stella-network-notion.mjs +151 -0
- package/packages/sync/create-stellavault-project-notion.mjs +322 -0
- package/packages/sync/logs/sync-2026-03-28.log +6 -0
- package/packages/sync/logs/sync-2026-03-29.log +12 -0
- package/packages/sync/logs/sync-2026-03-30.log +6 -0
- package/packages/sync/logs/sync-2026-03-31.log +6 -0
- package/packages/sync/logs/sync-2026-04-01.log +6 -0
- package/packages/sync/logs/sync-2026-04-02.log +6 -0
- package/packages/sync/package-lock.json +373 -0
- package/packages/sync/package.json +16 -0
- package/packages/sync/run-sync.bat +18 -0
- package/packages/sync/run-sync.mjs +46 -0
- package/packages/sync/setup-scheduler.mjs +119 -0
- package/packages/sync/structured-sync.mjs +187 -0
- package/packages/sync/sync-to-obsidian.mjs +264 -0
- package/packages/sync/upload-pdca-to-notion.mjs +495 -0
- package/tsconfig.base.json +18 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Design Ref: §12.1 F14 — 결정 저널 (기술 결정 자동 기록)
|
|
2
|
+
|
|
3
|
+
import { writeFileSync, existsSync, mkdirSync, readdirSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
5
|
+
import type { SearchEngine } from '../../search/index.js';
|
|
6
|
+
|
|
7
|
+
export const logDecisionToolDef = {
|
|
8
|
+
name: 'log-decision',
|
|
9
|
+
description: '기술적 결정을 구조화하여 기록합니다. 나중에 "왜 이 선택을 했지?"에 답변할 수 있습니다.',
|
|
10
|
+
inputSchema: {
|
|
11
|
+
type: 'object' as const,
|
|
12
|
+
properties: {
|
|
13
|
+
title: { type: 'string', description: '결정 제목 (예: Zustand 대신 Jotai 선택)' },
|
|
14
|
+
context: { type: 'string', description: '결정 배경/상황' },
|
|
15
|
+
decision: { type: 'string', description: '선택한 내용' },
|
|
16
|
+
alternatives: { type: 'array', items: { type: 'string' }, description: '고려한 대안들' },
|
|
17
|
+
reasoning: { type: 'string', description: '선택 이유' },
|
|
18
|
+
project: { type: 'string', description: '관련 프로젝트명' },
|
|
19
|
+
},
|
|
20
|
+
required: ['title', 'decision', 'reasoning'],
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const findDecisionsToolDef = {
|
|
25
|
+
name: 'find-decisions',
|
|
26
|
+
description: '과거 기술 결정을 검색합니다.',
|
|
27
|
+
inputSchema: {
|
|
28
|
+
type: 'object' as const,
|
|
29
|
+
properties: {
|
|
30
|
+
query: { type: 'string', description: '검색 쿼리' },
|
|
31
|
+
},
|
|
32
|
+
required: ['query'],
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export async function handleLogDecision(
|
|
37
|
+
vaultPath: string,
|
|
38
|
+
args: {
|
|
39
|
+
title: string; context?: string; decision: string;
|
|
40
|
+
alternatives?: string[]; reasoning: string; project?: string;
|
|
41
|
+
},
|
|
42
|
+
) {
|
|
43
|
+
const decisionsDir = join(vaultPath, 'decisions');
|
|
44
|
+
mkdirSync(decisionsDir, { recursive: true });
|
|
45
|
+
|
|
46
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
47
|
+
const slug = args.title.replace(/[^a-zA-Z가-힣0-9\s-]/g, '').replace(/\s+/g, '-').slice(0, 50);
|
|
48
|
+
if (!slug) throw new Error('Invalid decision title');
|
|
49
|
+
const fileName = `${date}-${slug}.md`;
|
|
50
|
+
|
|
51
|
+
const content = [
|
|
52
|
+
'---',
|
|
53
|
+
`title: "${args.title}"`,
|
|
54
|
+
`date: ${date}`,
|
|
55
|
+
`project: "${args.project ?? ''}"`,
|
|
56
|
+
'type: decision',
|
|
57
|
+
'---',
|
|
58
|
+
'',
|
|
59
|
+
`# ${args.title}`,
|
|
60
|
+
'',
|
|
61
|
+
args.context ? `## 배경\n\n${args.context}\n` : '',
|
|
62
|
+
`## 결정\n\n${args.decision}\n`,
|
|
63
|
+
args.alternatives?.length
|
|
64
|
+
? `## 고려한 대안\n\n${args.alternatives.map(a => `- ${a}`).join('\n')}\n`
|
|
65
|
+
: '',
|
|
66
|
+
`## 이유\n\n${args.reasoning}\n`,
|
|
67
|
+
].filter(Boolean).join('\n');
|
|
68
|
+
|
|
69
|
+
const filePath = resolve(decisionsDir, fileName);
|
|
70
|
+
if (!filePath.startsWith(resolve(decisionsDir))) {
|
|
71
|
+
throw new Error('Path traversal detected');
|
|
72
|
+
}
|
|
73
|
+
writeFileSync(filePath, content, 'utf-8');
|
|
74
|
+
|
|
75
|
+
return { saved: filePath, fileName };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function handleFindDecisions(vaultPath: string, args: { query: string }) {
|
|
79
|
+
const decisionsDir = join(vaultPath, 'decisions');
|
|
80
|
+
if (!existsSync(decisionsDir)) return { decisions: [], message: 'No decisions directory' };
|
|
81
|
+
|
|
82
|
+
const files = readdirSync(decisionsDir).filter(f => f.endsWith('.md'));
|
|
83
|
+
const query = args.query.toLowerCase();
|
|
84
|
+
|
|
85
|
+
const matches = files
|
|
86
|
+
.map(f => {
|
|
87
|
+
const content = readFileSync(join(decisionsDir, f), 'utf-8');
|
|
88
|
+
const score = content.toLowerCase().includes(query) ? 1 : 0;
|
|
89
|
+
return { file: f, content: content.slice(0, 300), score };
|
|
90
|
+
})
|
|
91
|
+
.filter(m => m.score > 0)
|
|
92
|
+
.slice(0, 10);
|
|
93
|
+
|
|
94
|
+
return { decisions: matches, total: files.length };
|
|
95
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Design Ref: §12.1 F20 — 지식 내보내기 포맷 (락인 방지)
|
|
2
|
+
|
|
3
|
+
import { writeFileSync, mkdirSync } from 'node:fs';
|
|
4
|
+
import { join, dirname, resolve } from 'node:path';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import type { VectorStore } from '../../store/types.js';
|
|
7
|
+
|
|
8
|
+
const ALLOWED_EXPORT_DIRS = [
|
|
9
|
+
resolve(homedir(), '.stellavault'),
|
|
10
|
+
resolve('.'),
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
function validateExportPath(outputPath: string): string {
|
|
14
|
+
const resolved = resolve(outputPath);
|
|
15
|
+
const isAllowed = ALLOWED_EXPORT_DIRS.some(dir => resolved.startsWith(resolve(dir)));
|
|
16
|
+
if (!isAllowed) {
|
|
17
|
+
throw new Error(`Export path must be within current directory or ~/.stellavault/. Got: ${resolved}`);
|
|
18
|
+
}
|
|
19
|
+
return resolved;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const exportToolDef = {
|
|
23
|
+
name: 'export',
|
|
24
|
+
description: '벡터 DB의 문서와 메타데이터를 JSON 파일로 내보냅니다. 다른 도구로 이전 가능.',
|
|
25
|
+
inputSchema: {
|
|
26
|
+
type: 'object' as const,
|
|
27
|
+
properties: {
|
|
28
|
+
outputPath: { type: 'string', description: '출력 파일 경로 (예: ./export.json)' },
|
|
29
|
+
format: { type: 'string', enum: ['json', 'csv'], description: '출력 포맷 (기본: json)' },
|
|
30
|
+
},
|
|
31
|
+
required: ['outputPath'],
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export async function handleExport(
|
|
36
|
+
store: VectorStore,
|
|
37
|
+
args: { outputPath: string; format?: string },
|
|
38
|
+
) {
|
|
39
|
+
const docs = await store.getAllDocuments();
|
|
40
|
+
const stats = await store.getStats();
|
|
41
|
+
const topics = await store.getTopics();
|
|
42
|
+
|
|
43
|
+
const safePath = validateExportPath(args.outputPath);
|
|
44
|
+
mkdirSync(dirname(safePath), { recursive: true });
|
|
45
|
+
|
|
46
|
+
if (args.format === 'csv') {
|
|
47
|
+
const header = 'id,filePath,title,tags,lastModified,contentHash';
|
|
48
|
+
const rows = docs.map(d =>
|
|
49
|
+
`"${d.id}","${d.filePath}","${d.title.replace(/"/g, '""')}","${d.tags.join(';')}","${d.lastModified}","${d.contentHash}"`
|
|
50
|
+
);
|
|
51
|
+
writeFileSync(safePath, [header, ...rows].join('\n'), 'utf-8');
|
|
52
|
+
} else {
|
|
53
|
+
const exported = {
|
|
54
|
+
exportedAt: new Date().toISOString(),
|
|
55
|
+
version: '1.0',
|
|
56
|
+
stats,
|
|
57
|
+
topics,
|
|
58
|
+
documents: docs.map(d => ({
|
|
59
|
+
id: d.id,
|
|
60
|
+
filePath: d.filePath,
|
|
61
|
+
title: d.title,
|
|
62
|
+
tags: d.tags,
|
|
63
|
+
frontmatter: d.frontmatter,
|
|
64
|
+
lastModified: d.lastModified,
|
|
65
|
+
contentLength: d.content.length,
|
|
66
|
+
})),
|
|
67
|
+
};
|
|
68
|
+
writeFileSync(safePath, JSON.stringify(exported, null, 2), 'utf-8');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { exported: docs.length, path: safePath, format: args.format ?? 'json' };
|
|
72
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// MCP Tool: federated-search (Federation Phase 1b)
|
|
2
|
+
// AI 에이전트가 연합 네트워크를 검색
|
|
3
|
+
|
|
4
|
+
import type { FederatedSearch } from '../../federation/search.js';
|
|
5
|
+
|
|
6
|
+
export function createFederatedSearchTool(federatedSearch: FederatedSearch | null) {
|
|
7
|
+
return {
|
|
8
|
+
name: 'federated-search',
|
|
9
|
+
description: 'Search across all connected Federation peers. Returns results from other Stellavault nodes in the P2P network. Only titles, similarity scores, and 50-char snippets are shared — no raw text leaves any node.',
|
|
10
|
+
inputSchema: {
|
|
11
|
+
type: 'object' as const,
|
|
12
|
+
properties: {
|
|
13
|
+
query: { type: 'string', description: 'Search query' },
|
|
14
|
+
limit: { type: 'number', description: 'Max results per peer (default: 5)' },
|
|
15
|
+
},
|
|
16
|
+
required: ['query'],
|
|
17
|
+
},
|
|
18
|
+
async handler(args: { query: string; limit?: number }) {
|
|
19
|
+
if (!federatedSearch) {
|
|
20
|
+
return { content: [{ type: 'text' as const, text: 'Federation not active. Run `sv federate join` first.' }] };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const results = await federatedSearch.search(args.query, { limit: args.limit ?? 5 });
|
|
24
|
+
|
|
25
|
+
if (results.length === 0) {
|
|
26
|
+
return { content: [{ type: 'text' as const, text: 'No results from federation peers. Either no peers connected or no matching documents.' }] };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const lines = [
|
|
30
|
+
`🌐 Federation Search: "${args.query}" — ${results.length} results from ${new Set(results.map(r => r.peerId)).size} peers`,
|
|
31
|
+
'',
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
for (const r of results) {
|
|
35
|
+
lines.push(`**${r.title}** (${Math.round(r.similarity * 100)}%) [${r.peerName}]`);
|
|
36
|
+
lines.push(` ${r.snippet}...`);
|
|
37
|
+
lines.push('');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return { content: [{ type: 'text' as const, text: lines.join('\n') }] };
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// Design Ref: §12.1 F13 — CLAUDE.md 자동 생성기 (킬러 유스케이스)
|
|
2
|
+
|
|
3
|
+
import type { SearchEngine } from '../../search/index.js';
|
|
4
|
+
import type { VectorStore } from '../../store/types.js';
|
|
5
|
+
|
|
6
|
+
export const generateClaudeMdToolDef = {
|
|
7
|
+
name: 'generate-claude-md',
|
|
8
|
+
description: '프로젝트명을 기반으로 관련 지식(아키텍처, 패턴, 교훈, 결정사항)을 검색하여 CLAUDE.md 초안을 자동 생성합니다.',
|
|
9
|
+
inputSchema: {
|
|
10
|
+
type: 'object' as const,
|
|
11
|
+
properties: {
|
|
12
|
+
projectName: { type: 'string', description: '프로젝트명 또는 주요 키워드' },
|
|
13
|
+
topics: {
|
|
14
|
+
type: 'array',
|
|
15
|
+
items: { type: 'string' },
|
|
16
|
+
description: '추가 검색할 토픽 (예: ["인증", "배포", "성능"])',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
required: ['projectName'],
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export async function handleGenerateClaudeMd(
|
|
24
|
+
searchEngine: SearchEngine,
|
|
25
|
+
store: VectorStore,
|
|
26
|
+
args: { projectName: string; topics?: string[] },
|
|
27
|
+
) {
|
|
28
|
+
const { projectName, topics = [] } = args;
|
|
29
|
+
|
|
30
|
+
// 프로젝트 관련 지식 검색 (여러 관점)
|
|
31
|
+
const queries = [
|
|
32
|
+
`${projectName} 아키텍처 설계`,
|
|
33
|
+
`${projectName} 패턴 컨벤션`,
|
|
34
|
+
`${projectName} 교훈 실수 주의사항`,
|
|
35
|
+
`${projectName} 기술 스택 의존성`,
|
|
36
|
+
...topics.map(t => `${projectName} ${t}`),
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const allResults = new Map<string, { title: string; content: string; score: number; category: string }>();
|
|
40
|
+
|
|
41
|
+
for (const query of queries) {
|
|
42
|
+
const results = await searchEngine.search({ query, limit: 3 });
|
|
43
|
+
const category = query.replace(projectName, '').trim();
|
|
44
|
+
for (const r of results) {
|
|
45
|
+
const key = r.chunk.id;
|
|
46
|
+
if (!allResults.has(key) || (allResults.get(key)!.score < r.score)) {
|
|
47
|
+
allResults.set(key, {
|
|
48
|
+
title: `${r.document.title} §${r.chunk.heading}`,
|
|
49
|
+
content: r.chunk.content.slice(0, 500),
|
|
50
|
+
score: r.score,
|
|
51
|
+
category,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 카테고리별 분류
|
|
58
|
+
const sorted = [...allResults.values()].sort((a, b) => b.score - a.score);
|
|
59
|
+
|
|
60
|
+
// CLAUDE.md 초안 생성
|
|
61
|
+
const sections: string[] = [];
|
|
62
|
+
sections.push(`# ${projectName} — CLAUDE.md`);
|
|
63
|
+
sections.push('');
|
|
64
|
+
sections.push('> 이 파일은 evan-knowledge-hub MCP에서 자동 생성되었습니다.');
|
|
65
|
+
sections.push(`> 생성일: ${new Date().toISOString().slice(0, 10)}`);
|
|
66
|
+
sections.push(`> 참조 지식: ${sorted.length}건`);
|
|
67
|
+
sections.push('');
|
|
68
|
+
|
|
69
|
+
// 아키텍처 섹션
|
|
70
|
+
const archResults = sorted.filter(r => r.category.includes('아키텍처') || r.category.includes('설계'));
|
|
71
|
+
if (archResults.length > 0) {
|
|
72
|
+
sections.push('## 아키텍처 & 설계');
|
|
73
|
+
sections.push('');
|
|
74
|
+
for (const r of archResults.slice(0, 3)) {
|
|
75
|
+
sections.push(`### ${r.title}`);
|
|
76
|
+
sections.push(r.content.trim());
|
|
77
|
+
sections.push('');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 패턴/컨벤션 섹션
|
|
82
|
+
const patternResults = sorted.filter(r => r.category.includes('패턴') || r.category.includes('컨벤션'));
|
|
83
|
+
if (patternResults.length > 0) {
|
|
84
|
+
sections.push('## 코딩 패턴 & 컨벤션');
|
|
85
|
+
sections.push('');
|
|
86
|
+
for (const r of patternResults.slice(0, 3)) {
|
|
87
|
+
sections.push(`### ${r.title}`);
|
|
88
|
+
sections.push(r.content.trim());
|
|
89
|
+
sections.push('');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 교훈 섹션
|
|
94
|
+
const lessonResults = sorted.filter(r => r.category.includes('교훈') || r.category.includes('실수'));
|
|
95
|
+
if (lessonResults.length > 0) {
|
|
96
|
+
sections.push('## 교훈 & 주의사항');
|
|
97
|
+
sections.push('');
|
|
98
|
+
for (const r of lessonResults.slice(0, 5)) {
|
|
99
|
+
sections.push(`- **${r.title}**: ${r.content.slice(0, 200).replace(/\n/g, ' ').trim()}`);
|
|
100
|
+
}
|
|
101
|
+
sections.push('');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 기타 관련 지식
|
|
105
|
+
const otherResults = sorted.filter(r =>
|
|
106
|
+
!r.category.includes('아키텍처') && !r.category.includes('설계') &&
|
|
107
|
+
!r.category.includes('패턴') && !r.category.includes('교훈') && !r.category.includes('실수')
|
|
108
|
+
);
|
|
109
|
+
if (otherResults.length > 0) {
|
|
110
|
+
sections.push('## 관련 지식');
|
|
111
|
+
sections.push('');
|
|
112
|
+
for (const r of otherResults.slice(0, 5)) {
|
|
113
|
+
sections.push(`- **${r.title}** (score: ${r.score.toFixed(3)})`);
|
|
114
|
+
}
|
|
115
|
+
sections.push('');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const claudeMd = sections.join('\n');
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
content: claudeMd,
|
|
122
|
+
stats: {
|
|
123
|
+
queriesRun: queries.length,
|
|
124
|
+
uniqueResults: sorted.length,
|
|
125
|
+
sections: ['아키텍처', '패턴', '교훈', '관련 지식'].filter((_, i) =>
|
|
126
|
+
[archResults, patternResults, lessonResults, otherResults][i].length > 0
|
|
127
|
+
),
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { VectorStore } from '../../store/types.js';
|
|
2
|
+
|
|
3
|
+
export const getDocumentToolDef = {
|
|
4
|
+
name: 'get-document',
|
|
5
|
+
description: '문서 ID 또는 파일 경로로 전체 문서 내용을 가져옵니다.',
|
|
6
|
+
inputSchema: {
|
|
7
|
+
type: 'object' as const,
|
|
8
|
+
properties: {
|
|
9
|
+
id: { type: 'string', description: '문서 ID 또는 파일 경로' },
|
|
10
|
+
},
|
|
11
|
+
required: ['id'],
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export async function handleGetDocument(store: VectorStore, args: { id: string }) {
|
|
16
|
+
const doc = await store.getDocument(args.id);
|
|
17
|
+
if (!doc) return { error: `Document not found: ${args.id}` };
|
|
18
|
+
return {
|
|
19
|
+
title: doc.title,
|
|
20
|
+
filePath: doc.filePath,
|
|
21
|
+
content: doc.content,
|
|
22
|
+
tags: doc.tags,
|
|
23
|
+
frontmatter: doc.frontmatter,
|
|
24
|
+
lastModified: doc.lastModified,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { SearchEngine } from '../../search/index.js';
|
|
2
|
+
import type { VectorStore } from '../../store/types.js';
|
|
3
|
+
|
|
4
|
+
export const getRelatedToolDef = {
|
|
5
|
+
name: 'get-related',
|
|
6
|
+
description: '특정 문서와 의미적으로 관련된 문서들을 반환합니다.',
|
|
7
|
+
inputSchema: {
|
|
8
|
+
type: 'object' as const,
|
|
9
|
+
properties: {
|
|
10
|
+
id: { type: 'string', description: '기준 문서 ID 또는 파일 경로' },
|
|
11
|
+
limit: { type: 'number', description: '반환할 관련 문서 수 (기본: 5)' },
|
|
12
|
+
},
|
|
13
|
+
required: ['id'],
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export async function handleGetRelated(
|
|
18
|
+
store: VectorStore,
|
|
19
|
+
searchEngine: SearchEngine,
|
|
20
|
+
args: { id: string; limit?: number },
|
|
21
|
+
) {
|
|
22
|
+
const doc = await store.getDocument(args.id);
|
|
23
|
+
if (!doc) return { error: `Document not found: ${args.id}` };
|
|
24
|
+
|
|
25
|
+
// 문서 제목+내용 일부를 쿼리로 사용하여 관련 문서 검색
|
|
26
|
+
const query = `${doc.title} ${doc.content.slice(0, 200)}`;
|
|
27
|
+
const results = await searchEngine.search({
|
|
28
|
+
query,
|
|
29
|
+
limit: (args.limit ?? 5) + 1, // 자기 자신 제외용 +1
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return results
|
|
33
|
+
.filter(r => r.document.id !== args.id)
|
|
34
|
+
.slice(0, args.limit ?? 5)
|
|
35
|
+
.map(r => ({
|
|
36
|
+
title: r.document.title,
|
|
37
|
+
filePath: r.document.filePath,
|
|
38
|
+
score: Math.round(r.score * 1000) / 1000,
|
|
39
|
+
tags: r.document.tags,
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// MCP Tool: get-learning-path (F-A11)
|
|
2
|
+
|
|
3
|
+
import type { VectorStore } from '../../store/types.js';
|
|
4
|
+
import { DecayEngine } from '../../intelligence/decay-engine.js';
|
|
5
|
+
import { detectKnowledgeGaps } from '../../intelligence/gap-detector.js';
|
|
6
|
+
import { generateLearningPath } from '../../intelligence/learning-path.js';
|
|
7
|
+
|
|
8
|
+
export function createLearningPathTool(store: VectorStore) {
|
|
9
|
+
return {
|
|
10
|
+
name: 'get-learning-path',
|
|
11
|
+
description: 'Generate a personalized learning path based on knowledge decay and gaps. Returns prioritized list of what to review, explore, or bridge.',
|
|
12
|
+
inputSchema: {
|
|
13
|
+
type: 'object' as const,
|
|
14
|
+
properties: {
|
|
15
|
+
limit: { type: 'number', description: 'Max items to return (default: 10)' },
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
async handler(args: { limit?: number }) {
|
|
19
|
+
const limit = args.limit ?? 10;
|
|
20
|
+
const db = store.getDb() as any;
|
|
21
|
+
if (!db) return { content: [{ type: 'text' as const, text: 'Database not available' }] };
|
|
22
|
+
|
|
23
|
+
const decayEngine = new DecayEngine(db);
|
|
24
|
+
const decayReport = await decayEngine.computeAll();
|
|
25
|
+
|
|
26
|
+
let gaps: any[] = [];
|
|
27
|
+
try {
|
|
28
|
+
const gapReport = await detectKnowledgeGaps(store);
|
|
29
|
+
gaps = gapReport.gaps ?? [];
|
|
30
|
+
} catch { /* ignore */ }
|
|
31
|
+
|
|
32
|
+
const path = generateLearningPath({ decayReport, gaps }, limit);
|
|
33
|
+
|
|
34
|
+
const lines = [
|
|
35
|
+
`🎯 Learning Path (${path.items.length} items, ~${path.summary.estimatedMinutes}min)`,
|
|
36
|
+
'',
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
for (const item of path.items) {
|
|
40
|
+
const icon = item.category === 'review' ? '📖' : item.category === 'bridge' ? '🌉' : '🔭';
|
|
41
|
+
lines.push(`${icon} [${item.priority}] ${item.title} (${item.score}pt)`);
|
|
42
|
+
lines.push(` ${item.reason}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (path.items.length === 0) {
|
|
46
|
+
lines.push('All clear! Knowledge is in great shape.');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { content: [{ type: 'text' as const, text: lines.join('\n') }] };
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { VectorStore } from '../../store/types.js';
|
|
2
|
+
|
|
3
|
+
export const listTopicsToolDef = {
|
|
4
|
+
name: 'list-topics',
|
|
5
|
+
description: '지식 베이스의 전체 토픽/태그 목록과 문서 수를 반환합니다.',
|
|
6
|
+
inputSchema: {
|
|
7
|
+
type: 'object' as const,
|
|
8
|
+
properties: {},
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export async function handleListTopics(store: VectorStore) {
|
|
13
|
+
const topics = await store.getTopics();
|
|
14
|
+
const stats = await store.getStats();
|
|
15
|
+
return {
|
|
16
|
+
topics,
|
|
17
|
+
totalDocuments: stats.documentCount,
|
|
18
|
+
totalChunks: stats.chunkCount,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { SearchEngine } from '../../search/index.js';
|
|
2
|
+
|
|
3
|
+
export const searchToolDef = {
|
|
4
|
+
name: 'search',
|
|
5
|
+
description: '개인 지식 베이스에서 관련 문서/청크를 검색합니다. 자연어 쿼리와 키워드 모두 지원합니다.',
|
|
6
|
+
inputSchema: {
|
|
7
|
+
type: 'object' as const,
|
|
8
|
+
properties: {
|
|
9
|
+
query: { type: 'string', description: '검색 쿼리 (자연어 또는 키워드)' },
|
|
10
|
+
limit: { type: 'number', description: '반환할 결과 수 (기본: 5)' },
|
|
11
|
+
tags: { type: 'array', items: { type: 'string' }, description: '태그 필터' },
|
|
12
|
+
},
|
|
13
|
+
required: ['query'],
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export async function handleSearch(
|
|
18
|
+
searchEngine: SearchEngine,
|
|
19
|
+
args: { query: string; limit?: number; tags?: string[] },
|
|
20
|
+
) {
|
|
21
|
+
const results = await searchEngine.search({
|
|
22
|
+
query: args.query,
|
|
23
|
+
limit: args.limit ?? 5,
|
|
24
|
+
tags: args.tags,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return results.map(r => ({
|
|
28
|
+
title: r.document.title,
|
|
29
|
+
filePath: r.document.filePath,
|
|
30
|
+
heading: r.chunk.heading,
|
|
31
|
+
content: r.chunk.content,
|
|
32
|
+
score: Math.round(r.score * 1000) / 1000,
|
|
33
|
+
tags: r.document.tags,
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Design Ref: §12.1 F10 — 컨텍스트 스냅샷 (프로젝트별 지식 묶음)
|
|
2
|
+
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
4
|
+
import { join, resolve, relative } from 'node:path';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import type { SearchEngine } from '../../search/index.js';
|
|
7
|
+
|
|
8
|
+
const SNAPSHOT_DIR = join(homedir(), '.stellavault', 'snapshots');
|
|
9
|
+
|
|
10
|
+
function sanitizeName(name: string): string {
|
|
11
|
+
const sanitized = name.replace(/[^a-zA-Z0-9가-힣_-]/g, '');
|
|
12
|
+
if (!sanitized) throw new Error('Invalid snapshot name');
|
|
13
|
+
return sanitized;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function ensureWithinDir(dir: string, filePath: string): string {
|
|
17
|
+
const resolved = resolve(dir, filePath);
|
|
18
|
+
if (!resolved.startsWith(resolve(dir))) {
|
|
19
|
+
throw new Error('Path traversal detected');
|
|
20
|
+
}
|
|
21
|
+
return resolved;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const createSnapshotToolDef = {
|
|
25
|
+
name: 'create-snapshot',
|
|
26
|
+
description: '현재 프로젝트 관련 지식을 스냅샷으로 저장합니다. 나중에 load-snapshot으로 즉시 컨텍스트 복원.',
|
|
27
|
+
inputSchema: {
|
|
28
|
+
type: 'object' as const,
|
|
29
|
+
properties: {
|
|
30
|
+
name: { type: 'string', description: '스냅샷 이름 (예: my-project-v1)' },
|
|
31
|
+
queries: { type: 'array', items: { type: 'string' }, description: '관련 지식 검색 쿼리들' },
|
|
32
|
+
},
|
|
33
|
+
required: ['name', 'queries'],
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const loadSnapshotToolDef = {
|
|
38
|
+
name: 'load-snapshot',
|
|
39
|
+
description: '저장된 스냅샷을 로드하여 프로젝트 컨텍스트를 즉시 복원합니다.',
|
|
40
|
+
inputSchema: {
|
|
41
|
+
type: 'object' as const,
|
|
42
|
+
properties: {
|
|
43
|
+
name: { type: 'string', description: '스냅샷 이름' },
|
|
44
|
+
},
|
|
45
|
+
required: ['name'],
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export async function handleCreateSnapshot(
|
|
50
|
+
searchEngine: SearchEngine,
|
|
51
|
+
args: { name: string; queries: string[] },
|
|
52
|
+
) {
|
|
53
|
+
mkdirSync(SNAPSHOT_DIR, { recursive: true });
|
|
54
|
+
|
|
55
|
+
const results = [];
|
|
56
|
+
for (const query of args.queries) {
|
|
57
|
+
const hits = await searchEngine.search({ query, limit: 5 });
|
|
58
|
+
results.push(...hits.map(r => ({
|
|
59
|
+
query,
|
|
60
|
+
title: r.document.title,
|
|
61
|
+
filePath: r.document.filePath,
|
|
62
|
+
heading: r.chunk.heading,
|
|
63
|
+
content: r.chunk.content.slice(0, 500),
|
|
64
|
+
score: r.score,
|
|
65
|
+
})));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 중복 제거
|
|
69
|
+
const seen = new Set<string>();
|
|
70
|
+
const unique = results.filter(r => {
|
|
71
|
+
const key = `${r.filePath}:${r.heading}`;
|
|
72
|
+
if (seen.has(key)) return false;
|
|
73
|
+
seen.add(key);
|
|
74
|
+
return true;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const snapshot = {
|
|
78
|
+
name: args.name,
|
|
79
|
+
createdAt: new Date().toISOString(),
|
|
80
|
+
queries: args.queries,
|
|
81
|
+
results: unique,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const safeName = sanitizeName(args.name);
|
|
85
|
+
const filePath = ensureWithinDir(SNAPSHOT_DIR, `${safeName}.json`);
|
|
86
|
+
writeFileSync(filePath, JSON.stringify(snapshot, null, 2), 'utf-8');
|
|
87
|
+
|
|
88
|
+
return { saved: filePath, resultCount: unique.length };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function handleLoadSnapshot(args: { name: string }) {
|
|
92
|
+
const safeName = sanitizeName(args.name);
|
|
93
|
+
const filePath = ensureWithinDir(SNAPSHOT_DIR, `${safeName}.json`);
|
|
94
|
+
if (!existsSync(filePath)) {
|
|
95
|
+
return { error: `Snapshot not found: ${args.name}` };
|
|
96
|
+
}
|
|
97
|
+
return JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
98
|
+
}
|