CLAUDE.md · diff

git:20260429.aa43ace to git:20260804.f397b3a

14 added, 168 removed. Audit A to A.

- # CLAUDE.md
-
- This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
-
- ## What This Is
-
- run402-mcp is an MCP (Model Context Protocol) server that exposes Run402 developer tools — provisioning Postgres databases, deploying static sites and serverless functions, managing storage, secrets, subdomains, and x402 USDC micropayments. It ships five interfaces from this monorepo:
-
- - **SDK** (`sdk/`) — typed TypeScript client for the run402 API. Used by external integrators, MCP/CLI/OpenClaw, and (eventually) inside deployed functions. Published as `@run402/sdk` on npm. Two entry points: root (isomorphic — works in Node 22, Deno, Bun, V8 isolates) and `/node` (zero-config Node defaults — keystore + allowance + x402).
- - **MCP server** (root `src/`) — the main package, published as `run402-mcp` on npm. Each tool is a thin shim over an SDK call.
- - **CLI** (`cli/`) — standalone CLI published as `run402` on npm. Each subcommand is a thin shim over an SDK call; argv parsing and JSON output stay at the CLI edge.
- - **Functions library** (`functions/`) — in-function helper library used inside deployed serverless functions. Exposes `db(req)`, `adminDb()`, `getUser()`, `email`, `ai`. Published as `@run402/functions` on npm. Distinct from the SDK: this is the request-scoped, in-function shape; the SDK is the typed external client. The two are complementary, not redundant.
- - **OpenClaw skill** (`openclaw/`) — skill for OpenClaw agents, re-exports from CLI modules.
-
- The first four packages release in lockstep via the `/publish` skill at the same version (the skill also supports per-package selection for off-cycle patches). All four also share the request kernel via `@run402/sdk` (except `@run402/functions`, which makes raw `fetch()` calls against the project's own endpoints using ambient request context). `core/` holds filesystem primitives (keystore, allowance) that the Node SDK provider wraps.
-
- ## Build & Test Commands
-
- ```bash
- npm run build:core # tsc -p core/tsconfig.json → core/dist/
- npm run build:sdk # tsc -p sdk/tsconfig.json → sdk/dist/
- npm run build:functions # tsc -p functions/tsconfig.json → functions/dist/
- npm run build # build:core + build:sdk + build:functions + tsc → dist/
- npm run start # node dist/index.js (stdio MCP transport)
- npm run test:skill # node --test --import tsx SKILL.test.ts (validates SKILL.md frontmatter/body)
- npm run test:sync # node --test --import tsx sync.test.ts (checks MCP/CLI/OpenClaw/SDK stay in sync)
- npm test # runs all tests (SKILL.test.ts + sync.test.ts + core/src/**/*.test.ts + sdk/src/**/*.test.ts + src/**/*.test.ts)
- npm run test:e2e # node --test cli-e2e.test.mjs (47 CLI end-to-end tests)
- ```
-
- Unit tests use Node's built-in `node:test` runner with `tsx` for TypeScript:
-
- ```bash
- # Run all unit tests
- node --test --import tsx core/src/**/*.test.ts src/**/*.test.ts
-
- # Run a single test file
- node --test --import tsx src/tools/run-sql.test.ts
- node --test --import tsx core/src/keystore.test.ts
- ```
-
- Tests are excluded from the build (`tsconfig.json` and `core/tsconfig.json` both exclude `**/*.test.ts`).
-
- ### Sync Test (`sync.test.ts`)
-
- `sync.test.ts` defines the canonical API surface in a `SURFACE` array and checks:
- - MCP tools in `src/index.ts` match the expected set (no missing, no extra)
- - CLI commands in `cli/lib/*.mjs` match the expected set
- - OpenClaw commands in `openclaw/scripts/*.mjs` match the expected set (follows re-exports to CLI)
- - CLI and OpenClaw have identical command sets (parity)
- - Every SURFACE capability maps to an SDK method (or explicit `null` in `SDK_BY_CAPABILITY`); every SDK method is referenced by SURFACE (orphan check)
- - If `~/Developer/run402-private/site/llms.txt` exists: MCP Tools table lists all tools, all endpoints documented
-
- When adding a new tool/command, add it to the `SURFACE` array **and** `SDK_BY_CAPABILITY` in `sync.test.ts`.
-
- ## Architecture
-
- ```
- @run402/sdk (typed TypeScript kernel — 17 namespaces, ~90 methods)
- │
- │ /index.ts (isomorphic: Node + sandbox)
- │ /node (Node-only: keystore + allowance + x402-wrapped fetch)
- │
- ├─── MCP tools (src/tools/*.ts) — thin shim → SDK → markdown format
- ├─── CLI (cli/lib/*.mjs) — thin shim → SDK → JSON output + exit code
- └─── OpenClaw (openclaw/scripts/*.mjs) — re-exports from CLI
-
- core/ ← Node-only primitives (keystore, allowance, SIWE signing, config paths)
- Imported by `sdk/src/node` via `../../../core/dist/`; not an npm package.
- ```
-
- The SDK is the canonical kernel — a single typed client with a `CredentialsProvider` interface for credential access and a pluggable `fetch` (for x402 wrapping in Node, session tokens in sandboxes). MCP handlers and CLI commands are thin shims: argv/schema parsing + SDK call + output formatting. When code-mode MCP ships, the same SDK runs inside a V8 isolate.
-
- ### SDK (`sdk/src/`)
-
- - **`index.ts`** — `Run402` class + `run402()` factory + `files()` helper. Isomorphic entry point.
- - **`kernel.ts`** — Request function, `Client` interface. Only place that calls `globalThis.fetch`.
- - **`errors.ts`** — `Run402Error` hierarchy: `PaymentRequired`, `ProjectNotFound`, `Unauthorized`, `ApiError`, `NetworkError`, `Run402DeployError` (the v1.34+ structured envelope from the deploy state machine). Never calls `process.exit`.
- - **`credentials.ts`** — `CredentialsProvider` interface. Required: `getAuth`, `getProject`. Optional: `saveProject`, `updateProject`, `removeProject`, `setActiveProject`, `getActiveProject`, `readAllowance`, `saveAllowance`, `createAllowance`, `getAllowancePath`.
- - **`namespaces/*.ts`** — One class per resource group (projects, blobs, functions, email, …). Namespaces hold a `Client` and expose typed methods. The canonical deploy primitive lives at **`namespaces/deploy.ts`** (with shared types in `deploy.types.ts`) — see "Unified Deploy" below.
- - **`node/*.ts`** — Node-only entry point (`@run402/sdk/node`). Wraps `core/` keystore + allowance into `NodeCredentialsProvider`. Sets up x402-wrapped fetch via `createLazyPaidFetch()`. Adds `fileSetFromDir(path)` for filesystem byte sources to the deploy primitive.
-
- ### Unified Deploy (v1.34+)
-
- - **`namespaces/deploy.ts`** — `Deploy` class exposing the canonical primitive. Three layers:
- - `r.deploy.apply(spec, opts?)` — one-shot, awaits to terminal (most agents use this).
- - `r.deploy.start(spec, opts?)` — returns a `DeployOperation` with `events()` async iterator + `result()` promise.
- - `r.deploy.plan` / `upload` / `commit` — low-level steps for CLI debugging.
- - Plus `r.deploy.resume(operationId)`, `status`, `getRelease`, `diff`.
- - **All bytes ride through CAS.** The plan request body never carries inline bytes — only `ContentRef` objects. When the normalized spec exceeds 5 MB JSON, the SDK uploads the manifest itself as a CAS object and references it (`manifest_ref` escape hatch — no body-size cliff).
- - **Replace vs patch semantics per resource.** `site.replace` = "this is the whole site" (files absent are removed in the new release); `site.patch.put` / `patch.delete` = surgical updates. Same for `functions`, `secrets`, `subdomains`. Top-level absence = leave untouched.
- - **Server-authoritative manifest digest.** The gateway returns the canonical digest in the plan response. The SDK no longer requires byte-for-byte canonicalize agreement — `canonicalize.ts` is now a UX helper only.
- - **Backward-compat shims.** `apps.bundleDeploy` translates legacy options (including `inherit: true` with a deprecation warning) into a `ReleaseSpec` and delegates to `deploy.apply`. `sites.deployDir` is a thin wrapper that uses `fileSetFromDir(dir)` and synthesizes both unified `DeployEvent` shapes and the legacy `{ phase: ... }` shapes for v1.32-era event consumers.
- - **MCP/CLI surface.** `deploy` and `deploy_resume` MCP tools (in `src/tools/deploy.ts` and `src/tools/deploy-resume.ts`) expose the new primitive directly. CLI subcommands `run402 deploy apply` and `run402 deploy resume` (in `cli/lib/deploy-v2.mjs`) mirror them. The legacy `bundle_deploy`/`deploy_site`/`deploy_site_dir` MCP tools and `run402 deploy --manifest` CLI continue to work and route through the same SDK shim.
-
- ### Shared Core (`core/src/`)
-
- The `core/` module contains shared logic imported by all three interfaces:
-
- - **`config.ts`** — Path resolution and env vars: `getApiBase()`, `getConfigDir()`, `getKeystorePath()`, `getAllowancePath()`.
- - **`allowance.ts`** — `readAllowance()`, `saveAllowance()` with atomic writes (temp-file + rename, mode 0600).
- - **`allowance-auth.ts`** — EIP-191 signing with `@noble/curves`. `getAllowanceAuthHeaders()` returns headers or null.
- - **`keystore.ts`** — Unified project credential store. Object schema: `{projects: {id: {anon_key, service_key, tier, lease_expires_at}}}`. Auto-migrates legacy array format and `expires_at` → `lease_expires_at`. Functions: `loadKeyStore()`, `saveKeyStore()`, `getProject()`, `saveProject()`, `removeProject()`.
- - **`client.ts`** — `apiRequest()` fetch wrapper. Handles JSON/text responses, 402 payment detection.
-
- Core functions return `null` or throw — they never call `process.exit()`. Each interface wraps with its own error behavior.
-
- ### MCP Server (`src/`)
-
- - **`sdk.ts`** — Lazy SDK singleton (`getSdk()`). Tests call `_resetSdk()` in beforeEach to pick up env changes.
- - **`errors.ts`** — `formatApiError`, `projectNotFound`, and `mapSdkError` which translates `Run402Error` → MCP `{isError, content}` shape.
- - **`config.ts`**, **`keystore.ts`**, **`allowance.ts`** — re-export from `core/dist/` (still used by a few compound handlers like `init`/`status` that read local state directly).
- - **`allowance-auth.ts`** — re-exports core's `getAllowanceAuthHeaders()` + adds `requireAllowanceAuth()` (early-exit with MCP error when no allowance). Kept for handlers where the UX message is more specific than the SDK's `Unauthorized`.
- - **`index.ts`** — Entry point. Registers all tools via `McpServer`.
- - **`tools/*.ts`** — Each tool exports a Zod schema + async handler. The handler is a thin shim: `getSdk().ns.op(...)` in a try/catch that translates errors via `mapSdkError`.
-
- ### CLI (`cli/`)
-
- - **`cli/lib/sdk.mjs`** — Fresh SDK per call via `getSdk()`. Sidesteps stale-env issues across test-file boundaries.
- - **`cli/lib/sdk-errors.mjs`** — `reportSdkError(err)` writes JSON envelope `{status: "error", http?, message?, body_preview?, ...body}` to stderr and calls `process.exit(1)`. Preserves `ProjectNotFound` plain-text format with the "Hint: project IDs start with prj_" guidance.
- - **`cli/lib/config.mjs`** — Imports from `../core-dist/`, adds CLI wrappers (`allowanceAuthHeaders()` with process.exit, `findProject()` with process.exit). Re-exports core keystore functions.
- - **`cli/lib/*.mjs`** — Each module exports `async run(sub, args)`. Subcommand bodies: argv parse + SDK call + JSON output + `reportSdkError` on failure.
- - **`cli/lib/blob.mjs`** retains raw `fetch` for the `put` subcommand only — resumable uploads + per-part concurrency are CLI-specific UX not modeled in the SDK.
- - **`cli/lib/deploy.mjs`** retains raw `undici.fetch` for long-timeout bundle deploys and retry-on-5xx. The SDK covers `apps.bundleDeploy` but the CLI wraps it with a custom dispatcher.
-
- ### OpenClaw (`openclaw/`)
-
- - **`openclaw/scripts/config.mjs`** — Re-exports from `cli/lib/config.mjs`
- - **`openclaw/scripts/*.mjs`** — Thin shims: `export { run } from "../../cli/lib/<name>.mjs"`
- - **`openclaw/scripts/init.mjs`** — Calls CLI's `run()` at top level (executable script)
-
- ### Tool Pattern
-
- Every tool in `src/tools/` exports two things:
- 1. A Zod schema object (e.g., `provisionSchema`) defining input parameters
- 2. An async handler function (e.g., `handleProvision`) returning `{ content: [{type: "text", text: string}], isError?: boolean }`
-
- Tools that require payment (provision, renew, deploy_site, bundle_deploy) return 402 payment details as **informational text** (not errors) so the LLM can reason about payment flow.
-
- ### Error Handling Pattern
-
- All tools use shared error helpers from `src/errors.ts`:
-
- - **`formatApiError(res, context)`** — Formats a non-OK API response into the standard `{ content: [...], isError: true }` shape. Includes HTTP status, extracts `hint`/`retry_after`/`renew_url`/`usage`/`expires_at` from the response body, and adds actionable guidance based on status code. The `context` parameter is a short verb phrase describing what failed (e.g. "running SQL").
-
- - **`projectNotFound(projectId)`** — Returns a consistent "project not found in key store" error with guidance to provision first.
-
- When adding a new tool, use these helpers instead of inline error formatting:
- ```ts
- if (!project) return projectNotFound(args.project_id);
- // ...
- if (!res.ok) return formatApiError(res, "doing the thing");
- ```
-
- ### Test Pattern
-
- Tests mock `globalThis.fetch` and use temp directories for keystore isolation. Each test file follows:
- - `beforeEach`: set `RUN402_API_BASE` env, create temp keystore, mock fetch
- - `afterEach`: restore original fetch and env, clean up temp dir
-
- ### SKILL.md
+ @AGENTS.md
- `SKILL.md` is the OpenClaw skill definition with YAML frontmatter + markdown body. `SKILL.test.ts` validates its structure (frontmatter fields, required sections, tool references, markdown integrity). Run with `npm run test:skill`.
+ ## Git workflow — worktrees, never branches
- ## Environment Variables
+ Multiple agent sessions work these repos in parallel. Isolation is by **git
+ worktree**, not by branching or stashing.
- | Variable | Default | Purpose |
- |----------|---------|---------|
- | `RUN402_API_BASE` | `https://api.run402.com` | API base URL (override for testing/staging) |
- | `RUN402_CONFIG_DIR` | `~/.config/run402` | Local credential storage directory |
- | `RUN402_ALLOWANCE_PATH` | `{config_dir}/allowance.json` | Custom allowance (wallet) file path |
+ - **ALWAYS worktree.** Do work inside a dedicated worktree (`git worktree add`).
+ Never edit, branch-switch, or otherwise disturb a shared checkout — another
+ session may be using it, and switching branches under them destroys their work.
+ - **NEVER create branches.** No feature-branch or PR ceremony on your own
+ initiative. A worktree's auto-created backing branch is plumbing, not workflow.
+ - **NEVER `git stash`.** It is invisible to everyone else and easy to lose.
+ - **Small changes commit directly to `main`, no ceremony.** From a worktree,
+ push with `git push origin HEAD:main` (expect to rebase).
+ - If you find yourself on a branch in a primary checkout, you are already in the
+ wrong place — make a worktree and clean up after yourself.