Files
Trilium/CLAUDE.md

16 KiB

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

# 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 <package-name> <command> 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<T> 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 ?safeModeTRILIUM_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 functionsisElectron(), 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 cautionimport { 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 <Trans> from react-i18next instead of t(). This lets translators reorder components freely (e.g. "<Note/> in <Parent/>" vs "in <Parent/>, <Note/>")
  • 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.<tool_name> — 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