Navigation: Root AGENTS.md | CLAUDE.md → Electron
Read docs/DEVELOPMENT_STANDARDS.md §12 Electron Security first. It is the canonical source for Electron security, IPC, sandboxing, and platform standards. The rules below are the area-specific overlay.
Desktop application wrapping the NodeTool web UI with native capabilities (local file system, SQLite, Python/Conda integration).
- Node.js 22.22.1 required. Matches Electron 39's embedded Node so dev and packaged app run on the same Node version (same APIs, same V8). The backend runs on vanilla Node, so
better-sqlite3(the only source-built native module) is rebuilt against Node headers byelectron/scripts/rebuild-native.mjs, invoked from the repo rootpostinstall. - Use
nvm usefrom the repo root (reads.nvmrc). npm install/npm ciruns the rebuild automatically (from the rootpostinstall, after reify). To force a rebuild:npm run rebuild:native(root) ornpm --prefix electron run rebuild:native.
cd electron
npm install # Install dependencies
npm start # Start Electron app (requires built web)
npm run build # Production build (tsc + vite + electron-builder)
npm run vite:build # Vite build only (main + preload)
npm run typecheck # TypeScript check
npm run lint # ESLint
npm run lint:fix # Auto-fix lint issues
npm test # Run unit tests
npm run test:coverage # Unit tests with coverageThe packaged backend is one esbuild bundle (resources/backend/server.mjs), so every file the backend resolves relative to import.meta.url — provider *-manifest.json files, example workflows, package:// assets — must be staged next to it by the repo-root scripts/bundle-backend.mjs. In dev these resolve through normal package resolution, so a staging gap only breaks the packaged app.
Data files a package loads at runtime go through one guarded path: declare the file in PACKAGE_RUNTIME_ASSETS (packages/config/src/package-asset-registry.ts) and load it with loadPackageAssetJson from @nodetool-ai/config — never with readFileSync(new URL(...)) or createRequire directly. The accessor resolves both layouts, rejects unregistered files immediately in dev, and records outcomes for diagnostics (getPackageAssetResolutions). The registry also drives packaging: the repo-root scripts/bundle-backend.mjs stages every entry (and fails on a dist/*-manifest.json that isn't registered), and scripts/verify-backend-bundle.mjs (run automatically after bundling, also npm run verify:backend-bundle) re-checks the final artifact — manifests referenced by server.mjs, examples, assets, webgpu dawn binaries.
# From repo root:
npm run electron # Build web + Electron, then start
npm run electron:dev # Start against web Vite server (requires active conda env)This workspace has no Playwright suite — main-process behavior is covered by
the Jest tests in src/__tests__/. Browser-level E2E lives in web/.
| Library | Version | Purpose |
|---|---|---|
| Electron | 39.8.10 | Desktop shell |
| React | 19.2 | UI framework |
| TypeScript | 5.9 | Type safety |
| Zustand | 5.0 | State management |
| Vite | 8.0 | Build tool (main + preload processes) |
| better-sqlite3 | — | Local SQLite database |
| sharp | — | Image processing |
| sqlite-vec | — | Vector embeddings |
electron/src/
├── main.ts # Main process entry point
├── preload.ts # Preload script (contextBridge)
├── config.ts # Conda environment detection
├── WorkflowRunner.ts # Workflow execution in main process
└── components/ # React components for Electron UI
Process model: Main process (Node.js) ↔ Preload script (contextBridge) ↔ Renderer process (React).
A vault is a switchable, isolated data store — its own SQLite database plus
its own assets and vector-store directories. It lets a user keep separate sets
of workflows/data apart and switch between them from the desktop app. This is
distinct from the in-database nodetool_workspaces concept (a per-user working
directory for file tools); vaults sit one level above the database.
vaults.ts— source of truth: the vault list + active id are persisted insettings.yaml(vaults,activeVaultId). The built-in Default vault hasnullpaths, meaning "use the backend defaults", so existing installs are untouched.getActiveVaultEnv()returnsDB_PATH/ASSET_FOLDER/VECTORSTORE_DB_PATHoverrides for the active vault.server.tsmerges those overrides into the backend's environment at startup (skipped when an externalDATABASE_URLis set — vaults are SQLite-only).vaultSwitch.ts—applyVaultSwitch(id): persist active id → restart the backend (so it opens the new database) → re-register shortcuts → reload the main window. Restarting the process is intentional: the backend holds its SQLite connection in a singleton, so a clean restart beats a live swap.- UI: a native Vaults menu (quick switch + "Manage Vaults…", which opens
the in-app settings) and a Vaults section in the web settings
(
web/src/components/menus/VaultsSettings.tsx) for create / rename / delete / switch, overwindow.api.vaults.*(IPCVAULT_*).
See DEVELOPMENT_STANDARDS §12 for the full checklist (CSP, auto-update signing,
setWindowOpenHandler, sandbox flag,webSecurity).
contextIsolation: trueon everyBrowserWindow. No exceptions.nodeIntegration: falseon everyBrowserWindow. No exceptions.sandbox: trueon renderer windows whenever possible.webSecurity: true— never disable.- Always use
contextBridge.exposeInMainWorldfor IPC. Never assign towindowdirectly. - Validate every IPC input with Zod in the main process before acting on it. Untrusted renderer is the attacker model.
shell.openExternalonly with an allowlisted URL scheme (https:,mailto:). Never pass user input directly.webContents.setWindowOpenHandlerdenies all by default; allow specific URLs only.- No
eval, nonew Functionanywhere in main or preload. - Auto-update must verify code signatures (
electron-updaterwith publisher verification). - CSP is set in production builds. target: drop
'unsafe-inline'for scripts.
- Define IPC channels as string constants — never use string literals inline.
- Use
ipcMain.handle/ipcRenderer.invokefor request-response (async). - Use
webContents.send/ipcRenderer.onfor main-to-renderer events. - Always clean up IPC listeners when windows are destroyed.
- IPC handlers wrap their body in try/catch and return a discriminated
{ ok: true, data } | { ok: false, error }— never throw across the IPC boundary. - Every IPC handler is span-instrumented for tracing. See DEVELOPMENT_STANDARDS §17.
- Guard platform-specific code with
process.platformchecks (darwin,win32,linux). - Test on macOS, Windows, and Linux.
- Use
path.join()for file paths — never hardcode path separators.
- Use worker threads for heavy operations in the main process.
- Implement proper cleanup on app quit (close DB connections, stop servers).
- Use lazy loading for non-critical modules in the main process.
- The Electron app bundles the backend server — it starts on launch and stops on quit.
- The app detects the active Conda environment via
config.ts. - Python-based nodes (HuggingFace, MLX) require a Conda environment with the correct dependencies.
- See
environment.ymlin the repo root for the Conda spec.