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
@@ -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(/&nbsp;/g, ' ')
422
+ .replace(/&amp;/g, '&')
423
+ .replace(/&lt;/g, '<')
424
+ .replace(/&gt;/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,5 @@
1
+ export type NoteLocale = 'en' | 'ko' | 'ja' | 'zh';
2
+ export declare function setNoteLocale(locale: NoteLocale): void;
3
+ export declare function getNoteLocale(): NoteLocale;
4
+ export declare function nt(key: string): string;
5
+ //# sourceMappingURL=note-strings.d.ts.map
@@ -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