Back to Kilocode

TESTING.md

TESTING.md

7.2.409.7 KB
Original Source

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).

TL;DR

bash
# 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

1. What this doc is for

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.

2. kilo serve vs bun dev serve — important

CommandWhat it runs
kilo serveThe 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.

3. Starting the backend (background)

bash
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]+$")

Fixed port (if you need a stable URL)

bash
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

No-auth quickstart (fastest)

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.

bash
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

Flags (packages/opencode/src/cli/network.ts)

FlagDefaultNotes
--port0 (OS-assigned)Must be passed literally when overriding opencode.json's server.port.
--hostname127.0.0.1Becomes 0.0.0.0 when --mdns is set without an override.
--mdnsfalsePublishes an mDNS SRV record.
--mdns-domainkilo.local
--cors[]Extra allowed origins.

4. The two mandatory request knobs

Auth header (only if KILO_SERVER_PASSWORD was set)

bash
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.

Directory header on every call

bash
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.

5. Common curl recipes

All of the below assume BASE, AUTH, DIR_HEADER are set. Drop -H "$AUTH" if you're running without a password.

Health check (no auth, no directory)

bash
curl -sS "$BASE/global/health"

Full endpoint list (OpenAPI spec)

bash
curl -sS "$BASE/doc" | jq .

This is the source of truth — anything not in this doc is discoverable from /doc without reading source.

Create a session

bash
SID=$(curl -sS -X POST "$BASE/session" \
  -H "$AUTH" -H "$DIR_HEADER" -H "Content-Type: application/json" \
  -d '{}' | jq -r .id)
echo "$SID"

List sessions (GET — directory goes in the query)

bash
curl -sS -H "$AUTH" \
  "$BASE/session?directory=$(printf %s "$PWD" | jq -sRr @uri)"

Send a message (fire-and-forget)

bash
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"}]}'

Read messages for a session

bash
curl -sS -H "$AUTH" \
  "$BASE/session/$SID/message?directory=$(printf %s "$PWD" | jq -sRr @uri)"

Abort an in-flight prompt

bash
curl -sS -X POST -H "$AUTH" -H "$DIR_HEADER" "$BASE/session/$SID/abort"

Get the resolved config (verify a config change took effect)

bash
curl -sS -H "$AUTH" \
  "$BASE/config?directory=$(printf %s "$PWD" | jq -sRr @uri)"

Stream global events (SSE)

bash
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":"…",…}}.

6. Stopping the backend

bash
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.

7. Useful environment variables

VarWhy you'd set it
KILO_SERVER_PASSWORDEnable Basic auth. Omit for auth-bypassed local testing.
KILO_DB=":memory:"Skip on-disk SQLite — hermetic runs.
KILO_DISABLE_DEFAULT_PLUGINS=trueDon't auto-load bundled plugins.
KILO_WORKSPACE_ID=<id>Single-workspace mode; disables control-plane routes.
KILO_TELEMETRY_LEVEL=offDisable PostHog during tests.
KILO_CONFIG_CONTENT='{…}'Inline JSON config without writing a file.

8. Last resort: typed SDK via a throwaway script

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.

ts
// /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
}
bash
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.

9. Pitfalls

  • Running kilo serve instead of bun dev serve runs the installed prod binary, not your edits.
  • Missing x-kilo-directory (or ?directory=) returns 400 from InstanceMiddleware.
  • curl without -N buffers SSE output — you won't see events until the connection closes.
  • Hardcoding port 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.
  • When sharing /tmp/kilo-serve.log / .pid across terminals, unique-suffix the paths to avoid clobbering parallel runs.

10. After changing server routes

Regenerate the SDK and OpenAPI spec so /doc and typed clients stay in sync:

bash
./script/generate.ts    # from repo root

See AGENTS.md for the full rationale.