Merge branch 'main' into local-review-base-picker

This commit is contained in:
Marian Alexandru Alecu
2026-05-22 19:16:55 +03:00
committed by GitHub
233 changed files with 9153 additions and 3049 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Improve the JetBrains model picker with sections, favorites, and smart filtering.
+5
View File
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---
Use the fallback logo in old Windows terminal emulators while keeping the Unicode logo available over SSH.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Add a JetBrains tool window action for starting new sessions and let recent sessions reopen in the side panel without duplicate views.
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Scale JetBrains chat transcript fonts from the editor font settings.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Keep the JetBrains model picker at the same row position when models are favorited or unfavorited.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Show JetBrains session connection status inline above the prompt, cap expanded connection details, and keep empty sessions in the shared chat scroll area.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Show detailed JetBrains session connection and configuration issues in the connection panel, and make retry recover app-side problems more reliably.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Match VS Code-style reasoning and tool run rendering in JetBrains chat.
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Center description text and remove the fixed-width cap from the empty session panel so recent sessions can stretch naturally.
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Open existing JetBrains sessions scrolled to the latest message after history loads.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Remember the JetBrains session header metrics expansion preference and show file names in read/write timeline tooltips.
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Improve JetBrains history navigation with Back support and a Show History action in recent sessions.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Improve the JetBrains history panel with native tabs, independent local and cloud search, and date-grouped cloud sessions.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Improve the JetBrains mode picker with descriptions, deprecated badges, and native IntelliJ selection behavior.
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Show JetBrains question prompts one question at a time with aligned native option rows.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Keep the JetBrains chat transcript pinned to bottom reliably while responses stream.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Show a scroll-to-bottom button when JetBrains chat sessions are scrolled away from the latest message.
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Render active question and permission prompts inside the scrollable JetBrains chat transcript.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Support rebinding the JetBrains prompt Send action in the IntelliJ keymap.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Show collapsible session metadata, context usage, token usage, and compaction controls in the JetBrains session header.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Add a native session history panel to the JetBrains plugin.
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Control JetBrains chat transcript spacing from the session layout.
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Show loading immediately when opening JetBrains sessions without showing duplicate progress for new sessions.
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Reduce redundant JetBrains chat scroll layout work while keeping bottom-follow behavior.
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Show completed assistant steps in the JetBrains session timeline.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Support stopping active JetBrains sessions from the prompt action button.
+5
View File
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---
Avoid leaving mouse and advanced keyboard modes enabled after exiting the TUI in mintty and MINGW terminals.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Size the JetBrains model picker from its rendered model rows instead of fixed popup dimensions.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Support reasoning effort selection and resetting JetBrains chat model overrides to the default model.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Align checked and unchecked reasoning picker rows in the JetBrains plugin.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Keep the JetBrains session connection status above the prompt without shifting the chat layout.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Refine JetBrains session loading UI and settings actions.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Preserve JetBrains chat scroll position when reviewing earlier session output.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-jetbrains": patch
---
Avoid flickering transient loading and connection error states in JetBrains sessions.
+2 -1
View File
@@ -175,6 +175,7 @@ jobs:
cli_version: ${{ needs.version.outputs.version }}
secrets: inherit
# kilocode_change end
publish:
needs:
- version
@@ -208,6 +209,7 @@ jobs:
with:
node-version: "24"
registry-url: "https://registry.npmjs.org"
package-manager-cache: false # kilocode_change
- name: Install @vscode/vsce
run: bun install -g @vscode/vsce
@@ -265,7 +267,6 @@ jobs:
GH_REPO: ${{ github.repository }}
AUR_KEY: ${{ secrets.AUR_KEY }}
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_CONFIG_PROVENANCE: true
VSCE_PAT: ${{ secrets.VSCE_TOKEN }}
OPENVSX_TOKEN: ${{ secrets.OVSX_TOKEN }}
+1 -1
View File
@@ -82,7 +82,7 @@ jobs:
turbo-${{ runner.os }}-
- name: Run unit tests
run: bun turbo test:ci --filter=!@kilocode/kilo-jetbrains # kilocode_change
run: bun turbo test:ci
env:
KILO_EXPERIMENTAL_DISABLE_FILEWATCHER: ${{ runner.os == 'Windows' && 'true' || 'false' }}
+8
View File
@@ -17,5 +17,13 @@ jobs:
- name: Setup Bun
uses: ./.github/actions/setup-bun
# kilocode_change start
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
# kilocode_change end
- name: Run typecheck
run: bun typecheck
+1
View File
@@ -34,6 +34,7 @@ a.out
target
.scripts
.direnv/
packages/kilo-jetbrains/build-tasks/.gradle/
# Local dev files
opencode-dev
+1 -1
View File
@@ -1 +1 @@
v1.14.34
v1.14.41
+2 -1
View File
@@ -12,7 +12,7 @@ Kilo CLI is an open source AI coding agent that generates code from natural lang
- **Dev**: `bun run dev` (runs from root) or `bun run --cwd packages/opencode --conditions=browser src/index.ts`
- **Dev with params**: `bun dev -- help`
- **Extension**: `bun run extension` (build + launch VS Code with the extension in dev mode). Pass `--no-build` to skip the build.
- **Typecheck**: `bun turbo typecheck` (uses `tsgo`, not `tsc`)
- **Typecheck**: `bun turbo typecheck` (uses `tsgo`, not `tsc`). Includes the JetBrains plugin — requires Java 21. Check with `java -version` before running. If missing, install via SDKMAN: `sdk install java 21-tem && sdk use java 21-tem`. If SDKMAN is not installed, see https://sdkman.io/install.
- **Test**: `bun test` from `packages/opencode/` (NOT from root -- root blocks tests)
- **Single test**: `bun test ./test/tool/tool-define.test.ts` from `packages/opencode/`
- **CLI build artifact size check**: after `bun run script/build.ts --single --skip-install` in `packages/opencode/`, use `du -h dist/*/*/bin/kilo` (scoped package output lives under `dist/@kilocode/`)
@@ -34,6 +34,7 @@ Before saying an implementation is ready, run the smallest relevant checks that
| CLI | From `packages/opencode/`: `bun run typecheck`, `bun test` or targeted `bun test ./path/to/file.test.ts` |
| VS Code extension | From `packages/kilo-vscode/`: `bun run typecheck`, `bun run lint`, `bun run test:unit` or `bun run test` |
| Extension build/package | From `packages/kilo-vscode/`: `bun run compile` or `bun run package` when touching build, packaging, SDK, or webview integration paths |
| JetBrains plugin | From `packages/kilo-jetbrains/`: `./gradlew typecheck`, `./gradlew test`. Requires Java 21 — check first with `java -version`. Install via SDKMAN if missing: `sdk install java 21-tem && sdk use java 21-tem`. |
| CI-only guards | Run affected guards documented above, such as `bun run knip`, `bun run check-kilocode-change`, `bun run script/check-opencode-annotations.ts`, or source link extraction |
Never run root `bun test`; the root script prints `do not run tests from root` and exits with code 1. Use package-level tests instead.
+43 -1
View File
@@ -14,9 +14,34 @@ There are lots of ways to contribute to the project:
The Kilo Community is [on Discord](https://kilo.ai/discord).
## Prerequisites
- **Bun 1.3.13+** — required for all packages.
- **Java 21** — required by the JetBrains plugin. The root `bun turbo typecheck` and `bun turbo test:ci` commands include `@kilocode/kilo-jetbrains` and will fail without Java 21.
The preferred way to install Java is via [SDKMAN](https://sdkman.io/install):
```bash
# Install SDKMAN (if not already installed)
curl -s "https://get.sdkman.io" | bash
# Install and activate Java 21 (Eclipse Temurin)
sdk install java 21-tem
sdk use java 21-tem
# Verify
java -version
```
If you don't plan to work on the JetBrains plugin, you can still run non-JetBrains checks directly:
```bash
bun turbo typecheck --filter=!@kilocode/kilo-jetbrains
```
## Developing Kilo CLI
- **Requirements:** Bun 1.3.13+
- **Requirements:** Bun 1.3.13+, Java 21 (see [Prerequisites](#prerequisites) above)
- Install dependencies and start the dev server from the repo root:
```bash
@@ -34,6 +59,23 @@ bun run extension # Build + launch in dev mode
This auto-detects VS Code on macOS, Linux, and Windows. Override with `--app-path PATH` or `VSCODE_EXEC_PATH`. Use `--insiders` to prefer Insiders, `--workspace PATH` to open a specific folder, or `--clean` to reset cached state.
### Developing the JetBrains Plugin
Requires Java 21 (see [Prerequisites](#prerequisites)). From `packages/kilo-jetbrains/`:
```bash
./gradlew typecheck # Compile-check all Kotlin sources
./gradlew test # Run all tests (backend + frontend)
./gradlew runIde # Launch sandboxed IntelliJ with the plugin
```
Or via the root turbo filter to run only JetBrains checks from the repo root:
```bash
bun turbo typecheck --filter=@kilocode/kilo-jetbrains
bun turbo test:ci --filter=@kilocode/kilo-jetbrains
```
### Running against a different directory
By default, `bun dev` runs Kilo CLI in the `packages/opencode` directory. To run it against a different directory or repository:
+121 -36
View File
@@ -28,12 +28,12 @@
"prettier": "3.6.2",
"semver": "^7.6.0",
"sst": "3.18.10",
"turbo": "2.8.13",
"turbo": "2.9.14",
},
},
"packages/core": {
"name": "@opencode-ai/core",
"version": "7.3.4",
"version": "7.3.8",
"bin": {
"opencode": "./bin/opencode",
},
@@ -68,7 +68,7 @@
},
"packages/kilo-docs": {
"name": "@kilocode/kilo-docs",
"version": "7.3.4",
"version": "7.3.8",
"dependencies": {
"@docsearch/css": "^4",
"@docsearch/js": "^4",
@@ -98,7 +98,7 @@
},
"packages/kilo-gateway": {
"name": "@kilocode/kilo-gateway",
"version": "7.3.4",
"version": "7.3.8",
"dependencies": {
"@ai-sdk/alibaba": "1.0.17",
"@ai-sdk/anthropic": "3.0.71",
@@ -134,7 +134,7 @@
},
"packages/kilo-i18n": {
"name": "@kilocode/kilo-i18n",
"version": "7.3.4",
"version": "7.3.8",
"devDependencies": {
"@tsconfig/node22": "catalog:",
"@types/bun": "catalog:",
@@ -144,7 +144,7 @@
},
"packages/kilo-indexing": {
"name": "@kilocode/kilo-indexing",
"version": "7.3.4",
"version": "7.3.8",
"dependencies": {
"@aws-sdk/client-bedrock-runtime": "3.1005.0",
"@aws-sdk/credential-provider-ini": "3.972.31",
@@ -176,10 +176,11 @@
},
"packages/kilo-jetbrains": {
"name": "@kilocode/kilo-jetbrains",
"version": "7.3.8",
},
"packages/kilo-telemetry": {
"name": "@kilocode/kilo-telemetry",
"version": "7.3.4",
"version": "7.3.8",
"dependencies": {
"@kilocode/kilo-gateway": "workspace:*",
"posthog-node": "4.4.0",
@@ -193,7 +194,7 @@
},
"packages/kilo-ui": {
"name": "@kilocode/kilo-ui",
"version": "7.3.4",
"version": "7.3.8",
"dependencies": {
"@kilocode/sdk": "workspace:*",
"@kobalte/core": "0.13.11",
@@ -230,7 +231,7 @@
},
"packages/kilo-vscode": {
"name": "kilo-code",
"version": "7.3.4",
"version": "7.3.8",
"dependencies": {
"@anthropic-ai/sdk": "^0.39.0",
"@kilocode/kilo-gateway": "workspace:*",
@@ -293,7 +294,7 @@
},
"packages/opencode": {
"name": "@kilocode/cli",
"version": "7.3.4",
"version": "7.3.8",
"bin": {
"kilo": "./bin/kilo",
"kilocode": "./bin/kilo",
@@ -301,7 +302,7 @@
"dependencies": {
"@actions/core": "1.11.1",
"@actions/github": "6.0.1",
"@agentclientprotocol/sdk": "0.16.1",
"@agentclientprotocol/sdk": "0.21.0",
"@ai-sdk/alibaba": "1.0.17",
"@ai-sdk/amazon-bedrock": "4.0.96",
"@ai-sdk/anthropic": "3.0.71",
@@ -451,7 +452,7 @@
},
"packages/plugin": {
"name": "@kilocode/plugin",
"version": "7.3.4",
"version": "7.3.8",
"dependencies": {
"@kilocode/sdk": "workspace:*",
"effect": "catalog:",
@@ -476,7 +477,7 @@
},
"packages/script": {
"name": "@opencode-ai/script",
"version": "7.3.4",
"version": "7.3.8",
"dependencies": {
"semver": "^7.6.3",
},
@@ -487,7 +488,7 @@
},
"packages/sdk/js": {
"name": "@kilocode/sdk",
"version": "7.3.4",
"version": "7.3.8",
"dependencies": {
"cross-spawn": "catalog:",
},
@@ -502,7 +503,7 @@
},
"packages/storybook": {
"name": "@opencode-ai/storybook",
"version": "7.3.4",
"version": "7.3.8",
"devDependencies": {
"@opencode-ai/ui": "workspace:*",
"@solidjs/meta": "catalog:",
@@ -525,7 +526,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "7.3.4",
"version": "7.3.8",
"dependencies": {
"@kilocode/sdk": "workspace:*",
"@kobalte/core": "catalog:",
@@ -614,7 +615,7 @@
"@openauthjs/openauth": "0.0.0-20250322224806",
"@opentui/core": "0.2.2",
"@opentui/solid": "0.2.2",
"@pierre/diffs": "1.1.0-beta.18",
"@pierre/diffs": "1.1.22",
"@playwright/test": "1.59.1",
"@solid-primitives/storage": "4.3.3",
"@solidjs/meta": "0.29.4",
@@ -626,7 +627,7 @@
"@types/bun": "1.3.12",
"@types/cross-spawn": "6.0.6",
"@types/luxon": "3.7.1",
"@types/node": "22.13.9",
"@types/node": "24.12.2",
"@types/semver": "7.7.1",
"@typescript/native-preview": "7.0.0-dev.20260316.1",
"ai": "6.0.168",
@@ -672,7 +673,7 @@
"@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="],
"@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.16.1", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-1ad+Sc/0sCtZGHthxxvgEUo5Wsbw16I+aF+YwdiLnPwkZG8KAGUEAPK6LM6Pf69lCyJPt1Aomk1d+8oE3C4ZEw=="],
"@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.21.0", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-ONj+Q8qOdNQp5XbH5jnMwzT9IKZJsSN0p0lkceS4GtUtNOPVLpNzSS8gqQdGMKfBvA0ESbkL8BTaSN1Rc9miEw=="],
"@ai-sdk/alibaba": ["@ai-sdk/alibaba@1.0.17", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZbE+U5bWz2JBc5DERLowx5+TKbjGBE93LqKZAWvuEn7HOSQMraxFMZuc0ST335QZJAyfBOzh7m1mPQ+y7EaaoA=="],
@@ -1644,9 +1645,9 @@
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="],
"@pierre/diffs": ["@pierre/diffs@1.1.0-beta.18", "", { "dependencies": { "@pierre/theme": "0.0.22", "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-7ZF3YD9fxdbYsPnltz5cUqHacN7ztp8RX/fJLxwv8wIEORpP4+7dHz1h/qx3o4EW2xUrIhmbM8ImywLasB787Q=="],
"@pierre/diffs": ["@pierre/diffs@1.1.22", "", { "dependencies": { "@pierre/theme": "0.0.28", "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-1Iv7kdl6OABFCd1n2HQbGUiRHouXPaHoIjcb7Lwg8zeJKY5ph+cESFcGEyIiwW0NCbKGtYS2bTnXmI+Eze5dwg=="],
"@pierre/theme": ["@pierre/theme@0.0.22", "", {}, "sha512-ePUIdQRNGjrveELTU7fY89Xa7YGHHEy5Po5jQy/18lm32eRn96+tnYJEtFooGdffrx55KBUtOXfvVy/7LDFFhA=="],
"@pierre/theme": ["@pierre/theme@0.0.28", "", {}, "sha512-1j/H/fECBuc9dEvntdWI+l435HZapw+RCJTlqCA6BboQ5TjlnE005j/ROWutXIs8aq5OAc82JI2Kwk4A1WWBgw=="],
"@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="],
@@ -2028,6 +2029,18 @@
"@tufjs/models": ["@tufjs/models@4.1.0", "", { "dependencies": { "@tufjs/canonical-json": "2.0.0", "minimatch": "^10.1.1" } }, "sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww=="],
"@turbo/darwin-64": ["@turbo/darwin-64@2.9.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-t7QiPflaEyBE4oayeZtSmu4mEfjgIrcNlNNl1z1dmIVPqEdtA7+CfTf8d7KXsOGPh6aNgWjKxyvQg9uGfDQF+A=="],
"@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.9.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-d23147mC9BsCPA9mJ0h/ubcpbRgcJBXbcG3+Vq7YLhjz3IXuvQsJ1UXH8f4MD76ZjJ4m/E4aRdJV+MW88CDfbw=="],
"@turbo/linux-64": ["@turbo/linux-64@2.9.14", "", { "os": "linux", "cpu": "x64" }, "sha512-P3ZKB5tuUDdDQWuAsACGUR1qv9W7BNWxdxqVJ0kZNuNNPRaVYTPPikLcp79+GiEcW3npsR+KyP38lnQiBc5aSA=="],
"@turbo/linux-arm64": ["@turbo/linux-arm64@2.9.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZRTlzcUMrrPv9ZuDzRF9n60Ym13bKeG9jDB8WjxyLhWNzV+AJQN+zdpIk3NJYf2zQsGUm1mNar2P0elRzLw25g=="],
"@turbo/windows-64": ["@turbo/windows-64@2.9.14", "", { "os": "win32", "cpu": "x64" }, "sha512-exanwN6sIduZwykYeiTQj8kCmOhazP5WOz3bvXMcYtjhL6Z3iRWLewKrXCBq0bqwSP3iBMb/AerRCnHI4lx46A=="],
"@turbo/windows-arm64": ["@turbo/windows-arm64@2.9.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-fVdCsnmYoKICsycbWuuGp6Jvi51/3G/UluFWuAUCvR8PIW5IJkAk5BM9UF8PSm0Q2IphWHFZjYEgjHsh3B9y/g=="],
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
@@ -2152,7 +2165,7 @@
"@types/mssql": ["@types/mssql@9.1.11", "", { "dependencies": { "@types/node": "*", "tarn": "^3.0.1", "tedious": "*" } }, "sha512-vcujgrDbDezCxNDO4KY6gjwduLYOKfrexpRUwhoysRvcXZ3+IgZ/PMYFDgh8c3cQIxZ6skAwYo+H6ibMrBWPjQ=="],
"@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="],
"@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="],
@@ -4296,19 +4309,7 @@
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
"turbo": ["turbo@2.8.13", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.13", "turbo-darwin-arm64": "2.8.13", "turbo-linux-64": "2.8.13", "turbo-linux-arm64": "2.8.13", "turbo-windows-64": "2.8.13", "turbo-windows-arm64": "2.8.13" }, "bin": { "turbo": "bin/turbo" } }, "sha512-nyM99hwFB9/DHaFyKEqatdayGjsMNYsQ/XBNO6MITc7roncZetKb97MpHxWf3uiU+LB9c9HUlU3Jp2Ixei2k1A=="],
"turbo-darwin-64": ["turbo-darwin-64@2.8.13", "", { "os": "darwin", "cpu": "x64" }, "sha512-PmOvodQNiOj77+Zwoqku70vwVjKzL34RTNxxoARjp5RU5FOj/CGiC6vcDQhNtFPUOWSAaogHF5qIka9TBhX4XA=="],
"turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kI+anKcLIM4L8h+NsM7mtAUpElkCOxv5LgiQVQR8BASyDFfc8Efj5kCk3cqxuxOvIqx0sLfCX7atrHQ2kwuNJQ=="],
"turbo-linux-64": ["turbo-linux-64@2.8.13", "", { "os": "linux", "cpu": "x64" }, "sha512-j29KnQhHyzdzgCykBFeBqUPS4Wj7lWMnZ8CHqytlYDap4Jy70l4RNG46pOL9+lGu6DepK2s1rE86zQfo0IOdPw=="],
"turbo-linux-arm64": ["turbo-linux-arm64@2.8.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-OEl1YocXGZDRDh28doOUn49QwNe82kXljO1HXApjU0LapkDiGpfl3jkAlPKxEkGDSYWc8MH5Ll8S16Rf5tEBYg=="],
"turbo-windows-64": ["turbo-windows-64@2.8.13", "", { "os": "win32", "cpu": "x64" }, "sha512-717bVk1+Pn2Jody7OmWludhEirEe0okoj1NpRbSm5kVZz/yNN/jfjbxWC6ilimXMz7xoMT3IDfQFJsFR3PMANA=="],
"turbo-windows-arm64": ["turbo-windows-arm64@2.8.13", "", { "os": "win32", "cpu": "arm64" }, "sha512-R819HShLIT0Wj6zWVnIsYvSNtRNj1q9VIyaUz0P24SMcLCbQZIm1sV09F4SDbg+KCCumqD2lcaR2UViQ8SnUJA=="],
"turbo": ["turbo@2.9.14", "", { "optionalDependencies": { "@turbo/darwin-64": "2.9.14", "@turbo/darwin-arm64": "2.9.14", "@turbo/linux-64": "2.9.14", "@turbo/linux-arm64": "2.9.14", "@turbo/windows-64": "2.9.14", "@turbo/windows-arm64": "2.9.14" }, "bin": { "turbo": "bin/turbo" } }, "sha512-BQqXRr4UoWI3UPFrtznCLykYHxwxWh53iCB57x092jPMjIlW1wnm3N895g5irpiXmnxUhREBB0n6+y8BHhs4nw=="],
"turndown": ["turndown@7.2.0", "", { "dependencies": { "@mixmark-io/domino": "^2.2.0" } }, "sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A=="],
@@ -4338,7 +4339,7 @@
"undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="],
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="],
@@ -4552,6 +4553,8 @@
"@antfu/install-pkg/tinyexec": ["tinyexec@1.1.1", "", {}, "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg=="],
"@anthropic-ai/sdk/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@aws-crypto/sha1-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
"@aws-crypto/sha256-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
@@ -4656,6 +4659,8 @@
"@kilocode/kilo-indexing/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="],
"@manypkg/find-root/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@manypkg/find-root/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
"@manypkg/find-root/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
@@ -4804,6 +4809,30 @@
"@textlint/linter-formatter/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"@types/cacache/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/cross-spawn/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/mssql/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/node-fetch/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/npm-registry-fetch/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/npmcli__arborist/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/npmlog/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/pacote/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/qrcode/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/readable-stream/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/ssri/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/ws/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
"@vscode/ripgrep/yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],
@@ -4840,6 +4869,8 @@
"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=="],
"apache-arrow/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"archiver-utils/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
"archiver-utils/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
@@ -4874,6 +4905,8 @@
"buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"bun-types/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
"c12/dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
@@ -4966,6 +4999,8 @@
"gray-matter/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="],
"image-q/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
"isomorphic-git/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
@@ -5088,6 +5123,8 @@
"pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
"protobufjs/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"proxy-addr/ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
"qrcode/yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="],
@@ -5154,6 +5191,8 @@
"tar-fs/tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
"tedious/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"tedious/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"test-exclude/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
@@ -5212,6 +5251,8 @@
"@ai-sdk/vercel/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@anthropic-ai/sdk/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
"@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
@@ -5250,10 +5291,14 @@
"@kilocode/kilo-gateway/@opentui/solid/babel-preset-solid": ["babel-preset-solid@1.9.9", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.1" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.8" }, "optionalPeers": ["solid-js"] }, "sha512-pCnxWrciluXCeli/dj5PIEHgbNzim3evtTn12snjqqg8QZWJNMjH1AWIp4iG/tbVjqQ72aBEymMSagvmgxubXw=="],
"@manypkg/find-root/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@manypkg/find-root/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
"@morphllm/morphsdk/ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.95", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZmUNNbZl3V42xwQzPaNUi+s8eqR2lnrxf0bvB6YbLXpLjHYv0k2Y78t12cNOfY0bxGeuVVTLyk856uLuQIuXEQ=="],
"@morphllm/morphsdk/openai/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@morphllm/morphsdk/openai/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
@@ -5360,6 +5405,30 @@
"@textlint/linter-formatter/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"@types/cacache/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@types/cross-spawn/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@types/mssql/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@types/node-fetch/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@types/npm-registry-fetch/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@types/npmcli__arborist/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@types/npmlog/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@types/pacote/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@types/qrcode/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@types/readable-stream/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@types/ssri/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@types/ws/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@vscode/ripgrep/yauzl/buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
"@vscode/test-cli/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
@@ -5402,6 +5471,8 @@
"ai-gateway-provider/@ai-sdk/xai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
"apache-arrow/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"archiver-utils/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
"archiver-utils/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
@@ -5432,6 +5503,8 @@
"bl/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"bun-types/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
"c8/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
@@ -5536,10 +5609,14 @@
"gray-matter/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
"image-q/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"jszip/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
"jszip/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
"kilo-code/openai/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"lazystream/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
"lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
@@ -5602,6 +5679,8 @@
"posthog-js/@opentelemetry/resources/@opentelemetry/core": ["@opentelemetry/core@2.7.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-DT12SXVwV2eoJrGf4nnsvZojxxeQo+LlNAsoYGRRObPWTeN6APiqZ2+nqDCQDvQX40eLi1AePONS0onoASp3yQ=="],
"protobufjs/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"qrcode/yargs/cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="],
"qrcode/yargs/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
@@ -5632,6 +5711,8 @@
"tar-fs/tar-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
"tedious/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"test-exclude/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
"test-exclude/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
@@ -5664,6 +5745,8 @@
"@morphllm/morphsdk/ai/@ai-sdk/gateway/@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="],
"@morphllm/morphsdk/openai/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@octokit/graphql/@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
"@octokit/rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="],
@@ -5776,6 +5859,8 @@
"gray-matter/js-yaml/argparse/sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
"kilo-code/openai/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"mocha/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
"mocha/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-lENbjYvGZewVWQcr7zXJn+v1hrvipHEnjE1yS+kEwLs=",
"aarch64-linux": "sha256-yqTdPGtoLEOaFc7PhBFRiS/6gI3xUgHG/cQcULKytyM=",
"aarch64-darwin": "sha256-lyn6yjD8qv2KwIDwlA8LXBPkcHTCZCtXIsktK5acoXM=",
"x86_64-darwin": "sha256-diaLa3lmWzLcm4TeAwH1hFIqQYQQUcG4YD306vQWCqA="
"x86_64-linux": "sha256-EbXkvlbexL1Gyy4vvGG5j+JWjv7SsVk6T5YnDfo3AQY=",
"aarch64-linux": "sha256-+MuJE4XGr0dArD/HuozaoK9Oymfgvx1CisE/Sm4ZstY=",
"aarch64-darwin": "sha256-Qdk+tZLydGld471ApL1cxfd85QQNFelTfqq7uAznfK4=",
"x86_64-darwin": "sha256-Fo6W65MfAcXaulBGCaE7+GfHtq3VVJyVDJpIENvR7lQ="
}
}
+5 -5
View File
@@ -9,7 +9,7 @@
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
"dev:storybook": "bun --cwd packages/storybook storybook",
"lint": "oxlint",
"typecheck": "bun turbo typecheck --filter=!@kilocode/kilo-jetbrains",
"typecheck": "bun turbo typecheck",
"postinstall": "bun run --cwd packages/opencode fix-node-pty && bun run script/setup-git.ts",
"prepare": "husky",
"random": "echo 'Random script'",
@@ -36,13 +36,13 @@
"ulid": "3.0.1",
"@kobalte/core": "0.13.11",
"@types/luxon": "3.7.1",
"@types/node": "22.13.9",
"@types/node": "24.12.2",
"@types/semver": "7.7.1",
"@tsconfig/node22": "22.0.2",
"@tsconfig/bun": "1.0.9",
"@cloudflare/workers-types": "4.20251008.0",
"@openauthjs/openauth": "0.0.0-20250322224806",
"@pierre/diffs": "1.1.0-beta.18",
"@pierre/diffs": "1.1.22",
"opentui-spinner": "0.0.6",
"@solid-primitives/storage": "4.3.3",
"@tailwindcss/vite": "4.1.11",
@@ -91,7 +91,7 @@
"prettier": "3.6.2",
"semver": "^7.6.0",
"sst": "3.18.10",
"turbo": "2.8.13",
"turbo": "2.9.14",
"@types/bun": "catalog:",
"@changesets/changelog-github": "^0.5.1",
"@changesets/cli": "^2.27.10"
@@ -145,6 +145,6 @@
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch"
},
"version": "7.3.4",
"version": "7.3.8",
"peerDependencies": {}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "7.3.4",
"version": "7.3.8",
"name": "@opencode-ai/core",
"type": "module",
"license": "MIT",
+2
View File
@@ -87,6 +87,8 @@ export const layer = Layer.effect(
Effect.sync(() => Service.of(make())),
)
export const defaultLayer = layer
export const layerWith = (input: Partial<Interface>) =>
Layer.effect(
Service,
+6 -6
View File
@@ -1,7 +1,7 @@
id = "kilo"
name = "Kilo"
description = "The open source coding agent."
version = "7.3.4"
version = "7.3.8"
schema_version = 1
authors = ["Anomaly"]
repository = "https://github.com/Kilo-Org/kilocode"
@@ -11,26 +11,26 @@ name = "Kilo"
icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.4/opencode-darwin-arm64.zip"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.8/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.4/opencode-darwin-x64.zip"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.8/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.4/opencode-linux-arm64.tar.gz"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.8/opencode-linux-arm64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.4/opencode-linux-x64.tar.gz"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.8/opencode-linux-x64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.4/opencode-windows-x64.zip"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.8/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@kilocode/kilo-docs",
"version": "7.3.4",
"version": "7.3.8",
"private": true,
"scripts": {
"dev": "next dev --webpack --port 3002",
@@ -68,7 +68,7 @@ To fill empty seats or remove members ahead of seat deletion, see the [team mana
## Automatic Top-Up
Ensure your team has uninterrupted access to Kilo Code by enabling Automatic Top-Up. This feature keeps your organization's balance funded so you never have to worry about manual recharges.
Ensure your team has uninterrupted access to Kilo Code by enabling [Automatic Top-Up](https://kilo.ai/features/auto-top-ups). This feature keeps your organization's balance funded so you never have to worry about manual recharges. Auto top-up is also available for individual accounts — see [Adding Credits](/docs/getting-started/adding-credits) for details.
### How It Works
@@ -9,9 +9,9 @@ Once you've used any initial free Kilo Credits, you can easily add more:
- Subscribe to the [Kilo Pass](https://kilo.ai/features/kilo-pass), the most cost effective way to add credits.
- Purchase additional credits as a one-time transaction.
- Enable automatic top-up, which purchases additional credits when your balance is below $5.
- Enable [automatic top-up](https://kilo.ai/features/auto-top-ups), which purchases additional credits when your balance runs low. Auto top-up is available for both individual and organization accounts.
These options are available to purchase from your [personal profile page](https://app.kilo.ai/profile).
These options are available to purchase from your [personal profile page](https://app.kilo.ai/profile). Organization owners can manage credits and auto top-up from the [Organization dashboard](https://app.kilo.ai).
You can also use subscriptions or credits you may have purchased directly with an AI provider by adding your keys on the [Bring your own Key (BYOK)](https://app.kilo.ai/byok) settings screen. For setup details and supported providers, see [AI Providers documentation](/docs/ai-providers). If your provider is not yet supported, you can also [directly connect your provider](/docs/getting-started/setup-authentication) in the extension and CLI.
@@ -871,4 +871,10 @@ module.exports = [
basePath: false,
permanent: true,
},
{
source: "/auto-top-ups",
destination: "https://kilo.ai/features/auto-top-ups",
basePath: false,
permanent: true,
},
]
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7aa0924c6679cbff6e816dd86371a2d483afb7f0840824503ef4ba325c25d6c5
size 26282
oid sha256:97728b91ed1c92556abfde08824b91536e87cc7afa4d121bb5b1b488946c1d05
size 26009
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f28a551185ec9d7607d02295803b155e0f7bac378fcefdf0996c6ca51c0afad6
size 21892
oid sha256:7b8e9f6e0b9a9a62efc9b1e807936049c7627674ba241239dfd777698f951acc
size 21820
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-gateway",
"version": "7.3.4",
"version": "7.3.8",
"type": "module",
"license": "MIT",
"description": "Unified Kilo Gateway package for OpenCode - authentication, provider, and API integration",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-i18n",
"version": "7.3.4",
"version": "7.3.8",
"type": "module",
"license": "MIT",
"description": "Kilo-specific i18n translations and overrides",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-indexing",
"version": "7.3.4",
"version": "7.3.8",
"type": "module",
"license": "MIT",
"description": "Standalone indexing engine and host helpers for Kilo Code",
@@ -302,10 +302,9 @@ export class CodeIndexConfigManager {
}
public get currentModelDimension(): number | undefined {
if (this.modelDimension && this.modelDimension > 0) return this.modelDimension
const id = this.modelId ?? getDefaultModelId(this.embedderProvider)
const dim = getModelDimension(this.embedderProvider, id)
if (!dim && this.modelDimension && this.modelDimension > 0) return this.modelDimension
return dim
return getModelDimension(this.embedderProvider, id)
}
public get currentSearchMinScore(): number {
@@ -25,7 +25,8 @@ interface EmbeddingItem {
}
interface OpenRouterEmbeddingResponse {
data: EmbeddingItem[]
data?: EmbeddingItem[]
error?: string | { code?: string | number; message?: string }
usage?: {
prompt_tokens?: number
total_tokens?: number
@@ -193,10 +194,7 @@ export class OpenRouterEmbedder implements IEmbedder {
const requestParams: any = {
input: batchTexts,
model: model,
// OpenAI package (as of v4.78.1) has a parsing issue that truncates embedding dimensions to 256
// when processing numeric arrays, which breaks compatibility with models using larger dimensions.
// By requesting base64 encoding, we bypass the package's parser and handle decoding ourselves.
encoding_format: "base64",
encoding_format: "float",
}
if (this.dimensions !== undefined) {
@@ -213,8 +211,24 @@ export class OpenRouterEmbedder implements IEmbedder {
}
const response = (await this.embeddingsClient.embeddings.create(requestParams)) as OpenRouterEmbeddingResponse
const err = response.error
const msg = typeof err === "string" ? err : err?.message
const code = typeof err === "object" && err ? err.code : undefined
if (!response.data || response.data.length === 0) {
log.warn("OpenRouter embedder batch returned invalid response", {
location: "OpenRouterEmbedder:_embedBatchWithRetries",
model,
dimensions: this.dimensions,
provider: this.specificProvider,
code,
err: msg,
})
const invalid = new Error(msg ?? "Invalid response from OpenRouter embedding endpoint") as HttpError
invalid.status = typeof code === "number" ? code : 422
throw invalid
}
// Convert base64 embeddings to float32 arrays
// Normalize base64 embeddings if OpenRouter returns them despite the float request.
const processedEmbeddings = response.data.map((item: EmbeddingItem) => {
if (typeof item.embedding === "string") {
const buffer = Buffer.from(item.embedding, "base64")
@@ -292,7 +306,7 @@ export class OpenRouterEmbedder implements IEmbedder {
const requestParams: any = {
input: testTexts,
model: modelToUse,
encoding_format: "base64",
encoding_format: "float",
}
if (this.dimensions !== undefined) {
@@ -315,6 +329,18 @@ export class OpenRouterEmbedder implements IEmbedder {
// Check if we got a valid response
if (!response?.data || response.data.length === 0) {
const err = response?.error
const msg = typeof err === "string" ? err : err?.message
const code = typeof err === "object" && err ? err.code : undefined
log.warn("OpenRouter embedder validation returned invalid response", {
location: "OpenRouterEmbedder:validateConfiguration",
model: modelToUse,
dimensions: this.dimensions,
provider: this.specificProvider,
dataCount: response?.data?.length ?? 0,
code,
err: msg,
})
return {
valid: false,
error: "Invalid response from OpenRouter embedding endpoint",
@@ -20,7 +20,7 @@ export function resolveEmbeddingProfile(
modelDimension?: number,
): EmbeddingProfile | undefined {
const id = modelId ?? getDefaultModelId(provider)
const dim = getModelDimension(provider, id) ?? parseDimension(modelDimension)
const dim = parseDimension(modelDimension) ?? getModelDimension(provider, id)
if (!dim) return undefined
return {
provider,
@@ -48,6 +48,7 @@ const profiles: Record<string, Record<string, ModelProfile>> = {
openrouter: {
"openai/text-embedding-3-small": { dimension: 1536, scoreThreshold: 0.4 },
"openai/text-embedding-3-large": { dimension: 3072, scoreThreshold: 0.4 },
"google/gemini-embedding-2-preview": { dimension: 3072, scoreThreshold: 0.35 },
},
"openai-compatible": {},
"vercel-ai-gateway": {
@@ -83,6 +83,20 @@ describe("CodeIndexConfigManager", () => {
expect(cfg.currentModelDimension).toBe(2048)
})
test("uses configured dimension before static model metadata", () => {
const cfg = new CodeIndexConfigManager(
createInput({
embedderProvider: "openrouter",
openAiKey: undefined,
openRouterApiKey: "or-test",
modelId: "google/gemini-embedding-2-preview",
modelDimension: 1536,
}),
)
expect(cfg.currentModelDimension).toBe(1536)
})
describe("loadConfiguration restart checks", () => {
test("requires restart when model changes with same dimension", () => {
const cfg = new CodeIndexConfigManager(createInput({ modelId: "text-embedding-3-small" }))
@@ -68,14 +68,10 @@ describe("OpenRouterEmbedder", () => {
})
test("should create embeddings successfully", async () => {
// Create base64 encoded embedding with values that can be exactly represented in Float32
const testEmbedding = new Float32Array([0.25, 0.5, 0.75])
const base64String = Buffer.from(testEmbedding.buffer).toString("base64")
const mockResponse = {
data: [
{
embedding: base64String,
embedding: [0.25, 0.5, 0.75],
},
],
usage: {
@@ -91,7 +87,7 @@ describe("OpenRouterEmbedder", () => {
expect(mockEmbeddingsCreate).toHaveBeenCalledWith({
input: ["test text"],
model: defaultModel,
encoding_format: "base64",
encoding_format: "float",
})
expect(result.embeddings).toHaveLength(1)
expect(result.embeddings[0]).toEqual([0.25, 0.5, 0.75])
@@ -99,6 +95,20 @@ describe("OpenRouterEmbedder", () => {
expect(result.usage?.totalTokens).toBe(5)
})
test("should not retry invalid responses without embedding data", async () => {
mockEmbeddingsCreate.mockResolvedValue({
error: {
code: 404,
message: "No successful provider responses.",
},
})
await expect(embedder.createEmbeddings(["test"])).rejects.toThrow(
"Embedding request failed after 3 attempts with status 404: No successful provider responses.",
)
expect(mockEmbeddingsCreate).toHaveBeenCalledTimes(1)
})
test("should handle multiple texts", async () => {
const embedding1 = new Float32Array([0.25, 0.5])
const embedding2 = new Float32Array([0.75, 1.0])
@@ -156,7 +166,7 @@ describe("OpenRouterEmbedder", () => {
expect(mockEmbeddingsCreate).toHaveBeenCalledWith({
input: ["test"],
model: customModel,
encoding_format: "base64",
encoding_format: "float",
})
})
@@ -187,7 +197,7 @@ describe("OpenRouterEmbedder", () => {
expect(mockEmbeddingsCreate).toHaveBeenCalledWith({
input: ["test"],
model: defaultModel,
encoding_format: "base64",
encoding_format: "float",
provider: {
order: [specificProvider],
only: [specificProvider],
@@ -221,7 +231,7 @@ describe("OpenRouterEmbedder", () => {
expect(mockEmbeddingsCreate).toHaveBeenCalledWith({
input: ["test"],
model: defaultModel,
encoding_format: "base64",
encoding_format: "float",
dimensions: 1024,
})
})
@@ -257,7 +267,7 @@ describe("OpenRouterEmbedder", () => {
expect(mockEmbeddingsCreate).toHaveBeenCalledWith({
input: ["test"],
model: defaultModel,
encoding_format: "base64",
encoding_format: "float",
})
})
})
@@ -271,13 +281,10 @@ describe("OpenRouterEmbedder", () => {
})
test("should validate configuration successfully", async () => {
const testEmbedding = new Float32Array([0.25, 0.5])
const base64String = Buffer.from(testEmbedding.buffer).toString("base64")
const mockResponse = {
data: [
{
embedding: base64String,
embedding: [0.25, 0.5],
},
],
usage: {
@@ -296,7 +303,7 @@ describe("OpenRouterEmbedder", () => {
{
input: ["test"],
model: defaultModel,
encoding_format: "base64",
encoding_format: "float",
},
{
timeout: REMOTE_EMBEDDER_VALIDATION_TIMEOUT_MS,
@@ -305,6 +312,20 @@ describe("OpenRouterEmbedder", () => {
)
})
test("should reject responses without embedding data", async () => {
mockEmbeddingsCreate.mockResolvedValue({
error: {
code: 404,
message: "No successful provider responses.",
},
})
const result = await embedder.validateConfiguration()
expect(result.valid).toBe(false)
expect(result.error).toBe("Invalid response from OpenRouter embedding endpoint")
})
test("should handle validation failure", async () => {
const authError = new Error("Invalid API key")
;(authError as any).status = 401
@@ -346,7 +367,7 @@ describe("OpenRouterEmbedder", () => {
{
input: ["test"],
model: defaultModel,
encoding_format: "base64",
encoding_format: "float",
provider: {
order: [specificProvider],
only: [specificProvider],
@@ -388,7 +409,7 @@ describe("OpenRouterEmbedder", () => {
{
input: ["test"],
model: defaultModel,
encoding_format: "base64",
encoding_format: "float",
dimensions: 1024,
},
{
@@ -149,11 +149,42 @@ describe("CodeIndexServiceFactory", () => {
expect(mockEmbeddingsCreate).toHaveBeenCalledWith({
input: ["hello"],
model: "openai/text-embedding-3-small",
encoding_format: "base64",
encoding_format: "float",
dimensions: 1024,
})
})
test("creates vector store for OpenRouter Gemini embedding preview", () => {
const factory = createFactory({
embedderProvider: "openrouter",
openAiKey: undefined,
openRouterApiKey: "or-test",
modelId: "google/gemini-embedding-2-preview",
vectorStoreProvider: "lancedb",
})
const store = factory.createVectorStore() as unknown as { vectorSize: number }
expect(store).toBeDefined()
expect(store.vectorSize).toBe(3072)
})
test("uses configured dimension before static model metadata for vector stores", () => {
const factory = createFactory({
embedderProvider: "openrouter",
openAiKey: undefined,
openRouterApiKey: "or-test",
modelId: "openai/text-embedding-3-small",
modelDimension: 1024,
vectorStoreProvider: "lancedb",
})
const store = factory.createVectorStore() as unknown as { vectorSize: number }
expect(store).toBeDefined()
expect(store.vectorSize).toBe(1024)
})
test("creates Kilo embedder with Cloud-provided model", async () => {
const factory = createFactory({
embedderProvider: "kilo",
@@ -177,6 +208,7 @@ describe("CodeIndexServiceFactory", () => {
input: ["hello"],
model: "mistralai/mistral-embed-2312",
encoding_format: "base64",
dimensions: 1024,
})
})
})
+11 -1
View File
@@ -9,7 +9,17 @@ AI coding agent plugin for JetBrains IDEs.
### Prerequisites
- **Bun** -- used to build CLI binaries and run build scripts
- **JDK 21+** -- required by Gradle and the IntelliJ Platform SDK
- **JDK 21+** -- required by Gradle and the IntelliJ Platform SDK. Check with `java -version`. The preferred way to install is via [SDKMAN](https://sdkman.io/install):
```bash
# Install SDKMAN (if not already installed)
curl -s "https://get.sdkman.io" | bash
# Install and activate Java 21 (Eclipse Temurin)
sdk install java 21-tem
sdk use java 21-tem
```
- **IntelliJ IDEA** -- to run the plugin in a sandboxed IDE
---
@@ -86,6 +86,10 @@ tasks.named("compileKotlin") {
dependsOn(fixGeneratedApi)
}
tasks.named("compileTestKotlin") {
dependsOn(fixGeneratedApi)
}
val cliDir = layout.buildDirectory.dir("generated/cli/cli")
val production = providers.gradleProperty("production").map { it.toBoolean() }.orElse(false)
@@ -1,24 +0,0 @@
package normalization
internal data class DuplicateTagRule(
val original: String,
val dedups: List<TagDedup>,
)
internal data class TagDedup(
val name: String,
val ops: List<String> = emptyList(),
)
internal val duplicateTagRules = listOf(
DuplicateTagRule(
original = "pty",
dedups = listOf(
TagDedup(name = "pty"),
TagDedup(
name = "pty-connect",
ops = listOf("pty.connect"),
),
),
),
)
@@ -11,105 +11,159 @@ internal object OpenApiSpecNormalizer {
fun normalize(raw: String): String {
val root = Json.parseToJsonElement(raw) as? JsonObject
?: throw GradleException("OpenAPI spec root must be a JSON object.")
val tags = root["tags"] as? JsonArray ?: return raw
val (fixedTags, renames) = fixTags(tags)
if (renames.isEmpty()) return raw
val paths = root["paths"] as? JsonObject
?: throw GradleException("OpenAPI spec paths must be a JSON object.")
val fixed = JsonObject(
root + mapOf(
"tags" to fixedTags,
"paths" to fixPaths(paths, renames),
)
)
// Step 1: Remove duplicate dot-notation schemas and remap their $refs to
// camelCase equivalents so the spec remains self-consistent.
// Step 2: Strip operation-level tags so all routes land in DefaultApi.
// Step 3: Deduplicate the root-level tags array.
// Step 4: Fix nullable fields in the /kilo/profile response that Effect's
// OpenAPI generator incorrectly emits as non-nullable.
val (noDotsRoot, _) = remapDotSchemas(root)
val stripped = stripTags(noDotsRoot)
val deduped = dedupRootTags(stripped)
val fixed = fixProfileNullable(deduped)
return encode(fixed)
}
private fun encode(obj: JsonObject): String {
val json = Json { prettyPrint = true }
.encodeToString(JsonElement.serializer(), fixed)
.encodeToString(JsonElement.serializer(), obj)
return "$json\n"
}
private fun fixTags(tags: JsonArray): Pair<JsonArray, Map<String, Rename>> {
val rules = rules()
val counts = mutableMapOf<String, Int>()
val renames = mutableMapOf<String, Rename>()
val fixed = tags.map { tag ->
val item = tag as? JsonObject ?: return@map tag
val name = text(item["name"]) ?: return@map tag
val rule = rules[name] ?: return@map tag
val index = counts.getOrDefault(name, 0)
counts[name] = index + 1
if (index >= rule.dedups.size) {
throw GradleException("Missing final OpenAPI tag name for duplicate '$name' at index $index.")
/**
* Find schemas whose names contain dots (e.g. "Event.tui.command.execute").
* If a camelCase equivalent (e.g. "EventTuiCommandExecute") exists in the
* same component map, remove the dot schema and rewrite every `$ref` that
* points to it to use the camelCase name instead.
*/
private fun remapDotSchemas(root: JsonObject): Pair<JsonObject, Map<String, String>> {
val components = root["components"] as? JsonObject ?: return root to emptyMap()
val schemas = components["schemas"] as? JsonObject ?: return root to emptyMap()
// Build a map of dot-name → camelCase-name for schemas that have a
// camelCase duplicate in the same spec.
val dotMap = schemas.keys
.filter { "." in it }
.mapNotNull { dot ->
val camel = dot.split(".").joinToString("") { w -> w.replaceFirstChar { c -> c.uppercase() } }
if (camel in schemas) dot to camel else null
}
val dedup = rule.dedups[index]
if (index == 0) return@map tag
renames[dedup.name] = Rename(rule.original, dedup)
JsonObject(item + ("name" to JsonPrimitive(dedup.name)))
}
rules.forEach { (name, rule) ->
val count = counts[name] ?: 0
if (count > 1 && count != rule.dedups.size) {
throw GradleException("Duplicate OpenAPI tag '$name' has $count entries but ${rule.dedups.size} final names.")
}
}
return JsonArray(fixed) to renames
.toMap()
if (dotMap.isEmpty()) return root to emptyMap()
// Remove dot schemas.
val cleaned = JsonObject(schemas.filterKeys { it !in dotMap })
val noDotsComponents = JsonObject(components + mapOf("schemas" to cleaned))
val noDotsRoot = JsonObject(root + mapOf("components" to noDotsComponents))
// Rewrite $ref strings throughout the whole spec.
val rewritten = rewriteRefs(noDotsRoot, dotMap)
return rewritten to dotMap
}
private fun fixPaths(paths: JsonObject, renames: Map<String, Rename>): JsonObject {
val ops = renames.values.flatMap { rename ->
rename.dedup.ops.map { id -> id to rename }
}.toMap()
val hits = mutableMapOf<String, Int>()
val fixed = JsonObject(paths.mapValues { (_, item) ->
/**
* Recursively rewrite every JsonPrimitive `$ref` value that matches a
* dot-notation schema name, replacing it with the camelCase equivalent.
*/
private fun rewriteRefs(element: JsonElement, map: Map<String, String>): JsonObject {
return rewriteElement(element, map) as JsonObject
}
private fun rewriteElement(element: JsonElement, map: Map<String, String>): JsonElement =
when (element) {
is JsonObject -> JsonObject(element.mapValues { (key, value) ->
if (key == "\$ref" && value is JsonPrimitive) {
val ref = value.content
val prefix = "#/components/schemas/"
if (ref.startsWith(prefix)) {
val name = ref.removePrefix(prefix)
val replaced = map[name]
if (replaced != null) JsonPrimitive("$prefix$replaced") else value
} else value
} else rewriteElement(value, map)
})
is JsonArray -> JsonArray(element.map { rewriteElement(it, map) })
else -> element
}
/**
* Remove the "tags" field from every operation so that openapi-generator
* collects all operations into a single DefaultApi class.
*/
private fun stripTags(root: JsonObject): JsonObject {
val paths = root["paths"] as? JsonObject ?: return root
val stripped = JsonObject(paths.mapValues { (_, item) ->
val path = item as? JsonObject ?: return@mapValues item
JsonObject(path.mapValues { (_, op) ->
val obj = op as? JsonObject ?: return@mapValues op
val id = text(obj["operationId"]) ?: return@mapValues op
val rename = ops[id] ?: return@mapValues op
hits[id] = hits.getOrDefault(id, 0) + 1
fixOp(obj, rename)
if ("tags" !in obj) return@mapValues op
JsonObject(obj.filterKeys { it != "tags" })
})
})
ops.keys.forEach { id ->
val count = hits[id] ?: 0
if (count != 1) {
throw GradleException("Expected one OpenAPI operation '$id' for tag normalization, found $count.")
}
}
return fixed
return JsonObject(root + mapOf("paths" to stripped))
}
private fun fixOp(op: JsonObject, rename: Rename): JsonObject {
val tags = op["tags"] as? JsonArray
?: throw GradleException("OpenAPI operation must declare tags before tag normalization.")
val count = tags.count { tag -> text(tag) == rename.from }
if (count != 1) {
throw GradleException("Expected one '${rename.from}' operation tag, found $count.")
}
return JsonObject(op + ("tags" to JsonArray(tags.map { tag ->
if (text(tag) != rename.from) return@map tag
JsonPrimitive(rename.dedup.name)
})))
/**
* Fix the `/kilo/profile` GET 200 response schema: Effect's OpenAPI generator
* emits `balance` and `currentOrgId` as non-nullable required fields even
* though the server schema is `Schema.NullOr(...)`. Wrap each non-nullable
* property in `anyOf: [<original-schema>, {"type": "null"}]` so the generated
* Kotlin model uses a nullable type. Already-nullable properties (those that
* already have `anyOf` containing `{"type":"null"}`) are left untouched.
*/
private fun fixProfileNullable(root: JsonObject): JsonObject {
val paths = root["paths"] as? JsonObject ?: return root
val profileItem = paths["/kilo/profile"] as? JsonObject ?: return root
val getOp = profileItem["get"] as? JsonObject ?: return root
val schema = getOp["responses"]
?.let { it as? JsonObject }?.get("200")
?.let { it as? JsonObject }?.get("content")
?.let { it as? JsonObject }?.get("application/json")
?.let { it as? JsonObject }?.get("schema")
as? JsonObject ?: return root
val props = schema["properties"] as? JsonObject ?: return root
val nullable = setOf("balance", "currentOrgId")
val fixed = JsonObject(props.mapValues { (key, value) ->
if (key !in nullable) return@mapValues value
val obj = value as? JsonObject ?: return@mapValues value
// Skip if already wrapped (has anyOf containing {type:null}).
val existing = obj["anyOf"] as? JsonArray
if (existing != null && existing.any {
(it as? JsonObject)?.get("type")?.let { t -> (t as? JsonPrimitive)?.content } == "null"
}) return@mapValues value
JsonObject(mapOf("anyOf" to JsonArray(listOf(obj, JsonObject(mapOf("type" to JsonPrimitive("null")))))))
})
// Rebuild nested objects up to root.
val responses = getOp["responses"]!! as JsonObject
val resp200 = responses["200"]!! as JsonObject
val content = resp200["content"]!! as JsonObject
val appJson = content["application/json"]!! as JsonObject
val newSchema = JsonObject(schema + mapOf("properties" to fixed))
val newApp = JsonObject(appJson + mapOf("schema" to newSchema))
val newContent = JsonObject(content + mapOf("application/json" to newApp))
val new200 = JsonObject(resp200 + mapOf("content" to newContent))
val newResponses = JsonObject(responses + mapOf("200" to new200))
val newGet = JsonObject(getOp + mapOf("responses" to newResponses))
val newProfile = JsonObject(profileItem + mapOf("get" to newGet))
val newPaths = JsonObject(paths + mapOf("/kilo/profile" to newProfile))
return JsonObject(root + mapOf("paths" to newPaths))
}
private fun rules(): Map<String, DuplicateTagRule> {
if (duplicateTagRules.map { it.original }.toSet().size != duplicateTagRules.size) {
throw GradleException("OpenAPI duplicate tag config must not repeat original tag names.")
}
return duplicateTagRules.associateBy { rule ->
if (rule.dedups.size < 2 || rule.dedups.first().name != rule.original) {
throw GradleException("OpenAPI tag rule '${rule.original}' must keep the original tag first.")
}
if (rule.dedups.map { it.name }.toSet().size != rule.dedups.size) {
throw GradleException("OpenAPI tag rule '${rule.original}' must use unique dedup names.")
}
rule.original
/**
* Deduplicate the root-level "tags" array by name — the spec validator
* rejects repeated tag names even when they describe different things.
*/
private fun dedupRootTags(root: JsonObject): JsonObject {
val tags = root["tags"] as? JsonArray ?: return root
val seen = mutableSetOf<String>()
val deduped = tags.filter { tag ->
val name = (tag as? JsonObject)?.let { (it["name"] as? JsonPrimitive)?.content }
?: return@filter true
seen.add(name)
}
return JsonObject(root + mapOf("tags" to JsonArray(deduped)))
}
private fun text(value: JsonElement?) = (value as? JsonPrimitive)?.content
private data class Rename(
val from: String,
val dedup: TagDedup,
)
}
@@ -5,15 +5,13 @@ import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import org.gradle.api.GradleException
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
import kotlin.test.assertNull
class OpenApiSpecNormalizerTest {
@Test
fun `renames configured duplicate tags and linked operations`() {
fun `strips tags from all operations`() {
val raw = """
{
"paths": {
@@ -23,61 +21,207 @@ class OpenApiSpecNormalizerTest {
"operationId": "pty.list"
}
},
"/pty/{ptyID}/connect": {
"/session": {
"post": {
"tags": ["session"],
"operationId": "session.create"
}
}
}
}
""".trimIndent()
val root = obj(OpenApiSpecNormalizer.normalize(raw))
val paths = obj(root["paths"])
val pty = obj(obj(paths["/pty"])["get"])
val session = obj(obj(paths["/session"])["post"])
assertNull(pty["tags"], "tags should be stripped from pty operation")
assertNull(session["tags"], "tags should be stripped from session operation")
}
@Test
fun `leaves operations without tags unchanged`() {
val raw = """
{
"paths": {
"/health": {
"get": {
"tags": ["pty"],
"operationId": "pty.connect"
"operationId": "health.get"
}
}
}
}
""".trimIndent()
val root = obj(OpenApiSpecNormalizer.normalize(raw))
val paths = obj(root["paths"])
val health = obj(obj(paths["/health"])["get"])
assertNull(health["tags"])
assertEquals("health.get", text(health["operationId"]))
}
@Test
fun `removes dot schemas and rewrites refs to camelCase equivalents`() {
val raw = """
{
"paths": {
"/tui/publish": {
"post": {
"requestBody": {
"content": {
"application/json": {
"schema": {
"${'$'}ref": "#/components/schemas/Event.tui.command.execute"
}
}
}
}
}
}
},
"components": {
"schemas": {
"EventTuiCommandExecute": { "type": "object" },
"Event.tui.command.execute": { "type": "object" },
"Session": { "type": "object" }
}
}
}
""".trimIndent()
val root = obj(OpenApiSpecNormalizer.normalize(raw))
val schemas = obj(obj(root["components"])["schemas"])
assertNull(schemas["Event.tui.command.execute"], "dot schema should be removed")
assert("EventTuiCommandExecute" in schemas) { "camelCase schema should be kept" }
assert("Session" in schemas) { "non-dot schema should be kept" }
// Check that the $ref was rewritten
val post = obj(obj(obj(obj(root["paths"])["/tui/publish"])["post"])["requestBody"])
val schema = obj(obj(obj(post["content"])["application/json"])["schema"])
assertEquals("#/components/schemas/EventTuiCommandExecute", text(schema["\$ref"]))
}
@Test
fun `makes balance and currentOrgId nullable in kilo profile response`() {
val raw = """
{
"paths": {
"/kilo/profile": {
"get": {
"operationId": "kilo.profile",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"profile": { "type": "object", "properties": { "email": { "type": "string" } }, "required": ["email"], "additionalProperties": false },
"balance": { "type": "object", "properties": { "balance": { "type": "number" } }, "required": ["balance"], "additionalProperties": false },
"currentOrgId": { "type": "string" }
},
"required": ["profile", "balance", "currentOrgId"],
"additionalProperties": false
}
}
}
}
}
}
}
}
}
""".trimIndent()
val root = obj(OpenApiSpecNormalizer.normalize(raw))
val schema = obj(obj(obj(obj(obj(obj(root["paths"])["/kilo/profile"])["get"])["responses"])["200"])["content"])
val props = obj(obj(obj(schema["application/json"])["schema"])["properties"])
// balance must be anyOf [object, null]
val balance = obj(props["balance"])
val balanceAnyOf = arr(balance["anyOf"])
assertEquals(2, balanceAnyOf.size, "balance should have anyOf with 2 entries")
val balanceTypes = balanceAnyOf.map { (it as? JsonObject)?.get("type").let { t -> (t as? JsonPrimitive)?.content } }
assert("null" in balanceTypes) { "balance anyOf should include null but got $balanceTypes" }
assert(balanceAnyOf.any { it is JsonObject && "properties" in it }) { "balance anyOf should include the object schema" }
// currentOrgId must be anyOf [string, null]
val orgId = obj(props["currentOrgId"])
val orgIdAnyOf = arr(orgId["anyOf"])
assertEquals(2, orgIdAnyOf.size, "currentOrgId should have anyOf with 2 entries")
val orgIdTypes = orgIdAnyOf.map { (it as? JsonObject)?.get("type").let { t -> (t as? JsonPrimitive)?.content } }
assert("null" in orgIdTypes) { "currentOrgId anyOf should include null but got $orgIdTypes" }
assert("string" in orgIdTypes) { "currentOrgId anyOf should include string but got $orgIdTypes" }
// profile must remain unchanged (not wrapped in anyOf)
val profile = obj(props["profile"])
assertNull(profile["anyOf"], "profile should not be wrapped in anyOf")
assertEquals("object", text(profile["type"]))
}
@Test
fun `leaves already-nullable fields unchanged in kilo profile response`() {
// If balance already has anyOf (i.e. the spec was generated correctly), normalizer must not double-wrap it.
val raw = """
{
"paths": {
"/kilo/profile": {
"get": {
"operationId": "kilo.profile",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"profile": { "type": "object", "properties": { "email": { "type": "string" } }, "required": ["email"], "additionalProperties": false },
"balance": { "anyOf": [{ "type": "object", "properties": { "balance": { "type": "number" } }, "required": ["balance"], "additionalProperties": false }, { "type": "null" }] },
"currentOrgId": { "anyOf": [{ "type": "string" }, { "type": "null" }] }
},
"required": ["profile", "balance", "currentOrgId"],
"additionalProperties": false
}
}
}
}
}
}
}
}
}
""".trimIndent()
val root = obj(OpenApiSpecNormalizer.normalize(raw))
val schema = obj(obj(obj(obj(obj(obj(root["paths"])["/kilo/profile"])["get"])["responses"])["200"])["content"])
val props = obj(obj(obj(schema["application/json"])["schema"])["properties"])
// balance must still have exactly 2 anyOf entries (not wrapped again)
val balance = obj(props["balance"])
val balanceAnyOf = arr(balance["anyOf"])
assertEquals(2, balanceAnyOf.size, "balance should still have exactly 2 anyOf entries, not be double-wrapped")
}
@Test
fun `deduplicates root-level tags array`() {
val raw = """
{
"paths": {},
"tags": [
{ "name": "pty", "description": "PTY routes." },
{ "name": "pty", "description": "PTY WebSocket route." }
{ "name": "pty", "description": "PTY WebSocket route." },
{ "name": "session", "description": "Session routes." }
]
}
""".trimIndent()
val root = obj(OpenApiSpecNormalizer.normalize(raw))
val tags = arr(root["tags"]).map { text(obj(it)["name"]) }
val paths = obj(root["paths"])
val pty = obj(obj(paths["/pty"])["get"])
val link = obj(obj(paths["/pty/{ptyID}/connect"])["get"])
assertEquals(listOf("pty", "pty-connect"), tags)
assertEquals(listOf("pty"), arr(pty["tags"]).map(::text))
assertEquals(listOf("pty-connect"), arr(link["tags"]).map(::text))
}
@Test
fun `keeps specs without duplicate configured tags unchanged`() {
val raw = """
{
"tags": [
{ "name": "pty", "description": "PTY routes." }
]
}
""".trimIndent()
assertEquals(raw, OpenApiSpecNormalizer.normalize(raw))
}
@Test
fun `fails when configured operation for renamed tag is absent`() {
val raw = """
{
"paths": {},
"tags": [
{ "name": "pty", "description": "PTY routes." },
{ "name": "pty", "description": "PTY WebSocket route." }
]
}
""".trimIndent()
val err = assertFailsWith<GradleException> {
OpenApiSpecNormalizer.normalize(raw)
}
assertTrue(err.message?.contains("Expected one OpenAPI operation 'pty.connect'") == true)
assertEquals(listOf("pty", "session"), tags, "duplicate pty tag should be removed")
}
private fun obj(raw: String) = Json.parseToJsonElement(raw) as JsonObject
+5 -1
View File
@@ -7,5 +7,9 @@
"typecheck": "./gradlew typecheck",
"test": "./gradlew test",
"test:ci": "bun script/test-ci.ts"
}
},
"version": "7.3.8",
"dependencies": {},
"devDependencies": {},
"peerDependencies": {}
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-telemetry",
"version": "7.3.4",
"version": "7.3.8",
"type": "module",
"license": "MIT",
"description": "Telemetry for Kilo CLI - PostHog analytics integration",
@@ -50,6 +50,8 @@ describe("TelemetryEvent", () => {
expect(TelemetryEvent.COMMAND_USED).toBeDefined()
expect(TelemetryEvent.TOOL_USED).toBeDefined()
expect(TelemetryEvent.AGENT_USED).toBeDefined()
expect(TelemetryEvent.SUGGESTION_SHOWN).toBeDefined()
expect(TelemetryEvent.SUGGESTION_ACCEPTED).toBeDefined()
})
test("indexing events are defined", () => {
@@ -88,4 +90,9 @@ describe("Telemetry", () => {
expect(typeof Telemetry.trackIndexingBatchRetry).toBe("function")
expect(typeof Telemetry.trackIndexingError).toBe("function")
})
test("suggestion helper is exposed", () => {
expect(typeof Telemetry.trackSuggestionShown).toBe("function")
expect(typeof Telemetry.trackSuggestionAccepted).toBe("function")
})
})
+2
View File
@@ -16,6 +16,8 @@ export enum TelemetryEvent {
TOOL_USED = "Tool Used",
AGENT_USED = "Agent Used",
PLAN_FOLLOWUP = "Plan Followup",
SUGGESTION_SHOWN = "Suggestion Shown",
SUGGESTION_ACCEPTED = "Suggestion Accepted",
// Code Indexing
INDEXING_STARTED = "Indexing Started",
+1 -1
View File
@@ -1,4 +1,4 @@
export { Telemetry } from "./telemetry.js"
export { TelemetryEvent } from "./events.js"
export { Identity } from "./identity.js"
export type { TelemetryProperties } from "./telemetry.js"
export type { ReviewCommand, TelemetryProperties } from "./telemetry.js"
+26 -1
View File
@@ -10,6 +10,8 @@ export interface TelemetryProperties {
vscodeVersion?: string
}
export type ReviewCommand = "review" | "local-review" | "local-review-uncommitted"
export interface IndexingTelemetryProperties extends Record<string, unknown> {
source: "scan" | "watcher"
provider: string
@@ -156,7 +158,8 @@ export namespace Telemetry {
taskId?: string
mode?: "review"
feature?: "code_reviews"
command?: "review" | "local-review" | "local-review-uncommitted"
command?: ReviewCommand
tool?: "suggest"
apiProvider: string
modelId: string
inputTokens?: number
@@ -187,6 +190,28 @@ export namespace Telemetry {
track(TelemetryEvent.PLAN_FOLLOWUP, { sessionId, choice })
}
export function trackSuggestionAccepted(properties: {
sessionId: string
requestId: string
index: number
tool: "suggest"
command: ReviewCommand
actionCount?: number
}) {
track(TelemetryEvent.SUGGESTION_ACCEPTED, properties)
}
export function trackSuggestionShown(properties: {
sessionId: string
requestId: string
index: number
tool: "suggest"
command: ReviewCommand
actionCount?: number
}) {
track(TelemetryEvent.SUGGESTION_SHOWN, properties)
}
export function trackIndexingStarted(properties: IndexingTelemetryProperties) {
track(TelemetryEvent.INDEXING_STARTED, properties)
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@kilocode/kilo-ui",
"version": "7.3.4",
"version": "7.3.8",
"type": "module",
"license": "MIT",
"exports": {
@@ -197,7 +197,9 @@
[data-slot="user-message-text"] [data-highlight="file"][data-clickable] {
cursor: pointer;
transition: background-color 0.15s, box-shadow 0.15s;
transition:
background-color 0.15s,
box-shadow 0.15s;
}
[data-slot="user-message-text"] [data-highlight="file"][data-clickable]:hover {
@@ -29,7 +29,9 @@ describe("reasoning heading", () => {
})
test("promotes HTML headings while flattening inline tags", () => {
expect(reasoningHeading('<h3 class="step">Check <em>provider</em> status</h3>\n\nContinue with the next item.')).toEqual({
expect(
reasoningHeading('<h3 class="step">Check <em>provider</em> status</h3>\n\nContinue with the next item.'),
).toEqual({
title: "Check provider status",
body: "Continue with the next item.",
})
@@ -280,8 +280,8 @@ html[data-theme="kilo-vscode"] {
--syntax-diff-unknown: var(--vscode-charts-red);
/* ===== Pierre diff engine (theme its CSS vars to VS Code editor colors) ===== */
--diffs-light-bg: var(--vscode-editor-background, #1e1e1e);
--diffs-dark-bg: var(--vscode-editor-background, #1e1e1e);
--diffs-light-bg: var(--background-stronger);
--diffs-dark-bg: var(--background-stronger);
--diffs-bg-addition-override: color-mix(
in lab,
+23
View File
@@ -1,5 +1,28 @@
# kilo-code
## 7.3.8
### Patch Changes
- [#10117](https://github.com/Kilo-Org/kilocode/pull/10117) [`8502012`](https://github.com/Kilo-Org/kilocode/commit/85020124b6a56acd093e0b1abf709b420ca863e1) Thanks [@shssoichiro](https://github.com/shssoichiro)! - Fix token usage display to show usage for entire session
## 7.3.7
## 7.3.5
### Patch Changes
- [#10481](https://github.com/Kilo-Org/kilocode/pull/10481) [`15bdd0f`](https://github.com/Kilo-Org/kilocode/commit/15bdd0f96f3f6713b552436806f6e218528443ab) - Prevent VS Code local CLI reconnect flapping while the event stream is unavailable.
- [#10479](https://github.com/Kilo-Org/kilocode/pull/10479) [`048338a`](https://github.com/Kilo-Org/kilocode/commit/048338a31422fd057bcaefc8db6c397cf33adff1) - Clear Agent Manager file diff previews stuck on `Loading diff...` when the active diff source stops or changes.
- [#10480](https://github.com/Kilo-Org/kilocode/pull/10480) [`9067de0`](https://github.com/Kilo-Org/kilocode/commit/9067de039ed820eab757d1e798abe264315c6f02) - Improve VS Code diff rendering stability across review and chat surfaces.
- Updated dependencies [[`205e22e`](https://github.com/Kilo-Org/kilocode/commit/205e22ee4672305d3cb2e0c34b607a4950f8f4e8), [`9067de0`](https://github.com/Kilo-Org/kilocode/commit/9067de039ed820eab757d1e798abe264315c6f02)]:
- @kilocode/kilo-indexing@7.3.5
- @kilocode/kilo-ui@7.3.5
- @opencode-ai/ui@7.3.5
## 7.3.3
### Minor Changes
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "kilo-code",
"displayName": "Kilo Code: AI Coding Agent, Copilot, and Autocomplete",
"description": "Open Source AI coding agent that generates code from natural language, automates tasks, and runs terminal commands. Features inline autocomplete, browser automation, automated refactoring, and custom modes for planning, coding, and debugging. Supports 500+ AI models including Claude (Anthropic), Gemini, Grok, GPT, Codex and GLM.",
"version": "7.3.4",
"version": "7.3.8",
"icon": "assets/icons/logo-outline-black.png",
"galleryBanner": {
"color": "#FFFFFF",
+9 -2
View File
@@ -23,6 +23,7 @@ const kiloVscodeDir = join(import.meta.dir, "..")
const packagesDir = join(kiloVscodeDir, "..")
const opencodeDir = join(packagesDir, "opencode")
const coreDir = join(packagesDir, "core")
const indexingDir = join(packagesDir, "kilo-indexing")
const targetBinDir = join(kiloVscodeDir, "bin")
const binName = process.platform === "win32" ? "kilo.exe" : "kilo"
@@ -37,7 +38,8 @@ async function cliSourceHash(): Promise<string | null> {
try {
const opencodeResult = await $`git log -1 --format=%H -- .`.cwd(opencodeDir).quiet()
const coreResult = await $`git log -1 --format=%H -- .`.cwd(coreDir).quiet()
return `${opencodeResult.text().trim()}-${coreResult.text().trim()}` || null
const indexingResult = await $`git log -1 --format=%H -- .`.cwd(indexingDir).quiet()
return `${opencodeResult.text().trim()}-${coreResult.text().trim()}-${indexingResult.text().trim()}` || null
} catch {
return null
}
@@ -47,7 +49,12 @@ async function isDirty(): Promise<boolean> {
try {
const opencodeResult = await $`git status --porcelain -- .`.cwd(opencodeDir).quiet()
const coreResult = await $`git status --porcelain -- .`.cwd(coreDir).quiet()
return opencodeResult.text().trim().length > 0 || coreResult.text().trim().length > 0
const indexingResult = await $`git status --porcelain -- .`.cwd(indexingDir).quiet()
return (
opencodeResult.text().trim().length > 0 ||
coreResult.text().trim().length > 0 ||
indexingResult.text().trim().length > 0
)
} catch {
return false
}
@@ -181,8 +181,12 @@ export class SourceController {
return
}
const diff = await source.fetchFile(file).catch(() => null)
// Drop the response if the source has been disposed/swapped while we waited.
if (this.epoch !== epoch) return
// Discard stale content after disposal/swap, but still complete the request
// so consumers can clear per-file loading state.
if (this.epoch !== epoch) {
this.send(this.messages.diffFile(source, file, null))
return
}
this.send(this.messages.diffFile(source, file, diff))
}
@@ -584,7 +584,8 @@ export class KiloConnectionService {
this.sseClient = new SdkSSEAdapter(this.client)
// Wait until SSE actually reaches a terminal state before resolving connect().
// Wait until SSE yields its first server event before resolving connect().
// Initial stream failures are handled by the adapter reconnect loop.
let resolveConnected: (() => void) | null = null
let rejectConnected: ((error: Error) => void) | null = null
const connectedPromise = new Promise<void>((resolve, reject) => {
@@ -601,11 +602,8 @@ export class KiloConnectionService {
}
})
this.sseClient.onError((error) => {
this.sseClient.onError(() => {
this.setState("error")
rejectConnected?.(error)
resolveConnected = null
rejectConnected = null
})
// Wire SSE state → broadcast to all registered state listeners
@@ -15,9 +15,8 @@ export type SSEStateHandler = (state: "connecting" | "connected" | "disconnected
*
* In this VS Code extension context the connection is localhost (extension ↔
* child-process server), so zombie-connection scenarios are less likely than in
* a browser client (which goes through proxies/CDNs). We keep the heartbeat for
* consistency with the original strategy but use a generous 90 s timeout to
* avoid false-positive reconnections during idle periods.
* a browser client (which goes through proxies/CDNs). We still keep a heartbeat
* grace window so dead local streams recover without waiting indefinitely.
*
* NOTE on event coalescing:
* The app batches rapid events into 16 ms windows before flushing to the UI.
@@ -37,6 +36,7 @@ export class SdkSSEAdapter {
// Reduced from 90s: with 90s a dead connection could linger for ~1.5 minutes.
private static readonly HEARTBEAT_TIMEOUT_MS = 15_000
private static readonly RECONNECT_DELAY_MS = 250
private static readonly MAX_RECONNECT_DELAY_MS = 5_000
constructor(private readonly client: KiloClient) {}
@@ -126,8 +126,11 @@ export class SdkSSEAdapter {
* Main reconnection loop — mirrors the pattern in `global-sdk.tsx`.
*/
private async consumeLoop(signal: AbortSignal): Promise<void> {
let delay = SdkSSEAdapter.RECONNECT_DELAY_MS
while (!signal.aborted) {
const attempt = new AbortController()
let ready = false
// Forward the outer abort to the per-attempt controller so
// `disconnect()` cancels the current fetch immediately.
@@ -159,8 +162,7 @@ export class SdkSSEAdapter {
},
})
console.log("[Kilo New] SSE: ✅ Stream opened successfully")
this.notifyState("connected")
console.log("[Kilo New] SSE: ⏳ Waiting for first stream event")
this.resetHeartbeat(attempt)
for await (const event of events.stream) {
@@ -170,6 +172,13 @@ export class SdkSSEAdapter {
this.resetHeartbeat(attempt)
if (!ready) {
ready = true
delay = SdkSSEAdapter.RECONNECT_DELAY_MS
console.log("[Kilo New] SSE: ✅ Stream opened successfully")
this.notifyState("connected")
}
// The SDK yields GlobalEvent = { directory, payload: Event }.
const globalEvent = event as GlobalEvent
const type = (globalEvent.payload as { type: string }).type
@@ -179,7 +188,9 @@ export class SdkSSEAdapter {
this.notifyEvent(globalEvent.payload as Event, globalEvent.directory)
}
console.log("[Kilo New] SSE: 📭 Stream ended normally")
console.log(
ready ? "[Kilo New] SSE: 📭 Stream ended normally" : "[Kilo New] SSE: 📭 Stream ended before first event",
)
} catch (error) {
// Suppress AbortErrors — they are expected when the heartbeat timer
// or reconnect() aborts the per-attempt controller.
@@ -198,9 +209,11 @@ export class SdkSSEAdapter {
break
}
console.log(`[Kilo New] SSE: 🔄 Reconnecting in ${SdkSSEAdapter.RECONNECT_DELAY_MS}ms...`)
const wait = delay
delay = ready ? SdkSSEAdapter.RECONNECT_DELAY_MS : Math.min(delay * 2, SdkSSEAdapter.MAX_RECONNECT_DELAY_MS)
console.log(`[Kilo New] SSE: 🔄 Reconnecting in ${wait}ms...`)
this.notifyState("connecting")
await new Promise((resolve) => setTimeout(resolve, SdkSSEAdapter.RECONNECT_DELAY_MS))
await new Promise((resolve) => setTimeout(resolve, wait))
}
this.notifyState("disconnected")
+1 -1
View File
@@ -1,6 +1,6 @@
{
"type": "module",
"version": "7.3.4",
"version": "7.3.8",
"dependencies": {},
"devDependencies": {},
"peerDependencies": {}
@@ -0,0 +1,148 @@
import { describe, expect, it } from "bun:test"
import type { KiloClient } from "@kilocode/sdk/v2/client"
import { KiloConnectionService } from "../../src/services/cli-backend/connection-service"
import { SdkSSEAdapter } from "../../src/services/cli-backend/sdk-sse-adapter"
type Opts = {
onSseError?: (error: unknown) => void
signal?: AbortSignal
}
type Stream = AsyncGenerator<unknown, void, unknown>
function client(open: (opts: Opts) => Stream): KiloClient {
return {
global: {
event: async (opts: Opts) => ({ stream: open(opts) }),
},
} as unknown as KiloClient
}
function event() {
return {
directory: "/repo",
payload: {
id: "evt_connected",
type: "server.connected",
properties: {},
},
}
}
function wait(ms: number) {
return new Promise<void>((resolve) => setTimeout(resolve, ms))
}
function aborted(signal?: AbortSignal) {
if (!signal || signal.aborted) return Promise.resolve()
return new Promise<void>((resolve) => signal.addEventListener("abort", () => resolve(), { once: true }))
}
describe("SdkSSEAdapter", () => {
it("reports connected only after the first SSE event arrives", async () => {
let release = () => {}
const gate = new Promise<void>((resolve) => {
release = resolve
})
const adapter = new SdkSSEAdapter(
client(async function* (opts) {
await gate
yield event()
await aborted(opts.signal)
}),
)
const states: string[] = []
const connected = new Promise<void>((resolve) => {
adapter.onStateChange((state) => {
states.push(state)
if (state === "connected") resolve()
})
})
adapter.connect()
await wait(10)
expect(states).toEqual(["connecting"])
release()
await connected
expect(states).toEqual(["connecting", "connected"])
adapter.disconnect()
})
it("backs off reconnects when an SSE fetch fails before opening", async () => {
const timer = globalThis.setTimeout
const delays: number[] = []
let count = 0
let finish = () => {}
const reached = new Promise<void>((resolve) => {
finish = resolve
})
globalThis.setTimeout = ((handler: TimerHandler, timeout?: number, ...args: unknown[]) => {
if (typeof timeout === "number" && timeout <= 5_000) {
delays.push(timeout)
return timer(handler, 0, ...args)
}
return timer(handler, timeout, ...args)
}) as typeof setTimeout
const failing = new SdkSSEAdapter(
client((opts) => {
count += 1
if (count === 3) finish()
return (async function* () {
opts.onSseError?.(new TypeError("fetch failed"))
})()
}),
)
try {
failing.connect()
await reached
failing.disconnect()
expect(delays.slice(0, 2)).toEqual([250, 500])
} finally {
failing.disconnect()
globalThis.setTimeout = timer
}
})
})
describe("KiloConnectionService SSE startup", () => {
it("waits through an initial SSE fetch failure until the stream opens", async () => {
const original = globalThis.fetch
const chunk = new TextEncoder().encode(
'data: {"payload":{"id":"evt_connected","type":"server.connected","properties":{}}}\n\n',
)
let calls = 0
globalThis.fetch = (async () => {
calls += 1
if (calls === 1) throw new TypeError("fetch failed")
return new Response(
new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(chunk)
},
}),
{
status: 200,
headers: { "content-type": "text/event-stream" },
},
)
}) as typeof fetch
const service = new KiloConnectionService({} as any)
;(service as any).serverManager.getServer = async () => ({ port: 52512, password: "secret", process: {} })
try {
await expect(service.connect("/tmp/workspace")).resolves.toBeUndefined()
expect(calls).toBe(2)
expect(service.getConnectionState()).toBe("connected")
} finally {
service.dispose()
globalThis.fetch = original
}
})
})
@@ -3,6 +3,7 @@ import {
computeStatus,
calcTotalCost,
calcContextUsage,
calcTokenUsage,
buildFamilyCosts,
buildFamilyParents,
buildFamilyLabels,
@@ -141,6 +142,35 @@ describe("calcContextUsage", () => {
})
})
describe("calcTokenUsage", () => {
it("sums assistant message input, output, and cache read tokens", () => {
const result = calcTokenUsage([
{ role: "assistant", tokens: { input: 100, output: 40, reasoning: 8, cache: { read: 10, write: 5 } } },
{ role: "assistant", tokens: { input: 25, output: 15, cache: { read: 7, write: 3 } } },
])
expect(result).toEqual({ input: 125, output: 55, cached: 17 })
})
it("ignores user messages, missing tokens, reasoning tokens, and cache writes", () => {
const result = calcTokenUsage([
{ role: "user", tokens: { input: 999, output: 999, cache: { read: 999, write: 999 } } },
{ role: "assistant" },
{ role: "assistant", tokens: { input: 10, output: 4, reasoning: 30, cache: { read: 2, write: 20 } } },
])
expect(result).toEqual({ input: 10, output: 4, cached: 2 })
})
it("returns undefined when there are no displayed token counts", () => {
const result = calcTokenUsage([
{ role: "assistant", tokens: { input: 0, output: 0, reasoning: 12, cache: { read: 0, write: 6 } } },
])
expect(result).toBeUndefined()
})
})
// ── Cost breakdown helpers ──────────────────────────────────────────────
function msg(id: string, role: string, cost?: number) {
@@ -345,6 +345,40 @@ describe("SourceController.requestFile", () => {
controller.stop()
})
it("posts null when a pending fetchFile result is invalidated by stop", async () => {
let release: () => void = () => {}
const workspace: DiffSource = {
descriptor: WORKSPACE_DESC,
async fetch() {
return { diffs: [] }
},
async fetchFile() {
await new Promise<void>((resolve) => (release = resolve))
return {
file: "foo.ts",
before: "a",
after: "b",
additions: 1,
deletions: 1,
}
},
}
const { controller, posted } = make({ workspace })
controller.setContext({ workspaceRoot: "/repo" })
await controller.activate("workspace")
posted.length = 0
const request = controller.requestFile("foo.ts")
controller.stop()
release()
await request
const files = byType(posted, "diffViewer.diffFile")
expect(files).toHaveLength(1)
expect(files[0]!.file).toBe("foo.ts")
expect(files[0]!.diff).toBeNull()
})
})
describe("SourceController.reactivate", () => {
@@ -14,7 +14,7 @@ import { Tooltip } from "@kilocode/kilo-ui/tooltip"
import { Icon } from "@kilocode/kilo-ui/icon"
import { Checkbox } from "@kilocode/kilo-ui/checkbox"
import { useSession } from "../../context/session"
import { collapseCostBreakdown } from "../../context/session-utils"
import { calcTokenUsage, collapseCostBreakdown } from "../../context/session-utils"
import { useLanguage } from "../../context/language"
import { useVSCode } from "../../context/vscode"
import { TaskTimeline } from "./TaskTimeline"
@@ -66,18 +66,7 @@ export const TaskHeader: Component<TaskHeaderProps> = (props) => {
return { tokens, pct }
})
// Token breakdown from the last assistant message — only return if at least one value is > 0
const tokens = createMemo(() => {
const msgs = session.visibleMessages()
for (let i = msgs.length - 1; i >= 0; i--) {
const m = msgs[i]
if (m.role !== "assistant" || !m.tokens) continue
const tk = m.tokens
const has = tk.input > 0 || tk.output > 0 || (tk.cache?.write ?? 0) > 0 || (tk.cache?.read ?? 0) > 0
if (has) return tk
}
return undefined
})
const tokens = createMemo(() => calcTokenUsage(session.visibleMessages()))
const hasTimeline = createMemo(() => {
for (const m of session.visibleMessages()) {
@@ -207,16 +196,10 @@ export const TaskHeader: Component<TaskHeaderProps> = (props) => {
{fmtNum(tk().output)}
</span>
</Show>
<Show when={tk().cache?.write && tk().cache!.write > 0}>
<span class="task-header-tokens-value">
<Icon name="arrow-up" size="small" />
cache {fmtNum(tk().cache!.write)}
</span>
</Show>
<Show when={tk().cache?.read && tk().cache!.read > 0}>
<Show when={tk().cached > 0}>
<span class="task-header-tokens-value">
<Icon name="arrow-down-to-line" size="small" />
cache {fmtNum(tk().cache!.read)}
cache {fmtNum(tk().cached)}
</span>
</Show>
</div>
@@ -98,6 +98,35 @@ export function calcContextUsage(
return { tokens: total, percentage }
}
export type TokenUsageMessage = {
role: string
tokens?: {
input: number
output: number
reasoning?: number
cache?: { read: number; write: number }
}
}
export function calcTokenUsage(
messages: TokenUsageMessage[],
): { input: number; output: number; cached: number } | undefined {
const total = messages.reduce(
(sum, m) => {
if (m.role !== "assistant" || !m.tokens) return sum
return {
input: sum.input + m.tokens.input,
output: sum.output + m.tokens.output,
cached: sum.cached + (m.tokens.cache?.read ?? 0),
}
},
{ input: 0, output: 0, cached: 0 },
)
if (total.input > 0 || total.output > 0 || total.cached > 0) return total
return undefined
}
/**
* Build a map of session ID → **own cost** for each session in the family
* that has non-zero own cost.
+21
View File
@@ -1,5 +1,26 @@
# @kilocode/cli
## 7.3.8
### Patch Changes
- [#8403](https://github.com/Kilo-Org/kilocode/pull/8403) [`42844e5`](https://github.com/Kilo-Org/kilocode/commit/42844e505475650c16f92251421ad792c6429184) Thanks [@saschabuehrle](https://github.com/saschabuehrle)! - Accept `env` as an alias for `environment` in local MCP server configuration. Configurations using the more common `env` key (matching Docker, npm, and VS Code conventions) are now normalised on load instead of failing strict validation.
- [#10495](https://github.com/Kilo-Org/kilocode/pull/10495) [`ae0fbe8`](https://github.com/Kilo-Org/kilocode/commit/ae0fbe89dc5859fcea3c5d1e459a77eb459a8f71) - Show recent and favorited models in provider-specific model lists.
## 7.3.7
### Patch Changes
- [#10297](https://github.com/Kilo-Org/kilocode/pull/10297) [`74e8604`](https://github.com/Kilo-Org/kilocode/commit/74e860431f3f9fcbfcea764711b8c1487d9a8f8d) Thanks [@IamCoder18](https://github.com/IamCoder18)! - Vertically center TUI dialogs on screen
## 7.3.5
### Patch Changes
- Updated dependencies [[`205e22e`](https://github.com/Kilo-Org/kilocode/commit/205e22ee4672305d3cb2e0c34b607a4950f8f4e8)]:
- @kilocode/kilo-indexing@7.3.5
## 7.3.3
### Patch Changes
@@ -0,0 +1 @@
ALTER TABLE `event_sequence` ADD `owner_id` text;
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "7.3.4",
"version": "7.3.8",
"name": "@kilocode/cli",
"type": "module",
"license": "MIT",
@@ -83,7 +83,7 @@
"dependencies": {
"@actions/core": "1.11.1",
"@actions/github": "6.0.1",
"@agentclientprotocol/sdk": "0.16.1",
"@agentclientprotocol/sdk": "0.21.0",
"@ai-sdk/alibaba": "1.0.17",
"@ai-sdk/amazon-bedrock": "4.0.96",
"@ai-sdk/anthropic": "3.0.71",
+2 -2
View File
@@ -776,9 +776,9 @@ const scenarios: Scenario[] = [
}))
.status(200),
http
.post("/experimental/workspace/{id}/session-restore", "experimental.workspace.sessionRestore")
.post("/experimental/workspace/warp", "experimental.workspace.warp")
.at((ctx) => ({
path: route("/experimental/workspace/{id}/session-restore", { id: "wrk_httpapi_missing" }),
path: "/experimental/workspace/warp",
headers: ctx.headers(),
body: {},
}))
+329
View File
@@ -0,0 +1,329 @@
# Typed error migration
Plan for moving `packages/opencode` from temporary defect/`NamedError`
compatibility toward typed Effect service errors and explicit HTTP error
contracts.
## Goal
- Expected service failures live on the Effect error channel.
- Service interfaces expose those failures in their return types.
- Domain errors are authored with Effect Schema so they are reusable by services,
tests, HTTP routes, tools, and OpenAPI generation.
- HTTP status codes and wire compatibility are handled at the HTTP boundary, not
inside service modules.
- `Effect.die`, `throw`, `catchDefect`, and global cause inspection are reserved
for defects, compatibility bridges, or final fallback behavior.
## Current State
- Many migrated services use Effect internally, but expected failures are still a
mix of `NamedError.create(...)`, `namedSchemaError(...)`, `class extends Error`,
`throw`, and `Effect.die(...)`.
- Some services already use `Schema.TaggedErrorClass`, for example `Account`,
`Auth`, `Permission`, `Question`, `Installation`, and parts of
`Workspace`.
- Legacy Hono error handling recognizes `NamedError`, `Session.BusyError`, and a
few name-based cases, then emits the legacy `{ name, data }` JSON body.
- Effect `HttpApi` only knows how to encode errors that are declared on the
endpoint, group, or middleware. Undeclared expected errors become defects and
eventually fall through to generic HTTP handling.
- The temporary HttpApi error middleware catches defect-wrapped legacy errors to
preserve runtime behavior, but it is intentionally a bridge rather than the
final model.
## End State
Service modules own domain failures.
```ts
export class SessionBusyError extends Schema.TaggedErrorClass<SessionBusyError>()("SessionBusyError", {
sessionID: SessionID,
message: Schema.String,
}) {}
export type Error = Storage.Error | SessionBusyError
export interface Interface {
readonly get: (id: SessionID) => Effect.Effect<Info, Error>
}
```
HTTP modules own transport mapping.
```ts
const get = Effect.fn("SessionHttpApi.get")(function* (ctx: { params: { sessionID: SessionID } }) {
return yield* session
.get(ctx.params.sessionID)
.pipe(
Effect.catchTag("StorageNotFoundError", () => new SessionNotFoundHttpError({ sessionID: ctx.params.sessionID })),
)
})
```
HTTP-visible error schemas carry their own response status through Effect
HttpApi's `httpApiStatus` annotation. Prefer `HttpApiSchema.status(...)`, or the
equivalent declaration annotation, instead of maintaining a parallel status map.
```ts
export class SessionNotFoundHttpError extends Schema.TaggedErrorClass<SessionNotFoundHttpError>()(
"SessionNotFoundHttpError",
{
sessionID: SessionID,
message: Schema.String,
},
{ httpApiStatus: 404 },
) {}
```
Endpoint definitions still declare which HTTP-visible error schemas can be
emitted. The status annotation is only used if the error is part of the endpoint,
group, or middleware error schema and the handler fails with that error on the
typed error channel.
```ts
HttpApiEndpoint.get("get", SessionPaths.get, {
success: Session.Info,
error: [SessionNotFoundHttpError, SessionBusyHttpError],
})
```
The service error and HTTP error may be the same class when the wire shape is a
deliberate public contract. They should be different classes when the service
error contains internals, low-level causes, retry hints, or anything that should
not be exposed to API clients.
## Rules
- Use `Schema.TaggedErrorClass` for new expected domain errors.
- Include `cause: Schema.optional(Schema.Defect)` only when preserving an
underlying unknown failure is useful for logs or callers.
- Export a domain-level error union from each service module, for example
`export type Error = NotFoundError | BusyError | Storage.Error`.
- Put expected errors in service method signatures, for example
`Effect.Effect<Result, Service.Error, R>`.
- Use `yield* new DomainError(...)` for direct early failures inside
`Effect.gen` / `Effect.fn`.
- Use `Effect.try({ try, catch })`, `Effect.mapError`, or `Effect.catchTag` to
convert external exceptions into domain errors.
- Use `HttpApiSchema.status(...)` or `{ httpApiStatus: code }` on HTTP-visible
error schemas so Effect `HttpApiBuilder` and OpenAPI generation get the status
from the schema itself.
- Do not use `Effect.die(...)` for user, IO, validation, missing-resource, auth,
provider, worktree, or busy-state failures.
- Do not use `catchDefect` to recover expected domain errors. If recovery is
needed, the upstream effect should fail with a typed error instead.
- Do not make service modules import `HttpApiError`, `HttpServerResponse`, HTTP
status codes, or route-specific error schemas.
- Keep raw `HttpRouter` routes free to use `HttpServerRespondable` when that is
the right transport abstraction, but prefer declared `HttpApi` errors for
normal JSON API endpoints.
## HTTP Boundary Shape
Create an HttpApi-local error module, likely
`src/server/routes/instance/httpapi/errors.ts`.
That module should provide:
- Legacy-compatible public schemas for `{ name, data }` error bodies that must
remain SDK-compatible during the Hono migration.
- Small constructors or mapping helpers for common API errors such as not found,
bad request, conflict, and unknown internal errors.
- Route-group-specific adapters only when they encode domain-specific public
data.
- A single place to document which public error shape is legacy-compatible and
which shape is new Effect-native API surface.
Avoid one giant `unknown -> status` mapper. Prefer small, explicit mappers close
to the handler or route group.
```ts
const mapSessionError = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
effect.pipe(
Effect.catchTag("StorageNotFoundError", (error) => new SessionNotFoundHttpError({ message: error.message })),
Effect.catchTag("SessionBusyError", (error) => new SessionBusyHttpError({ message: error.message })),
)
```
Use built-in `HttpApiError.BadRequest`, `HttpApiError.NotFound`, and related
types only when their generated response body and SDK surface are intentionally
acceptable. Use a custom schema-backed error when clients need the legacy
`{ name, data }` body or a domain-specific error payload.
## Migration Phases
### 1. Stabilize The Bridge
Keep the temporary HttpApi error middleware only as a compatibility bridge while
typed errors are introduced.
- Add tests that prove the bridge catches legacy `NamedError` defects.
- Add tests that prove declared HttpApi errors still use the declared endpoint
contract.
- Stop returning stack traces in unknown HTTP `500` responses; log the full
`Cause.pretty(cause)` server-side instead.
- Add a comment or TODO that names this plan and states the bridge must shrink
as route groups migrate.
### 2. Define The Shared HTTP Error Helpers
Add the `httpapi/errors.ts` module before converting route groups.
- Define a legacy `{ name, data }` body helper for SDK-compatible errors.
- Define `UnknownError` for generic internal failures with a safe public message.
- Define `BadRequestError` and `NotFoundError` equivalents only if the actual
wire body must match the legacy Hono SDK surface.
- Put the HTTP status on the public schema with `HttpApiSchema.status(...)` or
`{ httpApiStatus: code }`; do not keep a separate name-to-status table.
- Keep conversion helpers pure and small. They should not inspect `Cause` or
accept `unknown` unless they are final fallback helpers.
### 3. Convert One Vertical Slice
Start with session read routes because they already have local `mapNotFound`
logic and are heavily covered by existing HttpApi tests.
- Convert `Session.BusyError` from a plain `Error` to a typed service error, or
add a typed wrapper while preserving the old constructor until callers are
migrated.
- Replace `catchDefect` in `httpapi/handlers/session.ts` with typed error
mapping.
- Add endpoint error schemas for the affected session endpoints.
- Prove behavior with focused tests in `test/server/httpapi-session.test.ts`.
- Remove the migrated cases from the global compatibility middleware.
### 4. Convert Legacy NamedError Domains
Move legacy `NamedError.create(...)` services to Effect Schema-backed errors in
small domain PRs.
Priority order:
1. `storage/storage.ts` and `storage/db.ts` not-found errors.
2. `worktree/index.ts` `Worktree*` errors.
3. `provider/auth.ts` validation failures and `provider/provider.ts` model-not-found errors.
4. `mcp/index.ts`, `skill/index.ts`, `lsp/client.ts`, and `ide/index.ts` service errors.
5. Config and CLI-only errors after HTTP-facing domains are stable.
For each domain:
- Replace `NamedError.create(...)` with `Schema.TaggedErrorClass` when the error
is primarily a service error.
- Keep or add a separate HTTP error schema when the legacy `{ name, data }` wire
shape must remain stable.
- Update service interface return types to include the new error union.
- Replace `throw new X(...)` inside `Effect.fn` with `yield* new X(...)`.
- Replace async exceptions with `Effect.try({ catch })` or explicit `mapError`.
- Add service-level tests that assert the error tag and data, not just the HTTP
status.
### 5. Declare HttpApi Errors Group By Group
For each HttpApi group:
- Inventory every service call and the typed errors it can return.
- Add only the public error schemas that endpoint can actually emit.
- Map service errors to HTTP errors in the handler file.
- Keep built-in `HttpApiError` only for generic request/validation failures where
the generated contract is accepted.
- Update `httpapi/public.ts` compatibility transforms only when the generated
spec cannot represent the desired source shape directly.
- Regenerate the SDK after OpenAPI-visible changes and verify the diff is
intentional.
Suggested route order:
1. `session` not-found and busy-state reads.
2. `experimental` worktree mutations.
3. `provider` auth and model selection errors.
4. `mcp` OAuth and connection errors.
5. Remaining route groups as Hono deletion work progresses.
### 6. Remove Defect Recovery
After enough route groups declare their expected errors:
- Delete `catchDefect` recovery for domain errors.
- Delete name-prefix checks such as `error.name.startsWith("Worktree")` from
HTTP middleware.
- Delete `NamedError` branches from the Effect HttpApi compatibility middleware
once no Effect route depends on them.
- Leave one final unknown-defect fallback that logs server-side and returns a
safe generic `500` body.
## Inventory Checklist
Use this checklist when touching a service or route group.
- [ ] Does the service interface expose every expected failure in the Effect
error type?
- [ ] Are user-caused, provider-caused, IO, auth, missing-resource, and busy-state
failures modeled as typed errors instead of defects?
- [ ] Does the service avoid importing HTTP status, `HttpApiError`, or response
classes?
- [ ] Does the handler map each service error into a declared endpoint error?
- [ ] Does the endpoint `error` field include every public error the handler can
emit?
- [ ] Does OpenAPI/SDK output either stay byte-identical or have an explicitly
reviewed diff?
- [ ] Do tests cover both service-level error typing and HTTP-level status/body?
- [ ] Did the PR remove any now-unneeded case from the temporary compatibility
middleware?
## Testing Requirements
For service conversions:
- Test the service method directly with `testEffect(...)`.
- Assert on `_tag` or class identity and the structured fields.
- Avoid testing by string-matching `Cause.pretty(...)`.
For HttpApi conversions:
- Add or update the focused `test/server/httpapi-*.test.ts` file.
- Assert status code, content type, and exact JSON body for declared public
errors.
- Add a regression test that the temporary middleware is no longer needed for the
migrated route.
- Keep bridge/parity tests aligned with legacy Hono behavior until Hono is
deleted or the SDK contract intentionally changes.
## Verification Commands
Run from `packages/opencode` unless noted otherwise.
```bash
bun run prettier --write <changed files>
bunx oxlint <changed files>
bun typecheck
bun run test -- test/server/httpapi-session.test.ts
```
Run SDK generation from the repo root when schemas or OpenAPI-visible errors
change.
```bash
./packages/sdk/js/script/build.ts
```
## Open Questions
- Should legacy V1 routes keep `{ name, data }` forever while V2 routes expose a
more Effect-native tagged error body?
- Should storage not-found remain generic, or should callers map it to
domain-specific not-found errors before crossing service boundaries?
- Should `namedSchemaError(...)` stay as a long-term public-wire helper, or only
as a migration bridge for old `NamedError` contracts?
- Which SDK version boundary lets us stop remapping built-in Effect HttpApi error
schemas in `httpapi/public.ts`?
## Success Criteria
- New service code no longer uses `die` for expected failures.
- A route reviewer can read an endpoint definition and see every public error it
can return.
- The temporary HttpApi error middleware shrinks over time instead of gaining new
name-based cases.
- Service tests prove domain error types without going through HTTP.
- HTTP tests prove status/body contracts without relying on defect recovery.
+188 -69
View File
@@ -5,6 +5,8 @@ import {
type AuthenticateRequest,
type AuthMethod,
type CancelNotification,
type CloseSessionRequest,
type CloseSessionResponse,
type ForkSessionRequest,
type ForkSessionResponse,
type InitializeRequest,
@@ -51,9 +53,9 @@ import { LoadAPIKeyError } from "ai"
import type { AssistantMessage, Event, KiloClient, SessionMessageResponse, ToolPart } from "@kilocode/sdk/v2"
import { applyPatch } from "diff"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { ShellID } from "@/tool/shell/id"
import { fetchDefaultModel } from "@kilocode/kilo-gateway" // kilocode_change
import { ShellID } from "@/tool/shell/id"
type ModeOption = { id: string; name: string; description?: string }
type ModelOption = { modelId: string; name: string }
@@ -569,6 +571,7 @@ export class Agent implements ACPAgent {
image: true,
},
sessionCapabilities: {
close: {},
fork: {},
list: {},
resume: {},
@@ -631,6 +634,9 @@ export class Agent implements ACPAgent {
// Store ACP session state
await this.sessionManager.load(sessionId, params.cwd, params.mcpServers, model)
const messages = await this.loadSessionMessages(directory, sessionId)
this.restoreSessionStateFromMessages(sessionId, messages)
log.info("load_session", { sessionId, mcpServers: params.mcpServers.length })
const result = await this.loadSessionMode({
@@ -639,39 +645,6 @@ export class Agent implements ACPAgent {
sessionId,
})
// Replay session history
const messages = await this.sdk.session
.messages(
{
sessionID: sessionId,
directory,
},
{ throwOnError: true },
)
.then((x) => x.data)
.catch((err) => {
log.error("unexpected error when fetching message", { error: err })
return undefined
})
const lastUser = messages?.findLast((m) => m.info.role === "user")?.info
if (lastUser?.role === "user") {
result.models.currentModelId = `${lastUser.model.providerID}/${lastUser.model.modelID}`
this.sessionManager.setModel(sessionId, {
providerID: ProviderID.make(lastUser.model.providerID),
modelID: ModelID.make(lastUser.model.modelID),
})
if (result.modes?.availableModes.some((m) => m.id === lastUser.agent)) {
result.modes.currentModeId = lastUser.agent
this.sessionManager.setMode(sessionId, lastUser.agent)
}
result.configOptions = buildConfigOptions({
currentModelId: result.models.currentModelId,
availableModels: result.models.availableModels,
modes: result.modes,
})
}
for (const msg of messages ?? []) {
log.debug("replay message", msg)
await this.processMessage(msg)
@@ -760,6 +733,9 @@ export class Agent implements ACPAgent {
const sessionId = forked.id
await this.sessionManager.load(sessionId, directory, mcpServers, model)
const messages = await this.loadSessionMessages(directory, sessionId)
this.restoreSessionStateFromMessages(sessionId, messages)
log.info("fork_session", { sessionId, mcpServers: mcpServers.length })
const mode = await this.loadSessionMode({
@@ -768,20 +744,6 @@ export class Agent implements ACPAgent {
sessionId,
})
const messages = await this.sdk.session
.messages(
{
sessionID: sessionId,
directory,
},
{ throwOnError: true },
)
.then((x) => x.data)
.catch((err) => {
log.error("unexpected error when fetching message", { error: err })
return undefined
})
for (const msg of messages ?? []) {
log.debug("replay message", msg)
await this.processMessage(msg)
@@ -801,7 +763,7 @@ export class Agent implements ACPAgent {
}
}
async unstable_resumeSession(params: ResumeSessionRequest): Promise<ResumeSessionResponse> {
async resumeSession(params: ResumeSessionRequest): Promise<ResumeSessionResponse> {
const directory = params.cwd
const sessionId = params.sessionId
const mcpServers = params.mcpServers ?? []
@@ -810,6 +772,9 @@ export class Agent implements ACPAgent {
const model = await defaultModel(this.config, directory)
await this.sessionManager.load(sessionId, directory, mcpServers, model)
const messages = await this.loadSessionMessages(directory, sessionId, 20)
this.restoreSessionStateFromMessages(sessionId, messages)
log.info("resume_session", { sessionId, mcpServers: mcpServers.length })
const result = await this.loadSessionMode({
@@ -832,6 +797,27 @@ export class Agent implements ACPAgent {
}
}
async closeSession(params: CloseSessionRequest): Promise<CloseSessionResponse> {
const session = this.sessionManager.remove(params.sessionId)
if (!session) return {}
await this.sdk.session
.abort(
{
sessionID: params.sessionId,
directory: session.cwd,
},
{ throwOnError: true },
)
.catch((error) => {
log.error("failed to abort session while closing ACP session", { error, sessionID: params.sessionId })
})
this.permissionQueues.delete(params.sessionId)
log.info("close_session", { sessionId: params.sessionId })
return {}
}
private async processMessage(message: SessionMessageResponse) {
log.debug("process message", message)
if (message.info.role !== "assistant" && message.info.role !== "user") return
@@ -1163,23 +1149,26 @@ export class Agent implements ACPAgent {
sessionId: string,
): Promise<{ availableModes: ModeOption[]; currentModeId?: string }> {
const availableModes = await this.loadAvailableModes(directory)
const currentModeId =
this.sessionManager.get(sessionId).modeId ||
(await (async () => {
if (!availableModes.length) return undefined
const defaultAgentName = await AppRuntime.runPromise(AgentModule.Service.use((svc) => svc.defaultAgent()))
const resolvedModeId = availableModes.find((mode) => mode.name === defaultAgentName)?.id ?? availableModes[0].id
this.sessionManager.setMode(sessionId, resolvedModeId)
return resolvedModeId
})())
const storedModeId = this.sessionManager.get(sessionId).modeId
if (storedModeId && availableModes.some((mode) => mode.id === storedModeId)) {
return { availableModes, currentModeId: storedModeId }
}
const currentModeId = await (async () => {
if (!availableModes.length) return undefined
const defaultAgentName = await AppRuntime.runPromise(AgentModule.Service.use((svc) => svc.defaultAgent()))
const resolvedModeId = availableModes.find((mode) => mode.name === defaultAgentName)?.id ?? availableModes[0].id
this.sessionManager.setMode(sessionId, resolvedModeId)
return resolvedModeId
})()
return { availableModes, currentModeId }
}
private async loadSessionMode(params: LoadSessionRequest) {
const directory = params.cwd
const model = await defaultModel(this.config, directory)
const sessionId = params.sessionId
const model = this.sessionManager.get(sessionId).model ?? (await defaultModel(this.config, directory))
const providers = await this.sdk.config.providers({ directory }).then((x) => x.data!.providers)
const entries = sortProvidersByName(providers)
@@ -1188,7 +1177,7 @@ export class Agent implements ACPAgent {
if (currentVariant && !availableVariants.includes(currentVariant)) {
this.sessionManager.setVariant(sessionId, undefined)
}
const availableModels = buildAvailableModels(entries, { includeVariants: true })
const availableModels = buildAvailableModels(entries)
const modeState = await this.resolveModeState(directory, sessionId)
const currentModeId = modeState.currentModeId
const modes = currentModeId
@@ -1271,13 +1260,15 @@ export class Agent implements ACPAgent {
return {
sessionId,
models: {
currentModelId: formatModelIdWithVariant(model, currentVariant, availableVariants, true),
currentModelId: formatModelIdWithVariant(model, currentVariant, availableVariants, false),
availableModels,
},
modes,
configOptions: buildConfigOptions({
currentModelId: formatModelIdWithVariant(model, currentVariant, availableVariants, true),
currentModelId: formatModelIdWithVariant(model, currentVariant, availableVariants, false),
availableModels,
currentVariant,
availableVariants,
modes,
}),
_meta: buildVariantMeta({
@@ -1300,6 +1291,24 @@ export class Agent implements ACPAgent {
const entries = sortProvidersByName(providers)
const availableVariants = modelVariantsFromProviders(entries, selection.model)
const modeState = await this.resolveModeState(session.cwd, session.id)
const modes = modeState.currentModeId
? { availableModes: modeState.availableModes, currentModeId: modeState.currentModeId }
: undefined
await this.connection.sessionUpdate({
sessionId: session.id,
update: {
sessionUpdate: "config_option_update",
configOptions: buildConfigOptions({
currentModelId: formatModelIdWithVariant(selection.model, selection.variant, availableVariants, false),
availableModels: buildAvailableModels(entries),
currentVariant: selection.variant,
availableVariants,
modes,
}),
},
})
return {
_meta: buildVariantMeta({
@@ -1331,6 +1340,14 @@ export class Agent implements ACPAgent {
const selection = parseModelSelection(params.value, providers)
this.sessionManager.setModel(session.id, selection.model)
this.sessionManager.setVariant(session.id, selection.variant)
} else if (params.configId === "effort") {
if (typeof params.value !== "string") throw RequestError.invalidParams("effort value must be a string")
const current = session.model ?? (await defaultModel(this.config, session.cwd))
const availableVariants = modelVariantsFromProviders(entries, current)
if (!availableVariants.includes(params.value)) {
throw RequestError.invalidParams(JSON.stringify({ error: `Effort not found: ${params.value}` }))
}
this.sessionManager.setVariant(session.id, params.value)
} else if (params.configId === "mode") {
if (typeof params.value !== "string") throw RequestError.invalidParams("mode value must be a string")
const availableModes = await this.loadAvailableModes(session.cwd)
@@ -1345,15 +1362,21 @@ export class Agent implements ACPAgent {
const updatedSession = this.sessionManager.get(session.id)
const model = updatedSession.model ?? (await defaultModel(this.config, session.cwd))
const availableVariants = modelVariantsFromProviders(entries, model)
const currentModelId = formatModelIdWithVariant(model, updatedSession.variant, availableVariants, true)
const availableModels = buildAvailableModels(entries, { includeVariants: true })
const currentModelId = formatModelIdWithVariant(model, updatedSession.variant, availableVariants, false)
const availableModels = buildAvailableModels(entries)
const modeState = await this.resolveModeState(session.cwd, session.id)
const modes = modeState.currentModeId
? { availableModes: modeState.availableModes, currentModeId: modeState.currentModeId }
: undefined
return {
configOptions: buildConfigOptions({ currentModelId, availableModels, modes }),
configOptions: buildConfigOptions({
currentModelId,
availableModels,
currentVariant: updatedSession.variant,
availableVariants,
modes,
}),
}
}
@@ -1550,6 +1573,37 @@ export class Agent implements ACPAgent {
{ throwOnError: true },
)
}
private async loadSessionMessages(directory: string, sessionId: string, limit?: number) {
return this.sdk.session
.messages(
{
sessionID: sessionId,
directory,
limit,
},
{ throwOnError: true },
)
.then((x) => x.data)
.catch((error) => {
log.error("unexpected error when fetching message", { error })
return undefined
})
}
private restoreSessionStateFromMessages(sessionId: string, messages: SessionMessageResponse[] | undefined) {
const lastUser = messages?.findLast((message) => message.info.role === "user")?.info
if (lastUser?.role !== "user") return
this.sessionManager.setModel(sessionId, {
providerID: ProviderID.make(lastUser.model.providerID),
modelID: ModelID.make(lastUser.model.modelID),
})
this.sessionManager.setVariant(sessionId, lastUser.model.variant)
if (lastUser.agent) {
this.sessionManager.setMode(sessionId, lastUser.agent)
}
}
}
function toToolKind(toolName: string): ToolKind {
@@ -1634,6 +1688,9 @@ async function defaultModel(config: ACPConfig, cwd?: string): Promise<{ provider
if (specified && !providers.length) return specified
// kilocode_change start
const lastUsed = await lastUsedModel(sdk, directory, providers)
if (lastUsed) return lastUsed
const kiloProvider = providers.find((p) => p.id === "kilo")
if (kiloProvider) {
const [best] = Provider.sort(Object.values(kiloProvider.models))
@@ -1657,7 +1714,7 @@ async function defaultModel(config: ACPConfig, cwd?: string): Promise<{ provider
if (specified) return specified
// kilocode_change start
// kilocode_change start - prefer kilo free-tier default before erroring.
// Only fall back to the Kilo provider if it was present in the available
// providers list. When teams configure enabled_providers to use only their
// own models, this prevents silently routing requests to an external API.
@@ -1667,8 +1724,39 @@ async function defaultModel(config: ACPConfig, cwd?: string): Promise<{ provider
const freeModel = await fetchDefaultModel()
return { providerID: ProviderID.kilo, modelID: ModelID.make(freeModel) }
}
throw new Error("no model available: no providers are configured and no default model is set")
// kilocode_change end
throw new Error("No models available")
}
async function lastUsedModel(
sdk: KiloClient,
directory: string,
providers: Array<{ id: string; models: Record<string, unknown> }>,
): Promise<{ providerID: ProviderID; modelID: ModelID } | undefined> {
const session = await sdk.session
.list({ directory, roots: true, limit: 1 }, { throwOnError: true })
.then((x) => x.data?.[0])
.catch((error) => {
log.error("failed to list sessions for default model", { error })
return undefined
})
if (!session) return
const lastUser = await sdk.session
.messages({ sessionID: session.id, directory, limit: 20 }, { throwOnError: true })
.then((x) => x.data?.findLast((message) => message.info.role === "user")?.info)
.catch((error) => {
log.error("failed to load session messages for default model", { error, sessionID: session.id })
return undefined
})
if (lastUser?.role !== "user") return
const provider = providers.find((entry) => entry.id === lastUser.model.providerID)
if (!provider?.models[lastUser.model.modelID]) return
return {
providerID: ProviderID.make(lastUser.model.providerID),
modelID: ModelID.make(lastUser.model.modelID),
}
}
function parseUri(
@@ -1771,8 +1859,14 @@ function formatModelIdWithVariant(
includeVariant: boolean,
) {
const base = `${model.providerID}/${model.modelID}`
if (!includeVariant || !variant || !availableVariants.includes(variant)) return base
return `${base}/${variant}`
if (!includeVariant || availableVariants.length === 0) return base
const selectedVariant =
variant && availableVariants.includes(variant)
? variant
: availableVariants.includes(DEFAULT_VARIANT_VALUE)
? DEFAULT_VARIANT_VALUE
: availableVariants[0]
return `${base}/${selectedVariant}`
}
function buildVariantMeta(input: {
@@ -1824,6 +1918,8 @@ function parseModelSelection(
function buildConfigOptions(input: {
currentModelId: string
availableModels: ModelOption[]
currentVariant?: string
availableVariants?: string[]
modes?: { availableModes: ModeOption[]; currentModeId: string } | undefined
}): SessionConfigOption[] {
const options: SessionConfigOption[] = [
@@ -1836,6 +1932,22 @@ function buildConfigOptions(input: {
options: input.availableModels.map((m) => ({ value: m.modelId, name: m.name })),
},
]
if (input.availableVariants?.length) {
options.push({
id: "effort",
name: "Effort",
description: "Available effort levels for this model",
category: "thought_level",
type: "select",
currentValue:
input.currentVariant && input.availableVariants.includes(input.currentVariant)
? input.currentVariant
: input.availableVariants.includes(DEFAULT_VARIANT_VALUE)
? DEFAULT_VARIANT_VALUE
: input.availableVariants[0],
options: input.availableVariants.map((variant) => ({ value: variant, name: formatVariantName(variant) })),
})
}
if (input.modes) {
options.push({
id: "mode",
@@ -1853,4 +1965,11 @@ function buildConfigOptions(input: {
return options
}
function formatVariantName(variant: string) {
return variant
.split(/[_-]/)
.map((part) => (part ? part.charAt(0).toUpperCase() + part.slice(1) : part))
.join(" ")
}
export * as ACP from "./agent"
+6
View File
@@ -113,4 +113,10 @@ export class ACPSessionManager {
this.sessions.set(sessionId, session)
return session
}
remove(sessionId: string): ACPSessionState | undefined {
const session = this.sessions.get(sessionId)
this.sessions.delete(sessionId)
return session
}
}
+1 -1
View File
@@ -22,7 +22,7 @@ export const AcpCommand = effectCmd({
},
handler: Effect.fn("Cli.acp")(function* (args) {
process.env.KILO_CLIENT = "acp"
const opts = yield* Effect.promise(() => resolveNetworkOptions(args))
const opts = yield* resolveNetworkOptions(args)
const server = yield* Effect.promise(() => Server.listen(opts))
const sdk = createKiloClient({
+2 -2
View File
@@ -84,7 +84,7 @@ const AgentCreateCommand = effectCmd({
// Determine scope/path
let targetPath: string
if (cliPath) {
targetPath = path.join(cliPath, "agent")
targetPath = path.join(cliPath, "agents")
} else {
let scope: "global" | "project" = "global"
if (project.vcs === "git") {
@@ -108,7 +108,7 @@ const AgentCreateCommand = effectCmd({
}
targetPath = path.join(
scope === "global" ? Global.Path.config : path.join(ctx.worktree, ".kilo"), // kilocode_change
"agent",
"agents",
)
}

Some files were not shown because too many files have changed in this diff Show More