From 0e71d87f751fbe63b1b7f0e6b8607b9167bb8ca6 Mon Sep 17 00:00:00 2001 From: brunorezio Date: Sat, 25 Jul 2026 23:17:26 +0100 Subject: [PATCH] rewrite CLAUDE.md against the current codebase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The old version described a repo that no longer exists: apps/dashboard and apps/editor, a tracking server on port 5001, an ephemeral_db, and a dashboard and API running as separate processes. None of that is true. Replaces it with what the code actually does — one Bun process serving the SPA, the API and eight WebSocket providers; sidecars for the privileged work; the split between Postgres and the file-backed items store; and the single-user invariant stated as an invariant rather than a migration in progress. Also records two things that are easy to get wrong from reading alone: the database is maintained with db:push and has never had a migration applied, and agents run unsandboxed with permissions bypassed on purpose. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 362 +++++++++++++++++++++--------------------------------- 1 file changed, 142 insertions(+), 220 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index aee5f43a..994b186f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,267 +2,189 @@ ## Project Overview -Officer is an AI-assisted personal backend and frontend for life management. +Officer is a self-hosted personal platform for one person: the server owner. It bundles an AI agent, +a terminal, a file browser, a code editor, email, chat channels, a remote desktop and customisable +dashboards behind a single web app. -## Monorepo Structure +**Single-user is a hard invariant, not a stage.** There is exactly one account, created once by +`POST /auth/bootstrap` while the user table is empty. There are no roles, no invitations, no +sandboxing of one user from another, and no per-user isolation anywhere in the codebase. If a change +seems to need "which user is this", the answer is always the owner. + +## Architecture + +One Bun process (`src/server.tsx`) serves everything: + +- the React SPA, via Bun's HTML import of `src/apps/officer-web/index.html` (HMR in dev) +- the REST API, a Hono app mounted at `/api` (`src/servers/hono.ts`) +- eight WebSocket providers — terminal, chat, task-runner, pipeline, dev-server proxy, cliamp, + cliamp-audio, desktop — plus a sidecar registration socket +- a browser relay on its own port (`BROWSER_RELAY_PORT`, default 18792) + +Long-running and privileged work lives in **sidecars**: separate processes that dial back in over +`/api/sidecar/register` and are tracked in `src/servers/sidecar-registry.ts`. PM2 runs them +(`ecosystem.config.cjs`): `officer` (the server), `officer-claude`, `officer-opencode`, +`officer-email`, `officer-pty`, `officer-vnc`. + +Agents run **unsandboxed as the server owner**, with `--dangerously-skip-permissions`. This is +deliberate — it is the owner's own machine. Do not add a jail without being asked. + +## Repository Layout ``` src/ +├── server.tsx # the single entrypoint: routes, WS upgrades, static files ├── apps/ -│ ├── dashboard/ # Main UI (React 19) -│ └── editor/ # Visual editor +│ ├── officer-web/ # the SPA shell — Screens/Authentication + Screens/Dashboard +│ └── landing/ # marketing landing page ├── servers/ -│ ├── api/ # REST API (Hono, port 5000) -│ └── tracking/ # Event collection (port 5001) -├── databases/ -│ ├── officer_db/ # Main app data -│ └── ephemeral_db/ # Cache & temporary data -└── workspaces/ # Shared: components, hooks, helpers, types, config, etc. +│ ├── hono.ts # router composition; everything under /api +│ ├── _middlewares/ # auth, body parsing, origin validation, rate limiting +│ ├── api// # one folder per feature, each exporting a router +│ ├── channels/ # Telegram / WhatsApp / Discord bridges +│ ├── queue/ # background job engine +│ └── sidecar/ # sidecar implementations + the wire protocol +├── databases/officer_db/ # the only database (Postgres + Drizzle) +├── extensions/browser-relay/ +└── workspaces/ # shared packages, each a bun workspace ``` -**Path aliases**: `@/` → dashboard, `@@/` → servers +`src/workspaces/officerdev` is the biggest of these: the windowed "apps" (FileBrowser, Chat, +Terminal, CodeEditor, Desktop, Projects, Dashboards…) that the shell hosts, behind an `AppRegistry`. +`src/apps/officer-web` is only the shell — screens, routing and settings. + +**Path aliases** (`tsconfig.json`): `@/*` → `src/apps/officer-web`, `@/components/*` → +`src/workspaces/components`, `@@/*` → `src/servers`, `@/public/*` → `public`. Workspace packages are +imported by their package name (`officerdev`, `hooks`, `state`, `types`, `helpers`, …). ## Tech Stack -- **Runtime**: Bun -- **Language**: TypeScript (strict), some raw SQL -- **Frontend**: React 19, React Router, React Query + Context -- **UI**: shadcn/ui base + custom components, Tailwind CSS -- **Backend**: Hono framework -- **Database**: PostgreSQL, Drizzle ORM + raw SQL +- **Runtime**: Bun (Node 22 is enforced by a `preinstall` check) +- **Language**: TypeScript, strict. `bunx tsgo` is clean — keep it that way. +- **Frontend**: React 19, React Router 7, React Query, Tailwind 4, shadcn/ui + custom components +- **Backend**: Hono +- **Database**: one Postgres database via Drizzle +- **Agents**: Claude Code and opencode, driven through sidecars; tools exposed over MCP + +## Data + +Two stores, and the split matters: + +**Postgres** (`src/databases/officer_db`) holds the account, passkeys, settings, dashboards, +projects, email accounts, queue and pipeline jobs. Schema in `src/schema/`, hand-written queries in +`src/queries/`, types inferred from the schema in `src/types.ts`. + +**The filesystem** holds everything the agent authors. `OFFICER_ITEMS_DIR` contains one directory +per item under `skills/`, `tools/`, `tasks/`, `processes/`, `extensions/` — no database rows, no +scope tiers. `DATA_PATH//` holds the managed home, attachments and per-account email SQLite +stores. Path helpers live in `src/servers/data-path.ts`; note `getHomeDir` (the managed home under +`DATA_PATH`) versus `getOwnerHomeDir` (the owner's real login home when `HOME_DIR` is set, which is +where terminals, chats and task runs actually execute). + +### Schema changes use `push`, not migrations + +This database is kept in sync with `bun db:push`. **`drizzle-kit migrate` has never been run here** +— there is no `__drizzle_migrations` table, and the files in `officer_db/migrations/` are historical +residue that does not describe the live database. Change the schema, run `bun db:push`, done. + +## Security Model + +The perimeter is one credential, so the guards matter: + +- `IS_DEV_BUILD` (`src/servers/build-env.ts`) is true **only** when `PUBLIC_BUILD_ENV` is explicitly + `dev`/`development`. Everything else, including unset, is hardened. Origin validation, rate + limiting and password rules all key off it — they fail closed. +- Allowed origins come from `PUBLIC_URL`. Officer always sits behind an HTTPS reverse proxy, so the + forwarded `Host` must equal `PUBLIC_URL`'s authority exactly. +- JWTs are 30-day, blacklisted on signout, and invalidated by a password change (`passwordChangedAt`). +- A panic lockdown (`src/servers/api/auth/panic.ts`) is in-memory only and refuses every + authenticated request until the server restarts. + +## Commands + +```bash +bun dev # the whole app — SPA + API + WebSockets, watched +bun start # production +bunx tsgo # typecheck (not tsc) +bun test # tests +bun format # prettier over changed files only +bun db:push # apply the schema to Postgres +bun setup # guided install (writes .env, incl. PUBLIC_BUILD_ENV=production) +``` + +Sidecar control: `bun run start:sidecar` / `stop:sidecar` / `restart:sidecar` / `logs:sidecar`. + +Note: `build:editor*` in `package.json` points at `scripts/build/editor.ts`, which does not exist. ## Code Style -- **Paradigm**: Functional - pure functions, immutability, composition -- **TypeScript**: Strict - no `any`, proper types everywhere -- **Comments**: Minimal - code should be self-documenting -- **Async**: Always async/await -- **Errors**: Try/catch (detailed patterns in section-specific docs) - -## TypeScript Patterns +- **Paradigm**: functional — pure functions, immutability, composition +- **TypeScript**: strict, no `any`. Type-only imports are required (`verbatimModuleSyntax`). +- **Comments**: minimal, and about *why*. Don't narrate what the code already says. +- **Async**: always async/await +- **Exports**: named only, no defaults +- **Files**: `PascalCase.tsx` for components, `kebab-case.ts` for everything else +- **Prettier**: single quotes in JS, double in JSX, semicolons, trailing commas, 120 cols ### Imports -Order imports as: types → external → workspace → relative + +Order: types → external → workspace → relative. + ```ts -import type { Experiment, User } from 'types'; -import { useState, useEffect } from 'react'; +import type { User } from 'types'; +import { useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { useClient } from 'hooks/useClient'; import { formatDate } from 'helpers/formatters'; import { useWebsites } from '../useWebsites'; ``` -**Type-only imports**: With `verbatimModuleSyntax` enabled, types must use type-only imports: -```ts -// ✅ Good - inline type import -import { ActionModals, type ActionModalsTypes } from './ActionModals'; - -// ✅ Good - separate type import -import type { FormEvent } from 'react'; -import { useState } from 'react'; - -// ❌ Bad - will error with verbatimModuleSyntax -import { ActionModals, ActionModalsTypes } from './ActionModals'; -``` - -### Types -- Prefer `type` over `interface` -- Shared types go in `workspaces/types` - re-exports from database types -- Database types live in their respective `db/types.ts` files -- Component props: colocated as named type - -```ts -type ExperimentCardProps = { - experiment: Experiment; - onSelect: (id: number) => void; -}; -``` - -### Database Types -Use Drizzle-inferred types, never manual type definitions in schema files: - -```ts -// In officerdb/types.ts -import * as Schema from './schema'; - -// Simple table - just Select/Insert -export type Screenshot = typeof Schema.Screenshots.$inferSelect; -export type ScreenshotInsert = typeof Schema.Screenshots.$inferInsert; - -// Table with relations - Select + extended type for API responses -export type UserSelect = typeof Schema.Users.$inferSelect; -export type UserInsert = typeof Schema.Users.$inferInsert; -export type User = UserSelect & { - company: Company; - passkeys: Passkey[]; -}; -``` - -**In app code**, import types from `'types'` (the workspace re-exports everything): -```ts -// ✅ Good - in apps/dashboard, apps/editor, etc. -import type { User, Experiment, VariantStat } from 'types'; - -// ❌ Bad - don't import directly from database packages in app code -import type { User } from 'officerdb/types'; -import type { VariantStat } from 'statisticsdb/types'; -``` - -**In database/server code**, import from the specific package: -```ts -// ✅ Good - in servers/, databases/ -import type { User } from 'officerdb/types'; -``` +In app code import types from `'types'`, which re-exports the database types. Only server and +database code imports from `officerdb/types` directly. ### Functions -- Arrow functions for simple/one-liners -- Regular functions for complex logic -- Named exports only (no default exports) -- **No multiline parameter definitions** - extract params type when signature gets long + +Arrow functions for one-liners, regular functions for anything with real logic. No multiline +parameter lists — extract a params type instead. ```ts -// Simple utility - arrow -export const formatDate = (value: number) => new Date(value).toLocaleDateString(); - -// Complex logic - regular function -export function calculateStatistics(data: DataPoint[]) { - // multi-line logic -} - -// ❌ Bad - multiline params -export function processData( - body: Record | undefined, - query: Record, -): Result { ... } - -// ✅ Good - extract params type -type ProcessDataParams = { - body: Record | undefined; - query: Record; -}; - +type ProcessDataParams = { body: Record | undefined; query: Record }; export function processData({ body, query }: ProcessDataParams): Result { ... } ``` -### Null Handling -- Early returns for guards -- Optional chaining for property access -- Use `?? undefined` to convert `null` to `undefined` for props expecting `string | undefined` -- Use `?? 0` or `?? ''` for fallback values with state setters +### Components and hooks -```ts -function processExperiment(exp: Experiment | null) { - if (!exp) return null; // guard - - const websiteName = exp.website?.name ?? 'Unknown'; // access - // ... -} - -// Converting null to undefined for props - // avatar is string | null, src expects string | undefined - -// Fallback for state setters expecting non-null -setWebsite(website?.id ?? 0); // id might be undefined, setter expects number - -// Array access with known valid index - use non-null assertion -const items = ['a', 'b', 'c']; -if (items.length > 0) { - const first = items[0]!; // We know index 0 exists after length check -} -``` - -## React Patterns - -See `CONVENTIONS.md` for detailed patterns (feature folders, manager pattern, state management, React 19 rules). - -### Components -Arrow function with named props type: ```tsx -type ExperimentCardProps = { - experiment: Experiment; - onSelect: (id: number) => void; -}; +type ExperimentCardProps = { experiment: Experiment; onSelect: (id: number) => void }; -export const ExperimentCard = ({ experiment, onSelect }: ExperimentCardProps) => { - return (/* ... */); -}; +export const ExperimentCard = ({ experiment, onSelect }: ExperimentCardProps) => { ... }; ``` -### Hooks -Return objects for complex hooks, tuples for simple state: -```tsx -// Complex - return object -export const useExperiment = (id: number) => { - return { experiment, isLoading, update, delete: deleteExp }; -}; +Complex hooks return objects; simple state hooks return `as const` tuples. Event parameters are +always named `ev`, never `e`. -// Simple state - return tuple (like useState) -export const useGlobal = (key: string, initial: T) => { - return [value, setValue, refresh, reset] as const; -}; -``` +### API calls -## Directory Structure - -### Naming Conventions -- **Components**: `PascalCase.tsx` -- **Everything else**: `kebab-case.ts` -- **Hooks**: `use-*.ts` or `useFeatureName.ts` - -## Formatting - -Prettier config in `.prettierrc`: -- Semicolons: always -- Quotes: single (JS), double (JSX) -- Trailing commas: all -- Indent: 2 spaces -- Line width: 120 - -```bash -bun format # Format all files -bun format:check # Check without writing -``` - -## Development - -```bash -bun dev # Runs dashboard + API server -bun dev:tracking # Runs tracking server separately if needed -bunx tsgo # TypeScript check (not npx tsc) -``` - -**New features**: Usually database-first (schema → API → UI) - -**Environment variables**: -- Backend: `process.env.X` directly -- Frontend: Use existing config patterns - -**Logging**: `console.log` for debugging (Signoz integration coming) - -## Git - -Simple lowercase commit messages, no prefixes. +`useClient()` returns typed verbs — `client.get('/foo')` really does return `Foo`, so pass the +type parameter and let inference flow from it. ## Working With Me -- **Ask first**: Confirm approach before significant changes -- **Explore thoroughly**: Read related files for full context -- **Keep it simple**: No over-engineering or premature abstractions -- **Be explicit**: No magic or implicit behavior -- **Stay focused**: Note unrelated issues but don't fix them -- **Detailed output**: Provide full breakdown of changes when completing tasks +- **Ask first** — confirm the approach before a significant change +- **Explore thoroughly** — read the related files before editing +- **Keep it simple** — no over-engineering, no premature abstraction +- **Be explicit** — no magic, no implicit behaviour +- **Stay focused** — note unrelated problems, don't fix them uninvited +- **Report honestly** — say what you verified and what you didn't -## Section-Specific Docs +Commit messages: simple lowercase, no prefixes. -Detailed patterns for each area live in their respective directories: +## Further Reading -**Frontend:** -- `src/apps/CLAUDE.md` - Shared frontend patterns (components, hooks, state) -- `src/apps/officer-web/Screens/Dashboard/Automation/CLAUDE.md` - Automation screen specifics +- `CONVENTIONS.md` — component organisation, state management, React patterns, with rationale +- `TODO.md` — current direction and deferred work; **takes precedence over this file where they disagree** +- `src/apps/CLAUDE.md` — shared frontend patterns +- `src/databases/CLAUDE.md` — database patterns -**Backend:** -- `src/databases/CLAUDE.md` - Database schemas and patterns - -**Project-wide:** -- `CONVENTIONS.md` - Detailed code patterns with rationale (component organization, state management, React patterns) -- `TODO.md` - Current direction and deferred work. Takes precedence over this file where they disagree. +The other markdown files in the repo root (`OFFICERDEV_*.md`, `MARKETING_WEBSITE.md`, `PHONE_APP.md`, +`SECURITY_AUDIT.md`, `SETUP_*.md`, …) are older design notes. Treat the code as the source of truth.