diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 26a1a4cf..5af71ed9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -49,6 +49,12 @@ jobs:
working-directory: adapters/agent
- run: npm test && npm run check
working-directory: adapters/agent
+ - uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: "1.3.14"
+ - name: Verify OpenCode SDK adapter
+ working-directory: adapters/opencode-sdk
+ run: bun install --frozen-lockfile --ignore-scripts && bun run check && bun test --timeout 30000
- name: Test adapter against real backend
env:
SCOPEWEAVER_ADAPTER_E2E: "1"
diff --git a/README.es.md b/README.es.md
index 4b60812a..e7397bf6 100644
--- a/README.es.md
+++ b/README.es.md
@@ -126,6 +126,8 @@ activa ni contacta al proveedor. Coding Plan está restringido a herramientas of
ScopeWeaver no figura entre ellas. Consulta [configuración de proveedores y límites de compatibilidad](docs/es/llm-providers.md).
Para reducir tokens de nube, consulta [Controlar el uso de tokens](docs/es/llm-providers.md#controlar-el-uso-de-tokens).
+El **puente OpenCode SDK** opcional admite `native` CLI/cliente **2.1.4** (predeterminado) y `legacy` SDK/CLI **1.19.4**, con token independiente y `providerID/modelID`. Norma de ScopeWeaver conserva herramientas y aprobaciones. El modo heredado mantiene JSON seguro, permite normalizar espacios/escapes, no conserva bytes originales y rechaza enteros inseguros. Ambos modos carecen de soporte Windows; las pruebas previas cubrieron macOS arm64 y el resultado CI alojado de Linux sigue pendiente. El modo LLM solo local lo rechaza porque el proveedor puede ser nube. Consulta [inicio, límites y preparación de degradación SDK](docs/es/llm-providers.md#opencode-sdk-bridge): los binarios anteriores no arrancan mientras existan perfiles SDK.
+
Los perfiles opcionales de **Ollama nativo** funcionan sin clave API. Tres plantillas cubren Ollama en el mismo
equipo, en el host desde Docker y el servicio opcional `docker compose --profile local-llm`. Elegir una consulta los modelos instalados y rellena uno, dando preferencia a la compatibilidad con herramientas declarada;
no descarga ni ejecuta modelos. Envían un contexto de 32K (`num_ctx` 32,768), una solicitud a la vez y keep-alive de 30 minutos. El ajuste opcional
diff --git a/README.ja.md b/README.ja.md
index fa568df3..a8047372 100644
--- a/README.ja.md
+++ b/README.ja.md
@@ -103,6 +103,8 @@ PostgreSQL のデータベース 1 つにつき、実行中の ScopeWeaver サ
**LLM → 新規**フォームには、Z.ai General API 用の GLM-5.3 設定テンプレートと、明確に区別した Coding Plan の参考設定があります。自分の API キーを追加してください。テンプレートの選択では、保存、有効化、プロバイダーへの通信は行いません。Coding Plan は公式対応ツールのみに制限され、ScopeWeaver は一覧にありません。[プロバイダーの設定と対応範囲](docs/ja/llm-providers.md)を参照してください。クラウドのトークン使用量を抑える方法は[トークン使用量の管理](docs/ja/llm-providers.md#controlling-token-use)にあります。
+任意の **OpenCode SDK ブリッジ**は固定 `native` CLI/クライアント **2.1.4**(既定)と `legacy` SDK/CLI **1.19.4** に対応し、独立したブリッジトークンと `providerID/modelID` を使います。ホストツールと承認は ScopeWeaver の Norma が管理します。旧版は安全な JSON 構造を保持しますが、空白/エスケープを正規化でき、生バイトを保証せず、危険な整数を拒否します。両方とも Windows 非対応。過去の実行テストは macOS arm64、Linux のホステッド CI 結果は未確認です。下流がクラウドの場合があるためローカル専用 LLM では拒否します。[SDK 設定・制限・ダウングレード準備](docs/ja/llm-providers.md#opencode-sdk-bridge)を参照してください。SDK プロファイルが残ると旧バイナリは起動できません。
+
任意の**ネイティブ Ollama** プロファイルは、API キーなしで動作します。同じマシン、Docker からホスト、任意の `docker compose --profile local-llm` サービスの 3 テンプレートがあります。選択時はインストール済みモデルを読み、ツール対応を宣言したものを優先して設定しますが、ダウンロードや実行はしません。32K のコンテキスト(`num_ctx` 32,768)、同時リクエスト 1 件、キープアライブ 30 分を送ります。任意の**LLM のローカル限定ネットワーク**(既定で無効)は、ループバックと明示的に信頼した推論用 CIDR のみを許可し、LAN/Docker のアドレスにも許可リスト登録が必要です。すべてのエージェントのウェブ検索と更新の確認・ダウンロードもブロックし、ローカルのバージョン表示とロールバックは利用できます。対象用ツールのネットワーク接続は維持します。ホスト・リモートの Ollama では、別途 `OLLAMA_NO_CLOUD=1` が必要です。厳密モード以外では、クラウド形式、独自 URL、プロキシは引き続き動作します。ネイティブ要求は、待機中の判定・メインエージェント・補足の質問を次の空き枠に優先し、ネイティブ判定は 90 秒・ask の既定値と、上限付きのコンテキスト保護を持ちます。[ネイティブ Ollama](docs/ja/llm-providers.md#native-ollama-local-inference)を参照してください。
> **PostgreSQL** が必要です。探索には **LLM** の設定が必要です
diff --git a/README.ko.md b/README.ko.md
index e2bbcf75..2b7def47 100644
--- a/README.ko.md
+++ b/README.ko.md
@@ -118,6 +118,15 @@ PostgreSQL 데이터베이스 하나에는 ScopeWeaver 서버 프로세스를
[제공자 설정과 지원 범위](docs/ko/llm-providers.md)를 확인하세요.
클라우드 토큰 사용량은 [토큰 사용량 줄이기](docs/ko/llm-providers.md#토큰-사용량-줄이기)를 참고하세요.
+선택형 **OpenCode SDK 브리지** 프로필은 고정된 `native` CLI·클라이언트 **2.1.4**(기본값)와
+`legacy` SDK·CLI **1.19.4**, 별도 브리지 토큰, `providerID/modelID`를 지원합니다.
+호스트 도구와 승인은 ScopeWeaver의 Norma 런타임이 관리합니다. 레거시는 안전한 JSON 구조를 보존하며
+무해한 공백·이스케이프는 정규화될 수 있습니다. 원본 바이트는 보장하지 않으며 안전하지 않은 정수는 거부합니다.
+두 런타임 모두 Windows는 지원하지 않습니다. 이전 실제 런타임 테스트 범위는 macOS arm64이며
+Linux의 호스팅된 CI 결과는 아직 없습니다. 하위 제공자가 클라우드일 수 있어 로컬 전용 LLM 모드에서는 거부됩니다.
+[SDK 설정·제약·다운그레이드 준비](docs/ko/llm-providers.md#opencode-sdk-브리지)를 참고하세요.
+SDK 프로필이 남아 있으면 이전 바이너리는 시작할 수 없습니다.
+
선택 사항인 **기본 Ollama** 프로필은 API 키 없이 동작합니다. 같은 컴퓨터의 Ollama, Docker에서
접근하는 호스트 Ollama, 선택형 `docker compose --profile local-llm` 서비스용 템플릿 세 개를
제공합니다. 선택하면 서버에 설치된 모델을 조회하고 도구 호출 지원을 선언한 모델을 우선 채웁니다.
diff --git a/README.md b/README.md
index 01f086b5..e18430ee 100644
--- a/README.md
+++ b/README.md
@@ -124,6 +124,15 @@ activate, or contact a provider. Coding Plan use is restricted to officially sup
ScopeWeaver is not listed. See [provider setup and support limits](docs/llm-providers.md).
To reduce cloud token use, see [Controlling token use](docs/llm-providers.md#controlling-token-use).
+Optional **OpenCode SDK bridge** profiles support pinned `native` CLI/client **2.1.4** (default) and
+`legacy` SDK/CLI **1.19.4**, with a separate bridge token and `providerID/modelID`. ScopeWeaver's
+Norma runtime keeps host tools and approvals. Legacy preserves safe JSON structure, allowing benign
+whitespace/escape normalization; it does not preserve original bytes and rejects unsafe integers. Both runtimes
+are unsupported on Windows. Prior runtime tests cover macOS arm64; Linux hosted CI results are pending.
+Local-only LLM mode rejects this bridge because its downstream provider may be cloud-based. See
+[SDK setup, limits and downgrade preparation](docs/llm-providers.md#opencode-sdk-bridge);
+older binaries cannot start with SDK profiles present.
+
Optional **native Ollama** profiles run without an API key. Three templates cover Ollama on the same
machine, on the host from Docker, and the opt-in `docker compose --profile local-llm` service. Selecting one reads installed models and prefills one, preferring declared tool support;
it does not download or run a model. They send a 32K context (`num_ctx` 32,768), one request at a time and a 30-minute keep-alive. An opt-in
diff --git a/README.zh-Hans.md b/README.zh-Hans.md
index e2640025..15581a4d 100644
--- a/README.zh-Hans.md
+++ b/README.zh-Hans.md
@@ -96,6 +96,8 @@
「LLM → 新增」表单提供 Z.ai General API 的 GLM-5.3 配置模板,以及另行标示的 Coding Plan 参考配置。请自行加入 API 密钥;选取模板不会保存、启用或联络供应商。Coding Plan 仅限官方支持的工具使用,ScopeWeaver 不在清单内。详见[供应商配置与支持限制](docs/zh-Hans/llm-providers.md)。若要减少云端 token 用量,请参阅[控制 token 用量](docs/zh-Hans/llm-providers.md#controlling-token-use)。
+可选的 **OpenCode SDK 桥接器**支持固定的 `native` CLI/客户端 **2.1.4**(默认)与 `legacy` SDK/CLI **1.19.4**,使用独立桥接令牌及 `providerID/modelID`。宿主工具和审批仍由 ScopeWeaver 的 Norma 管理。旧版保留安全 JSON 结构,但可规范化空白/转义,不保留原始字节,并拒绝不安全整数。两个运行时均不支持 Windows;先前运行测试范围为 macOS arm64,Linux 托管 CI 结果尚待验证。下游可能是云端,因此仅本地 LLM 模式拒绝桥接器。见 [SDK 设置、限制与降级准备](docs/zh-Hans/llm-providers.md#opencode-sdk-bridge);SDK 配置仍存在时,旧二进制无法启动。
+
可选的**原生 Ollama** 配置不需要 API 密钥。三种模板分别适用于同一台电脑、从 Docker 存取主机,以及选择性启用的 `docker compose --profile local-llm` 服务。选取模板会读取已安装模型并预填一个模型,优先选择声明支持工具的模型;不会下载或运行模型。默认传送 32K 上下文(`num_ctx` 32,768)、每次一个请求,以及 30 分钟的 keep-alive。选择性启用的**仅限本地 LLM 网络**配置默认关闭,仅允许环回地址与明确信任的推论 CIDR;LAN 与 Docker 地址必须加入允许清单。此配置也会封锁所有智能体网页搜寻与更新检查/下载,但仍可查看本地版本与回滚。目标工具仍可存取网络;主机/远程 Ollama 必须另设 `OLLAMA_NO_CLOUD=1`。非严格模式仍可使用云端格式、自定义 URL 与代理服务器。原生请求会将下一个可用名额优先提供给排队中的裁决/主要智能体/侧问请求;原生裁决默认为 90 秒/ask,并有上下文边界保护。详见[原生 Ollama](docs/zh-Hans/llm-providers.md#native-ollama-local-inference)。
> 需要 **PostgreSQL** 数据库。探索需要已配置的 **LLM**(`ANTHROPIC_API_KEY`、`OPENAI_API_KEY`、免密钥 Ollama 配置,或在界面中配置)。
diff --git a/README.zh-Hant.md b/README.zh-Hant.md
index 0df9ac54..22c3d11f 100644
--- a/README.zh-Hant.md
+++ b/README.zh-Hant.md
@@ -96,6 +96,8 @@
「LLM → 新增」表單提供 Z.ai General API 的 GLM-5.3 設定範本,以及另行標示的 Coding Plan 參考設定。請自行加入 API 金鑰;選取範本不會儲存、啟用或聯絡供應商。Coding Plan 僅限官方支援的工具使用,ScopeWeaver 不在清單內。詳見[供應商設定與支援限制](docs/zh-Hant/llm-providers.md)。若要減少雲端 token 用量,請參閱[控制 token 用量](docs/zh-Hant/llm-providers.md#controlling-token-use)。
+選用的 **OpenCode SDK 橋接器**支援固定的 `native` CLI/用戶端 **2.1.4**(預設)與 `legacy` SDK/CLI **1.19.4**,使用獨立橋接權杖及 `providerID/modelID`。主機工具與核准仍由 ScopeWeaver 的 Norma 管理。舊版保留安全 JSON 結構,但可正規化空白/跳脫,不保留原始位元組,並拒絕不安全整數。兩個執行環境皆不支援 Windows;先前測試範圍為 macOS arm64,Linux 託管 CI 結果尚待驗證。下游可能是雲端,因此僅本機 LLM 模式拒絕橋接器。請見 [SDK 設定、限制與降級準備](docs/zh-Hant/llm-providers.md#opencode-sdk-bridge);SDK 設定檔仍存在時,舊二進位檔無法啟動。
+
可選的**原生 Ollama** 設定檔不需要 API 金鑰。三種範本分別適用於同一台電腦、從 Docker 存取主機,以及選擇性啟用的 `docker compose --profile local-llm` 服務。選取範本會讀取已安裝模型並預填一個模型,優先選擇宣告支援工具的模型;不會下載或執行模型。預設傳送 32K 上下文(`num_ctx` 32,768)、每次一個請求,以及 30 分鐘的 keep-alive。選擇性啟用的**僅限本機 LLM 網路**設定預設關閉,僅允許回送位址與明確信任的推論 CIDR;LAN 與 Docker 位址必須加入允許清單。此設定也會封鎖所有代理程式網頁搜尋與更新檢查/下載,但仍可查看本機版本與復原。目標工具仍可存取網路;主機/遠端 Ollama 必須另設 `OLLAMA_NO_CLOUD=1`。非嚴格模式仍可使用雲端格式、自訂 URL 與代理伺服器。原生請求會將下一個可用名額優先提供給排隊中的裁決/主要代理程式/側問請求;原生裁決預設為 90 秒/ask,並有上下文邊界保護。詳見[原生 Ollama](docs/zh-Hant/llm-providers.md#native-ollama-local-inference)。
> 需要 **PostgreSQL** 資料庫。探索需要已設定的 **LLM**(`ANTHROPIC_API_KEY`、`OPENAI_API_KEY`、免金鑰 Ollama 設定檔,或在介面中設定)。
diff --git a/adapters/opencode-sdk/.gitignore b/adapters/opencode-sdk/.gitignore
new file mode 100644
index 00000000..1905390a
--- /dev/null
+++ b/adapters/opencode-sdk/.gitignore
@@ -0,0 +1,16 @@
+node_modules/
+.env
+.env.*
+provider-config*.json
+credentials*.json
+auth.json
+*.secret.json
+*.secrets.json
+secrets/
+credentials/
+*.local.json
+*.log
+.runtime/
+.cache/
+tmp/
+coverage/
diff --git a/adapters/opencode-sdk/README.es.md b/adapters/opencode-sdk/README.es.md
new file mode 100644
index 00000000..d7a4aa19
--- /dev/null
+++ b/adapters/opencode-sdk/README.es.md
@@ -0,0 +1,122 @@
+# Puente OpenCode SDK
+
+[English](README.md) · [한국어](README.ko.md) · [简体中文](README.zh-Hans.md) · [繁體中文](README.zh-Hant.md) · [日本語](README.ja.md) · [Español](README.es.md)
+
+Puente opcional limitado a loopback para **CLI/cliente/SDK nativo de OpenCode 2.1.4** y la pareja **SDK/CLI heredada 1.19.4**, probada por separado. ScopeWeaver conserva la conversación, ejecución de herramientas, aprobaciones, controles de alcance, reintentos y compactación. El puente transmite un solo paso del modelo y captura solicitudes de herramientas; no ejecuta herramientas de ScopeWeaver ni herramientas internas de OpenCode. Los proveedores API y OpenCode Go existentes funcionan sin este puente.
+
+## Inicio
+
+El entorno de CI probado es **Bun 1.3.14**. No se han verificado versiones posteriores.
+
+```sh
+cd adapters/opencode-sdk
+bun install --frozen-lockfile --ignore-scripts
+export SCOPEWEAVER_OPENCODE_TOKEN="$(openssl rand -hex 32)"
+bun run start
+```
+
+El token del puente debe contener al menos **32 caracteres no blancos**. El servidor CLI nativo privado usa una contraseña independiente generada por llamada (`OPENCODE_PASSWORD`) y autenticación HTTP Basic. No es el token del puente ni una clave del proveedor.
+
+En el perfil de proveedor de ScopeWeaver, elige `opencode-sdk`, configura `http://127.0.0.1:4097` e introduce ese **token del puente**. Es distinto de una clave API de nube. No reutilices un token de OpenCode Go/Console. Carga los modelos y selecciona el `providerID/modelID` exacto.
+
+El entorno predeterminado, sin iniciar sesión, usa un catálogo aislado y el descubrimiento público. Solo realiza solicitudes GET al catálogo: **nunca inferencia**. Muestra únicamente modelos OpenCode de precio cero presentes en la lista pública y reconocidos por la versión fijada. Los fallos del catálogo producen un error explícito. Figurar en la lista no acredita elegibilidad, cuota disponible ni éxito de inferencia. Los modelos gratuitos pueden cambiar o desaparecer; no hay sustitución predeterminada por modelos de pago ni por otras credenciales.
+
+**Verificación anterior del 25 de octubre de 2026 (KST):** `opencode/deepseek-v4.1-flash-free` completó una conversación real de ScopeWeaver mediante el puente de producción CLI/cliente nativo 2.1.4. Tres inferencias reales cubrieron la prueba de conexión de la interfaz, una solicitud de herramienta del anfitrión revisada y aprobada antes de afectar al archivo y la respuesta final visible `FREE_OK`. El modelo procedía del catálogo predeterminado de precio cero. No se usaron claves API, importaciones de cuentas, simulaciones de modelos/respuestas, identidades falsificadas ni modelos de pago alternativos. Las herramientas nativas siguieron bloqueadas y ScopeWeaver ejecutó la acción del anfitrión. Es evidencia previa de esa fecha, no una repetición realizada ahora.
+
+La inferencia nativa usa el CLI auténtico fijado y las API oficiales del cliente; el descubrimiento sin inferencia usa el SDK integrado. La ruta anterior del SDK integrado dedicada a capturar herramientas recibió HTTP 403 `FreeTierError`. El acceso gratuito alojado de la versión heredada sigue sin verificarse. Los modelos y la elegibilidad pueden cambiar; la prueba fechada no establece una cuota numérica ni disponibilidad futura. Un rechazo estructurado se devuelve como `free_tier_restricted`, sin elegir un modelo de pago.
+
+`SCOPEWEAVER_OPENCODE_PORT` modifica el puerto predeterminado 4097. `SCOPEWEAVER_OPENCODE_HOST` debe ser `127.0.0.1`. Se rechazan solicitudes de navegador con `Origin` y URL que no sean de loopback. Todas las rutas requieren el token: protégelo como una contraseña.
+
+`SCOPEWEAVER_OPENCODE_TIMEOUT_MS` limita la llamada completa entre **0 y 86,400,000 ms**. El valor predeterminado **0** deja la duración bajo el control de cancelación del anfitrión; no hay un límite oculto de dos minutos. La cancelación y el cierre tienen un plazo independiente de **1.5 segundos** para terminar por la fuerza. La consulta del catálogo público tiene **10 segundos**. Los límites son **16 MiB** por solicitud, **2,000,000 caracteres** por JSON de argumentos, **8,000,000 caracteres** para la suma de texto/razonamiento/argumentos del modelo nativo y **20,000,000 bytes** de salida del trabajador. Superarlos causa un error explícito, sin truncar datos.
+
+## Configuración dedicada del proveedor
+
+Para un modelo explícito compatible con OpenAI o una prueba local, indica en `SCOPEWEAVER_OPENCODE_PROVIDER_CONFIG` un archivo JSON administrado por el operador del puente. Guárdalo fuera del checkout o con el nombre ignorado por Git `provider-config.local.json`:
+
+```json
+{
+ "providers": {
+ "local": {
+ "name": "Local model",
+ "package": "@opencode/ai/providers/openai-compatible",
+ "settings": {
+ "baseURL": "http://127.0.0.1:8000/v1",
+ "apiKey": "explicit-provider-key-if-needed"
+ },
+ "models": {
+ "my-model": {
+ "name": "My model",
+ "capabilities": { "tools": true },
+ "limit": { "context": 200000, "output": 32000 },
+ "compatibility": { "maxTokensField": "max_tokens" }
+ }
+ }
+ }
+ }
+}
+```
+
+Selecciona `local/my-model`. El archivo sustituye el descubrimiento predeterminado de ese puente. No carga configuración OpenCode de usuario/proyecto, archivos `.env`, complementos, servidores MCP ni credenciales guardadas. Solo entrega al proveedor su `apiKey` explícita. Un destino remoto puede cobrar la inferencia; configurarlo no lo convierte en gratuito. Se rechazan paquetes SDK arbitrarios y claves de configuración adicionales.
+
+## Límites de ejecución
+
+Cada llamada usa un trabajador Bun separado, un entorno limitado a una lista permitida y rutas temporales propias de home/configuración/datos. El modo nativo controla un servidor privado del CLI oficial, una ejecución real `run`, su base de datos temporal y un transporte de salida loopback autenticado de un solo uso. El cliente oficial gestiona descubrimiento, eventos de sesión e interrupción. No se heredan credenciales de nube, archivos de autenticación del usuario, proxies ni precargas. Se usan directamente los binarios opcionales de plataforma fijados; `--ignore-scripts` sigue siendo válido y no hace falta instalación global. **No omitas las dependencias opcionales**.
+
+Se conservan las definiciones y las implementaciones originales de las herramientas nativas de OpenCode. Todos sus permisos son ASK y no se usa `--auto`. Un hook público `execute.before` rechaza de manera independiente, antes de ejecutarse, cualquier herramienta ajena al anfitrión, incluida la generada `execute`. Una solicitud nativa hace fallar la llamada del puente y no llega a ScopeWeaver. Solo las funciones registradas de captura del anfitrión pueden esperar y convertirse en solicitudes de herramientas del anfitrión; se eliminan los demás agentes. Los alias sin colisiones `sw_tool_N` evitan reemplazar herramientas nativas. Las descripciones conservan el nombre y texto originales, y las respuestas restauran el nombre del anfitrión.
+
+Los segmentos system del anfitrión se unen con dos saltos de línea y se pasan sin cambios mediante la configuración pública del agente personalizado. También se conserva el contexto generado por OpenCode sobre modelo, entorno y herramientas. La política final informa de que solo se permiten funciones marcadas `Host tool:`. El hook público context mantiene roles, ID/resultados de herramientas y límites/configuración por llamada. No se garantiza que system contenga solo los segmentos del anfitrión. Se transmiten campos thinking/effort específicos del modelo; las opciones no admitidas fallan explícitamente. Un límite de salida declarado menor reduce el máximo solicitado. Entrada, salida y caché se contabilizan por separado.
+
+Se bloquea una segunda solicitud al modelo. El transporte privado reenvía la solicitud auténtica al URL original, rechaza redirecciones y no sustituye destino, modelo, identidad ni credenciales. Las herramientas capturadas solo se entregan después de interrumpir, borrar la sesión y limpiar los procesos propios. Desconexión, DELETE, vencimiento y cierre terminan el grupo POSIX del trabajador y lo fuerzan después de **1.5 segundos**. Esa limpieza del árbol de procesos no está implementada en Windows: **ambos modos, nativo y heredado, rechazan inferencia en Windows**.
+
+La configuración pública del CLI no permite desactivar por completo la búsqueda de configuración de sistema/MDM. El modo nativo rechaza el inicio con `configuration_error` si existe `/Library/Application Support/opencode` o `ai.opencode.managed.plist` bajo `/Library/Managed Preferences` global/del usuario en macOS, o `/etc/opencode` en Linux. También rechaza rutas inaccesibles y enlaces simbólicos rotos. No lee ni modifica esas fuentes.
+
+El modo nativo conserva el JSON original de argumentos, incluidos enteros de 64 bits, en respuestas completas, flujos e historial de herramientas del anfitrión. La proyección analizada por JavaScript nunca se ejecuta. Rechaza números inseguros fuera de los argumentos originales autoritativos, contenido no admitido, herramientas nativas desconocidas, ID duplicados y respuestas malformadas. Los esquemas admiten definiciones en línea, JSON Pointer local `$ref`/`$defs` y uniones; rechazan referencias remotas, `$id`, `$dynamicRef` y referencias sin resolver. La inicialización aún carga código propio de proveedores/catálogo: es una frontera de la aplicación, **no un aislamiento del sistema operativo**. Un puente loopback puede usar un modelo remoto y no cumple la política de inferencia solo local de ScopeWeaver.
+
+## Versiones y límites del modo heredado
+
+| `SCOPEWEAVER_OPENCODE_RUNTIME` | Pareja fijada | Capacidad `runtime` |
+| --- | --- | --- |
+| `native` (predeterminado) | CLI oficial, `@opencode/client` y SDK **2.1.4** | `native` |
+| `legacy` | `@opencode-ai/sdk/v2` y CLI **1.19.4** | `legacy` |
+
+Define `SCOPEWEAVER_OPENCODE_RUNTIME=legacy` antes de iniciar para usar la API antigua. La verificación local previa de ambos entornos reales cubrió **macOS arm64**. CI está configurada para pruebas reales en Linux, pero su resultado alojado sigue pendiente. Otras versiones SDK se rechazan antes de inferir; no se garantiza compatibilidad universal entre versiones y plataformas.
+
+El modo heredado usa directamente el binario oficial opcional correspondiente. Por eso admite `--ignore-scripts`: no llama a un envoltorio sin inicializar ni a una instalación global de `opencode`. No omitas las dependencias opcionales. Su CLI y el servidor MCP loopback de captura terminan con el grupo POSIX del trabajador. Windows no dispone de esa limpieza. Cada llamada usa una copia privada del paquete del complemento y de su archivo de bloqueo, sin enlaces escribibles a dependencias compartidas.
+
+El modo heredado admite JSON/historial estructuralmente válido y representable con seguridad. Puede normalizar espacios y escapes HTML/Unicode equivalentes; no conserva los bytes léxicos originales. Rechaza explícitamente enteros inseguros cuyo valor absoluto supere **9,007,199,254,740,991**, historial de razonamiento opaco/firmado y sobrescrituras de proveedor no admitidas. Usa el modo nativo para conservar argumentos originales de 64 bits. Los proveedores heredados personalizados usan la configuración explícita compatible con OpenAI anterior, pero solo se mapean la compatibilidad `max_tokens`, las capacidades de herramientas admitidas y los límites de contexto/salida. Se rechazan `reasoningField` y sobrescrituras de capacidades no admitidos. Los perfiles API/Go siguen disponibles para entradas fuera de los límites probados de ambos modos SDK.
+
+## Protocolo y comprobaciones
+
+Protocolo **`scopeweaver-opencode-sdk`, versión 1**, con rutas autenticadas: `GET /v1/capabilities`, `GET /v1/models`, `POST /v1/complete`, `POST /v1/stream` y `DELETE /v1/requests/{request_id}`. El flujo NDJSON transmite deltas reales de texto/razonamiento, solicitudes capturadas y uso final; en caso de éxito hay exactamente un `message_stop`. Los errores usan códigos acotados y mensajes con datos del proveedor ocultos. Solo un agotamiento estructurado explícito de facturación/cuota se clasifica como `quota_exhausted` (HTTP 402 al completar); límites genéricos de frecuencia, autenticación y 5xx no se reclasifican. Un exceso estructurado de contexto devuelve `context_length_exceeded` (HTTP 400), manteniendo la señal de compactación del anfitrión; no basta un 400 genérico ni palabras en el mensaje. `FreeTierError` estructurado produce `free_tier_restricted` (HTTP 403), separado de cuota y prohibiciones genéricas. El puente no elige un modelo alternativo; el anfitrión decide según su política configurada.
+
+Ejemplo de solicitud completa; el endpoint de flujo acepta el mismo cuerpo:
+
+```json
+{
+ "request_id": "unique-call-uuid",
+ "session_id": "stable-host-conversation-uuid",
+ "model": "local/my-model",
+ "system": ["Host system instructions"],
+ "messages": [{ "role": "user", "content": [{ "type": "text", "text": "Hello" }] }],
+ "tools": [],
+ "max_tokens": 64,
+ "thinking": "disabled"
+}
+```
+
+Campos opcionales: `session_id`, `temperature`, `stop` (lista de cadenas), `thinking`, `thinking_type` y `reasoning_effort`. Los bloques admitidos son `text`, `thinking`/`tool_use` del asistente y `tool_result` del usuario con texto. Las herramientas declaran `name`, `description` e `input_schema`. La respuesta completa contiene `message`, `stop_reason` (`end_turn`, `tool_use` o `max_tokens`) y `usage` con entrada/salida/lectura de caché/escritura de caché separadas. Consulta el contrato y límites en [protocol.mjs](src/protocol.mjs) y el transporte en [bridge.mjs](src/bridge.mjs).
+
+En `opencode-go`, `session_id` mantiene estable `x-opencode-session` entre sesiones SDK nuevas por llamada; si falta se usa `request_id`. `opencode` conserva las cabeceras genuinas de sesión e identidad generadas por el entorno. Los proveedores genéricos no reciben cabeceras de sesión específicas de la pasarela. Se conserva el User-Agent auténtico del CLI.
+
+```sh
+bun run check
+bun test --timeout 30000
+```
+
+Las pruebas usan la pareja real fijada de CLI/cliente/SDK con modelos SSE locales. Verifican descubrimiento sin inferencia, historial/configuración, captura segura, flujo/uso, argumentos numéricos originales, llamadas nativas hostiles, aislamiento concurrente y cancelación. No prueban la elegibilidad real de modelos gratuitos alojados.
+
+## Perfiles de ScopeWeaver y límite de degradación
+
+Un token vacío conserva el guardado solo para el mismo endpoint SDK. Al cambiar URL o formato no se reutiliza; `localhost`, `127.0.0.1` y `::1` son ámbitos distintos de credenciales. La plantilla SDK borra claves de nube sin guardar, proxy y cabecera de sesión personalizada, deja el modelo vacío y no descubre, infiere, guarda ni activa. Usa **Cargar modelos** de forma explícita; **Probar conexión** solo hace inferencia al pulsarlo.
+
+La restricción `CHECK` de formatos de perfil añade `opencode-sdk`. Los binarios antiguos no arrancan mientras queden perfiles SDK. Antes de degradar, respalda la base de datos y usa la versión actual para convertirlos a un formato antiguo compatible o eliminarlos. La verificación previa de migración usó bases desechables, sin migrar una base de producción. Consulta la [guía de proveedores](../../docs/es/llm-providers.md#opencode-sdk-bridge).
diff --git a/adapters/opencode-sdk/README.ja.md b/adapters/opencode-sdk/README.ja.md
new file mode 100644
index 00000000..8f087d8c
--- /dev/null
+++ b/adapters/opencode-sdk/README.ja.md
@@ -0,0 +1,122 @@
+# OpenCode SDK ブリッジ
+
+[English](README.md) · [한국어](README.ko.md) · [简体中文](README.zh-Hans.md) · [繁體中文](README.zh-Hant.md) · [日本語](README.ja.md) · [Español](README.es.md)
+
+任意で使用するループバック専用ブリッジです。固定した **OpenCode ネイティブ CLI/クライアント/SDK 2.1.4** と、別途テストした **旧 SDK/CLI 1.19.4** の組み合わせに対応します。対話、ツール実行、承認、スコープ検査、再試行、圧縮は ScopeWeaver が管理します。ブリッジはモデルの 1 ステップをストリーム配信してツール要求を収集し、ScopeWeaver のツールや OpenCode の組み込みツールを実行しません。従来の API/OpenCode Go プロバイダーはブリッジなしで利用できます。
+
+## 起動
+
+テスト済み CI ランタイムは **Bun 1.3.14** です。新しいバージョンは未検証です。
+
+```sh
+cd adapters/opencode-sdk
+bun install --frozen-lockfile --ignore-scripts
+export SCOPEWEAVER_OPENCODE_TOKEN="$(openssl rand -hex 32)"
+bun run start
+```
+
+ブリッジトークンは**空白以外の 32 文字以上**が必要です。非公開のネイティブ CLI サーバーは呼び出しごとに生成する独立したパスワード(`OPENCODE_PASSWORD`)と HTTP Basic 認証を使います。ブリッジトークンや上流プロバイダーキーとは別です。
+
+ScopeWeaver のプロバイダープロファイルで `opencode-sdk`、URL `http://127.0.0.1:4097` を選び、上記の**ブリッジトークン**を入力します。クラウド API キーとは別です。OpenCode Go/Console のトークンを流用しないでください。モデル一覧を読み込んで正確な `providerID/modelID` を選びます。
+
+既定の未ログインランタイムは、独立したカタログと公開モデル探索を使用します。探索は公開カタログへの GET のみで、**推論を実行しません**。公開一覧に現在存在し、固定ランタイムが認識する料金ゼロの OpenCode モデルだけを表示します。カタログ取得失敗はエラーです。掲載は利用資格、残り割当量、推論成功の証明ではありません。無料モデルは変更・削除される場合があり、既定では有料モデルや認証情報へのフォールバックを行いません。
+
+**2026-10-25(KST)の過去の検証:** 製品のネイティブ CLI/クライアント 2.1.4 ブリッジを通じ、`opencode/deepseek-v4.1-flash-free` で実際の ScopeWeaver 対話が完了しました。実推論 3 回で UI 接続テスト、ファイルに影響する前に確認・承認したホストツール要求、最終画面の回答 `FREE_OK` を確認しました。既定の料金ゼロのカタログを使用し、API キー、アカウントの取り込み、モデル/応答のモック、識別情報の偽装、有料モデルへの切替は使用していません。ネイティブツールは阻止され、ホスト操作は ScopeWeaver が実行しました。これは当日の既存の証拠であり、今回の再実行結果ではありません。
+
+ネイティブ推論は固定された本物の CLI と公式クライアント API を使用し、推論なしの探索には埋め込み SDK を使います。以前の収集専用の埋め込み SDK 経路は HTTP 403 `FreeTierError` で拒否されました。旧版でのホステッド無料アクセスは未検証です。モデルや利用資格は変わるため、当日のテストは数値的な割当量や継続提供を保証しません。構造化された拒否は `free_tier_restricted` として返し、有料モデルを選びません。
+
+`SCOPEWEAVER_OPENCODE_PORT` で既定の 4097 を変更できます。`SCOPEWEAVER_OPENCODE_HOST` は `127.0.0.1` に固定します。ブラウザーの `Origin` 付き要求と非ループバック URL は拒否します。すべての API 経路にトークンが必要です。パスワード同様に保護してください。
+
+`SCOPEWEAVER_OPENCODE_TIMEOUT_MS` は呼び出し全体の期限 **0–86,400,000 ミリ秒**です。既定 **0** はホストのキャンセルに委ね、隠れた 2 分の推論期限はありません。キャンセル/終了には別途 **1.5 秒**の強制終了期限があります。公開カタログ取得は **10 秒**です。要求本文は **16 MiB**、ツール引数 JSON 1 件は **2,000,000 文字**、ネイティブモデルのテキスト/思考/引数合計は **8,000,000 文字**、ワーカー出力は **20,000,000 バイト**が上限です。超過時は明示的に失敗し、データを切り詰めません。
+
+## 専用プロバイダー設定
+
+明示的な OpenAI 互換モデルやローカルテストモデルには、運用者が管理する JSON ファイルを `SCOPEWEAVER_OPENCODE_PROVIDER_CONFIG` に指定します。チェックアウト外、または Git が無視する `provider-config.local.json` という名前で保存します。
+
+```json
+{
+ "providers": {
+ "local": {
+ "name": "Local model",
+ "package": "@opencode/ai/providers/openai-compatible",
+ "settings": {
+ "baseURL": "http://127.0.0.1:8000/v1",
+ "apiKey": "explicit-provider-key-if-needed"
+ },
+ "models": {
+ "my-model": {
+ "name": "My model",
+ "capabilities": { "tools": true },
+ "limit": { "context": 200000, "output": 32000 },
+ "compatibility": { "maxTokensField": "max_tokens" }
+ }
+ }
+ }
+ }
+}
+```
+
+`local/my-model` を選択します。このファイルはブリッジの既定探索を置き換えます。ユーザー/プロジェクトの OpenCode 設定、`.env`、プラグイン、MCP サーバー、保存済み認証情報を読み込みません。明示した `apiKey` のみを対象プロバイダーに渡します。リモート推論は有料の場合があり、設定を追加しても無料にはなりません。任意の SDK パッケージや余分な設定キーは拒否します。
+
+## 実行境界
+
+呼び出しごとに独立した Bun ワーカーを使い、環境変数を許可リストに限定し、一時 home/設定/データを所有します。ネイティブモードは非公開の公式 CLI サーバー、実際の CLI `run`、一時 DB、1 回限りの認証付きループバック出口転送を所有します。公式クライアントが探索、セッションイベント、中断を処理します。クラウド認証情報、ユーザー認証ファイル、プロキシ、プリロード設定を継承しません。固定されたオプション依存のプラットフォームバイナリを直接使うため、`--ignore-scripts` が有効でグローバルインストールは不要です。**オプション依存を省略しないでください**。
+
+OpenCode 本来の組み込みツール定義と実装を保持します。ネイティブ権限はすべて ASK で、CLI `--auto` は使いません。公開 `execute.before` ポリシーフックが、生成された `execute` を含むすべての非ホストツールを実行前に独立して拒否します。ネイティブツール要求はブリッジ要求を失敗させ、ScopeWeaver に渡しません。登録されたホスト収集関数だけが待機し、ホストツール要求になります。他のエージェントは削除します。衝突しない `sw_tool_N` 別名で既定ツールの上書きを防ぎ、説明には正確な元の名前・説明を残し、応答では元のホスト名を復元します。
+
+ホストの system 部分を 2 改行で結合し、公開カスタムエージェント system 設定にそのまま渡します。OpenCode が生成するモデル/環境/ツールの文脈も保持します。最後のポリシー部分は `Host tool:` と記された関数だけが有効だと伝えます。公開 context フックでホストの役割、ツール ID/結果、呼び出しごとの制限・設定を保持します。system 配列がホスト部分のみになる保証はありません。モデル別の thinking/effort フィールドを転送し、未対応設定は明示的に失敗します。モデルの出力上限が要求より小さい場合はその上限に抑えます。入力、出力、キャッシュ使用量を分けて扱います。
+
+2 回目のモデル要求は阻止します。非公開出口は本物の CLI が生成した要求を元の上流 URL に転送し、リダイレクトを拒否します。宛先、モデル、識別情報、認証情報を差し替えません。ホストツール要求は中断、セッション削除、所有プロセスの後片付け後にのみ返します。切断、DELETE、タイムアウト、終了で POSIX ワーカープロセスグループを停止し、**1.5 秒**後に強制終了します。このプロセスツリーの後片付けは Windows に未実装のため、**ネイティブと旧版の両方で Windows の推論を拒否**します。
+
+CLI の公開設定ではシステム/MDM 設定探索を完全に無効化できません。macOS の `/Library/Application Support/opencode`、ユーザー/全体の `/Library/Managed Preferences` 下の `ai.opencode.managed.plist`、Linux の `/etc/opencode` が存在すると、ネイティブモードは `configuration_error` で起動を拒否します。アクセス不能な場所や壊れたシンボリックリンクも拒否し、その設定を読み取り・変更しません。
+
+ネイティブモードは完了/ストリーム応答とホスト履歴の再生で、64 ビット整数を含むツール引数の生 JSON を保持します。JavaScript で解析した値は実行しません。正本となる生引数以外の危険な数値、未対応コンテンツ、不明なネイティブツール、重複ツール ID、不正な応答は拒否します。スキーマはインライン定義、ローカル JSON Pointer `$ref`/`$defs`、ユニオンに対応し、リモート参照、`$id`、`$dynamicRef`、未解決参照を拒否します。初期化では本来のプロバイダー/カタログコードを読み込みます。これはアプリケーションの境界であり、**OS サンドボックスではありません**。ループバックの先にクラウドモデルがあり得るため、ScopeWeaver のローカル専用推論条件を満たしません。
+
+## バージョンと旧版の制限
+
+| `SCOPEWEAVER_OPENCODE_RUNTIME` | 固定した組み合わせ | 機能応答 `runtime` |
+| --- | --- | --- |
+| `native`(既定) | 公式 CLI、`@opencode/client`、SDK **2.1.4** | `native` |
+| `legacy` | `@opencode-ai/sdk/v2`、CLI **1.19.4** | `legacy` |
+
+起動前に `SCOPEWEAVER_OPENCODE_RUNTIME=legacy` を指定すると旧 API を選べます。過去に実際のランタイムをローカル検証した範囲は **macOS arm64** です。Linux CI に実ランタイムテストを設定していますが、ホステッド実行結果は未確認です。他の SDK バージョンは推論前に拒否します。任意のバージョン/プラットフォームの互換性は保証しません。
+
+旧版は対応する公式オプションバイナリを直接使用するため、`--ignore-scripts` が有効です。未初期化のラッパーやグローバル `opencode` は使いません。オプション依存を省略しないでください。所有する CLI とループバック MCP 収集サーバーは、要求ワーカーの POSIX プロセスグループと共に終了します。Windows ではこの境界を実装していません。呼び出しごとに専用のプラグインパッケージとロックのコピーを使い、共有依存への書き込み可能なリンクを作りません。
+
+旧版は構造的に正しく安全に表現できる JSON/履歴のみ対応します。空白や同値の HTML/Unicode エスケープは正規化されるため、元の字句・バイトを保証しません。絶対値が **9,007,199,254,740,991** を超える危険な整数、不透明/署名付き思考履歴、未対応のプロバイダー上書きは明示的に失敗します。生の 64 ビット引数が必要ならネイティブを使います。旧版のカスタムプロバイダーは上の明示的 OpenAI 互換設定を使いますが、`max_tokens` 互換フィールド、対応ツール機能、コンテキスト/出力上限のみをマッピングします。未対応の `reasoningField` と機能上書きは拒否します。SDK の境界外の入力には既存 API/Go プロファイルを使えます。
+
+## プロトコルと検証
+
+プロトコルは **`scopeweaver-opencode-sdk`、バージョン 1**。認証経路は `GET /v1/capabilities`、`GET /v1/models`、`POST /v1/complete`、`POST /v1/stream`、`DELETE /v1/requests/{request_id}` です。ストリームは NDJSON で、実際のネイティブテキスト/思考差分、収集したツール要求、最終使用量を送り、成功時は `message_stop` を 1 回だけ返します。エラーは限定した機械コードと機密を除いたメッセージを使用します。明示的で構造化された請求/割当量枯渇だけを `quota_exhausted`(完了要求 HTTP 402)とし、一般のレート制限、認証、5xx を枯渇へ置き換えません。構造化されたコンテキスト超過は `context_length_exceeded`(HTTP 400)としてホストの圧縮信号を維持します。一般の 400 やメッセージ内の語句だけでは分類しません。構造化された `FreeTierError` は `free_tier_restricted`(HTTP 403)で、枯渇や通常の禁止とは分けます。ブリッジは代替モデルを選ばず、ホストの設定済みポリシーが次の処理を決めます。
+
+完了要求例(ストリーム経路も同じ本文を受け付けます):
+
+```json
+{
+ "request_id": "unique-call-uuid",
+ "session_id": "stable-host-conversation-uuid",
+ "model": "local/my-model",
+ "system": ["Host system instructions"],
+ "messages": [{ "role": "user", "content": [{ "type": "text", "text": "Hello" }] }],
+ "tools": [],
+ "max_tokens": 64,
+ "thinking": "disabled"
+}
+```
+
+任意フィールドは `session_id`、`temperature`、文字列配列 `stop`、`thinking`、`thinking_type`、`reasoning_effort`。ホストコンテンツは `text`、assistant の `thinking`/`tool_use`、テキスト結果を持つ user の `tool_result` に対応します。ツール定義は `name`、`description`、`input_schema` を使います。完了応答は `message`、`stop_reason`(`end_turn`、`tool_use`、`max_tokens`)と入力/出力/キャッシュ読み取り/書き込みを分けた `usage` を含みます。公開契約と制限は [protocol.mjs](src/protocol.mjs)、転送は [bridge.mjs](src/bridge.mjs) を参照してください。
+
+`opencode-go` では `session_id` により、呼び出しごとの新 SDK セッションでも `x-opencode-session` を一定にします。未指定なら `request_id` を使います。`opencode` は実ランタイム生成のセッション/識別ヘッダーを維持します。通常のプロバイダーにゲートウェイ専用ヘッダーは付けません。本物の CLI の User-Agent を保持します。
+
+```sh
+bun run check
+bun test --timeout 30000
+```
+
+固定した実際の CLI/クライアント/SDK とローカル SSE モデルで、推論なし探索、ホスト履歴/設定、収集の安全性、ストリーム/使用量、生の数値引数、悪意あるネイティブ要求、同時実行の隔離、キャンセルを検証します。ホステッド無料モデルの実際の利用資格は証明しません。
+
+## ScopeWeaver プロファイルとダウングレード境界
+
+空のトークンで保存済み値を保持できるのは同じ SDK エンドポイントだけです。宛先や形式を変えた際に古いトークンを流用しません。`localhost`、`127.0.0.1`、`::1` は別の認証スコープです。SDK テンプレートは未保存のクラウドキー、プロキシ、独自セッションヘッダーを消し、モデルを空にします。探索・推論・保存・有効化は行いません。**モデル読み込み**を明示的に実行し、**接続テスト**はクリック時だけ推論します。
+
+プロファイル DB の `CHECK` 制約に `opencode-sdk` を追加します。SDK プロファイルが残ると古いバイナリは起動できません。ダウングレード前に DB をバックアップし、現在のバージョンで旧対応形式へ変換するか SDK プロファイルを削除します。過去の移行検証は使い捨て DB のみで、稼働中の DB は移行していません。[プロバイダーガイド](../../docs/ja/llm-providers.md#opencode-sdk-bridge)を参照してください。
diff --git a/adapters/opencode-sdk/README.ko.md b/adapters/opencode-sdk/README.ko.md
new file mode 100644
index 00000000..007995a5
--- /dev/null
+++ b/adapters/opencode-sdk/README.ko.md
@@ -0,0 +1,122 @@
+# OpenCode SDK 브리지
+
+[English](README.md) · [한국어](README.ko.md) · [简体中文](README.zh-Hans.md) · [繁體中文](README.zh-Hant.md) · [日本語](README.ja.md) · [Español](README.es.md)
+
+**네이티브 OpenCode CLI·클라이언트·SDK 2.1.4**와 따로 검증한 **구형 SDK·CLI 1.19.4**를 연결하는 선택 기능입니다. ScopeWeaver가 대화 기록, 도구 실행, 승인, 범위 검사, 재시도와 압축을 계속 관리합니다. 브리지는 모델의 한 단계만 스트리밍하고 도구 요청을 수집합니다. ScopeWeaver 도구나 OpenCode 기본 도구를 직접 실행하지 않습니다. 기존 API·OpenCode Go 공급자는 브리지 없이 사용할 수 있습니다.
+
+## 실행
+
+Bun **1.3.14**가 검증된 CI 런타임입니다. 이후 버전은 아직 검증하지 않았습니다.
+
+```sh
+cd adapters/opencode-sdk
+bun install --frozen-lockfile --ignore-scripts
+export SCOPEWEAVER_OPENCODE_TOKEN="$(openssl rand -hex 32)"
+bun run start
+```
+
+브리지 토큰은 **공백이 아닌 문자 32자 이상**이어야 합니다. native의 비공개 CLI 서버는 호출마다 별도로 생성한 비밀번호(`OPENCODE_PASSWORD`)와 HTTP Basic 인증을 사용합니다. 이 비밀번호는 브리지 토큰 및 하위 공급자 키와 서로 다릅니다.
+
+ScopeWeaver 공급자 프로필에서 `opencode-sdk`, 주소 `http://127.0.0.1:4097`, 위 **브리지 토큰**을 입력합니다. 클라우드 API 키가 아닙니다. 모델을 조회한 뒤 정확한 `providerID/modelID`를 선택하세요. Go·Console API 키를 브리지 토큰으로 재사용하지 마세요.
+
+기본 런타임은 로그인하지 않은 상태에서 격리된 카탈로그와 공개 모델 목록을 사용합니다. 조회는 공개 카탈로그 GET 요청만 수행하며 **모델 추론을 실행하지 않습니다**. 현재 목록에 있고 고정 SDK가 인식하는 무료 OpenCode 모델만 표시합니다. 조회 실패는 오류로 반환합니다. 목록에 있다고 사용 자격·할당량·추론 성공이 보장되지는 않습니다. 무료 모델은 변경되거나 사라질 수 있으며 기본 설정에 유료 모델·인증정보 대체 경로는 없습니다.
+
+**2026년 10월 25일(KST)의 이전 검증:** `opencode/deepseek-v4.1-flash-free`로 실제 ScopeWeaver 대화를 완료했습니다. 제품의 native CLI·클라이언트 2.1.4 브리지를 통해 실제 추론을 세 번 호출해 UI 연결 테스트, 파일에 영향을 주기 전에 검토·승인한 호스트 도구 요청, 최종 화면의 답변 `FREE_OK`를 확인했습니다. 기본 무료 카탈로그에서 모델을 선택했으며 API 키, 계정 가져오기, 모델·응답 모의 데이터, 식별 위장이나 유료 대체 경로를 사용하지 않았습니다. 기본 도구는 계속 차단했고 호스트 동작은 ScopeWeaver가 실행했습니다.
+
+native 추론은 고정한 실제 CLI와 공식 클라이언트 API를 사용하고 추론 없는 모델 조회는 내장 SDK를 사용합니다. 앞선 내장 SDK의 수집 전용 경로는 HTTP 403 `FreeTierError`로 거부되었습니다. legacy의 실제 무료 접근은 아직 검증하지 않았습니다. 무료 모델과 서비스 사용 자격은 바뀔 수 있으며 이번 날짜의 검증으로 수치화된 할당량이나 이후의 사용 가능성을 보장하지 않습니다. 구조화된 거부는 `free_tier_restricted`로 전달하며 유료 모델로 자동 전환하지 않습니다.
+
+포트는 `SCOPEWEAVER_OPENCODE_PORT`로 변경할 수 있습니다. 기본값은 4097입니다. `SCOPEWEAVER_OPENCODE_HOST`는 `127.0.0.1`만 허용합니다. 브라우저 `Origin` 요청과 루프백이 아닌 주소를 거부하며 모든 경로에 토큰 인증이 필요합니다.
+
+호출 시간 제한은 `SCOPEWEAVER_OPENCODE_TIMEOUT_MS`로 **0~86,400,000ms** 사이에서 설정합니다. 기본값 **0**은 호스트가 취소할 때까지 호출을 허용하며 숨겨진 2분 추론 제한이 없습니다. 취소·종료 시에는 별도의 **1.5초** 강제 종료 제한이 적용됩니다. 공개 카탈로그 네트워크 조회는 10초 제한입니다. 요청은 **16 MiB**, 반환되는 도구 인자 JSON 하나는 **2,000,000자**, 네이티브 모델의 텍스트·사고·인자 합계는 **8,000,000자**, 워커 출력은 **20,000,000바이트**로 제한합니다. 넘으면 오류를 반환하며 데이터를 잘라서 전달하지 않습니다.
+
+## 전용 공급자 설정
+
+명시적인 OpenAI 호환 공급자나 로컬 테스트 모델을 연결하려면 `SCOPEWEAVER_OPENCODE_PROVIDER_CONFIG`에 운영자가 관리하는 JSON 파일 경로를 지정합니다. 파일은 체크아웃 밖에 두거나 Git에서 제외되는 `provider-config.local.json` 이름을 사용하세요. JSON 예시는 [영문 안내](README.md#dedicated-provider-configuration)에 있습니다. 예시 모델 ID는 `local/my-model`입니다.
+
+```json
+{
+ "providers": {
+ "local": {
+ "name": "Local model",
+ "package": "@opencode/ai/providers/openai-compatible",
+ "settings": {
+ "baseURL": "http://127.0.0.1:8000/v1",
+ "apiKey": "explicit-provider-key-if-needed"
+ },
+ "models": {
+ "my-model": {
+ "name": "My model",
+ "capabilities": { "tools": true },
+ "limit": { "context": 200000, "output": 32000 },
+ "compatibility": { "maxTokensField": "max_tokens" }
+ }
+ }
+ }
+ }
+}
+```
+
+이 파일을 지정하면 기본 모델 조회 대신 파일에 명시한 모델만 사용합니다. 사용자·프로젝트 OpenCode 설정, `.env`, 외부 플러그인, MCP 서버, 저장된 인증정보를 불러오지 않습니다. 해당 공급자에는 파일에 명시한 `apiKey`만 전달합니다. 원격 공급자는 추론 비용을 청구할 수 있으며 설정을 추가한다고 무료가 되지는 않습니다. 임의 SDK 패키지와 지원하지 않는 설정 필드를 거부합니다.
+
+## 실행 경계
+
+각 호출은 환경 변수를 허용 목록으로 제한한 별도 Bun 워커와 임시 홈·설정·데이터 경로를 사용합니다. native는 실제 CLI의 비공개 서버, 실제 CLI `run`, 임시 DB, 한 번만 사용할 수 있는 인증된 루프백 추론 전송 서버를 소유합니다. 공식 클라이언트가 모델·세션 조회, 이벤트와 중단을 처리합니다. 클라우드 인증정보·사용자 인증 파일·프록시·사전 로드 설정을 상속하지 않습니다. 정확히 일치하는 공식 선택 의존성의 바이너리를 직접 사용하므로 `--ignore-scripts`가 유효하며 전역 설치가 필요 없습니다. 선택 의존성을 생략하지 마세요.
+
+native는 OpenCode의 실제 기본 도구 정의와 원래 구현을 유지합니다. 기본 도구 권한은 모두 ASK이고 CLI `--auto`는 사용하지 않습니다. 별도의 공개 `execute.before` 정책 훅이 생성된 `execute`를 포함한 모든 비호스트 도구를 실행 전에 거부합니다. 기본 도구를 요청하면 브리지 요청을 실패시키며 ScopeWeaver로 전달하지 않습니다. 등록한 호스트 수집 함수만 대기했다가 호스트 요청이 되고 다른 에이전트는 제거합니다. `sw_tool_N` 별칭으로 호스트 도구가 기본 도구를 덮어쓰지 않게 하며 설명과 반환값에 정확한 원래 이름을 보존합니다.
+
+호스트 system 구간은 두 줄바꿈으로 연결해 공개 사용자 지정 에이전트 system 설정에 그대로 넣습니다. OpenCode가 생성한 모델·환경·도구 문맥도 추가로 유지합니다. 마지막 정책 구간은 설명이 `Host tool:`로 시작하는 함수만 사용할 수 있음을 알립니다. 호스트 대화 역할, 도구 ID·결과, 호출별 제한·설정은 공개 context 훅으로 보존합니다. system 배열에 호스트 구간만 존재한다는 보장은 하지 않습니다. thinking·effort 필드는 공급자에 전달하며 지원하지 않는 설정은 명시적으로 실패합니다. 모델의 출력 한도가 작으면 그 한도까지 제한하고 입력·출력·캐시 사용량은 분리합니다.
+
+워커는 두 번째 모델 요청을 차단합니다. 비공개 전송 서버는 실제 CLI가 만든 요청을 원래 공급자 URL로 보내되 리디렉션을 거부하며 모델·주소·식별·인증정보를 대체하지 않습니다. 호스트 도구 요청은 중단, 세션 삭제, 소유 프로세스 정리가 끝난 뒤에만 전달합니다. 연결 종료·DELETE·시간 초과·서버 종료는 POSIX 워커 프로세스 그룹을 종료하며 1.5초 뒤 강제 종료합니다. 이 프로세스 트리 정리 경계가 구현되지 않아 **Windows에서는 native와 legacy 추론을 모두 거부**합니다.
+
+CLI의 공개 설정은 시스템·MDM 설정 탐색을 완전히 끌 수 없습니다. 따라서 macOS의 `/Library/Application Support/opencode`, 사용자·전역 `/Library/Managed Preferences` 아래 `ai.opencode.managed.plist`, Linux의 `/etc/opencode`가 있으면 native를 시작하지 않고 `configuration_error`로 실패합니다. 접근할 수 없는 경로와 끊어진 심볼릭 링크도 거부합니다. 해당 설정을 읽거나 변경하지 않습니다.
+
+native 모드는 완료·스트림 응답과 호스트 도구 기록 재생에서 64비트 정수를 포함한 도구 인자의 원본 JSON을 보존합니다. JavaScript로 파싱한 도구 인자를 실행하지 않습니다. 원본 도구 인자 밖에서 안전하게 표현할 수 없는 숫자, 지원하지 않는 콘텐츠, 알 수 없는 도구, 중복 ID, 잘못된 응답은 거부합니다. 스키마의 인라인 정의, 로컬 JSON Pointer `$ref`·`$defs`·유니온을 지원하며 원격 참조, `$id`, `$dynamicRef`, 해결되지 않는 참조는 거부합니다. 네이티브 초기화 시 자체 공급자·카탈로그 코드는 로드되며 **운영체제 샌드박스는 아닙니다**. 루프백 브리지도 원격 모델을 사용할 수 있어 ScopeWeaver의 로컬 전용 추론 조건을 충족하지 않습니다.
+
+## 런타임 버전과 구형 경로의 제한
+
+| `SCOPEWEAVER_OPENCODE_RUNTIME` | 고정한 버전 조합 | 기능 응답 `runtime` |
+| --- | --- | --- |
+| `native`(기본값) | 공식 CLI·`@opencode/client`·SDK **2.1.4** | `native` |
+| `legacy` | `@opencode-ai/sdk/v2`·CLI **1.19.4** | `legacy` |
+
+구형 API를 선택하려면 시작 전에 `SCOPEWEAVER_OPENCODE_RUNTIME=legacy`를 지정합니다. 두 조합은 로컬 **macOS arm64**에서 확인했으며 CI는 Linux에서 실제 런타임 테스트를 실행하도록 구성되어 있습니다. 다른 SDK 버전은 추론 전에 거부합니다. 모든 버전·플랫폼의 호환성을 보장하지 않습니다.
+
+구형 경로는 정확히 일치하는 공식 선택 의존성의 플랫폼 바이너리를 직접 실행하므로 `--ignore-scripts` 설치를 사용할 수 있습니다. 초기화되지 않은 래퍼나 전역 `opencode`는 실행하지 않습니다. 선택 의존성을 생략하지 마세요. CLI와 루프백 MCP 수집 서버는 요청 워커의 POSIX 프로세스 그룹과 함께 종료합니다. 이 정리 경계가 구현되지 않은 **Windows에서는 legacy를 거부**합니다. 호출마다 독립된 플러그인 패키지 복사본과 잠금 파일을 사용하며 공유 의존성에 쓰기 가능한 링크를 만들지 않습니다.
+
+legacy 정수의 절대값은 **9,007,199,254,740,991** 이하여야 합니다.
+
+구형 경로는 구조가 유효하고 안전하게 표현할 수 있는 JSON·기록을 지원합니다. 공백이나 같은 값을 나타내는 HTML·Unicode 이스케이프는 정규화될 수 있어 원본 JSON 표기 자체는 보장하지 않습니다. 안전하게 표현할 수 없는 정수, 불투명하거나 서명된 사고 기록, 지원하지 않는 공급자 덮어쓰기는 명시적으로 실패합니다. 원본 64비트 인자는 native 모드를 사용하세요. 구형 사용자 지정 공급자는 위의 OpenAI 호환 설정을 사용하되 `max_tokens` 호환 필드, 지원되는 도구 기능 필드, 컨텍스트·출력 한도만 매핑합니다. 지원하지 않는 `reasoningField`·기능 덮어쓰기를 거부합니다. SDK 경계 밖의 입력에는 기존 API·Go 프로필을 계속 사용할 수 있습니다.
+
+## 프로토콜과 검증
+
+프로토콜 **`scopeweaver-opencode-sdk`, 버전 1**의 인증 경로는 `GET /v1/capabilities`, `GET /v1/models`, `POST /v1/complete`, `POST /v1/stream`, `DELETE /v1/requests/{request_id}`입니다. 스트림은 NDJSON이며 실제 네이티브 텍스트·사고 델타, 수집한 도구 요청, 사용량을 전달하고 성공 시 `message_stop`을 정확히 한 번 보냅니다. 오류는 공급자 원문을 노출하지 않는 제한된 코드·메시지를 사용합니다. 명시적인 구조화 응답으로 확인된 잔액·할당량 소진은 `quota_exhausted`(완료 경로 HTTP 402)로 구분하며 일반 속도 제한·인증·5xx 오류를 할당량 오류로 바꾸지 않습니다. 구조화된 모델 컨텍스트 초과는 `context_length_exceeded`(HTTP 400)로 전달해 호스트의 압축 신호를 보존하며 일반 400 응답이나 메시지 키워드만으로 분류하지 않습니다. 구조화된 `FreeTierError`는 `free_tier_restricted`(HTTP 403)로 구분하며 할당량·일반 금지 응답과 섞지 않습니다. 브리지는 대체 모델을 선택하지 않으며 호스트의 기존 정책이 다음 동작을 결정합니다.
+
+```json
+{
+ "request_id": "unique-call-uuid",
+ "session_id": "stable-host-conversation-uuid",
+ "model": "local/my-model",
+ "system": ["Host system instructions"],
+ "messages": [{ "role": "user", "content": [{ "type": "text", "text": "Hello" }] }],
+ "tools": [],
+ "max_tokens": 64,
+ "thinking": "disabled"
+}
+```
+
+요청에는 `request_id`, `model`, `system` 배열, `messages` 배열, `tools` 배열, `max_tokens`가 필요합니다. 선택 필드는 `session_id`, `temperature`, 문자열 배열 `stop`, `thinking`, `thinking_type`, `reasoning_effort`입니다. 메시지는 `text`, 어시스턴트의 `thinking`·`tool_use`, 텍스트 결과를 담은 사용자의 `tool_result` 블록을 사용합니다. 도구 선언은 `name`, `description`, `input_schema`를 사용합니다. 완료 응답은 `message`, `stop_reason`(`end_turn`, `tool_use`, `max_tokens`), 입력·출력·캐시 읽기·캐시 쓰기 수치를 분리한 `usage`를 포함합니다. 공개 JSON 예시는 [영문 프로토콜](README.md#protocol-and-checks), 검증·제한은 [protocol.mjs](src/protocol.mjs), 전송 처리는 [bridge.mjs](src/bridge.mjs)에서 확인할 수 있습니다.
+
+`opencode-go` 공급자에서는 `session_id`로 호출별 SDK 세션이 달라도 `x-opencode-session`을 유지합니다. 없으면 `request_id`를 사용합니다. `opencode`는 실제 런타임이 생성한 세션·식별 헤더를 그대로 유지합니다. 일반 공급자에는 게이트웨이 세션 헤더를 추가하지 않으며 실제 CLI의 User-Agent를 유지합니다.
+
+```sh
+bun run check
+bun test --timeout 30000
+```
+
+테스트는 고정한 실제 CLI·클라이언트·SDK와 로컬 SSE 모델을 사용해 추론 없는 조회, 기록·설정, 도구 수집 안전성, 스트리밍·사용량, 원본 숫자 인자, 악성 기본 도구 요청, 동시 호출 격리와 취소를 확인합니다. 실제 무료 모델의 사용 자격을 증명하지는 않습니다.
+
+## ScopeWeaver 프로필과 다운그레이드 경계
+
+빈 토큰은 같은 SDK 주소에서만 저장된 값을 유지합니다. 주소나 형식을 바꾸면 저장된 토큰을 재사용하지 않으며 `localhost`, `127.0.0.1`, `::1`은 서로 다른 자격 증명 범위입니다. SDK 템플릿은 저장하지 않은 클라우드 키, 프록시, 사용자 지정 세션 헤더를 지우고 모델을 빈칸으로 둡니다. 조회·추론·저장·활성화는 수행하지 않습니다. **모델 불러오기**는 명시적으로 실행하고, **연결 테스트**는 클릭할 때만 추론합니다.
+
+프로필 데이터베이스의 `CHECK` 제약에 `opencode-sdk`가 추가됩니다. SDK 프로필이 남아 있으면 이전 바이너리는 시작할 수 없습니다. 다운그레이드 전에 데이터베이스를 백업하고 현재 버전에서 이전에 지원하던 형식으로 변환하거나 SDK 프로필을 제거하세요. 이전 마이그레이션 검증에는 임시 데이터베이스를 사용했으며 운영 데이터베이스를 마이그레이션하지 않았습니다. [제공자 안내](../../docs/ko/llm-providers.md#opencode-sdk-bridge)를 참고하세요.
diff --git a/adapters/opencode-sdk/README.md b/adapters/opencode-sdk/README.md
new file mode 100644
index 00000000..ce478b43
--- /dev/null
+++ b/adapters/opencode-sdk/README.md
@@ -0,0 +1,124 @@
+# OpenCode SDK bridge
+
+[English](README.md) · [한국어](README.ko.md) · [简体中文](README.zh-Hans.md) · [繁體中文](README.zh-Hant.md) · [日本語](README.ja.md) · [Español](README.es.md)
+
+Opt-in, loopback-only bridge for **native OpenCode CLI/client/SDK 2.1.4** and the separately tested **legacy SDK/CLI 1.19.4** pair. ScopeWeaver keeps its own conversation, tools, approvals, scope checks, retries, and compaction. The bridge streams a single model step and captures tool requests; it never runs ScopeWeaver tools or OpenCode's built-in tools. Existing API and OpenCode Go providers do not require this bridge.
+
+## Start
+
+Use Bun **1.3.14**, the tested CI runtime. Newer versions have not been verified:
+
+```sh
+cd adapters/opencode-sdk
+bun install --frozen-lockfile --ignore-scripts
+export SCOPEWEAVER_OPENCODE_TOKEN="$(openssl rand -hex 32)"
+bun run start
+```
+
+The bridge token must contain at least **32 non-whitespace characters**. The private native CLI server uses a separately generated per-call password (`OPENCODE_PASSWORD`) and HTTP Basic authentication. That password is distinct from the bridge token and upstream provider keys.
+
+In the ScopeWeaver provider profile, select `opencode-sdk`, set the endpoint to `http://127.0.0.1:4097`, and enter that **bridge token**. It is not a cloud API key. Discover models, then select an exact `providerID/modelID`. Do not reuse an OpenCode Go/Console token as the bridge token.
+
+The default signed-out runtime uses its own isolated catalog and public model discovery. Discovery makes public catalog GET requests, **never inference**. It exposes only currently listed zero-price OpenCode models that the pinned runtime recognizes. Catalog failures fail explicitly. A listed model does not prove available quota, eligibility, or successful inference; free models can change or disappear. No paid model or credential fallback is enabled by default.
+
+**Prior verification on October 25, 2026 (KST):** `opencode/deepseek-v4.1-flash-free` completed a real ScopeWeaver conversation through the production native CLI/client 2.1.4 bridge. Three actual inference calls covered the UI connection test, a host tool request reviewed and approved before its file effect, and the final visible answer `FREE_OK`. The model came from the default zero-price catalog; the run used no API keys, account imports, model/response fixtures, spoofed identities or paid fallback. Native tools remained blocked and ScopeWeaver owned the host action.
+
+Native inference uses the actual pinned CLI and official client APIs; inference-free discovery uses the embedded SDK. The earlier embedded capture-only SDK route received HTTP 403 `FreeTierError`. Legacy hosted free access remains unverified. Free models and service eligibility can change; this dated test establishes no numeric quota or continuing availability. `free_tier_restricted` reports a structured refusal without selecting a paid fallback.
+
+`SCOPEWEAVER_OPENCODE_PORT` changes port 4097. `SCOPEWEAVER_OPENCODE_HOST` must remain `127.0.0.1`. Browser `Origin` requests and non-loopback URLs are rejected. Protect the token like a password. All API routes require it.
+
+`SCOPEWEAVER_OPENCODE_TIMEOUT_MS` is an optional whole-call deadline, from **0 to 86,400,000 ms**. Default **0** leaves call duration under the host's cancellation control; there is no implicit two-minute model cutoff. Cancellation and shutdown still have a separate **1.5-second** hard-stop bound for an owned worker/process group. Public catalog discovery has a 10-second network deadline. Requests are limited to **16 MiB**, individual returned tool-argument JSON to **2,000,000 characters**, combined native model text/reasoning/arguments to **8,000,000 characters**, and worker output to **20,000,000 bytes**. Exceeding a bound fails explicitly and never truncates data.
+
+## Dedicated provider configuration
+
+For an explicit custom OpenAI-compatible model or a local fixture, set `SCOPEWEAVER_OPENCODE_PROVIDER_CONFIG` to a JSON file owned by the bridge operator. Keep it outside the checkout, or use the ignored name `provider-config.local.json`:
+
+```json
+{
+ "providers": {
+ "local": {
+ "name": "Local model",
+ "package": "@opencode/ai/providers/openai-compatible",
+ "settings": {
+ "baseURL": "http://127.0.0.1:8000/v1",
+ "apiKey": "explicit-provider-key-if-needed"
+ },
+ "models": {
+ "my-model": {
+ "name": "My model",
+ "capabilities": { "tools": true },
+ "limit": { "context": 200000, "output": 32000 },
+ "compatibility": { "maxTokensField": "max_tokens" }
+ }
+ }
+ }
+ }
+}
+```
+
+Select `local/my-model`. This file replaces default discovery for that bridge. It does not load OpenCode's user/project config, `.env` files, plugins, MCP servers, or saved credentials. Only the explicit `apiKey` is provided to that provider. A custom remote endpoint can charge for inference; configuration does not make it free. Arbitrary SDK packages and extra configuration keys are rejected.
+
+## Runtime boundary
+
+Every call runs in a separate Bun worker with an allowlisted environment and owned temporary home/config/data paths. Native owns a private official CLI server, an actual CLI `run`, its temporary database, and a one-use authenticated loopback egress transport. The official client supplies discovery, session events, and interruption. No worker inherits cloud credentials, user auth files, proxies, or preload settings. Pinned optional platform binaries are used directly; `--ignore-scripts` remains valid and no global install is needed. Do not omit optional dependencies.
+
+Native retains OpenCode's genuine tool definitions and original implementations. Every native permission is ASK and CLI `--auto` is absent. A public `execute.before` policy hook independently refuses every non-host tool before execution, including generated `execute`; a proposed native call fails the bridge request and never reaches ScopeWeaver. Only registered host capture functions may suspend and become host tool requests. Other agents are removed. Collision-free `sw_tool_N` aliases prevent host tools from replacing native definitions; descriptions retain exact original names and descriptions, and responses restore original host names.
+
+Host system segments use the public custom-agent system setting verbatim, joined by two newlines. OpenCode's generated model/environment/tool context is additionally retained. A final truthful policy part tells the model that only functions marked `Host tool:` are enabled. Host conversation roles, tool IDs/results, and per-call limits/settings use the public context hook. This preserves the host's messages while adding native runtime context; it does not promise a system array containing only the host segments. Model-specific thinking/effort fields are forwarded; unsupported provider settings fail explicitly. A smaller declared model output limit caps the requested limit. Native usage components and cache counts remain separate.
+
+The worker blocks any second model request. Its private egress transport forwards the genuine generated request to the original upstream URL with redirects disabled; it does not choose a different endpoint, model, identity, or credential. Host calls become visible only after interruption, session deletion, and owned process cleanup. Disconnect, DELETE, timeout, and shutdown terminate the POSIX worker process group, with a 1.5-second hard stop. Both native and legacy inference are rejected on Windows because that process-tree cleanup boundary is not implemented.
+
+The CLI public surface cannot disable all system/MDM configuration discovery. Native refuses to start when an unmanaged source exists: `/Library/Application Support/opencode` or an `ai.opencode.managed.plist` under the user's/global `/Library/Managed Preferences` on macOS, or `/etc/opencode` on Linux. Inaccessible sources and broken symlinks also fail closed with `configuration_error`. It never reads or changes those sources.
+
+In native mode, raw tool-argument JSON, including 64-bit integers, is preserved in completion, stream responses, and replayed host tool history. The parsed JavaScript projection is never executed. Unsafe numbers outside authoritative raw tool arguments, unsupported content, unknown native tool calls, duplicate tool IDs, and malformed responses fail closed. Tool schemas support inline definitions, local JSON Pointer `$ref`/`$defs`, and unions; remote references, `$id`, `$dynamicRef`, and unresolved references are rejected. Native initialization still loads its own built-in provider/catalog code; this is an application boundary, **not an operating-system sandbox**. A loopback bridge can use a remote model, so it does not satisfy ScopeWeaver's local-only inference policy.
+
+## Runtime versions and legacy limits
+
+| `SCOPEWEAVER_OPENCODE_RUNTIME` | Pinned pair | Capability `runtime` |
+| --- | --- | --- |
+| `native` (default) | official CLI, `@opencode/client`, and SDK **2.1.4** | `native` |
+| `legacy` | `@opencode-ai/sdk/v2` and CLI **1.19.4** | `legacy` |
+
+Set `SCOPEWEAVER_OPENCODE_RUNTIME=legacy` before starting to choose the older API. Both pairs were tested locally on **macOS arm64**; CI is configured to run their actual-runtime fixtures on Linux. Other SDK versions are rejected before inference. This is not a universal cross-version or platform guarantee.
+
+Legacy uses the matching official optional platform binary directly. `--ignore-scripts` therefore remains valid: it does not invoke the uninitialized wrapper or a global `opencode` installation. Do not omit optional dependencies. Its owned CLI and loopback MCP capture server are terminated with the request worker's POSIX process group. **Legacy is rejected on Windows**, where that cleanup boundary is not implemented. Each CLI call uses its own private plugin package copy and lock, with no writable link into shared dependencies.
+
+Legacy integers must have an absolute value no greater than **9,007,199,254,740,991**.
+
+Legacy supports structurally valid, safely representable JSON/history. Whitespace and equivalent HTML/Unicode escapes may normalize; it does not promise lexical JSON fidelity. Unsafe parsed integers, opaque/signed reasoning history, and unsupported provider overrides fail explicitly; choose native mode for raw 64-bit argument fidelity. Legacy custom providers use the same explicit OpenAI-compatible configuration above, but only `max_tokens` compatibility, supported tool capability fields, and context/output limits are mapped. Unsupported `reasoningField` and capability overrides are rejected. Existing API/Go profiles remain available for inputs outside either SDK mode's tested boundary.
+
+## Protocol and checks
+
+Protocol **`scopeweaver-opencode-sdk`, version 1** has authenticated routes: `GET /v1/capabilities`, `GET /v1/models`, `POST /v1/complete`, `POST /v1/stream`, `DELETE /v1/requests/{request_id}`. Streaming uses NDJSON with actual native text/thinking deltas, captured tool-use blocks, terminal usage, and exactly one `message_stop` on success. Errors have bounded machine codes and redact provider messages. Explicit structured upstream billing/quota exhaustion is returned as `quota_exhausted` (HTTP 402 for complete); generic rate limits, auth errors, and 5xx responses are never relabeled as quota. Structured model context overflow is returned as `context_length_exceeded` (HTTP 400), preserving the host's compaction signal; generic 400 responses and message keywords are not enough. Structured `FreeTierError` is returned as `free_tier_restricted` (HTTP 403), separate from quota and generic forbidden responses. The bridge does not select a fallback model; the host's configured policy decides what happens next.
+
+Example completion request (the stream endpoint accepts the same body):
+
+```json
+{
+ "request_id": "unique-call-uuid",
+ "session_id": "stable-host-conversation-uuid",
+ "model": "local/my-model",
+ "system": ["Host system instructions"],
+ "messages": [{ "role": "user", "content": [{ "type": "text", "text": "Hello" }] }],
+ "tools": [],
+ "max_tokens": 64,
+ "thinking": "disabled"
+}
+```
+
+Optional fields are `session_id`, `temperature`, `stop` (strings), `thinking`, `thinking_type`, and `reasoning_effort`. Host content blocks are `text`, assistant `thinking`/`tool_use`, and user `tool_result` with text content. Tool declarations use `name`, `description`, and `input_schema`. Complete responses contain `message`, `stop_reason` (`end_turn`, `tool_use`, or `max_tokens`), and `usage` with separate input/output/cache-read/cache-write token counts. See [validation and serialization](src/protocol.mjs) and [transport](src/bridge.mjs) for the public wire contract and limits.
+
+For `opencode-go`, `session_id` keeps `x-opencode-session` stable across fresh per-call SDK sessions; otherwise `request_id` is used. `opencode` preserves its genuine runtime-generated session and identity headers. Generic providers receive no gateway-specific session header. The genuine CLI User-Agent is preserved.
+
+```sh
+bun run check
+bun test --timeout 30000
+```
+
+Tests drive the **actual pinned CLI/client/SDK pair** against local SSE model fixtures. They verify inference-free discovery, host history/settings, capture safety, native streaming/usage, raw numeric arguments, hostile native calls, concurrent isolation, and cancellation. They do not prove real hosted free-model entitlement. See [한국어](README.ko.md).
+
+## ScopeWeaver profile and downgrade boundary
+
+A blank token retains a saved credential only for the same SDK endpoint. Changing endpoint or format must not reuse the saved token; `localhost`, `127.0.0.1`, and `::1` are distinct credential scopes. The SDK template clears unsaved cloud credentials, proxy and custom session header, leaves the model blank, and does not discover, infer, save or activate. Use **Load models** explicitly; **Test connection** performs inference only when clicked.
+
+The profile database `CHECK` constraint adds `opencode-sdk`. Older binaries cannot start while SDK profiles remain. Before downgrading, back up the database and use the current version to convert SDK profiles to an older supported format or remove them. Prior migration verification used disposable databases; it did not migrate a live database. See the [provider guide](../../docs/llm-providers.md#opencode-sdk-bridge).
diff --git a/adapters/opencode-sdk/README.zh-Hans.md b/adapters/opencode-sdk/README.zh-Hans.md
new file mode 100644
index 00000000..4591acf7
--- /dev/null
+++ b/adapters/opencode-sdk/README.zh-Hans.md
@@ -0,0 +1,122 @@
+# OpenCode SDK 桥接器
+
+[English](README.md) · [한국어](README.ko.md) · [简体中文](README.zh-Hans.md) · [繁體中文](README.zh-Hant.md) · [日本語](README.ja.md) · [Español](README.es.md)
+
+可选的仅限回环地址的桥接器,支持固定的 **OpenCode 原生 CLI/客户端/SDK 2.1.4**,以及单独测试的 **旧版 SDK/CLI 1.19.4**。ScopeWeaver 继续管理对话、工具执行、审批、范围检查、重试与压缩。桥接器只流式传送一次模型步骤并收集工具请求,不执行 ScopeWeaver 工具或 OpenCode 内置工具。现有 API 与 OpenCode Go 供应商无需此桥接器。
+
+## 启动
+
+已测试的 CI 运行时是 **Bun 1.3.14**;更新版本尚未验证。
+
+```sh
+cd adapters/opencode-sdk
+bun install --frozen-lockfile --ignore-scripts
+export SCOPEWEAVER_OPENCODE_TOKEN="$(openssl rand -hex 32)"
+bun run start
+```
+
+桥接令牌必须至少包含 **32 个非空白字符**。原生私有 CLI 服务器使用每次调用独立生成的密码(`OPENCODE_PASSWORD`)和 HTTP Basic 认证;该密码、桥接令牌与上游供应商密钥彼此独立。
+
+在 ScopeWeaver 供应商配置中选择 `opencode-sdk`,地址设为 `http://127.0.0.1:4097`,填入上述**桥接令牌**。它不是云端 API 密钥;不要复用 OpenCode Go/Console 令牌。读取模型后选择准确的 `providerID/modelID`。
+
+默认运行时不登录账户,使用隔离的目录与公开模型发现。发现只发送公开目录 GET 请求,**不执行推理**;仅显示当前公开列表中、固定运行时识别的零价格 OpenCode 模型。目录错误会明确返回。列入目录不代表具备资格、可用配额或推理成功;免费模型可能改变或消失。默认不启用付费模型或凭证回退。
+
+**2026-10-25(KST)的先前验证:** `opencode/deepseek-v4.1-flash-free` 曾通过产品的原生 CLI/客户端 2.1.4 桥接器完成真实 ScopeWeaver 对话。三次实际推理覆盖界面连接测试、文件产生影响前经审核批准的宿主工具请求,以及界面最终答案 `FREE_OK`。模型来自默认零价格目录,未使用 API 密钥、账户导入、模型/响应模拟数据、身份伪装或付费回退。原生工具保持阻止状态,宿主操作由 ScopeWeaver 执行。这是该日期的既有证据,不是本次重跑结果。
+
+原生推理使用真正的固定版本 CLI 和官方客户端 API,无推理的模型发现使用内嵌 SDK。此前仅收集工具的内嵌 SDK 路径收到 HTTP 403 `FreeTierError`。旧版的真实托管免费访问尚未验证。模型及服务资格可能变化;该日期的测试不保证数值配额或持续可用性。结构化拒绝返回 `free_tier_restricted`,不切换到付费模型。
+
+`SCOPEWEAVER_OPENCODE_PORT` 可修改默认端口 4097;`SCOPEWEAVER_OPENCODE_HOST` 必须为 `127.0.0.1`。拒绝浏览器 `Origin` 请求及非回环 URL。所有 API 路由均要求令牌;请像密码一样保护它。
+
+`SCOPEWEAVER_OPENCODE_TIMEOUT_MS` 的完整调用时限为 **0–86,400,000 毫秒**;默认 **0** 由宿主控制取消,没有隐藏的两分钟推理限制。取消与关闭另有 **1.5 秒**强制终止期限。公开目录请求限制为 **10 秒**。请求体上限 **16 MiB**,单个工具参数 JSON 上限 **2,000,000 字符**,原生模型文本/思考/参数合计上限 **8,000,000 字符**,工作进程输出上限 **20,000,000 字节**。超限明确失败,不截断数据。
+
+## 专用供应商配置
+
+自定义 OpenAI 兼容模型或本地测试模型可通过 `SCOPEWEAVER_OPENCODE_PROVIDER_CONFIG` 指定运营者管理的 JSON 文件。放在检出目录之外,或使用 Git 忽略的名称 `provider-config.local.json`:
+
+```json
+{
+ "providers": {
+ "local": {
+ "name": "Local model",
+ "package": "@opencode/ai/providers/openai-compatible",
+ "settings": {
+ "baseURL": "http://127.0.0.1:8000/v1",
+ "apiKey": "explicit-provider-key-if-needed"
+ },
+ "models": {
+ "my-model": {
+ "name": "My model",
+ "capabilities": { "tools": true },
+ "limit": { "context": 200000, "output": 32000 },
+ "compatibility": { "maxTokensField": "max_tokens" }
+ }
+ }
+ }
+ }
+}
+```
+
+选择 `local/my-model`。文件会替代该桥接器的默认模型发现,不载入用户/项目的 OpenCode 配置、`.env`、插件、MCP 服务器或已保存凭证。仅将明确配置的 `apiKey` 传给对应供应商。远程地址可能收取推理费用;添加配置不会使其免费。任意 SDK 软件包和额外配置字段均被拒绝。
+
+## 运行边界
+
+每次调用使用独立的 Bun 工作进程、允许列表中的环境变量及自有临时 home/配置/数据路径。原生模式拥有私有官方 CLI 服务器、实际 CLI `run`、临时数据库和一次性认证的回环出口传输服务;官方客户端处理发现、会话事件和中断。不继承云端凭证、用户认证文件、代理或预加载设置。直接使用固定的可选平台二进制,因此支持 `--ignore-scripts`,无需全局安装;**不要省略可选依赖**。
+
+保留 OpenCode 的真实内置工具定义及原始实现。所有原生权限为 ASK,CLI 不使用 `--auto`。公开的 `execute.before` 策略钩子独立阻止所有非宿主工具在执行前运行,包括生成的 `execute`。原生工具请求使桥接请求失败,不会变成 ScopeWeaver 工具请求。只有注册的宿主收集函数可暂停并成为宿主工具请求;移除其他代理。无冲突的 `sw_tool_N` 别名防止宿主工具覆盖内置定义,描述保留原始名称与内容,响应恢复原始宿主名称。
+
+宿主 system 片段以两个换行连接,原样写入公开的自定义代理 system 设置;同时保留 OpenCode 生成的模型/环境/工具上下文。最后的策略说明只有标记 `Host tool:` 的函数可用。通过公开 context 钩子保留宿主角色、工具 ID/结果及每次调用的限制/设置,不保证 system 数组只含宿主片段。按模型传递 thinking/effort 字段,不支持的设置明确失败;模型声明的较小输出限额会限制请求上限。输入、输出及缓存用量分开记录。
+
+阻止第二次模型请求。私有出口将实际 CLI 生成的请求转发到原始上游 URL,拒绝重定向,不替换地址、模型、身份或凭证。只有中断、删除会话及清理自有进程完成后才返回宿主工具请求。断连、DELETE、超时和关闭终止 POSIX 工作进程组,并在 **1.5 秒**后强制终止。该进程树清理边界未在 Windows 实现,**原生和旧版推理均不支持 Windows**。
+
+CLI 的公开设置不能完全关闭系统/MDM 配置发现。macOS 上存在 `/Library/Application Support/opencode` 或用户/全局 `/Library/Managed Preferences` 下的 `ai.opencode.managed.plist`,或 Linux 上存在 `/etc/opencode`,原生模式便以 `configuration_error` 拒绝启动。无法访问的路径和损坏符号链接也明确拒绝;不读取或修改这些来源。
+
+原生模式在完成响应、流响应和宿主工具历史重放中保留原始工具参数 JSON,包括 64 位整数;不执行 JavaScript 解析后的投影。原始参数之外的不安全数值、不支持的内容、未知原生工具、重复工具 ID 和格式错误响应均拒绝。工具 schema 支持内联定义、本地 JSON Pointer `$ref`/`$defs` 与联合类型;拒绝远程引用、`$id`、`$dynamicRef` 和无法解析的引用。初始化仍会加载原生供应商/目录代码;这是应用边界,**不是操作系统沙箱**。回环桥接器仍可调用远程模型,因此不满足 ScopeWeaver 的仅本地推理策略。
+
+## 版本与旧版限制
+
+| `SCOPEWEAVER_OPENCODE_RUNTIME` | 固定组合 | 能力响应 `runtime` |
+| --- | --- | --- |
+| `native`(默认) | 官方 CLI、`@opencode/client`、SDK **2.1.4** | `native` |
+| `legacy` | `@opencode-ai/sdk/v2`、CLI **1.19.4** | `legacy` |
+
+启动前设置 `SCOPEWEAVER_OPENCODE_RUNTIME=legacy` 可选择旧 API。两个组合的先前本地实际运行验证范围为 **macOS arm64**;Linux CI 已配置实际运行测试,但托管运行结果尚待验证。其他 SDK 版本在推理前被拒绝,不保证任意版本或平台兼容。
+
+旧版直接运行匹配的官方可选平台二进制,支持 `--ignore-scripts`,不调用未初始化的包装器或全局 `opencode`。不要省略可选依赖。其 CLI 与回环 MCP 收集服务器随请求工作进程的 POSIX 进程组终止;Windows 不支持此清理边界。每次调用使用独立插件软件包副本及锁文件,不建立到共享依赖的可写链接。
+
+旧版只支持结构有效、可安全表示的 JSON/历史。空白及等价 HTML/Unicode 转义可能被规范化,不承诺原始 JSON 字节。拒绝绝对值超过 **9,007,199,254,740,991** 的不安全整数、不透明/带签名的思考历史及不支持的供应商覆盖设置;需要原始 64 位参数保真时使用原生模式。旧版自定义供应商使用上面的显式 OpenAI 兼容配置,但只映射 `max_tokens` 兼容字段、支持的工具能力字段及上下文/输出限额;拒绝不支持的 `reasoningField` 和能力覆盖。超出任一 SDK 模式边界时,可继续使用 API/Go 配置。
+
+## 协议与验证
+
+协议为 **`scopeweaver-opencode-sdk`,版本 1**。认证路由:`GET /v1/capabilities`、`GET /v1/models`、`POST /v1/complete`、`POST /v1/stream`、`DELETE /v1/requests/{request_id}`。流使用 NDJSON,传递真实原生文本/思考增量、收集的工具请求及最终用量;成功时只发送一次 `message_stop`。错误使用有限机器代码并隐藏供应商消息。只有明确结构化的计费/配额耗尽才返回 `quota_exhausted`(完成请求 HTTP 402);普通限流、认证及 5xx 不会改标为配额错误。结构化上下文溢出返回 `context_length_exceeded`(HTTP 400),保留宿主压缩信号;普通 400 或消息关键词不足以分类。结构化 `FreeTierError` 返回 `free_tier_restricted`(HTTP 403),与配额及普通禁止响应分开。桥接器不选择后备模型;后续行为由宿主配置的策略决定。
+
+完成请求示例(流端点接受相同正文):
+
+```json
+{
+ "request_id": "unique-call-uuid",
+ "session_id": "stable-host-conversation-uuid",
+ "model": "local/my-model",
+ "system": ["Host system instructions"],
+ "messages": [{ "role": "user", "content": [{ "type": "text", "text": "Hello" }] }],
+ "tools": [],
+ "max_tokens": 64,
+ "thinking": "disabled"
+}
+```
+
+可选字段为 `session_id`、`temperature`、字符串数组 `stop`、`thinking`、`thinking_type`、`reasoning_effort`。宿主内容块支持 `text`、助手 `thinking`/`tool_use` 及带文本结果的用户 `tool_result`。工具声明使用 `name`、`description`、`input_schema`。完成响应包含 `message`、`stop_reason`(`end_turn`、`tool_use` 或 `max_tokens`)及分离输入/输出/缓存读取/缓存写入计数的 `usage`。公开契约与限额见 [protocol.mjs](src/protocol.mjs) 和 [bridge.mjs](src/bridge.mjs)。
+
+对于 `opencode-go`,`session_id` 使每次调用的新 SDK 会话保持相同 `x-opencode-session`;缺省时使用 `request_id`。`opencode` 保留真实运行时生成的会话及身份头部。普通供应商不会收到网关专用会话头部。保留真实 CLI 的 User-Agent。
+
+```sh
+bun run check
+bun test --timeout 30000
+```
+
+测试以固定的实际 CLI/客户端/SDK 和本地 SSE 模型验证无推理发现、宿主历史/设置、收集安全性、流/用量、原始数值参数、恶意原生调用、并发隔离及取消;不证明真实托管免费模型的使用资格。
+
+## ScopeWeaver 配置与降级边界
+
+空令牌仅在相同 SDK 地址保留已保存值。更改地址或格式不能复用旧令牌;`localhost`、`127.0.0.1`、`::1` 是不同凭证作用域。SDK 模板清除未保存的云端密钥、代理和自定义会话头部,模型留空,不发现、不推理、不保存、不启用。明确点击**读取模型**才发现模型;**测试连接**仅在点击时执行推理。
+
+配置数据库 `CHECK` 约束增加 `opencode-sdk`;仍有 SDK 配置时,旧二进制无法启动。降级前备份数据库,并在当前版本中转换为旧版本支持的格式或移除 SDK 配置。此前迁移验证只使用临时数据库,没有迁移生产数据库。详见[供应商指南](../../docs/zh-Hans/llm-providers.md#opencode-sdk-bridge)。
diff --git a/adapters/opencode-sdk/README.zh-Hant.md b/adapters/opencode-sdk/README.zh-Hant.md
new file mode 100644
index 00000000..a15dd232
--- /dev/null
+++ b/adapters/opencode-sdk/README.zh-Hant.md
@@ -0,0 +1,122 @@
+# OpenCode SDK 橋接器
+
+[English](README.md) · [한국어](README.ko.md) · [简体中文](README.zh-Hans.md) · [繁體中文](README.zh-Hant.md) · [日本語](README.ja.md) · [Español](README.es.md)
+
+可選的僅限迴路地址的橋接器,支持固定的 **OpenCode 原生 CLI/客戶端/SDK 2.1.4**,以及單獨測試的 **舊版 SDK/CLI 1.19.4**。ScopeWeaver 繼續管理對話、工具執行、審批、範圍檢查、重試與壓縮。橋接器只流式傳送一次模型步驟並收集工具請求,不執行 ScopeWeaver 工具或 OpenCode 內建工具。現有 API 與 OpenCode Go 供應商無需此橋接器。
+
+## 啓動
+
+已測試的 CI 運行時是 **Bun 1.3.14**;更新版本尚未驗證。
+
+```sh
+cd adapters/opencode-sdk
+bun install --frozen-lockfile --ignore-scripts
+export SCOPEWEAVER_OPENCODE_TOKEN="$(openssl rand -hex 32)"
+bun run start
+```
+
+橋接權杖必須至少包含 **32 個非空白字元**。原生私有 CLI 伺服器使用每次呼叫獨立產生的密碼(`OPENCODE_PASSWORD`)與 HTTP Basic 驗證;該密碼、橋接權杖與上游供應商金鑰彼此獨立。
+
+在 ScopeWeaver 供應商設定中選擇 `opencode-sdk`,地址設為 `http://127.0.0.1:4097`,填入上述**橋接權杖**。它不是雲端 API 密鑰;不要復用 OpenCode Go/Console 權杖。讀取模型後選擇準確的 `providerID/modelID`。
+
+預設運行時不登錄賬戶,使用隔離的目錄與公開模型發現。發現只發送公開目錄 GET 請求,**不執行推理**;僅顯示當前公開列表中、固定運行時識別的零價格 OpenCode 模型。目錄錯誤會明確返回。列入目錄不代表具備資格、可用配額或推理成功;免費模型可能改變或消失。預設不啓用付費模型或憑證回退。
+
+**2026-10-25(KST)的先前驗證:** `opencode/deepseek-v4.1-flash-free` 曾通過產品的原生 CLI/客戶端 2.1.4 橋接器完成真實 ScopeWeaver 對話。三次實際推理覆蓋界面連接測試、文件產生影響前經審核批准的宿主工具請求,以及界面最終答案 `FREE_OK`。模型來自預設零價格目錄,未使用 API 密鑰、賬戶導入、模型/響應模擬資料、身份偽裝或付費回退。原生工具保持阻止狀態,宿主操作由 ScopeWeaver 執行。這是該日期的既有證據,不是本次重跑結果。
+
+原生推理使用真正的固定版本 CLI 和官方客戶端 API,無推理的模型發現使用內嵌 SDK。此前僅收集工具的內嵌 SDK 路徑收到 HTTP 403 `FreeTierError`。舊版的真實托管免費訪問尚未驗證。模型及服務資格可能變化;該日期的測試不保證數值配額或持續可用性。結構化拒絕返回 `free_tier_restricted`,不切換到付費模型。
+
+`SCOPEWEAVER_OPENCODE_PORT` 可修改預設端口 4097;`SCOPEWEAVER_OPENCODE_HOST` 必須為 `127.0.0.1`。拒絕瀏覽器 `Origin` 請求及非迴路 URL。所有 API 路由均要求權杖;請像密碼一樣保護它。
+
+`SCOPEWEAVER_OPENCODE_TIMEOUT_MS` 的完整調用時限為 **0–86,400,000 毫秒**;預設 **0** 由宿主控制取消,沒有隱藏的兩分鐘推理限制。取消與關閉另有 **1.5 秒**強制終止期限。公開目錄請求限制為 **10 秒**。請求體上限 **16 MiB**,單個工具參數 JSON 上限 **2,000,000 字元**,原生模型文本/思考/參數合計上限 **8,000,000 字元**,工作進程輸出上限 **20,000,000 位元組**。超限明確失敗,不截斷資料。
+
+## 專用供應商設定
+
+自定義 OpenAI 兼容模型或本地測試模型可通過 `SCOPEWEAVER_OPENCODE_PROVIDER_CONFIG` 指定運營者管理的 JSON 文件。放在簽出目錄之外,或使用 Git 忽略的名稱 `provider-config.local.json`:
+
+```json
+{
+ "providers": {
+ "local": {
+ "name": "Local model",
+ "package": "@opencode/ai/providers/openai-compatible",
+ "settings": {
+ "baseURL": "http://127.0.0.1:8000/v1",
+ "apiKey": "explicit-provider-key-if-needed"
+ },
+ "models": {
+ "my-model": {
+ "name": "My model",
+ "capabilities": { "tools": true },
+ "limit": { "context": 200000, "output": 32000 },
+ "compatibility": { "maxTokensField": "max_tokens" }
+ }
+ }
+ }
+ }
+}
+```
+
+選擇 `local/my-model`。文件會替代該橋接器的預設模型發現,不載入用戶/項目的 OpenCode 設定、`.env`、插件、MCP 服務器或已保存憑證。僅將明確設定的 `apiKey` 傳給對應供應商。遠程地址可能收取推理費用;添加設定不會使其免費。任意 SDK 套件和額外設定字段均被拒絕。
+
+## 運行邊界
+
+每次調用使用獨立的 Bun 工作進程、允許列表中的環境變量及自有臨時 home/設定/資料路徑。原生模式擁有私有官方 CLI 服務器、實際 CLI `run`、臨時資料庫和一次性認證的迴路出口傳輸服務;官方客戶端處理髮現、工作階段事件和中斷。不繼承雲端憑證、用戶認證文件、代理或預載入設置。直接使用固定的可選平台二進制,因此支持 `--ignore-scripts`,無需全局安裝;**不要省略可選依賴**。
+
+保留 OpenCode 的真實內建工具定義及原始實現。所有原生權限為 ASK,CLI 不使用 `--auto`。公開的 `execute.before` 策略鈎子獨立阻止所有非宿主工具在執行前運行,包括生成的 `execute`。原生工具請求使橋接請求失敗,不會變成 ScopeWeaver 工具請求。只有註冊的宿主收集函數可暫停並成為宿主工具請求;移除其他代理。無衝突的 `sw_tool_N` 別名防止宿主工具覆蓋內建定義,描述保留原始名稱與內容,響應恢復原始宿主名稱。
+
+宿主 system 片段以兩個換行連接,原樣寫入公開的自定義代理 system 設置;同時保留 OpenCode 生成的模型/環境/工具上下文。最後的策略說明只有標記 `Host tool:` 的函數可用。通過公開 context 鈎子保留宿主角色、工具 ID/結果及每次調用的限制/設置,不保證 system 數組只含宿主片段。按模型傳遞 thinking/effort 字段,不支持的設置明確失敗;模型聲明的較小輸出限額會限制請求上限。輸入、輸出及快取用量分開記錄。
+
+阻止第二次模型請求。私有出口將實際 CLI 生成的請求轉發到原始上游 URL,拒絕重定向,不替換地址、模型、身份或憑證。只有中斷、刪除工作階段及清理自有進程完成後才返回宿主工具請求。斷連、DELETE、逾時和關閉終止 POSIX 工作進程組,並在 **1.5 秒**後強制終止。該進程樹清理邊界未在 Windows 實現,**原生和舊版推理均不支持 Windows**。
+
+CLI 的公開設置不能完全關閉系統/MDM 設定發現。macOS 上存在 `/Library/Application Support/opencode` 或用戶/全局 `/Library/Managed Preferences` 下的 `ai.opencode.managed.plist`,或 Linux 上存在 `/etc/opencode`,原生模式便以 `configuration_error` 拒絕啓動。無法訪問的路徑和損壞符號鏈接也明確拒絕;不讀取或修改這些來源。
+
+原生模式在完成響應、流響應和宿主工具歷史重放中保留原始工具參數 JSON,包括 64 位整數;不執行 JavaScript 解析後的投影。原始參數之外的不安全數值、不支持的內容、未知原生工具、重復工具 ID 和格式錯誤響應均拒絕。工具 schema 支持內聯定義、本地 JSON Pointer `$ref`/`$defs` 與聯合類型;拒絕遠程引用、`$id`、`$dynamicRef` 和無法解析的引用。初始化仍會載入原生供應商/目錄代碼;這是應用邊界,**不是操作系統沙箱**。迴路橋接器仍可調用遠程模型,因此不滿足 ScopeWeaver 的僅本地推理策略。
+
+## 版本與舊版限制
+
+| `SCOPEWEAVER_OPENCODE_RUNTIME` | 固定組合 | 能力響應 `runtime` |
+| --- | --- | --- |
+| `native`(預設) | 官方 CLI、`@opencode/client`、SDK **2.1.4** | `native` |
+| `legacy` | `@opencode-ai/sdk/v2`、CLI **1.19.4** | `legacy` |
+
+啓動前設置 `SCOPEWEAVER_OPENCODE_RUNTIME=legacy` 可選擇舊 API。兩個組合的先前本地實際運行驗證範圍為 **macOS arm64**;Linux CI 已設定實際運行測試,但托管運行結果尚待驗證。其他 SDK 版本在推理前被拒絕,不保證任意版本或平台兼容。
+
+舊版直接運行匹配的官方可選平台二進制,支持 `--ignore-scripts`,不調用未初始化的包裝器或全局 `opencode`。不要省略可選依賴。其 CLI 與迴路 MCP 收集服務器隨請求工作進程的 POSIX 進程組終止;Windows 不支持此清理邊界。每次調用使用獨立插件套件副本及鎖文件,不建立到共享依賴的可寫鏈接。
+
+舊版只支持結構有效、可安全表示的 JSON/歷史。空白及等價 HTML/Unicode 轉義可能被規範化,不承諾原始 JSON 位元組。拒絕絕對值超過 **9,007,199,254,740,991** 的不安全整數、不透明/帶簽名的思考歷史及不支持的供應商覆蓋設置;需要原始 64 位參數保真時使用原生模式。舊版自定義供應商使用上面的顯式 OpenAI 兼容設定,但只映射 `max_tokens` 兼容字段、支持的工具能力字段及上下文/輸出限額;拒絕不支持的 `reasoningField` 和能力覆蓋。超出任一 SDK 模式邊界時,可繼續使用 API/Go 設定。
+
+## 協議與驗證
+
+協議為 **`scopeweaver-opencode-sdk`,版本 1**。認證路由:`GET /v1/capabilities`、`GET /v1/models`、`POST /v1/complete`、`POST /v1/stream`、`DELETE /v1/requests/{request_id}`。流使用 NDJSON,傳遞真實原生文本/思考增量、收集的工具請求及最終用量;成功時只發送一次 `message_stop`。錯誤使用有限機器代碼並隱藏供應商消息。只有明確結構化的計費/配額耗盡才返回 `quota_exhausted`(完成請求 HTTP 402);普通限流、認證及 5xx 不會改標為配額錯誤。結構化上下文溢出返回 `context_length_exceeded`(HTTP 400),保留宿主壓縮信號;普通 400 或消息關鍵詞不足以分類。結構化 `FreeTierError` 返回 `free_tier_restricted`(HTTP 403),與配額及普通禁止響應分開。橋接器不選擇後備模型;後續行為由宿主設定的策略決定。
+
+完成請求示例(流端點接受相同正文):
+
+```json
+{
+ "request_id": "unique-call-uuid",
+ "session_id": "stable-host-conversation-uuid",
+ "model": "local/my-model",
+ "system": ["Host system instructions"],
+ "messages": [{ "role": "user", "content": [{ "type": "text", "text": "Hello" }] }],
+ "tools": [],
+ "max_tokens": 64,
+ "thinking": "disabled"
+}
+```
+
+可選字段為 `session_id`、`temperature`、字元串數組 `stop`、`thinking`、`thinking_type`、`reasoning_effort`。宿主內容塊支持 `text`、助手 `thinking`/`tool_use` 及帶文本結果的用戶 `tool_result`。工具聲明使用 `name`、`description`、`input_schema`。完成響應包含 `message`、`stop_reason`(`end_turn`、`tool_use` 或 `max_tokens`)及分離輸入/輸出/快取讀取/快取寫入計數的 `usage`。公開契約與限額見 [protocol.mjs](src/protocol.mjs) 和 [bridge.mjs](src/bridge.mjs)。
+
+對於 `opencode-go`,`session_id` 使每次調用的新 SDK 工作階段保持相同 `x-opencode-session`;缺省時使用 `request_id`。`opencode` 保留真實運行時生成的工作階段及身份標頭。普通供應商不會收到網關專用工作階段標頭。保留真實 CLI 的 User-Agent。
+
+```sh
+bun run check
+bun test --timeout 30000
+```
+
+測試以固定的實際 CLI/客戶端/SDK 和本地 SSE 模型驗證無推理髮現、宿主歷史/設置、收集安全性、流/用量、原始數值參數、惡意原生調用、並行隔離及取消;不證明真實托管免費模型的使用資格。
+
+## ScopeWeaver 設定與降級邊界
+
+空權杖僅在相同 SDK 地址保留已保存值。更改地址或格式不能復用舊權杖;`localhost`、`127.0.0.1`、`::1` 是不同憑證作用域。SDK 模板清除未保存的雲端密鑰、代理和自定義工作階段標頭,模型留空,不發現、不推理、不保存、不啓用。明確點擊**讀取模型**才發現模型;**測試連接**僅在點擊時執行推理。
+
+設定資料庫 `CHECK` 約束增加 `opencode-sdk`;仍有 SDK 設定時,舊二進制無法啓動。降級前備份資料庫,並在當前版本中轉換為舊版本支持的格式或移除 SDK 設定。此前遷移驗證只使用臨時資料庫,沒有遷移生產資料庫。詳見[供應商指南](../../docs/zh-Hant/llm-providers.md#opencode-sdk-bridge)。
diff --git a/adapters/opencode-sdk/bun.lock b/adapters/opencode-sdk/bun.lock
new file mode 100644
index 00000000..7ee4a9b8
--- /dev/null
+++ b/adapters/opencode-sdk/bun.lock
@@ -0,0 +1,946 @@
+{
+ "lockfileVersion": 1,
+ "configVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "scopeweaver-opencode-sdk",
+ "dependencies": {
+ "@opencode-ai/plugin": "1.19.4",
+ "@opencode-ai/sdk": "1.19.4",
+ "@opencode/ai": "2.1.4",
+ "@opencode/cli": "2.1.4",
+ "@opencode/client": "2.1.4",
+ "@opencode/core": "2.1.4",
+ "@opencode/plugin": "2.1.4",
+ "@opencode/sdk": "2.1.4",
+ "@opencode/util": "2.1.4",
+ "effect": "4.0.1",
+ "jsonc-parser": "3.3.1",
+ "opencode-ai": "1.19.4",
+ },
+ },
+ },
+ "overrides": {
+ "@ai-sdk/provider-utils": "4.0.33",
+ "effect": "4.0.1",
+ },
+ "packages": {
+ "@ai-sdk/cohere": ["@ai-sdk/cohere@3.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OqcCq2PiFY1dbK/0Ck45KuvE8jfdxRuuAE9Y5w46dAk6U+9vPOeg1CDcmR+ncqmrYrhRl3nmyDttyDahyjCzAw=="],
+
+ "@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.37", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-+POSFVcgiu47BK64dhsI6OpcDC0/VAE2ZSaXdXGNNhpC/ava++uSRJYks0k2bpfY0wwCTgpAWZsXn/dG2Yppiw=="],
+
+ "@ai-sdk/perplexity": ["@ai-sdk/perplexity@3.0.26", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-dXzrVsLR5f6tr+U04jq4AXoRroGFBTvODnLgss0SWbzNjGGQg3XqtQ9j7rCLo6o8qbYGuAHvqUrIpUCuiscuFg=="],
+
+ "@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
+
+ "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.33", "", { "dependencies": { "@ai-sdk/provider": "3.0.12", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-nJ0bAfegMAIJtrzMJtbzer1cS3nb7c7DsyU1S4nrPm7ZU0Mn6SBBZv5IGZZGTbpWTJwqKTSPeZJTXalbAxt1BA=="],
+
+ "@ai-sdk/vercel": ["@ai-sdk/vercel@2.0.39", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8eu3ljJpkCTP4ppcyYB+NcBrkcBoSOFthCSgk5VnjaxnDaOJFaxnPwfddM7wx3RwMk2CiK1O61Px/LlqNc7QkQ=="],
+
+ "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.71.2", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-TGNDEUuEstk/DKu0/TflXAEt+p+p/WhTlFzEnoosvbaDU2LTjm42igSdlL0VijrKpWejtOKxX0b8A7uc+XiSAQ=="],
+
+ "@anycable/core": ["@anycable/core@0.9.2", "", { "dependencies": { "nanoevents": "^7.0.1" } }, "sha512-x5ZXDcW/N4cxWl93CnbHs/u7qq4793jS2kNPWm+duPrXlrva+ml2ZGT7X9tuOBKzyIHf60zWCdIK7TUgMPAwXA=="],
+
+ "@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="],
+
+ "@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@5.2.0", "", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="],
+
+ "@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="],
+
+ "@aws-crypto/supports-web-crypto": ["@aws-crypto/supports-web-crypto@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg=="],
+
+ "@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="],
+
+ "@aws-sdk/client-cognito-identity": ["@aws-sdk/client-cognito-identity@3.1057.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.15", "@aws-sdk/credential-provider-node": "^3.972.47", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/fetch-http-handler": "^5.4.5", "@smithy/node-http-handler": "^4.7.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-5MliYkp2u0+2arTp5fZIaxl+xmm90LEKv/VeSxhfNQW4t0fvWJrNO429/jchWQenNoDRrOGE59VfbuZUfwFujg=="],
+
+ "@aws-sdk/core": ["@aws-sdk/core@3.978.5", "", { "dependencies": { "@aws-sdk/types": "^3.975.0", "@aws-sdk/xml-builder": "^3.972.41", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.36.1", "@smithy/signature-v4": "^5.7.4", "@smithy/types": "^4.19.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-ruI0AWqcrfL20XDS6SwZIJFPOBZ6N0ADa9nLzWlBhxk1aoUjbUX3JkTR9vEmk7o35XpAHi68ahZ4T6RE2U9XyA=="],
+
+ "@aws-sdk/credential-provider-cognito-identity": ["@aws-sdk/credential-provider-cognito-identity@3.972.75", "", { "dependencies": { "@aws-sdk/nested-clients": "^3.997.50", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-aJdScAJeeXC1cdMDCk8/3eDoHch8o0z+oxlqFALqBK2+hbtSz+hZQ9DhfarXJmva9+FM2gmC5PUJ8BB/HbAqXQ=="],
+
+ "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.76", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-YxmL1Ar7bV9ZkJvtv+lj+MF6le76NnZnbmgySyZuxpZaCv0/1lZ3KxKy6RIj1h9ui2hWnVjZxbmTMeO0y/NrQA=="],
+
+ "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.78", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/fetch-http-handler": "^5.8.0", "@smithy/node-http-handler": "^4.12.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-3VHknDRngoebsaqyT1iR75b5cBT2zfEtODK6yYmYltljkK1HEAQLs2FEd1IpWqAeOYdMGaG9wikngWLTufY7RA=="],
+
+ "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.973.21", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/credential-provider-env": "^3.972.76", "@aws-sdk/credential-provider-http": "^3.972.78", "@aws-sdk/credential-provider-login": "^3.972.83", "@aws-sdk/credential-provider-process": "^3.972.76", "@aws-sdk/credential-provider-sso": "^3.973.20", "@aws-sdk/credential-provider-web-identity": "^3.972.82", "@aws-sdk/nested-clients": "^3.997.50", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/credential-provider-imds": "^4.5.2", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-aLx9hbCX6mGAyv4ZZd8UagJ4RASHVJw6BFDSIyuKmLkqNL6H47EjBPK6qaJ4B5TDV6mc+vMa0SJFYyo4mkvVQg=="],
+
+ "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.83", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/nested-clients": "^3.997.50", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-9bJU6FjRacF4FK/QHf8Dyxhur7B1grRj8C1Yh4IkpoFW2exQflPdCv0VrbemLW9ZctLNle+AE7TETs9KNvuLYg=="],
+
+ "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.88", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.76", "@aws-sdk/credential-provider-http": "^3.972.78", "@aws-sdk/credential-provider-ini": "^3.973.21", "@aws-sdk/credential-provider-process": "^3.972.76", "@aws-sdk/credential-provider-sso": "^3.973.20", "@aws-sdk/credential-provider-web-identity": "^3.972.82", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/credential-provider-imds": "^4.5.2", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-AG0KID5qhQPzrFkh5b0Cws1Amytvzo0L0Xj6hMgJXEwD6ZXAqninDhSNAWJiN5zU4YVVZnuidnqwPwYJ04IamA=="],
+
+ "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.76", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-pdhKFTXv1zPYhh92MTlp0jVbLWvUEV9v+rziuEkoskGGQ2iJskxHTO38nA8UAbT/N4IM3n8OZJ9rIY5IBO15/A=="],
+
+ "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.973.20", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/nested-clients": "^3.997.50", "@aws-sdk/token-providers": "3.1155.0", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-DlxrAB3MnjNLx/da7b89W+3DyJQYso/q7Ns15MsST2IV2nkAYQOYTUCaffNSjgrpMP99Dm2mV2jxzcbyF94ZfA=="],
+
+ "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.82", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/nested-clients": "^3.997.50", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-LUo46IqPTJ4xKBsrLuoaxiv/D2GTBCcFDMD1y3jKIqPR/p7Rw14RJFZ0iRWftqZtyLPQX9JMGnSi3mBu4y/pNA=="],
+
+ "@aws-sdk/credential-providers": ["@aws-sdk/credential-providers@3.1057.0", "", { "dependencies": { "@aws-sdk/client-cognito-identity": "3.1057.0", "@aws-sdk/core": "^3.974.15", "@aws-sdk/credential-provider-cognito-identity": "^3.972.38", "@aws-sdk/credential-provider-env": "^3.972.41", "@aws-sdk/credential-provider-http": "^3.972.43", "@aws-sdk/credential-provider-ini": "^3.972.46", "@aws-sdk/credential-provider-login": "^3.972.45", "@aws-sdk/credential-provider-node": "^3.972.47", "@aws-sdk/credential-provider-process": "^3.972.41", "@aws-sdk/credential-provider-sso": "^3.972.45", "@aws-sdk/credential-provider-web-identity": "^3.972.45", "@aws-sdk/nested-clients": "^3.997.13", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/credential-provider-imds": "^4.3.6", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-rbrEHtz11g0kxsSkYr3fx2HABNNblp4AhB2MgPvJHgYOWfJ2eBviU7Mvoaef0PW8QH6lbZDfJcnM7eKvtvz3sw=="],
+
+ "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.50", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/signature-v4-multi-region": "^3.996.49", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/fetch-http-handler": "^5.8.0", "@smithy/node-http-handler": "^4.12.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-nKb8qu4QsnpZTq/6JIXc72mzuzfh7BK4sUQkpQznPec2z4W7Ob2xYkcM4i5e1yGTBnbJVeFv/0sf3jcz85hclA=="],
+
+ "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.49", "", { "dependencies": { "@aws-sdk/types": "^3.975.0", "@smithy/signature-v4": "^5.7.4", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-nvuAZNRrRRVCiARRKsYlQ0VnXiEQiq7vXPH+UA+LSo+kN3Djl9eo/V2r0udkw85PnsNshmv2Em1clld5M3j67w=="],
+
+ "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1155.0", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/nested-clients": "^3.997.50", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-iIcQnYLhWuq9qOLQbIWFrxF3PF0svVeZvHnUS4DUnO4zUFmFHIyGyjqrntXG+e/5PUgfavV0pYfDIKHxTzN2LA=="],
+
+ "@aws-sdk/types": ["@aws-sdk/types@3.975.0", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-WQlhs6juByzWOzj5fwxOx0mPJaEqMcG/sDpl3Id61B3XhKE63xY+0QWcnG+sWR/sCfprmreyUo1lM7IKZmHMzw=="],
+
+ "@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.965.10", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-ycwH6Zd2GhuSqdXX9ihbCjeGTB6xOJs+O3+Jb8/zDG9978XU80qs75dfkPJRMNKe5MvBZPuNeFpd4JZKPoUF4g=="],
+
+ "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.41", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-ctjVSyCMegrWfXlx6VqzSBFI6UqmQ5ZlnfMhdLIiWmhoH8UAQxSCP5N3OpG7X3k4LnS7ou74C4mt20+bfTW2aQ=="],
+
+ "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="],
+
+ "@babel/runtime": ["@babel/runtime@7.29.10", "", {}, "sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA=="],
+
+ "@effect/opentelemetry": ["@effect/opentelemetry@4.0.1", "", { "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <2.0.0", "@opentelemetry/api-logs": ">=0.203.0 <0.300.0", "@opentelemetry/resources": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-base": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-node": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-web": ">=2.0.0 <3.0.0", "@opentelemetry/semantic-conventions": ">=1.33.0 <2.0.0", "effect": "^4.0.1" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/api-logs", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-MZ7M1a1dxMqTqeNwIGSECjO7RrReEgeEwHo8BnbOqP+8IfVpdiJi/iPCmkFVWlAxanMl4qhsq69e0Hkul9t+bA=="],
+
+ "@effect/platform-node": ["@effect/platform-node@4.0.1", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.1", "undici": "^8.11.2" }, "peerDependencies": { "effect": "^4.0.1", "redis": ">=5.0.0 <7.0.0" } }, "sha512-FT0Fu5LO0/XP9zBpj0HqrtJNLluK4StNKqf6TSulUsjp8FPBsDVXGJiDuyoN5J2yVMT7tgvhxSwaDcObWVRIaQ=="],
+
+ "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.1", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.22.0" }, "peerDependencies": { "effect": "^4.0.1" } }, "sha512-VanaejudEhKJ4mpmqnEzdncVUhN6sGSO9ZDND54mRxAlZxj+4dYAWjDTYAy9o4tJo9aALeP2U8cVcOFzEdSeLg=="],
+
+ "@ff-labs/fff-bin-android-arm64": ["@ff-labs/fff-bin-android-arm64@0.10.5", "", { "os": "android", "cpu": "arm64" }, "sha512-idn1m9ycwv0Dzz6ih4H8e/X1WpGF6kglUxqP78oYgoC2sgAdPtLxD6hIAZK13p8isK7H8EMwrMze/4lynEDcKQ=="],
+
+ "@ff-labs/fff-bin-darwin-arm64": ["@ff-labs/fff-bin-darwin-arm64@0.10.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tG0VSt6fs3C2/Y6NDp57YJVXGEnrJ4F/e5gDrFCbNeuwJe9LmcCKh6oHWUEZOaaymYT90kQmwuXZTvORP3G6Vw=="],
+
+ "@ff-labs/fff-bin-darwin-x64": ["@ff-labs/fff-bin-darwin-x64@0.10.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-pwdFJUB/oeO4S/SfmRDdE5ipbAMYoVtRMwTZ12Wn2H46PgNU5DWnSjHhUNTLuKmkQU/ZIJL5Sn9j/fzwuIzcoA=="],
+
+ "@ff-labs/fff-bin-linux-arm64-gnu": ["@ff-labs/fff-bin-linux-arm64-gnu@0.10.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-prbY72gR+VPoSxl1InglwSwVHM0kErYMjUOpdvjQhS8eRdXYL3hTuYQv9L7fiB0mqaDzcwFa339uMr3p8ujDrw=="],
+
+ "@ff-labs/fff-bin-linux-arm64-musl": ["@ff-labs/fff-bin-linux-arm64-musl@0.10.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-MQN/CEKn4c4toooGCmZ6N8cP9mhgE4tnR6rr6a4k9JjH0xi05fYdG4+BabPkr+l/mDc01DsRpSKVNBmvoZ3IlQ=="],
+
+ "@ff-labs/fff-bin-linux-x64-gnu": ["@ff-labs/fff-bin-linux-x64-gnu@0.10.5", "", { "os": "linux", "cpu": "x64" }, "sha512-8NzFGGyqrFaCtb+SPTFd0xQGUlHCJMf0KYGwuca46IP37nN4OtDT1wnyZ9oaPDZ+2qqyxox10hCnqGlmj3PJzQ=="],
+
+ "@ff-labs/fff-bin-linux-x64-musl": ["@ff-labs/fff-bin-linux-x64-musl@0.10.5", "", { "os": "linux", "cpu": "x64" }, "sha512-mioRbn8aAW2Jmm4W0w3poePo65iSE+REfO+Jc0iqsjirImEy6pKt7dswj+7LSU6RPWwPsMb4POTMENwNLMlJww=="],
+
+ "@ff-labs/fff-bin-win32-arm64": ["@ff-labs/fff-bin-win32-arm64@0.10.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-GIZiea0AhuUDtJtXO2mGWs3wil1OR6lpLKOqXMW8EhHVHs8SB+xwzOgZ3Hg7YLEJm+Ui/2tFW/NLbqRuxpmtrQ=="],
+
+ "@ff-labs/fff-bin-win32-x64": ["@ff-labs/fff-bin-win32-x64@0.10.5", "", { "os": "win32", "cpu": "x64" }, "sha512-EvCWb7wLrtCvkziksAnTVxcGa0roaWNC978t4nG3ESKEty/Mvrj8kaLAzqWfuA2Xk+gTW+qisJYxa4FqFnqR0Q=="],
+
+ "@ff-labs/fff-bun": ["@ff-labs/fff-bun@0.10.5", "", { "optionalDependencies": { "@ff-labs/fff-bin-android-arm64": "0.10.5", "@ff-labs/fff-bin-darwin-arm64": "0.10.5", "@ff-labs/fff-bin-darwin-x64": "0.10.5", "@ff-labs/fff-bin-linux-arm64-gnu": "0.10.5", "@ff-labs/fff-bin-linux-arm64-musl": "0.10.5", "@ff-labs/fff-bin-linux-x64-gnu": "0.10.5", "@ff-labs/fff-bin-linux-x64-musl": "0.10.5", "@ff-labs/fff-bin-win32-arm64": "0.10.5", "@ff-labs/fff-bin-win32-x64": "0.10.5" }, "os": [ "!aix", "!sunos", "!freebsd", "!openbsd", ], "cpu": [ "x64", "arm64", ] }, "sha512-n4t5e4mzbEAXO6SyPHx8ajECGvfTZ/y4TNCqvKSwROpcTqfobaq2T+MdsfsKyA+j+B11NmpCVLjVhCTel8iB8Q=="],
+
+ "@ff-labs/fff-node": ["@ff-labs/fff-node@0.10.5", "", { "dependencies": { "ffi-rs": "^1.0.0" }, "optionalDependencies": { "@ff-labs/fff-bin-android-arm64": "0.10.5", "@ff-labs/fff-bin-darwin-arm64": "0.10.5", "@ff-labs/fff-bin-darwin-x64": "0.10.5", "@ff-labs/fff-bin-linux-arm64-gnu": "0.10.5", "@ff-labs/fff-bin-linux-arm64-musl": "0.10.5", "@ff-labs/fff-bin-linux-x64-gnu": "0.10.5", "@ff-labs/fff-bin-linux-x64-musl": "0.10.5", "@ff-labs/fff-bin-win32-arm64": "0.10.5", "@ff-labs/fff-bin-win32-x64": "0.10.5" }, "os": [ "!aix", "!sunos", "!freebsd", "!openbsd", ], "cpu": [ "x64", "arm64", ] }, "sha512-O4KZoO9lZluTqdHUYgd23xpEX/u7G87qQTgUIgtJ4BFTh1x/p243/M6AU2HdSHYWK+vrMRAdDXDzeUW/9rjk3Q=="],
+
+ "@gar/promise-retry": ["@gar/promise-retry@1.0.3", "", {}, "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA=="],
+
+ "@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="],
+
+ "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
+
+ "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
+
+ "@isaacs/string-locale-compare": ["@isaacs/string-locale-compare@1.1.0", "", {}, "sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ=="],
+
+ "@lydell/node-pty": ["@lydell/node-pty@1.2.0-beta.12", "", { "optionalDependencies": { "@lydell/node-pty-darwin-arm64": "1.2.0-beta.12", "@lydell/node-pty-darwin-x64": "1.2.0-beta.12", "@lydell/node-pty-linux-arm64": "1.2.0-beta.12", "@lydell/node-pty-linux-x64": "1.2.0-beta.12", "@lydell/node-pty-win32-arm64": "1.2.0-beta.12", "@lydell/node-pty-win32-x64": "1.2.0-beta.12" } }, "sha512-qIK890UwPupoj07osVvgOIa++1mxeHbcGry4PKRHhNVNs81V2SCG34eJr46GybiOmBtc8Sj5PB1/GGM5PL549g=="],
+
+ "@lydell/node-pty-darwin-arm64": ["@lydell/node-pty-darwin-arm64@1.2.0-beta.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tqaifcY9Cr41SblO1+FLzh8oxxtkNhuW9Dhl22lKme9BreYvKvxEZcdPIXTuqkJc5tagOEC4QHShKmJjLyLXLQ=="],
+
+ "@lydell/node-pty-darwin-x64": ["@lydell/node-pty-darwin-x64@1.2.0-beta.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-4LrS5pCJwqHKDVf1zS2gyNV0m4hKAXch+XZNhbZ6LY8uwVL8BhchzQBO40Os5anuRxRCWzHpw4Sp64Ie8q7E4Q=="],
+
+ "@lydell/node-pty-linux-arm64": ["@lydell/node-pty-linux-arm64@1.2.0-beta.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-Sx+A71x5BDGHt9ansfrtGxwq2VFVDWvJUAdlUL0Hv0qeiJUfts+hgopx+CgT4PSwahKjdEgtu0+FAfY9rICKRw=="],
+
+ "@lydell/node-pty-linux-x64": ["@lydell/node-pty-linux-x64@1.2.0-beta.12", "", { "os": "linux", "cpu": "x64" }, "sha512-bJzs94njofYhGg/UDqW1nj0dtvvu+2OvxMY+RlLS1T17VgcktKoIR6PuenTwE5HJ/D6StCPADmXcT0nNsCKmIQ=="],
+
+ "@lydell/node-pty-win32-arm64": ["@lydell/node-pty-win32-arm64@1.2.0-beta.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-p7POgjVEiFaBC3/y+AKuV1FzePCsJ6HmZDv2XK+jBZSfwP8+uBAw181ZiKYN1YuRa/XpmBGaWezcI8hZkbW++g=="],
+
+ "@lydell/node-pty-win32-x64": ["@lydell/node-pty-win32-x64@1.2.0-beta.12", "", { "os": "win32", "cpu": "x64" }, "sha512-IDFa00g7qUDGUYgByrUBJtC+mOjYVt/8KYyWivCg5JjGOHbBUACUQZLl0jTWmnr+tld/UyTpX90a2PY6oTVtRw=="],
+
+ "@modelcontextprotocol/client": ["@modelcontextprotocol/client@2.3.1", "", { "dependencies": { "@modelcontextprotocol/core": "2.3.1", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.8", "jose": "^6.1.3", "pkce-challenge": "^5.0.0", "zod": "^4.2.0" } }, "sha512-mIGZXpHsjnZ6lD+gD/WCMpR5k8yVQQ8nNFH1N0Srf7AvnwTUMYD6pvTY8ng2+He5FfV7YMZLmrjL7cLa/cc3dQ=="],
+
+ "@modelcontextprotocol/core": ["@modelcontextprotocol/core@2.3.1", "", { "dependencies": { "zod": "^4.2.0" } }, "sha512-laVmIhPGpWi7rG9q+0UigblRN0Hws/Yjlb6qLFtVqryXVLkc4h+yN8z731POD2LnAGQlFe7/tXhUm7Bi0YB91g=="],
+
+ "@npmcli/agent": ["@npmcli/agent@4.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.1", "lru-cache": "^11.2.1", "socks-proxy-agent": "^8.0.3" } }, "sha512-EUEuWAxnL07Sp5/iC/1X6Xj+XThUvnbei9zfRWZdEXa7lss9RTHMhAHBeg+MZ5To9s/gGaSI+UwZTPdYMvKSeg=="],
+
+ "@npmcli/arborist": ["@npmcli/arborist@9.4.0", "", { "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", "@npmcli/fs": "^5.0.0", "@npmcli/installed-package-contents": "^4.0.0", "@npmcli/map-workspaces": "^5.0.0", "@npmcli/metavuln-calculator": "^9.0.2", "@npmcli/name-from-folder": "^4.0.0", "@npmcli/node-gyp": "^5.0.0", "@npmcli/package-json": "^7.0.0", "@npmcli/query": "^5.0.0", "@npmcli/redact": "^4.0.0", "@npmcli/run-script": "^10.0.0", "bin-links": "^6.0.0", "cacache": "^20.0.1", "common-ancestor-path": "^2.0.0", "hosted-git-info": "^9.0.0", "json-stringify-nice": "^1.1.4", "lru-cache": "^11.2.1", "minimatch": "^10.0.3", "nopt": "^9.0.0", "npm-install-checks": "^8.0.0", "npm-package-arg": "^13.0.0", "npm-pick-manifest": "^11.0.1", "npm-registry-fetch": "^19.0.0", "pacote": "^21.0.2", "parse-conflict-json": "^5.0.1", "proc-log": "^6.0.0", "proggy": "^4.0.0", "promise-all-reject-late": "^1.0.0", "promise-call-limit": "^3.0.1", "semver": "^7.3.7", "ssri": "^13.0.0", "treeverse": "^3.0.0", "walk-up-path": "^4.0.0" }, "bin": { "arborist": "bin/index.js" } }, "sha512-4Bm8hNixJG/sii1PMnag0V9i/sGOX9VRzFrUiZMSBJpGlLR38f+Btl85d07G9GL56xO0l0OZjvrGNYsDYp0xKA=="],
+
+ "@npmcli/config": ["@npmcli/config@10.8.1", "", { "dependencies": { "@npmcli/map-workspaces": "^5.0.0", "@npmcli/package-json": "^7.0.0", "ci-info": "^4.0.0", "ini": "^6.0.0", "nopt": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", "walk-up-path": "^4.0.0" } }, "sha512-MAYk9IlIGiyC0c9fnjdBSQfIFPZT0g1MfeSiD1UXTq2zJOLX55jS9/sETJHqw/7LN18JjITrhYfgCfapbmZHiQ=="],
+
+ "@npmcli/fs": ["@npmcli/fs@5.0.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og=="],
+
+ "@npmcli/git": ["@npmcli/git@7.0.2", "", { "dependencies": { "@gar/promise-retry": "^1.0.0", "@npmcli/promise-spawn": "^9.0.0", "ini": "^6.0.0", "lru-cache": "^11.2.1", "npm-pick-manifest": "^11.0.1", "proc-log": "^6.0.0", "semver": "^7.3.5", "which": "^6.0.0" } }, "sha512-oeolHDjExNAJAnlYP2qzNjMX/Xi9bmu78C9dIGr4xjobrSKbuMYCph8lTzn4vnW3NjIqVmw/f8BCfouqyJXlRg=="],
+
+ "@npmcli/installed-package-contents": ["@npmcli/installed-package-contents@4.0.0", "", { "dependencies": { "npm-bundled": "^5.0.0", "npm-normalize-package-bin": "^5.0.0" }, "bin": { "installed-package-contents": "bin/index.js" } }, "sha512-yNyAdkBxB72gtZ4GrwXCM0ZUedo9nIbOMKfGjt6Cu6DXf0p8y1PViZAKDC8q8kv/fufx0WTjRBdSlyrvnP7hmA=="],
+
+ "@npmcli/map-workspaces": ["@npmcli/map-workspaces@5.0.3", "", { "dependencies": { "@npmcli/name-from-folder": "^4.0.0", "@npmcli/package-json": "^7.0.0", "glob": "^13.0.0", "minimatch": "^10.0.3" } }, "sha512-o2grssXo1e774E5OtEwwrgoszYRh0lqkJH+Pb9r78UcqdGJRDRfhpM8DvZPjzNLLNYeD/rNbjOKM3Ss5UABROw=="],
+
+ "@npmcli/metavuln-calculator": ["@npmcli/metavuln-calculator@9.0.3", "", { "dependencies": { "cacache": "^20.0.0", "json-parse-even-better-errors": "^5.0.0", "pacote": "^21.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5" } }, "sha512-94GLSYhLXF2t2LAC7pDwLaM4uCARzxShyAQKsirmlNcpidH89VA4/+K1LbJmRMgz5gy65E/QBBWQdUvGLe2Frg=="],
+
+ "@npmcli/name-from-folder": ["@npmcli/name-from-folder@4.0.0", "", {}, "sha512-qfrhVlOSqmKM8i6rkNdZzABj8MKEITGFAY+4teqBziksCQAOLutiAxM1wY2BKEd8KjUSpWmWCYxvXr0y4VTlPg=="],
+
+ "@npmcli/node-gyp": ["@npmcli/node-gyp@5.0.0", "", {}, "sha512-uuG5HZFXLfyFKqg8QypsmgLQW7smiRjVc45bqD/ofZZcR/uxEjgQU8qDPv0s9TEeMUiAAU/GC5bR6++UdTirIQ=="],
+
+ "@npmcli/package-json": ["@npmcli/package-json@7.0.5", "", { "dependencies": { "@npmcli/git": "^7.0.0", "glob": "^13.0.0", "hosted-git-info": "^9.0.0", "json-parse-even-better-errors": "^5.0.0", "proc-log": "^6.0.0", "semver": "^7.5.3", "spdx-expression-parse": "^4.0.0" } }, "sha512-iVuTlG3ORq2iaVa1IWUxAO/jIp77tUKBhoMjuzYW2kL4MLN1bi/ofqkZ7D7OOwh8coAx1/S2ge0rMdGv8sLSOQ=="],
+
+ "@npmcli/promise-spawn": ["@npmcli/promise-spawn@9.0.1", "", { "dependencies": { "which": "^6.0.0" } }, "sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q=="],
+
+ "@npmcli/query": ["@npmcli/query@5.0.0", "", { "dependencies": { "postcss-selector-parser": "^7.0.0" } }, "sha512-8TZWfTQOsODpLqo9SVhVjHovmKXNpevHU0gO9e+y4V4fRIOneiXy0u0sMP9LmS71XivrEWfZWg50ReH4WRT4aQ=="],
+
+ "@npmcli/redact": ["@npmcli/redact@4.0.0", "", {}, "sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q=="],
+
+ "@npmcli/run-script": ["@npmcli/run-script@10.0.4", "", { "dependencies": { "@npmcli/node-gyp": "^5.0.0", "@npmcli/package-json": "^7.0.0", "@npmcli/promise-spawn": "^9.0.0", "node-gyp": "^12.1.0", "proc-log": "^6.0.0" } }, "sha512-mGUWr1uMnf0le2TwfOZY4SFxZGXGfm4Jtay/nwAa2FLNAKXUoUwaGwBMNH36UHPtinWfTSJ3nqFQr0091CxVGg=="],
+
+ "@opencode-ai/plugin": ["@opencode-ai/plugin@1.19.4", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode-ai/sdk": "1.19.4", "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-VN71q/OmfggxIhiDbuzYDYmZcQMsNyVSw1o91e7LD6WUn/9jx4usI9ZpoPbC5p/Prpo/Umzpd+38+EuLoKRr+A=="],
+
+ "@opencode-ai/pty": ["@opencode-ai/pty@0.3.1", "", { "optionalDependencies": { "@opencode-ai/pty-darwin-arm64": "0.3.1", "@opencode-ai/pty-darwin-x64": "0.3.1", "@opencode-ai/pty-linux-arm64-gnu": "0.3.1", "@opencode-ai/pty-linux-arm64-musl": "0.3.1", "@opencode-ai/pty-linux-x64-gnu": "0.3.1", "@opencode-ai/pty-linux-x64-musl": "0.3.1", "@opencode-ai/pty-win32-arm64": "0.3.1", "@opencode-ai/pty-win32-x64": "0.3.1" }, "bin": { "opencode-pty": "bin/opencode-pty.js" } }, "sha512-xmxBVI1z/J6Jt3cC5Mlcc2iOxi5cFtPF3q+023sEZJMEE9hBqh7EdXKZesAzAJ2qBWXGEEqWTudnERY7suoAKw=="],
+
+ "@opencode-ai/pty-darwin-arm64": ["@opencode-ai/pty-darwin-arm64@0.3.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-pn3dqOX9F4HZ9Vzjb9UTm4B0dz5w9c8jBsy8/Dp1WzrLOjg91cjx/LzPqXG7rrzD39ygyxnHZE4694f7lh9kuQ=="],
+
+ "@opencode-ai/pty-darwin-x64": ["@opencode-ai/pty-darwin-x64@0.3.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-uBzxgFUxfGew7yR60YW5ng8is571AHWBg4MqytwDVAwlmkEaAzIvja/nxD+dWaszOq+PtJIaYnNqf4ybmG1JXg=="],
+
+ "@opencode-ai/pty-linux-arm64-gnu": ["@opencode-ai/pty-linux-arm64-gnu@0.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-/8GK/hfnHa4O5vvVuFOd/SMr07n19dLmoILjsn8BmTBlBEU5oZClssKoeCDiPigFsVuL2cMBTqb2bONuD5SyDg=="],
+
+ "@opencode-ai/pty-linux-arm64-musl": ["@opencode-ai/pty-linux-arm64-musl@0.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jemE95Tw4tVPUpmHGqDtTKlSnxFexZT6+R05esxdT9uiVnflK9mR0fLlgZpsgSDEQIZPywo5scA4wgqZaPIWKw=="],
+
+ "@opencode-ai/pty-linux-x64-gnu": ["@opencode-ai/pty-linux-x64-gnu@0.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-DfU49EkQrE/G+HQZmWg7jvwCTdOkYcFvXFts6MNRycnrtm/K7Qq0SKVGYmZC0J2LN4SmgqCUILzmS+rDNkyw8g=="],
+
+ "@opencode-ai/pty-linux-x64-musl": ["@opencode-ai/pty-linux-x64-musl@0.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-P+/oP0Xkfjecd/NmD+dwYQE+l/cUjhevONBauzjfOWSqd32ltznEpKqbfJW0RldR+Gztr9sJdkAATjRHURX1RQ=="],
+
+ "@opencode-ai/pty-win32-arm64": ["@opencode-ai/pty-win32-arm64@0.3.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-HM2t9xYnFU2NYbFowVINKiVzbTdRtnIf/1kPC5lulLcqWf5g+N/O+uXy4dGqErdun49vJ+MP8EydG52whkE/xQ=="],
+
+ "@opencode-ai/pty-win32-x64": ["@opencode-ai/pty-win32-x64@0.3.1", "", { "os": "win32", "cpu": "x64" }, "sha512-xUpe+TApPY81AII9EXb9MdN2/WyDJB0t3C/vMsSxBQZSvh6mfML7mqt2kK9lQntQn5wz1kmKi05NRr9bHtkzoQ=="],
+
+ "@opencode-ai/sdk": ["@opencode-ai/sdk@1.19.4", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-3Wuc4t8gEuWKHbX1Na9oDK6PgOveid70l4Ks5RCjRIo2RcP8FQN3NS6Mg3XWc2+WiUfpfrmvBWlh5u1zeQfDQw=="],
+
+ "@opencode/ai": ["@opencode/ai@2.1.4", "", { "dependencies": { "@aws-sdk/credential-providers": "3.1057.0", "@opencode/schema": "2.1.4", "@smithy/eventstream-codec": "4.2.14", "@smithy/util-utf8": "4.2.2", "aws4fetch": "1.0.20", "effect": "4.0.1", "google-auth-library": "10.5.0" } }, "sha512-980bht1i8xv6hN/TQ+3tGujHuxfduje3jW69SKBvmiDxQBZ468Mk7sR4ztOy3NB/4fbI8iWEYhrcc3GbWWMxrw=="],
+
+ "@opencode/cli": ["@opencode/cli@2.1.4", "", { "optionalDependencies": { "@opencode/cli-darwin-arm64": "2.1.4", "@opencode/cli-darwin-x64": "2.1.4", "@opencode/cli-darwin-x64-baseline": "2.1.4", "@opencode/cli-linux-arm64": "2.1.4", "@opencode/cli-linux-arm64-musl": "2.1.4", "@opencode/cli-linux-x64": "2.1.4", "@opencode/cli-linux-x64-baseline": "2.1.4", "@opencode/cli-linux-x64-baseline-musl": "2.1.4", "@opencode/cli-linux-x64-musl": "2.1.4", "@opencode/cli-windows-arm64": "2.1.4", "@opencode/cli-windows-x64": "2.1.4", "@opencode/cli-windows-x64-baseline": "2.1.4" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "opencode": "bin/opencode.exe", "opencode2": "bin/opencode.exe" } }, "sha512-vtHFa/1frSylpx1cZRuzmM2ecHWn6YB8cNvhw8eWKPvSFvDnckZXugDbi3LJYD9TGjRx2U6JRo725qwqtwMUfQ=="],
+
+ "@opencode/cli-darwin-arm64": ["@opencode/cli-darwin-arm64@2.1.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QwjyvovTdT0gFi5rxUyPVlYePIe3ELduR9olu+KJWe089XVIDOTMhxylBe/ZGIpvoXHYo6bp2NskIiqCFAEXsw=="],
+
+ "@opencode/cli-darwin-x64": ["@opencode/cli-darwin-x64@2.1.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-pdx0Iyl+IK1SdSQOhvz8JE4Rk0PGyacZ55FaxmyowYe9h0aFZNs3UAj7wNYod6Hsu4qsAeSVkdfQLr4SI8G1oA=="],
+
+ "@opencode/cli-darwin-x64-baseline": ["@opencode/cli-darwin-x64-baseline@2.1.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-htb+MYexlbFPc/da+jXePmclbXuMMX4m8kSIe24wbgy/npTC8wv1VHj0QTDt9YttTnnas+Yr1kNmJ7j/86yt+Q=="],
+
+ "@opencode/cli-linux-arm64": ["@opencode/cli-linux-arm64@2.1.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-drw0LWMG0niaE94ePXm1h7qjFBZZRcSZD3gact3hx3OYPXesp7xrOt0bn3sicYNUN6fojERrDKR6jYrXmZX+rA=="],
+
+ "@opencode/cli-linux-arm64-musl": ["@opencode/cli-linux-arm64-musl@2.1.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-tAYpbC/cY6ospekIftWTkwRfuHRfgKeOfg98A3RPs/2cckmGrOhyhFeHol22H4se7NRAFPboMCj0+UV7Wgt6uQ=="],
+
+ "@opencode/cli-linux-x64": ["@opencode/cli-linux-x64@2.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-o8EJ0ZsT4dzDKN+0mpG2EKbNGzJyp+F1++Av28UReRVfiEv+N34eZEnN/unkiMoerKHad6W6Ii7k4ukTnPNmNQ=="],
+
+ "@opencode/cli-linux-x64-baseline": ["@opencode/cli-linux-x64-baseline@2.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-0/H4wRDAW4hA5IWN440XwDrRATbvT3cno+UUsqFynTu0TeGXNADjK2AZfvhNTfyJaVyCa+v7VhB+r7ghuMa5EQ=="],
+
+ "@opencode/cli-linux-x64-baseline-musl": ["@opencode/cli-linux-x64-baseline-musl@2.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-zpQqwKXFHMB4Qw83t5yT95EPV46eZ066UVjm6w3WPP+RRl5NQasLAkFdBfQsl86iXKfie7HNOx8rA5seZjCAkQ=="],
+
+ "@opencode/cli-linux-x64-musl": ["@opencode/cli-linux-x64-musl@2.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-skzwj4raZAiDZKWgs1mjb3KRZGK1WlfBr4GEjAXAa3qQxnt+tAZ0rObU4T12foI99n1e+5COi4iHhl0qOkHqyA=="],
+
+ "@opencode/cli-windows-arm64": ["@opencode/cli-windows-arm64@2.1.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-ZDKG+2rhCYb2ngl1B/H8Lx2ll9NjNLhghj10/War9eK5ASYysWvR+26+Bdv+/J29eZdzeXViJnOwf3tZBdMakA=="],
+
+ "@opencode/cli-windows-x64": ["@opencode/cli-windows-x64@2.1.4", "", { "os": "win32", "cpu": "x64" }, "sha512-9h0Nb/3g6HxnFY6lawSqNSCxek0K3zUPjOAoF20pyPBaLMq65sXfWLVwgyS/21AM0NrUw77S9CeP0tPM+XrbGQ=="],
+
+ "@opencode/cli-windows-x64-baseline": ["@opencode/cli-windows-x64-baseline@2.1.4", "", { "os": "win32", "cpu": "x64" }, "sha512-76bsf2uQK/vj2CI0p6dhten9ZD8HmGLeC4B1XfwEaD1xmWHB+8hwTGy5VVvHsoqH+flRQeq3Xfk0VCyQ9zA9+g=="],
+
+ "@opencode/client": ["@opencode/client@2.1.4", "", { "dependencies": { "@opencode/protocol": "2.1.4", "@opencode/schema": "2.1.4" }, "peerDependencies": { "effect": "4.0.1", "solid-js": ">=1.9.0" }, "optionalPeers": ["effect", "solid-js"] }, "sha512-xNHP8Xu4wcq4G849Xnbb8IxYxMTzF0TqvohWVUQZH5aURrVHIIbHMfXbZY6s4i4C9kpVbwjXFFwWAo6KIK3oiw=="],
+
+ "@opencode/codemode": ["@opencode/codemode@2.1.4", "", { "dependencies": { "acorn": "8.15.0", "effect": "4.0.1" } }, "sha512-1gFyokMI4EgFyu0NgaCPr/HPZQKasZuwCPX4v7Plm+dYANetRt98vv3BXG4pJpn4ERPI+mc86tsHfpulDdZEfA=="],
+
+ "@opencode/core": ["@opencode/core@2.1.4", "", { "dependencies": { "@ai-sdk/cohere": "3.0.27", "@ai-sdk/perplexity": "3.0.26", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@ai-sdk/vercel": "2.0.39", "@ff-labs/fff-bun": "0.10.5", "@ff-labs/fff-node": "0.10.5", "@lydell/node-pty": "1.2.0-beta.12", "@modelcontextprotocol/client": "2.3.1", "@modelcontextprotocol/core": "2.3.1", "@opencode-ai/pty": "0.3.1", "@opencode/ai": "2.1.4", "@opencode/codemode": "2.1.4", "@opencode/plugin": "2.1.4", "@opencode/plugin-browser": "2.1.4", "@opencode/schema": "2.1.4", "@opencode/util": "2.1.4", "@parcel/watcher": "2.5.1", "@silvia-odwyer/photon-node": "0.3.4", "@standard-schema/spec": "1.1.0", "bun-pty": "0.4.11", "diff": "8.0.4", "drizzle-orm": "1.0.0-rc.5-169397b", "effect": "4.0.1", "fuzzysort": "3.1.0", "gitlab-ai-provider": "6.19.0", "gray-matter": "4.0.3", "htmlparser2": "8.0.2", "http-proxy-agent": "7.0.2", "https-proxy-agent": "7.0.6", "immer": "11.1.4", "jose": "6.0.11", "jsonc-parser": "3.3.1", "mime-types": "3.0.2", "tree-sitter-bash": "0.25.0", "tree-sitter-powershell": "0.25.10", "web-tree-sitter": "0.25.10", "which": "6.0.1", "zod": "4.1.8" } }, "sha512-0DI8oiOCPgZb1PC20+aF8Ns3Jx7xxa85xf++ZmtdnLp7OpblUaeUnidE8wIDkm5CHT8CgAXdKw6gK4aDm3ta6A=="],
+
+ "@opencode/plugin": ["@opencode/plugin@2.1.4", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode/ai": "2.1.4", "@opencode/client": "2.1.4", "@opencode/protocol": "2.1.4", "@opencode/schema": "2.1.4", "@opencode/util": "2.1.4", "effect": "4.0.1" }, "peerDependencies": { "@opencode/theme": "2.1.4", "@opentui/core": ">=0.6.8", "@opentui/solid": ">=0.6.8", "solid-js": ">=1.9.0" }, "optionalPeers": ["@opencode/theme", "@opentui/core", "@opentui/solid", "solid-js"] }, "sha512-SGAkdGp0VRxos2BEkRiyRl75G7DtuvP0ywgiEiHz1PbdUwUe2EAURMrzFgLCYCpWEm/zaNrguxPDVL0carrCsA=="],
+
+ "@opencode/plugin-browser": ["@opencode/plugin-browser@2.1.4", "", { "dependencies": { "@opencode/plugin": "2.1.4", "@opencode/schema": "2.1.4", "effect": "4.0.1" } }, "sha512-t7J4aaGrigJG/HiG7ya7UIU0yV9bnqmNdC9Gcrm4aJrBdNr9QyqUjPQSIdBDjIDW+35pSjnbQs1xTZ3j9hw84Q=="],
+
+ "@opencode/protocol": ["@opencode/protocol@2.1.4", "", { "dependencies": { "@opencode/schema": "2.1.4", "effect": "4.0.1" } }, "sha512-dnifgvY6p4xdHkXRH3PEaobfCqrHb1AhihKxPNRNkGn5SueRCFCfK2pZW1GkeC36fGCmcLxlfihuIMaQL1BVzA=="],
+
+ "@opencode/schema": ["@opencode/schema@2.1.4", "", { "dependencies": { "@standard-schema/spec": "1.1.0", "effect": "4.0.1" } }, "sha512-PLr8eFW81Yz6U7YsSgfSUClUUH//8omh3HiXu/L08VX5arorWNAu2uSjoXyHIGkm7TaTbdXBGPmmDfpvrDM2wQ=="],
+
+ "@opencode/sdk": ["@opencode/sdk@2.1.4", "", { "dependencies": { "@opencode/client": "2.1.4", "@opencode/core": "2.1.4", "@opencode/plugin": "2.1.4", "@opencode/schema": "2.1.4", "@opencode/server": "2.1.4", "@opencode/util": "2.1.4", "effect": "4.0.1" } }, "sha512-V2lxqZR0QbHVfyGoeaRtazI78rMOtErjq6UuIlY9Y3mtH3KenBFgI1v2xKyzFER43JDOIMxCEtRuFiWmRxpjRQ=="],
+
+ "@opencode/server": ["@opencode/server@2.1.4", "", { "dependencies": { "@effect/platform-node": "4.0.1", "@effect/platform-node-shared": "4.0.1", "@opencode/core": "2.1.4", "@opencode/protocol": "2.1.4", "@opencode/schema": "2.1.4", "@opencode/simulation": "2.1.4", "@opencode/util": "2.1.4", "effect": "4.0.1" } }, "sha512-WYsp+pk0AkzG89bPqs2qTl0UfYiBdLxVlzPYGP8jChQxaPy41AyybkvhvGywyr0tCXTKJXxXgYLgEcAzYf+6bg=="],
+
+ "@opencode/simulation": ["@opencode/simulation@2.1.4", "", { "dependencies": { "@opencode/ai": "2.1.4", "@opencode/core": "2.1.4", "@opencode/plugin": "2.1.4", "@opencode/protocol": "2.1.4", "@opencode/util": "2.1.4", "@opentui/core": "0.6.8", "effect": "4.0.1", "ws": "8.21.0" } }, "sha512-Vt0h1pnugzmrazPvkeAFUkDAlejQiHhUQrpqsmYBscxcFXBNNKQXDqfMsTktl4R7vCBWYZixGQMHobQm5s/bQg=="],
+
+ "@opencode/util": ["@opencode/util@2.1.4", "", { "dependencies": { "@effect/opentelemetry": "4.0.1", "@effect/platform-node": "4.0.1", "@effect/platform-node-shared": "4.0.1", "@npmcli/arborist": "9.4.0", "@npmcli/config": "10.8.1", "@opentelemetry/api": "1.9.0", "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/context-async-hooks": "2.8.0", "@opentelemetry/exporter-trace-otlp-http": "0.219.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-logs": "0.219.0", "@opentelemetry/sdk-metrics": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0", "@opentelemetry/sdk-trace-node": "2.8.0", "cross-spawn": "7.0.6", "effect": "4.0.1", "glob": "13.0.5", "mime-types": "3.0.2", "minimatch": "10.2.5", "npm-package-arg": "13.0.2", "open": "11.0.4", "pacote": "21.5.1" } }, "sha512-Ihj6DqbnnovgUYSXki4PgvGxNbmB1wggDySIdM55Rz+sTfwFZ+zwROh3PrC4a8Vx/cJ6xQg41Uxa4nbcTbGI4A=="],
+
+ "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
+
+ "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.219.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg=="],
+
+ "@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.8.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-/3FIraneMcng67SUJCxvyInk/oxzwsxyadufk0wwfOBLf5wqtAGX4MoQASwSbndBPeARzBryUM9Azr5kHIdWLw=="],
+
+ "@opentelemetry/core": ["@opentelemetry/core@2.8.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww=="],
+
+ "@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.219.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/otlp-exporter-base": "0.219.0", "@opentelemetry/otlp-transformer": "0.219.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-9t6SvBXXBEjOBcIzgozvBbd3jWrv3Gt3ngGhl1fhdZ/zRc7oZDVOFEqbi2zlBpW9BXhgDMKv422J0DL/3iQWfw=="],
+
+ "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.219.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/otlp-transformer": "0.219.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-zvIxQX/AZUVKDU+hCuYx+7UkiP7GRdnk1ZbFQRYzHvYp47cAWR4j3IhoPhV9KaeXEv2xdGq3IA6PnpzDmLcmSA=="],
+
+ "@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.219.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-logs": "0.219.0", "@opentelemetry/sdk-metrics": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-aaYKAyXhw9VchKZVGOopD3Gw/kPsyrX2c6IQ0AW32mTjqmZOh5Y6Gf5OYqTNqVktAeBjmFinhyFaCwW6GYK9YQ=="],
+
+ "@opentelemetry/resources": ["@opentelemetry/resources@2.8.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg=="],
+
+ "@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.219.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-s6lTKRakaPClvKoWHRChxnXjDMkM/TQ30ff78jN6EBGf7MI7VzANE5PU3f4z9qDUudWjvZjOLHG0rBnBKYvoXA=="],
+
+ "@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.8.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-UDBGaj6W0Rgy5rTTaoxs8gVGF/aGkAKyjurJv7se6wjRxJu7FoquTLT/vt54DZfo4crbprYfhX/SOK9+BPw1qg=="],
+
+ "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.8.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ=="],
+
+ "@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.8.0", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.8.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-nZt9OGufioAc3AfoLTqA9bsAeaMJAictYDdI2VcNQ+PmT+3rfKjAZDZvgPfd8VPX0O5Bw1hdQF6kDK8VSpZiWg=="],
+
+ "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.43.0", "", {}, "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg=="],
+
+ "@opentui/core": ["@opentui/core@0.6.8", "", { "dependencies": { "bun-ffi-structs": "0.3.1", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.6.8", "@opentui/core-darwin-x64": "0.6.8", "@opentui/core-linux-arm64": "0.6.8", "@opentui/core-linux-arm64-musl": "0.6.8", "@opentui/core-linux-x64": "0.6.8", "@opentui/core-linux-x64-musl": "0.6.8", "@opentui/core-win32-arm64": "0.6.8", "@opentui/core-win32-x64": "0.6.8" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-wjqLwLdYGy/6qEBTtZEifuwwVzt06o0BqBF02xJ46whGEUK/gy+h25+1GK4dE9LFnHphgsqeA31f7PHBJPp91w=="],
+
+ "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.6.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kmu5Rp5qC3StrGAzLvBu6SLEXd2Qq60Yf0YCmL7BmDgYt0QUZ4M2JsSS7BFg7crpk+4Yz17N1ZeOCPumE4s83w=="],
+
+ "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.6.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-eGJcssaONQ32+xYOTRCdGGXnsMJJJLn6mHYt0W79Lck8zlMuel5dWWlCbxdM3qvX/IEHr+f56bGoojTvJjDw6w=="],
+
+ "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.6.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-ji2LPeEX4RU6RKC0/l70hZWmqD0AvvMvnd3WvpZUNOeaIBFseSzE0QEl3apRPUqAoYiuL7uRgwSV4eewa+xXpg=="],
+
+ "@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.6.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-b+3TI8rtFBQa7T04F7XVt1I8/eL2+GX0X6x9WbDBnNmolgf1DX3ZdVPmKEP8PlxAmDYB+gek+cQAwnkwiYl5YQ=="],
+
+ "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.6.8", "", { "os": "linux", "cpu": "x64" }, "sha512-g8hOxXpff7hOnZNlJRKCqWJr/dwVK/EyEhNsgNK4jLS13BUoaYWXi878xLXJ5acC82qEyGlYWL/kR6ACkiD0YA=="],
+
+ "@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.6.8", "", { "os": "linux", "cpu": "x64" }, "sha512-eVQSjO5wXYiqunz/V92WweHvVTCV+LtZOnu0z6kYjwwbAvsnTxISOFAyA9H71Sy3tsibjiu1o+E27twDyM9HlA=="],
+
+ "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.6.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-P4llQH8kFeiknVp0kfTydKaJC95BSW4mf3fub9HMgFx49eiF3qgcctbebZN+/WaD8Pshu0wiN1KPitqDTyrIDw=="],
+
+ "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.6.8", "", { "os": "win32", "cpu": "x64" }, "sha512-6dZuSw883BDetY59G2QwWHJG1kwfPkL+lWhDQ3a6ICxW4/7nito3uPkMRaRDb8DiTHgx0wMt/RdiydjQmZd+lw=="],
+
+ "@parcel/watcher": ["@parcel/watcher@2.5.1", "", { "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", "micromatch": "^4.0.5", "node-addon-api": "^7.0.0" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-freebsd-x64": "2.5.1", "@parcel/watcher-linux-arm-glibc": "2.5.1", "@parcel/watcher-linux-arm-musl": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", "@parcel/watcher-linux-arm64-musl": "2.5.1", "@parcel/watcher-linux-x64-glibc": "2.5.1", "@parcel/watcher-linux-x64-musl": "2.5.1", "@parcel/watcher-win32-arm64": "2.5.1", "@parcel/watcher-win32-ia32": "2.5.1", "@parcel/watcher-win32-x64": "2.5.1" } }, "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg=="],
+
+ "@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.1", "", { "os": "android", "cpu": "arm64" }, "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA=="],
+
+ "@parcel/watcher-darwin-arm64": ["@parcel/watcher-darwin-arm64@2.5.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw=="],
+
+ "@parcel/watcher-darwin-x64": ["@parcel/watcher-darwin-x64@2.5.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg=="],
+
+ "@parcel/watcher-freebsd-x64": ["@parcel/watcher-freebsd-x64@2.5.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ=="],
+
+ "@parcel/watcher-linux-arm-glibc": ["@parcel/watcher-linux-arm-glibc@2.5.1", "", { "os": "linux", "cpu": "arm" }, "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA=="],
+
+ "@parcel/watcher-linux-arm-musl": ["@parcel/watcher-linux-arm-musl@2.5.1", "", { "os": "linux", "cpu": "arm" }, "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q=="],
+
+ "@parcel/watcher-linux-arm64-glibc": ["@parcel/watcher-linux-arm64-glibc@2.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w=="],
+
+ "@parcel/watcher-linux-arm64-musl": ["@parcel/watcher-linux-arm64-musl@2.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg=="],
+
+ "@parcel/watcher-linux-x64-glibc": ["@parcel/watcher-linux-x64-glibc@2.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A=="],
+
+ "@parcel/watcher-linux-x64-musl": ["@parcel/watcher-linux-x64-musl@2.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg=="],
+
+ "@parcel/watcher-win32-arm64": ["@parcel/watcher-win32-arm64@2.5.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw=="],
+
+ "@parcel/watcher-win32-ia32": ["@parcel/watcher-win32-ia32@2.5.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ=="],
+
+ "@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="],
+
+ "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
+
+ "@redis/bloom": ["@redis/bloom@6.3.0", "", { "peerDependencies": { "@redis/client": "^6.3.0" } }, "sha512-NQ5poYpZr0jv6zazN1cy6JY5kIipKFZ3R9GTL4kOUTX3a9YAfY29U8bsE4lWhVQEM+G4V0BXVC57mubIdDijeA=="],
+
+ "@redis/client": ["@redis/client@6.3.0", "", { "peerDependencies": { "@node-rs/xxhash": "^1.1.0", "@opentelemetry/api": ">=1 <2" }, "optionalPeers": ["@node-rs/xxhash", "@opentelemetry/api"] }, "sha512-fHrjwGCBvAANGi+TuRuwKi83sIOzgHr8eh+Uf48w3fSX7Hhrz174NrWqfJhAC2vnN0d/tfaZpdr6+o6JC3QHwg=="],
+
+ "@redis/json": ["@redis/json@6.3.0", "", { "peerDependencies": { "@redis/client": "^6.3.0" } }, "sha512-yt6vRfPBXtW/3qm53cF81TSQpLMjWdW4/2E9WQ2GgbvsKkuCKCFQIcMiKlil1ZMnezK+IQuS795xKU5psbaegw=="],
+
+ "@redis/search": ["@redis/search@6.3.0", "", { "peerDependencies": { "@redis/client": "^6.3.0" } }, "sha512-RPXAZmKjZZG7Tm7m1w9keFdXzbL91PiSHQBfQRe9xwW40jNsTRbG0FwhIz4QlHPU4gKDAsCy8IQHj4yZl4H9rw=="],
+
+ "@redis/time-series": ["@redis/time-series@6.3.0", "", { "peerDependencies": { "@redis/client": "^6.3.0" } }, "sha512-+fJOB8mN1z5WRakCJNh5lVdz1KqvHGSOistsKeitD24QsCtPwyRQXWC0iq6T/i/ubLE/YcguZqPm8F598MKk4A=="],
+
+ "@sigstore/bundle": ["@sigstore/bundle@4.0.0", "", { "dependencies": { "@sigstore/protobuf-specs": "^0.5.0" } }, "sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A=="],
+
+ "@sigstore/core": ["@sigstore/core@3.2.1", "", {}, "sha512-qRsxPnCrbC/puegGxKuynfnxgLiHqWStrSjxkoB4YKqq3Z3s4cyZyj42ZdWFAEblNP65C+rBH8EuREHIXoi83g=="],
+
+ "@sigstore/protobuf-specs": ["@sigstore/protobuf-specs@0.5.2", "", {}, "sha512-SQqvFMt4V78fdjcDdYX6HbiVSOR4QK3ZgwCa2KOsopAgPIHy1rU5UDUmzLl02r5oyyaYcYHR1hpwDRk/yUe+Mw=="],
+
+ "@sigstore/sign": ["@sigstore/sign@4.1.1", "", { "dependencies": { "@gar/promise-retry": "^1.0.2", "@sigstore/bundle": "^4.0.0", "@sigstore/core": "^3.2.0", "@sigstore/protobuf-specs": "^0.5.0", "make-fetch-happen": "^15.0.4", "proc-log": "^6.1.0" } }, "sha512-Hf4xglukg0XXQ2RiD5vSoLjdPe8OBUPA8XeVjUObheuDcWdYWrnH/BNmxZCzkAy68MzmNCxXLeurJvs6hcP2OQ=="],
+
+ "@sigstore/tuf": ["@sigstore/tuf@4.0.2", "", { "dependencies": { "@sigstore/protobuf-specs": "^0.5.0", "tuf-js": "^4.1.0" } }, "sha512-TCAzTy0xzdP79EnxSjq9KQ3eaR7+FmudLC6eRKknVKZbV7ZNlGLClAAQb/HMNJ5n2OBNk2GT1tEmU0xuPr+SLQ=="],
+
+ "@sigstore/verify": ["@sigstore/verify@3.1.1", "", { "dependencies": { "@sigstore/bundle": "^4.0.0", "@sigstore/core": "^3.2.1", "@sigstore/protobuf-specs": "^0.5.0" } }, "sha512-qv7+G3J2cc6wwFj3yKvXOamzqhMwSk1ogPGmhpS8iXllcPrJaIIBA+4HbttlHVu1pqWTdmaCH/WE7UOC51kdoA=="],
+
+ "@silvia-odwyer/photon-node": ["@silvia-odwyer/photon-node@0.3.4", "", {}, "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA=="],
+
+ "@smithy/core": ["@smithy/core@3.36.2", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-oRw0rB9QSUiCZfEs23G5F4Af+b2rQt1lItIICWu5IejFNd+uPM8SWqiKLMpQ6bX1Q7slsLbjuoE2niErFtkVPQ=="],
+
+ "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.5.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg=="],
+
+ "@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.14", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw=="],
+
+ "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.8.0", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.18.0", "tslib": "^2.6.2" } }, "sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA=="],
+
+ "@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
+
+ "@smithy/node-http-handler": ["@smithy/node-http-handler@4.12.1", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.18.0", "tslib": "^2.6.2" } }, "sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw=="],
+
+ "@smithy/signature-v4": ["@smithy/signature-v4@5.7.4", "", { "dependencies": { "@smithy/core": "^3.35.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ=="],
+
+ "@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="],
+
+ "@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.5.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "tslib": "^2.6.2" } }, "sha512-nxu3SgmAw9JXT2CtkU0m/XNLWpP9MsaBx1zAGAypCbYj15tIFlmcYwpF+Oh18le83d+IM9PT7ENdXnE4C+d5mA=="],
+
+ "@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.5.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "tslib": "^2.6.2" } }, "sha512-iq+cW3mAb7vfcxEEpYi3zXKpDtbrIFyanWjQl4zBq4seWD4OSxXDWSfespZxenX6aEaighn+NR3u1nU1DSvs3w=="],
+
+ "@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
+
+ "@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="],
+
+ "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
+
+ "@tufjs/canonical-json": ["@tufjs/canonical-json@2.0.0", "", {}, "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA=="],
+
+ "@tufjs/models": ["@tufjs/models@4.1.0", "", { "dependencies": { "@tufjs/canonical-json": "2.0.0", "minimatch": "^10.1.1" } }, "sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww=="],
+
+ "@types/node": ["@types/node@26.6.5", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg=="],
+
+ "@types/ws": ["@types/ws@8.18.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-67MQl+fpWKVTT1NYdnmo3U4sc/xPo/zQBncVnI74qmQa0z/b+1g6iYqNmGCPbxO+zz2aklb08a0oHfegiVd0/w=="],
+
+ "@yuuang/ffi-rs-android-arm64": ["@yuuang/ffi-rs-android-arm64@1.3.7", "", { "os": "android", "cpu": "arm64" }, "sha512-t6Wx3Xll6c07Nuk0k3xnZsxKFxlshm92i0U/BiTHc6kQbvu+fMJF+gKsj4yEj886jH51CM3EqZT9Xdhq9CdUVw=="],
+
+ "@yuuang/ffi-rs-darwin-arm64": ["@yuuang/ffi-rs-darwin-arm64@1.3.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-OueBlUFBT9IwD9pQnoYs0UszRBEySskfrEPXXfvKfGjL/DXnfn6kUheQ3oIP6sSmshVGNQUwrTCPo6feAa4QjA=="],
+
+ "@yuuang/ffi-rs-darwin-x64": ["@yuuang/ffi-rs-darwin-x64@1.3.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-x4mxXOKwSgwYx6OBAKrW+Ocp8um2KoPpUwaD0Z2tmR6EekoRvFGN7s/eEK1avqCxcsLN7/MzGOnCCFyGpOVfPA=="],
+
+ "@yuuang/ffi-rs-linux-arm-gnueabihf": ["@yuuang/ffi-rs-linux-arm-gnueabihf@1.3.7", "", { "os": "linux", "cpu": "arm" }, "sha512-9r//Z022QYVoIAY458Dpk8HWkisk/y4NJC3d52RlkdsLD+9p0zn0cKKSoMF/xUsOWDFSmOf+cwU/Xadi/wxFMQ=="],
+
+ "@yuuang/ffi-rs-linux-arm64-gnu": ["@yuuang/ffi-rs-linux-arm64-gnu@1.3.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-nrW4MlFlyQInfxTsI5wtcIpbV4KlHLWo2BCk4CHIETlUT+PUspNFqDg+Byy7My3hpCZlXp0WsMVP/3N0LlybNw=="],
+
+ "@yuuang/ffi-rs-linux-arm64-musl": ["@yuuang/ffi-rs-linux-arm64-musl@1.3.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-eLLx4P8DzNnuPif19nAvKPaf88lSf6KFbDetH3nAIRZvI495h0jigN835Ayr7uAADssohGfO9MPMH33DXmFErA=="],
+
+ "@yuuang/ffi-rs-linux-x64-gnu": ["@yuuang/ffi-rs-linux-x64-gnu@1.3.7", "", { "os": "linux", "cpu": "x64" }, "sha512-upEz1Q98T51x2In872fsjgHCJbE3e8r9JxhJD+5NvSMk6BpieYWRodTbsMxV6oKWmfl12loKwF2j4OBSWKl0lA=="],
+
+ "@yuuang/ffi-rs-linux-x64-musl": ["@yuuang/ffi-rs-linux-x64-musl@1.3.7", "", { "os": "linux", "cpu": "x64" }, "sha512-YOcOkIwVvIhpwuGJ428h15vpE0KnT3teFG0g2J31FrBxrVh9IH4HeTGXjelI3l9SI5gyiZDnD4PQnQ1U6oqTcA=="],
+
+ "@yuuang/ffi-rs-win32-arm64-msvc": ["@yuuang/ffi-rs-win32-arm64-msvc@1.3.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-ZfYzLV1w7Mhzh9XRScEMpw9oazIEiwO+0+TYHHRL882d5oD0Q99YikmRhdifsoHCCQefKmWF/4BXCeFkzg8NFA=="],
+
+ "@yuuang/ffi-rs-win32-ia32-msvc": ["@yuuang/ffi-rs-win32-ia32-msvc@1.3.7", "", { "os": "win32", "cpu": [ "x64", "ia32", ] }, "sha512-ifYz+f+giKpT39lKWDru+om6QnA9J7J953ns60xHgB46ESZvPd/Y2W5+PesRNC9U+vnFEiLZ9CNC+R3fBWC1pg=="],
+
+ "@yuuang/ffi-rs-win32-x64-msvc": ["@yuuang/ffi-rs-win32-x64-msvc@1.3.7", "", { "os": "win32", "cpu": "x64" }, "sha512-H3s4wOLtZtKtmuPjUNE+bhnSKxxpXcuxG70jRC8dUMHqIm5iCvdoZ4XYDeq7/uUYlySerXFmRL4dAVCX/TUNTg=="],
+
+ "abbrev": ["abbrev@4.0.0", "", {}, "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA=="],
+
+ "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
+
+ "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
+
+ "ansi-regex": ["ansi-regex@6.4.0", "", {}, "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw=="],
+
+ "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
+
+ "argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
+
+ "aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="],
+
+ "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
+
+ "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
+
+ "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
+
+ "bin-links": ["bin-links@6.0.2", "", { "dependencies": { "cmd-shim": "^8.0.0", "npm-normalize-package-bin": "^5.0.0", "proc-log": "^6.0.0", "read-cmd-shim": "^6.0.0", "write-file-atomic": "^7.0.0" } }, "sha512-frE1t78WOwJ45PKV2cF2tNPjTcs9L1J9s6VkrV59wanRP4GlaomuxYPVma7BwthMg8WnfSory4w5PTE6FZZ81w=="],
+
+ "bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="],
+
+ "brace-expansion": ["brace-expansion@5.0.12", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ=="],
+
+ "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
+
+ "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
+
+ "bun-ffi-structs": ["bun-ffi-structs@0.3.1", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-3gM7PpVWLyrwxWjcilSiGuhWanhZivvo6l0u573NziPH6f/gwk6McbaYgn7oJWov6pKGRTDbrg94W5DcJsKTtQ=="],
+
+ "bun-pty": ["bun-pty@0.4.11", "", {}, "sha512-DpBfXCoWbcDziqL4gT3sFw/O6vfe2Gm1LmsnjfGjpqNhmsPjBcVorTrRv7OVATsy88PiXfn486vp0w4x3/Q84g=="],
+
+ "bundle-name": ["bundle-name@4.1.1", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-DdH81/zPLVS11EUgWq3tEu/xn+EzljlMYooDNdzWEnFha3R3NBMpMV1UqYIpjYHV/SgpFKMIX1Oh7o06SjM/oA=="],
+
+ "cacache": ["cacache@20.0.4", "", { "dependencies": { "@npmcli/fs": "^5.0.0", "fs-minipass": "^3.0.0", "glob": "^13.0.0", "lru-cache": "^11.1.0", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^7.0.2", "ssri": "^13.0.0" } }, "sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA=="],
+
+ "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
+
+ "ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="],
+
+ "cmd-shim": ["cmd-shim@8.0.0", "", {}, "sha512-Jk/BK6NCapZ58BKUxlSI+ouKRbjH1NLZCgJkYoab+vEHUY3f6OzpNBN9u7HFSv9J6TRDGs4PLOHezoKGaFRSCA=="],
+
+ "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
+
+ "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
+
+ "common-ancestor-path": ["common-ancestor-path@2.0.0", "", {}, "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng=="],
+
+ "content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
+
+ "cross-fetch": ["cross-fetch@3.2.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q=="],
+
+ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
+
+ "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
+
+ "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
+
+ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+
+ "default-browser": ["default-browser@5.5.1", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw=="],
+
+ "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="],
+
+ "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="],
+
+ "detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
+
+ "diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="],
+
+ "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
+
+ "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
+
+ "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
+
+ "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
+
+ "drizzle-orm": ["drizzle-orm@1.0.0-rc.5-169397b", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@drizzle-team/minipg": ">=0.3.2", "@effect/sql-d1": ">=4.0.0-beta.105 || >=4.0.0", "@effect/sql-libsql": ">=4.0.0-beta.105 || >=4.0.0", "@effect/sql-mysql2": ">=4.0.0-beta.105 || >=4.0.0", "@effect/sql-pg": ">=4.0.0-beta.105 || >=4.0.0", "@effect/sql-pglite": ">=4.0.0-beta.105 || >=4.0.0", "@effect/sql-sqlite-bun": ">=4.0.0-beta.105 || >=4.0.0", "@effect/sql-sqlite-do": ">=4.0.0-beta.105 || >=4.0.0", "@effect/sql-sqlite-node": ">=4.0.0-beta.105 || >=4.0.0", "@effect/sql-sqlite-wasm": ">=4.0.0-beta.105 || >=4.0.0", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=17", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@sinclair/typebox": ">=0.34.8", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.7.1", "@tursodatabase/database-common": ">=0.7.1", "@tursodatabase/database-wasm": ">=0.7.1", "@tursodatabase/serverless": ">=1.4.0", "@tursodatabase/sync": ">=0.7.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "arktype": ">=2.0.0", "better-sqlite3": ">=9.3.0", "bun-types": "*", "effect": ">=4.0.0-beta.105 || >=4.0.0", "expo-sqlite": ">=14.0.0", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5", "typebox": ">=1.2.0", "valibot": ">=1.0.0-beta.7", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@drizzle-team/minipg", "@effect/sql-d1", "@effect/sql-libsql", "@effect/sql-mysql2", "@effect/sql-pg", "@effect/sql-pglite", "@effect/sql-sqlite-bun", "@effect/sql-sqlite-do", "@effect/sql-sqlite-node", "@effect/sql-sqlite-wasm", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@sinclair/typebox", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@tursodatabase/serverless", "@tursodatabase/sync", "@types/better-sqlite3", "@types/mssql", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "arktype", "better-sqlite3", "bun-types", "effect", "expo-sqlite", "mssql", "mysql2", "pg", "postgres", "sql.js", "sqlite3", "typebox", "valibot", "zod"] }, "sha512-xf5TudQhFplMMzjwgmMY5f64Sqbv4iwXxdTHBHqvLMt+SHyY8rAYhzNLQchEkDxiqMclTS/txPEOg5pdQy0Tcw=="],
+
+ "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
+
+ "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
+
+ "effect": ["effect@4.0.1", "", {}, "sha512-b1VlQG9g8fwxE5QnIZuPoOP/0MsqHJSRKxUijjoM80E5q95FXrX5yWtY5XI8G+GJsBQAVSDbLS458z9a++uVvw=="],
+
+ "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
+
+ "engine.io-client": ["engine.io-client@6.6.7", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-parser": "~5.2.1", "ws": "~8.21.0", "xmlhttprequest-ssl": "~2.1.1" } }, "sha512-GyAvmPA7gnRCpDQbYACAy79Lg4ewevrmaPzySZz0OVvgyZc3gEfLLqPTYk8wVBScjOEpM9ZO4KryyFpVItjGEA=="],
+
+ "engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="],
+
+ "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
+
+ "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
+
+ "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
+
+ "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
+
+ "eventsource-parser": ["eventsource-parser@3.1.1", "", {}, "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ=="],
+
+ "exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="],
+
+ "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
+
+ "extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="],
+
+ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
+
+ "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
+
+ "ffi-rs": ["ffi-rs@1.3.7", "", { "optionalDependencies": { "@yuuang/ffi-rs-android-arm64": "1.3.7", "@yuuang/ffi-rs-darwin-arm64": "1.3.7", "@yuuang/ffi-rs-darwin-x64": "1.3.7", "@yuuang/ffi-rs-linux-arm-gnueabihf": "1.3.7", "@yuuang/ffi-rs-linux-arm64-gnu": "1.3.7", "@yuuang/ffi-rs-linux-arm64-musl": "1.3.7", "@yuuang/ffi-rs-linux-x64-gnu": "1.3.7", "@yuuang/ffi-rs-linux-x64-musl": "1.3.7", "@yuuang/ffi-rs-win32-arm64-msvc": "1.3.7", "@yuuang/ffi-rs-win32-ia32-msvc": "1.3.7", "@yuuang/ffi-rs-win32-x64-msvc": "1.3.7" } }, "sha512-5MMZQS2t/6f/ec3sx0f51gBcKGZPGfolPAW04lvLOSYnr7gmOKs8YzH1Rh+34WGO+VtcgcTuxaUUoJLcR4Tirg=="],
+
+ "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
+
+ "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
+
+ "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
+
+ "fs-minipass": ["fs-minipass@3.0.3", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw=="],
+
+ "fuzzysort": ["fuzzysort@3.1.0", "", {}, "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ=="],
+
+ "gaxios": ["gaxios@7.3.1", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ=="],
+
+ "gcp-metadata": ["gcp-metadata@8.1.4", "", { "dependencies": { "gaxios": "7.1.3", "google-logging-utils": "1.1.3", "json-bigint": "^1.0.0" } }, "sha512-iJ9KMsiu+xKtNRX0PmGLSaIU3bUBAyzWTyqKemKPzNPsmmsBCQYmlNg+brEbES7IHSXtdVwzBPzx1vz3FAaipw=="],
+
+ "get-east-asian-width": ["get-east-asian-width@1.7.0", "", {}, "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q=="],
+
+ "gitlab-ai-provider": ["gitlab-ai-provider@6.19.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-ym+p6PxKvCKfBE3VKYTiacJwsmgq/DrOuGctpBnbIBekdnZT31RM6yTTNdzVS83K3+Ym4M+qNY34HEcTVM6EEQ=="],
+
+ "glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="],
+
+ "google-auth-library": ["google-auth-library@10.5.0", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.0.0", "gcp-metadata": "^8.0.0", "google-logging-utils": "^1.0.0", "gtoken": "^8.0.0", "jws": "^4.0.0" } }, "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w=="],
+
+ "google-logging-utils": ["google-logging-utils@1.2.0", "", {}, "sha512-WE9av4wKDZgRjBwgVUabocx8T6/7o3Ca1Fat46FXDhXVAFibzNadedcOXrdgd1Kzmk8tsk/9ZH89Wyf/SqeZ3A=="],
+
+ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
+
+ "graphql": ["graphql@16.14.2", "", {}, "sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA=="],
+
+ "graphql-request": ["graphql-request@6.1.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.2.0", "cross-fetch": "^3.1.5" }, "peerDependencies": { "graphql": "14 - 16" } }, "sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw=="],
+
+ "gray-matter": ["gray-matter@4.0.3", "", { "dependencies": { "js-yaml": "^3.13.1", "kind-of": "^6.0.2", "section-matter": "^1.0.0", "strip-bom-string": "^1.0.0" } }, "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q=="],
+
+ "gtoken": ["gtoken@8.0.0", "", { "dependencies": { "gaxios": "^7.0.0", "jws": "^4.0.0" } }, "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw=="],
+
+ "hosted-git-info": ["hosted-git-info@9.0.3", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg=="],
+
+ "htmlparser2": ["htmlparser2@8.0.2", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1", "entities": "^4.4.0" } }, "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA=="],
+
+ "http-cache-semantics": ["http-cache-semantics@4.3.0", "", {}, "sha512-M5t5LlJpS1UHMjvwRQVdFHvPISGeLAxNcrWuJkeGh0KxsqCHZ1O3NXZU/8x7cD0BDcGW8kapxMKTvwlqrNkHkA=="],
+
+ "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
+
+ "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
+
+ "iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="],
+
+ "ignore-walk": ["ignore-walk@8.0.0", "", { "dependencies": { "minimatch": "^10.0.3" } }, "sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A=="],
+
+ "immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="],
+
+ "ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="],
+
+ "ip-address": ["ip-address@10.7.5", "", {}, "sha512-t2v0rkILYv9WXmSL7TpAaU3HXgaFYOPUcH3QHgwQ++gpJuH3bqyMy4Fvt/DJCTKwzH3Cf+bAp5W/mFL39KPLzA=="],
+
+ "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
+
+ "is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="],
+
+ "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
+
+ "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
+
+ "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
+
+ "is-in-ssh": ["is-in-ssh@1.0.0", "", {}, "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw=="],
+
+ "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="],
+
+ "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
+
+ "is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="],
+
+ "isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="],
+
+ "isomorphic-ws": ["isomorphic-ws@5.0.0", "", { "peerDependencies": { "ws": "*" } }, "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw=="],
+
+ "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
+
+ "jose": ["jose@6.0.11", "", {}, "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg=="],
+
+ "js-yaml": ["js-yaml@3.15.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w=="],
+
+ "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="],
+
+ "json-parse-even-better-errors": ["json-parse-even-better-errors@5.0.0", "", {}, "sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ=="],
+
+ "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
+
+ "json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="],
+
+ "json-stringify-nice": ["json-stringify-nice@1.1.4", "", {}, "sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw=="],
+
+ "jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
+
+ "jsonparse": ["jsonparse@1.3.1", "", {}, "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg=="],
+
+ "just-diff": ["just-diff@6.0.2", "", {}, "sha512-S59eriX5u3/QhMNq3v/gm8Kd0w8OS6Tz2FS1NG4blv+z0MuQcBRJyFWjdovM0Rad4/P4aUPFtnkNjMjyMlMSYA=="],
+
+ "just-diff-apply": ["just-diff-apply@5.5.0", "", {}, "sha512-OYTthRfSh55WOItVqwpefPtNt2VdKsq5AnAK6apdtR6yCH8pr0CmSr710J0Mf+WdQy7K/OzMy7K2MgAfdQURDw=="],
+
+ "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
+
+ "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="],
+
+ "kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
+
+ "lru-cache": ["lru-cache@11.5.3", "", {}, "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg=="],
+
+ "make-fetch-happen": ["make-fetch-happen@15.0.6", "", { "dependencies": { "@gar/promise-retry": "^1.0.0", "@npmcli/agent": "^4.0.0", "@npmcli/redact": "^4.0.0", "cacache": "^20.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", "minipass-fetch": "^5.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^6.0.0", "ssri": "^13.0.0" } }, "sha512-Je0fLJ0F5atA7F+eIlLzk+Wkcl57JDf4kf+EW8xiP5E31xOQxkIxTbgf1Oi1Lw9tRI9UEMRdI5Vz2xTzoNU1Jw=="],
+
+ "marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="],
+
+ "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
+
+ "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
+
+ "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
+
+ "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
+
+ "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
+
+ "minipass-collect": ["minipass-collect@2.0.1", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw=="],
+
+ "minipass-fetch": ["minipass-fetch@5.0.2", "", { "dependencies": { "minipass": "^7.0.3", "minipass-sized": "^2.0.0", "minizlib": "^3.0.1" }, "optionalDependencies": { "iconv-lite": "^0.7.2" } }, "sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ=="],
+
+ "minipass-flush": ["minipass-flush@1.0.7", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA=="],
+
+ "minipass-pipeline": ["minipass-pipeline@1.2.4", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A=="],
+
+ "minipass-sized": ["minipass-sized@2.0.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA=="],
+
+ "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="],
+
+ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+
+ "nanoevents": ["nanoevents@7.0.1", "", {}, "sha512-o6lpKiCxLeijK4hgsqfR6CNToPyRU3keKyyI6uwuHRvpRTbZ0wXw51WRgyldVugZqoJfkGFrjrIenYH3bfEO3Q=="],
+
+ "negotiator": ["negotiator@1.1.0", "", { "dependencies": { "content-type": "^2.1.0" } }, "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg=="],
+
+ "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
+
+ "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
+
+ "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
+
+ "node-gyp": ["node-gyp@12.4.0", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", "nopt": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", "tar": "^7.5.4", "tinyglobby": "^0.2.12", "undici": "^6.25.0", "which": "^6.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw=="],
+
+ "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="],
+
+ "nopt": ["nopt@9.0.0", "", { "dependencies": { "abbrev": "^4.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw=="],
+
+ "npm-bundled": ["npm-bundled@5.0.0", "", { "dependencies": { "npm-normalize-package-bin": "^5.0.0" } }, "sha512-JLSpbzh6UUXIEoqPsYBvVNVmyrjVZ1fzEFbqxKkTJQkWBO3xFzFT+KDnSKQWwOQNbuWRwt5LSD6HOTLGIWzfrw=="],
+
+ "npm-install-checks": ["npm-install-checks@8.0.0", "", { "dependencies": { "semver": "^7.1.1" } }, "sha512-ScAUdMpyzkbpxoNekQ3tNRdFI8SJ86wgKZSQZdUxT+bj0wVFpsEMWnkXP0twVe1gJyNF5apBWDJhhIbgrIViRA=="],
+
+ "npm-normalize-package-bin": ["npm-normalize-package-bin@5.0.0", "", {}, "sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag=="],
+
+ "npm-package-arg": ["npm-package-arg@13.0.2", "", { "dependencies": { "hosted-git-info": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", "validate-npm-package-name": "^7.0.0" } }, "sha512-IciCE3SY3uE84Ld8WZU23gAPPV9rIYod4F+rc+vJ7h7cwAJt9Vk6TVsK60ry7Uj3SRS3bqRRIGuTp9YVlk6WNA=="],
+
+ "npm-packlist": ["npm-packlist@10.0.4", "", { "dependencies": { "ignore-walk": "^8.0.0", "proc-log": "^6.0.0" } }, "sha512-uMW73iajD8hiH4ZBxEV3HC+eTnppIqwakjOYuvgddnalIw2lJguKviK1pcUJDlIWm1wSJkchpDZDSVVsZEYRng=="],
+
+ "npm-pick-manifest": ["npm-pick-manifest@11.0.3", "", { "dependencies": { "npm-install-checks": "^8.0.0", "npm-normalize-package-bin": "^5.0.0", "npm-package-arg": "^13.0.0", "semver": "^7.3.5" } }, "sha512-buzyCfeoGY/PxKqmBqn1IUJrZnUi1VVJTdSSRPGI60tJdUhUoSQFhs0zycJokDdOznQentgrpf8LayEHyyYlqQ=="],
+
+ "npm-registry-fetch": ["npm-registry-fetch@19.1.1", "", { "dependencies": { "@npmcli/redact": "^4.0.0", "jsonparse": "^1.3.1", "make-fetch-happen": "^15.0.0", "minipass": "^7.0.2", "minipass-fetch": "^5.0.0", "minizlib": "^3.0.1", "npm-package-arg": "^13.0.0", "proc-log": "^6.0.0" } }, "sha512-TakBap6OM1w0H73VZVDf44iFXsOS3h+L4wVMXmbWOQroZgFhMch0juN6XSzBNlD965yIKvWg2dfu7NSiaYLxtw=="],
+
+ "open": ["open@11.0.4", "", { "dependencies": { "default-browser": "^5.5.1", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.2.1", "wsl-utils": "^1.0.0" } }, "sha512-++Zlftm0kVLPmzC06t6epuWmcRMDbI4z5P3NNX979WA/k23+NtSOynEGzsVfZwguKw2mi5umVgnBlJQMwRz4Pg=="],
+
+ "openai": ["openai@6.49.0", "", { "peerDependencies": { "@aws-sdk/credential-provider-node": ">=3.972.0 <4", "@smithy/hash-node": ">=4.3.0 <5", "@smithy/signature-v4": ">=5.4.0 <6", "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["@aws-sdk/credential-provider-node", "@smithy/hash-node", "@smithy/signature-v4", "ws", "zod"] }, "sha512-aYCc0C6L864eR6WSYIwQGyXriw/nIyZx0ObvhzOEVuk0zoBDpynjSbrionWI7q65B5H8jJX0DXR9snEzM6bfPg=="],
+
+ "opencode-ai": ["opencode-ai@1.19.4", "", { "optionalDependencies": { "opencode-darwin-arm64": "1.19.4", "opencode-darwin-x64": "1.19.4", "opencode-darwin-x64-baseline": "1.19.4", "opencode-linux-arm64": "1.19.4", "opencode-linux-arm64-musl": "1.19.4", "opencode-linux-x64": "1.19.4", "opencode-linux-x64-baseline": "1.19.4", "opencode-linux-x64-baseline-musl": "1.19.4", "opencode-linux-x64-musl": "1.19.4", "opencode-windows-arm64": "1.19.4", "opencode-windows-x64": "1.19.4", "opencode-windows-x64-baseline": "1.19.4" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "opencode": "bin/opencode.exe" } }, "sha512-O3rTc9OCd6CeeJohCAjFYD3MX5U0weKzKnySfsJ9sw9uAbNZZK4meIcjd+7k3inmZB5G2qONSen3yA45yECiFA=="],
+
+ "opencode-darwin-arm64": ["opencode-darwin-arm64@1.19.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-31sy6eA88QO2vXra395rPxe995a2G0IKKAqm7B/ZESwIGVK9auMdYfKd7mf5icI72oFMLUwRJGQ1f4B/ykbDJg=="],
+
+ "opencode-darwin-x64": ["opencode-darwin-x64@1.19.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ILgql0oUVdZeWnWVGsv5by/gPeY9ytYS5KTCQSoxgo0H1TzbnNMSd+IAn9dHL1H4XSZBtgPTfdMK9r1sWPbYbA=="],
+
+ "opencode-darwin-x64-baseline": ["opencode-darwin-x64-baseline@1.19.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-hy8iFxdW2CFbr842M8uzCO1bgCAEm6UIRgtSJKj19+a4UtZyS+PwbAat2R2rwIx+O6PqavzDas5DNE+dz0+K3A=="],
+
+ "opencode-linux-arm64": ["opencode-linux-arm64@1.19.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-bA/QpgwRgv20xVay0b4EGo3IjMQQl4GbODjhXWGUvovoJYYyFosueox2aLASxTbHK6oP1zpUxaZRFhgNrhhfgA=="],
+
+ "opencode-linux-arm64-musl": ["opencode-linux-arm64-musl@1.19.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-Sn41KddzgT+UBXtwuOR7/4X4ww8xOIEaw1bS74X0cO+BBgj7EY28gSRN4DNSJatXdDRJhDvhmNebWxly/cfYCw=="],
+
+ "opencode-linux-x64": ["opencode-linux-x64@1.19.4", "", { "os": "linux", "cpu": "x64" }, "sha512-rQ/l3jmJoxf8ZYnBargg9alURlz6buEilysGODtzkcmcYtcZ91LOgF034oUUHHQl8/y1hbRTDvg6ujqUjQsMXw=="],
+
+ "opencode-linux-x64-baseline": ["opencode-linux-x64-baseline@1.19.4", "", { "os": "linux", "cpu": "x64" }, "sha512-oU1Plu0b4x8DRdUHxjnEdQ/IR01THw6teGf3blNysJHKS5fqXztsHiFiy6sXD1Isne+1Oa7Wi0sysfXEaBrudQ=="],
+
+ "opencode-linux-x64-baseline-musl": ["opencode-linux-x64-baseline-musl@1.19.4", "", { "os": "linux", "cpu": "x64" }, "sha512-HxG4Y5XlpyQ64tlbQ3PMTwtIXlOLEuhI2VZ9CfKdAsSvsfo2Nh2w0FzS1esuLXNvdaDaO1CWZ3p7wk1JrerSkg=="],
+
+ "opencode-linux-x64-musl": ["opencode-linux-x64-musl@1.19.4", "", { "os": "linux", "cpu": "x64" }, "sha512-SW/DNSbM2fjLN2q1IfPHgWTNSpQ8TgXjRUVNLc1VWlgKROfizpPTtWmjGWmxie2nZVuBcSVw/RlQE3ulgA8vVA=="],
+
+ "opencode-windows-arm64": ["opencode-windows-arm64@1.19.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-4rY75jyh4A/q0eIWLzo3g4yigWceRLdFUpb49hHeGFJgEXjNIWWT0P+OUzYcA3EQeJtTyRcIlLOjFqr+AeMi1Q=="],
+
+ "opencode-windows-x64": ["opencode-windows-x64@1.19.4", "", { "os": "win32", "cpu": "x64" }, "sha512-Rr5k+esRBsEBGCjqjVwvFpybQMmDO18Hu5vlBWGj4m0aMMoeR/m+qcW/tJGIKhQXy7Mu31izTn60LXjDMwfFAg=="],
+
+ "opencode-windows-x64-baseline": ["opencode-windows-x64-baseline@1.19.4", "", { "os": "win32", "cpu": "x64" }, "sha512-7CPsIyrI6Jzef8e0YlCHQ3HgmNADSTZWgzfS2e/P8LScMG7D5xVFro/2pHkE+VCjZ42NGs1ZdMnSdfsKpx6h8g=="],
+
+ "p-map": ["p-map@7.1.1", "", {}, "sha512-6Lh3jQPLthYujSrlw6ccWXBGZTkoTCz27Hd92g79LH/kjAbgoEu0t442cCCNbegPSJsA0lwDX8z0bcKtkpA32g=="],
+
+ "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
+
+ "pacote": ["pacote@21.5.1", "", { "dependencies": { "@gar/promise-retry": "^1.0.0", "@npmcli/git": "^7.0.0", "@npmcli/installed-package-contents": "^4.0.0", "@npmcli/package-json": "^7.0.0", "@npmcli/promise-spawn": "^9.0.0", "@npmcli/run-script": "^10.0.0", "cacache": "^20.0.0", "fs-minipass": "^3.0.0", "minipass": "^7.0.2", "npm-package-arg": "^13.0.0", "npm-packlist": "^10.0.1", "npm-pick-manifest": "^11.0.1", "npm-registry-fetch": "^19.0.0", "proc-log": "^6.0.0", "sigstore": "^4.0.0", "ssri": "^13.0.0", "tar": "^7.4.3" }, "bin": { "pacote": "bin/index.js" } }, "sha512-KvcJ9iy3crysCsgqc4+PknH/w6jkrp8JN36mpZBPwNaDRwTfMZD37YzRazNstiZUOhuF5pno9f78n9mEJBavwg=="],
+
+ "parse-conflict-json": ["parse-conflict-json@5.0.1", "", { "dependencies": { "json-parse-even-better-errors": "^5.0.0", "just-diff": "^6.0.0", "just-diff-apply": "^5.2.0" } }, "sha512-ZHEmNKMq1wyJXNwLxyHnluPfRAFSIliBvbK/UiOceROt4Xh9Pz0fq49NytIaeaCUf5VR86hwQ/34FCcNU5/LKQ=="],
+
+ "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
+
+ "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="],
+
+ "picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
+
+ "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
+
+ "postcss-selector-parser": ["postcss-selector-parser@7.1.7", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-wWBro/7hJ40ye6TPiXkUQoXKCw23xSsjriUvqd7Uls9Sso3oLiON1flc18cCdnjgCRXw4fBVqUJJeOdDTh3P3g=="],
+
+ "powershell-utils": ["powershell-utils@0.2.1", "", {}, "sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A=="],
+
+ "proc-log": ["proc-log@6.1.0", "", {}, "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ=="],
+
+ "proggy": ["proggy@4.0.0", "", {}, "sha512-MbA4R+WQT76ZBm/5JUpV9yqcJt92175+Y0Bodg3HgiXzrmKu7Ggq+bpn6y6wHH+gN9NcyKn3yg1+d47VaKwNAQ=="],
+
+ "promise-all-reject-late": ["promise-all-reject-late@1.0.1", "", {}, "sha512-vuf0Lf0lOxyQREH7GDIOUMLS7kz+gs8i6B+Yi8dC68a2sychGrHTJYghMBD6k7eUcH0H5P73EckCA48xijWqXw=="],
+
+ "promise-call-limit": ["promise-call-limit@3.0.2", "", {}, "sha512-mRPQO2T1QQVw11E7+UdCJu7S61eJVWknzml9sC1heAdj1jxl0fWMBypIt9ZOcLFf8FkG995ZD7RnVk7HH72fZw=="],
+
+ "read-cmd-shim": ["read-cmd-shim@6.0.0", "", {}, "sha512-1zM5HuOfagXCBWMN83fuFI/x+T/UhZ7k+KIzhrHXcQoeX5+7gmaDYjELQHmmzIodumBHeByBJT4QYS7ufAgs7A=="],
+
+ "redis": ["redis@6.3.0", "", { "dependencies": { "@redis/bloom": "6.3.0", "@redis/client": "6.3.0", "@redis/json": "6.3.0", "@redis/search": "6.3.0", "@redis/time-series": "6.3.0" } }, "sha512-XFQbPie1lGpKeUZ8ySYY43yLnQy/iIQtHNvIr/1XdaABpb96rUSdm3Mfl+98FEVbv9ZJIzINMno5s0Nh8E5LEw=="],
+
+ "rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="],
+
+ "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="],
+
+ "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
+
+ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
+
+ "section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="],
+
+ "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
+
+ "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=="],
+
+ "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
+
+ "sigstore": ["sigstore@4.1.1", "", { "dependencies": { "@sigstore/bundle": "^4.0.0", "@sigstore/core": "^3.2.1", "@sigstore/protobuf-specs": "^0.5.0", "@sigstore/sign": "^4.1.1", "@sigstore/tuf": "^4.0.2", "@sigstore/verify": "^3.1.1" } }, "sha512-endqECJkfhozrXMK5ngu/UAA0xVcVEFdnHJCElGaExypjW+HK5i6zu3NteLoaX/iFbRUbC3+DjttQs0GARr+5w=="],
+
+ "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="],
+
+ "socket.io-client": ["socket.io-client@4.8.4", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-client": "~6.6.1", "socket.io-parser": "~4.2.4" } }, "sha512-7wjKAvVXYOJsbmUgkWbnhPD4bFpkqrUR0++XOjpGFmqDqsqV1CI93VTraeExLxpjxEqrWgFoSRZu1ihjLWfkAA=="],
+
+ "socket.io-parser": ["socket.io-parser@4.2.7", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1" } }, "sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg=="],
+
+ "socks": ["socks@2.8.10", "", { "dependencies": { "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" } }, "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ=="],
+
+ "socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="],
+
+ "spdx-exceptions": ["spdx-exceptions@2.5.0", "", {}, "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w=="],
+
+ "spdx-expression-parse": ["spdx-expression-parse@4.0.0", "", { "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ=="],
+
+ "spdx-license-ids": ["spdx-license-ids@3.0.24", "", {}, "sha512-cLS9TtWkIQFyLkJ3/5aFQAOHOSKTlOs/7WDut/XPSdjom1fJhUdkepeJAKXa9Y05+FabHd0sti+Et559vDtkpQ=="],
+
+ "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
+
+ "ssri": ["ssri@13.0.1", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ=="],
+
+ "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
+
+ "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
+
+ "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
+
+ "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
+ "strip-bom-string": ["strip-bom-string@1.0.0", "", {}, "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g=="],
+
+ "tar": ["tar@7.5.22", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA=="],
+
+ "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
+
+ "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
+
+ "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
+
+ "tree-sitter-bash": ["tree-sitter-bash@0.25.0", "", { "dependencies": { "node-addon-api": "^8.2.1", "node-gyp-build": "^4.8.2" }, "peerDependencies": { "tree-sitter": "^0.25.0" }, "optionalPeers": ["tree-sitter"] }, "sha512-gZtlj9+qFS81qKxpLfD6H0UssQ3QBc/F0nKkPsiFDyfQF2YBqYvglFJUzchrPpVhZe9kLZTrJ9n2J6lmka69Vg=="],
+
+ "tree-sitter-powershell": ["tree-sitter-powershell@0.25.10", "", { "dependencies": { "node-addon-api": "^7.1.0", "node-gyp-build": "^4.8.0" }, "peerDependencies": { "tree-sitter": "^0.25.0" }, "optionalPeers": ["tree-sitter"] }, "sha512-bEt8QoySpGFnU3aa8WedQyNMaN6aTwy/WUbvIVt0JSKF+BbJoSHNHu+wCbhj7xLMsfB0AuffmiJm+B8gzva8Lg=="],
+
+ "treeverse": ["treeverse@3.0.0", "", {}, "sha512-gcANaAnd2QDZFmHFEOF4k7uc1J/6a6z3DJMd/QwEyxLoKGiptJRwid582r7QIsFlFMIZ3SnxfS52S4hm2DHkuQ=="],
+
+ "ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="],
+
+ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+ "tuf-js": ["tuf-js@4.1.0", "", { "dependencies": { "@tufjs/models": "4.1.0", "debug": "^4.4.3", "make-fetch-happen": "^15.0.1" } }, "sha512-50QV99kCKH5P/Vs4E2Gzp7BopNV+KzTXqWeaxrfu5IQJBOULRsTIS9seSsOVT8ZnGXzCyx55nYWAi4qJzpZKEQ=="],
+
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+
+ "undici": ["undici@8.12.3", "", {}, "sha512-+fdndUTwBP86cVjihTp21hZHo7d6oj1c/47XPA3lL8mM0XHs+C4EkcjmgRsFnFk2f/UONGDy9ItBItmXsLOQHw=="],
+
+ "undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="],
+
+ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
+
+ "validate-npm-package-name": ["validate-npm-package-name@7.0.2", "", {}, "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A=="],
+
+ "vscode-jsonrpc": ["vscode-jsonrpc@8.2.1", "", {}, "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ=="],
+
+ "walk-up-path": ["walk-up-path@4.0.0", "", {}, "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A=="],
+
+ "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
+
+ "web-tree-sitter": ["web-tree-sitter@0.25.10", "", { "peerDependencies": { "@types/emscripten": "^1.40.0" }, "optionalPeers": ["@types/emscripten"] }, "sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA=="],
+
+ "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
+
+ "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
+
+ "which": ["which@6.0.1", "", { "dependencies": { "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg=="],
+
+ "wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
+
+ "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
+
+ "write-file-atomic": ["write-file-atomic@7.0.1", "", { "dependencies": { "signal-exit": "^4.0.1" } }, "sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg=="],
+
+ "ws": ["ws@8.22.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg=="],
+
+ "wsl-utils": ["wsl-utils@1.0.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.2.0" } }, "sha512-c16PKLIRxBYpR3jSWsdS7SFp0COm5SOQdiFjY818dYFHJbMGuLBszpF07LBWmOS1uzrHa9VGdynsEgoeKy0oOQ=="],
+
+ "xmlhttprequest-ssl": ["xmlhttprequest-ssl@2.1.2", "", {}, "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ=="],
+
+ "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
+
+ "zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="],
+
+ "@ai-sdk/provider-utils/@ai-sdk/provider": ["@ai-sdk/provider@3.0.12", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-sj9DWTJ2Ze0WR9qsiOPqoqzNx3OxL6iMxHImbhvoe9qOspekbzxNDMiJ4TIGfYHYh9w4OmBjz3prvqhzTi96+Q=="],
+
+ "@aws-crypto/sha256-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
+
+ "@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
+
+ "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
+
+ "@modelcontextprotocol/client/jose": ["jose@6.2.13", "", {}, "sha512-7TIg10ZE1x82YediN+lcrPNvGMKge9yFHQD+Qho/b8y2adfugU3AsvvHfxV0fzyFllT6l9xCjf4RWY9g/Z9PJQ=="],
+
+ "@modelcontextprotocol/client/zod": ["zod@4.6.7", "", {}, "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg=="],
+
+ "@modelcontextprotocol/core/zod": ["zod@4.6.7", "", {}, "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg=="],
+
+ "@opencode/simulation/ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
+
+ "@opentui/core/diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="],
+
+ "cross-fetch/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
+
+ "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
+
+ "engine.io-client/ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="],
+
+ "gcp-metadata/gaxios": ["gaxios@7.1.3", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2", "rimraf": "^5.0.1" } }, "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ=="],
+
+ "gcp-metadata/google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="],
+
+ "gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
+
+ "minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
+
+ "minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
+
+ "node-gyp/undici": ["undici@6.29.1", "", {}, "sha512-EvRlUt/QqAEvXdQeRMYUfwOQsfzpDztykrG2owGEE/rBOBoYv9uF39ZzsNDh4lTFHhTr2XOZjKkYgueD12ZmGA=="],
+
+ "rimraf/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
+
+ "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
+
+ "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
+ "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+
+ "tinyglobby/picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="],
+
+ "tree-sitter-bash/node-addon-api": ["node-addon-api@8.9.2", "", {}, "sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg=="],
+
+ "wrap-ansi/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
+
+ "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
+
+ "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
+
+ "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
+ "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
+
+ "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
+
+ "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
+
+ "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
+
+ "minipass-flush/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
+
+ "minipass-pipeline/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
+
+ "rimraf/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
+
+ "rimraf/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
+
+ "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+
+ "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
+
+ "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+
+ "wrap-ansi/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
+
+ "rimraf/glob/minimatch/brace-expansion": ["brace-expansion@2.1.7", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g=="],
+
+ "rimraf/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
+
+ "rimraf/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
+ }
+}
diff --git a/adapters/opencode-sdk/package.json b/adapters/opencode-sdk/package.json
new file mode 100644
index 00000000..dbc58d2f
--- /dev/null
+++ b/adapters/opencode-sdk/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "scopeweaver-opencode-sdk",
+ "private": true,
+ "version": "0.1.0",
+ "type": "module",
+ "engines": {
+ "bun": ">=1.3.14 <2"
+ },
+ "scripts": {
+ "start": "bun --no-env-file src/server.mjs",
+ "test": "bun test --timeout 30000",
+ "check": "node --check src/server.mjs && node --check src/bridge.mjs && node --check src/worker.mjs && node --check src/native.mjs && node --check src/protocol.mjs && node --check src/legacy.mjs && node --check src/legacy-plugin.mjs && node --check src/native-cli.mjs && node --check src/native-plugin.mjs"
+ },
+ "dependencies": {
+ "@opencode/ai": "2.1.4",
+ "@opencode/client": "2.1.4",
+ "@opencode/core": "2.1.4",
+ "@opencode/plugin": "2.1.4",
+ "@opencode/sdk": "2.1.4",
+ "@opencode/util": "2.1.4",
+ "@opencode-ai/sdk": "1.19.4",
+ "@opencode-ai/plugin": "1.19.4",
+ "opencode-ai": "1.19.4",
+ "effect": "4.0.1",
+ "jsonc-parser": "3.3.1",
+ "@opencode/cli": "2.1.4"
+ },
+ "overrides": {
+ "@ai-sdk/provider-utils": "4.0.33",
+ "effect": "4.0.1"
+ }
+}
diff --git a/adapters/opencode-sdk/src/bridge.mjs b/adapters/opencode-sdk/src/bridge.mjs
new file mode 100644
index 00000000..fb789d3d
--- /dev/null
+++ b/adapters/opencode-sdk/src/bridge.mjs
@@ -0,0 +1,137 @@
+import { spawn } from "node:child_process";
+import { mkdtemp, rm } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { dirname, join } from "node:path";
+import { fileURLToPath } from "node:url";
+import { timingSafeEqual } from "node:crypto";
+import { StringDecoder } from "node:string_decoder";
+import { readFileSync } from "node:fs";
+import { BridgeError, fail, publicError, safeJSON, parseRequest, validateProviderConfig, serializeComplete } from "./protocol.mjs";
+
+// Each worker owns its SDK state, storage, environment, and one model request.
+export const MAX_REQUEST_BYTES=16*1024*1024;
+export function workerEnvironment(root,source=process.env) {
+ const env={PATH:source.PATH??"/usr/bin:/bin",TMPDIR:root,OPENCODE_TEST_HOME:join(root,"home"),XDG_CONFIG_HOME:join(root,"xdg-config"),XDG_DATA_HOME:join(root,"xdg-data"),XDG_CACHE_HOME:join(root,"xdg-cache"),XDG_STATE_HOME:join(root,"xdg-state")};
+ for(const key of ["SystemRoot","WINDIR","LANG","LC_ALL","TZ"])if(source[key])env[key]=source[key];
+ return env;
+}
+export async function executeWorker(input,{signal,onEvent=()=>{},timeout=0}={}) {
+ const root=await mkdtemp(join(tmpdir(),"scopeweaver-opencode-worker-"));
+ const env=workerEnvironment(root);
+ let child,timer,killTimer,canceled=false,timedOut=false;
+ try {
+ return await new Promise((resolve,reject)=>{
+ const grouped=process.platform!=="win32";
+ child=spawn(process.execPath,["--no-env-file",fileURLToPath(new URL("./worker.mjs",import.meta.url))],{cwd:root,env,detached:grouped,stdio:["pipe","pipe","pipe"]});
+ let buffer="",size=0,result,error,pendingStop;const decoder=new StringDecoder("utf8");
+ const kill=signal=>{try{if(grouped&&child.pid)process.kill(-child.pid,signal);else child.kill(signal)}catch{}};
+ const stop=()=>{kill("SIGTERM");killTimer??=setTimeout(()=>kill("SIGKILL"),1500)};
+ const abort=()=>{canceled=true;stop()};signal?.addEventListener("abort",abort,{once:true});
+ if(timeout>0)timer=setTimeout(()=>{timedOut=true;stop()},timeout);
+ if(signal?.aborted)abort();
+ child.stderr.on("data",()=>{}); // Provider errors may contain credentials; never forward stderr.
+ child.stdout.on("data",chunk=>{
+ size+=chunk.length;if(size>20000000){error=new BridgeError("protocol_error",502);stop();return}
+ buffer+=decoder.write(chunk);
+ let index;while((index=buffer.indexOf("\n"))>=0){
+ const line=buffer.slice(0,index);buffer=buffer.slice(index+1);if(!line)continue;
+ try {
+ const value=safeJSON(line,"protocol_error");
+ if(value.kind==="event"){
+ if(value.event.type==="message_stop"){if(pendingStop)throw new BridgeError("protocol_error",502);pendingStop=value.event}
+ else onEvent(value.event);
+ }
+ else if(value.kind==="result")result=value.result;
+ else if(value.kind==="models")result=value.models;
+ else if(value.kind==="error")error=new BridgeError(value.code,({unknown_model:400,unsupported_input:422,invalid_request:400,configuration_error:400,canceled:499,timeout:504,quota_exhausted:402,context_length_exceeded:400,free_tier_restricted:403})[value.code]??502);
+ else throw new BridgeError("protocol_error",502);
+ }catch(e){error=e;stop()}
+ }
+ });
+ child.on("error",()=>{error=new BridgeError("sdk_failure",502)});
+ child.on("close",code=>{
+ signal?.removeEventListener("abort",abort);clearTimeout(timer);clearTimeout(killTimer);
+ kill("SIGKILL"); // An SDK-owned subprocess must not outlive its request worker.
+ if(timedOut)reject(new BridgeError("timeout",504));
+ else if(canceled||signal?.aborted)reject(new BridgeError("canceled",499));
+ else if(error)reject(error);
+ else if(code!==0||result===undefined||(buffer+decoder.end()).trim()||(input.mode!=="models"&&!pendingStop))reject(new BridgeError("sdk_failure",502));
+ else {try{if(pendingStop)onEvent(pendingStop);resolve(result)}catch{reject(new BridgeError("canceled",499))}}
+ });
+ child.stdin.on("error",()=>{});
+ child.stdin.end(JSON.stringify({...input,root}));
+ });
+ } finally {clearTimeout(timer);clearTimeout(killTimer);await rm(root,{recursive:true,force:true})}
+}
+
+const encoder=new TextEncoder();
+const nativeVersionMatches=["@opencode/cli/package.json","@opencode/sdk","@opencode/client","@opencode/plugin","@opencode/ai","@opencode/core/config","@opencode/util/global"].every(id=>{
+ try{
+ let path=dirname(fileURLToPath(import.meta.resolve(id)));
+ const name=id.split("/").slice(0,2).join("/");
+ while(path!==dirname(path)){try{const p=JSON.parse(readFileSync(join(path,"package.json"),"utf8"));if(p.name===name)return p.version==="2.1.4"}catch{}path=dirname(path)}
+ return false;
+ }catch{return false}
+});
+const legacyVersionMatches=["@opencode-ai/sdk/v2","opencode-ai/package.json"].every(id=>{
+ try{
+ let path=dirname(fileURLToPath(import.meta.resolve(id)));
+ const name=id.startsWith("@")?id.split("/").slice(0,2).join("/"):id.split("/")[0];
+ while(path!==dirname(path)){try{const p=JSON.parse(readFileSync(join(path,"package.json"),"utf8"));if(p.name===name)return p.version==="1.19.4"}catch{}path=dirname(path)}
+ return false;
+ }catch{return false}
+});
+const json=(value,status=200)=>new Response(JSON.stringify(value),{status,headers:{"content-type":"application/json","cache-control":"no-store"}});
+async function readBody(req) {
+ if(Number(req.headers.get("content-length"))>MAX_REQUEST_BYTES)fail("body_too_large",413);
+ const reader=req.body?.getReader();if(!reader)fail("invalid_request");
+ const chunks=[];let size=0;
+ try{while(true){const {done,value}=await reader.read();if(done)break;size+=value.byteLength;if(size>MAX_REQUEST_BYTES){await reader.cancel();fail("body_too_large",413)}chunks.push(value)}}finally{reader.releaseLock()}
+ return new TextDecoder("utf-8",{fatal:true}).decode(Buffer.concat(chunks));
+}
+export function createBridge({token,providerConfig,timeout=0,runtime="native",worker=executeWorker}={}) {
+ if(typeof token!=="string"||token.length<32||/[\s\r\n]/.test(token))throw new Error("SCOPEWEAVER_OPENCODE_TOKEN must contain at least 32 non-whitespace characters");
+ if(!Number.isSafeInteger(timeout)||timeout<0||timeout>86400000)throw new Error("Bridge timeout must be 0..86400000 milliseconds");
+ providerConfig=validateProviderConfig(providerConfig);
+ const active=new Map();let closed=false;
+ const authorize=req=>{
+ const u=new URL(req.url);
+ if(req.headers.has("origin")||!["127.0.0.1","localhost","[::1]"].includes(u.hostname)||u.username||u.password||u.search||u.hash)fail("forbidden",403);
+ const supplied=req.headers.get("authorization")??"",expected=`Bearer ${token}`;
+ if(Buffer.byteLength(supplied)!==Buffer.byteLength(expected)||!timingSafeEqual(Buffer.from(supplied),Buffer.from(expected)))fail("unauthorized",401);
+ if(!((runtime==="native"&&nativeVersionMatches&&process.platform!=="win32")||(runtime==="legacy"&&legacyVersionMatches&&process.platform!=="win32")))fail("unsupported_runtime",400);
+ };
+ const controllerFor=req=>{
+ const controller=new AbortController();const abort=()=>controller.abort();req.signal.addEventListener("abort",abort,{once:true});if(req.signal.aborted)abort();
+ return {controller,dispose:()=>req.signal.removeEventListener("abort",abort)};
+ };
+ return {
+ async fetch(req){
+ try {
+ authorize(req);if(closed)fail("canceled",503);
+ const path=new URL(req.url).pathname;
+ if(req.method==="GET"&&path==="/v1/capabilities")return json({protocol:"scopeweaver-opencode-sdk",version:1,sdk_version:runtime==="native"?"2.1.4":"1.19.4",runtime,tool_execution:"capture-only",streaming:true});
+ if(req.method==="DELETE"&&/^\/v1\/requests\/[A-Za-z0-9_-]{1,128}$/.test(path)){active.get(path.slice("/v1/requests/".length))?.abort();return json({aborted:true})}
+ if(req.method==="GET"&&path==="/v1/models"){
+ const {controller,dispose}=controllerFor(req);const key=Symbol();active.set(key,controller);
+ try{return json({models:await worker({mode:"models",runtime,providerConfig},{signal:controller.signal,timeout})})}finally{active.delete(key);dispose()}
+ }
+ if(req.method!=="POST"||!["/v1/complete","/v1/stream"].includes(path))fail("not_found",404);
+ if(!req.headers.get("content-type")?.toLowerCase().startsWith("application/json"))fail("invalid_request",415);
+ const {request,rawHistory}=parseRequest(await readBody(req));
+ if(active.has(request.request_id))fail("duplicate_request",409);
+ const {controller,dispose}=controllerFor(req);active.set(request.request_id,controller);
+ const cleanup=()=>{active.delete(request.request_id);dispose()};
+ if(path==="/v1/complete"){
+ try{const result=await worker({mode:"complete",runtime,request,rawHistory,providerConfig},{signal:controller.signal,timeout});return new Response(serializeComplete(result),{headers:{"content-type":"application/json","cache-control":"no-store"}})}finally{cleanup()}
+ }
+ const stream=new ReadableStream({start(output){
+ void worker({mode:"complete",runtime,request,rawHistory,providerConfig},{signal:controller.signal,timeout,onEvent:event=>output.enqueue(encoder.encode(JSON.stringify(event)+"\n"))})
+ .then(()=>{output.close()},error=>{try{output.enqueue(encoder.encode(JSON.stringify({type:"error",...publicError(error)})+"\n"));output.close()}catch{}}).finally(cleanup);
+ },cancel(){controller.abort()}});
+ return new Response(stream,{headers:{"content-type":"application/x-ndjson","cache-control":"no-store"}});
+ }catch(error){return json({error:publicError(error)},error instanceof BridgeError?error.status:400)}
+ },
+ async close(){closed=true;for(const c of active.values())c.abort();while(active.size)await new Promise(resolve=>setTimeout(resolve,20));},
+ };
+}
diff --git a/adapters/opencode-sdk/src/legacy-plugin.mjs b/adapters/opencode-sdk/src/legacy-plugin.mjs
new file mode 100644
index 00000000..275ce25f
--- /dev/null
+++ b/adapters/opencode-sdk/src/legacy-plugin.mjs
@@ -0,0 +1,79 @@
+// Loaded only by the owned, isolated CLI 1.19.4 process. No host tools execute here.
+import { readFileSync } from "node:fs";
+
+export default async (_input, options) => {
+ const state = JSON.parse(readFileSync(options.state, "utf8"));
+ const attempts = new Map();
+ let network = 0, quota = false, contextOverflow = false, freeTier = false, redirected = false;
+ const blocked = () => redirected ? "SCOPEWEAVER_LEGACY_REDIRECT" : freeTier ? "SCOPEWEAVER_LEGACY_FREE_TIER" : contextOverflow ? "SCOPEWEAVER_LEGACY_CONTEXT_LENGTH" : quota ? "SCOPEWEAVER_LEGACY_QUOTA" : "SCOPEWEAVER_LEGACY_SECOND_REQUEST";
+ return {
+ config: async config => {
+ config.provider ??= {};
+ config.provider[state.provider] ??= {};
+ config.provider[state.provider].options ??= {};
+ // Public provider options support custom fetch. It gates actual network
+ // attempts too, including AI SDK retries, and refuses HTTP redirects.
+ config.provider[state.provider].options.fetch = async (input, init) => {
+ if (++network !== 1) throw Error(blocked());
+ const response = await fetch(input, { ...init, redirect: "error" }).catch(error => {
+ // Preserve the first redirect failure if the CLI retries either gate.
+ redirected = error?.code === "UnexpectedRedirect";
+ throw error;
+ });
+ if ([400, 413, 402, 429, 403].includes(response.status)) {
+ const reader = response.clone().body?.getReader();
+ if (reader) {
+ try {
+ const decoder = new TextDecoder(); let text = "";
+ while (text.length < 100000) { const chunk = await reader.read(); if (chunk.done) break; text += decoder.decode(chunk.value, { stream: true }); }
+ if (text.length < 100000) {
+ const body = JSON.parse(text);
+ quota = [402, 429].includes(response.status) && ["insufficient_quota", "billing_hard_limit_reached", "billing_not_active", "credit_balance_exhausted", "quota_exceeded"].includes(body?.error?.code);
+ contextOverflow = [400, 413].includes(response.status) && [body?.error?.code, body?.error?.type].some(code => ["context_length_exceeded", "model_context_window_exceeded", "context_window_exceeded", "prompt_too_long"].includes(code));
+ freeTier = response.status === 403 && [body?.error?.code, body?.error?.type, body?.error?.name].includes("FreeTierError");
+ }
+ } catch {} finally { void reader.cancel().catch(() => {}); }
+ }
+ }
+ // The CLI otherwise normalizes context errors and can discard the
+ // structured HTTP evidence. Preserve only our proven typed marker.
+ if (contextOverflow || freeTier) throw Error(blocked());
+ return response;
+ };
+ },
+ "tool.execute.before": async input => {
+ if (!state.allowed.includes(input.tool)) throw Error("SCOPEWEAVER_LEGACY_NATIVE_TOOL");
+ },
+ "chat.params": async (input, output) => {
+ const count = (attempts.get(input.sessionID) ?? 0) + 1;
+ attempts.set(input.sessionID, count);
+ if (count !== 1) throw Error(blocked());
+ output.maxOutputTokens = state.request.max_tokens;
+ if (state.request.temperature !== undefined) output.temperature = state.request.temperature;
+ output.options = {
+ ...output.options,
+ ...(state.request.stop !== undefined ? { stop: state.request.stop } : {}),
+ ...(state.request.thinking || state.request.thinking_type
+ ? { thinking: { type: state.request.thinking || state.request.thinking_type } } : {}),
+ ...(state.request.reasoning_effort ? { reasoningEffort: state.request.reasoning_effort } : {}),
+ };
+ },
+ "chat.headers": async (_input, output) => {
+ // OpenCode owns its internal session identity; upstream affinity remains host-owned.
+ if (["opencode", "opencode-go"].includes(state.provider))
+ output.headers["x-opencode-session"] = state.request.session_id ?? state.request.request_id;
+ },
+ "experimental.chat.system.transform": async (_input, output) => {
+ output.system.splice(0, output.system.length, ...state.request.system);
+ },
+ "experimental.chat.messages.transform": async (_input, output) => {
+ const sessionID = output.messages[0]?.info.sessionID;
+ if (!sessionID) throw Error("SCOPEWEAVER_LEGACY_HISTORY");
+ const history = state.history.map(message => ({
+ info: { ...message.info, sessionID },
+ parts: message.parts.map(part => ({ ...part, sessionID })),
+ }));
+ output.messages.splice(0, output.messages.length, ...history);
+ },
+ };
+};
diff --git a/adapters/opencode-sdk/src/legacy.mjs b/adapters/opencode-sdk/src/legacy.mjs
new file mode 100644
index 00000000..467e7dc8
--- /dev/null
+++ b/adapters/opencode-sdk/src/legacy.mjs
@@ -0,0 +1,365 @@
+import { createOpencodeClient } from "@opencode-ai/sdk/v2/client";
+import { createRequire } from "node:module";
+import { createServer } from "node:http";
+import { spawn } from "node:child_process";
+import { mkdir, writeFile, readFile, access, cp, lstat } from "node:fs/promises";
+import { dirname, resolve } from "node:path";
+import { randomUUID } from "node:crypto";
+import { fileURLToPath } from "node:url";
+import { BridgeError, fail, safeJSON } from "./protocol.mjs";
+
+const require = createRequire(import.meta.url);
+const gateMarker = "SCOPEWEAVER_LEGACY_SECOND_REQUEST";
+const canceled = () => new BridgeError("canceled", 499);
+const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
+function nativeError(error) {
+ const data = error?.data;
+ if (data?.message === "SCOPEWEAVER_LEGACY_QUOTA") return new BridgeError("quota_exhausted", 402);
+ if (data?.message === "SCOPEWEAVER_LEGACY_CONTEXT_LENGTH") return new BridgeError("context_length_exceeded", 400);
+ if (data?.message === "SCOPEWEAVER_LEGACY_FREE_TIER") return new BridgeError("free_tier_restricted", 403);
+ if (error?.name === "APIError" && data?.statusCode === 403 && typeof data.responseBody === "string" && data.responseBody.length < 100000) {
+ try {
+ const body = JSON.parse(data.responseBody);
+ if ([body?.error?.code, body?.error?.type, body?.error?.name].includes("FreeTierError")) return new BridgeError("free_tier_restricted", 403);
+ } catch {}
+ }
+ if (error?.name === "APIError" && [400, 413].includes(data?.statusCode) && typeof data.responseBody === "string" && data.responseBody.length < 100000) {
+ try {
+ const body = JSON.parse(data.responseBody), codes = [body?.error?.code, body?.error?.type];
+ if (codes.some(code => ["context_length_exceeded", "model_context_window_exceeded", "context_window_exceeded", "prompt_too_long"].includes(code)))
+ return new BridgeError("context_length_exceeded", 400);
+ } catch {}
+ }
+ if (error?.name === "APIError" && [402, 429].includes(data?.statusCode) && typeof data.responseBody === "string" && data.responseBody.length < 100000) {
+ try {
+ const body = JSON.parse(data.responseBody);
+ if (["insufficient_quota", "billing_hard_limit_reached", "billing_not_active", "credit_balance_exhausted", "quota_exceeded"].includes(body?.error?.code))
+ return new BridgeError("quota_exhausted", 402);
+ } catch {}
+ }
+ return new BridgeError(data?.message === gateMarker ? "second_model_request" : "sdk_failure", 502);
+}
+
+async function binary() {
+ const platform = { darwin: "darwin", linux: "linux", win32: "windows" }[process.platform];
+ if (!platform || !["arm64", "x64"].includes(process.arch)) fail("unsupported_runtime");
+ // opencode-ai's postinstall is intentionally unnecessary (--ignore-scripts supported).
+ let musl = false;
+ if (process.platform === "linux" && !process.report?.getReport?.()?.header?.glibcVersionRuntime) {
+ try { await access(`/lib/ld-musl-${process.arch === "arm64" ? "aarch64" : "x86_64"}.so.1`); musl = true; } catch {}
+ }
+ const base = `opencode-${platform}-${process.arch}`, suffix = musl ? "-musl" : "";
+ const names = process.arch === "x64" ? [`${base}-baseline${suffix}`, `${base}${suffix}`] : [`${base}${suffix}`];
+ let path;
+ for (const name of names) {
+ try { path = require.resolve(`${name}/bin/opencode${process.platform === "win32" ? ".exe" : ""}`); break; } catch {}
+ }
+ if (!path) fail("unsupported_runtime");
+ if (JSON.parse(await readFile(resolve(dirname(path), "../package.json"), "utf8")).version !== "1.19.4") fail("unsupported_runtime");
+ await access(path);
+ return path;
+}
+
+function aliases(request) {
+ const forward = new Map(), reverse = new Map();
+ const names = [...request.tools.map(tool => tool.name), ...request.messages.flatMap(message => message.content.filter(b => b.type === "tool_use").map(b => b.name))];
+ for (const name of names) if (!forward.has(name)) {
+ let local = /^[A-Za-z_][A-Za-z0-9_-]{0,55}$/.test(name) ? name : `host_${forward.size}`;
+ let suffix = 0;
+ while (reverse.has(`capture_${local}`)) local = `host_${forward.size}_${++suffix}`;
+ const alias = `capture_${local}`;
+ forward.set(name, alias); reverse.set(alias, name);
+ }
+ return { forward, reverse };
+}
+
+function history(request, map, rawHistory) {
+ // CLI 1.x carries structural JSON. Normal whitespace and string escapes
+ // (including Go's HTML escaping) preserve values; unsafe numbers do not.
+ // Native2 remains the lossless path for raw bytes and 64-bit IDs.
+ for (const raw of Object.values(rawHistory)) {
+ safeJSON(raw, "unsupported_input");
+ }
+ const results = new Map();
+ for (const message of request.messages) for (const block of message.content)
+ if (block.type === "tool_result") results.set(block.tool_use_id, block);
+ const [providerID, ...modelParts] = request.model.split("/");
+ const modelID = modelParts.join("/");
+ return request.messages.map((message, index) => {
+ const id = `msg_scopeweaver_${index}`, parts = [];
+ for (const block of message.content) {
+ const common = { id: `prt_scopeweaver_${index}_${parts.length}`, messageID: id };
+ if (block.type === "text") parts.push({ ...common, type: "text", text: block.text });
+ else if (block.type === "thinking") {
+ // The pinned OpenAI-compatible converter preserves unsigned text only;
+ // opaque provider signatures would be silently discarded, even if the
+ // current request disables thinking. Reject before creating a runtime.
+ if (block.signature !== undefined && block.signature !== "") fail("unsupported_input");
+ if (request.thinking !== "disabled") parts.push({ ...common, type: "reasoning", text: block.thinking,
+ time: { start: 0, end: 0 } });
+ } else if (block.type === "tool_use") {
+ const result = results.get(block.id); if (!result) fail("unsupported_input");
+ const raw = rawHistory[block.id] ?? JSON.stringify(block.input);
+ const input = safeJSON(raw, "unsupported_input");
+ const text = result.content.map(part => part.text).join("\n");
+ parts.push({ ...common, type: "tool", callID: block.id, tool: map.forward.get(block.name),
+ state: result.is_error ? { status: "error", input, error: text, time: { start: 0, end: 0 } }
+ : { status: "completed", input, output: text, title: "", metadata: {}, time: { start: 0, end: 0 } } });
+ } else if (block.type !== "tool_result") fail("unsupported_input");
+ }
+ const info = message.role === "assistant"
+ ? { id, role: "assistant", parentID: `msg_scopeweaver_${Math.max(0, index - 1)}`, modelID, providerID,
+ agent: "capture", mode: "capture", path: { cwd: "/", root: "/" }, cost: 0,
+ tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, time: { created: 0, completed: 0 }, finish: "stop" }
+ : { id, role: "user", time: { created: 0 }, agent: "capture", model: { providerID, modelID } };
+ return { info, parts };
+ }).filter(message => message.parts.length);
+}
+
+function providers(config) {
+ if (!config) return {};
+ const out = {};
+ for (const [id, provider] of Object.entries(config.providers)) {
+ // The trusted common configuration deliberately has a narrow legacy mapping.
+ if (provider.package !== "@opencode/ai/providers/openai-compatible") fail("configuration_error");
+ const models = {};
+ for (const [modelID, model] of Object.entries(provider.models)) {
+ if (model.compatibility?.maxTokensField && model.compatibility.maxTokensField !== "max_tokens") fail("unsupported_input");
+ if (model.compatibility?.reasoningField || Object.keys(model.capabilities ?? {}).some(key => key !== "tools")) fail("unsupported_input");
+ models[modelID] = { id: modelID, name: model.name ?? modelID, tool_call: model.capabilities?.tools ?? true, temperature: true,
+ limit: { context: model.limit?.context ?? 200000, output: model.limit?.output ?? 32768 } };
+ }
+ out[id] = { name: provider.name ?? id, npm: "@ai-sdk/openai-compatible", options: provider.settings, models };
+ }
+ return out;
+}
+
+async function start({ root, providerConfig, request, map, rawHistory = {}, signal }) {
+ if (signal?.aborted) throw canceled();
+ const runtime = `${root}/legacy`;
+ for (const dir of ["home", "config", "data", "state", "cache", "work"]) await mkdir(`${runtime}/${dir}`, { recursive: true });
+ // The CLI normally bootstraps its plugin package on startup. Give it a real,
+ // private package tree and matching lock; never a writable link to our tree.
+ const pluginRoot = resolve(dirname(fileURLToPath(import.meta.resolve("@opencode-ai/plugin"))), "..");
+ if (JSON.parse(await readFile(`${pluginRoot}/package.json`, "utf8")).version !== "1.19.4") fail("unsupported_runtime");
+ for (const dir of [`${runtime}/config`, `${runtime}/config/opencode`]) {
+ await mkdir(dir, { recursive: true });
+ const dependencies = { "@opencode-ai/plugin": "1.19.4" };
+ await writeFile(`${dir}/package.json`, JSON.stringify({ dependencies }));
+ await writeFile(`${dir}/package-lock.json`, JSON.stringify({ lockfileVersion: 3, packages: { "": { dependencies } } }));
+ try { if ((await lstat(`${dir}/node_modules`)).isSymbolicLink()) fail("configuration_error"); }
+ catch (error) { if (error.code !== "ENOENT") throw error; }
+ await mkdir(`${dir}/node_modules/@opencode-ai`, { recursive: true });
+ await cp(pluginRoot, `${dir}/node_modules/@opencode-ai/plugin`, { recursive: true,
+ filter: source => !source.slice(pluginRoot.length).split(/[\\/]/).includes("node_modules") });
+ }
+ const configured = providers(providerConfig), captures = new Map();
+ let captureServer, processHandle, stopped = false, captureListening = false, captureClosing;
+ const stopNow = () => {
+ if (processHandle && processHandle.exitCode === null && processHandle.signalCode === null) processHandle.kill("SIGTERM");
+ if (captureServer && captureListening && !captureClosing) {
+ captureServer.closeAllConnections(); captureClosing = new Promise(resolve => captureServer.close(() => resolve()));
+ }
+ };
+ signal?.addEventListener("abort", stopNow, { once: true });
+ const close = async () => {
+ if (stopped) return; stopped = true;
+ signal?.removeEventListener("abort", stopNow);
+ stopNow();
+ const exited = () => processHandle.exitCode !== null || processHandle.signalCode !== null;
+ if (processHandle && !exited()) {
+ processHandle.kill("SIGTERM");
+ await Promise.race([new Promise(resolve => processHandle.once("exit", resolve)), sleep(1500)]);
+ if (!exited()) {
+ processHandle.kill("SIGKILL");
+ await Promise.race([new Promise(resolve => processHandle.once("exit", resolve)), sleep(1500)]);
+ if (!exited()) fail("sdk_failure", 502);
+ }
+ }
+ if (captureClosing) await captureClosing;
+ };
+ try {
+ const cfg = { autoupdate: false, share: "disabled", snapshot: false, compaction: { auto: false }, plugin: [],
+ lsp: false, formatter: false, permission: { "*": "deny" }, tools: { "*": false },
+ agent: { capture: { mode: "primary", permission: { "*": "deny" }, tools: { "*": false } }, title: { disable: true }, summary: { disable: true }, compaction: { disable: true } },
+ default_agent: "capture", provider: configured };
+ if (request) {
+ const separator = request.model.indexOf("/"), provider = request.model.slice(0, separator), model = request.model.slice(separator + 1);
+ cfg.model = request.model; cfg.small_model = request.model;
+ if (providerConfig && !configured[provider]?.models[model]) fail("unknown_model");
+ if (request.tools.length && configured[provider]?.models[model]?.tool_call === false) fail("unsupported_input");
+ const max = configured[provider]?.models[model]?.limit.output;
+ if (max && request.max_tokens > max) request = { ...request, max_tokens: max };
+ if (configured[provider]) configured[provider].models[model].limit.output = request.max_tokens;
+ const statePath = `${runtime}/state.json`;
+ await writeFile(statePath, JSON.stringify({ request, provider, allowed: request.tools.map(tool => map.forward.get(tool.name)), history: history(request, map, rawHistory) }), { mode: 0o600 });
+ cfg.plugin = [[new URL("./legacy-plugin.mjs", import.meta.url).href, { state: statePath }]];
+ const token = randomUUID(), schemas = request.tools.map(tool => ({ name: map.forward.get(tool.name).slice("capture_".length), description: `ScopeWeaver host tool ${tool.name}. Invoke this advertised function when host instructions refer to ${tool.name}.\n${tool.description}`, inputSchema: tool.input_schema }));
+ captureServer = createServer(async (req, res) => {
+ if (req.headers.authorization !== `Bearer ${token}`) { res.writeHead(401); res.end(); return; }
+ if (req.method !== "POST") { res.writeHead(req.method === "DELETE" ? 200 : 405); res.end(); return; }
+ try {
+ let text = ""; for await (const chunk of req) { text += chunk; if (text.length > 2000000) throw Error(); }
+ const call = safeJSON(text, "protocol_error");
+ if (call.id === undefined) { res.writeHead(202); res.end(); return; }
+ let result;
+ if (call.method === "initialize") result = { protocolVersion: call.params.protocolVersion, capabilities: { tools: {} }, serverInfo: { name: "scopeweaver-capture", version: "1" } };
+ else if (call.method === "tools/list") result = { tools: schemas };
+ else if (call.method === "tools/call") {
+ if (!schemas.some(schema => schema.name === call.params.name)) throw Error();
+ const key = `capture_${call.params.name}`, values = captures.get(key) ?? [];
+ values.push(JSON.stringify(call.params.arguments)); captures.set(key, values);
+ result = { content: [{ type: "text", text: "deferred_to_host" }] };
+ } else if (call.method === "ping") result = {};
+ else { res.writeHead(400); res.end(); return; }
+ res.writeHead(200, { "content-type": "application/json" }); res.end(JSON.stringify({ jsonrpc: "2.0", id: call.id, result }));
+ } catch { res.writeHead(400); res.end(); }
+ });
+ await new Promise(resolve => captureServer.listen(0, "127.0.0.1", resolve));
+ captureListening = true;
+ if (signal?.aborted) throw canceled();
+ cfg.mcp = { capture: { type: "remote", url: `http://127.0.0.1:${captureServer.address().port}/mcp`, headers: { Authorization: `Bearer ${token}` }, oauth: false, enabled: true, timeout: 10000 } };
+ for (const tool of request.tools) {
+ const name = map.forward.get(tool.name);
+ cfg.permission[name] = "allow"; cfg.tools[name] = true;
+ cfg.agent.capture.permission[name] = "allow"; cfg.agent.capture.tools[name] = true;
+ }
+ }
+ await writeFile(`${runtime}/opencode.json`, JSON.stringify(cfg), { mode: 0o600 });
+ const password = randomUUID();
+ const env = { PATH: `${process.execPath.slice(0, process.execPath.lastIndexOf("/"))}:/usr/bin:/bin`, HOME: `${runtime}/home`,
+ XDG_CONFIG_HOME: `${runtime}/config`, XDG_DATA_HOME: `${runtime}/data`, XDG_STATE_HOME: `${runtime}/state`, XDG_CACHE_HOME: `${runtime}/cache`,
+ OPENCODE_CONFIG: `${runtime}/opencode.json`, OPENCODE_CONFIG_DIR: `${runtime}/config`, OPENCODE_DISABLE_DEFAULT_PLUGINS: "true",
+ OPENCODE_DISABLE_AUTOUPDATE: "true", OPENCODE_DISABLE_MODELS_FETCH: "true", OPENCODE_DISABLE_LSP_DOWNLOAD: "true", OPENCODE_DISABLE_PROJECT_CONFIG: "true", OPENCODE_SERVER_PASSWORD: password, CI: "1" };
+ // CLI treats --port 0 as its default port; reserve then release a real ephemeral port.
+ const reservation = createServer(); await new Promise(resolve => reservation.listen(0, "127.0.0.1", resolve));
+ const port = reservation.address().port; await new Promise(resolve => reservation.close(resolve));
+ const executable = await binary();
+ if (signal?.aborted) throw canceled();
+ processHandle = spawn(executable, ["serve", "--hostname", "127.0.0.1", "--port", String(port)], { cwd: `${runtime}/work`, env, stdio: ["ignore", "pipe", "pipe"] });
+ const url = await new Promise((resolve, reject) => {
+ let text = "";
+ const timer = setTimeout(() => reject(new BridgeError("sdk_failure", 502)), 20000);
+ const abort = () => { clearTimeout(timer); reject(canceled()); };
+ signal?.addEventListener("abort", abort, { once: true });
+ const data = chunk => {
+ text = (text + chunk).slice(-4000);
+ if (text.includes(`http://127.0.0.1:${port}`)) { clearTimeout(timer); signal?.removeEventListener("abort", abort); resolve(`http://127.0.0.1:${port}`); }
+ };
+ processHandle.stdout.on("data", data); processHandle.stderr.on("data", data);
+ processHandle.once("error", () => { clearTimeout(timer); signal?.removeEventListener("abort", abort); reject(new BridgeError("unsupported_runtime")); });
+ processHandle.once("exit", () => { clearTimeout(timer); signal?.removeEventListener("abort", abort); reject(new BridgeError("sdk_failure", 502)); });
+ });
+ return { client: createOpencodeClient({ baseUrl: url, directory: `${runtime}/work`, headers: { Authorization: `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}` }, throwOnError: true }), directory: `${runtime}/work`, close, stopNow, captures, request };
+ } catch (error) { await close(); throw error; }
+}
+
+export async function discoverLegacyModels({ root, providerConfig }) {
+ const runtime = await start({ root, providerConfig });
+ try {
+ const data = (await runtime.client.provider.list()).data;
+ let publicIDs;
+ if (!providerConfig) {
+ const response = await fetch("https://opencode.ai/inference/v1/models", { redirect: "error", signal: AbortSignal.timeout(10000) });
+ if (!response.ok) fail("sdk_failure", 502);
+ const catalog = safeJSON(await response.text(), "sdk_failure"); publicIDs = new Set(catalog.data.map(model => model.id));
+ }
+ const models = data.all.flatMap(provider => Object.values(provider.models).filter(model => providerConfig
+ ? Object.hasOwn(providerConfig.providers, provider.id) && Object.hasOwn(providerConfig.providers[provider.id].models, model.id)
+ : provider.id === "opencode" && publicIDs.has(model.id) && model.cost?.input === 0 && model.cost?.output === 0)
+ .map(model => ({ id: `${provider.id}/${model.id}`, name: model.name ?? model.id })));
+ if (!models.length) fail("unknown_model");
+ return models.sort((a, b) => a.id.localeCompare(b.id));
+ } finally { await runtime.close(); }
+}
+
+export async function completeLegacy(request, { root, providerConfig, rawHistory = {}, signal, emit = () => {} }) {
+ const map = aliases(request);
+ history(request, map, rawHistory); // all representability failures precede SDK inference
+ if (!providerConfig && !(await discoverLegacyModels({ root: `${root}/catalog` })).some(model => model.id === request.model)) fail("unknown_model");
+ const runtime = await start({ root, providerConfig, request, map, rawHistory, signal });
+ const { client, captures, directory } = runtime;
+ const abortEvents = new AbortController(), blocks = new Map(), content = [], calls = new Map();
+ let session, consumer, error, firstUsage, finish, ended = false, bytes = 0, wake;
+ const notify = () => { wake?.(); wake = undefined; };
+ const abort = () => { runtime.stopNow(); error = canceled(); notify(); };
+ signal?.addEventListener("abort", abort, { once: true });
+ const cleanupSession = async () => {
+ if (!session) return;
+ await Promise.race([(async () => {
+ await client.session.abort({ sessionID: session.id, directory });
+ await client.session.delete({ sessionID: session.id, directory });
+ })(), sleep(1500)]);
+ session = undefined;
+ };
+ try {
+ if (signal?.aborted) throw canceled();
+ const sub = await client.event.subscribe({ directory }, { signal: abortEvents.signal });
+ consumer = (async () => {
+ try { for await (const event of sub.stream) {
+ if (event.properties?.sessionID !== session?.id && event.properties?.part?.sessionID !== session?.id) continue;
+ const part = event.properties?.part;
+ if (event.type === "message.part.updated" && part) {
+ if (["text", "reasoning"].includes(part.type) && !blocks.has(part.id)) {
+ const block = part.type === "text" ? { type: "text", text: "" } : { type: "thinking", thinking: "" };
+ blocks.set(part.id, block); content.push(block);
+ } else if (part.type === "tool") {
+ if (!map.reverse.has(part.tool)) fail("protocol_error");
+ if (!calls.has(part.callID)) {
+ if (calls.size >= 128) fail("protocol_error");
+ const call = { type: "tool_use", id: `sdk_${request.request_id}_${calls.size}`, name: map.reverse.get(part.tool) }; calls.set(part.callID, { call, nativeName: part.tool }); content.push(call);
+ } else if (calls.get(part.callID).nativeName !== part.tool) fail("protocol_error");
+ if (part.state.status === "error") fail("protocol_error");
+ if (part.state.status === "completed") {
+ const raw = JSON.stringify(part.state.input); safeJSON(raw, "protocol_error");
+ const call = calls.get(part.callID).call;
+ if (part.state.output !== "deferred_to_host" || (call.raw !== undefined && call.raw !== raw)) fail("protocol_error");
+ if (call.raw === undefined) {
+ const values = captures.get(part.tool), index = values?.indexOf(raw) ?? -1;
+ if (index < 0 || raw.length > 2000000) fail("protocol_error");
+ bytes += raw.length; if (bytes > 8000000) fail("protocol_error");
+ values.splice(index, 1); call.raw = raw;
+ }
+ }
+ } else if (part.type === "step-finish" && !firstUsage) {
+ firstUsage = part.tokens; finish = part.reason;
+ }
+ } else if (event.type === "message.part.delta") {
+ const delta = event.properties;
+ if (delta.field !== "text") continue;
+ const block = blocks.get(delta.partID); if (!block) fail("protocol_error");
+ bytes += delta.delta.length; if (bytes > 8000000) fail("protocol_error");
+ block[block.type === "text" ? "text" : "thinking"] += delta.delta;
+ emit({ type: block.type === "text" ? "text_delta" : "thinking_delta", text: delta.delta });
+ } else if (event.type === "session.error") {
+ if (event.properties.error?.data?.message === gateMarker && firstUsage && calls.size) ended = true;
+ else throw nativeError(event.properties.error);
+ } else if (event.type === "session.status" && event.properties.status.type === "idle" && firstUsage && !calls.size) ended = true;
+ notify();
+ } } catch (e) { if (!abortEvents.signal.aborted) { error = e instanceof BridgeError ? e : new BridgeError("sdk_failure", 502); notify(); } }
+ })();
+ const permissions = [{ permission: "*", pattern: "*", action: "deny" }, ...request.tools.map(tool => ({ permission: map.forward.get(tool.name), pattern: "*", action: "allow" }))];
+ session = (await client.session.create({ directory, title: "ScopeWeaver capture", permission: permissions })).data;
+ emit({ type: "message_start" });
+ const split = request.model.indexOf("/");
+ await client.session.promptAsync({ sessionID: session.id, directory, model: { providerID: request.model.slice(0, split), modelID: request.model.slice(split + 1) }, agent: "capture", parts: [{ type: "text", text: "ScopeWeaver host turn" }] });
+ while (!ended && !error) await new Promise(resolve => { wake = resolve; if (ended || error || signal?.aborted) notify(); });
+ if (error) throw error;
+ if (!firstUsage || !content.length || [...calls.values()].some(value => !value.call.raw)) fail("protocol_error");
+ const usage = { input_tokens: firstUsage.input, output_tokens: firstUsage.output, cache_read_tokens: firstUsage.cache?.read ?? 0, cache_write_tokens: firstUsage.cache?.write ?? 0 };
+ if (!Object.values(usage).every(value => Number.isSafeInteger(value) && value >= 0)) fail("protocol_error");
+ const stop_reason = calls.size ? "tool_use" : ["length", "max-tokens"].includes(finish) ? "max_tokens" : finish === "stop" ? "end_turn" : undefined;
+ if (!stop_reason) fail("protocol_error");
+ await cleanupSession();
+ abortEvents.abort(); await runtime.close();
+ if (signal?.aborted) throw canceled();
+ for (const { call } of calls.values()) { emit({ type: "tool_use_start", tool_id: call.id, tool_name: call.name }); emit({ type: "tool_input_delta", text: call.raw }); }
+ emit({ type: "message_delta", stop_reason, usage }); emit({ type: "message_stop" });
+ return { message: { role: "assistant", content }, stop_reason, usage };
+ } finally {
+ signal?.removeEventListener("abort", abort); abortEvents.abort();
+ if (session) runtime.stopNow();
+ try { await cleanupSession(); } catch {}
+ await runtime.close(); if (consumer) await Promise.race([consumer, sleep(500)]);
+ }
+}
diff --git a/adapters/opencode-sdk/src/native-cli.mjs b/adapters/opencode-sdk/src/native-cli.mjs
new file mode 100644
index 00000000..c72b414b
--- /dev/null
+++ b/adapters/opencode-sdk/src/native-cli.mjs
@@ -0,0 +1,131 @@
+import { OpenCode } from "@opencode/client";
+import { spawn } from "node:child_process";
+import { createRequire } from "node:module";
+import { mkdir, readFile, writeFile, lstat } from "node:fs/promises";
+import { dirname, join } from "node:path";
+import { userInfo } from "node:os";
+import { pathToFileURL } from "node:url";
+import { BridgeError, fail } from "./protocol.mjs";
+
+const require=createRequire(import.meta.url);
+export const nativeRules=[{action:"*",resource:"*",effect:"ask"},{action:"host_capture",resource:"*",effect:"allow"}];
+export function unmanagedConfigPaths(platform=process.platform,user=userInfo().username,env=process.env){
+ if(platform==="darwin")return ["/Library/Application Support/opencode",`/Library/Managed Preferences/${user}/ai.opencode.managed.plist`,"/Library/Managed Preferences/ai.opencode.managed.plist"];
+ return [platform==="win32"?join(env.ProgramData??"C:\\ProgramData","opencode"):"/etc/opencode"];
+}
+export async function assertNoUnmanagedConfig(paths=unmanagedConfigPaths()){
+ for(const path of paths){
+ try{await lstat(path);fail("configuration_error");}catch(error){if(error.code!=="ENOENT")throw error instanceof BridgeError?error:new BridgeError("configuration_error");}
+ }
+}
+async function binaryPath(){
+ if(!["darwin","linux"].includes(process.platform)||!["x64","arm64"].includes(process.arch))fail("unsupported_runtime");
+ const platform=process.platform==="win32"?"windows":process.platform;
+ const base=`@opencode/cli-${platform}-${process.arch}`;
+ const names=process.platform==="linux"?(process.arch==="x64"?[`${base}-baseline`,`${base}-baseline-musl`,base,`${base}-musl`]:[base,`${base}-musl`]):process.arch==="x64"&&process.platform!=="win32"?[`${base}-baseline`,base]:[base];
+ for(const name of names){
+ try{
+ const file=require.resolve(`${name}/package.json`),metadata=JSON.parse(await readFile(file,"utf8"));
+ if(metadata.version!=="2.1.4")continue;
+ const binary=join(dirname(file),"bin",process.platform==="win32"?"opencode.exe":"opencode");
+ const valid=await new Promise(resolve=>{
+ const child=spawn(binary,["--version"],{stdio:"ignore",windowsHide:true});
+ const timer=setTimeout(()=>{child.kill("SIGKILL");resolve(false)},5000);
+ child.on("error",()=>{clearTimeout(timer);resolve(false)});child.on("exit",code=>{clearTimeout(timer);resolve(code===0)});
+ });
+ if(valid)return binary;
+ }catch{}
+ }
+ fail("unsupported_runtime");
+}
+
+export async function createNativeCLI({root,providerConfig,request,history,tools,rawHistory,signal,onControl}){
+ await assertNoUnmanagedConfig();
+ const binary=await binaryPath(),channel=crypto.randomUUID(),password=crypto.randomUUID()+crypto.randomUUID();
+ const paths=Object.fromEntries(["home","config","data","cache","state","tmp","work"].map(name=>[name,join(root,`cli-${name}`)]));
+ for(const path of Object.values(paths))await mkdir(path,{recursive:true});
+ const plugin=join(paths.config,"host-capture");await mkdir(plugin,{recursive:true});
+ await writeFile(join(plugin,"package.json"),JSON.stringify({name:"scopeweaver-host-capture",type:"module",main:"index.mjs"}));
+ await writeFile(join(plugin,"index.mjs"),await readFile(new URL("./native-plugin.mjs",import.meta.url)));
+ const input={request,history,tools,rawHistory,channel},inputPath=join(plugin,"bridge-input.json");
+ const setRequest=async next=>{input.request=next;await writeFile(inputPath,JSON.stringify(input),{mode:0o600})};
+ await setRequest(request);
+ const providers=structuredClone(providerConfig?.providers??{}),split=request.model.indexOf("/"),provider=request.model.slice(0,split),model=request.model.slice(split+1);
+ providers[provider]??={};providers[provider].models??={};providers[provider].models[model]??={};
+ const selected=providers[provider].models[model];
+ selected.body={...(request.thinking||request.thinking_type?{thinking:{type:request.thinking||request.thinking_type}}:{}),...(request.reasoning_effort?{reasoning_effort:request.reasoning_effort}:{})};
+ selected.compatibility={...selected.compatibility,maxTokensField:selected.compatibility?.maxTokensField??"max_tokens"};
+ const config={providers,plugins:[pathToFileURL(plugin).href],default_agent:"capture",agents:{capture:{mode:"primary",system:request.system.join("\n\n"),permissions:nativeRules}},permissions:nativeRules,update:"disable",share:"disabled",snapshots:false,warming:false,compaction:{auto:false}};
+ const configPath=join(paths.config,"opencode.json");await writeFile(configPath,JSON.stringify(config),{mode:0o600});
+ const env={PATH:process.env.PATH??"/usr/bin:/bin",PWD:paths.work,HOME:paths.home,USERPROFILE:paths.home,TMPDIR:paths.tmp,OPENCODE_TEST_HOME:paths.home,XDG_CONFIG_HOME:paths.config,XDG_DATA_HOME:paths.data,XDG_CACHE_HOME:paths.cache,XDG_STATE_HOME:paths.state,OPENCODE_CONFIG_DIR:paths.config,OPENCODE_CONFIG:configPath,OPENCODE_CONFIG_PROJECT_DISABLE:"1",OPENCODE_DISABLE_CLAUDE_CODE:"1",OPENCODE_PASSWORD:password};
+ for(const key of ["SystemRoot","WINDIR","LANG","LC_ALL","TZ"])if(process.env[key])env[key]=process.env[key];
+ const children=new Set();let client,runner,runnerExit,egress,closed=false;
+ const start=args=>{
+ if(signal.aborted)throw new BridgeError("canceled",499);
+ const child=spawn(binary,args,{cwd:paths.work,env,windowsHide:true,stdio:["pipe","pipe","pipe"]});children.add(child);
+ child.stdin.on("error",()=>{});child.stderr.on("data",()=>{});child.once("close",()=>children.delete(child));return child;
+ };
+ const close=async()=>{
+ if(closed)return;closed=true;signal.removeEventListener("abort",abort);
+ egress?.stop(true);
+ for(const child of children){child.stdin.end();child.kill("SIGTERM")}
+ if(children.size)await Promise.race([Promise.all([...children].map(child=>new Promise(resolve=>child.once("close",resolve)))),new Promise(resolve=>setTimeout(resolve,200))]);
+ const remaining=[...children],exited=Promise.all(remaining.map(child=>new Promise(resolve=>child.once("close",resolve))));
+ for(const child of remaining)child.kill("SIGKILL");
+ if(remaining.length){
+ let timer;try{await Promise.race([exited,new Promise((_,reject)=>{timer=setTimeout(()=>reject(new BridgeError("sdk_failure",502)),1000)})])}finally{clearTimeout(timer)}
+ }
+ };
+ const abort=()=>{if(client)void client.session.interrupt({sessionID:activeSession,resume:false}).catch(()=>{});for(const child of children)child.kill("SIGTERM")};
+ let activeSession;
+ signal.addEventListener("abort",abort,{once:true});
+ try{
+ const token=crypto.randomUUID()+crypto.randomUUID();let sent=false;
+ // Raw tool history becomes JSON inside JSON; allow its encoded expansion
+ // while the public bridge continues to enforce its 16 MiB input limit.
+ egress=Bun.serve({hostname:"127.0.0.1",port:0,idleTimeout:0,maxRequestBodySize:64*1024*1024,async fetch(incoming){
+ if(incoming.method!=="POST"||new URL(incoming.url).pathname!=="/model"||incoming.headers.has("origin")||incoming.headers.get("x-scopeweaver-egress")!==token||sent)return new Response(null,{status:403});
+ sent=true;
+ try{
+ const target=new URL(incoming.headers.get("x-scopeweaver-upstream")??"");
+ if(target.username||target.password||target.hash||!["http:","https:"].includes(target.protocol))throw Error();
+ const expected=providerConfig?new URL(providerConfig.providers[provider].settings.baseURL).origin:"https://opencode.ai";
+ if(target.origin!==expected)throw Error();
+ const headers=new Headers(incoming.headers);
+ for(const name of ["host","connection","content-length","x-scopeweaver-egress","x-scopeweaver-upstream"])headers.delete(name);
+ const response=await fetch(target,{method:"POST",headers,body:await incoming.arrayBuffer(),redirect:"error",signal:AbortSignal.any([signal,incoming.signal])});
+ // Fetch exposes decoded bytes but retains the upstream encoding header.
+ // The downstream CLI must not attempt to decode those bytes again.
+ const received=new Headers(response.headers);
+ for(const name of ["content-encoding","content-length","transfer-encoding","connection"])received.delete(name);
+ return new Response(response.body,{status:response.status,headers:received});
+ }catch{return new Response(null,{status:502})}
+ }});
+ input.egress={url:`http://127.0.0.1:${egress.port}/model`,token};await setRequest(request);
+ const server=start(["serve","--stdio","--hostname","127.0.0.1","--port","0"]);
+ const url=await new Promise((resolve,reject)=>{
+ let buffer="";const timer=setTimeout(()=>reject(new BridgeError("sdk_failure",502)),15000);
+ const done=(error,value)=>{clearTimeout(timer);error?reject(error):resolve(value)};
+ server.stdout.on("data",chunk=>{
+ buffer+=chunk.toString();if(buffer.length>1000000){done(new BridgeError("protocol_error",502));server.kill("SIGTERM");return}
+ let index;while((index=buffer.indexOf("\n"))>=0){
+ const line=buffer.slice(0,index);buffer=buffer.slice(index+1);let value;try{value=JSON.parse(line)}catch{continue}
+ if(value._scopeweaver_native===channel){try{onControl(value.event)}catch{server.kill("SIGTERM")}}
+ else if(typeof value.url==="string"){
+ const parsed=new URL(value.url);if(parsed.protocol!=="http:"||parsed.hostname!=="127.0.0.1"||parsed.username||parsed.password||parsed.pathname!=="/")done(new BridgeError("protocol_error",502));else done(undefined,value.url);
+ }
+ }
+ });
+ server.once("error",()=>done(new BridgeError("sdk_failure",502)));server.once("exit",()=>done(new BridgeError(signal.aborted?"canceled":"sdk_failure",502)));
+ });
+ client=OpenCode.make({baseUrl:url,headers:{Authorization:`Basic ${Buffer.from(`opencode:${password}`).toString("base64")}`}});
+ if((await client.server.info()).version!=="2.1.4")fail("unsupported_runtime");
+ return {...client,directory:paths.work,setRequest,close,session:{...client.session,async prompt({sessionID}){
+ activeSession=sessionID;
+ runner=start(["run","--server",url,"--session",sessionID,"--model",request.model,"--agent","capture","--format","json"]);
+ runnerExit=new Promise(resolve=>{runner.once("error",()=>resolve(-1));runner.once("exit",resolve)});
+ let size=0;runner.stdout.on("data",chunk=>{size+=chunk.length;if(size>20000000){onControl({type:"failure",code:"protocol_error"});runner.kill("SIGTERM")}});
+ runner.stdin.end("ScopeWeaver authoritative conversation is supplied by the host plugin.");
+ }},async waitRunner(){if(runnerExit&&(await runnerExit)!==0)fail("sdk_failure",502)}};
+ }catch(error){await close();throw error}
+}
diff --git a/adapters/opencode-sdk/src/native-plugin.mjs b/adapters/opencode-sdk/src/native-plugin.mjs
new file mode 100644
index 00000000..3acdd6de
--- /dev/null
+++ b/adapters/opencode-sdk/src/native-plugin.mjs
@@ -0,0 +1,85 @@
+import { readFile } from "node:fs/promises";
+
+// This local plugin is copied into the request's private config directory.
+// Keep the real OpenCode tool registry and implementations; policy blocks execution.
+const inputURL=new URL("./bridge-input.json",import.meta.url);
+const input=JSON.parse(await readFile(inputURL,"utf8"));
+const {request,history,tools,rawHistory,channel,egress}=input;
+const exposed=new Set(tools.map(t=>t.name));
+const notify=event=>process.stdout.write(JSON.stringify({_scopeweaver_native:channel,event})+"\n");
+let network=0;
+
+export default {id:"scopeweaver.host-capture",async setup(ctx){
+ await ctx.agent.transform(editor=>{
+ for(const agent of editor.list())if(agent.id!=="capture")editor.remove(agent.id);
+ editor.default("capture");
+ });
+ await ctx.tool.transform(editor=>{
+ const nativeNames=new Set(editor.list().map(t=>t.id));
+ for(const tool of tools){
+ if(nativeNames.has(tool.name)||tool.name==="execute")throw Error("HOST_TOOL_COLLISION");
+ editor.add({...tool,options:{codemode:false,permission:"host_capture"},async execute(_,context){
+ notify({type:"capture",id:context.id});
+ return new Promise((_,reject)=>{
+ const stop=()=>reject(Error("CAPTURE_INTERRUPTED"));
+ if(context.signal.aborted)stop();else context.signal.addEventListener("abort",stop,{once:true});
+ });
+ }});
+ }
+ });
+ await ctx.tool.hook("execute.before",event=>{
+ if(!exposed.has(event.tool)){
+ notify({type:"failure",code:"protocol_error"});
+ throw Error("NATIVE_EXECUTION_DENIED");
+ }
+ });
+ await ctx.session.hook("context",async event=>{
+ // Host instructions use the public custom-agent system setting. Keep the
+ // runtime's generated initial context; replace only conversation history.
+ event.messages=structuredClone(history);
+ event.system.push({type:"text",text:'ScopeWeaver tool policy: The host application owns tool execution. Use only direct function tools whose description starts with "Host tool:". All other OpenCode tools, including execute and native filesystem, shell, browser, and subagent tools, are disabled by policy. Do not call them. If no host tools are listed, respond without tools.'});
+ const current=JSON.parse(await readFile(inputURL,"utf8"));
+ event.options.maxTokens=current.request.max_tokens;
+ if(request.temperature!==undefined)event.options.temperature=request.temperature;
+ if(request.stop!==undefined)event.options.stop=[...request.stop];
+ if(request.reasoning_effort)event.options.reasoningEffort=request.reasoning_effort;
+ });
+ await ctx.session.hook("http.request",async event=>{
+ if(event.kind!=="primary"||++network!==1){
+ notify({type:"failure",code:"second_model_request"});
+ throw Error("SECOND_MODEL_REQUEST_BLOCKED");
+ }
+ const body=await event.request.clone().json();
+ const names=(body.tools??[]).map(t=>t.function?.name??t.name);
+ if(new Set(names).size!==names.length||[...exposed].some(name=>!names.includes(name)))throw Error("SDK_TOOL_CATALOG_CHANGED");
+ if(Object.keys(rawHistory).length){
+ const restored=new Set();
+ for(const message of body.messages??[])for(const call of message.tool_calls??[])if(Object.hasOwn(rawHistory,call.id)){
+ call.function.arguments=rawHistory[call.id];restored.add(call.id);
+ }
+ if(restored.size!==Object.keys(rawHistory).length)throw Error("SDK_HISTORY_FORMAT_UNSUPPORTED");
+ const headers=new Headers(event.request.headers);headers.delete("content-length");
+ event.request=new Request(event.request,{headers,body:JSON.stringify(body)});
+ }
+ if(event.model.providerID==="opencode-go")event.request.headers.set("x-opencode-session",request.session_id??request.request_id);
+ else if(event.model.providerID!=="opencode")event.request.headers.delete("x-opencode-session");
+ // The CLI's Effect transport drops Request.redirect. Route the unchanged
+ // genuine request through the worker's one-use, redirect-blocking transport.
+ const headers=new Headers(event.request.headers);
+ headers.set("x-scopeweaver-egress",egress.token);
+ headers.set("x-scopeweaver-upstream",event.request.url);
+ event.request=new Request(egress.url,{method:"POST",headers,body:await event.request.clone().arrayBuffer(),redirect:"error"});
+ notify({type:"request"});
+ });
+ await ctx.session.hook("http.response",async event=>{
+ if(event.kind!=="primary"||![400,402,403,413,429].includes(event.response.status)||!event.response.headers.get("content-type")?.includes("application/json"))return;
+ try{
+ const text=await event.response.clone().text();if(text.length>65536)return;
+ const data=JSON.parse(text),code=data.error?.code??data.error?.type??data.error?.name,status=event.response.status;
+ const billing=["insufficient_quota","billing_hard_limit_reached","insufficient_balance","credit_balance_exhausted","credits_exhausted"];
+ if([402,429].includes(status)&&(billing.includes(code)||(status===402&&code==="quota_exceeded")))notify({type:"evidence",code:"quota_exhausted"});
+ if([400,413].includes(status)&&["context_length_exceeded","model_context_window_exceeded","context_window_exceeded","prompt_too_long"].includes(code))notify({type:"evidence",code:"context_length_exceeded"});
+ if(status===403&&code==="FreeTierError")notify({type:"evidence",code:"free_tier_restricted"});
+ }catch{}
+ });
+}};
diff --git a/adapters/opencode-sdk/src/native.mjs b/adapters/opencode-sdk/src/native.mjs
new file mode 100644
index 00000000..c3828bcb
--- /dev/null
+++ b/adapters/opencode-sdk/src/native.mjs
@@ -0,0 +1,168 @@
+import { OpenCode } from "@opencode/sdk";
+import { createNativeCLI, nativeRules } from "./native-cli.mjs";
+import { Config } from "@opencode/core/config";
+import { Global } from "@opencode/util/global";
+import { mkdir } from "node:fs/promises";
+import { BridgeError, fail, safeJSON, rawObject, aliasMap, nativeHistory, zeroPrice } from "./protocol.mjs";
+
+const rules=nativeRules;
+const abortError = () => new BridgeError("canceled",499);
+const signature = native => {
+ const found=[];
+ const walk=(v,d=0)=>{if(!v||typeof v!=="object"||d>3)return;for(const [k,x]of Object.entries(v)){if(["signature","encrypted"].includes(k)&&typeof x==="string")found.push(x);else if(typeof x==="object")walk(x,d+1)}};
+ walk(native);const unique=[...new Set(found)];if(unique.length>1)fail("protocol_error");return unique[0];
+};
+function usage(tokens) {
+ const result={input_tokens:tokens?.input,output_tokens:tokens?.output,cache_read_tokens:tokens?.cache?.read??0,cache_write_tokens:tokens?.cache?.write??0};
+ if(!Object.values(result).every(n=>Number.isSafeInteger(n)&&n>=0))fail("protocol_error");
+ return result;
+}
+async function createHost(root, providerConfig, plugins=[], request) {
+ const paths=Object.fromEntries(["home","data","cache","config","state","tmp","bin","log","repos"].map(k=>[k,`${root}/${k}`]));
+ await mkdir(paths.config,{recursive:true});
+ const providers=structuredClone(providerConfig?.providers??{});
+ if(request) {
+ const split=request.model.indexOf("/");const provider=request.model.slice(0,split),model=request.model.slice(split+1);
+ providers[provider]??={};providers[provider].models??={};providers[provider].models[model]??={};
+ const selected=providers[provider].models[model];
+ selected.body={...(request.thinking||request.thinking_type?{thinking:{type:request.thinking||request.thinking_type}}:{}),...(request.reasoning_effort?{reasoning_effort:request.reasoning_effort}:{})};
+ selected.compatibility={...selected.compatibility,maxTokensField:selected.compatibility?.maxTokensField??"max_tokens"};
+ }
+ const content=JSON.stringify({providers,plugins:[],update:"disable",share:"disabled",snapshots:false,warming:false,compaction:{auto:false},permissions:rules,default_agent:"capture",agents:{capture:{mode:"primary",system:"",permissions:rules}}});
+ return OpenCode.create({app:{name:"sdk",version:"2.1.4",channel:"latest"},database:{path:":memory:"},models:{fetch:!providerConfig,snapshot:!providerConfig},config:{directory:paths.config,project:false,claude:false,content},fs:{filewatcher:false,fff:false},plugins}, {overrides:[Global.node.replace(Global.layerWith(paths)),Config.node.replace(Config.configured({global:false,system:false,project:false,claude:false,content}))]});
+}
+export async function discoverModels({root,providerConfig}) {
+ let publicIDs;
+ if(!providerConfig) {
+ try {
+ const response=await fetch("https://opencode.ai/inference/v1/models",{redirect:"error",signal:AbortSignal.timeout(10000)});
+ if(!response.ok)throw Error();
+ const catalog=safeJSON(await response.text(),"sdk_failure");
+ if(!Array.isArray(catalog.data))throw Error();
+ publicIDs=new Set(catalog.data.map(m=>m.id));
+ } catch { fail("sdk_failure",502); }
+ }
+ const host=await createHost(root,providerConfig);
+ try {
+ const {data}=await host.model.list({location:{directory:`${root}/config`}});
+ const models=data.filter(m=>providerConfig?Object.hasOwn(providerConfig.providers,m.providerID)&&Object.hasOwn(providerConfig.providers[m.providerID].models,m.id):m.providerID==="opencode"&&publicIDs.has(m.id)&&zeroPrice(m.cost))
+ .map(m=>({id:`${m.providerID}/${m.id}`,name:m.name??m.id})).sort((a,b)=>a.id.localeCompare(b.id));
+ if(!models.length)fail("sdk_failure",502);
+ return models;
+ } finally { await host.close(); }
+}
+
+export async function completeNative(request,{root,providerConfig,rawHistory={},signal,emit}) {
+ // Validate against the original catalog, before a per-call model overlay can create a new model.
+ if(providerConfig) {
+ const split=request.model.indexOf("/");
+ if(!Object.hasOwn(providerConfig.providers,request.model.slice(0,split))||!Object.hasOwn(providerConfig.providers[request.model.slice(0,split)].models,request.model.slice(split+1)))fail("unknown_model");
+ } else if(!(await discoverModels({root:`${root}/catalog`,providerConfig})).some(m=>m.id===request.model))fail("unknown_model");
+ const names=[...new Set([...request.tools.map(t=>t.name),...request.messages.flatMap(m=>m.content.filter(b=>b.type==="tool_use").map(b=>b.name))])];
+ const forward=new Map(names.map((name,index)=>[name,`sw_tool_${index}`])),aliases={forward,reverse:new Map([...forward].map(([name,alias])=>[alias,name]))},history=nativeHistory(request,aliases);
+ const exposed=new Set(request.tools.map(t=>aliases.forward.get(t.name)));
+ const oldIDs=new Set(request.messages.flatMap(m=>m.content.filter(b=>b.type==="tool_use").map(b=>b.id)));
+ const calls=new Map(),rawChunks=new Map(),captures=new Set(),blocks=new Map(),content=[];let outputSize=0;
+ let host,session,consumer,error,ownedInterrupt=false,streamed=false,terminal=false,rawFinish,tokens,network=0,wake,quotaEvidence=false,contextEvidence=false,freeTierEvidence=false,eventsReady=false;
+ const eventAbort=new AbortController();
+ const notify=()=>{wake?.();wake=undefined};
+ const reject=(code="protocol_error")=>{error??=new BridgeError(code,502);notify()};
+ const aborted=()=>{reject("canceled");if(session)void host.session.interrupt({sessionID:session.id,resume:false}).catch(()=>{})};
+ signal.addEventListener("abort",aborted,{once:true});
+ const wait=async predicate=>{while(!predicate()){if(signal.aborted)throw abortError();if(error)throw error;await new Promise(resolve=>{wake=resolve;if(predicate()||error||signal.aborted)notify()})}if(error)throw error;};
+ const onControl=event=>{
+ if(event.type==="capture"){captures.add(event.id);notify()}
+ else if(event.type==="request"){network++;if(network!==1)reject("second_model_request")}
+ else if(event.type==="failure")reject(event.code);
+ else if(event.type==="evidence"){
+ if(event.code==="quota_exhausted")quotaEvidence=true;
+ if(event.code==="context_length_exceeded")contextEvidence=true;
+ if(event.code==="free_tier_restricted")freeTierEvidence=true;
+ }
+ };
+ const consume=e=>{
+ const d=e.data;if(!session||d?.sessionID!==session.id)return;
+ try {
+ if(e.type==="session.text.started"||e.type==="session.reasoning.started"){
+ const type=e.type.includes("reasoning")?"thinking":"text",key=`${type}:${d.ordinal}`;
+ if(blocks.has(key))fail("protocol_error");
+ const block={type,[type==="thinking"?"thinking":"text"]:""};blocks.set(key,block);content.push(block);
+ } else if(e.type==="session.text.delta"||e.type==="session.reasoning.delta") {
+ outputSize+=d.delta.length;if(outputSize>8000000)fail("protocol_error");
+ const type=e.type.includes("reasoning")?"thinking":"text",key=`${type}:${d.ordinal}`;
+ if(!blocks.has(key)){const block={type,[type==="thinking"?"thinking":"text"]:""};blocks.set(key,block);content.push(block)}
+ blocks.get(key)[type==="thinking"?"thinking":"text"]+=d.delta;
+ emit({type:type==="thinking"?"thinking_delta":"text_delta",text:d.delta});
+ } else if(e.type==="session.reasoning.ended") {
+ const sig=signature(d.native);if(sig){const b=blocks.get(`thinking:${d.ordinal}`);if(!b)fail("protocol_error");b.signature=sig;emit({type:"thinking_signature",text:sig})}
+ } else if(e.type==="session.tool.input.started") {
+ if(!exposed.has(d.name)||calls.has(d.id)||!d.id)fail("protocol_error");
+ let index=calls.size,id=`sdk_${request.request_id}_${index}`;
+ while(oldIDs.has(id))id=`sdk_${request.request_id}_${++index}`;
+ oldIDs.add(id);
+ const call={type:"tool_use",id,name:aliases.reverse.get(d.name)};calls.set(d.id,call);content.push(call);
+ } else if(e.type==="session.tool.input.delta") {
+ if(!calls.has(d.id)||calls.get(d.id).raw!==undefined)fail("protocol_error");
+ outputSize+=d.delta.length;if(outputSize>8000000)fail("protocol_error");
+ rawChunks.set(d.id,(rawChunks.get(d.id)??"")+d.delta);
+ } else if(e.type==="session.tool.input.ended") {
+ const call=calls.get(d.id);if(!call||call.raw!==undefined)fail("protocol_error");
+ if(rawChunks.has(d.id)&&rawChunks.get(d.id)!==d.text)fail("protocol_error");
+ rawObject(d.text);call.raw=d.text;
+ } else if(e.type==="session.tool.called") {if(d.executed!==false||!calls.has(d.id))fail("protocol_error");}
+ else if(e.type==="session.tool.failed") {if(!ownedInterrupt||d.error?.type!=="aborted")fail("protocol_error");}
+ else if(e.type==="session.step.streamed")streamed=true;
+ else if(e.type==="session.step.ended"||e.type==="session.step.failed") {
+ if(e.type.endsWith("failed")&&!ownedInterrupt)fail("sdk_failure");
+ tokens=usage(d.tokens);rawFinish=d.rawFinish??d.finish;
+ } else if(e.type==="session.execution.failed") {
+ reject(d.error?.message==="SECOND_MODEL_REQUEST_BLOCKED"?"second_model_request":"sdk_failure");terminal=true;
+ } else if(e.type==="session.execution.succeeded"||e.type==="session.execution.interrupted")terminal=true;
+ } catch(x) { reject(x.code); }
+ notify();
+ };
+ let result;
+ try {
+ if(signal.aborted)throw abortError();
+ host=await createNativeCLI({root,providerConfig,request,history,rawHistory,signal,onControl,tools:request.tools.map(t=>({name:aliases.forward.get(t.name),description:`Host tool: ${JSON.stringify(t.name)}. Use this function when host instructions refer to that tool.\n\n${t.description}`,input:t.input_schema}))});
+ const available=await host.model.list({location:{directory:host.directory}});
+ const model=available.data.find(m=>`${m.providerID}/${m.id}`===request.model);
+ if(!model||(!providerConfig&&(model.providerID!=="opencode"||!zeroPrice(model.cost))))fail("unknown_model");
+ if(model.limit?.output&&request.max_tokens>model.limit.output) {
+ // The requested upper bound is preserved when the model has a smaller hard limit.
+ request={...request,max_tokens:model.limit.output};
+ }
+ await host.setRequest(request);
+ const split=request.model.indexOf("/");
+ consumer=(async()=>{try{for await(const e of host.event.subscribe({signal:eventAbort.signal})){if(e.type==="server.connected"){eventsReady=true;notify()}else consume(e)}}catch{if(!eventAbort.signal.aborted)reject("sdk_failure")}})();
+ await wait(()=>eventsReady);
+ session=await host.session.create({location:{directory:host.directory},agent:"capture",model:{providerID:request.model.slice(0,split),id:request.model.slice(split+1)},permissions:rules,title:"ScopeWeaver model request"});
+ emit({type:"message_start"});
+ await host.session.prompt({sessionID:session.id,text:"ScopeWeaver authoritative context is supplied by the host."});
+ await wait(()=>streamed||terminal);
+ if(calls.size) {
+ await wait(()=>[...calls.entries()].every(([id,c])=>c.raw!==undefined&&captures.has(id))||terminal);
+ if(terminal||!calls.size)fail("protocol_error");
+ ownedInterrupt=true;
+ await host.session.interrupt({sessionID:session.id,resume:false});
+ }
+ await wait(()=>terminal);
+ if(!ownedInterrupt)await host.waitRunner();
+ if(!tokens||network!==1)fail("protocol_error");
+ const stop_reason=calls.size?"tool_use":["length","max_tokens","max-tokens"].includes(rawFinish)?"max_tokens":["stop","end_turn","end-turn"].includes(rawFinish)?"end_turn":undefined;
+ if(!stop_reason)fail("protocol_error");
+ result={message:{role:"assistant",content},stop_reason,usage:tokens};
+ } catch(e) { error=e instanceof BridgeError?e:new BridgeError(signal.aborted?"canceled":"sdk_failure",502); }
+ finally {
+ if(session){try{await host.session.interrupt({sessionID:session.id,resume:false});await host.session.wait({sessionID:session.id});await host.session.remove({sessionID:session.id})}catch{error??=new BridgeError("sdk_failure",502)}}
+ eventAbort.abort();if(consumer)await consumer;
+ if(host){try{await host.close()}catch{error??=new BridgeError("sdk_failure",502)}}
+ signal.removeEventListener("abort",aborted);
+ }
+ if(error){if(error.code!=="canceled"){if(quotaEvidence)throw new BridgeError("quota_exhausted",402);if(contextEvidence)throw new BridgeError("context_length_exceeded",400);if(freeTierEvidence)throw new BridgeError("free_tier_restricted",403)}throw error;}
+ if(signal.aborted)throw abortError();
+ // No tool request becomes visible to the host until native cleanup has completed.
+ for(const call of calls.values()){emit({type:"tool_use_start",tool_id:call.id,tool_name:call.name});emit({type:"tool_input_delta",text:call.raw})}
+ emit({type:"message_delta",stop_reason:result.stop_reason,usage:result.usage});emit({type:"message_stop"});
+ return result;
+}
diff --git a/adapters/opencode-sdk/src/protocol.mjs b/adapters/opencode-sdk/src/protocol.mjs
new file mode 100644
index 00000000..b3755000
--- /dev/null
+++ b/adapters/opencode-sdk/src/protocol.mjs
@@ -0,0 +1,160 @@
+export class BridgeError extends Error {
+ constructor(code, status = 400) { super(code); this.code = code; this.status = status; }
+}
+export const fail = (code, status) => { throw new BridgeError(code, status); };
+export const zeroPrice = cost => Array.isArray(cost)&&cost.length>0&&cost.every(c=>c?.input===0&&c?.output===0);
+export const messages = Object.freeze({
+ unauthorized: "Bridge authentication required.", forbidden: "Only non-browser loopback requests are accepted.",
+ invalid_request: "Invalid completion request.", unsupported_input: "This input cannot be represented safely by the SDK.",
+ unsupported_runtime: "Unsupported SDK/runtime pair or platform. Use a tested pinned runtime.",
+ unknown_model: "The selected model is unavailable in this bridge.", duplicate_request: "This request ID is already active.",
+ canceled: "The request was canceled.", timeout: "The SDK request timed out.",
+ sdk_failure: "The SDK request failed. Check the selected model and provider configuration.",
+ protocol_error: "The SDK response could not be represented safely.", configuration_error: "Invalid bridge provider configuration.",
+ second_model_request: "An unexpected additional SDK model request was blocked.",
+ quota_exhausted: "The upstream provider reported exhausted quota or billing credit.",
+ context_length_exceeded: "The upstream model reported that the context length was exceeded.",
+ free_tier_restricted: "The hosted free tier is restricted for this SDK runtime.",
+ not_found: "Route not found.", body_too_large: "Request body exceeds the bridge limit.",
+});
+export const publicError = error => {
+ const code = Object.hasOwn(messages, error?.code) ? error.code : "sdk_failure";
+ return { code, message: messages[code] };
+};
+export function safeJSON(text, code = "invalid_request") {
+ try {
+ return JSON.parse(text, (_, value) => {
+ if (typeof value === "number" && (!Number.isFinite(value) || (Number.isInteger(value) && !Number.isSafeInteger(value)))) throw Error();
+ if (value && typeof value === "object" && !Array.isArray(value) && Object.hasOwn(value, "__proto__")) throw Error();
+ return value;
+ });
+ } catch { fail(code); }
+}
+export function rawObject(text) {
+ if(typeof text!=="string"||text.length>2000000)fail("protocol_error");
+ let value;try{value=JSON.parse(text)}catch{fail("protocol_error")}
+ if(!value||typeof value!=="object"||Array.isArray(value))fail("protocol_error");
+ const errors=[],tree=parseTree(text,errors,{disallowComments:true,allowTrailingComma:false});
+ if(!tree||errors.length)fail("protocol_error");
+ const walk=node=>{if(node.type==="object"){const keys=new Set();for(const p of node.children??[]){const key=p.children[0].value;if(keys.has(key)||key==="__proto__")fail("protocol_error");keys.add(key)}}for(const c of node.children??[])walk(c)};walk(tree);
+ return value;
+}
+export function parseRequest(text) {
+ const errors=[],tree=parseTree(text,errors,{disallowComments:true,allowTrailingComma:false});
+ if(!tree||errors.length)fail("invalid_request");
+ const rawNodes=new Set(),rawHistory=Object.create(null);
+ // Tool arguments are authoritative raw JSON. Their JS projection is never executed.
+ let request;try{request=JSON.parse(text)}catch{fail("invalid_request")}
+ for(const [i,m]of (Array.isArray(request.messages)?request.messages:[]).entries())for(const [j,b]of (Array.isArray(m?.content)?m.content:[]).entries())if(b?.type==="tool_use"){
+ const node=findNodeAtLocation(tree,["messages",i,"content",j,"input"]);
+ if(node){rawNodes.add(node);rawHistory[b.id]=text.slice(node.offset,node.offset+node.length)}
+ }
+ const walk=(node,raw=false)=>{
+ raw=raw||rawNodes.has(node);
+ if(node.type==="object"){
+ const keys=new Set();for(const p of node.children??[]){const key=p.children[0].value;if(keys.has(key)||key==="__proto__")fail("invalid_request");keys.add(key)}
+ }
+ if(!raw&&node.type==="number"&&(!Number.isFinite(node.value)||(Number.isInteger(node.value)&&!Number.isSafeInteger(node.value))))fail("unsupported_input");
+ for(const c of node.children??[])walk(c,raw);
+ };walk(tree);
+ return {request:validateRequest(request),rawHistory};
+}
+const object = x => x && typeof x === "object" && !Array.isArray(x);
+const str = (x, max = 16*1024*1024) => typeof x === "string" && x.length <= max;
+const knownKeys = (x, allowed) => Object.keys(x).every(k => allowed.includes(k));
+export function validateRequest(x) {
+ if (!object(x) || !knownKeys(x, ["request_id","session_id","model","system","messages","tools","max_tokens","temperature","stop","thinking","thinking_type","reasoning_effort"])) fail("invalid_request");
+ if (!str(x.request_id, 128) || !/^[A-Za-z0-9_-]+$/.test(x.request_id) || !str(x.model, 512) || !/^[^/\s]+\/\S+$/.test(x.model)) fail("invalid_request");
+ if(x.session_id!==undefined&&(!str(x.session_id,256)||!x.session_id||/[\r\n\x00-\x1f\x7f]/.test(x.session_id)))fail("invalid_request");
+ if (!Array.isArray(x.system) || !x.system.every(s => str(s)) || !Array.isArray(x.messages) || !x.messages.length || x.messages.length > 1000 || !Array.isArray(x.tools) || x.tools.length > 128) fail("invalid_request");
+ if (!Number.isSafeInteger(x.max_tokens) || x.max_tokens < 1 || x.max_tokens > 1000000) fail("invalid_request");
+ if (x.temperature !== undefined && (typeof x.temperature !== "number" || !Number.isFinite(x.temperature) || x.temperature < 0 || x.temperature > 2)) fail("invalid_request");
+ if (x.stop !== undefined && (!Array.isArray(x.stop) || x.stop.length > 16 || !x.stop.every(s => str(s, 4096) && s.length))) fail("invalid_request");
+ for (const k of ["thinking", "thinking_type"]) if (x[k] !== undefined && !["", "enabled", "disabled"].includes(x[k])) fail("unsupported_input");
+ if (x.reasoning_effort !== undefined && !["", "none", "minimal", "low", "medium", "high", "xhigh", "max"].includes(x.reasoning_effort)) fail("unsupported_input");
+ const names = new Set();
+ for (const t of x.tools) {
+ if (!object(t) || !knownKeys(t,["name","description","input_schema"]) || !str(t.name, 512) || !t.name || names.has(t.name) || !str(t.description, 32000) || !object(t.input_schema)) fail("invalid_request");
+ schemaCheck(t.input_schema); names.add(t.name);
+ }
+ const calls = new Map(), results = new Set();
+ for (const m of x.messages) {
+ if (!object(m) || !knownKeys(m,["role","content"]) || !["assistant","user"].includes(m.role) || !Array.isArray(m.content)) fail("invalid_request");
+ for (const b of m.content) {
+ if (!object(b)) fail("invalid_request");
+ if (b.type === "text") { if (!knownKeys(b,["type","text"]) || !str(b.text)) fail("invalid_request"); }
+ else if (b.type === "thinking") { if (m.role !== "assistant" || !knownKeys(b,["type","thinking","signature"]) || !str(b.thinking) || (b.signature !== undefined && !str(b.signature))) fail("invalid_request"); }
+ else if (b.type === "tool_use") {
+ if (m.role !== "assistant" || !knownKeys(b,["type","id","name","input"]) || !str(b.id,256) || !b.id || calls.has(b.id) || !str(b.name,512) || !b.name || !object(b.input)) fail("invalid_request");
+ calls.set(b.id,b.name);
+ } else if (b.type === "tool_result") {
+ if (m.role !== "user" || !knownKeys(b,["type","tool_use_id","content","is_error"]) || !calls.has(b.tool_use_id) || results.has(b.tool_use_id) || !Array.isArray(b.content) || !b.content.every(c => object(c) && knownKeys(c,["type","text"]) && c.type === "text" && str(c.text)) || (b.is_error !== undefined && typeof b.is_error !== "boolean")) fail("unsupported_input");
+ results.add(b.tool_use_id);
+ } else fail("unsupported_input");
+ }
+ }
+ if ([...calls.keys()].some(id => !results.has(id))) fail("unsupported_input");
+ return x;
+}
+function schemaCheck(value, depth = 0, root = value) {
+ if (depth > 32) fail("unsupported_input");
+ if (Array.isArray(value)) { for (const v of value) schemaCheck(v, depth+1,root); }
+ else if (object(value)) {
+ if (Object.keys(value).length > 1000 || ["$dynamicRef", "$id"].some(k => Object.hasOwn(value,k))) fail("unsupported_input");
+ if(Object.hasOwn(value,"$ref")) {
+ if(typeof value.$ref!=="string"||(value.$ref!=="#"&&!value.$ref.startsWith("#/")))fail("unsupported_input");
+ let target=root;
+ for(const part of value.$ref==="#"?[]:value.$ref.slice(2).split("/")) {
+ if(/~(?![01])/.test(part))fail("unsupported_input");
+ const key=part.replaceAll("~1","/").replaceAll("~0","~");
+ if(!target||typeof target!=="object"||!Object.hasOwn(target,key))fail("unsupported_input");target=target[key];
+ }
+ }
+ for (const v of Object.values(value)) schemaCheck(v, depth+1,root);
+ }
+}
+export function aliasMap(request) {
+ const names = [...request.tools.map(t=>t.name), ...request.messages.flatMap(m=>m.content.filter(b=>b.type === "tool_use").map(b=>b.name))];
+ const unique=[...new Set(names)],valid=name=>/^[A-Za-z0-9_-]{1,64}$/.test(name),reserved=new Set(unique.filter(valid)),forward=new Map();let index=0;
+ for(const name of unique){let alias=name;if(!valid(name)){do{alias=`sw_tool_${index++}`}while(reserved.has(alias));reserved.add(alias)}forward.set(name,alias)}
+ return { forward, reverse: new Map([...forward].map(([k,v])=>[v,k])) };
+}
+export function nativeHistory(request, aliases) {
+ const names = new Map(); const out=[];
+ const thinking = request.thinking || request.thinking_type;
+ for (const m of request.messages) {
+ let content=[];
+ const flush = () => { if(content.length)out.push({role:m.role,content}); content=[]; };
+ for (const b of m.content) {
+ if(b.type === "text") content.push({type:"text",text:b.text});
+ else if(b.type === "thinking") { if(thinking !== "disabled")content.push({type:"reasoning",text:b.thinking,...(b.signature?{encrypted:b.signature}:{})}); }
+ else if(b.type === "tool_use") { const name=aliases.forward.get(b.name);names.set(b.id,name);content.push({type:"tool-call",id:b.id,name,input:b.input}); }
+ else { flush();out.push({role:"tool",content:[{type:"tool-result",id:b.tool_use_id,name:names.get(b.tool_use_id),result:{type:b.is_error?"error":"text",value:b.content.map(t=>t.text).join("\n")}}]}); }
+ }
+ flush();
+ }
+ return out;
+}
+export function serializeComplete(result) {
+ const blocks=result.message.content.map(b=>b.type === "tool_use"
+ ? `{"type":"tool_use","id":${JSON.stringify(b.id)},"name":${JSON.stringify(b.name)},"input":${b.raw}}`
+ : JSON.stringify(b));
+ return `{"message":{"role":"assistant","content":[${blocks.join(",")}]},"stop_reason":${JSON.stringify(result.stop_reason)},"usage":${JSON.stringify(result.usage)}}`;
+}
+export function validateProviderConfig(config) {
+ if (config === undefined) return undefined;
+ if (!object(config) || !knownKeys(config,["providers"]) || !object(config.providers) || !Object.keys(config.providers).length) fail("configuration_error");
+ for(const [id,p] of Object.entries(config.providers)) {
+ if(!/^[A-Za-z0-9_-]+$/.test(id) || !object(p) || !knownKeys(p,["name","package","settings","models"]) || p.package !== "@opencode/ai/providers/openai-compatible" || !object(p.settings) || !knownKeys(p.settings,["baseURL","apiKey","timeout"]) || !object(p.models))fail("configuration_error");
+ let url;try{url=new URL(p.settings.baseURL)}catch{fail("configuration_error")}
+ if(!["http:","https:"].includes(url.protocol)||url.username||url.password||url.search||url.hash)fail("configuration_error");
+ if(p.settings.apiKey!==undefined&&!str(p.settings.apiKey,8192))fail("configuration_error");
+ for(const [name,m] of Object.entries(p.models)) {
+ if(!name||!object(m)||!knownKeys(m,["name","limit","capabilities","compatibility"]))fail("configuration_error");
+ if(m.limit!==undefined&&(!object(m.limit)||!knownKeys(m.limit,["context","input","output"])||!Object.values(m.limit).every(n=>Number.isSafeInteger(n)&&n>0)))fail("configuration_error");
+ if(m.compatibility!==undefined&&(!object(m.compatibility)||!knownKeys(m.compatibility,["maxTokensField","reasoningField"])||(m.compatibility.maxTokensField!==undefined&&!["max_tokens","max_completion_tokens"].includes(m.compatibility.maxTokensField))))fail("configuration_error");
+ }
+ }
+ return structuredClone(config);
+}
+import { parseTree, findNodeAtLocation } from "jsonc-parser";
diff --git a/adapters/opencode-sdk/src/server.mjs b/adapters/opencode-sdk/src/server.mjs
new file mode 100644
index 00000000..e73dbf24
--- /dev/null
+++ b/adapters/opencode-sdk/src/server.mjs
@@ -0,0 +1,15 @@
+import { readFile } from "node:fs/promises";
+import { createBridge, MAX_REQUEST_BYTES } from "./bridge.mjs";
+import { safeJSON } from "./protocol.mjs";
+const host=process.env.SCOPEWEAVER_OPENCODE_HOST??"127.0.0.1";
+if(host!=="127.0.0.1")throw new Error("The OpenCode bridge must listen on 127.0.0.1");
+const port=Number(process.env.SCOPEWEAVER_OPENCODE_PORT??4097);
+if(!Number.isInteger(port)||port<1||port>65535)throw new Error("Invalid bridge port");
+const configPath=process.env.SCOPEWEAVER_OPENCODE_PROVIDER_CONFIG;
+const timeout=Number(process.env.SCOPEWEAVER_OPENCODE_TIMEOUT_MS??0);
+const bridge=createBridge({token:process.env.SCOPEWEAVER_OPENCODE_TOKEN,providerConfig:configPath?safeJSON(await readFile(configPath,"utf8"),"configuration_error"):undefined,runtime:process.env.SCOPEWEAVER_OPENCODE_RUNTIME??"native",timeout});
+const server=Bun.serve({hostname:host,port,idleTimeout:0,maxRequestBodySize:MAX_REQUEST_BYTES,fetch:req=>bridge.fetch(req)});
+console.log(`ScopeWeaver OpenCode SDK bridge listening on http://${host}:${server.port}`);
+let stopping=false;
+const stop=async()=>{if(stopping)return;stopping=true;server.stop();await bridge.close();process.exit(0)};
+process.on("SIGTERM",stop);process.on("SIGINT",stop);
diff --git a/adapters/opencode-sdk/src/worker.mjs b/adapters/opencode-sdk/src/worker.mjs
new file mode 100644
index 00000000..dc627774
--- /dev/null
+++ b/adapters/opencode-sdk/src/worker.mjs
@@ -0,0 +1,22 @@
+import { mkdtemp, rm } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { fail, publicError } from "./protocol.mjs";
+const abort=new AbortController();
+// This process handles one call. Set policy before lazily importing either SDK.
+const platformFetch=globalThis.fetch;
+globalThis.fetch=(input,init)=>platformFetch(input,{...init,redirect:"error"});
+process.on("SIGTERM",()=>abort.abort());process.on("SIGINT",()=>abort.abort());
+const write=value=>process.stdout.write(JSON.stringify(value)+"\n");
+let root;
+try {
+ const input=JSON.parse(await new Response(Bun.stdin.stream()).text());
+ root=input.root??await mkdtemp(join(tmpdir(),"scopeweaver-opencode-"));
+ const runtime=input.runtime??"native";let discover,complete;
+ if(runtime==="native"){const backend=await import("./native.mjs");discover=backend.discoverModels;complete=backend.completeNative}
+ else if(runtime==="legacy"){const backend=await import("./legacy.mjs");discover=backend.discoverLegacyModels;complete=backend.completeLegacy}
+ else fail("unsupported_runtime");
+ if(input.mode==="models")write({kind:"models",models:await discover({root,providerConfig:input.providerConfig})});
+ else write({kind:"result",result:await complete(input.request,{root,rawHistory:input.rawHistory,providerConfig:input.providerConfig,signal:abort.signal,emit:event=>write({kind:"event",event})})});
+} catch(e){write({kind:"error",...publicError(e)});process.exitCode=1;}
+finally{if(root)await rm(root,{recursive:true,force:true});}
diff --git a/adapters/opencode-sdk/test/legacy.test.mjs b/adapters/opencode-sdk/test/legacy.test.mjs
new file mode 100644
index 00000000..ee7d9e72
--- /dev/null
+++ b/adapters/opencode-sdk/test/legacy.test.mjs
@@ -0,0 +1,350 @@
+import { test, expect, beforeAll, afterAll } from "bun:test";
+import http from "node:http";
+import { mkdtemp, rm, readFile, mkdir, writeFile } from "node:fs/promises";
+import { createHash } from "node:crypto";
+import { fileURLToPath } from "node:url";
+import { execFileSync } from "node:child_process";
+import { tmpdir } from "node:os";
+import { existsSync } from "node:fs";
+import { completeLegacy, discoverLegacyModels } from "../src/legacy.mjs";
+import legacyPlugin from "../src/legacy-plugin.mjs";
+import { createBridge } from "../src/bridge.mjs";
+import { parseRequest } from "../src/protocol.mjs";
+let dependencySnapshot;
+async function dependencyDigest() {
+ const root = fileURLToPath(new URL("../", import.meta.url)), paths = [];
+ for await (const path of new Bun.Glob("**/package.json").scan({ cwd: `${root}/node_modules`, onlyFiles: true, followSymlinks: false })) paths.push(`node_modules/${path}`);
+ paths.push("package.json", "bun.lock"); paths.sort();
+ const hash = createHash("sha256");
+ for (const path of paths) { hash.update(path); hash.update(await readFile(`${root}/${path}`)); }
+ return hash.digest("hex");
+}
+beforeAll(async () => { dependencySnapshot = await dependencyDigest(); });
+afterAll(async () => { expect(await dependencyDigest()).toBe(dependencySnapshot); });
+function cliPIDs() {
+ return new Set(execFileSync("ps", ["-axo", "pid=,command="], { encoding: "utf8" }).split("\n")
+ .filter(line => /opencode-[^\s/]+\/bin\/opencode serve --hostname 127\.0\.0\.1/.test(line)).map(line => Number(line.trim().split(/\s+/)[0])));
+}
+function noNewCLI(before) { expect([...cliPIDs()].filter(pid => !before.has(pid))).toEqual([]); }
+const tool = { name: "host.read", description: "Capture only", input_schema: { type: "object", properties: { path: { type: "string" } }, required: ["path"], additionalProperties: false } };
+const request = (extra = {}) => ({ request_id: crypto.randomUUID(), session_id: "host-session", model: "fixture/test", system: ["HOST SYSTEM EXACT"], messages: [{ role: "user", content: [{ type: "text", text: "HOST USER EXACT" }] }], tools: [tool], max_tokens: 37, temperature: 0.2, stop: ["END"], thinking: "disabled", ...extra });
+
+async function fixture(mode, callback) {
+ const root = await mkdtemp(`${tmpdir()}/scopeweaver-test-legacy-`), requests = [], events = [];
+ let markStarted; const started = new Promise(resolve => { markStarted = resolve; });
+ const server = http.createServer(async (req, res) => {
+ let text = ""; for await (const chunk of req) text += chunk;
+ if (req.url === "/v1/models") { res.writeHead(200, { "content-type": "application/json" }); res.end('{"data":[{"id":"test"}]}'); return; }
+ const body = JSON.parse(text); requests.push(body); markStarted();
+ if (mode === "redirect" && req.url !== "/escaped") { res.writeHead(307, { location: `http://127.0.0.1:${server.address().port}/escaped` }); res.end(); return; }
+ if ([500, 429, 402].includes(mode)) { res.writeHead(mode, { "content-type": "application/json" }); res.end(mode === 402 ? '{"error":{"code":"insufficient_quota"}}' : '{"error":{"message":"fixture failure"}}'); return; }
+ if (mode === "quota429") { res.writeHead(429, { "content-type": "application/json" }); res.end('{"error":{"code":"insufficient_quota"}}'); return; }
+ if (["context400", "context413", "generic400"].includes(mode)) {
+ res.writeHead(mode === "context413" ? 413 : 400, { "content-type": "application/json" });
+ res.end(JSON.stringify({ error: mode === "context400" ? { code: "context_length_exceeded" } : mode === "context413" ? { type: "prompt_too_long" } : { code: "invalid_request_error", message: "context length exceeded" } })); return;
+ }
+ if (["freetier403", "generic403"].includes(mode)) {
+ res.writeHead(403, { "content-type": "application/json" });
+ res.end(JSON.stringify({ error: mode === "freetier403" ? { name: "FreeTierError", data: { message: "fixture restriction" } } : { code: "forbidden", message: "FreeTierError" } })); return;
+ }
+ if (mode === "hang") return;
+ res.writeHead(200, { "content-type": "text/event-stream" });
+ const emit = (delta, finish = null, usage) => res.write(`data: ${JSON.stringify({ id: "fixture", object: "chat.completion.chunk", created: 1, model: "test", choices: [{ index: 0, delta, finish_reason: finish }], ...(usage ? { usage } : {}) })}\n\n`);
+ emit({ role: "assistant", content: "native text" });
+ if (mode !== "text" && (requests.length === 1 || mode === "reuse")) {
+ const name = body.tools?.[0]?.function?.name;
+ const count = mode === "two" ? 2 : 1;
+ for (let i = 0; i < count; i++) emit({ tool_calls: [{ index: i, id: `native-call-${i}`, type: "function", function: { name: mode === "evil" ? "bash" : name, arguments: mode === "unsafe" ? '{"id":9223372036854775807}' : mode === "evil" ? JSON.stringify({ command: `touch '${root}/NATIVE_BASH_RAN'` }) : JSON.stringify({ path: `file-${i}.txt` }) } }] });
+ emit({}, "tool_calls", { prompt_tokens: 101, completion_tokens: 13, total_tokens: 114, prompt_tokens_details: { cached_tokens: 5 } });
+ } else emit({}, "stop", { prompt_tokens: 101, completion_tokens: 13, total_tokens: 114, prompt_tokens_details: { cached_tokens: 5 } });
+ res.end("data: [DONE]\n\n");
+ });
+ await mkdir(`${root}/.opencode/plugins`, { recursive: true });
+ await writeFile(`${root}/.opencode/plugins/hostile.mjs`, `import {writeFileSync} from 'node:fs';export default async()=>{writeFileSync(${JSON.stringify(`${root}/PROJECT_PLUGIN_RAN`)},'bad');return {}}`);
+ await new Promise(resolve => server.listen(0, "127.0.0.1", resolve));
+ const providerConfig = { providers: { fixture: { name: "Fixture", package: "@opencode/ai/providers/openai-compatible", settings: { baseURL: `http://127.0.0.1:${server.address().port}/v1`, apiKey: "fixture-no-secret" }, models: { test: { name: "Fixture model", limit: { context: 200000, output: 777 }, compatibility: { maxTokensField: "max_tokens" } } } } } };
+ try { return await callback({ root, providerConfig, requests, events, started, emit: e => events.push(e), signal: AbortSignal.timeout(45000) }); }
+ finally { server.closeAllConnections(); await new Promise(resolve => server.close(resolve)); const loaded = existsSync(`${root}/PROJECT_PLUGIN_RAN`); await rm(root, { recursive: true, force: true }); expect(loaded).toBe(false); }
+}
+
+test("legacy actual CLI captures two tools, streams, gates the second step and forwards frozen settings", async () => {
+ await fixture("two", async context => {
+ const result = await completeLegacy(request({ reasoning_effort: "low" }), context);
+ expect(context.requests).toHaveLength(1);
+ expect(context.requests[0]).toMatchObject({ max_tokens: 37, temperature: 0.2, stop: ["END"], thinking: { type: "disabled" }, reasoning_effort: "low" });
+ expect(context.requests[0].tools.map(t => t.function.name)).toEqual(["capture_host_0"]);
+ expect(context.requests[0].tools[0].function.description).toContain(tool.name);
+ expect(context.requests[0].tools[0].function.description).toContain(tool.description);
+ expect(JSON.stringify(context.requests[0].messages)).toContain("HOST USER EXACT");
+ expect(context.requests[0].messages.filter(message => message.role === "system").map(message => message.content).join("\n")).toBe("HOST SYSTEM EXACT");
+ expect(result.stop_reason).toBe("tool_use");
+ expect(result.message.content.filter(b => b.type === "tool_use").map(b => b.raw)).toEqual(['{"path":"file-0.txt"}', '{"path":"file-1.txt"}']);
+ expect(result.usage).toEqual({ input_tokens: 96, output_tokens: 13, cache_read_tokens: 5, cache_write_tokens: 0 });
+ expect(context.events.filter(e => e.type === "text_delta").map(e => e.text).join("")).toBe("native text");
+ expect(context.events.at(-1).type).toBe("message_stop");
+ });
+}, 30000);
+
+test("legacy discovers configured real models without inference and completes a tool-free turn", async () => {
+ await fixture("text", async context => {
+ expect(await discoverLegacyModels(context)).toEqual([{ id: "fixture/test", name: "Fixture model" }]);
+ expect(context.requests).toHaveLength(0);
+ const result = await completeLegacy(request({ tools: [] }), { ...context, root: `${context.root}/complete` });
+ expect(result.stop_reason).toBe("end_turn"); expect(context.requests).toHaveLength(1);
+ expect(context.requests[0].tools ?? []).toHaveLength(0);
+ });
+}, 30000);
+
+test("legacy preserves host call/result IDs and safe JSON history while rejecting unsafe numbers", async () => {
+ await fixture("text", async context => {
+ const messages = [{ role: "user", content: [{ type: "text", text: "old user" }] }, { role: "assistant", content: [{ type: "tool_use", id: "host-call", name: tool.name, input: { path: "old" } }] }, { role: "user", content: [{ type: "tool_result", tool_use_id: "host-call", is_error: true, content: [{ type: "text", text: "HOST REAL FAILURE" }] }] }];
+ await completeLegacy(request({ messages }), { ...context, rawHistory: { "host-call": '{"path":"old"}' } });
+ const wire = JSON.stringify(context.requests[0].messages);
+ expect(wire).toContain("host-call"); expect(wire).toContain("HOST REAL FAILURE");
+ await completeLegacy(request({ messages }), { ...context, root: `${context.root}/whitespace`, rawHistory: { "host-call": '{ "path": "old" }' } });
+ const call = context.requests[1].messages.flatMap(message => message.tool_calls ?? []).find(call => call.id === "host-call");
+ expect(JSON.parse(call.function.arguments)).toEqual({ path: "old" });
+ await expect(completeLegacy(request({ messages }), { ...context, root: `${context.root}/int64`, rawHistory: { "host-call": '{"id":9223372036854775807}' } })).rejects.toMatchObject({ code: "unsupported_input" });
+ expect(context.requests).toHaveLength(2);
+ });
+}, 30000);
+
+const safeHistoryEncodings = [
+ // Exact input object captured from product QA's real Go encoding/json request.
+ ["Go HTML escaping", String.raw`{"command":"printf 'gh49-allow-proof\\n' \u003e\u003e /tmp/scopeweaver-gh49-product-qa/app-legacy/allow.txt"}`, "printf 'gh49-allow-proof\\n' >> /tmp/scopeweaver-gh49-product-qa/app-legacy/allow.txt"],
+ ["whitespace and escaped Unicode", String.raw`{
+ "command" : "printf '\ud55c\uad6d\\n' \u003e\u003e /tmp/legacy-unicode.txt"
+ }`, "printf '한국\\n' >> /tmp/legacy-unicode.txt"],
+];
+for (const [name, raw, command] of safeHistoryEncodings) test(`legacy continues actual host history with ${name}`, async () => {
+ await fixture("text", async context => {
+ const input = { command }, callID = "real-go-bash-call";
+ const messages = [
+ { role: "user", content: [{ type: "text", text: "run the allowed command" }] },
+ { role: "assistant", content: [{ type: "tool_use", id: callID, name: "Bash", input }] },
+ { role: "user", content: [{ type: "tool_result", tool_use_id: callID, content: [{ type: "text", text: "REAL HOST RESULT" }] }] },
+ ];
+ const serialized = JSON.stringify(request({ messages, tools: [] })).replace(JSON.stringify(input), raw);
+ const parsed = parseRequest(serialized);
+ expect(parsed.rawHistory[callID]).toBe(raw);
+ const result = await completeLegacy(parsed.request, { ...context, rawHistory: parsed.rawHistory });
+ expect(result.stop_reason).toBe("end_turn"); expect(context.requests).toHaveLength(1);
+ const call = context.requests[0].messages.flatMap(message => message.tool_calls ?? []).find(call => call.id === callID);
+ expect(JSON.parse(call.function.arguments)).toEqual(input);
+ expect(JSON.parse(call.function.arguments).command).toBe(command);
+ expect(JSON.stringify(context.requests[0].messages)).toContain("REAL HOST RESULT");
+ });
+}, 30000);
+
+test("legacy rejects unsafe Go rawHistory numbers with zero model HTTP requests", async () => {
+ await fixture("text", async context => {
+ const input = { id: 1 }, callID = "go-int64-call";
+ const messages = [
+ { role: "user", content: [{ type: "text", text: "old user" }] },
+ { role: "assistant", content: [{ type: "tool_use", id: callID, name: tool.name, input }] },
+ { role: "user", content: [{ type: "tool_result", tool_use_id: callID, content: [{ type: "text", text: "old result" }] }] },
+ ];
+ const serialized = JSON.stringify(request({ messages })).replace(JSON.stringify(input), '{"id":9223372036854775807}');
+ const parsed = parseRequest(serialized);
+ await expect(completeLegacy(parsed.request, { ...context, rawHistory: parsed.rawHistory })).rejects.toMatchObject({ code: "unsupported_input" });
+ expect(context.requests).toHaveLength(0); expect(existsSync(`${context.root}/legacy`)).toBe(false);
+ });
+}, 30000);
+
+for (const thinking of ["enabled", "disabled"]) test(`legacy rejects opaque reasoning history before inference with thinking ${thinking}`, async () => {
+ await fixture("text", async context => {
+ const messages = [
+ { role: "user", content: [{ type: "text", text: "old user" }] },
+ { role: "assistant", content: [{ type: "thinking", thinking: "old thought", signature: "opaque-signature" }, { type: "text", text: "old answer" }] },
+ { role: "user", content: [{ type: "text", text: "continue" }] },
+ ];
+ let error;
+ try { await completeLegacy(request({ messages, tools: [], thinking }), context); } catch (caught) { error = caught; }
+ expect(context.requests).toHaveLength(0);
+ expect(existsSync(`${context.root}/legacy`)).toBe(false);
+ expect(error).toMatchObject({ code: "unsupported_input" });
+ });
+}, 30000);
+
+test("legacy keeps unsigned reasoning history supported through the actual converter", async () => {
+ await fixture("text", async context => {
+ const messages = [
+ { role: "user", content: [{ type: "text", text: "old user" }] },
+ { role: "assistant", content: [{ type: "thinking", thinking: "UNSIGNED HISTORY" }, { type: "text", text: "old answer" }] },
+ { role: "user", content: [{ type: "text", text: "continue" }] },
+ ];
+ const result = await completeLegacy(request({ messages, tools: [], thinking: "enabled" }), context);
+ expect(result.stop_reason).toBe("end_turn"); expect(context.requests).toHaveLength(1);
+ expect(context.requests[0].messages.some(message => message.role === "assistant" && message.reasoning_content === "UNSIGNED HISTORY")).toBe(true);
+ });
+}, 30000);
+
+test("legacy reuses a native call ID across two host turns without reusing host IDs", async () => {
+ await fixture("reuse", async context => {
+ const first = await completeLegacy(request(), context);
+ const call = first.message.content.find(block => block.type === "tool_use");
+ const next = request({ messages: [
+ ...request().messages,
+ { role: "assistant", content: [{ type: "tool_use", id: call.id, name: call.name, input: JSON.parse(call.raw) }] },
+ { role: "user", content: [{ type: "tool_result", tool_use_id: call.id, is_error: false, content: [{ type: "text", text: "REAL HOST RESULT" }] }] },
+ ] });
+ const second = await completeLegacy(next, { ...context, root: `${context.root}/second`, rawHistory: { [call.id]: call.raw } });
+ const nextCall = second.message.content.find(block => block.type === "tool_use");
+ expect(nextCall.id).not.toBe(call.id); expect(context.requests).toHaveLength(2);
+ expect(JSON.stringify(context.requests[1].messages)).toContain(call.id);
+ expect(JSON.stringify(context.requests[1].messages)).toContain("REAL HOST RESULT");
+ });
+}, 45000);
+
+for (const status of [500, 429]) test(`legacy ${status} cannot retry a second upstream network request`, async () => {
+ await fixture(status, async context => {
+ await expect(completeLegacy(request(), context)).rejects.toMatchObject({ code: "second_model_request" });
+ expect(context.requests).toHaveLength(1); expect(context.events.some(e => e.type === "tool_use_start")).toBe(false);
+ });
+}, 30000);
+
+test("legacy structured 402 quota remains distinct from generic 429", async () => {
+ await fixture(402, async context => {
+ await expect(completeLegacy(request(), context)).rejects.toMatchObject({ code: "quota_exhausted" });
+ expect(context.requests).toHaveLength(1);
+ });
+}, 30000);
+
+test("legacy retains structured 429 quota through the native retry boundary", async () => {
+ await fixture("quota429", async context => {
+ await expect(completeLegacy(request(), context)).rejects.toMatchObject({ code: "quota_exhausted" });
+ expect(context.requests).toHaveLength(1);
+ });
+}, 30000);
+
+for (const mode of ["context400", "context413"]) test(`legacy preserves structured ${mode} for host compaction`, async () => {
+ await fixture(mode, async context => {
+ await expect(completeLegacy(request(), context)).rejects.toMatchObject({ code: "context_length_exceeded", status: 400 });
+ expect(context.requests).toHaveLength(1); expect(context.events.some(event => event.type === "tool_use_start")).toBe(false);
+ });
+}, 30000);
+
+test("legacy does not classify generic 400 prose as context overflow", async () => {
+ await fixture("generic400", async context => {
+ await expect(completeLegacy(request(), context)).rejects.toMatchObject({ code: "sdk_failure" });
+ expect(context.requests).toHaveLength(1);
+ });
+}, 30000);
+
+test("legacy preserves structured FreeTierError as an explicit runtime restriction", async () => {
+ await fixture("freetier403", async context => {
+ await expect(completeLegacy(request(), context)).rejects.toMatchObject({ code: "free_tier_restricted", status: 403 });
+ expect(context.requests).toHaveLength(1); expect(context.events.some(event => event.type === "tool_use_start")).toBe(false);
+ });
+}, 30000);
+
+test("legacy does not classify generic 403 prose as a free tier restriction", async () => {
+ await fixture("generic403", async context => {
+ await expect(completeLegacy(request(), context)).rejects.toMatchObject({ code: "sdk_failure" });
+ expect(context.requests).toHaveLength(1);
+ });
+}, 30000);
+
+test("legacy retains redirect failure through both pre-network retry gates", async () => {
+ const root = await mkdtemp(`${tmpdir()}/scopeweaver-test-legacy-redirect-`), paths = [];
+ const server = http.createServer((req, res) => {
+ paths.push(req.url);
+ res.writeHead(307, { location: `http://127.0.0.1:${server.address().port}/escaped` }); res.end();
+ });
+ await new Promise(resolve => server.listen(0, "127.0.0.1", resolve));
+ try {
+ const state = `${root}/state.json`;
+ await writeFile(state, JSON.stringify({ provider: "fixture", request: request(), allowed: [], history: [] }));
+ const hooks = await legacyPlugin({}, { state }), config = {};
+ await hooks.config(config);
+ await hooks["chat.params"]({ sessionID: "redirect" }, { options: {} });
+ const fetch = config.provider.fixture.options.fetch;
+ await expect(fetch(`http://127.0.0.1:${server.address().port}/model`)).rejects.toMatchObject({ code: "UnexpectedRedirect" });
+ await expect(hooks["chat.params"]({ sessionID: "redirect" }, { options: {} })).rejects.toThrow("SCOPEWEAVER_LEGACY_REDIRECT");
+ await expect(fetch(`http://127.0.0.1:${server.address().port}/model`)).rejects.toThrow("SCOPEWEAVER_LEGACY_REDIRECT");
+ expect(paths).toEqual(["/model"]);
+ } finally {
+ server.closeAllConnections(); await new Promise(resolve => server.close(resolve));
+ await rm(root, { recursive: true, force: true });
+ }
+});
+
+test("legacy refuses model redirects before the redirected request leaves the CLI", async () => {
+ await fixture("redirect", async context => {
+ await expect(completeLegacy(request(), context)).rejects.toMatchObject({ code: "sdk_failure" });
+ expect(context.requests).toHaveLength(1);
+ expect(context.events.some(event => event.type === "tool_use_start")).toBe(false);
+ });
+}, 30000);
+
+test("legacy rejects an unadvertised native bash call without publishing host tools", async () => {
+ await fixture("evil", async context => {
+ await expect(completeLegacy(request(), context)).rejects.toMatchObject({ code: "protocol_error" });
+ expect(context.requests).toHaveLength(1); expect(context.events.some(e => e.type === "tool_use_start")).toBe(false);
+ expect(existsSync(`${context.root}/NATIVE_BASH_RAN`)).toBe(false);
+ });
+}, 30000);
+
+test("legacy cancellation aborts owned inference and concurrent peer remains isolated", async () => {
+ await Promise.all([fixture("hang", async context => {
+ const controller = new AbortController();
+ const completion = completeLegacy(request(), { ...context, signal: controller.signal });
+ await context.started;
+ controller.abort(); await expect(completion).rejects.toMatchObject({ code: "canceled" });
+ expect(context.requests).toHaveLength(1);
+ }), fixture("two", async context => {
+ const result = await completeLegacy(request({ max_tokens: 19 }), context);
+ expect(result.stop_reason).toBe("tool_use"); expect(context.requests[0].max_tokens).toBe(19); expect(context.requests).toHaveLength(1);
+ })]);
+}, 60000);
+
+test("legacy rejects unsafe numeric output before delivering tool calls", async () => {
+ await fixture("unsafe", async context => {
+ await expect(completeLegacy(request(), context)).rejects.toMatchObject({ code: "protocol_error" });
+ expect(context.events.some(e => e.type === "tool_use_start")).toBe(false); expect(context.requests).toHaveLength(1);
+ });
+}, 30000);
+
+test("legacy bridge preserves local schema refs and retains the host tool contract", async () => {
+ await fixture("two", async context => {
+ const token = "legacy-ref-token-01234567890123456789";
+ const bridge = createBridge({ token, runtime: "legacy", providerConfig: context.providerConfig });
+ try {
+ const schema = { type: "object", $defs: { path: { type: "string" } }, properties: { path: { $ref: "#/$defs/path" } }, required: ["path"], additionalProperties: false };
+ const response = await bridge.fetch(new Request("http://127.0.0.1:4097/v1/complete", { method: "POST", headers: { Authorization: `Bearer ${token}`, "content-type": "application/json" }, body: JSON.stringify(request({ tools: [{ ...tool, input_schema: schema }] })) }));
+ expect(response.status).toBe(200);
+ const result = await response.json();
+ expect(result.message.content.filter(block => block.type === "tool_use")).toHaveLength(2);
+ expect(context.requests[0].tools[0].function.parameters).toMatchObject(schema);
+ expect(context.requests).toHaveLength(1);
+ } finally { await bridge.close(); }
+ });
+}, 30000);
+
+test("legacy cancellation during startup cannot create an orphan CLI", async () => {
+ await fixture("hang", async context => {
+ const before = cliPIDs(), controller = new AbortController();
+ const pending = completeLegacy(request(), { ...context, signal: controller.signal });
+ setTimeout(() => controller.abort(), 20);
+ await expect(pending).rejects.toMatchObject({ code: "canceled" });
+ noNewCLI(before); expect(context.requests).toHaveLength(0);
+ });
+}, 30000);
+
+for (const mode of ["disconnect", "shutdown"]) test(`legacy bridge ${mode} stops a blocked model and owned CLI`, async () => {
+ await fixture("hang", async context => {
+ const before = cliPIDs(), token = "legacy-test-token-01234567890123456789";
+ const bridge = createBridge({ token, runtime: "legacy", providerConfig: context.providerConfig });
+ const response = await bridge.fetch(new Request("http://127.0.0.1:4097/v1/stream", { method: "POST", headers: { Authorization: `Bearer ${token}`, "content-type": "application/json" }, body: JSON.stringify(request()) }));
+ expect(response.status).toBe(200);
+ const reader = response.body.getReader();
+ try {
+ await reader.read(); await context.started;
+ expect([...cliPIDs()].filter(pid => !before.has(pid)).length).toBeGreaterThan(0);
+ if (mode === "disconnect") await reader.cancel();
+ await bridge.close();
+ if (mode === "shutdown") await reader.cancel();
+ noNewCLI(before); expect(context.requests).toHaveLength(1);
+ } finally { await reader.cancel().catch(() => {}); await bridge.close(); }
+ });
+}, 30000);
diff --git a/adapters/opencode-sdk/test/native.test.mjs b/adapters/opencode-sdk/test/native.test.mjs
new file mode 100644
index 00000000..e62920c0
--- /dev/null
+++ b/adapters/opencode-sdk/test/native.test.mjs
@@ -0,0 +1,234 @@
+import { describe, test, expect } from "bun:test";
+import { mkdtemp, stat, rm, writeFile, symlink } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { createBridge, MAX_REQUEST_BYTES, workerEnvironment } from "../src/bridge.mjs";
+import { aliasMap, nativeHistory, safeJSON, validateRequest, zeroPrice } from "../src/protocol.mjs";
+
+import { assertNoUnmanagedConfig, unmanagedConfigPaths } from "../src/native-cli.mjs";
+
+const token="test-bridge-token-with-at-least-32-characters";
+const auth={authorization:`Bearer ${token}`,"content-type":"application/json"};
+const request=(changes={})=>({request_id:crypto.randomUUID(),model:"fixture/model",system:["Exact host system", "Second segment"],messages:[{role:"user",content:[{type:"text",text:"host question"}]}],tools:[],max_tokens:64,thinking:"disabled",...changes});
+const tool=(name="read/file")=>({name,description:"Host owned action",input_schema:{type:"object",properties:{n:{type:"number"},text:{type:"string"}},required:["n","text"],additionalProperties:false}});
+const fetchBridge=(bridge,path,body,extra={})=>bridge.fetch(new Request(`http://127.0.0.1:4097${path}`,{method:body?"POST":"GET",headers:auth,...extra,...(body?{body:JSON.stringify(body)}:{})}));
+async function fixture(mode="text",providerID="fixture",timeout=20000) {
+ const requests=[],headers=[],hits=[];let canceled=false;
+ const root=await mkdtemp(join(tmpdir(),"gh49-native-test-")),marker=join(root,"MUST_NOT_EXIST");
+ const server=Bun.serve({hostname:"127.0.0.1",port:0,async fetch(req){
+ hits.push(req.url);if(req.method!=="POST")return new Response("redirect reached",{status:500});
+ const body=await req.json();requests.push(body);headers.push(Object.fromEntries(req.headers));
+ if(mode==="redirect")return Response.redirect(`http://127.0.0.1:${server.port}/redirect-target`,302);
+ if(mode==="quota")return Response.json({error:{code:"insufficient_quota",message:"secret billing details"}},{status:402});
+ if(mode==="rate")return Response.json({error:{code:"rate_limit_exceeded",message:"transient"}},{status:429});
+ if(mode==="context")return Response.json({error:{code:"context_length_exceeded",message:"private request details"}},{status:400});
+ if(mode==="generic400")return Response.json({error:{code:"invalid_request_error",message:"context_length_exceeded quota"}},{status:400});
+ if(mode==="free-tier")return Response.json({error:{name:"FreeTierError",data:{message:"private policy details"}}},{status:403});
+ if(mode==="generic403")return Response.json({error:{code:"permission_denied",message:"FreeTierError quota"}},{status:403});
+ if(mode==="failure"||mode==="retry")return new Response("untrusted secret provider error",{status:mode==="retry"?500:401});
+ if(mode==="hang"||(mode==="selective"&&body.messages.some(m=>m.role==="user"&&m.content.includes("please hang"))))return new Response(new ReadableStream({cancel(){canceled=true}}),{headers:{"content-type":"text/event-stream"}});
+ const finish=mode==="multi"&&body.messages.at(-1)?.content==="finish";
+ const names=(body.tools??[]).filter(t=>t.function.description?.startsWith("Host tool: ")).map(t=>t.function.name);
+ const calls=mode==="unknown"?[{id:"evil",name:"shell",raw:JSON.stringify({command:`touch ${marker}`})}]:mode==="unsafe"?[{id:"call1",name:names[0],raw:'{"n":9007199254740993,"text":"x"}'}]:mode==="badjson"?[{id:"call1",name:names[0],raw:'{"n":1,"text":"x",}'}]:mode==="schema"?[{id:"call1",name:names[0],raw:'{"n":"bad","text":"x"}'}]:(mode==="tools"||(mode==="multi"&&!finish))?[{id:"call1",name:names[0],raw:'{ "n": 1.0, "text": "한글" }'},{id:"call2",name:names[1],raw:'{"n":2,"text":"second"}'}]:[];
+ const chunks=[{choices:[{index:0,delta:{role:"assistant",reasoning_content:"native thought"},finish_reason:null}]},{choices:[{index:0,delta:{content:mode==="oversize"?"x".repeat(8000001):"native answer"},finish_reason:null}]}];
+ for(const [index,call] of calls.entries()) {
+ chunks.push({choices:[{index:0,delta:{tool_calls:[{index,id:call.id,type:"function",function:{name:call.name,arguments:call.raw.slice(0,5)}}]},finish_reason:null}]});
+ chunks.push({choices:[{index:0,delta:{tool_calls:[{index,function:{arguments:call.raw.slice(5)}}]},finish_reason:null}]});
+ }
+ chunks.push({choices:[{index:0,delta:{},finish_reason:calls.length?"tool_calls":"stop"}],usage:{prompt_tokens:101,completion_tokens:23,total_tokens:124,prompt_tokens_details:{cached_tokens:5,cache_write_tokens:3}}});
+ if(mode==="gzip"){const sse=chunks.map(c=>`data: ${JSON.stringify({id:"fixture",object:"chat.completion.chunk",created:1,model:"model",...c})}\n\n`).join("")+"data: [DONE]\n\n";return new Response(Bun.gzipSync(sse),{headers:{"content-type":"text/event-stream","content-encoding":"gzip"}})}
+ return new Response(new ReadableStream({async start(output){try{for(const c of chunks){output.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({id:"fixture",object:"chat.completion.chunk",created:1,model:"model",...c})}\n\n`));await new Promise(r=>setTimeout(r,5))}output.enqueue(new TextEncoder().encode("data: [DONE]\n\n"));output.close()}catch{}},cancel(){canceled=true}}),{headers:{"content-type":"text/event-stream"}});
+ }});
+ const providerConfig={providers:{[providerID]:{name:"Fixture",package:"@opencode/ai/providers/openai-compatible",settings:{baseURL:`http://127.0.0.1:${server.port}/v1`,apiKey:"explicit-fixture-only"},models:{model:{name:"Fixture model",capabilities:{tools:true},limit:{context:200000,output:32768},compatibility:{maxTokensField:"max_tokens"}}}}}};
+ const bridge=createBridge({token,providerConfig,timeout});
+ return {bridge,requests,headers,hits,marker,get canceled(){return canceled},async close(){await bridge.close();server.stop(true);await rm(root,{recursive:true,force:true})}};
+}
+
+describe("bridge boundary",()=>{
+ test("native CLI refuses unmanaged configuration entries, including broken symlinks",async()=>{
+ const root=await mkdtemp(join(tmpdir(),"native-config-policy-"));try{
+ expect(unmanagedConfigPaths("linux")).toEqual(["/etc/opencode"]);
+ const absent=join(root,"absent");await assertNoUnmanagedConfig([absent]);
+ const file=join(root,"managed");await writeFile(file,"{}");await expect(assertNoUnmanagedConfig([file])).rejects.toMatchObject({code:"configuration_error"});
+ if(process.platform!=="win32"){const link=join(root,"broken");await symlink(absent,link);await expect(assertNoUnmanagedConfig([link])).rejects.toMatchObject({code:"configuration_error"})}
+ }finally{await rm(root,{recursive:true,force:true})}
+ });
+ test("auth, browser origin, loopback, runtime and malformed input fail before worker",async()=>{
+ let invoked=0;const bridge=createBridge({token,worker:async()=>{invoked++;return []}});
+ expect((await bridge.fetch(new Request("http://127.0.0.1/v1/capabilities"))).status).toBe(401);
+ expect((await fetchBridge(bridge,"/v1/capabilities",null,{headers:{...auth,origin:"null"}})).status).toBe(403);
+ expect((await bridge.fetch(new Request("http://example.com/v1/capabilities",{headers:auth}))).status).toBe(403);
+ expect((await fetchBridge(bridge,"/v1/capabilities?x=1")).status).toBe(403);
+ expect((await fetchBridge(bridge,"/v1/complete",request({max_tokens:0}))).status).toBe(400);
+ expect((await fetchBridge(bridge,"/v1/capabilities")).status).toBe(200);
+ expect((await (await fetchBridge(bridge,"/v1/capabilities")).json()).runtime).toBe("native");
+ expect((await bridge.fetch(new Request("http://127.0.0.1/v1/complete",{method:"POST",headers:{...auth,"content-length":String(MAX_REQUEST_BYTES+1)},body:"{}"}))).status).toBe(413);
+ const legacy=createBridge({token,runtime:"legacy1",worker:async()=>{invoked++}});
+ expect((await fetchBridge(legacy,"/v1/models")).status).toBe(400);
+ expect(invoked).toBe(0);await bridge.close();await legacy.close();
+ });
+ test("unsafe history numbers and unsupported references fail closed",()=>{
+ expect(()=>safeJSON('{"n":9007199254740993}')).toThrow();
+ expect(()=>validateRequest(request({tools:[{...tool(),input_schema:{$ref:"https://example.com/schema"}}]}))).toThrow();
+ expect(()=>validateRequest(request({messages:[{role:"user",content:[{type:"image"}]}]}))).toThrow();
+ });
+ test("unknown and empty pricing never imply free eligibility",()=>{
+ for(const c of [undefined,[],[{}],[{input:0}],[{input:0,output:1}]])expect(zeroPrice(c)).toBe(false);
+ expect(zeroPrice([{input:0,output:0}])).toBe(true);
+ });
+ test("worker environments never inherit cloud credentials, proxies or user config",()=>{
+ const env=workerEnvironment("/tmp/owned-worker",{PATH:"/bin",LANG:"en_US.UTF-8",OPENCODE_API_KEY:"private",AWS_SECRET_ACCESS_KEY:"private",HOME:"/real-home",HTTP_PROXY:"http://proxy",BUN_OPTIONS:"--preload unwanted",NODE_OPTIONS:"--require unwanted",XDG_CONFIG_HOME:"/user-config"});
+ expect(env.PATH).toBe("/bin");expect(env.LANG).toBe("en_US.UTF-8");expect(env.XDG_CONFIG_HOME).toBe("/tmp/owned-worker/xdg-config");
+ for(const key of ["OPENCODE_API_KEY","AWS_SECRET_ACCESS_KEY","HOME","HTTP_PROXY","BUN_OPTIONS","NODE_OPTIONS"])expect(env[key]).toBeUndefined();
+ });
+ test("valid host names remain meaningful in tool-free history and aliases never collide",()=>{
+ const input=request({tools:[tool("Bash"),tool("set_goals"),tool("sw_tool_0"),tool("read/file")]});const aliases=aliasMap(input);
+ expect(aliases.forward.get("Bash")).toBe("Bash");expect(aliases.forward.get("set_goals")).toBe("set_goals");expect(aliases.forward.get("sw_tool_0")).toBe("sw_tool_0");expect(aliases.forward.get("read/file")).toBe("sw_tool_1");
+ });
+ test("a four-MiB host context fits the documented input boundary",async()=>{
+ let length=0;const bridge=createBridge({token,worker:async input=>{length=input.request.system[0].length;return {message:{role:"assistant",content:[{type:"text",text:"ok"}]},stop_reason:"end_turn",usage:{input_tokens:0,output_tokens:0,cache_read_tokens:0,cache_write_tokens:0}}}});
+ try{expect((await fetchBridge(bridge,"/v1/complete",request({system:["x".repeat(4*1024*1024)]}))).status).toBe(200);expect(length).toBe(4*1024*1024)}finally{await bridge.close()}
+ });
+ test("host tool correlation and roles stay exact in native history",()=>{
+ const input=validateRequest(request({tools:[tool()],messages:[{role:"user",content:[{type:"text",text:"question"}]},{role:"assistant",content:[{type:"thinking",thinking:"old thought",signature:"old-signature"},{type:"tool_use",id:"old-id",name:"read/file",input:{n:1,text:"old"}}]},{role:"user",content:[{type:"tool_result",tool_use_id:"old-id",content:[{type:"text",text:"host result"}],is_error:true},{type:"text",text:"continue"}]}]}));
+ const native=nativeHistory(input,aliasMap(input));
+ expect(native[1].content).toEqual([{type:"tool-call",id:"old-id",name:"sw_tool_0",input:{n:1,text:"old"}}]);
+ expect(native[2]).toEqual({role:"tool",content:[{type:"tool-result",id:"old-id",name:"sw_tool_0",result:{type:"error",value:"host result"}}]});
+ expect(native[3]).toEqual({role:"user",content:[{type:"text",text:"continue"}]});
+ });
+});
+
+describe("actual native 2.1.4 runtime",()=>{
+ test("model list is inference-free; exact system/history/settings and cache usage survive",async()=>{
+ const f=await fixture();try{
+ const models=await (await fetchBridge(f.bridge,"/v1/models")).json();expect(models.models).toEqual([{id:"fixture/model",name:"Fixture model"}]);expect(f.requests).toHaveLength(0);
+ const res=await fetchBridge(f.bridge,"/v1/complete",request({temperature:0.25,stop:["STOP"]}));const body=await res.json();
+ expect(res.status).toBe(200);expect(body.stop_reason).toBe("end_turn");expect(body.usage).toEqual({input_tokens:93,output_tokens:23,cache_read_tokens:5,cache_write_tokens:3});
+ expect(f.requests).toHaveLength(1);const wire=f.requests[0];expect(wire.max_tokens).toBe(64);expect(wire.max_completion_tokens).toBeUndefined();expect(wire.temperature).toBe(0.25);expect(wire.stop).toEqual(["STOP"]);expect(wire.thinking).toEqual({type:"disabled"});
+ expect(wire.messages[0].role).toBe("system");expect(wire.messages[0].content.startsWith("Exact host system\n\nSecond segment\n")).toBe(true);expect(wire.messages[0].content).toContain("Here is some useful information about the environment");expect(wire.messages.slice(1)).toEqual([{role:"user",content:"host question"}]);expect(wire.tools.map(t=>t.function.name)).toContain("shell");expect(wire.tools.filter(t=>t.function.description?.startsWith("Host tool: "))).toEqual([]);
+ expect(f.headers[0]["x-opencode-session"]).toBeUndefined();expect(f.headers[0].authorization).toBe("Bearer explicit-fixture-only");expect(f.headers[0]["x-scopeweaver-egress"]).toBeUndefined();expect(f.headers[0]["x-scopeweaver-upstream"]).toBeUndefined();
+ }finally{await f.close()}
+ });
+ test("native egress forwards compressed SSE without a second content decoding",async()=>{
+ const f=await fixture("gzip");try{
+ const res=await fetchBridge(f.bridge,"/v1/complete",request()),body=await res.json();expect(res.status).toBe(200);expect(body.message.content.find(b=>b.type==="text").text).toBe("native answer");expect(f.requests).toHaveLength(1);
+ }finally{await f.close()}
+ });
+ test("multiple capture tools retain names/order/raw arguments; NDJSON stops once after cleanup",async()=>{
+ const f=await fixture("tools");try{
+ const input=request({tools:[tool("read/file"),tool("other.tool")]}),res=await fetchBridge(f.bridge,"/v1/stream",input);
+ const events=(await res.text()).trim().split("\n").map(JSON.parse);
+ expect(events.find(e=>e.type==="error")).toBeUndefined();expect(events.filter(e=>e.type==="message_stop")).toHaveLength(1);
+ expect(events.filter(e=>e.type==="tool_use_start").map(e=>[e.tool_id,e.tool_name])).toEqual([[`sdk_${input.request_id}_0`,"read/file"],[`sdk_${input.request_id}_1`,"other.tool"]]);
+ expect(events.filter(e=>e.type==="tool_input_delta").map(e=>e.text)).toEqual(['{ "n": 1.0, "text": "한글" }','{"n":2,"text":"second"}']);
+ expect(events.find(e=>e.type==="thinking_delta").text).toBe("native thought");expect(events.find(e=>e.type==="text_delta").text).toBe("native answer");expect(f.requests).toHaveLength(1);
+ const hostTools=f.requests[0].tools.filter(t=>t.function.description?.startsWith("Host tool: "));expect(hostTools.map(t=>t.function.name)).toEqual(["sw_tool_0","sw_tool_1"]);
+ expect(hostTools[0].function.description).toContain('Host tool: "read/file"');expect(hostTools[0].function.description.endsWith(tool().description)).toBe(true);
+ }finally{await f.close()}
+ });
+ test("complete retains raw numeric argument representation",async()=>{
+ const f=await fixture("tools");try{const res=await fetchBridge(f.bridge,"/v1/complete",request({tools:[tool(),tool("second")]}));const raw=await res.text();expect(res.status).toBe(200);expect(raw).toContain('"input":{ "n": 1.0, "text": "한글" }')}finally{await f.close()}
+ });
+ for(const mode of ["unknown","schema","badjson","failure","retry","redirect","oversize"])test(`${mode} fails without host tool delivery or native action`,async()=>{
+ const f=await fixture(mode);try{
+ const events=(await (await fetchBridge(f.bridge,"/v1/stream",request({tools:[tool()]}))).text()).trim().split("\n").map(JSON.parse);
+ expect(events.some(e=>e.type==="error")).toBe(true);expect(events.some(e=>e.type==="tool_use_start"||e.type==="message_stop")).toBe(false);
+ expect(f.requests).toHaveLength(1);expect(f.hits).toHaveLength(1);expect(await stat(f.marker).then(()=>true,()=>false)).toBe(false);expect(JSON.stringify(events)).not.toContain("untrusted secret provider error");
+ }finally{await f.close()}
+ });
+ test("concurrent per-call settings and tool catalogs are isolated",async()=>{
+ const f=await fixture();try{
+ const responses=await Promise.all([fetchBridge(f.bridge,"/v1/complete",request({max_tokens:37,temperature:0.1})),fetchBridge(f.bridge,"/v1/complete",request({max_tokens:73,temperature:0.9,thinking:"enabled",reasoning_effort:"low",tools:[tool()]}))]);
+ expect(responses.map(r=>r.status)).toEqual([200,200]);expect(f.requests.map(r=>r.max_tokens).sort((a,b)=>a-b)).toEqual([37,73]);expect(f.requests.find(r=>r.max_tokens===37).temperature).toBe(0.1);expect(f.requests.find(r=>r.max_tokens===73).temperature).toBe(0.9);expect(f.requests.find(r=>r.max_tokens===73).tools.filter(t=>t.function.description?.startsWith("Host tool: "))).toHaveLength(1);expect(f.requests.find(r=>r.max_tokens===37).tools.filter(t=>t.function.description?.startsWith("Host tool: "))).toHaveLength(0);expect(f.requests.find(r=>r.max_tokens===73).reasoning_effort).toBe("low");
+ }finally{await f.close()}
+ });
+ test("DELETE aborts an active SDK worker; peer request remains usable",async()=>{
+ const f=await fixture("selective");try{
+ const input=request({messages:[{role:"user",content:[{type:"text",text:"please hang"}]}]}),res=await fetchBridge(f.bridge,"/v1/stream",input);const reader=res.body.getReader();await reader.read();
+ expect((await fetchBridge(f.bridge,"/v1/complete",request())).status).toBe(200);
+ expect((await fetchBridge(f.bridge,`/v1/requests/${input.request_id}`,null,{method:"DELETE"})).status).toBe(200);
+ let output="";while(true){const x=await reader.read();if(x.done)break;output+=new TextDecoder().decode(x.value)}expect(output).toContain('"type":"error"');expect(output).not.toContain('"type":"message_stop"');
+ expect((await fetchBridge(f.bridge,"/v1/capabilities")).status).toBe(200);
+ }finally{await f.close()}
+ });
+ test("host tool-result history is injected without native conversation growth",async()=>{
+ const f=await fixture();try{
+ const req=request({tools:[tool()],messages:[{role:"user",content:[{type:"text",text:"first"}]},{role:"assistant",content:[{type:"tool_use",id:"prior",name:"read/file",input:{n:7,text:"old"}}]},{role:"user",content:[{type:"tool_result",tool_use_id:"prior",content:[{type:"text",text:"host result"}]},{type:"text",text:"next"}]}]});
+ const res=await fetchBridge(f.bridge,"/v1/complete",req);expect(res.status).toBe(200);
+ const wire=f.requests[0].messages;expect(wire).toHaveLength(5);expect(wire[2].tool_calls[0]).toEqual({id:"prior",type:"function",function:{name:"sw_tool_0",arguments:'{"n":7,"text":"old"}'}});expect(wire[3]).toEqual({role:"tool",tool_call_id:"prior",content:"host result"});expect(wire[4]).toEqual({role:"user",content:"next"});
+ }finally{await f.close()}
+ });
+ test("unknown explicit model is rejected before inference",async()=>{
+ const f=await fixture();try{expect((await fetchBridge(f.bridge,"/v1/complete",request({model:"fixture/missing"}))).status).toBe(400);expect(f.requests).toHaveLength(0)}finally{await f.close()}
+ });
+ test("HTTP response cancellation cleans an owned SDK worker",async()=>{
+ const f=await fixture("hang");try{const res=await fetchBridge(f.bridge,"/v1/stream",request());const reader=res.body.getReader();await reader.read();await reader.cancel();await f.bridge.close()}finally{await f.close()}
+ });
+ test("gateway session header stays stable across fresh native sessions",async()=>{
+ const f=await fixture("text","opencode-go");try{
+ const a=request({model:"opencode-go/model",session_id:"host-session"}),b=request({model:"opencode-go/model",session_id:"host-session"});expect(a.request_id).not.toBe(b.request_id);
+ expect((await fetchBridge(f.bridge,"/v1/complete",a)).status).toBe(200);expect((await fetchBridge(f.bridge,"/v1/complete",b)).status).toBe(200);
+ expect(f.headers.map(h=>h["x-opencode-session"])).toEqual(["host-session","host-session"]);
+ expect(f.headers.map(h=>h["user-agent"])).toEqual([f.headers[0]["user-agent"],f.headers[0]["user-agent"]]);
+ }finally{await f.close()}
+ });
+ test("OpenCode provider preserves its genuine native session identity",async()=>{
+ const f=await fixture("text","opencode");try{
+ expect((await fetchBridge(f.bridge,"/v1/complete",request({model:"opencode/model",session_id:"host-session"}))).status).toBe(200);
+ expect(f.headers[0]["x-opencode-session"]).not.toBe("host-session");expect(f.headers[0]["x-opencode-session"]).toStartWith("ses_");
+ expect(f.headers[0]["user-agent"]).toBe("opencode/latest/2.1.4/cli");expect(f.headers[0]["x-opencode-client"]).toBe("cli");
+ }finally{await f.close()}
+ });
+ test("64-bit tool IDs retain authoritative raw arguments in stream and complete",async()=>{
+ const f=await fixture("unsafe");try{
+ const response=await fetchBridge(f.bridge,"/v1/complete",request({tools:[tool()]}));expect(response.status).toBe(200);expect(await response.text()).toContain('"input":{"n":9007199254740993,"text":"x"}');
+ const events=(await (await fetchBridge(f.bridge,"/v1/stream",request({tools:[tool()]}))).text()).trim().split("\n").map(JSON.parse);expect(events.find(e=>e.type==="tool_input_delta").text).toBe('{"n":9007199254740993,"text":"x"}');expect(events.filter(e=>e.type==="message_stop")).toHaveLength(1);
+ }finally{await f.close()}
+ });
+ test("64-bit host history is restored exactly before the native HTTP request",async()=>{
+ const f=await fixture();try{
+ const input=request({tools:[tool()],messages:[{role:"assistant",content:[{type:"tool_use",id:"old64",name:"read/file",input:{n:0,text:"raw"}}]},{role:"user",content:[{type:"tool_result",tool_use_id:"old64",content:[{type:"text",text:"host result"}]}]}]});
+ const raw=JSON.stringify(input).replace('"n":0','"n":9007199254740993');
+ const res=await f.bridge.fetch(new Request("http://127.0.0.1:4097/v1/complete",{method:"POST",headers:auth,body:raw}));expect(res.status).toBe(200);expect(f.requests[0].messages[1].tool_calls[0].function.arguments).toBe('{"n":9007199254740993,"text":"raw"}');
+ }finally{await f.close()}
+ });
+ test("local JSON Pointer refs, defs and unions work in actual native tool schemas",async()=>{
+ const f=await fixture("tools");try{
+ const first=tool();first.input_schema={$defs:{Number:{anyOf:[{type:"number"},{type:"string"}]}},type:"object",properties:{n:{$ref:"#/$defs/Number"},text:{type:"string"}},required:["n","text"],additionalProperties:false};
+ const res=await fetchBridge(f.bridge,"/v1/complete",request({tools:[first,tool("other")]}));expect(res.status).toBe(200);expect(f.requests[0].tools.find(t=>t.function.name==="sw_tool_0").function.parameters).toEqual(first.input_schema);
+ }finally{await f.close()}
+ });
+ test("structured quota exhaustion is distinct from transient rate limiting",async()=>{
+ for(const mode of ["quota","rate"]){const f=await fixture(mode);try{
+ const res=await fetchBridge(f.bridge,"/v1/complete",request()),body=await res.json();
+ expect(body.error.code==="quota_exhausted").toBe(mode==="quota");if(mode==="quota")expect(res.status).toBe(402);
+ expect(JSON.stringify(body)).not.toContain("secret billing details");expect(f.requests).toHaveLength(1);
+ }finally{await f.close()}}
+ });
+ test("reused upstream call IDs remain unique across real host tool turns",async()=>{
+ const f=await fixture("multi");try{
+ const history=[{role:"user",content:[{type:"text",text:"host question"}]}],allIDs=[];
+ for(const text of ["continue","finish"]){
+ const input=request({messages:structuredClone(history),tools:[tool(),tool("other")]}),response=await fetchBridge(f.bridge,"/v1/complete",input),result=await response.json();expect(response.status).toBe(200);
+ const calls=result.message.content.filter(b=>b.type==="tool_use");expect(calls).toHaveLength(2);allIDs.push(...calls.map(c=>c.id));
+ history.push(result.message,{role:"user",content:[...calls.map(c=>({type:"tool_result",tool_use_id:c.id,content:[{type:"text",text:"host approved result"}]})),{type:"text",text}]});
+ }
+ expect(new Set(allIDs).size).toBe(4);
+ const last=await fetchBridge(f.bridge,"/v1/complete",request({messages:history,tools:[tool(),tool("other")]}));expect(last.status).toBe(200);expect((await last.json()).stop_reason).toBe("end_turn");expect(f.requests).toHaveLength(3);
+ expect(f.requests[2].messages.filter(m=>m.role==="tool").map(m=>m.tool_call_id)).toEqual(allIDs);
+ }finally{await f.close()}
+ });
+ test("an explicit deadline is bounded independently of host cancellation",async()=>{
+ const f=await fixture("hang","fixture",1000);try{const res=await fetchBridge(f.bridge,"/v1/complete",request());expect(res.status).toBe(504);expect((await res.json()).error.code).toBe("timeout")}finally{await f.close()}
+ });
+ test("structured context overflow stays distinct from quota and generic bad requests",async()=>{
+ for(const mode of ["context","generic400"]){const f=await fixture(mode);try{
+ const res=await fetchBridge(f.bridge,"/v1/complete",request()),body=await res.json();expect(body.error.code==="context_length_exceeded").toBe(mode==="context");expect(body.error.code).not.toBe("quota_exhausted");if(mode==="context")expect(res.status).toBe(400);expect(f.requests).toHaveLength(1);
+ }finally{await f.close()}}
+ });
+ test("structured hosted free-tier restriction stays distinct from generic forbidden responses",async()=>{
+ for(const mode of ["free-tier","generic403"]){const f=await fixture(mode);try{
+ const res=await fetchBridge(f.bridge,"/v1/complete",request()),body=await res.json();expect(body.error.code==="free_tier_restricted").toBe(mode==="free-tier");expect(body.error.code).not.toBe("quota_exhausted");if(mode==="free-tier")expect(res.status).toBe(403);expect(f.requests).toHaveLength(1);expect(JSON.stringify(body)).not.toContain("private policy details");
+ }finally{await f.close()}}
+ });
+});
diff --git a/agent/provider.go b/agent/provider.go
index 39a46e17..9246f0bd 100644
--- a/agent/provider.go
+++ b/agent/provider.go
@@ -10,6 +10,7 @@ package agent
import (
"bytes"
"context"
+ "errors"
"fmt"
"github.com/Autumn-27/artex/locale"
"io"
@@ -24,6 +25,7 @@ import (
"github.com/Autumn-27/artex/llmnet"
"github.com/Autumn-27/artex/llmrec"
"github.com/Autumn-27/artex/ollama"
+ "github.com/Autumn-27/artex/opencodesdk"
"github.com/Autumn-27/norma/agentcore"
"github.com/Autumn-27/norma/compaction"
"github.com/Autumn-27/norma/llm"
@@ -185,7 +187,9 @@ func FromEnv() (Config, bool) {
// Native Ollama never falls back to a cloud credential.
_, explicitKey := runtimeconfig.LLMEnv("API_KEY")
if c.APIKey == "" && !explicitKey {
- if prov == "opencode-go" {
+ if prov == opencodesdk.Format {
+ c.APIKey = os.Getenv("SCOPEWEAVER_OPENCODE_TOKEN")
+ } else if prov == "opencode-go" {
// The wire format resolved to "openai", but the credential is the
// OpenCode subscription key — never OPENAI_API_KEY.
c.APIKey = os.Getenv("OPENCODE_API_KEY")
@@ -232,6 +236,11 @@ func ConfigFrom(provider, model, baseURL, apiKey, proxy string) Config {
Stream: true, // Streaming defaults on; callers override from the profile.
}
switch strings.TrimSpace(provider) {
+ case opencodesdk.Format:
+ c.Format = llm.Format(opencodesdk.Format)
+ if c.BaseURL == "" {
+ c.BaseURL = opencodesdk.DefaultBaseURL
+ }
case "ollama":
c.Format = llm.Format("ollama")
c.BaseURL = ollama.NormalizeBaseURL(c.BaseURL)
@@ -297,6 +306,8 @@ func isFalsy(s string) bool {
// Provider returns the short provider name ("anthropic"/"openai").
func (c Config) Provider() string {
switch c.Format {
+ case llm.Format(opencodesdk.Format):
+ return opencodesdk.Format
case llm.Format("ollama"):
return "ollama"
case llm.FormatOpenAI:
@@ -315,6 +326,23 @@ func (c Config) NewProvider() (llm.Provider, error) {
if err != nil {
return nil, err
}
+ if c.Provider() == opencodesdk.Format {
+ if !opencodesdk.ValidModel(c.Model) {
+ return nil, locale.NewError("OpenCode SDK model must be providerID/modelID")
+ }
+ policy := func() bool {
+ if c.PolicyResolver != nil {
+ strict, _ := c.PolicyResolver()
+ return strict
+ }
+ return c.LocalLLMOnly
+ }
+ p, err := opencodesdk.New(c.BaseURL, c.APIKey, c.Model, opencodesdk.Options{Client: client, LocalOnly: policy, ThinkingType: c.ThinkingType, ReasoningEffort: c.ReasoningEffort})
+ if err != nil {
+ return nil, err
+ }
+ return llm.RateLimited(p, llm.RateLimit{PerSecond: c.RatePerSecond, PerMinute: c.RatePerMinute}), nil
+ }
if c.Provider() == "ollama" {
native, err := ollama.NewClient(c.BaseURL, c.APIKey, client)
if err != nil {
@@ -366,6 +394,18 @@ func (c Config) NewProvider() (llm.Provider, error) {
return provider, nil
}
+// IsQuotaExhaustedError trusts structured SDK evidence and preserves other providers' existing message rules.
+func IsQuotaExhaustedError(err error) bool {
+ if err == nil {
+ return false
+ }
+ var bridge *opencodesdk.BridgeError
+ if errors.As(err, &bridge) {
+ return bridge.QuotaExhausted()
+ }
+ return IsQuotaExhaustedMessage(err.Error())
+}
+
// IsQuotaExhaustedMessage deliberately recognizes only explicit balance,
// billing, credit, or quota-exhaustion signals. Generic 429/rate-limit text,
// authentication failures, network errors, and server failures are excluded.
@@ -417,6 +457,7 @@ type quotaAwareTransport struct {
// carries; its value is the session id read from the request context. Empty
// disables it. See Config.SessionHeaderKey.
sessionHeaderKey string
+ preserveStatus bool
}
func (t quotaAwareTransport) RoundTrip(req *http.Request) (*http.Response, error) {
@@ -471,7 +512,7 @@ func (t quotaAwareTransport) RoundTrip(req *http.Request) (*http.Response, error
}}
}
- if resp.StatusCode != http.StatusTooManyRequests {
+ if resp.StatusCode != http.StatusTooManyRequests || t.preserveStatus {
return resp, nil
}
body, readErr := io.ReadAll(resp.Body)
@@ -509,6 +550,9 @@ func requestBodySnapshot(req *http.Request) string {
// Ready allows keyless native Ollama and explicit local compatible servers.
func (c Config) Ready() bool {
+ if c.Provider() == opencodesdk.Format {
+ return strings.TrimSpace(c.APIKey) != ""
+ }
return c.APIKey != "" || c.Provider() == "ollama" || llmnet.IsLocalEndpoint(c.BaseURL)
}
@@ -516,7 +560,13 @@ func (c Config) Ready() bool {
func (c Config) HTTPClient() (*http.Client, error) {
var client *http.Client
var err error
- if c.PolicyResolver != nil {
+ if c.Provider() == opencodesdk.Format {
+ if c.Proxy != "" {
+ return nil, locale.NewError("OpenCode SDK bridge proxy settings are not allowed")
+ }
+ // Pin bridge sockets to loopback even if localhost DNS changes; downstream policy is checked separately.
+ client, err = llmnet.NewClient("", true, nil)
+ } else if c.PolicyResolver != nil {
client, err = llmnet.NewClientWithPolicy(c.Proxy, c.PolicyResolver)
} else {
client, err = llmnet.NewClient(c.Proxy, c.LocalLLMOnly, c.LocalLLMAllowedCIDRs)
@@ -528,7 +578,7 @@ func (c Config) HTTPClient() (*http.Client, error) {
if c.officialClaude55() {
replay = &claudeReplay{}
}
- client.Transport = quotaAwareTransport{base: client.Transport, sessionHeaderKey: strings.TrimSpace(c.SessionHeaderKey), claude: replay}
+ client.Transport = quotaAwareTransport{base: client.Transport, sessionHeaderKey: strings.TrimSpace(c.SessionHeaderKey), claude: replay, preserveStatus: c.Provider() == opencodesdk.Format}
return client, nil
}
@@ -595,12 +645,16 @@ func TestConnection(ctx context.Context, c Config) (time.Duration, string, error
// yielding finish=length and confusing interruption/resume indications despite
// transport success. A sufficient budget lets the model emit OK and stop cleanly.
// Keep EscalateMaxTokens false to avoid wasteful retry/resume loops after truncation.
+ maxTokens := 8192
+ if c.Provider() == opencodesdk.Format && c.MaxTokens > 0 {
+ maxTokens = c.MaxTokens
+ }
reply, err := agentcore.Run(ctx, agentcore.Options{
Provider: prov,
SystemPrompt: []string{locale.Text(locale.FromContext(ctx), "This is a connection test. Output exactly the two characters OK, with no reasoning, explanation, or other content.")},
PermissionMode: acperm.ModeBypass,
MaxTurns: 1,
- MaxTokens: 8192,
+ MaxTokens: maxTokens,
NonStreaming: !c.Stream, // Test using this profile's actual streaming/non-streaming mode.
}, "ping")
lat := time.Since(start)
diff --git a/agent/provider_sdk_test.go b/agent/provider_sdk_test.go
new file mode 100644
index 00000000..d4c8811f
--- /dev/null
+++ b/agent/provider_sdk_test.go
@@ -0,0 +1,210 @@
+package agent
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "sync/atomic"
+ "testing"
+
+ "github.com/Autumn-27/artex/opencodesdk"
+ "github.com/Autumn-27/norma/llm"
+)
+
+func TestConfigFromOpenCodeSDK(t *testing.T) {
+ c := ConfigFrom(" opencode-sdk ", "opencode/test/model", "", "bridge-token", "")
+ if c.Provider() != opencodesdk.Format || c.Format != llm.Format(opencodesdk.Format) || c.BaseURL != opencodesdk.DefaultBaseURL || c.Model != "opencode/test/model" || !c.Ready() {
+ t.Fatalf("config=%+v", c)
+ }
+ c.APIKey = ""
+ if c.Ready() {
+ t.Fatal("SDK must require its bridge token even on loopback")
+ }
+}
+
+func TestSDKConnectionHonorsConfiguredTokenLimit(t *testing.T) {
+ for _, stream := range []bool{false, true} {
+ for _, tc := range []struct {
+ name, provider string
+ configured, want int
+ }{
+ {"SDK configured", opencodesdk.Format, 1024, 1024},
+ {"SDK unspecified", opencodesdk.Format, 0, 8192},
+ {"SDK nonpositive", opencodesdk.Format, -1, 8192},
+ {"OpenAI default unchanged", "openai", 1024, 8192},
+ } {
+ t.Run(fmt.Sprintf("%s/stream=%v", tc.name, stream), func(t *testing.T) {
+ var calls, maxTokens atomic.Int32
+ bridge := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Header.Get("Authorization") != "Bearer bridge-token" {
+ t.Error("connection request did not use its dedicated token")
+ http.Error(w, "unauthorized", http.StatusUnauthorized)
+ return
+ }
+ if r.URL.Path == "/v1/capabilities" {
+ fmt.Fprint(w, `{"protocol":"scopeweaver-opencode-sdk","version":1,"runtime":"native","sdk_version":"2.1.4","tool_execution":"capture-only","streaming":true}`)
+ return
+ }
+ if r.Method == http.MethodDelete {
+ return
+ }
+ var body struct {
+ MaxTokens int `json:"max_tokens"`
+ }
+ if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
+ t.Error(err)
+ http.Error(w, "invalid request", http.StatusBadRequest)
+ return
+ }
+ calls.Add(1)
+ maxTokens.Store(int32(body.MaxTokens))
+ if tc.provider == opencodesdk.Format {
+ if stream {
+ w.Header().Set("Content-Type", "application/x-ndjson")
+ fmt.Fprintln(w, `{"type":"message_start"}`)
+ fmt.Fprintln(w, `{"type":"text_delta","text":"OK"}`)
+ fmt.Fprintln(w, `{"type":"message_delta","stop_reason":"end_turn","usage":{"input_tokens":1,"output_tokens":1,"cache_read_tokens":0,"cache_write_tokens":0}}`)
+ fmt.Fprintln(w, `{"type":"message_stop"}`)
+ } else {
+ fmt.Fprint(w, `{"message":{"role":"assistant","content":[{"type":"text","text":"OK"}]},"stop_reason":"end_turn","usage":{"input_tokens":1,"output_tokens":1,"cache_read_tokens":0,"cache_write_tokens":0}}`)
+ }
+ } else if stream {
+ w.Header().Set("Content-Type", "text/event-stream")
+ fmt.Fprint(w, "data: {\"choices\":[{\"delta\":{\"content\":\"OK\"},\"finish_reason\":null}]}\n\ndata: {\"choices\":[{\"delta\":{},\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":1,\"completion_tokens\":1}}\n\ndata: [DONE]\n\n")
+ } else {
+ fmt.Fprint(w, `{"choices":[{"message":{"role":"assistant","content":"OK"},"finish_reason":"stop"}],"usage":{"prompt_tokens":1,"completion_tokens":1}}`)
+ }
+ }))
+ defer bridge.Close()
+ cfg := ConfigFrom(tc.provider, "fixture/model", bridge.URL, "bridge-token", "")
+ cfg.Stream, cfg.MaxTokens = stream, tc.configured
+ _, reply, err := TestConnection(t.Context(), cfg)
+ if err != nil || reply != "OK" || calls.Load() != 1 || int(maxTokens.Load()) != tc.want {
+ t.Fatalf("reply=%q error=%v inference calls=%d max_tokens=%d want 1/%d", reply, err, calls.Load(), maxTokens.Load(), tc.want)
+ }
+ })
+ }
+ }
+}
+func TestFromEnvOpenCodeSDKTokenIsolation(t *testing.T) {
+ for _, tc := range []struct {
+ name, modern, dedicated string
+ explicit bool
+ want string
+ }{{"dedicated only", "", "dedicated-token", false, "dedicated-token"}, {"generic override", "generic-token", "dedicated-token", true, "generic-token"}, {"explicit blank suppresses fallback", "", "dedicated-token", true, ""}, {"never cloud fallback", "", "", false, ""}} {
+ t.Run(tc.name, func(t *testing.T) {
+ isolateLLMEnvironment(t)
+ t.Setenv("SCOPEWEAVER_LLM_PROVIDER", opencodesdk.Format)
+ t.Setenv("SCOPEWEAVER_LLM_MODEL", "opencode/test")
+ t.Setenv("SCOPEWEAVER_OPENCODE_TOKEN", tc.dedicated)
+ t.Setenv("OPENAI_API_KEY", "cloud-openai")
+ t.Setenv("OPENCODE_API_KEY", "cloud-go")
+ if tc.explicit {
+ t.Setenv("SCOPEWEAVER_LLM_API_KEY", tc.modern)
+ }
+ cfg, ok := FromEnv()
+ if cfg.APIKey != tc.want || ok != (tc.want != "") {
+ t.Fatalf("key=%q ready=%v want=%q", cfg.APIKey, ok, tc.want)
+ }
+ })
+ }
+}
+func TestOpenCodeSDKPolicyAndProxy(t *testing.T) {
+ c := ConfigFrom(opencodesdk.Format, "opencode/test", "", "bridge-token", "http://127.0.0.1:9999")
+ if _, err := c.NewProvider(); err == nil {
+ t.Fatal("SDK accepted bridge proxy")
+ }
+ c.Proxy = ""
+ var strict atomic.Bool
+ strict.Store(true)
+ c.PolicyResolver = func() (bool, []string) { return strict.Load(), nil }
+ if _, err := c.NewProvider(); err == nil || !strings.Contains(err.Error(), "local_llm_only") {
+ t.Fatalf("policy error=%v", err)
+ }
+ strict.Store(false)
+ p, err := c.NewProvider()
+ if err != nil {
+ t.Fatal(err)
+ }
+ strict.Store(true)
+ if _, _, _, err = p.Complete(t.Context(), llm.CompletionRequest{}); err == nil || !strings.Contains(err.Error(), "local_llm_only") {
+ t.Fatalf("cached provider ignored policy change: %v", err)
+ }
+}
+
+func TestSDKQuotaClassificationUsesStructuredEvidence(t *testing.T) {
+ cases := []struct {
+ name string
+ status int
+ code, message string
+ quota bool
+ }{
+ {"genuine quota", 402, "quota_exhausted", "Account allowance unavailable", true},
+ {"generic payment failure", 402, "sdk_failure", "quota exceeded", false},
+ {"transient rate limit", 429, "rate_limit", "quota exceeded RPM", false},
+ {"untrusted 429 quota code", 429, "quota_exhausted", "quota exhausted", false},
+ {"auth", 401, "quota_exhausted", "quota exhausted", false},
+ {"forbidden", 403, "quota_exhausted", "quota exhausted", false},
+ {"server failure", 503, "quota_exhausted", "quota exhausted", false},
+ }
+ for _, stream := range []bool{false, true} {
+ for _, tc := range cases {
+ if stream && tc.status != 402 {
+ continue
+ }
+ t.Run(fmt.Sprintf("%v/%s", stream, tc.name), func(t *testing.T) {
+ b := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path == "/v1/capabilities" {
+ fmt.Fprint(w, `{"protocol":"scopeweaver-opencode-sdk","version":1,"runtime":"native","sdk_version":"2.1.4","tool_execution":"capture-only","streaming":true}`)
+ return
+ }
+ if r.Method == "DELETE" {
+ return
+ }
+ if stream {
+ w.Header().Set("Content-Type", "application/x-ndjson")
+ _ = json.NewEncoder(w).Encode(map[string]any{"type": "error", "code": tc.code, "message": tc.message})
+ } else {
+ w.WriteHeader(tc.status)
+ _ = json.NewEncoder(w).Encode(map[string]any{"error": map[string]any{"code": tc.code, "message": tc.message}})
+ }
+ }))
+ defer b.Close()
+ c := ConfigFrom(opencodesdk.Format, "opencode/test", b.URL, "bridge-token", "")
+ p, err := c.NewProvider()
+ if err != nil {
+ t.Fatal(err)
+ }
+ var callErr error
+ if stream {
+ for _, e := range p.Stream(t.Context(), llm.CompletionRequest{MaxTokens: 64}) {
+ if e != nil {
+ callErr = e
+ }
+ }
+ } else {
+ _, _, _, callErr = p.Complete(t.Context(), llm.CompletionRequest{MaxTokens: 64})
+ }
+ if callErr == nil || IsQuotaExhaustedError(callErr) != tc.quota {
+ t.Fatalf("quota=%v error=%v want=%v", IsQuotaExhaustedError(callErr), callErr, tc.quota)
+ }
+ var bridge *opencodesdk.BridgeError
+ if !errors.As(callErr, &bridge) {
+ t.Fatalf("lost structured bridge error: %v", callErr)
+ }
+ if !stream && bridge.Status != tc.status {
+ t.Fatalf("transport rewrote status %d to %d", tc.status, bridge.Status)
+ }
+ })
+ }
+ }
+ for _, model := range []string{"", "model", "p/", "/m", "p//m", "p/m m"} {
+ c := ConfigFrom(opencodesdk.Format, model, "", "token", "")
+ if _, err := c.NewProvider(); err == nil {
+ t.Fatalf("configured invalid SDK model %q", model)
+ }
+ }
+}
diff --git a/build.sh b/build.sh
index be0789de..1a3c4e7d 100755
--- a/build.sh
+++ b/build.sh
@@ -283,6 +283,9 @@ package_binary() {
mkdir -p "$package_root/adapters/agent"
cp adapters/agent/package.json adapters/agent/package-lock.json adapters/agent/README.md adapters/agent/pi-extension.js "$package_root/adapters/agent/"
cp -R adapters/agent/src adapters/agent/test "$package_root/adapters/agent/"
+ mkdir -p "$package_root/adapters/opencode-sdk"
+ cp adapters/opencode-sdk/package.json adapters/opencode-sdk/bun.lock adapters/opencode-sdk/README*.md "$package_root/adapters/opencode-sdk/"
+ cp -R adapters/opencode-sdk/src adapters/opencode-sdk/test "$package_root/adapters/opencode-sdk/"
mkdir -p "$package_root/log"
cp log/changelog-*.md "$package_root/log/"
(cd "$ARTEX_PACKAGE_DIR" && zip -q -r -9 "$(basename "$archive")" "$(basename "$package_root")")
diff --git a/db/config.go b/db/config.go
index eb979cd9..0d54320f 100644
--- a/db/config.go
+++ b/db/config.go
@@ -6,6 +6,7 @@ import (
"encoding/json"
"github.com/Autumn-27/artex/llmnet"
"github.com/Autumn-27/artex/locale"
+ "github.com/Autumn-27/artex/opencodesdk"
"time"
)
@@ -152,6 +153,9 @@ ORDER BY is_default DESC, priority DESC, id ASC`)
if err := scanProfile(rows, &p.APIKey, &p); err != nil {
return nil, err
}
+ if p.Format == opencodesdk.Format && p.APIKey == "" {
+ continue
+ }
if p.APIKey != "" || p.Format == "ollama" || llmnet.IsLocalEndpoint(p.BaseURL) {
out = append(out, &p)
}
@@ -161,7 +165,13 @@ ORDER BY is_default DESC, priority DESC, id ASC`)
// SaveProfile inserts (id==0) or updates a profile. Empty apiKey on update keeps existing.
func (d *DB) SaveProfile(p *LLMProfile) (int64, error) {
+ if p.Format == opencodesdk.Format && !opencodesdk.ValidModel(p.Model) {
+ return 0, locale.NewError("OpenCode SDK model must be providerID/modelID")
+ }
hint := p.APIKeyHint
+ if p.APIKey != "" {
+ hint = ""
+ }
if len(p.APIKey) >= 4 {
hint = "…" + p.APIKey[len(p.APIKey)-4:]
}
@@ -175,10 +185,27 @@ VALUES ($1,$2,NULLIF($3,''),NULLIF($4,''),$5,NULLIF($6,''),NULLIF($7,''),$8,$9,$
return id, err
}
if p.APIKey == "" {
- _, err := d.Exec(`UPDATE llm_profiles SET name=$1,format=$2,api_key=CASE WHEN $2='ollama' AND format<>'ollama' THEN NULL ELSE api_key END,api_key_hint=CASE WHEN $2='ollama' AND format<>'ollama' THEN NULL ELSE api_key_hint END,base_url=NULLIF($3,''),proxy=NULLIF($4,''),model=$5,rate_per_second=$6,rate_per_minute=$7,context_window_k=$8,reasoning_effort=$9,priority=$10,pool_exclude=$11,thinking_type=$12,streaming=$13,max_tokens=$14,max_tokens_field=$15,session_header_key=$16,retry_connect_attempts=$17,retry_connect_interval_ms=$18,retry_empty_attempts=$19,retry_empty_interval_ms=$20,retry_stream_attempts=$21,retry_stream_interval_ms=$22,keep_alive=$23,llm_max_concurrent=$24 WHERE id=$25`,
+ // Lock the credential scope and update key/hint together; concurrent edits cannot reuse a token from another bridge.
+ tx, err := d.Begin()
+ if err != nil {
+ return p.ID, err
+ }
+ defer tx.Rollback()
+ var oldFormat, oldURL string
+ if err = tx.QueryRow(`SELECT format,COALESCE(base_url,'') FROM llm_profiles WHERE id=$1 FOR UPDATE`, p.ID).Scan(&oldFormat, &oldURL); err != nil {
+ if err == sql.ErrNoRows {
+ return p.ID, nil
+ }
+ return p.ID, err
+ }
+ clearSDK := (oldFormat == opencodesdk.Format || p.Format == opencodesdk.Format) && (oldFormat != p.Format || !opencodesdk.SameEndpoint(oldURL, p.BaseURL))
+ _, err = tx.Exec(`UPDATE llm_profiles SET name=$1,format=$2,api_key=CASE WHEN $26 OR ($2='ollama' AND format<>'ollama') THEN NULL ELSE api_key END,api_key_hint=CASE WHEN $26 OR ($2='ollama' AND format<>'ollama') THEN NULL ELSE api_key_hint END,base_url=NULLIF($3,''),proxy=NULLIF($4,''),model=$5,rate_per_second=$6,rate_per_minute=$7,context_window_k=$8,reasoning_effort=$9,priority=$10,pool_exclude=$11,thinking_type=$12,streaming=$13,max_tokens=$14,max_tokens_field=$15,session_header_key=$16,retry_connect_attempts=$17,retry_connect_interval_ms=$18,retry_empty_attempts=$19,retry_empty_interval_ms=$20,retry_stream_attempts=$21,retry_stream_interval_ms=$22,keep_alive=$23,llm_max_concurrent=$24 WHERE id=$25`,
p.Name, p.Format, p.BaseURL, p.Proxy, p.Model, p.RatePerSecond, p.RatePerMinute, p.ContextWindowK, p.ReasoningEffort, p.Priority, p.PoolExclude, p.ThinkingType, p.Streaming, p.MaxTokens, p.MaxTokensField, p.SessionHeaderKey,
- r.Connect.Attempts, r.Connect.IntervalMS, r.Empty.Attempts, r.Empty.IntervalMS, r.Stream.Attempts, r.Stream.IntervalMS, p.KeepAlive, p.MaxConcurrent, p.ID)
- return p.ID, err
+ r.Connect.Attempts, r.Connect.IntervalMS, r.Empty.Attempts, r.Empty.IntervalMS, r.Stream.Attempts, r.Stream.IntervalMS, p.KeepAlive, p.MaxConcurrent, p.ID, clearSDK)
+ if err != nil {
+ return p.ID, err
+ }
+ return p.ID, tx.Commit()
}
_, err := d.Exec(`UPDATE llm_profiles SET name=$1,format=$2,base_url=NULLIF($3,''),proxy=NULLIF($4,''),model=$5,api_key=$6,api_key_hint=$7,rate_per_second=$8,rate_per_minute=$9,context_window_k=$10,reasoning_effort=$11,priority=$12,pool_exclude=$13,thinking_type=$14,streaming=$15,max_tokens=$16,max_tokens_field=$17,session_header_key=$18,retry_connect_attempts=$19,retry_connect_interval_ms=$20,retry_empty_attempts=$21,retry_empty_interval_ms=$22,retry_stream_attempts=$23,retry_stream_interval_ms=$24,keep_alive=$25,llm_max_concurrent=$26 WHERE id=$27`,
p.Name, p.Format, p.BaseURL, p.Proxy, p.Model, p.APIKey, hint, p.RatePerSecond, p.RatePerMinute, p.ContextWindowK, p.ReasoningEffort, p.Priority, p.PoolExclude, p.ThinkingType, p.Streaming, p.MaxTokens, p.MaxTokensField, p.SessionHeaderKey,
diff --git a/db/config_sdk_test.go b/db/config_sdk_test.go
new file mode 100644
index 00000000..c4602f66
--- /dev/null
+++ b/db/config_sdk_test.go
@@ -0,0 +1,94 @@
+package db
+
+import (
+ "fmt"
+ "testing"
+)
+
+func TestSDKProfileCredentialTransitions(t *testing.T) {
+ d, err := Open(testDSN(t))
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer d.Close()
+ cases := []struct{ name, oldFormat, oldURL, newFormat, newURL, key, want string }{
+ {"SDK same endpoint", "opencode-sdk", "http://localhost:4097/a", "opencode-sdk", "http://localhost:4097/a/", "", "old-token"},
+ {"SDK default port casing", "opencode-sdk", "HTTP://LOCALHOST:80/a/", "opencode-sdk", "http://localhost/a", "", "old-token"},
+ {"SDK different path", "opencode-sdk", "http://localhost:4097/a", "opencode-sdk", "http://localhost:4097/b", "", ""},
+ {"SDK different host alias", "opencode-sdk", "http://localhost:4097", "opencode-sdk", "http://127.0.0.1:4097", "", ""},
+ {"SDK different port", "opencode-sdk", "http://localhost:4097", "opencode-sdk", "http://localhost:4098", "", ""},
+ {"SDK to cloud", "opencode-sdk", "http://localhost:4097", "openai", "https://api.openai.com/v1", "", ""},
+ {"cloud to SDK", "openai", "https://api.openai.com/v1", "opencode-sdk", "http://localhost:4097", "", ""},
+ {"SDK explicit replacement", "opencode-sdk", "http://localhost:4097/a", "opencode-sdk", "http://localhost:4097/b", "new-token", "new-token"},
+ {"cloud unchanged behavior", "openai", "https://one.example/v1", "openai", "https://two.example/v1", "", "old-token"},
+ }
+ for i, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ p := &LLMProfile{Name: fmt.Sprintf("sdk-token-transition-%d", i), Format: tc.oldFormat, BaseURL: tc.oldURL, Model: "opencode/test", APIKey: "old-token", Streaming: true}
+ id, e := d.SaveProfile(p)
+ if e != nil {
+ t.Fatal(e)
+ }
+ defer d.Exec(`DELETE FROM llm_profiles WHERE id=$1`, id)
+ p.ID = id
+ p.Format = tc.newFormat
+ p.BaseURL = tc.newURL
+ p.APIKey = tc.key
+ p.APIKeyHint = "stale-hint"
+ if _, e = d.SaveProfile(p); e != nil {
+ t.Fatal(e)
+ }
+ got, e := d.ProfileByID(id)
+ if e != nil {
+ t.Fatal(e)
+ }
+ var savedHint string
+ if e = d.QueryRow(`SELECT COALESCE(api_key_hint,'') FROM llm_profiles WHERE id=$1`, id).Scan(&savedHint); e != nil {
+ t.Fatal(e)
+ }
+ wantHint := ""
+ if tc.want != "" {
+ wantHint = "…" + tc.want[len(tc.want)-4:]
+ }
+ if got.APIKey != tc.want || savedHint != wantHint || got.Format != tc.newFormat || got.BaseURL != tc.newURL {
+ t.Fatalf("saved=%+v want key=%q hint=%q", got, tc.want, wantHint)
+ }
+ })
+ }
+}
+
+func TestSDKFormatUpgradePreservesExistingProfiles(t *testing.T) {
+ d, err := Open(testDSN(t))
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer d.Close()
+ old := &LLMProfile{Name: "sdk-upgrade-existing", Format: "openai", Model: "existing-model", BaseURL: "https://existing.example/v1", APIKey: "existing-key"}
+ id, err := d.SaveProfile(old)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer d.Exec(`DELETE FROM llm_profiles WHERE id=$1`, id)
+ // Recreate the previous release's constraint only inside this disposable test database.
+ if _, err = d.Exec(`ALTER TABLE llm_profiles DROP CONSTRAINT llm_profiles_format_check; ALTER TABLE llm_profiles ADD CONSTRAINT llm_profiles_format_check CHECK (format IN ('openai','anthropic','openai-responses','ollama'))`); err != nil {
+ t.Fatal(err)
+ }
+ sdk := &LLMProfile{Name: "sdk-upgrade-new", Format: "opencode-sdk", Model: "opencode/test", BaseURL: "http://localhost:4097", APIKey: "sdk-token"}
+ if _, err = d.SaveProfile(sdk); err == nil {
+ t.Fatal("previous schema unexpectedly accepted SDK format")
+ }
+ upgraded, err := Open(testDSN(t))
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer upgraded.Close()
+ sdkID, err := upgraded.SaveProfile(sdk)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer upgraded.Exec(`DELETE FROM llm_profiles WHERE id=$1`, sdkID)
+ got, err := upgraded.ProfileByID(id)
+ if err != nil || got.APIKey != old.APIKey || got.Format != old.Format || got.Model != old.Model || got.BaseURL != old.BaseURL {
+ t.Fatalf("existing profile changed: %+v err=%v", got, err)
+ }
+}
diff --git a/db/schema.sql b/db/schema.sql
index 5da9c2c7..cc011d38 100644
--- a/db/schema.sql
+++ b/db/schema.sql
@@ -365,7 +365,7 @@ CREATE TABLE IF NOT EXISTS settings (
CREATE TABLE IF NOT EXISTS llm_profiles (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
- format TEXT NOT NULL CHECK (format IN ('openai','anthropic','openai-responses','ollama')),
+ format TEXT NOT NULL CHECK (format IN ('openai','anthropic','openai-responses','ollama','opencode-sdk')),
base_url TEXT,
proxy TEXT,
model TEXT NOT NULL,
@@ -422,7 +422,7 @@ ALTER TABLE llm_profiles ADD COLUMN IF NOT EXISTS streaming BOOLEAN NOT NULL
-- Idempotent on every startup: drop the old CHECK and recreate with all supported values.
ALTER TABLE llm_profiles DROP CONSTRAINT IF EXISTS llm_profiles_format_check;
ALTER TABLE llm_profiles ADD CONSTRAINT llm_profiles_format_check
- CHECK (format IN ('openai','anthropic','openai-responses','ollama'));
+ CHECK (format IN ('openai','anthropic','openai-responses','ollama','opencode-sdk'));
-- Native Ollama options are additive; cloud profiles retain their existing defaults.
ALTER TABLE llm_profiles ADD COLUMN IF NOT EXISTS keep_alive TEXT NOT NULL DEFAULT '';
diff --git a/docs/es/llm-providers.md b/docs/es/llm-providers.md
index f236730b..f79802fc 100644
--- a/docs/es/llm-providers.md
+++ b/docs/es/llm-providers.md
@@ -2,7 +2,9 @@
[English](../llm-providers.md) · [한국어](../ko/llm-providers.md) · [简体中文](../zh-Hans/llm-providers.md) · [繁體中文](../zh-Hant/llm-providers.md) · [日本語](../ja/llm-providers.md) · Español
-ScopeWeaver admite cuatro formatos de perfil: **Anthropic**, **OpenAI Chat Completions**, **OpenAI Responses** y **Ollama** nativo. Todos conservan URL base personalizada, proxy, cabecera de sesión, reintentos, frecuencia y concurrencia, por lo que siguen funcionando sin cambios proveedores de nube, pasarelas y servidores autoalojados compatibles con OpenAI. Ollama es opcional y los proveedores de nube siguen siendo la ruta compatible predeterminada.
+ScopeWeaver admite **Anthropic**, **OpenAI Chat Completions**, **OpenAI Responses**, **Ollama** nativo y el **puente OpenCode SDK** opcional. Los cuatro formatos anteriores conservan URL base, proxy, cabecera de sesión, reintentos, frecuencia y concurrencia; los proveedores de nube, pasarelas y servidores OpenAI compatibles siguen funcionando. SDK usa credenciales independientes y los límites descritos abajo; no sustituye API/OpenCode Go.
+
+La plantilla SDK tampoco hace solicitudes; el descubrimiento requiere pulsar **Cargar modelos** explícitamente.
Una plantilla rellena el **formulario de un perfil nuevo** sin guardar credenciales, activar perfiles ni modificar los existentes. Las de Ollama también leen modelos instalados del servidor elegido; las de nube no hacen solicitudes. El formato, URL base y modelo siguen editables tras cualquier plantilla.
@@ -34,6 +36,55 @@ Introduce nombre de perfil y tu clave API. Aplicar cualquiera de las plantillas
Verificado con documentación oficial el **2026-10-03**. La disponibilidad, derechos de cuenta y políticas pueden cambiar.
+
+
+## Puente OpenCode SDK
+
+`opencode-sdk` es un formato de perfil opcional e independiente, no la API HTTP interna de OpenCode. El entorno Norma de ScopeWeaver conserva conversación, herramientas, aprobaciones, alcance, reintentos y compactación. El puente solo transmite un paso del modelo y captura solicitudes de herramientas del anfitrión.
+
+| `SCOPEWEAVER_OPENCODE_RUNTIME` | Entorno fijado | Límites de argumentos e historial |
+| --- | --- | --- |
+| `native` (predeterminado) | CLI, cliente y SDK oficiales **2.1.4** | Conserva JSON original de argumentos, incluidos enteros de 64 bits, en respuestas e historial. Rechaza números inseguros fuera de los argumentos originales autoritativos. |
+| `legacy` | SDK y CLI **1.19.4** | JSON estructuralmente válido y representable con seguridad; enteros de valor absoluto no superior a **9,007,199,254,740,991**. Puede normalizar espacios y escapes HTML/Unicode equivalentes, sin conservar bytes originales. Rechaza enteros inseguros e historial opaco/firmado de razonamiento. Usa `native` para fidelidad de argumentos originales. |
+
+El modo nativo usa el CLI auténtico fijado con `serve --stdio` y `run --server --session`; el cliente oficial gestiona sesiones, eventos e interrupción. Cada llamada controla home/configuración/datos temporales privados y su base de datos. Conserva las definiciones e implementaciones nativas reales, con permisos ASK y sin `--auto`; un hook público `execute.before` bloquea todas las herramientas ajenas al anfitrión antes de ejecutarse. Solo devuelve las capturas a Norma para ejecución y aprobación. Conserva el contexto generado de modelo/entorno/herramientas junto con system e historial del anfitrión. Rechaza fuentes de configuración de sistema/MDM no controladas sin leerlas ni modificarlas; consulta la guía del adaptador.
+
+Los proveedores personalizados heredados usan un mapeo explícito compatible con OpenAI. **Ninguno de los dos entornos admite Windows**, porque no está implementada la limpieza necesaria del árbol POSIX. La verificación local del entorno cubre **macOS arm64**. CI ejecuta pruebas con el entorno real en **Linux x64**; no se afirma que otras plataformas estén verificadas. Las versiones SDK no probadas requieren verificación independiente.
+
+Sigue el [inicio del adaptador](../../adapters/opencode-sdk/README.es.md) con **Bun 1.3.14**. El puente tiene configuración propia y no importa credenciales guardadas de OpenCode ni de nube. Define un `SCOPEWEAVER_OPENCODE_TOKEN` independiente de al menos **32 caracteres no blancos** y úsalo en **LLM → Nuevo → Plantilla de configuración → Puente OpenCode SDK**. No pegues una clave API de OpenCode Go/Console.
+
+```sh
+cd adapters/opencode-sdk
+bun install --frozen-lockfile --ignore-scripts
+export SCOPEWEAVER_OPENCODE_TOKEN="$(openssl rand -hex 32)"
+bun run start
+```
+
+Conserva el token para el campo **Token del puente**. El entorno predeterminado es `native`; define `SCOPEWEAVER_OPENCODE_RUNTIME=legacy` antes del inicio para elegir el antiguo. Escucha en `127.0.0.1:4097`. Para proveedores explícitos, usa un JSON dedicado mediante `SCOPEWEAVER_OPENCODE_PROVIDER_CONFIG`, siguiendo el esquema del adaptador, no un archivo de autenticación del usuario. Conserva las dependencias opcionales de plataforma.
+
+`SCOPEWEAVER_OPENCODE_TIMEOUT_MS` admite **0–86,400,000 ms**, con **0** predeterminado bajo control de cancelación del anfitrión. La solicitud tiene un máximo de **16 MiB** y el exceso falla explícitamente. Los demás límites de salida y limpieza están en la guía del adaptador.
+
+| Campo | Valor |
+| --- | --- |
+| Formato | `opencode-sdk` |
+| URL del puente | `http://127.0.0.1:4097` |
+| Modelo | `providerID/modelID`, mediante **Cargar modelos** o entrada explícita |
+| Token del puente | `SCOPEWEAVER_OPENCODE_TOKEN`, no una clave API de nube |
+| Streaming | Activado, con el entorno fijado seleccionado |
+| Razonamiento / esfuerzo | Omitidos por la plantilla; usa solo valores admitidos por el modelo |
+| Límite de salida | `0` usa el valor existente de la ruta del anfitrión; el puente aplica el límite declarado menor del modelo |
+| Proxy / cabecera de sesión personalizada | No admitidos; la plantilla borra ambos |
+
+La plantilla borra claves de nube sin guardar, deja el modelo vacío y **no descubre, infiere, guarda ni activa**. Introduce el token y pulsa **Cargar modelos** para consultar el catálogo sin inferencia. Guarda y activa por separado cuando esté listo. **Probar conexión** solo hace una solicitud al modelo al pulsarlo; descubrir metadatos no demuestra acceso de inferencia.
+
+**Verificación gratuita anterior — 2026-10-25 KST:** si figura en la lista, selecciona `opencode/deepseek-v4.1-flash-free`. La ejecución anterior sin iniciar sesión en macOS arm64, con CLI auténtico 2.1.4, superó conexión de interfaz, escritura Bash del anfitrión tras aprobación ASK manual, archivo con `FREE_OK` exacto más salto de línea y respuesta final. Hubo **3 POST de inferencia**, reintentos desactivados, sin modelo de pago alternativo ni ejecución nativa observada. Solo demuestra un pequeño flujo aislado de esa fecha, no una repetición actual, disponibilidad permanente ni uso ilimitado. Verifica disponibilidad, elegibilidad y límites actuales. El acceso gratuito alojado heredado sigue sin verificarse.
+
+Al editar, un token vacío conserva el guardado solo en el mismo endpoint SDK. Cambiar URL borra el token introducido; cambiar endpoint o formato con token vacío no reutiliza credenciales. `localhost`, `127.0.0.1` y `::1` son ámbitos distintos. ID y límites de salida proceden de metadatos actuales. El razonamiento depende del proveedor/modelo y falla explícitamente si no es compatible.
+
+**Red y cuentas:** el puente loopback debe compartir el espacio de nombres de red con ScopeWeaver; localhost dentro de un contenedor es el propio contenedor. El destino puede ser nube, por lo que el modo **LLM solo local** rechaza SDK. El puente no admite proxies; configura la conectividad del proveedor en el puente. Un ID gratuito no demuestra derechos de suscripción, continuidad ni uso ilimitado; revisa términos y límites. No selecciona alternativas de pago ni importa cuentas. La política de failover configurada por el anfitrión es independiente. Los perfiles API/OpenCode Go existentes siguen separados.
+
+**Preparar una degradación:** la restricción de formato `CHECK` de la base de datos añade `opencode-sdk`. Un binario anterior no arranca mientras queden perfiles SDK. Respalda la base y usa la versión actual para convertirlos a un formato antiguo admitido o eliminarlos antes de degradar. La verificación previa usó bases desechables, sin migrar producción.
+
## OpenCode Go (pasarela de suscripción opencode)
Abre **LLM → Nuevo → Plantilla de configuración** y elige **OpenCode Go · glm-5.3-flash**.
diff --git a/docs/ja/llm-providers.md b/docs/ja/llm-providers.md
index 705fffde..98bd5cc7 100644
--- a/docs/ja/llm-providers.md
+++ b/docs/ja/llm-providers.md
@@ -5,7 +5,9 @@
-ScopeWeaver は **Anthropic**、**OpenAI Chat Completions**、**OpenAI Responses**、ネイティブ **Ollama** の 4 つのプロファイル形式に対応します。各形式は独自のベース URL、プロキシ、セッションヘッダー、再試行、レート、同時実行設定を保持し、クラウド、ゲートウェイ、セルフホストの OpenAI 互換サーバーはそのまま動作します。Ollama は任意であり、クラウドは引き続き既定と互換性のある経路です。
+ScopeWeaver は **Anthropic**、**OpenAI Chat Completions**、**OpenAI Responses**、ネイティブ **Ollama** と任意の **OpenCode SDK ブリッジ**に対応します。元の 4 形式は独自ベース URL、プロキシ、セッションヘッダー、再試行、レート、同時実行設定を保持し、クラウド、ゲートウェイ、セルフホスト OpenAI 互換サーバーの動作は変わりません。SDK は独立した認証情報と下記の制限を使い、API/OpenCode Go を置き換えません。
+
+SDK テンプレートも要求を送信しません。モデル探索には明示的な**モデル読み込み**操作が必要です。
テンプレートは、認証情報を保存したり、プロファイルを有効化したり、既存のものを変更したりせず、**新規プロファイルのフォーム**を設定します。Ollama のテンプレートは選択したサーバーのインストール済みモデルも読みますが、クラウドのテンプレートは要求を送りません。適用後も形式、ベース URL、モデルを編集できます。
@@ -39,6 +41,55 @@ GLM-5.3 はテキストを受け付け、コンテキストは 1M トークン
公式資料の確認日: **2026-10-03**。プロバイダーの可用性、アカウントの権利、方針は変わる場合があります。
+
+
+## OpenCode SDK ブリッジ
+
+`opencode-sdk` は独立した任意のプロファイル形式であり、OpenCode 内蔵 HTTP API ではありません。ScopeWeaver の Norma が対話、ツール、承認、スコープ、再試行、圧縮を管理します。ブリッジはモデルの 1 ステップとホストツール要求の収集のみを処理します。
+
+| `SCOPEWEAVER_OPENCODE_RUNTIME` | 固定ランタイム | 引数・履歴の境界 |
+| --- | --- | --- |
+| `native`(既定) | 公式 CLI/クライアント/SDK **2.1.4** | 応答とホスト履歴の生ツール引数 JSON を 64 ビット整数も含め保持。正本の生引数以外で危険な数値は拒否。 |
+| `legacy` | SDK/CLI **1.19.4** | 正しく安全に表現できる JSON、絶対値 **9,007,199,254,740,991** 以下の整数のみ。空白や同値 HTML/Unicode エスケープは正規化され、生のバイトは保持しません。危険な整数と不透明/署名付き思考履歴は拒否。生引数の忠実性が必要なら `native` を使用。 |
+
+ネイティブは固定した本物の CLI `serve --stdio`、`run --server --session` と公式クライアントでセッション、イベント、中断を処理します。呼び出しごとに私有の一時 home/設定/データと DB を所有します。実際の組み込みツール定義・実装は残しますが、権限は ASK、`--auto` は使わず、公開 `execute.before` が非ホストツールをすべて実行前に拒否します。ホスト収集要求だけを Norma に戻して実行・承認します。OpenCode のモデル/環境/ツール文脈をホスト system と履歴に加えて保持します。管理できないシステム/MDM 設定がある場合の起動拒否は、読み取り・変更を行わずに適用します。詳細はアダプターガイドにあります。
+
+旧版のカスタムプロバイダーは明示的な OpenAI 互換マッピングを使います。必要な POSIX プロセスツリー終了処理が未実装のため、**両ランタイムとも Windows 非対応**です。ローカルのランタイム検証範囲は **macOS arm64** です。CI は **Linux x64** で実際のランタイムを使うテストを実行します。他のプラットフォームを検証済みとはしません。未テストの SDK バージョンには別途検証が必要です。
+
+[アダプターの起動手順](../../adapters/opencode-sdk/README.ja.md)に従い **Bun 1.3.14** を使います。ブリッジは独自設定を持ち、保存済み OpenCode/クラウド認証情報を取り込みません。空白以外の文字 **32 文字以上**の独立した `SCOPEWEAVER_OPENCODE_TOKEN` を設定し、**LLM → 新規 → 設定テンプレート → OpenCode SDK ブリッジ**に入力します。OpenCode Go/Console API キーを貼り付けないでください。
+
+```sh
+cd adapters/opencode-sdk
+bun install --frozen-lockfile --ignore-scripts
+export SCOPEWEAVER_OPENCODE_TOKEN="$(openssl rand -hex 32)"
+bun run start
+```
+
+トークンをプロファイルの**ブリッジトークン**欄用に保持します。既定は `native`、起動前の `SCOPEWEAVER_OPENCODE_RUNTIME=legacy` で旧版を選択します。待受は `127.0.0.1:4097`。明示的プロバイダーには `SCOPEWEAVER_OPENCODE_PROVIDER_CONFIG` の専用 JSON を使います。ユーザーの認証ファイルではなく、アダプター記載の形式を使用してください。オプションのプラットフォーム依存を省略しません。
+
+`SCOPEWEAVER_OPENCODE_TIMEOUT_MS` は **0–86,400,000 ミリ秒**、既定 **0** はホストによるキャンセルに委ねます。要求本文は **16 MiB** が上限で、超過は明示的に失敗します。他の出力・終了処理制限はアダプターガイドを参照してください。
+
+| 項目 | 値 |
+| --- | --- |
+| 形式 | `opencode-sdk` |
+| ブリッジ URL | `http://127.0.0.1:4097` |
+| モデル | **モデル読み込み**で選択、または正確な `providerID/modelID` |
+| ブリッジトークン | ブリッジの `SCOPEWEAVER_OPENCODE_TOKEN`。クラウド API キーではありません |
+| ストリーミング | 有効。選択した固定ランタイムを使用 |
+| 思考/推論強度 | テンプレートでは省略。モデル対応値のみ指定 |
+| 出力上限 | `0` は従来のホスト経路の既定値。モデルの宣言する小さい上限で制限 |
+| プロキシ/独自セッションヘッダー | 非対応。テンプレートで両方を消去 |
+
+テンプレートは未保存のクラウドキーを消し、モデルを空にして、**探索・推論・保存・有効化を行いません**。トークン入力後、明示的に**モデル読み込み**を押すと推論せずに一覧を取得します。保存と有効化は別操作です。**接続テスト**はクリック時だけモデル要求を実行し、メタデータの取得成功は推論可能という証明ではありません。
+
+**過去の無料モデル検証 — 2026-10-25 KST:** 一覧にある場合、`opencode/deepseek-v4.1-flash-free` を選べます。当時の未ログイン macOS arm64 実行では本物の CLI 2.1.4 を使い、UI 接続テスト、手動 ASK 承認後のホスト Bash 書き込み、正確な `FREE_OK` と改行のファイル内容、最終回答が成功しました。推論 POST **3 回**、再試行無効、有料代替なしで、ネイティブツール実行は観測していません。これは当時の小さな隔離ワークフローだけの証拠であり、今回の再実行、永続提供、無制限利用を示しません。現在の提供状況、資格、制限を確認してください。旧版のホステッド無料アクセスは未検証です。
+
+編集中に空トークンで保存値を保持できるのは同じ SDK URL のみです。URL 変更で入力トークンを消し、宛先・形式変更後の空欄では保存認証情報を流用しません。`localhost`、`127.0.0.1`、`::1` は別スコープです。モデル ID/出力上限は現在のメタデータを使います。推論設定はモデル・プロバイダー次第で、未対応なら明示的に失敗します。
+
+**ネットワークとアカウント:** ブリッジと ScopeWeaver は同じネットワーク名前空間で動かします。コンテナー内 localhost はコンテナー自身です。下流はクラウドの場合があるため、**ローカル専用 LLM** モードは SDK を拒否します。ブリッジプロキシは非対応で、下流接続はブリッジ側で設定します。無料モデル ID は購読資格、継続提供、無制限利用を保証しません。規約と現在の制限を確認してください。ブリッジは有料代替やアカウント取り込みを行いません。ホストの設定済みフェイルオーバーは別管理です。既存 API/OpenCode Go プロファイルは独立しています。
+
+**ダウングレード準備:** DB のプロファイル形式 `CHECK` 制約に `opencode-sdk` を追加します。SDK プロファイルが残ると旧バイナリは起動できません。DB をバックアップし、現バージョンで旧対応形式に変換するか SDK プロファイルを削除してから戻します。過去の検証は使い捨て DB のみで、稼働 DB は移行していません。
+
## OpenCode Go(opencode のサブスクリプションゲートウェイ)
**LLM → 新規 → 設定テンプレート**で **OpenCode Go · glm-5.3-flash** を選択します。
diff --git a/docs/ko/llm-providers.md b/docs/ko/llm-providers.md
index 15ebdf71..1fcb7b20 100644
--- a/docs/ko/llm-providers.md
+++ b/docs/ko/llm-providers.md
@@ -2,9 +2,11 @@
[English](../llm-providers.md) · 한국어 · [简体中文](../zh-Hans/llm-providers.md) · [繁體中文](../zh-Hant/llm-providers.md) · [日本語](../ja/llm-providers.md) · [Español](../es/llm-providers.md)
-ScopeWeaver는 **Anthropic**, **OpenAI Chat Completions**, **OpenAI Responses**, 기본 **Ollama**의 네 가지 프로필 형식을 지원합니다. 모든 형식에서 사용자 지정 기본 URL, 프록시, 세션 헤더, 재시도, 속도 제한, 동시 요청 설정을 그대로 사용할 수 있으므로 클라우드 제공자, 게이트웨이, 직접 운영하는 OpenAI 호환 서버도 이전처럼 동작합니다. Ollama는 선택 사항이며 클라우드 제공자는 기본적으로 계속 호환됩니다.
+ScopeWeaver의 기존 네 가지 프로필 형식은 **Anthropic**, **OpenAI Chat Completions**, **OpenAI Responses**, 기본 **Ollama**입니다. 모든 형식에서 사용자 지정 기본 URL, 프록시, 세션 헤더, 재시도, 속도 제한, 동시 요청 설정을 그대로 사용할 수 있으므로 클라우드 제공자, 게이트웨이, 직접 운영하는 OpenAI 호환 서버도 이전처럼 동작합니다. Ollama는 선택 사항이며 클라우드 제공자는 기본적으로 계속 호환됩니다.
-템플릿은 자격 증명을 저장하거나 프로필을 활성화하거나 기존 프로필을 변경하지 않고 **새 프로필의 양식을 채웁니다**. Ollama 템플릿은 선택한 서버에 설치된 모델도 조회하며, 클라우드 템플릿은 요청을 보내지 않습니다. 템플릿을 적용한 뒤에도 형식, 기본 URL, 모델은 수정할 수 있습니다.
+선택형 **OpenCode SDK 브리지**는 별도 자격 증명과 아래의 제약을 사용하는 프로필 형식을 추가합니다. 기존 API나 OpenCode Go 게이트웨이 경로를 대체하지 않습니다.
+
+템플릿은 자격 증명을 저장하거나 프로필을 활성화하거나 기존 프로필을 변경하지 않고 **새 프로필의 양식을 채웁니다**. Ollama 템플릿은 선택한 서버에 설치된 모델도 조회하지만, 클라우드와 SDK 템플릿은 요청을 보내지 않습니다. SDK 모델 조회는 **모델 불러오기**를 눌러야 실행됩니다. 어느 템플릿을 선택하든 형식, 기본 URL, 모델을 계속 수정할 수 있습니다.
## Z.ai GLM-5.3
@@ -34,6 +36,57 @@ GLM-5.3은 텍스트 입력과 100만 토큰의 문맥 창을 지원하며 추
공식 문서 확인일: **2026-10-03**. 모델 제공 여부, 계정 권한 및 정책은 바뀔 수 있습니다.
+
+
+## OpenCode SDK 브리지
+
+같은 `opencode-sdk` 프로필 형식으로 고정된 두 런타임을 연결하는 별도 백엔드입니다. 호스트 도구 실행과 승인 검사는 ScopeWeaver의 기존 Norma 런타임이 계속 관리합니다.
+
+| `SCOPEWEAVER_OPENCODE_RUNTIME` | 고정 런타임 | 지원하는 입력 범위 |
+| --- | --- | --- |
+| `native`(기본값) | 공식 CLI·`@opencode/client` 2.1.4 | 응답과 호스트 기록에서 64비트 정수를 포함한 도구 인자의 원본 JSON을 보존합니다. 원본 도구 인자 밖의 안전하지 않은 숫자는 거부합니다. |
+| `legacy` | SDK·CLI 1.19.4 | 구조가 유효하고 안전하게 표현할 수 있는 JSON 기록과 JavaScript 안전 정수를 허용합니다(절댓값 9,007,199,254,740,991 이하). 무해한 공백이나 같은 값을 나타내는 HTML·Unicode 이스케이프는 정규화될 수 있으며 원본 표기 바이트는 보존하지 않습니다. 안전하지 않은 정수와 불투명하거나 서명된 사고 기록은 명시적으로 거부합니다. 도구 인자 원본을 손실 없이 보존해야 하면 `native`를 사용하세요. |
+
+`native`는 고정한 실제 CLI의 `serve --stdio`와 `run --server --session`을 실행하며, 공식 클라이언트가 모델 조회·이벤트·중단을 관찰하고 제어합니다. 호출마다 비공개 임시 CLI home/config/data 경로와 데이터베이스 저장소를 소유합니다. 실제 기본 도구 정의와 원래 구현은 등록된 상태로 유지합니다. 기본 도구 권한은 ASK이며, 별도의 공개 `execute.before` 가드가 모든 비호스트 도구의 실행을 사전에 거부합니다. 수집한 호스트 도구 요청은 Norma가 실행·승인하도록 반환합니다. 호스트의 시스템 지침과 기록에 OpenCode가 생성한 모델·환경·도구 문맥도 유지됩니다. 관리되지 않는 시스템·MDM 설정이 있으면 실행하지 않는 검사는 어댑터 안내를 참고하세요. 해당 설정을 읽거나 변경하지 않습니다.
+
+`legacy`의 사용자 지정 제공자는 어댑터 안내의 명시적인 OpenAI 호환 매핑을 사용합니다. 필요한 POSIX 프로세스 트리 정리 경계가 구현되지 않아 **두 런타임 모두 Windows에서 지원하지 않습니다.** 로컬 런타임 검증 범위는 **macOS arm64**입니다. CI는 **Linux x64**에서 실제 런타임 테스트를 실행하며, 그 밖의 플랫폼을 검증했다고 주장하지 않습니다. 테스트하지 않은 SDK 버전은 별도 검증이 필요합니다.
+
+[어댑터 설정 안내](../../adapters/opencode-sdk/README.ko.md)에 따라 Bun 브리지를 실행하세요. 브리지는 자체 제공자 설정을 사용하며, 사용자가 저장한 OpenCode 또는 클라우드 제공자 자격 증명을 가져오지 않습니다. 브리지에 별도의 `SCOPEWEAVER_OPENCODE_TOKEN`을 설정한 뒤(공백 없는 32자 이상), **LLM → 새로 만들기 → 설정 템플릿 → OpenCode SDK · 브리지**를 선택하세요.
+
+검증한 **Bun 1.3.14**를 사용하세요. 이후 Bun 버전은 아직 검증하지 않았습니다. 저장소 루트에서 실행하세요.
+
+```bash
+cd adapters/opencode-sdk
+bun install --frozen-lockfile --ignore-scripts
+export SCOPEWEAVER_OPENCODE_TOKEN="$(openssl rand -hex 32)"
+bun run start
+```
+
+생성한 토큰은 프로필의 **브리지 토큰** 필드에도 입력해야 합니다. 기본 런타임은 `native`(SDK 2.1.4)입니다. SDK 1.19.4를 선택하려면 브리지 실행 전에 `SCOPEWEAVER_OPENCODE_RUNTIME=legacy`를 설정하세요. 기본 수신 주소는 `127.0.0.1:4097`입니다. 제공자를 직접 설정하려면 전용 JSON 파일을 `SCOPEWEAVER_OPENCODE_PROVIDER_CONFIG`로 지정하세요. 사용자 인증 파일을 지정하지 말고 어댑터 안내의 스키마를 따르세요.
+
+`SCOPEWEAVER_OPENCODE_TIMEOUT_MS`는 0~86,400,000ms를 받습니다. 기본값 `0`에서는 호스트의 취소가 실행 시간을 제어합니다. 요청 본문은 **16 MiB**로 제한하며 초과 입력은 오류로 알립니다. 나머지 출력·정리 한도는 어댑터 안내를 참고하세요.
+
+| 필드 | 값 |
+| --- | --- |
+| 형식 | `opencode-sdk` |
+| 브리지 URL | `http://127.0.0.1:4097` |
+| 모델 | **모델 불러오기**로 선택하거나 직접 입력하는 `providerID/modelID` |
+| 브리지 토큰 | 클라우드 API 키가 아닌 브리지의 `SCOPEWEAVER_OPENCODE_TOKEN` |
+| 스트리밍 | 켜짐, 선택한 고정 런타임의 스트리밍 |
+| 추론 / 추론 강도 | 템플릿에서는 생략, 선택한 모델이 지원하는 값만 설정 |
+| 출력 한도 | `0`은 기존 호스트 호출 경로의 기본값을 사용하며, 브리지가 네이티브 모델에 선언된 출력 한도 이내로 제한 |
+| 프록시 / 사용자 지정 세션 헤더 | 지원하지 않으며 템플릿이 두 값을 모두 지움 |
+
+템플릿은 저장하지 않은 클라우드 키를 지우고 모델을 빈칸으로 두며, **모델 조회·추론·저장·활성화 요청을 보내지 않습니다**. 브리지 토큰을 입력하고 **모델 불러오기**를 눌러야 추론 없이 목록을 조회합니다. 준비되면 저장하고 별도로 활성화하세요. **연결 테스트**는 눌렀을 때만 모델을 호출합니다. 메타데이터를 조회할 수 있다고 추론 접근 권한까지 확인되는 것은 아닙니다.
+
+**이전 검증의 무료 모델 시작점 — 2026-10-25 KST:** **모델 불러오기**에 표시되면 `opencode/deepseek-v4.1-flash-free`를 선택할 수 있습니다. macOS arm64에서 로그인하지 않은 상태로 실제 native CLI 2.1.4를 사용해 UI 연결 테스트, 수동 ASK 승인 후 호스트 Bash 쓰기, 정확한 `FREE_OK` 파일 내용과 줄바꿈, 최종 어시스턴트 응답을 확인했습니다. 재시도 끄기 설정으로 추론 POST 3회를 사용했으며 유료 대체 모델이나 기본 도구 실행은 관찰하지 않았습니다. 작은 격리 작업 하나의 검증이며, 현재 제공 여부·무료 이용 자격·한도는 별도로 적용됩니다.
+
+수정할 때 토큰을 비우면 같은 SDK 엔드포인트에 저장된 값만 유지합니다. URL을 수정하면 입력한 토큰을 지웁니다. 엔드포인트나 형식을 바꾼 뒤 토큰을 비우면 저장된 자격 증명을 재사용하지 않습니다. `localhost`, `127.0.0.1`, `::1` 같은 호스트 별칭은 서로 다른 토큰 범위입니다. 모델 ID와 출력 한도는 현재 네이티브 모델의 메타데이터를 따릅니다. 추론 설정은 모델·제공자의 지원 여부에 따라 제공자에서 실패할 수 있으며, 오류는 명시적으로 알립니다.
+
+**네트워크와 계정 제약:** 루프백 전용 브리지는 ScopeWeaver와 같은 네트워크 네임스페이스에서 실행하세요. Docker 안의 localhost는 해당 컨테이너를 가리킵니다. 하위 제공자는 클라우드 서비스일 수 있습니다. 따라서 SDK 프로필은 **로컬 전용 LLM** 모드에서 거부됩니다. 브리지 프록시는 지원하지 않으며, 하위 제공자의 연결은 브리지에서 구성하세요. 무료 모델 ID는 현재 이용 가능하다는 뜻일 뿐 구독 사용 권한, 영구 제공, 무제한 이용을 보장하지 않습니다. 제공자의 약관과 현재 한도를 확인하세요. 브리지는 유료 대체 모델을 선택하거나 사용자 계정을 가져오지 않으며, 호스트에 설정한 장애 조치 정책은 별도로 유지됩니다. 기존 API와 OpenCode Go 프로필은 별도로 유지됩니다.
+
+**다운그레이드 준비:** 이 변경은 프로필 형식의 데이터베이스 `CHECK` 제약에 `opencode-sdk`를 추가합니다. SDK 프로필이 남아 있으면 이전 바이너리는 시작할 수 없습니다. 다운그레이드 전에 데이터베이스를 백업하고 현재 버전에서 이전 버전이 지원하는 프로필 형식으로 변환하거나 SDK 프로필을 제거할 준비를 하세요. 검증은 임시 데이터베이스에서 수행했으며 여기서 실제 운영 데이터베이스를 마이그레이션하지 않았습니다.
+
## OpenCode Go(opencode 구독 게이트웨이)
**LLM → 새로 만들기 → 설정 템플릿**에서 **OpenCode Go · glm-5.3-flash**를 고르세요.
diff --git a/docs/llm-providers.md b/docs/llm-providers.md
index 20d20866..20d50965 100644
--- a/docs/llm-providers.md
+++ b/docs/llm-providers.md
@@ -2,9 +2,11 @@
English · [한국어](ko/llm-providers.md) · [简体中文](zh-Hans/llm-providers.md) · [繁體中文](zh-Hant/llm-providers.md) · [日本語](ja/llm-providers.md) · [Español](es/llm-providers.md)
-ScopeWeaver supports four profile formats: **Anthropic**, **OpenAI Chat Completions**, **OpenAI Responses** and native **Ollama**. Every format keeps its custom base URL, proxy, session header, retry, rate and concurrency settings, so cloud providers, gateways and self-hosted OpenAI-compatible servers keep working unchanged. Ollama is optional, and cloud providers remain the default-compatible path.
+The four existing ScopeWeaver profile formats are: **Anthropic**, **OpenAI Chat Completions**, **OpenAI Responses** and native **Ollama**. Every format keeps its custom base URL, proxy, session header, retry, rate and concurrency settings, so cloud providers, gateways and self-hosted OpenAI-compatible servers keep working unchanged. Ollama is optional, and cloud providers remain the default-compatible path.
-A template fills a **new profile's form** without saving credentials, activating a profile, or modifying existing profiles. Ollama templates also read installed models from the selected server; cloud templates make no requests. Format, base URL and model stay editable after any template.
+The optional **OpenCode SDK bridge** adds a separate profile format with its own credentials and restrictions described below. It does not replace the existing API or OpenCode Go gateway paths.
+
+A template fills a **new profile's form** without saving credentials, activating a profile, or modifying existing profiles. Ollama templates also read installed models from the selected server; cloud and SDK templates make no requests. SDK model discovery requires an explicit **Load models** action. Format, base URL and model stay editable after any template.
## Z.ai GLM-5.3
@@ -34,6 +36,57 @@ Enter a profile name and your own API key. Applying either template preserves an
Verified against official documentation on **2026-10-03**. Provider availability, account entitlement, and policies may change.
+
+
+## OpenCode SDK bridge
+
+This separate backend supports two pinned runtimes through the same `opencode-sdk` profile format. ScopeWeaver's existing Norma runtime remains responsible for host tool execution and approval checks.
+
+| `SCOPEWEAVER_OPENCODE_RUNTIME` | Pinned runtime | Supported input boundary |
+| --- | --- | --- |
+| `native` (default) | Official CLI and `@opencode/client` 2.1.4 | Preserves raw tool-argument JSON, including 64-bit integers, through responses and host history. Unsafe numeric values outside authoritative raw arguments are rejected. |
+| `legacy` | SDK and CLI 1.19.4 | Structurally valid, safely representable JSON history and JavaScript-safe integers (absolute value no greater than 9,007,199,254,740,991). Benign whitespace and equivalent HTML/Unicode escapes may be normalized; original lexical bytes are not preserved. Unsafe integers and opaque/signed thinking history fail explicitly. Use `native` when lossless raw tool arguments are required. |
+
+`native` starts the genuine pinned CLI through `serve --stdio` and `run --server --session`; the official client handles discovery, event observation and interruption. Each call owns private temporary CLI home/config/data paths and database storage. Genuine native tool definitions and original implementations remain registered: native permissions are ASK, and a separate public `execute.before` guard denies every non-host tool before execution. Host capture requests return to Norma for execution and approvals. OpenCode's generated model/environment/tool context remains alongside the host's system instructions and history. See the adapter guide for the fail-closed unmanaged system/MDM configuration checks; those sources are not read or changed.
+
+Custom providers for `legacy` use the explicit OpenAI-compatible mapping in the adapter guide. **Both runtimes are unsupported on Windows** because the required POSIX process-tree cleanup boundary is not implemented there. Local runtime verification covers **macOS arm64**. CI runs actual-runtime fixtures on **Linux x64**; other platforms are not claimed verified. Untested SDK versions require separate verification.
+
+Start the Bun bridge using the [adapter setup instructions](../adapters/opencode-sdk/README.md). It has its own provider configuration; it does not import saved user OpenCode or cloud-provider credentials. Set a separate `SCOPEWEAVER_OPENCODE_TOKEN` on the bridge (at least 32 non-whitespace characters), then open **LLM → New → Configuration template → OpenCode SDK · bridge**.
+
+Use the tested **Bun 1.3.14** runtime; newer Bun versions have not been verified. From the repository root:
+
+```bash
+cd adapters/opencode-sdk
+bun install --frozen-lockfile --ignore-scripts
+export SCOPEWEAVER_OPENCODE_TOKEN="$(openssl rand -hex 32)"
+bun run start
+```
+
+Keep this token available for the profile's **Bridge token** field. The default runtime is `native` (SDK 2.1.4); set `SCOPEWEAVER_OPENCODE_RUNTIME=legacy` before starting the bridge to select SDK 1.19.4. The listener defaults to `127.0.0.1:4097`. For explicit provider settings, use a dedicated JSON file through `SCOPEWEAVER_OPENCODE_PROVIDER_CONFIG`; follow the adapter guide's schema rather than pointing it at a saved user auth file.
+
+`SCOPEWEAVER_OPENCODE_TIMEOUT_MS` accepts 0–86,400,000 ms; its default `0` leaves duration under host cancellation control. Request bodies are limited to **16 MiB** and oversized input fails explicitly. See the adapter guide for the remaining output and cleanup bounds.
+
+| Field | Value |
+| --- | --- |
+| Format | `opencode-sdk` |
+| Bridge URL | `http://127.0.0.1:4097` |
+| Model | `providerID/modelID`, chosen using **Load models** or entered explicitly |
+| Bridge token | The bridge's `SCOPEWEAVER_OPENCODE_TOKEN`, not a cloud API key |
+| Streaming | On; streams the selected pinned runtime |
+| Thinking / reasoning effort | Omitted by the template; set only values supported by the selected model |
+| Output limit | `0` uses the existing host call-path default; the bridge caps requests at the native model's declared output limit |
+| Proxy / custom session header | Unsupported; the template clears both |
+
+The template clears an unsaved cloud key, leaves the model blank and makes **no model, inference, save or activation request**. Enter the bridge token and click **Load models** to read its catalog without inference. Save when ready and activate separately. **Test connection** performs a model request only when clicked; metadata discovery does not establish inference access.
+
+**Prior verified free-model starting point — 2026-10-25 KST:** choose `opencode/deepseek-v4.1-flash-free` from **Load models** when it is listed. A signed-out macOS arm64 run through genuine native CLI 2.1.4 passed the UI connection test, a host Bash write after manual ASK approval, exact `FREE_OK` file contents plus newline, and the final assistant answer. It used 3 inference POSTs with retries disabled, with no paid fallback or observed native built-in execution. This proves one small isolated workflow; current availability, free-tier eligibility and limits still apply.
+
+When editing, a blank token retains the saved value only for the same SDK endpoint. Changing the URL clears an entered token; changing the endpoint or format with a blank token does not reuse the saved credential. Host aliases such as `localhost`, `127.0.0.1` and `::1` are distinct token scopes. Model IDs and output limits use current native model metadata. Reasoning settings depend on model/provider support and can fail at the provider; errors remain explicit.
+
+**Network and account limits:** run the loopback-only bridge in the same network namespace as ScopeWeaver; inside Docker, localhost means that container. Its downstream provider can still be a cloud service. SDK profiles are therefore rejected in **local-only LLM** mode. Bridge proxies are unsupported; configure downstream provider connectivity on the bridge. A free model ID means current availability, not subscription entitlement, permanent availability, or unlimited use. Confirm the provider's terms and current limits. The bridge does not select a paid fallback or import user accounts; any host-configured failover policy remains separate. Existing API and OpenCode Go profiles remain separate.
+
+**Downgrade preparation:** this change adds `opencode-sdk` to the profile-format database `CHECK` constraint. An older binary cannot start while SDK profiles are present. Back up the database and prepare conversion to an older supported profile format or removal of SDK profiles using the current version before downgrading. Verification used disposable databases; no live database migration was performed here.
+
## OpenCode Go (opencode subscription gateway)
Open **LLM → New → Configuration template** and choose **OpenCode Go · glm-5.3-flash**.
diff --git a/docs/zh-Hans/llm-providers.md b/docs/zh-Hans/llm-providers.md
index d71307d7..16a584f1 100644
--- a/docs/zh-Hans/llm-providers.md
+++ b/docs/zh-Hans/llm-providers.md
@@ -6,7 +6,9 @@
各语言版本请见本页上方导览。
-ScopeWeaver 支持四种配置格式:**Anthropic**、**OpenAI Chat Completions**、**OpenAI Responses** 与原生 **Ollama**。每种格式保留自定义基础 URL、代理、会话头部、重试、速率与并行配置,因此云端供应商、网关与自行托管的 OpenAI 兼容服务器均维持既有运作。Ollama 为选用,云端供应商仍是默认兼容路径。
+ScopeWeaver 支持 **Anthropic**、**OpenAI Chat Completions**、**OpenAI Responses**、原生 **Ollama** 和可选的 **OpenCode SDK 桥接器**。原有四种格式继续保留自定义基础 URL、代理、会话头部、重试、速率与并行设置;云端、网关与自行托管的 OpenAI 兼容服务器保持原有行为。SDK 使用独立凭证及下文的限制,不替代 API/OpenCode Go 路径。
+
+SDK 模板也不发送请求;模型发现必须明确点击**读取模型**。
模板只填写**新配置表单**,不保存凭证、不启用配置、不修改既有配置。Ollama 模板亦从所选服务器读取已安装模型;云端模板不发出请求。应用后格式、基础 URL 与模型均可编辑。
@@ -40,6 +42,55 @@ GLM-5.3 接受文字,上下文为 1M token,且必须保持推理启用。支
已于 **2026-10-03** 对照官方文档验证。供应商可用性、帐号资格与政策可能改变。
+
+
+## OpenCode SDK 桥接器
+
+`opencode-sdk` 是独立的可选配置格式,不是 OpenCode 的内置 HTTP API。ScopeWeaver 的 Norma 运行时继续管理对话、工具、审批、范围、重试与压缩;桥接器只完成一次模型步骤并收集宿主工具请求。
+
+| `SCOPEWEAVER_OPENCODE_RUNTIME` | 固定的运行时 | 参数与历史边界 |
+| --- | --- | --- |
+| `native`(默认) | 官方 CLI、客户端、SDK **2.1.4** | 保留响应和宿主历史中的原始工具参数 JSON,包括 64 位整数;拒绝原始参数之外不安全的数值。 |
+| `legacy` | SDK、CLI **1.19.4** | 只支持结构有效、安全可表示的 JSON;整数绝对值不超过 **9,007,199,254,740,991**。空白及等价 HTML/Unicode 转义可规范化,不保留原始字节;拒绝不安全整数和不透明/带签名思考历史。需要原始参数保真时用 `native`。 |
+
+原生模式通过实际固定 CLI 的 `serve --stdio`、`run --server --session` 及官方客户端管理会话、事件和中断。每次调用拥有私有临时 home/配置/数据路径和数据库。保留真实内置工具定义及原始实现;原生权限为 ASK,不使用 `--auto`,公开 `execute.before` 钩子在执行前阻止所有非宿主工具。只有收集的宿主请求交回 Norma 执行和审批。OpenCode 生成的模型/环境/工具上下文与宿主 system、历史共同保留。遇到无法受控的系统/MDM 配置来源时拒绝启动,不读取或修改它们;细节见适配器指南。
+
+旧版自定义供应商使用显式 OpenAI 兼容配置。**两个运行时均不支持 Windows**,因为必需的 POSIX 进程树清理边界尚未实现。本地运行时验证覆盖 **macOS arm64**。CI 在 **Linux x64** 上运行实际运行时测试;不声称已验证其他平台。其他 SDK 版本需单独验证。
+
+按[适配器启动指南](../../adapters/opencode-sdk/README.zh-Hans.md)使用 **Bun 1.3.14**。桥接器使用自己的供应商配置,不导入用户 OpenCode 或云端已保存凭证。设置至少 **32 个非空白字符**的独立 `SCOPEWEAVER_OPENCODE_TOKEN`,在 **LLM → 新增 → 配置模板 → OpenCode SDK 桥接器**中填入;不要粘贴 OpenCode Go/Console API 密钥。
+
+```sh
+cd adapters/opencode-sdk
+bun install --frozen-lockfile --ignore-scripts
+export SCOPEWEAVER_OPENCODE_TOKEN="$(openssl rand -hex 32)"
+bun run start
+```
+
+保留令牌供配置的**桥接令牌**字段使用。默认 `native`;启动前设 `SCOPEWEAVER_OPENCODE_RUNTIME=legacy` 可改用旧版。监听地址为 `127.0.0.1:4097`。通过 `SCOPEWEAVER_OPENCODE_PROVIDER_CONFIG` 指定专用 JSON 可配置显式供应商,必须使用适配器文档中的格式,不要指向用户认证文件。安装时保留可选平台依赖。
+
+`SCOPEWEAVER_OPENCODE_TIMEOUT_MS` 为 **0–86,400,000 毫秒**,默认 **0** 由宿主取消控制。请求体上限 **16 MiB**;超限明确失败。其余输出与清理限额见适配器指南。
+
+| 字段 | 值 |
+| --- | --- |
+| 格式 | `opencode-sdk` |
+| 桥接 URL | `http://127.0.0.1:4097` |
+| 模型 | 点击**读取模型**选择或手动输入 `providerID/modelID` |
+| 桥接令牌 | 桥接器的 `SCOPEWEAVER_OPENCODE_TOKEN`,不是云端 API 密钥 |
+| 流式传输 | 开启,使用所选固定运行时 |
+| 思考/推理强度 | 模板省略,仅设置模型支持的值 |
+| 输出上限 | `0` 使用宿主调用路径的现有默认值;桥接器按原生模型声明的较小输出上限限制请求 |
+| 代理/自定义会话头部 | 不支持;模板清除两者 |
+
+模板清除未保存的云端密钥,模型留空,**不发现、不推理、不保存、不启用**。填入令牌后明确点击**读取模型**,可不执行推理而查看目录;准备好后保存并单独启用。**测试连接**只在点击时发起模型请求,元数据发现不代表推理访问成功。
+
+**先前免费模型验证 — 2026-10-25 KST:** 当目录列出时,可从**读取模型**中选择 `opencode/deepseek-v4.1-flash-free`。此前未登录的 macOS arm64 运行通过真正的原生 CLI 2.1.4 完成 UI 连接测试、手动 ASK 批准后的宿主 Bash 写入、精确的 `FREE_OK` 加换行文件内容及最终助手答案。共 **3 次推理 POST**,关闭重试,没有付费回退,也未观察到内置工具执行。这只证明当时一个小型隔离流程;不代表本次重跑、永久可用或无限免费。当前模型、免费资格及限额仍需确认。旧版托管免费访问尚未验证。
+
+编辑时,空令牌只在同一 SDK 地址保留已保存值。更改 URL 会清除输入的令牌;更改地址或格式后留空不会复用已保存凭证。`localhost`、`127.0.0.1`、`::1` 是不同凭证作用域。模型 ID 与输出限制以当前元数据为准。思考设置依赖模型/供应商支持,不支持时明确失败。
+
+**网络与账户限制:** 桥接器必须和 ScopeWeaver 位于同一网络命名空间;容器中的 localhost 指容器本身。下游仍可能是云服务,所以**仅本地 LLM** 模式拒绝 SDK 配置。桥接器不支持代理;下游网络在桥接器侧配置。免费模型 ID 不证明订阅权限、持续提供或无限用量;确认服务条款和当前限额。桥接器不选择付费回退、不导入账户;宿主已配置的故障转移策略另行管理。现有 API/OpenCode Go 配置保持独立。
+
+**降级准备:** 数据库配置格式的 `CHECK` 约束增加 `opencode-sdk`。仍有 SDK 配置时旧二进制不能启动。降级前备份数据库,在当前版本将 SDK 配置转换为旧支持格式或移除。先前验证使用临时数据库,没有迁移生产数据库。
+
## OpenCode Go(opencode 订阅网关)
diff --git a/docs/zh-Hant/llm-providers.md b/docs/zh-Hant/llm-providers.md
index ce2a817b..16b9b087 100644
--- a/docs/zh-Hant/llm-providers.md
+++ b/docs/zh-Hant/llm-providers.md
@@ -6,7 +6,9 @@
各語言版本請見本頁上方導覽。
-ScopeWeaver 支援四種設定檔格式:**Anthropic**、**OpenAI Chat Completions**、**OpenAI Responses** 與原生 **Ollama**。每種格式保留自訂基底 URL、代理、工作階段標頭、重試、速率與並行設定,因此雲端供應商、閘道與自行代管的 OpenAI 相容伺服器均維持既有運作。Ollama 為選用,雲端供應商仍是預設相容路徑。
+ScopeWeaver 支援 **Anthropic**、**OpenAI Chat Completions**、**OpenAI Responses**、原生 **Ollama** 和選用的 **OpenCode SDK 橋接器**。原有四種格式繼續保留自訂基底 URL、代理、工作階段標頭、重試、速率與並行設定;雲端、閘道與自行代管的 OpenAI 相容伺服器維持原有行為。SDK 使用獨立憑證及下文的限制,不取代 API/OpenCode Go 路徑。
+
+SDK 範本也不發送請求;模型探索必須明確點選**讀取模型**。
範本只填寫**新設定檔表單**,不儲存憑證、不啟用設定檔、不修改既有設定檔。Ollama 範本亦從所選伺服器讀取已安裝模型;雲端範本不發出請求。套用後格式、基底 URL 與模型均可編輯。
@@ -40,6 +42,55 @@ GLM-5.3 接受文字,上下文為 1M token,且必須保持推理啟用。支
已於 **2026-10-03** 對照官方文件驗證。供應商可用性、帳號資格與政策可能改變。
+
+
+## OpenCode SDK 橋接器
+
+`opencode-sdk` 是獨立的可選設定格式,不是 OpenCode 的內建 HTTP API。ScopeWeaver 的 Norma 運行時繼續管理對話、工具、審批、範圍、重試與壓縮;橋接器只完成一次模型步驟並收集宿主工具請求。
+
+| `SCOPEWEAVER_OPENCODE_RUNTIME` | 固定的運行時 | 參數與歷史邊界 |
+| --- | --- | --- |
+| `native`(預設) | 官方 CLI、客戶端、SDK **2.1.4** | 保留響應和宿主歷史中的原始工具參數 JSON,包括 64 位整數;拒絕原始參數之外不安全的數值。 |
+| `legacy` | SDK、CLI **1.19.4** | 只支持結構有效、安全可表示的 JSON;整數絕對值不超過 **9,007,199,254,740,991**。空白及等價 HTML/Unicode 轉義可規範化,不保留原始位元組;拒絕不安全整數和不透明/帶簽名思考歷史。需要原始參數保真時用 `native`。 |
+
+原生模式通過實際固定 CLI 的 `serve --stdio`、`run --server --session` 及官方客戶端管理工作階段、事件和中斷。每次調用擁有私有臨時 home/設定/資料路徑和資料庫。保留真實內建工具定義及原始實現;原生權限為 ASK,不使用 `--auto`,公開 `execute.before` 鈎子在執行前阻止所有非宿主工具。只有收集的宿主請求交回 Norma 執行和審批。OpenCode 生成的模型/環境/工具上下文與宿主 system、歷史共同保留。遇到無法受控的系統/MDM 設定來源時拒絕啓動,不讀取或修改它們;細節見適配器指南。
+
+舊版自定義供應商使用顯式 OpenAI 兼容設定。**兩個運行時均不支持 Windows**,因為必需的 POSIX 進程樹清理邊界尚未實現。本機執行環境驗證涵蓋 **macOS arm64**。CI 在 **Linux x64** 執行實際執行環境測試;不宣稱已驗證其他平台。其他 SDK 版本需單獨驗證。
+
+按[適配器啓動指南](../../adapters/opencode-sdk/README.zh-Hant.md)使用 **Bun 1.3.14**。橋接器使用自己的供應商設定,不導入用戶 OpenCode 或雲端已保存憑證。設置至少 **32 個非空白字元**的獨立 `SCOPEWEAVER_OPENCODE_TOKEN`,在 **LLM → 新增 → 設定模板 → OpenCode SDK 橋接器**中填入;不要粘貼 OpenCode Go/Console API 密鑰。
+
+```sh
+cd adapters/opencode-sdk
+bun install --frozen-lockfile --ignore-scripts
+export SCOPEWEAVER_OPENCODE_TOKEN="$(openssl rand -hex 32)"
+bun run start
+```
+
+保留權杖供設定的**橋接權杖**字段使用。預設 `native`;啓動前設 `SCOPEWEAVER_OPENCODE_RUNTIME=legacy` 可改用舊版。監聽地址為 `127.0.0.1:4097`。通過 `SCOPEWEAVER_OPENCODE_PROVIDER_CONFIG` 指定專用 JSON 可設定顯式供應商,必須使用適配器文檔中的格式,不要指向用戶認證文件。安裝時保留可選平台依賴。
+
+`SCOPEWEAVER_OPENCODE_TIMEOUT_MS` 為 **0–86,400,000 毫秒**,預設 **0** 由宿主取消控制。請求體上限 **16 MiB**;超限明確失敗。其餘輸出與清理限額見適配器指南。
+
+| 字段 | 值 |
+| --- | --- |
+| 格式 | `opencode-sdk` |
+| 橋接 URL | `http://127.0.0.1:4097` |
+| 模型 | 點擊**讀取模型**選擇或手動輸入 `providerID/modelID` |
+| 橋接權杖 | 橋接器的 `SCOPEWEAVER_OPENCODE_TOKEN`,不是雲端 API 密鑰 |
+| 流式傳輸 | 開啓,使用所選固定運行時 |
+| 思考/推理強度 | 模板省略,僅設置模型支持的值 |
+| 輸出上限 | `0` 使用宿主調用路徑的現有預設值;橋接器按原生模型聲明的較小輸出上限限制請求 |
+| 代理/自定義工作階段標頭 | 不支持;模板清除兩者 |
+
+模板清除未保存的雲端密鑰,模型留空,**不發現、不推理、不保存、不啓用**。填入權杖後明確點擊**讀取模型**,可不執行推理而查看目錄;準備好後保存並單獨啓用。**測試連接**只在點擊時發起模型請求,元資料發現不代表推理訪問成功。
+
+**先前免費模型驗證 — 2026-10-25 KST:** 當目錄列出時,可從**讀取模型**中選擇 `opencode/deepseek-v4.1-flash-free`。此前未登錄的 macOS arm64 運行通過真正的原生 CLI 2.1.4 完成 UI 連接測試、手動 ASK 批准後的宿主 Bash 寫入、精確的 `FREE_OK` 加換行文件內容及最終助手答案。共 **3 次推理 POST**,關閉重試,沒有付費回退,也未觀察到內建工具執行。這只證明當時一個小型隔離流程;不代表本次重跑、永久可用或無限免費。當前模型、免費資格及限額仍需確認。舊版托管免費訪問尚未驗證。
+
+編輯時,空權杖只在同一 SDK 地址保留已保存值。更改 URL 會清除輸入的權杖;更改地址或格式後留空不會復用已保存憑證。`localhost`、`127.0.0.1`、`::1` 是不同憑證作用域。模型 ID 與輸出限制以當前元資料為準。思考設置依賴模型/供應商支持,不支持時明確失敗。
+
+**網絡與賬戶限制:** 橋接器必須和 ScopeWeaver 位於同一網絡命名空間;容器中的 localhost 指容器本身。下游仍可能是雲服務,所以**僅本地 LLM** 模式拒絕 SDK 設定。橋接器不支持代理;下游網絡在橋接器側設定。免費模型 ID 不證明訂閱權限、持續提供或無限用量;確認服務條款和當前限額。橋接器不選擇付費回退、不導入賬戶;宿主已設定的故障轉移策略另行管理。現有 API/OpenCode Go 設定保持獨立。
+
+**降級準備:** 資料庫設定格式的 `CHECK` 約束增加 `opencode-sdk`。仍有 SDK 設定時舊二進制不能啓動。降級前備份資料庫,在當前版本將 SDK 設定轉換為舊支持格式或移除。先前驗證使用臨時資料庫,沒有遷移生產資料庫。
+
## OpenCode Go(opencode 訂閱閘道)
diff --git a/e2e/chat-context.mjs b/e2e/chat-context.mjs
index 77913d4d..fdcd2d5e 100644
--- a/e2e/chat-context.mjs
+++ b/e2e/chat-context.mjs
@@ -8,7 +8,7 @@ const labels = namespaceMessages("interface");
async function closeContext(page, label) {
const button=page.getByRole("button",{name:label,exact:true});
try { await button.click(); } catch(error) {
- console.error("context-close-diagnostic",await page.evaluate(()=>({animations:document.getAnimations().map(a=>({state:a.playState,time:a.currentTime,target:a.effect?.target?.outerHTML?.slice(0,500)})),drawer:document.querySelector("[data-vaul-drawer]")?.outerHTML.slice(0,1800),panel:document.querySelector("[data-chat-context-panel]")?.getBoundingClientRect().toJSON()})));
+ try { console.error("context-close-diagnostic",await page.evaluate(()=>({animations:document.getAnimations().map(a=>({state:a.playState,time:a.currentTime,target:a.effect?.target?.outerHTML?.slice(0,500)})),drawer:document.querySelector("[data-vaul-drawer]")?.outerHTML.slice(0,1800),panel:document.querySelector("[data-chat-context-panel]")?.getBoundingClientRect().toJSON()}))); } catch(captureError) { console.error("context-close-capture-failure",String(captureError)); }
throw error;
}
}
@@ -26,14 +26,14 @@ export async function runChatContextChecks({base,token,fixture,step,check,openPa
const p=await openPage(locale,token);await p.setViewportSize({width,height:900});
try{await step(`[${locale}/${width}/${surface}] context visibility, capacity switch and manual compaction`,async()=>{
await p.goto(url);const input=p.locator("textarea[data-chat-draft]").first();await input.waitFor();await p.locator("[data-chat-context-trigger]").waitFor();
- const calls=await fixture.calls();await input.fill("/context");await p.getByRole("button",{name:t.m0073,exact:true}).click();await p.locator("[data-chat-context-panel]").waitFor();await p.getByText(t.chatContextUnknown,{exact:true}).last().waitFor();check(await p.locator("[data-chat-compact]").isDisabled(),"unobserved context allows compact");check(await fixture.calls()===calls,"/context called the provider");await closeContext(p,t.chatContextClose);
+ const calls=await fixture.calls({operation:"context-command",boundary:"before"});await input.fill("/context");await p.getByRole("button",{name:t.m0073,exact:true}).click();await p.locator("[data-chat-context-panel]").waitFor();await p.getByText(t.chatContextUnknown,{exact:true}).last().waitFor();check(await p.locator("[data-chat-compact]").isDisabled(),"unobserved context allows compact");check(await fixture.calls({operation:"context-command",boundary:"after"})===calls,"/context called the provider");await closeContext(p,t.chatContextClose);
for(let i=0;i<5;i++){await input.fill(`context turn ${i}: `+(i===0?`${caseMarker} `:"")+"preserve detail ".repeat(120));if(i===0)await fixture.holdNext();await p.getByRole("button",{name:t.m0073,exact:true}).click();
if(i===0){await waitChatState(api,path,state=>!!state.active);await p.locator("[data-chat-context-trigger]").click();await p.locator("[data-chat-context-panel]").waitFor();if(process.env.CHAT_CONTEXT_MUTATION)await p.locator("[data-chat-compact]").evaluate(button=>button.disabled=false);check(await p.locator("[data-chat-compact]").isDisabled(),"busy Compact now is enabled");await closeContext(p,t.chatContextClose);await fixture.release()}
await waitChatState(api,path,state=>!state.active&&state.latest_run_status==="completed");await p.getByRole("button",{name:t.chatEdit,exact:true}).nth(i).waitFor();
}
await p.locator("[data-chat-context-trigger]").click();await p.locator("[data-chat-context-panel]").waitFor();await p.getByText("fixture-64",{exact:true}).waitFor();await p.waitForFunction(()=>{const panel=document.querySelector("[data-chat-context-panel]");if(!panel)return false;const box=panel.getBoundingClientRect();return box.y>=0&&box.x>=0&&box.right<=innerWidth+1});const box=await p.locator("[data-chat-context-panel]").boundingBox();check(box&&box.y>=0&&box.x>=0&&box.x+box.width<=width+1,`context panel extends outside the viewport: ${JSON.stringify(box)}`);check(await p.getByLabel(t.chatContextKeep,{exact:true}).inputValue()==="2","default retention changed");
- const before=await (await api.get(actionURL(path,"context"))).json();check(before.window===64000&&before.measured_input===80,"provider input anchor missing");const count=await fixture.calls();await fixture.setBudget(32);
- const switched=await (await api.get(actionURL(path,"context"))).json();check(switched.window===32000&&switched.measured_input===0&&switched.estimated,"stale anchor survived model switch");check(await fixture.calls()===count,"capacity inspection called provider");await p.getByText("fixture-32",{exact:true}).waitFor();
+ const before=await (await api.get(actionURL(path,"context"))).json();check(before.window===64000&&before.measured_input===80,"provider input anchor missing");const count=await fixture.calls({operation:"capacity-switch",boundary:"before"});await fixture.setBudget(32);
+ const switched=await (await api.get(actionURL(path,"context"))).json();check(switched.window===32000&&switched.measured_input===0&&switched.estimated,"stale anchor survived model switch");check(await fixture.calls({operation:"capacity-switch",boundary:"after"})===count,"capacity inspection called provider");await p.getByText("fixture-32",{exact:true}).waitFor();
const state=await (await api.get(path)).json();const expectedFocus="Preserve URLs";await p.getByLabel(t.chatContextFocus,{exact:true}).fill(expectedFocus);
const stripFocus=process.env.CHAT_CONTEXT_STRIP_FOCUS_CASE===caseName;
const compactURL=withLocaleQuery(`${base}${actionURL(path,"compact")}`,locale);let focusMutation;
@@ -46,10 +46,10 @@ export async function runChatContextChecks({base,token,fixture,step,check,openPa
await route.continue({postData:JSON.stringify(mutated)});
};
if(stripFocus)await p.route(compactURL,stripCurrentFocus);
- const requestCursor=(await fixture.requests()).length;
+ const requestCursor=(await fixture.requests({operation:"manual-compact",boundary:"before",marker:caseMarker})).length;
await p.locator("[data-chat-compact]").click();await p.getByText(t.chatContextSuccess,{exact:true}).waitFor();
if(stripFocus){await p.unroute(compactURL,stripCurrentFocus);check(!!focusMutation,"designated compact POST was not focus-mutated")}
- const compacted=await (await api.get(actionURL(path,"context"))).json();check(Number(compacted.history_revision)===Number(state.history_revision)+1,"compact did not publish revision");check(compacted.summary&&compacted.used {swt("providers.introduction")} {swt("providers.sdkHelp")} {swt("providers.sdkSecurity")} {modelsError} {swt("providers.sdkModelHelp")}
{swt("providers.noTools")}
@@ -781,13 +813,13 @@ function ProfileSheet({
- {swt("interface.m1393")}
- {swt("interface.m1396")}{swt("interface.m1397")}{swt("interface.m1398")}
+ {isSDK ? swt("providers.sdkSessionHelp") : <>{swt("interface.m1396")}{swt("interface.m1397")}{swt("interface.m1398")}>}{swt("providers.ollamaKeyHelp")}
} + {isSDK &&{swt("providers.sdkTokenHelp")}
}