mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
Merge branch 'main' into fix/queued-messages-test
This commit is contained in:
@@ -0,0 +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 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.
|
||||
@@ -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
|
||||
|
||||
|
||||
+261
@@ -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 <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)
|
||||
|
||||
### 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=<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
|
||||
|
||||
| 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. |
|
||||
|
||||
## 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.
|
||||
@@ -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:",
|
||||
@@ -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=="],
|
||||
|
||||
@@ -5286,6 +5286,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=="],
|
||||
|
||||
+4
-4
@@ -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="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1,483 @@
|
||||
---
|
||||
title: "Plugins"
|
||||
description: "Extend the Kilo CLI with custom hooks, tools, auth providers, and more"
|
||||
platform: new
|
||||
---
|
||||
|
||||
# Plugins
|
||||
|
||||
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
|
||||
|
||||
- **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 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
|
||||
|
||||
- **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)
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -183,7 +183,34 @@ function normalizeMessages(
|
||||
return result
|
||||
}
|
||||
|
||||
if (typeof model.capabilities.interleaved === "object" && model.capabilities.interleaved.field) {
|
||||
// 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) => {
|
||||
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: "" },
|
||||
],
|
||||
}
|
||||
})
|
||||
}
|
||||
// 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)) {
|
||||
@@ -425,7 +452,12 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
|
||||
const adaptiveEfforts = anthropicAdaptiveEfforts(model.api.id)
|
||||
|
||||
if (
|
||||
id.includes("deepseek") ||
|
||||
// kilocode_change start - cherry-picked from anomalyco/opencode#24157
|
||||
id.includes("deepseek-chat") ||
|
||||
id.includes("deepseek-reasoner") ||
|
||||
id.includes("deepseek-r1") ||
|
||||
id.includes("deepseek-v3") ||
|
||||
// kilocode_change end
|
||||
id.includes("minimax") ||
|
||||
// id.includes("glm") || // kilocode_change
|
||||
id.includes("mistral") ||
|
||||
@@ -569,7 +601,13 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
|
||||
case "venice-ai-sdk-provider":
|
||||
// https://docs.venice.ai/overview/guides/reasoning-models#reasoning-effort
|
||||
case "@ai-sdk/openai-compatible":
|
||||
return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
|
||||
// kilocode_change start - cherry-picked from anomalyco/opencode#24163
|
||||
const efforts = [...WIDELY_SUPPORTED_EFFORTS]
|
||||
if (model.api.id.includes("deepseek-v4")) {
|
||||
efforts.push("max")
|
||||
}
|
||||
return Object.fromEntries(efforts.map((effort) => [effort, { reasoningEffort: effort }]))
|
||||
// kilocode_change end
|
||||
|
||||
case "@ai-sdk/azure":
|
||||
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/azure
|
||||
|
||||
@@ -792,34 +792,41 @@ 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 // kilocode_change - moved up to use in invocations below
|
||||
const invocations: Record<string, { args: string[] }> = {
|
||||
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",
|
||||
`
|
||||
__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,
|
||||
],
|
||||
// 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",
|
||||
`
|
||||
__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,
|
||||
],
|
||||
// kilocode_change end
|
||||
},
|
||||
cmd: { args: ["/c", input.command] },
|
||||
powershell: { args: ["-NoProfile", "-Command", input.command] },
|
||||
@@ -828,7 +835,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 },
|
||||
|
||||
@@ -803,6 +803,81 @@ 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 = {
|
||||
...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" },
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
// kilocode_change end
|
||||
|
||||
test("splits assistant messages on step-start boundaries", async () => {
|
||||
const assistantID = "m-assistant"
|
||||
|
||||
|
||||
@@ -1086,6 +1086,32 @@ 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) =>
|
||||
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 },
|
||||
),
|
||||
)
|
||||
// kilocode_change end
|
||||
|
||||
unix("shell lists files from the project directory", () =>
|
||||
provideTmpdirInstance(
|
||||
(dir) =>
|
||||
|
||||
Reference in New Issue
Block a user