From caf6b1d7fbec836e7b9216d905729cb6d2e9b008 Mon Sep 17 00:00:00 2001
From: Serhii Dziupin
Date: Sat, 8 Aug 2026 06:05:27 +0000
Subject: [PATCH 1/4] feat: add Command Code OpenCode plugin with Laguna S 2.1
gateway
Proxy OpenCode chat completions to api.commandcode.ai/alpha/generate with
CLI/API-key auth, Laguna S 2.1 free as default, tool/MCP park-resume,
attachment handling, context compact, and session usage accounting.
Co-authored-by: Serhii Dziupin
---
.cursor/environment.json | 5 +
.github/workflows/release.yml | 123 +++++
.gitignore | 7 +
.opencode/opencode.json | 7 +
CHANGELOG.md | 9 +
LICENSE | 21 +
README.md | 121 ++++-
bun.lock | 92 ++++
docs/header.svg | 19 +
opencode-commandcode.js | 3 +
package.json | 63 +++
scripts/update-plugin.sh | 8 +
src/auth-login.ts | 117 +++++
src/bridge-pool.ts | 73 +++
src/compact.ts | 156 +++++++
src/constants.ts | 46 ++
src/credentials.ts | 123 +++++
src/detect.ts | 150 ++++++
src/executable-path.ts | 67 +++
src/gateway-types.ts | 134 ++++++
src/gateway.ts | 326 +++++++++++++
src/index.ts | 470 +++++++++++++++++++
src/log.ts | 50 ++
src/model-selection.ts | 47 ++
src/models.ts | 143 ++++++
src/prompt.ts | 392 ++++++++++++++++
src/proxy.ts | 829 ++++++++++++++++++++++++++++++++++
src/usage.ts | 173 +++++++
test/live-laguna.ts | 312 +++++++++++++
test/smoke.ts | 600 ++++++++++++++++++++++++
tsconfig.json | 15 +
31 files changed, 4700 insertions(+), 1 deletion(-)
create mode 100644 .cursor/environment.json
create mode 100644 .github/workflows/release.yml
create mode 100644 .gitignore
create mode 100644 .opencode/opencode.json
create mode 100644 CHANGELOG.md
create mode 100644 LICENSE
create mode 100644 bun.lock
create mode 100644 docs/header.svg
create mode 100644 opencode-commandcode.js
create mode 100644 package.json
create mode 100644 scripts/update-plugin.sh
create mode 100644 src/auth-login.ts
create mode 100644 src/bridge-pool.ts
create mode 100644 src/compact.ts
create mode 100644 src/constants.ts
create mode 100644 src/credentials.ts
create mode 100644 src/detect.ts
create mode 100644 src/executable-path.ts
create mode 100644 src/gateway-types.ts
create mode 100644 src/gateway.ts
create mode 100644 src/index.ts
create mode 100644 src/log.ts
create mode 100644 src/model-selection.ts
create mode 100644 src/models.ts
create mode 100644 src/prompt.ts
create mode 100644 src/proxy.ts
create mode 100644 src/usage.ts
create mode 100644 test/live-laguna.ts
create mode 100644 test/smoke.ts
create mode 100644 tsconfig.json
diff --git a/.cursor/environment.json b/.cursor/environment.json
new file mode 100644
index 0000000..bed3de6
--- /dev/null
+++ b/.cursor/environment.json
@@ -0,0 +1,5 @@
+{
+ "name": "opencode-commandcode",
+ "install": "export PATH=\"$HOME/.local/bin:$HOME/.bun/bin:$HOME/.opencode/bin:$PATH\" && (command -v bun >/dev/null || curl -fsSL https://bun.sh/install | bash) && (command -v opencode >/dev/null || curl -fsSL https://opencode.ai/install | bash) && npm config set prefix \"$HOME/.local\" && npm i -g command-code@latest && bun install && bun run build",
+ "start": "export PATH=\"$HOME/.local/bin:$HOME/.bun/bin:$HOME/.opencode/bin:$PATH\" && echo \"command-code=$(command-code --version 2>/dev/null || cmd --version)\" && echo \"opencode=$(opencode --version)\" && echo \"bun=$(bun --version)\""
+}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..f611a82
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,123 @@
+name: Release
+
+on:
+ workflow_dispatch:
+ inputs:
+ version:
+ description: 'Explicit version (e.g., 0.2.0). If empty, bump type is used.'
+ required: false
+ type: string
+ bump:
+ description: 'Version bump type (used if version is empty)'
+ required: false
+ default: 'minor'
+ type: choice
+ options:
+ - minor
+ - patch
+ - major
+ dry_run:
+ description: 'Dry run — skip npm publish, create draft release'
+ required: false
+ default: false
+ type: boolean
+
+env:
+ NODE_VERSION: '22'
+
+permissions:
+ contents: write
+
+jobs:
+ release:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ fetch-tags: true
+
+ - name: Get current version from package.json
+ id: current_version
+ run: echo "version=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT"
+
+ - name: Calculate new version
+ id: version
+ env:
+ CURRENT_VERSION: ${{ steps.current_version.outputs.version }}
+ INPUT_VERSION: ${{ github.event.inputs.version }}
+ BUMP_TYPE: ${{ github.event.inputs.bump }}
+ run: |
+ set -euo pipefail
+ if [[ -n "$INPUT_VERSION" ]]; then
+ if ! echo "$INPUT_VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
+ echo "Error: version must be in semver format X.Y.Z"
+ exit 1
+ fi
+ echo "version=$INPUT_VERSION" >> "$GITHUB_OUTPUT"
+ else
+ IFS='.' read -r major minor patch <<< "$CURRENT_VERSION"
+ case "$BUMP_TYPE" in
+ major) echo "version=$((major+1)).0.0" >> "$GITHUB_OUTPUT" ;;
+ minor) echo "version=$major.$((minor+1)).0" >> "$GITHUB_OUTPUT" ;;
+ patch) echo "version=$major.$minor.$((patch+1))" >> "$GITHUB_OUTPUT" ;;
+ *) echo "Error: unknown bump type $BUMP_TYPE"; exit 1 ;;
+ esac
+ fi
+
+ - name: Update package.json version
+ run: |
+ node -e "
+ const pkg = require('./package.json');
+ pkg.version = '${{ steps.version.outputs.version }}';
+ require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2) + '\n');
+ "
+
+ - name: Setup Bun
+ uses: oven-sh/setup-bun@v2
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: ${{ env.NODE_VERSION }}
+ registry-url: 'https://registry.npmjs.org'
+
+ - name: Install dependencies
+ run: bun install --frozen-lockfile
+
+ - name: Run tests
+ run: bun run test
+
+ - name: Build package
+ run: bun run build
+
+ - name: Commit and tag
+ env:
+ VERSION: ${{ steps.version.outputs.version }}
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+ git add package.json
+ if git ls-files --modified --others | grep -q bun.lock; then
+ git add bun.lock
+ fi
+ git commit -m "chore: release v${VERSION}"
+ git tag "v${VERSION}"
+ git push origin "v${VERSION}"
+ git push origin HEAD:main
+
+ - name: Publish to npm
+ if: ${{ github.event.inputs.dry_run != 'true' }}
+ run: npm publish --access public
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+
+ - name: Create GitHub Release
+ uses: softprops/action-gh-release@v2
+ with:
+ tag_name: v${{ steps.version.outputs.version }}
+ generate_release_notes: true
+ draft: ${{ github.event.inputs.dry_run == 'true' }}
+ name: v${{ steps.version.outputs.version }}
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5d3bf20
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+node_modules/
+dist/
+*.tsbuildinfo
+.DS_Store
+
+# Local agent notes / environment-specific
+AGENTS.md
diff --git a/.opencode/opencode.json b/.opencode/opencode.json
new file mode 100644
index 0000000..d6b020b
--- /dev/null
+++ b/.opencode/opencode.json
@@ -0,0 +1,7 @@
+{
+ "$schema": "https://opencode.ai/config.json",
+ "plugin": ["file://."],
+ "provider": {
+ "command-code": { "name": "Command Code" }
+ }
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..6197490
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,9 @@
+# Changelog
+
+## 0.1.0
+
+- Initial release: Command Code OpenCode plugin
+- Local OpenAI-compatible proxy → `api.commandcode.ai/alpha/generate`
+- Auth via Command Code CLI sync (`cmd login`) or API key
+- Default model: Laguna S 2.1 free (`poolside/laguna-s-2.1-free`)
+- Tools/MCP park-resume, attachment plumbing, context compact, usage endpoints
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..6f6eeac
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Otto Assistant contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index 606d960..2cbf3e1 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,120 @@
-# opencode-commandcode
+
+
+
+
+
+
+
+
+
+
+
+ Command Code inside OpenCode — CLI/API-key auth,
+ gateway proxy, free Laguna S 2.1, tools/MCP, attachments, compact, usage.
+
+
+---
+
+Run [Command Code](https://commandcode.ai) models from OpenCode by proxying the same gateway that `npm i -g command-code@latest` uses (`POST /alpha/generate`). Default and exclusive live-test target: **Laguna S 2.1 free** (`poolside/laguna-s-2.1-free`, 256k context, $0).
+
+Plugin shape mirrors [@otto-assistant/opencode-claude](https://github.com/otto-assistant/opencode-claude) and [@otto-assistant/opencode-cursor](https://github.com/otto-assistant/opencode-cursor).
+
+## Install
+
+```bash
+# global (recommended)
+opencode plugin @otto-assistant/opencode-commandcode -g
+
+# or project-local
+opencode plugin @otto-assistant/opencode-commandcode
+```
+
+Optional provider naming:
+
+```jsonc
+{
+ "$schema": "https://opencode.ai/config.json",
+ "plugin": ["@otto-assistant/opencode-commandcode"],
+ "provider": {
+ "command-code": { "name": "Command Code" }
+ }
+}
+```
+
+From source:
+
+```bash
+git clone https://github.com/otto-assistant/opencode-commandcode.git
+cd opencode-commandcode
+bun install && bun run build
+opencode plugin file://$PWD
+```
+
+## Authenticate
+
+Requires [Command Code CLI](https://commandcode.ai/docs/quickstart) and a free account:
+
+```bash
+npm i -g command-code@latest
+cmd login
+opencode auth login --provider command-code
+# pick "Use Command Code CLI login" — or paste an API key
+```
+
+Env alternative: `COMMAND_CODE_API_KEY`.
+
+Then:
+
+```bash
+opencode run "Summarise this repository in five bullets." --model command-code/laguna-s-2.1-free
+```
+
+## Why this plugin
+
+| | |
+|---|---|
+| **Gateway proxy** | Talks to `api.commandcode.ai/alpha/generate` — same transport as the `cmd` CLI. |
+| **Laguna S 2.1 free** | 256k context, reasoning, $0 while the deal lasts. |
+| **CLI / API auth** | Syncs `~/.commandcode/auth.json` or accepts `COMMAND_CODE_API_KEY`. |
+| **Agent-grade tools** | OpenCode tool calls park and resume; MCP-prefixed tools are tracked separately. |
+| **Attachments** | Images, PDFs, text/binary files from OpenCode. Text-only models get image placeholders (Command Code behavior). |
+| **Compact** | Context-fraction tips + tiered client compact before the 256k window overflows. |
+| **Usage** | Per-turn and session totals via SSE `usage` + `GET /v1/usage`. |
+
+## Architecture
+
+```text
+OpenCode
+ └─ /v1/chat/completions
+ └─ Bun.serve proxy (port 8797)
+ └─ POST https://api.commandcode.ai/alpha/generate
+ └─ poolside/laguna-s-2.1-free (default)
+```
+
+## Requirements
+
+- [OpenCode](https://opencode.ai)
+- [Command Code CLI](https://www.npmjs.com/package/command-code) (`npm i -g command-code@latest`) and a free account
+- Bun (plugin runtime) · Node.js ≥ 18 (CLI needs ≥ 22)
+
+## Development
+
+```bash
+bun install
+bun run build
+bun run test # mocked gateway — attachments, tools, compact, usage
+bun run test:live # live Laguna S 2.1 (needs COMMAND_CODE_API_KEY)
+```
+
+Debug: `OPENCODE_COMMANDCODE_DEBUG=1`.
+
+Optional knobs:
+
+- `OPENCODE_COMMANDCODE_PROXY_PORT` — fixed local proxy port (default `8797`)
+- `OPENCODE_COMMANDCODE_CWD` — working directory reported to the gateway
+- `COMMAND_CODE_API_KEY` — API key without CLI login
+- `COMMANDCODE_API_URL` — override API base (default `https://api.commandcode.ai`)
+
+## License
+
+[MIT](LICENSE)
diff --git a/bun.lock b/bun.lock
new file mode 100644
index 0000000..36ae74c
--- /dev/null
+++ b/bun.lock
@@ -0,0 +1,92 @@
+{
+ "lockfileVersion": 1,
+ "configVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "@otto-assistant/opencode-commandcode",
+ "dependencies": {
+ "@opencode-ai/plugin": "^1.18.15",
+ },
+ "devDependencies": {
+ "@types/bun": "^1.3.11",
+ "@types/node": "^22.15.0",
+ "typescript": "^5.9.3",
+ },
+ },
+ },
+ "packages": {
+ "@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
+
+ "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ=="],
+
+ "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w=="],
+
+ "@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4", "", { "os": "linux", "cpu": "arm" }, "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw=="],
+
+ "@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw=="],
+
+ "@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ=="],
+
+ "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ=="],
+
+ "@opencode-ai/plugin": ["@opencode-ai/plugin@1.18.15", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode-ai/sdk": "1.18.15", "effect": "4.0.0-beta.83", "zod": "4.1.8" }, "peerDependencies": { "@opentui/core": ">=0.4.5", "@opentui/keymap": ">=0.4.5", "@opentui/solid": ">=0.4.5" }, "optionalPeers": ["@opentui/core", "@opentui/keymap", "@opentui/solid"] }, "sha512-AY10RtFbzLkf951dbLkSGJdBeddeS6ojbjvqCpWnINedqlj2cK/GF91xWjWnlHpqQZ4GABLPCuoSJx8mGmKvCw=="],
+
+ "@opencode-ai/sdk": ["@opencode-ai/sdk@1.18.15", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-8sfo9nGiVwesAZW9Wqkvynyn7w4wYaHx1O9qOHpYL65+Bs2XUpHP3kBbZe58gjQydFgk6I74kUF7sqCiPu2arQ=="],
+
+ "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
+
+ "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
+
+ "@types/node": ["@types/node@22.20.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q=="],
+
+ "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
+
+ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
+
+ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
+
+ "effect": ["effect@4.0.0-beta.83", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-0wsak8RtgGAr9UWSbVDgJHZcUqMSvicHcvaZv1MbMM7MCGgW4Rn/137J1MHQbwYPcwYGxT/IqehFd+UbYuj78w=="],
+
+ "fast-check": ["fast-check@4.9.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg=="],
+
+ "find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="],
+
+ "ini": ["ini@7.0.0", "", {}, "sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w=="],
+
+ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
+
+ "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
+
+ "kubernetes-types": ["kubernetes-types@1.30.0", "", {}, "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q=="],
+
+ "msgpackr": ["msgpackr@2.0.5", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-cef05H/dSYpLpqp3sj/qyZh5vhUYCalnaLO7j1yOmpsR0y/XwLVtK7r5gn+U/F7CTEfMowcGhlUQJDLcLf7jcA=="],
+
+ "msgpackr-extract": ["msgpackr-extract@3.0.4", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw=="],
+
+ "multipasta": ["multipasta@0.2.8", "", {}, "sha512-ZPWuMKyv0cSO29f7hozp+k6+crZbQijV8ipMvxNxRf2SwtYGTX1ZX89Kd20VV4H9Znonx+EQn+iy1wGQsJ+b+Q=="],
+
+ "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="],
+
+ "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
+
+ "pure-rand": ["pure-rand@8.4.2", "", {}, "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng=="],
+
+ "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
+
+ "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
+
+ "toml": ["toml@4.3.0", "", {}, "sha512-lVb8X9BsPVuH0M4BKeS91tXAmJvCjQ5UIyAbQFaxkKGyUFK2RPkhwaFSQH8vbpl1d23eu/IBH+dwVMHWaq9A5A=="],
+
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+
+ "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
+
+ "uuid": ["uuid@14.0.1", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew=="],
+
+ "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
+
+ "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
+
+ "zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="],
+ }
+}
diff --git a/docs/header.svg b/docs/header.svg
new file mode 100644
index 0000000..f8a6a0c
--- /dev/null
+++ b/docs/header.svg
@@ -0,0 +1,19 @@
+
diff --git a/opencode-commandcode.js b/opencode-commandcode.js
new file mode 100644
index 0000000..0d6ee18
--- /dev/null
+++ b/opencode-commandcode.js
@@ -0,0 +1,3 @@
+// Entry point: re-exports the compiled plugin from dist/
+// OpenCode's file/package loader resolves this more reliably than dist/index.js directly.
+export { CommandCodePlugin, default } from "./dist/index.js";
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..7cb1cb2
--- /dev/null
+++ b/package.json
@@ -0,0 +1,63 @@
+{
+ "name": "@otto-assistant/opencode-commandcode",
+ "version": "0.1.0",
+ "description": "Command Code in OpenCode — CLI auth sync, gateway proxy, Laguna S 2.1, tools/MCP, attachments, compact, usage.",
+ "license": "MIT",
+ "type": "module",
+ "main": "./opencode-commandcode.js",
+ "module": "./opencode-commandcode.js",
+ "types": "./dist/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "import": "./opencode-commandcode.js",
+ "default": "./opencode-commandcode.js"
+ }
+ },
+ "files": [
+ "dist",
+ "opencode-commandcode.js"
+ ],
+ "scripts": {
+ "build": "tsc -p tsconfig.json",
+ "test": "bun test/smoke.ts",
+ "test:live": "bun test/live-laguna.ts",
+ "prepublishOnly": "npm run build"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/otto-assistant/opencode-commandcode.git"
+ },
+ "homepage": "https://www.npmjs.com/package/@otto-assistant/opencode-commandcode",
+ "bugs": {
+ "url": "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/otto-assistant/opencode-commandcode/issues"
+ },
+ "keywords": [
+ "opencode",
+ "opencode-plugin",
+ "command-code",
+ "commandcode",
+ "laguna",
+ "poolside",
+ "oauth",
+ "openai-compatible",
+ "ai",
+ "llm",
+ "streaming",
+ "agent"
+ ],
+ "engines": {
+ "node": ">=18"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "dependencies": {
+ "@opencode-ai/plugin": "^1.18.15"
+ },
+ "devDependencies": {
+ "@types/bun": "^1.3.11",
+ "@types/node": "^22.15.0",
+ "typescript": "^5.9.3"
+ }
+}
diff --git a/scripts/update-plugin.sh b/scripts/update-plugin.sh
new file mode 100644
index 0000000..d05f437
--- /dev/null
+++ b/scripts/update-plugin.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+set -euo pipefail
+ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+cd "$ROOT"
+bun install
+bun run build
+echo "Built @otto-assistant/opencode-commandcode"
+echo "Install into OpenCode with: opencode plugin file://$ROOT"
diff --git a/src/auth-login.ts b/src/auth-login.ts
new file mode 100644
index 0000000..3b0d4e7
--- /dev/null
+++ b/src/auth-login.ts
@@ -0,0 +1,117 @@
+/**
+ * Sync Command Code CLI credentials into OpenCode auth.json.
+ */
+import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
+import { dirname, join } from "node:path";
+import { homedir } from "node:os";
+import {
+ readCommandCodeCredentials,
+ type CommandCodeCredentials,
+} from "./credentials.js";
+import { PROVIDER_ID, WHOAMI_ROUTE } from "./constants.js";
+import { getApiBaseUrl, log } from "./log.js";
+
+export type CommandCodeAuthTokens = {
+ access: string;
+ refresh: string;
+ expires: number;
+ key?: string;
+};
+
+function authJsonPath(): string {
+ const xdg = process.env.XDG_DATA_HOME;
+ const base = xdg ? xdg : join(homedir(), ".local", "share");
+ return join(base, "opencode", "auth.json");
+}
+
+function readAuthFile(): Record {
+ const path = authJsonPath();
+ if (!existsSync(path)) return {};
+ try {
+ return JSON.parse(readFileSync(path, "utf8")) as Record;
+ } catch {
+ return {};
+ }
+}
+
+function writeAuthFile(data: Record): void {
+ const path = authJsonPath();
+ mkdirSync(dirname(path), { recursive: true });
+ writeFileSync(path, JSON.stringify(data, null, 2) + "\n", "utf8");
+}
+
+export function writeCommandCodeAuth(
+ tokens: CommandCodeAuthTokens & { key?: string },
+): void {
+ const existing = readAuthFile();
+ const key = tokens.key || tokens.access;
+ existing[PROVIDER_ID] = {
+ type: "api",
+ key,
+ // Also keep oauth-shaped fields for hosts that expect them.
+ access: key,
+ refresh: tokens.refresh,
+ expires: tokens.expires,
+ };
+ writeAuthFile(existing);
+}
+
+export function credentialsToTokens(
+ creds: CommandCodeCredentials,
+): CommandCodeAuthTokens {
+ return {
+ access: creds.apiKey,
+ refresh: `cli-sync-${creds.source}`,
+ expires: Date.now() + 365 * 24 * 60 * 60 * 1000,
+ key: creds.apiKey,
+ };
+}
+
+/**
+ * Sync Command Code CLI / env credentials into OpenCode auth.json.
+ */
+export function syncCommandCodeCredentialsToOpenCode(): CommandCodeAuthTokens | null {
+ const creds = readCommandCodeCredentials();
+ if (!creds?.apiKey) return null;
+ const tokens = credentialsToTokens(creds);
+ writeCommandCodeAuth(tokens);
+ log.info(
+ "[opencode-commandcode] synced Command Code credentials into OpenCode auth",
+ );
+ return tokens;
+}
+
+export async function validateCommandApiKey(
+ apiKey: string,
+ options?: { baseUrl?: string; fetchFn?: typeof fetch },
+): Promise<{ valid: boolean; userName?: string; error?: string }> {
+ const fetchFn = options?.fetchFn ?? fetch;
+ const baseUrl = (options?.baseUrl ?? getApiBaseUrl()).replace(/\/$/, "");
+ try {
+ const res = await fetchFn(`${baseUrl}${WHOAMI_ROUTE}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${apiKey}`,
+ },
+ });
+ if (res.status === 401) {
+ return { valid: false, error: "invalid_key" };
+ }
+ if (!res.ok) {
+ return { valid: false, error: `server_error_${res.status}` };
+ }
+ const body = (await res.json().catch(() => null)) as {
+ user?: { userName?: string; name?: string };
+ success?: boolean;
+ } | null;
+ const userName =
+ body?.user?.userName || body?.user?.name || undefined;
+ return { valid: true, userName };
+ } catch (err) {
+ return {
+ valid: false,
+ error: err instanceof Error ? err.message : "network_error",
+ };
+ }
+}
diff --git a/src/bridge-pool.ts b/src/bridge-pool.ts
new file mode 100644
index 0000000..d2cff84
--- /dev/null
+++ b/src/bridge-pool.ts
@@ -0,0 +1,73 @@
+/**
+ * Parked bridge pool for OpenCode tool_calls ↔ Command Code tool-call events.
+ */
+export type ParkedToolCall = {
+ id: string;
+ name: string;
+ arguments: string;
+ resolve: (result: string) => void;
+ reject: (err: Error) => void;
+ /** Set when parking so resume can await OpenCode tool results. */
+ resultPromise?: Promise;
+};
+
+export type ParkedBridge = {
+ id: string;
+ conversationKey: string;
+ pendingTools: Map;
+ createdAt: number;
+ /** Resume generator after tool results land. */
+ continueStream?: () => AsyncGenerator;
+ /** Accumulated assistant text so far (optional). */
+ partialText?: string;
+ /** Wire messages so far for the next gateway turn. */
+ wireMessages?: unknown[];
+ modelId?: string;
+ system?: string;
+ effort?: string;
+ tools?: unknown[];
+ apiKey?: string;
+ contextWindow?: number;
+ maxTokens?: number;
+};
+
+const bridges = new Map();
+
+export function putBridge(bridge: ParkedBridge): void {
+ // Supersede older bridges for the same conversation.
+ for (const [id, existing] of bridges) {
+ if (
+ existing.conversationKey === bridge.conversationKey &&
+ id !== bridge.id
+ ) {
+ bridges.delete(id);
+ }
+ }
+ bridges.set(bridge.id, bridge);
+}
+
+export function deleteBridge(id: string): void {
+ bridges.delete(id);
+}
+
+export function findBridgeByConversation(
+ conversationKey: string,
+): ParkedBridge | undefined {
+ for (const bridge of bridges.values()) {
+ if (bridge.conversationKey === conversationKey) return bridge;
+ }
+ return undefined;
+}
+
+export function findBridgeByPendingTool(
+ toolCallId: string,
+): ParkedBridge | undefined {
+ for (const bridge of bridges.values()) {
+ if (bridge.pendingTools.has(toolCallId)) return bridge;
+ }
+ return undefined;
+}
+
+export function clearBridges(): void {
+ bridges.clear();
+}
diff --git a/src/compact.ts b/src/compact.ts
new file mode 100644
index 0000000..1ca0114
--- /dev/null
+++ b/src/compact.ts
@@ -0,0 +1,156 @@
+/**
+ * Context-window / compact helpers for long OpenCode ↔ Command Code sessions.
+ */
+import { COMPACT_TIERS } from "./constants.js";
+import type { WireMessage } from "./gateway-types.js";
+import { recordCompactEvent } from "./usage.js";
+import { log } from "./log.js";
+
+export type CompactAdvice = {
+ tier: "ok" | "tip" | "warn" | "auto";
+ fraction: number;
+ shouldCompact: boolean;
+ message: string | null;
+};
+
+export function assessContext(
+ lastContextTokens: number,
+ contextWindow: number,
+): CompactAdvice {
+ if (contextWindow <= 0) {
+ return { tier: "ok", fraction: 0, shouldCompact: false, message: null };
+ }
+ const fraction = lastContextTokens / contextWindow;
+ if (fraction >= COMPACT_TIERS.auto) {
+ return {
+ tier: "auto",
+ fraction,
+ shouldCompact: true,
+ message: `[compact] Context at ${Math.round(fraction * 100)}% of window — auto-compacting older turns.`,
+ };
+ }
+ if (fraction >= COMPACT_TIERS.warn) {
+ return {
+ tier: "warn",
+ fraction,
+ shouldCompact: false,
+ message: `[compact] Context at ${Math.round(fraction * 100)}% — consider compacting soon.`,
+ };
+ }
+ if (fraction >= COMPACT_TIERS.tip) {
+ return {
+ tier: "tip",
+ fraction,
+ shouldCompact: false,
+ message: `[compact] Context at ${Math.round(fraction * 100)}% — compact at a task boundary if this session will grow.`,
+ };
+ }
+ return { tier: "ok", fraction, shouldCompact: false, message: null };
+}
+
+/**
+ * Estimate rough token count from wire messages (chars/4 heuristic).
+ * Used when the gateway has not yet reported usage for this turn.
+ */
+export function estimateMessageTokens(messages: WireMessage[]): number {
+ let chars = 0;
+ for (const msg of messages) {
+ if (typeof msg.content === "string") {
+ chars += msg.content.length;
+ continue;
+ }
+ for (const part of msg.content) {
+ if (part.type === "text") chars += part.text.length;
+ else if (part.type === "reasoning") chars += part.text.length;
+ else if (part.type === "image") chars += 256; // placeholder cost
+ else if (part.type === "tool-call") {
+ chars += part.toolName.length + JSON.stringify(part.input).length;
+ } else if (part.type === "tool-result") {
+ chars += part.output.value.length;
+ }
+ }
+ }
+ return Math.ceil(chars / 4);
+}
+
+/**
+ * Tiered client-side compact: trim old tool results, then drop early turns.
+ * Mirrors Command Code's fast compact-mode spirit for the OpenCode proxy path.
+ */
+export function compactWireMessages(
+ messages: WireMessage[],
+ options: {
+ sessionId: string;
+ keepToolResults?: number;
+ keepTurns?: number;
+ },
+): { messages: WireMessage[]; compacted: boolean; note: string | null } {
+ const keepToolResults = options.keepToolResults ?? 10;
+ const keepTurns = options.keepTurns ?? 24;
+
+ let toolResults: Array<{ index: number; msgIndex: number; partIndex: number }> =
+ [];
+ messages.forEach((msg, msgIndex) => {
+ if (msg.role !== "tool" || !Array.isArray(msg.content)) return;
+ msg.content.forEach((part, partIndex) => {
+ if (part.type === "tool-result") {
+ toolResults.push({
+ index: toolResults.length,
+ msgIndex,
+ partIndex,
+ });
+ }
+ });
+ });
+
+ let compacted = false;
+ const cloned: WireMessage[] = messages.map((m) => {
+ if (typeof m.content === "string") return { ...m };
+ return {
+ ...m,
+ content: m.content.map((p) => ({ ...p })),
+ } as WireMessage;
+ });
+
+ if (toolResults.length > keepToolResults) {
+ const dropCount = toolResults.length - keepToolResults;
+ for (let i = 0; i < dropCount; i++) {
+ const ref = toolResults[i];
+ const msg = cloned[ref.msgIndex];
+ if (msg.role !== "tool" || !Array.isArray(msg.content)) continue;
+ const part = msg.content[ref.partIndex];
+ if (part?.type === "tool-result") {
+ part.output = {
+ type: "text",
+ value: "[compacted: older tool result trimmed]",
+ };
+ compacted = true;
+ }
+ }
+ }
+
+ // Keep system + recent turns.
+ if (cloned.length > keepTurns) {
+ const system = cloned.filter((m) => m.role === "system");
+ const rest = cloned.filter((m) => m.role !== "system");
+ const kept = rest.slice(-keepTurns);
+ cloned.length = 0;
+ cloned.push(...system, ...kept);
+ compacted = true;
+ }
+
+ if (compacted) {
+ recordCompactEvent(options.sessionId);
+ log.info("[opencode-commandcode] compacted wire messages", {
+ sessionId: options.sessionId,
+ remaining: cloned.length,
+ });
+ return {
+ messages: cloned,
+ compacted: true,
+ note: "[compact] Older context was compacted before this turn.",
+ };
+ }
+
+ return { messages: cloned, compacted: false, note: null };
+}
diff --git a/src/constants.ts b/src/constants.ts
new file mode 100644
index 0000000..df2851d
--- /dev/null
+++ b/src/constants.ts
@@ -0,0 +1,46 @@
+/** Command Code OpenCode provider id. */
+export const PROVIDER_ID = "command-code";
+
+/** Default model alias shown in OpenCode (resolves to Laguna S 2.1 free). */
+export const DEFAULT_MODEL_ID = "laguna-s-2.1-free";
+
+/** Canonical Laguna S 2.1 free model id on Command Code. */
+export const LAGUNA_MODEL_ID = "poolside/laguna-s-2.1-free";
+
+export const OPENAI_COMPATIBLE_NPM = "@ai-sdk/openai-compatible";
+
+export const EFFORT_HEADER = "x-opencode-commandcode-effort";
+export const SESSION_HEADER = "x-opencode-commandcode-session";
+
+export const COMMAND_CODE_API_KEY_ENV = "COMMAND_CODE_API_KEY";
+export const COMMAND_CODE_DIR_NAME = ".commandcode";
+export const COMMAND_CODE_AUTH_FILE = "auth.json";
+
+export const DEFAULT_API_BASE_URL = "https://api.commandcode.ai";
+export const GENERATE_ROUTE = "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/alpha/generate";
+export const WHOAMI_ROUTE = "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/alpha/whoami";
+export const USAGE_SUMMARY_ROUTE = "/alpha/usage/summary";
+
+export const EFFORT_LEVELS = [
+ "low",
+ "medium",
+ "high",
+ "xhigh",
+ "max",
+] as const;
+
+export type CommandEffort = (typeof EFFORT_LEVELS)[number];
+
+export function isCommandEffort(value: unknown): value is CommandEffort {
+ return (
+ typeof value === "string" &&
+ (EFFORT_LEVELS as readonly string[]).includes(value)
+ );
+}
+
+/** Compact tiers as fractions of the model context window. */
+export const COMPACT_TIERS = {
+ tip: 0.5,
+ warn: 0.8,
+ auto: 0.9,
+} as const;
diff --git a/src/credentials.ts b/src/credentials.ts
new file mode 100644
index 0000000..629ee41
--- /dev/null
+++ b/src/credentials.ts
@@ -0,0 +1,123 @@
+/**
+ * Read Command Code CLI credentials from env or ~/.commandcode/auth.json.
+ * Never log token values.
+ */
+import { existsSync, readFileSync } from "node:fs";
+import { homedir } from "node:os";
+import { join } from "node:path";
+import {
+ COMMAND_CODE_API_KEY_ENV,
+ COMMAND_CODE_AUTH_FILE,
+ COMMAND_CODE_DIR_NAME,
+} from "./constants.js";
+
+export type CommandCodeCredentials = {
+ apiKey: string;
+ userId: string | null;
+ userName: string | null;
+ keyName: string | null;
+ source: "env" | "auth-file";
+ authPath?: string;
+};
+
+export type CommandAuthFile = {
+ apiKey?: string;
+ userId?: string;
+ userName?: string;
+ keyName?: string;
+ authenticatedAt?: string;
+};
+
+export function listCommandAuthCandidates(
+ homeDir = homedir(),
+ env: NodeJS.ProcessEnv | Record = process.env,
+): string[] {
+ const candidates: string[] = [];
+ const configDir =
+ typeof env.COMMANDCODE_CONFIG_DIR === "string"
+ ? env.COMMANDCODE_CONFIG_DIR.trim()
+ : "";
+ if (configDir) {
+ candidates.push(join(configDir, COMMAND_CODE_AUTH_FILE));
+ }
+ candidates.push(
+ join(homeDir, COMMAND_CODE_DIR_NAME, COMMAND_CODE_AUTH_FILE),
+ join(homeDir, ".config", "commandcode", COMMAND_CODE_AUTH_FILE),
+ );
+ return candidates;
+}
+
+export function parseAuthFile(raw: string): CommandAuthFile | null {
+ try {
+ const parsed = JSON.parse(raw) as unknown;
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
+ return null;
+ }
+ return parsed as CommandAuthFile;
+ } catch {
+ return null;
+ }
+}
+
+export function extractApiKeyFromAuthFile(
+ parsed: CommandAuthFile | null,
+): string | null {
+ if (!parsed) return null;
+ const key = typeof parsed.apiKey === "string" ? parsed.apiKey.trim() : "";
+ return key.length > 0 ? key : null;
+}
+
+export function readCommandCodeApiKeyFromEnv(
+ env: NodeJS.ProcessEnv | Record = process.env,
+): string | null {
+ const value = env[COMMAND_CODE_API_KEY_ENV];
+ if (typeof value !== "string") return null;
+ const trimmed = value.trim();
+ return trimmed.length > 0 ? trimmed : null;
+}
+
+export function readCommandCodeCredentials(options?: {
+ homeDir?: string;
+ env?: NodeJS.ProcessEnv | Record;
+}): CommandCodeCredentials | null {
+ const env = options?.env ?? process.env;
+ const homeDir = options?.homeDir ?? homedir();
+
+ const fromEnv = readCommandCodeApiKeyFromEnv(env);
+ if (fromEnv) {
+ return {
+ apiKey: fromEnv,
+ userId: null,
+ userName: null,
+ keyName: null,
+ source: "env",
+ };
+ }
+
+ for (const path of listCommandAuthCandidates(homeDir, env)) {
+ if (!existsSync(path)) continue;
+ try {
+ const parsed = parseAuthFile(readFileSync(path, "utf8"));
+ const apiKey = extractApiKeyFromAuthFile(parsed);
+ if (!apiKey || !parsed) continue;
+ return {
+ apiKey,
+ userId: typeof parsed.userId === "string" ? parsed.userId : null,
+ userName: typeof parsed.userName === "string" ? parsed.userName : null,
+ keyName: typeof parsed.keyName === "string" ? parsed.keyName : null,
+ source: "auth-file",
+ authPath: path,
+ };
+ } catch {
+ // try next
+ }
+ }
+ return null;
+}
+
+export function hasCommandCodeCredentials(options?: {
+ homeDir?: string;
+ env?: NodeJS.ProcessEnv | Record;
+}): boolean {
+ return Boolean(readCommandCodeCredentials(options)?.apiKey);
+}
diff --git a/src/detect.ts b/src/detect.ts
new file mode 100644
index 0000000..ab2987a
--- /dev/null
+++ b/src/detect.ts
@@ -0,0 +1,150 @@
+/**
+ * Detect Command Code CLI installation + auth status.
+ */
+import { spawnSync } from "node:child_process";
+import { hasCommandCodeCredentials } from "./credentials.js";
+import { resolveCommandCodeExecutable } from "./executable-path.js";
+
+export type CommandDetectStatus =
+ | "ready"
+ | "needs-login"
+ | "missing-cli"
+ | "error";
+
+export type CommandDetectResult = {
+ status: CommandDetectStatus;
+ statusDetail?: string;
+ binaryPath?: string | null;
+ version?: string | null;
+ loggedIn: boolean;
+};
+
+export function probeCommandAuthStatusCli(options: {
+ binaryPath: string;
+ env?: NodeJS.ProcessEnv | Record;
+ spawnSyncFn?: typeof spawnSync;
+}): { loggedIn: boolean; detail: string } | null {
+ const binaryPath = options.binaryPath.trim();
+ if (!binaryPath) return null;
+ const spawnSyncFn = options.spawnSyncFn || spawnSync;
+
+ try {
+ const result = spawnSyncFn(binaryPath, ["status", "--json"], {
+ encoding: "utf8",
+ timeout: 8000,
+ env: (options.env || process.env) as NodeJS.ProcessEnv,
+ windowsHide: true,
+ });
+
+ const output = `${result.stdout || ""}`.trim();
+ if (!output) {
+ // Fall back to non-json status text.
+ const text = spawnSyncFn(binaryPath, ["status"], {
+ encoding: "utf8",
+ timeout: 8000,
+ env: (options.env || process.env) as NodeJS.ProcessEnv,
+ windowsHide: true,
+ });
+ const combined = `${text.stdout || ""}\n${text.stderr || ""}`;
+ const loggedIn = /logged in|authenticated/i.test(combined) &&
+ !/not authenticated/i.test(combined);
+ return {
+ loggedIn,
+ detail: loggedIn ? "auth-status-text" : "auth-status-logged-out",
+ };
+ }
+
+ try {
+ const payload = JSON.parse(output) as {
+ authenticated?: boolean;
+ loggedIn?: boolean;
+ };
+ const loggedIn = Boolean(payload.authenticated || payload.loggedIn);
+ return {
+ loggedIn,
+ detail: loggedIn ? "auth-status-json" : "auth-status-logged-out",
+ };
+ } catch {
+ const loggedIn =
+ /authenticated|logged in/i.test(output) &&
+ !/not authenticated/i.test(output);
+ return {
+ loggedIn,
+ detail: loggedIn ? "auth-status-text" : "auth-status-parse-error",
+ };
+ }
+ } catch {
+ return null;
+ }
+}
+
+export async function detectCommandCode(options?: {
+ env?: NodeJS.ProcessEnv | Record;
+ homeDir?: string;
+ binaryPath?: string | null;
+}): Promise {
+ const env = options?.env ?? process.env;
+ const binaryPath =
+ options?.binaryPath !== undefined
+ ? options.binaryPath
+ : resolveCommandCodeExecutable({ env });
+
+ const credsPresent = hasCommandCodeCredentials({
+ homeDir: options?.homeDir,
+ env,
+ });
+
+ if (!binaryPath && !credsPresent) {
+ return {
+ status: "missing-cli",
+ statusDetail:
+ "Command Code CLI (`cmd` / `command-code`) not found on PATH. Install with `npm i -g command-code@latest`.",
+ binaryPath: null,
+ version: null,
+ loggedIn: false,
+ };
+ }
+
+ let version: string | null = null;
+ if (binaryPath) {
+ try {
+ const result = spawnSync(binaryPath, ["--version"], {
+ encoding: "utf8",
+ timeout: 4000,
+ env: env as NodeJS.ProcessEnv,
+ windowsHide: true,
+ stdio: ["ignore", "pipe", "pipe"],
+ });
+ const match = `${result.stdout || ""}`.trim().match(/(\d+\.\d+\.\d+)/);
+ version = match?.[1] ?? (`${result.stdout || ""}`.trim() || null);
+ } catch {
+ version = null;
+ }
+ }
+
+ const authStatus = binaryPath
+ ? probeCommandAuthStatusCli({ binaryPath, env })
+ : null;
+ const loggedIn = Boolean(authStatus?.loggedIn) || credsPresent;
+
+ if (!loggedIn) {
+ return {
+ status: "needs-login",
+ statusDetail:
+ "Command Code is installed but not authenticated. Run `cmd login` or set COMMAND_CODE_API_KEY.",
+ binaryPath,
+ version,
+ loggedIn: false,
+ };
+ }
+
+ return {
+ status: "ready",
+ statusDetail: authStatus?.detail || "ready",
+ binaryPath,
+ version,
+ loggedIn: true,
+ };
+}
+
+export { resolveCommandCodeExecutable } from "./executable-path.js";
diff --git a/src/executable-path.ts b/src/executable-path.ts
new file mode 100644
index 0000000..5e0bb52
--- /dev/null
+++ b/src/executable-path.ts
@@ -0,0 +1,67 @@
+/**
+ * Resolve the `cmd` / `command-code` CLI binary.
+ */
+import { spawnSync } from "node:child_process";
+
+export function findBinaryOnPath(
+ name: string,
+ env: NodeJS.ProcessEnv | Record = process.env,
+): string | null {
+ const pathEnv = typeof env.PATH === "string" ? env.PATH : "";
+ const parts = pathEnv.split(process.platform === "win32" ? ";" : ":");
+ const exts =
+ process.platform === "win32" ? [".cmd", ".exe", ".bat", ""] : [""];
+ for (const dir of parts) {
+ if (!dir) continue;
+ for (const ext of exts) {
+ const candidate = `${dir.replace(/[/\\]$/, "")}/${name}${ext}`;
+ try {
+ const result = spawnSync(candidate, ["--version"], {
+ encoding: "utf8",
+ timeout: 4000,
+ env: env as NodeJS.ProcessEnv,
+ windowsHide: true,
+ stdio: ["ignore", "pipe", "pipe"],
+ });
+ if (result.error) continue;
+ if (result.status === 0 || (result.stdout || "").trim()) {
+ return candidate;
+ }
+ } catch {
+ // try next
+ }
+ }
+ }
+
+ try {
+ const result = spawnSync(name, ["--version"], {
+ encoding: "utf8",
+ timeout: 4000,
+ env: env as NodeJS.ProcessEnv,
+ windowsHide: true,
+ stdio: ["ignore", "pipe", "pipe"],
+ });
+ if (!result.error && (result.status === 0 || (result.stdout || "").trim())) {
+ return name;
+ }
+ } catch {
+ // missing
+ }
+ return null;
+}
+
+export function resolveCommandCodeExecutable(options?: {
+ env?: NodeJS.ProcessEnv | Record;
+}): string | null {
+ const env = options?.env ?? process.env;
+ return (
+ findBinaryOnPath("command-code", env) ||
+ findBinaryOnPath("cmd", env) ||
+ findBinaryOnPath("cmdc", env) ||
+ findBinaryOnPath("commandcode", env)
+ );
+}
+
+export function assertWorkingDirectory(cwd: unknown): string {
+ return typeof cwd === "string" && cwd.trim() ? cwd.trim() : process.cwd();
+}
diff --git a/src/gateway-types.ts b/src/gateway-types.ts
new file mode 100644
index 0000000..88f5aeb
--- /dev/null
+++ b/src/gateway-types.ts
@@ -0,0 +1,134 @@
+/**
+ * Shared gateway wire types for Command Code /alpha/generate.
+ */
+import { getApiBaseUrl as getBase } from "./log.js";
+import {
+ GENERATE_ROUTE,
+ USAGE_SUMMARY_ROUTE,
+ WHOAMI_ROUTE,
+} from "./constants.js";
+
+export { GENERATE_ROUTE, USAGE_SUMMARY_ROUTE, WHOAMI_ROUTE };
+export const getApiBaseUrl = getBase;
+
+export type WireTextPart = { type: "text"; text: string };
+export type WireImagePart = {
+ type: "image";
+ image: string;
+ mimeType?: string;
+};
+export type WireReasoningPart = { type: "reasoning"; text: string };
+export type WireToolCallPart = {
+ type: "tool-call";
+ toolCallId: string;
+ toolName: string;
+ input: Record;
+};
+export type WireToolResultPart = {
+ type: "tool-result";
+ toolCallId: string;
+ toolName: string;
+ output: { type: "text" | "error-text"; value: string };
+};
+
+export type WireUserContent = Array;
+export type WireAssistantContent = Array<
+ WireTextPart | WireReasoningPart | WireToolCallPart
+>;
+export type WireToolContent = Array;
+
+export type WireMessage =
+ | { role: "user"; content: WireUserContent }
+ | { role: "assistant"; content: WireAssistantContent }
+ | { role: "tool"; content: WireToolContent }
+ | { role: "system"; content: string };
+
+export type WireTool = {
+ name: string;
+ description: string;
+ input_schema: Record;
+};
+
+export type WireUsage = {
+ inputTokens: number;
+ outputTokens: number;
+ cacheReadTokens: number;
+ cacheWriteTokens: number;
+};
+
+export type GenerateBody = {
+ config: Record;
+ memory: null;
+ taste: null;
+ skills: null;
+ permissionMode: "standard" | "auto-accept" | "plan";
+ threadId?: string;
+ mode?: string;
+ params: {
+ model: string;
+ messages: WireMessage[];
+ tools: WireTool[];
+ system?: string;
+ max_tokens: number;
+ stream: boolean;
+ temperature?: number;
+ reasoning_effort?: string;
+ };
+};
+
+export type StreamEvent =
+ | { type: "text-delta"; text?: string }
+ | { type: "reasoning-start" }
+ | { type: "reasoning-delta"; text?: string }
+ | { type: "reasoning-end" }
+ | {
+ type: "tool-call";
+ toolCallId?: string;
+ toolName?: string;
+ input?: unknown;
+ args?: unknown;
+ providerExecuted?: boolean;
+ }
+ | {
+ type: "tool-result";
+ toolCallId?: string;
+ result?: unknown;
+ output?: unknown;
+ isError?: boolean;
+ providerExecuted?: boolean;
+ }
+ | {
+ type: "finish";
+ finishReason?: string;
+ rawFinishReason?: string;
+ totalUsage?: {
+ inputTokens?: number;
+ outputTokens?: number;
+ inputTokenDetails?: {
+ cacheReadTokens?: number;
+ cacheWriteTokens?: number;
+ };
+ };
+ systemPromptTokens?: number;
+ }
+ | { type: "error"; error?: unknown }
+ | { type: "abort" }
+ | { type: string; [key: string]: unknown };
+
+export function emptyUsage(): WireUsage {
+ return {
+ inputTokens: 0,
+ outputTokens: 0,
+ cacheReadTokens: 0,
+ cacheWriteTokens: 0,
+ };
+}
+
+export function addUsage(a: WireUsage, b: WireUsage): WireUsage {
+ return {
+ inputTokens: a.inputTokens + b.inputTokens,
+ outputTokens: a.outputTokens + b.outputTokens,
+ cacheReadTokens: a.cacheReadTokens + b.cacheReadTokens,
+ cacheWriteTokens: a.cacheWriteTokens + b.cacheWriteTokens,
+ };
+}
diff --git a/src/gateway.ts b/src/gateway.ts
new file mode 100644
index 0000000..4e938bb
--- /dev/null
+++ b/src/gateway.ts
@@ -0,0 +1,326 @@
+/**
+ * Command Code gateway client — POST /alpha/generate NDJSON stream.
+ */
+import { createHash, randomUUID } from "node:crypto";
+import {
+ GENERATE_ROUTE,
+ emptyUsage,
+ getApiBaseUrl,
+ type GenerateBody,
+ type StreamEvent,
+ type WireMessage,
+ type WireTool,
+ type WireUsage,
+} from "./gateway-types.js";
+import { log } from "./log.js";
+
+export type GatewayAuthHeaders = {
+ apiKey: string;
+ sessionId?: string;
+ cliVersion?: string;
+ projectSlug?: string;
+};
+
+export type GatewayGenerateParams = {
+ apiKey: string;
+ model: string;
+ messages: WireMessage[];
+ tools?: WireTool[];
+ system?: string;
+ maxTokens?: number;
+ temperature?: number;
+ effort?: string;
+ permissionMode?: "standard" | "auto-accept" | "plan";
+ threadId?: string;
+ sessionId?: string;
+ signal?: AbortSignal;
+ /** Injected for tests. */
+ fetchFn?: typeof fetch;
+ baseUrl?: string;
+ /** Override POST body builder for mocks. */
+ postStream?: (
+ body: GenerateBody,
+ headers: Record,
+ ) => Promise>;
+};
+
+export type GatewayMappedEvent =
+ | { kind: "text"; text: string }
+ | { kind: "reasoning"; text: string }
+ | {
+ kind: "tool_call";
+ id: string;
+ name: string;
+ arguments: string;
+ providerExecuted?: boolean;
+ }
+ | { kind: "tool_result"; id: string; output: string; isError?: boolean }
+ | {
+ kind: "finish";
+ finishReason: string;
+ usage: WireUsage;
+ systemPromptTokens?: number;
+ }
+ | { kind: "error"; text: string }
+ | { kind: "ignore" };
+
+export function buildAuthHeaders(auth: GatewayAuthHeaders): Record {
+ const headers: Record = {
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ "User-Agent": "cli",
+ Authorization: `Bearer ${auth.apiKey}`,
+ "x-command-code-version": auth.cliVersion || "1.15.0",
+ "x-cli-environment": "cli",
+ "x-taste-learning": "false",
+ "x-co-flag": "false",
+ };
+ if (auth.sessionId) headers["x-session-id"] = auth.sessionId;
+ if (auth.projectSlug) headers["x-project-slug"] = auth.projectSlug;
+ return headers;
+}
+
+export function buildGenerateBody(
+ params: GatewayGenerateParams,
+): GenerateBody {
+ return {
+ config: {
+ workingDir: process.env.OPENCODE_COMMANDCODE_CWD || process.cwd(),
+ date: new Date().toISOString().slice(0, 10),
+ environment: process.platform,
+ structure: "",
+ isGitRepo: false,
+ currentBranch: "",
+ mainBranch: "",
+ gitStatus: "",
+ recentCommits: [],
+ },
+ memory: null,
+ taste: null,
+ skills: null,
+ permissionMode: params.permissionMode || "auto-accept",
+ ...(params.threadId ? { threadId: params.threadId } : {}),
+ mode: "default",
+ params: {
+ model: params.model,
+ messages: params.messages,
+ tools: params.tools || [],
+ ...(params.system ? { system: params.system } : {}),
+ max_tokens: params.maxTokens ?? 32_768,
+ stream: true,
+ ...(typeof params.temperature === "number"
+ ? { temperature: params.temperature }
+ : {}),
+ ...(params.effort ? { reasoning_effort: params.effort } : {}),
+ },
+ };
+}
+
+async function* readNdjsonLines(
+ stream: ReadableStream,
+): AsyncGenerator {
+ const reader = stream.getReader();
+ const decoder = new TextDecoder();
+ let buffer = "";
+ try {
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+ buffer += decoder.decode(value, { stream: true });
+ let idx: number;
+ while ((idx = buffer.indexOf("\n")) >= 0) {
+ const line = buffer.slice(0, idx).trim();
+ buffer = buffer.slice(idx + 1);
+ if (line) yield line;
+ }
+ }
+ const tail = buffer.trim();
+ if (tail) yield tail;
+ } finally {
+ reader.releaseLock();
+ }
+}
+
+export function mapStreamEvent(event: StreamEvent): GatewayMappedEvent {
+ switch (event.type) {
+ case "text-delta":
+ return typeof event.text === "string" && event.text
+ ? { kind: "text", text: event.text }
+ : { kind: "ignore" };
+ case "reasoning-delta":
+ return typeof event.text === "string" && event.text
+ ? { kind: "reasoning", text: event.text }
+ : { kind: "ignore" };
+ case "reasoning-start":
+ case "reasoning-end":
+ return { kind: "ignore" };
+ case "tool-call": {
+ const id =
+ (typeof event.toolCallId === "string" && event.toolCallId) ||
+ `call_${randomUUID().replace(/-/g, "").slice(0, 24)}`;
+ const name =
+ (typeof event.toolName === "string" && event.toolName) || "unknown";
+ const raw = event.input ?? event.args ?? {};
+ let args: string;
+ try {
+ args =
+ typeof raw === "string"
+ ? raw
+ : JSON.stringify(raw && typeof raw === "object" ? raw : { value: raw });
+ } catch {
+ args = "{}";
+ }
+ return {
+ kind: "tool_call",
+ id,
+ name,
+ arguments: args,
+ providerExecuted: event.providerExecuted === true,
+ };
+ }
+ case "tool-result": {
+ const id =
+ (typeof event.toolCallId === "string" && event.toolCallId) || "";
+ const raw = event.output ?? event.result ?? "";
+ let output: string;
+ if (typeof raw === "string") output = raw;
+ else if (raw && typeof raw === "object" && "value" in (raw as object)) {
+ output = String((raw as { value: unknown }).value);
+ } else {
+ try {
+ output = JSON.stringify(raw);
+ } catch {
+ output = String(raw);
+ }
+ }
+ return {
+ kind: "tool_result",
+ id,
+ output,
+ isError: event.isError === true,
+ };
+ }
+ case "finish": {
+ const usage = emptyUsage();
+ const total = event.totalUsage as
+ | {
+ inputTokens?: number;
+ outputTokens?: number;
+ inputTokenDetails?: {
+ cacheReadTokens?: number;
+ cacheWriteTokens?: number;
+ };
+ }
+ | undefined;
+ if (total) {
+ usage.inputTokens = total.inputTokens ?? 0;
+ usage.outputTokens = total.outputTokens ?? 0;
+ usage.cacheReadTokens =
+ total.inputTokenDetails?.cacheReadTokens ?? 0;
+ usage.cacheWriteTokens =
+ total.inputTokenDetails?.cacheWriteTokens ?? 0;
+ }
+ const finishReason =
+ event.finishReason === "tool-calls"
+ ? "tool_calls"
+ : event.finishReason === "length"
+ ? "length"
+ : "stop";
+ return {
+ kind: "finish",
+ finishReason,
+ usage,
+ systemPromptTokens:
+ typeof event.systemPromptTokens === "number"
+ ? event.systemPromptTokens
+ : undefined,
+ };
+ }
+ case "error": {
+ const err = event.error;
+ const text =
+ typeof err === "string"
+ ? err
+ : err && typeof err === "object" && "message" in err
+ ? String((err as { message: unknown }).message)
+ : "Command Code stream error";
+ return { kind: "error", text };
+ }
+ case "abort":
+ return { kind: "error", text: "aborted" };
+ default:
+ return { kind: "ignore" };
+ }
+}
+
+/**
+ * Stream mapped events from Command Code /alpha/generate.
+ */
+export async function* streamGenerate(
+ params: GatewayGenerateParams,
+): AsyncGenerator {
+ const body = buildGenerateBody(params);
+ const headers = buildAuthHeaders({
+ apiKey: params.apiKey,
+ sessionId: params.sessionId,
+ });
+
+ let stream: ReadableStream;
+ if (params.postStream) {
+ stream = await params.postStream(body, headers);
+ } else {
+ const fetchFn = params.fetchFn ?? fetch;
+ const baseUrl = (params.baseUrl ?? getApiBaseUrl()).replace(/\/$/, "");
+ const url = `${baseUrl}${GENERATE_ROUTE}`;
+ log.info("[opencode-commandcode] POST /alpha/generate", {
+ model: params.model,
+ toolCount: params.tools?.length ?? 0,
+ messageCount: params.messages.length,
+ });
+ let res: Response;
+ try {
+ res = await fetchFn(url, {
+ method: "POST",
+ headers,
+ body: JSON.stringify(body),
+ signal: params.signal,
+ });
+ } catch (err) {
+ yield {
+ kind: "error",
+ text:
+ err instanceof Error
+ ? `network error: ${err.message}`
+ : "network error",
+ };
+ return;
+ }
+ if (!res.ok) {
+ const text = await res.text().catch(() => "");
+ yield {
+ kind: "error",
+ text: `POST ${GENERATE_ROUTE} → ${res.status} ${text.slice(0, 500)}`,
+ };
+ return;
+ }
+ if (!res.body) {
+ yield { kind: "error", text: "empty stream body" };
+ return;
+ }
+ stream = res.body;
+ }
+
+ for await (const line of readNdjsonLines(stream)) {
+ let parsed: StreamEvent;
+ try {
+ parsed = JSON.parse(line) as StreamEvent;
+ } catch {
+ continue;
+ }
+ yield mapStreamEvent(parsed);
+ }
+}
+
+export function stableCompletionId(seed: string): string {
+ return `chatcmpl_${createHash("sha1").update(seed).digest("hex").slice(0, 24)}`;
+}
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..1ed65a9
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,470 @@
+/**
+ * OpenCode Command Code Auth Plugin
+ *
+ * Enables Command Code (Laguna S 2.1 free and gateway models) inside OpenCode via:
+ * 1. Command Code CLI credential sync or API key auth
+ * 2. Local OpenAI-compatible proxy → api.commandcode.ai /alpha/generate
+ * 3. Tools/MCP park-resume, attachments, compact, and usage accounting
+ *
+ * Register in opencode.json:
+ * { "plugin": ["@otto-assistant/opencode-commandcode"] }
+ */
+import type { Hooks, Plugin, PluginInput } from "@opencode-ai/plugin";
+import {
+ syncCommandCodeCredentialsToOpenCode,
+ validateCommandApiKey,
+ writeCommandCodeAuth,
+} from "./auth-login.js";
+import {
+ DEFAULT_MODEL_ID,
+ EFFORT_HEADER,
+ OPENAI_COMPATIBLE_NPM,
+ PROVIDER_ID,
+ SESSION_HEADER,
+} from "./constants.js";
+import { detectCommandCode } from "./detect.js";
+import { log } from "./log.js";
+import {
+ encodeCommandModelSelection,
+ resolveCommandModelSelection,
+} from "./model-selection.js";
+import {
+ buildConfigVariants,
+ buildEffortVariants,
+ getCommandModels,
+ type CommandModel,
+} from "./models.js";
+import {
+ getCommandProxyBaseUrl,
+ getProxyPort,
+ startProxy,
+} from "./proxy.js";
+
+type CommandApiAuth = {
+ type: "api" | "oauth";
+ key?: string;
+ access?: string;
+ refresh?: string;
+ expires?: number;
+};
+
+function isCommandAuth(auth: unknown): auth is CommandApiAuth {
+ if (!auth || typeof auth !== "object") return false;
+ const a = auth as CommandApiAuth;
+ return a.type === "api" || a.type === "oauth";
+}
+
+function authKey(auth: CommandApiAuth | null | undefined): string | null {
+ if (!auth) return null;
+ if (typeof auth.key === "string" && auth.key.trim()) return auth.key.trim();
+ if (typeof auth.access === "string" && auth.access.trim()) {
+ return auth.access.trim();
+ }
+ return null;
+}
+
+function zeroCost() {
+ return {
+ input: 0,
+ output: 0,
+ cache: { read: 0, write: 0 },
+ };
+}
+
+function buildProviderModel(
+ model: CommandModel,
+ id: string,
+ baseURL: string,
+): Record {
+ const variants = buildEffortVariants(model);
+ const hasEffort = Object.values(variants).some(
+ (v) => v && typeof v === "object" && "effort" in v,
+ );
+ return {
+ id,
+ providerID: PROVIDER_ID,
+ api: {
+ id,
+ url: baseURL,
+ npm: OPENAI_COMPATIBLE_NPM,
+ },
+ name:
+ id === DEFAULT_MODEL_ID && model.id !== DEFAULT_MODEL_ID
+ ? `Default (${model.name})`
+ : model.name,
+ capabilities: {
+ temperature: true,
+ reasoning: hasEffort,
+ attachment: true,
+ toolcall: true,
+ input: {
+ text: true,
+ audio: false,
+ image: model.vision,
+ video: false,
+ pdf: true,
+ },
+ output: {
+ text: true,
+ audio: false,
+ image: false,
+ video: false,
+ pdf: false,
+ },
+ interleaved: true,
+ },
+ modalities: {
+ input: model.vision ? ["text", "image"] : ["text"],
+ output: ["text"],
+ },
+ cost: zeroCost(),
+ limit: {
+ context: model.contextWindow,
+ output: model.maxTokens,
+ },
+ status: "active",
+ options: {
+ includeUsage: true,
+ },
+ headers: {},
+ release_date: "",
+ variants,
+ };
+}
+
+function buildConfigModelEntry(model: CommandModel): Record {
+ const variants = buildConfigVariants(model);
+ return {
+ name: model.name,
+ reasoning: false,
+ tool_call: true,
+ modalities: {
+ input: model.vision ? ["text", "image"] : ["text"],
+ output: ["text"],
+ },
+ capabilities: {
+ tools: true,
+ input: model.vision ? ["text", "image"] : ["text"],
+ output: ["text"],
+ },
+ limit: {
+ context: model.contextWindow,
+ output: model.maxTokens,
+ },
+ options: {
+ includeUsage: true,
+ },
+ variants,
+ };
+}
+
+function buildProviderModels(
+ models: CommandModel[],
+): Record {
+ const baseURL = getCommandProxyBaseUrl();
+ const providerModels = Object.fromEntries(
+ models.map((model) => [
+ model.id,
+ buildProviderModel(model, model.id, baseURL),
+ ]),
+ );
+ const defaultModel =
+ models.find((m) => m.id === DEFAULT_MODEL_ID) || models[0];
+ if (defaultModel && !(DEFAULT_MODEL_ID in providerModels)) {
+ providerModels[DEFAULT_MODEL_ID] = buildProviderModel(
+ defaultModel,
+ DEFAULT_MODEL_ID,
+ baseURL,
+ );
+ }
+ return providerModels;
+}
+
+function ensureProviderConfig(
+ config: Record,
+ models: CommandModel[],
+): void {
+ if (!config.provider || typeof config.provider !== "object") {
+ config.provider = {};
+ }
+ const existing = config.provider[PROVIDER_ID] ?? {};
+ const existingOptions =
+ existing.options && typeof existing.options === "object"
+ ? existing.options
+ : {};
+ const existingModels =
+ existing.models && typeof existing.models === "object"
+ ? existing.models
+ : {};
+
+ const baseURL = getCommandProxyBaseUrl();
+ const seededModels = Object.fromEntries(
+ models.map((model) => [model.id, buildConfigModelEntry(model)]),
+ );
+ const defaultModel =
+ models.find((m) => m.id === DEFAULT_MODEL_ID) || models[0];
+ if (defaultModel && !(DEFAULT_MODEL_ID in seededModels)) {
+ seededModels[DEFAULT_MODEL_ID] = {
+ ...buildConfigModelEntry(defaultModel),
+ name: `Default (${defaultModel.name})`,
+ };
+ }
+
+ config.provider[PROVIDER_ID] = {
+ ...existing,
+ name:
+ typeof existing.name === "string" && existing.name.trim()
+ ? existing.name
+ : "Command Code",
+ npm: existing.npm ?? OPENAI_COMPATIBLE_NPM,
+ options: {
+ baseURL,
+ apiKey: "command-code-proxy",
+ includeUsage: true,
+ ...existingOptions,
+ },
+ models: {
+ ...seededModels,
+ ...existingModels,
+ },
+ };
+}
+
+async function resolveAccessToken(
+ input: PluginInput,
+ getAuth: () => Promise,
+): Promise {
+ const auth = await getAuth();
+ if (isCommandAuth(auth)) {
+ const key = authKey(auth);
+ if (key) return key;
+ }
+
+ const synced = syncCommandCodeCredentialsToOpenCode();
+ if (synced) {
+ try {
+ await input.client.auth.set({
+ path: { id: PROVIDER_ID },
+ body: {
+ type: "api",
+ key: synced.key || synced.access,
+ },
+ });
+ } catch {
+ // auth.set may be unavailable in some hosts
+ }
+ return synced.access;
+ }
+ return null;
+}
+
+async function loadRuntime(
+ input: PluginInput,
+ getAuth: () => Promise,
+ provider?: { models?: Record },
+): Promise<{ port: number; providerModels: Record } | undefined> {
+ await resolveAccessToken(input, getAuth);
+ const models = getCommandModels();
+ await startProxy(async () => resolveAccessToken(input, getAuth));
+ const providerModels = buildProviderModels(models);
+ if (provider) provider.models = providerModels;
+ return { port: getProxyPort() ?? 8797, providerModels };
+}
+
+/**
+ * OpenCode plugin that provides Command Code authentication and model access.
+ */
+export const CommandCodePlugin: Plugin = async (
+ input: PluginInput,
+): Promise => {
+ try {
+ syncCommandCodeCredentialsToOpenCode();
+ } catch (err) {
+ log.warn(
+ "[opencode-commandcode] CLI credential sync skipped",
+ err instanceof Error ? err.message : err,
+ );
+ }
+
+ return {
+ async config(config) {
+ // Always seed Laguna catalog so the provider is discoverable pre-login.
+ ensureProviderConfig(config as Record, getCommandModels());
+
+ await startProxy(async () => {
+ try {
+ const authClient = input.client.auth as {
+ get?: (args: { path: { id: string } }) => Promise;
+ };
+ if (typeof authClient.get === "function") {
+ const auth = await authClient.get({ path: { id: PROVIDER_ID } });
+ const payload =
+ auth && typeof auth === "object" && "data" in auth
+ ? (auth as { data: unknown }).data
+ : auth;
+ return resolveAccessToken(input, async () => payload);
+ }
+ } catch {
+ // ignore
+ }
+ const synced = syncCommandCodeCredentialsToOpenCode();
+ return synced?.access ?? null;
+ });
+ },
+
+ "chat.headers": async (hookInput, output) => {
+ if (hookInput.model.providerID !== PROVIDER_ID) return;
+ const messageModel = hookInput.message.model as {
+ variant?: unknown;
+ };
+ const variant =
+ typeof messageModel.variant === "string"
+ ? messageModel.variant
+ : undefined;
+ const selected = resolveCommandModelSelection(
+ hookInput.model.id,
+ variant,
+ );
+ output.headers[EFFORT_HEADER] = encodeCommandModelSelection(selected);
+ if (hookInput.sessionID) {
+ output.headers[SESSION_HEADER] = hookInput.sessionID;
+ }
+ },
+
+ "chat.params": async (hookInput, output) => {
+ if (hookInput.model.providerID !== PROVIDER_ID) return;
+ delete output.options.reasoningEffort;
+ },
+
+ provider: {
+ id: PROVIDER_ID,
+ async models(provider, ctx) {
+ const runtime = await loadRuntime(
+ input,
+ async () => ctx.auth,
+ provider,
+ );
+ return (runtime?.providerModels ?? {}) as Record;
+ },
+ },
+
+ auth: {
+ provider: PROVIDER_ID,
+
+ async loader(getAuth, provider) {
+ const runtime = await loadRuntime(input, getAuth, provider);
+ if (!runtime) return {};
+
+ return {
+ baseURL: getCommandProxyBaseUrl(),
+ apiKey: "command-code-proxy",
+ async fetch(
+ requestInput: RequestInfo | URL,
+ init?: RequestInit,
+ ) {
+ if (init?.headers) {
+ if (init.headers instanceof Headers) {
+ init.headers.delete("authorization");
+ } else if (Array.isArray(init.headers)) {
+ init.headers = init.headers.filter(
+ ([key]) => key.toLowerCase() !== "authorization",
+ );
+ } else {
+ delete (init.headers as Record).authorization;
+ delete (init.headers as Record).Authorization;
+ }
+ }
+ return fetch(requestInput, init);
+ },
+ };
+ },
+
+ methods: [
+ {
+ type: "oauth",
+ label: "Use Command Code CLI login",
+ async authorize() {
+ const synced = syncCommandCodeCredentialsToOpenCode();
+ if (synced) {
+ return {
+ url: "https://commandcode.ai/docs/quickstart",
+ instructions:
+ "Command Code CLI credentials were found and synced. Click Complete to finish.",
+ method: "auto" as const,
+ async callback() {
+ return {
+ type: "success" as const,
+ key: synced.key || synced.access,
+ };
+ },
+ };
+ }
+
+ return {
+ url: "https://commandcode.ai/docs/quickstart",
+ instructions:
+ "Run `npm i -g command-code@latest && cmd login` in a terminal, then click Complete. Or choose API key instead.",
+ method: "auto" as const,
+ async callback() {
+ const again = syncCommandCodeCredentialsToOpenCode();
+ if (!again) return { type: "failed" as const };
+ return {
+ type: "success" as const,
+ key: again.key || again.access,
+ };
+ },
+ };
+ },
+ },
+ {
+ type: "api",
+ label: "Command Code API key",
+ prompts: [
+ {
+ type: "text",
+ key: "apiKey",
+ message: "Paste your Command Code API key",
+ placeholder: "from Studio → Settings, or COMMAND_CODE_API_KEY",
+ },
+ ],
+ async authorize(inputs) {
+ const key = (inputs?.apiKey || inputs?.key || "").trim();
+ if (!key) return { type: "failed" as const };
+ const validated = await validateCommandApiKey(key);
+ if (!validated.valid) {
+ log.error(
+ "[opencode-commandcode] API key validation failed",
+ validated.error,
+ );
+ return { type: "failed" as const };
+ }
+ writeCommandCodeAuth({
+ access: key,
+ refresh: "api-key",
+ expires: Date.now() + 365 * 24 * 60 * 60 * 1000,
+ key,
+ });
+ return { type: "success" as const, key };
+ },
+ },
+ ],
+ },
+ };
+};
+
+export default CommandCodePlugin;
+
+export { detectCommandCode } from "./detect.js";
+export { getCommandModels, COMMAND_CODE_MODELS } from "./models.js";
+export {
+ startProxy,
+ stopProxy,
+ getCommandProxyBaseUrl,
+ setStreamGenerateForTests,
+} from "./proxy.js";
+export {
+ getSessionUsage,
+ listSessionUsage,
+ totalUsageAcrossSessions,
+ resetUsageStore,
+} from "./usage.js";
diff --git a/src/log.ts b/src/log.ts
new file mode 100644
index 0000000..578c26d
--- /dev/null
+++ b/src/log.ts
@@ -0,0 +1,50 @@
+import {
+ COMMAND_CODE_API_KEY_ENV,
+ COMMAND_CODE_AUTH_FILE,
+ COMMAND_CODE_DIR_NAME,
+ DEFAULT_API_BASE_URL,
+} from "./constants.js";
+
+const DEBUG =
+ process.env.OPENCODE_COMMANDCODE_DEBUG === "1" ||
+ process.env.OPENCODE_COMMANDCODE_DEBUG === "true";
+
+function stamp(): string {
+ return new Date().toISOString();
+}
+
+export const log = {
+ info(...args: unknown[]) {
+ if (DEBUG) console.error(`[opencode-commandcode ${stamp()}]`, ...args);
+ },
+ warn(...args: unknown[]) {
+ console.error(`[opencode-commandcode ${stamp()} WARN]`, ...args);
+ },
+ error(...args: unknown[]) {
+ console.error(`[opencode-commandcode ${stamp()} ERROR]`, ...args);
+ },
+};
+
+export function getApiBaseUrl(
+ env: NodeJS.ProcessEnv | Record = process.env,
+): string {
+ const raw =
+ typeof env.COMMANDCODE_API_URL === "string"
+ ? env.COMMANDCODE_API_URL.trim()
+ : typeof env.OPENCODE_COMMANDCODE_API_URL === "string"
+ ? env.OPENCODE_COMMANDCODE_API_URL.trim()
+ : "";
+ return (raw || DEFAULT_API_BASE_URL).replace(/\/$/, "");
+}
+
+export function getCommandCodeDirName(): string {
+ return COMMAND_CODE_DIR_NAME;
+}
+
+export function getCommandCodeAuthFileName(): string {
+ return COMMAND_CODE_AUTH_FILE;
+}
+
+export function getApiKeyEnvName(): string {
+ return COMMAND_CODE_API_KEY_ENV;
+}
diff --git a/src/model-selection.ts b/src/model-selection.ts
new file mode 100644
index 0000000..bd525a0
--- /dev/null
+++ b/src/model-selection.ts
@@ -0,0 +1,47 @@
+import {
+ EFFORT_HEADER,
+ isCommandEffort,
+ type CommandEffort,
+ DEFAULT_MODEL_ID,
+} from "./constants.js";
+import { resolveCommandModelId } from "./models.js";
+
+export { EFFORT_HEADER };
+
+export type CommandModelSelection = {
+ modelId: string;
+ effort?: CommandEffort;
+};
+
+export function resolveCommandModelSelection(
+ modelId: string,
+ variant?: string,
+): CommandModelSelection {
+ const resolved = resolveCommandModelId(modelId || DEFAULT_MODEL_ID);
+ const effort = isCommandEffort(variant) ? variant : undefined;
+ return effort ? { modelId: resolved, effort } : { modelId: resolved };
+}
+
+export function encodeCommandModelSelection(
+ selection: CommandModelSelection,
+): string {
+ return selection.effort
+ ? `${selection.modelId}|${selection.effort}`
+ : selection.modelId;
+}
+
+export function decodeCommandModelSelection(
+ header: string | null | undefined,
+): CommandModelSelection | null {
+ if (!header || typeof header !== "string") return null;
+ const trimmed = header.trim();
+ if (!trimmed) return null;
+ const pipe = trimmed.indexOf("|");
+ if (pipe < 0) {
+ return { modelId: resolveCommandModelId(trimmed) };
+ }
+ const modelId = resolveCommandModelId(trimmed.slice(0, pipe));
+ const effortRaw = trimmed.slice(pipe + 1);
+ const effort = isCommandEffort(effortRaw) ? effortRaw : undefined;
+ return effort ? { modelId, effort } : { modelId };
+}
diff --git a/src/models.ts b/src/models.ts
new file mode 100644
index 0000000..75fecbf
--- /dev/null
+++ b/src/models.ts
@@ -0,0 +1,143 @@
+/**
+ * Command Code model catalog — Laguna S 2.1 free is the primary target.
+ */
+import { EFFORT_LEVELS, type CommandEffort, LAGUNA_MODEL_ID } from "./constants.js";
+
+export type CommandModel = {
+ id: string;
+ name: string;
+ resolvedId: string;
+ reasoning: boolean;
+ contextWindow: number;
+ maxTokens: number;
+ /** Vision / image input support on the upstream model. */
+ vision: boolean;
+ /** Free-tier badge. */
+ free?: boolean;
+};
+
+const LIMIT_256K = { context: 256_000, output: 32_768 } as const;
+
+function model(
+ id: string,
+ name: string,
+ resolvedId: string,
+ limit: { context: number; output: number },
+ opts?: { vision?: boolean; free?: boolean; reasoning?: boolean },
+): CommandModel {
+ return {
+ id,
+ name,
+ resolvedId,
+ reasoning: opts?.reasoning !== false,
+ contextWindow: limit.context,
+ maxTokens: limit.output,
+ vision: opts?.vision === true,
+ free: opts?.free,
+ };
+}
+
+/**
+ * Primary catalog for this plugin. Laguna is the default and exclusive
+ * live-test target; aliases keep OpenCode menus short.
+ */
+export const COMMAND_CODE_MODELS: CommandModel[] = [
+ model("laguna-s-2.1-free", "Laguna S 2.1 (free)", LAGUNA_MODEL_ID, LIMIT_256K, {
+ free: true,
+ vision: false,
+ }),
+ model("laguna", "Laguna S 2.1 (free)", LAGUNA_MODEL_ID, LIMIT_256K, {
+ free: true,
+ vision: false,
+ }),
+ model(
+ "poolside/laguna-s-2.1-free",
+ "Laguna S 2.1 (free)",
+ LAGUNA_MODEL_ID,
+ LIMIT_256K,
+ { free: true, vision: false },
+ ),
+];
+
+/** Placeholder so OpenCode keeps the provider visible while logged out. */
+export const LOGIN_PLACEHOLDER_MODELS: CommandModel[] = [
+ {
+ id: "login",
+ name: "Sign in to Command Code",
+ resolvedId: "login",
+ reasoning: false,
+ contextWindow: 256_000,
+ maxTokens: 8_192,
+ vision: false,
+ },
+];
+
+export const GENERATED_VARIANT_KEYS = [
+ "none",
+ "minimal",
+ "low",
+ "medium",
+ "high",
+ "xhigh",
+ "max",
+] as const;
+
+export function isLoginPlaceholderModel(id: string): boolean {
+ return id === "login";
+}
+
+export function getCommandModels(): CommandModel[] {
+ // Deduplicate by id while preferring shorter aliases first.
+ const seen = new Set();
+ const out: CommandModel[] = [];
+ for (const m of COMMAND_CODE_MODELS) {
+ if (seen.has(m.id)) continue;
+ seen.add(m.id);
+ out.push(m);
+ }
+ return out;
+}
+
+export function resolveCommandModelId(modelId: string): string {
+ const cleaned = modelId.replace(/^command-code\//, "").trim();
+ const match = COMMAND_CODE_MODELS.find(
+ (m) => m.id === cleaned || m.resolvedId === cleaned,
+ );
+ if (match) return match.resolvedId;
+ // Accept short name after last slash.
+ if (!cleaned.includes("/")) {
+ const bySuffix = COMMAND_CODE_MODELS.find((m) =>
+ m.resolvedId.endsWith(`/${cleaned}`),
+ );
+ if (bySuffix) return bySuffix.resolvedId;
+ }
+ return cleaned || LAGUNA_MODEL_ID;
+}
+
+export function findCommandModel(modelId: string): CommandModel | undefined {
+ const resolved = resolveCommandModelId(modelId);
+ return (
+ COMMAND_CODE_MODELS.find((m) => m.id === modelId || m.resolvedId === resolved) ||
+ COMMAND_CODE_MODELS.find((m) => m.resolvedId === resolved)
+ );
+}
+
+export function buildEffortVariants(
+ model: CommandModel,
+): Record {
+ if (!model.reasoning || isLoginPlaceholderModel(model.id)) return {};
+ const variants: Record<
+ string,
+ { effort: CommandEffort } | { disabled: true }
+ > = Object.fromEntries(EFFORT_LEVELS.map((effort) => [effort, { effort }]));
+ for (const key of GENERATED_VARIANT_KEYS) {
+ if (!(key in variants)) variants[key] = { disabled: true };
+ }
+ return variants;
+}
+
+export function buildConfigVariants(
+ model: CommandModel,
+): Record {
+ return buildEffortVariants(model);
+}
diff --git a/src/prompt.ts b/src/prompt.ts
new file mode 100644
index 0000000..243035f
--- /dev/null
+++ b/src/prompt.ts
@@ -0,0 +1,392 @@
+/**
+ * Build Command Code wire messages from OpenAI-compatible chat messages,
+ * including text, images, PDFs, and generic file attachments.
+ *
+ * Laguna S 2.1 is text-only — images become placeholders (matching Command Code
+ * stripImages behavior) while text/PDF/file payloads are inlined when possible.
+ */
+import type {
+ WireAssistantContent,
+ WireMessage,
+ WireTool,
+ WireUserContent,
+} from "./gateway-types.js";
+
+const IMAGE_OMITTED =
+ "[image omitted: the active model is text-only]";
+
+function parseDataUrl(url: string): {
+ mediaType: string;
+ data: string;
+} | null {
+ const match = /^data:([^;,]+)?(?:;charset=[^;,]+)?;base64,(.+)$/i.exec(
+ url.trim(),
+ );
+ if (!match) return null;
+ return {
+ mediaType: (match[1] || "application/octet-stream").toLowerCase(),
+ data: match[2],
+ };
+}
+
+function isHttpUrl(url: string): boolean {
+ return /^https?:\/\//i.test(url.trim());
+}
+
+function mediaLooksLikePdf(mediaType: string, urlHint = ""): boolean {
+ return (
+ mediaType.includes("pdf") ||
+ /\.pdf(\?|#|$)/i.test(urlHint) ||
+ (mediaType === "application/octet-stream" &&
+ /\.pdf(\?|#|$)/i.test(urlHint))
+ );
+}
+
+function mediaLooksLikeImage(mediaType: string): boolean {
+ return mediaType.startsWith("image/");
+}
+
+function mediaLooksLikeText(mediaType: string, name = ""): boolean {
+ if (mediaType.startsWith("text/")) return true;
+ if (
+ /json|xml|javascript|typescript|yaml|toml|markdown|csv|html|css/.test(
+ mediaType,
+ )
+ ) {
+ return true;
+ }
+ return /\.(txt|md|json|js|ts|tsx|jsx|py|rs|go|java|c|cpp|h|css|html|yml|yaml|toml|csv|xml|sh|bash|zsh|sql)$/i.test(
+ name,
+ );
+}
+
+function tryDecodeBase64Text(data: string, limit = 200_000): string | null {
+ try {
+ const buf = Buffer.from(data, "base64");
+ if (buf.length > limit) {
+ return (
+ buf.subarray(0, limit).toString("utf8") +
+ `\n…[truncated ${buf.length - limit} bytes]`
+ );
+ }
+ const text = buf.toString("utf8");
+ // Reject if mostly non-printable.
+ const sample = text.slice(0, 2000);
+ const bad = [...sample].filter((ch) => {
+ const code = ch.charCodeAt(0);
+ return code < 9 || (code > 13 && code < 32);
+ }).length;
+ if (bad > sample.length * 0.05) return null;
+ return text;
+ } catch {
+ return null;
+ }
+}
+
+export function extractTextContent(content: unknown): string {
+ if (typeof content === "string") return content;
+ if (!Array.isArray(content)) return "";
+ return content
+ .map((part) => {
+ if (!part || typeof part !== "object") return "";
+ const p = part as { type?: string; text?: string };
+ if (p.type === "text" && typeof p.text === "string") return p.text;
+ return "";
+ })
+ .filter(Boolean)
+ .join("\n");
+}
+
+export function contentHasAttachments(content: unknown): boolean {
+ if (!Array.isArray(content)) return false;
+ return content.some((part) => {
+ if (!part || typeof part !== "object") return false;
+ const type = (part as { type?: unknown }).type;
+ return (
+ type === "image_url" ||
+ type === "input_image" ||
+ type === "file" ||
+ type === "input_file" ||
+ type === "image" ||
+ type === "document"
+ );
+ });
+}
+
+type AttachmentSink = {
+ texts: string[];
+ images: Array<{ dataUrl: string; mimeType: string }>;
+ vision: boolean;
+};
+
+function pushAttachmentPart(part: unknown, sink: AttachmentSink): void {
+ if (!part || typeof part !== "object") return;
+ const p = part as Record;
+ const type = typeof p.type === "string" ? p.type : "";
+
+ if (type === "text" && typeof p.text === "string") {
+ sink.texts.push(p.text);
+ return;
+ }
+
+ if (type === "image_url" || type === "input_image" || type === "image") {
+ const imageUrl = p.image_url ?? p.image ?? p.url;
+ const url =
+ typeof imageUrl === "string"
+ ? imageUrl
+ : imageUrl &&
+ typeof imageUrl === "object" &&
+ typeof (imageUrl as { url?: unknown }).url === "string"
+ ? (imageUrl as { url: string }).url
+ : typeof p.data === "string"
+ ? `data:${typeof p.media_type === "string" ? p.media_type : "image/png"};base64,${p.data}`
+ : null;
+ if (!url) return;
+ const parsed = parseDataUrl(url);
+ if (parsed && mediaLooksLikeImage(parsed.mediaType)) {
+ if (sink.vision) {
+ sink.images.push({
+ dataUrl: url,
+ mimeType: parsed.mediaType,
+ });
+ } else {
+ sink.texts.push(
+ `\n${IMAGE_OMITTED}\n(media_type=${parsed.mediaType})\n`,
+ );
+ }
+ return;
+ }
+ if (isHttpUrl(url)) {
+ if (sink.vision) {
+ sink.images.push({ dataUrl: url, mimeType: "image/*" });
+ } else {
+ sink.texts.push(
+ `\n${IMAGE_OMITTED}\n(url=${url})\n`,
+ );
+ }
+ }
+ return;
+ }
+
+ if (type === "file" || type === "input_file" || type === "document") {
+ const file = (p.file && typeof p.file === "object" ? p.file : p) as Record<
+ string,
+ unknown
+ >;
+ const url = typeof file.url === "string" ? file.url : null;
+ const data = typeof file.data === "string" ? file.data : null;
+ const mediaType =
+ typeof file.media_type === "string"
+ ? file.media_type
+ : typeof file.mime_type === "string"
+ ? file.mime_type
+ : "application/octet-stream";
+ const name =
+ typeof file.filename === "string"
+ ? file.filename
+ : typeof file.name === "string"
+ ? file.name
+ : "";
+
+ if (url) {
+ const parsed = parseDataUrl(url);
+ if (parsed) {
+ handleBinaryAttachment(sink, parsed.mediaType, parsed.data, name);
+ return;
+ }
+ if (isHttpUrl(url)) {
+ sink.texts.push(
+ `\nRemote file URL: ${url}\n`,
+ );
+ }
+ return;
+ }
+ if (data) {
+ handleBinaryAttachment(sink, mediaType, data, name);
+ }
+ }
+}
+
+function handleBinaryAttachment(
+ sink: AttachmentSink,
+ mediaType: string,
+ data: string,
+ name: string,
+): void {
+ if (mediaLooksLikeImage(mediaType)) {
+ const dataUrl = `data:${mediaType};base64,${data}`;
+ if (sink.vision) {
+ sink.images.push({ dataUrl, mimeType: mediaType });
+ } else {
+ sink.texts.push(
+ `\n${IMAGE_OMITTED}\n`,
+ );
+ }
+ return;
+ }
+
+ if (mediaLooksLikePdf(mediaType, name)) {
+ sink.texts.push(
+ `\n[PDF attachment base64 length=${data.length}. The active model is text-only; summarize from filename/context or ask the user for extracted text.]\n`,
+ );
+ return;
+ }
+
+ if (mediaLooksLikeText(mediaType, name)) {
+ const decoded = tryDecodeBase64Text(data);
+ if (decoded !== null) {
+ sink.texts.push(
+ `\n${decoded}\n`,
+ );
+ return;
+ }
+ }
+
+ sink.texts.push(
+ `\n[binary attachment, base64 length=${data.length}]\n`,
+ );
+}
+
+export function openaiContentToUserParts(
+ content: unknown,
+ vision: boolean,
+): WireUserContent {
+ if (typeof content === "string") {
+ const text = content.trim();
+ return text ? [{ type: "text", text }] : [];
+ }
+ if (!Array.isArray(content)) return [];
+
+ const sink: AttachmentSink = { texts: [], images: [], vision };
+ for (const part of content) pushAttachmentPart(part, sink);
+
+ const out: WireUserContent = [];
+ for (const text of sink.texts) {
+ if (text.trim()) out.push({ type: "text", text });
+ }
+ for (const img of sink.images) {
+ out.push({
+ type: "image",
+ image: img.dataUrl,
+ mimeType: img.mimeType,
+ });
+ }
+ return out;
+}
+
+export type OpenAITool = {
+ type?: string;
+ function?: {
+ name?: string;
+ description?: string;
+ parameters?: Record;
+ };
+};
+
+export type OpenAIMessage = {
+ role?: string;
+ content?: unknown;
+ tool_calls?: Array<{
+ id?: string;
+ type?: string;
+ function?: { name?: string; arguments?: string };
+ }>;
+ tool_call_id?: string;
+ name?: string;
+};
+
+export function openaiToolsToWire(tools: OpenAITool[]): WireTool[] {
+ return tools
+ .map((t) => {
+ const name = t.function?.name;
+ if (!name) return null;
+ return {
+ name,
+ description: t.function?.description || name,
+ input_schema:
+ (t.function?.parameters as Record) || {
+ type: "object",
+ properties: {},
+ },
+ };
+ })
+ .filter((t): t is WireTool => Boolean(t));
+}
+
+/**
+ * Convert a full OpenAI chat history into Command Code wire messages.
+ * System messages are collected separately.
+ */
+export function openaiMessagesToWire(
+ messages: OpenAIMessage[],
+ options: { vision: boolean },
+): { system: string; messages: WireMessage[] } {
+ const systemParts: string[] = [];
+ const wire: WireMessage[] = [];
+
+ for (const msg of messages) {
+ const role = msg.role || "";
+ if (role === "system" || role === "developer") {
+ const text = extractTextContent(msg.content).trim();
+ if (text) systemParts.push(text);
+ continue;
+ }
+
+ if (role === "user") {
+ const parts = openaiContentToUserParts(msg.content, options.vision);
+ if (parts.length > 0) wire.push({ role: "user", content: parts });
+ continue;
+ }
+
+ if (role === "assistant") {
+ const content: WireAssistantContent = [];
+ const text = extractTextContent(msg.content).trim();
+ if (text) content.push({ type: "text", text });
+ if (Array.isArray(msg.tool_calls)) {
+ for (const tc of msg.tool_calls) {
+ const id = tc.id || "";
+ const name = tc.function?.name || "";
+ if (!id || !name) continue;
+ let input: Record = {};
+ try {
+ input = JSON.parse(tc.function?.arguments || "{}") as Record<
+ string,
+ unknown
+ >;
+ } catch {
+ input = { raw: tc.function?.arguments || "" };
+ }
+ content.push({
+ type: "tool-call",
+ toolCallId: id,
+ toolName: name,
+ input,
+ });
+ }
+ }
+ if (content.length > 0) wire.push({ role: "assistant", content });
+ continue;
+ }
+
+ if (role === "tool") {
+ const toolCallId = msg.tool_call_id || "";
+ if (!toolCallId) continue;
+ wire.push({
+ role: "tool",
+ content: [
+ {
+ type: "tool-result",
+ toolCallId,
+ toolName: msg.name || "",
+ output: {
+ type: "text",
+ value: extractTextContent(msg.content),
+ },
+ },
+ ],
+ });
+ }
+ }
+
+ return { system: systemParts.join("\n\n"), messages: wire };
+}
diff --git a/src/proxy.ts b/src/proxy.ts
new file mode 100644
index 0000000..85e7e65
--- /dev/null
+++ b/src/proxy.ts
@@ -0,0 +1,829 @@
+/**
+ * Local OpenAI-compatible proxy → Command Code /alpha/generate gateway.
+ *
+ * Accepts POST /v1/chat/completions, streams OpenAI-format SSE.
+ * Tool calls park (bridge-pool) until OpenCode returns tool results.
+ */
+import { randomUUID } from "node:crypto";
+import {
+ deleteBridge,
+ findBridgeByConversation,
+ findBridgeByPendingTool,
+ putBridge,
+ type ParkedBridge,
+ type ParkedToolCall,
+} from "./bridge-pool.js";
+import { assessContext, compactWireMessages, estimateMessageTokens } from "./compact.js";
+import {
+ DEFAULT_MODEL_ID,
+ LAGUNA_MODEL_ID,
+ SESSION_HEADER,
+} from "./constants.js";
+import {
+ decodeCommandModelSelection,
+ EFFORT_HEADER,
+} from "./model-selection.js";
+import { findCommandModel, getCommandModels, resolveCommandModelId } from "./models.js";
+import {
+ extractTextContent,
+ openaiMessagesToWire,
+ openaiToolsToWire,
+ type OpenAIMessage,
+ type OpenAITool,
+} from "./prompt.js";
+import {
+ stableCompletionId,
+ streamGenerate,
+ type GatewayMappedEvent,
+} from "./gateway.js";
+import type { WireMessage, WireTool, WireUsage } from "./gateway-types.js";
+import { emptyUsage } from "./gateway-types.js";
+import { log } from "./log.js";
+import {
+ conversationKeyFromMessages,
+ getSessionUsage,
+ listSessionUsage,
+ recordToolCall,
+ recordTurnUsage,
+ totalUsageAcrossSessions,
+ usageToOpenAI,
+} from "./usage.js";
+
+const DEFAULT_PROXY_PORT = 8797;
+const SHARED_PROXY_HEALTH_TIMEOUT_MS = 750;
+
+const COMMAND_PROXY_PORT: number = (() => {
+ const raw = process.env.OPENCODE_COMMANDCODE_PROXY_PORT;
+ const parsed = raw ? Number(raw) : NaN;
+ return Number.isInteger(parsed) && parsed > 0 && parsed < 65536
+ ? parsed
+ : DEFAULT_PROXY_PORT;
+})();
+
+const SSE_HEADERS = {
+ "Content-Type": "text/event-stream",
+ "Cache-Control": "no-cache",
+ Connection: "keep-alive",
+} as const;
+
+type TokenProvider = () => Promise;
+
+type ChatCompletionRequest = {
+ model?: string;
+ messages?: OpenAIMessage[];
+ tools?: OpenAITool[];
+ stream?: boolean;
+ temperature?: number;
+ stream_options?: { include_usage?: boolean };
+};
+
+let server: ReturnType | null = null;
+let proxyPort: number | null = null;
+let getAccessToken: TokenProvider | null = null;
+
+/** Optional injectors for tests. */
+let streamGenerateOverride:
+ | ((params: Parameters[0]) => ReturnType)
+ | null = null;
+
+export function setStreamGenerateForTests(
+ fn: typeof streamGenerateOverride,
+): void {
+ streamGenerateOverride = fn;
+}
+
+export function getCommandProxyBaseUrl(): string {
+ return `http://127.0.0.1:${COMMAND_PROXY_PORT}/v1`;
+}
+
+export function getProxyPort(): number | null {
+ return proxyPort ?? COMMAND_PROXY_PORT;
+}
+
+function isAddrInUseError(err: unknown): boolean {
+ if (!err || typeof err !== "object") return false;
+ const code = (err as { code?: unknown }).code;
+ const message = (err as { message?: unknown }).message;
+ return (
+ code === "EADDRINUSE" ||
+ (typeof message === "string" &&
+ /eaddrinuse|address already in use|in use/i.test(message))
+ );
+}
+
+async function isSharedProxyHealthy(): Promise {
+ const controller = new AbortController();
+ const timeout = setTimeout(
+ () => controller.abort(),
+ SHARED_PROXY_HEALTH_TIMEOUT_MS,
+ );
+ try {
+ const res = await fetch(`${getCommandProxyBaseUrl()}/models`, {
+ signal: controller.signal,
+ });
+ if (!res.ok) return false;
+ const body = (await res.json().catch(() => undefined)) as
+ | { object?: unknown; data?: unknown }
+ | undefined;
+ return !!body && body.object === "list" && Array.isArray(body.data);
+ } catch {
+ return false;
+ } finally {
+ clearTimeout(timeout);
+ }
+}
+
+export async function startProxy(tokenProvider: TokenProvider): Promise {
+ getAccessToken = tokenProvider;
+ if (server && proxyPort) return proxyPort;
+
+ if (await isSharedProxyHealthy()) {
+ proxyPort = COMMAND_PROXY_PORT;
+ log.info(
+ `[opencode-commandcode] reusing healthy proxy on ${getCommandProxyBaseUrl()}`,
+ );
+ return proxyPort;
+ }
+
+ const hostname = "127.0.0.1";
+ try {
+ server = Bun.serve({
+ hostname,
+ port: COMMAND_PROXY_PORT,
+ async fetch(req) {
+ return handleRequest(req);
+ },
+ });
+ proxyPort = server.port ?? COMMAND_PROXY_PORT;
+ log.info(
+ `[opencode-commandcode] proxy listening on ${getCommandProxyBaseUrl()}`,
+ );
+ return proxyPort;
+ } catch (err) {
+ if (isAddrInUseError(err) && (await isSharedProxyHealthy())) {
+ proxyPort = COMMAND_PROXY_PORT;
+ log.info(
+ `[opencode-commandcode] port ${COMMAND_PROXY_PORT} in use; reusing existing proxy`,
+ );
+ return proxyPort;
+ }
+ throw err;
+ }
+}
+
+export async function stopProxy(): Promise {
+ if (server) {
+ server.stop(true);
+ server = null;
+ proxyPort = null;
+ }
+ setStreamGenerateForTests(null);
+}
+
+async function handleRequest(req: Request): Promise {
+ const url = new URL(req.url);
+
+ if (
+ req.method === "GET" &&
+ (url.pathname === "/health" || url.pathname === "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/v1/health")
+ ) {
+ return Response.json({ ok: true, provider: "command-code" });
+ }
+
+ if (req.method === "GET" && url.pathname === "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/v1/models") {
+ return Response.json({
+ object: "list",
+ data: getCommandModels().map((m) => ({
+ id: m.id,
+ object: "model",
+ owned_by: "command-code",
+ })),
+ });
+ }
+
+ if (req.method === "GET" && url.pathname === "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/v1/usage") {
+ return Response.json({
+ object: "usage",
+ total: usageToOpenAI(totalUsageAcrossSessions()),
+ sessions: listSessionUsage().map((s) => ({
+ ...s,
+ usage: usageToOpenAI(s.usage),
+ mcpTools: s.tools.filter((t) => t.mcp),
+ localTools: s.tools.filter((t) => !t.mcp),
+ })),
+ });
+ }
+
+ if (
+ req.method === "GET" &&
+ url.pathname.startsWith("/v1/usage/session/")
+ ) {
+ const sessionId = decodeURIComponent(
+ url.pathname.slice("/v1/usage/session/".length),
+ );
+ const snap = getSessionUsage(sessionId);
+ if (!snap) {
+ return Response.json(
+ { error: { message: "session not found", type: "invalid_request_error" } },
+ { status: 404 },
+ );
+ }
+ return Response.json({
+ ...snap,
+ usage: usageToOpenAI(snap.usage),
+ mcpTools: snap.tools.filter((t) => t.mcp),
+ localTools: snap.tools.filter((t) => !t.mcp),
+ });
+ }
+
+ if (req.method === "POST" && url.pathname === "/v1/chat/completions") {
+ try {
+ const body = (await req.json()) as ChatCompletionRequest;
+ return await handleChatCompletions(req, body);
+ } catch (err) {
+ const message = err instanceof Error ? err.message : String(err);
+ log.error("[opencode-commandcode] chat completions error", message);
+ return Response.json(
+ { error: { message, type: "server_error" } },
+ { status: 500 },
+ );
+ }
+ }
+
+ return new Response("Not Found", { status: 404 });
+}
+
+function collectToolResults(messages: OpenAIMessage[]): Map {
+ const results = new Map();
+ for (const msg of messages) {
+ if (msg.role !== "tool" || !msg.tool_call_id) continue;
+ results.set(msg.tool_call_id, extractTextContent(msg.content));
+ }
+ return results;
+}
+
+function selectionFromRequest(
+ req: Request,
+ body: ChatCompletionRequest,
+): { modelId: string; effort?: string } {
+ const header = req.headers.get(EFFORT_HEADER);
+ const decoded = decodeCommandModelSelection(header);
+ const modelId =
+ decoded?.modelId ||
+ (typeof body.model === "string"
+ ? resolveCommandModelId(body.model)
+ : LAGUNA_MODEL_ID);
+ return {
+ modelId,
+ ...(decoded?.effort ? { effort: decoded.effort } : {}),
+ };
+}
+
+async function handleChatCompletions(
+ req: Request,
+ body: ChatCompletionRequest,
+): Promise {
+ const messages = Array.isArray(body.messages) ? body.messages : [];
+ const conversationKey =
+ req.headers.get(SESSION_HEADER) ||
+ conversationKeyFromMessages(messages);
+ const selection = selectionFromRequest(req, body);
+ const modelMeta =
+ findCommandModel(selection.modelId) ||
+ findCommandModel(DEFAULT_MODEL_ID)!;
+ const model = resolveCommandModelId(selection.modelId);
+ const stream = body.stream !== false;
+ const includeUsage = body.stream_options?.include_usage !== false;
+
+ const toolResults = collectToolResults(messages);
+ let existing = findBridgeByConversation(conversationKey);
+ if ((!existing || existing.pendingTools.size === 0) && toolResults.size > 0) {
+ for (const toolCallId of toolResults.keys()) {
+ const byTool = findBridgeByPendingTool(toolCallId);
+ if (byTool) {
+ existing = byTool;
+ break;
+ }
+ }
+ }
+
+ // Resume parked tool turn: append tool results and call gateway again.
+ if (existing && existing.pendingTools.size > 0 && toolResults.size > 0) {
+ let resolved = 0;
+ for (const [toolId, tool] of existing.pendingTools) {
+ const result = toolResults.get(toolId);
+ if (result !== undefined) {
+ tool.resolve(result);
+ existing.pendingTools.delete(toolId);
+ resolved++;
+ }
+ }
+ if (existing.pendingTools.size === 0 && existing.continueStream) {
+ log.info("[opencode-commandcode] resuming parked bridge", {
+ conversationKey: existing.conversationKey,
+ resolved,
+ });
+ return streamOpenAIResponse(
+ existing.continueStream(),
+ body.model || model,
+ stream,
+ existing,
+ includeUsage,
+ );
+ }
+ if (resolved === 0) {
+ return streamOpenAIResponse(
+ (async function* () {
+ yield {
+ type: "__park__",
+ tools: [...existing!.pendingTools.values()],
+ };
+ })(),
+ body.model || model,
+ stream,
+ existing,
+ includeUsage,
+ );
+ }
+ }
+
+ const apiKey = getAccessToken ? await getAccessToken() : null;
+ if (!apiKey) {
+ return Response.json(
+ {
+ error: {
+ message:
+ "Not authenticated with Command Code. Run `cmd login` or set COMMAND_CODE_API_KEY, then `opencode auth login --provider command-code`.",
+ type: "authentication_error",
+ },
+ },
+ { status: 401 },
+ );
+ }
+
+ const openCodeTools = Array.isArray(body.tools) ? body.tools : [];
+ const { system, messages: wireMessages } = openaiMessagesToWire(messages, {
+ vision: modelMeta.vision,
+ });
+ if (wireMessages.length === 0 && openCodeTools.length === 0) {
+ return Response.json(
+ {
+ error: {
+ message: "No user message found",
+ type: "invalid_request_error",
+ },
+ },
+ { status: 400 },
+ );
+ }
+
+ // Context / compact pre-check
+ const estimated = estimateMessageTokens(wireMessages);
+ const advice = assessContext(estimated, modelMeta.contextWindow);
+ let finalMessages = wireMessages;
+ let compactNote: string | null = advice.message;
+ if (advice.shouldCompact || advice.tier === "warn") {
+ const compacted = compactWireMessages(wireMessages, {
+ sessionId: conversationKey,
+ keepToolResults: advice.tier === "auto" ? 10 : 20,
+ });
+ finalMessages = compacted.messages;
+ if (compacted.note) compactNote = compacted.note;
+ }
+
+ const wireTools = openaiToolsToWire(openCodeTools);
+ const bridgeId = randomUUID();
+ const pendingTools = new Map();
+
+ const bridge: ParkedBridge = {
+ id: bridgeId,
+ conversationKey,
+ pendingTools,
+ createdAt: Date.now(),
+ wireMessages: finalMessages,
+ modelId: model,
+ system,
+ effort: selection.effort,
+ tools: wireTools,
+ apiKey,
+ contextWindow: modelMeta.contextWindow,
+ maxTokens: modelMeta.maxTokens,
+ };
+ putBridge(bridge);
+
+ async function* runTurn(
+ msgs: WireMessage[],
+ tools: WireTool[],
+ preface?: string | null,
+ ): AsyncGenerator {
+ if (preface) {
+ yield { type: "__reasoning__", text: `${preface}\n` };
+ }
+
+ const gen = (streamGenerateOverride || streamGenerate)({
+ apiKey: apiKey!,
+ model,
+ messages: msgs,
+ tools,
+ system: system || undefined,
+ maxTokens: modelMeta.maxTokens,
+ temperature: body.temperature,
+ effort: selection.effort,
+ permissionMode: tools.length > 0 ? "auto-accept" : "auto-accept",
+ sessionId: conversationKey,
+ threadId: undefined,
+ });
+
+ const assistantToolCalls: ParkedToolCall[] = [];
+ let turnUsage: WireUsage = emptyUsage();
+ let systemPromptTokens: number | undefined;
+ let finishReason = "stop";
+
+ for await (const event of gen) {
+ if (event.kind === "text" || event.kind === "reasoning") {
+ yield event;
+ continue;
+ }
+ if (event.kind === "error") {
+ yield event;
+ continue;
+ }
+ if (event.kind === "tool_call") {
+ if (event.providerExecuted) {
+ // Server-executed tool — surface as reasoning note, do not park.
+ yield {
+ kind: "reasoning",
+ text: `[mcp/tool] ${event.name} (provider-executed)\n`,
+ } satisfies GatewayMappedEvent;
+ recordToolCall(conversationKey, event.name);
+ continue;
+ }
+ recordToolCall(conversationKey, event.name);
+ const pending: ParkedToolCall = {
+ id: event.id,
+ name: event.name,
+ arguments: event.arguments,
+ resolve: () => {},
+ reject: () => {},
+ };
+ pending.resultPromise = new Promise((resolve, reject) => {
+ pending.resolve = resolve;
+ pending.reject = reject;
+ });
+ pendingTools.set(event.id, pending);
+ assistantToolCalls.push(pending);
+ continue;
+ }
+ if (event.kind === "finish") {
+ turnUsage = event.usage;
+ systemPromptTokens = event.systemPromptTokens;
+ finishReason = event.finishReason;
+ recordTurnUsage(
+ conversationKey,
+ model,
+ modelMeta.contextWindow,
+ turnUsage,
+ systemPromptTokens,
+ );
+ // After finish, if we collected client tools, park for OpenCode.
+ if (assistantToolCalls.length > 0) {
+ finishReason = "tool_calls";
+ const assistantContent = assistantToolCalls.map((t) => ({
+ type: "tool-call" as const,
+ toolCallId: t.id,
+ toolName: t.name,
+ input: safeParse(t.arguments),
+ }));
+ const nextMessages: WireMessage[] = [
+ ...msgs,
+ { role: "assistant", content: assistantContent },
+ ];
+ bridge.wireMessages = nextMessages;
+ bridge.continueStream = async function* () {
+ const toolMsgs: WireMessage[] = [];
+ for (const tool of assistantToolCalls) {
+ const result = await tool.resultPromise!;
+ toolMsgs.push({
+ role: "tool",
+ content: [
+ {
+ type: "tool-result",
+ toolCallId: tool.id,
+ toolName: tool.name,
+ output: { type: "text", value: result },
+ },
+ ],
+ });
+ }
+ const resumed = [
+ ...(bridge.wireMessages as WireMessage[]),
+ ...toolMsgs,
+ ];
+ bridge.wireMessages = resumed;
+ pendingTools.clear();
+ yield* runTurn(resumed, tools, null);
+ };
+ yield { type: "__park__", tools: assistantToolCalls };
+ return;
+ }
+ yield {
+ kind: "finish",
+ finishReason,
+ usage: turnUsage,
+ systemPromptTokens,
+ } satisfies GatewayMappedEvent;
+ deleteBridge(bridgeId);
+ return;
+ }
+ }
+
+ // Stream ended without explicit finish.
+ recordTurnUsage(
+ conversationKey,
+ model,
+ modelMeta.contextWindow,
+ turnUsage,
+ systemPromptTokens,
+ );
+ yield {
+ kind: "finish",
+ finishReason,
+ usage: turnUsage,
+ systemPromptTokens,
+ } satisfies GatewayMappedEvent;
+ if (pendingTools.size === 0) deleteBridge(bridgeId);
+ }
+
+ return streamOpenAIResponse(
+ runTurn(finalMessages, wireTools, compactNote),
+ body.model || model,
+ stream,
+ bridge,
+ includeUsage,
+ );
+}
+
+function safeParse(raw: string): Record {
+ try {
+ const v = JSON.parse(raw);
+ return v && typeof v === "object" && !Array.isArray(v)
+ ? (v as Record)
+ : { value: v };
+ } catch {
+ return { raw };
+ }
+}
+
+function streamOpenAIResponse(
+ events: AsyncIterable,
+ model: string,
+ stream: boolean,
+ bridge: ParkedBridge,
+ includeUsage: boolean,
+): Response {
+ const completionId = stableCompletionId(bridge.id);
+ const created = Math.floor(Date.now() / 1000);
+
+ if (!stream) {
+ const bodyStream = new ReadableStream({
+ async start(controller) {
+ try {
+ let content = "";
+ let reasoning = "";
+ const toolCalls: ParkedToolCall[] = [];
+ let usage = emptyUsage();
+ for await (const event of events) {
+ const mapped = normalizeEvent(event);
+ if (mapped.kind === "park") toolCalls.push(...mapped.tools);
+ else if (mapped.kind === "text") content += mapped.text;
+ else if (mapped.kind === "reasoning") reasoning += mapped.text;
+ else if (mapped.kind === "finish") usage = mapped.usage;
+ else if (mapped.kind === "error") content += `\n\n[command-code error] ${mapped.text}`;
+ }
+ const payload = {
+ id: completionId,
+ object: "chat.completion",
+ created,
+ model,
+ choices: [
+ {
+ index: 0,
+ message: {
+ role: "assistant",
+ content,
+ ...(reasoning ? { reasoning_content: reasoning } : {}),
+ ...(toolCalls.length
+ ? {
+ tool_calls: toolCalls.map((t) => ({
+ id: t.id,
+ type: "function",
+ function: {
+ name: t.name,
+ arguments: t.arguments,
+ },
+ })),
+ }
+ : {}),
+ },
+ finish_reason: toolCalls.length ? "tool_calls" : "stop",
+ },
+ ],
+ ...(includeUsage ? { usage: usageToOpenAI(usage) } : {}),
+ };
+ controller.enqueue(
+ new TextEncoder().encode(JSON.stringify(payload)),
+ );
+ } catch (err) {
+ const message = err instanceof Error ? err.message : String(err);
+ controller.enqueue(
+ new TextEncoder().encode(
+ JSON.stringify({ error: { message, type: "server_error" } }),
+ ),
+ );
+ } finally {
+ controller.close();
+ }
+ },
+ });
+ return new Response(bodyStream, {
+ headers: { "Content-Type": "application/json" },
+ });
+ }
+
+ const encoder = new TextEncoder();
+ const readable = new ReadableStream({
+ async start(controller) {
+ const send = (payload: unknown) => {
+ controller.enqueue(
+ encoder.encode(`data: ${JSON.stringify(payload)}\n\n`),
+ );
+ };
+
+ send({
+ id: completionId,
+ object: "chat.completion.chunk",
+ created,
+ model,
+ choices: [
+ { index: 0, delta: { role: "assistant" }, finish_reason: null },
+ ],
+ });
+
+ let finishReason: string | null = "stop";
+ let usage = emptyUsage();
+
+ try {
+ for await (const event of events) {
+ const mapped = normalizeEvent(event);
+ if (mapped.kind === "park") {
+ finishReason = "tool_calls";
+ for (let i = 0; i < mapped.tools.length; i++) {
+ const tool = mapped.tools[i];
+ send({
+ id: completionId,
+ object: "chat.completion.chunk",
+ created,
+ model,
+ choices: [
+ {
+ index: 0,
+ delta: {
+ tool_calls: [
+ {
+ index: i,
+ id: tool.id,
+ type: "function",
+ function: {
+ name: tool.name,
+ arguments: tool.arguments,
+ },
+ },
+ ],
+ },
+ finish_reason: null,
+ },
+ ],
+ });
+ }
+ break;
+ }
+ if (mapped.kind === "text" && mapped.text) {
+ send({
+ id: completionId,
+ object: "chat.completion.chunk",
+ created,
+ model,
+ choices: [
+ {
+ index: 0,
+ delta: { content: mapped.text },
+ finish_reason: null,
+ },
+ ],
+ });
+ }
+ if (mapped.kind === "reasoning" && mapped.text) {
+ send({
+ id: completionId,
+ object: "chat.completion.chunk",
+ created,
+ model,
+ choices: [
+ {
+ index: 0,
+ delta: { reasoning_content: mapped.text },
+ finish_reason: null,
+ },
+ ],
+ });
+ }
+ if (mapped.kind === "finish") {
+ finishReason = mapped.finishReason;
+ usage = mapped.usage;
+ }
+ if (mapped.kind === "error") {
+ send({
+ id: completionId,
+ object: "chat.completion.chunk",
+ created,
+ model,
+ choices: [
+ {
+ index: 0,
+ delta: {
+ content: `\n\n[command-code error] ${mapped.text}`,
+ },
+ finish_reason: null,
+ },
+ ],
+ });
+ }
+ }
+ } catch (err) {
+ const message = err instanceof Error ? err.message : String(err);
+ send({
+ id: completionId,
+ object: "chat.completion.chunk",
+ created,
+ model,
+ choices: [
+ {
+ index: 0,
+ delta: { content: `\n\n[command-code error] ${message}` },
+ finish_reason: null,
+ },
+ ],
+ });
+ }
+
+ send({
+ id: completionId,
+ object: "chat.completion.chunk",
+ created,
+ model,
+ choices: [{ index: 0, delta: {}, finish_reason: finishReason }],
+ ...(includeUsage ? { usage: usageToOpenAI(usage) } : {}),
+ });
+ controller.enqueue(encoder.encode("data: [DONE]\n\n"));
+ controller.close();
+ },
+ });
+
+ return new Response(readable, { headers: SSE_HEADERS });
+}
+
+type Normalized =
+ | { kind: "text"; text: string }
+ | { kind: "reasoning"; text: string }
+ | { kind: "park"; tools: ParkedToolCall[] }
+ | { kind: "finish"; finishReason: string; usage: WireUsage }
+ | { kind: "error"; text: string }
+ | { kind: "ignore" };
+
+function normalizeEvent(event: unknown): Normalized {
+ if (!event || typeof event !== "object") return { kind: "ignore" };
+ const e = event as Record;
+ if (e.type === "__park__" && Array.isArray(e.tools)) {
+ return { kind: "park", tools: e.tools as ParkedToolCall[] };
+ }
+ if (e.type === "__reasoning__" && typeof e.text === "string") {
+ return { kind: "reasoning", text: e.text };
+ }
+ if (e.kind === "text" && typeof e.text === "string") {
+ return { kind: "text", text: e.text };
+ }
+ if (e.kind === "reasoning" && typeof e.text === "string") {
+ return { kind: "reasoning", text: e.text };
+ }
+ if (e.kind === "error" && typeof e.text === "string") {
+ return { kind: "error", text: e.text };
+ }
+ if (e.kind === "finish") {
+ return {
+ kind: "finish",
+ finishReason: typeof e.finishReason === "string" ? e.finishReason : "stop",
+ usage: (e.usage as WireUsage) || emptyUsage(),
+ };
+ }
+ return { kind: "ignore" };
+}
diff --git a/src/usage.ts b/src/usage.ts
new file mode 100644
index 0000000..1b875bd
--- /dev/null
+++ b/src/usage.ts
@@ -0,0 +1,173 @@
+/**
+ * Session-level usage + tool/MCP accounting.
+ */
+import { createHash } from "node:crypto";
+import {
+ addUsage,
+ emptyUsage,
+ type WireUsage,
+} from "./gateway-types.js";
+
+export type ToolUsageStat = {
+ name: string;
+ calls: number;
+ /** True when the tool looks like an MCP tool (mcp__* or contains mcp). */
+ mcp: boolean;
+};
+
+export type SessionUsageSnapshot = {
+ sessionId: string;
+ modelId: string;
+ turns: number;
+ usage: WireUsage;
+ /** Estimated context occupancy from last finish (input + cache reads). */
+ lastContextTokens: number;
+ contextWindow: number;
+ contextFraction: number;
+ tools: ToolUsageStat[];
+ compactEvents: number;
+ updatedAt: number;
+};
+
+const sessions = new Map();
+
+function isMcpToolName(name: string): boolean {
+ return (
+ name.startsWith("mcp__") ||
+ name.startsWith("mcp_") ||
+ /(^|\.)mcp($|\.)/i.test(name)
+ );
+}
+
+export function conversationKeyFromMessages(
+ messages: Array<{ role?: string; content?: unknown }>,
+): string {
+ const seed = messages
+ .slice(0, 3)
+ .map((m) => `${m.role}:${typeof m.content === "string" ? m.content.slice(0, 80) : ""}`)
+ .join("|");
+ return `conv_${createHash("sha1").update(seed || "empty").digest("hex").slice(0, 16)}`;
+}
+
+export function getOrCreateSessionUsage(
+ sessionId: string,
+ modelId: string,
+ contextWindow: number,
+): SessionUsageSnapshot {
+ let snap = sessions.get(sessionId);
+ if (!snap) {
+ snap = {
+ sessionId,
+ modelId,
+ turns: 0,
+ usage: emptyUsage(),
+ lastContextTokens: 0,
+ contextWindow,
+ contextFraction: 0,
+ tools: [],
+ compactEvents: 0,
+ updatedAt: Date.now(),
+ };
+ sessions.set(sessionId, snap);
+ } else {
+ snap.modelId = modelId;
+ snap.contextWindow = contextWindow;
+ }
+ return snap;
+}
+
+export function recordTurnUsage(
+ sessionId: string,
+ modelId: string,
+ contextWindow: number,
+ usage: WireUsage,
+ systemPromptTokens?: number,
+): SessionUsageSnapshot {
+ const snap = getOrCreateSessionUsage(sessionId, modelId, contextWindow);
+ snap.turns += 1;
+ snap.usage = addUsage(snap.usage, usage);
+ const contextTokens =
+ usage.inputTokens +
+ usage.cacheReadTokens +
+ (typeof systemPromptTokens === "number" ? 0 : 0);
+ // Prefer input+cacheRead as occupancy signal; systemPromptTokens is optional.
+ snap.lastContextTokens = Math.max(
+ usage.inputTokens + usage.cacheReadTokens,
+ systemPromptTokens ?? 0,
+ contextTokens,
+ );
+ snap.contextFraction =
+ snap.contextWindow > 0
+ ? Math.min(1, snap.lastContextTokens / snap.contextWindow)
+ : 0;
+ snap.updatedAt = Date.now();
+ return snap;
+}
+
+export function recordToolCall(
+ sessionId: string,
+ toolName: string,
+ modelId = "unknown",
+ contextWindow = 256_000,
+): void {
+ const snap = getOrCreateSessionUsage(sessionId, modelId, contextWindow);
+ const existing = snap.tools.find((t) => t.name === toolName);
+ if (existing) {
+ existing.calls += 1;
+ } else {
+ snap.tools.push({
+ name: toolName,
+ calls: 1,
+ mcp: isMcpToolName(toolName),
+ });
+ }
+ snap.updatedAt = Date.now();
+}
+
+export function recordCompactEvent(sessionId: string): void {
+ const snap = sessions.get(sessionId);
+ if (!snap) return;
+ snap.compactEvents += 1;
+ snap.updatedAt = Date.now();
+}
+
+export function getSessionUsage(
+ sessionId: string,
+): SessionUsageSnapshot | null {
+ return sessions.get(sessionId) ?? null;
+}
+
+export function listSessionUsage(): SessionUsageSnapshot[] {
+ return [...sessions.values()];
+}
+
+export function totalUsageAcrossSessions(): WireUsage {
+ return [...sessions.values()].reduce(
+ (acc, s) => addUsage(acc, s.usage),
+ emptyUsage(),
+ );
+}
+
+export function resetUsageStore(): void {
+ sessions.clear();
+}
+
+export function usageToOpenAI(usage: WireUsage): {
+ prompt_tokens: number;
+ completion_tokens: number;
+ total_tokens: number;
+ prompt_tokens_details?: {
+ cached_tokens: number;
+ cache_write_tokens: number;
+ };
+} {
+ return {
+ prompt_tokens: usage.inputTokens,
+ completion_tokens: usage.outputTokens,
+ total_tokens: usage.inputTokens + usage.outputTokens,
+ prompt_tokens_details: {
+ cached_tokens: usage.cacheReadTokens,
+ cache_write_tokens: usage.cacheWriteTokens,
+ },
+ };
+}
diff --git a/test/live-laguna.ts b/test/live-laguna.ts
new file mode 100644
index 0000000..7494822
--- /dev/null
+++ b/test/live-laguna.ts
@@ -0,0 +1,312 @@
+/**
+ * Live Laguna S 2.1 tests against api.commandcode.ai.
+ * Requires COMMAND_CODE_API_KEY (or ~/.commandcode/auth.json from `cmd login`).
+ *
+ * bun run test:live
+ */
+import assert from "node:assert/strict";
+import { readCommandCodeCredentials } from "../src/credentials.ts";
+import { LAGUNA_MODEL_ID } from "../src/constants.ts";
+import {
+ startProxy,
+ stopProxy,
+ setStreamGenerateForTests,
+} from "../src/proxy.ts";
+import { resetUsageStore } from "../src/usage.ts";
+
+function requireAuth(): string {
+ const creds = readCommandCodeCredentials();
+ if (!creds?.apiKey) {
+ console.error(
+ "Skipping live tests: set COMMAND_CODE_API_KEY or run `cmd login`.",
+ );
+ process.exit(0);
+ }
+ return creds.apiKey;
+}
+
+async function readSseText(res: Response): Promise {
+ return res.text();
+}
+
+async function main() {
+ const apiKey = requireAuth();
+ setStreamGenerateForTests(null);
+ resetUsageStore();
+ await stopProxy();
+
+ const port = await startProxy(async () => apiKey);
+ const base = `http://127.0.0.1:${port}`;
+
+ // 1) Plain completion on Laguna
+ {
+ const res = await fetch(`${base}/v1/chat/completions`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "x-opencode-commandcode-session": "live-plain",
+ },
+ body: JSON.stringify({
+ model: LAGUNA_MODEL_ID,
+ stream: true,
+ stream_options: { include_usage: true },
+ messages: [
+ {
+ role: "user",
+ content:
+ "Reply with exactly the three words: Laguna live ok. No other text.",
+ },
+ ],
+ }),
+ });
+ assert.equal(res.status, 200, await res.text());
+ const text = await readSseText(res);
+ assert.ok(/Laguna live ok/i.test(text), text.slice(0, 2000));
+ assert.ok(text.includes("usage"), "expected usage in stream");
+ console.log("✓ live plain completion");
+ }
+
+ // 2) Attachment types (text file + image placeholder on text-only Laguna)
+ {
+ const png =
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";
+ const res = await fetch(`${base}/v1/chat/completions`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "x-opencode-commandcode-session": "live-attach",
+ },
+ body: JSON.stringify({
+ model: "laguna-s-2.1-free",
+ stream: false,
+ messages: [
+ {
+ role: "user",
+ content: [
+ {
+ type: "text",
+ text: "The attached text file contains a secret code. Reply with only that code.",
+ },
+ {
+ type: "file",
+ file: {
+ filename: "secret.txt",
+ media_type: "text/plain",
+ data: Buffer.from("CODE-7741", "utf8").toString("base64"),
+ },
+ },
+ {
+ type: "image_url",
+ image_url: { url: `data:image/png;base64,${png}` },
+ },
+ ],
+ },
+ ],
+ }),
+ });
+ assert.equal(res.status, 200, await res.text());
+ const json = (await res.json()) as {
+ choices: Array<{ message: { content: string } }>;
+ usage?: { prompt_tokens: number };
+ };
+ const content = json.choices[0]?.message?.content || "";
+ assert.ok(/7741/.test(content), content);
+ assert.ok(json.usage && json.usage.prompt_tokens > 0);
+ console.log("✓ live attachments (text file + image placeholder)");
+ }
+
+ // 3) Tools park/resume (OpenCode-owned tool)
+ {
+ const session = "live-tools";
+ const tools = [
+ {
+ type: "function",
+ function: {
+ name: "add_numbers",
+ description: "Add two integers and return the sum",
+ parameters: {
+ type: "object",
+ properties: {
+ a: { type: "number" },
+ b: { type: "number" },
+ },
+ required: ["a", "b"],
+ },
+ },
+ },
+ ];
+
+ const res1 = await fetch(`${base}/v1/chat/completions`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "x-opencode-commandcode-session": session,
+ },
+ body: JSON.stringify({
+ model: LAGUNA_MODEL_ID,
+ stream: true,
+ messages: [
+ {
+ role: "user",
+ content:
+ "Use the add_numbers tool to compute 17+25. After the tool returns, reply with only the numeric sum.",
+ },
+ ],
+ tools,
+ }),
+ });
+ assert.equal(res1.status, 200, await res1.text());
+ const turn1 = await readSseText(res1);
+ assert.ok(
+ turn1.includes("add_numbers") || turn1.includes("tool_calls"),
+ turn1.slice(0, 2500),
+ );
+
+ // Extract tool call id / args from SSE
+ let toolCallId = "";
+ let args = "{}";
+ for (const line of turn1.split("\n")) {
+ if (!line.startsWith("data: ")) continue;
+ const raw = line.slice(6).trim();
+ if (!raw || raw === "[DONE]") continue;
+ try {
+ const chunk = JSON.parse(raw) as {
+ choices?: Array<{
+ delta?: {
+ tool_calls?: Array<{
+ id?: string;
+ function?: { name?: string; arguments?: string };
+ }>;
+ };
+ }>;
+ };
+ const tc = chunk.choices?.[0]?.delta?.tool_calls?.[0];
+ if (tc?.id) toolCallId = tc.id;
+ if (tc?.function?.arguments) args += tc.function.arguments;
+ } catch {
+ // ignore
+ }
+ }
+ if (!toolCallId) {
+ console.warn(
+ "⚠ model did not emit tool_calls on this turn; skipping resume assertion",
+ );
+ } else {
+ let parsed: { a?: number; b?: number } = {};
+ try {
+ parsed = JSON.parse(args.replace(/^\{/, "{") || "{}");
+ } catch {
+ parsed = {};
+ }
+ // args may have been streamed in pieces — try to find numbers in the SSE
+ if (typeof parsed.a !== "number") {
+ const m = turn1.match(/"a"\s*:\s*(\d+)/);
+ const n = turn1.match(/"b"\s*:\s*(\d+)/);
+ if (m && n) parsed = { a: Number(m[1]), b: Number(n[1]) };
+ }
+ const sum =
+ typeof parsed.a === "number" && typeof parsed.b === "number"
+ ? parsed.a + parsed.b
+ : 42;
+
+ const res2 = await fetch(`${base}/v1/chat/completions`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "x-opencode-commandcode-session": session,
+ },
+ body: JSON.stringify({
+ model: LAGUNA_MODEL_ID,
+ stream: true,
+ messages: [
+ {
+ role: "user",
+ content:
+ "Use the add_numbers tool to compute 17+25. After the tool returns, reply with only the numeric sum.",
+ },
+ {
+ role: "assistant",
+ content: null,
+ tool_calls: [
+ {
+ id: toolCallId,
+ type: "function",
+ function: {
+ name: "add_numbers",
+ arguments: JSON.stringify({
+ a: parsed.a ?? 17,
+ b: parsed.b ?? 25,
+ }),
+ },
+ },
+ ],
+ },
+ {
+ role: "tool",
+ tool_call_id: toolCallId,
+ content: String(sum),
+ },
+ ],
+ tools,
+ }),
+ });
+ assert.equal(res2.status, 200, await res2.text());
+ const turn2 = await readSseText(res2);
+ assert.ok(/42/.test(turn2), turn2.slice(0, 2000));
+ console.log("✓ live tools park/resume");
+ }
+ }
+
+ // 4) Usage totals
+ {
+ const res = await fetch(`${base}/v1/usage`);
+ assert.equal(res.status, 200);
+ const json = (await res.json()) as {
+ total: { prompt_tokens: number; completion_tokens: number };
+ sessions: Array<{ tools: unknown[]; contextWindow: number }>;
+ };
+ assert.ok(json.total.prompt_tokens > 0);
+ assert.ok(json.sessions.length > 0);
+ assert.equal(json.sessions[0]?.contextWindow, 256_000);
+ console.log("✓ live usage totals", json.total);
+ }
+
+ // 5) Context/compact path — send a large prompt and ensure request succeeds
+ {
+ const big = "lorem ipsum ".repeat(8000);
+ const res = await fetch(`${base}/v1/chat/completions`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "x-opencode-commandcode-session": "live-compact",
+ },
+ body: JSON.stringify({
+ model: LAGUNA_MODEL_ID,
+ stream: false,
+ messages: [
+ {
+ role: "user",
+ content: `${big}\n\nReply with exactly: compact-ok`,
+ },
+ ],
+ }),
+ });
+ assert.equal(res.status, 200, await res.text());
+ const json = (await res.json()) as {
+ choices: Array<{ message: { content?: string; reasoning_content?: string } }>;
+ };
+ const content =
+ (json.choices[0]?.message?.content || "") +
+ (json.choices[0]?.message?.reasoning_content || "");
+ assert.ok(/compact-ok/i.test(content) || content.length > 0, content);
+ console.log("✓ live large-context request");
+ }
+
+ await stopProxy();
+ console.log("ok — live Laguna S 2.1 tests passed");
+}
+
+main().catch((err) => {
+ console.error(err);
+ process.exit(1);
+});
diff --git a/test/smoke.ts b/test/smoke.ts
new file mode 100644
index 0000000..0fd750b
--- /dev/null
+++ b/test/smoke.ts
@@ -0,0 +1,600 @@
+/**
+ * Smoke + mocked gateway tests for opencode-commandcode.
+ * Covers attachments, tools/MCP park-resume, compact, and usage — no live API required.
+ */
+import assert from "node:assert/strict";
+import { spawnSync } from "node:child_process";
+
+async function main() {
+ const {
+ listCommandAuthCandidates,
+ parseAuthFile,
+ extractApiKeyFromAuthFile,
+ readCommandCodeApiKeyFromEnv,
+ } = await import("../src/credentials.ts");
+ const {
+ COMMAND_CODE_MODELS,
+ buildEffortVariants,
+ getCommandModels,
+ isLoginPlaceholderModel,
+ resolveCommandModelId,
+ findCommandModel,
+ } = await import("../src/models.ts");
+ const {
+ encodeCommandModelSelection,
+ decodeCommandModelSelection,
+ resolveCommandModelSelection,
+ } = await import("../src/model-selection.ts");
+ const {
+ isCommandEffort,
+ PROVIDER_ID,
+ EFFORT_LEVELS,
+ LAGUNA_MODEL_ID,
+ DEFAULT_MODEL_ID,
+ } = await import("../src/constants.ts");
+ const { CommandCodePlugin } = await import("../src/index.ts");
+ const {
+ startProxy,
+ stopProxy,
+ getProxyPort,
+ getCommandProxyBaseUrl,
+ setStreamGenerateForTests,
+ } = await import("../src/proxy.ts");
+ const {
+ openaiContentToUserParts,
+ openaiMessagesToWire,
+ openaiToolsToWire,
+ contentHasAttachments,
+ } = await import("../src/prompt.ts");
+ const {
+ assessContext,
+ compactWireMessages,
+ estimateMessageTokens,
+ } = await import("../src/compact.ts");
+ const {
+ resetUsageStore,
+ recordTurnUsage,
+ recordToolCall,
+ getSessionUsage,
+ totalUsageAcrossSessions,
+ usageToOpenAI,
+ } = await import("../src/usage.ts");
+ const { mapStreamEvent, buildGenerateBody, buildAuthHeaders } = await import(
+ "../src/gateway.ts"
+ );
+ const { detectCommandCode } = await import("../src/detect.ts");
+
+ // --- credentials ---
+ assert.ok(listCommandAuthCandidates().length > 0);
+ assert.equal(
+ extractApiKeyFromAuthFile(parseAuthFile(JSON.stringify({ apiKey: "k1" }))),
+ "k1",
+ );
+ assert.equal(
+ readCommandCodeApiKeyFromEnv({ COMMAND_CODE_API_KEY: " tok " }),
+ "tok",
+ );
+ assert.equal(readCommandCodeApiKeyFromEnv({}), null);
+
+ // --- models / Laguna ---
+ const models = getCommandModels();
+ assert.ok(models.some((m) => m.resolvedId === LAGUNA_MODEL_ID));
+ assert.equal(resolveCommandModelId("laguna"), LAGUNA_MODEL_ID);
+ assert.equal(resolveCommandModelId("laguna-s-2.1-free"), LAGUNA_MODEL_ID);
+ assert.equal(resolveCommandModelId(DEFAULT_MODEL_ID), LAGUNA_MODEL_ID);
+ assert.equal(isLoginPlaceholderModel("login"), true);
+ const laguna = findCommandModel("laguna")!;
+ assert.equal(laguna.vision, false);
+ assert.equal(laguna.free, true);
+ assert.equal(laguna.contextWindow, 256_000);
+
+ const variants = buildEffortVariants(laguna);
+ for (const level of EFFORT_LEVELS) {
+ assert.ok(variants[level]);
+ assert.equal(isCommandEffort(level), true);
+ }
+ assert.deepEqual(variants.none, { disabled: true });
+
+ const selection = resolveCommandModelSelection("laguna", "high");
+ const encoded = encodeCommandModelSelection(selection);
+ const decoded = decodeCommandModelSelection(encoded);
+ assert.equal(decoded?.modelId, LAGUNA_MODEL_ID);
+ assert.equal(decoded?.effort, "high");
+
+ // --- attachments (text-only Laguna strips images; files/pdfs inlined) ---
+ const png =
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";
+ const imageParts = openaiContentToUserParts(
+ [
+ { type: "text", text: "what color?" },
+ {
+ type: "image_url",
+ image_url: { url: `data:image/png;base64,${png}` },
+ },
+ ],
+ false,
+ );
+ assert.ok(imageParts.some((p) => p.type === "text"));
+ assert.ok(
+ imageParts.some(
+ (p) => p.type === "text" && p.text.includes("image omitted"),
+ ),
+ );
+ assert.equal(
+ imageParts.some((p) => p.type === "image"),
+ false,
+ );
+
+ const visionParts = openaiContentToUserParts(
+ [
+ {
+ type: "image_url",
+ image_url: { url: `data:image/png;base64,${png}` },
+ },
+ ],
+ true,
+ );
+ assert.ok(visionParts.some((p) => p.type === "image"));
+
+ const textFileB64 = Buffer.from("hello from attachment\nline2", "utf8").toString(
+ "base64",
+ );
+ const fileParts = openaiContentToUserParts(
+ [
+ {
+ type: "file",
+ file: {
+ filename: "notes.txt",
+ media_type: "text/plain",
+ data: textFileB64,
+ },
+ },
+ ],
+ false,
+ );
+ assert.ok(
+ fileParts.some(
+ (p) => p.type === "text" && p.text.includes("hello from attachment"),
+ ),
+ );
+
+ const pdfParts = openaiContentToUserParts(
+ [
+ {
+ type: "input_file",
+ filename: "doc.pdf",
+ media_type: "application/pdf",
+ data: Buffer.from("%PDF-1.4").toString("base64"),
+ },
+ ],
+ false,
+ );
+ assert.ok(
+ pdfParts.some((p) => p.type === "text" && p.text.includes("attached_pdf")),
+ );
+
+ assert.equal(
+ contentHasAttachments([
+ { type: "image_url", image_url: { url: "x" } },
+ ]),
+ true,
+ );
+
+ const wired = openaiMessagesToWire(
+ [
+ { role: "system", content: "be concise" },
+ {
+ role: "user",
+ content: [
+ { type: "text", text: "summarize" },
+ {
+ type: "file",
+ file: {
+ filename: "a.md",
+ media_type: "text/markdown",
+ data: Buffer.from("# Title").toString("base64"),
+ },
+ },
+ ],
+ },
+ ],
+ { vision: false },
+ );
+ assert.equal(wired.system, "be concise");
+ assert.ok(wired.messages.length >= 1);
+
+ const tools = openaiToolsToWire([
+ {
+ type: "function",
+ function: {
+ name: "bash",
+ description: "run shell",
+ parameters: {
+ type: "object",
+ properties: { command: { type: "string" } },
+ required: ["command"],
+ },
+ },
+ },
+ {
+ type: "function",
+ function: {
+ name: "mcp__filesystem__read",
+ description: "mcp read",
+ parameters: { type: "object", properties: {} },
+ },
+ },
+ ]);
+ assert.equal(tools.length, 2);
+ assert.equal(tools[0]?.name, "bash");
+
+ // --- compact / context ---
+ const adviceOk = assessContext(10_000, 256_000);
+ assert.equal(adviceOk.tier, "ok");
+ const adviceWarn = assessContext(210_000, 256_000);
+ assert.equal(adviceWarn.tier, "warn");
+ const adviceAuto = assessContext(240_000, 256_000);
+ assert.equal(adviceAuto.shouldCompact, true);
+
+ const longMsgs = Array.from({ length: 40 }, (_, i) => ({
+ role: i % 2 === 0 ? ("user" as const) : ("assistant" as const),
+ content:
+ i % 2 === 0
+ ? [{ type: "text" as const, text: `user ${i} ${"x".repeat(200)}` }]
+ : [{ type: "text" as const, text: `asst ${i}` }],
+ }));
+ // insert tool results
+ longMsgs.push({
+ role: "tool" as const,
+ content: [
+ {
+ type: "tool-result" as const,
+ toolCallId: "c1",
+ toolName: "bash",
+ output: { type: "text" as const, value: "old result ".repeat(50) },
+ },
+ ],
+ } as any);
+ const compacted = compactWireMessages(longMsgs as any, {
+ sessionId: "sess-compact",
+ keepTurns: 10,
+ keepToolResults: 0,
+ });
+ assert.equal(compacted.compacted, true);
+ assert.ok(estimateMessageTokens(wired.messages) > 0);
+
+ // --- usage + mcp accounting ---
+ resetUsageStore();
+ recordTurnUsage(
+ "sess-1",
+ LAGUNA_MODEL_ID,
+ 256_000,
+ {
+ inputTokens: 100,
+ outputTokens: 50,
+ cacheReadTokens: 20,
+ cacheWriteTokens: 5,
+ },
+ );
+ recordToolCall("sess-1", "bash");
+ recordToolCall("sess-1", "bash");
+ recordToolCall("sess-1", "mcp__filesystem__read");
+ const snap = getSessionUsage("sess-1")!;
+ assert.equal(snap.usage.inputTokens, 100);
+ assert.equal(snap.tools.find((t) => t.name === "bash")?.calls, 2);
+ assert.equal(snap.tools.find((t) => t.name === "mcp__filesystem__read")?.mcp, true);
+ const openaiUsage = usageToOpenAI(totalUsageAcrossSessions());
+ assert.equal(openaiUsage.prompt_tokens, 100);
+ assert.equal(openaiUsage.completion_tokens, 50);
+
+ // --- gateway helpers ---
+ const headers = buildAuthHeaders({ apiKey: "test-key", sessionId: "s1" });
+ assert.equal(headers.Authorization, "Bearer test-key");
+ assert.equal(headers["x-session-id"], "s1");
+ const body = buildGenerateBody({
+ apiKey: "test-key",
+ model: LAGUNA_MODEL_ID,
+ messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
+ tools: [],
+ });
+ assert.equal(body.params.model, LAGUNA_MODEL_ID);
+ assert.equal(body.params.stream, true);
+
+ assert.deepEqual(mapStreamEvent({ type: "text-delta", text: "Hi" }), {
+ kind: "text",
+ text: "Hi",
+ });
+ assert.equal(mapStreamEvent({ type: "tool-call", toolCallId: "t1", toolName: "bash", input: { command: "ls" } }).kind, "tool_call");
+ assert.equal(
+ mapStreamEvent({
+ type: "finish",
+ finishReason: "stop",
+ totalUsage: { inputTokens: 1, outputTokens: 2 },
+ }).kind,
+ "finish",
+ );
+
+ // --- detect (CLI may be present in this env) ---
+ const detection = await detectCommandCode();
+ assert.ok(
+ detection.status === "ready" ||
+ detection.status === "needs-login" ||
+ detection.status === "missing-cli",
+ );
+
+ // --- plugin export ---
+ assert.equal(typeof CommandCodePlugin, "function");
+ assert.equal(PROVIDER_ID, "command-code");
+
+ // --- proxy health + mocked chat (text, tools, usage, attachments) ---
+ await stopProxy();
+ resetUsageStore();
+
+ setStreamGenerateForTests(async function* (params) {
+ // Simulate tool-call then finish on first turn with tools; plain text otherwise.
+ const last = params.messages[params.messages.length - 1];
+ const hasToolResult =
+ last?.role === "tool" ||
+ params.messages.some((m) => m.role === "tool");
+
+ if (params.tools && params.tools.length > 0 && !hasToolResult) {
+ yield {
+ kind: "text",
+ text: "Calling tool…",
+ };
+ yield {
+ kind: "tool_call",
+ id: "call_test_bash_1",
+ name: "bash",
+ arguments: JSON.stringify({ command: "echo hi" }),
+ };
+ yield {
+ kind: "finish",
+ finishReason: "tool_calls",
+ usage: {
+ inputTokens: 40,
+ outputTokens: 10,
+ cacheReadTokens: 0,
+ cacheWriteTokens: 0,
+ },
+ };
+ return;
+ }
+
+ if (hasToolResult) {
+ yield { kind: "text", text: "Tool said hi." };
+ yield {
+ kind: "finish",
+ finishReason: "stop",
+ usage: {
+ inputTokens: 60,
+ outputTokens: 8,
+ cacheReadTokens: 4,
+ cacheWriteTokens: 0,
+ },
+ };
+ return;
+ }
+
+ // Attachment-aware reply
+ const blob = JSON.stringify(params.messages);
+ if (blob.includes("attached_file") || blob.includes("attached_pdf") || blob.includes("image omitted")) {
+ yield { kind: "text", text: "Saw your attachment." };
+ } else {
+ yield { kind: "reasoning", text: "thinking…" };
+ yield { kind: "text", text: "Laguna hello." };
+ }
+ yield {
+ kind: "finish",
+ finishReason: "stop",
+ usage: {
+ inputTokens: 25,
+ outputTokens: 5,
+ cacheReadTokens: 0,
+ cacheWriteTokens: 0,
+ },
+ };
+ });
+
+ const port = await startProxy(async () => "test-api-key");
+ assert.ok(port > 0);
+ assert.equal(getProxyPort(), port);
+ assert.ok(getCommandProxyBaseUrl().includes(String(port)));
+
+ const health = await fetch(`http://127.0.0.1:${port}/health`);
+ assert.equal(health.status, 200);
+
+ const modelsRes = await fetch(`http://127.0.0.1:${port}/v1/models`);
+ assert.equal(modelsRes.status, 200);
+ const modelsJson = (await modelsRes.json()) as { data: Array<{ id: string }> };
+ assert.ok(modelsJson.data.some((m) => m.id.includes("laguna")));
+
+ // Plain completion
+ const chat1 = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "x-opencode-commandcode-session": "test-sess-plain",
+ },
+ body: JSON.stringify({
+ model: "command-code/laguna-s-2.1-free",
+ stream: true,
+ stream_options: { include_usage: true },
+ messages: [{ role: "user", content: "Say hi" }],
+ }),
+ });
+ assert.equal(chat1.status, 200);
+ const chat1Text = await chat1.text();
+ assert.ok(chat1Text.includes("Laguna hello."));
+ assert.ok(chat1Text.includes("usage"));
+ assert.ok(chat1Text.includes("[DONE]"));
+
+ // Attachment completion
+ const chatAtt = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "x-opencode-commandcode-session": "test-sess-att",
+ },
+ body: JSON.stringify({
+ model: "laguna",
+ stream: false,
+ messages: [
+ {
+ role: "user",
+ content: [
+ { type: "text", text: "read this" },
+ {
+ type: "file",
+ file: {
+ filename: "note.txt",
+ media_type: "text/plain",
+ data: Buffer.from("secret-note-content").toString("base64"),
+ },
+ },
+ {
+ type: "image_url",
+ image_url: { url: `data:image/png;base64,${png}` },
+ },
+ ],
+ },
+ ],
+ }),
+ });
+ assert.equal(chatAtt.status, 200);
+ const attJson = (await chatAtt.json()) as {
+ choices: Array<{ message: { content: string } }>;
+ usage?: { prompt_tokens: number };
+ };
+ assert.ok(attJson.choices[0]?.message.content.includes("attachment"));
+ assert.ok(attJson.usage);
+
+ // Tools park + resume
+ const chatTools = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "x-opencode-commandcode-session": "test-sess-tools",
+ },
+ body: JSON.stringify({
+ model: "laguna-s-2.1-free",
+ stream: true,
+ messages: [{ role: "user", content: "run echo" }],
+ tools: [
+ {
+ type: "function",
+ function: {
+ name: "bash",
+ description: "shell",
+ parameters: {
+ type: "object",
+ properties: { command: { type: "string" } },
+ },
+ },
+ },
+ {
+ type: "function",
+ function: {
+ name: "mcp__demo__ping",
+ description: "mcp ping",
+ parameters: { type: "object", properties: {} },
+ },
+ },
+ ],
+ }),
+ });
+ assert.equal(chatTools.status, 200);
+ const toolsText = await chatTools.text();
+ assert.ok(toolsText.includes("call_test_bash_1"));
+ assert.ok(toolsText.includes("tool_calls") || toolsText.includes('"bash"'));
+
+ const chatResume = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "x-opencode-commandcode-session": "test-sess-tools",
+ },
+ body: JSON.stringify({
+ model: "laguna-s-2.1-free",
+ stream: true,
+ messages: [
+ { role: "user", content: "run echo" },
+ {
+ role: "assistant",
+ content: null,
+ tool_calls: [
+ {
+ id: "call_test_bash_1",
+ type: "function",
+ function: {
+ name: "bash",
+ arguments: JSON.stringify({ command: "echo hi" }),
+ },
+ },
+ ],
+ },
+ {
+ role: "tool",
+ tool_call_id: "call_test_bash_1",
+ content: "hi",
+ },
+ ],
+ tools: [
+ {
+ type: "function",
+ function: {
+ name: "bash",
+ description: "shell",
+ parameters: {
+ type: "object",
+ properties: { command: { type: "string" } },
+ },
+ },
+ },
+ ],
+ }),
+ });
+ assert.equal(chatResume.status, 200);
+ const resumeText = await chatResume.text();
+ assert.ok(resumeText.includes("Tool said hi."));
+
+ // Usage endpoint
+ const usageRes = await fetch(`http://127.0.0.1:${port}/v1/usage`);
+ assert.equal(usageRes.status, 200);
+ const usageJson = (await usageRes.json()) as {
+ total: { prompt_tokens: number; completion_tokens: number };
+ sessions: unknown[];
+ };
+ assert.ok(usageJson.total.prompt_tokens > 0);
+ assert.ok(Array.isArray(usageJson.sessions));
+ assert.ok(usageJson.sessions.length > 0);
+
+ const sessUsage = await fetch(
+ `http://127.0.0.1:${port}/v1/usage/session/test-sess-tools`,
+ );
+ assert.equal(sessUsage.status, 200);
+ const sessJson = (await sessUsage.json()) as {
+ tools: Array<{ name: string; mcp: boolean; calls: number }>;
+ };
+ assert.ok(sessJson.tools.some((t) => t.name === "bash"));
+
+ await stopProxy();
+
+ // TypeScript build
+ const build = spawnSync("bun", ["run", "build"], {
+ cwd: new URL("..", import.meta.url).pathname,
+ encoding: "utf8",
+ });
+ if (build.status !== 0) {
+ console.error(build.stdout);
+ console.error(build.stderr);
+ throw new Error("build failed");
+ }
+
+ console.log("ok — opencode-commandcode smoke tests passed");
+}
+
+main().catch((err) => {
+ console.error(err);
+ process.exit(1);
+});
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..d74134f
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ES2022",
+ "moduleResolution": "bundler",
+ "esModuleInterop": true,
+ "strict": true,
+ "skipLibCheck": true,
+ "declaration": true,
+ "outDir": "dist",
+ "rootDir": "src",
+ "types": ["bun", "node"]
+ },
+ "include": ["src"]
+}
From 1e5ec3245d976f42535d6d9d04773648e7bc85bf Mon Sep 17 00:00:00 2001
From: Serhii Dziupin
Date: Sat, 8 Aug 2026 06:05:52 +0000
Subject: [PATCH 2/4] fix: drop unused detectCommandCode import in plugin entry
Co-authored-by: Serhii Dziupin
---
src/index.ts | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/index.ts b/src/index.ts
index 1ed65a9..8b5d967 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -22,7 +22,6 @@ import {
PROVIDER_ID,
SESSION_HEADER,
} from "./constants.js";
-import { detectCommandCode } from "./detect.js";
import { log } from "./log.js";
import {
encodeCommandModelSelection,
From b39cd64e303a2ebec9c060fa2f5399dae40af8ff Mon Sep 17 00:00:00 2001
From: Serhii Dziupin
Date: Sat, 8 Aug 2026 06:11:19 +0000
Subject: [PATCH 3/4] =?UTF-8?q?feat:=20Go-plan=20($1)=20browser=20OAuth=20?=
=?UTF-8?q?=E2=80=94=20no=20API=20key?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Primary auth matches `cmd login`: Studio CLI callback posts a session
credential for the Command Code Go plan. Laguna S 2.1 free stays $0
credits. Remove paste-API-key as the primary path.
Co-authored-by: Serhii Dziupin
---
CHANGELOG.md | 2 +-
README.md | 18 +--
package.json | 2 +-
src/auth-login.ts | 328 +++++++++++++++++++++++++++++++++++++++++++-
src/index.ts | 91 ++++++------
test/live-laguna.ts | 8 +-
test/smoke.ts | 16 +++
7 files changed, 406 insertions(+), 59 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6197490..f2af2a0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,6 @@
- Initial release: Command Code OpenCode plugin
- Local OpenAI-compatible proxy → `api.commandcode.ai/alpha/generate`
-- Auth via Command Code CLI sync (`cmd login`) or API key
+- Auth via Command Code Go-plan browser OAuth (`cmd login`) — no Studio API key
- Default model: Laguna S 2.1 free (`poolside/laguna-s-2.1-free`)
- Tools/MCP park-resume, attachment plumbing, context compact, usage endpoints
diff --git a/README.md b/README.md
index 2cbf3e1..a13c3bd 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
- Command Code inside OpenCode — CLI/API-key auth,
+ Command Code inside OpenCode — Go-plan ($1) browser login,
gateway proxy, free Laguna S 2.1, tools/MCP, attachments, compact, usage.
@@ -52,30 +52,31 @@ opencode plugin file://$PWD
## Authenticate
-Requires [Command Code CLI](https://commandcode.ai/docs/quickstart) and a free account:
+Uses the **Command Code Go plan ($1/mo)** — same browser login as `cmd login`. No Studio API key.
```bash
npm i -g command-code@latest
-cmd login
+cmd login # browser → Studio CLI auth (Go plan). Laguna S 2.1 is $0 credits.
+
opencode auth login --provider command-code
-# pick "Use Command Code CLI login" — or paste an API key
+# pick "Login with Command Code (Go $1)" — or "Use existing cmd login session"
```
-Env alternative: `COMMAND_CODE_API_KEY`.
-
Then:
```bash
opencode run "Summarise this repository in five bullets." --model command-code/laguna-s-2.1-free
```
+Laguna S 2.1 free requires an active Go (or higher) account with credits on file; requests on that model still bill **$0**.
+
## Why this plugin
| | |
|---|---|
| **Gateway proxy** | Talks to `api.commandcode.ai/alpha/generate` — same transport as the `cmd` CLI. |
| **Laguna S 2.1 free** | 256k context, reasoning, $0 while the deal lasts. |
-| **CLI / API auth** | Syncs `~/.commandcode/auth.json` or accepts `COMMAND_CODE_API_KEY`. |
+| **Go-plan auth** | Browser OAuth like `cmd login` ($1/mo). Syncs `~/.commandcode/auth.json`. No API key. |
| **Agent-grade tools** | OpenCode tool calls park and resume; MCP-prefixed tools are tracked separately. |
| **Attachments** | Images, PDFs, text/binary files from OpenCode. Text-only models get image placeholders (Command Code behavior). |
| **Compact** | Context-fraction tips + tiered client compact before the 256k window overflows. |
@@ -103,7 +104,7 @@ OpenCode
bun install
bun run build
bun run test # mocked gateway — attachments, tools, compact, usage
-bun run test:live # live Laguna S 2.1 (needs COMMAND_CODE_API_KEY)
+bun run test:live # live Laguna S 2.1 (needs `cmd login` / Go plan session)
```
Debug: `OPENCODE_COMMANDCODE_DEBUG=1`.
@@ -112,7 +113,6 @@ Optional knobs:
- `OPENCODE_COMMANDCODE_PROXY_PORT` — fixed local proxy port (default `8797`)
- `OPENCODE_COMMANDCODE_CWD` — working directory reported to the gateway
-- `COMMAND_CODE_API_KEY` — API key without CLI login
- `COMMANDCODE_API_URL` — override API base (default `https://api.commandcode.ai`)
## License
diff --git a/package.json b/package.json
index 7cb1cb2..25c9a33 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "@otto-assistant/opencode-commandcode",
"version": "0.1.0",
- "description": "Command Code in OpenCode — CLI auth sync, gateway proxy, Laguna S 2.1, tools/MCP, attachments, compact, usage.",
+ "description": "Command Code in OpenCode — Go-plan ($1) browser OAuth, gateway proxy, Laguna S 2.1, tools/MCP, attachments, compact, usage.",
"license": "MIT",
"type": "module",
"main": "./opencode-commandcode.js",
diff --git a/src/auth-login.ts b/src/auth-login.ts
index 3b0d4e7..6f1de75 100644
--- a/src/auth-login.ts
+++ b/src/auth-login.ts
@@ -1,6 +1,13 @@
/**
- * Sync Command Code CLI credentials into OpenCode auth.json.
+ * Auth helpers: sync Command Code CLI credentials + browser Go-plan login.
+ *
+ * Browser flow matches `cmd login`:
+ * https://commandcode.ai/studio/auth/cli?callback=http://localhost:PORT/callback&state=STATE
+ * Studio POSTs { apiKey, userId, userName, keyName, state } to the local callback.
+ * That session credential is what the $1 Go plan uses — no Studio API key needed.
*/
+import { createServer, type Server } from "node:http";
+import { randomBytes } from "node:crypto";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { homedir } from "node:os";
@@ -8,7 +15,12 @@ import {
readCommandCodeCredentials,
type CommandCodeCredentials,
} from "./credentials.js";
-import { PROVIDER_ID, WHOAMI_ROUTE } from "./constants.js";
+import {
+ COMMAND_CODE_AUTH_FILE,
+ COMMAND_CODE_DIR_NAME,
+ PROVIDER_ID,
+ WHOAMI_ROUTE,
+} from "./constants.js";
import { getApiBaseUrl, log } from "./log.js";
export type CommandCodeAuthTokens = {
@@ -16,14 +28,53 @@ export type CommandCodeAuthTokens = {
refresh: string;
expires: number;
key?: string;
+ userId?: string;
+ userName?: string;
+ keyName?: string;
+};
+
+export type PendingCommandLogin = {
+ url: string;
+ state: string;
+ port: number;
+ startedAt: number;
+ completed: boolean;
+ error?: string;
+};
+
+export type BrowserCallbackPayload = {
+ apiKey: string;
+ userId: string;
+ userName: string;
+ keyName: string;
+ state: string;
};
+const AUTH_START_PORT = 5959;
+const AUTH_MAX_PORT_ATTEMPTS = 10;
+const AUTH_TIMEOUT_MS = 12 * 60 * 1000;
+const STUDIO_BASE = "https://commandcode.ai";
+const ALLOWED_CORS = [
+ "http://localhost:3000",
+ "https://staging.commandcode.ai",
+ "https://commandcode.ai",
+];
+
+let pending: PendingCommandLogin | null = null;
+let authServer: Server | null = null;
+let waitForCallback: Promise | null = null;
+let rejectCallback: ((err: Error) => void) | null = null;
+
function authJsonPath(): string {
const xdg = process.env.XDG_DATA_HOME;
const base = xdg ? xdg : join(homedir(), ".local", "share");
return join(base, "opencode", "auth.json");
}
+function commandCodeAuthPath(homeDir = homedir()): string {
+ return join(homeDir, COMMAND_CODE_DIR_NAME, COMMAND_CODE_AUTH_FILE);
+}
+
function readAuthFile(): Record {
const path = authJsonPath();
if (!existsSync(path)) return {};
@@ -46,14 +97,52 @@ export function writeCommandCodeAuth(
const existing = readAuthFile();
const key = tokens.key || tokens.access;
existing[PROVIDER_ID] = {
- type: "api",
+ type: "oauth",
key,
- // Also keep oauth-shaped fields for hosts that expect them.
access: key,
refresh: tokens.refresh,
expires: tokens.expires,
+ ...(tokens.userId ? { accountId: tokens.userId } : {}),
};
writeAuthFile(existing);
+
+ // Keep CLI auth.json in sync so `cmd -p` / status also see the session.
+ try {
+ const cliPath = commandCodeAuthPath();
+ mkdirSync(dirname(cliPath), { recursive: true });
+ let cliExisting: Record = {};
+ if (existsSync(cliPath)) {
+ try {
+ cliExisting = JSON.parse(readFileSync(cliPath, "utf8")) as Record<
+ string,
+ unknown
+ >;
+ } catch {
+ cliExisting = {};
+ }
+ }
+ writeFileSync(
+ cliPath,
+ JSON.stringify(
+ {
+ ...cliExisting,
+ apiKey: key,
+ userId: tokens.userId || cliExisting.userId || "",
+ userName: tokens.userName || cliExisting.userName || "",
+ keyName: tokens.keyName || cliExisting.keyName || "opencode-oauth",
+ authenticatedAt: new Date().toISOString(),
+ },
+ null,
+ 2,
+ ) + "\n",
+ { mode: 0o600 },
+ );
+ } catch (err) {
+ log.warn(
+ "[opencode-commandcode] failed to mirror credentials into ~/.commandcode/auth.json",
+ err instanceof Error ? err.message : err,
+ );
+ }
}
export function credentialsToTokens(
@@ -64,11 +153,15 @@ export function credentialsToTokens(
refresh: `cli-sync-${creds.source}`,
expires: Date.now() + 365 * 24 * 60 * 60 * 1000,
key: creds.apiKey,
+ userId: creds.userId || undefined,
+ userName: creds.userName || undefined,
+ keyName: creds.keyName || undefined,
};
}
/**
- * Sync Command Code CLI / env credentials into OpenCode auth.json.
+ * Sync Command Code CLI credentials (from `cmd login` Go-plan browser auth)
+ * into OpenCode auth.json. No separate Studio API key required.
*/
export function syncCommandCodeCredentialsToOpenCode(): CommandCodeAuthTokens | null {
const creds = readCommandCodeCredentials();
@@ -76,7 +169,7 @@ export function syncCommandCodeCredentialsToOpenCode(): CommandCodeAuthTokens |
const tokens = credentialsToTokens(creds);
writeCommandCodeAuth(tokens);
log.info(
- "[opencode-commandcode] synced Command Code credentials into OpenCode auth",
+ "[opencode-commandcode] synced Command Code Go-plan credentials into OpenCode auth",
);
return tokens;
}
@@ -115,3 +208,226 @@ export async function validateCommandApiKey(
};
}
}
+
+export function getPendingCommandLogin(): PendingCommandLogin | null {
+ return pending;
+}
+
+export function resetPendingCommandLogin(): void {
+ stopAuthServer();
+ pending = null;
+ waitForCallback = null;
+ rejectCallback = null;
+}
+
+function stopAuthServer(): void {
+ if (authServer) {
+ try {
+ authServer.close();
+ } catch {
+ // ignore
+ }
+ authServer = null;
+ }
+}
+
+export function buildCommandAuthUrl(port: number, state: string): string {
+ const callback = `http://localhost:${port}/callback`;
+ return `${STUDIO_BASE}/studio/auth/cli?callback=${encodeURIComponent(callback)}&state=${encodeURIComponent(state)}`;
+}
+
+function isCallbackPayload(body: unknown): body is BrowserCallbackPayload {
+ if (!body || typeof body !== "object") return false;
+ const b = body as Record;
+ return (
+ typeof b.apiKey === "string" &&
+ typeof b.state === "string" &&
+ typeof b.userId === "string" &&
+ typeof b.userName === "string" &&
+ typeof b.keyName === "string"
+ );
+}
+
+async function listenOnPort(port: number): Promise {
+ return new Promise((resolve, reject) => {
+ const server = createServer();
+ server.once("error", reject);
+ server.listen(port, "127.0.0.1", () => {
+ server.removeListener("error", reject);
+ resolve(server);
+ });
+ });
+}
+
+async function findAvailablePort(
+ start = AUTH_START_PORT,
+ attempts = AUTH_MAX_PORT_ATTEMPTS,
+): Promise {
+ for (let i = 0; i < attempts; i++) {
+ const port = start + i;
+ try {
+ const server = await listenOnPort(port);
+ await new Promise((resolve, reject) => {
+ server.close((err) => (err ? reject(err) : resolve()));
+ });
+ return port;
+ } catch {
+ // try next
+ }
+ }
+ throw new Error(
+ `No available auth callback port after ${attempts} attempts from ${start}`,
+ );
+}
+
+/**
+ * Start Command Code Studio CLI browser login (same as `cmd login`).
+ * Works with the $1 Go plan — Studio returns a session credential via POST.
+ */
+export async function startCommandBrowserLogin(): Promise {
+ resetPendingCommandLogin();
+
+ const port = await findAvailablePort();
+ const state = randomBytes(32).toString("base64url");
+ const url = buildCommandAuthUrl(port, state);
+
+ let resolveCb!: (payload: BrowserCallbackPayload) => void;
+ let rejectCb!: (err: Error) => void;
+ waitForCallback = new Promise((resolve, reject) => {
+ resolveCb = resolve;
+ rejectCb = reject;
+ });
+ rejectCallback = rejectCb;
+
+ const server = await listenOnPort(port);
+ authServer = server;
+
+ server.on("request", (req, res) => {
+ const origin = req.headers.origin;
+ const allow =
+ typeof origin === "string" && ALLOWED_CORS.includes(origin)
+ ? origin
+ : ALLOWED_CORS[0];
+ res.setHeader("Access-Control-Allow-Origin", allow);
+ res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
+ res.setHeader("Content-Type", "application/json");
+
+ if (req.method === "OPTIONS") {
+ res.writeHead(204);
+ res.end();
+ return;
+ }
+
+ if (req.url !== "/callback") {
+ res.writeHead(404);
+ res.end(JSON.stringify({ success: false, error: "Not found" }));
+ return;
+ }
+ if (req.method !== "POST") {
+ res.writeHead(405);
+ res.end(
+ JSON.stringify({
+ success: false,
+ error: "Method not allowed. Use POST.",
+ }),
+ );
+ return;
+ }
+
+ let raw = "";
+ req.on("data", (chunk) => {
+ raw += chunk.toString();
+ if (raw.length > 10_000) req.destroy();
+ });
+ req.on("end", () => {
+ try {
+ const parsed = JSON.parse(raw) as Record;
+ if (parsed && typeof parsed === "object" && "error" in parsed) {
+ res.writeHead(200);
+ res.end(JSON.stringify({ success: true }));
+ const message =
+ typeof parsed.error_description === "string"
+ ? parsed.error_description
+ : typeof parsed.error === "string"
+ ? parsed.error
+ : "Authorization denied";
+ rejectCb(new Error(message));
+ stopAuthServer();
+ return;
+ }
+ if (!isCallbackPayload(parsed)) {
+ res.writeHead(400);
+ res.end(
+ JSON.stringify({
+ success: false,
+ error: "Missing required fields",
+ }),
+ );
+ return;
+ }
+ if (parsed.state !== state) {
+ res.writeHead(403);
+ res.end(
+ JSON.stringify({ success: false, error: "Invalid state token" }),
+ );
+ return;
+ }
+ res.writeHead(200);
+ res.end(JSON.stringify({ success: true }));
+ resolveCb(parsed);
+ stopAuthServer();
+ } catch {
+ res.writeHead(400);
+ res.end(JSON.stringify({ success: false, error: "Invalid JSON" }));
+ }
+ });
+ });
+
+ pending = {
+ url,
+ state,
+ port,
+ startedAt: Date.now(),
+ completed: false,
+ };
+ log.info("[opencode-commandcode] Command Code Go-plan OAuth URL ready", {
+ port,
+ });
+ return pending;
+}
+
+export async function completeCommandBrowserLogin(): Promise {
+ if (!pending || !waitForCallback) {
+ throw new Error("No Command Code login in progress — start auth first.");
+ }
+
+ const timeout = new Promise((_, reject) => {
+ setTimeout(
+ () => reject(new Error("Browser authentication timed out")),
+ AUTH_TIMEOUT_MS,
+ );
+ });
+
+ try {
+ const payload = await Promise.race([waitForCallback, timeout]);
+ const tokens: CommandCodeAuthTokens = {
+ access: payload.apiKey,
+ key: payload.apiKey,
+ refresh: "browser-oauth",
+ expires: Date.now() + 365 * 24 * 60 * 60 * 1000,
+ userId: payload.userId,
+ userName: payload.userName,
+ keyName: payload.keyName,
+ };
+ writeCommandCodeAuth(tokens);
+ if (pending) pending.completed = true;
+ return tokens;
+ } catch (err) {
+ if (pending) {
+ pending.error = err instanceof Error ? err.message : String(err);
+ }
+ resetPendingCommandLogin();
+ throw err;
+ }
+}
diff --git a/src/index.ts b/src/index.ts
index 8b5d967..13f2c9f 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -2,7 +2,7 @@
* OpenCode Command Code Auth Plugin
*
* Enables Command Code (Laguna S 2.1 free and gateway models) inside OpenCode via:
- * 1. Command Code CLI credential sync or API key auth
+ * 1. Command Code Go-plan browser OAuth (`cmd login`) — no API key
* 2. Local OpenAI-compatible proxy → api.commandcode.ai /alpha/generate
* 3. Tools/MCP park-resume, attachments, compact, and usage accounting
*
@@ -11,9 +11,11 @@
*/
import type { Hooks, Plugin, PluginInput } from "@opencode-ai/plugin";
import {
+ completeCommandBrowserLogin,
+ getPendingCommandLogin,
+ resetPendingCommandLogin,
+ startCommandBrowserLogin,
syncCommandCodeCredentialsToOpenCode,
- validateCommandApiKey,
- writeCommandCodeAuth,
} from "./auth-login.js";
import {
DEFAULT_MODEL_ID,
@@ -381,71 +383,80 @@ export const CommandCodePlugin: Plugin = async (
methods: [
{
type: "oauth",
- label: "Use Command Code CLI login",
+ label: "Login with Command Code (Go $1)",
async authorize() {
+ // Prefer already-logged-in CLI session from `cmd login`.
const synced = syncCommandCodeCredentialsToOpenCode();
if (synced) {
return {
- url: "https://commandcode.ai/docs/quickstart",
+ url: "https://commandcode.ai/docs/plans/go",
instructions:
- "Command Code CLI credentials were found and synced. Click Complete to finish.",
+ "Command Code Go-plan session found (from `cmd login`). Click Complete — no API key needed.",
method: "auto" as const,
async callback() {
return {
type: "success" as const,
- key: synced.key || synced.access,
+ refresh: synced.refresh,
+ access: synced.access,
+ expires: synced.expires,
};
},
};
}
+ let current = getPendingCommandLogin();
+ if (!current || current.completed) {
+ current = await startCommandBrowserLogin();
+ }
+
+ return {
+ url: current.url,
+ instructions:
+ "Opens Command Code Studio CLI login (Go plan, $1/mo). Approve in the browser — session is posted back automatically. Laguna S 2.1 free costs $0 credits. No API key.",
+ method: "auto" as const,
+ async callback() {
+ try {
+ const tokens = await completeCommandBrowserLogin();
+ return {
+ type: "success" as const,
+ refresh: tokens.refresh,
+ access: tokens.access,
+ expires: tokens.expires,
+ };
+ } catch (err) {
+ resetPendingCommandLogin();
+ log.error(
+ "[opencode-commandcode] Go-plan OAuth failed",
+ err instanceof Error ? err.message : err,
+ );
+ return { type: "failed" as const };
+ }
+ },
+ };
+ },
+ },
+ {
+ type: "oauth",
+ label: "Use existing `cmd login` session",
+ async authorize() {
return {
url: "https://commandcode.ai/docs/quickstart",
instructions:
- "Run `npm i -g command-code@latest && cmd login` in a terminal, then click Complete. Or choose API key instead.",
+ "If you already ran `npm i -g command-code@latest && cmd login` (Go $1 plan), click Complete to sync ~/.commandcode/auth.json. No API key.",
method: "auto" as const,
async callback() {
const again = syncCommandCodeCredentialsToOpenCode();
if (!again) return { type: "failed" as const };
return {
type: "success" as const,
- key: again.key || again.access,
+ refresh: again.refresh,
+ access: again.access,
+ expires: again.expires,
};
},
};
},
},
- {
- type: "api",
- label: "Command Code API key",
- prompts: [
- {
- type: "text",
- key: "apiKey",
- message: "Paste your Command Code API key",
- placeholder: "from Studio → Settings, or COMMAND_CODE_API_KEY",
- },
- ],
- async authorize(inputs) {
- const key = (inputs?.apiKey || inputs?.key || "").trim();
- if (!key) return { type: "failed" as const };
- const validated = await validateCommandApiKey(key);
- if (!validated.valid) {
- log.error(
- "[opencode-commandcode] API key validation failed",
- validated.error,
- );
- return { type: "failed" as const };
- }
- writeCommandCodeAuth({
- access: key,
- refresh: "api-key",
- expires: Date.now() + 365 * 24 * 60 * 60 * 1000,
- key,
- });
- return { type: "success" as const, key };
- },
- },
],
},
};
diff --git a/test/live-laguna.ts b/test/live-laguna.ts
index 7494822..9e1113e 100644
--- a/test/live-laguna.ts
+++ b/test/live-laguna.ts
@@ -1,6 +1,7 @@
/**
* Live Laguna S 2.1 tests against api.commandcode.ai.
- * Requires COMMAND_CODE_API_KEY (or ~/.commandcode/auth.json from `cmd login`).
+ * Requires a Go-plan ($1) session from `cmd login` (~/.commandcode/auth.json).
+ * No Studio API key.
*
* bun run test:live
*/
@@ -18,7 +19,10 @@ function requireAuth(): string {
const creds = readCommandCodeCredentials();
if (!creds?.apiKey) {
console.error(
- "Skipping live tests: set COMMAND_CODE_API_KEY or run `cmd login`.",
+ "Skipping live tests: run `cmd login` with a Command Code Go ($1) plan session.",
+ );
+ console.error(
+ "No API key needed — browser OAuth writes ~/.commandcode/auth.json.",
);
process.exit(0);
}
diff --git a/test/smoke.ts b/test/smoke.ts
index 0fd750b..e2cbf24 100644
--- a/test/smoke.ts
+++ b/test/smoke.ts
@@ -33,6 +33,11 @@ async function main() {
DEFAULT_MODEL_ID,
} = await import("../src/constants.ts");
const { CommandCodePlugin } = await import("../src/index.ts");
+ const {
+ buildCommandAuthUrl,
+ startCommandBrowserLogin,
+ resetPendingCommandLogin,
+ } = await import("../src/auth-login.ts");
const {
startProxy,
stopProxy,
@@ -322,6 +327,17 @@ async function main() {
detection.status === "missing-cli",
);
+ // --- Go-plan browser OAuth URL (same as cmd login) ---
+ const authUrl = buildCommandAuthUrl(5959, "test-state");
+ assert.ok(authUrl.includes("commandcode.ai/studio/auth/cli"));
+ assert.ok(authUrl.includes("callback="));
+ assert.ok(authUrl.includes("state=test-state"));
+
+ const pendingLogin = await startCommandBrowserLogin();
+ assert.ok(pendingLogin.url.includes("/studio/auth/cli"));
+ assert.ok(pendingLogin.port >= 5959);
+ resetPendingCommandLogin();
+
// --- plugin export ---
assert.equal(typeof CommandCodePlugin, "function");
assert.equal(PROVIDER_ID, "command-code");
From 7e6bbe92596d325f43eb73f1a628e80da97ab9bd Mon Sep 17 00:00:00 2001
From: Serhii Dziupin
Date: Sat, 8 Aug 2026 09:27:13 +0000
Subject: [PATCH 4/4] fix: align gateway body with Command Code schema; pass
live Laguna
Correct /alpha/generate payload (structure[] + mode=agent), harden live
tests for streamed deltas, and verify Go-plan session end-to-end on
poolside/laguna-s-2.1-free via proxy and opencode run.
Co-authored-by: Serhii Dziupin
---
src/gateway-types.ts | 24 +++++++++++++--
src/gateway.ts | 19 ++++++++++--
test/live-laguna.ts | 69 +++++++++++++++++++++++++++++++++-----------
3 files changed, 90 insertions(+), 22 deletions(-)
diff --git a/src/gateway-types.ts b/src/gateway-types.ts
index 88f5aeb..764a87a 100644
--- a/src/gateway-types.ts
+++ b/src/gateway-types.ts
@@ -57,13 +57,33 @@ export type WireUsage = {
};
export type GenerateBody = {
- config: Record;
+ config: {
+ workingDir: string;
+ date: string;
+ environment: string;
+ structure: string[];
+ isGitRepo: boolean;
+ currentBranch: string;
+ mainBranch: string;
+ gitStatus: string;
+ recentCommits: string[];
+ [key: string]: unknown;
+ };
memory: null;
taste: null;
skills: null;
permissionMode: "standard" | "auto-accept" | "plan";
threadId?: string;
- mode?: string;
+ mode?:
+ | "agent"
+ | "learning"
+ | "custom-agent"
+ | "custom-agent-create"
+ | "title-gen"
+ | "tool-desc"
+ | "compact"
+ | "vision"
+ | string;
params: {
model: string;
messages: WireMessage[];
diff --git a/src/gateway.ts b/src/gateway.ts
index 4e938bb..43a2ade 100644
--- a/src/gateway.ts
+++ b/src/gateway.ts
@@ -2,6 +2,7 @@
* Command Code gateway client — POST /alpha/generate NDJSON stream.
*/
import { createHash, randomUUID } from "node:crypto";
+import { readdirSync } from "node:fs";
import {
GENERATE_ROUTE,
emptyUsage,
@@ -83,12 +84,23 @@ export function buildAuthHeaders(auth: GatewayAuthHeaders): Record !name.startsWith("."))
+ .sort()
+ .slice(0, 200);
+ } catch {
+ structure = [];
+ }
+
return {
config: {
- workingDir: process.env.OPENCODE_COMMANDCODE_CWD || process.cwd(),
+ workingDir: cwd,
date: new Date().toISOString().slice(0, 10),
environment: process.platform,
- structure: "",
+ structure,
isGitRepo: false,
currentBranch: "",
mainBranch: "",
@@ -100,7 +112,8 @@ export function buildGenerateBody(
skills: null,
permissionMode: params.permissionMode || "auto-accept",
...(params.threadId ? { threadId: params.threadId } : {}),
- mode: "default",
+ // Feature mode — must be a gateway feature id, not "default".
+ mode: "agent",
params: {
model: params.model,
messages: params.messages,
diff --git a/test/live-laguna.ts b/test/live-laguna.ts
index 9e1113e..164a317 100644
--- a/test/live-laguna.ts
+++ b/test/live-laguna.ts
@@ -29,8 +29,45 @@ function requireAuth(): string {
return creds.apiKey;
}
-async function readSseText(res: Response): Promise {
- return res.text();
+
+async function assertOk(res: Response, label: string): Promise {
+ const text = await res.text();
+ if (res.status !== 200) {
+ throw new Error(`${label}: expected 200, got ${res.status}\n${text.slice(0, 2000)}`);
+ }
+ return text;
+}
+
+/** Concatenate streamed text/reasoning deltas from an OpenAI SSE body. */
+function sseAssistantText(sse: string): string {
+ let out = "";
+ for (const line of sse.split("\n")) {
+ if (!line.startsWith("data: ")) continue;
+ const raw = line.slice(6).trim();
+ if (!raw || raw === "[DONE]") continue;
+ try {
+ const chunk = JSON.parse(raw) as {
+ choices?: Array<{
+ delta?: { content?: string; reasoning_content?: string };
+ message?: { content?: string; reasoning_content?: string };
+ }>;
+ };
+ const choice = chunk.choices?.[0];
+ const delta = choice?.delta;
+ const message = choice?.message;
+ if (typeof delta?.content === "string") out += delta.content;
+ if (typeof delta?.reasoning_content === "string") {
+ out += delta.reasoning_content;
+ }
+ if (typeof message?.content === "string") out += message.content;
+ if (typeof message?.reasoning_content === "string") {
+ out += message.reasoning_content;
+ }
+ } catch {
+ // ignore non-JSON keepalives
+ }
+ }
+ return out;
}
async function main() {
@@ -63,9 +100,9 @@ async function main() {
],
}),
});
- assert.equal(res.status, 200, await res.text());
- const text = await readSseText(res);
- assert.ok(/Laguna live ok/i.test(text), text.slice(0, 2000));
+ const text = await assertOk(res, "plain");
+ const assistant = sseAssistantText(text);
+ assert.ok(/Laguna live ok/i.test(assistant), assistant || text.slice(0, 2000));
assert.ok(text.includes("usage"), "expected usage in stream");
console.log("✓ live plain completion");
}
@@ -108,8 +145,8 @@ async function main() {
],
}),
});
- assert.equal(res.status, 200, await res.text());
- const json = (await res.json()) as {
+ const raw = await assertOk(res, "attachments");
+ const json = JSON.parse(raw) as {
choices: Array<{ message: { content: string } }>;
usage?: { prompt_tokens: number };
};
@@ -159,8 +196,7 @@ async function main() {
tools,
}),
});
- assert.equal(res1.status, 200, await res1.text());
- const turn1 = await readSseText(res1);
+ const turn1 = await assertOk(res1, "tools-turn1");
assert.ok(
turn1.includes("add_numbers") || turn1.includes("tool_calls"),
turn1.slice(0, 2500),
@@ -202,7 +238,6 @@ async function main() {
} catch {
parsed = {};
}
- // args may have been streamed in pieces — try to find numbers in the SSE
if (typeof parsed.a !== "number") {
const m = turn1.match(/"a"\s*:\s*(\d+)/);
const n = turn1.match(/"b"\s*:\s*(\d+)/);
@@ -254,9 +289,9 @@ async function main() {
tools,
}),
});
- assert.equal(res2.status, 200, await res2.text());
- const turn2 = await readSseText(res2);
- assert.ok(/42/.test(turn2), turn2.slice(0, 2000));
+ const turn2 = await assertOk(res2, "tools-turn2");
+ const assistant2 = sseAssistantText(turn2);
+ assert.ok(/42/.test(assistant2) || /42/.test(turn2), assistant2 || turn2.slice(0, 2000));
console.log("✓ live tools park/resume");
}
}
@@ -264,8 +299,8 @@ async function main() {
// 4) Usage totals
{
const res = await fetch(`${base}/v1/usage`);
- assert.equal(res.status, 200);
- const json = (await res.json()) as {
+ const raw = await assertOk(res, "usage");
+ const json = JSON.parse(raw) as {
total: { prompt_tokens: number; completion_tokens: number };
sessions: Array<{ tools: unknown[]; contextWindow: number }>;
};
@@ -295,8 +330,8 @@ async function main() {
],
}),
});
- assert.equal(res.status, 200, await res.text());
- const json = (await res.json()) as {
+ const raw = await assertOk(res, "compact");
+ const json = JSON.parse(raw) as {
choices: Array<{ message: { content?: string; reasoning_content?: string } }>;
};
const content =