From 24385747f40d839e894dbba7fb7ff6df94738938 Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Mon, 20 Apr 2026 23:37:47 -0400 Subject: [PATCH 01/20] docs(kilo-docs): add plugins documentation page Document the plugin system for the Kilo CLI: installation paths, hooks reference, custom tools, examples, and troubleshooting. Plugins were previously only covered in upstream OpenCode docs. --- packages/kilo-docs/lib/nav/automate.ts | 5 + .../pages/automate/extending/plugins.md | 487 ++++++++++++++++++ 2 files changed, 492 insertions(+) create mode 100644 packages/kilo-docs/pages/automate/extending/plugins.md diff --git a/packages/kilo-docs/lib/nav/automate.ts b/packages/kilo-docs/lib/nav/automate.ts index ce3ddf622b..3bbab572fc 100644 --- a/packages/kilo-docs/lib/nav/automate.ts +++ b/packages/kilo-docs/lib/nav/automate.ts @@ -33,6 +33,11 @@ export const AutomateNav: NavSection[] = [ href: "/automate/extending/shell-integration", children: "Shell Integration", }, + { + href: "/automate/extending/plugins", + children: "Plugins", + platform: "new", + }, { href: "/automate/extending/auto-launch", children: "Auto-launch Configuration", diff --git a/packages/kilo-docs/pages/automate/extending/plugins.md b/packages/kilo-docs/pages/automate/extending/plugins.md new file mode 100644 index 0000000000..5ae61bc47e --- /dev/null +++ b/packages/kilo-docs/pages/automate/extending/plugins.md @@ -0,0 +1,487 @@ +--- +title: "Plugins" +description: "Extend the Kilo CLI with custom hooks, tools, auth providers, and more" +platform: new +--- + +# Plugins + +Plugins extend the Kilo CLI by hooking into events, adding custom tools, registering auth or model providers, and customizing runtime behavior. They are TypeScript or JavaScript modules loaded at startup. + +{% callout type="note" %} +Plugins run in the Kilo CLI. They are available everywhere the CLI runs — directly, through `kilo serve`, and inside the VS Code extension (which spawns `kilo serve` under the hood). +{% /callout %} + +## What plugins can do + +- **Add custom tools** the model can call (like `read`, `write`, `bash`). +- **Intercept tool calls** to mutate arguments, rewrite output, or block dangerous operations. +- **Subscribe to events** — sessions, messages, permissions, LSP diagnostics, file changes, etc. +- **Register auth providers** — OAuth or API-key flows for model providers. +- **Register model providers** — dynamic model catalogs. +- **Mutate chat parameters or headers** sent to the LLM. +- **Customize compaction** — inject or replace the prompt used when a session is compacted. +- **Inject shell environment variables** for commands executed by the agent or user. + +--- + +## Use a plugin + +There are three ways to load plugins. + +### From a config file + +Add an array of plugin specifiers to your config file: + +```json +{ + "$schema": "https://app.kilo.ai/config.json", + "plugin": [ + "@your-org/your-plugin", + "your-plugin@1.2.3", + ["your-plugin", { "apiKey": "{env:MY_API_KEY}" }], + "./plugins/local.ts", + "file:///abs/path/plugin.ts" + ] +} +``` + +Each entry can be: + +| Form | Loaded from | +| -------------------------------------- | ---------------------------------------------------------------- | +| `"package-name"` | Latest version from npm | +| `"package-name@1.2.3"` | Pinned version from npm | +| `["package-name", { options }]` | npm package with options passed to the plugin function | +| `"./path/plugin.ts"` / `"file:///..."` | Local file (relative to the config file or absolute `file:` URL) | + +Config files live in the same locations as the rest of your CLI configuration — see the [CLI configuration reference](/docs/code-with-ai/platforms/cli#configuration). + +### From a plugin directory + +Drop TypeScript or JavaScript files into a `plugin/` or `plugins/` folder inside any config directory: + +- Global: `~/.config/kilo/plugin/` +- Project: `.kilo/plugin/`, `.kilocode/plugin/`, or `.opencode/plugin/` + +Every `.ts` or `.js` file in those directories is auto-registered at startup — no need to list them in the config file. + +```text +my-project/ +├── kilo.json +└── .kilo/ + └── plugin/ + ├── env-guard.ts + └── notifications.ts +``` + +### From the `kilo plugin` command + +Install an npm plugin and patch your config in one step: + +```bash +# Install into the current project's config +kilo plugin my-plugin + +# Install into your global config +kilo plugin my-plugin --global + +# Replace an existing entry +kilo plugin my-plugin --force +``` + +The command resolves the package, reads its `package.json` for `exports["./server"]` / `exports["./tui"]`, and writes the entry into your `kilo.json` / `opencode.json` while preserving comments. + +### How plugins are installed + +- **npm plugins** are installed automatically at startup using Bun. Packages and their dependencies are cached in Kilo's XDG cache directory (`~/.cache/kilo/` on Linux, `~/Library/Caches/kilo/` on macOS, `%LOCALAPPDATA%\kilo\` on Windows). +- **Local plugins** are loaded directly from the plugin directory. If your plugin imports external packages, add a `package.json` to your config directory (see [Dependencies](#dependencies)) — Kilo runs `bun install` on startup so imports resolve. + +### Load order + +Plugins from all sources run on every session. They load in this order: + +1. Internal built-ins (Kilo Gateway auth, Codex auth, Copilot auth, Cloudflare, etc.) +2. Global config plugin array (`~/.config/kilo/kilo.json`) +3. Global plugin directory (`~/.config/kilo/plugin/`) +4. Project config plugin array (`kilo.json` / `opencode.json`) +5. Project plugin directory (`.kilo/plugin/` and friends) + +Duplicates (same package, same version) are deduplicated. Hooks from multiple plugins run sequentially in load order. + +### Disabling external plugins + +Set the `KILO_PURE=1` environment variable to skip all external plugins — only built-in plugins will load. Useful for reproducible CI runs or debugging. + +--- + +## Create a plugin + +A plugin is a module that exports a function returning a set of [hooks](#hooks-reference). + +### Basic structure + +Create a file in your plugin directory: + +```ts +// .kilo/plugin/hello.ts +import type { Plugin } from "@kilocode/plugin" + +const hello: Plugin = async ({ project, client, $, directory, worktree }) => { + console.log("hello plugin loaded") + + return { + // hook implementations go here + } +} + +export default { id: "hello", server: hello } +``` + +The plugin function receives a context object: + +| Field | Description | +| ------------------------ | ----------------------------------------------------------------- | +| `project` | Current project metadata. | +| `directory` | Current working directory for this session. | +| `worktree` | Git worktree root for this session. | +| `client` | A Kilo SDK client (`@kilocode/sdk`) for calling the local server. | +| `$` | [Bun's shell API](https://bun.com/docs/runtime/shell). | +| `serverUrl` | URL of the local Kilo server. | +| `experimental_workspace` | Register workspace adaptors (used by Agent Manager). | + +The function returns a `Hooks` object. Any second argument is the options object passed via config (e.g. the `{ apiKey: "..." }` from `["my-plugin", { apiKey: "..." }]`). + +### Module shape + +Plugins must default-export a module descriptor. `id` is required for local-file plugins and inferred from `package.json#name` for npm plugins. + +```ts +import type { Plugin } from "@kilocode/plugin" + +const server: Plugin = async (ctx) => ({ + /* hooks */ +}) + +export default { + id: "my-plugin", + server, +} +``` + +An npm plugin can also expose a TUI entry point (`tui`) for [TUI plugins](#tui-plugins), but `server` and `tui` are separate modules. + +### TypeScript support + +Install the plugin package locally and import its types: + +```bash +bun add -d @kilocode/plugin +``` + +```ts +import type { Plugin } from "@kilocode/plugin" +import { tool } from "@kilocode/plugin/tool" +``` + +Kilo automatically creates a `package.json` in config directories that contain a `plugin/` folder and installs `@kilocode/plugin` so types resolve out of the box. + +### Engine compatibility + +Declare a CLI version range to prevent a plugin from loading against an incompatible build: + +```json +{ + "name": "my-plugin", + "engines": { "opencode": "^7.0.0" } +} +``` + +If the running CLI does not satisfy the range, the plugin is skipped and a warning is surfaced. + +### Dependencies + +Local plugins and custom tools can use external npm packages. Add a `package.json` to your config directory: + +```json +// .kilo/package.json +{ + "dependencies": { + "shescape": "^2.1.0" + } +} +``` + +Kilo runs `bun install` at startup so your plugins can import the packages: + +```ts +// .kilo/plugin/escape-bash.ts +import { escape } from "shescape" +import type { Plugin } from "@kilocode/plugin" + +const EscapeBash: Plugin = async () => ({ + "tool.execute.before": async (input, output) => { + if (input.tool === "bash") { + output.args.command = escape(output.args.command) + } + }, +}) + +export default { id: "escape-bash", server: EscapeBash } +``` + +--- + +## Hooks reference + +Every hook is optional. Return only the ones you care about. + +### Lifecycle + +| Hook | Description | +| -------- | --------------------------------------------------------------------------------- | +| `config` | Receives the fully-resolved config at startup. Read-only — useful for inspection. | +| `event` | Called for **every** event on the internal bus (see [Events](#events)). | + +### Tools + +| Hook | Description | +| --------------------- | ----------------------------------------------------------------------------------------------- | +| `tool` | Map of tool name → [tool definition](#custom-tools). Added tools are callable by the model. | +| `tool.execute.before` | Fires before a tool runs; you can mutate `output.args`. | +| `tool.execute.after` | Fires after a tool returns; you can rewrite `output.title`, `output.output`, `output.metadata`. | +| `tool.definition` | Mutate a tool's `description` and `parameters` before they are sent to the model. | + +### Chat + +| Hook | Description | +| ------------------------ | ---------------------------------------------------------------------------- | +| `chat.message` | Fires when a new user message arrives. Inspect or modify `parts`. | +| `chat.params` | Mutate `temperature`, `topP`, `topK`, `maxOutputTokens`, provider `options`. | +| `chat.headers` | Add or replace HTTP headers on the LLM API call. | +| `permission.ask` | Auto-allow or auto-deny permission prompts. | +| `command.execute.before` | Intercept slash command execution; mutate the resulting `parts`. | +| `shell.env` | Inject environment variables into every shell command Kilo runs. | + +### Providers & auth + +| Hook | Description | +| ---------- | ------------------------------------------------------------------------------------ | +| `auth` | Register an auth method (OAuth or API key) for a provider, with interactive prompts. | +| `provider` | Dynamically supply a model catalog for a provider (useful for BYO-model gateways). | + +### Experimental + +These hooks live behind the `experimental.` prefix and may change between releases. + +| Hook | Description | +| -------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| `experimental.chat.messages.transform` | Rewrite the full message history before it is sent to the model. | +| `experimental.chat.system.transform` | Modify the system prompt array. | +| `experimental.session.compacting` | Inject extra context (`output.context`) or replace the compaction prompt entirely (`output.prompt`). | +| `experimental.compaction.autocontinue` | Disable the synthetic "continue" turn that follows compaction. | +| `experimental.text.complete` | Post-process final text parts (e.g. append signatures, redact secrets). | + +### Events + +The `event` hook fires for every event on Kilo's internal bus. Common event types include: + +- **Session**: `session.created`, `session.updated`, `session.idle`, `session.error`, `session.deleted`, `session.compacted`, `session.diff`, `session.status` +- **Message**: `message.updated`, `message.removed`, `message.part.updated`, `message.part.removed` +- **Tool**: `tool.execute.before`, `tool.execute.after` +- **Permission**: `permission.asked`, `permission.replied` +- **File**: `file.edited`, `file.watcher.updated` +- **Shell**: `shell.env` +- **Command**: `command.executed` +- **LSP**: `lsp.updated`, `lsp.client.diagnostics` +- **Todo**: `todo.updated` +- **Server**: `server.connected` +- **Installation**: `installation.updated` + +```ts +const server: Plugin = async () => ({ + event: async ({ event }) => { + if (event.type === "session.idle") { + // session finished responding + } + }, +}) +``` + +--- + +## Custom tools + +Plugins can register tools the model can call alongside the built-in ones. Use the `tool()` helper for type-safety: + +```ts +// .kilo/plugin/database.ts +import type { Plugin } from "@kilocode/plugin" +import { tool } from "@kilocode/plugin/tool" + +const DatabasePlugin: Plugin = async () => ({ + tool: { + query: tool({ + description: "Run a read-only SQL query against the project database", + args: { + sql: tool.schema.string().describe("SQL query to execute"), + }, + async execute(args, context) { + const { directory, worktree } = context + // your query logic here + return `ran: ${args.sql}` + }, + }), + }, +}) + +export default { id: "database", server: DatabasePlugin } +``` + +`args` uses a [Zod](https://zod.dev) schema via `tool.schema`. The tool's `execute` function receives: + +- `args` — validated against your schema +- `context` — `{ sessionID, messageID, agent, directory, worktree, abort, metadata, ask }` + +### Name precedence + +If a custom tool uses the same name as a built-in tool, **the custom tool wins**. Prefer unique names unless you intentionally want to override a built-in (for example, to wrap `bash` with extra validation). + +### Alternative: standalone tool files + +For tools that don't need the full plugin context, drop them in a `tool/` or `tools/` folder inside any config directory — for example `.kilo/tool/database.ts` or `~/.config/kilo/tool/database.ts`. The filename becomes the tool name, and each file exports a `tool()` definition directly. The layout is identical to the [OpenCode custom tools guide](https://opencode.ai/docs/custom-tools); substitute `.kilo/` (or `.kilocode/` / `.opencode/`) for `.opencode/`. + +--- + +## Examples + +### Send a notification when a session finishes + +```ts +// .kilo/plugin/notify.ts +import type { Plugin } from "@kilocode/plugin" + +const Notify: Plugin = async ({ $ }) => ({ + event: async ({ event }) => { + if (event.type === "session.idle") { + await $`osascript -e 'display notification "Session complete!" with title "Kilo"'` + } + }, +}) + +export default { id: "notify", server: Notify } +``` + +{% callout type="tip" %} +The VS Code extension already emits system notifications when a session finishes or errors — this plugin is for the raw CLI / TUI. +{% /callout %} + +### Block reads of `.env` files + +```ts +// .kilo/plugin/env-guard.ts +import type { Plugin } from "@kilocode/plugin" + +const EnvGuard: Plugin = async () => ({ + "tool.execute.before": async (input, output) => { + if (input.tool === "read" && String(output.args.filePath).includes(".env")) { + throw new Error("reading .env files is blocked") + } + }, +}) + +export default { id: "env-guard", server: EnvGuard } +``` + +### Inject environment variables into every shell command + +```ts +// .kilo/plugin/inject-env.ts +import type { Plugin } from "@kilocode/plugin" + +const InjectEnv: Plugin = async () => ({ + "shell.env": async (input, output) => { + output.env.MY_API_KEY = "secret" + output.env.PROJECT_ROOT = input.cwd + }, +}) + +export default { id: "inject-env", server: InjectEnv } +``` + +### Structured logging + +Prefer `client.app.log()` over `console.log` so entries land in Kilo's log pipeline: + +```ts +import type { Plugin } from "@kilocode/plugin" + +const Logger: Plugin = async ({ client }) => { + await client.app.log({ + body: { + service: "my-plugin", + level: "info", + message: "plugin initialized", + extra: { version: "1.0.0" }, + }, + }) + return {} +} + +export default { id: "logger", server: Logger } +``` + +Levels: `debug`, `info`, `warn`, `error`. + +### Inject context during session compaction + +```ts +// .kilo/plugin/compaction.ts +import type { Plugin } from "@kilocode/plugin" + +const Compaction: Plugin = async () => ({ + "experimental.session.compacting": async (input, output) => { + output.context.push( + "## Persist across compaction\n- current task status\n- files being actively edited\n- key decisions", + ) + }, +}) + +export default { id: "compaction", server: Compaction } +``` + +Set `output.prompt` to replace the default compaction prompt entirely — when present, `output.context` is ignored. + +--- + +## TUI plugins + +Plugins can also target the Kilo TUI itself — registering slash commands, routes, sidebar slots, dialogs, and keybinds. TUI plugins are SolidJS modules exported from `"./tui"` in your plugin package. + +TUI plugins live in a separate module namespace (`@kilocode/plugin/tui`) and have their own API surface (`TuiPluginApi`). Because the TUI API is larger and still evolving, this guide doesn't cover it exhaustively — use the types in `@kilocode/plugin/tui` as the reference, and look at the built-in TUI plugins under `packages/opencode/src/cli/cmd/tui/feature-plugins/` for working examples. + +--- + +## Troubleshooting + +- **Plugin failed to load** — check the CLI logs with `kilo --print-logs --log-level DEBUG`. Load failures are also surfaced as session errors in the TUI and VS Code extension. +- **Plugin loaded but hooks never fire** — make sure the default export includes `server`: + + ```ts + export default { id: "my-plugin", server } + ``` + + Named function exports are also accepted for backwards compatibility but should be considered legacy. + +- **Local plugin can't find an npm import** — add a `package.json` in the config directory so `bun install` picks up the dependency (see [Dependencies](#dependencies)). +- **Plugin loads in dev but not in CI** — verify `KILO_PURE` is not set, and that npm-installed plugins are cached (Kilo's XDG cache directory — `~/.cache/kilo/` on Linux, `~/Library/Caches/kilo/` on macOS, `%LOCALAPPDATA%\kilo\` on Windows). Run with `--log-level DEBUG` to see install output. +- **Reset the plugin cache** — delete the `node_modules/` under Kilo's cache directory (or the `node_modules` cache under your config directory) and restart Kilo. + +--- + +## Reference + +- Types: [`@kilocode/plugin`](https://github.com/Kilo-Org/kilocode/tree/main/packages/plugin) — `Plugin`, `Hooks`, `PluginInput`, `ToolDefinition`, `AuthHook`, `ProviderHook`. +- Example plugin: [`packages/plugin/src/example.ts`](https://github.com/Kilo-Org/kilocode/blob/main/packages/plugin/src/example.ts) +- CLI command: [`kilo plugin`](/docs/code-with-ai/platforms/cli-reference#kilo-plugin) +- Upstream docs (behavior is identical to OpenCode): [opencode.ai/docs/plugins](https://opencode.ai/docs/plugins) and [opencode.ai/docs/custom-tools](https://opencode.ai/docs/custom-tools) From c58886c99f749bba0743d43438c6bffae3a29dfe Mon Sep 17 00:00:00 2001 From: Alex Alecu Date: Fri, 24 Apr 2026 19:40:09 +0300 Subject: [PATCH 02/20] docs: add TESTING.md for local backend iteration Document how to spawn the local main-branch backend with bun dev serve and drive it via curl for HTTP-level testing. Contrasts against the installed kilo binary and the SDK's createKiloServer helper, both of which spawn the prod CLI. AGENTS.md gets a one-line pointer. --- AGENTS.md | 1 + TESTING.md | 261 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 262 insertions(+) create mode 100644 TESTING.md diff --git a/AGENTS.md b/AGENTS.md index 8c68695e79..9ddcc480e7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,6 +20,7 @@ Kilo CLI is an open source AI coding agent that generates code from natural lang - **Source links**: After adding or changing URLs in `packages/kilo-vscode/`, `packages/kilo-vscode/webview-ui/`, or `packages/opencode/src/`, run `bun run script/extract-source-links.ts` from the repo root and commit the updated `packages/kilo-docs/source-links.md`. CI runs this check — the build fails if the file is stale. - **kilocode_change check**: `bun run check-kilocode-change` from `packages/kilo-vscode/`. CI runs this — `kilocode_change` is a marker for upstream merge conflicts and must not appear in `packages/kilo-vscode/` or `packages/kilo-ui/` (these are entirely Kilo Code additions). Remove the markers before pushing. - **opencode annotation check**: `bun run script/check-opencode-annotations.ts` from repo root. CI runs this on PRs touching `packages/opencode/` — every Kilo-specific change in shared opencode files must be annotated with `kilocode_change` markers. Exempt paths (no markers needed): `packages/opencode/src/kilocode/`, `packages/opencode/test/kilocode/`, and any path containing `kilocode` in the name. +- **Backend/SDK programmatic testing**: see [TESTING.md](./TESTING.md) for spawning the local main-branch backend (`bun dev serve`) and driving it via `curl` — use this instead of `kilo serve` (prod binary) when testing backend fixes. ## Products diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000000..a600692205 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,261 @@ +# 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, TUI, or desktop app. + +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 + +| 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 ` forwards `` 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) + +### Random port (recommended) + +```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`) + +| 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. | + +## 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=` 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 + +| 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=` | 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. | + +## 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. From 0472e8555fe6d2b17002ca70c5d6d1ab89a0a037 Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Sat, 25 Apr 2026 13:10:34 -0400 Subject: [PATCH 03/20] docs(kilo-docs): drop kilo serve reference from plugins page Address review feedback: refer to plugin availability in terms of the CLI and VS Code extension instead of the kilo serve internals. --- packages/kilo-docs/pages/automate/extending/plugins.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kilo-docs/pages/automate/extending/plugins.md b/packages/kilo-docs/pages/automate/extending/plugins.md index 5ae61bc47e..6423156cc5 100644 --- a/packages/kilo-docs/pages/automate/extending/plugins.md +++ b/packages/kilo-docs/pages/automate/extending/plugins.md @@ -9,7 +9,7 @@ platform: new Plugins extend the Kilo CLI by hooking into events, adding custom tools, registering auth or model providers, and customizing runtime behavior. They are TypeScript or JavaScript modules loaded at startup. {% callout type="note" %} -Plugins run in the Kilo CLI. They are available everywhere the CLI runs — directly, through `kilo serve`, and inside the VS Code extension (which spawns `kilo serve` under the hood). +Plugins work in both the Kilo CLI and the VS Code extension. {% /callout %} ## What plugins can do From fcfaec28ed2c9da62e631ac9194b5405e7c86700 Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Sat, 25 Apr 2026 13:17:45 -0400 Subject: [PATCH 04/20] docs(kilo-docs): inline plugin availability note into intro --- packages/kilo-docs/pages/automate/extending/plugins.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/kilo-docs/pages/automate/extending/plugins.md b/packages/kilo-docs/pages/automate/extending/plugins.md index 6423156cc5..f57f0c844c 100644 --- a/packages/kilo-docs/pages/automate/extending/plugins.md +++ b/packages/kilo-docs/pages/automate/extending/plugins.md @@ -6,11 +6,7 @@ platform: new # Plugins -Plugins extend the Kilo CLI by hooking into events, adding custom tools, registering auth or model providers, and customizing runtime behavior. They are TypeScript or JavaScript modules loaded at startup. - -{% callout type="note" %} -Plugins work in both the Kilo CLI and the VS Code extension. -{% /callout %} +Plugins extend Kilo by hooking into events, adding custom tools, registering auth or model providers, and customizing runtime behavior. They are TypeScript or JavaScript modules loaded at startup, and work in both the Kilo CLI and the VS Code extension. ## What plugins can do From 50f35690d9bafbdf9952bddd34406ced1d8fc2d8 Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Sat, 25 Apr 2026 13:25:22 -0400 Subject: [PATCH 05/20] docs(kilo-docs): correct kilo plugin config file paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The command writes to .opencode/opencode.jsonc and .opencode/tui.jsonc (or ~/.config/kilo/* for --global), not kilo.json. See #9503 for the underlying bug — when fixed, this doc must be updated in the same PR. --- packages/kilo-docs/pages/automate/extending/plugins.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kilo-docs/pages/automate/extending/plugins.md b/packages/kilo-docs/pages/automate/extending/plugins.md index f57f0c844c..813ed59bea 100644 --- a/packages/kilo-docs/pages/automate/extending/plugins.md +++ b/packages/kilo-docs/pages/automate/extending/plugins.md @@ -86,7 +86,7 @@ kilo plugin my-plugin --global kilo plugin my-plugin --force ``` -The command resolves the package, reads its `package.json` for `exports["./server"]` / `exports["./tui"]`, and writes the entry into your `kilo.json` / `opencode.json` while preserving comments. +The command resolves the package, reads its `package.json` for plugin entrypoints, and writes the entry into the appropriate config file (currently `.opencode/opencode.jsonc` / `.opencode/tui.jsonc` for local installs, or `~/.config/kilo/opencode.jsonc` / `~/.config/kilo/tui.jsonc` for `--global`) while preserving JSONC comments. ### How plugins are installed From 67de590e7138ea2236cf9dbfbe42b5126f7ce3ec Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Fri, 24 Apr 2026 08:34:48 -0400 Subject: [PATCH 06/20] fix: deepseek variants (#24157) --- packages/opencode/src/provider/transform.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index f3dee7e6ec..4a799d1dc3 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -425,7 +425,10 @@ export function variants(model: Provider.Model): Record Date: Fri, 24 Apr 2026 08:48:52 -0400 Subject: [PATCH 07/20] fix: support `max` for deepseek (#24163) --- packages/opencode/src/provider/transform.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 4a799d1dc3..c1dddc2b52 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -572,7 +572,11 @@ export function variants(model: Provider.Model): Record [effort, { reasoningEffort: effort }])) + const efforts = [...WIDELY_SUPPORTED_EFFORTS] + if (model.api.id.includes("deepseek-v4")) { + efforts.push("max") + } + return Object.fromEntries(efforts.map((effort) => [effort, { reasoningEffort: effort }])) case "@ai-sdk/azure": // https://v5.ai-sdk.dev/providers/ai-sdk-providers/azure From f724b8f3af8cfc929ad041b8b57531498fbb2fbb Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Fri, 24 Apr 2026 11:10:47 -0400 Subject: [PATCH 08/20] fix: ensure assistant messages always have reasoning on them for deepseek (#24180) --- packages/opencode/src/provider/transform.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index c1dddc2b52..c6aed4818b 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -183,6 +183,24 @@ function normalizeMessages( return result } + // Deepseek requires all assistant messages to have reasoning on them + if (model.api.id.includes("deepseek")) { + msgs = msgs.map((msg) => { + if (msg.role !== "assistant") return msg + if (Array.isArray(msg.content)) { + if (msg.content.some((part) => part.type === "reasoning")) return msg + return { ...msg, content: [...msg.content, { type: "reasoning", text: "" }] } + } + return { + ...msg, + content: [ + ...(msg.content ? [{ type: "text" as const, text: msg.content }] : []), + { type: "reasoning" as const, text: "" }, + ], + } + }) + } + if (typeof model.capabilities.interleaved === "object" && model.capabilities.interleaved.field) { const field = model.capabilities.interleaved.field return msgs.map((msg) => { From be25acb17a5fcb4045fb0a884409cfbd66bca128 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sat, 25 Apr 2026 23:21:19 -0500 Subject: [PATCH 09/20] fix: bump openrouter sdk version to resolve deepseek reasoning issue (bug was in sdk pkg) --- bun.lock | 8 ++++++-- packages/opencode/package.json | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/bun.lock b/bun.lock index c8226ba5e2..266687142c 100644 --- a/bun.lock +++ b/bun.lock @@ -416,7 +416,7 @@ "@octokit/rest": "catalog:", "@openauthjs/openauth": "catalog:", "@opencode-ai/script": "workspace:*", - "@openrouter/ai-sdk-provider": "2.5.1", + "@openrouter/ai-sdk-provider": "2.8.1", "@opentelemetry/api": "1.9.0", "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/exporter-trace-otlp-http": "0.214.0", @@ -1576,7 +1576,7 @@ "@opencode-ai/ui": ["@opencode-ai/ui@workspace:packages/ui"], - "@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.5.1", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-r1fJL1Cb3gQDa2MpWH/sfx1BsEW0uzlRriJM6eihaKqbtKDmZoBisF32VcVaQYassighX7NGCkF68EsrZA43uQ=="], + "@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.8.1", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Y6j3yivgoEUf/kutD/k5GX/mzZfioRFoSx0gbQ+mIOzMaH/vJv1rCkztiuvlLw5xRYQil7oxHUZvmSfXqOx1NQ=="], "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], @@ -5084,6 +5084,8 @@ "@kilocode/kilo-gateway/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.37", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-+POSFVcgiu47BK64dhsI6OpcDC0/VAE2ZSaXdXGNNhpC/ava++uSRJYks0k2bpfY0wwCTgpAWZsXn/dG2Yppiw=="], + "@kilocode/kilo-gateway/@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.5.1", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-r1fJL1Cb3gQDa2MpWH/sfx1BsEW0uzlRriJM6eihaKqbtKDmZoBisF32VcVaQYassighX7NGCkF68EsrZA43uQ=="], + "@kilocode/kilo-gateway/@opentui/core": ["@opentui/core@0.1.75", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.75", "@opentui/core-darwin-x64": "0.1.75", "@opentui/core-linux-arm64": "0.1.75", "@opentui/core-linux-x64": "0.1.75", "@opentui/core-win32-arm64": "0.1.75", "@opentui/core-win32-x64": "0.1.75", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-8ARRZxSG+BXkJmEVtM2DQ4se7DAF1ZCKD07d+AklgTr2mxCzmdxxPbOwRzboSQ6FM7qGuTVPVbV4O2W9DpUmoA=="], "@kilocode/kilo-gateway/@opentui/solid": ["@opentui/solid@0.1.75", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.75", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-WjKsZIfrm29znfRlcD9w3uUn/+uvoy2MmeoDwTvg1YOa0OjCTCmjZ43L9imp0m9S4HmVU8ma6o2bR4COzcyDdg=="], @@ -5286,6 +5288,8 @@ "ai-gateway-provider/@ai-sdk/xai": ["@ai-sdk/xai@3.0.75", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-V8UKK4fNpI9cnrtsZBvUp9O9J6Y9fTKBRoSLyEaNGPirACewixmLDbXsSgAeownPVWiWpK34bFysd+XouI5Ywg=="], + "ai-gateway-provider/@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.5.1", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-r1fJL1Cb3gQDa2MpWH/sfx1BsEW0uzlRriJM6eihaKqbtKDmZoBisF32VcVaQYassighX7NGCkF68EsrZA43uQ=="], + "ajv-keywords/ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], "app-builder-lib/@electron/get": ["@electron/get@3.1.0", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ=="], diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 24e0a3c1fa..5ff8e85338 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -121,7 +121,7 @@ "@octokit/rest": "catalog:", "@openauthjs/openauth": "catalog:", "@opencode-ai/script": "workspace:*", - "@openrouter/ai-sdk-provider": "2.5.1", + "@openrouter/ai-sdk-provider": "2.8.1", "@opentelemetry/api": "1.9.0", "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/exporter-trace-otlp-http": "0.214.0", From c884cea5ba4d0259c76bcc6ca1e4339850fb3738 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sat, 25 Apr 2026 23:52:56 -0500 Subject: [PATCH 10/20] fix: ensure openrouter behaves correctly --- packages/opencode/src/provider/transform.ts | 6 +- .../opencode/test/session/message-v2.test.ts | 73 +++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index c6aed4818b..3d868ef321 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -201,7 +201,11 @@ function normalizeMessages( }) } - if (typeof model.capabilities.interleaved === "object" && model.capabilities.interleaved.field) { + if ( + typeof model.capabilities.interleaved === "object" && + model.capabilities.interleaved.field && + model.api.npm !== "@openrouter/ai-sdk-provider" + ) { const field = model.capabilities.interleaved.field return msgs.map((msg) => { if (msg.role === "assistant" && Array.isArray(msg.content)) { diff --git a/packages/opencode/test/session/message-v2.test.ts b/packages/opencode/test/session/message-v2.test.ts index 55ae65c560..296db72bab 100644 --- a/packages/opencode/test/session/message-v2.test.ts +++ b/packages/opencode/test/session/message-v2.test.ts @@ -803,6 +803,79 @@ describe("session.message-v2.toModelMessage", () => { ]) }) + test("preserves OpenRouter reasoning details through provider transform", async () => { + const assistantID = "m-assistant" + const openrouterModel: Provider.Model = { + ...model, + id: ModelID.make("deepseek/deepseek-v4-pro"), + providerID: ProviderID.make("openrouter"), + api: { + id: "deepseek/deepseek-v4-pro", + url: "https://openrouter.ai/api/v1", + npm: "@openrouter/ai-sdk-provider", + }, + capabilities: { + ...model.capabilities, + reasoning: true, + interleaved: { field: "reasoning_details" }, + }, + } + const reasoningDetails = [ + { + type: "reasoning.text", + text: "thinking", + format: "unknown", + index: 0, + }, + ] + const input: MessageV2.WithParts[] = [ + { + info: assistantInfo(assistantID, "m-parent", undefined, { + providerID: openrouterModel.providerID, + modelID: openrouterModel.id, + }), + parts: [ + { + ...basePart(assistantID, "a1"), + type: "reasoning", + text: "thinking", + time: { start: 0 }, + metadata: { + openrouter: { + reasoning_details: reasoningDetails, + }, + }, + }, + { + ...basePart(assistantID, "a2"), + type: "text", + text: "answer", + }, + ] as MessageV2.Part[], + }, + ] + + expect( + ProviderTransform.message(await MessageV2.toModelMessages(input, openrouterModel), openrouterModel, {}), + ).toStrictEqual([ + { + role: "assistant", + content: [ + { + type: "reasoning", + text: "thinking", + providerOptions: { + openrouter: { + reasoning_details: reasoningDetails, + }, + }, + }, + { type: "text", text: "answer" }, + ], + }, + ]) + }) + test("splits assistant messages on step-start boundaries", async () => { const assistantID = "m-assistant" From a1557d1a1fcafe361bb5a0d92f989319a3553b3f Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Sun, 26 Apr 2026 11:09:07 -0400 Subject: [PATCH 11/20] chore: annotate cherry-picked upstream changes with kilocode_change markers --- packages/opencode/src/provider/transform.ts | 9 +++++++++ packages/opencode/test/session/message-v2.test.ts | 2 ++ 2 files changed, 11 insertions(+) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 3d868ef321..51725600af 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -183,6 +183,8 @@ function normalizeMessages( return result } + // kilocode_change start - cherry-picked from anomalyco/opencode#24180; + // will be reverted on the next wholesale upstream merge. // Deepseek requires all assistant messages to have reasoning on them if (model.api.id.includes("deepseek")) { msgs = msgs.map((msg) => { @@ -200,12 +202,15 @@ function normalizeMessages( } }) } + // kilocode_change end + // kilocode_change start - cherry-picked from anomalyco/opencode#24435 if ( typeof model.capabilities.interleaved === "object" && model.capabilities.interleaved.field && model.api.npm !== "@openrouter/ai-sdk-provider" ) { + // kilocode_change end const field = model.capabilities.interleaved.field return msgs.map((msg) => { if (msg.role === "assistant" && Array.isArray(msg.content)) { @@ -447,10 +452,12 @@ export function variants(model: Provider.Model): Record [effort, { reasoningEffort: effort }])) + // kilocode_change end case "@ai-sdk/azure": // https://v5.ai-sdk.dev/providers/ai-sdk-providers/azure diff --git a/packages/opencode/test/session/message-v2.test.ts b/packages/opencode/test/session/message-v2.test.ts index 296db72bab..a3137ff01d 100644 --- a/packages/opencode/test/session/message-v2.test.ts +++ b/packages/opencode/test/session/message-v2.test.ts @@ -803,6 +803,7 @@ describe("session.message-v2.toModelMessage", () => { ]) }) + // kilocode_change start - cherry-picked from anomalyco/opencode#24435 test("preserves OpenRouter reasoning details through provider transform", async () => { const assistantID = "m-assistant" const openrouterModel: Provider.Model = { @@ -875,6 +876,7 @@ describe("session.message-v2.toModelMessage", () => { }, ]) }) + // kilocode_change end test("splits assistant messages on step-start boundaries", async () => { const assistantID = "m-assistant" From c8113f27b190f5c08ce642da57d68646132e1828 Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Sun, 26 Apr 2026 11:10:11 -0400 Subject: [PATCH 12/20] chore: add changeset for openrouter/deepseek reasoning fix --- .changeset/openrouter-deepseek-reasoning.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/openrouter-deepseek-reasoning.md diff --git a/.changeset/openrouter-deepseek-reasoning.md b/.changeset/openrouter-deepseek-reasoning.md new file mode 100644 index 0000000000..5d49508f2d --- /dev/null +++ b/.changeset/openrouter-deepseek-reasoning.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Fix multi-turn DeepSeek reasoning round-tripping on OpenRouter by bumping `@openrouter/ai-sdk-provider` to 2.8.1 and letting the SDK handle reasoning details, plus pulling in upstream DeepSeek variant, reasoning-effort, and assistant-reasoning fixes. From 1dd436d61ab29729df5417d09c0cd16a71d9074f Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Sun, 26 Apr 2026 11:29:26 -0400 Subject: [PATCH 13/20] fix(cli): also skip interleaved reasoning transform for @kilocode/kilo-gateway The gateway wraps @openrouter/ai-sdk-provider internally, so the same openrouter SDK-side reasoning handling applies. Without this, users routing through api.kilo.ai still see DeepSeek 400 'reasoning_content must be passed back' errors. --- packages/opencode/src/provider/transform.ts | 6 +- .../opencode/test/session/message-v2.test.ts | 57 +++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 51725600af..83ee6691b9 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -204,11 +204,13 @@ function normalizeMessages( } // kilocode_change end - // kilocode_change start - cherry-picked from anomalyco/opencode#24435 + // kilocode_change start - cherry-picked from anomalyco/opencode#24435; + // also skip @kilocode/kilo-gateway since it wraps @openrouter/ai-sdk-provider internally. if ( typeof model.capabilities.interleaved === "object" && model.capabilities.interleaved.field && - model.api.npm !== "@openrouter/ai-sdk-provider" + model.api.npm !== "@openrouter/ai-sdk-provider" && + model.api.npm !== "@kilocode/kilo-gateway" ) { // kilocode_change end const field = model.capabilities.interleaved.field diff --git a/packages/opencode/test/session/message-v2.test.ts b/packages/opencode/test/session/message-v2.test.ts index a3137ff01d..25f8b21086 100644 --- a/packages/opencode/test/session/message-v2.test.ts +++ b/packages/opencode/test/session/message-v2.test.ts @@ -878,6 +878,63 @@ describe("session.message-v2.toModelMessage", () => { }) // kilocode_change end + // kilocode_change start - Kilo gateway wraps @openrouter/ai-sdk-provider so same skip applies + test("preserves reasoning details through Kilo gateway (wraps openrouter SDK)", async () => { + const assistantID = "m-assistant" + const gatewayModel: Provider.Model = { + ...model, + id: ModelID.make("deepseek/deepseek-v4-pro"), + providerID: ProviderID.make("kilocode"), + api: { + id: "deepseek/deepseek-v4-pro", + url: "https://api.kilo.ai/api/openrouter", + npm: "@kilocode/kilo-gateway", + }, + capabilities: { + ...model.capabilities, + reasoning: true, + interleaved: { field: "reasoning_details" }, + }, + } + const reasoningDetails = [{ type: "reasoning.text", text: "thinking", format: "unknown", index: 0 }] + const input: MessageV2.WithParts[] = [ + { + info: assistantInfo(assistantID, "m-parent", undefined, { + providerID: gatewayModel.providerID, + modelID: gatewayModel.id, + }), + parts: [ + { + ...basePart(assistantID, "a1"), + type: "reasoning", + text: "thinking", + time: { start: 0 }, + metadata: { openrouter: { reasoning_details: reasoningDetails } }, + }, + { ...basePart(assistantID, "a2"), type: "text", text: "answer" }, + ] as MessageV2.Part[], + }, + ] + + expect( + ProviderTransform.message(await MessageV2.toModelMessages(input, gatewayModel), gatewayModel, {}), + ).toStrictEqual([ + { + role: "assistant", + providerOptions: undefined, + content: [ + { + type: "reasoning", + text: "thinking", + providerOptions: { openrouter: { reasoning_details: reasoningDetails } }, + }, + { type: "text", text: "answer", providerOptions: undefined }, + ], + }, + ]) + }) + // kilocode_change end + test("splits assistant messages on step-start boundaries", async () => { const assistantID = "m-assistant" From fa23641ebfec8f0b54da241093c449ad6d3e594f Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Sun, 26 Apr 2026 11:29:47 -0400 Subject: [PATCH 14/20] chore: update changeset to mention Kilo gateway coverage --- .changeset/openrouter-deepseek-reasoning.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/openrouter-deepseek-reasoning.md b/.changeset/openrouter-deepseek-reasoning.md index 5d49508f2d..cc25c5c575 100644 --- a/.changeset/openrouter-deepseek-reasoning.md +++ b/.changeset/openrouter-deepseek-reasoning.md @@ -2,4 +2,4 @@ "@kilocode/cli": patch --- -Fix multi-turn DeepSeek reasoning round-tripping on OpenRouter by bumping `@openrouter/ai-sdk-provider` to 2.8.1 and letting the SDK handle reasoning details, plus pulling in upstream DeepSeek variant, reasoning-effort, and assistant-reasoning fixes. +Fix multi-turn DeepSeek reasoning round-tripping on OpenRouter (direct and via Kilo gateway) by bumping `@openrouter/ai-sdk-provider` to 2.8.1 and letting the SDK handle reasoning details, plus pulling in upstream DeepSeek variant, reasoning-effort, and assistant-reasoning fixes. From 7e8e98fa374d930fd64f5fcccad76af6942e688b Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Sun, 26 Apr 2026 19:55:56 -0400 Subject: [PATCH 15/20] Revert "chore: update changeset to mention Kilo gateway coverage" This reverts commit fa23641ebfec8f0b54da241093c449ad6d3e594f. --- .changeset/openrouter-deepseek-reasoning.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/openrouter-deepseek-reasoning.md b/.changeset/openrouter-deepseek-reasoning.md index cc25c5c575..5d49508f2d 100644 --- a/.changeset/openrouter-deepseek-reasoning.md +++ b/.changeset/openrouter-deepseek-reasoning.md @@ -2,4 +2,4 @@ "@kilocode/cli": patch --- -Fix multi-turn DeepSeek reasoning round-tripping on OpenRouter (direct and via Kilo gateway) by bumping `@openrouter/ai-sdk-provider` to 2.8.1 and letting the SDK handle reasoning details, plus pulling in upstream DeepSeek variant, reasoning-effort, and assistant-reasoning fixes. +Fix multi-turn DeepSeek reasoning round-tripping on OpenRouter by bumping `@openrouter/ai-sdk-provider` to 2.8.1 and letting the SDK handle reasoning details, plus pulling in upstream DeepSeek variant, reasoning-effort, and assistant-reasoning fixes. From 55ddf870406762b38974d01411c02e3c37fd494c Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Sun, 26 Apr 2026 19:55:56 -0400 Subject: [PATCH 16/20] Revert "fix(cli): also skip interleaved reasoning transform for @kilocode/kilo-gateway" This reverts commit 1dd436d61ab29729df5417d09c0cd16a71d9074f. --- packages/opencode/src/provider/transform.ts | 6 +- .../opencode/test/session/message-v2.test.ts | 57 ------------------- 2 files changed, 2 insertions(+), 61 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 83ee6691b9..51725600af 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -204,13 +204,11 @@ function normalizeMessages( } // kilocode_change end - // kilocode_change start - cherry-picked from anomalyco/opencode#24435; - // also skip @kilocode/kilo-gateway since it wraps @openrouter/ai-sdk-provider internally. + // kilocode_change start - cherry-picked from anomalyco/opencode#24435 if ( typeof model.capabilities.interleaved === "object" && model.capabilities.interleaved.field && - model.api.npm !== "@openrouter/ai-sdk-provider" && - model.api.npm !== "@kilocode/kilo-gateway" + model.api.npm !== "@openrouter/ai-sdk-provider" ) { // kilocode_change end const field = model.capabilities.interleaved.field diff --git a/packages/opencode/test/session/message-v2.test.ts b/packages/opencode/test/session/message-v2.test.ts index 25f8b21086..a3137ff01d 100644 --- a/packages/opencode/test/session/message-v2.test.ts +++ b/packages/opencode/test/session/message-v2.test.ts @@ -878,63 +878,6 @@ describe("session.message-v2.toModelMessage", () => { }) // kilocode_change end - // kilocode_change start - Kilo gateway wraps @openrouter/ai-sdk-provider so same skip applies - test("preserves reasoning details through Kilo gateway (wraps openrouter SDK)", async () => { - const assistantID = "m-assistant" - const gatewayModel: Provider.Model = { - ...model, - id: ModelID.make("deepseek/deepseek-v4-pro"), - providerID: ProviderID.make("kilocode"), - api: { - id: "deepseek/deepseek-v4-pro", - url: "https://api.kilo.ai/api/openrouter", - npm: "@kilocode/kilo-gateway", - }, - capabilities: { - ...model.capabilities, - reasoning: true, - interleaved: { field: "reasoning_details" }, - }, - } - const reasoningDetails = [{ type: "reasoning.text", text: "thinking", format: "unknown", index: 0 }] - const input: MessageV2.WithParts[] = [ - { - info: assistantInfo(assistantID, "m-parent", undefined, { - providerID: gatewayModel.providerID, - modelID: gatewayModel.id, - }), - parts: [ - { - ...basePart(assistantID, "a1"), - type: "reasoning", - text: "thinking", - time: { start: 0 }, - metadata: { openrouter: { reasoning_details: reasoningDetails } }, - }, - { ...basePart(assistantID, "a2"), type: "text", text: "answer" }, - ] as MessageV2.Part[], - }, - ] - - expect( - ProviderTransform.message(await MessageV2.toModelMessages(input, gatewayModel), gatewayModel, {}), - ).toStrictEqual([ - { - role: "assistant", - providerOptions: undefined, - content: [ - { - type: "reasoning", - text: "thinking", - providerOptions: { openrouter: { reasoning_details: reasoningDetails } }, - }, - { type: "text", text: "answer", providerOptions: undefined }, - ], - }, - ]) - }) - // kilocode_change end - test("splits assistant messages on step-start boundaries", async () => { const assistantID = "m-assistant" From 604f44854afc1eaf204616a8fb8834cb6b83d899 Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Sun, 26 Apr 2026 20:04:03 -0400 Subject: [PATCH 17/20] fix(gateway): use updated OpenRouter SDK for DeepSeek reasoning --- .changeset/openrouter-deepseek-reasoning.md | 3 ++- bun.lock | 4 +--- packages/kilo-gateway/package.json | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.changeset/openrouter-deepseek-reasoning.md b/.changeset/openrouter-deepseek-reasoning.md index 5d49508f2d..ea67392d3e 100644 --- a/.changeset/openrouter-deepseek-reasoning.md +++ b/.changeset/openrouter-deepseek-reasoning.md @@ -1,5 +1,6 @@ --- "@kilocode/cli": patch +"@kilocode/kilo-gateway": patch --- -Fix multi-turn DeepSeek reasoning round-tripping on OpenRouter by bumping `@openrouter/ai-sdk-provider` to 2.8.1 and letting the SDK handle reasoning details, plus pulling in upstream DeepSeek variant, reasoning-effort, and assistant-reasoning fixes. +Fix multi-turn DeepSeek reasoning round-tripping on OpenRouter by bumping `@openrouter/ai-sdk-provider` to 2.8.1 in both the CLI and Kilo Gateway packages and letting the SDK handle reasoning details, plus pulling in upstream DeepSeek variant, reasoning-effort, and assistant-reasoning fixes. New DeepSeek conversations are fixed; existing sessions that already stored empty reasoning metadata may still need to be restarted. diff --git a/bun.lock b/bun.lock index 266687142c..e8a9617664 100644 --- a/bun.lock +++ b/bun.lock @@ -210,7 +210,7 @@ "@clack/prompts": "1.0.0-alpha.1", "@kilocode/plugin": "workspace:*", "@kilocode/sdk": "workspace:*", - "@openrouter/ai-sdk-provider": "2.5.1", + "@openrouter/ai-sdk-provider": "2.8.1", "ai": "catalog:", "open": "10.1.2", "zod": "catalog:", @@ -5084,8 +5084,6 @@ "@kilocode/kilo-gateway/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.37", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-+POSFVcgiu47BK64dhsI6OpcDC0/VAE2ZSaXdXGNNhpC/ava++uSRJYks0k2bpfY0wwCTgpAWZsXn/dG2Yppiw=="], - "@kilocode/kilo-gateway/@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.5.1", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-r1fJL1Cb3gQDa2MpWH/sfx1BsEW0uzlRriJM6eihaKqbtKDmZoBisF32VcVaQYassighX7NGCkF68EsrZA43uQ=="], - "@kilocode/kilo-gateway/@opentui/core": ["@opentui/core@0.1.75", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.75", "@opentui/core-darwin-x64": "0.1.75", "@opentui/core-linux-arm64": "0.1.75", "@opentui/core-linux-x64": "0.1.75", "@opentui/core-win32-arm64": "0.1.75", "@opentui/core-win32-x64": "0.1.75", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-8ARRZxSG+BXkJmEVtM2DQ4se7DAF1ZCKD07d+AklgTr2mxCzmdxxPbOwRzboSQ6FM7qGuTVPVbV4O2W9DpUmoA=="], "@kilocode/kilo-gateway/@opentui/solid": ["@opentui/solid@0.1.75", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.75", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-WjKsZIfrm29znfRlcD9w3uUn/+uvoy2MmeoDwTvg1YOa0OjCTCmjZ43L9imp0m9S4HmVU8ma6o2bR4COzcyDdg=="], diff --git a/packages/kilo-gateway/package.json b/packages/kilo-gateway/package.json index cedd11ba49..1a35b3cb9d 100644 --- a/packages/kilo-gateway/package.json +++ b/packages/kilo-gateway/package.json @@ -33,7 +33,7 @@ "@ai-sdk/anthropic": "3.0.71", "@ai-sdk/openai": "3.0.48", "@ai-sdk/openai-compatible": "2.0.37", - "@openrouter/ai-sdk-provider": "2.5.1", + "@openrouter/ai-sdk-provider": "2.8.1", "@clack/prompts": "1.0.0-alpha.1", "ai": "catalog:", "open": "10.1.2", From e8c13e0ee8e32bfd92888e410f8661b7fac3e989 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 07:31:57 +0000 Subject: [PATCH 18/20] fix(cli): preserve shell cwd after login startup --- packages/opencode/src/session/prompt.ts | 12 ++++++---- .../test/session/prompt-effect.test.ts | 24 +++++++++++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 0281aaf281..cb52b9e478 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -792,6 +792,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the const shellName = ( process.platform === "win32" ? path.win32.basename(sh, ".exe") : path.basename(sh) ).toLowerCase() + const cwd = ctx.directory const invocations: Record = { nu: { args: ["-c", input.command] }, fish: { args: ["-c", input.command] }, @@ -800,12 +801,13 @@ NOTE: At any point in time through this workflow you should feel free to ask the "-l", "-c", ` - __oc_cwd=$PWD [[ -f ~/.zshenv ]] && source ~/.zshenv >/dev/null 2>&1 || true [[ -f "\${ZDOTDIR:-$HOME}/.zshrc" ]] && source "\${ZDOTDIR:-$HOME}/.zshrc" >/dev/null 2>&1 || true - cd "$__oc_cwd" + cd -- "$1" eval ${JSON.stringify(input.command)} `, + "opencode", + cwd, ], }, bash: { @@ -813,12 +815,13 @@ NOTE: At any point in time through this workflow you should feel free to ask the "-l", "-c", ` - __oc_cwd=$PWD shopt -s expand_aliases [[ -f ~/.bashrc ]] && source ~/.bashrc >/dev/null 2>&1 || true - cd "$__oc_cwd" + cd -- "$1" eval ${JSON.stringify(input.command)} `, + "opencode", + cwd, ], }, cmd: { args: ["/c", input.command] }, @@ -828,7 +831,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the } const args = (invocations[shellName] ?? invocations[""]).args - const cwd = ctx.directory const shellEnv = yield* plugin.trigger( "shell.env", { cwd, sessionID: input.sessionID, callID: part.callID }, diff --git a/packages/opencode/test/session/prompt-effect.test.ts b/packages/opencode/test/session/prompt-effect.test.ts index 1cf6abb779..21b2a0d50a 100644 --- a/packages/opencode/test/session/prompt-effect.test.ts +++ b/packages/opencode/test/session/prompt-effect.test.ts @@ -1080,6 +1080,30 @@ unix("shell completes a fast command on the preferred shell", () => ), ) +unix("shell commands can change directory after startup", () => + provideTmpdirInstance( + (dir) => + Effect.gen(function* () { + const { prompt, run, chat } = yield* boot() + const parent = path.dirname(dir) + const result = yield* prompt.shell({ + sessionID: chat.id, + agent: "code", + command: "cd .. && pwd", + }) + + expect(result.info.role).toBe("assistant") + const tool = completedTool(result.parts) + if (!tool) return + + expect(tool.state.output).toContain(parent) + expect(tool.state.metadata.output).toContain(parent) + yield* run.assertNotBusy(chat.id) + }), + { git: true, config: cfg }, + ), +) + unix("shell lists files from the project directory", () => provideTmpdirInstance( (dir) => From f631338576830b1de993324ae8ff10d870a69ab7 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 07:38:01 +0000 Subject: [PATCH 19/20] chore(cli): add kilocode_change markers to ported shell cwd fix --- packages/opencode/src/session/prompt.ts | 6 +++++- packages/opencode/test/session/prompt-effect.test.ts | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index cb52b9e478..193b289c55 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -792,11 +792,12 @@ NOTE: At any point in time through this workflow you should feel free to ask the const shellName = ( process.platform === "win32" ? path.win32.basename(sh, ".exe") : path.basename(sh) ).toLowerCase() - const cwd = ctx.directory + const cwd = ctx.directory // kilocode_change - moved up to use in invocations below const invocations: Record = { nu: { args: ["-c", input.command] }, fish: { args: ["-c", input.command] }, zsh: { + // kilocode_change start - port anomalyco/opencode#24215: pass cwd as positional arg instead of $PWD (CI resets $PWD after startup files) args: [ "-l", "-c", @@ -809,8 +810,10 @@ NOTE: At any point in time through this workflow you should feel free to ask the "opencode", cwd, ], + // kilocode_change end }, bash: { + // kilocode_change start - port anomalyco/opencode#24215: pass cwd as positional arg instead of $PWD (CI resets $PWD after startup files) args: [ "-l", "-c", @@ -823,6 +826,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the "opencode", cwd, ], + // kilocode_change end }, cmd: { args: ["/c", input.command] }, powershell: { args: ["-NoProfile", "-Command", input.command] }, diff --git a/packages/opencode/test/session/prompt-effect.test.ts b/packages/opencode/test/session/prompt-effect.test.ts index 21b2a0d50a..4b3777f32b 100644 --- a/packages/opencode/test/session/prompt-effect.test.ts +++ b/packages/opencode/test/session/prompt-effect.test.ts @@ -1080,6 +1080,7 @@ unix("shell completes a fast command on the preferred shell", () => ), ) +// kilocode_change start - port anomalyco/opencode#24215 cover shell cwd changes (agent "build" → "code" for our fork) unix("shell commands can change directory after startup", () => provideTmpdirInstance( (dir) => @@ -1103,6 +1104,7 @@ unix("shell commands can change directory after startup", () => { git: true, config: cfg }, ), ) +// kilocode_change end unix("shell lists files from the project directory", () => provideTmpdirInstance( From ede13b7ec5f17cfd3b33531fa89d5a88d2326704 Mon Sep 17 00:00:00 2001 From: "kilo-maintainer[bot]" Date: Mon, 27 Apr 2026 08:24:36 +0000 Subject: [PATCH 20/20] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index fa8e8edd5f..1e170f7619 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-tWXPyyUluVXzMJny376Nly7hJuiGonPRMv1m48hdplY=", - "aarch64-linux": "sha256-MFJ/ihYcJC1QdfJvgalo4/deQ+gaGN+c53Ex1P1ExoI=", - "aarch64-darwin": "sha256-wZ+gntTCb/Wzq6yur/1QoqWp6xX21A+XCsPn32KemyY=", - "x86_64-darwin": "sha256-lBwBWeXR9ca2TKmeE8E4HK4ZQrf83Ks0M0WAGBOGBfM=" + "x86_64-linux": "sha256-uDu9FY2G8j6AzpXAQ3WoWLJ9uyh5R7xqcAVzij6dpdU=", + "aarch64-linux": "sha256-GtVSse+wWwmFA9e3XkAp+1spPH2u4C2jqJ9HUKQRJ44=", + "aarch64-darwin": "sha256-UrZIY7v59gB0k0+/CIXA/NFrR3FSE+5Lcsz9qZffsqM=", + "x86_64-darwin": "sha256-/thTDanC0f1rczDj7RbgeFyaRfuhMdbRXrgfH6KRejs=" } }