# Contributing **English** | [简体中文](./CONTRIBUTING.zh-CN.md) Keep changes small and focused. Run the minimum verification before you commit. ## Prerequisites - **Node 22** (locked in `.nvmrc` / `.node-version` / `engines`). `better-sqlite3` is a native module; ABI must match Node — **do not switch versions**. The bundled agent runtime also uses Node **22.19.0**. - **pnpm** (`packageManager` pins the version; `corepack enable` or install that version yourself). Workspace source of truth is `pnpm-workspace.yaml`; npm's `package-lock.json` is gone. - After clone: `pnpm install` — installs every `packages/*` workspace once, plus the husky git hooks (`prepare`). - When running the server, `cp .env.example .env` if you need it. Loopback starts with no `AUTH_*` at all (single-user `none` mode). ```bash nvm use # Node 22 pnpm install ``` ## Common commands | Command | What it does | |---|---| | `pnpm run dev` | Vite only (`http://localhost:5173`). Backend URL is `VITE_RELAY_URL` (`packages/web/.env.development` → `http://127.0.0.1:3000`; production uses the page origin) | | `pnpm run dev:server` | This repo's Fastify server | | `pnpm run dev:relay` | Same server under `tsx watch` | | `pnpm run agent` | Local agent (separate terminal when you need CDP) | | `npx tsc --noEmit` | Typecheck | | `pnpm lint` | ESLint (`@antfu/eslint-config`, flat config in `eslint.config.mjs`) — must stay clean | | `pnpm lint:fix` | Same, with autofix — this **is** the formatter (there is no prettier) | | `pnpm test` | Full suite (`node:test` + tsx, `tests/*.test.ts`) | | `pnpm run build` | Server build (`tsc`; copies `dist/cli/lifeline.mjs` into `dist/client/public/` when present) | | `pnpm run build:web` | Frontend production build (Vite) | | `pnpm run build:cli` | Single-file CLI bundle | | `pnpm run build:runtime` | Agent runtime tarball (server distributes it under `/public/`) | | `pnpm run build:docker` | Self-host image (see [`docs/selfhost.md`](docs/selfhost.md)) | Two version numbers live in the root `package.json`: `cliVersion` is the machine-side artifact (the number `lifeline update` compares against the server's `/public/cli-latest.txt`) and `version` is the product release (the top entry in [`CHANGELOG.md`](CHANGELOG.md)). They move on different schedules — bump `cliVersion` only when `packages/agent`, `packages/cli`, or `packages/protocol` changes. Lint + format is `@antfu/eslint-config` (`eslint.config.mjs`) — there is no prettier. A handful of rules are deliberately overridden for this codebase (`node:test` titles, semicolons, the global `process`, one-line arrow bodies); each override carries its reason next to it. The advisory rules at the end of the config are expected to pass as warnings: they flag pre-linter code that still needs a human pass. **Minimum verification after every change:** ```bash npx tsc --noEmit && pnpm lint && pnpm test ``` **Two pnpm hard rules** (they fail on the user's machine, not yours, if you get them wrong): - `allowBuilds.better-sqlite3: true` in `pnpm-workspace.yaml` **must stay**. pnpm skips dependency build scripts by default, and the package does not ship a prebuilt `.node`. Delete it and local DB tests plus the server will not start. - Copies of packages out of `node_modules` in `scripts/pack-runtime.ts` **must use `dereference: true`**. pnpm's top-level packages are symlinks; without dereference the runtime tarball contains dangling links. ## Layout | Package | Role | |---|---| | `packages/server` | Cloud: static pages + Fastify + socket.io; machine register and ownership filter; session mirror. **No CDP.** | | `packages/agent` | Local thin client: CDP live stack + on-disk session projection + command dispatch (`COMMAND_EVENTS` in protocol; handlers in `src/command-router.ts`) | | `packages/cli` | `lifeline` CLI; config at `~/.lifeline/config.json` | | `packages/web` | Vite + React: landing (`/`) + console (`/console`). Path literals live only in `src/lib/routes.ts`. Wire types re-exported from `src/net/protocol.ts`. | | `packages/protocol` | Shared types, `IDE_KINDS`, `COMMAND_EVENTS` | Architecture boundaries, entry-point table, and do-not-revert criteria: [`AGENTS.md`](AGENTS.md). That file is the agent-context source of truth; `CLAUDE.md` is a symlink to it — **edit `AGENTS.md` only**. ## Commits and branches - Default branch is `master`. A merge can ship. Sizeable work goes on `feat/xxx` / `fix/xxx` and a merge request; tiny fixes may land directly. - Commit messages follow `type(scope): short summary`. Types: `feat` / `fix` / `docs` / `chore` / `test` / `refactor`. Examples: - `fix(agent): keep Linux as a content source; push identity checks to the connection layer` - `docs: keep agent context in AGENTS.md only` - `chore: bump 0.1.67 (CDP: do not pre-reserve ports; classify failures)` - Enforced, not just written down: husky runs commitlint on `commit-msg` (`commitlint.config.mjs`) and `lint-staged` (eslint --fix, staged files only) on `pre-commit` (`lint-staged.config.mjs`). `pnpm commit` opens the commitizen prompt for the same shape, with the package names as scopes. - One commit, one job. Opportunistic refactors go in their own commit. ## Where docs go | Content | Where | |---|---| | Always-needed conclusions (what it is, how to run, how to verify) | `AGENTS.md` — conclusions only (English; no language pair) | | Public topic docs | English at `docs/.md` (canonical), 简体中文 at `docs/zh-CN/.md`. Root `README` / `CONTRIBUTING` use sibling `*.zh-CN.md` | | Implementation detail, war stories, design specs, plans | Topic docs under `docs/` (e.g. [`ide-drivers.md`](docs/ide-drivers.md), [`selfhost.md`](docs/selfhost.md), [`lifeline-intro.md`](docs/lifeline-intro.md), [`naming-brief.md`](docs/naming-brief.md)). Edit English first, then the Chinese pair | New pitfalls go in the matching `docs/` topic file. `AGENTS.md` gets at most a one-line pointer. ## Red lines - **Unauthenticated CDP = arbitrary code execution.** Port 9222 must never be on the public internet. The agent reaches it over loopback only. - Secrets stay out of the tree and out of commits: server `.env`, per-machine `~/.lifeline/config.json`. - Do not ship a shared `AGENT_TOKEN`. Machine ownership is the login identity at enroll time — first registration sticks, not transferable. - Do not shrink `EVALUATE_TIMEOUT_MS`. Do not switch Node versions. - Implementations marked **Do not revert** in `AGENTS.md` (auth exemption is those three paths only; Linux is a content source; SIGTERM has a hard timeout; timeline is not re-virtualized; `session-index-reporter` does not call `listSessions()`; Windows shortcut classes include `^`): read the code comments and the matching `docs/` topic first. - `agentId` is `machine-`. Do not fall back to a hostname-derived id. - Linux control is not a product surface (`canControlIde()`). The escape hatch is `LIFELINE_LINUX_CDP=1`, not probing `DISPLAY`.