TESTING.md
How to spin up the local main-branch Kilo backend and test it with curl / fetch. Aimed at a running Kilo CLI agent iterating on backend fixes without rebuilding the VS Code extension or TUI.
All examples use plain shell + curl. Writing TypeScript files is a last resort (see Section 8).
# From repo root. Starts the LOCAL main-branch backend in the background.
PASS=$(openssl rand -hex 16)
KILO_SERVER_PASSWORD="$PASS" bun dev serve --port 0 >/tmp/kilo-serve.log 2>&1 &
echo $! >/tmp/kilo-serve.pid
while ! grep -q "kilo server listening" /tmp/kilo-serve.log 2>/dev/null; do sleep 0.1; done
PORT=$(grep -oE "listening on http://[^:]+:[0-9]+" /tmp/kilo-serve.log | grep -oE "[0-9]+$")
AUTH="Authorization: Basic $(printf 'kilo:%s' "$PASS" | base64 | tr -d '\n')"
BASE="http://127.0.0.1:$PORT"
# Call any endpoint
curl -sS -H "$AUTH" -H "x-kilo-directory: $PWD" "$BASE/global/health"
# Stop when done
kill "$(cat /tmp/kilo-serve.pid)" 2>/dev/null || true
rm -f /tmp/kilo-serve.pid /tmp/kilo-serve.log
Testing local backend fixes against a real running server, talking to it over HTTP the same way the VS Code extension, TUI, and kilo run --attach do — but without any of those clients. Every request is a curl the agent can copy-paste.
For in-process tests (no socket, fastest loop) see packages/opencode/test/kilocode/server/permission-allow-everything.test.ts for the Server.Default().app.request(...) pattern. That's the right tool inside the packages/opencode/ test suite; this doc is for out-of-process HTTP testing.
kilo serve vs bun dev serve — important| Command | What it runs |
|---|---|
kilo serve | The npm-installed production CLI on $PATH. Not the code in this repo. |
bun dev serve … (repo root) | The local main-branch backend from this worktree. This is what you want. |
bun run --cwd packages/opencode --conditions=browser src/index.ts serve … | Same as bun dev serve, fully expanded. |
Root package.json defines "dev" as the full bun run --cwd packages/opencode --conditions=browser src/index.ts invocation, so bun dev <args> forwards <args> to the local CLI entry point (packages/opencode/src/index.ts) without touching the installed binary.
bun dev imports the source directly — no rebuild is needed between code edits. Just kill the running server and relaunch.
Do not use createKiloServer() from @kilocode/sdk/v2 to test local code: it spawns the PATH kilo binary (packages/sdk/js/src/v2/server.ts:38-136), which is the wrong tool here.
KILO_SERVER_PASSWORD=$(openssl rand -hex 16) \
bun dev serve --port 0 >/tmp/kilo-serve.log 2>&1 &
echo $! >/tmp/kilo-serve.pid
while ! grep -q "kilo server listening" /tmp/kilo-serve.log; do sleep 0.1; done
PORT=$(grep -oE "listening on http://[^:]+:[0-9]+" /tmp/kilo-serve.log | grep -oE "[0-9]+$")
KILO_SERVER_PASSWORD=secret \
bun dev serve --port 4096 --hostname 127.0.0.1 \
>/tmp/kilo-serve.log 2>&1 &
echo $! >/tmp/kilo-serve.pid
while ! grep -q "kilo server listening" /tmp/kilo-serve.log; do sleep 0.1; done
PORT=4096
Omit KILO_SERVER_PASSWORD entirely. The server prints Warning: KILO_SERVER_PASSWORD is not set; server is unsecured. and the auth middleware is bypassed — fine for throwaway local testing, never for anything else.
bun dev serve --port 0 >/tmp/kilo-serve.log 2>&1 &
echo $! >/tmp/kilo-serve.pid
while ! grep -q "kilo server listening" /tmp/kilo-serve.log; do sleep 0.1; done
PORT=$(grep -oE "listening on http://[^:]+:[0-9]+" /tmp/kilo-serve.log | grep -oE "[0-9]+$")
BASE="http://127.0.0.1:$PORT"
# no AUTH var needed
packages/opencode/src/cli/network.ts)| Flag | Default | Notes |
|---|---|---|
--port | 0 (OS-assigned) | Must be passed literally when overriding opencode.json's server.port. |
--hostname | 127.0.0.1 | Becomes 0.0.0.0 when --mdns is set without an override. |
--mdns | false | Publishes an mDNS SRV record. |
--mdns-domain | kilo.local | |
--cors | [] | Extra allowed origins. |
KILO_SERVER_PASSWORD was set)AUTH="Authorization: Basic $(printf 'kilo:%s' "$KILO_SERVER_PASSWORD" | base64 | tr -d '\n')"
The username is literally kilo (same as the VS Code extension). Skip this whole block if you launched without a password.
DIR_HEADER="x-kilo-directory: $PWD"
InstanceMiddleware uses this to scope the request to a project. For GET / HEAD, pass ?directory=<urlencoded> in the URL instead — that's what the SDK does internally.
curl recipesAll of the below assume BASE, AUTH, DIR_HEADER are set. Drop -H "$AUTH" if you're running without a password.
curl -sS "$BASE/global/health"
curl -sS "$BASE/doc" | jq .
This is the source of truth — anything not in this doc is discoverable from /doc without reading source.
SID=$(curl -sS -X POST "$BASE/session" \
-H "$AUTH" -H "$DIR_HEADER" -H "Content-Type: application/json" \
-d '{}' | jq -r .id)
echo "$SID"
curl -sS -H "$AUTH" \
"$BASE/session?directory=$(printf %s "$PWD" | jq -sRr @uri)"
curl -sS -X POST "$BASE/session/$SID/prompt_async" \
-H "$AUTH" -H "$DIR_HEADER" -H "Content-Type: application/json" \
-d '{"parts":[{"type":"text","text":"hello"}]}'
curl -sS -H "$AUTH" \
"$BASE/session/$SID/message?directory=$(printf %s "$PWD" | jq -sRr @uri)"
curl -sS -X POST -H "$AUTH" -H "$DIR_HEADER" "$BASE/session/$SID/abort"
curl -sS -H "$AUTH" \
"$BASE/config?directory=$(printf %s "$PWD" | jq -sRr @uri)"
curl -N -sS -H "$AUTH" \
"$BASE/global/event?directory=$(printf %s "$PWD" | jq -sRr @uri)"
-N disables curl's output buffering so events appear live. Expect lines like data: {"directory":"…","payload":{"type":"…",…}}.
kill "$(cat /tmp/kilo-serve.pid)" 2>/dev/null || true
rm -f /tmp/kilo-serve.pid /tmp/kilo-serve.log
ServeCommand handles SIGTERM / SIGINT / SIGHUP and runs Instance.disposeAll() + server.stop(true) before exiting (packages/opencode/src/cli/cmd/serve.ts:29-31). -9 is only needed if the process hangs past ~5 s.
| Var | Why you'd set it |
|---|---|
KILO_SERVER_PASSWORD | Enable Basic auth. Omit for auth-bypassed local testing. |
KILO_DB=":memory:" | Skip on-disk SQLite — hermetic runs. |
KILO_DISABLE_DEFAULT_PLUGINS=true | Don't auto-load bundled plugins. |
KILO_WORKSPACE_ID=<id> | Single-workspace mode; disables control-plane routes. |
KILO_TELEMETRY_LEVEL=off | Disable PostHog during tests. |
KILO_CONFIG_CONTENT='{…}' | Inline JSON config without writing a file. |
Use this only when curl can't express what you need — typed request/response shapes, complex multi-turn orchestration, SSE consumers that need to coalesce events. Keep the file short and delete it after.
// /tmp/probe.ts — delete after use. Talks to an already-running backend.
import { createKiloClient } from "@kilocode/sdk/v2"
const port = process.env.PORT!
const pass = process.env.KILO_SERVER_PASSWORD
const headers = pass ? { Authorization: "Basic " + Buffer.from("kilo:" + pass).toString("base64") } : undefined
const client = createKiloClient({
baseUrl: `http://127.0.0.1:${port}`,
headers,
directory: process.cwd(),
})
const { data: session } = await client.session.create({}, { throwOnError: true })
await client.session.promptAsync({
sessionID: session.id,
parts: [{ type: "text", text: "hello" }],
})
const events = await client.global.event({})
for await (const ev of events.stream) {
const e = ev as { payload: { type: string } }
console.log(e.payload.type)
if (e.payload.type === "session.idle") break
}
PORT="$PORT" KILO_SERVER_PASSWORD="$KILO_SERVER_PASSWORD" bun /tmp/probe.ts
rm /tmp/probe.ts
Reminder: this script connects to the server you launched in Section 3 — it does not start one. createKiloServer() from the SDK would spawn the PATH kilo binary (production CLI), which defeats the point of testing local code.
kilo serve instead of bun dev serve runs the installed prod binary, not your edits.x-kilo-directory (or ?directory=) returns 400 from InstanceMiddleware.curl without -N buffers SSE output — you won't see events until the connection closes.4096 breaks when a previous run didn't exit cleanly. Parse the log instead.--port must appear literally in argv to override opencode.json's server.port (packages/opencode/src/cli/network.ts:45).KILO_SERVER_PASSWORD must be set before launch — changing it after doesn't rotate credentials./tmp/kilo-serve.log / .pid across terminals, unique-suffix the paths to avoid clobbering parallel runs.Regenerate the SDK and OpenAPI spec so /doc and typed clients stay in sync:
./script/generate.ts # from repo root
See AGENTS.md for the full rationale.