stellavault 0.3.0 → 0.4.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 (46) hide show
  1. package/Autonomous_Knowledge_Foundry.pdf +0 -0
  2. package/CHANGELOG.md +60 -0
  3. package/README.md +140 -95
  4. package/package.json +1 -1
  5. package/packages/cli/dist/commands/ask-cmd.d.ts +4 -0
  6. package/packages/cli/dist/commands/ask-cmd.js +35 -0
  7. package/packages/cli/dist/commands/autopilot-cmd.d.ts +4 -0
  8. package/packages/cli/dist/commands/autopilot-cmd.js +76 -0
  9. package/packages/cli/dist/commands/compile-cmd.d.ts +6 -0
  10. package/packages/cli/dist/commands/compile-cmd.js +30 -0
  11. package/packages/cli/dist/commands/digest-cmd.d.ts +1 -0
  12. package/packages/cli/dist/commands/digest-cmd.js +57 -0
  13. package/packages/cli/dist/commands/fleeting-cmd.d.ts +4 -0
  14. package/packages/cli/dist/commands/fleeting-cmd.js +45 -0
  15. package/packages/cli/dist/commands/graph-cmd.js +13 -1
  16. package/packages/cli/dist/commands/ingest-cmd.d.ts +9 -0
  17. package/packages/cli/dist/commands/ingest-cmd.js +131 -0
  18. package/packages/cli/dist/commands/init-cmd.js +39 -1
  19. package/packages/cli/dist/commands/lint-cmd.d.ts +2 -0
  20. package/packages/cli/dist/commands/lint-cmd.js +61 -0
  21. package/packages/cli/dist/index.js +46 -1
  22. package/packages/cli/package.json +1 -1
  23. package/packages/core/dist/api/server.js +284 -0
  24. package/packages/core/dist/i18n/note-strings.d.ts +5 -0
  25. package/packages/core/dist/i18n/note-strings.js +94 -0
  26. package/packages/core/dist/index.d.ts +9 -0
  27. package/packages/core/dist/index.js +5 -0
  28. package/packages/core/dist/intelligence/ask-engine.d.ts +23 -0
  29. package/packages/core/dist/intelligence/ask-engine.js +108 -0
  30. package/packages/core/dist/intelligence/ingest-pipeline.d.ts +31 -0
  31. package/packages/core/dist/intelligence/ingest-pipeline.js +192 -0
  32. package/packages/core/dist/intelligence/knowledge-lint.d.ts +27 -0
  33. package/packages/core/dist/intelligence/knowledge-lint.js +132 -0
  34. package/packages/core/dist/intelligence/wiki-compiler.d.ts +30 -0
  35. package/packages/core/dist/intelligence/wiki-compiler.js +222 -0
  36. package/packages/core/dist/intelligence/youtube-extractor.d.ts +29 -0
  37. package/packages/core/dist/intelligence/youtube-extractor.js +311 -0
  38. package/packages/core/dist/intelligence/zettelkasten.d.ts +59 -0
  39. package/packages/core/dist/intelligence/zettelkasten.js +234 -0
  40. package/packages/core/dist/mcp/server.d.ts +2 -0
  41. package/packages/core/dist/mcp/server.js +18 -1
  42. package/packages/core/dist/mcp/tools/agentic-graph.d.ts +6 -0
  43. package/packages/core/dist/mcp/tools/agentic-graph.js +35 -7
  44. package/packages/core/dist/mcp/tools/ask.d.ts +29 -0
  45. package/packages/core/dist/mcp/tools/ask.js +40 -0
  46. package/packages/core/package.json +5 -1
