stellavault 0.4.2 → 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/CLAUDE.md +5 -2
- package/README.md +49 -3
- package/index.html +33 -7
- 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 +37 -3
- package/packages/cli/dist/index.js +13 -2
- package/packages/core/dist/api/graph-data.js +1 -1
- package/packages/core/dist/api/server.js +12 -1
- package/packages/core/dist/indexer/scanner.js +12 -1
- 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 +7 -1
- package/packages/core/dist/intelligence/draft-generator.js +131 -15
- 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 +37 -7
- package/packages/core/dist/intelligence/youtube-extractor.js +11 -1
- package/packages/core/dist/mcp/tools/ask.js +5 -2
package/CLAUDE.md
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
# Stellavault — Project Rules
|
|
2
2
|
|
|
3
3
|
## Project Info
|
|
4
|
-
- **Name**: Stellavault
|
|
4
|
+
- **Name**: Stellavault — Self-compiling knowledge MCP server
|
|
5
|
+
- **Version**: 0.4.2
|
|
5
6
|
- **GitHub**: https://github.com/Evanciel/stellavault
|
|
7
|
+
- **npm**: stellavault (171KB)
|
|
6
8
|
- **Stack**: Node.js 20+, TypeScript, ESM, Monorepo (npm workspaces)
|
|
7
|
-
- **Packages**: core, cli, graph, sync
|
|
9
|
+
- **Packages**: core (21 MCP tools), cli (39+ commands), graph (3D UI), sync
|
|
10
|
+
- **Architecture**: Karpathy self-compiling KB (ingest→compile→wiki→session→flush loop)
|
|
8
11
|
|
|
9
12
|
## Autopilot 추가 규칙
|
|
10
13
|
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> **Self-compiling knowledge MCP server** — ingest anything, auto-organize into Zettelkasten wiki, and let Claude access your entire knowledge base.
|
|
4
4
|
|
|
5
|
-
Drop a PDF, paste a YouTube link, type a thought — Stellavault compiles it into structured knowledge, connects the dots, and gives your AI agent full access through
|
|
5
|
+
Drop a PDF, paste a YouTube link, type a thought — Stellavault compiles it into structured knowledge, connects the dots, and gives your AI agent full access through 21 MCP tools.
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<img src="images/screenshots/graph-dark-full.png" alt="3D Knowledge Graph" width="800" />
|
|
@@ -21,7 +21,7 @@ PDF, DOCX, YouTube, URL, text — everything goes through the same pipeline. You
|
|
|
21
21
|
```bash
|
|
22
22
|
claude mcp add stellavault -- stellavault serve
|
|
23
23
|
```
|
|
24
|
-
|
|
24
|
+
21 MCP tools give Claude direct access to search, ask, draft, and navigate your entire knowledge base.
|
|
25
25
|
|
|
26
26
|
## 5-Minute Setup
|
|
27
27
|
|
|
@@ -66,6 +66,27 @@ stellavault draft --format outline # All-knowledge outline
|
|
|
66
66
|
|
|
67
67
|
Or in Claude Code: *"Write a blog post about machine learning from my notes"* — Claude uses MCP `generate-draft` tool (free, no API key).
|
|
68
68
|
|
|
69
|
+
## Self-Evolving Memory (Karpathy's Compounding Loop)
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
Session → session-save → daily-log → flush → wiki
|
|
73
|
+
↑ ↓
|
|
74
|
+
└──── Claude reads wiki via MCP (20 tools) ←─┘
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Every conversation makes your knowledge base smarter:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
# Auto-capture session summary to daily log
|
|
81
|
+
echo "Decided to use JWT. Lesson: never store tokens in localStorage" | stellavault session-save
|
|
82
|
+
|
|
83
|
+
# Flush daily logs → extract concepts → rebuild wiki
|
|
84
|
+
stellavault flush
|
|
85
|
+
|
|
86
|
+
# Or set up Claude Code hooks for full automation
|
|
87
|
+
# See: docs/hooks-setup.md
|
|
88
|
+
```
|
|
89
|
+
|
|
69
90
|
## Daily Commands
|
|
70
91
|
|
|
71
92
|
```bash
|
|
@@ -74,10 +95,11 @@ stellavault brief # Morning knowledge briefing
|
|
|
74
95
|
stellavault decay # What's fading from memory?
|
|
75
96
|
stellavault lint # Health score (0-100)
|
|
76
97
|
stellavault learn # AI learning path
|
|
98
|
+
stellavault flush # Daily logs → wiki compilation
|
|
77
99
|
stellavault digest --visual # Weekly Mermaid chart report
|
|
78
100
|
```
|
|
79
101
|
|
|
80
|
-
## MCP Tools (
|
|
102
|
+
## MCP Tools (21)
|
|
81
103
|
|
|
82
104
|
| Tool | What it does |
|
|
83
105
|
|------|-------------|
|
|
@@ -99,6 +121,16 @@ stellavault digest --visual # Weekly Mermaid chart report
|
|
|
99
121
|
| `create-snapshot` / `load-snapshot` | Context snapshots |
|
|
100
122
|
| `generate-claude-md` | Auto-generate CLAUDE.md |
|
|
101
123
|
| `export` | JSON/CSV export |
|
|
124
|
+
| `federated-search` | P2P federated search |
|
|
125
|
+
|
|
126
|
+
## Self-Evolving Commands
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
stellavault session-save # Capture session summary to daily log
|
|
130
|
+
stellavault flush # Daily logs → wiki (Karpathy compile)
|
|
131
|
+
stellavault promote note.md --to lit # Upgrade note stage
|
|
132
|
+
stellavault autopilot # Full cycle: inbox → compile → lint → archive
|
|
133
|
+
```
|
|
102
134
|
|
|
103
135
|
## Zettelkasten (Luhmann + Karpathy)
|
|
104
136
|
|
|
@@ -161,6 +193,20 @@ stellavault autopilot # Full cycle: inbox → compile
|
|
|
161
193
|
| 3D | React Three Fiber + Three.js |
|
|
162
194
|
| AI | MCP (Model Context Protocol) + Anthropic SDK |
|
|
163
195
|
|
|
196
|
+
## Full Feature List
|
|
197
|
+
|
|
198
|
+
| Category | Features |
|
|
199
|
+
|----------|----------|
|
|
200
|
+
| **Capture** | ingest (URL/YouTube/PDF/DOCX/PPTX/XLSX/text), fleeting, web drag & drop, mobile PWA |
|
|
201
|
+
| **Organize** | Zettelkasten 3-stage, auto index codes, wikilink auto-connect, configurable folders |
|
|
202
|
+
| **Distill** | compile (raw→wiki), lint (health score), gaps, contradictions, duplicates |
|
|
203
|
+
| **Express** | draft (blog/report/outline), --ai (Claude API), MCP generate-draft (free) |
|
|
204
|
+
| **Memory** | FSRS decay tracking, session-save (daily logs), flush (logs→wiki), compounding loop |
|
|
205
|
+
| **Search** | hybrid (BM25+vector+RRF), multilingual (50+ langs), ask Q&A |
|
|
206
|
+
| **Visualize** | 3D graph, constellation, heatmap, timeline, decay overlay, dark/light |
|
|
207
|
+
| **AI Integration** | 21 MCP tools, Claude Code hooks, Anthropic SDK, generate-draft |
|
|
208
|
+
| **CLI** | 39+ commands, `sv` alias |
|
|
209
|
+
|
|
164
210
|
## License
|
|
165
211
|
|
|
166
212
|
MIT
|
package/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Stellavault — Self-Compiling Knowledge MCP Server</title>
|
|
7
|
-
<meta name="description" content="Drop anything. It organizes itself. Claude knows everything you know. Self-compiling Zettelkasten with
|
|
7
|
+
<meta name="description" content="Drop anything. It organizes itself. Claude knows everything you know. Self-compiling Zettelkasten with 21 MCP tools.">
|
|
8
8
|
<meta name="theme-color" content="#6366f1">
|
|
9
9
|
<meta property="og:title" content="Stellavault — Self-Compiling Knowledge MCP Server">
|
|
10
10
|
<meta property="og:description" content="Drop anything. It organizes itself. Claude knows everything you know.">
|
|
@@ -233,7 +233,7 @@ footer{padding:60px 0;text-align:center}
|
|
|
233
233
|
<div class="hero-inner container">
|
|
234
234
|
<div class="hero-badge">open source <span class="sep">●</span> local-first <span class="sep">●</span> MIT license</div>
|
|
235
235
|
<h1>Drop anything.<br>It <em>compiles itself</em><br>into knowledge.</h1>
|
|
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
|
|
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">
|
|
238
238
|
<button class="cta-install" id="ctaInstall">
|
|
239
239
|
<span class="prompt">$</span> npm install -g stellavault
|
|
@@ -243,7 +243,7 @@ footer{padding:60px 0;text-align:center}
|
|
|
243
243
|
<a href="https://github.com/Evanciel/stellavault" class="cta-mobile">View on GitHub →</a>
|
|
244
244
|
</div>
|
|
245
245
|
<div class="hero-proof">
|
|
246
|
-
<span><span class="proof-num">
|
|
246
|
+
<span><span class="proof-num">171KB</span> on npm</span>
|
|
247
247
|
<span class="proof-sep">●</span>
|
|
248
248
|
<span><span class="proof-num">20</span> MCP tools</span>
|
|
249
249
|
<span class="proof-sep">●</span>
|
|
@@ -285,7 +285,7 @@ footer{padding:60px 0;text-align:center}
|
|
|
285
285
|
<div class="problem-title">AI doesn't know what you know</div>
|
|
286
286
|
<div class="problem-desc">ChatGPT has the internet. It doesn't have your notes, your decisions, your context.</div>
|
|
287
287
|
<span class="problem-arrow">↓</span>
|
|
288
|
-
<div class="problem-solution">
|
|
288
|
+
<div class="problem-solution">21 MCP tools — Claude searches, asks, drafts from your vault</div>
|
|
289
289
|
</div>
|
|
290
290
|
</div>
|
|
291
291
|
</div>
|
|
@@ -308,6 +308,27 @@ footer{padding:60px 0;text-align:center}
|
|
|
308
308
|
</div>
|
|
309
309
|
</section>
|
|
310
310
|
|
|
311
|
+
<!-- ═══ COMPOUNDING LOOP ═══ -->
|
|
312
|
+
<section style="padding:80px 0">
|
|
313
|
+
<div class="container" style="text-align:center">
|
|
314
|
+
<div class="section-label reveal">The Compounding Loop</div>
|
|
315
|
+
<h2 class="section-title reveal">Gets smarter every session.</h2>
|
|
316
|
+
<p class="section-desc reveal" style="margin:0 auto 40px">Every conversation with Claude becomes knowledge. Knowledge improves the next conversation. Karpathy's self-evolving architecture.</p>
|
|
317
|
+
<div class="reveal" style="max-width:700px;margin:0 auto;padding:32px;background:var(--surface);border:1px solid var(--border);border-radius:14px;font-family:var(--font-mono);font-size:13px;line-height:2.2;color:var(--text-dim);text-align:left">
|
|
318
|
+
<span style="color:var(--cyan)">Session</span> → <code style="color:var(--violet-bright)">session-save</code> → <span style="color:var(--text)">daily-log</span> → <code style="color:var(--violet-bright)">flush</code> → <span style="color:var(--emerald)">wiki</span><br>
|
|
319
|
+
<span style="color:var(--text-muted);margin-left:16px">↑</span><span style="padding-left:280px;color:var(--text-muted)">↓</span><br>
|
|
320
|
+
<span style="color:var(--text-muted);margin-left:16px">←← Claude reads wiki via MCP (20 tools) ←←←</span>
|
|
321
|
+
</div>
|
|
322
|
+
<div style="display:flex;gap:16px;justify-content:center;margin-top:28px;flex-wrap:wrap" class="reveal">
|
|
323
|
+
<div style="padding:14px 20px;background:var(--surface);border:1px solid var(--border);border-radius:10px;font-size:12px"><span style="color:var(--violet-bright);font-family:var(--font-mono)">session-save</span><br><span style="color:var(--text-dim)">Auto-capture decisions & lessons</span></div>
|
|
324
|
+
<div style="padding:14px 20px;background:var(--surface);border:1px solid var(--border);border-radius:10px;font-size:12px"><span style="color:var(--violet-bright);font-family:var(--font-mono)">flush</span><br><span style="color:var(--text-dim)">Daily logs → wiki compilation</span></div>
|
|
325
|
+
<div style="padding:14px 20px;background:var(--surface);border:1px solid var(--border);border-radius:10px;font-size:12px"><span style="color:var(--violet-bright);font-family:var(--font-mono)">Claude Code hooks</span><br><span style="color:var(--text-dim)">Fully automatic capture</span></div>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
</section>
|
|
329
|
+
|
|
330
|
+
<div class="divider"></div>
|
|
331
|
+
|
|
311
332
|
<!-- ═══ SCREENSHOTS ═══ -->
|
|
312
333
|
<section class="s-screenshots">
|
|
313
334
|
<div class="container">
|
|
@@ -345,7 +366,8 @@ footer{padding:60px 0;text-align:center}
|
|
|
345
366
|
<section class="s-mcp">
|
|
346
367
|
<div class="container">
|
|
347
368
|
<div class="section-label reveal">Claude Integration</div>
|
|
348
|
-
<h2 class="section-title reveal">
|
|
369
|
+
<h2 class="section-title reveal">21 MCP tools. Claude knows<br>everything you know.</h2>
|
|
370
|
+
<p class="section-desc reveal">One command connects your vault to Claude Code. No API key needed.</p>
|
|
349
371
|
<p class="section-desc reveal">One command connects your vault to Claude Code. No API key needed.</p>
|
|
350
372
|
<div class="mcp-connect reveal"><span class="prompt">$</span> <code>claude mcp add stellavault -- stellavault serve</code></div>
|
|
351
373
|
<div class="mcp-grid reveal">
|
|
@@ -365,6 +387,10 @@ footer{padding:60px 0;text-align:center}
|
|
|
365
387
|
<div class="mcp-tool"><span class="dot"></span><code>get-morning-brief</code><span class="desc">daily briefing</span></div>
|
|
366
388
|
<div class="mcp-tool"><span class="dot"></span><code>list-topics</code><span class="desc">topic cloud</span></div>
|
|
367
389
|
<div class="mcp-tool"><span class="dot"></span><code>export</code><span class="desc">JSON / CSV</span></div>
|
|
390
|
+
<div class="mcp-tool"><span class="dot"></span><code>find-decisions</code><span class="desc">decision lookup</span></div>
|
|
391
|
+
<div class="mcp-tool"><span class="dot"></span><code>create-snapshot</code><span class="desc">context save/load</span></div>
|
|
392
|
+
<div class="mcp-tool"><span class="dot"></span><code>generate-claude-md</code><span class="desc">auto CLAUDE.md</span></div>
|
|
393
|
+
<div class="mcp-tool"><span class="dot"></span><code>federated-search</code><span class="desc">P2P search</span></div>
|
|
368
394
|
</div>
|
|
369
395
|
</div>
|
|
370
396
|
</section>
|
|
@@ -415,9 +441,9 @@ footer{padding:60px 0;text-align:center}
|
|
|
415
441
|
<div class="tech-item"><span class="label">Search</span><span class="value">BM25 + Cosine + RRF</span></div>
|
|
416
442
|
<div class="tech-item"><span class="label">Memory</span><span class="value">FSRS repetition</span></div>
|
|
417
443
|
<div class="tech-item"><span class="label">3D Graph</span><span class="value">React Three Fiber</span></div>
|
|
418
|
-
<div class="tech-item"><span class="label">AI Protocol</span><span class="value">MCP (
|
|
444
|
+
<div class="tech-item"><span class="label">AI Protocol</span><span class="value">MCP (21 tools)</span></div>
|
|
419
445
|
<div class="tech-item"><span class="label">Runtime</span><span class="value">Node.js 20+ ESM</span></div>
|
|
420
|
-
<div class="tech-item"><span class="label">Package</span><span class="value">
|
|
446
|
+
<div class="tech-item"><span class="label">Package</span><span class="value">171KB on npm</span></div>
|
|
421
447
|
</div>
|
|
422
448
|
</div>
|
|
423
449
|
</section>
|
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...'));
|
|
@@ -1,13 +1,47 @@
|
|
|
1
1
|
// stellavault ingest — 통합 인제스트 (URL, 텍스트, 파일 → 자동 분류 저장)
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { loadConfig, ingest, promoteNote } from '@stellavault/core';
|
|
4
|
-
import { readFileSync, existsSync } from 'node:fs';
|
|
5
|
-
import { extname, resolve } from 'node:path';
|
|
4
|
+
import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
|
|
5
|
+
import { extname, resolve, join } from 'node:path';
|
|
6
6
|
export async function ingestCommand(input, options) {
|
|
7
7
|
if (!input) {
|
|
8
|
-
console.error(chalk.yellow('Usage: stellavault ingest <url|file|text
|
|
8
|
+
console.error(chalk.yellow('Usage: stellavault ingest <url|file|text|folder/> [--tags t1,t2]'));
|
|
9
9
|
process.exit(1);
|
|
10
10
|
}
|
|
11
|
+
// 배치 모드: 폴더 경로이면 내부 파일 전부 처리
|
|
12
|
+
if (existsSync(input) && statSync(input).isDirectory()) {
|
|
13
|
+
const files = readdirSync(input)
|
|
14
|
+
.filter(f => /\.(md|txt|pdf|docx|pptx|xlsx|xls|csv)$/i.test(f))
|
|
15
|
+
.map(f => join(input, f));
|
|
16
|
+
if (files.length === 0) {
|
|
17
|
+
console.error(chalk.yellow(`No supported files found in ${input}`));
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
console.log(chalk.dim(`Batch ingest: ${files.length} files from ${input}\n`));
|
|
21
|
+
let success = 0;
|
|
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}...`);
|
|
28
|
+
try {
|
|
29
|
+
await ingestCommand(file, { ...options, title: undefined });
|
|
30
|
+
success++;
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
failed.push(`${name}: ${err instanceof Error ? err.message : 'error'}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
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
|
+
}
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
11
45
|
const config = loadConfig();
|
|
12
46
|
const tags = options.tags?.split(',').map(t => t.trim()) ?? [];
|
|
13
47
|
const stage = (options.stage ?? 'fleeting');
|
|
@@ -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')
|
|
@@ -130,7 +130,7 @@ export async function buildGraphData(store, options = {}) {
|
|
|
130
130
|
const size = 1 + 6 * Math.pow(ratio, 0.5);
|
|
131
131
|
return {
|
|
132
132
|
id: doc.id,
|
|
133
|
-
label: doc.title.slice(0, 40),
|
|
133
|
+
label: doc.title.replace(/'/g, "'").replace(/'/g, "'").replace(/"/g, '"').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\s*-\s*YouTube$/, '').slice(0, 40),
|
|
134
134
|
filePath: doc.filePath,
|
|
135
135
|
tags: doc.tags,
|
|
136
136
|
clusterId: assignmentMap.get(doc.id) ?? 0,
|
|
@@ -41,22 +41,32 @@ export function createApiServer(options) {
|
|
|
41
41
|
res.status(500).json({ error: 'Internal server error' });
|
|
42
42
|
}
|
|
43
43
|
});
|
|
44
|
+
// GET /api/reindex/status — 인덱싱 진행률 조회
|
|
45
|
+
let reindexProgress = { active: false, current: 0, total: 0, phase: '' };
|
|
46
|
+
app.get('/api/reindex/status', (_req, res) => {
|
|
47
|
+
res.json(reindexProgress);
|
|
48
|
+
});
|
|
44
49
|
// POST /api/reindex — 웹에서 인덱싱 트리거
|
|
45
50
|
let isReindexing = false;
|
|
46
51
|
app.post('/api/reindex', async (_req, res) => {
|
|
47
52
|
if (isReindexing) {
|
|
48
|
-
res.json({ success: false, error: '
|
|
53
|
+
res.json({ success: false, error: 'Reindexing already in progress', progress: reindexProgress });
|
|
49
54
|
return;
|
|
50
55
|
}
|
|
51
56
|
isReindexing = true;
|
|
57
|
+
reindexProgress = { active: true, current: 0, total: 0, phase: 'initializing' };
|
|
52
58
|
try {
|
|
53
59
|
const indexer = await import('../indexer/index.js');
|
|
60
|
+
reindexProgress.phase = 'loading embedder';
|
|
54
61
|
const embedder = indexer.createLocalEmbedder('all-MiniLM-L6-v2');
|
|
55
62
|
await embedder.initialize();
|
|
63
|
+
reindexProgress.phase = 'indexing';
|
|
56
64
|
const result = await indexer.indexVault(vaultPath, {
|
|
57
65
|
store,
|
|
58
66
|
embedder,
|
|
59
67
|
onProgress: (current, total) => {
|
|
68
|
+
reindexProgress.current = current;
|
|
69
|
+
reindexProgress.total = total;
|
|
60
70
|
if (current % 50 === 0)
|
|
61
71
|
console.error(`[reindex] ${current}/${total}`);
|
|
62
72
|
},
|
|
@@ -76,6 +86,7 @@ export function createApiServer(options) {
|
|
|
76
86
|
}
|
|
77
87
|
finally {
|
|
78
88
|
isReindexing = false;
|
|
89
|
+
reindexProgress = { active: false, current: 0, total: 0, phase: 'done' };
|
|
79
90
|
}
|
|
80
91
|
});
|
|
81
92
|
// GET /api/search?q=&limit=
|
|
@@ -46,9 +46,20 @@ function parseDocument(vaultPath, filePath) {
|
|
|
46
46
|
?? extractFirstHeading(content)
|
|
47
47
|
?? relativePath.replace(/\.md$/, '');
|
|
48
48
|
const tags = extractTags(frontmatter, content);
|
|
49
|
+
// Obsidian frontmatter 호환: aliases를 tags에 추가
|
|
50
|
+
if (frontmatter.aliases) {
|
|
51
|
+
const aliases = Array.isArray(frontmatter.aliases) ? frontmatter.aliases : [frontmatter.aliases];
|
|
52
|
+
for (const alias of aliases) {
|
|
53
|
+
if (typeof alias === 'string' && alias.length > 1)
|
|
54
|
+
tags.push(alias);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
49
57
|
// source/type 자동 추출 (원본 파일 수정 없이 DB에만 저장)
|
|
50
58
|
const source = inferSource(frontmatter, relativePath);
|
|
51
59
|
const type = inferType(frontmatter, relativePath);
|
|
60
|
+
// Obsidian frontmatter 호환: date/created 우선 사용
|
|
61
|
+
const fmDate = frontmatter.date ?? frontmatter.created ?? frontmatter.created_at;
|
|
62
|
+
const lastModified = fmDate ? new Date(fmDate).toISOString() : stat.mtime.toISOString();
|
|
52
63
|
return {
|
|
53
64
|
id,
|
|
54
65
|
filePath: relativePath,
|
|
@@ -56,7 +67,7 @@ function parseDocument(vaultPath, filePath) {
|
|
|
56
67
|
content,
|
|
57
68
|
frontmatter,
|
|
58
69
|
tags,
|
|
59
|
-
lastModified
|
|
70
|
+
lastModified,
|
|
60
71
|
contentHash,
|
|
61
72
|
source,
|
|
62
73
|
type,
|
|
@@ -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
|
-
format?: 'blog' | 'report' | 'outline';
|
|
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,18 +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
|
-
|
|
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');
|
|
57
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
|
+
}
|
|
58
93
|
const wordCount = body.split(/\s+/).filter(Boolean).length;
|
|
59
94
|
// _drafts/ 폴더에 저장
|
|
60
95
|
const draftsDir = resolve(vaultPath, '_drafts');
|
|
@@ -97,11 +132,23 @@ function generateBlog(title, concepts, docs) {
|
|
|
97
132
|
}
|
|
98
133
|
lines.push('<!-- TODO: Add your analysis and insights here -->', '');
|
|
99
134
|
}
|
|
100
|
-
// 참고 자료
|
|
135
|
+
// 참고 자료 (APA-style citations)
|
|
101
136
|
lines.push('## References', '');
|
|
102
137
|
const uniqueDocs = [...new Map(docs.map(d => [d.filePath, d])).values()].slice(0, 20);
|
|
103
|
-
|
|
104
|
-
|
|
138
|
+
const now = new Date();
|
|
139
|
+
for (let i = 0; i < uniqueDocs.length; i++) {
|
|
140
|
+
const doc = uniqueDocs[i];
|
|
141
|
+
const dateMatch = doc.content.match(/(?:created|date):\s*(\d{4}-\d{2}-\d{2})/);
|
|
142
|
+
const year = dateMatch ? dateMatch[1].split('-')[0] : now.getFullYear().toString();
|
|
143
|
+
const sourceMatch = doc.content.match(/source:\s*(.+)/);
|
|
144
|
+
const source = sourceMatch ? sourceMatch[1].trim() : '';
|
|
145
|
+
// APA-style: Author/Title (Year). Source.
|
|
146
|
+
if (source.startsWith('http')) {
|
|
147
|
+
lines.push(`[${i + 1}] ${doc.title} (${year}). Retrieved from ${source}`);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
lines.push(`[${i + 1}] ${doc.title} (${year}). [[${basename(doc.filePath, extname(doc.filePath))}|Link]]`);
|
|
151
|
+
}
|
|
105
152
|
}
|
|
106
153
|
lines.push('');
|
|
107
154
|
lines.push(`---`, `*Generated by \`stellavault draft\` at ${new Date().toISOString()}*`);
|
|
@@ -146,6 +193,75 @@ function generateOutline(title, concepts, docs) {
|
|
|
146
193
|
lines.push('', `---`, `*Generated by \`stellavault draft\` at ${new Date().toISOString()}*`);
|
|
147
194
|
return lines.join('\n');
|
|
148
195
|
}
|
|
196
|
+
// ─── SNS 포맷 ───
|
|
197
|
+
function generateInstagram(title, concepts, docs) {
|
|
198
|
+
const lines = [];
|
|
199
|
+
const topicWords = concepts.slice(0, 3).map(([c]) => c);
|
|
200
|
+
lines.push(`${title}`, '');
|
|
201
|
+
lines.push('---', '');
|
|
202
|
+
// 슬라이드 1: Hook
|
|
203
|
+
lines.push('**Slide 1 (Hook)**', '');
|
|
204
|
+
lines.push(`Did you know about ${topicWords.join(', ')}?`, '');
|
|
205
|
+
// 슬라이드 2-4: Key Points
|
|
206
|
+
for (let i = 0; i < Math.min(3, concepts.length); i++) {
|
|
207
|
+
const [concept] = concepts[i];
|
|
208
|
+
const doc = docs.find(d => d.tags.includes(concept) || d.title.toLowerCase().includes(concept.toLowerCase()));
|
|
209
|
+
const excerpt = doc ? extractExcerpt(doc.content, 80) : '';
|
|
210
|
+
lines.push(`**Slide ${i + 2} (${capitalize(concept)})**`, '');
|
|
211
|
+
lines.push(excerpt || `Key insight about ${concept}`, '');
|
|
212
|
+
}
|
|
213
|
+
// CTA
|
|
214
|
+
lines.push('**Last Slide (CTA)**', '');
|
|
215
|
+
lines.push('Save this for later! Follow for more knowledge drops.', '');
|
|
216
|
+
// Caption
|
|
217
|
+
lines.push('---', '', '**Caption:**', '');
|
|
218
|
+
lines.push(`${topicWords.map(w => `#${w}`).join(' ')} #knowledge #secondbrain #stellavault`, '');
|
|
219
|
+
lines.push(`---`, `*Generated by \`stellavault draft --format instagram\`*`);
|
|
220
|
+
return lines.join('\n');
|
|
221
|
+
}
|
|
222
|
+
function generateThread(title, concepts, docs) {
|
|
223
|
+
const lines = [];
|
|
224
|
+
// Tweet 1: Hook
|
|
225
|
+
lines.push(`**1/** ${title}`, '');
|
|
226
|
+
lines.push(`Here's what I learned from ${docs.length} sources:`, '');
|
|
227
|
+
// Tweet 2-N: One per concept
|
|
228
|
+
for (let i = 0; i < Math.min(6, concepts.length); i++) {
|
|
229
|
+
const [concept, docPaths] = concepts[i];
|
|
230
|
+
const doc = docs.find(d => docPaths.includes(d.filePath));
|
|
231
|
+
const excerpt = doc ? extractExcerpt(doc.content, 200) : '';
|
|
232
|
+
lines.push(`**${i + 2}/** ${capitalize(concept)}`, '');
|
|
233
|
+
lines.push(excerpt || `Key point about ${concept}`, '');
|
|
234
|
+
}
|
|
235
|
+
// Final tweet
|
|
236
|
+
lines.push(`**${Math.min(6, concepts.length) + 2}/** That's a wrap!`, '');
|
|
237
|
+
lines.push('If this was useful, repost the first tweet.', '');
|
|
238
|
+
lines.push(`---`, `*Generated by \`stellavault draft --format thread\`*`);
|
|
239
|
+
return lines.join('\n');
|
|
240
|
+
}
|
|
241
|
+
function generateScript(title, concepts, docs) {
|
|
242
|
+
const lines = [];
|
|
243
|
+
lines.push(`# ${title} — Video Script`, '');
|
|
244
|
+
lines.push(`**Duration:** ~${Math.max(3, concepts.length * 2)} minutes`, '');
|
|
245
|
+
// Intro
|
|
246
|
+
lines.push('## Intro (30s)', '');
|
|
247
|
+
lines.push(`"Hey everyone, today we're diving into ${concepts.slice(0, 2).map(([c]) => c).join(' and ')}."`, '');
|
|
248
|
+
// Sections
|
|
249
|
+
for (let i = 0; i < Math.min(5, concepts.length); i++) {
|
|
250
|
+
const [concept, docPaths] = concepts[i];
|
|
251
|
+
const relatedDocs = docs.filter(d => docPaths.includes(d.filePath)).slice(0, 2);
|
|
252
|
+
lines.push(`## Section ${i + 1}: ${capitalize(concept)} (~${Math.max(1, Math.floor(relatedDocs.length))}min)`, '');
|
|
253
|
+
for (const doc of relatedDocs) {
|
|
254
|
+
const excerpt = extractExcerpt(doc.content, 150);
|
|
255
|
+
if (excerpt)
|
|
256
|
+
lines.push(`[Visual: ${doc.title}]`, excerpt, '');
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// Outro
|
|
260
|
+
lines.push('## Outro (15s)', '');
|
|
261
|
+
lines.push('"If you found this helpful, like and subscribe!"', '');
|
|
262
|
+
lines.push(`---`, `*Generated by \`stellavault draft --format script\`*`);
|
|
263
|
+
return lines.join('\n');
|
|
264
|
+
}
|
|
149
265
|
// ─── 유틸 ───
|
|
150
266
|
function extractExcerpt(content, maxLen) {
|
|
151
267
|
// frontmatter 제거
|
|
@@ -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: {
|
|
@@ -9,16 +9,24 @@ import { scanFrontmatter, assignIndexCodes, archiveFile } from './zettelkasten.j
|
|
|
9
9
|
import { compileWiki } from './wiki-compiler.js';
|
|
10
10
|
import { autoLink } from './auto-linker.js';
|
|
11
11
|
import { DEFAULT_FOLDERS } from '../config.js';
|
|
12
|
+
/** HTML 엔티티 디코딩 */
|
|
13
|
+
function decodeHtmlEntities(text) {
|
|
14
|
+
return text
|
|
15
|
+
.replace(/'/g, "'").replace(/'/g, "'")
|
|
16
|
+
.replace(/"/g, '"').replace(/&/g, '&')
|
|
17
|
+
.replace(/</g, '<').replace(/>/g, '>')
|
|
18
|
+
.replace(/ /g, ' ');
|
|
19
|
+
}
|
|
12
20
|
/** YAML 값에서 위험한 문자를 이스케이프 */
|
|
13
21
|
function sanitizeYaml(val) {
|
|
14
|
-
return val.replace(/["\\]/g, '\\$&').replace(/\n/g, ' ').slice(0, 200);
|
|
22
|
+
return decodeHtmlEntities(val).replace(/["\\]/g, '\\$&').replace(/\n/g, ' ').slice(0, 200);
|
|
15
23
|
}
|
|
16
24
|
/**
|
|
17
25
|
* 어떤 입력이든 Stellavault 표준 포맷으로 변환하여 저장.
|
|
18
26
|
*/
|
|
19
27
|
export function ingest(vaultPath, input, folders = DEFAULT_FOLDERS) {
|
|
20
28
|
const stage = input.stage ?? 'fleeting';
|
|
21
|
-
const title = input.title ?? extractTitleFromContent(input.content, input.type);
|
|
29
|
+
const title = decodeHtmlEntities(input.title ?? extractTitleFromContent(input.content, input.type));
|
|
22
30
|
const tags = input.tags ?? extractAutoTags(input.content, input.type);
|
|
23
31
|
const source = input.source ?? (input.type === 'url' || input.type === 'youtube' ? input.content.split('\n')[0] : 'manual');
|
|
24
32
|
// 본문 정리
|
|
@@ -57,7 +65,9 @@ export function ingest(vaultPath, input, folders = DEFAULT_FOLDERS) {
|
|
|
57
65
|
}]);
|
|
58
66
|
indexCode = assignments.get(filePath);
|
|
59
67
|
}
|
|
60
|
-
catch {
|
|
68
|
+
catch (err) {
|
|
69
|
+
console.warn('[ingest] Index code skipped:', err instanceof Error ? err.message : err);
|
|
70
|
+
}
|
|
61
71
|
// Stellavault 표준 포맷으로 저장
|
|
62
72
|
let md = buildStandardNote({
|
|
63
73
|
title,
|
|
@@ -73,7 +83,9 @@ export function ingest(vaultPath, input, folders = DEFAULT_FOLDERS) {
|
|
|
73
83
|
try {
|
|
74
84
|
md = autoLink(md, vaultPath, title, folders);
|
|
75
85
|
}
|
|
76
|
-
catch {
|
|
86
|
+
catch (err) {
|
|
87
|
+
console.warn('[ingest] Auto-link skipped:', err instanceof Error ? err.message : err);
|
|
88
|
+
}
|
|
77
89
|
writeFileSync(fullPath, md, 'utf-8');
|
|
78
90
|
// 자동 compile: fleeting → wiki (rule-based, <100ms)
|
|
79
91
|
try {
|
|
@@ -166,7 +178,25 @@ function extractAutoTags(content, type) {
|
|
|
166
178
|
// 인라인 #태그 추출
|
|
167
179
|
const inline = content.match(/#([a-zA-Z가-힣][a-zA-Z0-9가-힣_-]{2,})/g) ?? [];
|
|
168
180
|
inline.forEach(t => tags.add(t.slice(1)));
|
|
169
|
-
|
|
181
|
+
// 스마트 자동 태깅: 문서 내용 분석 → 카테고리 분류
|
|
182
|
+
const lc = content.toLowerCase();
|
|
183
|
+
if (/회의|meeting|minutes|참석자|agenda/.test(lc))
|
|
184
|
+
tags.add('meeting-notes');
|
|
185
|
+
if (/기획|prd|요구사항|spec|feature|유저\s*스토리/.test(lc))
|
|
186
|
+
tags.add('planning');
|
|
187
|
+
if (/api|endpoint|서버|backend|database|쿼리/.test(lc))
|
|
188
|
+
tags.add('technical');
|
|
189
|
+
if (/디자인|design|ui|ux|figma|wireframe|mockup/.test(lc))
|
|
190
|
+
tags.add('design');
|
|
191
|
+
if (/논문|paper|abstract|methodology|conclusion|참고문헌/.test(lc))
|
|
192
|
+
tags.add('research');
|
|
193
|
+
if (/tutorial|강의|강좌|배우|learn|course/.test(lc))
|
|
194
|
+
tags.add('learning');
|
|
195
|
+
if (/경쟁|competitor|시장|market|swot|분석/.test(lc))
|
|
196
|
+
tags.add('analysis');
|
|
197
|
+
if (/일기|diary|journal|오늘|today|daily/.test(lc))
|
|
198
|
+
tags.add('journal');
|
|
199
|
+
return [...tags].slice(0, 15);
|
|
170
200
|
}
|
|
171
201
|
function cleanContent(content, type) {
|
|
172
202
|
if (type === 'url' || type === 'youtube') {
|
|
@@ -195,10 +225,10 @@ function buildStandardNote(params) {
|
|
|
195
225
|
'---',
|
|
196
226
|
`title: "${sanitizeYaml(params.title)}"`,
|
|
197
227
|
`type: ${params.stage}`,
|
|
198
|
-
`source: ${params.source}`,
|
|
228
|
+
`source: "${sanitizeYaml(params.source)}"`,
|
|
199
229
|
`input_type: ${params.inputType}`,
|
|
200
230
|
params.indexCode ? `zettel_id: "${params.indexCode}"` : null,
|
|
201
|
-
`tags: [${params.tags.map(t => `"${t}"`).join(', ')}]`,
|
|
231
|
+
`tags: [${params.tags.map(t => `"${sanitizeYaml(t)}"`).join(', ')}]`,
|
|
202
232
|
`created: ${params.created}`,
|
|
203
233
|
`summary: "${sanitizeYaml(params.body.slice(0, 100))}"`,
|
|
204
234
|
'---',
|
|
@@ -49,7 +49,17 @@ export function formatYouTubeNote(content) {
|
|
|
49
49
|
lines.push(`> ${nt('views')}: ${Number(content.viewCount).toLocaleString()}`);
|
|
50
50
|
lines.push(`> ${content.url}`, '');
|
|
51
51
|
if (content.summary) {
|
|
52
|
-
|
|
52
|
+
// 핵심 포인트를 bullet list로 포맷
|
|
53
|
+
const points = content.summary.split(/[.。!?]\s+/).filter(s => s.length > 15).slice(0, 5);
|
|
54
|
+
if (points.length > 1) {
|
|
55
|
+
lines.push(`## Key Points`, '');
|
|
56
|
+
for (const p of points)
|
|
57
|
+
lines.push(`- ${p.trim()}`);
|
|
58
|
+
lines.push('');
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
lines.push(`## ${nt('summary')}`, '', content.summary, '');
|
|
62
|
+
}
|
|
53
63
|
}
|
|
54
64
|
if (content.description.length > 30) {
|
|
55
65
|
lines.push(`## ${nt('description')}`, '', content.description.slice(0, 3000), '');
|
|
@@ -24,12 +24,15 @@ export function createAskTool(searchEngine, vaultPath) {
|
|
|
24
24
|
save: args.save ?? false,
|
|
25
25
|
vaultPath,
|
|
26
26
|
});
|
|
27
|
+
const sourceList = result.sources.slice(0, 5).map((s, i) => `${i + 1}. **${s.title}** (score: ${s.score})\n > ${s.snippet.slice(0, 120)}...`).join('\n');
|
|
27
28
|
const text = [
|
|
28
29
|
result.answer,
|
|
29
30
|
'',
|
|
30
|
-
|
|
31
|
+
'---',
|
|
32
|
+
`### Sources (${result.sources.length} documents)`,
|
|
33
|
+
sourceList,
|
|
31
34
|
'',
|
|
32
|
-
`
|
|
35
|
+
result.savedTo ? `Saved to: ${result.savedTo}` : '',
|
|
33
36
|
].filter(Boolean).join('\n');
|
|
34
37
|
return {
|
|
35
38
|
content: [{ type: 'text', text }],
|