From 008de32c065739302db387a96f0ae6055709b0fd Mon Sep 17 00:00:00 2001 From: Mohammed Faheem Date: Sun, 5 Jul 2026 17:40:25 +0700 Subject: [PATCH] docs: add CLAUDE.md guidance for Claude Code (#2354) Top-level guide for Claude Code / AI coding agents working in this repo, complementing docs/ARCHITECTURE.md and open-sse/AGENTS.md. Docs-only: PR's incidental code changes were dropped as they reverted #2366. Co-Authored-By: Mohammed Faheem Co-authored-by: Cursor --- CLAUDE.md | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..d7c21345 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,91 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this is + +9Router (`9router-app`) — a local AI routing gateway + Next.js dashboard. It exposes one OpenAI-compatible endpoint (`/v1/*`) and routes traffic across 40+ upstream providers with format translation, model-combo fallback, multi-account fallback, OAuth/API-key credential management, token refresh, quota/usage tracking, and optional cloud sync. + +Two published artifacts live in this one repo: +- The **dashboard + gateway** (root `package.json`, `9router-app`) — the Next.js server that does the actual routing. +- The **CLI launcher** (`cli/`, published to npm as `9router`) — a separate package that installs/starts the server and manages the tray. It has its own `package.json`, version, and build. + +The code lives in `src/` (Next.js app + dashboard/compat APIs), `open-sse/` (the provider-agnostic routing/translation engine), `cli/` (the launcher package), and `tests/`. + +## Commands + +Dashboard/gateway (run from repo root): +```bash +cp .env.example .env +npm install +PORT=20128 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run dev # dev (webpack, port 20127 by default via next dev) +npm run build && PORT=20128 HOSTNAME=0.0.0.0 npm run start # production +``` +- Bun variants: `npm run dev:bun` / `build:bun` / `start:bun`. +- Default runtime port is **20128** (dashboard at `/dashboard`, API at `/v1`). +- Lint: `npx eslint .` (config `eslint.config.mjs`, extends `eslint-config-next`). + +CLI package (`cli/`): +```bash +npm run cli:pack # build + npm pack from root +cd cli && npm run dev # nodemon watch +``` + +Tests (vitest, in `tests/`, an **independent** ESM package — not wired into root `npm test`): +```bash +npm install # ROOT deps first — tests import from src/ which needs `open`, `undici`, etc. +cd tests && npm install # then tests' own deps (vitest) → tests/node_modules (allowed by tests/.gitignore) +npx vitest run # all tests; auto-discovers tests/vitest.config.js +npx vitest run unit/capabilities.test.js # single file (path relative to tests/) +``` +> The committed `tests/package.json` `test` script hardcodes Unix paths (`NODE_PATH=/tmp/node_modules …`) — a shared-install workaround from upstream. On Windows (or anywhere), ignore it and use the `npx vitest` form above; `vitest.config.js` resolves the `open-sse`/`@/` aliases from the repo root regardless of where vitest lives. +> +> **The suite is NOT expected to be all-green on a plain checkout.** ~938 pass, ~64 fail. Judge regressions with `tests/__baseline__/verify-no-regression.mjs`, not a raw run. Expected red: +> - 26 catalogued in `tests/__baseline__/known-fails.txt` (rtk, oauth-cursor-auto-import, translator-request-normalization, …). +> - `unit/embeddings.cloud.test.js` imports `cloud/src/handlers/embeddings.js` — the `cloud/` worker dir is **not in this repo**, so it always fails here. +> - `unit/xai-oauth-service.test.js` times out (5s) when the xAI endpoint-discovery fetch isn't reachable/mocked. +> - `real/*.real.test.js` make live provider calls — need credentials, skip otherwise. +- `*.real.test.js` under `tests/translator/real/` make live provider calls — skip unless credentials are set. +- Regression baselines: `tests/__baseline__/verify-*.mjs` compare against committed snapshots (providers, aliases, OAuth URLs). Run these after touching provider registry / alias logic. + +## Architecture + +Two authoritative docs already exist — read them before working in these areas rather than re-deriving: +- `docs/ARCHITECTURE.md` — full system: request lifecycle, combo/account fallback, OAuth + token refresh, cloud sync, data model. +- `open-sse/AGENTS.md` — the routing/translation engine's own conventions and "how to add a provider/executor/translator". **Read this before editing anything under `open-sse/`.** + +### Request flow (the thing to understand first) +`src/app/api/v1/*` route (Next rewrite maps `/v1/*` → `/api/v1/*` in `next.config.mjs`) +→ `src/sse/handlers/chat.js` (parse, combo expansion, account-selection loop) +→ `open-sse/handlers/chatCore.js` (detect source format, translate request, dispatch to executor, retry/refresh, stream setup) +→ `open-sse/executors/*` (per-provider upstream call; `default.js` handles any OpenAI-compatible provider) +→ `open-sse/translator/*` (client format ↔ provider format) +→ SSE back to client. + +`src/sse/` is the app-side entry glue; `open-sse/` is the provider-agnostic engine (also usable standalone). Cross that boundary consciously. + +### Translator engine (`open-sse/translator/`) +- Pivots through **OpenAI as the intermediate format**. A translator registered on an exact `source:target` pair (e.g. `claude:kiro`) runs as a **direct route**, skipping the lossy double-hop. Prefer a direct route for fragile pairs (thinking blocks, tool ids, non-base64 images, `is_error`). +- Translators **self-register** via `register(from, to, reqFn, resFn)` as an import side effect — a new translator file MUST be imported in `open-sse/translator/index.js` or it never runs. +- Never hardcode role/block/model strings — use `open-sse/translator/schema/` and `open-sse/config/` constants. Config-driven and DRY is enforced by convention here. + +### Provider registry (`open-sse/providers/registry/*`) +- One file per provider. `providers/registry/index.js` is an **auto-generated** static import list — regenerate it with `scripts/migrate-registry.mjs` / `injectDisplayToRegistry.mjs`, don't hand-edit. +- Add a provider: copy `providers/REGISTRY_TEMPLATE.js`, add models to `config/providerModels.js`. Only add an executor for non-OpenAI-compatible upstreams. + +### Persistence — IMPORTANT (ARCHITECTURE.md is stale here) +State is **no longer `db.json`**. It's a SQLite layer under `src/lib/db/` with an adapter fallback chain (`driver.js`): `bun:sqlite` → `better-sqlite3` (optional native dep) → `node:sqlite` (Node ≥22.5) → `sql.js` (pure-JS fallback, always works). `better-sqlite3` is deliberately in `optionalDependencies` so install never fails without build tools. +- `src/lib/localDb.js` is a **backward-compat shim** re-exporting `src/lib/db/index.js`. New code should import from `@/lib/db/index.js`; per-entity logic lives in `src/lib/db/repos/*`. Schema/migrations in `src/lib/db/migrations/`. +- DB file location resolves via `src/lib/db/paths.js` (`DATA_DIR`, else `~/.9router/`). +- Usage/logs (`src/lib/usageDb.js`, `usage.json` + `log.txt`) still live under `~/.9router` and do **not** follow `DATA_DIR`. + +### RTK token saver (`open-sse/rtk/`) +Pre-translate hooks that compress `tool_result` content in-place to cut tokens. **Fail-open**: any error returns null and leaves the body untouched — never throw out of them. Skips `is_error`/`status:"error"` results to preserve traces. + +## Conventions & gotchas + +- Plain JavaScript (ESM), no TypeScript. `@/*` path alias → `src/*` (`jsconfig.json`). +- `custom-server.js` wraps the Next standalone server to derive client IP from the TCP socket and strip attacker-controlled `X-Forwarded-For` — trusting forwarding headers only from a loopback reverse proxy. Preserve this when touching request/IP/rate-limit code. +- Security-sensitive env: `JWT_SECRET` (session cookie), `INITIAL_PASSWORD` (default `123456` — must override), `API_KEY_SECRET`, `MACHINE_ID_SALT`. Full env contract in `.env.example` and ARCHITECTURE.md's env matrix. +- Binary/protobuf upstreams (kiro EventStream, cursor protobuf, commandcode NDJSON) don't round-trip through OpenAI — they're handled inside their own executor, not the translator. +- Versioning: root and `cli/` are versioned independently; changes are logged in `CHANGELOG.md`. Commit style is Conventional Commits (`fix(translator): …`, `feat(...)`).