stellavault 0.6.0 → 0.6.1

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 CHANGED
@@ -2,249 +2,297 @@
2
2
 
3
3
  > **Drop anything. It compiles itself into knowledge.** Claude remembers everything you know.
4
4
 
5
- Self-compiling Zettelkasten MCP server. Ingest PDFs, YouTube, documentsauto-organized into linked wiki. Claude accesses your entire knowledge base. **Your vault files are never modified.**
5
+ Self-compiling knowledge base with 3D neural graph, AI-powered search, and spaced repetition available as a **desktop app**, **CLI**, **Obsidian plugin**, and **MCP server**. Your vault files are never modified.
6
6
 
7
7
  <p align="center">
8
8
  <img src="images/screenshots/graph-main-2.png" alt="3D Knowledge Graph" width="800" />
9
9
  <br><em>Your vault as a neural network. Local-first, no cloud required.</em>
10
10
  </p>
11
11
 
12
- ## Two Core Ideas
12
+ ## Three Ways to Use Stellavault
13
13
 
14
- **1. "Drop it and forget it"** (Inspired by Karpathy's Self-Compiling Knowledge)
15
- ```
16
- Any input → auto-classify → raw/ → compile → wiki → connected knowledge
17
- ```
18
- PDF, DOCX, PPTX, XLSX, YouTube (with transcript), URL, text — everything goes through the same pipeline. You never manually organize.
14
+ ### 1. Desktop App (Recommended)
19
15
 
20
- **2. "Claude remembers what you know"** (MCP Integration)
21
- ```bash
22
- claude mcp add stellavault -- stellavault serve
23
- ```
24
- Claude searches, asks, drafts from your vault directly. Local-first no data leaves your machine.
16
+ Download and run no terminal needed.
17
+
18
+ | Platform | Download | Size |
19
+ |----------|----------|------|
20
+ | **Windows x64** | [Stellavault-win32-x64-0.1.0.zip](https://github.com/Evanciel/stellavault/releases/download/desktop-v0.1.0/Stellavault-win32-x64-0.1.0.zip) | 116 MB |
21
+ | **Linux x64** | [Stellavault-linux-x64-0.1.0.zip](https://github.com/Evanciel/stellavault/releases/download/desktop-v0.1.0/Stellavault-linux-x64-0.1.0.zip) | 107 MB |
22
+ | macOS | Coming soon (requires Apple code signing) | — |
25
23
 
26
- ## 5-Minute Setup
24
+ **What you get:**
25
+ - Full markdown editor with WYSIWYG toolbar
26
+ - File tree sidebar with search filter
27
+ - `[[wikilink]]` autocomplete as you type
28
+ - Multi-tab editing with Ctrl+S save
29
+ - 3D knowledge graph panel
30
+ - AI panel — semantic search, vault stats, re-index
31
+ - Backlinks panel — see who links to your note
32
+ - Quick Switcher (Ctrl+P) and Command Palette (Ctrl+Shift+P)
33
+ - Dark/light theme
34
+
35
+ ### 2. CLI + Web Graph
36
+
37
+ For developers and power users.
27
38
 
28
39
  ```bash
29
- npm install -g stellavault
30
- stellavault init # Interactive setup + vault indexing
31
- stellavault graph # Launch 3D graph + API server
40
+ npm install -g stellavault # or: npx stellavault
41
+ stellavault init # Interactive setup wizard
42
+ stellavault graph # Launch 3D graph in browser
32
43
  ```
33
44
 
34
- > **Prerequisites**: Node.js 20+
35
- >
36
- > **Upgrading from 0.4.x / 0.5.0 / 0.5.1 / 0.5.2 / 0.5.3?** Earlier releases had three showstopper packaging bugs: the `stellavault` bin shim wasn't created after install (0.4.x–0.5.1), the 3D graph UI wasn't bundled (0.4.x–0.5.2), and the SPA fallback was hijacking `/sw.js` and `/manifest.json` causing PWA install errors (0.5.3). **v0.5.4 fixes all of these and is verified end-to-end with Playwright (9/9 checks)** — install, bin link, graph UI, node click, federation toggle, PWA assets, zero console errors. Reinstall: `npm i -g stellavault@latest`.
45
+ > **Prerequisites**: Node.js 20+. Run `stellavault doctor` to diagnose setup issues.
46
+
47
+ ### 3. Obsidian Plugin
48
+
49
+ Use Stellavault intelligence inside Obsidian.
50
+
51
+ 1. Download from [stellavault-obsidian releases](https://github.com/Evanciel/stellavault-obsidian/releases/latest)
52
+ 2. Place `main.js`, `manifest.json`, `styles.css` in `.obsidian/plugins/stellavault/`
53
+ 3. Enable in Settings → Community plugins
54
+ 4. Start the API server: `npx stellavault graph` in your vault folder
55
+
56
+ **Features:** Semantic search modal, memory decay sidebar, learning path suggestions, auto-indexing on file changes.
57
+
58
+ ---
37
59
 
38
60
  ## The Pipeline
39
61
 
40
62
  ```
41
63
  Capture ──→ Organize ──→ Distill ──→ Express
42
64
 
43
- stellavault ingest <anything> # PDF, DOCX, URL, YouTube, text
44
- → auto-extract text # unpdf, mammoth, yt-dlp
45
- → raw/ (fleeting) # Zettelkasten inbox
46
- → compile → _wiki/ # Auto: concepts + backlinks
47
- → stellavault draft "topic" # Blog, report, or outline
65
+ Drop anything auto-extract raw/ → compile → _wiki/ → draft
48
66
  ```
49
67
 
50
- ### Ingest Anything
68
+ Inspired by [Karpathy's self-compiling knowledge](https://karpathy.ai/) architecture. Every input flows through the same four-stage pipeline.
69
+
70
+ ### Ingest Anything (14 formats)
51
71
 
52
72
  | Input | How |
53
73
  |-------|-----|
54
- | PDF, DOCX, PPTX, XLSX | `stellavault ingest report.pdf` — auto text extraction |
55
- | JSON, CSV, XML, YAML | `stellavault ingest data.json` — structured format preserved |
56
- | HTML, RTF | `stellavault ingest page.html` — clean text extraction |
74
+ | PDF, DOCX, PPTX, XLSX | `stellavault ingest report.pdf` |
75
+ | JSON, CSV, XML, YAML, HTML, RTF | `stellavault ingest data.json` |
57
76
  | YouTube | `stellavault ingest https://youtu.be/...` — transcript + timestamps |
58
- | URL | `stellavault ingest https://...` — HTML → clean text |
77
+ | URL | `stellavault ingest https://...` — HTML → markdown |
59
78
  | Text | `stellavault ingest "quick thought"` |
60
79
  | Folder | `stellavault ingest ./papers/` — batch all files |
61
- | Web UI | Drag & drop files in browser (mobile too) |
80
+ | Desktop / Web UI | Drag & drop files directly |
62
81
 
63
82
  ### Express: Get Knowledge Out
64
83
 
65
84
  ```bash
66
85
  stellavault draft "AI" # Rule-based scaffold (free)
67
- stellavault draft "AI" --ai # Claude API writes full draft ($0.03)
68
- stellavault draft "AI" --format report # Formal report format
69
- stellavault draft --format outline # All-knowledge outline
86
+ stellavault draft "AI" --ai # Claude API writes full draft
87
+ stellavault draft "AI" --format report # Formal report
88
+ stellavault draft --format instagram # Social media format
70
89
  ```
71
90
 
72
- Or in Claude Code: *"Write a blog post about machine learning from my notes"* — Claude uses MCP `generate-draft` tool (free, no API key).
91
+ ## MCP Integration (21 Tools)
73
92
 
74
- ## Self-Evolving Memory (Karpathy's Compounding Loop)
75
-
76
- ```
77
- Session → session-save → daily-log → flush → wiki
78
- ↑ ↓
79
- └──── Claude reads wiki via MCP (20 tools) ←─┘
80
- ```
81
-
82
- Every conversation makes your knowledge base smarter:
93
+ Connect Stellavault to Claude Code or Claude Desktop:
83
94
 
84
95
  ```bash
85
- # Auto-capture session summary to daily log
86
- echo "Decided to use JWT. Lesson: never store tokens in localStorage" | stellavault session-save
87
-
88
- # Flush daily logs → extract concepts → rebuild wiki
89
- stellavault flush
90
-
91
- # Or set up Claude Code hooks for full automation
92
- # See: docs/hooks-setup.md
93
- ```
94
-
95
- ## Daily Commands
96
-
97
- ```bash
98
- stellavault ask "What did I learn about X?" # Q&A from vault
99
- stellavault brief # Morning knowledge briefing
100
- stellavault decay # What's fading from memory?
101
- stellavault lint # Health score (0-100)
102
- stellavault learn # AI learning path
103
- stellavault flush # Daily logs → wiki compilation
104
- stellavault digest --visual # Weekly Mermaid chart report
96
+ claude mcp add stellavault -- stellavault serve
105
97
  ```
106
98
 
107
- ## MCP Tools (21)
99
+ Claude can now search, ask, draft, lint, and analyze your vault directly.
108
100
 
109
101
  | Tool | What it does |
110
102
  |------|-------------|
111
- | `search` | Hybrid search (BM25 + vector + RRF) |
112
- | `ask` | Q&A with optional vault filing |
113
- | `generate-draft` | Gather vault context for AI draft writing |
114
- | `get-document` | Full document with metadata |
115
- | `get-related` | Semantically similar documents |
116
- | `list-topics` | Topic cloud |
117
- | `get-decay-status` | Memory decay report |
118
- | `get-morning-brief` | Daily knowledge briefing |
119
- | `get-learning-path` | AI learning recommendations |
103
+ | `search` | Hybrid BM25 + vector + RRF search |
104
+ | `ask` | Q&A with vault-grounded answers |
105
+ | `generate-draft` | AI drafts from your knowledge |
106
+ | `get-decay-status` | Memory decay report (FSRS) |
120
107
  | `detect-gaps` | Knowledge gap analysis |
121
- | `get-evolution` | Semantic drift tracking |
122
- | `link-code` | Code-knowledge connections |
108
+ | `get-learning-path` | Personalized review recommendations |
123
109
  | `create-knowledge-node` | AI creates wiki-quality notes |
124
- | `create-knowledge-link` | AI connects existing notes |
125
- | `log-decision` / `find-decisions` | Decision journal |
126
- | `create-snapshot` / `load-snapshot` | Context snapshots |
127
- | `generate-claude-md` | Auto-generate CLAUDE.md |
128
- | `export` | JSON/CSV export |
129
- | `federated-search` | P2P federated search |
110
+ | `federated-search` | P2P search across connected vaults |
111
+ | + 13 more | Documents, topics, decisions, snapshots, export |
130
112
 
131
- ## Self-Evolving Commands
113
+ ## Intelligence
132
114
 
133
- ```bash
134
- stellavault session-save # Capture session summary to daily log
135
- stellavault flush # Daily logs wiki (Karpathy compile)
136
- stellavault promote note.md --to lit # Upgrade note stage
137
- stellavault autopilot # Full cycle: inbox compile lint → archive
138
- ```
115
+ | Feature | Command |
116
+ |---------|---------|
117
+ | Memory Decay | `stellavault decay` what you're forgetting (FSRS) |
118
+ | Gap Detection | `stellavault gaps` weak connections between topics |
119
+ | Contradictions | `stellavault contradictions` conflicting statements |
120
+ | Duplicates | `stellavault duplicates` — redundant notes |
121
+ | Learning Path | `stellavault learn` — AI review recommendations |
122
+ | Health Check | `stellavault lint` — overall knowledge score |
123
+ | Daily Brief | `stellavault brief` — morning knowledge briefing |
124
+ | Weekly Digest | `stellavault digest --visual` — Mermaid chart report |
139
125
 
140
- ## Zettelkasten (Luhmann + Karpathy)
126
+ ## Self-Evolving Memory
141
127
 
142
- ```bash
143
- stellavault fleeting "raw idea" # → raw/
144
- stellavault ingest report.pdf # → auto text extract → raw/
145
- stellavault compile # → raw/ → _wiki/ (concepts + backlinks)
146
- stellavault promote note.md --to permanent # Upgrade stage
147
- stellavault autopilot # Full cycle: inbox → compile → lint → archive
148
128
  ```
149
-
150
- - **3-stage flow**: fleeting → literature → permanent
151
- - **Luhmann index codes**: auto-assigned (1A 1A1)
152
- - **Frontmatter-first scanning**: 10x token reduction
153
- - **Configurable folders**: override raw/_wiki/_literature/ in `.stellavault.json`
154
-
155
- ```json
156
- {
157
- "vaultPath": "/path/to/vault",
158
- "folders": {
159
- "fleeting": "01-Inbox",
160
- "literature": "02-Reading",
161
- "permanent": "03-Notes",
162
- "wiki": "04-Wiki"
163
- }
164
- }
129
+ Session → session-save → daily-log → flush → wiki
130
+ ↑ ↓
131
+ └──── Claude reads wiki via MCP (21 tools) ←─┘
165
132
  ```
166
133
 
167
- ## Intelligence
168
-
169
- | Feature | Command |
170
- |---------|---------|
171
- | FSRS Decay | `sv decay` — spaced repetition memory tracking |
172
- | Gap Detection | `sv gaps` — missing connections between topics |
173
- | Contradictions | `sv contradictions` — conflicting statements |
174
- | Duplicates | `sv duplicates` — redundant notes |
175
- | Learning Path | `sv learn` — AI review recommendations |
176
- | Code Linker | MCP `link-code` — connect code to knowledge |
177
-
178
- ## 3D Visualization
179
-
180
- - Neural graph with cluster coloring
181
- - Constellation view (MST star patterns)
182
- - Heatmap overlay (activity score)
183
- - Timeline slider (creation/modification filter)
184
- - Decay overlay (fading knowledge)
185
- - **Multiverse view** — your vault as a universe in a P2P network
186
- - Dark/Light theme
187
- - Mobile responsive + PWA installable
188
-
189
- ## Multiverse — P2P Knowledge Federation
190
-
191
- <p align="center">
192
- <img src="images/screenshots/multiverse-view.png" alt="Multiverse View" width="800" />
193
- <br><em>"Your universe floats alone — for now."</em>
194
- </p>
134
+ Every conversation makes your knowledge base smarter. Set up [Claude Code hooks](docs/hooks-setup.md) for full automation.
195
135
 
196
- Your vault is a universe. Connect with others through P2P federation.
136
+ ## Zettelkasten Workflow
197
137
 
198
- **From the web UI** (easiest): open `stellavault graph`, then click the **Offline · Join** badge in the top-left header. Live peer count, one-click disconnect, and a popover showing connected peers.
138
+ Three-stage flow: **fleeting literature permanent** (Luhmann + Karpathy).
199
139
 
200
- **From the CLI**:
201
140
  ```bash
202
- stellavault federate join # Connect to the Stella Network
203
- stellavault federate status # See connected peers
141
+ stellavault fleeting "raw idea" # raw/
142
+ stellavault ingest report.pdf # auto-extract → raw/
143
+ stellavault compile # → raw/ → _wiki/ (concepts + backlinks)
144
+ stellavault promote note.md --to permanent # Upgrade stage
145
+ stellavault autopilot # Full cycle: inbox → compile → lint
204
146
  ```
205
147
 
206
- **How it works:**
207
- - **Hyperswarm P2P** — NAT-traversal mesh networking, no central server
208
- - **Embeddings only** — your original text never leaves your machine
148
+ Auto-assigned Luhmann index codes, frontmatter-first scanning, configurable folders.
149
+
150
+ ## P2P Federation (Multiverse)
151
+
152
+ Your vault is a universe. Connect with others through P2P federation.
153
+
154
+ - **Hyperswarm P2P** — NAT-traversal, no central server
155
+ - **Embeddings only** — original text never leaves your machine
209
156
  - **Differential privacy** — mathematical privacy guarantees
210
- - **Trust & reputation** — good knowledge earns credits
211
- - **Federated search** — search across connected vaults via MCP
212
157
 
213
- The Multiverse view shows your universe and connected peers as neighboring constellations in 3D. Click to explore their shared knowledge.
158
+ In the desktop app or web UI, click the **Federation badge** in the header to join/leave the Stella Network.
214
159
 
215
160
  ## Tech Stack
216
161
 
217
162
  | Layer | Tech |
218
163
  |-------|------|
164
+ | Desktop | Electron + React + TipTap + Zustand |
219
165
  | Runtime | Node.js 20+ (ESM, TypeScript) |
220
- | Vector Store | SQLite-vec (local, no server) |
221
- | Embedding | paraphrase-multilingual-MiniLM-L12-v2 (local, 50+ languages) |
166
+ | Vector Store | SQLite-vec (local, zero config) |
167
+ | Embedding | MiniLM-L12-v2 (local, 50+ languages) |
222
168
  | Search | BM25 + Cosine + RRF Fusion |
223
169
  | File Parsing | unpdf, mammoth, officeparser, SheetJS |
224
170
  | Memory | FSRS (Free Spaced Repetition Scheduler) |
225
171
  | 3D | React Three Fiber + Three.js |
226
172
  | AI | MCP (Model Context Protocol) + Anthropic SDK |
173
+ | P2P | Hyperswarm (optional) |
227
174
 
228
175
  ## Full Feature List
229
176
 
230
177
  | Category | Features |
231
178
  |----------|----------|
232
- | **Capture** | ingest 14 formats (PDF/DOCX/PPTX/XLSX/JSON/CSV/XML/HTML/YAML/RTF/YouTube/URL/text), batch folders, web drag & drop, Quick Capture, mobile PWA |
179
+ | **Desktop** | File tree sidebar, multi-tab editor, [[wikilink]] autocomplete, Quick Switcher, Command Palette, 3D graph panel, AI panel, backlinks, dark/light theme |
180
+ | **Capture** | 14 formats (PDF/DOCX/PPTX/XLSX/JSON/CSV/XML/HTML/YAML/RTF/YouTube/URL/text), batch folders, drag & drop, voice capture, Quick Capture |
233
181
  | **Organize** | Zettelkasten 3-stage, auto index codes, wikilink auto-connect, configurable folders |
234
182
  | **Distill** | compile (raw→wiki), lint (health score), gaps, contradictions, duplicates |
235
- | **Express** | draft (blog/report/outline/instagram/thread/script), blueprint, --ai, MCP generate-draft |
183
+ | **Express** | draft (blog/report/outline/instagram/thread/script), blueprint, --ai mode |
236
184
  | **Memory** | FSRS decay, session-save, flush, compounding loop, ADR templates |
237
185
  | **Search** | hybrid (BM25+vector+RRF), multilingual 50+, ask Q&A, quotes mode |
238
- | **Visualize** | 3D graph, heatmap, timeline, right-click context menu, TipTap WYSIWYG editor |
239
- | **AI Integration** | 21 MCP tools, Claude Code hooks, Anthropic SDK |
240
- | **Security** | DOMPurify, YAML sanitize, 50MB guard, SSRF protection |
241
- | **CLI** | 40+ commands, `sv` alias, batch ingest |
186
+ | **Visualize** | 3D graph, heatmap, timeline, constellation view, decay overlay, multiverse |
187
+ | **AI** | 21 MCP tools, Claude Code hooks, Anthropic SDK |
188
+ | **Federation** | Hyperswarm P2P, embedding-only sharing, differential privacy |
189
+ | **CLI** | 40+ commands, `sv` alias, `stellavault doctor` diagnostics |
242
190
 
243
- ## Security
191
+ ## Getting Started Guide
192
+
193
+ ### Desktop App (easiest)
194
+
195
+ 1. **Download** from [Releases](https://github.com/Evanciel/stellavault/releases/latest)
196
+ 2. **Unzip** to any folder
197
+ 3. **Run** `stellavault.exe` (Windows) — first launch asks you to pick your notes folder
198
+ 4. **Explore** — your notes appear in the sidebar, click to open in the editor
199
+ 5. **Search** — press `Ctrl+P` to quick-switch between notes, or open the AI panel (✦ button) for semantic search
200
+
201
+ ### CLI (for developers)
202
+
203
+ ```bash
204
+ # Step 1: Install
205
+ npm install -g stellavault
206
+
207
+ # Step 2: Setup (interactive wizard)
208
+ stellavault init
209
+ # → Asks for vault path → indexes all .md files → tests search
210
+
211
+ # Step 3: Daily use
212
+ stellavault search "machine learning" # Find notes
213
+ stellavault ingest paper.pdf # Add new knowledge
214
+ stellavault graph # Open 3D graph in browser
215
+ stellavault brief # Morning briefing
216
+ stellavault decay # What are you forgetting?
217
+
218
+ # Step 4: Connect to Claude
219
+ claude mcp add stellavault -- stellavault serve
220
+ # → Claude can now read your vault via MCP
221
+ ```
222
+
223
+ ### Obsidian Plugin
224
+
225
+ ```bash
226
+ # Step 1: Start the API server (keep running)
227
+ npx stellavault graph
244
228
 
245
- Your vault files are never modified. Stellavault is local-first — no data leaves your machine unless you explicitly use `--ai` (Anthropic API).
229
+ # Step 2: Install plugin
230
+ # Download main.js + manifest.json + styles.css from:
231
+ # https://github.com/Evanciel/stellavault-obsidian/releases/latest
232
+ # Place in: <vault>/.obsidian/plugins/stellavault/
233
+
234
+ # Step 3: Enable in Settings → Community Plugins → Stellavault
235
+
236
+ # Step 4: Use
237
+ # - Click brain icon (🧠) for semantic search
238
+ # - Cmd+Shift+D for memory decay panel
239
+ # - Cmd+Shift+L for learning path suggestions
240
+ ```
241
+
242
+ ### Quick Reference
243
+
244
+ | Action | Desktop | CLI | Obsidian |
245
+ |--------|---------|-----|----------|
246
+ | Search notes | Ctrl+P or AI panel | `stellavault search "query"` | 🧠 icon |
247
+ | Add a note | + Note button | `stellavault ingest "text"` | Normal editing |
248
+ | See 3D graph | ◉ button | `stellavault graph` | N/A |
249
+ | Check memory decay | AI panel → Memory | `stellavault decay` | Decay sidebar |
250
+ | Find duplicates | AI panel → Stats | `stellavault duplicates` | N/A |
251
+ | Generate draft | N/A (v0.2) | `stellavault draft "topic"` | N/A |
252
+ | Connect to Claude | N/A (v0.2) | `claude mcp add stellavault` | N/A |
253
+
254
+ ### Configuration
255
+
256
+ All settings live in `~/.stellavault.json`:
257
+
258
+ ```json
259
+ {
260
+ "vaultPath": "/path/to/your/notes",
261
+ "dbPath": "~/.stellavault/index.db",
262
+ "embedding": { "model": "local", "localModel": "all-MiniLM-L6-v2" },
263
+ "mcp": { "mode": "stdio", "port": 3333 }
264
+ }
265
+ ```
266
+
267
+ Run `stellavault doctor` anytime to check your setup.
268
+
269
+ ### Keyboard Shortcuts (Desktop)
270
+
271
+ | Shortcut | Action |
272
+ |----------|--------|
273
+ | `Ctrl+P` | Quick Switcher (fuzzy file search) |
274
+ | `Ctrl+Shift+P` | Command Palette (all actions) |
275
+ | `Ctrl+S` | Save current note |
276
+ | `Ctrl+B` | Toggle bold |
277
+ | `Ctrl+I` | Toggle italic |
278
+ | `Ctrl+E` | Toggle inline code |
279
+ | `[[` | Wikilink autocomplete |
280
+
281
+ ## Troubleshooting
282
+
283
+ ```bash
284
+ stellavault doctor # Check config, vault, DB, model, Node version
285
+ ```
286
+
287
+ Common issues:
288
+ - **"Command not found"** → Reinstall: `npm i -g stellavault@latest`
289
+ - **"API server not found"** → Start the server: `npx stellavault graph`
290
+ - **Empty graph** → Run `stellavault index` to re-index your vault
291
+ - **Slow first run** → The AI model downloads ~30MB on first use (one time only)
292
+
293
+ ## Security
246
294
 
247
- See [SECURITY.md](SECURITY.md) for full details.
295
+ Local-first — no data leaves your machine unless you explicitly use `--ai` (Anthropic API). Vault files are never modified. See [SECURITY.md](SECURITY.md).
248
296
 
249
297
  ## License
250
298
 
@@ -252,6 +300,7 @@ MIT — full source code available for audit.
252
300
 
253
301
  ## Links
254
302
 
303
+ - **[Download Desktop App](https://github.com/Evanciel/stellavault/releases/latest)**
255
304
  - [Landing Page](https://evanciel.github.io/stellavault/)
256
305
  - [Obsidian Plugin](https://github.com/Evanciel/stellavault-obsidian)
257
306
  - [npm](https://www.npmjs.com/package/stellavault)
package/SECURITY.md CHANGED
@@ -39,6 +39,29 @@ Stellavault is **local-first**. Your knowledge stays on your machine.
39
39
  - **URL validation**: Image URLs restricted to `https://` scheme
40
40
  - **SSRF protection**: Private/local IP addresses blocked for URL ingest
41
41
 
42
+ ## Desktop App Security (Electron)
43
+
44
+ - **Context Isolation**: enabled — renderer cannot access Node.js APIs
45
+ - **Sandbox**: enabled — renderer runs with reduced OS privileges
46
+ - **Node Integration**: disabled — no `require()` in renderer
47
+ - **IPC Allowlist**: explicit channel whitelist in preload (17 channels)
48
+ - **Path Validation**: all vault filesystem IPC handlers validate paths stay inside vault root
49
+ - **Auth Token**: API server generates per-session random token for all mutating endpoints
50
+ - **CSP**: strict Content Security Policy (no unsafe-eval in production)
51
+
52
+ ## Federation Security
53
+
54
+ - **Embeddings only**: original text never transmitted over the network
55
+ - **Buffer limits**: 1MB per connection, 64KB per message
56
+ - **Message validation**: schema checking on all incoming messages
57
+ - **Leave authentication**: leave messages only accepted from the owning connection
58
+ - **Differential privacy**: noise added to shared embeddings
59
+
60
+ ## Known Accepted Risks
61
+
62
+ - **LOW-03**: `data:` URIs allowed in desktop CSP for inline images in markdown editor
63
+ - **LOW-05**: Cloud sync uses Bearer token instead of AWS Signature v4 (R2-specific)
64
+
42
65
  ## Reporting Vulnerabilities
43
66
 
44
67
  Please report security issues to: https://github.com/Evanciel/stellavault/issues (label: security)
@@ -433,10 +433,15 @@ function createLocalEmbedder(modelName = "nomic-embed-text-v1.5") {
433
433
  const output = await pipeline(text, { pooling: "mean", normalize: true });
434
434
  return Array.from(output.data).slice(0, dims);
435
435
  },
436
- async embedBatch(texts) {
436
+ async embedBatch(texts, batchSize = 32) {
437
437
  const results = [];
438
- for (const text of texts) {
439
- results.push(await this.embed(text));
438
+ for (let i = 0; i < texts.length; i += batchSize) {
439
+ const batch = texts.slice(i, i + batchSize);
440
+ const output = await pipeline(batch, { pooling: "mean", normalize: true });
441
+ const flat = output.data;
442
+ for (let j = 0; j < batch.length; j++) {
443
+ results.push(Array.from(flat.slice(j * dims, (j + 1) * dims)));
444
+ }
440
445
  }
441
446
  return results;
442
447
  },
@@ -1971,6 +1976,104 @@ function extractAutoTags(content, type) {
1971
1976
  tags.add("analysis");
1972
1977
  if (/일기|diary|journal|오늘|today|daily/.test(lc))
1973
1978
  tags.add("journal");
1979
+ const stopWords = /* @__PURE__ */ new Set([
1980
+ "the",
1981
+ "a",
1982
+ "an",
1983
+ "and",
1984
+ "or",
1985
+ "but",
1986
+ "in",
1987
+ "on",
1988
+ "at",
1989
+ "to",
1990
+ "for",
1991
+ "of",
1992
+ "with",
1993
+ "by",
1994
+ "from",
1995
+ "is",
1996
+ "are",
1997
+ "was",
1998
+ "were",
1999
+ "be",
2000
+ "been",
2001
+ "being",
2002
+ "have",
2003
+ "has",
2004
+ "had",
2005
+ "do",
2006
+ "does",
2007
+ "did",
2008
+ "will",
2009
+ "would",
2010
+ "could",
2011
+ "should",
2012
+ "may",
2013
+ "might",
2014
+ "can",
2015
+ "this",
2016
+ "that",
2017
+ "these",
2018
+ "those",
2019
+ "it",
2020
+ "its",
2021
+ "they",
2022
+ "them",
2023
+ "their",
2024
+ "we",
2025
+ "our",
2026
+ "you",
2027
+ "your",
2028
+ "he",
2029
+ "she",
2030
+ "his",
2031
+ "her",
2032
+ "not",
2033
+ "no",
2034
+ "all",
2035
+ "each",
2036
+ "every",
2037
+ "both",
2038
+ "few",
2039
+ "more",
2040
+ "most",
2041
+ "other",
2042
+ "some",
2043
+ "such",
2044
+ "than",
2045
+ "too",
2046
+ "very",
2047
+ "just",
2048
+ "about",
2049
+ "above",
2050
+ "after",
2051
+ "before",
2052
+ "between",
2053
+ "into",
2054
+ "through",
2055
+ "during",
2056
+ "without",
2057
+ "also",
2058
+ "how",
2059
+ "what",
2060
+ "which",
2061
+ "who",
2062
+ "when",
2063
+ "where",
2064
+ "why",
2065
+ "if",
2066
+ "then"
2067
+ ]);
2068
+ const wordFreq = /* @__PURE__ */ new Map();
2069
+ const words = content.toLowerCase().replace(/[^a-z가-힣\s]/g, " ").split(/\s+/);
2070
+ for (const w of words) {
2071
+ if (w.length < 3 || stopWords.has(w))
2072
+ continue;
2073
+ wordFreq.set(w, (wordFreq.get(w) ?? 0) + 1);
2074
+ }
2075
+ const topKeywords = [...wordFreq.entries()].filter(([, count]) => count >= 2).sort((a, b) => b[1] - a[1]).slice(0, 3).map(([word]) => word);
2076
+ topKeywords.forEach((k) => tags.add(k));
1974
2077
  return [...tags].slice(0, 15);
1975
2078
  }
1976
2079
  function cleanContent(content, type) {
@@ -2499,8 +2602,11 @@ var init_node = __esm({
2499
2602
  break;
2500
2603
  }
2501
2604
  case "leave": {
2502
- this.peers.delete(msg.peerId);
2503
- this.emit("peer_left", { peerId: msg.peerId });
2605
+ const legit = this.peers.get(msg.peerId);
2606
+ if (legit && legit.conn === conn) {
2607
+ this.peers.delete(msg.peerId);
2608
+ this.emit("peer_left", { peerId: msg.peerId });
2609
+ }
2504
2610
  break;
2505
2611
  }
2506
2612
  }
@@ -4506,7 +4612,7 @@ function createMcpServer(options) {
4506
4612
  const askTool = createAskTool(searchEngine, vaultPath);
4507
4613
  const generateDraftTool = createGenerateDraftTool(searchEngine, vaultPath);
4508
4614
  const agenticTools = embedder ? createAgenticGraphTools(store, embedder, vaultPath) : [];
4509
- const server = new Server({ name: "stellavault", version: "0.2.0" }, { capabilities: { tools: {} } });
4615
+ const server = new Server({ name: "stellavault", version: "0.6.0" }, { capabilities: { tools: {} } });
4510
4616
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
4511
4617
  tools: [
4512
4618
  searchToolDef,
@@ -4847,6 +4953,7 @@ Author: ${pack2.author}`,
4847
4953
  init_graph_data();
4848
4954
  import express from "express";
4849
4955
  import cors from "cors";
4956
+ import { randomBytes as randomBytes2 } from "node:crypto";
4850
4957
 
4851
4958
  // packages/core/dist/intelligence/duplicate-detector.js
4852
4959
  async function detectDuplicates(store, threshold = 0.88, limit = 20) {
@@ -4898,8 +5005,51 @@ function cosineSimilarity2(a, b) {
4898
5005
  function createApiServer(options) {
4899
5006
  const { store, searchEngine, port = 3333, vaultName = "", vaultPath = "", decayEngine, graphUiPath } = options;
4900
5007
  const app = express();
4901
- app.use(cors({ origin: ["http://localhost:5173", "http://127.0.0.1:5173"] }));
5008
+ const authToken = randomBytes2(32).toString("hex");
5009
+ app.use((_req, res, next) => {
5010
+ res.setHeader("X-Content-Type-Options", "nosniff");
5011
+ res.setHeader("X-Frame-Options", "DENY");
5012
+ next();
5013
+ });
5014
+ const allowedOrigins = [
5015
+ "http://localhost:5173",
5016
+ "http://127.0.0.1:5173",
5017
+ `http://127.0.0.1:${port}`,
5018
+ `http://localhost:${port}`
5019
+ ];
5020
+ app.use(cors({ origin: allowedOrigins }));
4902
5021
  app.use(express.json());
5022
+ const rateLimiter = /* @__PURE__ */ new Map();
5023
+ function rateLimit(key, windowMs = 6e4, maxHits = 30) {
5024
+ const now = Date.now();
5025
+ const hits = (rateLimiter.get(key) ?? []).filter((t2) => now - t2 < windowMs);
5026
+ if (hits.length >= maxHits)
5027
+ return false;
5028
+ hits.push(now);
5029
+ rateLimiter.set(key, hits);
5030
+ return true;
5031
+ }
5032
+ function requireAuth(req, res, next) {
5033
+ const token = req.headers["x-stellavault-token"];
5034
+ if (token === authToken)
5035
+ return next();
5036
+ if (req.query.token === authToken)
5037
+ return next();
5038
+ res.status(403).json({ error: "Invalid or missing auth token. GET /api/token first." });
5039
+ }
5040
+ app.get("/api/token", (_req, res) => {
5041
+ res.json({ token: authToken });
5042
+ });
5043
+ function assertNotPrivateUrl(url) {
5044
+ const parsed = new URL(url);
5045
+ if (!["http:", "https:"].includes(parsed.protocol))
5046
+ throw new Error("Only http/https URLs allowed");
5047
+ const host = parsed.hostname.toLowerCase();
5048
+ if (host === "localhost" || host === "127.0.0.1" || host === "::1" || host.endsWith(".local") || host.startsWith("192.168.") || host.startsWith("10.") || // Fix: 172.16.0.0/12 covers 172.16.* through 172.31.*
5049
+ /^172\.(1[6-9]|2\d|3[01])\./.test(host) || host.startsWith("169.254.") || host === "0.0.0.0") {
5050
+ throw new Error("Internal URLs not allowed");
5051
+ }
5052
+ }
4903
5053
  if (graphUiPath) {
4904
5054
  app.use(express.static(graphUiPath, { index: "index.html", extensions: ["html"] }));
4905
5055
  }
@@ -4935,7 +5085,7 @@ function createApiServer(options) {
4935
5085
  res.json(reindexProgress);
4936
5086
  });
4937
5087
  let isReindexing = false;
4938
- app.post("/api/reindex", async (_req, res) => {
5088
+ app.post("/api/reindex", requireAuth, async (_req, res) => {
4939
5089
  if (isReindexing) {
4940
5090
  res.json({ success: false, error: "Reindexing already in progress", progress: reindexProgress });
4941
5091
  return;
@@ -4967,7 +5117,7 @@ function createApiServer(options) {
4967
5117
  });
4968
5118
  } catch (err) {
4969
5119
  console.error("[reindex]", err);
4970
- res.status(500).json({ error: `Reindex failed: ${err?.message ?? String(err)}` });
5120
+ res.status(500).json({ error: "Reindex failed" });
4971
5121
  } finally {
4972
5122
  isReindexing = false;
4973
5123
  reindexProgress = { active: false, current: 0, total: 0, phase: "done" };
@@ -4975,8 +5125,8 @@ function createApiServer(options) {
4975
5125
  });
4976
5126
  app.get("/api/search", async (req, res) => {
4977
5127
  try {
4978
- const query = String(req.query.q || "");
4979
- const limit = parseInt(String(req.query.limit || "10"), 10);
5128
+ const query = String(req.query.q || "").slice(0, 1e3);
5129
+ const limit = Math.min(parseInt(String(req.query.limit || "10"), 10), 100);
4980
5130
  if (!query) {
4981
5131
  res.json({ results: [], query: "" });
4982
5132
  return;
@@ -5008,7 +5158,7 @@ function createApiServer(options) {
5008
5158
  });
5009
5159
  app.get("/api/document/:id", async (req, res) => {
5010
5160
  try {
5011
- const doc = await store.getDocument(req.params.id);
5161
+ const doc = await store.getDocument(String(req.params.id));
5012
5162
  if (!doc) {
5013
5163
  res.status(404).json({ error: "Not found" });
5014
5164
  return;
@@ -5129,9 +5279,9 @@ function createApiServer(options) {
5129
5279
  res.status(500).json({ error: "Internal server error" });
5130
5280
  }
5131
5281
  });
5132
- app.put("/api/document/:id", async (req, res) => {
5282
+ app.put("/api/document/:id", requireAuth, async (req, res) => {
5133
5283
  try {
5134
- const { id } = req.params;
5284
+ const id = String(req.params.id);
5135
5285
  const { title, content, tags } = req.body;
5136
5286
  const doc = await store.getDocument(id);
5137
5287
  if (!doc) {
@@ -5151,7 +5301,8 @@ function createApiServer(options) {
5151
5301
  updated = updated.replace(/^title:\s*.+$/m, `title: "${title.replace(/"/g, "''")}"`);
5152
5302
  }
5153
5303
  if (tags) {
5154
- const tagStr = `tags: [${tags.map((t2) => `"${t2}"`).join(", ")}]`;
5304
+ const safeTags = tags.map((t2) => t2.replace(/["\\\n\r\]]/g, "").trim()).filter(Boolean);
5305
+ const tagStr = `tags: [${safeTags.map((t2) => `"${t2}"`).join(", ")}]`;
5155
5306
  if (updated.match(/^tags:\s*.+$/m)) {
5156
5307
  updated = updated.replace(/^tags:\s*.+$/m, tagStr);
5157
5308
  }
@@ -5176,12 +5327,12 @@ function createApiServer(options) {
5176
5327
  res.json({ success: true, id, title: title ?? doc.title });
5177
5328
  } catch (err) {
5178
5329
  console.error("[edit]", err);
5179
- res.status(500).json({ error: err?.message ?? "Edit failed" });
5330
+ res.status(500).json({ error: "Edit failed" });
5180
5331
  }
5181
5332
  });
5182
- app.delete("/api/document/:id", async (req, res) => {
5333
+ app.delete("/api/document/:id", requireAuth, async (req, res) => {
5183
5334
  try {
5184
- const { id } = req.params;
5335
+ const id = String(req.params.id);
5185
5336
  const doc = await store.getDocument(id);
5186
5337
  if (!doc) {
5187
5338
  res.status(404).json({ error: "Document not found" });
@@ -5201,7 +5352,7 @@ function createApiServer(options) {
5201
5352
  res.json({ success: true, id, deleted: doc.filePath });
5202
5353
  } catch (err) {
5203
5354
  console.error("[delete]", err);
5204
- res.status(500).json({ error: err?.message ?? "Delete failed" });
5355
+ res.status(500).json({ error: "Delete failed" });
5205
5356
  }
5206
5357
  });
5207
5358
  app.get("/api/ask", async (req, res) => {
@@ -5223,7 +5374,7 @@ function createApiServer(options) {
5223
5374
  res.status(500).json({ error: "Ask failed" });
5224
5375
  }
5225
5376
  });
5226
- app.post("/api/ingest", async (req, res) => {
5377
+ app.post("/api/ingest", requireAuth, async (req, res) => {
5227
5378
  try {
5228
5379
  const { input, type, tags, title, stage, locale } = req.body;
5229
5380
  if (locale) {
@@ -5260,6 +5411,12 @@ function createApiServer(options) {
5260
5411
  }
5261
5412
  }
5262
5413
  } else if (input.startsWith("http")) {
5414
+ try {
5415
+ assertNotPrivateUrl(input);
5416
+ } catch (e) {
5417
+ res.status(400).json({ error: e.message });
5418
+ return;
5419
+ }
5263
5420
  try {
5264
5421
  const resp = await fetch(input, { signal: AbortSignal.timeout(8e3) });
5265
5422
  const html = await resp.text();
@@ -5313,7 +5470,7 @@ function createApiServer(options) {
5313
5470
  res.status(500).json({ error: "Ingest failed" });
5314
5471
  }
5315
5472
  });
5316
- app.post("/api/ingest/file", async (req, res) => {
5473
+ app.post("/api/ingest/file", requireAuth, async (req, res) => {
5317
5474
  try {
5318
5475
  const multer = (await import("multer")).default;
5319
5476
  const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 50 * 1024 * 1024 } });
@@ -5354,7 +5511,8 @@ function createApiServer(options) {
5354
5511
  const { writeFileSync: writeFileSync21, unlinkSync } = await import("node:fs");
5355
5512
  const { join: join30 } = await import("node:path");
5356
5513
  const { tmpdir } = await import("node:os");
5357
- const tmpPath = join30(tmpdir(), `sv-upload-${Date.now()}-${file.originalname}`);
5514
+ const safeName = (file.originalname ?? "upload").replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 100);
5515
+ const tmpPath = join30(tmpdir(), `sv-upload-${Date.now()}-${safeName}`);
5358
5516
  writeFileSync21(tmpPath, file.buffer);
5359
5517
  const { extractFileContent: extractFileContent2, isBinaryFormat: isBinaryFormat2 } = await Promise.resolve().then(() => (init_file_extractors(), file_extractors_exports));
5360
5518
  const ext = file.originalname.split(".").pop()?.toLowerCase() ?? "";
@@ -5416,7 +5574,7 @@ function createApiServer(options) {
5416
5574
  wordCount: result.wordCount
5417
5575
  });
5418
5576
  } catch (err) {
5419
- res.status(500).json({ error: err instanceof Error ? err.message : "Processing failed" });
5577
+ res.status(500).json({ error: "Processing failed" });
5420
5578
  }
5421
5579
  });
5422
5580
  } catch (err) {
@@ -5517,7 +5675,7 @@ function createApiServer(options) {
5517
5675
  res.status(500).json({ error: "Internal server error" });
5518
5676
  }
5519
5677
  });
5520
- app.post("/api/duplicates/merge", async (req, res) => {
5678
+ app.post("/api/duplicates/merge", requireAuth, async (req, res) => {
5521
5679
  try {
5522
5680
  const { docAId, docBId } = req.body;
5523
5681
  if (!docAId || !docBId) {
@@ -5564,7 +5722,7 @@ ${removed.content}`;
5564
5722
  res.status(500).json({ error: "Merge failed" });
5565
5723
  }
5566
5724
  });
5567
- app.post("/api/gaps/create-bridge", async (req, res) => {
5725
+ app.post("/api/gaps/create-bridge", requireAuth, async (req, res) => {
5568
5726
  try {
5569
5727
  const { clusterA, clusterB } = req.body;
5570
5728
  if (!clusterA || !clusterB) {
@@ -5715,7 +5873,6 @@ ${removed.content}`;
5715
5873
  if (month)
5716
5874
  monthlyActivity[month] = (monthlyActivity[month] ?? 0) + 1;
5717
5875
  }
5718
- res.setHeader("Access-Control-Allow-Origin", "*");
5719
5876
  res.json({
5720
5877
  name: vaultName || "Knowledge Vault",
5721
5878
  stats: {
@@ -5768,7 +5925,6 @@ ${removed.content}`;
5768
5925
  ]
5769
5926
  };
5770
5927
  });
5771
- res.setHeader("Access-Control-Allow-Origin", "*");
5772
5928
  res.json({
5773
5929
  nodes: embedNodes,
5774
5930
  edges: selectedEdges,
@@ -5787,7 +5943,7 @@ ${removed.content}`;
5787
5943
  result: "",
5788
5944
  output: ""
5789
5945
  };
5790
- app.post("/api/sync", async (_req, res) => {
5946
+ app.post("/api/sync", requireAuth, async (_req, res) => {
5791
5947
  if (syncState.running) {
5792
5948
  res.json({ success: false, error: "Sync already running", state: syncState });
5793
5949
  return;
@@ -5807,7 +5963,7 @@ ${removed.content}`;
5807
5963
  return;
5808
5964
  }
5809
5965
  syncState = { running: true, startedAt: (/* @__PURE__ */ new Date()).toISOString(), completedAt: "", result: "", output: "" };
5810
- const child = spawn3("node", [syncScript], { cwd: syncDir, stdio: ["ignore", "pipe", "pipe"], shell: true });
5966
+ const child = spawn3("node", [syncScript], { cwd: syncDir, stdio: ["ignore", "pipe", "pipe"], shell: false });
5811
5967
  let output = "";
5812
5968
  child.stdout.on("data", (d) => {
5813
5969
  output += d.toString();
@@ -5831,7 +5987,7 @@ ${removed.content}`;
5831
5987
  app.get("/api/sync/status", (_req, res) => {
5832
5988
  res.json(syncState);
5833
5989
  });
5834
- app.post("/api/clip", async (req, res) => {
5990
+ app.post("/api/clip", requireAuth, async (req, res) => {
5835
5991
  try {
5836
5992
  const { url } = req.body;
5837
5993
  if (!url) {
@@ -5839,18 +5995,9 @@ ${removed.content}`;
5839
5995
  return;
5840
5996
  }
5841
5997
  try {
5842
- const parsed = new URL(url);
5843
- if (!["http:", "https:"].includes(parsed.protocol)) {
5844
- res.status(400).json({ error: "Only http/https URLs allowed" });
5845
- return;
5846
- }
5847
- const host = parsed.hostname.toLowerCase();
5848
- if (host === "localhost" || host === "127.0.0.1" || host === "::1" || host.endsWith(".local") || host.startsWith("192.168.") || host.startsWith("10.") || host.startsWith("172.16.")) {
5849
- res.status(400).json({ error: "Internal URLs not allowed" });
5850
- return;
5851
- }
5852
- } catch {
5853
- res.status(400).json({ error: "Invalid URL" });
5998
+ assertNotPrivateUrl(url);
5999
+ } catch (e) {
6000
+ res.status(400).json({ error: e.message });
5854
6001
  return;
5855
6002
  }
5856
6003
  const isYT = /youtube\.com\/watch|youtu\.be\//.test(url);
@@ -6438,7 +6585,7 @@ async function captureVoice(audioPath, options) {
6438
6585
  }
6439
6586
 
6440
6587
  // packages/core/dist/cloud/sync.js
6441
- import { createCipheriv, createDecipheriv, randomBytes as randomBytes2, createHash as createHash5 } from "node:crypto";
6588
+ import { createCipheriv, createDecipheriv, randomBytes as randomBytes3, createHash as createHash5 } from "node:crypto";
6442
6589
  import { readFileSync as readFileSync14, writeFileSync as writeFileSync14, existsSync as existsSync14, mkdirSync as mkdirSync14, chmodSync as chmodSync2 } from "node:fs";
6443
6590
  import { join as join15 } from "node:path";
6444
6591
  import { homedir as homedir7 } from "node:os";
@@ -6446,7 +6593,7 @@ var CLOUD_DIR = join15(homedir7(), ".stellavault", "cloud");
6446
6593
  var KEY_FILE = join15(CLOUD_DIR, "encryption.key");
6447
6594
  var SYNC_STATE_FILE = join15(CLOUD_DIR, "sync-state.json");
6448
6595
  function encrypt(data, key) {
6449
- const iv = randomBytes2(16);
6596
+ const iv = randomBytes3(16);
6450
6597
  const cipher = createCipheriv("aes-256-gcm", key, iv);
6451
6598
  const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
6452
6599
  const tag = cipher.getAuthTag();
@@ -6467,7 +6614,7 @@ function getOrCreateEncryptionKey(userKey) {
6467
6614
  if (existsSync14(KEY_FILE)) {
6468
6615
  return Buffer.from(readFileSync14(KEY_FILE, "utf-8").trim(), "hex");
6469
6616
  }
6470
- const key = randomBytes2(32);
6617
+ const key = randomBytes3(32);
6471
6618
  writeFileSync14(KEY_FILE, key.toString("hex"), { encoding: "utf-8", mode: 384 });
6472
6619
  try {
6473
6620
  chmodSync2(KEY_FILE, 384);
@@ -6807,10 +6954,9 @@ async function graphCommand() {
6807
6954
  const hasDevGraph = existsSync15(resolve11(devGraphDir, "package.json"));
6808
6955
  if (hasDevGraph) {
6809
6956
  console.error(chalk5.dim(" \u{1F680} Starting Vite dev server..."));
6810
- const vite = spawn("npx", ["vite", "--host"], {
6957
+ const vite = spawn(process.platform === "win32" ? "npx.cmd" : "npx", ["vite", "--host"], {
6811
6958
  cwd: devGraphDir,
6812
- stdio: ["ignore", "pipe", "pipe"],
6813
- shell: true
6959
+ stdio: ["ignore", "pipe", "pipe"]
6814
6960
  });
6815
6961
  vite.stderr.on("data", (data) => {
6816
6962
  const line = data.toString();
@@ -7048,7 +7194,7 @@ function runScript(scriptPath, cwd) {
7048
7194
  const child = spawn2("node", [scriptPath], {
7049
7195
  cwd,
7050
7196
  stdio: "inherit",
7051
- shell: true
7197
+ shell: false
7052
7198
  });
7053
7199
  child.on("close", (code) => {
7054
7200
  if (code === 0)
@@ -9337,7 +9483,7 @@ if (nodeVersion < 20) {
9337
9483
  process.exit(1);
9338
9484
  }
9339
9485
  var program = new Command();
9340
- var SV_VERSION = true ? "0.6.0" : "0.0.0-dev";
9486
+ var SV_VERSION = true ? "0.6.1" : "0.0.0-dev";
9341
9487
  program.name("stellavault").description("Stellavault \u2014 Self-compiling knowledge base for your Obsidian vault").version(SV_VERSION).option("--json", "Output in JSON format (for scripting)").option("--quiet", "Suppress non-essential output");
9342
9488
  program.command("init").description("Interactive setup wizard \u2014 get started in 3 minutes").action(initCommand);
9343
9489
  program.command("doctor").description("Diagnose setup issues (config, vault, DB, model, Node version)").action(doctorCommand);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stellavault",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
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",