From d47e376b43c834cd63d80e62557831a6c5d30e32 Mon Sep 17 00:00:00 2001 From: Rafael Moreira Date: Fri, 25 Sep 2026 07:08:28 -0300 Subject: [PATCH] feat(clients): add native Command Code integration and catalog sync Add Command Code as an export target and managed file integration. Support ~/.commandcode/providers.json export, loopback API key placeholder, CLI commands (ocx commandcode / ocx cmd), and catalog sync. --- gui/src/app-routing.ts | 1 + .../client-config-clients.ts | 4 +- gui/src/components/integration-marks.ts | 1 + gui/src/i18n/de.ts | 3 + gui/src/i18n/en.ts | 3 + gui/src/i18n/fr.ts | 3 + gui/src/i18n/ja.ts | 3 + gui/src/i18n/ko.ts | 3 + gui/src/i18n/ru.ts | 3 + gui/src/i18n/tr.ts | 3 + gui/src/i18n/vi.ts | 3 + gui/src/i18n/zh-TW.ts | 3 + gui/src/i18n/zh.ts | 3 + .../integrations/FileIntegrationPage.tsx | 2 + gui/src/pages/integrations/integration-api.ts | 1 + .../pages/integrations/integration-tabs.ts | 2 + .../pages/integrations/overview-clients.ts | 1 + gui/tests/client-config-panel.test.tsx | 3 +- gui/tests/integrations-api.test.ts | 2 +- gui/tests/integrations-overview-rows.test.ts | 3 +- scripts/test-layout/layout.json | 1 + src/cli/dispatch.ts | 10 +- src/cli/help.ts | 4 +- src/cli/integrations.ts | 21 ++ src/cli/registry.ts | 20 +- src/clients/config-export.ts | 35 +++ src/clients/config-export/commandcode.ts | 96 +++++++ src/clients/config-export/contracts.ts | 3 +- src/integrations/mutation-plan.ts | 1 + src/integrations/registry.ts | 8 + tests/clients/command-code-client.test.ts | 248 ++++++++++++++++++ tests/clients/integrations-state.test.ts | 4 +- tests/clients/integrations-writer.test.ts | 2 +- .../client-config-export-new-clients.test.ts | 2 +- tests/config/client-config-export.test.ts | 2 +- tests/fixtures/test-layout-expected.json | 1 + tests/gui/integrations-invariants.test.ts | 6 +- 37 files changed, 498 insertions(+), 16 deletions(-) create mode 100644 src/clients/config-export/commandcode.ts create mode 100644 tests/clients/command-code-client.test.ts diff --git a/gui/src/app-routing.ts b/gui/src/app-routing.ts index 3f7f107b8c1..de2c68d2885 100644 --- a/gui/src/app-routing.ts +++ b/gui/src/app-routing.ts @@ -104,6 +104,7 @@ export const INTEGRATION_TAB_HASHES = [ "integrations/dsh", "integrations/mcode", "integrations/zcode", + "integrations/commandcode", "integrations/prime", "integrations/aside", "integrations/raycast", diff --git a/gui/src/components/apikeys-workspace/client-config-clients.ts b/gui/src/components/apikeys-workspace/client-config-clients.ts index e164810fbb7..83674691752 100644 --- a/gui/src/components/apikeys-workspace/client-config-clients.ts +++ b/gui/src/components/apikeys-workspace/client-config-clients.ts @@ -8,7 +8,7 @@ * with EXPORT_CLIENT_IDS by hand; adding a client server-side renders no row * until this tuple changes. */ -export const CLIENTS = ["opencode", "pi", "omp", "hermes", "openclaw", "kimi", "gajae", "dsh", "mcode", "zcode", "prime", "aside", "raycast", "omo", "cline"] as const; +export const CLIENTS = ["opencode", "pi", "omp", "hermes", "openclaw", "kimi", "gajae", "dsh", "mcode", "zcode", "commandcode", "prime", "aside", "raycast", "omo", "cline"] as const; export type ExportClientId = (typeof CLIENTS)[number]; export const CLIENT_LABEL_KEYS = { @@ -22,6 +22,7 @@ export const CLIENT_LABEL_KEYS = { dsh: "api.clientConfig.clientDsh", mcode: "api.clientConfig.clientMcode", zcode: "api.clientConfig.clientZcode", + commandcode: "api.clientConfig.clientCommandCode", prime: "api.clientConfig.clientPrime", aside: "api.clientConfig.clientAside", raycast: "api.clientConfig.clientRaycast", @@ -79,6 +80,7 @@ export const CLIENT_MARKS: Partial> = { // so it would paint the plate and throw the face away — see the README. omo: "/provider-icons/omo.svg", cline: "/provider-icons/cline-color.svg", + commandcode: "/provider-icons/commandcode-color.svg", }; /** diff --git a/gui/src/components/integration-marks.ts b/gui/src/components/integration-marks.ts index 58873cb1cb0..226620dd306 100644 --- a/gui/src/components/integration-marks.ts +++ b/gui/src/components/integration-marks.ts @@ -60,6 +60,7 @@ export const INTEGRATION_MARKS: Record = { raycast: CLIENT_MARKS.raycast ?? null, omo: CLIENT_MARKS.omo ?? null, cline: CLIENT_MARKS.cline ?? null, + commandcode: CLIENT_MARKS.commandcode ?? null, }; /** diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 2be9e6b24f8..459cea60afa 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -1380,6 +1380,7 @@ export const de: Record = { "integrations.tab.dsh": "DSH", "integrations.tab.mcode": "MiniMax Code", "integrations.tab.zcode": "ZCode", + "integrations.tab.commandcode": "Command Code", "integrations.tab.prime": "Prime Agent", "integrations.tab.aside": "Aside", "integrations.tab.raycast": "Raycast", @@ -1588,6 +1589,7 @@ export const de: Record = { "integrations.semantics.dsh": "OpenCodex verwaltet nur llm-pi-ai.providers.opencodex in $DSH_HOME/settings.yaml. DSH lädt diesen Anbieter im laufenden Betrieb neu; Ihr Standardmodell und deepseek-official bleiben unverändert. Derzeit nur über Loopback; es werden keine echten Zugangsdaten geschrieben.", "integrations.semantics.mcode": "Verwaltet nur custom_provider.opencodex. Standardmodell und MiniMax-Anmeldung bleiben unverändert.", "integrations.semantics.zcode": "Verwaltet nur provider.opencodex in ~/.zcode/v2/config.json. Z.ai-Anmeldung und andere Provider bleiben unverändert. ZCode nach Änderungen neu starten.", + "integrations.semantics.commandcode": "Verwaltet nur provider.opencodex in ~/.commandcode/providers.json. Ihre anderen Provider und Einstellungen bleiben unverändert.", "integrations.semantics.prime": "Verwaltet nur providers.opencodex in der models.json von Prime Agent — ~/.prime/agent, sofern PRIME_AGENT_CODING_AGENT_DIR sie nicht umleitet. Andere Provider und Modell-Overrides bleiben unverändert. Gilt für neue Sitzungen.", "integrations.semantics.aside": "Verwaltet nur providers.opencodex in der ~/.aside/u//models.json dieses Profils. Andere Provider bleiben unverändert. Beende Aside nach dem Anwenden vollständig und öffne es erneut.", "integrations.semantics.raycast": "Fügt einen OpenCodex-Provider-Eintrag in die providers.yaml von Raycast ein, damit jedes geroutete Modell in der Modellauswahl von Raycast AI erscheint. Raycast Pro erforderlich.", @@ -2082,6 +2084,7 @@ export const de: Record = { "api.clientConfig.clientDsh": "DeepSeek Harness (DSH)", "api.clientConfig.clientMcode": "MiniMax Code", "api.clientConfig.clientZcode": "ZCode", + "api.clientConfig.clientCommandCode": "Command Code", "api.clientConfig.clientPrime": "Prime Agent", "api.clientConfig.clientAside": "Aside", "api.clientConfig.clientRaycast": "Raycast", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index 6e614cd9d45..8630aaf6772 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -1928,6 +1928,7 @@ export const en = { "integrations.tab.dsh": "DSH", "integrations.tab.mcode": "MiniMax Code", "integrations.tab.zcode": "ZCode", + "integrations.tab.commandcode": "Command Code", "integrations.tab.prime": "Prime Agent", "integrations.tab.aside": "Aside", "integrations.tab.raycast": "Raycast", @@ -2176,6 +2177,7 @@ export const en = { "integrations.semantics.dsh": "OpenCodex manages only llm-pi-ai.providers.opencodex in $DSH_HOME/settings.yaml. DSH hot reloads this provider; your default model and deepseek-official stay unchanged. Currently loopback-only; no real credential is written.", "integrations.semantics.mcode": "Manages only custom_provider.opencodex. Your default model and MiniMax login stay unchanged.", "integrations.semantics.zcode": "Manages only provider.opencodex in ~/.zcode/v2/config.json. Your Z.ai login and other providers stay unchanged. Restart ZCode after changes.", + "integrations.semantics.commandcode": "Manages only provider.opencodex in ~/.commandcode/providers.json. Your other providers and settings stay unchanged.", "integrations.semantics.prime": "Manages only providers.opencodex in Prime Agent's models.json — ~/.prime/agent unless PRIME_AGENT_CODING_AGENT_DIR redirects it. Your other providers and model overrides stay unchanged. Applies to new sessions.", "integrations.semantics.aside": "Manages only providers.opencodex in this profile’s ~/.aside/u//models.json. Your other providers stay unchanged. Fully quit and reopen Aside after applying.", "integrations.semantics.raycast": "Adds an OpenCodex provider entry to Raycast's providers.yaml so every routed model appears in the Raycast AI model picker. Raycast Pro required.", @@ -2681,6 +2683,7 @@ export const en = { "api.clientConfig.clientDsh": "DeepSeek Harness (DSH)", "api.clientConfig.clientMcode": "MiniMax Code", "api.clientConfig.clientZcode": "ZCode", + "api.clientConfig.clientCommandCode": "Command Code", "api.clientConfig.clientPrime": "Prime Agent", "api.clientConfig.clientAside": "Aside", "api.clientConfig.clientRaycast": "Raycast", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index 8b1012a1797..783737b34a0 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -1900,6 +1900,7 @@ export const fr: Record = { "integrations.tab.dsh": "DSH", "integrations.tab.mcode": "MiniMax Code", "integrations.tab.zcode": "ZCode", + "integrations.tab.commandcode": "Command Code", "integrations.tab.prime": "Prime Agent", "integrations.tab.aside": "Aside", "integrations.tab.raycast": "Raycast", @@ -2108,6 +2109,7 @@ export const fr: Record = { "integrations.semantics.dsh": "OpenCodex gère uniquement llm-pi-ai.providers.opencodex dans $DSH_HOME/settings.yaml. DSH recharge ce fournisseur à chaud ; votre modèle par défaut et deepseek-official restent inchangés. Seule l’adresse de bouclage est actuellement prise en charge ; aucun identifiant réel n’est écrit.", "integrations.semantics.mcode": "Gère uniquement custom_provider.opencodex. Votre modèle par défaut et votre connexion MiniMax restent inchangés.", "integrations.semantics.zcode": "Gère uniquement provider.opencodex dans ~/.zcode/v2/config.json. Votre connexion Z.ai et les autres fournisseurs restent inchangés. Redémarrez ZCode après toute modification.", + "integrations.semantics.commandcode": "Gère uniquement provider.opencodex dans ~/.commandcode/providers.json. Vos autres fournisseurs et paramètres restent inchangés.", "integrations.semantics.prime": "Gère uniquement providers.opencodex dans le models.json de Prime Agent — ~/.prime/agent, sauf si PRIME_AGENT_CODING_AGENT_DIR le redirige. Vos autres fournisseurs et surcharges de modèles restent inchangés. S'applique aux nouvelles sessions.", "integrations.semantics.aside": "Gère uniquement providers.opencodex dans le fichier ~/.aside/u//models.json de ce profil. Vos autres fournisseurs restent inchangés. Quittez complètement Aside et relancez-le après application.", "integrations.semantics.raycast": "Ajoute une entrée de fournisseur OpenCodex dans le providers.yaml de Raycast afin que chaque modèle routé apparaisse dans le sélecteur de modèles de Raycast AI. Raycast Pro requis.", @@ -2602,6 +2604,7 @@ export const fr: Record = { "api.clientConfig.clientDsh": "DeepSeek Harness (DSH)", "api.clientConfig.clientMcode": "MiniMax Code", "api.clientConfig.clientZcode": "ZCode", + "api.clientConfig.clientCommandCode": "Command Code", "api.clientConfig.clientPrime": "Prime Agent", "api.clientConfig.clientAside": "Aside", "api.clientConfig.clientRaycast": "Raycast", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index 34df0413060..73376e33851 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -1823,6 +1823,7 @@ export const ja: Record = { "integrations.tab.dsh": "DSH", "integrations.tab.mcode": "MiniMax Code", "integrations.tab.zcode": "ZCode", + "integrations.tab.commandcode": "Command Code", "integrations.tab.prime": "Prime Agent", "integrations.tab.aside": "Aside", "integrations.tab.raycast": "Raycast", @@ -2031,6 +2032,7 @@ export const ja: Record = { "integrations.semantics.dsh": "OpenCodex が管理するのは $DSH_HOME/settings.yaml 内の llm-pi-ai.providers.opencodex だけです。DSH はこのプロバイダーをホットリロードし、既定のモデルと deepseek-official は変更しません。現在はループバック専用で、実際の認証情報は書き込みません。", "integrations.semantics.mcode": "custom_provider.opencodex のみを管理します。既定モデルと MiniMax ログインは変更しません。", "integrations.semantics.zcode": "~/.zcode/v2/config.json の provider.opencodex のみを管理します。Z.ai ログインと他のプロバイダーは変更しません。変更後は ZCode を再起動してください。", + "integrations.semantics.commandcode": "~/.commandcode/providers.json の provider.opencodex のみを管理します。他のプロバイダーや設定は変更しません。", "integrations.semantics.prime": "Prime Agent の models.json 内の providers.opencodex のみを管理します。場所は ~/.prime/agent ですが、PRIME_AGENT_CODING_AGENT_DIR が設定されている場合はそちらが優先されます。他のプロバイダーとモデルオーバーライドは変更しません。新しいセッションから適用されます。", "integrations.semantics.aside": "このプロファイルの ~/.aside/u//models.json 内の providers.opencodex のみを管理します。他のプロバイダーは変更しません。適用後は Aside を完全に終了してから開き直してください。", "integrations.semantics.raycast": "Raycast の providers.yaml に OpenCodex のプロバイダーエントリを追加し、ルーティングされたすべてのモデルを Raycast AI のモデル選択に表示します。Raycast Pro が必要です。", @@ -2531,6 +2533,7 @@ export const ja: Record = { "api.clientConfig.clientDsh": "DeepSeek Harness (DSH)", "api.clientConfig.clientMcode": "MiniMax Code", "api.clientConfig.clientZcode": "ZCode", + "api.clientConfig.clientCommandCode": "Command Code", "api.clientConfig.clientPrime": "Prime Agent", "api.clientConfig.clientAside": "Aside", "api.clientConfig.clientRaycast": "Raycast", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 40d09ec0115..0d12e681119 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -1416,6 +1416,7 @@ export const ko: Record = { "integrations.tab.dsh": "DSH", "integrations.tab.mcode": "MiniMax Code", "integrations.tab.zcode": "ZCode", + "integrations.tab.commandcode": "Command Code", "integrations.tab.prime": "Prime Agent", "integrations.tab.aside": "Aside", "integrations.tab.raycast": "Raycast", @@ -1624,6 +1625,7 @@ export const ko: Record = { "integrations.semantics.dsh": "OpenCodex는 $DSH_HOME/settings.yaml의 llm-pi-ai.providers.opencodex만 관리합니다. DSH는 이 provider를 hot reload하며 기본 model과 deepseek-official은 변경하지 않습니다. 현재 loopback 전용이며 실제 credential을 기록하지 않습니다.", "integrations.semantics.mcode": "custom_provider.opencodex만 관리하며 기본 모델과 MiniMax 로그인은 변경하지 않습니다.", "integrations.semantics.zcode": "~/.zcode/v2/config.json의 provider.opencodex만 관리하며 Z.ai 로그인과 다른 프로바이더는 변경하지 않습니다. 변경 후 ZCode를 재시작하세요.", + "integrations.semantics.commandcode": "~/.commandcode/providers.json의 provider.opencodex만 관리합니다. 다른 프로바이더와 설정은 변경되지 않습니다.", "integrations.semantics.prime": "Prime Agent의 models.json에서 providers.opencodex만 관리합니다. 위치는 ~/.prime/agent이며 PRIME_AGENT_CODING_AGENT_DIR가 설정되면 그쪽이 우선합니다. 다른 프로바이더와 모델 오버라이드는 변경하지 않습니다. 새 세션부터 적용됩니다.", "integrations.semantics.aside": "이 프로필의 ~/.aside/u//models.json에서 providers.opencodex만 관리합니다. 다른 프로바이더는 그대로 유지됩니다. 적용 후 Aside를 완전히 종료하고 다시 여세요.", "integrations.semantics.raycast": "Raycast의 providers.yaml에 OpenCodex 프로바이더 항목을 추가해 라우팅된 모든 모델이 Raycast AI 모델 선택기에 표시되도록 합니다. Raycast Pro가 필요합니다.", @@ -2121,6 +2123,7 @@ export const ko: Record = { "api.clientConfig.clientDsh": "DeepSeek Harness (DSH)", "api.clientConfig.clientMcode": "MiniMax Code", "api.clientConfig.clientZcode": "ZCode", + "api.clientConfig.clientCommandCode": "Command Code", "api.clientConfig.clientPrime": "Prime Agent", "api.clientConfig.clientAside": "Aside", "api.clientConfig.clientRaycast": "Raycast", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 6f4a0a42778..a39106aef7a 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -1895,6 +1895,7 @@ export const ru: Record = { "integrations.tab.dsh": "DSH", "integrations.tab.mcode": "MiniMax Code", "integrations.tab.zcode": "ZCode", + "integrations.tab.commandcode": "Command Code", "integrations.tab.prime": "Prime Agent", "integrations.tab.aside": "Aside", "integrations.tab.raycast": "Raycast", @@ -2103,6 +2104,7 @@ export const ru: Record = { "integrations.semantics.dsh": "OpenCodex управляет только llm-pi-ai.providers.opencodex в $DSH_HOME/settings.yaml. DSH применяет этот провайдер горячей перезагрузкой; модель по умолчанию и deepseek-official остаются без изменений. Сейчас поддерживается только loopback; реальные учётные данные не записываются.", "integrations.semantics.mcode": "Управляет только custom_provider.opencodex. Модель по умолчанию и вход MiniMax не меняются.", "integrations.semantics.zcode": "Управляет только provider.opencodex в ~/.zcode/v2/config.json. Вход Z.ai и другие провайдеры не меняются. Перезапустите ZCode после изменений.", + "integrations.semantics.commandcode": "Управляет только provider.opencodex в ~/.commandcode/providers.json. Другие провайдеры и настройки остаются без изменений.", "integrations.semantics.prime": "Управляет только providers.opencodex в models.json Prime Agent — ~/.prime/agent, если PRIME_AGENT_CODING_AGENT_DIR не переопределяет путь. Другие провайдеры и переопределения моделей не меняются. Применяется к новым сессиям.", "integrations.semantics.aside": "Управляет только providers.opencodex в файле ~/.aside/u//models.json этого профиля. Другие провайдеры остаются без изменений. После применения полностью закройте и снова откройте Aside.", "integrations.semantics.raycast": "Добавляет запись провайдера OpenCodex в providers.yaml Raycast, чтобы каждая маршрутизируемая модель появилась в выборе моделей Raycast AI. Требуется Raycast Pro.", @@ -2602,6 +2604,7 @@ export const ru: Record = { "api.clientConfig.clientDsh": "DeepSeek Harness (DSH)", "api.clientConfig.clientMcode": "MiniMax Code", "api.clientConfig.clientZcode": "ZCode", + "api.clientConfig.clientCommandCode": "Command Code", "api.clientConfig.clientPrime": "Prime Agent", "api.clientConfig.clientAside": "Aside", "api.clientConfig.clientRaycast": "Raycast", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index 563823498a4..7c7ca4a2443 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -1914,6 +1914,7 @@ export const tr: Record = { "integrations.tab.dsh": "DSH", "integrations.tab.mcode": "MiniMax Code", "integrations.tab.zcode": "ZCode", + "integrations.tab.commandcode": "Command Code", "integrations.tab.prime": "Prime Agent", "integrations.tab.aside": "Aside", "integrations.tab.raycast": "Raycast", @@ -2121,6 +2122,7 @@ export const tr: Record = { "integrations.semantics.dsh": "OpenCodex yalnızca $DSH_HOME/settings.yaml içindeki llm-pi-ai.providers.opencodex bölümünü yönetir. DSH bu sağlayıcıyı çalışırken yeniden yükler; varsayılan modeliniz ve deepseek-official değişmez. Şimdilik yalnızca geri döngü desteklenir; gerçek kimlik bilgisi yazılmaz.", "integrations.semantics.mcode": "Yalnızca custom_provider.opencodex bölümünü yönetir. Varsayılan model ve MiniMax oturumu değişmez.", "integrations.semantics.zcode": "Yalnızca ~/.zcode/v2/config.json içindeki provider.opencodex bölümünü yönetir. Z.ai oturumu ve diğer sağlayıcılar değişmez. Değişikliklerden sonra ZCode'u yeniden başlatın.", + "integrations.semantics.commandcode": "Yalnızca ~/.commandcode/providers.json içindeki provider.opencodex bölümünü yönetir. Diğer sağlayıcılarınız ve ayarlarınız değişmez.", "integrations.semantics.prime": "Yalnızca Prime Agent'ın models.json dosyasındaki providers.opencodex bölümünü yönetir — PRIME_AGENT_CODING_AGENT_DIR ayarlı değilse ~/.prime/agent. Diğer sağlayıcılar ve model geçersiz kılmaları değişmez. Yeni oturumlarda geçerli olur.", "integrations.semantics.aside": "Yalnızca bu profilin ~/.aside/u//models.json dosyasındaki providers.opencodex bölümünü yönetir. Diğer sağlayıcılarınız değişmez. Uyguladıktan sonra Aside’ı tamamen kapatıp yeniden açın.", "integrations.semantics.raycast": "Raycast'in providers.yaml dosyasına bir OpenCodex sağlayıcı girdisi ekler; böylece yönlendirilen her model Raycast AI model seçicisinde görünür. Raycast Pro gerekir.", @@ -2621,6 +2623,7 @@ export const tr: Record = { "api.clientConfig.clientDsh": "DeepSeek Harness (DSH)", "api.clientConfig.clientMcode": "MiniMax Code", "api.clientConfig.clientZcode": "ZCode", + "api.clientConfig.clientCommandCode": "Command Code", "api.clientConfig.clientPrime": "Prime Agent", "api.clientConfig.clientAside": "Aside", "api.clientConfig.clientRaycast": "Raycast", diff --git a/gui/src/i18n/vi.ts b/gui/src/i18n/vi.ts index 8ce937847a7..fe26c68d72f 100644 --- a/gui/src/i18n/vi.ts +++ b/gui/src/i18n/vi.ts @@ -1883,6 +1883,7 @@ export const vi: Record = { "integrations.tab.dsh": "DSH", "integrations.tab.mcode": "MiniMax Code", "integrations.tab.zcode": "ZCode", + "integrations.tab.commandcode": "Command Code", "integrations.tab.prime": "Prime Agent", "integrations.tab.aside": "Aside", "integrations.tab.raycast": "Raycast", @@ -2131,6 +2132,7 @@ export const vi: Record = { "integrations.semantics.dsh": "OpenCodex chỉ quản lý llm-pi-ai.providers.opencodex trong $DSH_HOME/settings.yaml. DSH sẽ hot reload provider này; model mặc định của bạn và deepseek-official không thay đổi. Hiện chỉ hỗ trợ loopback; không ghi credential thật nào.", "integrations.semantics.mcode": "Chỉ quản lý custom_provider.opencodex. Model mặc định của bạn và đăng nhập MiniMax không thay đổi.", "integrations.semantics.zcode": "Chỉ quản lý provider.opencodex trong ~/.zcode/v2/config.json. Đăng nhập Z.ai của bạn và các provider khác không thay đổi. Khởi động lại ZCode sau khi thay đổi.", + "integrations.semantics.commandcode": "Chỉ quản lý provider.opencodex trong ~/.commandcode/providers.json. Các provider và cài đặt khác của bạn vẫn không thay đổi.", "integrations.semantics.prime": "Chỉ quản lý providers.opencodex trong models.json của Prime Agent — ~/.prime/agent trừ khi PRIME_AGENT_CODING_AGENT_DIR điều hướng nó. Các provider khác và ngoại lệ model của bạn không thay đổi. Áp dụng cho các session mới.", "integrations.semantics.aside": "Chỉ quản lý providers.opencodex trong ~/.aside/u//models.json của profile này. Các provider khác của bạn không thay đổi. Hãy thoát hoàn toàn và mở lại Aside sau khi áp dụng.", "integrations.semantics.raycast": "Thêm một provider OpenCodex vào providers.yaml của Raycast để mọi model được định tuyến xuất hiện trong bộ chọn model AI của Raycast. Yêu cầu Raycast Pro.", @@ -2615,6 +2617,7 @@ export const vi: Record = { "api.clientConfig.clientDsh": "DeepSeek Harness (DSH)", "api.clientConfig.clientMcode": "MiniMax Code", "api.clientConfig.clientZcode": "ZCode", + "api.clientConfig.clientCommandCode": "Command Code", "api.clientConfig.clientPrime": "Prime Agent", "api.clientConfig.clientAside": "Aside", "api.clientConfig.clientRaycast": "Raycast", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index c92a5c5d5c4..fe7622af619 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -2702,6 +2702,7 @@ export const zhTW: Record = { "integrations.tab.dsh": "DSH", "integrations.tab.mcode": "MiniMax Code", "integrations.tab.zcode": "ZCode", + "integrations.tab.commandcode": "Command Code", "integrations.tab.prime": "Prime Agent", "integrations.tab.aside": "Aside", "integrations.tab.raycast": "Raycast", @@ -2910,6 +2911,7 @@ export const zhTW: Record = { "integrations.semantics.dsh": "OpenCodex 只管理 $DSH_HOME/settings.yaml 中的 llm-pi-ai.providers.opencodex。DSH 會熱重載該 provider;你的預設模型與 deepseek-official 維持不變。目前僅支援 loopback,且不會寫入真實憑證。", "integrations.semantics.mcode": "僅管理 custom_provider.opencodex,不會變更預設模型或 MiniMax 登入狀態。", "integrations.semantics.zcode": "僅管理 ~/.zcode/v2/config.json 中的 provider.opencodex,不會變更 Z.ai 登入狀態或其他供應商。變更後請重新啟動 ZCode。", + "integrations.semantics.commandcode": "僅管理 ~/.commandcode/providers.json 中的 provider.opencodex。其他供應商和設定保持不變。", "integrations.semantics.prime": "僅管理 Prime Agent 的 models.json 中的 providers.opencodex;預設位於 ~/.prime/agent,若設定 PRIME_AGENT_CODING_AGENT_DIR 則以其為準。不會變更其他供應商或模型覆寫設定。對新工作階段生效。", "integrations.semantics.aside": "僅管理此設定檔的 ~/.aside/u//models.json 中的 providers.opencodex。其他供應商維持不變。套用後請完全結束並重新開啟 Aside。", "integrations.semantics.raycast": "在 Raycast 的 providers.yaml 中新增一個 OpenCodex 供應商項目,讓所有已路由的模型出現在 Raycast AI 模型選擇器中。需要 Raycast Pro。", @@ -2963,6 +2965,7 @@ export const zhTW: Record = { "api.clientConfig.clientDsh": "DeepSeek Harness (DSH)", "api.clientConfig.clientMcode": "MiniMax Code", "api.clientConfig.clientZcode": "ZCode", + "api.clientConfig.clientCommandCode": "Command Code", "api.clientConfig.clientPrime": "Prime Agent", "api.clientConfig.clientAside": "Aside", "api.clientConfig.clientRaycast": "Raycast", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 69ef71c1ab5..1f46d1d07fe 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -1397,6 +1397,7 @@ export const zh: Record = { "integrations.tab.dsh": "DSH", "integrations.tab.mcode": "MiniMax Code", "integrations.tab.zcode": "ZCode", + "integrations.tab.commandcode": "Command Code", "integrations.tab.prime": "Prime Agent", "integrations.tab.aside": "Aside", "integrations.tab.raycast": "Raycast", @@ -1605,6 +1606,7 @@ export const zh: Record = { "integrations.semantics.dsh": "OpenCodex 只管理 $DSH_HOME/settings.yaml 中的 llm-pi-ai.providers.opencodex。DSH 会热重载该 provider;你的默认模型和 deepseek-official 保持不变。目前仅支持环回地址,且不会写入真实凭据。", "integrations.semantics.mcode": "仅管理 custom_provider.opencodex,不会更改默认模型或 MiniMax 登录状态。", "integrations.semantics.zcode": "仅管理 ~/.zcode/v2/config.json 中的 provider.opencodex,不会更改 Z.ai 登录状态或其他提供商。更改后请重启 ZCode。", + "integrations.semantics.commandcode": "仅管理 ~/.commandcode/providers.json 中的 provider.opencodex。其他提供商和设置保持不变。", "integrations.semantics.prime": "仅管理 Prime Agent 的 models.json 中的 providers.opencodex;默认位于 ~/.prime/agent,若设置 PRIME_AGENT_CODING_AGENT_DIR 则以其为准。不会更改其他提供商或模型覆盖设置。对新会话生效。", "integrations.semantics.aside": "仅管理此配置文件的 ~/.aside/u//models.json 中的 providers.opencodex。其他提供商保持不变。应用后请完全退出并重新打开 Aside。", "integrations.semantics.raycast": "在 Raycast 的 providers.yaml 中添加一个 OpenCodex 提供商条目,让所有已路由的模型出现在 Raycast AI 模型选择器中。需要 Raycast Pro。", @@ -2102,6 +2104,7 @@ export const zh: Record = { "api.clientConfig.clientDsh": "DeepSeek Harness (DSH)", "api.clientConfig.clientMcode": "MiniMax Code", "api.clientConfig.clientZcode": "ZCode", + "api.clientConfig.clientCommandCode": "Command Code", "api.clientConfig.clientPrime": "Prime Agent", "api.clientConfig.clientAside": "Aside", "api.clientConfig.clientRaycast": "Raycast", diff --git a/gui/src/pages/integrations/FileIntegrationPage.tsx b/gui/src/pages/integrations/FileIntegrationPage.tsx index b8806d563df..92290fdb6d5 100644 --- a/gui/src/pages/integrations/FileIntegrationPage.tsx +++ b/gui/src/pages/integrations/FileIntegrationPage.tsx @@ -78,6 +78,7 @@ const SEMANTICS_KEY: Record = { dsh: "integrations.semantics.dsh", mcode: "integrations.semantics.mcode", zcode: "integrations.semantics.zcode", + commandcode: "integrations.semantics.commandcode", prime: "integrations.semantics.prime", aside: "integrations.semantics.aside", raycast: "integrations.semantics.raycast", @@ -96,6 +97,7 @@ const TAB_LABEL_KEY: Record = { dsh: "integrations.tab.dsh", mcode: "integrations.tab.mcode", zcode: "integrations.tab.zcode", + commandcode: "integrations.tab.commandcode", prime: "integrations.tab.prime", aside: "integrations.tab.aside", raycast: "integrations.tab.raycast", diff --git a/gui/src/pages/integrations/integration-api.ts b/gui/src/pages/integrations/integration-api.ts index 02733e095ce..d329e8599b1 100644 --- a/gui/src/pages/integrations/integration-api.ts +++ b/gui/src/pages/integrations/integration-api.ts @@ -12,6 +12,7 @@ export const FILE_INTEGRATION_CLIENTS = [ "dsh", "mcode", "zcode", + "commandcode", "prime", "aside", "raycast", diff --git a/gui/src/pages/integrations/integration-tabs.ts b/gui/src/pages/integrations/integration-tabs.ts index a1810763ddb..f8624d8b335 100644 --- a/gui/src/pages/integrations/integration-tabs.ts +++ b/gui/src/pages/integrations/integration-tabs.ts @@ -44,6 +44,7 @@ export const TABS: readonly TabDefinition[] = [ { id: "dsh", hash: "integrations/dsh", labelKey: "integrations.tab.dsh" }, { id: "mcode", hash: "integrations/mcode", labelKey: "integrations.tab.mcode" }, { id: "zcode", hash: "integrations/zcode", labelKey: "integrations.tab.zcode" }, + { id: "commandcode", hash: "integrations/commandcode", labelKey: "integrations.tab.commandcode" }, { id: "prime", hash: "integrations/prime", labelKey: "integrations.tab.prime" }, { id: "aside", hash: "integrations/aside", labelKey: "integrations.tab.aside" }, { id: "raycast", hash: "integrations/raycast", labelKey: "integrations.tab.raycast" }, @@ -62,6 +63,7 @@ export const FILE_CLIENTS = new Set([ "dsh", "mcode", "zcode", + "commandcode", "prime", "aside", "raycast", diff --git a/gui/src/pages/integrations/overview-clients.ts b/gui/src/pages/integrations/overview-clients.ts index d54d99d0057..aba10352959 100644 --- a/gui/src/pages/integrations/overview-clients.ts +++ b/gui/src/pages/integrations/overview-clients.ts @@ -150,6 +150,7 @@ const FILE_LABEL_KEY: Record = { dsh: "integrations.tab.dsh", mcode: "integrations.tab.mcode", zcode: "integrations.tab.zcode", + commandcode: "integrations.tab.commandcode", prime: "integrations.tab.prime", aside: "integrations.tab.aside", raycast: "integrations.tab.raycast", diff --git a/gui/tests/client-config-panel.test.tsx b/gui/tests/client-config-panel.test.tsx index dffaf4d0994..96be79d52b9 100644 --- a/gui/tests/client-config-panel.test.tsx +++ b/gui/tests/client-config-panel.test.tsx @@ -171,10 +171,11 @@ function rowButton(container: HTMLElement, name: string, label: string): HTMLBut } test("the API download surface includes DSH, MiniMax Code, Aside, Raycast and omo as clients", () => { - expect(CLIENTS).toEqual(["opencode", "pi", "omp", "hermes", "openclaw", "kimi", "gajae", "dsh", "mcode", "zcode", "prime", "aside", "raycast", "omo", "cline"]); + expect(CLIENTS).toEqual(["opencode", "pi", "omp", "hermes", "openclaw", "kimi", "gajae", "dsh", "mcode", "zcode", "commandcode", "prime", "aside", "raycast", "omo", "cline"]); expect(CLIENT_LABEL_KEYS.dsh).toBe("api.clientConfig.clientDsh"); expect(CLIENT_LABEL_KEYS.mcode).toBe("api.clientConfig.clientMcode"); expect(CLIENT_LABEL_KEYS.zcode).toBe("api.clientConfig.clientZcode"); + expect(CLIENT_LABEL_KEYS.commandcode).toBe("api.clientConfig.clientCommandCode"); expect(CLIENT_LABEL_KEYS.aside).toBe("api.clientConfig.clientAside"); expect(CLIENT_LABEL_KEYS.omo).toBe("api.clientConfig.clientOmo"); }); diff --git a/gui/tests/integrations-api.test.ts b/gui/tests/integrations-api.test.ts index 17599aa4998..11e68eb2d54 100644 --- a/gui/tests/integrations-api.test.ts +++ b/gui/tests/integrations-api.test.ts @@ -21,7 +21,7 @@ const originalFetch = globalThis.fetch; test("all registered export clients include Cline in file integrations", () => { expect(FILE_INTEGRATION_CLIENTS).toEqual([ - "opencode", "pi", "omp", "hermes", "openclaw", "kimi", "gajae", "dsh", "mcode", "zcode", "prime", "aside", "raycast", "omo", "cline", + "opencode", "pi", "omp", "hermes", "openclaw", "kimi", "gajae", "dsh", "mcode", "zcode", "commandcode", "prime", "aside", "raycast", "omo", "cline", ]); }); diff --git a/gui/tests/integrations-overview-rows.test.ts b/gui/tests/integrations-overview-rows.test.ts index 4e44e8d674b..bbf2849a23c 100644 --- a/gui/tests/integrations-overview-rows.test.ts +++ b/gui/tests/integrations-overview-rows.test.ts @@ -290,8 +290,9 @@ test("every client counts toward the summary, not just the file clients", () => test("an unsettled file list renders unknown rows instead of dropping them", () => { const built = buildOverviewRows(sources({ clients: [], clientsSettled: false })); - expect(built.rows).toHaveLength(20); + expect(built.rows).toHaveLength(21); expect(rowById(built, "cline")).toMatchObject({ hash: "integrations/cline", labelKey: "integrations.tab.cline", state: "unknown" }); + expect(rowById(built, "commandcode")).toMatchObject({ hash: "integrations/commandcode", labelKey: "integrations.tab.commandcode", state: "unknown" }); expect(rowById(built, "omp").state).toBe("unknown"); expect(rowById(built, "mcode").state).toBe("unknown"); expect(rowById(built, "zcode").state).toBe("unknown"); diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index 7729d699366..95743d5a0a1 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -570,6 +570,7 @@ "cline-pass-reasoning-efforts.test.ts": "providers", "cline-provider.test.ts": "providers", "cline-writer.test.ts": "clients", + "command-code-client.test.ts": "clients", "closed-pr-branch-cleanup.test.ts": "ci-workflows", "codebuddy-adapter.test.ts": "providers", "codebuddy-live-acceptance.test.ts": "providers", diff --git a/src/cli/dispatch.ts b/src/cli/dispatch.ts index 9df115fa24b..8de2f0325fc 100644 --- a/src/cli/dispatch.ts +++ b/src/cli/dispatch.ts @@ -510,7 +510,7 @@ const commandRunners: Record = { }, config, port: live.port, - }, ["mcode", "pi", "raycast", "omo", "cline"])); + }, ["mcode", "pi", "raycast", "omo", "cline", "commandcode"])); } catch (error) { console.warn(`Client integrations were not refreshed: ${error instanceof Error ? error.message : String(error)}`); } @@ -975,6 +975,14 @@ const commandRunners: Record = { const { handleZcodeCommand } = await import("./integrations"); return await handleZcodeCommand(deps.args.slice(1)); }, + commandcode: async deps => { + const { handleCommandcodeCommand } = await import("./integrations"); + return await handleCommandcodeCommand(deps.args.slice(1)); + }, + cmd: async deps => { + const { handleCommandcodeCommand } = await import("./integrations"); + return await handleCommandcodeCommand(deps.args.slice(1)); + }, help: async () => { printUsage(); return 0; diff --git a/src/cli/help.ts b/src/cli/help.ts index 967e431add0..af5d2f20c07 100644 --- a/src/cli/help.ts +++ b/src/cli/help.ts @@ -83,7 +83,7 @@ Usage: ocx api-key Alias of ocx access key ocx access External API keys and endpoint information ocx api Protocol paths: vocabulary, request-path preview, and policy - ocx export --client Print a client config wired to the running proxy (15 clients) + ocx export --client Print a client config wired to the running proxy (16 clients) ocx integration client Enable, disable, inspect or roll back a client integration ocx grok Grok Build model selection and apply ocx system Runtime settings, startup, sync, OpenCodex updates, and Codex CLI inspection @@ -96,6 +96,8 @@ Usage: ocx mcode [args...] Launch MiniMax Code through its managed provider ocx mmx text [args] Launch MiniMax CLI text through the proxy ocx zcode [sub] Connect ZCode to the proxy (managed provider) + ocx commandcode [sub] Connect Command Code CLI to the proxy (managed provider) + ocx cmd [sub] Alias of ocx commandcode ocx help [command] Show help ocx --version | -v Print version diff --git a/src/cli/integrations.ts b/src/cli/integrations.ts index 9afcae078a3..2645e8c83a9 100644 --- a/src/cli/integrations.ts +++ b/src/cli/integrations.ts @@ -361,3 +361,24 @@ export async function handleZcodeCommand(argv: string[], deps: RuntimeApiDeps = } return code; } + +const COMMANDCODE_USAGE = "Usage: ocx commandcode [status|show|list|enable|disable|history|journal|restore] [--json]"; + +export async function handleCommandcodeCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise { + const args = [...argv]; + const verbIndex = args.findIndex(arg => !arg.startsWith("-")); + const action = (verbIndex === -1 ? "status" : args[verbIndex]).toLowerCase(); + const known = ["status", "show", "list", "enable", "disable", "history", "journal", "restore"]; + if (!known.includes(action)) { + console.error(`unknown commandcode command ${action}`); + console.error(COMMANDCODE_USAGE); + return 2; + } + const rest = verbIndex === -1 ? args : [...args.slice(0, verbIndex), ...args.slice(verbIndex + 1)]; + const forwarded = action === "restore" ? [action, ...rest] : [action, ...rest, "--client", "commandcode"]; + const code = await handleClientIntegrationCommand(forwarded, deps); + if (code === 0 && (action === "enable" || action === "disable")) { + console.error("Command Code reads providers on startup."); + } + return code; +} diff --git a/src/cli/registry.ts b/src/cli/registry.ts index 214baa31a25..3a64a8380aa 100644 --- a/src/cli/registry.ts +++ b/src/cli/registry.ts @@ -416,8 +416,8 @@ export const CLI_COMMANDS: CliCommandEntry[] = [ }, { name: "export", - usage: "ocx export --client [--json] [--out ] [--force]", - summary: "Print a client config (OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, gjc, DeepSeek Harness, MiniMax Code, ZCode, Prime Agent, Aside, Raycast, omo, Cline) wired to the running proxy.", + usage: "ocx export --client [--json] [--out ] [--force]", + summary: "Print a client config (OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, gjc, DeepSeek Harness, MiniMax Code, ZCode, Prime Agent, Aside, Raycast, omo, Cline, Command Code) wired to the running proxy.", details: [ "--json prints the generated document as JSON on stdout; use --out for the client's native format.", "--out writes the native config there and refuses to replace an existing file without --force.", @@ -535,6 +535,22 @@ export const CLI_COMMANDS: CliCommandEntry[] = [ "Select OpenCodex Proxy// from ZCode's model picker.", ], }, + { + name: "commandcode", + usage: "ocx commandcode [status|show|list|enable|disable|history|restore --op ] [--json]", + summary: "Connect Command Code CLI to the proxy via its managed provider.", + details: [ + "Alias of ocx integration client --client commandcode.", + "enable writes the managed provider.opencodex block into ~/.commandcode/providers.json; disable removes only that block.", + "Command Code reads providers on startup.", + "`restore --op ` replays the exact previous managed block; take the id from `ocx commandcode history`.", + ], + }, + { + name: "cmd", + usage: "ocx cmd [status|show|list|enable|disable|history|restore --op ] [--json]", + summary: "Alias of ocx commandcode.", + }, { name: "restart", usage: "ocx restart", diff --git a/src/clients/config-export.ts b/src/clients/config-export.ts index 51fb364bde0..adebb4da2d3 100644 --- a/src/clients/config-export.ts +++ b/src/clients/config-export.ts @@ -61,6 +61,17 @@ import { buildMcodeClientConfig, summarizeMcode, buildMcodeContribution } from " import { buildZcodeClientConfig, summarizeZcode, buildZcodeContribution } from "./config-export/zcode"; import { buildClineClientConfig, summarizeCline, buildClineContribution } from "./config-export/cline"; import { buildRaycastClientConfig, summarizeRaycast, buildRaycastContribution } from "./config-export/raycast"; +import { + buildCommandCodeClientConfig, + summarizeCommandCode, + buildCommandCodeContribution, + type CommandCodeGeneratedConfig, + type CommandCodeModelEntry, + type CommandCodeProviderBlock, +} from "./config-export/commandcode"; + +export type { CommandCodeGeneratedConfig, CommandCodeModelEntry, CommandCodeProviderBlock }; +export { buildCommandCodeClientConfig, summarizeCommandCode, buildCommandCodeContribution }; @@ -459,6 +470,16 @@ export function zcodeConfigPath(env: OpencodeLaunchEnv = process.env, home: stri return join(zcodeHomeDir(env, home), "v2", "config.json"); } +export function commandCodeHomeDir(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string { + const override = env.COMMANDCODE_HOME?.trim(); + if (override) return absoluteClientPath(override, home, "COMMANDCODE_HOME"); + return join(home, ".commandcode"); +} + +export function commandCodeConfigPath(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string { + return join(commandCodeHomeDir(env, home), "providers.json"); +} + /** * The provider store a current ZCode reads, which is NOT the file above. * @@ -1462,6 +1483,20 @@ export const EXPORT_CLIENTS: Record = { // remote binds refuse — same reasoning as MCode. loopbackOnly: true, }, + commandcode: { + id: "commandcode", + filename: "providers.json", + destination: env => commandCodeConfigPath(env), + // No env var exists behind this integration: buildCommandCodeClientConfig + // references the service token file (`!cat`) or the loopback placeholder. + apiKeyEnv: "", + exportHint: "Command Code reads service token from serviceApiTokenFilePath or loopback placeholder.", + build: buildCommandCodeClientConfig, + format: "json", + summarize: summarizeCommandCode, + buildContribution: buildCommandCodeContribution, + loopbackOnly: true, + }, prime: { id: "prime", filename: "prime-models.json", diff --git a/src/clients/config-export/commandcode.ts b/src/clients/config-export/commandcode.ts new file mode 100644 index 00000000000..a65b1c73b61 --- /dev/null +++ b/src/clients/config-export/commandcode.ts @@ -0,0 +1,96 @@ +// Command Code config export. +import type { ExportContext, ManagedContribution } from "./contracts"; +import { normalizeExportModels, authoritativeContextWindow, singleFragment } from "./model-metadata"; +import { OPENCODE_PROVIDER_ID, LOOPBACK_API_KEY_PLACEHOLDER } from "./constants"; +import { sanitizeCodexReasoningEfforts } from "../../reasoning-effort"; + +export interface CommandCodeModelEntry { + reasoningEfforts?: string[]; + contextWindow?: number; +} + +export interface CommandCodeProviderBlock { + name: "OpenCodex"; + api: "openai-completions"; + baseURL: string; + apiKey: string; + models: Record; +} + +export interface CommandCodeGeneratedConfig { + provider: Record; +} + +/** + * LOSSY spelling-collision key for a routed selector — not model identity. + * + * One provider model reaches this exporter under interchangeable spellings: the raw + * selector keeps inner slashes (`command-code/deepseek/deepseek-v4.1-flash`, what + * `/v1/models` publishes) while the Codex-facing form encodes them as dashes + * (`command-code/deepseek-deepseek-v4.1-flash`, what `~/.codex/config.toml` stores and + * what an operator typing `--model` copies). Command Code addresses models by exact key, + * so a catalog that carries both spellings can resolve the active model against one and + * miss it in the other. + * + * The slash is meaningful: it separates the provider from the model id, and a provider id + * never contains one. But the encoding is NOT bijective — `p/a/b` and `p/a-b` are + * different model ids that encode to the same string. That is the same lossy relation + * `slugEquivalenceKey` documents in src/providers/slug-codec.ts and the same ambiguity + * `resolveSlugAliasCollisions` guards in the catalog. Callers must therefore never drop + * a row on this key alone; buildCommandCodeClientConfig additionally compares provider + * and id before folding two rows into one. + */ +function canonicalSpellingOf(namespaced: string): string { + const slash = namespaced.indexOf("/"); + if (slash <= 0) return namespaced; + return namespaced.slice(0, slash) + "/" + namespaced.slice(slash + 1).replaceAll("/", "-"); +} + +export function buildCommandCodeClientConfig(ctx: ExportContext): CommandCodeGeneratedConfig { + const models: Record = {}; + // Fold interchangeable spellings of one model onto a single key. The first + // occurrence wins per provider+id identity; distinct model ids that happen to + // share a lossy spelling collision must both remain addressable. + const emittedIdentitiesByCollision = new Map>(); + for (const model of normalizeExportModels(ctx.models)) { + const emittedKey = model.namespaced; + const collisionKey = canonicalSpellingOf(model.namespaced); + const identity = `${model.provider}\u0000${model.id}`; + const identities = emittedIdentitiesByCollision.get(collisionKey) ?? new Set(); + if (identities.has(identity)) continue; + identities.add(identity); + emittedIdentitiesByCollision.set(collisionKey, identities); + const entry: CommandCodeModelEntry = {}; + const context = authoritativeContextWindow(model.contextWindow); + if (context !== undefined) { + entry.contextWindow = context; + } + const efforts = sanitizeCodexReasoningEfforts(model.reasoningEfforts) + ?.filter(effort => ["low", "medium", "high", "xhigh", "max"].includes(effort)); + if (efforts && efforts.length > 0) { + entry.reasoningEfforts = efforts; + } + models[emittedKey] = entry; + } + return { + provider: { + [OPENCODE_PROVIDER_ID]: { + name: "OpenCodex", + api: "openai-completions", + baseURL: ctx.baseUrl.replace(/\/v1\/?$/, "") + "/v1", + apiKey: LOOPBACK_API_KEY_PLACEHOLDER, + models, + }, + }, + }; +} + +export function summarizeCommandCode(document: unknown): { modelCount: number; modelsWithoutLimits: number } { + const models = Object.values((document as CommandCodeGeneratedConfig | undefined)?.provider?.[OPENCODE_PROVIDER_ID]?.models ?? {}); + return { modelCount: models.length, modelsWithoutLimits: models.filter(model => model.contextWindow === undefined).length }; +} + +export function buildCommandCodeContribution(ctx: ExportContext): ManagedContribution { + const doc = buildCommandCodeClientConfig(ctx); + return singleFragment("commandcode", ["provider", OPENCODE_PROVIDER_ID], doc.provider[OPENCODE_PROVIDER_ID]); +} diff --git a/src/clients/config-export/contracts.ts b/src/clients/config-export/contracts.ts index 6744150d7a7..085e761d6da 100644 --- a/src/clients/config-export/contracts.ts +++ b/src/clients/config-export/contracts.ts @@ -105,7 +105,8 @@ export type ExportClientId = | "aside" | "raycast" | "omo" - | "cline"; + | "cline" + | "commandcode"; export interface ExportClientSpec { id: ExportClientId; diff --git a/src/integrations/mutation-plan.ts b/src/integrations/mutation-plan.ts index 9ab2fac0d82..e418ff8ebe3 100644 --- a/src/integrations/mutation-plan.ts +++ b/src/integrations/mutation-plan.ts @@ -136,6 +136,7 @@ const CLIENT_MANAGED_PATHS = { [...ZCODE_STORE_PROVIDER_RULES_PATH, `[providerId=${OPENCODE_PROVIDER_ID}]`], [...ZCODE_STORE_MODEL_RULES_PATH, DYNAMIC_SEGMENT], ], + commandcode: [["provider", OPENCODE_PROVIDER_ID]], prime: [["providers", OPENCODE_PROVIDER_ID]], aside: [["providers", OPENCODE_PROVIDER_ID]], raycast: [["providers", `[id=${OPENCODE_PROVIDER_ID}]`]], diff --git a/src/integrations/registry.ts b/src/integrations/registry.ts index 55d87a60ac4..c3376b4227b 100644 --- a/src/integrations/registry.ts +++ b/src/integrations/registry.ts @@ -48,6 +48,8 @@ import { zcodeStoreSchemaEstablished, type BuildContribution, type ConfigFormat, + commandCodeConfigPath, + commandCodeHomeDir, type ExportClientId, } from "../clients/config-export"; @@ -270,6 +272,12 @@ export const INTEGRATION_CLIENTS: Record commandCodeConfigPath(env, home), + detectDir: (env = process.env, home = homedir()) => commandCodeHomeDir(env, home), + writerLock: { suffix: ".lock" }, + }, prime: { id: "prime", configPath: (env = process.env, home = homedir()) => primeConfigPath(env, home), diff --git a/tests/clients/command-code-client.test.ts b/tests/clients/command-code-client.test.ts new file mode 100644 index 00000000000..cb5c9e63eee --- /dev/null +++ b/tests/clients/command-code-client.test.ts @@ -0,0 +1,248 @@ +import { describe, expect, test } from "bun:test"; +import { join } from "node:path"; +import { + ClientPathError, + EXPORT_CLIENTS, + LOOPBACK_API_KEY_PLACEHOLDER, + OPENCODE_PROVIDER_ID, + buildClientConfig, + buildClientConfigText, + buildClientContribution, + buildCommandCodeClientConfig, + commandCodeConfigPath, + commandCodeHomeDir, + type CommandCodeGeneratedConfig, + type ExportContext, +} from "../../src/clients/config-export"; +import { INTEGRATION_CLIENTS } from "../../src/integrations/registry"; +import type { OcxConfig } from "../../src/types"; + +const CONFIG = { + port: 10100, + hostname: "127.0.0.1", + defaultProvider: "mock", + providers: { mock: { adapter: "openai-chat", baseUrl: "http://127.0.0.1/v1" } }, +} as OcxConfig; + +function context(): ExportContext { + return { + baseUrl: "http://127.0.0.1:10100/v1", + config: CONFIG, + models: [ + { namespaced: "anthropic/claude-opus-5", provider: "anthropic", id: "claude-opus-5", contextWindow: 200_000, inputModalities: ["text", "image"] }, + { namespaced: "openai/gpt-5.6-sol", provider: "openai", id: "gpt-5.6-sol", contextWindow: 922_000, reasoningEfforts: ["low", "medium", "high"] }, + { namespaced: "google-antigravity/gemini-3.8-flash", provider: "google-antigravity", id: "gemini-3.8-flash", contextWindow: 1_048_576, reasoningEfforts: ["low", "medium", "high"] }, + { namespaced: "mystery/model", provider: "mystery", id: "model" }, + ], + }; +} + +describe("Command Code client config", () => { + test("generates valid JSON with provider.opencodex block", () => { + const built = buildClientConfigText("commandcode", context()); + expect(built.format).toBe("json"); + expect(JSON.parse(built.text)).toEqual(built.document as never); + }); + + test("adds only provider.opencodex, wired to the loopback proxy", () => { + const document = buildClientConfig("commandcode", context()) as CommandCodeGeneratedConfig; + expect(Object.keys(document)).toEqual(["provider"]); + expect(Object.keys(document.provider)).toEqual([OPENCODE_PROVIDER_ID]); + const provider = document.provider[OPENCODE_PROVIDER_ID]!; + expect(provider.name).toBe("OpenCodex"); + expect(provider.api).toBe("openai-completions"); + expect(provider.baseURL).toBe("http://127.0.0.1:10100/v1"); + expect(provider.apiKey).toBeDefined(); + }); + + test("emits contextWindow and reasoningEfforts correctly without guessing", () => { + const document = buildClientConfig("commandcode", context()) as CommandCodeGeneratedConfig; + const provider = document.provider[OPENCODE_PROVIDER_ID]!; + const gemini = provider.models["google-antigravity/gemini-3.8-flash"]; + expect(gemini).toBeDefined(); + expect(gemini?.contextWindow).toBe(1_048_576); + expect(gemini?.reasoningEfforts).toEqual(["low", "medium", "high"]); + + const mystery = provider.models["mystery/model"]; + expect(mystery).toBeDefined(); + expect(mystery?.contextWindow).toBeUndefined(); + expect(mystery?.reasoningEfforts).toBeUndefined(); + }); + + test("the contribution owns exactly the provider.opencodex path under its own id", () => { + const contribution = buildClientContribution("commandcode", context()); + expect(contribution.clientId).toBe("commandcode"); + expect(contribution.fragments.map(f => f.path)).toEqual([["provider", OPENCODE_PROVIDER_ID]]); + }); + + test("resolves the home directory override and the documented destination", () => { + expect(commandCodeHomeDir({}, "/home/u")).toBe(join("/home/u", ".commandcode")); + expect(commandCodeConfigPath({}, "/home/u")).toBe(join("/home/u", ".commandcode", "providers.json")); + expect(commandCodeConfigPath({ COMMANDCODE_HOME: "/elsewhere" }, "/home/u")).toBe(join("/elsewhere", "providers.json")); + expect(commandCodeConfigPath({ COMMANDCODE_HOME: "~/alt" }, "/home/u")).toBe(join("/home/u", "alt", "providers.json")); + expect(() => commandCodeConfigPath({ COMMANDCODE_HOME: "relative" }, "/home/u")).toThrow(ClientPathError); + }); + + test("detects installation by the .commandcode directory the override names", () => { + const spec = INTEGRATION_CLIENTS.commandcode; + expect(spec.detectDir({}, "/home/u")).toBe(join("/home/u", ".commandcode")); + expect(spec.detectDir({ COMMANDCODE_HOME: "/elsewhere" } as NodeJS.ProcessEnv, "/home/u")).toBe("/elsewhere"); + }); + + test("ships as a loopback-only integration with proper export metadata", () => { + const spec = EXPORT_CLIENTS.commandcode; + expect(spec.id).toBe("commandcode"); + expect(spec.filename).toBe("providers.json"); + expect(spec.format).toBe("json"); + expect(spec.loopbackOnly).toBe(true); + }); +}); + +// A routed model reaches this exporter under ONE canonical selector, but the proxy +// publishes it under two interchangeable spellings: the raw selector with inner +// slashes (what /v1/models emits) and the Codex-facing encoded form where inner +// slashes became dashes (what ~/.codex/config.toml stores). Command Code addresses +// models by exact key, so a client that resolves the active model from one surface +// and looks it up in the other writes a config whose own active model is absent +// from its catalog. +describe("Command Code model-key spelling", () => { + test("keys every model by the spelling the client can actually call", () => { + const document = buildCommandCodeClientConfig({ + ...context(), + models: [ + { namespaced: "command-code/deepseek/deepseek-v4.1-flash", provider: "command-code", id: "deepseek/deepseek-v4.1-flash", contextWindow: 1_000_000 }, + ], + }) as CommandCodeGeneratedConfig; + const keys = Object.keys(document.provider[OPENCODE_PROVIDER_ID]!.models); + expect(keys).toContain("command-code/deepseek/deepseek-v4.1-flash"); + }); + + test("never emits two keys that differ only by slash-versus-dash encoding", () => { + const document = buildCommandCodeClientConfig({ + ...context(), + models: [ + { namespaced: "command-code/deepseek/deepseek-v4.1-flash", provider: "command-code", id: "deepseek/deepseek-v4.1-flash", contextWindow: 1_000_000 }, + { namespaced: "command-code/deepseek-deepseek-v4.1-flash", provider: "command-code", id: "deepseek/deepseek-v4.1-flash", contextWindow: 1_000_000 }, + ], + }) as CommandCodeGeneratedConfig; + const keys = Object.keys(document.provider[OPENCODE_PROVIDER_ID]!.models); + const encoded = keys.map(key => key.replaceAll("/", "-")); + expect(new Set(encoded).size).toBe(encoded.length); + }); + + test("collapses a duplicate pair to one entry instead of shipping both", () => { + const document = buildCommandCodeClientConfig({ + ...context(), + models: [ + { namespaced: "command-code/meta/muse-spark-1.3-contributor", provider: "command-code", id: "meta/muse-spark-1.3-contributor", contextWindow: 1_048_576 }, + { namespaced: "command-code/meta-muse-spark-1.3-contributor", provider: "command-code", id: "meta/muse-spark-1.3-contributor", contextWindow: 1_048_576 }, + ], + }) as CommandCodeGeneratedConfig; + const models = document.provider[OPENCODE_PROVIDER_ID]!.models; + const matching = Object.keys(models).filter(key => key.includes("muse-spark-1.3-contributor")); + expect(matching.length).toBe(1); + expect(models[matching[0]!]?.contextWindow).toBe(1_048_576); + }); + + test("keeps genuinely distinct models apart", () => { + const document = buildCommandCodeClientConfig({ + ...context(), + models: [ + { namespaced: "command-code/deepseek/deepseek-v4-pro", provider: "command-code", id: "deepseek/deepseek-v4-pro", contextWindow: 1_000_000 }, + { namespaced: "command-code/deepseek/deepseek-v4.1-flash", provider: "command-code", id: "deepseek/deepseek-v4.1-flash", contextWindow: 1_000_000 }, + ], + }) as CommandCodeGeneratedConfig; + expect(Object.keys(document.provider[OPENCODE_PROVIDER_ID]!.models).length).toBe(2); + }); + + test("keeps two genuinely distinct ids that encode to the same dash form callable", () => { + // The known dangerous pair: vendor/a/b (id with an inner slash) and vendor/a-b + // (id with a literal dash) both encode to vendor/a-b. They are DISTINCT models. + const document = buildCommandCodeClientConfig({ + ...context(), + models: [ + { namespaced: "command-code/vendor/a/b", provider: "command-code", id: "vendor/a/b", contextWindow: 1_000_000 }, + { namespaced: "command-code/vendor/a-b", provider: "command-code", id: "vendor/a-b", contextWindow: 500_000 }, + ], + }) as CommandCodeGeneratedConfig; + const models = document.provider[OPENCODE_PROVIDER_ID]!.models; + // Both must survive: neither is a spelling of the other. + expect(Object.keys(models).length).toBe(2); + expect(models["command-code/vendor/a/b"]?.contextWindow).toBe(1_000_000); + expect(models["command-code/vendor/a-b"]?.contextWindow).toBe(500_000); + }); + + test("folds the same model across spellings regardless of input order", () => { + const build = (order: "raw-first" | "encoded-first") => buildCommandCodeClientConfig({ + ...context(), + models: order === "raw-first" + ? [ + { namespaced: "command-code/deepseek/deepseek-v4.1-flash", provider: "command-code", id: "deepseek/deepseek-v4.1-flash", contextWindow: 1_000_000 }, + { namespaced: "command-code/deepseek-deepseek-v4.1-flash", provider: "command-code", id: "deepseek/deepseek-v4.1-flash", contextWindow: 1_000_000 }, + ] + : [ + { namespaced: "command-code/deepseek-deepseek-v4.1-flash", provider: "command-code", id: "deepseek/deepseek-v4.1-flash", contextWindow: 1_000_000 }, + + { namespaced: "command-code/deepseek/deepseek-v4.1-flash", provider: "command-code", id: "deepseek/deepseek-v4.1-flash", contextWindow: 1_000_000 }, + ], + }) as CommandCodeGeneratedConfig; + const a = build("raw-first"); + const b = build("encoded-first"); + const keysA = Object.keys(a.provider[OPENCODE_PROVIDER_ID]!.models); + const keysB = Object.keys(b.provider[OPENCODE_PROVIDER_ID]!.models); + // One entry either way, and the surviving key is the same (normalizeExportModels + // sorts, so input order cannot change the winner). + expect(keysA.length).toBe(1); + expect(keysB.length).toBe(1); + expect(keysA[0]).toBe(keysB[0]); + // Pin the EXACT compatibility contract: the encoded spelling is the surviving key, + // because that is the form ~/.codex/config.toml stores and what an operator's + // --model reference resolves against. + expect(keysA[0]).toBe("command-code/deepseek-deepseek-v4.1-flash"); + // Metadata is the first occurrence's, never a merge or a max: document that so a + // future metadata-merge policy is a deliberate change, not an accident. + expect(a.provider[OPENCODE_PROVIDER_ID]!.models[keysA[0]!]?.contextWindow).toBe(1_000_000); + // And the serialized bytes are identical regardless of input order. + const rawFirst = buildClientConfigText("commandcode", { + ...context(), + models: [ + { namespaced: "command-code/deepseek/deepseek-v4.1-flash", provider: "command-code", id: "deepseek/deepseek-v4.1-flash", contextWindow: 1_000_000 }, + { namespaced: "command-code/deepseek-deepseek-v4.1-flash", provider: "command-code", id: "deepseek/deepseek-v4.1-flash", contextWindow: 1_000_000 }, + ], + }); + const encodedFirst = buildClientConfigText("commandcode", { + ...context(), + models: [ + { namespaced: "command-code/deepseek-deepseek-v4.1-flash", provider: "command-code", id: "deepseek/deepseek-v4.1-flash", contextWindow: 1_000_000 }, + { namespaced: "command-code/deepseek/deepseek-v4.1-flash", provider: "command-code", id: "deepseek/deepseek-v4.1-flash", contextWindow: 1_000_000 }, + ], + }); + expect(rawFirst.text).toBe(encodedFirst.text); + }); + + test("never normalizes the provider segment", () => { + // Two providers whose names differ around dash handling must never collapse. + const document = buildCommandCodeClientConfig({ + ...context(), + models: [ + { namespaced: "my-provider/model", provider: "my-provider", id: "model", contextWindow: 100 }, + { namespaced: "my_provider/model", provider: "my_provider", id: "model", contextWindow: 200 }, + ], + }) as CommandCodeGeneratedConfig; + const models = document.provider[OPENCODE_PROVIDER_ID]!.models; + expect(Object.keys(models).length).toBe(2); + expect(models["my-provider/model"]?.contextWindow).toBe(100); + expect(models["my_provider/model"]?.contextWindow).toBe(200); + }); + test("preserves distinct ids that collide only after slash encoding", () => { + const document = buildCommandCodeClientConfig({ + ...context(), + models: [ + { namespaced: "command-code/a/b-c", provider: "command-code", id: "a/b-c" }, + { namespaced: "command-code/a-b/c", provider: "command-code", id: "a-b/c" }, + ], + }) as CommandCodeGeneratedConfig; + const models = document.provider[OPENCODE_PROVIDER_ID]!.models; + expect(Object.keys(models)).toEqual(["command-code/a-b/c", "command-code/a/b-c"]); + }); +}); diff --git a/tests/clients/integrations-state.test.ts b/tests/clients/integrations-state.test.ts index f96cf134c51..f97d0eb011d 100644 --- a/tests/clients/integrations-state.test.ts +++ b/tests/clients/integrations-state.test.ts @@ -794,9 +794,9 @@ describe("installation detection is independent of config state", () => { * from. Rationale and the per-client table: 020 §1 amendment. */ describe("the loopback-only set is one fact, read through one seam", () => { - test("omp, pi, kimi, gajae, dsh, mcode, zcode, prime, aside, raycast and omo are loopback-only and nobody else is", () => { + test("omp, pi, kimi, gajae, dsh, mcode, zcode, commandcode, prime, aside, raycast, omo and cline are loopback-only and nobody else is", () => { const loopbackOnly = INTEGRATION_CLIENT_IDS.filter(id => isLoopbackOnly(id)); - expect(loopbackOnly).toEqual(["pi", "omp", "kimi", "gajae", "dsh", "mcode", "zcode", "prime", "aside", "raycast", "omo", "cline"]); + expect(loopbackOnly).toEqual(["pi", "omp", "kimi", "gajae", "dsh", "mcode", "zcode", "commandcode", "prime", "aside", "raycast", "omo", "cline"]); }); test("the registry restates nothing — it reads the export spec", () => { diff --git a/tests/clients/integrations-writer.test.ts b/tests/clients/integrations-writer.test.ts index 57311a676db..4c4ac402614 100644 --- a/tests/clients/integrations-writer.test.ts +++ b/tests/clients/integrations-writer.test.ts @@ -806,7 +806,7 @@ describe("apply", () => { }); test("refuses a loopback-only client on a remote bind without denying manual OMP headers", () => { - for (const clientId of ["gajae", "omp", "dsh"] as const) { + for (const clientId of ["gajae", "omp", "dsh", "commandcode"] as const) { const spec = INTEGRATION_CLIENTS[clientId]; mkdirSync(spec.detectDir(TEST_ENV, home), { recursive: true }); const result = applyIntegration(input({ diff --git a/tests/config/client-config-export-new-clients.test.ts b/tests/config/client-config-export-new-clients.test.ts index f4b3a64bcbc..2cff5338b88 100644 --- a/tests/config/client-config-export-new-clients.test.ts +++ b/tests/config/client-config-export-new-clients.test.ts @@ -64,7 +64,7 @@ describe("no secret reaches a client config", () => { // credential wiring is deliberately deferred from those initial generated // integrations -- omo reuses Pi's builder, which emits no headers at all. const loopbackOnly = EXPORT_CLIENT_IDS.filter(id => EXPORT_CLIENTS[id].loopbackOnly); - expect(loopbackOnly).toEqual(["pi", "omp", "kimi", "gajae", "dsh", "mcode", "zcode", "prime", "aside", "raycast", "omo", "cline"]); + expect(loopbackOnly).toEqual(["pi", "omp", "kimi", "gajae", "dsh", "mcode", "zcode", "commandcode", "prime", "aside", "raycast", "omo", "cline"]); }); test("every client that is not loopback-only carries the header on a remote bind", () => { diff --git a/tests/config/client-config-export.test.ts b/tests/config/client-config-export.test.ts index 0b6fd2686a1..dcc3d9dcd9b 100644 --- a/tests/config/client-config-export.test.ts +++ b/tests/config/client-config-export.test.ts @@ -848,7 +848,7 @@ describe("hub-resolved Fast exports", () => { describe("EXPORT_CLIENTS registry", () => { test("covers exactly the fourteen file-toggle clients", () => { - expect(EXPORT_CLIENT_IDS).toEqual(["opencode", "pi", "omp", "hermes", "openclaw", "kimi", "gajae", "dsh", "mcode", "zcode", "prime", "aside", "raycast", "omo", "cline"]); + expect(EXPORT_CLIENT_IDS).toEqual(["opencode", "pi", "omp", "hermes", "openclaw", "kimi", "gajae", "dsh", "mcode", "zcode", "commandcode", "prime", "aside", "raycast", "omo", "cline"]); for (const id of EXPORT_CLIENT_IDS) expect(isExportClientId(id)).toBe(true); // The exception clients keep their own surfaces and are not export clients. expect(isExportClientId("claude-desktop")).toBe(false); diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json index 61df140603b..4f493ec8b83 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -396,6 +396,7 @@ "cline-pass-reasoning-efforts.test.ts": "providers", "cline-provider.test.ts": "providers", "cline-writer.test.ts": "clients", + "command-code-client.test.ts": "clients", "closed-pr-branch-cleanup.test.ts": "ci-workflows", "codebuddy-adapter.test.ts": "providers", "codebuddy-live-acceptance.test.ts": "providers", diff --git a/tests/gui/integrations-invariants.test.ts b/tests/gui/integrations-invariants.test.ts index 0378ac8e1b6..6004cc3e650 100644 --- a/tests/gui/integrations-invariants.test.ts +++ b/tests/gui/integrations-invariants.test.ts @@ -99,7 +99,7 @@ describe("the client registries cannot drift apart", () => { const guiRouting = await import("../../gui/src/app-routing"); const expected = [...EXPORT_CLIENT_IDS].sort(); - expect(expected).toHaveLength(15); + expect(expected).toHaveLength(16); expect([...INTEGRATION_CLIENT_IDS].sort()).toEqual(expected); expect([...gui.CLIENTS].sort()).toEqual(expected); @@ -209,9 +209,10 @@ describe("the client registries cannot drift apart", () => { expect(INTEGRATION_CLIENTS.dsh.sourcePreservingYaml?.path).toEqual([ "llm-pi-ai", "providers", "opencodex", ]); - expect(INTEGRATION_CLIENT_IDS.filter(id => INTEGRATION_CLIENTS[id].writerLock)).toEqual(["dsh", "mcode", "cline"]); + expect(INTEGRATION_CLIENT_IDS.filter(id => INTEGRATION_CLIENTS[id].writerLock)).toEqual(["dsh", "mcode", "commandcode", "cline"]); expect(INTEGRATION_CLIENTS.dsh.writerLock).toEqual({ suffix: ".lock" }); expect(INTEGRATION_CLIENTS.mcode.writerLock).toEqual({ suffix: ".lock" }); + expect(INTEGRATION_CLIENTS.commandcode.writerLock).toEqual({ suffix: ".lock" }); }); }); @@ -252,6 +253,7 @@ describe("every client survives a full lifecycle", () => { /** A pre-existing user document in each client's own format. */ const SEED: Record = { cline: '{"version":1,"modes":{},"providers":{"mine":{"settings":{"provider":"mine"},"updatedAt":"2026-01-01T00:00:00.000Z","tokenSource":"manual"}}}\n', + commandcode: '{\n "provider": {\n "mine": { "name": "Keep Me", "api": "openai-completions", "baseURL": "http://keep-me/v1", "apiKey": "test-key", "models": {} }\n }\n}\n', opencode: '{\n "provider": {\n "mine": { "npm": "keep-me" }\n }\n}\n', pi: '{\n "providers": {\n "mine": { "api": "http://keep-me" }\n }\n}\n', omp: "providers:\n mine:\n api: http://keep-me\n",