- Never bump the version in package.json manually. The version is set exclusively by the GitHub Actions Release workflow (
release.yml) vianpm version --no-git-tag-version. - To release: trigger the Release workflow with the desired version string. It handles version bumping, building, and creating the GitHub release.
- Main process (
src/main/): CommonJS viatsc -p tsconfig.main.json(module: "node16") - Renderer (
src/renderer/): ESM bundled by Vite - Shared types:
src/shared/types.ts— imported by both main and renderer - IPC flow: renderer →
preload.tscontextBridge →ipc-handlers.ts→ domain modules - Database: Split into
src/main/db/modules (connection, papers, collections, tags) withdatabase.tsre-exporting everything for backward compatibility - MCP tools: Split into
src/main/mcp/tools/modules (search-tools, paper-tools, organization-tools, resolvers) withmcp/tools.tsre-exporting - Services: Shared business logic in
src/main/services/(save-paper, pdf-reader, pdf-annotator) - Constants:
src/main/constants.ts(backend),src/renderer/constants.ts(frontend)
Pattern: <domain>:<action> — e.g., papers:save, collections:list, tags:addToPaper, mcp:getStatus
- IPC handlers for mutations return
{ success: boolean; error?: string }or{ success: boolean; paper?: LibraryPaper } - IPC handlers for queries return data directly (throw on failure)
- MCP tools return
{ content: [{ type: 'text', text }] }always — errors are formatted as text
if (isEnabled('tool_name'))
server.tool('tool_name', 'description', { ...zodSchema }, async (params) => {
return { content: [{ type: 'text' as const, text: result }] };
});Tools are registered per group via registerSearchTools, registerPaperTools, registerOrganizationTools.
export const useStore = create<StoreState>((set, get) => ({
...initialState,
action: async () => {
const result = await window.electronAPI.someCall();
set({ field: result });
},
}));- Framework: Vitest with
globals: true - Database tests: Create temp SQLite file in
os.tmpdir(), clean up inafterEach - Mock paths:
vi.mock('../paths', () => ({ getDataDir: () => '' })) - Test file location:
src/main/__tests__/ - Run:
npm run test(rebuilds native modules before/after)
- Create
src/main/db/<domain>.tswith query/mutation functions - Re-export from
src/main/db/index.ts - Re-export from
src/main/database.ts(keeps backward-compat imports working)
All three files must be updated together:
- Handler —
src/main/ipc-handlers.ts: addipcMain.handle('<domain>:<action>', ...) - Preload bridge —
src/main/preload.ts: add method to theapiobject - Type —
src/shared/types.ts: add method signature toElectronAPIinterface
- Add the tool handler to the appropriate
register*Tools()function insrc/main/mcp/tools/ - Add an entry to
TOOL_METADATAinsrc/main/mcp/tools/index.ts(drives the Settings UI) - Tool calls are automatically instrumented (logged to
tool_call_log) via the Proxy inregisterTools()
The full path for surfacing new backend data in the renderer:
- DB function (
src/main/db/) → re-export chain → IPC handler → preload bridge →ElectronAPItype - Zustand store action calls
window.electronAPI.newMethod()and sets state - Component reads from store and calls the load action on mount
DEFAULT_COLOR:#007AFF(macOS system blue)COLOR_PALETTE: 8 macOS system colors (renderer only)
- macOS: Install deps with
npm installand run directly — no extra tooling needed - Linux: Use the Nix flake (
flake.nix) which provides all Electron runtime deps (GTK, X11, Mesa, etc.) and native module build tools. Usenix developfor a dev shell ornix runto start dev mode directly - Releases are macOS-only (Apple Silicon) — no Linux distributable is built yet
npm run build— build main + renderernpm run test— rebuild native modules, run vitest, rebuild for electronnpm run lint— biome checknpm run lint:fix— biome auto-fixPAPERSHELF_DATA_DIR=/tmp/papershelf-dev npm run dev— dev mode with isolated data