# 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 npm install -g pnpm && pnpm install # setup (pnpm 12 is a native binary; corepack < 0.34.5 cannot start it) pnpm server:start # dev server at http://localhost:8080 pnpm desktop:start # Electron dev app pnpm standalone:start # standalone (in-browser) client pnpm client:build | server:build | desktop:build pnpm --filter test # e.g. pnpm --filter server test spec/etapi/search.spec.ts pnpm --filter coverage # per-package coverage (client, server, standalone) pnpm typecheck # all projects, native compiler, a few seconds pnpm dev:format-check | dev:format-fix # stylistic formatting ``` - **Never run ESLint** (`dev:linter-check`, `dev:linter-fix`, `npx eslint`) — it dies with an out-of-memory error, so a run tells you nothing. CI does not lint either — there is no `eslint`/`linter-check` step anywhere in `.github/workflows/`, so lint findings never block a merge; the gates are `pnpm typecheck` and the per-package `test --coverage` runs. (`packages/*` is ignored by the ESLint config anyway.) - **Never run `pnpm test:all`, `test:parallel`, `test:sequential`, or a whole-package `coverage`** during development — CI runs them on every push. Run the **narrowest** suite that covers what you touched: `pnpm --filter test ` (Vitest treats the trailing argument as a substring filter over spec paths). Core specs need **two** runs, server and standalone (see Testing). Only reach for a full suite if the user asks or wants a final check. - **Typecheck with `pnpm typecheck`, not a raw `tsc`** — it resolves the project references a hand-written `tsc -p …` gets wrong, and is far cheaper than a test suite. Still, run it **once, after a piece of work is finished** — not after every edit: it builds the project references, and repeating it is a common way to lose minutes across a session. Two ways its output misleads: the incremental build does not recheck the files that use an ambient `.d.ts` (`types-lib.d.ts`) after it changes — delete `apps/client/dist/*.tsbuildinfo` (gitignored) before trusting the result; and a dependent project (`tsconfig.spec.json`, `apps/build-docs`) is checked only once the app project compiles, so its errors appear only after the app's are fixed, and some vanish with them. - **Two TypeScript versions, on purpose**: root `package.json` has `typescript` 6.x (the JS compiler API that TypeDoc, typescript-eslint and the browser-bundled language service in `packages/codemirror` load) and `@typescript/native` (7, drives `pnpm typecheck` and owns the `tsc` bin). Do **not** bump `typescript` to 7, dedupe them, or switch to the `@typescript/typescript6` shim (no `lib.*.d.ts` → client build breaks). Reasoning: `docs/Developer Guide/Developer Guide/Environment Setup.md`, "TypeScript". ## Git Workflow - **Committing directly on `main` is allowed and expected** for small fixes and self-contained features — do **not** create a branch first for those. The default "branch before committing on the default branch" rule does not apply to this repository. - **Large or risky work goes on a branch**: multi-commit features, migrations, refactors spanning many packages, anything that needs review or a PR before landing. - Only commit when explicitly asked to **in that message**, and the ask covers only the step it accompanies — a later step is left uncommitted until asked again. A remark like "commits go on main" is about branch choice, not standing permission to commit. Otherwise leave changes staged/unstaged for review. - **The issue-closing keyword goes in the commit/PR subject**, not the body — `fix(markdown): wrap imported tables (closes #10270)`. A bugfix commit **must** carry it. - **That keyword is the only way an issue is ever closed or annotated.** Never post a comment on a GitHub issue, never `gh issue close`, never change labels — the maintainer speaks to reporters, and the merge closes the issue. Reading issues (`gh issue view`) is fine; writing to them is not. - **`git rm` is not a neutral delete.** It leaves the deletion staged, so a commit made before the matching reference updates are staged lands a `HEAD` pointing at a module that no longer exists. When removing a file whose references are edited in the same pass, delete it from the filesystem instead, or land the deletion and the reference updates together — never leave the index in a state that is broken on its own. - **Commit by explicit path** — `git commit -F -- `, not whatever the index holds. The user stages and unstages in this worktree too, so between two commands the index can gain their files or lose yours. Give each commit its own message file: a `Write` onto an existing path that has not been read fails, and the commit then takes the old message. - **Never run a tree-wide git operation — no `git stash`, `git checkout --`, `git restore`.** The user edits this worktree at the same time, so a stash takes their uncommitted work with it, and their IDE can flush a partial buffer onto the stashed state and block the pop. To decide whether a failure predates your change, argue statically instead — check whether the failing spec even imports the module you touched. If a real A/B is unavoidable, copy the file, edit it in place, and restore it, touching only files you wrote; prefer `git worktree add` on a temp dir over anything that reaches the whole tree. ### Commit messages - **Format**: `type(area): imperative summary`, e.g. `fix(search): keep result paths out of the bookmarks clone (closes #10021)`. The area names the subsystem (`search`, `share`, `desktop`, `collections/table`), never a file; the types in use are `fix`, `feat`, `perf`, `docs`, `chore` and `build`. - **Subject**: imperative mood, short (aim for 50 characters before any suffix), no trailing period, and it states the user-visible outcome rather than the mechanism. - **Issue references go in the subject**: `(closes #N)` for a bugfix, `(#N)` when the issue stays open, `(regression of #N)` for a regression. The body never carries the closing keyword. - **Body**: separated from the subject by a blank line, wrapped at 72 characters, and omitted when the subject says it all. Use it to explain what the user saw and why the code did it (naming the offending identifier), what changes and why that shape — including the alternative rejected — and what is deliberately unchanged or out of scope. Measurements, before/after numbers and red/green verification belong here, not in code comments. - **AI attribution is an `Assisted-by` trailer, not prose.** When an AI tool wrote or materially shaped the commit, end the message with a blank line and `Assisted-by: ` (e.g. `Assisted-by: DeepSeek V4.1 Flash`), one trailer per tool. `Co-Authored-By` is for human co-authors and `Signed-off-by` is the human's DCO certification, so neither names a tool; never announce the tool in the subject or as a `Generated with …` footer. - **One logical change per commit**, each message self-contained: PR bodies are assembled from these bodies, so a message that only reads in sequence with the previous commit loses its rationale. ### Opening a PR `git push -u origin ` then `gh pr create --base main --title … --body-file `. There is no PR template in `.github/`, so the body is yours to shape. - **`origin` is `TriliumNext/Trilium`** — the working directory is named `Notes` and ~19 contributor forks are configured as remotes, so neither the path nor the remote list names the target. `gh` resolves it from `remote.origin.gh-resolved`, already set, so `--repo` is redundant; `upstream` is the archived `zadam/trilium` and is never a base. - **Write the body from the commit bodies, grouped by what the change does** — `git log --format='=== %s%n%b' main..HEAD`. A feature branch here carries a long, self-contained rationale per commit (the measurements, the rejected alternative, the reason a value is what it is), and a PR description that restates the subject lines throws all of it away. Group the commits under headings by area, not chronologically: a reviewer reads the reworked subsystem, not the order it was built in. - **Pass the body via `--body-file`, not `--body`.** Markdown of this length in an argument hits shell quoting and heredoc-in-`fish` problems; write it to the scratchpad first. - **Cosmetic `edit-docs` churn is ignored, not mentioned.** `pnpm edit-docs:edit-docs` (and `edit-docs:sync-docs`) reformats whole `apps/server/src/assets/doc_notes/**/*.html` files it merely opened (`
  • x

  • ` ↔ `
  • x
  • `, ` ` shifted across a tag, attributes rewrapped), so a docs commit routinely touches pages the branch has nothing to do with. That is expected and needs neither a rebase nor a note in the description. Read the diff first, though: reflow is safe, but a changed `href`, a dropped `` or a paragraph that lost its text is a real defect and must be fixed before the PR goes up. ## Monorepo Structure ``` apps/ client/ # Preact frontend (+ legacy jQuery widgets); shared by server, desktop, standalone server/ # Node.js backend (Express, better-sqlite3); serves the client, REST/WebSocket desktop/ # Electron: server + client in one process (see developing-electron-desktop skill) standalone/ # whole stack in the browser: core in a Web Worker on SQLite WASM + OPFS, no Node (see developing-standalone skill) mobile/ # Capacitor shell around standalone (see developing-capacitor-mobile skill) web-clipper/, website/, db-compare/, dump-db/, edit-docs/, build-docs/, icon-pack-builder/, script-deployer/ packages/ trilium-core/ # entities, services, SQL, sync, most API routes — shared by server, desktop, standalone commons/ # types + utilities shared with the client trilium-e2e/ # shared Playwright tests ckeditor5/, codemirror/, highlightjs/, share-theme/, pdfjs-viewer/, splitjs/, turndown-plugin-gfm/ ``` `pnpm --filter ` runs a command in one package. **`packages/trilium-core` is shared by server, desktop and standalone — *not* by the client.** `apps/client` has zero `@triliumnext/core` imports; it reaches the backend over REST/WebSocket and shares only **types** via `@triliumnext/commons`. So: a dependency added to core lands in server, desktop and standalone bundles (standalone's worker imports core at startup — that is the cost to weigh, never "the client would pay for it"), and frontend code can never call a core function — it needs an API route or a type in commons. The split is backend-vs-frontend, not Node-vs-browser: standalone runs core in a browser worker, which is why core carries the no-Node-built-ins rules below. ## 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. Fluent builder pattern: `.child()`, `.class()`, `.css()` chaining with position-based ordering. These legacy widgets are jQuery; new UI is Preact under `widgets/react/` — don't mix the two inside one component. The client runs **Preact 11**: components, `createPortal` and types come from `preact`, hooks from `preact/hooks`, and `preact/compat` only for `memo`, `lazy`, `Suspense`, `flushSync` and `useSyncExternalStore`. Compat is nonetheless always loaded, so `onChange` on a text input fires on `input` and `onFocus`/`onBlur` bubble. A removed component's `useEffect` cleanup runs after paint, so one that must happen during the unmount is a `useLayoutEffect` cleanup. Ref typing, per-element ARIA types and the rest: the **`building-client-ui` skill**, "Preact 11 conventions"; for specs (unmount inside `act()`, which events to dispatch): **`writing-unit-tests`**. #### Reusable Preact Components Shared components live in `apps/client/src/widgets/react/` — **always** reuse them (`FormTextBox`, `FormSelect`, `Button`, `Badge`, `NoItems`, `Dropdown`, `Table`, `Calendar`, …) instead of writing raw HTML elements or a custom implementation, and never put Bootstrap utility classes (`form-control-sm`, `input-group`, …) on them. Any control floating **over** a note's content (map, mind map, image, diagram) goes on `OverlayControlGroup` / `OverlayToolbar` — never a hand-rolled `