Tusk's Vault

Dependencies โ€” Tusk's Vault

Every package Vault depends on, with the rationale for each choice. Skip to Why these choices for the high-level reasoning.


๐Ÿงฑ Base runtime dependencies

These are required for Vault to run at all.

PackageVersionPurpose
@google/genai^1.29.0Gemini provider adapter (src/server/llm/gemini.ts)
openai^6.38.0OpenRouter speaks the OpenAI wire format, so its adapter uses this SDK (src/server/llm/openrouter.ts). Claude and GPT models are reached through it.
discord.js^14.25.1Discord bot client (src/server/discord/)
@huggingface/transformers^4.2.0Local MiniLM embeddings on CPU (src/server/embeddings/)
express^4.21.2HTTP server framework
vite^6.2.0Build tool + dev server (also used as Express middleware in dev)
@vitejs/plugin-react^5.0.4Vite plugin for React JSX/TSX
@tailwindcss/vite^4.1.14Tailwind CSS 4 build integration
react^19.0.0Dashboard UI
react-dom^19.0.0Dashboard UI
react-markdown^10.1.0Renders these very docs inside the dashboard's Help โ†’ Docs viewer (DocsViewer.tsx)
remark-gfm^4.0.1GitHub-flavoured markdown extensions for react-markdown โ€” tables and task lists
rehype-raw^7.0.0Lets the same renderer handle the raw <details class="docs-section"> blocks these pages are written with
motion^12.23.24UI animations (transitions, candle loader)
lucide-react^0.546.0Icon set used throughout the dashboard
mammoth^1.12.0DOCX โ†’ text extraction
pdf-parse^2.4.5PDF โ†’ text extraction
multer^2.1.1Multipart file upload handling
dotenv^17.2.3Load .env.local at boot
env-paths^3.0.0Cross-platform per-user config dir resolver โ€” where the encrypted key store, personas, the OpenRouter catalogue cache, the model-availability cache, the paired-MCP-client list and the vault map live
@types/multer^2.1.0Type definitions for multer

๐Ÿ”Œ What the optional connections need

None. Every provider Vault talks to reuses what is already here.

ConnectionExtra dependencies pulled inWhy
๐Ÿฆ™ OllamaNoneA raw fetch to http://localhost:11434 โ€” no SDK required. See src/server/llm/ollama.ts.
๐ŸŒ OpenRouterNoneWire-compatible with the OpenAI API, so it uses the openai SDK already in the base table โ€” the one dependency, shared. See src/server/llm/openrouter.ts.
๐Ÿ–ฅ Claude CodeNoneA child process running the CLI you already installed.
๐ŸŽญ PersonasNoneReuses the existing LLM adapter (via the active provider) for the AI-generate feature. Stores user personas in a plain JSON file via the base fs module.
๐ŸŽฒ Foundry VTT / MCPNoneThe bridge is hand-rolled JSON-RPC over Express โ€” no MCP SDK. Three tools and one transport is less code than wiring a framework in. See Foundry contract.

A new provider that needs a package should declare it in this table and add it to package.json like any other dependency. Vault deliberately does not lazy-load packages at runtime โ€” that would make whether a feature works depend on whether the network is up.

๐Ÿงช Dev dependencies

