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.
Files changed (30) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +96 -151
  3. package/index.html +483 -0
  4. package/package.json +2 -2
  5. package/packages/cli/dist/commands/draft-cmd.d.ts +5 -0
  6. package/packages/cli/dist/commands/draft-cmd.js +102 -0
  7. package/packages/cli/dist/commands/flush-cmd.d.ts +2 -0
  8. package/packages/cli/dist/commands/flush-cmd.js +58 -0
  9. package/packages/cli/dist/commands/ingest-cmd.js +42 -12
  10. package/packages/cli/dist/commands/session-cmd.d.ts +7 -0
  11. package/packages/cli/dist/commands/session-cmd.js +95 -0
  12. package/packages/cli/dist/index.js +21 -0
  13. package/packages/core/dist/api/server.js +109 -0
  14. package/packages/core/dist/config.d.ts +8 -0
  15. package/packages/core/dist/config.js +9 -1
  16. package/packages/core/dist/index.d.ts +2 -2
  17. package/packages/core/dist/index.js +1 -1
  18. package/packages/core/dist/intelligence/auto-linker.d.ts +19 -0
  19. package/packages/core/dist/intelligence/auto-linker.js +122 -0
  20. package/packages/core/dist/intelligence/draft-generator.d.ts +19 -0
  21. package/packages/core/dist/intelligence/draft-generator.js +161 -0
  22. package/packages/core/dist/intelligence/file-extractors.d.ts +18 -0
  23. package/packages/core/dist/intelligence/file-extractors.js +127 -0
  24. package/packages/core/dist/intelligence/ingest-pipeline.d.ts +4 -3
  25. package/packages/core/dist/intelligence/ingest-pipeline.js +35 -12
  26. package/packages/core/dist/mcp/server.js +6 -0
  27. package/packages/core/dist/mcp/tools/generate-draft.d.ts +34 -0
  28. package/packages/core/dist/mcp/tools/generate-draft.js +120 -0
  29. package/packages/core/package.json +24 -1
  30. package/Autonomous_Knowledge_Foundry.pdf +0 -0
