stellavault 0.4.3 → 0.4.5
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/README.md +24 -15
- package/SECURITY.md +50 -0
- package/index.html +3 -3
- package/package.json +2 -2
- 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 +26 -8
- 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 +48 -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 +11 -5
package/README.md
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
# Stellavault
|
|
2
2
|
|
|
3
|
-
> **
|
|
3
|
+
> **Drop anything. It compiles itself into knowledge.** Claude remembers everything you know.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Self-compiling Zettelkasten MCP server. Ingest PDFs, YouTube, documents — auto-organized into linked wiki. Claude accesses your entire knowledge base. **Your vault files are never modified.**
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
<img src="images/screenshots/graph-
|
|
9
|
-
<br><em>Your vault as a neural network.
|
|
8
|
+
<img src="images/screenshots/graph-main-2.png" alt="3D Knowledge Graph" width="800" />
|
|
9
|
+
<br><em>Your vault as a neural network. Local-first, no cloud required.</em>
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
12
|
## Two Core Ideas
|
|
13
13
|
|
|
14
|
-
**1. "Drop it and forget it"** (Karpathy's Self-Compiling Knowledge)
|
|
14
|
+
**1. "Drop it and forget it"** (Inspired by Karpathy's Self-Compiling Knowledge)
|
|
15
15
|
```
|
|
16
16
|
Any input → auto-classify → raw/ → compile → wiki → connected knowledge
|
|
17
17
|
```
|
|
18
|
-
PDF, DOCX, YouTube, URL, text — everything goes through the same pipeline. You never manually organize.
|
|
18
|
+
PDF, DOCX, PPTX, XLSX, YouTube (with transcript), URL, text — everything goes through the same pipeline. You never manually organize.
|
|
19
19
|
|
|
20
|
-
**2. "Claude
|
|
20
|
+
**2. "Claude remembers what you know"** (MCP Integration)
|
|
21
21
|
```bash
|
|
22
22
|
claude mcp add stellavault -- stellavault serve
|
|
23
23
|
```
|
|
24
|
-
|
|
24
|
+
Claude searches, asks, drafts from your vault directly. Local-first — no data leaves your machine.
|
|
25
25
|
|
|
26
26
|
## 5-Minute Setup
|
|
27
27
|
|
|
@@ -200,19 +200,28 @@ stellavault autopilot # Full cycle: inbox → compile
|
|
|
200
200
|
| **Capture** | ingest (URL/YouTube/PDF/DOCX/PPTX/XLSX/text), fleeting, web drag & drop, mobile PWA |
|
|
201
201
|
| **Organize** | Zettelkasten 3-stage, auto index codes, wikilink auto-connect, configurable folders |
|
|
202
202
|
| **Distill** | compile (raw→wiki), lint (health score), gaps, contradictions, duplicates |
|
|
203
|
-
| **Express** | draft (blog/report/outline), --ai
|
|
204
|
-
| **Memory** | FSRS decay
|
|
205
|
-
| **Search** | hybrid (BM25+vector+RRF), multilingual
|
|
206
|
-
| **Visualize** | 3D graph,
|
|
207
|
-
| **AI Integration** | 21 MCP tools, Claude Code hooks, Anthropic SDK
|
|
208
|
-
| **
|
|
203
|
+
| **Express** | draft (blog/report/outline/instagram/thread/script), blueprint, --ai, MCP generate-draft |
|
|
204
|
+
| **Memory** | FSRS decay, session-save, flush, compounding loop, ADR templates |
|
|
205
|
+
| **Search** | hybrid (BM25+vector+RRF), multilingual 50+, ask Q&A, quotes mode |
|
|
206
|
+
| **Visualize** | 3D graph, heatmap, timeline, right-click context menu, TipTap WYSIWYG editor |
|
|
207
|
+
| **AI Integration** | 21 MCP tools, Claude Code hooks, Anthropic SDK |
|
|
208
|
+
| **Security** | DOMPurify, YAML sanitize, 50MB guard, SSRF protection |
|
|
209
|
+
| **CLI** | 40+ commands, `sv` alias, batch ingest |
|
|
210
|
+
|
|
211
|
+
## Security
|
|
212
|
+
|
|
213
|
+
Your vault files are never modified. Stellavault is local-first — no data leaves your machine unless you explicitly use `--ai` (Anthropic API).
|
|
214
|
+
|
|
215
|
+
See [SECURITY.md](SECURITY.md) for full details.
|
|
209
216
|
|
|
210
217
|
## License
|
|
211
218
|
|
|
212
|
-
MIT
|
|
219
|
+
MIT — full source code available for audit.
|
|
213
220
|
|
|
214
221
|
## Links
|
|
215
222
|
|
|
223
|
+
- [Landing Page](https://evanciel.github.io/stellavault/)
|
|
216
224
|
- [Obsidian Plugin](https://github.com/Evanciel/stellavault-obsidian)
|
|
217
225
|
- [npm](https://www.npmjs.com/package/stellavault)
|
|
218
226
|
- [GitHub Releases](https://github.com/Evanciel/stellavault/releases)
|
|
227
|
+
- [Security Policy](SECURITY.md)
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Data Access
|
|
4
|
+
|
|
5
|
+
Stellavault is **local-first**. Your knowledge stays on your machine.
|
|
6
|
+
|
|
7
|
+
### What Stellavault reads
|
|
8
|
+
- `.md`, `.txt`, `.pdf`, `.docx`, `.pptx`, `.xlsx` files **inside your configured vault path only**
|
|
9
|
+
- Files are read to build a search index (SQLite-vec database stored in `~/.stellavault/`)
|
|
10
|
+
- **Vault original files are never modified by the indexer** — Stellavault creates its own files in `raw/`, `_wiki/`, `_drafts/` folders
|
|
11
|
+
|
|
12
|
+
### When network requests occur
|
|
13
|
+
- **YouTube ingest**: fetches video metadata + captions from youtube.com (via yt-dlp)
|
|
14
|
+
- **URL ingest**: fetches the target URL to extract text
|
|
15
|
+
- **`stellavault draft --ai`**: sends vault excerpts to Anthropic API (requires explicit `ANTHROPIC_API_KEY` env var — opt-in only)
|
|
16
|
+
- **MCP serve**: local stdio/HTTP only — no external connections
|
|
17
|
+
- **Embedding model**: downloaded once from Hugging Face on first `stellavault index`, then cached locally
|
|
18
|
+
|
|
19
|
+
### What never leaves your machine
|
|
20
|
+
- Your vault files
|
|
21
|
+
- Your search index database
|
|
22
|
+
- Your session logs and daily logs
|
|
23
|
+
- Your draft outputs
|
|
24
|
+
- All MCP tool responses
|
|
25
|
+
|
|
26
|
+
## Vault Safety
|
|
27
|
+
|
|
28
|
+
- **Read-only default**: The search indexer reads files but does not modify them
|
|
29
|
+
- **New files only**: `ingest`, `session-save`, `compile`, `draft` create new `.md` files — they never overwrite existing vault notes
|
|
30
|
+
- **Edit is explicit**: The web UI edit feature and `PUT /api/document` require deliberate user action
|
|
31
|
+
- **Path traversal protection**: All file operations validate paths stay within vault root
|
|
32
|
+
- **Configurable folders**: `raw/`, `_wiki/`, `_literature/` names can be changed in `.stellavault.json`
|
|
33
|
+
|
|
34
|
+
## Input Sanitization
|
|
35
|
+
|
|
36
|
+
- **DOMPurify**: All markdown rendered in the web UI is sanitized against XSS
|
|
37
|
+
- **YAML sanitization**: Frontmatter values are escaped to prevent injection
|
|
38
|
+
- **File size limit**: 50MB max for binary file extraction
|
|
39
|
+
- **URL validation**: Image URLs restricted to `https://` scheme
|
|
40
|
+
- **SSRF protection**: Private/local IP addresses blocked for URL ingest
|
|
41
|
+
|
|
42
|
+
## Reporting Vulnerabilities
|
|
43
|
+
|
|
44
|
+
Please report security issues to: https://github.com/Evanciel/stellavault/issues (label: security)
|
|
45
|
+
|
|
46
|
+
Or email: [create a security@stellavault.dev when domain is registered]
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
|
|
50
|
+
MIT — full source code is available for audit at https://github.com/Evanciel/stellavault
|
package/index.html
CHANGED
|
@@ -231,7 +231,7 @@ footer{padding:60px 0;text-align:center}
|
|
|
231
231
|
<section class="hero">
|
|
232
232
|
<div class="graph-visual" id="graphVisual"></div>
|
|
233
233
|
<div class="hero-inner container">
|
|
234
|
-
<div class="hero-badge">
|
|
234
|
+
<div class="hero-badge">local-first <span class="sep">●</span> vault files never modified <span class="sep">●</span> MIT</div>
|
|
235
235
|
<h1>Drop anything.<br>It <em>compiles itself</em><br>into knowledge.</h1>
|
|
236
236
|
<p class="hero-sub">Self-compiling Zettelkasten MCP server. Ingest PDFs, YouTube, documents—auto-organized into linked wiki. Claude accesses your entire knowledge base through 21 MCP tools.</p>
|
|
237
237
|
<div class="hero-cta">
|
|
@@ -366,8 +366,8 @@ footer{padding:60px 0;text-align:center}
|
|
|
366
366
|
<section class="s-mcp">
|
|
367
367
|
<div class="container">
|
|
368
368
|
<div class="section-label reveal">Claude Integration</div>
|
|
369
|
-
<h2 class="section-title reveal">
|
|
370
|
-
<p class="section-desc reveal">One command connects your vault
|
|
369
|
+
<h2 class="section-title reveal">Claude remembers<br>everything you know.</h2>
|
|
370
|
+
<p class="section-desc reveal">One command connects your vault. Claude searches, asks, and drafts from your knowledge. Local-first, no data leaves your machine.</p>
|
|
371
371
|
<p class="section-desc reveal">One command connects your vault to Claude Code. No API key needed.</p>
|
|
372
372
|
<div class="mcp-connect reveal"><span class="prompt">$</span> <code>claude mcp add stellavault -- stellavault serve</code></div>
|
|
373
373
|
<div class="mcp-grid reveal">
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stellavault",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.4.5",
|
|
4
|
+
"description": "Drop anything. It compiles itself into knowledge. Claude remembers everything you know. Local-first MCP server, vault files never modified.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/Evanciel/stellavault"
|
|
@@ -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,21 +17,39 @@ 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
|
-
await
|
|
29
|
+
await ingestSingleFile(file, options);
|
|
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
|
+
await ingestSingleFile(input, options);
|
|
46
|
+
}
|
|
47
|
+
/** 캐시된 config (배치 시 반복 로드 방지) */
|
|
48
|
+
let _configCache = null;
|
|
49
|
+
function getConfig() { return _configCache ?? (_configCache = loadConfig()); }
|
|
50
|
+
/** 단일 파일/URL/텍스트 인제스트 (배치에서 재사용) */
|
|
51
|
+
async function ingestSingleFile(input, options) {
|
|
52
|
+
const config = getConfig();
|
|
35
53
|
const tags = options.tags?.split(',').map(t => t.trim()) ?? [];
|
|
36
54
|
const stage = (options.stage ?? 'fleeting');
|
|
37
55
|
// 입력 타입 감지
|
|
@@ -80,8 +98,8 @@ export async function ingestCommand(input, options) {
|
|
|
80
98
|
// 일반 URL: HTML → 텍스트 변환
|
|
81
99
|
let content = input + '\n';
|
|
82
100
|
try {
|
|
83
|
-
const resp = await fetch(input);
|
|
84
|
-
const html = await resp.text();
|
|
101
|
+
const resp = await fetch(input, { signal: AbortSignal.timeout(15000) });
|
|
102
|
+
const html = (await resp.text()).slice(0, 500000); // 500KB max
|
|
85
103
|
const text = html
|
|
86
104
|
.replace(/<script[\s\S]*?<\/script>/gi, '')
|
|
87
105
|
.replace(/<style[\s\S]*?<\/style>/gi, '')
|
|
@@ -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,54 @@ 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
|
+
}
|
|
93
|
+
} // end else (non-blueprint)
|
|
67
94
|
const wordCount = body.split(/\s+/).filter(Boolean).length;
|
|
68
95
|
// _drafts/ 폴더에 저장
|
|
69
96
|
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 {
|
|
@@ -91,7 +95,9 @@ export function ingest(vaultPath, input, folders = DEFAULT_FOLDERS) {
|
|
|
91
95
|
compileWiki(rawDir, wikiDir);
|
|
92
96
|
}
|
|
93
97
|
}
|
|
94
|
-
catch {
|
|
98
|
+
catch (err) {
|
|
99
|
+
console.warn('[ingest] Auto-compile skipped:', err instanceof Error ? err.message : err);
|
|
100
|
+
}
|
|
95
101
|
return {
|
|
96
102
|
savedTo: filePath,
|
|
97
103
|
stage: autoStage,
|
|
@@ -221,10 +227,10 @@ function buildStandardNote(params) {
|
|
|
221
227
|
'---',
|
|
222
228
|
`title: "${sanitizeYaml(params.title)}"`,
|
|
223
229
|
`type: ${params.stage}`,
|
|
224
|
-
`source: ${params.source}`,
|
|
230
|
+
`source: "${sanitizeYaml(params.source)}"`,
|
|
225
231
|
`input_type: ${params.inputType}`,
|
|
226
232
|
params.indexCode ? `zettel_id: "${params.indexCode}"` : null,
|
|
227
|
-
`tags: [${params.tags.map(t => `"${t}"`).join(', ')}]`,
|
|
233
|
+
`tags: [${params.tags.map(t => `"${sanitizeYaml(t)}"`).join(', ')}]`,
|
|
228
234
|
`created: ${params.created}`,
|
|
229
235
|
`summary: "${sanitizeYaml(params.body.slice(0, 100))}"`,
|
|
230
236
|
'---',
|