stellavault 0.4.3 → 0.4.4
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/package.json +1 -1
- package/packages/cli/dist/commands/adr-cmd.d.ts +7 -0
- package/packages/cli/dist/commands/adr-cmd.js +45 -0
- package/packages/cli/dist/commands/ask-cmd.d.ts +1 -0
- package/packages/cli/dist/commands/ask-cmd.js +1 -0
- package/packages/cli/dist/commands/draft-cmd.d.ts +1 -0
- package/packages/cli/dist/commands/draft-cmd.js +9 -1
- package/packages/cli/dist/commands/ingest-cmd.js +15 -4
- package/packages/cli/dist/index.js +13 -2
- package/packages/core/dist/intelligence/ask-engine.d.ts +1 -0
- package/packages/core/dist/intelligence/ask-engine.js +36 -2
- package/packages/core/dist/intelligence/auto-linker.js +5 -1
- package/packages/core/dist/intelligence/draft-generator.d.ts +6 -0
- package/packages/core/dist/intelligence/draft-generator.js +47 -21
- package/packages/core/dist/intelligence/file-extractors.d.ts +0 -5
- package/packages/core/dist/intelligence/file-extractors.js +35 -7
- package/packages/core/dist/intelligence/ingest-pipeline.js +8 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stellavault",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"description": "Self-compiling knowledge MCP server — ingest anything, auto-organize into Zettelkasten wiki, search with AI, and let Claude access your entire knowledge base.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// stellavault adr — Architecture Decision Record 구조화 인제스트
|
|
2
|
+
// 의사결정 기록: 제목/맥락/옵션/결정/결과
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { loadConfig, ingest } from '@stellavault/core';
|
|
5
|
+
export async function adrCommand(title, options) {
|
|
6
|
+
if (!title) {
|
|
7
|
+
console.error(chalk.yellow('Usage: stellavault adr "Decision Title" --context "..." --options "..." --decision "..." --consequences "..."'));
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
const config = loadConfig();
|
|
11
|
+
const now = new Date().toISOString().split('T')[0];
|
|
12
|
+
const content = [
|
|
13
|
+
`# ADR: ${title}`,
|
|
14
|
+
'',
|
|
15
|
+
`**Date:** ${now}`,
|
|
16
|
+
`**Status:** Accepted`,
|
|
17
|
+
'',
|
|
18
|
+
'## Context',
|
|
19
|
+
options.context ?? '<!-- Why is this decision needed? -->',
|
|
20
|
+
'',
|
|
21
|
+
'## Options Considered',
|
|
22
|
+
options.options ?? '<!-- What alternatives were evaluated? -->',
|
|
23
|
+
'',
|
|
24
|
+
'## Decision',
|
|
25
|
+
options.decision ?? '<!-- What was decided and why? -->',
|
|
26
|
+
'',
|
|
27
|
+
'## Consequences',
|
|
28
|
+
options.consequences ?? '<!-- What are the implications? -->',
|
|
29
|
+
'',
|
|
30
|
+
].join('\n');
|
|
31
|
+
const result = ingest(config.vaultPath, {
|
|
32
|
+
type: 'text',
|
|
33
|
+
content,
|
|
34
|
+
tags: ['adr', 'decision'],
|
|
35
|
+
title: `ADR: ${title}`,
|
|
36
|
+
stage: 'literature',
|
|
37
|
+
}, config.folders);
|
|
38
|
+
console.log(chalk.green(`ADR created: ${title}`));
|
|
39
|
+
console.log(chalk.dim(` Saved: ${result.savedTo}`));
|
|
40
|
+
console.log(chalk.dim(` Stage: literature`));
|
|
41
|
+
console.log(chalk.dim(` Tags: adr, decision`));
|
|
42
|
+
console.log('');
|
|
43
|
+
console.log(chalk.dim(`Find later: stellavault ask "why did we choose ${title}?"`));
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=adr-cmd.js.map
|
|
@@ -13,7 +13,15 @@ export async function draftCommand(topic, options) {
|
|
|
13
13
|
const format = (options.format ?? 'blog');
|
|
14
14
|
try {
|
|
15
15
|
// Step 1: rule-based 초안 생성 (스캐폴딩 + 소스 수집)
|
|
16
|
-
|
|
16
|
+
// Blueprint 파싱: "Chapter 1:tag1,tag2; Chapter 2:tag3" 형식
|
|
17
|
+
let blueprint;
|
|
18
|
+
if (options.blueprint) {
|
|
19
|
+
blueprint = options.blueprint.split(';').map(s => {
|
|
20
|
+
const [title, tagsStr] = s.split(':').map(p => p.trim());
|
|
21
|
+
return { title, tags: tagsStr?.split(',').map(t => t.trim()) };
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
const result = generateDraft(config.vaultPath, { topic, format, blueprint }, config.folders);
|
|
17
25
|
if (options.ai) {
|
|
18
26
|
// Step 2: --ai 모드 → Claude API로 실제 글 생성
|
|
19
27
|
console.log(chalk.dim(' AI mode: generating with Claude...'));
|
|
@@ -17,18 +17,29 @@ export async function ingestCommand(input, options) {
|
|
|
17
17
|
console.error(chalk.yellow(`No supported files found in ${input}`));
|
|
18
18
|
process.exit(1);
|
|
19
19
|
}
|
|
20
|
-
console.log(chalk.dim(`Batch ingest: ${files.length} files from ${input}`));
|
|
20
|
+
console.log(chalk.dim(`Batch ingest: ${files.length} files from ${input}\n`));
|
|
21
21
|
let success = 0;
|
|
22
|
-
|
|
22
|
+
const failed = [];
|
|
23
|
+
for (let i = 0; i < files.length; i++) {
|
|
24
|
+
const file = files[i];
|
|
25
|
+
const name = file.split(/[/\\]/).pop() ?? file;
|
|
26
|
+
const progress = `[${i + 1}/${files.length}]`;
|
|
27
|
+
process.stderr.write(`\r${chalk.dim(progress)} ${name}...`);
|
|
23
28
|
try {
|
|
24
29
|
await ingestCommand(file, { ...options, title: undefined });
|
|
25
30
|
success++;
|
|
26
31
|
}
|
|
27
32
|
catch (err) {
|
|
28
|
-
|
|
33
|
+
failed.push(`${name}: ${err instanceof Error ? err.message : 'error'}`);
|
|
29
34
|
}
|
|
30
35
|
}
|
|
31
|
-
|
|
36
|
+
process.stderr.write('\r' + ' '.repeat(80) + '\r');
|
|
37
|
+
console.log(chalk.green(`Batch complete: ${success}/${files.length} files ingested`));
|
|
38
|
+
if (failed.length > 0) {
|
|
39
|
+
console.log(chalk.yellow(`\nFailed (${failed.length}):`));
|
|
40
|
+
for (const f of failed)
|
|
41
|
+
console.log(chalk.yellow(` - ${f}`));
|
|
42
|
+
}
|
|
32
43
|
return;
|
|
33
44
|
}
|
|
34
45
|
const config = loadConfig();
|
|
@@ -26,6 +26,7 @@ import { compileCommand } from './commands/compile-cmd.js';
|
|
|
26
26
|
import { draftCommand } from './commands/draft-cmd.js';
|
|
27
27
|
import { sessionSaveCommand } from './commands/session-cmd.js';
|
|
28
28
|
import { flushCommand } from './commands/flush-cmd.js';
|
|
29
|
+
import { adrCommand } from './commands/adr-cmd.js';
|
|
29
30
|
import { lintCommand } from './commands/lint-cmd.js';
|
|
30
31
|
import { fleetingCommand } from './commands/fleeting-cmd.js';
|
|
31
32
|
import { ingestCommand, promoteCommand } from './commands/ingest-cmd.js';
|
|
@@ -134,6 +135,7 @@ program
|
|
|
134
135
|
.command('ask <question>')
|
|
135
136
|
.description('Ask a question about your knowledge base — search, compose answer, optionally save')
|
|
136
137
|
.option('-s, --save', 'Save answer as a new note in your vault')
|
|
138
|
+
.option('-q, --quotes', 'Show direct quotes from sources (Insight Extraction mode)')
|
|
137
139
|
.action((question, opts) => askCommand(question, opts));
|
|
138
140
|
program
|
|
139
141
|
.command('compile')
|
|
@@ -144,9 +146,10 @@ program
|
|
|
144
146
|
.action((opts) => compileCommand(opts));
|
|
145
147
|
program
|
|
146
148
|
.command('draft [topic]')
|
|
147
|
-
.description('Express: Generate
|
|
148
|
-
.option('--format <type>', 'Output format: blog, report, outline
|
|
149
|
+
.description('Express: Generate draft from knowledge (blog/report/outline/instagram/thread/script)')
|
|
150
|
+
.option('--format <type>', 'Output format: blog, report, outline, instagram, thread, script')
|
|
149
151
|
.option('--ai', 'Use Claude API for AI-enhanced draft (requires ANTHROPIC_API_KEY)')
|
|
152
|
+
.option('--blueprint <spec>', 'Chapter structure: "Ch1:tag1,tag2; Ch2:tag3"')
|
|
150
153
|
.action((topic, opts) => draftCommand(topic, opts));
|
|
151
154
|
program
|
|
152
155
|
.command('session-save')
|
|
@@ -160,6 +163,14 @@ program
|
|
|
160
163
|
.command('flush')
|
|
161
164
|
.description('Flush daily logs → wiki: extract concepts, rebuild connections (Karpathy compile)')
|
|
162
165
|
.action(() => flushCommand());
|
|
166
|
+
program
|
|
167
|
+
.command('adr <title>')
|
|
168
|
+
.description('Create an Architecture Decision Record (structured decision log)')
|
|
169
|
+
.option('--context <text>', 'Why is this decision needed?')
|
|
170
|
+
.option('--options <text>', 'What alternatives were considered?')
|
|
171
|
+
.option('--decision <text>', 'What was decided and why?')
|
|
172
|
+
.option('--consequences <text>', 'What are the implications?')
|
|
173
|
+
.action((title, opts) => adrCommand(title, opts));
|
|
163
174
|
program
|
|
164
175
|
.command('lint')
|
|
165
176
|
.description('Knowledge health check — find gaps, duplicates, contradictions, stale notes')
|
|
@@ -7,7 +7,7 @@ import { join, resolve } from 'node:path';
|
|
|
7
7
|
* LLM 없이 검색 결과를 구조화하는 버전 (LLM 연동은 MCP ask tool에서 처리).
|
|
8
8
|
*/
|
|
9
9
|
export async function askVault(searchEngine, question, options = {}) {
|
|
10
|
-
const { limit = 10, save = false, vaultPath, outputDir = '_stellavault/answers' } = options;
|
|
10
|
+
const { limit = 10, save = false, vaultPath, outputDir = '_stellavault/answers', mode = 'default' } = options;
|
|
11
11
|
// 1. 검색
|
|
12
12
|
const results = await searchEngine.search({ query: question, limit });
|
|
13
13
|
// 2. 소스 정리
|
|
@@ -18,7 +18,9 @@ export async function askVault(searchEngine, question, options = {}) {
|
|
|
18
18
|
snippet: r.chunk?.content?.substring(0, 200) ?? '',
|
|
19
19
|
}));
|
|
20
20
|
// 3. 답변 구성 (검색 결과 기반 구조화)
|
|
21
|
-
const answer =
|
|
21
|
+
const answer = mode === 'quotes'
|
|
22
|
+
? composeQuotes(question, results)
|
|
23
|
+
: composeAnswer(question, results);
|
|
22
24
|
// 4. vault에 저장 (선택)
|
|
23
25
|
let savedTo = null;
|
|
24
26
|
if (save && vaultPath) {
|
|
@@ -66,6 +68,38 @@ function composeAnswer(question, results) {
|
|
|
66
68
|
lines.push(`- Find knowledge gaps: \`stellavault gaps\``);
|
|
67
69
|
return lines.join('\n');
|
|
68
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Quotes 모드: 원문 인용을 카드 형태로 나열.
|
|
73
|
+
*/
|
|
74
|
+
function composeQuotes(question, results) {
|
|
75
|
+
if (results.length === 0) {
|
|
76
|
+
return `No quotes found for "${question}".`;
|
|
77
|
+
}
|
|
78
|
+
const lines = [];
|
|
79
|
+
lines.push(`## Quotes: "${question}"\n`);
|
|
80
|
+
lines.push(`*${results.length} sources found. Each quote is a direct excerpt.*\n`);
|
|
81
|
+
for (let i = 0; i < Math.min(8, results.length); i++) {
|
|
82
|
+
const r = results[i];
|
|
83
|
+
const quote = r.chunk?.content
|
|
84
|
+
?.replace(/^---[\s\S]*?---\n?/, '')
|
|
85
|
+
?.replace(/^#+\s+.+\n/m, '')
|
|
86
|
+
?.trim()
|
|
87
|
+
?.substring(0, 300) ?? '';
|
|
88
|
+
if (!quote)
|
|
89
|
+
continue;
|
|
90
|
+
const score = Math.round(r.score * 100);
|
|
91
|
+
lines.push(`---`);
|
|
92
|
+
lines.push(`**[${i + 1}] ${r.document.title}** (${score}% match)`);
|
|
93
|
+
lines.push(`> ${quote.replace(/\n/g, '\n> ')}`);
|
|
94
|
+
if (r.document.tags.length > 0) {
|
|
95
|
+
lines.push(`Tags: ${r.document.tags.slice(0, 5).map(t => `#${t}`).join(' ')}`);
|
|
96
|
+
}
|
|
97
|
+
lines.push('');
|
|
98
|
+
}
|
|
99
|
+
lines.push('---');
|
|
100
|
+
lines.push(`*Use \`stellavault ask "${question}" --save\` to save these quotes as a note.*`);
|
|
101
|
+
return lines.join('\n');
|
|
102
|
+
}
|
|
69
103
|
/**
|
|
70
104
|
* 답변을 vault에 .md 파일로 저장.
|
|
71
105
|
* "_stellavault/answers/YYYY-MM-DD-question.md" 형식.
|
|
@@ -95,10 +95,14 @@ export function insertWikilinks(body, vaultTitles, selfTitle) {
|
|
|
95
95
|
// 긴 구문부터 매칭 (짧은 구문이 긴 구문의 부분 매칭되는 것 방지)
|
|
96
96
|
const sortedPhrases = [...phraseToTitle.keys()].sort((a, b) => b.length - a.length);
|
|
97
97
|
const linkedPhrases = new Set(); // 이미 링크된 구문 (중복 방지)
|
|
98
|
+
const contentLower = content.toLowerCase();
|
|
98
99
|
for (const phrase of sortedPhrases) {
|
|
99
100
|
const targetTitle = phraseToTitle.get(phrase);
|
|
100
101
|
if (linkedPhrases.has(targetTitle))
|
|
101
|
-
continue;
|
|
102
|
+
continue;
|
|
103
|
+
// 사전 필터: 본문에 포함되지 않으면 regex 생성 스킵 (성능 최적화)
|
|
104
|
+
if (!contentLower.includes(phrase.toLowerCase()))
|
|
105
|
+
continue;
|
|
102
106
|
const escaped = phrase.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
103
107
|
const regex = new RegExp(`(?<!\\[\\[)(?<!#\\s)\\b(${escaped})\\b(?!\\]\\])(?![^\\[]*\\]\\])`, 'gi');
|
|
104
108
|
let replaced = false;
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { type FolderNames } from '../config.js';
|
|
2
|
+
export interface BlueprintSection {
|
|
3
|
+
title: string;
|
|
4
|
+
tags?: string[];
|
|
5
|
+
description?: string;
|
|
6
|
+
}
|
|
2
7
|
export interface DraftOptions {
|
|
3
8
|
topic?: string;
|
|
4
9
|
format?: 'blog' | 'report' | 'outline' | 'instagram' | 'thread' | 'script';
|
|
5
10
|
maxSections?: number;
|
|
11
|
+
blueprint?: BlueprintSection[];
|
|
6
12
|
}
|
|
7
13
|
export interface DraftResult {
|
|
8
14
|
title: string;
|
|
@@ -10,7 +10,7 @@ import { DEFAULT_FOLDERS } from '../config.js';
|
|
|
10
10
|
* Express 단계: 지식이 vault에서 나가는 출구.
|
|
11
11
|
*/
|
|
12
12
|
export function generateDraft(vaultPath, options = {}, folders = DEFAULT_FOLDERS) {
|
|
13
|
-
const { topic, format = 'blog', maxSections = 8 } = options;
|
|
13
|
+
const { topic, format = 'blog', maxSections = 8, blueprint } = options;
|
|
14
14
|
// raw + wiki 문서 스캔
|
|
15
15
|
const rawDir = resolve(vaultPath, folders.fleeting);
|
|
16
16
|
const wikiDir = resolve(vaultPath, folders.wiki);
|
|
@@ -43,27 +43,53 @@ export function generateDraft(vaultPath, options = {}, folders = DEFAULT_FOLDERS
|
|
|
43
43
|
? `Draft: ${topic}`
|
|
44
44
|
: `Knowledge Draft — ${new Date().toISOString().split('T')[0]}`;
|
|
45
45
|
let body;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
46
|
+
// Blueprint 모드: 사용자 정의 챕터 구성
|
|
47
|
+
if (blueprint && blueprint.length > 0) {
|
|
48
|
+
const bpLines = [`# ${draftTitle}\n`];
|
|
49
|
+
bpLines.push(`> Blueprint draft from ${filteredDocs.length} sources\n`);
|
|
50
|
+
for (const section of blueprint) {
|
|
51
|
+
bpLines.push(`## ${section.title}\n`);
|
|
52
|
+
if (section.description)
|
|
53
|
+
bpLines.push(`*${section.description}*\n`);
|
|
54
|
+
// 태그 필터로 관련 문서 찾기
|
|
55
|
+
const sectionDocs = section.tags
|
|
56
|
+
? filteredDocs.filter(d => d.tags.some(t => section.tags.includes(t)) || d.title.toLowerCase().includes(section.title.toLowerCase()))
|
|
57
|
+
: filteredDocs;
|
|
58
|
+
const usedDocs = sectionDocs.slice(0, 5);
|
|
59
|
+
for (const doc of usedDocs) {
|
|
60
|
+
const excerpt = extractExcerpt(doc.content, 200);
|
|
61
|
+
if (excerpt)
|
|
62
|
+
bpLines.push(`> ${excerpt}\n> — *${doc.title}*\n`);
|
|
63
|
+
}
|
|
64
|
+
if (usedDocs.length > 0) {
|
|
65
|
+
bpLines.push(`*Sources: ${usedDocs.map(d => d.title).join(', ')}*\n`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
bpLines.push(`---\n*Generated by \`stellavault draft --blueprint\` at ${new Date().toISOString()}*`);
|
|
69
|
+
body = bpLines.join('\n');
|
|
66
70
|
}
|
|
71
|
+
else
|
|
72
|
+
switch (format) {
|
|
73
|
+
case 'outline':
|
|
74
|
+
body = generateOutline(draftTitle, topConcepts, filteredDocs);
|
|
75
|
+
break;
|
|
76
|
+
case 'report':
|
|
77
|
+
body = generateReport(draftTitle, topConcepts, filteredDocs);
|
|
78
|
+
break;
|
|
79
|
+
case 'instagram':
|
|
80
|
+
body = generateInstagram(draftTitle, topConcepts, filteredDocs);
|
|
81
|
+
break;
|
|
82
|
+
case 'thread':
|
|
83
|
+
body = generateThread(draftTitle, topConcepts, filteredDocs);
|
|
84
|
+
break;
|
|
85
|
+
case 'script':
|
|
86
|
+
body = generateScript(draftTitle, topConcepts, filteredDocs);
|
|
87
|
+
break;
|
|
88
|
+
case 'blog':
|
|
89
|
+
default:
|
|
90
|
+
body = generateBlog(draftTitle, topConcepts, filteredDocs);
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
67
93
|
const wordCount = body.split(/\s+/).filter(Boolean).length;
|
|
68
94
|
// _drafts/ 폴더에 저장
|
|
69
95
|
const draftsDir = resolve(vaultPath, '_drafts');
|
|
@@ -9,10 +9,5 @@ export interface ExtractedContent {
|
|
|
9
9
|
sourceFormat: 'pdf' | 'docx' | 'pptx' | 'xlsx' | 'xls' | 'text';
|
|
10
10
|
}
|
|
11
11
|
export declare function isBinaryFormat(filePath: string): boolean;
|
|
12
|
-
/**
|
|
13
|
-
* 파일 경로에서 텍스트 추출. 확장자 기반 파서 디스패치.
|
|
14
|
-
* 지원: .pdf, .docx, .pptx, .xlsx, .xls
|
|
15
|
-
* 미지원 확장자: utf-8 텍스트로 읽기
|
|
16
|
-
*/
|
|
17
12
|
export declare function extractFileContent(filePath: string): Promise<ExtractedContent>;
|
|
18
13
|
//# sourceMappingURL=file-extractors.d.ts.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Design Ref: §file-ingest-v2 — Binary file text extraction dispatchers
|
|
2
2
|
// Plan SC: SC1-SC5 (format-specific extraction + fallback)
|
|
3
|
-
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
4
4
|
import { extname, basename } from 'node:path';
|
|
5
5
|
const BINARY_EXTS = new Set(['.pdf', '.docx', '.pptx', '.xlsx', '.xls']);
|
|
6
6
|
export function isBinaryFormat(filePath) {
|
|
@@ -11,8 +11,13 @@ export function isBinaryFormat(filePath) {
|
|
|
11
11
|
* 지원: .pdf, .docx, .pptx, .xlsx, .xls
|
|
12
12
|
* 미지원 확장자: utf-8 텍스트로 읽기
|
|
13
13
|
*/
|
|
14
|
+
const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB
|
|
14
15
|
export async function extractFileContent(filePath) {
|
|
15
16
|
const ext = extname(filePath).toLowerCase();
|
|
17
|
+
const { size } = statSync(filePath);
|
|
18
|
+
if (size > MAX_FILE_SIZE) {
|
|
19
|
+
throw new Error(`File too large (${Math.round(size / 1024 / 1024)}MB > 50MB limit)`);
|
|
20
|
+
}
|
|
16
21
|
const buffer = readFileSync(filePath);
|
|
17
22
|
switch (ext) {
|
|
18
23
|
case '.pdf': return extractPdf(buffer, filePath);
|
|
@@ -86,12 +91,35 @@ async function extractXlsx(buffer, filePath) {
|
|
|
86
91
|
try {
|
|
87
92
|
const XLSX = await import('xlsx');
|
|
88
93
|
const workbook = XLSX.read(buffer);
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
.
|
|
94
|
+
const parts = [];
|
|
95
|
+
for (const name of workbook.SheetNames) {
|
|
96
|
+
const sheet = workbook.Sheets[name];
|
|
97
|
+
// 마크다운 테이블 형식 (헤더-값 구조 보존)
|
|
98
|
+
const rows = XLSX.utils.sheet_to_json(sheet, { header: 1 });
|
|
99
|
+
if (rows.length === 0)
|
|
100
|
+
continue;
|
|
101
|
+
parts.push(`## ${name}\n`);
|
|
102
|
+
const headers = rows[0].map((h) => String(h ?? ''));
|
|
103
|
+
parts.push(`| ${headers.join(' | ')} |`);
|
|
104
|
+
parts.push(`| ${headers.map(() => '---').join(' | ')} |`);
|
|
105
|
+
for (const row of rows.slice(1)) {
|
|
106
|
+
const cells = headers.map((_, i) => String(row[i] ?? ''));
|
|
107
|
+
parts.push(`| ${cells.join(' | ')} |`);
|
|
108
|
+
}
|
|
109
|
+
parts.push('');
|
|
110
|
+
// JSON 구조도 포함 (AI 검색/ask에서 수치 활용 가능)
|
|
111
|
+
if (rows.length <= 100) {
|
|
112
|
+
const jsonRows = XLSX.utils.sheet_to_json(sheet);
|
|
113
|
+
if (jsonRows.length > 0) {
|
|
114
|
+
parts.push(`<details><summary>Structured Data (${jsonRows.length} rows)</summary>\n`);
|
|
115
|
+
parts.push('```json');
|
|
116
|
+
parts.push(JSON.stringify(jsonRows.slice(0, 50), null, 2));
|
|
117
|
+
parts.push('```');
|
|
118
|
+
parts.push('</details>\n');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const text = parts.join('\n');
|
|
95
123
|
return {
|
|
96
124
|
text,
|
|
97
125
|
metadata: {
|
|
@@ -65,7 +65,9 @@ export function ingest(vaultPath, input, folders = DEFAULT_FOLDERS) {
|
|
|
65
65
|
}]);
|
|
66
66
|
indexCode = assignments.get(filePath);
|
|
67
67
|
}
|
|
68
|
-
catch {
|
|
68
|
+
catch (err) {
|
|
69
|
+
console.warn('[ingest] Index code skipped:', err instanceof Error ? err.message : err);
|
|
70
|
+
}
|
|
69
71
|
// Stellavault 표준 포맷으로 저장
|
|
70
72
|
let md = buildStandardNote({
|
|
71
73
|
title,
|
|
@@ -81,7 +83,9 @@ export function ingest(vaultPath, input, folders = DEFAULT_FOLDERS) {
|
|
|
81
83
|
try {
|
|
82
84
|
md = autoLink(md, vaultPath, title, folders);
|
|
83
85
|
}
|
|
84
|
-
catch {
|
|
86
|
+
catch (err) {
|
|
87
|
+
console.warn('[ingest] Auto-link skipped:', err instanceof Error ? err.message : err);
|
|
88
|
+
}
|
|
85
89
|
writeFileSync(fullPath, md, 'utf-8');
|
|
86
90
|
// 자동 compile: fleeting → wiki (rule-based, <100ms)
|
|
87
91
|
try {
|
|
@@ -221,10 +225,10 @@ function buildStandardNote(params) {
|
|
|
221
225
|
'---',
|
|
222
226
|
`title: "${sanitizeYaml(params.title)}"`,
|
|
223
227
|
`type: ${params.stage}`,
|
|
224
|
-
`source: ${params.source}`,
|
|
228
|
+
`source: "${sanitizeYaml(params.source)}"`,
|
|
225
229
|
`input_type: ${params.inputType}`,
|
|
226
230
|
params.indexCode ? `zettel_id: "${params.indexCode}"` : null,
|
|
227
|
-
`tags: [${params.tags.map(t => `"${t}"`).join(', ')}]`,
|
|
231
|
+
`tags: [${params.tags.map(t => `"${sanitizeYaml(t)}"`).join(', ')}]`,
|
|
228
232
|
`created: ${params.created}`,
|
|
229
233
|
`summary: "${sanitizeYaml(params.body.slice(0, 100))}"`,
|
|
230
234
|
'---',
|