Local setup
Environment
Required
- a Bash-compatible shell
- Bun: runtime, package manager, test runner and bundler
- Biome: linter and formatter for the code
- Tombi: formatter and linter for the TOML files
Neither is a dependency, so bun install doesn’t bring them. Install the versions the repo expects (bun scripts/tool.ts biome --pinned, bun scripts/tool.ts tombi --pinned); without a package manager that has them, bun add -g @biomejs/biome@<version> tombi@<version> works too. bun lint says what’s missing, and warns when a major.minor version differs from CI’s.
Recommended
- Docker Compose for PostgreSQL, a local mail catcher and S3-compatible object storage (RustFS)
- VSCode (or compatible) with the recommended extensions, which wire up TOML schemas and Biome. Biome isn’t a dependency, so point the extension at your installed CLI in your user settings:
"biome.lsp.bin": "/usr/bin/biome"(or whereverwhich biomepoints) - Claude Code (the agent notes are
CLAUDE.mdfiles)
Setup
git clone https://github.com/SubZtep/kaja.git
cd kaja
bun install
bunx lefthook install # git hooks
docker compose up -d db mail storage
The database volume lives in ./pgdata, and the migrations in apps/api/migrations run on first boot only. After pulling a schema change, see how the schema is managed.
Bootstrap the env files and generate a local auth secret:
cp apps/api/.env.example apps/api/.env
cp apps/sandbox/.env.example apps/sandbox/.env
cp apps/tui/.env.example apps/tui/.env
cp apps/web/.env.example apps/web/.env
./scripts/create_local_secrets.sh # appends BETTER_AUTH_SECRET to apps/api/.env
The API won’t start without BETTER_AUTH_SECRET. Run the script once, not on every setup, or it appends a second line.
Commands
bun dev # API (with the widget bundle) + web, hot reload
bun dev:api # just the API
bun dev:web # just the web app
bun dev:tui # the terminal client (use this, it passes your TTY through)
bun dev:sandbox # the MCP sandbox (uses your own Chrome; `docker compose up -d sandbox` for the real image)
bun lint # Biome check + tombi TOML format/lint
bun lint:fix # apply fixes, including unsafe ones
bun typecheck # tsc --noEmit across every workspace
bun test # API integration + CLI unit tests
bun docs # this site, with Jekyll (needs Ruby and `bundle install` in docs/)
bun run ./scripts/mass_user_create.ts [n] # create n random local users (default 10)
bun run scripts/barkochba.ts ["secret"] # self-play the barkochba persona against a thinker
Use
bun dev:tui, notbun run --filter @kaja/tui start. The workspace runner doesn’t pass the TTY through, so Ink fails with “Raw mode is not supported”.
After the TUI’s first run has written its config (~/.config/kaja), link it into the repo to edit it beside the code, with the same TOML schemas as the templates in docs/config/:
./scripts/link_user_config.sh # .user-config -> ~/.config/kaja (gitignored)
To keep the TUI you develop apart from the one you use every day, set KAJA_PROFILE. It’s appended to the config and data folder names, so KAJA_PROFILE=dev gives the dev TUI its own ~/.config/kaja-dev/, and the link script points .user-config there instead (rerun it after switching profiles to repoint the link). The dev profile also syncs the marketplace from your checkout’s current branch instead of GitHub’s main, so a first start already sees your marketplace changes once they’re committed. In fish, this sets the variable while you’re inside the repo and clears it everywhere else:
# ~/.config/fish/config.fish
function __kaja_profile --on-variable PWD
if string match -q -- "$HOME/Projects/kaja" $PWD; or string match -q -- "$HOME/Projects/kaja/*" $PWD
set -gx KAJA_PROFILE dev
else
set -e KAJA_PROFILE
end
end
__kaja_profile
--on-variable PWD runs it on every cd, and the call after it covers a shell that opens inside the repo (like the editor’s terminal), where no cd happens. Change the path to where you cloned the repo.
Code generation
Never hand-edit the outputs of these. Their inputs are the source of truth:
bun generate:env # apps/*/.env.example from packages/schema/env/*
bun check:env # fail if any .env.example has drifted
bun generate:env-types # ambient Bun.Env typings per workspace
bun generate:schemas # JSON Schemas for the TOML config files (docs/config/schemas)
bun generate:models # docs/config/models.*.toml from docs/config/catalog.toml
bun check:models # fail if the model examples have drifted
bun sync:locales # give every language the en-GB keys, with placeholders for new text
bun check:locales # fail on drift or untranslated placeholders
The generators run on commit through lefthook whenever their inputs change.
Git hooks
Lefthook checks your commit message (conventional commits), runs the generators and lint:fix and typecheck on commit, and lint, typecheck (plus test on main) and the translations on push. That’s why you rarely run them by hand. Vibe coding has the details.
Local URLs
| Service | Where |
|---|---|
| PostgreSQL | postgresql://testuser:testpass@localhost:5433/kaja |
| MailDev SMTP | localhost:1025 |
| MailDev inbox | http://localhost:1080 |
| API | http://localhost:3001 |
| API reference (dev only) | http://localhost:3001/reference |
| Web | http://localhost:3000 |
| Object storage (RustFS) S3 API | http://localhost:9000 (key kaja, secret kaja-dev-storage) |
| Object storage console | http://localhost:9001 |
Environment variables
Each app under apps/*/ has two env files:
| File | Committed | Purpose |
|---|---|---|
.env.example | yes | generated template, no real values |
.env | no (gitignored) | your local copy with real values |
Never edit .env.example by hand. It’s generated from packages/schema/env/* (see code generation). Compose build args (VITE_API_URL, VITE_APP_URL) default to localhost. Override them with a gitignored root .env, which docker compose loads on its own.
Production ships no .env* files at all. The host or orchestrator injects the variables.
Testing
bun test # everything
bun run --filter @kaja/tui test # CLI unit tests only
bun run --filter @kaja/nasi test # agent brain only
Run bun test from the repo root. API integration tests need a running PostgreSQL matching DATABASE_URL, but use their own database, <dev database>_test beside it (or TEST_DATABASE_URL). It’s rebuilt from apps/api/migrations whenever those files change, so a running bun dev can’t race the tests. The runner preloads apps/api/.env.example then apps/api/.env (via bunfig.toml), and rate limiting turns itself off under bun test. bun test:tz reruns the date-sensitive tests in three far-apart time zones.
CI runs Biome, the type checker, the env-drift check and the full test suite against a PostgreSQL service with the migrations applied and the config seeded (.github/workflows/ci.yaml). A separate workflow builds and releases the CLI binaries.
Next: