stellavault 0.4.0 → 0.4.2
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/CHANGELOG.md +46 -0
- package/README.md +96 -151
- package/index.html +483 -0
- package/package.json +2 -2
- package/packages/cli/dist/commands/draft-cmd.d.ts +5 -0
- package/packages/cli/dist/commands/draft-cmd.js +102 -0
- package/packages/cli/dist/commands/flush-cmd.d.ts +2 -0
- package/packages/cli/dist/commands/flush-cmd.js +58 -0
- package/packages/cli/dist/commands/ingest-cmd.js +42 -12
- package/packages/cli/dist/commands/session-cmd.d.ts +7 -0
- package/packages/cli/dist/commands/session-cmd.js +95 -0
- package/packages/cli/dist/index.js +21 -0
- package/packages/core/dist/api/server.js +109 -0
- package/packages/core/dist/config.d.ts +8 -0
- package/packages/core/dist/config.js +9 -1
- package/packages/core/dist/index.d.ts +2 -2
- package/packages/core/dist/index.js +1 -1
- package/packages/core/dist/intelligence/auto-linker.d.ts +19 -0
- package/packages/core/dist/intelligence/auto-linker.js +122 -0
- package/packages/core/dist/intelligence/draft-generator.d.ts +19 -0
- package/packages/core/dist/intelligence/draft-generator.js +161 -0
- package/packages/core/dist/intelligence/file-extractors.d.ts +18 -0
- package/packages/core/dist/intelligence/file-extractors.js +127 -0
- package/packages/core/dist/intelligence/ingest-pipeline.d.ts +4 -3
- package/packages/core/dist/intelligence/ingest-pipeline.js +35 -12
- package/packages/core/dist/mcp/server.js +6 -0
- package/packages/core/dist/mcp/tools/generate-draft.d.ts +34 -0
- package/packages/core/dist/mcp/tools/generate-draft.js +120 -0
- package/packages/core/package.json +24 -1
- package/Autonomous_Knowledge_Foundry.pdf +0 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// Express 단계: stellavault draft — wiki 기반 초안 생성
|
|
2
|
+
// Plan SC: SC3 (Express)
|
|
3
|
+
// 카파시의 "자가 컴파일" 결과물을 외부로 표현하는 출구
|
|
4
|
+
import { writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
5
|
+
import { join, resolve, basename, extname } from 'node:path';
|
|
6
|
+
import { scanRawDirectory, extractConcepts } from './wiki-compiler.js';
|
|
7
|
+
import { DEFAULT_FOLDERS } from '../config.js';
|
|
8
|
+
/**
|
|
9
|
+
* vault의 wiki 데이터를 기반으로 초안(draft)을 생성.
|
|
10
|
+
* Express 단계: 지식이 vault에서 나가는 출구.
|
|
11
|
+
*/
|
|
12
|
+
export function generateDraft(vaultPath, options = {}, folders = DEFAULT_FOLDERS) {
|
|
13
|
+
const { topic, format = 'blog', maxSections = 8 } = options;
|
|
14
|
+
// raw + wiki 문서 스캔
|
|
15
|
+
const rawDir = resolve(vaultPath, folders.fleeting);
|
|
16
|
+
const wikiDir = resolve(vaultPath, folders.wiki);
|
|
17
|
+
const litDir = resolve(vaultPath, folders.literature);
|
|
18
|
+
const allDocs = [];
|
|
19
|
+
for (const dir of [rawDir, wikiDir, litDir]) {
|
|
20
|
+
if (existsSync(dir)) {
|
|
21
|
+
allDocs.push(...scanRawDirectory(dir));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (allDocs.length === 0) {
|
|
25
|
+
throw new Error('No documents found in vault. Run `stellavault ingest` first.');
|
|
26
|
+
}
|
|
27
|
+
// 토픽 필터
|
|
28
|
+
const filteredDocs = topic
|
|
29
|
+
? allDocs.filter(d => d.tags.some(t => t.toLowerCase().includes(topic.toLowerCase())) ||
|
|
30
|
+
d.title.toLowerCase().includes(topic.toLowerCase()) ||
|
|
31
|
+
d.content.toLowerCase().includes(topic.toLowerCase()))
|
|
32
|
+
: allDocs;
|
|
33
|
+
if (filteredDocs.length === 0) {
|
|
34
|
+
throw new Error(`No documents found for topic "${topic}". Try a broader term.`);
|
|
35
|
+
}
|
|
36
|
+
// 개념 추출
|
|
37
|
+
const concepts = extractConcepts(filteredDocs);
|
|
38
|
+
const topConcepts = [...concepts.entries()]
|
|
39
|
+
.sort((a, b) => b[1].length - a[1].length)
|
|
40
|
+
.slice(0, maxSections);
|
|
41
|
+
// 초안 생성
|
|
42
|
+
const draftTitle = topic
|
|
43
|
+
? `Draft: ${topic}`
|
|
44
|
+
: `Knowledge Draft — ${new Date().toISOString().split('T')[0]}`;
|
|
45
|
+
let body;
|
|
46
|
+
switch (format) {
|
|
47
|
+
case 'outline':
|
|
48
|
+
body = generateOutline(draftTitle, topConcepts, filteredDocs);
|
|
49
|
+
break;
|
|
50
|
+
case 'report':
|
|
51
|
+
body = generateReport(draftTitle, topConcepts, filteredDocs);
|
|
52
|
+
break;
|
|
53
|
+
case 'blog':
|
|
54
|
+
default:
|
|
55
|
+
body = generateBlog(draftTitle, topConcepts, filteredDocs);
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
const wordCount = body.split(/\s+/).filter(Boolean).length;
|
|
59
|
+
// _drafts/ 폴더에 저장
|
|
60
|
+
const draftsDir = resolve(vaultPath, '_drafts');
|
|
61
|
+
if (!existsSync(draftsDir))
|
|
62
|
+
mkdirSync(draftsDir, { recursive: true });
|
|
63
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
64
|
+
const slug = (topic ?? 'knowledge').replace(/[^a-zA-Z0-9가-힣\s]/g, '').replace(/\s+/g, '-').toLowerCase().slice(0, 40);
|
|
65
|
+
const filename = `${timestamp}-${slug}.md`;
|
|
66
|
+
const filePath = join('_drafts', filename);
|
|
67
|
+
const fullPath = resolve(vaultPath, filePath);
|
|
68
|
+
writeFileSync(fullPath, body, 'utf-8');
|
|
69
|
+
return {
|
|
70
|
+
title: draftTitle,
|
|
71
|
+
filePath,
|
|
72
|
+
wordCount,
|
|
73
|
+
sourceCount: filteredDocs.length,
|
|
74
|
+
concepts: topConcepts.map(([c]) => c),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
// ─── 포맷별 생성기 ───
|
|
78
|
+
function generateBlog(title, concepts, docs) {
|
|
79
|
+
const lines = [];
|
|
80
|
+
lines.push(`# ${title}`, '');
|
|
81
|
+
lines.push(`> Auto-generated from ${docs.length} knowledge notes`, '');
|
|
82
|
+
// 도입부: 전체 주제 요약
|
|
83
|
+
lines.push('## Introduction', '');
|
|
84
|
+
const topicWords = concepts.slice(0, 3).map(([c]) => c).join(', ');
|
|
85
|
+
lines.push(`This post explores ${topicWords} based on ${docs.length} curated knowledge notes.`, '');
|
|
86
|
+
// 섹션별: 개념 + 관련 문서 발췌
|
|
87
|
+
for (const [concept, docPaths] of concepts) {
|
|
88
|
+
lines.push(`## ${capitalize(concept)}`, '');
|
|
89
|
+
const relatedDocs = docs.filter(d => docPaths.includes(d.filePath) ||
|
|
90
|
+
d.tags.includes(concept) ||
|
|
91
|
+
d.title.toLowerCase().includes(concept.toLowerCase())).slice(0, 3);
|
|
92
|
+
for (const doc of relatedDocs) {
|
|
93
|
+
const excerpt = extractExcerpt(doc.content, 150);
|
|
94
|
+
if (excerpt) {
|
|
95
|
+
lines.push(`> ${excerpt}`, `> — *${doc.title}*`, '');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
lines.push('<!-- TODO: Add your analysis and insights here -->', '');
|
|
99
|
+
}
|
|
100
|
+
// 참고 자료
|
|
101
|
+
lines.push('## References', '');
|
|
102
|
+
const uniqueDocs = [...new Map(docs.map(d => [d.filePath, d])).values()].slice(0, 20);
|
|
103
|
+
for (const doc of uniqueDocs) {
|
|
104
|
+
lines.push(`- [[${basename(doc.filePath, extname(doc.filePath))}|${doc.title}]]`);
|
|
105
|
+
}
|
|
106
|
+
lines.push('');
|
|
107
|
+
lines.push(`---`, `*Generated by \`stellavault draft\` at ${new Date().toISOString()}*`);
|
|
108
|
+
return lines.join('\n');
|
|
109
|
+
}
|
|
110
|
+
function generateReport(title, concepts, docs) {
|
|
111
|
+
const lines = [];
|
|
112
|
+
lines.push(`# ${title}`, '');
|
|
113
|
+
lines.push(`**Date:** ${new Date().toISOString().split('T')[0]}`);
|
|
114
|
+
lines.push(`**Sources:** ${docs.length} documents`);
|
|
115
|
+
lines.push(`**Key Topics:** ${concepts.map(([c]) => c).join(', ')}`, '');
|
|
116
|
+
lines.push('## Executive Summary', '');
|
|
117
|
+
lines.push('<!-- TODO: Write 2-3 sentence summary -->', '');
|
|
118
|
+
for (const [concept, docPaths] of concepts) {
|
|
119
|
+
lines.push(`## ${capitalize(concept)}`, '');
|
|
120
|
+
lines.push(`**Related documents:** ${docPaths.length}`, '');
|
|
121
|
+
const relatedDocs = docs.filter(d => docPaths.includes(d.filePath)).slice(0, 3);
|
|
122
|
+
for (const doc of relatedDocs) {
|
|
123
|
+
const excerpt = extractExcerpt(doc.content, 200);
|
|
124
|
+
if (excerpt)
|
|
125
|
+
lines.push(`- ${excerpt} *(${doc.title})*`);
|
|
126
|
+
}
|
|
127
|
+
lines.push('');
|
|
128
|
+
lines.push('**Analysis:** <!-- TODO -->', '');
|
|
129
|
+
}
|
|
130
|
+
lines.push('## Conclusion', '', '<!-- TODO -->', '');
|
|
131
|
+
lines.push(`---`, `*Generated by \`stellavault draft\` at ${new Date().toISOString()}*`);
|
|
132
|
+
return lines.join('\n');
|
|
133
|
+
}
|
|
134
|
+
function generateOutline(title, concepts, docs) {
|
|
135
|
+
const lines = [];
|
|
136
|
+
lines.push(`# ${title} — Outline`, '');
|
|
137
|
+
lines.push(`Sources: ${docs.length} documents`, '');
|
|
138
|
+
for (let i = 0; i < concepts.length; i++) {
|
|
139
|
+
const [concept, docPaths] = concepts[i];
|
|
140
|
+
lines.push(`${i + 1}. **${capitalize(concept)}** (${docPaths.length} sources)`);
|
|
141
|
+
const relatedDocs = docs.filter(d => docPaths.includes(d.filePath)).slice(0, 3);
|
|
142
|
+
for (const doc of relatedDocs) {
|
|
143
|
+
lines.push(` - ${doc.title}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
lines.push('', `---`, `*Generated by \`stellavault draft\` at ${new Date().toISOString()}*`);
|
|
147
|
+
return lines.join('\n');
|
|
148
|
+
}
|
|
149
|
+
// ─── 유틸 ───
|
|
150
|
+
function extractExcerpt(content, maxLen) {
|
|
151
|
+
// frontmatter 제거
|
|
152
|
+
const body = content.replace(/^---[\s\S]*?---\n?/, '').replace(/^#+\s+.+\n/m, '').trim();
|
|
153
|
+
// 첫 의미 있는 문단
|
|
154
|
+
const paragraphs = body.split(/\n\n+/).filter(p => p.length > 20 && !p.startsWith('> ') && !p.startsWith('- '));
|
|
155
|
+
const first = paragraphs[0] ?? '';
|
|
156
|
+
return first.length > maxLen ? first.slice(0, maxLen) + '...' : first;
|
|
157
|
+
}
|
|
158
|
+
function capitalize(s) {
|
|
159
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=draft-generator.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface ExtractedContent {
|
|
2
|
+
text: string;
|
|
3
|
+
metadata: {
|
|
4
|
+
title?: string;
|
|
5
|
+
author?: string;
|
|
6
|
+
pageCount?: number;
|
|
7
|
+
wordCount: number;
|
|
8
|
+
};
|
|
9
|
+
sourceFormat: 'pdf' | 'docx' | 'pptx' | 'xlsx' | 'xls' | 'text';
|
|
10
|
+
}
|
|
11
|
+
export declare function isBinaryFormat(filePath: string): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* 파일 경로에서 텍스트 추출. 확장자 기반 파서 디스패치.
|
|
14
|
+
* 지원: .pdf, .docx, .pptx, .xlsx, .xls
|
|
15
|
+
* 미지원 확장자: utf-8 텍스트로 읽기
|
|
16
|
+
*/
|
|
17
|
+
export declare function extractFileContent(filePath: string): Promise<ExtractedContent>;
|
|
18
|
+
//# sourceMappingURL=file-extractors.d.ts.map
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Design Ref: §file-ingest-v2 — Binary file text extraction dispatchers
|
|
2
|
+
// Plan SC: SC1-SC5 (format-specific extraction + fallback)
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { extname, basename } from 'node:path';
|
|
5
|
+
const BINARY_EXTS = new Set(['.pdf', '.docx', '.pptx', '.xlsx', '.xls']);
|
|
6
|
+
export function isBinaryFormat(filePath) {
|
|
7
|
+
return BINARY_EXTS.has(extname(filePath).toLowerCase());
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* 파일 경로에서 텍스트 추출. 확장자 기반 파서 디스패치.
|
|
11
|
+
* 지원: .pdf, .docx, .pptx, .xlsx, .xls
|
|
12
|
+
* 미지원 확장자: utf-8 텍스트로 읽기
|
|
13
|
+
*/
|
|
14
|
+
export async function extractFileContent(filePath) {
|
|
15
|
+
const ext = extname(filePath).toLowerCase();
|
|
16
|
+
const buffer = readFileSync(filePath);
|
|
17
|
+
switch (ext) {
|
|
18
|
+
case '.pdf': return extractPdf(buffer, filePath);
|
|
19
|
+
case '.docx': return extractDocx(buffer, filePath);
|
|
20
|
+
case '.pptx': return extractPptx(buffer, filePath);
|
|
21
|
+
case '.xlsx': return extractXlsx(buffer, filePath);
|
|
22
|
+
case '.xls': return extractXlsx(buffer, filePath);
|
|
23
|
+
default: return extractText(filePath);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function extractPdf(buffer, filePath) {
|
|
27
|
+
try {
|
|
28
|
+
const { extractText } = await import('unpdf');
|
|
29
|
+
const result = await extractText(new Uint8Array(buffer));
|
|
30
|
+
const text = Array.isArray(result.text) ? result.text.join('\n\n') : (result.text ?? '');
|
|
31
|
+
return {
|
|
32
|
+
text,
|
|
33
|
+
metadata: {
|
|
34
|
+
title: basename(filePath, '.pdf'),
|
|
35
|
+
pageCount: result.totalPages,
|
|
36
|
+
wordCount: text.split(/\s+/).filter(Boolean).length,
|
|
37
|
+
},
|
|
38
|
+
sourceFormat: 'pdf',
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
console.error(`PDF extraction failed: ${err instanceof Error ? err.message : 'unknown'}`);
|
|
43
|
+
return fallback(filePath, 'pdf');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function extractDocx(buffer, filePath) {
|
|
47
|
+
try {
|
|
48
|
+
const mammoth = await import('mammoth');
|
|
49
|
+
const result = await mammoth.default.extractRawText({ buffer });
|
|
50
|
+
const text = result.value ?? '';
|
|
51
|
+
return {
|
|
52
|
+
text,
|
|
53
|
+
metadata: {
|
|
54
|
+
title: basename(filePath, '.docx'),
|
|
55
|
+
wordCount: text.split(/\s+/).filter(Boolean).length,
|
|
56
|
+
},
|
|
57
|
+
sourceFormat: 'docx',
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
console.error(`DOCX extraction failed: ${err instanceof Error ? err.message : 'unknown'}`);
|
|
62
|
+
return fallback(filePath, 'docx');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function extractPptx(buffer, filePath) {
|
|
66
|
+
try {
|
|
67
|
+
const officeparser = await import('officeparser');
|
|
68
|
+
const text = String(await officeparser.default.parseOffice(buffer));
|
|
69
|
+
return {
|
|
70
|
+
text: text ?? '',
|
|
71
|
+
metadata: {
|
|
72
|
+
title: basename(filePath, '.pptx'),
|
|
73
|
+
wordCount: (text ?? '').split(/\s+/).filter(Boolean).length,
|
|
74
|
+
},
|
|
75
|
+
sourceFormat: 'pptx',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
console.error(`PPTX extraction failed: ${err instanceof Error ? err.message : 'unknown'}`);
|
|
80
|
+
return fallback(filePath, 'pptx');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function extractXlsx(buffer, filePath) {
|
|
84
|
+
const ext = extname(filePath).toLowerCase();
|
|
85
|
+
const format = ext === '.xls' ? 'xls' : 'xlsx';
|
|
86
|
+
try {
|
|
87
|
+
const XLSX = await import('xlsx');
|
|
88
|
+
const workbook = XLSX.read(buffer);
|
|
89
|
+
const text = workbook.SheetNames
|
|
90
|
+
.map((name) => {
|
|
91
|
+
const csv = XLSX.utils.sheet_to_csv(workbook.Sheets[name]);
|
|
92
|
+
return `## ${name}\n\n${csv}`;
|
|
93
|
+
})
|
|
94
|
+
.join('\n\n');
|
|
95
|
+
return {
|
|
96
|
+
text,
|
|
97
|
+
metadata: {
|
|
98
|
+
title: basename(filePath, ext),
|
|
99
|
+
wordCount: text.split(/\s+/).filter(Boolean).length,
|
|
100
|
+
},
|
|
101
|
+
sourceFormat: format,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
console.error(`XLSX extraction failed: ${err instanceof Error ? err.message : 'unknown'}`);
|
|
106
|
+
return fallback(filePath, format);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function extractText(filePath) {
|
|
110
|
+
const text = readFileSync(filePath, 'utf-8');
|
|
111
|
+
return {
|
|
112
|
+
text,
|
|
113
|
+
metadata: {
|
|
114
|
+
title: basename(filePath),
|
|
115
|
+
wordCount: text.split(/\s+/).filter(Boolean).length,
|
|
116
|
+
},
|
|
117
|
+
sourceFormat: 'text',
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function fallback(filePath, format) {
|
|
121
|
+
return {
|
|
122
|
+
text: `[Failed to extract text from ${basename(filePath)}. Install required parser or convert to a text format.]`,
|
|
123
|
+
metadata: { title: basename(filePath), wordCount: 0 },
|
|
124
|
+
sourceFormat: format,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=file-extractors.js.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { type FolderNames } from '../config.js';
|
|
1
2
|
export type NoteStage = 'fleeting' | 'literature' | 'permanent';
|
|
2
3
|
export interface IngestInput {
|
|
3
|
-
type: 'url' | 'text' | 'file' | 'youtube' | 'pdf-text';
|
|
4
|
+
type: 'url' | 'text' | 'file' | 'youtube' | 'pdf-text' | 'pdf' | 'docx' | 'pptx' | 'xlsx' | 'xls';
|
|
4
5
|
content: string;
|
|
5
6
|
title?: string;
|
|
6
7
|
tags?: string[];
|
|
@@ -18,7 +19,7 @@ export interface IngestResult {
|
|
|
18
19
|
/**
|
|
19
20
|
* 어떤 입력이든 Stellavault 표준 포맷으로 변환하여 저장.
|
|
20
21
|
*/
|
|
21
|
-
export declare function ingest(vaultPath: string, input: IngestInput): IngestResult;
|
|
22
|
+
export declare function ingest(vaultPath: string, input: IngestInput, folders?: FolderNames): IngestResult;
|
|
22
23
|
/**
|
|
23
24
|
* 여러 입력을 배치 처리.
|
|
24
25
|
*/
|
|
@@ -27,5 +28,5 @@ export declare function ingestBatch(vaultPath: string, inputs: IngestInput[]): I
|
|
|
27
28
|
* 노트 승격: fleeting → literature → permanent.
|
|
28
29
|
* 내용이 충분히 정제되면 다음 단계로 이동.
|
|
29
30
|
*/
|
|
30
|
-
export declare function promoteNote(vaultPath: string, filePath: string, targetStage: NoteStage): string;
|
|
31
|
+
export declare function promoteNote(vaultPath: string, filePath: string, targetStage: NoteStage, folders?: FolderNames): string;
|
|
31
32
|
//# sourceMappingURL=ingest-pipeline.d.ts.map
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
import { writeFileSync, mkdirSync, existsSync, readFileSync } from 'node:fs';
|
|
7
7
|
import { join, resolve, basename } from 'node:path';
|
|
8
8
|
import { scanFrontmatter, assignIndexCodes, archiveFile } from './zettelkasten.js';
|
|
9
|
+
import { compileWiki } from './wiki-compiler.js';
|
|
10
|
+
import { autoLink } from './auto-linker.js';
|
|
11
|
+
import { DEFAULT_FOLDERS } from '../config.js';
|
|
9
12
|
/** YAML 값에서 위험한 문자를 이스케이프 */
|
|
10
13
|
function sanitizeYaml(val) {
|
|
11
14
|
return val.replace(/["\\]/g, '\\$&').replace(/\n/g, ' ').slice(0, 200);
|
|
@@ -13,7 +16,7 @@ function sanitizeYaml(val) {
|
|
|
13
16
|
/**
|
|
14
17
|
* 어떤 입력이든 Stellavault 표준 포맷으로 변환하여 저장.
|
|
15
18
|
*/
|
|
16
|
-
export function ingest(vaultPath, input) {
|
|
19
|
+
export function ingest(vaultPath, input, folders = DEFAULT_FOLDERS) {
|
|
17
20
|
const stage = input.stage ?? 'fleeting';
|
|
18
21
|
const title = input.title ?? extractTitleFromContent(input.content, input.type);
|
|
19
22
|
const tags = input.tags ?? extractAutoTags(input.content, input.type);
|
|
@@ -23,11 +26,11 @@ export function ingest(vaultPath, input) {
|
|
|
23
26
|
const wordCount = body.split(/\s+/).length;
|
|
24
27
|
// 자동 분류: 길이/구조에 따라 stage 결정
|
|
25
28
|
const autoStage = classifyStage(body, stage, wordCount);
|
|
26
|
-
// 폴더 결정
|
|
29
|
+
// 폴더 결정 (config-driven)
|
|
27
30
|
const folderMap = {
|
|
28
|
-
fleeting:
|
|
29
|
-
literature:
|
|
30
|
-
permanent:
|
|
31
|
+
fleeting: folders.fleeting,
|
|
32
|
+
literature: folders.literature,
|
|
33
|
+
permanent: folders.permanent,
|
|
31
34
|
};
|
|
32
35
|
const folder = folderMap[autoStage];
|
|
33
36
|
const dir = resolve(vaultPath, folder);
|
|
@@ -56,7 +59,7 @@ export function ingest(vaultPath, input) {
|
|
|
56
59
|
}
|
|
57
60
|
catch { /* index code is optional */ }
|
|
58
61
|
// Stellavault 표준 포맷으로 저장
|
|
59
|
-
|
|
62
|
+
let md = buildStandardNote({
|
|
60
63
|
title,
|
|
61
64
|
body,
|
|
62
65
|
tags,
|
|
@@ -66,7 +69,21 @@ export function ingest(vaultPath, input) {
|
|
|
66
69
|
created: now.toISOString(),
|
|
67
70
|
inputType: input.type,
|
|
68
71
|
});
|
|
72
|
+
// wikilink 자동 삽입: 기존 노트 제목과 매칭
|
|
73
|
+
try {
|
|
74
|
+
md = autoLink(md, vaultPath, title, folders);
|
|
75
|
+
}
|
|
76
|
+
catch { /* autoLink 실패해도 저장은 진행 */ }
|
|
69
77
|
writeFileSync(fullPath, md, 'utf-8');
|
|
78
|
+
// 자동 compile: fleeting → wiki (rule-based, <100ms)
|
|
79
|
+
try {
|
|
80
|
+
const rawDir = resolve(vaultPath, folders.fleeting);
|
|
81
|
+
const wikiDir = resolve(vaultPath, folders.wiki);
|
|
82
|
+
if (existsSync(rawDir)) {
|
|
83
|
+
compileWiki(rawDir, wikiDir);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch { /* compile 실패해도 ingest 성공 */ }
|
|
70
87
|
return {
|
|
71
88
|
savedTo: filePath,
|
|
72
89
|
stage: autoStage,
|
|
@@ -86,18 +103,18 @@ export function ingestBatch(vaultPath, inputs) {
|
|
|
86
103
|
* 노트 승격: fleeting → literature → permanent.
|
|
87
104
|
* 내용이 충분히 정제되면 다음 단계로 이동.
|
|
88
105
|
*/
|
|
89
|
-
export function promoteNote(vaultPath, filePath, targetStage) {
|
|
106
|
+
export function promoteNote(vaultPath, filePath, targetStage, folders = DEFAULT_FOLDERS) {
|
|
90
107
|
const fullPath = resolve(vaultPath, filePath);
|
|
91
108
|
if (!existsSync(fullPath))
|
|
92
109
|
throw new Error(`File not found: ${filePath}`);
|
|
93
110
|
const content = readFileSync(fullPath, 'utf-8');
|
|
94
111
|
// frontmatter의 type 변경
|
|
95
112
|
const updated = content.replace(/^type:\s*.+$/m, `type: ${targetStage}`);
|
|
96
|
-
// 대상 폴더로 이동
|
|
113
|
+
// 대상 폴더로 이동 (config-driven)
|
|
97
114
|
const folderMap = {
|
|
98
|
-
fleeting:
|
|
99
|
-
literature:
|
|
100
|
-
permanent:
|
|
115
|
+
fleeting: folders.fleeting,
|
|
116
|
+
literature: folders.literature,
|
|
117
|
+
permanent: folders.permanent,
|
|
101
118
|
};
|
|
102
119
|
const newDir = resolve(vaultPath, folderMap[targetStage]);
|
|
103
120
|
if (!existsSync(newDir))
|
|
@@ -138,8 +155,14 @@ function extractAutoTags(content, type) {
|
|
|
138
155
|
tags.add('web-clip');
|
|
139
156
|
if (type === 'youtube')
|
|
140
157
|
tags.add('youtube');
|
|
141
|
-
if (type === 'pdf-text')
|
|
158
|
+
if (type === 'pdf-text' || type === 'pdf')
|
|
142
159
|
tags.add('pdf');
|
|
160
|
+
if (type === 'docx')
|
|
161
|
+
tags.add('document');
|
|
162
|
+
if (type === 'pptx')
|
|
163
|
+
tags.add('presentation');
|
|
164
|
+
if (type === 'xlsx' || type === 'xls')
|
|
165
|
+
tags.add('spreadsheet');
|
|
143
166
|
// 인라인 #태그 추출
|
|
144
167
|
const inline = content.match(/#([a-zA-Z가-힣][a-zA-Z0-9가-힣_-]{2,})/g) ?? [];
|
|
145
168
|
inline.forEach(t => tags.add(t.slice(1)));
|
|
@@ -18,6 +18,7 @@ import { createDetectGapsTool } from './tools/detect-gaps.js';
|
|
|
18
18
|
import { createGetEvolutionTool } from './tools/get-evolution.js';
|
|
19
19
|
import { createLinkCodeTool } from './tools/link-code.js';
|
|
20
20
|
import { createAskTool } from './tools/ask.js';
|
|
21
|
+
import { createGenerateDraftTool } from './tools/generate-draft.js';
|
|
21
22
|
import { createAgenticGraphTools } from './tools/agentic-graph.js';
|
|
22
23
|
export function createMcpServer(options) {
|
|
23
24
|
const { store, searchEngine, embedder, vaultPath = '', decayEngine } = options;
|
|
@@ -26,6 +27,7 @@ export function createMcpServer(options) {
|
|
|
26
27
|
const getEvolutionTool = createGetEvolutionTool(store);
|
|
27
28
|
const linkCodeTool = createLinkCodeTool(searchEngine);
|
|
28
29
|
const askTool = createAskTool(searchEngine, vaultPath);
|
|
30
|
+
const generateDraftTool = createGenerateDraftTool(searchEngine, vaultPath);
|
|
29
31
|
const agenticTools = embedder ? createAgenticGraphTools(store, embedder, vaultPath) : [];
|
|
30
32
|
const server = new Server({ name: 'stellavault', version: '0.2.0' }, { capabilities: { tools: {} } });
|
|
31
33
|
// List tools
|
|
@@ -40,6 +42,7 @@ export function createMcpServer(options) {
|
|
|
40
42
|
{ name: getEvolutionTool.name, description: getEvolutionTool.description, inputSchema: getEvolutionTool.inputSchema },
|
|
41
43
|
{ name: linkCodeTool.name, description: linkCodeTool.description, inputSchema: linkCodeTool.inputSchema },
|
|
42
44
|
{ name: askTool.name, description: askTool.description, inputSchema: askTool.inputSchema },
|
|
45
|
+
{ name: generateDraftTool.name, description: generateDraftTool.description, inputSchema: generateDraftTool.inputSchema },
|
|
43
46
|
...agenticTools.map(t => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })),
|
|
44
47
|
],
|
|
45
48
|
}));
|
|
@@ -120,6 +123,9 @@ export function createMcpServer(options) {
|
|
|
120
123
|
case 'ask':
|
|
121
124
|
result = await askTool.handler(args);
|
|
122
125
|
return result;
|
|
126
|
+
case 'generate-draft':
|
|
127
|
+
result = await generateDraftTool.handler(args);
|
|
128
|
+
return result;
|
|
123
129
|
default:
|
|
124
130
|
{
|
|
125
131
|
// Agentic graph tools (create-knowledge-node, create-knowledge-link)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { SearchEngine } from '../../search/index.js';
|
|
2
|
+
export declare function createGenerateDraftTool(searchEngine: SearchEngine, vaultPath: string): {
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
inputSchema: {
|
|
6
|
+
type: "object";
|
|
7
|
+
properties: {
|
|
8
|
+
topic: {
|
|
9
|
+
type: string;
|
|
10
|
+
description: string;
|
|
11
|
+
};
|
|
12
|
+
format: {
|
|
13
|
+
type: string;
|
|
14
|
+
enum: string[];
|
|
15
|
+
description: string;
|
|
16
|
+
};
|
|
17
|
+
maxSources: {
|
|
18
|
+
type: string;
|
|
19
|
+
description: string;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
handler: (args: {
|
|
24
|
+
topic?: string;
|
|
25
|
+
format?: string;
|
|
26
|
+
maxSources?: number;
|
|
27
|
+
}) => Promise<{
|
|
28
|
+
content: {
|
|
29
|
+
type: "text";
|
|
30
|
+
text: string;
|
|
31
|
+
}[];
|
|
32
|
+
}>;
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=generate-draft.d.ts.map
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// MCP Tool: generate-draft — Claude Code에서 직접 초안 생성
|
|
2
|
+
// Claude가 vault 컨텍스트를 받아 직접 글을 쓰도록 구조화된 재료를 반환
|
|
3
|
+
// 비용: $0 (이미 Claude Code 세션 안)
|
|
4
|
+
import { scanRawDirectory, extractConcepts } from '../../intelligence/wiki-compiler.js';
|
|
5
|
+
import { loadConfig, DEFAULT_FOLDERS } from '../../config.js';
|
|
6
|
+
import { resolve } from 'node:path';
|
|
7
|
+
import { existsSync } from 'node:fs';
|
|
8
|
+
export function createGenerateDraftTool(searchEngine, vaultPath) {
|
|
9
|
+
return {
|
|
10
|
+
name: 'generate-draft',
|
|
11
|
+
description: 'Gather knowledge from your vault to write a draft. Returns structured context (notes, concepts, excerpts) so you can compose a blog post, report, or outline. Use this when the user asks you to "write a draft", "blog post from my notes", or "summarize my knowledge about X".',
|
|
12
|
+
inputSchema: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {
|
|
15
|
+
topic: {
|
|
16
|
+
type: 'string',
|
|
17
|
+
description: 'Topic or keyword to focus on. Leave empty for all knowledge.',
|
|
18
|
+
},
|
|
19
|
+
format: {
|
|
20
|
+
type: 'string',
|
|
21
|
+
enum: ['blog', 'report', 'outline'],
|
|
22
|
+
description: 'Desired output format (default: blog)',
|
|
23
|
+
},
|
|
24
|
+
maxSources: {
|
|
25
|
+
type: 'number',
|
|
26
|
+
description: 'Maximum number of source documents to include (default: 10)',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
handler: async (args) => {
|
|
31
|
+
const { topic, format = 'blog', maxSources = 10 } = args;
|
|
32
|
+
const config = loadConfig();
|
|
33
|
+
const folders = config.folders ?? DEFAULT_FOLDERS;
|
|
34
|
+
// 1. 토픽 기반 검색 (있으면)
|
|
35
|
+
let searchResults = [];
|
|
36
|
+
if (topic) {
|
|
37
|
+
const results = await searchEngine.search({ query: topic, limit: maxSources });
|
|
38
|
+
searchResults = results.map(r => ({
|
|
39
|
+
title: r.document?.title ?? 'Untitled',
|
|
40
|
+
content: r.chunk?.content ?? '',
|
|
41
|
+
filePath: r.document?.filePath ?? '',
|
|
42
|
+
score: r.score ?? 0,
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
// 2. vault 전체 스캔 (검색 결과가 부족하면)
|
|
46
|
+
const allDocs = [];
|
|
47
|
+
for (const dir of [folders.fleeting, folders.literature, folders.permanent, folders.wiki]) {
|
|
48
|
+
const fullDir = resolve(vaultPath, dir);
|
|
49
|
+
if (existsSync(fullDir)) {
|
|
50
|
+
const docs = scanRawDirectory(fullDir);
|
|
51
|
+
allDocs.push(...docs);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// 토픽 필터
|
|
55
|
+
const relevantDocs = topic
|
|
56
|
+
? allDocs.filter(d => d.title.toLowerCase().includes(topic.toLowerCase()) ||
|
|
57
|
+
d.tags.some(t => t.toLowerCase().includes(topic.toLowerCase())) ||
|
|
58
|
+
d.content.toLowerCase().includes(topic.toLowerCase()))
|
|
59
|
+
: allDocs;
|
|
60
|
+
// 3. 개념 추출
|
|
61
|
+
const concepts = extractConcepts(relevantDocs.length > 0 ? relevantDocs : allDocs);
|
|
62
|
+
const topConcepts = [...concepts.entries()]
|
|
63
|
+
.sort((a, b) => b[1].length - a[1].length)
|
|
64
|
+
.slice(0, 8)
|
|
65
|
+
.map(([name, docs]) => ({ name, documentCount: docs.length }));
|
|
66
|
+
// 4. 발췌 준비
|
|
67
|
+
const excerpts = (searchResults.length > 0 ? searchResults : relevantDocs.slice(0, maxSources))
|
|
68
|
+
.map(doc => {
|
|
69
|
+
const body = doc.content
|
|
70
|
+
.replace(/^---[\s\S]*?---\n?/, '') // frontmatter 제거
|
|
71
|
+
.replace(/^#+\s+.+\n/m, '') // 첫 heading 제거
|
|
72
|
+
.trim()
|
|
73
|
+
.slice(0, 500);
|
|
74
|
+
return {
|
|
75
|
+
title: doc.title,
|
|
76
|
+
excerpt: body,
|
|
77
|
+
filePath: doc.filePath,
|
|
78
|
+
};
|
|
79
|
+
})
|
|
80
|
+
.filter(e => e.excerpt.length > 20);
|
|
81
|
+
// 5. 구조화된 컨텍스트 반환
|
|
82
|
+
const context = {
|
|
83
|
+
topic: topic ?? 'All Knowledge',
|
|
84
|
+
format,
|
|
85
|
+
totalDocuments: relevantDocs.length || allDocs.length,
|
|
86
|
+
concepts: topConcepts,
|
|
87
|
+
sources: excerpts,
|
|
88
|
+
instruction: `Use the sources above to write a ${format}. Rules:
|
|
89
|
+
- Only use information from the provided sources
|
|
90
|
+
- Cite sources using [[title]] wikilink format
|
|
91
|
+
- Write in the same language as the source material
|
|
92
|
+
- Add your analysis connecting ideas across sources
|
|
93
|
+
- Format: ${format === 'blog' ? 'Engaging blog post with intro, body sections, conclusion' : format === 'report' ? 'Formal report with executive summary, sections, conclusion' : 'Hierarchical outline with numbered sections'}`,
|
|
94
|
+
};
|
|
95
|
+
const text = `# Draft Context: ${context.topic}
|
|
96
|
+
|
|
97
|
+
## Format: ${format}
|
|
98
|
+
## Sources: ${context.totalDocuments} documents, ${excerpts.length} excerpts
|
|
99
|
+
|
|
100
|
+
## Key Concepts
|
|
101
|
+
${topConcepts.map(c => `- **${c.name}** (${c.documentCount} documents)`).join('\n')}
|
|
102
|
+
|
|
103
|
+
## Source Excerpts
|
|
104
|
+
|
|
105
|
+
${excerpts.map(e => `### ${e.title}
|
|
106
|
+
> ${e.excerpt}
|
|
107
|
+
`).join('\n')}
|
|
108
|
+
|
|
109
|
+
## Instructions
|
|
110
|
+
${context.instruction}
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
Please write the ${format} draft based on the context above. Save the result to the vault's _drafts/ folder.`;
|
|
114
|
+
return {
|
|
115
|
+
content: [{ type: 'text', text }],
|
|
116
|
+
};
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=generate-draft.js.map
|
|
@@ -13,6 +13,22 @@
|
|
|
13
13
|
"./intelligence/youtube-extractor": {
|
|
14
14
|
"import": "./dist/intelligence/youtube-extractor.js",
|
|
15
15
|
"types": "./dist/intelligence/youtube-extractor.d.ts"
|
|
16
|
+
},
|
|
17
|
+
"./intelligence/file-extractors": {
|
|
18
|
+
"import": "./dist/intelligence/file-extractors.js",
|
|
19
|
+
"types": "./dist/intelligence/file-extractors.d.ts"
|
|
20
|
+
},
|
|
21
|
+
"./intelligence/draft-generator": {
|
|
22
|
+
"import": "./dist/intelligence/draft-generator.js",
|
|
23
|
+
"types": "./dist/intelligence/draft-generator.d.ts"
|
|
24
|
+
},
|
|
25
|
+
"./intelligence/wiki-compiler": {
|
|
26
|
+
"import": "./dist/intelligence/wiki-compiler.js",
|
|
27
|
+
"types": "./dist/intelligence/wiki-compiler.d.ts"
|
|
28
|
+
},
|
|
29
|
+
"./intelligence/knowledge-lint": {
|
|
30
|
+
"import": "./dist/intelligence/knowledge-lint.js",
|
|
31
|
+
"types": "./dist/intelligence/knowledge-lint.d.ts"
|
|
16
32
|
}
|
|
17
33
|
},
|
|
18
34
|
"scripts": {
|
|
@@ -30,7 +46,9 @@
|
|
|
30
46
|
"vitest": "^3.0.0"
|
|
31
47
|
},
|
|
32
48
|
"dependencies": {
|
|
49
|
+
"@anthropic-ai/sdk": "^0.82.0",
|
|
33
50
|
"@modelcontextprotocol/sdk": "^1.28.0",
|
|
51
|
+
"@types/multer": "^2.1.0",
|
|
34
52
|
"@xenova/transformers": "^2.17.2",
|
|
35
53
|
"b4a": "^1.8.0",
|
|
36
54
|
"better-sqlite3": "^12.8.0",
|
|
@@ -39,6 +57,11 @@
|
|
|
39
57
|
"express": "^5.2.1",
|
|
40
58
|
"gray-matter": "^4.0.3",
|
|
41
59
|
"hyperswarm": "^4.17.0",
|
|
42
|
-
"
|
|
60
|
+
"mammoth": "^1.12.0",
|
|
61
|
+
"multer": "^2.1.1",
|
|
62
|
+
"officeparser": "^6.0.7",
|
|
63
|
+
"sqlite-vec": "^0.1.7",
|
|
64
|
+
"unpdf": "^1.4.0",
|
|
65
|
+
"xlsx": "^0.18.5"
|
|
43
66
|
}
|
|
44
67
|
}
|
|
Binary file
|