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,118 @@
|
|
|
1
|
+
// Cross-Vault Federation (P3-F24)
|
|
2
|
+
// 한 사람의 여러 vault를 통합 검색
|
|
3
|
+
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import type { VectorStore } from '../store/types.js';
|
|
8
|
+
import type { Embedder } from '../indexer/embedder.js';
|
|
9
|
+
import type { ScoredChunk } from '../types/chunk.js';
|
|
10
|
+
|
|
11
|
+
export interface VaultEntry {
|
|
12
|
+
id: string; // short alias
|
|
13
|
+
name: string; // display name
|
|
14
|
+
path: string; // vault path
|
|
15
|
+
dbPath: string; // index.db path
|
|
16
|
+
shared: boolean; // 공유 허용 여부 (Federation에서)
|
|
17
|
+
addedAt: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface CrossVaultSearchResult {
|
|
21
|
+
vaultId: string;
|
|
22
|
+
vaultName: string;
|
|
23
|
+
title: string;
|
|
24
|
+
score: number;
|
|
25
|
+
snippet: string;
|
|
26
|
+
filePath: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const VAULTS_FILE = join(homedir(), '.stellavault', 'vaults.json');
|
|
30
|
+
|
|
31
|
+
function loadVaults(): VaultEntry[] {
|
|
32
|
+
if (!existsSync(VAULTS_FILE)) return [];
|
|
33
|
+
return JSON.parse(readFileSync(VAULTS_FILE, 'utf-8'));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function saveVaults(vaults: VaultEntry[]): void {
|
|
37
|
+
mkdirSync(join(homedir(), '.stellavault'), { recursive: true });
|
|
38
|
+
writeFileSync(VAULTS_FILE, JSON.stringify(vaults, null, 2), 'utf-8');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function addVault(id: string, name: string, vaultPath: string, dbPath: string, shared = false): VaultEntry {
|
|
42
|
+
const vaults = loadVaults();
|
|
43
|
+
if (vaults.some(v => v.id === id)) {
|
|
44
|
+
throw new Error(`Vault "${id}" already exists`);
|
|
45
|
+
}
|
|
46
|
+
const entry: VaultEntry = { id, name, path: vaultPath, dbPath, shared, addedAt: new Date().toISOString() };
|
|
47
|
+
vaults.push(entry);
|
|
48
|
+
saveVaults(vaults);
|
|
49
|
+
return entry;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function removeVault(id: string): boolean {
|
|
53
|
+
const vaults = loadVaults();
|
|
54
|
+
const filtered = vaults.filter(v => v.id !== id);
|
|
55
|
+
if (filtered.length === vaults.length) return false;
|
|
56
|
+
saveVaults(filtered);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function listVaults(): VaultEntry[] {
|
|
61
|
+
return loadVaults();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getVault(id: string): VaultEntry | undefined {
|
|
65
|
+
return loadVaults().find(v => v.id === id);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 모든 vault를 통합 검색
|
|
69
|
+
export async function searchAllVaults(
|
|
70
|
+
query: string,
|
|
71
|
+
embedder: Embedder,
|
|
72
|
+
createStore: (dbPath: string) => VectorStore,
|
|
73
|
+
options: { limit?: number } = {},
|
|
74
|
+
): Promise<CrossVaultSearchResult[]> {
|
|
75
|
+
const { limit = 10 } = options;
|
|
76
|
+
const vaults = loadVaults();
|
|
77
|
+
const results: CrossVaultSearchResult[] = [];
|
|
78
|
+
|
|
79
|
+
// 쿼리 임베딩 (한 번만)
|
|
80
|
+
const embedding = await embedder.embed(query);
|
|
81
|
+
|
|
82
|
+
// 각 vault에서 병렬 검색
|
|
83
|
+
const searches = vaults.map(async (vault) => {
|
|
84
|
+
try {
|
|
85
|
+
if (!existsSync(vault.dbPath)) return;
|
|
86
|
+
|
|
87
|
+
const store = createStore(vault.dbPath);
|
|
88
|
+
await store.initialize();
|
|
89
|
+
|
|
90
|
+
const scored = await store.searchSemantic(embedding, limit);
|
|
91
|
+
|
|
92
|
+
for (const s of scored) {
|
|
93
|
+
const chunk = await store.getChunk(s.chunkId);
|
|
94
|
+
if (!chunk) continue;
|
|
95
|
+
const doc = await store.getDocument(chunk.documentId);
|
|
96
|
+
|
|
97
|
+
results.push({
|
|
98
|
+
vaultId: vault.id,
|
|
99
|
+
vaultName: vault.name,
|
|
100
|
+
title: doc?.title ?? 'Untitled',
|
|
101
|
+
score: s.score,
|
|
102
|
+
snippet: chunk.content.slice(0, 80),
|
|
103
|
+
filePath: doc?.filePath ?? '',
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
await store.close();
|
|
108
|
+
} catch {
|
|
109
|
+
// vault 접근 실패 시 건너뜀
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await Promise.allSettled(searches);
|
|
114
|
+
|
|
115
|
+
return results
|
|
116
|
+
.sort((a, b) => b.score - a.score)
|
|
117
|
+
.slice(0, limit);
|
|
118
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Design Ref: Phase 3 FR-01~03 — 검색/클러스터 기반 팩 생성
|
|
2
|
+
|
|
3
|
+
import type { VectorStore } from '../store/types.js';
|
|
4
|
+
import type { SearchEngine } from '../search/index.js';
|
|
5
|
+
import type { Embedder } from '../indexer/embedder.js';
|
|
6
|
+
import type { KnowledgePack, PackChunk } from './types.js';
|
|
7
|
+
import { maskPII } from './pii-masker.js';
|
|
8
|
+
|
|
9
|
+
export interface CreatePackOptions {
|
|
10
|
+
name: string;
|
|
11
|
+
author?: string;
|
|
12
|
+
license?: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
fromSearch?: string;
|
|
15
|
+
fromCluster?: number;
|
|
16
|
+
limit?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function createPack(
|
|
20
|
+
store: VectorStore,
|
|
21
|
+
searchEngine: SearchEngine,
|
|
22
|
+
embedder: Embedder,
|
|
23
|
+
options: CreatePackOptions,
|
|
24
|
+
): Promise<{ pack: KnowledgePack; piiReport: { redactedCount: number; types: string[] } }> {
|
|
25
|
+
const { name, author = 'anonymous', license = 'CC-BY-4.0', description = '', limit = 100 } = options;
|
|
26
|
+
|
|
27
|
+
let chunkIds: string[] = [];
|
|
28
|
+
|
|
29
|
+
if (options.fromSearch) {
|
|
30
|
+
// 검색 결과에서 청크 수집
|
|
31
|
+
const results = await searchEngine.search({ query: options.fromSearch, limit });
|
|
32
|
+
chunkIds = results.map(r => r.chunk.id);
|
|
33
|
+
} else if (options.fromCluster !== undefined) {
|
|
34
|
+
// 클러스터의 문서에서 청크 수집
|
|
35
|
+
const docs = await store.getAllDocuments();
|
|
36
|
+
const embeddings = await store.getDocumentEmbeddings();
|
|
37
|
+
|
|
38
|
+
// 간단한 클러스터 매칭: graph-data의 K-means 결과를 재활용할 수 없으므로
|
|
39
|
+
// 해당 클러스터 문서의 모든 청크를 가져옴
|
|
40
|
+
// 여기서는 fromSearch를 권장하되, fromCluster는 문서 ID 기반으로 처리
|
|
41
|
+
const clusterDocs = docs.slice(0, limit);
|
|
42
|
+
for (const doc of clusterDocs) {
|
|
43
|
+
const chunk = await store.getChunk(`${doc.id}#0`);
|
|
44
|
+
if (chunk) chunkIds.push(chunk.id);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 청크 데이터 수집 + PII 마스킹
|
|
49
|
+
const chunks: PackChunk[] = [];
|
|
50
|
+
let totalRedacted = 0;
|
|
51
|
+
const allTypes = new Set<string>();
|
|
52
|
+
|
|
53
|
+
for (const chunkId of chunkIds) {
|
|
54
|
+
const chunk = await store.getChunk(chunkId);
|
|
55
|
+
if (!chunk) continue;
|
|
56
|
+
|
|
57
|
+
const doc = await store.getDocument(chunk.documentId);
|
|
58
|
+
if (!doc) continue;
|
|
59
|
+
|
|
60
|
+
// PII 마스킹
|
|
61
|
+
const { masked, redactedCount, redactedTypes } = maskPII(chunk.content);
|
|
62
|
+
totalRedacted += redactedCount;
|
|
63
|
+
redactedTypes.forEach(t => allTypes.add(t));
|
|
64
|
+
|
|
65
|
+
// 임베딩 재생성 (마스킹된 텍스트로)
|
|
66
|
+
const embedding = await embedder.embed(masked);
|
|
67
|
+
|
|
68
|
+
chunks.push({
|
|
69
|
+
id: chunkId,
|
|
70
|
+
content: masked,
|
|
71
|
+
heading: chunk.heading,
|
|
72
|
+
embedding,
|
|
73
|
+
metadata: {
|
|
74
|
+
sourceFile: doc.filePath,
|
|
75
|
+
category: detectCategory(doc.filePath, chunk.content),
|
|
76
|
+
language: detectLanguage(chunk.content),
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const pack: KnowledgePack = {
|
|
82
|
+
name,
|
|
83
|
+
version: '1.0.0',
|
|
84
|
+
author,
|
|
85
|
+
license,
|
|
86
|
+
description: description || `Knowledge pack: ${name}`,
|
|
87
|
+
tags: extractPackTags(chunks),
|
|
88
|
+
embeddingModel: embedder.modelName,
|
|
89
|
+
embeddingDimensions: embedder.dimensions,
|
|
90
|
+
schemaVersion: '1.0',
|
|
91
|
+
chunks,
|
|
92
|
+
createdAt: new Date().toISOString(),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
pack,
|
|
97
|
+
piiReport: { redactedCount: totalRedacted, types: [...allTypes] },
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function detectCategory(filePath: string, content: string): string {
|
|
102
|
+
const lower = filePath.toLowerCase() + ' ' + content.slice(0, 200).toLowerCase();
|
|
103
|
+
if (lower.includes('lesson') || lower.includes('교훈')) return 'lesson';
|
|
104
|
+
if (lower.includes('pattern') || lower.includes('패턴')) return 'pattern';
|
|
105
|
+
if (lower.includes('decision') || lower.includes('결정')) return 'decision';
|
|
106
|
+
if (lower.includes('design') || lower.includes('설계')) return 'reference';
|
|
107
|
+
return 'reference';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function detectLanguage(content: string): string | undefined {
|
|
111
|
+
if (content.includes('typescript') || content.includes('.ts')) return 'typescript';
|
|
112
|
+
if (content.includes('python') || content.includes('.py')) return 'python';
|
|
113
|
+
if (content.includes('react') || content.includes('jsx')) return 'react';
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function extractPackTags(chunks: PackChunk[]): string[] {
|
|
118
|
+
const wordCounts = new Map<string, number>();
|
|
119
|
+
for (const c of chunks) {
|
|
120
|
+
const words = c.heading.split(/\s+/).filter(w => w.length > 2);
|
|
121
|
+
for (const w of words) wordCounts.set(w, (wordCounts.get(w) ?? 0) + 1);
|
|
122
|
+
}
|
|
123
|
+
return [...wordCounts.entries()]
|
|
124
|
+
.sort((a, b) => b[1] - a[1])
|
|
125
|
+
.slice(0, 10)
|
|
126
|
+
.map(([w]) => w);
|
|
127
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Design Ref: Phase 3 FR-05 — .sv-pack 파일 내보내기
|
|
2
|
+
|
|
3
|
+
import { writeFileSync, mkdirSync } from 'node:fs';
|
|
4
|
+
import { dirname } from 'node:path';
|
|
5
|
+
import type { KnowledgePack } from './types.js';
|
|
6
|
+
|
|
7
|
+
export function exportPack(pack: KnowledgePack, outputPath: string): void {
|
|
8
|
+
mkdirSync(dirname(outputPath), { recursive: true });
|
|
9
|
+
writeFileSync(outputPath, JSON.stringify(pack, null, 2), 'utf-8');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function packToSummary(pack: KnowledgePack): string {
|
|
13
|
+
return [
|
|
14
|
+
`Name: ${pack.name} v${pack.version}`,
|
|
15
|
+
`Author: ${pack.author} (${pack.license})`,
|
|
16
|
+
`Chunks: ${pack.chunks.length}`,
|
|
17
|
+
`Model: ${pack.embeddingModel} (${pack.embeddingDimensions}d)`,
|
|
18
|
+
`Tags: ${pack.tags.join(', ')}`,
|
|
19
|
+
`Created: ${pack.createdAt}`,
|
|
20
|
+
].join('\n');
|
|
21
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Design Ref: Phase 3 FR-06~07 — .sv-pack 가져오기 + 벡터 DB 병합
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import type { VectorStore } from '../store/types.js';
|
|
5
|
+
import type { Embedder } from '../indexer/embedder.js';
|
|
6
|
+
import type { KnowledgePack } from './types.js';
|
|
7
|
+
|
|
8
|
+
export interface ImportResult {
|
|
9
|
+
imported: number;
|
|
10
|
+
skipped: number;
|
|
11
|
+
reEmbedded: number;
|
|
12
|
+
modelMismatch: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function importPack(
|
|
16
|
+
store: VectorStore,
|
|
17
|
+
embedder: Embedder,
|
|
18
|
+
filePath: string,
|
|
19
|
+
): Promise<ImportResult> {
|
|
20
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
21
|
+
const pack: KnowledgePack = JSON.parse(raw);
|
|
22
|
+
|
|
23
|
+
// 임베딩 모델 불일치 감지
|
|
24
|
+
const modelMismatch = pack.embeddingModel !== embedder.modelName ||
|
|
25
|
+
pack.embeddingDimensions !== embedder.dimensions;
|
|
26
|
+
|
|
27
|
+
let imported = 0;
|
|
28
|
+
let skipped = 0;
|
|
29
|
+
let reEmbedded = 0;
|
|
30
|
+
|
|
31
|
+
// 팩 문서를 pack_{name} prefix로 저장
|
|
32
|
+
const packDocId = `pack_${pack.name}`;
|
|
33
|
+
|
|
34
|
+
await store.upsertDocument({
|
|
35
|
+
id: packDocId,
|
|
36
|
+
filePath: `[pack] ${pack.name}`,
|
|
37
|
+
title: `${pack.name} (Knowledge Pack)`,
|
|
38
|
+
content: `Imported pack: ${pack.description}\nChunks: ${pack.chunks.length}\nAuthor: ${pack.author}`,
|
|
39
|
+
frontmatter: { pack: pack.name, license: pack.license },
|
|
40
|
+
tags: pack.tags,
|
|
41
|
+
lastModified: pack.createdAt,
|
|
42
|
+
contentHash: `pack_${pack.name}_${pack.version}`,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const chunks = [];
|
|
46
|
+
for (let i = 0; i < pack.chunks.length; i++) {
|
|
47
|
+
const pc = pack.chunks[i];
|
|
48
|
+
|
|
49
|
+
let embedding = pc.embedding;
|
|
50
|
+
|
|
51
|
+
// 모델 불일치 시 재임베딩
|
|
52
|
+
if (modelMismatch) {
|
|
53
|
+
embedding = await embedder.embed(pc.content);
|
|
54
|
+
reEmbedded++;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 차원 검증
|
|
58
|
+
if (embedding.length !== embedder.dimensions) {
|
|
59
|
+
skipped++;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
chunks.push({
|
|
64
|
+
id: `${packDocId}#${i}`,
|
|
65
|
+
documentId: packDocId,
|
|
66
|
+
content: pc.content,
|
|
67
|
+
heading: pc.heading || pack.name,
|
|
68
|
+
startLine: 0,
|
|
69
|
+
endLine: 0,
|
|
70
|
+
tokenCount: Math.ceil(pc.content.length / 4),
|
|
71
|
+
embedding,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
imported++;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (chunks.length > 0) {
|
|
78
|
+
await store.upsertChunks(chunks);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { imported, skipped, reEmbedded, modelMismatch };
|
|
82
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type { KnowledgePack, PackChunk, PackInfo } from './types.js';
|
|
2
|
+
export { createPack, type CreatePackOptions } from './creator.js';
|
|
3
|
+
export { exportPack, packToSummary } from './exporter.js';
|
|
4
|
+
export { importPack, type ImportResult } from './importer.js';
|
|
5
|
+
export { maskPII, type MaskResult } from './pii-masker.js';
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Pack Marketplace (F-A07) — npm registry + GitHub Releases 기반
|
|
2
|
+
// 서버 불필요: npm/GitHub를 마켓플레이스로 활용
|
|
3
|
+
|
|
4
|
+
export interface PackListing {
|
|
5
|
+
name: string;
|
|
6
|
+
version: string;
|
|
7
|
+
description: string;
|
|
8
|
+
author: string;
|
|
9
|
+
downloads?: number;
|
|
10
|
+
tags: string[];
|
|
11
|
+
source: 'npm' | 'github';
|
|
12
|
+
installCommand: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// npm에서 @stellavault/pack-* 패키지 검색
|
|
16
|
+
export async function searchMarketplace(query: string, limit = 10): Promise<PackListing[]> {
|
|
17
|
+
const results: PackListing[] = [];
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
// npm registry search
|
|
21
|
+
const safeLimit = Math.min(limit, 20); // MED: 최대 20개 제한
|
|
22
|
+
const npmUrl = `https://registry.npmjs.org/-/v1/search?text=stellavault-pack+${encodeURIComponent(query)}&size=${safeLimit}`;
|
|
23
|
+
const res = await fetch(npmUrl, { signal: AbortSignal.timeout(5000) });
|
|
24
|
+
if (res.ok) {
|
|
25
|
+
const data = await res.json() as any;
|
|
26
|
+
for (const pkg of data.objects ?? []) {
|
|
27
|
+
results.push({
|
|
28
|
+
name: pkg.package.name,
|
|
29
|
+
version: pkg.package.version,
|
|
30
|
+
description: pkg.package.description ?? '',
|
|
31
|
+
author: pkg.package.author?.name ?? pkg.package.publisher?.username ?? 'unknown',
|
|
32
|
+
downloads: pkg.downloads?.weekly ?? 0,
|
|
33
|
+
tags: pkg.package.keywords ?? [],
|
|
34
|
+
source: 'npm',
|
|
35
|
+
installCommand: `npm install ${pkg.package.name}`,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
} catch { /* npm search failed — continue */ }
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
// GitHub search for stellavault-pack repos
|
|
43
|
+
const ghUrl = `https://api.github.com/search/repositories?q=stellavault-pack+${encodeURIComponent(query)}&per_page=${limit}`;
|
|
44
|
+
const res = await fetch(ghUrl, {
|
|
45
|
+
headers: { 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'stellavault-marketplace' },
|
|
46
|
+
signal: AbortSignal.timeout(5000),
|
|
47
|
+
});
|
|
48
|
+
if (res.ok) {
|
|
49
|
+
const data = await res.json() as any;
|
|
50
|
+
for (const repo of data.items ?? []) {
|
|
51
|
+
// npm과 중복 방지
|
|
52
|
+
if (results.some(r => r.name === repo.name)) continue;
|
|
53
|
+
results.push({
|
|
54
|
+
name: repo.full_name,
|
|
55
|
+
version: 'latest',
|
|
56
|
+
description: repo.description ?? '',
|
|
57
|
+
author: repo.owner?.login ?? 'unknown',
|
|
58
|
+
downloads: repo.stargazers_count,
|
|
59
|
+
tags: repo.topics ?? [],
|
|
60
|
+
source: 'github',
|
|
61
|
+
installCommand: `sv pack import <(curl -sL ${repo.html_url}/releases/latest/download/pack.sv-pack)`,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
} catch { /* GitHub search failed — continue */ }
|
|
66
|
+
|
|
67
|
+
return results;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// sv-pack 파일을 npm에 publish할 수 있도록 패키지 생성
|
|
71
|
+
export function createPackageJson(packName: string, description: string, author: string, version = '1.0.0'): string {
|
|
72
|
+
return JSON.stringify({
|
|
73
|
+
name: `stellavault-pack-${packName}`,
|
|
74
|
+
version,
|
|
75
|
+
description,
|
|
76
|
+
author,
|
|
77
|
+
license: 'CC-BY-4.0',
|
|
78
|
+
keywords: ['stellavault', 'knowledge-pack', packName],
|
|
79
|
+
files: ['*.sv-pack', 'README.md'],
|
|
80
|
+
stellavault: { type: 'knowledge-pack' },
|
|
81
|
+
}, null, 2);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// GitHub Release로 pack 배포용 명령어 생성
|
|
85
|
+
export function getPublishInstructions(packName: string): string {
|
|
86
|
+
return `
|
|
87
|
+
To publish your Knowledge Pack:
|
|
88
|
+
|
|
89
|
+
Option A: npm
|
|
90
|
+
1. cd your-pack-directory/
|
|
91
|
+
2. npm init (or use: sv pack prepare ${packName})
|
|
92
|
+
3. npm publish
|
|
93
|
+
|
|
94
|
+
Option B: GitHub Release
|
|
95
|
+
1. Create a GitHub repo: stellavault-pack-${packName}
|
|
96
|
+
2. Add your .sv-pack file
|
|
97
|
+
3. Create a release with the .sv-pack as an asset
|
|
98
|
+
|
|
99
|
+
Users install with:
|
|
100
|
+
npm: sv pack install stellavault-pack-${packName}
|
|
101
|
+
GitHub: sv pack import <url-to-sv-pack-file>
|
|
102
|
+
`.trim();
|
|
103
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Design Ref: Phase 3 FR-04 — PII 감지 + 마스킹
|
|
2
|
+
|
|
3
|
+
const PII_PATTERNS: Array<{ name: string; regex: RegExp }> = [
|
|
4
|
+
{ name: 'email', regex: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g },
|
|
5
|
+
{ name: 'api_key', regex: /(?:sk|pk|api|key|token|secret|password)[_\-a-zA-Z]*[_\-][a-zA-Z0-9]{16,}/gi },
|
|
6
|
+
{ name: 'aws_key', regex: /AKIA[0-9A-Z]{16}/g },
|
|
7
|
+
{ name: 'url_with_auth', regex: /https?:\/\/[^:]+:[^@]+@[^\s]+/g },
|
|
8
|
+
{ name: 'ip_address', regex: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g },
|
|
9
|
+
{ name: 'phone', regex: /\b0\d{1,2}[-.\s]?\d{3,4}[-.\s]?\d{4}\b/g },
|
|
10
|
+
{ name: 'jwt', regex: /eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}/g },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export interface MaskResult {
|
|
14
|
+
masked: string;
|
|
15
|
+
redactedCount: number;
|
|
16
|
+
redactedTypes: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function maskPII(text: string): MaskResult {
|
|
20
|
+
let masked = text;
|
|
21
|
+
let redactedCount = 0;
|
|
22
|
+
const redactedTypes = new Set<string>();
|
|
23
|
+
|
|
24
|
+
for (const { name, regex } of PII_PATTERNS) {
|
|
25
|
+
const matches = masked.match(regex);
|
|
26
|
+
if (matches) {
|
|
27
|
+
redactedCount += matches.length;
|
|
28
|
+
redactedTypes.add(name);
|
|
29
|
+
masked = masked.replace(regex, `[REDACTED:${name}]`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
masked,
|
|
35
|
+
redactedCount,
|
|
36
|
+
redactedTypes: [...redactedTypes],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Design Ref: Phase 3 §1 — .sv-pack.json 표준 포맷
|
|
2
|
+
|
|
3
|
+
export interface KnowledgePack {
|
|
4
|
+
name: string;
|
|
5
|
+
version: string;
|
|
6
|
+
author: string;
|
|
7
|
+
license: string;
|
|
8
|
+
description: string;
|
|
9
|
+
tags: string[];
|
|
10
|
+
embeddingModel: string;
|
|
11
|
+
embeddingDimensions: number;
|
|
12
|
+
schemaVersion: string;
|
|
13
|
+
chunks: PackChunk[];
|
|
14
|
+
createdAt: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface PackChunk {
|
|
18
|
+
id: string;
|
|
19
|
+
content: string;
|
|
20
|
+
heading: string;
|
|
21
|
+
embedding: number[];
|
|
22
|
+
metadata: {
|
|
23
|
+
sourceFile: string;
|
|
24
|
+
category: string;
|
|
25
|
+
language?: string;
|
|
26
|
+
framework?: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface PackInfo {
|
|
31
|
+
name: string;
|
|
32
|
+
version: string;
|
|
33
|
+
author: string;
|
|
34
|
+
license: string;
|
|
35
|
+
description: string;
|
|
36
|
+
chunkCount: number;
|
|
37
|
+
embeddingModel: string;
|
|
38
|
+
createdAt: string;
|
|
39
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Plugin SDK (F-A15) — Event-driven plugin system
|
|
2
|
+
|
|
3
|
+
export interface PluginContext {
|
|
4
|
+
store: {
|
|
5
|
+
search(query: string, limit?: number): Promise<any[]>;
|
|
6
|
+
getDocument(id: string): Promise<any>;
|
|
7
|
+
getStats(): Promise<any>;
|
|
8
|
+
};
|
|
9
|
+
config: Record<string, unknown>;
|
|
10
|
+
log: (message: string) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type PluginEvent =
|
|
14
|
+
| 'onIndex' // After a document is indexed
|
|
15
|
+
| 'onSearch' // After a search is performed
|
|
16
|
+
| 'onDecay' // When decay check runs
|
|
17
|
+
| 'onGapDetected' // When a knowledge gap is found
|
|
18
|
+
| 'onStartup' // When stellavault starts
|
|
19
|
+
| 'onShutdown'; // When stellavault stops
|
|
20
|
+
|
|
21
|
+
export interface PluginManifest {
|
|
22
|
+
name: string;
|
|
23
|
+
version: string;
|
|
24
|
+
description: string;
|
|
25
|
+
author?: string;
|
|
26
|
+
events: PluginEvent[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface StellavaultPlugin {
|
|
30
|
+
manifest: PluginManifest;
|
|
31
|
+
activate(context: PluginContext): Promise<void>;
|
|
32
|
+
deactivate?(): Promise<void>;
|
|
33
|
+
onEvent?(event: PluginEvent, data: unknown): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type EventHandler = (data: unknown) => Promise<void>;
|
|
37
|
+
|
|
38
|
+
export class PluginManager {
|
|
39
|
+
private plugins = new Map<string, StellavaultPlugin>();
|
|
40
|
+
private handlers = new Map<PluginEvent, Array<{ pluginName: string; handler: EventHandler }>>();
|
|
41
|
+
private context: PluginContext;
|
|
42
|
+
|
|
43
|
+
constructor(context: PluginContext) {
|
|
44
|
+
this.context = context;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async register(plugin: StellavaultPlugin): Promise<void> {
|
|
48
|
+
if (this.plugins.has(plugin.manifest.name)) {
|
|
49
|
+
throw new Error(`Plugin "${plugin.manifest.name}" is already registered`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.plugins.set(plugin.manifest.name, plugin);
|
|
53
|
+
|
|
54
|
+
// Register event handlers (MED fix: track by plugin name for proper unregister)
|
|
55
|
+
for (const event of plugin.manifest.events) {
|
|
56
|
+
if (!this.handlers.has(event)) this.handlers.set(event, []);
|
|
57
|
+
if (plugin.onEvent) {
|
|
58
|
+
this.handlers.get(event)!.push({
|
|
59
|
+
pluginName: plugin.manifest.name,
|
|
60
|
+
handler: (data) => plugin.onEvent!(event, data),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
await plugin.activate(this.context);
|
|
66
|
+
this.context.log(`Plugin "${plugin.manifest.name}" v${plugin.manifest.version} activated`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async unregister(name: string): Promise<void> {
|
|
70
|
+
const plugin = this.plugins.get(name);
|
|
71
|
+
if (!plugin) return;
|
|
72
|
+
|
|
73
|
+
await plugin.deactivate?.();
|
|
74
|
+
this.plugins.delete(name);
|
|
75
|
+
|
|
76
|
+
// MED fix: properly remove handlers by plugin name
|
|
77
|
+
for (const [event, entries] of this.handlers) {
|
|
78
|
+
this.handlers.set(event, entries.filter(e => e.pluginName !== name));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async emit(event: PluginEvent, data: unknown): Promise<void> {
|
|
83
|
+
const entries = this.handlers.get(event) ?? [];
|
|
84
|
+
for (const { handler } of entries) {
|
|
85
|
+
try {
|
|
86
|
+
await handler(data);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
this.context.log(`Plugin error on ${event}: ${err instanceof Error ? err.message : String(err)}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
listPlugins(): PluginManifest[] {
|
|
94
|
+
return [...this.plugins.values()].map(p => p.manifest);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
getPlugin(name: string): StellavaultPlugin | undefined {
|
|
98
|
+
return this.plugins.get(name);
|
|
99
|
+
}
|
|
100
|
+
}
|