@@ -0,0 +1,192 @@
1
+ // 통합 인제스트 파이프라인
2
+ // 어떤 입력이든 → Stellavault 포맷으로 자동 변환 + 분류 + 연결
3
+ //
4
+ // 지원 입력: URL, PDF 텍스트, 마크다운, 플레인텍스트, YouTube
5
+ // 출력: frontmatter 포맷 .md → raw/ → compile → lint
6
+ import { writeFileSync, mkdirSync, existsSync, readFileSync } from 'node:fs';
7
+ import { join, resolve, basename } from 'node:path';
8
+ import { scanFrontmatter, assignIndexCodes, archiveFile } from './zettelkasten.js';
9
+ /** YAML 값에서 위험한 문자를 이스케이프 */
10
+ function sanitizeYaml(val) {
11
+ return val.replace(/["\\]/g, '\\$&').replace(/\n/g, ' ').slice(0, 200);
12
+ }
13
+ /**
14
+ * 어떤 입력이든 Stellavault 표준 포맷으로 변환하여 저장.
15
+ */
16
+ export function ingest(vaultPath, input) {
17
+ const stage = input.stage ?? 'fleeting';
18
+ const title = input.title ?? extractTitleFromContent(input.content, input.type);
19
+ const tags = input.tags ?? extractAutoTags(input.content, input.type);
20
+ const source = input.source ?? (input.type === 'url' || input.type === 'youtube' ? input.content.split('\n')[0] : 'manual');
21
+ // 본문 정리
22
+ const body = cleanContent(input.content, input.type);
23
+ const wordCount = body.split(/\s+/).length;
24
+ // 자동 분류: 길이/구조에 따라 stage 결정
25
+ const autoStage = classifyStage(body, stage, wordCount);
26
+ // 폴더 결정
27
+ const folderMap = {
28
+ fleeting: 'raw',
29
+ literature: '_literature',
30
+ permanent: '_permanent',
31
+ };
32
+ const folder = folderMap[autoStage];
33
+ const dir = resolve(vaultPath, folder);
34
+ if (!existsSync(dir))
35
+ mkdirSync(dir, { recursive: true });
36
+ // 파일명 생성
37
+ const now = new Date();
38
+ const timestamp = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
39
+ const slug = title.slice(0, 50).replace(/[^a-zA-Z0-9가-힣\s]/g, '').replace(/\s+/g, '-').toLowerCase();
40
+ const filename = `${timestamp}-${slug}.md`;
41
+ const filePath = join(folder, filename);
42
+ const fullPath = resolve(vaultPath, filePath);
43
+ // path traversal 방지
44
+ if (!fullPath.startsWith(resolve(vaultPath))) {
45
+ throw new Error('Invalid path');
46
+ }
47
+ // 인덱스 코드 생성 (lazy — 전체 스캔 건너뛸 수 있음)
48
+ let indexCode;
49
+ try {
50
+ // 성능: raw/ 폴더만 스캔 (전체 vault 스캔 대신)
51
+ const rawEntries = scanFrontmatter(resolve(vaultPath, folder));
52
+ const assignments = assignIndexCodes([...rawEntries, {
53
+ filePath, title, tags, connections: [], wordCount,
54
+ }]);
55
+ indexCode = assignments.get(filePath);
56
+ }
57
+ catch { /* index code is optional */ }
58
+ // Stellavault 표준 포맷으로 저장
59
+ const md = buildStandardNote({
60
+ title,
61
+ body,
62
+ tags,
63
+ stage: autoStage,
64
+ source,
65
+ indexCode,
66
+ created: now.toISOString(),
67
+ inputType: input.type,
68
+ });
69
+ writeFileSync(fullPath, md, 'utf-8');
70
+ return {
71
+ savedTo: filePath,
72
+ stage: autoStage,
73
+ title,
74
+ indexCode,
75
+ tags,
76
+ wordCount,
77
+ };
78
+ }
79
+ /**
80
+ * 여러 입력을 배치 처리.
81
+ */
82
+ export function ingestBatch(vaultPath, inputs) {
83
+ return inputs.map(input => ingest(vaultPath, input));
84
+ }
85
+ /**
86
+ * 노트 승격: fleeting → literature → permanent.
87
+ * 내용이 충분히 정제되면 다음 단계로 이동.
88
+ */
89
+ export function promoteNote(vaultPath, filePath, targetStage) {
90
+ const fullPath = resolve(vaultPath, filePath);
91
+ if (!existsSync(fullPath))
92
+ throw new Error(`File not found: ${filePath}`);
93
+ const content = readFileSync(fullPath, 'utf-8');
94
+ // frontmatter의 type 변경
95
+ const updated = content.replace(/^type:\s*.+$/m, `type: ${targetStage}`);
96
+ // 대상 폴더로 이동
97
+ const folderMap = {
98
+ fleeting: 'raw',
99
+ literature: '_literature',
100
+ permanent: '_permanent',
101
+ };
102
+ const newDir = resolve(vaultPath, folderMap[targetStage]);
103
+ if (!existsSync(newDir))
104
+ mkdirSync(newDir, { recursive: true });
105
+ const newPath = join(folderMap[targetStage], basename(filePath));
106
+ const newFullPath = resolve(vaultPath, newPath);
107
+ if (!newFullPath.startsWith(resolve(vaultPath))) {
108
+ throw new Error('Invalid path');
109
+ }
110
+ writeFileSync(newFullPath, updated, 'utf-8');
111
+ // 원본에 archive 플래그
112
+ archiveFile(fullPath);
113
+ return newPath;
114
+ }
115
+ // ─── 내부 헬퍼 ───
116
+ function extractTitleFromContent(content, type) {
117
+ if (type === 'url' || type === 'youtube') {
118
+ // URL에서 도메인 + 경로 추출
119
+ try {
120
+ const url = new URL(content.split('\n')[0]);
121
+ return url.hostname + url.pathname.slice(0, 40);
122
+ }
123
+ catch {
124
+ return 'Untitled Clip';
125
+ }
126
+ }
127
+ // 첫 heading 또는 첫 줄
128
+ const heading = content.match(/^#\s+(.+)$/m);
129
+ if (heading)
130
+ return heading[1];
131
+ const firstLine = content.split('\n')[0].trim();
132
+ return firstLine.slice(0, 80) || 'Untitled';
133
+ }
134
+ function extractAutoTags(content, type) {
135
+ const tags = new Set();
136
+ // 입력 타입 태그
137
+ if (type === 'url')
138
+ tags.add('web-clip');
139
+ if (type === 'youtube')
140
+ tags.add('youtube');
141
+ if (type === 'pdf-text')
142
+ tags.add('pdf');
143
+ // 인라인 #태그 추출
144
+ const inline = content.match(/#([a-zA-Z가-힣][a-zA-Z0-9가-힣_-]{2,})/g) ?? [];
145
+ inline.forEach(t => tags.add(t.slice(1)));
146
+ return [...tags].slice(0, 10);
147
+ }
148
+ function cleanContent(content, type) {
149
+ if (type === 'url' || type === 'youtube') {
150
+ // URL은 첫 줄이 URL, 나머지가 내용
151
+ const lines = content.split('\n');
152
+ return lines.slice(1).join('\n').trim() || lines[0];
153
+ }
154
+ return content.trim();
155
+ }
156
+ function classifyStage(body, requestedStage, wordCount) {
157
+ // 요청된 단계가 permanent면 그대로
158
+ if (requestedStage === 'permanent')
159
+ return 'permanent';
160
+ // 자동 분류 기준:
161
+ // - 200단어 미만 + 구조 없음 → fleeting
162
+ // - 200-1000단어 + 출처 있음 → literature
163
+ // - 1000단어+ + 구조 있음 → permanent 후보 (but 수동 승격 권장)
164
+ if (wordCount < 200 && !body.includes('## '))
165
+ return 'fleeting';
166
+ if (wordCount >= 200 && (body.includes('## ') || body.includes('> ')))
167
+ return 'literature';
168
+ return requestedStage;
169
+ }
170
+ function buildStandardNote(params) {
171
+ const lines = [
172
+ '---',
173
+ `title: "${sanitizeYaml(params.title)}"`,
174
+ `type: ${params.stage}`,
175
+ `source: ${params.source}`,
176
+ `input_type: ${params.inputType}`,
177
+ params.indexCode ? `zettel_id: "${params.indexCode}"` : null,
178
+ `tags: [${params.tags.map(t => `"${t}"`).join(', ')}]`,
179
+ `created: ${params.created}`,
180
+ `summary: "${sanitizeYaml(params.body.slice(0, 100))}"`,
181
+ '---',
182
+ '',
183
+ `# ${params.title}`,
184
+ '',
185
+ params.body,
186
+ '',
187
+ '---',
188
+ `*Ingested via \`stellavault ingest\` (${params.inputType}) at ${params.created}*`,
189
+ ];
190
+ return lines.filter(l => l !== null).join('\n');
191
+ }
192
+ //# sourceMappingURL=ingest-pipeline.js.map
@@ -0,0 +1,27 @@
1
+ import type { VectorStore } from '../store/types.js';
2
+ export interface LintResult {
3
+ score: number;
4
+ issues: LintIssue[];
5
+ suggestions: string[];
6
+ stats: {
7
+ totalDocs: number;
8
+ gaps: number;
9
+ duplicates: number;
10
+ contradictions: number;
11
+ isolatedNodes: number;
12
+ orphanTags: number;
13
+ };
14
+ }
15
+ export interface LintIssue {
16
+ type: 'gap' | 'duplicate' | 'contradiction' | 'isolated' | 'stale' | 'empty';
17
+ severity: 'critical' | 'warning' | 'info';
18
+ message: string;
19
+ filePath?: string;
20
+ suggestion?: string;
21
+ }
22
+ /**
23
+ * vault 전체를 스캔하여 지식 건강도를 평가.
24
+ * 기존 gap-detector, duplicate-detector, contradiction-detector를 통합.
25
+ */
26
+ export declare function lintKnowledge(store: VectorStore): Promise<LintResult>;
27
+ //# sourceMappingURL=knowledge-lint.d.ts.map
@@ -0,0 +1,132 @@
1
+ // Feature: stellavault lint — 자동 지식 건강 검사
2
+ // 불일치, 누락, 연결 제안, 고립 노드 등을 종합 리포트
3
+ import { detectKnowledgeGaps } from './gap-detector.js';
4
+ import { detectDuplicates } from './duplicate-detector.js';
5
+ import { detectContradictions } from './contradiction-detector.js';
6
+ /**
7
+ * vault 전체를 스캔하여 지식 건강도를 평가.
8
+ * 기존 gap-detector, duplicate-detector, contradiction-detector를 통합.
9
+ */
10
+ export async function lintKnowledge(store) {
11
+ const issues = [];
12
+ const suggestions = [];
13
+ const docs = await store.getAllDocuments();
14
+ const totalDocs = docs.length;
15
+ // 1. 갭 탐지
16
+ let gapCount = 0;
17
+ let isolatedCount = 0;
18
+ try {
19
+ const gapReport = await detectKnowledgeGaps(store);
20
+ gapCount = gapReport.totalGaps;
21
+ isolatedCount = gapReport.isolatedNodes.length;
22
+ for (const gap of gapReport.gaps.filter(g => g.severity !== 'low')) {
23
+ issues.push({
24
+ type: 'gap',
25
+ severity: gap.severity === 'high' ? 'critical' : 'warning',
26
+ message: `Knowledge gap: ${gap.clusterA} ↔ ${gap.clusterB} (${gap.bridgeCount} connections)`,
27
+ suggestion: gap.suggestedTopic,
28
+ });
29
+ }
30
+ for (const node of gapReport.isolatedNodes.slice(0, 10)) {
31
+ issues.push({
32
+ type: 'isolated',
33
+ severity: 'warning',
34
+ message: `Isolated note: "${node.title}" (${node.connections} connections)`,
35
+ filePath: node.id,
36
+ suggestion: 'Add links to related topics or tags to integrate this note.',
37
+ });
38
+ }
39
+ }
40
+ catch (err) {
41
+ console.error('[lint] Gap detection failed:', err instanceof Error ? err.message : err);
42
+ }
43
+ // 2. 중복 탐지
44
+ let dupCount = 0;
45
+ try {
46
+ const dups = await detectDuplicates(store, 0.88, 10);
47
+ dupCount = dups.length;
48
+ for (const dup of dups) {
49
+ issues.push({
50
+ type: 'duplicate',
51
+ severity: 'warning',
52
+ message: `Possible duplicate: "${dup.docA.title}" ≈ "${dup.docB.title}" (${Math.round(dup.similarity * 100)}%)`,
53
+ suggestion: 'Consider merging these notes.',
54
+ });
55
+ }
56
+ }
57
+ catch (err) {
58
+ console.error('[lint] Detection failed:', err instanceof Error ? err.message : err);
59
+ }
60
+ // 3. 모순 탐지
61
+ let contradictionCount = 0;
62
+ try {
63
+ const contradictions = await detectContradictions(store, 5);
64
+ contradictionCount = contradictions.length;
65
+ for (const c of contradictions) {
66
+ issues.push({
67
+ type: 'contradiction',
68
+ severity: 'warning',
69
+ message: `Potential contradiction: "${c.docA.title}" vs "${c.docB.title}"`,
70
+ suggestion: 'Review these documents for conflicting information.',
71
+ });
72
+ }
73
+ }
74
+ catch (err) {
75
+ console.error('[lint] Detection failed:', err instanceof Error ? err.message : err);
76
+ }
77
+ // 4. 빈 문서 / 매우 짧은 문서
78
+ for (const doc of docs) {
79
+ if (doc.content.trim().length < 50) {
80
+ issues.push({
81
+ type: 'empty',
82
+ severity: 'info',
83
+ message: `Very short note: "${doc.title}" (${doc.content.trim().length} chars)`,
84
+ filePath: doc.filePath,
85
+ suggestion: 'Consider expanding or removing this note.',
86
+ });
87
+ }
88
+ }
89
+ // 5. 오래된 문서 (180일 이상 미수정)
90
+ const now = Date.now();
91
+ const staleDocs = docs.filter(d => {
92
+ if (!d.lastModified)
93
+ return false;
94
+ const age = (now - new Date(d.lastModified).getTime()) / 86400000;
95
+ return age > 180;
96
+ });
97
+ if (staleDocs.length > totalDocs * 0.3) {
98
+ suggestions.push(`${staleDocs.length} documents haven't been modified in 6+ months. Run \`stellavault decay\` to review.`);
99
+ }
100
+ // 6. 건강도 점수 계산
101
+ const criticalCount = issues.filter(i => i.severity === 'critical').length;
102
+ const warningCount = issues.filter(i => i.severity === 'warning').length;
103
+ const penalty = criticalCount * 10 + warningCount * 3;
104
+ const score = Math.max(0, Math.min(100, 100 - penalty));
105
+ // 7. 종합 제안
106
+ if (gapCount > 5) {
107
+ suggestions.push(`${gapCount} knowledge gaps found. Start filling the most critical ones.`);
108
+ }
109
+ if (dupCount > 3) {
110
+ suggestions.push(`${dupCount} duplicate documents detected. Run \`stellavault duplicates\` to merge.`);
111
+ }
112
+ if (isolatedCount > 10) {
113
+ suggestions.push(`${isolatedCount} isolated notes found. Add tags or links to connect them.`);
114
+ }
115
+ if (totalDocs > 0 && issues.length === 0) {
116
+ suggestions.push('Your knowledge base is healthy! Keep it up.');
117
+ }
118
+ return {
119
+ score,
120
+ issues,
121
+ suggestions,
122
+ stats: {
123
+ totalDocs,
124
+ gaps: gapCount,
125
+ duplicates: dupCount,
126
+ contradictions: contradictionCount,
127
+ isolatedNodes: isolatedCount,
128
+ orphanTags: 0,
129
+ },
130
+ };
131
+ }
132
+ //# sourceMappingURL=knowledge-lint.js.map
@@ -0,0 +1,30 @@
1
+ export interface CompileResult {
2
+ rawDocCount: number;
3
+ wikiArticles: string[];
4
+ indexFile: string;
5
+ concepts: string[];
6
+ }
7
+ export interface RawDocument {
8
+ filePath: string;
9
+ title: string;
10
+ content: string;
11
+ tags: string[];
12
+ }
13
+ /**
14
+ * raw/ 폴더를 스캔하여 문서 목록 반환
15
+ */
16
+ export declare function scanRawDirectory(rawPath: string): RawDocument[];
17
+ /**
18
+ * raw 문서들에서 핵심 개념(concepts) 추출
19
+ */
20
+ export declare function extractConcepts(docs: RawDocument[]): Map<string, string[]>;
21
+ /**
22
+ * 컴파일: raw/ → wiki/
23
+ * - 개념별 문서 생성
24
+ * - 인덱스 파일 생성
25
+ * - 백링크 추가
26
+ */
27
+ export declare function compileWiki(rawPath: string, wikiPath: string, options?: {
28
+ force?: boolean;
29
+ }): CompileResult;
30
+ //# sourceMappingURL=wiki-compiler.d.ts.map
@@ -0,0 +1,222 @@
1
+ // Feature: stellavault compile — raw → wiki 컴파일
2
+ // raw/ 폴더의 문서를 분석하여 구조화된 wiki .md 파일 생성
3
+ import { readdirSync, readFileSync, writeFileSync, mkdirSync, existsSync, statSync } from 'node:fs';
4
+ import { join, resolve, basename, extname } from 'node:path';
5
+ /**
6
+ * raw/ 폴더를 스캔하여 문서 목록 반환
7
+ */
8
+ export function scanRawDirectory(rawPath) {
9
+ if (!existsSync(rawPath))
10
+ return [];
11
+ const docs = [];
12
+ const files = readdirSync(rawPath, { recursive: true });
13
+ for (const file of files) {
14
+ const fullPath = join(rawPath, file);
15
+ if (!statSync(fullPath).isFile())
16
+ continue;
17
+ const ext = extname(file).toLowerCase();
18
+ if (!['.md', '.txt', '.html'].includes(ext))
19
+ continue;
20
+ const content = readFileSync(fullPath, 'utf-8');
21
+ const title = extractTitle(content, file);
22
+ const tags = extractTags(content);
23
+ docs.push({ filePath: file, title, content, tags });
24
+ }
25
+ return docs;
26
+ }
27
+ /**
28
+ * 문서에서 제목 추출
29
+ */
30
+ function extractTitle(content, filename) {
31
+ // YAML frontmatter title
32
+ const fmMatch = content.match(/^---[\s\S]*?title:\s*["']?(.+?)["']?\s*$/m);
33
+ if (fmMatch)
34
+ return fmMatch[1];
35
+ // First heading
36
+ const headingMatch = content.match(/^#\s+(.+)$/m);
37
+ if (headingMatch)
38
+ return headingMatch[1];
39
+ // Filename
40
+ return basename(filename, extname(filename));
41
+ }
42
+ /**
43
+ * 문서에서 태그 추출
44
+ */
45
+ function extractTags(content) {
46
+ const tags = new Set();
47
+ // YAML tags
48
+ const fmMatch = content.match(/^---[\s\S]*?tags:\s*\[([^\]]*)\]/m);
49
+ if (fmMatch) {
50
+ fmMatch[1].split(',').map(t => t.trim().replace(/["']/g, '')).filter(Boolean).forEach(t => tags.add(t));
51
+ }
52
+ // Inline #tags
53
+ const inlineTags = content.match(/#([a-zA-Z가-힣][a-zA-Z0-9가-힣_-]*)/g) ?? [];
54
+ inlineTags.forEach(t => tags.add(t.slice(1)));
55
+ return [...tags];
56
+ }
57
+ /**
58
+ * raw 문서들에서 핵심 개념(concepts) 추출
59
+ */
60
+ export function extractConcepts(docs) {
61
+ const conceptMap = new Map(); // concept → [docPaths]
62
+ for (const doc of docs) {
63
+ // 태그를 개념으로
64
+ for (const tag of doc.tags) {
65
+ const existing = conceptMap.get(tag) ?? [];
66
+ existing.push(doc.filePath);
67
+ conceptMap.set(tag, existing);
68
+ }
69
+ // 제목 키워드를 개념으로
70
+ const titleWords = doc.title
71
+ .split(/[\s\-_,]+/)
72
+ .filter(w => w.length > 3)
73
+ .map(w => w.toLowerCase());
74
+ for (const word of titleWords) {
75
+ const existing = conceptMap.get(word) ?? [];
76
+ existing.push(doc.filePath);
77
+ conceptMap.set(word, existing);
78
+ }
79
+ }
80
+ // 2개 이상 문서에서 등장하는 개념만
81
+ const filtered = new Map();
82
+ for (const [concept, docPaths] of conceptMap) {
83
+ if (docPaths.length >= 2) {
84
+ filtered.set(concept, [...new Set(docPaths)]);
85
+ }
86
+ }
87
+ return filtered;
88
+ }
89
+ /**
90
+ * 컴파일: raw/ → wiki/
91
+ * - 개념별 문서 생성
92
+ * - 인덱스 파일 생성
93
+ * - 백링크 추가
94
+ */
95
+ export function compileWiki(rawPath, wikiPath, options = {}) {
96
+ const docs = scanRawDirectory(rawPath);
97
+ if (docs.length === 0) {
98
+ return { rawDocCount: 0, wikiArticles: [], indexFile: '', concepts: [] };
99
+ }
100
+ // wiki 디렉토리 생성
101
+ if (!existsSync(wikiPath)) {
102
+ mkdirSync(wikiPath, { recursive: true });
103
+ }
104
+ const concepts = extractConcepts(docs);
105
+ const wikiArticles = [];
106
+ // 1. 각 raw 문서의 요약 문서 생성
107
+ for (const doc of docs) {
108
+ const slug = doc.title.replace(/[^a-zA-Z0-9가-힣\s]/g, '').replace(/\s+/g, '-').toLowerCase();
109
+ const wikiFile = `${slug}.md`;
110
+ const wikiFullPath = resolve(wikiPath, wikiFile);
111
+ // path traversal 방지
112
+ if (!wikiFullPath.startsWith(resolve(wikiPath)))
113
+ continue;
114
+ const relatedDocs = findRelatedDocs(doc, docs);
115
+ const articleContent = generateArticle(doc, relatedDocs, concepts);
116
+ writeFileSync(wikiFullPath, articleContent, 'utf-8');
117
+ wikiArticles.push(wikiFile);
118
+ }
119
+ // 2. 개념별 허브 문서 생성
120
+ for (const [concept, docPaths] of concepts) {
121
+ const slug = concept.replace(/[^a-zA-Z0-9가-힣]/g, '-').toLowerCase();
122
+ const hubFile = `_concept-${slug}.md`;
123
+ const hubFullPath = resolve(wikiPath, hubFile);
124
+ if (!hubFullPath.startsWith(resolve(wikiPath)))
125
+ continue;
126
+ const relatedArticles = docPaths.map(p => {
127
+ const doc = docs.find(d => d.filePath === p);
128
+ return doc?.title ?? p;
129
+ });
130
+ const hubContent = [
131
+ '---',
132
+ `title: "Concept: ${concept}"`,
133
+ 'type: concept',
134
+ `date: ${new Date().toISOString()}`,
135
+ '---',
136
+ '',
137
+ `# ${concept}`,
138
+ '',
139
+ `This concept appears in ${relatedArticles.length} documents.`,
140
+ '',
141
+ '## Related Documents',
142
+ ...relatedArticles.map(a => `- [[${a}]]`),
143
+ '',
144
+ '---',
145
+ '*Auto-generated by `stellavault compile`*',
146
+ ].join('\n');
147
+ writeFileSync(hubFullPath, hubContent, 'utf-8');
148
+ wikiArticles.push(hubFile);
149
+ }
150
+ // 3. 인덱스 파일 생성
151
+ const indexContent = generateIndex(docs, concepts, wikiArticles);
152
+ const indexPath = resolve(wikiPath, '_index.md');
153
+ writeFileSync(indexPath, indexContent, 'utf-8');
154
+ return {
155
+ rawDocCount: docs.length,
156
+ wikiArticles,
157
+ indexFile: '_index.md',
158
+ concepts: [...concepts.keys()],
159
+ };
160
+ }
161
+ function findRelatedDocs(doc, allDocs) {
162
+ return allDocs
163
+ .filter(d => d.filePath !== doc.filePath)
164
+ .filter(d => {
165
+ const sharedTags = doc.tags.filter(t => d.tags.includes(t));
166
+ return sharedTags.length > 0;
167
+ })
168
+ .slice(0, 5);
169
+ }
170
+ function generateArticle(doc, related, concepts) {
171
+ const docConcepts = [...concepts.keys()].filter(c => doc.content.toLowerCase().includes(c.toLowerCase()) || doc.tags.includes(c));
172
+ return [
173
+ '---',
174
+ `title: "${doc.title}"`,
175
+ `source: "${doc.filePath}"`,
176
+ `date: ${new Date().toISOString()}`,
177
+ `tags: [${doc.tags.map(t => `"${t}"`).join(', ')}]`,
178
+ 'type: wiki-article',
179
+ '---',
180
+ '',
181
+ `# ${doc.title}`,
182
+ '',
183
+ '## Summary',
184
+ doc.content.substring(0, 500).replace(/^#.+$/gm, '').trim() + '...',
185
+ '',
186
+ docConcepts.length > 0 ? `## Related Concepts\n${docConcepts.map(c => `- [[_concept-${c}|${c}]]`).join('\n')}` : '',
187
+ '',
188
+ related.length > 0 ? `## 관련 문서\n${related.map(r => `- [[${r.title}]]`).join('\n')}` : '',
189
+ '',
190
+ `## Source`,
191
+ `- Source: \`${doc.filePath}\``,
192
+ '',
193
+ '---',
194
+ '*Compiled by `stellavault compile`*',
195
+ ].filter(Boolean).join('\n');
196
+ }
197
+ function generateIndex(docs, concepts, articles) {
198
+ return [
199
+ '---',
200
+ 'title: "Wiki Index"',
201
+ `date: ${new Date().toISOString()}`,
202
+ 'type: wiki-index',
203
+ '---',
204
+ '',
205
+ '# Wiki Index',
206
+ '',
207
+ `Compiled ${articles.length} wiki articles from ${docs.length} source documents.`,
208
+ '',
209
+ '## Concepts',
210
+ ...[...concepts.entries()]
211
+ .sort((a, b) => b[1].length - a[1].length)
212
+ .map(([c, paths]) => `- [[_concept-${c}|${c}]] (${paths.length})`),
213
+ '',
214
+ '## Articles',
215
+ ...docs.map(d => `- [[${d.title}]] — ${d.tags.map(t => `#${t}`).join(' ')}`),
216
+ '',
217
+ '---',
218
+ `*Last compiled: ${new Date().toISOString()}*`,
219
+ '*Generated by `stellavault compile`*',
220
+ ].join('\n');
221
+ }
222
+ //# sourceMappingURL=wiki-compiler.js.map
@@ -0,0 +1,29 @@
1
+ export interface YouTubeContent {
2
+ title: string;
3
+ channelName: string;
4
+ description: string;
5
+ transcript: TimedSegment[];
6
+ rawTranscript: string;
7
+ duration: string;
8
+ publishDate: string;
9
+ tags: string[];
10
+ url: string;
11
+ videoId: string;
12
+ viewCount: string;
13
+ thumbnail: string;
14
+ summary: string;
15
+ }
16
+ export interface TimedSegment {
17
+ startTime: number;
18
+ text: string;
19
+ }
20
+ /**
21
+ * YouTube URL에서 모든 콘텐츠 추출.
22
+ * 반환값은 데이터만 — 노트 포맷팅은 별도.
23
+ */
24
+ export declare function extractYouTubeContent(url: string): Promise<YouTubeContent>;
25
+ /**
26
+ * 추출된 콘텐츠 → Stellavault .md 노트 (frontmatter 포함하지 않음 — pipeline이 처리).
27
+ */
28
+ export declare function formatYouTubeNote(content: YouTubeContent): string;
29
+ //# sourceMappingURL=youtube-extractor.d.ts.map