PackageVersionPurpose
@types/express^4.17.21Type definitions for Express
@types/node^22.14.0Type definitions for Node
autoprefixer^10.4.21PostCSS plugin for vendor prefixes
rehype-stringify^10.0.1Markdown โ†’ HTML, for the docs-site builder (maintainer tooling, not shipped)
remark-parse^11.0.0Markdown parser for the same builder
remark-rehype^11.1.2Bridges the markdown AST to the HTML AST for the same builder
tailwindcss^4.1.14CSS framework
tsx^4.21.0TypeScript runner โ€” npm start is tsx server.ts
typescript~5.8.2TypeScript compiler (npm run lint = tsc --noEmit)
unified^11.0.5The pipeline the three remark/rehype plugins above plug into
vite^6.2.0Build tool (also listed in runtime โ€” npm handles the dedup)
vitest^4.1.6Test runner. npm test covers src/server/**/*.test.ts (path resolution, key store, prompt assembly, the Obsidian reader's read-only guard, atomic writes, security middleware, the MCP bridge and its Foundry wire contract), src/lib/**/*.test.ts, and scripts/**/*.test.mjs. fileParallelism is off so files run sequentially.

๐Ÿค” Why these choices

Why only two cloud LLM SDKs?

Vault is LLM-agnostic by design (see How it's built), and the obvious shape is one SDK per lab. It isn't the one used: OpenRouter reaches Claude and GPT on one key and one bill, so a per-lab SDK would add a dependency, a key slot and a bill to reach models already reachable.

What is left is @google/genai and openai. Gemini keeps a direct slot deliberately: it is meaningfully cheaper called directly than through the aggregator. openai stays because OpenRouter is the OpenAI wire format โ€” that dependency is not drift, it is the OpenRouter client.

Note that both are ordinary top-level imports, not lazy ones: registry.ts imports every adapter statically, so both SDKs load at boot regardless of which provider you have selected. That is the deliberate trade โ€” see the note on lazy-loading in the connections table above. Adding a fifth provider is one new file in src/server/llm/ implementing the LlmAdapter interface, plus a registry entry.

Why MiniLM via @huggingface/transformers?

We needed local, CPU-only embeddings. MiniLM-L6-v2 produces 384-dim vectors that are good enough for short-question semantic matching (clarification retrieval), runs in ~50โ€“300 ms per embedding on CPU, and weighs ~25 MB cached. The transformers.js port lets us run it without a Python sidecar.

Alternatives considered: OpenAI embeddings (cloud round-trip, conflicts with local-first), e5-small (similar quality, larger model), nomic-embed-text (better quality, much larger). MiniLM hit the sweet spot โ€” clarifications are short, the embedding step has to be invisible to the user, and we can't assume a GPU.

Why pdf-parse + mammoth?

  • pdf-parse takes a Buffer โ€” exactly what the multer upload flow and fs.readFileSync already hand it โ€” and needs no system-level PDF library, so an install doesn't turn into a Poppler hunt.
  • mammoth flattens complex tables, nested lists and embedded styles down to plain text, which is the only thing an LLM prompt wants from a DOCX.

Both are widely used and actively maintained. Neither sandboxes documents, and neither has a per-parse timeout or size cap today โ€” see Known issues.

Why no database?

The data model is small files per concern, split by what should survive what.

Campaign state lives inside the resolved Tusks-Lore/ folder, so it survives a clean reinstall of the app:

  • clarifications.json + clarifications.embeddings.json โ€” corrections + vectors.
  • lore_gaps.json โ€” unanswered questions.

Per-install state lives in the platform config directory โ€” per-machine, rebuildable, and deliberately not following a campaign folder around:

  • keys.enc + keys.salt โ€” your keys, encrypted (AES-256-GCM; the key is scrypt-derived from a stable machine identity). keys/crypto.ts is honest about what this is: obfuscation with authenticated encryption, not high-grade cryptography โ€” anything running as you on this machine can derive the key. What it defeats is the realistic threat, a backup client or a support bundle hoovering up a file literally named api-keys.json.
  • personas.user.json โ€” your own personas.
  • The OpenRouter model-catalogue cache, the model-availability cache, and the per-vault AI vault map.
  • mcp-clients.json โ€” paired MCP clients, tokens stored only as SHA-256 hashes.

And settings.json stays at the repo root for dashboard preferences. Every one of these writes goes through the atomic-write helper, and on POSIX the key store is 0o600.

For a single-user local-first tool with hundreds (not millions) of records, JSON files are simpler, debuggable by hand, and have zero runtime cost. If your campaign grows to thousands of lore documents and the JSON stores get unwieldy, that's the trigger to add a real vector store / SQLite layer โ€” tracked in the Roadmap.

Why Vite as Express middleware in dev?

Vault runs one HTTP server, not two. Vite is registered as Express middleware in development mode (see src/server/index.ts), so the React dashboard and the bot's HTTP API share the same socket. That means:

  • No "Vite is on 5173, server is on 3000, why don't they talk to each other" confusion.
  • One port to bind, one port to fall back from.
  • Same fetch("/api/...") from the dashboard, no proxy config.

In production (npm run build), Vite drops out entirely โ€” Express serves the prebuilt static bundle directly.

Why React 19 + Tailwind 4 + Motion?

  • React 19 for the concurrent rendering improvements (matters for the multi-tab dashboard).
  • Tailwind 4 because it's the current major and the new Vite plugin is meaningfully faster.
  • Motion (the rebranded Framer Motion) for the candle loaders, rune dividers, and panel transitions โ€” light footprint, mature API.

If you're forking and want a smaller bundle, the easiest cut is Motion. The fantasy animations are 60-80 KB; the rest of the dashboard is bone-stock React.

Why env-paths?

Per-install state โ€” the encrypted key store, personas, caches โ€” lives in a cross-platform per-user config directory rather than the repo. Vault asks for envPaths("tusks-vault", { suffix: "" }).config (the empty suffix drops env-paths' default -nodejs), which resolves to:

  • Windows: %APPDATA%\tusks-vault\Config\
  • macOS: ~/Library/Preferences/tusks-vault/
  • Linux: $XDG_CONFIG_HOME/tusks-vault/, defaulting to ~/.config/tusks-vault/

This means your keys and settings survive a git clean -fdx and can't be accidentally committed. TUSKS_VAULT_CONFIG_DIR overrides it, for a portable install or a test.

๐Ÿ”’ Security posture of dependencies

Check, don't assume. A "0 vulnerabilities as of this commit" line in a doc is true for about a week โ€” advisories land against dependencies that have not changed. The current answer is whatever npm audit --omit=dev says on your checkout, and the honest thing for this page to record is the policy, not a number that quietly rots.

The gate is --omit=dev --audit-level=high: shipping dependencies only, high and critical only. Dev-only tooling is excluded deliberately โ€” a vulnerability in a test runner is not a vulnerability in what a user installs โ€” and low/moderate findings in a locally-bound, single-user tool do not justify a forced major bump.

Hardening practices:

  • npm audit --omit=dev --audit-level=high runs in CI on every push to main and every pull request against it, scoped to the dependencies that actually ship.
  • The two highest-risk parsers (pdf-parse, mammoth) will gain resource limits (timeout + size cap) in a future release โ€” tracked in Known issues.
  • The in-app updater already uses git pull --ff-only (refuses on diverged branches) and deliberately never runs npm install in-process โ€” see Setup.md โ†’ Updating.

See SECURITY.md for the full threat model.

๐Ÿงฎ What's NOT in here

Choices we explicitly avoided, with reasons:

  • No telemetry / analytics SDK. Local-first means no phone-home.
  • No vector-database client (Pinecone / Weaviate / Chroma). Embeddings live in a 384-dim Float32Array on disk; we don't need a service.
  • No auth library (yet). The dashboard is loopback-only by default. A one-time setup token is on the roadmap for users who need LAN exposure.
  • No state management library (Redux / Zustand). The dashboard is small enough that useState + lifted state + a custom hook or two (usePersonas) is fine.
  • No CSS-in-JS runtime. Tailwind classes handle 100% of the styling.
  • No PostgreSQL / SQLite / Redis. See "Why no database" above.
  • No Python sidecar. Embeddings run via @huggingface/transformers in pure JS. A future feature might want one โ€” offline speech-to-text, say โ€” but it would be optional and never required to run Vault.

Next steps