# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Overview Trilium Notes is a hierarchical note-taking application with synchronization, scripting, and rich text editing. TypeScript monorepo using pnpm with multiple apps and shared packages. ## Development Commands ```bash # Setup corepack enable && pnpm install # Run pnpm server:start # Dev server at http://localhost:8080 pnpm desktop:start # Electron dev app pnpm standalone:start # Standalone client dev # Build pnpm client:build # Frontend pnpm server:build # Backend pnpm desktop:build # Electron # Test pnpm test:all # All tests (parallel + sequential) pnpm test:parallel # Client + most package tests pnpm test:sequential # Server, ckeditor5-mermaid, ckeditor5-math (shared DB) pnpm --filter server test # Single package tests pnpm coverage # Coverage reports # Lint & Format pnpm dev:linter-check # ESLint check pnpm dev:linter-fix # ESLint fix pnpm dev:format-check # Format check (stricter stylistic rules) pnpm dev:format-fix # Format fix pnpm typecheck # TypeScript type check across all projects ``` **Running a single test file**: `pnpm --filter server test spec/etapi/search.spec.ts` ## Main Applications The four main apps share `packages/trilium-core/` for business logic but differ in runtime: - **client** (`apps/client/`): Preact frontend with jQuery widget system. Shared UI layer used by both server and desktop. - **server** (`apps/server/`): Node.js backend (Express, better-sqlite3). Serves the client and provides REST/WebSocket APIs. - **desktop** (`apps/desktop/`): Electron wrapper around server + client, running both in a single process. - **standalone** (`apps/client-standalone/` + `apps/standalone-desktop/`): Runs the entire stack in the browser — server logic compiled to WASM via sql.js, executed in a service worker. No Node.js dependency at runtime. ## Monorepo Structure ``` apps/ client/ # Preact frontend (shared by server, desktop, standalone) server/ # Node.js backend (Express, better-sqlite3) desktop/ # Electron (bundles server + client) client-standalone/ # Standalone client (WASM + service workers, no Node.js) standalone-desktop/ # Standalone desktop variant server-e2e/ # Playwright E2E tests for server web-clipper/ # Browser extension website/ # Project website db-compare/, dump-db/, edit-docs/, build-docs/, icon-pack-builder/ packages/ trilium-core/ # Core business logic: entities, services, SQL, sync commons/ # Shared interfaces and utilities ckeditor5/ # Custom rich text editor bundle codemirror/ # Code editor integration highlightjs/ # Syntax highlighting share-theme/ # Theme for shared/published notes ckeditor5-admonition/, ckeditor5-footnotes/, ckeditor5-math/, ckeditor5-mermaid/ ckeditor5-keyboard-marker/, express-partial-content/, pdfjs-viewer/, splitjs/ turndown-plugin-gfm/ ``` Use `pnpm --filter ` to run commands in specific packages. ## Core Architecture ### Three-Layer Cache System All data access goes through cache layers — never bypass with direct DB queries: - **Becca** (`packages/trilium-core/src/becca/`): Server-side entity cache. Access via `becca.notes[noteId]`. - **Froca** (`apps/client/src/services/froca.ts`): Client-side mirror synced via WebSocket. Access via `froca.getNote()`. - **Shaca** (`apps/server/src/share/`): Optimized cache for shared/published notes. **Critical**: Always use cache methods, not direct DB writes. Cache methods create `EntityChange` records needed for synchronization. ### Entity System Core entities live in `packages/trilium-core/src/becca/entities/` (not `apps/server/`): - `BNote` — Notes with content and metadata - `BBranch` — Multi-parent tree relationships (cloning supported) - `BAttribute` — Key-value metadata (labels and relations) - `BRevision` — Version history - `BOption` — Application configuration - `BBlob` — Binary content storage Entities extend `AbstractBeccaEntity` with built-in change tracking, hash generation, and date management. ### Entity Change & Sync Protocol Every entity modification creates an `EntityChange` record driving sync: 1. Login with HMAC authentication (document secret + timestamp) 2. Push changes → Pull changes → Push again (conflict resolution) 3. Content hash verification with retry loop Sync services: `packages/trilium-core/src/services/sync.ts`, `syncMutexService`, `syncUpdateService`. ### Widget-Based UI Frontend widgets in `apps/client/src/widgets/`: - `BasicWidget` / `TypedBasicWidget` — Base classes (jQuery `this.$widget` for DOM) - `NoteContextAwareWidget` — Responds to note changes - `RightPanelWidget` — Sidebar widgets with position ordering - Type-specific widgets in `type_widgets/` directory **Widget lifecycle**: `doRenderBody()` for initial render, `refreshWithNote()` for note changes, `entitiesReloadedEvent({loadResults})` for entity updates. Uses jQuery — don't mix React patterns. Fluent builder pattern: `.child()`, `.class()`, `.css()` chaining with position-based ordering. ### API Architecture - **Internal API** (`apps/server/src/routes/api/`): REST endpoints, trusts frontend - **ETAPI** (`apps/server/src/etapi/`): External API with basic auth tokens — maintain backwards compatibility - **WebSocket** (`apps/server/src/services/ws.ts`): Real-time sync ### Platform Abstraction `packages/trilium-core/src/services/platform.ts` defines `PlatformProvider` interface with implementations in `apps/desktop/`, `apps/server/`, and `apps/client-standalone/`. Singleton via `initPlatform()`/`getPlatform()`. **PlatformProvider** provides: - `crash(message)` — Platform-specific fatal error handling - `getEnv(key)` — Environment variable access (server/desktop use `process.env`, standalone maps URL query params like `?safeMode` → `TRILIUM_SAFE_MODE`) - `isElectron`, `isMac`, `isWindows` — Platform detection flags **Critical rules for `trilium-core`**: - **No `process.env` in core** — use `getPlatform().getEnv()` instead (not available in standalone/browser) - **No `import path from "path"` in core** — Node's `path` module is externalized in browser builds. Use `packages/trilium-core/src/services/utils/path.ts` for `extname()`/`basename()` equivalents - **No Node.js built-in modules in core** — core runs in both Node.js and the browser (standalone). Use platform-agnostic alternatives or platform providers - **Platform detection via functions** — `isElectron()`, `isMac()`, `isWindows()` from `utils/index.ts` are functions (not constants) that call `getPlatform()`. They can only be called after `initializeCore()`, not at module top-level. If used in static definitions, wrap in a closure: `value: () => isWindows() ? "0.9" : "1.0"` - **Barrel import caution** — `import { x } from "@triliumnext/core"` loads ALL core exports. Early-loading modules like `config.ts` should import specific subpaths (e.g. `@triliumnext/core/src/services/utils/index`) to avoid circular dependencies or initialization ordering issues - **Electron IPC** — In desktop mode, client API calls use Electron IPC (not HTTP). The IPC handler in `apps/server/src/routes/electron.ts` must be registered via `utils.isElectron` from the **server's** utils (which correctly checks `process.versions["electron"]`), not from core's utils ### Database SQLite via `better-sqlite3`. SQL abstraction in `packages/trilium-core/src/services/sql/` with `DatabaseProvider` interface, prepared statement caching, and transaction support. - Schema: `apps/server/src/assets/db/schema.sql` - Migrations: `apps/server/src/migrations/YYMMDD_HHMM__description.sql` ### Internationalization - Translation files in `apps/client/src/translations/` - Supported languages: English, German, Spanish, French, Romanian, Chinese - **Only add new translation keys to `en/translation.json`** — translations for other languages are managed via Weblate and will be contributed by the community - Third-party components (e.g., mind-map context menu) should use i18next `t()` for their labels, with the English strings added to `en/translation.json` under a dedicated namespace (e.g., `"mind-map"`) - When a translated string contains **interpolated components** (e.g. links, note references) whose order may vary across languages, use `` from `react-i18next` instead of `t()`. This lets translators reorder components freely (e.g. `" in "` vs `"in , "`) - When adding a new locale, follow the step-by-step guide in `docs/Developer Guide/Developer Guide/Concepts/Internationalisation Translations/Adding a new locale.md` - **Server-side translations** (e.g. hidden subtree titles) go in `apps/server/src/assets/translations/en/server.json`, not in the client `translation.json` ### Electron Desktop App - Desktop entry point: `apps/desktop/src/main.ts`, window management: `apps/server/src/services/window.ts` - IPC communication: use `electron.ipcMain.on(channel, handler)` on server side, `electron.ipcRenderer.send(channel, data)` on client side - Electron-only features should check `isElectron()` from `apps/client/src/services/utils.ts` (client) or `utils.isElectron` (server) Three inheritance mechanisms: 1. **Standard**: `note.getInheritableAttributes()` walks parent tree 2. **Child prefix**: `child:label` on parent copies to children 3. **Template relation**: `#template=noteNoteId` includes template's inheritable attributes ### Attribute Inheritance Use `note.getOwnedAttribute()` for direct, `note.getAttribute()` for inherited. ### Client-Side API Restrictions - **Do not use `crypto.randomUUID()`** or other Web Crypto APIs that require secure contexts - Trilium can run over HTTP, not just HTTPS - Use `randomString()` from `apps/client/src/services/utils.ts` for generating IDs instead ### Shared Types Policy - Types shared between client and server belong in `@triliumnext/commons` (`packages/commons/src/lib/`) - Import shared types directly from `@triliumnext/commons` - do not re-export them from app-specific modules - Keep app-specific types (e.g., `LlmProvider` for server, `StreamCallbacks` for client) in their respective apps ## Important Patterns - **Protected notes**: Check `note.isContentAvailable()` before accessing content; use `note.getTitleOrProtected()` for safe title access - **Long operations**: Use `TaskContext` for progress reporting via WebSocket - **Event system** (`packages/trilium-core/src/services/events.ts`): Events emitted in order (notes → branches → attributes) during load for referential integrity - **Search**: Expression-based, scoring happens in-memory — cannot add SQL-level LIMIT/OFFSET without losing scoring - **Widget cleanup**: Unsubscribe from events in `cleanup()`/`doDestroy()` to prevent memory leaks ## Code Style - 4-space indentation, semicolons always required - Double quotes (enforced by format config) - Max line length: 100 characters - Unix line endings - Import sorting via `eslint-plugin-simple-import-sort` ## Testing - **Server tests** (`apps/server/spec/`): Vitest, must run sequentially (shared DB), forks pool, max 6 workers - **Client tests** (`apps/client/src/`): Vitest with happy-dom environment, can run in parallel - **E2E tests** (`apps/server-e2e/`): Playwright, Chromium, server started automatically on port 8082 - **ETAPI tests** (`apps/server/spec/etapi/`): External API contract tests ## Documentation - `docs/Script API/` — Auto-generated, never edit directly - `docs/User Guide/` — Edit via `pnpm edit-docs:edit-docs`, not manually - `docs/Developer Guide/` and `docs/Release Notes/` — Safe for direct Markdown editing ## Key Entry Points - `apps/server/src/main.ts` — Server startup - `apps/client/src/desktop.ts` — Client initialization - `packages/trilium-core/src/becca/becca.ts` — Backend data management - `apps/client/src/services/froca.ts` — Frontend cache - `apps/server/src/routes/routes.ts` — API route registration - `packages/trilium-core/src/services/sql/sql.ts` — Database abstraction ### Adding Hidden System Notes The hidden subtree (`_hidden`) contains system notes with predictable IDs (prefixed with `_`). Defined in `apps/server/src/services/hidden_subtree.ts` via the `HiddenSubtreeItem` interface from `@triliumnext/commons`. 1. Add the note definition to `buildHiddenSubtreeDefinition()` in `apps/server/src/services/hidden_subtree.ts` 2. Add a translation key for the title in `apps/server/src/assets/translations/en/server.json` under `"hidden-subtree"` 3. The note is auto-created on startup by `checkHiddenSubtree()` — uses deterministic IDs so all sync cluster instances generate the same structure 4. Key properties: `id` (must start with `_`), `title`, `type`, `icon` (format: `bx-icon-name` without `bx ` prefix), `attributes`, `children`, `content` 5. Use `enforceAttributes: true` to keep attributes in sync, `enforceBranches: true` for correct placement, `enforceDeleted: true` to remove deprecated notes 6. For launcher bar entries, see `hidden_subtree_launcherbar.ts`; for templates, see `hidden_subtree_templates.ts` ### Writing to Notes from Server Services - `note.setContent()` requires a CLS (Continuation Local Storage) context — wrap calls in `cls.init(() => { ... })` (from `apps/server/src/services/cls.ts`) - Operations called from Express routes already have CLS context; standalone services (schedulers, Electron IPC handlers) do not ### Adding New LLM Tools Tools are defined using `defineTools()` in `apps/server/src/services/llm/tools/` and automatically registered for both the LLM chat and MCP server. 1. Add the tool definition in the appropriate module (`note_tools.ts`, `attribute_tools.ts`, `attachment_tools.ts`, `hierarchy_tools.ts`) or create a new module 2. Each tool needs: `description`, `inputSchema` (Zod), `execute` function, and optionally `mutates: true` for write operations 3. If creating a new module, wrap tools in `defineTools({...})` and add the registry to `allToolRegistries` in `tools/index.ts` 4. Add a client-side friendly name in `apps/client/src/translations/en/translation.json` under `llm.tools.` — use **imperative tense** (e.g. "Search notes", "Create note", "Get attributes"), not present continuous 5. Use ETAPI (`apps/server/src/etapi/`) as inspiration for what fields to expose, but **do not import ETAPI mappers** — inline the field mappings directly in the tool so the LLM layer stays decoupled from the API layer ### Updating PDF.js 1. Update `pdfjs-dist` version in `packages/pdfjs-viewer/package.json` 2. Run `npx tsx scripts/update-viewer.ts` from that directory 3. Run `pnpm build` to verify success 4. Commit all changes including updated viewer files ### Database Migrations - Add migration scripts in `apps/server/src/migrations/` - Update schema in `apps/server/src/assets/db/schema.sql` ### Server-Side Static Assets - Static assets (templates, SQL, translations, etc.) go in `apps/server/src/assets/` - Access them at runtime via `RESOURCE_DIR` from `apps/server/src/services/resource_dir.ts` (e.g. `path.join(RESOURCE_DIR, "llm", "skills", "file.md")`) - **Do not use `import.meta.url`/`fileURLToPath`** to resolve file paths — the server is bundled into CJS for production, so `import.meta.url` will not point to the source directory - **Do not use `__dirname` with relative paths** from source files — after bundling, `__dirname` points to the bundle output, not the original source tree ## MCP Server - Trilium exposes an MCP (Model Context Protocol) server at `http://localhost:8080/mcp`, configured in `.mcp.json` - The MCP server is **only available when the Trilium server is running** (`pnpm run server:start`) - It provides tools for reading, searching, and modifying notes directly from the AI assistant - Use it to interact with actual note data when developing or debugging note-related features ## Build System Notes - Uses pnpm for monorepo management - Vite for fast development builds - ESBuild for production optimization - pnpm workspaces for dependency management - Docker support with multi-stage builds