Skip to content

Latest commit

 

History

History
530 lines (344 loc) · 23 KB

File metadata and controls

530 lines (344 loc) · 23 KB

Contributing — macOS Local Development Setup

This guide walks you through setting up the Kilo Code monorepo for local development on macOS.

Prerequisites

You need the following system-level tools installed before proceeding. If you already have any of these, skip the relevant step.

Xcode Command Line Tools

xcode-select --install

Homebrew

Install from https://brew.sh or from the GitHub releases.

If Homebrew isn't on your PATH yet:

echo 'export PATH=/opt/homebrew/bin:$PATH' >> ~/.zshrc
source ~/.zshrc

Git and Git LFS

brew install git git-lfs
git lfs install --skip-repo

The --skip-repo flag avoids conflicts with the project's Husky hooks. Git LFS is used for large binary files (videos).

Node.js 24.14.1 (via nvm)

The project requires Node.js 24.14.1 locally (see .nvmrc) and accepts any Node.js 24.x runtime in package.json engines.

brew install nvm
mkdir -p ~/.nvm

Add the following to your ~/.zshrc:

# nvm (Node Version Manager)
export NVM_DIR="$HOME/.nvm"
[ -s "/opt/homebrew/opt/nvm/nvm.sh" ] && \. "/opt/homebrew/opt/nvm/nvm.sh"
[ -s "/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm" ] && \. "/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm"

Then reload your shell:

source ~/.zshrc

pnpm

The project uses pnpm as its package manager. Use Corepack so the active pnpm version matches the version pinned in package.json (packageManager).

corepack enable
corepack install

Docker

Install Docker Desktop either from the website or via Homebrew:

brew install --cask docker

Important: Open Docker Desktop at least once after installation — it configures the CLI tools needed for docker compose.

Vercel CLI (recommended if you have access)

Used to pull environment variables from the Vercel project:

pnpm add -g vercel

Stripe CLI (optional, for payment testing)

Install it to enable local Stripe webhook forwarding. pnpm dev:start skips the Stripe forwarder when the CLI is not installed.

brew install stripe/stripe-cli/stripe

Project Setup

1. Clone the repository

git clone git@github.com:Kilo-Org/cloud.git
cd cloud
nvm install
nvm use

2. Install dependencies and pull LFS assets

pnpm install
git lfs pull

3. Set up environment variables

a. Set up using Vercel

The project pulls environment variables from Vercel. Run these commands interactively (each will prompt for browser-based authentication):

vercel login
vercel link --project kilocode-app
vercel env pull

This creates .env.local with all required environment variables.

