stellavault 0.4.0 → 0.4.1
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 +24 -0
- package/README.md +96 -151
- package/package.json +2 -2
- package/packages/cli/dist/commands/draft-cmd.d.ts +5 -0
- package/packages/cli/dist/commands/draft-cmd.js +99 -0
- package/packages/cli/dist/commands/ingest-cmd.js +42 -12
- package/packages/cli/dist/index.js +7 -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/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 +28 -11
- 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 +16 -1
- package/Autonomous_Knowledge_Foundry.pdf +0 -0
|
@@ -474,6 +474,115 @@ export function createApiServer(options) {
|
|
|
474
474
|
res.status(500).json({ error: 'Ingest failed' });
|
|
475
475
|
}
|
|
476
476
|
});
|
|
477
|
+
// POST /api/ingest/file — 웹 UI에서 파일 드래그앤드롭 인제스트
|
|
478
|
+
app.post('/api/ingest/file', async (req, res) => {
|
|
479
|
+
try {
|
|
480
|
+
const multer = (await import('multer')).default;
|
|
481
|
+
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 50 * 1024 * 1024 } });
|
|
482
|
+
upload.single('file')(req, res, async (uploadErr) => {
|
|
483
|
+
if (uploadErr) {
|
|
484
|
+
res.status(400).json({ error: uploadErr.message || 'Upload failed' });
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
const file = req.file;
|
|
488
|
+
if (!file) {
|
|
489
|
+
res.status(400).json({ error: 'No file provided' });
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
// MIME 화이트리스트
|
|
493
|
+
const allowedMimes = new Set([
|
|
494
|
+
'application/pdf',
|
|
495
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
496
|
+
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
497
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
498
|
+
'application/vnd.ms-excel',
|
|
499
|
+
'text/plain', 'text/markdown', 'text/csv',
|
|
500
|
+
]);
|
|
501
|
+
if (!allowedMimes.has(file.mimetype)) {
|
|
502
|
+
res.status(400).json({ error: `Unsupported file type: ${file.mimetype}` });
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
try {
|
|
506
|
+
const { writeFileSync, unlinkSync } = await import('node:fs');
|
|
507
|
+
const { join } = await import('node:path');
|
|
508
|
+
const { tmpdir } = await import('node:os');
|
|
509
|
+
const tmpPath = join(tmpdir(), `sv-upload-${Date.now()}-${file.originalname}`);
|
|
510
|
+
// 임시 파일 저장 → 파서가 파일 경로 필요
|
|
511
|
+
writeFileSync(tmpPath, file.buffer);
|
|
512
|
+
const { extractFileContent, isBinaryFormat } = await import('../intelligence/file-extractors.js');
|
|
513
|
+
const ext = file.originalname.split('.').pop()?.toLowerCase() ?? '';
|
|
514
|
+
const binaryExts = new Set(['pdf', 'docx', 'pptx', 'xlsx', 'xls']);
|
|
515
|
+
let content;
|
|
516
|
+
let extractedTitle;
|
|
517
|
+
let formatTag = ext;
|
|
518
|
+
if (binaryExts.has(ext)) {
|
|
519
|
+
const extracted = await extractFileContent(tmpPath);
|
|
520
|
+
content = extracted.text;
|
|
521
|
+
extractedTitle = extracted.metadata.title;
|
|
522
|
+
formatTag = extracted.sourceFormat;
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
content = file.buffer.toString('utf-8');
|
|
526
|
+
}
|
|
527
|
+
// 임시 파일 삭제
|
|
528
|
+
try {
|
|
529
|
+
unlinkSync(tmpPath);
|
|
530
|
+
}
|
|
531
|
+
catch { /* ok */ }
|
|
532
|
+
// locale 설정
|
|
533
|
+
const locale = req.body?.locale;
|
|
534
|
+
if (locale) {
|
|
535
|
+
const { setNoteLocale } = await import('../i18n/note-strings.js');
|
|
536
|
+
setNoteLocale(locale);
|
|
537
|
+
}
|
|
538
|
+
const tags = req.body?.tags ? (Array.isArray(req.body.tags) ? req.body.tags : req.body.tags.split(',').map((t) => t.trim())) : [];
|
|
539
|
+
const { ingest } = await import('../intelligence/ingest-pipeline.js');
|
|
540
|
+
const result = ingest(vaultPath, {
|
|
541
|
+
type: formatTag,
|
|
542
|
+
content,
|
|
543
|
+
tags: [...tags, formatTag],
|
|
544
|
+
title: req.body?.title ?? extractedTitle,
|
|
545
|
+
stage: 'fleeting',
|
|
546
|
+
source: file.originalname,
|
|
547
|
+
});
|
|
548
|
+
// 자동 인덱싱
|
|
549
|
+
try {
|
|
550
|
+
const doc = {
|
|
551
|
+
id: require('node:crypto').createHash('sha256').update(result.savedTo).digest('hex').slice(0, 16),
|
|
552
|
+
filePath: result.savedTo,
|
|
553
|
+
title: result.title,
|
|
554
|
+
content,
|
|
555
|
+
frontmatter: {},
|
|
556
|
+
tags: result.tags,
|
|
557
|
+
lastModified: new Date().toISOString(),
|
|
558
|
+
contentHash: '',
|
|
559
|
+
source: 'upload',
|
|
560
|
+
type: result.stage,
|
|
561
|
+
};
|
|
562
|
+
await store.upsertDocument(doc);
|
|
563
|
+
}
|
|
564
|
+
catch (indexErr) {
|
|
565
|
+
console.error('[ingest/file] Auto-index failed:', indexErr instanceof Error ? indexErr.message : indexErr);
|
|
566
|
+
}
|
|
567
|
+
res.json({
|
|
568
|
+
success: true,
|
|
569
|
+
savedTo: result.savedTo,
|
|
570
|
+
stage: result.stage,
|
|
571
|
+
title: result.title,
|
|
572
|
+
indexCode: result.indexCode,
|
|
573
|
+
tags: result.tags,
|
|
574
|
+
wordCount: result.wordCount,
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
catch (err) {
|
|
578
|
+
res.status(500).json({ error: err instanceof Error ? err.message : 'Processing failed' });
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
catch (err) {
|
|
583
|
+
res.status(500).json({ error: 'File upload initialization failed' });
|
|
584
|
+
}
|
|
585
|
+
});
|
|
477
586
|
// GET /api/recent — 최근 저장된 노트 목록
|
|
478
587
|
app.get('/api/recent', async (_req, res) => {
|
|
479
588
|
try {
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
+
export interface FolderNames {
|
|
2
|
+
fleeting: string;
|
|
3
|
+
literature: string;
|
|
4
|
+
permanent: string;
|
|
5
|
+
wiki: string;
|
|
6
|
+
}
|
|
1
7
|
export interface StellavaultConfig {
|
|
2
8
|
vaultPath: string;
|
|
3
9
|
dbPath: string;
|
|
10
|
+
folders: FolderNames;
|
|
4
11
|
embedding: {
|
|
5
12
|
model: 'local' | 'openai';
|
|
6
13
|
localModel: string;
|
|
@@ -19,6 +26,7 @@ export interface StellavaultConfig {
|
|
|
19
26
|
port: number;
|
|
20
27
|
};
|
|
21
28
|
}
|
|
29
|
+
export declare const DEFAULT_FOLDERS: FolderNames;
|
|
22
30
|
/**
|
|
23
31
|
* .stellavault.json 파일을 찾아 로드합니다.
|
|
24
32
|
* 탐색 순서: cwd → home directory → defaults
|
|
@@ -2,12 +2,19 @@
|
|
|
2
2
|
import { readFileSync, existsSync } from 'node:fs';
|
|
3
3
|
import { resolve, join } from 'node:path';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
|
+
export const DEFAULT_FOLDERS = {
|
|
6
|
+
fleeting: 'raw',
|
|
7
|
+
literature: '_literature',
|
|
8
|
+
permanent: '_permanent',
|
|
9
|
+
wiki: '_wiki',
|
|
10
|
+
};
|
|
5
11
|
const DEFAULT_CONFIG = {
|
|
6
12
|
vaultPath: '',
|
|
7
13
|
dbPath: join(homedir(), '.stellavault', 'index.db'),
|
|
14
|
+
folders: { ...DEFAULT_FOLDERS },
|
|
8
15
|
embedding: {
|
|
9
16
|
model: 'local',
|
|
10
|
-
localModel: '
|
|
17
|
+
localModel: 'paraphrase-multilingual-MiniLM-L12-v2',
|
|
11
18
|
},
|
|
12
19
|
chunking: {
|
|
13
20
|
maxTokens: 300,
|
|
@@ -46,6 +53,7 @@ function mergeConfig(defaults, overrides) {
|
|
|
46
53
|
return {
|
|
47
54
|
vaultPath: overrides.vaultPath ?? defaults.vaultPath,
|
|
48
55
|
dbPath: overrides.dbPath ?? defaults.dbPath,
|
|
56
|
+
folders: { ...defaults.folders, ...overrides.folders },
|
|
49
57
|
embedding: { ...defaults.embedding, ...overrides.embedding },
|
|
50
58
|
chunking: { ...defaults.chunking, ...overrides.chunking },
|
|
51
59
|
search: { ...defaults.search, ...overrides.search },
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { loadConfig } from './config.js';
|
|
2
|
-
export type { StellavaultConfig } from './config.js';
|
|
1
|
+
export { loadConfig, DEFAULT_FOLDERS } from './config.js';
|
|
2
|
+
export type { StellavaultConfig, FolderNames } from './config.js';
|
|
3
3
|
export type { Document } from './types/document.js';
|
|
4
4
|
export type { Chunk, ScoredChunk } from './types/chunk.js';
|
|
5
5
|
export type { SearchResult, SearchOptions, TopicInfo, StoreStats, } from './types/search.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Design Ref: §4.2 — Core Internal API (Facade)
|
|
2
2
|
// Design Ref: §9.3 — Dependency Injection Pattern
|
|
3
|
-
export { loadConfig } from './config.js';
|
|
3
|
+
export { loadConfig, DEFAULT_FOLDERS } from './config.js';
|
|
4
4
|
// Store
|
|
5
5
|
export { createSqliteVecStore } from './store/index.js';
|
|
6
6
|
// Indexer
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type FolderNames } from '../config.js';
|
|
2
|
+
export interface DraftOptions {
|
|
3
|
+
topic?: string;
|
|
4
|
+
format?: 'blog' | 'report' | 'outline';
|
|
5
|
+
maxSections?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface DraftResult {
|
|
8
|
+
title: string;
|
|
9
|
+
filePath: string;
|
|
10
|
+
wordCount: number;
|
|
11
|
+
sourceCount: number;
|
|
12
|
+
concepts: string[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* vault의 wiki 데이터를 기반으로 초안(draft)을 생성.
|
|
16
|
+
* Express 단계: 지식이 vault에서 나가는 출구.
|
|
17
|
+
*/
|
|
18
|
+
export declare function generateDraft(vaultPath: string, options?: DraftOptions, folders?: FolderNames): DraftResult;
|
|
19
|
+
//# sourceMappingURL=draft-generator.d.ts.map
|
|
@@ -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,8 @@
|
|
|
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 { DEFAULT_FOLDERS } from '../config.js';
|
|
9
11
|
/** YAML 값에서 위험한 문자를 이스케이프 */
|
|
10
12
|
function sanitizeYaml(val) {
|
|
11
13
|
return val.replace(/["\\]/g, '\\$&').replace(/\n/g, ' ').slice(0, 200);
|
|
@@ -13,7 +15,7 @@ function sanitizeYaml(val) {
|
|
|
13
15
|
/**
|
|
14
16
|
* 어떤 입력이든 Stellavault 표준 포맷으로 변환하여 저장.
|
|
15
17
|
*/
|
|
16
|
-
export function ingest(vaultPath, input) {
|
|
18
|
+
export function ingest(vaultPath, input, folders = DEFAULT_FOLDERS) {
|
|
17
19
|
const stage = input.stage ?? 'fleeting';
|
|
18
20
|
const title = input.title ?? extractTitleFromContent(input.content, input.type);
|
|
19
21
|
const tags = input.tags ?? extractAutoTags(input.content, input.type);
|
|
@@ -23,11 +25,11 @@ export function ingest(vaultPath, input) {
|
|
|
23
25
|
const wordCount = body.split(/\s+/).length;
|
|
24
26
|
// 자동 분류: 길이/구조에 따라 stage 결정
|
|
25
27
|
const autoStage = classifyStage(body, stage, wordCount);
|
|
26
|
-
// 폴더 결정
|
|
28
|
+
// 폴더 결정 (config-driven)
|
|
27
29
|
const folderMap = {
|
|
28
|
-
fleeting:
|
|
29
|
-
literature:
|
|
30
|
-
permanent:
|
|
30
|
+
fleeting: folders.fleeting,
|
|
31
|
+
literature: folders.literature,
|
|
32
|
+
permanent: folders.permanent,
|
|
31
33
|
};
|
|
32
34
|
const folder = folderMap[autoStage];
|
|
33
35
|
const dir = resolve(vaultPath, folder);
|
|
@@ -67,6 +69,15 @@ export function ingest(vaultPath, input) {
|
|
|
67
69
|
inputType: input.type,
|
|
68
70
|
});
|
|
69
71
|
writeFileSync(fullPath, md, 'utf-8');
|
|
72
|
+
// 자동 compile: fleeting → wiki (rule-based, <100ms)
|
|
73
|
+
try {
|
|
74
|
+
const rawDir = resolve(vaultPath, folders.fleeting);
|
|
75
|
+
const wikiDir = resolve(vaultPath, folders.wiki);
|
|
76
|
+
if (existsSync(rawDir)) {
|
|
77
|
+
compileWiki(rawDir, wikiDir);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch { /* compile 실패해도 ingest 성공 */ }
|
|
70
81
|
return {
|
|
71
82
|
savedTo: filePath,
|
|
72
83
|
stage: autoStage,
|
|
@@ -86,18 +97,18 @@ export function ingestBatch(vaultPath, inputs) {
|
|
|
86
97
|
* 노트 승격: fleeting → literature → permanent.
|
|
87
98
|
* 내용이 충분히 정제되면 다음 단계로 이동.
|
|
88
99
|
*/
|
|
89
|
-
export function promoteNote(vaultPath, filePath, targetStage) {
|
|
100
|
+
export function promoteNote(vaultPath, filePath, targetStage, folders = DEFAULT_FOLDERS) {
|
|
90
101
|
const fullPath = resolve(vaultPath, filePath);
|
|
91
102
|
if (!existsSync(fullPath))
|
|
92
103
|
throw new Error(`File not found: ${filePath}`);
|
|
93
104
|
const content = readFileSync(fullPath, 'utf-8');
|
|
94
105
|
// frontmatter의 type 변경
|
|
95
106
|
const updated = content.replace(/^type:\s*.+$/m, `type: ${targetStage}`);
|
|
96
|
-
// 대상 폴더로 이동
|
|
107
|
+
// 대상 폴더로 이동 (config-driven)
|
|
97
108
|
const folderMap = {
|
|
98
|
-
fleeting:
|
|
99
|
-
literature:
|
|
100
|
-
permanent:
|
|
109
|
+
fleeting: folders.fleeting,
|
|
110
|
+
literature: folders.literature,
|
|
111
|
+
permanent: folders.permanent,
|
|
101
112
|
};
|
|
102
113
|
const newDir = resolve(vaultPath, folderMap[targetStage]);
|
|
103
114
|
if (!existsSync(newDir))
|
|
@@ -138,8 +149,14 @@ function extractAutoTags(content, type) {
|
|
|
138
149
|
tags.add('web-clip');
|
|
139
150
|
if (type === 'youtube')
|
|
140
151
|
tags.add('youtube');
|
|
141
|
-
if (type === 'pdf-text')
|
|
152
|
+
if (type === 'pdf-text' || type === 'pdf')
|
|
142
153
|
tags.add('pdf');
|
|
154
|
+
if (type === 'docx')
|
|
155
|
+
tags.add('document');
|
|
156
|
+
if (type === 'pptx')
|
|
157
|
+
tags.add('presentation');
|
|
158
|
+
if (type === 'xlsx' || type === 'xls')
|
|
159
|
+
tags.add('spreadsheet');
|
|
143
160
|
// 인라인 #태그 추출
|
|
144
161
|
const inline = content.match(/#([a-zA-Z가-힣][a-zA-Z0-9가-힣_-]{2,})/g) ?? [];
|
|
145
162
|
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)
|