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.
- package/Autonomous_Knowledge_Foundry.pdf +0 -0
- package/CHANGELOG.md +60 -0
- package/README.md +140 -95
- package/package.json +1 -1
- package/packages/cli/dist/commands/ask-cmd.d.ts +4 -0
- package/packages/cli/dist/commands/ask-cmd.js +35 -0
- package/packages/cli/dist/commands/autopilot-cmd.d.ts +4 -0
- package/packages/cli/dist/commands/autopilot-cmd.js +76 -0
- package/packages/cli/dist/commands/compile-cmd.d.ts +6 -0
- package/packages/cli/dist/commands/compile-cmd.js +30 -0
- package/packages/cli/dist/commands/digest-cmd.d.ts +1 -0
- package/packages/cli/dist/commands/digest-cmd.js +57 -0
- package/packages/cli/dist/commands/fleeting-cmd.d.ts +4 -0
- package/packages/cli/dist/commands/fleeting-cmd.js +45 -0
- package/packages/cli/dist/commands/graph-cmd.js +13 -1
- package/packages/cli/dist/commands/ingest-cmd.d.ts +9 -0
- package/packages/cli/dist/commands/ingest-cmd.js +131 -0
- package/packages/cli/dist/commands/init-cmd.js +39 -1
- package/packages/cli/dist/commands/lint-cmd.d.ts +2 -0
- package/packages/cli/dist/commands/lint-cmd.js +61 -0
- package/packages/cli/dist/index.js +46 -1
- package/packages/cli/package.json +1 -1
- package/packages/core/dist/api/server.js +284 -0
- package/packages/core/dist/i18n/note-strings.d.ts +5 -0
- package/packages/core/dist/i18n/note-strings.js +94 -0
- package/packages/core/dist/index.d.ts +9 -0
- package/packages/core/dist/index.js +5 -0
- package/packages/core/dist/intelligence/ask-engine.d.ts +23 -0
- package/packages/core/dist/intelligence/ask-engine.js +108 -0
- package/packages/core/dist/intelligence/ingest-pipeline.d.ts +31 -0
- package/packages/core/dist/intelligence/ingest-pipeline.js +192 -0
- package/packages/core/dist/intelligence/knowledge-lint.d.ts +27 -0
- package/packages/core/dist/intelligence/knowledge-lint.js +132 -0
- package/packages/core/dist/intelligence/wiki-compiler.d.ts +30 -0
- package/packages/core/dist/intelligence/wiki-compiler.js +222 -0
- package/packages/core/dist/intelligence/youtube-extractor.d.ts +29 -0
- package/packages/core/dist/intelligence/youtube-extractor.js +311 -0
- package/packages/core/dist/intelligence/zettelkasten.d.ts +59 -0
- package/packages/core/dist/intelligence/zettelkasten.js +234 -0
- package/packages/core/dist/mcp/server.d.ts +2 -0
- package/packages/core/dist/mcp/server.js +18 -1
- package/packages/core/dist/mcp/tools/agentic-graph.d.ts +6 -0
- package/packages/core/dist/mcp/tools/agentic-graph.js +35 -7
- package/packages/core/dist/mcp/tools/ask.d.ts +29 -0
- package/packages/core/dist/mcp/tools/ask.js +40 -0
- package/packages/core/package.json +5 -1
|
@@ -41,6 +41,43 @@ export function createApiServer(options) {
|
|
|
41
41
|
res.status(500).json({ error: 'Internal server error' });
|
|
42
42
|
}
|
|
43
43
|
});
|
|
44
|
+
// POST /api/reindex — 웹에서 인덱싱 트리거
|
|
45
|
+
let isReindexing = false;
|
|
46
|
+
app.post('/api/reindex', async (_req, res) => {
|
|
47
|
+
if (isReindexing) {
|
|
48
|
+
res.json({ success: false, error: '인덱싱이 이미 진행 중입니다' });
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
isReindexing = true;
|
|
52
|
+
try {
|
|
53
|
+
const indexer = await import('../indexer/index.js');
|
|
54
|
+
const embedder = indexer.createLocalEmbedder('all-MiniLM-L6-v2');
|
|
55
|
+
await embedder.initialize();
|
|
56
|
+
const result = await indexer.indexVault(vaultPath, {
|
|
57
|
+
store,
|
|
58
|
+
embedder,
|
|
59
|
+
onProgress: (current, total) => {
|
|
60
|
+
if (current % 50 === 0)
|
|
61
|
+
console.error(`[reindex] ${current}/${total}`);
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
// 그래프 캐시 리셋
|
|
65
|
+
graphCaches.clear();
|
|
66
|
+
res.json({
|
|
67
|
+
success: true,
|
|
68
|
+
indexed: result.indexed,
|
|
69
|
+
skipped: result.skipped,
|
|
70
|
+
chunks: result.totalChunks,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
console.error('[reindex]', err);
|
|
75
|
+
res.status(500).json({ error: `Reindex failed: ${err?.message ?? String(err)}` });
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
isReindexing = false;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
44
81
|
// GET /api/search?q=&limit=
|
|
45
82
|
app.get('/api/search', async (req, res) => {
|
|
46
83
|
try {
|
|
@@ -62,7 +99,10 @@ export function createApiServer(options) {
|
|
|
62
99
|
results: results.map(r => ({
|
|
63
100
|
documentId: r.document.id,
|
|
64
101
|
title: r.document.title,
|
|
102
|
+
filePath: r.document.filePath,
|
|
65
103
|
score: Math.round(r.score * 1000) / 1000,
|
|
104
|
+
snippet: r.chunk?.content?.substring(0, 200) ?? '',
|
|
105
|
+
tags: r.document.tags ?? [],
|
|
66
106
|
highlights: r.highlights,
|
|
67
107
|
})),
|
|
68
108
|
query,
|
|
@@ -213,6 +253,250 @@ export function createApiServer(options) {
|
|
|
213
253
|
res.status(500).json({ error: 'Internal server error' });
|
|
214
254
|
}
|
|
215
255
|
});
|
|
256
|
+
// PUT /api/document/:id — 노트 편집 (vault 파일 직접 수정)
|
|
257
|
+
app.put('/api/document/:id', async (req, res) => {
|
|
258
|
+
try {
|
|
259
|
+
const { id } = req.params;
|
|
260
|
+
const { title, content, tags } = req.body;
|
|
261
|
+
const doc = await store.getDocument(id);
|
|
262
|
+
if (!doc) {
|
|
263
|
+
res.status(404).json({ error: 'Document not found' });
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const { resolve, join } = await import('node:path');
|
|
267
|
+
const { writeFileSync, readFileSync } = await import('node:fs');
|
|
268
|
+
const fullPath = resolve(vaultPath, doc.filePath);
|
|
269
|
+
// path traversal 방지
|
|
270
|
+
if (!fullPath.startsWith(resolve(vaultPath))) {
|
|
271
|
+
res.status(403).json({ error: 'Access denied' });
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
// 기존 파일 읽기
|
|
275
|
+
const existing = readFileSync(fullPath, 'utf-8');
|
|
276
|
+
// frontmatter 업데이트
|
|
277
|
+
let updated = existing;
|
|
278
|
+
if (title && title !== doc.title) {
|
|
279
|
+
updated = updated.replace(/^title:\s*.+$/m, `title: "${title.replace(/"/g, "''")}"`);
|
|
280
|
+
}
|
|
281
|
+
if (tags) {
|
|
282
|
+
const tagStr = `tags: [${tags.map((t) => `"${t}"`).join(', ')}]`;
|
|
283
|
+
if (updated.match(/^tags:\s*.+$/m)) {
|
|
284
|
+
updated = updated.replace(/^tags:\s*.+$/m, tagStr);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (content !== undefined) {
|
|
288
|
+
// frontmatter 이후 본문 교체
|
|
289
|
+
const fmEnd = updated.indexOf('---', 4);
|
|
290
|
+
if (fmEnd > 0) {
|
|
291
|
+
const fm = updated.substring(0, fmEnd + 3);
|
|
292
|
+
updated = fm + '\n\n' + content;
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
updated = content;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
writeFileSync(fullPath, updated, 'utf-8');
|
|
299
|
+
// DB 업데이트
|
|
300
|
+
await store.upsertDocument({
|
|
301
|
+
...doc,
|
|
302
|
+
title: title ?? doc.title,
|
|
303
|
+
content: content ?? doc.content,
|
|
304
|
+
tags: tags ?? doc.tags,
|
|
305
|
+
lastModified: new Date().toISOString(),
|
|
306
|
+
});
|
|
307
|
+
res.json({ success: true, id, title: title ?? doc.title });
|
|
308
|
+
}
|
|
309
|
+
catch (err) {
|
|
310
|
+
console.error('[edit]', err);
|
|
311
|
+
res.status(500).json({ error: err?.message ?? 'Edit failed' });
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
// DELETE /api/document/:id — 노트 삭제 (vault 파일 + DB)
|
|
315
|
+
app.delete('/api/document/:id', async (req, res) => {
|
|
316
|
+
try {
|
|
317
|
+
const { id } = req.params;
|
|
318
|
+
const doc = await store.getDocument(id);
|
|
319
|
+
if (!doc) {
|
|
320
|
+
res.status(404).json({ error: 'Document not found' });
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const { resolve } = await import('node:path');
|
|
324
|
+
const { unlinkSync, existsSync } = await import('node:fs');
|
|
325
|
+
const fullPath = resolve(vaultPath, doc.filePath);
|
|
326
|
+
// path traversal 방지
|
|
327
|
+
if (!fullPath.startsWith(resolve(vaultPath))) {
|
|
328
|
+
res.status(403).json({ error: 'Access denied' });
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
// 파일 삭제
|
|
332
|
+
if (existsSync(fullPath)) {
|
|
333
|
+
unlinkSync(fullPath);
|
|
334
|
+
}
|
|
335
|
+
// DB에서 삭제
|
|
336
|
+
await store.deleteByDocumentId(id);
|
|
337
|
+
res.json({ success: true, id, deleted: doc.filePath });
|
|
338
|
+
}
|
|
339
|
+
catch (err) {
|
|
340
|
+
console.error('[delete]', err);
|
|
341
|
+
res.status(500).json({ error: err?.message ?? 'Delete failed' });
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
// GET /api/ask — 웹 UI Q&A
|
|
345
|
+
app.get('/api/ask', async (req, res) => {
|
|
346
|
+
try {
|
|
347
|
+
const question = String(req.query.q || '');
|
|
348
|
+
if (!question) {
|
|
349
|
+
res.json({ question: '', answer: '', sources: [] });
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const { askVault } = await import('../intelligence/ask-engine.js');
|
|
353
|
+
const result = await askVault(searchEngine, question, {
|
|
354
|
+
limit: 10,
|
|
355
|
+
save: req.query.save === 'true',
|
|
356
|
+
vaultPath,
|
|
357
|
+
});
|
|
358
|
+
res.json(result);
|
|
359
|
+
}
|
|
360
|
+
catch (err) {
|
|
361
|
+
console.error(err);
|
|
362
|
+
res.status(500).json({ error: 'Ask failed' });
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
// POST /api/ingest — 웹 UI에서 URL/텍스트 인제스트
|
|
366
|
+
app.post('/api/ingest', async (req, res) => {
|
|
367
|
+
try {
|
|
368
|
+
const { input, type, tags, title, stage, locale } = req.body;
|
|
369
|
+
// 노트 언어 설정
|
|
370
|
+
if (locale) {
|
|
371
|
+
const { setNoteLocale } = await import('../i18n/note-strings.js');
|
|
372
|
+
setNoteLocale(locale);
|
|
373
|
+
}
|
|
374
|
+
if (!input || typeof input !== 'string') {
|
|
375
|
+
res.status(400).json({ error: 'input is required' });
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
// URL인 경우 제목+내용 가져오기
|
|
379
|
+
let content = input;
|
|
380
|
+
let autoTitle = title;
|
|
381
|
+
let autoTags = tags ?? [];
|
|
382
|
+
let autoStage = stage ?? 'fleeting';
|
|
383
|
+
const isYouTube = /youtube\.com\/watch|youtu\.be\//.test(input);
|
|
384
|
+
if (isYouTube) {
|
|
385
|
+
// YouTube 전용: 자막 + 메타데이터 추출
|
|
386
|
+
try {
|
|
387
|
+
const { extractYouTubeContent, formatYouTubeNote } = await import('../intelligence/youtube-extractor.js');
|
|
388
|
+
const ytContent = await extractYouTubeContent(input);
|
|
389
|
+
autoTitle = ytContent.title;
|
|
390
|
+
autoTags = [...new Set(['youtube', ...ytContent.tags, ...(tags ?? [])])];
|
|
391
|
+
autoStage = 'literature';
|
|
392
|
+
// formatYouTubeNote는 frontmatter 없이 본문만 생성 → pipeline이 frontmatter 담당
|
|
393
|
+
content = formatYouTubeNote(ytContent);
|
|
394
|
+
}
|
|
395
|
+
catch (ytErr) {
|
|
396
|
+
console.error('[ingest] YouTube extraction failed, falling back to basic:', ytErr instanceof Error ? ytErr.message : ytErr);
|
|
397
|
+
// 폴백: 기본 HTML 추출
|
|
398
|
+
try {
|
|
399
|
+
const resp = await fetch(input, { signal: AbortSignal.timeout(8000) });
|
|
400
|
+
const html = await resp.text();
|
|
401
|
+
const titleMatch = html.match(/<title[^>]*>([^<]+)<\/title>/i);
|
|
402
|
+
if (titleMatch && !autoTitle)
|
|
403
|
+
autoTitle = titleMatch[1].trim();
|
|
404
|
+
content = input + '\n\n' + html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 5000);
|
|
405
|
+
}
|
|
406
|
+
catch { /* URL만 저장 */ }
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
else if (input.startsWith('http')) {
|
|
410
|
+
// 일반 웹페이지
|
|
411
|
+
try {
|
|
412
|
+
const resp = await fetch(input, { signal: AbortSignal.timeout(8000) });
|
|
413
|
+
const html = await resp.text();
|
|
414
|
+
const titleMatch = html.match(/<title[^>]*>([^<]+)<\/title>/i);
|
|
415
|
+
if (titleMatch && !autoTitle)
|
|
416
|
+
autoTitle = titleMatch[1].trim();
|
|
417
|
+
const text = html
|
|
418
|
+
.replace(/<script[\s\S]*?<\/script>/gi, '')
|
|
419
|
+
.replace(/<style[\s\S]*?<\/style>/gi, '')
|
|
420
|
+
.replace(/<[^>]+>/g, ' ')
|
|
421
|
+
.replace(/ /g, ' ')
|
|
422
|
+
.replace(/&/g, '&')
|
|
423
|
+
.replace(/</g, '<')
|
|
424
|
+
.replace(/>/g, '>')
|
|
425
|
+
.replace(/\s+/g, ' ')
|
|
426
|
+
.trim()
|
|
427
|
+
.slice(0, 5000);
|
|
428
|
+
content = input + '\n\n' + text;
|
|
429
|
+
}
|
|
430
|
+
catch { /* URL만 저장 */ }
|
|
431
|
+
}
|
|
432
|
+
const { ingest } = await import('../intelligence/ingest-pipeline.js');
|
|
433
|
+
const result = ingest(vaultPath, {
|
|
434
|
+
type: type ?? (isYouTube ? 'youtube' : input.startsWith('http') ? 'url' : 'text'),
|
|
435
|
+
content,
|
|
436
|
+
tags: autoTags,
|
|
437
|
+
title: autoTitle,
|
|
438
|
+
stage: autoStage,
|
|
439
|
+
source: input.startsWith('http') ? input : undefined,
|
|
440
|
+
});
|
|
441
|
+
// 저장 후 자동 인덱싱 (그래프에 바로 반영)
|
|
442
|
+
try {
|
|
443
|
+
const fullPath = require('node:path').resolve(vaultPath, result.savedTo);
|
|
444
|
+
const { chunkDocument } = await import('../indexer/index.js');
|
|
445
|
+
const doc = {
|
|
446
|
+
id: require('node:crypto').createHash('sha256').update(result.savedTo).digest('hex').slice(0, 16),
|
|
447
|
+
filePath: result.savedTo,
|
|
448
|
+
title: result.title,
|
|
449
|
+
content: content,
|
|
450
|
+
frontmatter: {},
|
|
451
|
+
tags: result.tags,
|
|
452
|
+
lastModified: new Date().toISOString(),
|
|
453
|
+
contentHash: '',
|
|
454
|
+
source: 'ingest',
|
|
455
|
+
type: result.stage,
|
|
456
|
+
};
|
|
457
|
+
await store.upsertDocument(doc);
|
|
458
|
+
}
|
|
459
|
+
catch (indexErr) {
|
|
460
|
+
console.error('[ingest] Auto-index failed:', indexErr instanceof Error ? indexErr.message : indexErr);
|
|
461
|
+
}
|
|
462
|
+
res.json({
|
|
463
|
+
success: true,
|
|
464
|
+
savedTo: result.savedTo,
|
|
465
|
+
stage: result.stage,
|
|
466
|
+
title: result.title,
|
|
467
|
+
indexCode: result.indexCode,
|
|
468
|
+
tags: result.tags,
|
|
469
|
+
wordCount: result.wordCount,
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
catch (err) {
|
|
473
|
+
console.error(err);
|
|
474
|
+
res.status(500).json({ error: 'Ingest failed' });
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
// GET /api/recent — 최근 저장된 노트 목록
|
|
478
|
+
app.get('/api/recent', async (_req, res) => {
|
|
479
|
+
try {
|
|
480
|
+
const docs = await store.getAllDocuments();
|
|
481
|
+
const recent = docs
|
|
482
|
+
.filter(d => d.lastModified)
|
|
483
|
+
.sort((a, b) => new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime())
|
|
484
|
+
.slice(0, 20)
|
|
485
|
+
.map(d => ({
|
|
486
|
+
id: d.id,
|
|
487
|
+
title: d.title,
|
|
488
|
+
filePath: d.filePath,
|
|
489
|
+
lastModified: d.lastModified,
|
|
490
|
+
tags: d.tags.slice(0, 5),
|
|
491
|
+
type: d.type ?? 'note',
|
|
492
|
+
}));
|
|
493
|
+
res.json({ recent });
|
|
494
|
+
}
|
|
495
|
+
catch (err) {
|
|
496
|
+
console.error(err);
|
|
497
|
+
res.status(500).json({ error: 'Failed' });
|
|
498
|
+
}
|
|
499
|
+
});
|
|
216
500
|
// GET /api/heatmap — Design Ref: §2.2 — 지식 히트맵 활동 점수
|
|
217
501
|
app.get('/api/heatmap', async (_req, res) => {
|
|
218
502
|
try {
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// 저장 노트용 다국어 문자열 — 사용자 언어에 따라 노트 섹션 제목 결정
|
|
2
|
+
const strings = {
|
|
3
|
+
en: {
|
|
4
|
+
'summary': 'Summary',
|
|
5
|
+
'description': 'Description',
|
|
6
|
+
'transcript': 'Transcript',
|
|
7
|
+
'views': 'Views',
|
|
8
|
+
'relatedDocs': 'Related Documents',
|
|
9
|
+
'relatedConcepts': 'Related Concepts',
|
|
10
|
+
'relatedTags': 'Related Tags',
|
|
11
|
+
'source': 'Source',
|
|
12
|
+
'articles': 'Articles',
|
|
13
|
+
'concepts': 'Concepts',
|
|
14
|
+
'exploreFurther': 'Explore Further',
|
|
15
|
+
'noResults': 'No results found',
|
|
16
|
+
'tryDifferent': 'Try different keywords or create a note on this topic.',
|
|
17
|
+
'digDeeper': 'Dig deeper',
|
|
18
|
+
'findGaps': 'Find knowledge gaps',
|
|
19
|
+
'thisConceptAppears': 'This concept appears in',
|
|
20
|
+
'documents': 'documents',
|
|
21
|
+
'compiledFrom': 'Compiled',
|
|
22
|
+
'wikiArticlesFrom': 'wiki articles from',
|
|
23
|
+
'sourceDocuments': 'source documents',
|
|
24
|
+
'staleWarning': "documents haven't been modified in 6+ months. Run `stellavault decay` to review.",
|
|
25
|
+
'gapsFound': 'knowledge gaps found. Start filling the most critical ones.',
|
|
26
|
+
'duplicatesFound': 'duplicate documents detected. Run `stellavault duplicates` to merge.',
|
|
27
|
+
'isolatedFound': 'isolated notes found. Add tags or links to connect them.',
|
|
28
|
+
'healthy': 'Your knowledge base is healthy! Keep it up.',
|
|
29
|
+
'youtubeVideo': 'YouTube video',
|
|
30
|
+
},
|
|
31
|
+
ko: {
|
|
32
|
+
'summary': '핵심 요약',
|
|
33
|
+
'description': '설명',
|
|
34
|
+
'transcript': '영상 내용',
|
|
35
|
+
'views': '조회수',
|
|
36
|
+
'relatedDocs': '관련 문서',
|
|
37
|
+
'relatedConcepts': '관련 개념',
|
|
38
|
+
'relatedTags': '관련 태그',
|
|
39
|
+
'source': '원본',
|
|
40
|
+
'articles': '문서 목록',
|
|
41
|
+
'concepts': '개념',
|
|
42
|
+
'exploreFurther': '더 알아보기',
|
|
43
|
+
'noResults': '검색 결과 없음',
|
|
44
|
+
'tryDifferent': '다른 키워드로 검색하거나, 이 주제에 대한 노트를 작성해보세요.',
|
|
45
|
+
'digDeeper': '더 깊이 알아보기',
|
|
46
|
+
'findGaps': '빠진 지식 찾기',
|
|
47
|
+
'thisConceptAppears': '이 개념이 등장하는 문서',
|
|
48
|
+
'documents': '개',
|
|
49
|
+
'compiledFrom': '컴파일됨',
|
|
50
|
+
'wikiArticlesFrom': '개 위키 문서 생성 (원본',
|
|
51
|
+
'sourceDocuments': '개)',
|
|
52
|
+
'staleWarning': '개 문서가 6개월 이상 수정되지 않았어요. `stellavault decay`로 확인하세요.',
|
|
53
|
+
'gapsFound': '개 지식 빈틈이 발견됐어요. 중요한 것부터 채워보세요.',
|
|
54
|
+
'duplicatesFound': '개 겹치는 문서가 있어요. `stellavault duplicates`로 합치세요.',
|
|
55
|
+
'isolatedFound': '개 떨어진 노트가 있어요. 태그나 링크로 연결하세요.',
|
|
56
|
+
'healthy': '지식이 건강해요! 계속 유지하세요.',
|
|
57
|
+
'youtubeVideo': 'YouTube 영상',
|
|
58
|
+
},
|
|
59
|
+
ja: {
|
|
60
|
+
'summary': '要約',
|
|
61
|
+
'description': '説明',
|
|
62
|
+
'transcript': '内容',
|
|
63
|
+
'views': '再生回数',
|
|
64
|
+
'relatedDocs': '関連ドキュメント',
|
|
65
|
+
'relatedConcepts': '関連コンセプト',
|
|
66
|
+
'source': 'ソース',
|
|
67
|
+
'noResults': '検索結果なし',
|
|
68
|
+
'healthy': 'ナレッジベースは健全です!',
|
|
69
|
+
'youtubeVideo': 'YouTube動画',
|
|
70
|
+
},
|
|
71
|
+
zh: {
|
|
72
|
+
'summary': '摘要',
|
|
73
|
+
'description': '描述',
|
|
74
|
+
'transcript': '内容',
|
|
75
|
+
'views': '观看次数',
|
|
76
|
+
'relatedDocs': '相关文档',
|
|
77
|
+
'relatedConcepts': '相关概念',
|
|
78
|
+
'source': '来源',
|
|
79
|
+
'noResults': '未找到结果',
|
|
80
|
+
'healthy': '知识库很健康!',
|
|
81
|
+
'youtubeVideo': 'YouTube视频',
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
let currentNoteLocale = 'en';
|
|
85
|
+
export function setNoteLocale(locale) {
|
|
86
|
+
currentNoteLocale = locale;
|
|
87
|
+
}
|
|
88
|
+
export function getNoteLocale() {
|
|
89
|
+
return currentNoteLocale;
|
|
90
|
+
}
|
|
91
|
+
export function nt(key) {
|
|
92
|
+
return strings[currentNoteLocale]?.[key] ?? strings.en[key] ?? key;
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=note-strings.js.map
|
|
@@ -29,6 +29,15 @@ export { predictKnowledgeGaps } from './intelligence/predictive-gaps.js';
|
|
|
29
29
|
export type { PredictedGap } from './intelligence/predictive-gaps.js';
|
|
30
30
|
export type { DuplicatePair } from './intelligence/duplicate-detector.js';
|
|
31
31
|
export { generateLearningPath } from './intelligence/learning-path.js';
|
|
32
|
+
export { askVault } from './intelligence/ask-engine.js';
|
|
33
|
+
export { compileWiki, scanRawDirectory, extractConcepts } from './intelligence/wiki-compiler.js';
|
|
34
|
+
export { lintKnowledge } from './intelligence/knowledge-lint.js';
|
|
35
|
+
export { scanFrontmatter, generateNextIndexCode, assignIndexCodes, getInboxItems, archiveFile, checkAtomicity, detectOrphansAndBrokenLinks, } from './intelligence/zettelkasten.js';
|
|
36
|
+
export type { FrontmatterEntry } from './intelligence/zettelkasten.js';
|
|
37
|
+
export { ingest, ingestBatch, promoteNote } from './intelligence/ingest-pipeline.js';
|
|
38
|
+
export type { IngestInput, IngestResult, NoteStage } from './intelligence/ingest-pipeline.js';
|
|
39
|
+
export type { LintResult, LintIssue } from './intelligence/knowledge-lint.js';
|
|
40
|
+
export type { AskResult } from './intelligence/ask-engine.js';
|
|
32
41
|
export type { LearningPath, LearningItem, LearningPathInput } from './intelligence/learning-path.js';
|
|
33
42
|
export { checkNotifications } from './intelligence/notifications.js';
|
|
34
43
|
export type { Notification, NotificationConfig } from './intelligence/notifications.js';
|
|
@@ -22,6 +22,11 @@ export { detectContradictions } from './intelligence/contradiction-detector.js';
|
|
|
22
22
|
export { computeSemanticDrift, findMostDrifted, hashEmbedding } from './intelligence/semantic-versioning.js';
|
|
23
23
|
export { predictKnowledgeGaps } from './intelligence/predictive-gaps.js';
|
|
24
24
|
export { generateLearningPath } from './intelligence/learning-path.js';
|
|
25
|
+
export { askVault } from './intelligence/ask-engine.js';
|
|
26
|
+
export { compileWiki, scanRawDirectory, extractConcepts } from './intelligence/wiki-compiler.js';
|
|
27
|
+
export { lintKnowledge } from './intelligence/knowledge-lint.js';
|
|
28
|
+
export { scanFrontmatter, generateNextIndexCode, assignIndexCodes, getInboxItems, archiveFile, checkAtomicity, detectOrphansAndBrokenLinks, } from './intelligence/zettelkasten.js';
|
|
29
|
+
export { ingest, ingestBatch, promoteNote } from './intelligence/ingest-pipeline.js';
|
|
25
30
|
export { checkNotifications } from './intelligence/notifications.js';
|
|
26
31
|
// Multi-Vault
|
|
27
32
|
export { addVault, removeVault, listVaults, getVault, searchAllVaults } from './multi-vault/index.js';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { SearchEngine } from '../search/index.js';
|
|
2
|
+
export interface AskResult {
|
|
3
|
+
question: string;
|
|
4
|
+
answer: string;
|
|
5
|
+
sources: Array<{
|
|
6
|
+
title: string;
|
|
7
|
+
filePath: string;
|
|
8
|
+
score: number;
|
|
9
|
+
snippet: string;
|
|
10
|
+
}>;
|
|
11
|
+
savedTo: string | null;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* 질문에 대해 vault를 검색하고 구조화된 답변을 생성.
|
|
15
|
+
* LLM 없이 검색 결과를 구조화하는 버전 (LLM 연동은 MCP ask tool에서 처리).
|
|
16
|
+
*/
|
|
17
|
+
export declare function askVault(searchEngine: SearchEngine, question: string, options?: {
|
|
18
|
+
limit?: number;
|
|
19
|
+
save?: boolean;
|
|
20
|
+
vaultPath?: string;
|
|
21
|
+
outputDir?: string;
|
|
22
|
+
}): Promise<AskResult>;
|
|
23
|
+
//# sourceMappingURL=ask-engine.d.ts.map
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Feature: stellavault ask — Q&A + auto-filing
|
|
2
|
+
// vault 대상 자연어 질문 → 검색 → 답변 구조화 → vault에 .md 저장
|
|
3
|
+
import { writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
5
|
+
/**
|
|
6
|
+
* 질문에 대해 vault를 검색하고 구조화된 답변을 생성.
|
|
7
|
+
* LLM 없이 검색 결과를 구조화하는 버전 (LLM 연동은 MCP ask tool에서 처리).
|
|
8
|
+
*/
|
|
9
|
+
export async function askVault(searchEngine, question, options = {}) {
|
|
10
|
+
const { limit = 10, save = false, vaultPath, outputDir = '_stellavault/answers' } = options;
|
|
11
|
+
// 1. 검색
|
|
12
|
+
const results = await searchEngine.search({ query: question, limit });
|
|
13
|
+
// 2. 소스 정리
|
|
14
|
+
const sources = results.map((r) => ({
|
|
15
|
+
title: r.document.title,
|
|
16
|
+
filePath: r.document.filePath,
|
|
17
|
+
score: Math.round(r.score * 1000) / 1000,
|
|
18
|
+
snippet: r.chunk?.content?.substring(0, 200) ?? '',
|
|
19
|
+
}));
|
|
20
|
+
// 3. 답변 구성 (검색 결과 기반 구조화)
|
|
21
|
+
const answer = composeAnswer(question, results);
|
|
22
|
+
// 4. vault에 저장 (선택)
|
|
23
|
+
let savedTo = null;
|
|
24
|
+
if (save && vaultPath) {
|
|
25
|
+
savedTo = saveAnswerToVault(question, answer, sources, vaultPath, outputDir);
|
|
26
|
+
}
|
|
27
|
+
return { question, answer, sources, savedTo };
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 검색 결과를 구조화된 답변으로 구성.
|
|
31
|
+
* LLM 없이도 유용한 요약을 만듦.
|
|
32
|
+
*/
|
|
33
|
+
function composeAnswer(question, results) {
|
|
34
|
+
if (results.length === 0) {
|
|
35
|
+
return `No results found for "${question}". Try different keywords or create a note on this topic.`;
|
|
36
|
+
}
|
|
37
|
+
const lines = [];
|
|
38
|
+
lines.push(`## ${question}\n`);
|
|
39
|
+
// Related documents
|
|
40
|
+
lines.push(`### Related Documents (${results.length})\n`);
|
|
41
|
+
for (const r of results.slice(0, 5)) {
|
|
42
|
+
const score = Math.round(r.score * 100);
|
|
43
|
+
lines.push(`- **${r.document.title}** (${score}% 관련)`);
|
|
44
|
+
if (r.chunk?.content) {
|
|
45
|
+
const snippet = r.chunk.content.substring(0, 150).replace(/\n/g, ' ').trim();
|
|
46
|
+
lines.push(` > ${snippet}...`);
|
|
47
|
+
}
|
|
48
|
+
if (r.document.tags.length > 0) {
|
|
49
|
+
lines.push(` Tags: ${r.document.tags.map(t => `#${t}`).join(' ')}`);
|
|
50
|
+
}
|
|
51
|
+
lines.push('');
|
|
52
|
+
}
|
|
53
|
+
// Related tags
|
|
54
|
+
const allTags = new Set();
|
|
55
|
+
for (const r of results) {
|
|
56
|
+
r.document.tags.forEach((t) => allTags.add(t));
|
|
57
|
+
}
|
|
58
|
+
if (allTags.size > 0) {
|
|
59
|
+
lines.push(`### Related Tags`);
|
|
60
|
+
lines.push([...allTags].map(t => `#${t}`).join(' '));
|
|
61
|
+
lines.push('');
|
|
62
|
+
}
|
|
63
|
+
// Explore further
|
|
64
|
+
lines.push(`### Explore Further`);
|
|
65
|
+
lines.push(`- Dig deeper: \`stellavault ask "${question} advanced"\``);
|
|
66
|
+
lines.push(`- Find knowledge gaps: \`stellavault gaps\``);
|
|
67
|
+
return lines.join('\n');
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* 답변을 vault에 .md 파일로 저장.
|
|
71
|
+
* "_stellavault/answers/YYYY-MM-DD-question.md" 형식.
|
|
72
|
+
*/
|
|
73
|
+
function saveAnswerToVault(question, answer, sources, vaultPath, outputDir) {
|
|
74
|
+
const dir = resolve(vaultPath, outputDir);
|
|
75
|
+
if (!existsSync(dir)) {
|
|
76
|
+
mkdirSync(dir, { recursive: true });
|
|
77
|
+
}
|
|
78
|
+
const date = new Date().toISOString().split('T')[0];
|
|
79
|
+
const slug = question
|
|
80
|
+
.toLowerCase()
|
|
81
|
+
.replace(/[^a-z0-9가-힣\s]/g, '')
|
|
82
|
+
.replace(/\s+/g, '-')
|
|
83
|
+
.substring(0, 60);
|
|
84
|
+
const filename = `${date}-${slug}.md`;
|
|
85
|
+
const filePath = join(outputDir, filename);
|
|
86
|
+
const fullPath = resolve(vaultPath, filePath);
|
|
87
|
+
// vault 경로 내인지 확인 (path traversal 방지)
|
|
88
|
+
if (!fullPath.startsWith(resolve(vaultPath))) {
|
|
89
|
+
throw new Error('Invalid output path');
|
|
90
|
+
}
|
|
91
|
+
const content = [
|
|
92
|
+
'---',
|
|
93
|
+
`title: "${question}"`,
|
|
94
|
+
`date: ${new Date().toISOString()}`,
|
|
95
|
+
'type: answer',
|
|
96
|
+
'source: stellavault-ask',
|
|
97
|
+
`tags: [${sources.slice(0, 5).flatMap(s => s.title.split(/\s+/).slice(0, 2)).map(t => `"${t}"`).join(', ')}]`,
|
|
98
|
+
'---',
|
|
99
|
+
'',
|
|
100
|
+
answer,
|
|
101
|
+
'',
|
|
102
|
+
'---',
|
|
103
|
+
'*Generated by `stellavault ask`. Sources from your vault.*',
|
|
104
|
+
].join('\n');
|
|
105
|
+
writeFileSync(fullPath, content, 'utf-8');
|
|
106
|
+
return filePath;
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=ask-engine.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type NoteStage = 'fleeting' | 'literature' | 'permanent';
|
|
2
|
+
export interface IngestInput {
|
|
3
|
+
type: 'url' | 'text' | 'file' | 'youtube' | 'pdf-text';
|
|
4
|
+
content: string;
|
|
5
|
+
title?: string;
|
|
6
|
+
tags?: string[];
|
|
7
|
+
source?: string;
|
|
8
|
+
stage?: NoteStage;
|
|
9
|
+
}
|
|
10
|
+
export interface IngestResult {
|
|
11
|
+
savedTo: string;
|
|
12
|
+
stage: NoteStage;
|
|
13
|
+
title: string;
|
|
14
|
+
indexCode?: string;
|
|
15
|
+
tags: string[];
|
|
16
|
+
wordCount: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 어떤 입력이든 Stellavault 표준 포맷으로 변환하여 저장.
|
|
20
|
+
*/
|
|
21
|
+
export declare function ingest(vaultPath: string, input: IngestInput): IngestResult;
|
|
22
|
+
/**
|
|
23
|
+
* 여러 입력을 배치 처리.
|
|
24
|
+
*/
|
|
25
|
+
export declare function ingestBatch(vaultPath: string, inputs: IngestInput[]): IngestResult[];
|
|
26
|
+
/**
|
|
27
|
+
* 노트 승격: fleeting → literature → permanent.
|
|
28
|
+
* 내용이 충분히 정제되면 다음 단계로 이동.
|
|
29
|
+
*/
|
|
30
|
+
export declare function promoteNote(vaultPath: string, filePath: string, targetStage: NoteStage): string;
|
|
31
|
+
//# sourceMappingURL=ingest-pipeline.d.ts.map
|