@@ -0,0 +1,58 @@
1
+ // stellavault flush — Daily Logs → Wiki 플러시 (카파시의 Flush Process)
2
+ // Raw daily logs에서 개념/연결 추출 → wiki 갱신
3
+ // "소스 코드를 실행 파일로 컴파일하듯, 원시 데이터를 구조화된 지식으로 변환"
4
+ import chalk from 'chalk';
5
+ import { loadConfig } from '@stellavault/core';
6
+ import { readdirSync, readFileSync, existsSync } from 'node:fs';
7
+ import { resolve, join } from 'node:path';
8
+ export async function flushCommand() {
9
+ const config = loadConfig();
10
+ if (!config.vaultPath) {
11
+ console.error(chalk.red('No vault configured. Run `stellavault init` first.'));
12
+ process.exit(1);
13
+ }
14
+ const folders = config.folders;
15
+ const logDir = resolve(config.vaultPath, folders.fleeting, '_daily-logs');
16
+ if (!existsSync(logDir)) {
17
+ console.log(chalk.yellow('No daily logs found. Use `stellavault session-save` or let Claude Code hooks capture sessions.'));
18
+ return;
19
+ }
20
+ // 1. Daily logs 수집
21
+ const logFiles = readdirSync(logDir).filter(f => f.startsWith('daily-log-') && f.endsWith('.md'));
22
+ if (logFiles.length === 0) {
23
+ console.log(chalk.yellow('No daily log files found.'));
24
+ return;
25
+ }
26
+ console.log(chalk.dim(`Found ${logFiles.length} daily logs`));
27
+ // 2. 모든 로그에서 세션 요약 추출
28
+ let totalSessions = 0;
29
+ const allContent = [];
30
+ for (const file of logFiles) {
31
+ const content = readFileSync(join(logDir, file), 'utf-8');
32
+ const sessions = content.split(/^## Session/m).slice(1);
33
+ totalSessions += sessions.length;
34
+ allContent.push(content);
35
+ }
36
+ console.log(chalk.dim(`Total sessions: ${totalSessions}`));
37
+ // 3. Compile: raw/ 전체 (daily-logs 포함) → wiki
38
+ try {
39
+ const { compileWiki } = await import('@stellavault/core/intelligence/wiki-compiler');
40
+ const rawDir = resolve(config.vaultPath, folders.fleeting);
41
+ const wikiDir = resolve(config.vaultPath, folders.wiki);
42
+ const result = compileWiki(rawDir, wikiDir);
43
+ console.log(chalk.green(`Flush complete!`));
44
+ console.log(chalk.dim(` Daily logs: ${logFiles.length} files, ${totalSessions} sessions`));
45
+ console.log(chalk.dim(` Wiki articles: ${result.wikiArticles.length}`));
46
+ console.log(chalk.dim(` Concepts extracted: ${result.concepts.length}`));
47
+ if (result.concepts.length > 0) {
48
+ console.log(chalk.dim(` Top concepts: ${result.concepts.slice(0, 8).join(', ')}`));
49
+ }
50
+ console.log(chalk.dim(` Index: ${result.indexFile}`));
51
+ }
52
+ catch (err) {
53
+ console.error(chalk.red(`Flush failed: ${err instanceof Error ? err.message : 'unknown'}`));
54
+ process.exit(1);
55
+ }
56
+ console.log(chalk.dim(' Tip: Run `stellavault lint` to check knowledge health'));
57
+ }
58
+ //# sourceMappingURL=flush-cmd.js.map
@@ -2,7 +2,7 @@
2
2
  import chalk from 'chalk';
3
3
  import { loadConfig, ingest, promoteNote } from '@stellavault/core';
4
4
  import { readFileSync, existsSync } from 'node:fs';
5
- import { extname } from 'node:path';
5
+ import { extname, resolve } from 'node:path';
6
6
  export async function ingestCommand(input, options) {
7
7
  if (!input) {
8
8
  console.error(chalk.yellow('Usage: stellavault ingest <url|file|text> [--tags t1,t2] [--stage fleeting|literature|permanent]'));
@@ -85,15 +85,46 @@ export async function ingestCommand(input, options) {
85
85
  else if (existsSync(input)) {
86
86
  // 파일
87
87
  const ext = extname(input).toLowerCase();
88
- const fileContent = readFileSync(input, 'utf-8');
89
- ingestInput = {
90
- type: ext === '.pdf' ? 'pdf-text' : 'file',
91
- content: fileContent,
92
- tags,
93
- stage,
94
- title: options.title,
95
- source: input,
96
- };
88
+ const binaryExts = new Set(['.pdf', '.docx', '.pptx', '.xlsx', '.xls']);
89
+ if (binaryExts.has(ext)) {
90
+ // 바이너리 파일: 전용 파서로 텍스트 추출
91
+ try {
92
+ const { extractFileContent } = await import('@stellavault/core/intelligence/file-extractors');
93
+ const extracted = await extractFileContent(resolve(input));
94
+ console.log(chalk.dim(` Extracted ${extracted.metadata.wordCount} words from ${ext} file`));
95
+ ingestInput = {
96
+ type: extracted.sourceFormat,
97
+ content: extracted.text,
98
+ tags: [...tags, extracted.sourceFormat],
99
+ stage, // 제텔카스텐: 모든 인풋은 fleeting에서 시작
100
+ title: options.title ?? extracted.metadata.title,
101
+ source: input,
102
+ };
103
+ }
104
+ catch (err) {
105
+ console.error(chalk.yellow(`Binary file extraction failed, saving as-is. (${err instanceof Error ? err.message : 'error'})`));
106
+ ingestInput = {
107
+ type: 'file',
108
+ content: readFileSync(input, 'utf-8'),
109
+ tags,
110
+ stage,
111
+ title: options.title,
112
+ source: input,
113
+ };
114
+ }
115
+ }
116
+ else {
117
+ // 텍스트 파일: 기존 동작
118
+ const fileContent = readFileSync(input, 'utf-8');
119
+ ingestInput = {
120
+ type: 'file',
121
+ content: fileContent,
122
+ tags,
123
+ stage,
124
+ title: options.title,
125
+ source: input,
126
+ };
127
+ }
97
128
  }
98
129
  else {
99
130
  // 플레인 텍스트
@@ -114,9 +145,8 @@ export async function ingestCommand(input, options) {
114
145
  console.log(chalk.dim(` Index: ${result.indexCode}`));
115
146
  if (result.tags.length > 0)
116
147
  console.log(chalk.dim(` Tags: ${result.tags.join(', ')}`));
148
+ console.log(chalk.dim(' Wiki: auto-compiled'));
117
149
  console.log('');
118
- console.log(chalk.dim('Run `stellavault compile` to process into wiki.'));
119
- console.log(chalk.dim('Run `stellavault autopilot` for full pipeline.'));
120
150
  }
121
151
  export async function promoteCommand(filePath, options) {
122
152
  const config = loadConfig();
@@ -0,0 +1,7 @@
1
+ export declare function sessionSaveCommand(options: {
2
+ summary?: string;
3
+ decisions?: string;
4
+ lessons?: string;
5
+ actions?: string;
6
+ }): Promise<void>;
7
+ //# sourceMappingURL=session-cmd.d.ts.map
@@ -0,0 +1,95 @@
1
+ // stellavault session-save — 세션 요약을 daily log로 자동 저장
2
+ // 카파시 아키텍처: 세션 종료 시 대화의 핵심 결정/교훈/실행항목을 캡처
3
+ // Claude Code hooks의 pre-compact/session-end에서 호출됨
4
+ import chalk from 'chalk';
5
+ import { loadConfig } from '@stellavault/core';
6
+ import { writeFileSync, mkdirSync, existsSync, appendFileSync } from 'node:fs';
7
+ import { resolve, join } from 'node:path';
8
+ export async function sessionSaveCommand(options) {
9
+ const config = loadConfig();
10
+ if (!config.vaultPath) {
11
+ console.error(chalk.red('No vault configured. Run `stellavault init` first.'));
12
+ process.exit(1);
13
+ }
14
+ const folders = config.folders;
15
+ const logDir = resolve(config.vaultPath, folders.fleeting, '_daily-logs');
16
+ if (!existsSync(logDir))
17
+ mkdirSync(logDir, { recursive: true });
18
+ const now = new Date();
19
+ const dateStr = now.toISOString().split('T')[0];
20
+ const timeStr = now.toTimeString().split(' ')[0];
21
+ const logFile = join(logDir, `daily-log-${dateStr}.md`);
22
+ // stdin에서 요약 읽기 (Claude Code hook에서 파이프로 전달)
23
+ let summary = options.summary ?? '';
24
+ if (!summary && !process.stdin.isTTY) {
25
+ const chunks = [];
26
+ for await (const chunk of process.stdin) {
27
+ chunks.push(chunk);
28
+ }
29
+ summary = Buffer.concat(chunks).toString('utf-8').trim();
30
+ }
31
+ if (!summary) {
32
+ // 대화형: 직접 입력
33
+ console.log(chalk.dim('Enter session summary (Ctrl+D to finish):'));
34
+ const chunks = [];
35
+ for await (const chunk of process.stdin) {
36
+ chunks.push(chunk);
37
+ }
38
+ summary = Buffer.concat(chunks).toString('utf-8').trim();
39
+ }
40
+ if (!summary) {
41
+ console.error(chalk.yellow('No summary provided. Skipping.'));
42
+ return;
43
+ }
44
+ // daily log 엔트리 구성
45
+ const entry = [
46
+ '',
47
+ `## Session — ${timeStr}`,
48
+ '',
49
+ '### Summary',
50
+ summary,
51
+ '',
52
+ ];
53
+ if (options.decisions) {
54
+ entry.push('### Decisions', options.decisions, '');
55
+ }
56
+ if (options.lessons) {
57
+ entry.push('### Lessons Learned', options.lessons, '');
58
+ }
59
+ if (options.actions) {
60
+ entry.push('### Action Items', options.actions, '');
61
+ }
62
+ entry.push('---', '');
63
+ // 파일이 없으면 헤더 생성, 있으면 append
64
+ if (!existsSync(logFile)) {
65
+ const header = [
66
+ '---',
67
+ `title: "Daily Log — ${dateStr}"`,
68
+ 'type: fleeting',
69
+ 'tags: ["daily-log", "session"]',
70
+ `created: ${now.toISOString()}`,
71
+ '---',
72
+ '',
73
+ `# Daily Log — ${dateStr}`,
74
+ '',
75
+ ].join('\n');
76
+ writeFileSync(logFile, header + entry.join('\n'), 'utf-8');
77
+ }
78
+ else {
79
+ appendFileSync(logFile, entry.join('\n'), 'utf-8');
80
+ }
81
+ console.log(chalk.green(`Session saved to daily log: ${dateStr}`));
82
+ console.log(chalk.dim(` File: ${logFile}`));
83
+ console.log(chalk.dim(` Time: ${timeStr}`));
84
+ console.log(chalk.dim(` Words: ${summary.split(/\s+/).length}`));
85
+ // 자동 compile 트리거
86
+ try {
87
+ const { compileWiki } = await import('@stellavault/core/intelligence/wiki-compiler');
88
+ const rawDir = resolve(config.vaultPath, folders.fleeting);
89
+ const wikiDir = resolve(config.vaultPath, folders.wiki);
90
+ compileWiki(rawDir, wikiDir);
91
+ console.log(chalk.dim(' Wiki: auto-compiled'));
92
+ }
93
+ catch { /* compile 실패해도 저장은 성공 */ }
94
+ }
95
+ //# sourceMappingURL=session-cmd.js.map
@@ -23,6 +23,9 @@ import { vaultAddCommand, vaultListCommand, vaultRemoveCommand, vaultSearchAllCo
23
23
  import { captureCommand } from './commands/capture-cmd.js';
24
24
  import { askCommand } from './commands/ask-cmd.js';
25
25
  import { compileCommand } from './commands/compile-cmd.js';
26
+ import { draftCommand } from './commands/draft-cmd.js';
27
+ import { sessionSaveCommand } from './commands/session-cmd.js';
28
+ import { flushCommand } from './commands/flush-cmd.js';
26
29
  import { lintCommand } from './commands/lint-cmd.js';
27
30
  import { fleetingCommand } from './commands/fleeting-cmd.js';
28
31
  import { ingestCommand, promoteCommand } from './commands/ingest-cmd.js';
@@ -139,6 +142,24 @@ program
139
142
  .option('-w, --wiki <dir>', 'Wiki output directory (default: _wiki/)')
140
143
  .option('-f, --force', 'Overwrite existing wiki files')
141
144
  .action((opts) => compileCommand(opts));
145
+ program
146
+ .command('draft [topic]')
147
+ .description('Express: Generate a blog post, report, or outline draft from your knowledge')
148
+ .option('--format <type>', 'Output format: blog, report, outline (default: blog)')
149
+ .option('--ai', 'Use Claude API for AI-enhanced draft (requires ANTHROPIC_API_KEY)')
150
+ .action((topic, opts) => draftCommand(topic, opts));
151
+ program
152
+ .command('session-save')
153
+ .description('Save session summary to daily log (used by Claude Code hooks)')
154
+ .option('-s, --summary <text>', 'Session summary text (or pipe via stdin)')
155
+ .option('-d, --decisions <text>', 'Key decisions made')
156
+ .option('-l, --lessons <text>', 'Lessons learned')
157
+ .option('-a, --actions <text>', 'Action items')
158
+ .action((opts) => sessionSaveCommand(opts));
159
+ program
160
+ .command('flush')
161
+ .description('Flush daily logs → wiki: extract concepts, rebuild connections (Karpathy compile)')
162
+ .action(() => flushCommand());
142
163
  program
143
164
  .command('lint')
144
165
  .description('Knowledge health check — find gaps, duplicates, contradictions, stale notes')
@@ -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: 'all-MiniLM-L6-v2',
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
+ /**
3
+ * vault의 모든 .md 노트 제목을 수집.
4
+ * frontmatter title 또는 첫 heading 또는 파일명.
5
+ */
6
+ export declare function collectVaultTitles(vaultPath: string, folders?: FolderNames): string[];
7
+ /**
8
+ * 노트 본문에서 기존 노트 제목/핵심 구문과 매칭하여 [[wikilink]]를 삽입.
9
+ * - 이미 [[...]]로 감싸진 부분은 건드리지 않음
10
+ * - frontmatter 영역은 건드리지 않음
11
+ * - 자기 자신의 제목은 건드리지 않음
12
+ * - heading(#) 줄은 건드리지 않음
13
+ */
14
+ export declare function insertWikilinks(body: string, vaultTitles: string[], selfTitle?: string): string;
15
+ /**
16
+ * vault 스캔 + wikilink 삽입을 한 번에 수행하는 편의 함수.
17
+ */
18
+ export declare function autoLink(body: string, vaultPath: string, selfTitle?: string, folders?: FolderNames): string;
19
+ //# sourceMappingURL=auto-linker.d.ts.map
@@ -0,0 +1,122 @@
1
+ // Auto-Linker: 노트 본문에서 기존 노트 제목과 매칭하여 [[wikilink]] 자동 삽입
2
+ // 제텔카스텐 핵심: 노트 간 연결이 지식 네트워크를 만든다
3
+ import { readdirSync, readFileSync, existsSync } from 'node:fs';
4
+ import { join, resolve, extname } from 'node:path';
5
+ import { DEFAULT_FOLDERS } from '../config.js';
6
+ /**
7
+ * vault의 모든 .md 노트 제목을 수집.
8
+ * frontmatter title 또는 첫 heading 또는 파일명.
9
+ */
10
+ export function collectVaultTitles(vaultPath, folders = DEFAULT_FOLDERS) {
11
+ const titles = new Set();
12
+ const dirs = [folders.fleeting, folders.literature, folders.permanent, folders.wiki, '_drafts'];
13
+ for (const dir of dirs) {
14
+ const fullDir = resolve(vaultPath, dir);
15
+ if (!existsSync(fullDir))
16
+ continue;
17
+ try {
18
+ const files = readdirSync(fullDir).filter(f => extname(f) === '.md');
19
+ for (const file of files) {
20
+ try {
21
+ const content = readFileSync(join(fullDir, file), 'utf-8');
22
+ // frontmatter title
23
+ const titleMatch = content.match(/^title:\s*"?([^"\n]+)"?\s*$/m);
24
+ if (titleMatch && titleMatch[1].length > 2) {
25
+ titles.add(titleMatch[1].trim());
26
+ continue;
27
+ }
28
+ // first heading
29
+ const headingMatch = content.match(/^#\s+(.+)$/m);
30
+ if (headingMatch && headingMatch[1].length > 2) {
31
+ titles.add(headingMatch[1].trim());
32
+ continue;
33
+ }
34
+ // filename without extension + timestamp prefix
35
+ const name = file.replace(/^\d{4}-\d{2}-\d{2}T[\d-]+-/, '').replace(/\.md$/, '').replace(/-/g, ' ');
36
+ if (name.length > 2)
37
+ titles.add(name);
38
+ }
39
+ catch { /* skip unreadable files */ }
40
+ }
41
+ }
42
+ catch { /* dir not readable */ }
43
+ }
44
+ return [...titles].filter(t => t.length > 3); // ignore very short titles
45
+ }
46
+ /**
47
+ * 제목에서 링크 가능한 핵심 구문(phrases)을 추출.
48
+ * 예: "Steve Jobs' 2005 Stanford Commencement Address"
49
+ * → ["Steve Jobs", "Stanford Commencement Address", "Stanford"]
50
+ */
51
+ function extractLinkablePhrases(title) {
52
+ const phrases = [];
53
+ // 전체 제목 (짧으면)
54
+ if (title.length <= 40)
55
+ phrases.push(title);
56
+ // 고유명사/핵심 구문 추출: 대문자로 시작하는 연속 단어
57
+ const properNouns = title.match(/[A-Z\uAC00-\uD7A3][a-z\uAC00-\uD7A3]+(?:\s+[A-Z\uAC00-\uD7A3][a-z\uAC00-\uD7A3]+)*/g);
58
+ if (properNouns) {
59
+ for (const pn of properNouns) {
60
+ if (pn.length > 4)
61
+ phrases.push(pn);
62
+ }
63
+ }
64
+ // 한국어: 긴 제목에서 의미 있는 부분 (4글자 이상 한글 연속)
65
+ const koreanPhrases = title.match(/[\uAC00-\uD7A3]{4,}/g);
66
+ if (koreanPhrases)
67
+ phrases.push(...koreanPhrases);
68
+ return [...new Set(phrases)].filter(p => p.length > 3);
69
+ }
70
+ /**
71
+ * 노트 본문에서 기존 노트 제목/핵심 구문과 매칭하여 [[wikilink]]를 삽입.
72
+ * - 이미 [[...]]로 감싸진 부분은 건드리지 않음
73
+ * - frontmatter 영역은 건드리지 않음
74
+ * - 자기 자신의 제목은 건드리지 않음
75
+ * - heading(#) 줄은 건드리지 않음
76
+ */
77
+ export function insertWikilinks(body, vaultTitles, selfTitle) {
78
+ if (vaultTitles.length === 0)
79
+ return body;
80
+ // frontmatter 분리
81
+ const fmMatch = body.match(/^(---[\s\S]*?---\n?)/);
82
+ const frontmatter = fmMatch ? fmMatch[1] : '';
83
+ let content = fmMatch ? body.slice(frontmatter.length) : body;
84
+ // 제목 → 링크 가능 구문 매핑
85
+ const phraseToTitle = new Map();
86
+ for (const title of vaultTitles) {
87
+ if (title === selfTitle)
88
+ continue;
89
+ for (const phrase of extractLinkablePhrases(title)) {
90
+ if (!phraseToTitle.has(phrase)) {
91
+ phraseToTitle.set(phrase, title);
92
+ }
93
+ }
94
+ }
95
+ // 긴 구문부터 매칭 (짧은 구문이 긴 구문의 부분 매칭되는 것 방지)
96
+ const sortedPhrases = [...phraseToTitle.keys()].sort((a, b) => b.length - a.length);
97
+ const linkedPhrases = new Set(); // 이미 링크된 구문 (중복 방지)
98
+ for (const phrase of sortedPhrases) {
99
+ const targetTitle = phraseToTitle.get(phrase);
100
+ if (linkedPhrases.has(targetTitle))
101
+ continue; // 같은 노트에 여러 구문 매칭 방지
102
+ const escaped = phrase.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
103
+ const regex = new RegExp(`(?<!\\[\\[)(?<!#\\s)\\b(${escaped})\\b(?!\\]\\])(?![^\\[]*\\]\\])`, 'gi');
104
+ let replaced = false;
105
+ content = content.replace(regex, (match) => {
106
+ if (replaced)
107
+ return match;
108
+ replaced = true;
109
+ linkedPhrases.add(targetTitle);
110
+ return `[[${targetTitle}|${match}]]`;
111
+ });
112
+ }
113
+ return frontmatter + content;
114
+ }
115
+ /**
116
+ * vault 스캔 + wikilink 삽입을 한 번에 수행하는 편의 함수.
117
+ */
118
+ export function autoLink(body, vaultPath, selfTitle, folders = DEFAULT_FOLDERS) {
119
+ const titles = collectVaultTitles(vaultPath, folders);
120
+ return insertWikilinks(body, titles, selfTitle);
121
+ }
122
+ //# sourceMappingURL=auto-linker.js.map
@@ -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