The KiloClaw pages (/claw/*) render the Pylon support chat widget, which requires two env vars to activate:

  • NEXT_PUBLIC_PYLON_APP_ID — the Pylon app ID from the Pylon dashboard
  • PYLON_IDENTITY_SECRET — the identity verification secret used to HMAC-sign user emails

Both are already present in Vercel and pulled by vercel env pull. If either is missing the widget is silently skipped, so local dev continues to work without Pylon configured.

b. Set up manually

If you do not have Vercel access (typical for non-Kilo-employees), run the interactive setup CLI to bootstrap .env.local:

pnpm dev:setup-env

This prompts for the 8 required env vars only, generates secrets automatically, and writes .env.local. It warns if .env.local already exists before making changes.

After it completes, run:

pnpm dev:env

The setup covers: NEXTAUTH_SECRET, NEXTAUTH_URL, POSTGRES_URL, CALLBACK_TOKEN_SECRET, BYOK_ENCRYPTION_KEY, INTERNAL_API_SECRET, STRIPE_SECRET_KEY, and NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY.

These changes will allow you to do local testing with a fake account.

c. Add or rotate shared web environment variables

Use the repository workflow instead of editing Vercel projects independently. It updates kilocode-app and kilocode-global-app together for Development, Staging, and Production:

pnpm web:env set EXAMPLE_API_TOKEN

Prerequisites:

  • Sign in with vercel login and have access to both projects in the kilocode scope.
  • Install the 1Password CLI and have write access to the Kilo Web ENV Production vault. If needed, the CLI prompts you to sign in with Touch ID.
  • Have pnpm available; the command runs the pinned Vercel CLI with pnpm dlx.

The command asks whether the variable is sensitive, defaulting to yes. Sensitive Production and Staging values use Vercel's sensitive type, while Development remains encrypted but exportable through vercel env pull. The Production value is also stored as a concealed, exact-name item in Kilo Web ENV Production; its notes identify the local user and computer that last updated it.

Answer no for public or otherwise non-secret configuration. NEXT_PUBLIC_* variables must be non-sensitive because Next.js exposes them to browsers. Non-sensitive values are not copied to 1Password.

The command prompts for single-line values without echoing them, then asks for a default value for each tracked root and apps/web dotenv file. Enter a value directly, or press Return to skip that file. If every file is skipped, the command warns that the application must work without the variable so external contributors can still run it. A tracked default cannot match a remote value; use a non-secret local default instead. Invalid yes/no answers and empty remote values are prompted again instead of terminating the command. For multiline values, use --development-file, --staging-file, and --production-file. Use --dry-run to preview the redacted plan.

Remote updates are sequential rather than transactional. If a provider fails partway through, fix the problem and rerun the same command; it safely upserts every target. The workflow does not deploy, so trigger the appropriate deployment separately.

4. Start the database

The project uses PostgreSQL 18 with pgvector, running via Docker. The compose file is at dev/docker-compose.yml:

docker compose -f dev/docker-compose.yml up -d

This starts a PostgreSQL container on port 5432 with:

  • User: postgres
  • Password: postgres
  • Database: postgres

5. Run database migrations

pnpm drizzle migrate

You need to re-run this every time you pull new migrations from the repository.

If you want to fully reset the local dev database first, use:

pnpm dev:db:reset
pnpm drizzle migrate

To smoke-test that migrations still bootstrap correctly from a fresh empty database, run:

pnpm drizzle:verify-bootstrap

6. Start the development server

KILO_PORT_OFFSET=auto pnpm dev:start

This launches a tmux dashboard with the Next.js app and local infrastructure. The automatic offset keeps secondary worktrees from colliding with the root checkout. When the Stripe CLI is installed, the command also starts the Stripe webhook forwarder. Run pnpm dev:status to get the web app's port.

To stop all services:

pnpm dev:stop

Verifying Your Setup

Run the root test script to confirm everything is working:

pnpm test

This runs the web tests and web environment tests. They should pass against the local PostgreSQL database.

Common Development Commands

Command Description
KILO_PORT_OFFSET=auto pnpm dev:start Start all local services in a tmux dashboard with worktree-safe ports
pnpm dev:stop Stop the tmux session and all services
pnpm dev:env Sync .dev.vars files from .env.local (see Worker .dev.vars setup)
pnpm test Run web tests and web environment tests
pnpm typecheck Run the TypeScript type checker
pnpm lint Lint all source files
pnpm format Format all supported files with oxfmt
pnpm format:changed Format only files changed since main
pnpm validate Run the root typecheck, lint, and test scripts
pnpm drizzle migrate Apply pending database migrations
pnpm drizzle generate Generate a new migration after schema changes
pnpm drizzle:verify-bootstrap Create a temporary empty database and verify pnpm drizzle migrate bootstraps it cleanly
pnpm dev:db:reset Drop all app-owned schemas in the local dev database, recreate public, and leave the DB truly empty before re-migrating
pnpm --filter web stripe Start Stripe webhook forwarding to localhost
pnpm test:e2e Run Playwright end-to-end tests

Git Workflow

  • Direct commits to main are blocked by a git hook. Always work on a feature branch.

Stripe Webhook Testing

To test Stripe integration locally:

  1. Install and log in to Stripe CLI: stripe login
  2. Start local development: pnpm dev:start
  3. The dev launcher starts the webhook forwarder and writes STRIPE_WEBHOOK_SECRET to apps/web/.env.development.local.

If the Stripe CLI is not installed, pnpm dev:start skips webhook forwarding. To run only the webhook forwarder manually, use pnpm --filter web stripe.

Database Schema Changes

  1. Edit the schema in packages/db/src/schema.ts
  2. Generate a migration: pnpm drizzle generate
  3. Apply it: pnpm drizzle migrate

Nix Alternative

If you prefer Nix, the project includes a flake.nix with a dev shell that provides all required tools. With direnv installed, the .envrc file will automatically activate the Nix environment when you enter the project directory.

Fake Login (Local Authentication)

In local development, you can sign in without real OAuth by navigating to:

http://localhost:<port>/users/sign_in?fakeUser=<email>&callbackPath=<path>

Use the port reported by pnpm dev:status. This creates a local-only user with the @@fake@@ hosted domain. Set callbackPath to go directly to a page after login:

http://localhost:<port>/users/sign_in?fakeUser=someone@example.com&callbackPath=/profile

Admin access

Some features (e.g., admin panels) are only visible to users with is_admin = true.

  • Real OAuth: new @kilocode.ai signups are never automatically made admins. An existing qualifying @kilocode.ai admin must grant access explicitly from /admin/admins.
  • Fake login: emails must end in @admin.example.com to get admin access automatically at signup. This bootstrap only applies in environments where fake login is enabled; note that a fake-login someone@kilocode.ai user does not qualify for production-domain admin grants because its hosted domain is @@fake@@, not kilocode.ai.

To sign in as a fake admin:

http://localhost:<port>/users/sign_in?fakeUser=yourname@admin.example.com&callbackPath=/admin

A non-@admin.example.com email (e.g., someone@kilocode.ai) used via fake login will not be an admin, because the fake-login provider sets hosted_domain to @@fake@@, not kilocode.ai.

Organizations & Enterprise Trials

New organizations start with a 30-day enterprise trial. After expiry, the UI progressively locks down: first a soft lock (read-only with dismiss option), then a hard lock (no access without subscribing). This can be inconvenient in local development.

Using the built-in dev organization

The easiest approach is to use the pre-configured dev organization. While signed in, run the following in the browser console (the endpoint is POST-only):

fetch('/api/dev/create-kilocode-org', { method: 'POST' })
  .then(r => r.json())
  .then(console.log);

This creates a "Kilocode Local" org (id: 00000000-0000-0000-0000-000000000000) with:

  • plan: 'enterprise'
  • require_seats: false — bypasses all trial/subscription checks
  • free_trial_end_at: '9999-12-31' — effectively never expires

Making any organization never expire

If you've already created an organization and want to prevent its trial from expiring, you have two options:

Option A: Set require_seats to false in the database

This is the most reliable bypass — it short-circuits all trial enforcement (server-side middleware, client-side UI, and login redirects):

UPDATE organizations SET require_seats = false WHERE id = '<your-org-id>';

Option B: Use the admin panel

  1. Sign in as a fake admin (yourname@admin.example.com)
  2. Open the admin panel from the account dropdown in the top-right corner
  3. Find your organization and either:
    • Set free_trial_end_at to a far-future date
    • Toggle on suppress_trial_messaging (hides all trial UI)

How trial enforcement works

Trial status is checked at three layers:

Layer Mechanism Bypassed by require_seats = false
tRPC mutations requireActiveSubscriptionOrTrial() middleware throws FORBIDDEN on hard expiry Yes
Login redirect isOrganizationHardLocked() redirects to /profile Yes
Client UI OrganizationTrialWrapper shows banners and lock dialogs Yes

Test organizations with various trial states

A script creates 6 organizations with different trial states for UI testing:

pnpm --filter web script:run db create-trial-test-orgs yourname@admin.example.com

Cloudflare Workers & AI Inference

The application consists of the Next.js app plus several Cloudflare Worker services (see pnpm-workspace.yaml). In local development, most day-to-day work only requires the Next.js app and PostgreSQL — workers are started individually as needed.

AI / model inference

AI inference works locally without any extra services. The Next.js app includes an OpenRouter proxy route (/api/openrouter/[...path]) that calls real AI providers using API keys from .env.local. There are no mocks or local stubs — all inference hits real APIs (OpenRouter, OpenAI, Anthropic, Mistral, etc.).

Running workers locally

Each worker in the workspace can be started individually with wrangler dev (or pnpm dev) from its directory. Workers communicate with Next.js over HTTP using env vars like CLOUD_AGENT_NEXT_API_URL, CODE_REVIEW_WORKER_URL, etc. Dev ports are defined in each worker's wrangler.jsonc.

The easiest way to run workers is with pnpm dev:start (see Common Development Commands), which starts groups of related services in a tmux dashboard.

KiloClaw local setup

KiloClaw uses docker-local by default for local development — no Fly.io access required. To set it up:

  1. Expose the Docker socket over loopback: socat TCP-LISTEN:23750,bind=127.0.0.1,reuseaddr,fork UNIX-CONNECT:/var/run/docker.sock
  2. Build the local image: cd services/kiloclaw && ./scripts/build-local-image.sh
  3. Run pnpm dev:env to create the .dev.vars file (already configured for docker-local)
  4. Start KiloClaw: pnpm dev:start kiloclaw

See services/kiloclaw/README.md for more details, including how to switch to the Fly provider.

Worker .dev.vars setup

Most workers require a .dev.vars file with secrets like NEXTAUTH_SECRET and INTERNAL_API_SECRET. A script automates this:

pnpm dev:env

The script (dev/local/env-sync/) scans every .dev.vars.example in the repo and apps/web/.env.development.local.example, resolves each variable's value, and writes (or patches) the corresponding generated local env file. Before applying, it shows a diff of what will change and asks for confirmation.

Values are resolved using annotations in example env file comment lines:

Annotation What it does Example
(none) Copies the value from .env.local if the key matches, otherwise keeps the template literal INTERNAL_API_SECRET=your-secret-here
# @override Always uses the template literal, even when .env.local contains the same key # @override above a development-only bucket name
# @url <service> Builds http://localhost:<port> from the service's dev port in wrangler.jsonc # @url nextjshttp://localhost:3000
# @from <KEY> Copies the value of a different key from .env.local # @from CODE_REVIEW_WORKER_AUTH_TOKEN
# @pkcs8 Copies from .env.local and converts PKCS#1 PEM keys to PKCS#8 format # @pkcs8 above a private key var

For example, in a .dev.vars.example:

# @url nextjs
API_URL=http://localhost:3000

# @from CODE_REVIEW_WORKER_AUTH_TOKEN
BACKEND_AUTH_TOKEN=your-backend-auth-token

The @url annotation accepts multiple comma-separated services (e.g., # @url svc-a,svc-b) and appends path suffixes (e.g., # @url nextjs/api/events).

Run pnpm dev:env again after pulling changes that add new env vars to any .dev.vars.example.

RSA environment keypair generation

Generate a dedicated RSA keypair when one runtime encrypts environment-backed secrets and another runtime decrypts them:

pnpm exec tsx dev/generate-rsa-env-keypair.ts -- \
  --out-dir <secure-output-dir> \
  --public-env <PUBLIC_KEY_ENV> \
  --private-env <PRIVATE_KEY_ENV>

The command requires a new output directory outside the repository, then writes restricted PKCS#8 private-key, SPKI public-key, and base64 env-assignment files without overwriting existing output. Store private.pem and private.env in an approved secrets manager and never commit them. Generate a separate keypair for each encryption domain; do not reuse deployment, agent-profile, or GitHub user-token keypairs.

Local Grafana (reads prod Analytics Engine)

KiloClaw emits events to Cloudflare Analytics Engine (datasets kiloclaw_events, kiloclaw_controller_telemetry). A local-only Grafana is available for querying those datasets against the real production CF account — there is no local ClickHouse, and wrangler dev cannot simulate AE writes, but Grafana can always read what prod has already written.

Grafana is part of the observability group in the dev runner, so pnpm dev:start observability (or pnpm dev:start all) boots it alongside the other observability workers. It shows up in the tmux sidebar under OBSERVABILITY on port 4000.

One-time setup:

  1. Create a Cloudflare user API token with a single permission: All accounts → Account Analytics: Read. No zone, DNS, or write permissions required — this is strictly a read-only token.
  2. Add it to .env.local (the same file used by pnpm dev:env):
    CF_AE_TOKEN=<token>
    
  3. pnpm dev:start observability — Grafana is available at http://localhost:4000 (default admin/admin).

The dev runner passes --env-file .env.local to docker compose when starting infra, so the token reaches the Grafana container via env substitution without being loaded into the runner's process.env. Shell exports still override file values.

If CF_AE_TOKEN is missing, Grafana will still boot — only dashboard queries fail. The runner prints an advisory warning at startup. See dev/grafana/README.md for full provisioning details and dashboard coverage.

Limitations in local dev

  • Service bindings resolve locally for Workers launched together by pnpm dev:start when the bound target is running. Bindings to optional services remain unavailable unless their owning group is started (for example, session-ingest -> o11y requires the observability group).
  • Webhook → KiloClaw Chat triggers require the KiloClaw worker running on port 8795. The webhook worker calls it via KILOCLAW_API_URL (HTTP, not a service binding) to deliver messages to Stream Chat. Stream Chat credentials (STREAM_CHAT_API_KEY, STREAM_CHAT_API_SECRET) must be in kiloclaw/.dev.vars.
  • Cloudflare Containers (used by Cloud Agent Next and App Builder) always run on Cloudflare's remote infrastructure, even in dev mode. Purely local execution is not possible.
  • Analytics Engine writes are no-ops in wrangler dev — there is no local AE simulator. Reads against the real prod datasets still work via the local Grafana above. Pipelines and dispatch namespaces don't work locally.

What works without running any workers

The core Next.js app handles profiles, organizations, usage tracking, billing, and the OpenRouter inference proxy without any workers. Features that require a specific worker (e.g., Cloud Agent sessions, code reviews, app builder) will fail gracefully or show connection errors if that worker isn't running.

Multi-worktree support

If you use git worktree to run multiple checkouts simultaneously, set the KILO_PORT_OFFSET environment variable to avoid port collisions between worktrees:

# Automatic offset derived from the worktree directory name (0 for the primary worktree):
export KILO_PORT_OFFSET=auto

# Or a fixed numeric offset (added to every service port):
export KILO_PORT_OFFSET=100

With auto, the primary worktree gets offset 0 (default ports), and secondary worktrees get a deterministic offset based on the directory name. The offset is added to the Next.js port (3000), all worker dev ports, and the URLs generated by pnpm dev:env. Use the same offset when syncing env values and starting or restarting services in a worktree.

pnpm dev:start also passes a worktree-local Wrangler service-discovery registry at .wrangler/dev-registry into its tmux session. For worktrees with distinct kilo-dev-* session names, this allows concurrent offset Worker stacks such as agents to use the same local Worker names without resolving bindings to Workers running from sibling worktrees. The absolute registry path is recorded in dev/logs/manifest.json for diagnostics.

Infrastructure containers (postgres on 5432, redis on 6379, redis-http on 8079, grafana on 4000) always bind to their fixed host ports regardless of the offset - they are shared services, not per-worktree instances. Concurrent worktrees reuse those containers, and pnpm dev:stop leaves them running while another kilo-dev-* session remains active.

The Next.js dev script exports local Redis defaults before next dev starts: UPSTASH_REDIS_REST_URL=http://localhost:8079 and UPSTASH_REDIS_REST_TOKEN=example_token for the shared @upstash/redis REST helper, plus REDIS_URL=redis://localhost:6379 for Chat SDK state because @chat-adapter/state-redis uses the Redis TCP protocol.

Troubleshooting

Node version mismatch

If you see errors about unsupported Node.js versions, ensure you're using the pinned Node 24 release:

nvm use
node --version  # Should output v24.14.1

Database connection errors

Make sure the PostgreSQL container is running:

docker compose -f dev/docker-compose.yml up -d
docker ps | grep postgres

The connection string used by the app is postgres://postgres:postgres@localhost:5432/postgres.

Missing .env.local

The dev server won't start without environment variables. Run vercel env pull to create .env.local. If you don't have Vercel access yet, ask a team member for help.

pnpm install fails with engine mismatch

This means your active Node.js version doesn't match the supported 24.x range in package.json. Switch to the pinned local version with nvm use.

Git LFS files show as pointer files

If image/video files appear as small text files with oid sha256:..., run:

git lfs pull