diff --git a/docs-site/src/content/docs/fr/guides/integrations.md b/docs-site/src/content/docs/fr/guides/integrations.md index 6be2373dfdf..33182c3b817 100644 --- a/docs-site/src/content/docs/fr/guides/integrations.md +++ b/docs-site/src/content/docs/fr/guides/integrations.md @@ -1,10 +1,10 @@ --- title: Intégrations -description: Connectez opencodex à OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, gjc, DeepSeek Harness, MiniMax Code, ZCode, Prime Agent, Aside, Raycast, omo, Cline CLI, Kilo et Factory Droid depuis le tableau de bord — un commutateur par client, avec une sauvegarde avant chaque écriture. +description: Connectez opencodex à OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, gjc, DeepSeek Harness, MiniMax Code, ZCode, Prime Agent, Aside, Raycast, omo, Cline CLI, Kilo, Command Code et Factory Droid depuis le tableau de bord — un commutateur par client, avec une sauvegarde avant chaque écriture. --- L'onglet **Connexion** écrit le bloc fournisseur d'opencodex dans le fichier de configuration du client, -puis peut le retirer. Dix-sept clients fonctionnent ainsi, chacun avec son propre commutateur : +puis peut le retirer. Dix-huit clients fonctionnent ainsi, chacun avec son propre commutateur : | Client | Fichier de configuration | Format | Prise d'effet de la modification | Identifiant | |---|---|---|---|---| @@ -24,6 +24,7 @@ puis peut le retirer. Dix-sept clients fonctionnent ainsi, chacun avec son propr | omo | `~/.omo/agent/models.json` | JSON | nouvelles sessions | espace réservé de bouclage | | Cline CLI | `~/.cline/data/settings/providers.json` + `models.json` | JSON | après arrêt et redémarrage | bouclage uniquement | | Kilo | premier fichier existant parmi `kilo.jsonc`, `kilo.json`, `opencode.jsonc`, `opencode.json` ou `config.json` sous `~/.config/kilo` (`XDG_CONFIG_HOME` déplace ce répertoire ; `kilo.jsonc` est créé si aucun n'existe) | JSONC | nouvelles sessions | `OPENCODEX_KILO_API_KEY` | +| Command Code | `~/.commandcode/providers.json` (sous Windows : `%HOME%` si défini, sinon `%USERPROFILE%`, puis `.commandcode\providers.json`) | JSON | au prochain lancement de Command Code | aucun — boucle locale sans clé (`apiKey: false`) | | Factory Droid | `~/.factory/settings.json` (`%USERPROFILE%\.factory\settings.json` sous Windows) | JSON | dès la détection du fichier | boucle locale sans clé | Les modèles GJC dotés d'une échelle d'effort de raisonnement prise en charge exportent `reasoning: true`, `thinking.levels` et `compat.supportsReasoningEffort`, afin que GJC propose le choix de l'effort. Les modèles Codex natifs reçoivent leur échelle standard même si le catalogue l'omet. Ces champs sont absents sans échelle connue ; `none` n'envoie pas d'effort et `ultra` devient `max` sur le réseau. Actualisez l'intégration pour mettre à jour ces options. diff --git a/docs-site/src/content/docs/fr/reference/cli/agents.md b/docs-site/src/content/docs/fr/reference/cli/agents.md index c7fb629c9a5..a3bb13feaa4 100644 --- a/docs-site/src/content/docs/fr/reference/cli/agents.md +++ b/docs-site/src/content/docs/fr/reference/cli/agents.md @@ -179,7 +179,7 @@ Gérez et appliquez la clôture du modèle Grok Build. ## Exportation de la configuration client -### `ocx export --client ` +### `ocx export --client ` Imprimez une configuration client connectée au proxy en cours d'exécution. La commande sérialise le bloc fournisseur `opencodex` — URL de base, liste de modèles et référence d’identifiant du client @@ -190,7 +190,7 @@ les modèles Codex peuvent actuellement voir. | Option | Actions | | --- | --- | -| `--client ` | Requis. Sélectionne le dialecte de configuration client. | +| `--client ` | Requis. Sélectionne le dialecte de configuration client. | | `--json` | Imprimez le document généré en tant que JSON sur la sortie standard pour les scripts. Il s'agit de JSON même lorsque le format natif du client sélectionné est YAML, TOML ou JSON5. | | `--out ` | Écrivez le format de configuration natif du client dans ``. Refuse de remplacer un fichier existant. | | `--force` | Autoriser `--out` à remplacer un fichier existant. | @@ -224,6 +224,7 @@ propres valeurs par défaut à ces lignes. | `raycast` | `~/.config/raycast/ai/providers.yaml`, sur macOS comme sur Windows (Raycast n'honore pas `XDG_CONFIG_HOME`) | `raycast-providers.yaml` | aucun — bouclage uniquement, aucune entrée `api_keys` n'est écrite | | `omo` | `~/.omo/agent/models.json` (`OMO_CODING_AGENT_DIR`, puis `SENPI_CODING_AGENT_DIR`, puis `PI_CODING_AGENT_DIR` l'emportent dans cet ordre une fois définis ; une valeur relative est refusée) | `omo-models.json` | aucun — espace réservé de bouclage | | `kilo` | premier fichier existant parmi `kilo.jsonc`, `kilo.json`, `opencode.jsonc`, `opencode.json` ou `config.json` sous `~/.config/kilo` (`XDG_CONFIG_HOME` déplace ce répertoire) ; utilise `kilo.jsonc` si aucun n'existe | `kilo.jsonc` | `OPENCODEX_KILO_API_KEY` | +| `commandcode` | `~/.commandcode/providers.json` (sous Windows : `HOME` si défini, sinon `%USERPROFILE%`) | `providers.json` | boucle locale uniquement ; `apiKey: false` ; aucune variable d’environnement | | `droid` | `~/.factory/settings.json` (`%USERPROFILE%\.factory\settings.json` on Windows) | `factory-settings.json` | boucle locale uniquement ; aucune variable d’environnement | L'exportation Raycast est un document `providers.yaml` autonome contenant un seul élément `id: opencodex` diff --git a/docs-site/src/content/docs/guides/integrations.md b/docs-site/src/content/docs/guides/integrations.md index 2ad08330021..40357f7717e 100644 --- a/docs-site/src/content/docs/guides/integrations.md +++ b/docs-site/src/content/docs/guides/integrations.md @@ -1,10 +1,10 @@ --- title: Integrations -description: Connect opencodex to OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, gjc, DeepSeek Harness, MiniMax Code, ZCode, Prime Agent, Aside, Raycast, omo, Cline CLI, Kilo and Factory Droid from the dashboard — one switch per client, with a backup taken before every write. +description: Connect opencodex to OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, gjc, DeepSeek Harness, MiniMax Code, ZCode, Prime Agent, Aside, Raycast, omo, Cline CLI, Kilo, Command Code and Factory Droid from the dashboard — one switch per client, with a backup taken before every write. --- The **Connect** page writes opencodex's provider block into a client's own config -file, and removes it again. Seventeen clients work this way, each with a switch: +file, and removes it again. Eighteen clients work this way, each with a switch: | Client | Config file | Format | When the change takes effect | Credential | |---|---|---|---|---| @@ -24,6 +24,7 @@ file, and removes it again. Seventeen clients work this way, each with a switch: | omo (Pi / senpi) | `~/.omo/agent/models.json` | JSON | new sessions | loopback placeholder | | Cline CLI | `~/.cline/data/settings/providers.json` and sibling `models.json` | JSON pair | after stopping and restarting Cline | loopback placeholder | | Kilo | first existing `kilo.jsonc`, `kilo.json`, `opencode.jsonc`, `opencode.json`, or `config.json` under `~/.config/kilo` | JSONC | new sessions | `OPENCODEX_KILO_API_KEY` | +| Command Code | `~/.commandcode/providers.json` (on Windows: `%HOME%` if set, otherwise `%USERPROFILE%`, under `.commandcode\providers.json`) | JSON | next launch of Command Code | none — keyless loopback (`apiKey: false`) | | Factory Droid | `~/.factory/settings.json` (`%USERPROFILE%\.factory\settings.json` on Windows) | JSON | immediately via file watching | none — keyless loopback | "omo" names three products that share the `~/.omo` folder. The **omo** tab manages Pi-based omo @@ -741,6 +742,39 @@ ocx integration client enable --client kilo ocx export --client kilo --out ./kilo.jsonc ``` +## Command Code + +Command Code keeps its custom providers in `~/.commandcode/providers.json`. This +integration writes `provider.opencodex` into that file and is loopback-only. + +**Which root it writes.** Command Code resolves its provider map as +`document.provider ?? document.providers`, so the singular root wins whenever it +exists. The integration therefore reads the target file first and writes into the +root that file already uses: a document that already carries a `providers` map keeps +it, and your other providers stay readable. A fresh file gets the singular `provider` +root. Both roots are declared as managed paths, so Disable removes the block from +whichever one it was written into. + +**The credential form.** The provider is written with `apiKey: false`, the documented +form for an endpoint that needs no key. A raw string is refused by Command Code, and +no service-token file is read, referenced, or exported by this integration. + +**Home resolution.** There is no `COMMANDCODE_HOME` override here. The published +client resolves `HOME ?? USERPROFILE` and then appends `/.commandcode/providers.json`; +it does not consult that variable. An override this integration honoured but the +client ignored would make Apply report success at a path Command Code never opens, so +the path is always `~/.commandcode/providers.json`. +On Windows, the integration follows the same HOME-then-USERPROFILE order as the +client, so a HOME set by Git for Windows or MSYS2 is honoured. + +Paste the key yourself if you later move the provider off loopback: Command Code +stores keys in `~/.commandcode/auth.json` (via `/connect`), not in `providers.json`. + +```bash +ocx integration client enable --client commandcode +ocx export --client commandcode --out ./providers.json +``` + ## Factory Droid Run Droid once to create `~/.factory`, then explicitly enable this integration with diff --git a/docs-site/src/content/docs/ja/guides/integrations.md b/docs-site/src/content/docs/ja/guides/integrations.md index adc738aac7b..76950d6f354 100644 --- a/docs-site/src/content/docs/ja/guides/integrations.md +++ b/docs-site/src/content/docs/ja/guides/integrations.md @@ -1,9 +1,9 @@ --- title: クライアント統合 -description: ダッシュボードから opencodex を OpenCode、Pi、OMP、Hermes、OpenClaw、Kimi Code、gjc、DeepSeek Harness、MiniMax Code、ZCode、Prime Agent、Aside、Raycast、omo、Cline CLI、Kilo、Factory Droid に接続します。クライアントごとにスイッチがあり、書き込み前には必ずバックアップを取ります。 +description: ダッシュボードから opencodex を OpenCode、Pi、OMP、Hermes、OpenClaw、Kimi Code、gjc、DeepSeek Harness、MiniMax Code、ZCode、Prime Agent、Aside、Raycast、omo、Cline CLI、Kilo、Command Code、Factory Droid に接続します。クライアントごとにスイッチがあり、書き込み前には必ずバックアップを取ります。 --- -**接続** タブは、各クライアントの設定ファイルに opencodex のプロバイダーブロックを書き込み、必要に応じて削除します。次の 17 クライアントは、それぞれのスイッチで管理できます。 +**接続** タブは、各クライアントの設定ファイルに opencodex のプロバイダーブロックを書き込み、必要に応じて削除します。次の 18 クライアントは、それぞれのスイッチで管理できます。 | クライアント | 設定ファイル | 形式 | 変更が反映される時点 | 認証情報 | |---|---|---|---|---| @@ -23,6 +23,7 @@ description: ダッシュボードから opencodex を OpenCode、Pi、OMP、Her | omo | `~/.omo/agent/models.json` | JSON | 新しいセッション | ループバック用プレースホルダー | | Cline CLI | `~/.cline/data/settings/providers.json` と同階層の `models.json` | JSON のペア | Cline の停止と再起動後 | ループバック用プレースホルダー | | Kilo | `~/.config/kilo` 内で最初に存在する `kilo.jsonc`、`kilo.json`、`opencode.jsonc`、`opencode.json`、`config.json`(`XDG_CONFIG_HOME` でディレクトリを変更可能。どれもなければ `kilo.jsonc` を作成) | JSONC | 新しいセッション | `OPENCODEX_KILO_API_KEY` | +| Command Code | `~/.commandcode/providers.json` (Windows: `%HOME%` が設定されていればその配下、なければ `%USERPROFILE%` 配下の `.commandcode\providers.json`) | JSON | Command Code の次回起動時 | なし — キー不要のループバック (`apiKey: false`) | | Factory Droid | `~/.factory/settings.json` (`%USERPROFILE%\.factory\settings.json` Windows の場合) | JSON | ファイル変更を即時反映 | キー不要のループバック | 生成されるカタログには、各プロバイダーの選択で有効なモデルのみが含まれます。これはダウンロードと管理対象の統合の両方に適用され、Pi と Aside も対象です。管理画面のモデル一覧にはすべてのモデルが表示されるため、追加のモデルを有効にできます。 diff --git a/docs-site/src/content/docs/ja/reference/cli/agents.md b/docs-site/src/content/docs/ja/reference/cli/agents.md index 562f64aaaf5..13bd4a21a84 100644 --- a/docs-site/src/content/docs/ja/reference/cli/agents.md +++ b/docs-site/src/content/docs/ja/reference/cli/agents.md @@ -140,7 +140,7 @@ Grok Build モデル フェンスを管理および適用します。 ## クライアント設定のエクスポート -### `ocx export --client ` +### `ocx export --client ` 実行中のプロキシに接続するクライアント設定を出力します。このコマンドは、ベース URL、モデル一覧、およびクライアントに応じた認証情報参照または `opencodex-loopback` プレースホルダーを含む `opencodex` プロバイダーブロックを、選択したクライアントのネイティブ形式でシリアル化します。 @@ -148,7 +148,7 @@ Grok Build モデル フェンスを管理および適用します。 |旗 |アクション | | --- | --- | -| `--client ` |必須。クライアントの設定形式を選択します。 | +| `--client ` |必須。クライアントの設定形式を選択します。 | | `--json` |構成 JSON のみを標準出力に出力するため、リダイレクトはバイト正確な出力をキャプチャします。 `--out` 書き込みメモを含むすべての診断は stderr に送られます。 | | `--out ` |設定を `` に書き込みます。既存のファイルの置き換えを拒否します。 | | `--force` | `--out` が既存のファイルを置き換えることを許可します。 | @@ -179,6 +179,7 @@ ocx export --client opencode --out ~/opencodex-opencode.json | `raycast` | `~/.config/raycast/ai/providers.yaml` (macOS と Windows で同じ。Raycast は `XDG_CONFIG_HOME` を尊重しません) | `raycast-providers.yaml` | なし — loopback のみ。`api_keys` エントリは書き込まれません | | `omo` | `~/.omo/agent/models.json` (`OMO_CODING_AGENT_DIR`、次に `SENPI_CODING_AGENT_DIR`、次に `PI_CODING_AGENT_DIR` の順で設定時に優先。相対値は拒否されます) | `omo-models.json` | なし — loopback placeholder | | `kilo` | `~/.config/kilo` 配下で最初に存在する `kilo.jsonc`、`kilo.json`、`opencode.jsonc`、`opencode.json`、`config.json`(`XDG_CONFIG_HOME` が設定されていればその配下)。候補がなければ `kilo.jsonc` | `kilo.jsonc` | `OPENCODEX_KILO_API_KEY` | +| `commandcode` | `~/.commandcode/providers.json` (Windows: `HOME` が設定されていればその値、なければ `%USERPROFILE%`) | `providers.json` | ループバックのみ・`apiKey: false`・環境変数不要 | | `droid` | `~/.factory/settings.json` (`%USERPROFILE%\.factory\settings.json` on Windows) | `factory-settings.json` | ループバックのみ・環境変数不要 | Raycast のエクスポートは、`providers` シーケンスに `id: opencodex` 要素を 1 つだけ持つ独立した `providers.yaml` 文書です。内容は `name: OpenCodex`、プロキシの `/v1` ベース URL、および `abilities` 付きのルーティング済み全モデルです (`tools` と `system_message` は常にサポート、`vision` はカタログの入力モダリティから、`reasoning_effort` はモデルに effort ラダーがある場合、`temperature` は推論モデルではオフ)。Custom Providers は Raycast Pro の機能で、Raycast はこのファイルを監視しているため、保存した変更は再起動なしで反映されます。形式は [manual.raycast.com/ai/custom-providers](https://manual.raycast.com/ai/custom-providers) に記載されています。`api_keys` エントリは書き込まれないため、このエクスポートは loopback 専用で、loopback 以外のバインドは拒否されます。 diff --git a/docs-site/src/content/docs/ko/guides/integrations.md b/docs-site/src/content/docs/ko/guides/integrations.md index 9e09c8167eb..bb738f1d0fd 100644 --- a/docs-site/src/content/docs/ko/guides/integrations.md +++ b/docs-site/src/content/docs/ko/guides/integrations.md @@ -1,9 +1,9 @@ --- title: 연동 -description: 대시보드에서 OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, gjc, DeepSeek Harness, MiniMax Code, ZCode, Prime Agent, Aside, Raycast, omo, Cline CLI, Kilo와 Factory Droid를 opencodex에 연결합니다. 클라이언트마다 스위치가 하나씩 있으며 기록 전마다 백업합니다. +description: 대시보드에서 OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, gjc, DeepSeek Harness, MiniMax Code, ZCode, Prime Agent, Aside, Raycast, omo, Cline CLI, Kilo, Command Code와 Factory Droid를 opencodex에 연결합니다. 클라이언트마다 스위치가 하나씩 있으며 기록 전마다 백업합니다. --- -**연결** 페이지는 클라이언트의 설정 파일에 opencodex 프로바이더 블록을 쓰고 다시 제거합니다. 다음 17개 클라이언트는 각각 스위치로 관리합니다. +**연결** 페이지는 클라이언트의 설정 파일에 opencodex 프로바이더 블록을 쓰고 다시 제거합니다. 다음 18개 클라이언트는 각각 스위치로 관리합니다. | 클라이언트 | 설정 파일 | 형식 | 변경 적용 시점 | 자격 증명 | |---|---|---|---|---| @@ -23,6 +23,7 @@ description: 대시보드에서 OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, | omo | `~/.omo/agent/models.json` | JSON | 새 세션에서 | 루프백 자리표시자 | | Cline CLI | `~/.cline/data/settings/providers.json` 및 같은 위치의 `models.json` | JSON 파일 쌍 | Cline을 중지하고 다시 시작한 뒤 | 루프백 자리표시자 | | Kilo | `~/.config/kilo`에서 먼저 존재하는 `kilo.jsonc`, `kilo.json`, `opencode.jsonc`, `opencode.json`, `config.json` (`XDG_CONFIG_HOME`로 디렉터리 변경 가능, 모두 없으면 `kilo.jsonc` 생성) | JSONC | 새 세션에서 | `OPENCODEX_KILO_API_KEY` | +| Command Code | `~/.commandcode/providers.json` (Windows: `%HOME%`이 설정되어 있으면 해당 경로, 아니면 `%USERPROFILE%` 아래의 `.commandcode\providers.json`) | JSON | 다음 Command Code 실행 시 | 없음 — 키 없는 루프백 (`apiKey: false`) | | Factory Droid | `~/.factory/settings.json` (`%USERPROFILE%\.factory\settings.json` Windows에서) | JSON | 파일 변경 시 즉시 | 키 없는 루프백 | 생성된 카탈로그에는 각 프로바이더 선택에서 활성화된 모델만 들어갑니다. Pi와 Aside를 포함한 다운로드와 관리형 연동 모두에 적용됩니다. 관리 모델 목록에는 전체 모델이 계속 표시되어 추가 모델을 활성화할 수 있습니다. diff --git a/docs-site/src/content/docs/ko/reference/cli/agents.md b/docs-site/src/content/docs/ko/reference/cli/agents.md index 53f508c4f98..21a6b4e511f 100644 --- a/docs-site/src/content/docs/ko/reference/cli/agents.md +++ b/docs-site/src/content/docs/ko/reference/cli/agents.md @@ -170,7 +170,7 @@ Grok Build model fence를 관리하고 적용합니다. ## 클라이언트 설정 내보내기 -### `ocx export --client ` +### `ocx export --client ` 실행 중인 프록시에 연결할 client config를 출력합니다. 이 명령은 base URL, model list, 그리고 client에 따라 credential reference 또는 `opencodex-loopback` placeholder를 포함한 `opencodex` provider block을 선택한 client의 네이티브 형식으로 직렬화합니다. @@ -178,7 +178,7 @@ Grok Build model fence를 관리하고 적용합니다. | 플래그 | 동작 | | --- | --- | -| `--client ` | 필수입니다. 클라이언트 설정 형식을 선택합니다. | +| `--client ` | 필수입니다. 클라이언트 설정 형식을 선택합니다. | | `--json` | config JSON만 stdout에 출력하므로, redirect가 byte-exact 출력을 캡처합니다. `--out` write note를 포함한 모든 진단 메시지는 stderr로 갑니다. | | `--out ` | config를 ``에 씁니다. 기존 파일이 있으면 덮어쓰지 않습니다. | | `--force` | `--out`이 기존 파일을 덮어쓰도록 허용합니다. | @@ -209,6 +209,7 @@ ocx export --client opencode --out ~/opencodex-opencode.json | `raycast` | `~/.config/raycast/ai/providers.yaml` (macOS와 Windows 모두 동일. Raycast는 `XDG_CONFIG_HOME`을 따르지 않습니다) | `raycast-providers.yaml` | 없음 — loopback 전용. `api_keys` 항목은 쓰지 않습니다 | | `omo` | `~/.omo/agent/models.json` (`OMO_CODING_AGENT_DIR`, `SENPI_CODING_AGENT_DIR`, `PI_CODING_AGENT_DIR` 순서로 설정된 값이 우선. 상대 경로는 거부됩니다) | `omo-models.json` | 없음 — loopback placeholder | | `kilo` | `~/.config/kilo`에서 먼저 존재하는 `kilo.jsonc`, `kilo.json`, `opencode.jsonc`, `opencode.json` 또는 `config.json` (`XDG_CONFIG_HOME`이 설정되면 해당 디렉터리 사용); 후보가 없으면 `kilo.jsonc` | `kilo.jsonc` | `OPENCODEX_KILO_API_KEY` | +| `commandcode` | `~/.commandcode/providers.json` (Windows: `HOME`이 설정되어 있으면 해당 경로, 아니면 `%USERPROFILE%`) | `providers.json` | 루프백 전용; `apiKey: false`; 환경 변수 불필요 | | `droid` | `~/.factory/settings.json` (`%USERPROFILE%\.factory\settings.json` on Windows) | `factory-settings.json` | 루프백 전용, 환경 변수 불필요 | Raycast 내보내기는 `providers` 시퀀스에 `id: opencodex` 요소 하나만 담은 독립 `providers.yaml` 문서입니다. 내용은 `name: OpenCodex`, proxy의 `/v1` base URL, 그리고 `abilities`가 붙은 라우팅된 모든 모델입니다(`tools`와 `system_message`는 항상 지원, `vision`은 카탈로그의 입력 모달리티를 따름, `reasoning_effort`는 모델에 effort 사다리가 있을 때, `temperature`는 추론 모델에서 꺼짐). Custom Providers는 Raycast Pro 기능이며, Raycast가 이 파일을 감시하므로 저장한 변경은 재시작 없이 적용됩니다. 형식은 [manual.raycast.com/ai/custom-providers](https://manual.raycast.com/ai/custom-providers)에 문서화되어 있습니다. `api_keys` 항목은 쓰지 않으므로 이 내보내기는 loopback 전용이며, loopback이 아닌 bind는 거부됩니다. diff --git a/docs-site/src/content/docs/reference/cli/agents.md b/docs-site/src/content/docs/reference/cli/agents.md index 76dbf9947bb..5f032984ade 100644 --- a/docs-site/src/content/docs/reference/cli/agents.md +++ b/docs-site/src/content/docs/reference/cli/agents.md @@ -903,7 +903,7 @@ Manage and apply the Grok Build model fence. ## Client config export -### `ocx export --client ` +### `ocx export --client ` Print a client config wired to the running proxy. The command serializes the `opencodex` provider block — base URL, model list, and the client's credential @@ -919,7 +919,7 @@ See [client integrations](/guides/integrations/) for managed refresh and upgrade | Flag | Action | | --- | --- | -| `--client ` | Required. Selects the client config dialect. | +| `--client ` | Required. Selects the client config dialect. | | `--json` | Print the generated document as JSON on stdout for scripts. This is JSON even when the selected client's native format is YAML, TOML, or JSON5. | | `--out ` | Write the client's native config format to ``. Refuses to replace an existing file. | | `--force` | Allow `--out` to replace an existing file. | @@ -952,6 +952,7 @@ client applies its own defaults for those). | `raycast` | `~/.config/raycast/ai/providers.yaml` on macOS and Windows alike (Raycast does not honor `XDG_CONFIG_HOME`) | `raycast-providers.yaml` | none — loopback only, no `api_keys` entry is written | | `omo` | `~/.omo/agent/models.json` (`OMO_CODING_AGENT_DIR`, then `SENPI_CODING_AGENT_DIR`, then `PI_CODING_AGENT_DIR` win in that order when set; a relative value is refused) | `omo-models.json` | none — loopback placeholder | | `kilo` | first existing `kilo.jsonc`, `kilo.json`, `opencode.jsonc`, `opencode.json`, or `config.json` under `~/.config/kilo` (`XDG_CONFIG_HOME` relocates that directory); uses `kilo.jsonc` when none exists | `kilo.jsonc` | `OPENCODEX_KILO_API_KEY` | +| `commandcode` | `~/.commandcode/providers.json` (on Windows: `HOME` if set, otherwise `%USERPROFILE%`) | `providers.json` | loopback only; `apiKey: false`; no environment variable | | `droid` | `~/.factory/settings.json` (`%USERPROFILE%\.factory\settings.json` on Windows) | `factory-settings.json` | loopback only; no environment variable | The managed DSH export requires DSH 0.1.0-rc.6 or newer and owns only diff --git a/docs-site/src/content/docs/reference/configuration.md b/docs-site/src/content/docs/reference/configuration.md index 909edc30e26..06cf68b87c6 100644 --- a/docs-site/src/content/docs/reference/configuration.md +++ b/docs-site/src/content/docs/reference/configuration.md @@ -61,7 +61,7 @@ a known configured model id. Cursor may require a model-list refresh or restart `fastRows` is an optional boolean and defaults to `true`. The raw OpenAI-style `/v1/models` list, Claude Code discovery, and client config exports (including pi, OpenCode, -OMP, Hermes, OpenClaw, Kimi, gjc, DSH, MCode, ZCode, Prime, Aside, Raycast, omo, Cline, and Kilo) add a `--fast` selector for every model whose +OMP, Hermes, OpenClaw, Kimi, gjc, DSH, MCode, ZCode, Prime, Aside, Raycast, omo, Cline, Kilo, and Command Code) add a `--fast` selector for every model whose resolved Fast policy is eligible. Selecting one routes the base model and requests the `priority` service tier — the same Fast the Codex app exposes through its picker toggle. The base row stays listed, so the row is an addition rather than a replacement. diff --git a/docs-site/src/content/docs/ru/guides/integrations.md b/docs-site/src/content/docs/ru/guides/integrations.md index 6f46dcd8282..583d9896f54 100644 --- a/docs-site/src/content/docs/ru/guides/integrations.md +++ b/docs-site/src/content/docs/ru/guides/integrations.md @@ -1,10 +1,10 @@ --- title: Интеграции -description: Подключайте opencodex к OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, gjc, DeepSeek Harness, MiniMax Code, ZCode, Prime Agent, Aside, Raycast, omo, Cline CLI, Kilo и Factory Droid из дашборда — отдельный переключатель для каждого клиента и резервная копия перед каждой записью. +description: Подключайте opencodex к OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, gjc, DeepSeek Harness, MiniMax Code, ZCode, Prime Agent, Aside, Raycast, omo, Cline CLI, Kilo, Command Code и Factory Droid из дашборда — отдельный переключатель для каждого клиента и резервная копия перед каждой записью. --- Вкладка **Подключение** записывает блок провайдера opencodex в собственный файл -конфигурации клиента и при необходимости удаляет его. Так работают семнадцать +конфигурации клиента и при необходимости удаляет его. Так работают восемнадцать клиентов, у каждого свой переключатель: | Клиент | Файл конфигурации | Формат | Когда изменение начинает действовать | Учётные данные | @@ -25,6 +25,7 @@ description: Подключайте opencodex к OpenCode, Pi, OMP, Hermes, Open | omo | `~/.omo/agent/models.json` | JSON | в новых сессиях | заглушка для loopback | | Cline CLI | `~/.cline/data/settings/providers.json` и соседний `models.json` | пара JSON | после остановки и повторного запуска Cline | заглушка для loopback | | Kilo | первый существующий файл среди `kilo.jsonc`, `kilo.json`, `opencode.jsonc`, `opencode.json` и `config.json` в `~/.config/kilo` (`XDG_CONFIG_HOME` переносит каталог; если файлов нет, создаётся `kilo.jsonc`) | JSONC | в новых сессиях | `OPENCODEX_KILO_API_KEY` | +| Command Code | `~/.commandcode/providers.json` (в Windows: `%HOME%`, если задана, иначе `%USERPROFILE%`, затем `.commandcode\providers.json`) | JSON | при следующем запуске Command Code | нет — loopback без ключа (`apiKey: false`) | | Factory Droid | `~/.factory/settings.json` (`%USERPROFILE%\.factory\settings.json` в Windows) | JSON | сразу при изменении файла | loopback без ключа | Создаваемые каталоги включают только модели, включённые в настройках каждого diff --git a/docs-site/src/content/docs/ru/reference/cli/agents.md b/docs-site/src/content/docs/ru/reference/cli/agents.md index 545181350e4..2e9ee5ccee3 100644 --- a/docs-site/src/content/docs/ru/reference/cli/agents.md +++ b/docs-site/src/content/docs/ru/reference/cli/agents.md @@ -166,7 +166,7 @@ override, но файлы на диске никогда не меняются. ## Экспорт client config -### `ocx export --client ` +### `ocx export --client ` Печатает client config, направленный на работающий прокси. Команда сериализует блок провайдера `opencodex` в нативном формате выбранного клиента: base URL, список моделей и, @@ -177,7 +177,7 @@ override, но файлы на диске никогда не меняются. | Флаг | Действие | | --- | --- | -| `--client ` | Обязателен. Выбирает формат конфигурации клиента. | +| `--client ` | Обязателен. Выбирает формат конфигурации клиента. | | `--json` | Печатать только JSON-конфиг в stdout, чтобы redirect сохранял побайтно точный вывод. Вся диагностика, включая заметку о записи через `--out`, идёт в stderr. | | `--out ` | Записать конфиг в ``. Перезаписывать существующий файл не позволит. | | `--force` | Разрешить `--out` заменить существующий файл. | @@ -211,6 +211,7 @@ ocx export --client opencode --out ~/opencodex-opencode.json | `raycast` | `~/.config/raycast/ai/providers.yaml` одинаково на macOS и Windows (Raycast не учитывает `XDG_CONFIG_HOME`) | `raycast-providers.yaml` | нет — только loopback, запись `api_keys` не создаётся | | `omo` | `~/.omo/agent/models.json` (`OMO_CODING_AGENT_DIR`, затем `SENPI_CODING_AGENT_DIR`, затем `PI_CODING_AGENT_DIR` имеют приоритет в этом порядке, если заданы; относительное значение отклоняется) | `omo-models.json` | нет — loopback placeholder | | `kilo` | первый существующий файл среди `kilo.jsonc`, `kilo.json`, `opencode.jsonc`, `opencode.json` или `config.json` в `~/.config/kilo` (`XDG_CONFIG_HOME` переносит каталог); если ни одного нет, используется `kilo.jsonc` | `kilo.jsonc` | `OPENCODEX_KILO_API_KEY` | +| `commandcode` | `~/.commandcode/providers.json` (в Windows: `HOME`, если задана, иначе `%USERPROFILE%`) | `providers.json` | только loopback; `apiKey: false`; переменная окружения не нужна | | `droid` | `~/.factory/settings.json` (`%USERPROFILE%\.factory\settings.json` on Windows) | `factory-settings.json` | только loopback; переменная окружения не нужна | Экспорт для Raycast — это отдельный документ `providers.yaml` с одним элементом `id: opencodex` в diff --git a/docs-site/src/content/docs/tr/guides/integrations.md b/docs-site/src/content/docs/tr/guides/integrations.md index 77b3def8080..9859f980232 100644 --- a/docs-site/src/content/docs/tr/guides/integrations.md +++ b/docs-site/src/content/docs/tr/guides/integrations.md @@ -1,10 +1,10 @@ --- title: Entegrasyonlar -description: Kontrol panelinden OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, gjc, DeepSeek Harness, MiniMax Code, ZCode, Prime Agent, Aside, Raycast, omo, Cline CLI, Kilo ve Factory Droid'u opencodex'e bağlayın — istemci başına tek bir anahtar ve her yazmadan önce alınan bir yedek. +description: Kontrol panelinden OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, gjc, DeepSeek Harness, MiniMax Code, ZCode, Prime Agent, Aside, Raycast, omo, Cline CLI, Kilo, Command Code ve Factory Droid'u opencodex'e bağlayın — istemci başına tek bir anahtar ve her yazmadan önce alınan bir yedek. --- **Bağlantı** sekmesi, opencodex'in sağlayıcı bloğunu istemcinin kendi -yapılandırma dosyasına yazar ve tekrar kaldırır. On yedi istemci bu şekilde +yapılandırma dosyasına yazar ve tekrar kaldırır. On sekiz istemci bu şekilde çalışır, her biri bir anahtarla: | İstemci | Yapılandırma dosyası | Format | Değişiklik ne zaman geçerli olur? | Kimlik bilgisi | @@ -25,6 +25,7 @@ yapılandırma dosyasına yazar ve tekrar kaldırır. On yedi istemci bu şekild | omo | `~/.omo/agent/models.json` | JSON | yeni oturumlarda | geri döngü yer tutucusu | | Cline CLI | `~/.cline/data/settings/providers.json` + `models.json` | JSON | kapatıp yeniden başlattıktan sonra | yalnızca loopback | | Kilo | `~/.config/kilo` altında ilk bulunan `kilo.jsonc`, `kilo.json`, `opencode.jsonc`, `opencode.json` veya `config.json` (`XDG_CONFIG_HOME` bu dizini taşır; hiçbiri yoksa `kilo.jsonc` oluşturulur) | JSONC | yeni oturumlarda | `OPENCODEX_KILO_API_KEY` | +| Command Code | `~/.commandcode/providers.json` (Windows'ta: ayarlanmışsa `%HOME%`, aksi halde `%USERPROFILE%` altında `.commandcode\providers.json`) | JSON | Command Code'un sonraki başlatılmasında | yok — anahtarsız geri döngü (`apiKey: false`) | | Factory Droid | `~/.factory/settings.json` (`%USERPROFILE%\.factory\settings.json` Windows'ta) | JSON | dosya değişince hemen | anahtarsız geri döngü | Desteklenen akıl yürütme düzeylerine sahip GJC modelleri, GJC'nin düzey seçimi sunabilmesi için `reasoning: true`, `thinking.levels` ve `compat.supportsReasoningEffort` alanlarını dışa aktarır. Yerel Codex modelleri, katalogda belirtilmese bile standart düzeylerini alır. Bilinen düzeyi olmayan modellerde bu alanlar bulunmaz. `none` düzey göndermez ve `ultra` gönderimde `max` düzeyine dönüşür; bu yüzden seçeneklerde yer almazlar. Model seçeneklerini güncellemek için entegrasyonu yenileyin. diff --git a/docs-site/src/content/docs/tr/reference/cli/agents.md b/docs-site/src/content/docs/tr/reference/cli/agents.md index 7f77c3e01ba..bbee270d4b4 100644 --- a/docs-site/src/content/docs/tr/reference/cli/agents.md +++ b/docs-site/src/content/docs/tr/reference/cli/agents.md @@ -204,7 +204,7 @@ Grok Build model çitini yönetin ve uygulayın. ## İstemci yapılandırma dışa aktarma -### `ocx export --client ` +### `ocx export --client ` Çalışan proxy'ye bağlı bir istemci yapılandırmasını yazdırın. Komut, `opencodex` sağlayıcı bloğunu — temel URL, model listesi ve istemcinin kimlik bilgisi @@ -216,7 +216,7 @@ yalnızca Codex'in şu anda görebildiği modelleri yayınlar. | Bayrak | Eylem | | --- | --- | -| `--client ` | Gerekli. İstemci yapılandırma lehçesini seçer. | +| `--client ` | Gerekli. İstemci yapılandırma lehçesini seçer. | | `--json` | Betikler için stdout üzerinde oluşturulan belgeyi JSON olarak yazdırın. Bu, seçilen istemcinin yerel formatı YAML, TOML veya JSON5 olsa bile JSON'dur. | | `--out ` | İstemcinin yerel yapılandırma formatını `` konumuna yazın. Mevcut bir dosyanın üzerine yazmayı reddeder. | | `--force` | `--out`'un mevcut bir dosyanın üzerine yazmasına izin verin. | @@ -250,6 +250,7 @@ için kendi varsayılanlarını uygular) gelir. | `raycast` | `~/.config/raycast/ai/providers.yaml`, macOS ve Windows'ta aynı (Raycast `XDG_CONFIG_HOME` değerini dikkate almaz) | `raycast-providers.yaml` | yok — yalnızca geri döngü, `api_keys` girdisi yazılmaz | | `omo` | `~/.omo/agent/models.json` (ayarlandığında sırasıyla `OMO_CODING_AGENT_DIR`, `SENPI_CODING_AGENT_DIR`, `PI_CODING_AGENT_DIR` öncelikli; göreli değer reddedilir) | `omo-models.json` | yok — geri döngü yer tutucusu | | `kilo` | `~/.config/kilo` altında ilk bulunan `kilo.jsonc`, `kilo.json`, `opencode.jsonc`, `opencode.json` veya `config.json` (`XDG_CONFIG_HOME` bu dizini taşır); hiçbiri yoksa `kilo.jsonc` kullanılır | `kilo.jsonc` | `OPENCODEX_KILO_API_KEY` | +| `commandcode` | `~/.commandcode/providers.json` (Windows'ta: ayarlanmışsa `HOME`, aksi halde `%USERPROFILE%`) | `providers.json` | yalnızca geri döngü; `apiKey: false`; ortam değişkeni gerekmez | | `droid` | `~/.factory/settings.json` (`%USERPROFILE%\.factory\settings.json` on Windows) | `factory-settings.json` | yalnızca loopback; ortam değişkeni gerekmez | Raycast dışa aktarımı, `providers` dizisinde tek bir `id: opencodex` öğesi içeren bağımsız diff --git a/docs-site/src/content/docs/zh-cn/guides/integrations.md b/docs-site/src/content/docs/zh-cn/guides/integrations.md index 17f43b6a644..ea354ca2eca 100644 --- a/docs-site/src/content/docs/zh-cn/guides/integrations.md +++ b/docs-site/src/content/docs/zh-cn/guides/integrations.md @@ -1,9 +1,9 @@ --- title: 集成 -description: 从仪表盘将 opencodex 连接到 OpenCode、Pi、OMP、Hermes、OpenClaw、Kimi Code、gjc、DeepSeek Harness、MiniMax Code、ZCode、Prime Agent、Aside、Raycast、omo、Cline CLI、Kilo 和 Factory Droid;每个客户端都有独立开关,且每次写入前都会备份。 +description: 从仪表盘将 opencodex 连接到 OpenCode、Pi、OMP、Hermes、OpenClaw、Kimi Code、gjc、DeepSeek Harness、MiniMax Code、ZCode、Prime Agent、Aside、Raycast、omo、Cline CLI、Kilo、Command Code 和 Factory Droid;每个客户端都有独立开关,且每次写入前都会备份。 --- -**连接** 标签页可将 opencodex 的提供商配置块写入客户端自己的配置文件,也可再次移除。以下 17 个客户端都采用这种方式,各有独立开关: +**连接** 标签页可将 opencodex 的提供商配置块写入客户端自己的配置文件,也可再次移除。以下 18 个客户端都采用这种方式,各有独立开关: | 客户端 | 配置文件 | 格式 | 变更生效时间 | 凭据 | |---|---|---|---|---| @@ -23,6 +23,7 @@ description: 从仪表盘将 opencodex 连接到 OpenCode、Pi、OMP、Hermes、 | omo | `~/.omo/agent/models.json` | JSON | 新会话 | 回环占位符 | | Cline CLI | `~/.cline/data/settings/providers.json` 及同目录下的 `models.json` | JSON 文件对 | 停止并重启 Cline 后 | 回环占位符 | | Kilo | `~/.config/kilo` 下最先存在的 `kilo.jsonc`、`kilo.json`、`opencode.jsonc`、`opencode.json` 或 `config.json`(`XDG_CONFIG_HOME` 可迁移目录;均不存在时创建 `kilo.jsonc`) | JSONC | 新会话 | `OPENCODEX_KILO_API_KEY` | +| Command Code | `~/.commandcode/providers.json` (Windows:已设置 `%HOME%` 时使用该目录,否则使用 `%USERPROFILE%`,文件位于其下的 `.commandcode\providers.json`) | JSON | 下次启动 Command Code 时 | 无 — 无密钥回环 (`apiKey: false`) | | Factory Droid | `~/.factory/settings.json` (`%USERPROFILE%\.factory\settings.json` Windows 上) | JSON | 文件变更时立即生效 | 无密钥回环 | 生成的目录只包含各提供商选择中已启用的模型。下载文件和托管集成都遵循这一规则,Pi 和 Aside 也不例外。管理模型列表仍显示完整阵容,以便启用更多模型。 diff --git a/docs-site/src/content/docs/zh-cn/reference/cli/agents.md b/docs-site/src/content/docs/zh-cn/reference/cli/agents.md index 7032e860d07..ab4fd732b0f 100644 --- a/docs-site/src/content/docs/zh-cn/reference/cli/agents.md +++ b/docs-site/src/content/docs/zh-cn/reference/cli/agents.md @@ -144,7 +144,7 @@ ocx claude desktop import [--apply] Validate and import JSON ## Client config export -### `ocx export --client ` +### `ocx export --client ` 输出连接到正在运行代理的客户端配置。此命令会以所选客户端的原生格式序列化 `opencodex` provider 块,其中包含基础 URL、模型列表,以及该客户端适用的凭据引用或 `opencodex-loopback` 占位值。 @@ -152,7 +152,7 @@ ocx claude desktop import [--apply] Validate and import JSON | 标志 | 动作 | | --- | --- | -| `--client ` | 必需。选择客户端配置格式。 | +| `--client ` | 必需。选择客户端配置格式。 | | `--json` | 仅在 stdout 打印配置 JSON,这样重定向即可捕获字节级精确输出。包括 `--out` 写入提示在内的所有诊断信息都会输出到 stderr。 | | `--out ` | 将配置写入 ``。拒绝替换已存在的文件。 | | `--force` | 允许 `--out` 替换已存在的文件。 | @@ -183,6 +183,7 @@ ocx export --client opencode --out ~/opencodex-opencode.json | `raycast` | `~/.config/raycast/ai/providers.yaml`(macOS 与 Windows 相同;Raycast 不遵循 `XDG_CONFIG_HOME`) | `raycast-providers.yaml` | 无 — 仅限回环,不会写入 `api_keys` 条目 | | `omo` | `~/.omo/agent/models.json`(设置后依次由 `OMO_CODING_AGENT_DIR`、`SENPI_CODING_AGENT_DIR`、`PI_CODING_AGENT_DIR` 优先;相对路径会被拒绝) | `omo-models.json` | 无 — loopback placeholder | | `kilo` | `~/.config/kilo` 下最先存在的 `kilo.jsonc`、`kilo.json`、`opencode.jsonc`、`opencode.json` 或 `config.json`(`XDG_CONFIG_HOME` 可更改该目录);均不存在时使用 `kilo.jsonc` | `kilo.jsonc` | `OPENCODEX_KILO_API_KEY` | +| `commandcode` | `~/.commandcode/providers.json` (Windows:已设置 `HOME` 时使用该目录,否则使用 `%USERPROFILE%`) | `providers.json` | 仅限回环;`apiKey: false`;无需环境变量 | | `droid` | `~/.factory/settings.json` (`%USERPROFILE%\.factory\settings.json` on Windows) | `factory-settings.json` | 仅限回环;无需环境变量 | Raycast 导出是一份独立的 `providers.yaml` 文档,在 `providers` 序列中只有一个 `id: opencodex` 元素:`name: OpenCodex`、代理的 `/v1` 基础 URL,以及每个已路由模型及其 `abilities`(`tools` 与 `system_message` 始终支持,`vision` 取自目录的输入模态,`reasoning_effort` 在模型有 effort 阶梯时设置,`temperature` 对推理模型关闭)。Custom Providers 是 Raycast Pro 功能,且 Raycast 会监视该文件,因此保存后的更改无需重启即可生效。格式见 [manual.raycast.com/ai/custom-providers](https://manual.raycast.com/ai/custom-providers)。不会写入任何 `api_keys` 条目,所以该导出仅限回环,非回环绑定会被拒绝。 diff --git a/docs-site/src/content/docs/zh-tw/guides/integrations.md b/docs-site/src/content/docs/zh-tw/guides/integrations.md index 38c9170b76f..6b0ba0eb5e7 100644 --- a/docs-site/src/content/docs/zh-tw/guides/integrations.md +++ b/docs-site/src/content/docs/zh-tw/guides/integrations.md @@ -1,9 +1,9 @@ --- title: 整合 -description: 從儀表板把 opencodex 連接到 OpenCode、Pi、OMP、Hermes、OpenClaw、Kimi Code、gjc、DeepSeek Harness、MiniMax Code、ZCode、Prime Agent、Aside、Raycast、omo、Cline CLI、Kilo 與 Factory Droid——每個客戶端一個開關,每次寫入前都會先備份。 +description: 從儀表板把 opencodex 連接到 OpenCode、Pi、OMP、Hermes、OpenClaw、Kimi Code、gjc、DeepSeek Harness、MiniMax Code、ZCode、Prime Agent、Aside、Raycast、omo、Cline CLI、Kilo、Command Code 與 Factory Droid——每個客戶端一個開關,每次寫入前都會先備份。 --- -**連線** 分頁會把 opencodex 的 provider 區塊寫入客戶端自己的設定檔,也會把它移除。共有十七個客戶端以這種方式運作,每個都有一個開關: +**連線** 分頁會把 opencodex 的 provider 區塊寫入客戶端自己的設定檔,也會把它移除。共有十八個客戶端以這種方式運作,每個都有一個開關: | 客戶端 | 設定檔 | 格式 | 變更生效時機 | 憑證 | |---|---|---|---|---| @@ -23,6 +23,7 @@ description: 從儀表板把 opencodex 連接到 OpenCode、Pi、OMP、Hermes、 | omo | `~/.omo/agent/models.json` | JSON | 新工作階段 | loopback 佔位符 | | Cline CLI | `~/.cline/data/settings/providers.json` + `models.json` | JSON | 結束並重新啟動後 | 僅限 loopback | | Kilo | `~/.config/kilo` 下最先存在的 `kilo.jsonc`、`kilo.json`、`opencode.jsonc`、`opencode.json` 或 `config.json`(`XDG_CONFIG_HOME` 會移動該目錄;若都不存在則建立 `kilo.jsonc`) | JSONC | 新工作階段 | `OPENCODEX_KILO_API_KEY` | +| Command Code | `~/.commandcode/providers.json` (Windows:已設定 `%HOME%` 時使用該目錄,否則使用 `%USERPROFILE%`,檔案位於其下的 `.commandcode\providers.json`) | JSON | 下次啟動 Command Code 時 | 無 — 無金鑰迴環 (`apiKey: false`) | | Factory Droid | `~/.factory/settings.json` (`%USERPROFILE%\.factory\settings.json` Windows 上) | JSON | 檔案變更時立即生效 | 無金鑰迴環 | 具有受支援推理強度階梯的 GJC 模型會匯出 `reasoning: true`、`thinking.levels` 與 `compat.supportsReasoningEffort`,讓 GJC 提供強度選擇。原生 Codex 模型即使未在目錄中列出階梯,也會取得標準階梯。沒有已知階梯的模型會省略這些欄位;`none` 不傳送強度,`ultra` 在傳輸時會折疊成 `max`,因此不會列為選項。重新整理整合即可更新模型選項。 diff --git a/docs-site/src/content/docs/zh-tw/reference/cli/agents.md b/docs-site/src/content/docs/zh-tw/reference/cli/agents.md index 1d2476344d2..4b1fe8de9b5 100644 --- a/docs-site/src/content/docs/zh-tw/reference/cli/agents.md +++ b/docs-site/src/content/docs/zh-tw/reference/cli/agents.md @@ -142,7 +142,7 @@ ocx claude desktop import [--apply] 驗證並匯入 JSON ## 客戶端設定匯出 -### `ocx export --client ` +### `ocx export --client ` 印出連接到執行中代理的客戶端設定。此指令會用所選客戶端的原生格式,序列化含有 base URL、模型清單,以及適用的環境變數參考或 loopback 佔位符的 `opencodex` provider 區塊。 @@ -150,7 +150,7 @@ ocx claude desktop import [--apply] 驗證並匯入 JSON | 旗標 | 動作 | | --- | --- | -| `--client ` | 必填。選擇客戶端設定格式。 | +| `--client ` | 必填。選擇客戶端設定格式。 | | `--json` | 僅在 stdout 印出設定 JSON,使重導向能擷取逐位元組輸出。所有診斷訊息(含 `--out` 寫入提示)皆送至 stderr。 | | `--out ` | 將設定寫入 ``。拒絕覆寫既有檔案。 | | `--force` | 允許 `--out` 覆寫既有檔案。 | @@ -181,6 +181,7 @@ ocx export --client opencode --out ~/opencodex-opencode.json | `raycast` | `~/.config/raycast/ai/providers.yaml`(macOS 與 Windows 相同;Raycast 不遵循 `XDG_CONFIG_HOME`) | `raycast-providers.yaml` | 無——僅限 loopback,不會寫入 `api_keys` 項目 | | `omo` | `~/.omo/agent/models.json`(設定後依序由 `OMO_CODING_AGENT_DIR`、`SENPI_CODING_AGENT_DIR`、`PI_CODING_AGENT_DIR` 優先;相對路徑會被拒絕) | `omo-models.json` | 無——loopback 佔位符 | | `kilo` | `~/.config/kilo` 下最先存在的 `kilo.jsonc`、`kilo.json`、`opencode.jsonc`、`opencode.json` 或 `config.json`(`XDG_CONFIG_HOME` 可變更該目錄);皆不存在時使用 `kilo.jsonc` | `kilo.jsonc` | `OPENCODEX_KILO_API_KEY` | +| `commandcode` | `~/.commandcode/providers.json` (Windows:已設定 `HOME` 時使用該目錄,否則使用 `%USERPROFILE%`) | `providers.json` | 僅限迴環;`apiKey: false`;不需環境變數 | | `droid` | `~/.factory/settings.json` (`%USERPROFILE%\.factory\settings.json` on Windows) | `factory-settings.json` | 僅限迴環;不需環境變數 | Raycast 匯出是一份獨立的 `providers.yaml` 文件,在 `providers` 序列中只有一個 `id: opencodex` 元素:`name: OpenCodex`、proxy 的 `/v1` base URL,以及每個路由模型及其 `abilities`(`tools` 與 `system_message` 一律支援,`vision` 依目錄的輸入模態而定,`reasoning_effort` 在模型有 effort 階梯時設定,`temperature` 對推理模型關閉)。Custom Providers 是 Raycast Pro 功能,且 Raycast 會監看該檔案,因此儲存後的變更不需重新啟動即可生效。格式說明見 [manual.raycast.com/ai/custom-providers](https://manual.raycast.com/ai/custom-providers)。不會寫入任何 `api_keys` 項目,所以此匯出僅限 loopback,非 loopback 的 bind 會被拒絕。 diff --git a/gui/public/provider-icons/README.md b/gui/public/provider-icons/README.md index c54807fc8d3..0e595d7b4f7 100644 --- a/gui/public/provider-icons/README.md +++ b/gui/public/provider-icons/README.md @@ -14,6 +14,9 @@ Export-client marks (used by the API tab's connect rows, not the provider list): - `cline-color.svg` — reuses the existing provider mark already tracked in this directory for Cline CLI; no new image was imported for the file integration. +- `commandcode-color.svg` — reuses the Command Code provider mark already tracked in this + directory (the ⌘ mark in brand purple `#8C4EDD`, added in `b143984642`) for the Command + Code client integration; no new image was imported. - `pi.svg` — fetched 2026-08-02 from `https://pi.dev/favicon.svg`, the Pi project's own favicon, unmodified. Pi is `earendil-works/pi` diff --git a/gui/src/app-routing.ts b/gui/src/app-routing.ts index b286b95ee5d..be3c3c8efe0 100644 --- a/gui/src/app-routing.ts +++ b/gui/src/app-routing.ts @@ -137,6 +137,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 e4ac4a62e1a..8689f3b0a46 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", "kilo", "droid"] as const; +export const CLIENTS = ["opencode", "pi", "omp", "hermes", "openclaw", "kimi", "gajae", "dsh", "mcode", "zcode", "commandcode", "prime", "aside", "raycast", "omo", "cline", "kilo", "droid"] 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", @@ -81,6 +82,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", kilo: "/provider-icons/kilo.svg", droid: "/provider-icons/factory-droid.svg", }; diff --git a/gui/src/components/integration-marks.ts b/gui/src/components/integration-marks.ts index 669cc2ba114..a7db3023ba9 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, kilo: CLIENT_MARKS.kilo ?? null, droid: CLIENT_MARKS.droid ?? null, }; diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 79edb9efe70..b451fd11332 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -1500,6 +1500,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", @@ -1723,6 +1724,7 @@ export const de: Record = { "integrations.semantics.dsh": "OpenCodex verwaltet nur den Anbieter opencodex in der llm-pi-ai-Zeile von $DSH_HOME/profiles/desktop/cordis.patch.yml, dem Desktop-Profil, das DSH ab 0.1.7 liest ($DSH_HOME/settings.yaml, solange dieses Profil fehlt). 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.", @@ -2311,6 +2313,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 fa03097a41d..d272be88d0c 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -2066,6 +2066,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", @@ -2334,6 +2335,7 @@ export const en = { "integrations.semantics.dsh": "OpenCodex manages only the opencodex provider of the llm-pi-ai row in $DSH_HOME/profiles/desktop/cordis.patch.yml, the Desktop profile DSH 0.1.7+ reads ($DSH_HOME/settings.yaml until that profile exists). 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.", @@ -2933,6 +2935,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 9b4924eccb6..274bf6b17af 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -2038,6 +2038,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", @@ -2261,6 +2262,7 @@ export const fr: Record = { "integrations.semantics.dsh": "OpenCodex gère uniquement le fournisseur opencodex de la ligne llm-pi-ai dans $DSH_HOME/profiles/desktop/cordis.patch.yml, le profil Desktop que lit DSH 0.1.7+ ($DSH_HOME/settings.yaml tant que ce profil n’existe pas). 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.", @@ -2849,6 +2851,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 68c601aa680..c6eb3da49af 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -1933,6 +1933,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", @@ -2156,6 +2157,7 @@ export const ja: Record = { "integrations.semantics.dsh": "OpenCodex が管理するのは $DSH_HOME/profiles/desktop/cordis.patch.yml(DSH 0.1.7 以降が読む Desktop プロファイル。存在しない間は $DSH_HOME/settings.yaml)の llm-pi-ai 行にある 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 が必要です。", @@ -2750,6 +2752,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 d582632ceaa..3e5bc27f2bd 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -1544,6 +1544,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", @@ -1767,6 +1768,7 @@ export const ko: Record = { "integrations.semantics.dsh": "OpenCodex는 $DSH_HOME/profiles/desktop/cordis.patch.yml(DSH 0.1.7+가 읽는 Desktop profile, 이 profile이 없으면 $DSH_HOME/settings.yaml)의 llm-pi-ai 행에 있는 opencodex provider만 관리합니다. 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가 필요합니다.", @@ -2358,6 +2360,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/pt.ts b/gui/src/i18n/pt.ts index 484fb8bacf2..a41a90300b4 100644 --- a/gui/src/i18n/pt.ts +++ b/gui/src/i18n/pt.ts @@ -2055,6 +2055,7 @@ export const pt: Record = { "integrations.tab.raycast": "Raycast", "integrations.tab.omo": "omo", "integrations.tab.cline": "Cline CLI", + "integrations.tab.commandcode": "Command Code", "integrations.tab.kilo": "Kilo", "integrations.tab.droid": "Factory Droid", "integrations.aside.profilesTitle": "Perfis do Aside", @@ -2357,6 +2358,7 @@ export const pt: Record = { "integrations.lazycodexRoles.auto.moveUp": "Suba de nível se: {text}", "integrations.lazycodexRoles.auto.moveDown": "Desça de nível se: {text}", "integrations.semantics.cline": "Gerencia o OpenCodex no providers.json e no models.json do Cline CLI. Pare o Cline antes de alterar ou sincronizar esses arquivos e reinicie depois. Desfazer restaura os dois originais. Seu provedor padrão permanece inalterado; selecione o OpenCodex no Cline.", + "integrations.semantics.commandcode": "Gerencia apenas provider.opencodex em ~/.commandcode/providers.json. Seus outros provedores e configurações permanecem inalterados.", "integrations.semantics.kilo": "Gerencia apenas provider.opencodex na configuração global do Kilo — o primeiro arquivo existente entre kilo.jsonc, kilo.json, opencode.jsonc, opencode.json ou config.json em ~/.config/kilo (XDG_CONFIG_HOME realoca esse diretório; kilo.jsonc é criado quando nenhum existe). As demais chaves permanecem inalteradas. Aplicar reescreve o arquivo inteiro, então comentários e vírgulas finais não são preservados. Selecione opencodex/ no Kilo.", "integrations.semantics.droid": "Adiciona modelos personalizados do OpenCodex ao settings.json do Factory Droid. Desativar remove apenas as linhas gerenciadas; desfazer restaura o arquivo salvo.", "integrations.raycast.proRequired": "Custom Providers é um recurso do Raycast Pro. O arquivo será gravado, mas o Raycast o ignora até que uma assinatura Pro esteja ativa.", @@ -2897,6 +2899,7 @@ export const pt: Record = { "api.clientConfig.clientRaycast": "Raycast", "api.clientConfig.clientOmo": "omo", "api.clientConfig.clientCline": "Cline CLI", + "api.clientConfig.clientCommandCode": "Command Code", "api.clientConfig.clientKilo": "Kilo", "api.clientConfig.clientDroid": "Factory Droid", "api.clientConfig.clineBundle": "Este é um pacote de dois arquivos, não um arquivo de configurações do Cline. Mescle settings em providers.json e catalog no models.json vizinho. Use o interruptor de integração para uma gravação com backup.", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 62f35b7ef0e..311b4f0865b 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -2024,6 +2024,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", @@ -2247,6 +2248,7 @@ export const ru: Record = { "integrations.semantics.dsh": "OpenCodex управляет только провайдером opencodex в строке llm-pi-ai файла $DSH_HOME/profiles/desktop/cordis.patch.yml — профиля Desktop, который читает DSH 0.1.7+ ($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.", @@ -2840,6 +2842,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 aa16851efed..1ee24d1cbe3 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -2051,6 +2051,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", @@ -2273,6 +2274,7 @@ export const tr: Record = { "integrations.semantics.dsh": "OpenCodex yalnızca $DSH_HOME/profiles/desktop/cordis.patch.yml içindeki llm-pi-ai satırının opencodex sağlayıcısını yönetir; bu, DSH 0.1.7+ sürümünün okuduğu Desktop profilidir (profil yokken $DSH_HOME/settings.yaml). 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.", @@ -2867,6 +2869,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 d5c0905edde..499f946728b 100644 --- a/gui/src/i18n/vi.ts +++ b/gui/src/i18n/vi.ts @@ -2021,6 +2021,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", @@ -2289,6 +2290,7 @@ export const vi: Record = { "integrations.semantics.dsh": "OpenCodex chỉ quản lý provider opencodex trong dòng llm-pi-ai của $DSH_HOME/profiles/desktop/cordis.patch.yml, profile Desktop mà DSH 0.1.7+ đọc ($DSH_HOME/settings.yaml khi profile đó chưa tồn tại). 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.", @@ -2867,6 +2869,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 4fb66eaa91d..070f08fcf26 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -2946,6 +2946,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", @@ -3169,6 +3170,7 @@ export const zhTW: Record = { "integrations.semantics.dsh": "OpenCodex 只管理 $DSH_HOME/profiles/desktop/cordis.patch.yml(DSH 0.1.7 起讀取的 Desktop profile;該 profile 不存在時為 $DSH_HOME/settings.yaml)中 llm-pi-ai 這一列的 opencodex provider。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。", @@ -3268,6 +3270,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 bb6a39dac49..8111262373e 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -1517,6 +1517,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", @@ -1740,6 +1741,7 @@ export const zh: Record = { "integrations.semantics.dsh": "OpenCodex 只管理 $DSH_HOME/profiles/desktop/cordis.patch.yml(DSH 0.1.7 起读取的 Desktop profile;该 profile 不存在时为 $DSH_HOME/settings.yaml)中 llm-pi-ai 这一行的 opencodex provider。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。", @@ -2331,6 +2333,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 ca86b0f4ba1..aa07718901a 100644 --- a/gui/src/pages/integrations/FileIntegrationPage.tsx +++ b/gui/src/pages/integrations/FileIntegrationPage.tsx @@ -80,6 +80,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", @@ -100,6 +101,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 3b9848263b6..0d78dc62233 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 aa0d1a6093f..8fbad751f89 100644 --- a/gui/src/pages/integrations/integration-tabs.ts +++ b/gui/src/pages/integrations/integration-tabs.ts @@ -48,6 +48,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" }, @@ -68,6 +69,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 e68db9ad38d..105a967a30e 100644 --- a/gui/src/pages/integrations/overview-clients.ts +++ b/gui/src/pages/integrations/overview-clients.ts @@ -151,6 +151,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 57d7a577ebb..5d144e200d5 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", "kilo", "droid"]); + expect(CLIENTS).toEqual(["opencode", "pi", "omp", "hermes", "openclaw", "kimi", "gajae", "dsh", "mcode", "zcode", "commandcode", "prime", "aside", "raycast", "omo", "cline", "kilo", "droid"]); 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/fr-localization.test.ts b/gui/tests/fr-localization.test.ts index b7cb63af7b4..8843dcf0fd5 100644 --- a/gui/tests/fr-localization.test.ts +++ b/gui/tests/fr-localization.test.ts @@ -152,6 +152,9 @@ const INTENTIONAL_ENGLISH = new Set([ "api.clientConfig.clientCline", "integrations.tab.kilo", "api.clientConfig.clientKilo", + // Command Code is a product name, identical in every locale. + "integrations.tab.commandcode", + "api.clientConfig.clientCommandCode", // Factory Droid is a product name, identical in every locale. "integrations.tab.droid", "api.clientConfig.clientDroid", diff --git a/gui/tests/integrations-api.test.ts b/gui/tests/integrations-api.test.ts index 118dff774ad..29d2c29bae1 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", "kilo", "droid", + "opencode", "pi", "omp", "hermes", "openclaw", "kimi", "gajae", "dsh", "mcode", "zcode", "commandcode", "prime", "aside", "raycast", "omo", "cline", "kilo", "droid", ]); }); diff --git a/gui/tests/integrations-overview-rows.test.ts b/gui/tests/integrations-overview-rows.test.ts index 995634dccb2..e31af504ee6 100644 --- a/gui/tests/integrations-overview-rows.test.ts +++ b/gui/tests/integrations-overview-rows.test.ts @@ -312,6 +312,7 @@ test("an unsettled file list renders unknown rows instead of dropping them", () const built = buildOverviewRows(sources({ clients: [], clientsSettled: false })); expect(built.rows).toHaveLength(FILE_INTEGRATION_CLIENTS.length + 5); 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, "kilo")).toMatchObject({ hash: "integrations/kilo", labelKey: "integrations.tab.kilo", state: "unknown" }); expect(rowById(built, "droid")).toMatchObject({ hash: "integrations/droid", labelKey: "integrations.tab.droid", state: "unknown" }); expect(rowById(built, "omp").state).toBe("unknown"); diff --git a/gui/tests/locale-parity.test.ts b/gui/tests/locale-parity.test.ts index 84868651397..e3421cfa7d8 100644 --- a/gui/tests/locale-parity.test.ts +++ b/gui/tests/locale-parity.test.ts @@ -169,6 +169,9 @@ const ZH_TW_KEEP_ENGLISH: ReadonlySet = new Set([ "integrations.tab.kilo", "api.clientConfig.clientKilo", "api.clientConfig.clientDroid", + // Command Code is a product name, identical in every locale. + "integrations.tab.commandcode", + "api.clientConfig.clientCommandCode", "integrations.codex.title", // Provider proper nouns kept in English "provider.name.commandCodeAuth", diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index 4e327359f78..7c6fac498b4 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -501,6 +501,7 @@ "cline-pass-reasoning-efforts.test.ts": "providers", "cline-provider.test.ts": "providers", "cline-writer.test.ts": "clients", + "command-code-client-contract.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/scripts/test-layout/seeds.json b/scripts/test-layout/seeds.json index f029f71d948..45792a9c4bc 100644 --- a/scripts/test-layout/seeds.json +++ b/scripts/test-layout/seeds.json @@ -9,7 +9,7 @@ "domains": { "providers": { "match": [ - "^(?:aside(?!-profile)|auto|azure|baseten|chutes|cline(?!-(?:client|writer))|command|commandcode|context(?!-compat|-history)|crusoe|cyber|deepinfra|deepseek|digitalocean|exa|featherless|forward|hyperbolic|kimi|meta|mimo|minimax|moonshot|muse|new|nous|novita|nscale|nvidia|opencode(?!-kilo)|openrouter|qwen38|sambanova|umans|vercel|zcode|zhipu)-" + "^(?:aside(?!-profile)|auto|azure|baseten|chutes|cline(?!-(?:client|writer))|command(?!-code-client)|commandcode|context(?!-compat|-history)|crusoe|cyber|deepinfra|deepseek|digitalocean|exa|featherless|forward|hyperbolic|kimi|meta|mimo|minimax|moonshot|muse|new|nous|novita|nscale|nvidia|opencode(?!-kilo)|openrouter|qwen38|sambanova|umans|vercel|zcode|zhipu)-" ], "children": { "cursor": [ @@ -114,7 +114,8 @@ "^aside-profile(?!s-routes)", "^cline-(?:client|writer)(?:-|[.])", "^opencode-kilo-", - "^(?:desktop|omp|pi|prime|remote|sync)-" + "^(?:desktop|omp|pi|prime|remote|sync)-", + "^command-code-client(?:-|[.])" ] }, "service": { diff --git a/src/cli/dispatch.ts b/src/cli/dispatch.ts index abe2952213e..f2574b7d41a 100644 --- a/src/cli/dispatch.ts +++ b/src/cli/dispatch.ts @@ -466,7 +466,7 @@ const commandRunners: Record = { }, config, port: live.port, - }, ["mcode", "pi", "raycast", "omo", "cline", "droid", "opencode", "kilo"])); + }, ["mcode", "pi", "raycast", "omo", "cline", "commandcode", "droid", "opencode", "kilo"])); } catch (error) { console.warn(`Client integrations were not refreshed: ${error instanceof Error ? error.message : String(error)}`); } @@ -962,6 +962,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 264ac457262..28437ef324f 100644 --- a/src/cli/help.ts +++ b/src/cli/help.ts @@ -92,7 +92,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 (17 clients) + ocx export --client Print a client config wired to the running proxy (18 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 @@ -106,6 +106,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/integration-plan-dto.ts b/src/cli/integration-plan-dto.ts index 0d83c94989b..d43196971aa 100644 --- a/src/cli/integration-plan-dto.ts +++ b/src/cli/integration-plan-dto.ts @@ -23,6 +23,7 @@ export const FILE_INTEGRATION_CLIENTS = [ "raycast", "omo", "cline", + "commandcode", "kilo", "droid", ] as const; diff --git a/src/cli/integrations.ts b/src/cli/integrations.ts index fba67ea1cf2..b373e93e4bc 100644 --- a/src/cli/integrations.ts +++ b/src/cli/integrations.ts @@ -387,3 +387,24 @@ export async function handleClaudeInterceptCommand(argv: string[], deps: Runtime printData(result, wantsJson, summaryLines(result)); }); } + +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 3cd0b0efbe8..afddd4f1733 100644 --- a/src/cli/registry.ts +++ b/src/cli/registry.ts @@ -473,8 +473,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, Kilo, Factory Droid) 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, Command Code, Prime Agent, Aside, Raycast, omo, Cline, Kilo, Factory Droid) 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.", @@ -604,6 +604,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 de305117fcf..77594695d4f 100644 --- a/src/clients/config-export.ts +++ b/src/clients/config-export.ts @@ -21,7 +21,7 @@ */ import { homedir } from "node:os"; import { existsSync, lstatSync, readFileSync, realpathSync, statSync } from "node:fs"; -import { basename, dirname, isAbsolute, join, resolve } from "node:path"; +import { basename, dirname, isAbsolute, join, resolve, win32 } from "node:path"; import { shouldInjectApiAuthHeader, standaloneCodexRoutingTarget } from "../codex/inject"; import { FORMAT_MEDIA_TYPE, serializeDocument, type ConfigFormat } from "../integrations/serialize"; import { canonicalizeReasoningEfforts } from "../reasoning-effort"; @@ -69,6 +69,18 @@ import { buildKiloClientConfig, summarizeKilo, buildKiloContribution, kiloConfig export { kiloConfigPath, kiloHomeDir, kiloCandidatePath, KILO_CONFIG_CANDIDATES } from "./config-export/kilo"; export type { KiloGeneratedConfig, KiloProviderBlock, KiloModelEntry } from "./config-export/kilo"; import { droidConfigPath, buildDroidClientConfig, summarizeDroid, buildDroidContribution } from "./config-export/droid"; +import { + buildCommandCodeClientConfig, + summarizeCommandCode, + buildCommandCodeContribution, + commandCodeProviderRoot, + type CommandCodeGeneratedConfig, + type CommandCodeModelEntry, + type CommandCodeProviderBlock, +} from "./config-export/commandcode"; + +export type { CommandCodeGeneratedConfig, CommandCodeModelEntry, CommandCodeProviderBlock }; +export { buildCommandCodeClientConfig, summarizeCommandCode, buildCommandCodeContribution, commandCodeProviderRoot }; @@ -474,6 +486,43 @@ export function zcodeConfigPath(env: OpencodeLaunchEnv = process.env, home: stri return join(zcodeHomeDir(env, home), "v2", "config.json"); } +/** + * The home directory Command Code itself resolves: `env.HOME ?? env.USERPROFILE` + * (`homeDir15` in the published `command-code@1.66.0` bundle). + * + * On POSIX `os.homedir()` already reads `$HOME`, so the two agree and `home` is + * returned as is. On Windows `os.homedir()` reads `%USERPROFILE%` and ignores + * `HOME`, which Git for Windows, MSYS2 and Cygwin set, so a user whose `HOME` differs + * would get a provider block in a store Command Code never opens. Only the OS-default + * home is replaced: a caller that injects a different root (a test, the writer's + * `input.home`) keeps it, and a blank or relative value falls back to `home`. + */ +export function commandCodeUserHome(env: OpencodeLaunchEnv = process.env, home: string = homedir(), platform: NodeJS.Platform = process.platform): string { + if (platform !== "win32" || home !== homedir()) return home; + const value = env.HOME ?? env.USERPROFILE; + return typeof value === "string" && value.trim() && win32.isAbsolute(value) ? value : home; +} + +/** + * Command Code's provider store directory — `.commandcode` under the home the client + * resolves (see {@link commandCodeUserHome}), with no override. + * + * There is deliberately no `COMMANDCODE_HOME` here, unlike the other clients in this + * file: `grep -c COMMANDCODE_HOME` over the shipped `dist/cli.mjs` and + * `dist/index.mjs` returns 0. An override we honour but the client ignores is worse + * than none: `enable` would report success and write a provider block at a path no + * Command Code process ever opens, and the user would see an empty model list with no + * error anywhere. An apply against a relocated home is instead refused by `detectDir` + * not existing. + */ +export function commandCodeHomeDir(env: OpencodeLaunchEnv = process.env, home: string = homedir(), platform: NodeJS.Platform = process.platform): string { + return join(commandCodeUserHome(env, home, platform), ".commandcode"); +} + +export function commandCodeConfigPath(env: OpencodeLaunchEnv = process.env, home: string = homedir(), platform: NodeJS.Platform = process.platform): string { + return join(commandCodeHomeDir(env, home, platform), "providers.json"); +} + /** * The provider store a current ZCode reads, which is NOT the file above. * @@ -1487,6 +1536,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 + // writes `apiKey: false`, the documented keyless form for a loopback endpoint. + apiKeyEnv: "", + exportHint: "The provider is written with `apiKey: false` — Command Code treats a keyless loopback endpoint as already authenticated. No key is stored or referenced.", + 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..e32c77046ae --- /dev/null +++ b/src/clients/config-export/commandcode.ts @@ -0,0 +1,131 @@ +// 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 | boolean; + models: Record; +} + +export interface CommandCodeGeneratedConfig { + provider: Record; + /** The root a pre-existing target already uses; see {@link commandCodeProviderRoot}. */ + providers?: 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: false, + models, + }, + }, + }; +} + +/** + * The root key Command Code actually reads: `provider`, else `providers`. + * + * Verified against the published client (`command-code@1.66.0`, `dist/cli.mjs`): + * `const o = e.provider ?? e.providers;` — singular wins, and the plural branch is + * only reached when the singular root is nullish (`null` or `undefined`). Adding a + * singular root to a document that already carries a plural one therefore does not + * merge the two: the old entries stay on disk, byte for byte, and become invisible + * to the consumer. + * + * Evaluates nullish precedence before container validation so that non-nullish + * invalid singular values (such as strings or arrays) remain selected and are refused + * by the container guard rather than falling through to write an unreachable plural block. + */ +export function commandCodeProviderRoot(document: unknown): "provider" | "providers" { + const doc = document as { provider?: unknown; providers?: unknown } | null | undefined; + if (doc?.provider !== undefined && doc?.provider !== null) return "provider"; + if (doc?.providers !== undefined && doc?.providers !== null) return "providers"; + // Nothing established yet: the singular root is the one this exporter writes. + return "provider"; +} + +export function summarizeCommandCode(document: unknown): { modelCount: number; modelsWithoutLimits: number } { + const doc = document as CommandCodeGeneratedConfig | undefined; + const root = commandCodeProviderRoot(document); + const models = Object.values(doc?.[root]?.[OPENCODE_PROVIDER_ID]?.models ?? {}) as CommandCodeModelEntry[]; + return { modelCount: models.length, modelsWithoutLimits: models.filter(model => model.contextWindow === undefined).length }; +} + +/** + * The provider block, written under whichever root the target document already uses. + * + * `ExportContext` carries the parsed target document so the root can be chosen from + * what is on disk rather than assumed. A fresh file gets the singular root this + * exporter has always written; a document that already carries a plural root keeps + * it, so enabling OpenCodex never makes the user's other providers unreadable. + */ +export function buildCommandCodeContribution(ctx: ExportContext): ManagedContribution { + const doc = buildCommandCodeClientConfig(ctx); + const root = commandCodeProviderRoot(ctx.document); + return singleFragment("commandcode", [root, 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 e46c937d67a..082d6be1451 100644 --- a/src/clients/config-export/contracts.ts +++ b/src/clients/config-export/contracts.ts @@ -185,6 +185,17 @@ export interface ExportContext { */ config?: OcxConfig; droidReasoningDefaults?: DroidReasoningDefaults; + /** + * The parsed target document, when the caller has one in hand. + * + * A client whose reader picks between two roots needs the bytes on disk to + * choose: Command Code resolves `document.provider ?? document.providers`, so + * writing the singular root into a document that already carries the plural one + * leaves the user's providers unreadable rather than merging with them. Absent + * for `ocx export`, which has no target file — the builder then writes its + * default root, exactly as before. + */ + document?: unknown; } /** Namespaced model selector to one of that model's declared reasoning efforts. */ @@ -206,6 +217,7 @@ export type ExportClientId = | "raycast" | "omo" | "cline" + | "commandcode" | "kilo" | "droid"; diff --git a/src/integrations/catalog-refresh.ts b/src/integrations/catalog-refresh.ts index 1f1294ffa98..b299ad08216 100644 --- a/src/integrations/catalog-refresh.ts +++ b/src/integrations/catalog-refresh.ts @@ -11,7 +11,7 @@ import { /** Refresh only previously connected clients; a refused file never blocks its peers. */ export async function refreshOwnedCatalogIntegrations( input: Omit, - clientIds: readonly IntegrationClientId[] = ["pi", "aside", "raycast", "omo", "droid", "opencode", "kilo"], + clientIds: readonly IntegrationClientId[] = ["pi", "aside", "raycast", "omo", "commandcode", "droid", "opencode", "kilo"], ): Promise { // Client files are shared with the live proxy a sibling instance runs beside; their entries // point at the owner's port, and refreshing them here would re-point them at this one. diff --git a/src/integrations/mutation-plan.ts b/src/integrations/mutation-plan.ts index 16739f0c34f..9270210864d 100644 --- a/src/integrations/mutation-plan.ts +++ b/src/integrations/mutation-plan.ts @@ -149,6 +149,13 @@ const CLIENT_MANAGED_PATHS = { [...ZCODE_STORE_PROVIDER_RULES_PATH, `[providerId=${OPENCODE_PROVIDER_ID}]`], [...ZCODE_STORE_MODEL_RULES_PATH, DYNAMIC_SEGMENT], ], + /* + * Both roots, because the client picks between them rather than reading both: + * `document.provider ?? document.providers` in `command-code@1.66.0`. The + * builder chooses the one the target already uses, and a block we wrote under + * either root has to be removable from either root afterwards. + */ + commandcode: [["provider", OPENCODE_PROVIDER_ID], ["providers", 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 e02a8372b7f..c5c5ba0bc6a 100644 --- a/src/integrations/registry.ts +++ b/src/integrations/registry.ts @@ -58,6 +58,8 @@ import { DSH_PROFILE_PROVIDER_PATH, type BuildContribution, type ConfigFormat, + commandCodeConfigPath, + commandCodeHomeDir, kiloConfigPath, kiloHomeDir, kiloCandidatePath, @@ -369,6 +371,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/src/integrations/state.ts b/src/integrations/state.ts index 215617476ee..d3518177a8d 100644 --- a/src/integrations/state.ts +++ b/src/integrations/state.ts @@ -490,7 +490,8 @@ export function exportContextOf(input: { config: OcxConfig; port: number; droidReasoningDefaults?: Record; -}): { baseUrl: string; models: readonly ExportModel[]; config: OcxConfig; droidReasoningDefaults?: Record } { + document?: unknown; +}): { baseUrl: string; models: readonly ExportModel[]; config: OcxConfig; droidReasoningDefaults?: Record; document?: unknown } { return { /* * Composed through the SAME helper `ocx export` uses. Interpolating the @@ -505,6 +506,9 @@ export function exportContextOf(input: { models: input.models, config: input.config, ...(input.droidReasoningDefaults === undefined ? {} : { droidReasoningDefaults: input.droidReasoningDefaults }), + // Forwarded only when the caller parsed a target: a client that resolves + // between two document roots (Command Code) needs the bytes on disk. + ...(input.document === undefined ? {} : { document: input.document }), }; } @@ -514,7 +518,10 @@ export function buildIntegrationContribution( parsed: unknown, record: OwnershipRecord | null, ): ManagedContribution { - const context = exportContextOf(input); + // The parsed target travels with the context: a client whose reader picks + // between two document roots (Command Code) must see the bytes on disk, and + // routing it through this one builder keeps classify, plan and write agreeing. + const context = exportContextOf({ ...input, document: parsed }); if (input.clientId === "droid" && input.droidReasoningDefaults === undefined && record?.configPath === effective.configPath) { const inherited = recordedDroidContributionMatches(parsed, record) ? droidDefaultsFromOwnedRows(context, parsed, record.fragmentPaths) diff --git a/src/integrations/writer.ts b/src/integrations/writer.ts index b9d5378ee56..73949af0926 100644 --- a/src/integrations/writer.ts +++ b/src/integrations/writer.ts @@ -18,7 +18,7 @@ import { shouldInjectApiAuthHeader } from "../codex/inject"; import { detachedConfigSnapshot } from "../config/admitted-identity"; import { copyPlainData } from "../lib/plain-data"; import type { OcxConfig } from "../types"; -import { defaultIntegrationIO, loadTarget, parseConfig, type IntegrationIO } from "./config-io"; +import { PARSE_FAILED, defaultIntegrationIO, loadTarget, parseConfig, type IntegrationIO } from "./config-io"; import { inspectKiloCandidates } from "./kilo-candidates"; import { fingerprint, @@ -727,9 +727,15 @@ export function restoreIntegration(input: IntegrationRestoreInput): WriteOutcome // exact bytes when the snapshot was taken. Re-deriving it from the file would // mean guessing which entries are ours, and a wrong guess deletes a user's. const restoredRecord = entry.priorRecord; + let restoredDocument = clientId === "droid" && restoredText !== null ? parseConfig(restoredText, "json") : undefined; + if (clientId === "commandcode" && restoredText !== null) { + const parsed = parseConfig(restoredText, "json"); + // Restoring raw snapshot bytes must remain possible even when they are not parseable. + restoredDocument = parsed === PARSE_FAILED ? undefined : parsed; + } const fresh = buildIntegrationContribution( { ...input, droidReasoningDefaults: undefined }, rowTarget, - clientId === "droid" && restoredText !== null ? parseConfig(restoredText, "json") : undefined, + restoredDocument, restoredRecord, ); /* diff --git a/src/server/management/config-routes.ts b/src/server/management/config-routes.ts index 9f4dedb8a00..eb0735a0d36 100644 --- a/src/server/management/config-routes.ts +++ b/src/server/management/config-routes.ts @@ -276,7 +276,7 @@ export async function syncEnabledClientIntegrations( }, config, port, - }, ["mcode", "pi", "aside", "raycast", "omo", "cline", "droid", "opencode", "kilo"])); + }, ["mcode", "pi", "aside", "raycast", "omo", "cline", "commandcode", "droid", "opencode", "kilo"])); return out; } diff --git a/structure/clients/integrations.md b/structure/clients/integrations.md index a74250e935e..a0c47e541c2 100644 --- a/structure/clients/integrations.md +++ b/structure/clients/integrations.md @@ -51,8 +51,7 @@ would need an admission header. When a previously managed catalog becomes empty or wholly unaddressable, classification still checks recorded fragment paths and their fingerprints so disable can remove owned rows without deleting foreign edits. The legacy settings guard also checks the recorded model IDs and endpoints when -those rows leave the current catalog. -Apply and refresh still refuse an empty managed contribution. +those rows leave the current catalog. Apply and refresh still refuse an empty managed contribution. The builder omits `apiKey` and unsupported metadata. The shared writer snapshots prior bytes and refuses changed managed rows or unsafe paths. `src/integrations/droid-settings.ts` refuses legacy `config.json` rows that share the exported endpoint, a generated model ID, or an @@ -91,8 +90,7 @@ ordinary removal preview. Disable and restore remove or restore the rows and the defaults as one owned value. The request preference is interpreted after initial Chat route selection and before -concrete dispatch under the -[inbound effort contract](../data-planes/inbound-compat.md#droid-request-defaults). +concrete dispatch under the [inbound effort contract](../data-planes/inbound-compat.md#droid-request-defaults). It never authenticates a client or changes admission policy. ## Cursor installed capability reads @@ -108,8 +106,7 @@ size; the returned table always uses the current install version, including on a is found, `src/integrations/cursor-local-installer.ts` resolves the Private Inference installer that Cursor's `cursor-local` update channel advertises for the host platform and architecture (only `x64` and `arm64` on Windows, macOS and Linux map; any other host resolves to `unsupported-platform` -with no request) -(`/updates/api/update//cursor-local/0.0.0/manual-check/stable`, 4 s timeout). +with no request) (`/updates/api/update//cursor-local/0.0.0/manual-check/stable`, 4 s timeout). The decoded manifest is capped at 64 KiB before JSON parsing. Only a bounded `https://downloads.cursor.com/local-mode/` URL with a bounded version is accepted (a Linux `.AppImage.zsync` delta-metadata URL is mapped to its sibling `.AppImage`); anything else @@ -170,8 +167,7 @@ the resulting configuration for projection and retention. A superseded gather ap to its detached projection and retains no snapshot; a failed discovery commit refuses the export. The identity a caller carries between a preview and the mutation that confirms it is process-local and opaque, and describes nothing about the -configuration. The -Integrations collection read populates one when discovery succeeds and the configuration can be +configuration. The Integrations collection read populates one when discovery succeeds and the configuration can be identified, so the page an operator opens before confirming anything is the usual way back rather than a guarantee. @@ -179,8 +175,7 @@ Each operation is decided the way that operation decides it. Apply checks instal admission before the classifier and reports a conflict ahead of unsafe; disable asks neither, because removing what we wrote from a file that still exists is meaningful regardless of installation; restore reads the journal row and the target's bytes and never parses, so a file -that is readable but unparseable is still restorable. An operation that would write nothing says -so and names no places. +that is readable but unparseable is still restorable. An operation that would write nothing says so and names no places. Published paths are declared, not inferred. Every client states where its managed fragments live, a path is emitted only on an exact template match, and the string emitted is the template rather @@ -219,8 +214,8 @@ their existing visibility rules. ## Owned catalog convergence -Visibility, selected-model and preset writes refresh already-owned catalog contributions (including OpenCode and Kilo) after -persisting the selection. Explicit sync also refreshes owned OpenCode and Kilo blocks. The shared catalog-refresh +Visibility, selected-model and preset writes refresh already-owned catalog contributions (including OpenCode, Kilo and +Command Code) after persisting the selection. Explicit sync also refreshes owned OpenCode, Kilo and Command Code blocks. The shared catalog-refresh fan-out loads the filtered roster lazily once, leaves unowned clients alone, and reports each refusal independently. Existing coordinated writers retain all no-clobber and ownership checks. Implicit refresh operations use distinct flight keys: overlapping desired catalogs return busy @@ -343,8 +338,7 @@ runtime-derived fields back into an owned fragment may additionally record: - `protectedBlockFingerprint`: the contribution fingerprint after only those paths are removed. The paths are stored with the operation instead of recomputed from the latest catalog. That keeps a -later catalog expansion from silently widening what an older ownership record allows. Malformed or -incomplete policy records fail closed. +later catalog expansion from silently widening what an older ownership record allows. Malformed or incomplete policy records fail closed. ## ZCode Runtime Metadata @@ -383,9 +377,7 @@ observed. Naming the store without the last three would be naming a file we cann file this operation reads, writes, journals and records, and whether a write there reaches the client. It decides from three facts, in order: -1. No declared store, or no store on disk — the config file, unchanged, unless the declaration's `missingStore` says the client still reads the absent store; that is an ineffective write carrying its remedy; status rows and refused previews also publish `supersededReason` and the store's `missingStoreDocument`, so the dashboard names what to create in its own language. A client that has never run - still imports what we write there, which is why the rule keys on the store's presence rather - than on a client version. +1. No declared store, or no store on disk — the config file, unchanged, unless the declaration's `missingStore` says the client still reads the absent store; that is an ineffective write carrying its remedy; status rows and refused previews also publish `supersededReason` and the store's `missingStoreDocument`, so the dashboard names what to create in its own language. A client that has never run still imports what we write there, which is why the rule keys on the store's presence rather than on a client version. 2. This project's own block already in one of the two files — that file. Disable removes what we wrote from where we wrote it, and no apply leaves a block in one file while writing another. 3. Otherwise the store, and only when its schema establishes. @@ -402,14 +394,11 @@ Four properties are load-bearing: before the client created its store cannot be committed afterwards — and neither can one taken before the store's schema version moved under an unchanged path. - Disable is never gated on it. Removing bytes this project wrote from the file it wrote them to is - unaffected by where the client reads, and refusing it would leave the block unremovable through - the tool. + unaffected by where the client reads, and refusing it would leave the block unremovable through the tool. Writing the store does not relax ownership anywhere. The store keys a model rule by the pair `(providerId, modelId)`, so the managed path names both: a selector naming only the model would -match another provider's rule for the same model and replace it. A rule carrying this project's -provider id that no record accounts for — including one the client's own migration created — is a -conflict, and the explicit overwrite remains the only way past it. +match another provider's rule for the same model and replace it. A rule carrying this project's provider id that no record accounts for — including one the client's own migration created — is a conflict, and the explicit overwrite remains the only way past it. Persisted selector segments have two disjoint grammars owned by `src/integrations/merge.ts`. An unversioned `[field=value]` segment is permanently a one-criterion selector; commas and later @@ -530,8 +519,7 @@ An explicit `null` in any of those three places counts as not an object. A file cannot be read lists as `unreadable`, so the role table still loads, and a save reports `write_failed` for the mirror. Without LazyCodex, `GET /api/codex-agent-roles` answers `lazycodex.detected: false` with no roles -and without opening omo.jsonc, and `PUT` answers 409 `lazycodex_not_detected` before touching a -role file. +and without opening omo.jsonc, and `PUT` answers 409 `lazycodex_not_detected` before touching a role file. ## Kilo global JSONC @@ -571,6 +559,15 @@ holds the single ownership record. Committing the older row's prior record would ownership back at the old file and leave the active block on disk with nothing to disable it. +## Command Code + +Command Code owns one block in `~/.commandcode/providers.json`. Shipped `command-code@1.66.0` +resolves `env.HOME ?? env.USERPROFILE` plus that path with no relocation variable, so +`commandCodeHomeDir` takes no override. On Windows, `commandCodeUserHome` resolves the default home as `HOME ?? USERPROFILE` (non-blank and Windows-absolute) to match the client, preserving an injected home (tests or writer `input.home`). Reader `e.provider ?? e.providers` prefers singular: +`commandCodeProviderRoot` mirrors that against the target and writes under the existing root +(singular for fresh files); both roots are in `CLIENT_MANAGED_PATHS` so Disable removes either. +Uses keyless `apiKey: false`; no service-token is read. Contract: `tests/clients/command-code-client-contract.test.ts`. + ## Cline paired files Cline CLI uses `providers.json` for connection settings and sibling `models.json` for its diff --git a/tests/cli/cli-dispatch.test.ts b/tests/cli/cli-dispatch.test.ts index f8fbb308166..63328072f59 100644 --- a/tests/cli/cli-dispatch.test.ts +++ b/tests/cli/cli-dispatch.test.ts @@ -182,7 +182,7 @@ describe("dispatchCommand exit codes", () => { expect(code).toBe(0); expect(order).toEqual(["catalog", "refresh"]); expect(refresh).toHaveBeenCalledTimes(1); - expect(refresh.mock.calls[0]![1]).toEqual(["mcode", "pi", "raycast", "omo", "cline", "droid", "opencode", "kilo"]); + expect(refresh.mock.calls[0]![1]).toEqual(["mcode", "pi", "raycast", "omo", "cline", "commandcode", "droid", "opencode", "kilo"]); } finally { sync.mockRestore(); refresh.mockRestore(); aside.mockRestore(); log.mockRestore(); } diff --git a/tests/clients/command-code-client-contract.test.ts b/tests/clients/command-code-client-contract.test.ts new file mode 100644 index 00000000000..4f0ba6ce1b5 --- /dev/null +++ b/tests/clients/command-code-client-contract.test.ts @@ -0,0 +1,392 @@ +/** + * Client-contract regression for the Command Code integration. + * + * The cases below were found by running the PUBLISHED consumer + * (`command-code@1.66.0`) against our exported provider, not by reading our own + * output. Each test carries the client-side expression it is proving, taken from + * `dist/cli.mjs` of that release: + * + * - root selection: `const o = e.provider ?? e.providers;` + * - home resolution: `homeDir15(e) => e.env().HOME ?? e.env().USERPROFILE`, + * then `${home}/.commandcode/providers.json` + * - credential form: a raw string is refused; a `$ENV` / `{env:VAR}` / `!command` + * reference, or `false` for a keyless endpoint, is accepted + * + * The parser is reproduced here as `publishedCommandCodeRoot`, not imported, so + * the test states the contract explicitly and fails loudly if we ever contradict + * it. `COMMANDCODE_HOME` does not appear anywhere in the shipped bundle + * (`grep -c COMMANDCODE_HOME dist/cli.mjs` → 0), which is why the path resolver + * takes no override. + */ +import { beforeEach, describe, expect, test } from "bun:test"; +import { homedir } from "node:os"; +import { dirname, join } from "node:path"; +import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { + buildClientContribution, + buildCommandCodeClientConfig, + commandCodeConfigPath, + commandCodeHomeDir, + commandCodeProviderRoot, + type ExportContext, +} from "../../src/clients/config-export"; +import { setPath } from "../../src/integrations/merge"; +import { refreshOwnedCatalogIntegrations } from "../../src/integrations/catalog-refresh"; +import { OPENCODE_PROVIDER_ID } from "../../src/clients/config-export/constants"; +import { MANAGED_PATH_TEMPLATES } from "../../src/integrations/mutation-plan"; +import { INTEGRATION_CLIENTS } from "../../src/integrations/registry"; +import { createIntegrationStateStore, type IntegrationStateStore } from "../../src/integrations/store"; +import { readIntegrationState } from "../../src/integrations/state"; +import { + applyIntegration, + disableIntegration, + restoreIntegration, + type IntegrationWriteInput, +} from "../../src/integrations/writer"; + +/** `const o = e.provider ?? e.providers;` — verbatim from command-code@1.66.0. */ +function publishedCommandCodeRoot(document: unknown): Record | undefined { + const doc = document as { provider?: unknown; providers?: unknown } | null; + const o = doc?.provider ?? doc?.providers; + return typeof o === "object" && o !== null && !Array.isArray(o) + ? (o as Record) + : undefined; +} + +/** `isApiKeyReference` — a raw secret is not one. */ +function publishedAcceptsApiKey(value: unknown): boolean { + if (value === false) return true; + if (typeof value !== "string") return false; + return /^\$[A-Za-z_][A-Za-z0-9_]*$/.test(value) + || /^\{env:[A-Za-z_][A-Za-z0-9_]*\}$/.test(value) + || value.startsWith("!"); +} + +const CONTEXT: ExportContext = { + baseUrl: "http://127.0.0.1:10100/v1", + models: [ + { namespaced: "anthropic/claude-opus-5", provider: "anthropic", id: "claude-opus-5", contextWindow: 200_000 }, + { namespaced: "openai/gpt-5.6-sol", provider: "openai", id: "gpt-5.6-sol", contextWindow: 922_000 }, + ], +}; + +describe("Command Code client contract (published command-code@1.66.0)", () => { + test("a plural-root target keeps its root, and the client still reads every provider", () => { + // BEFORE: the user's own plural-root document. + const before = { + providers: { + acme: { name: "Acme", api: "openai-completions", baseURL: "https://api.acme.test/v1", apiKey: "secret", models: { "acme/large": {} } }, + }, + }; + + const contribution = buildClientContribution("commandcode", { ...CONTEXT, document: before }); + const after = contribution.fragments.reduce( + (doc, fragment) => setPath(doc, fragment.path, fragment.value), + before, + ); + + // The fragment landed under the root the target already used. + expect(contribution.fragments).toHaveLength(1); + expect(contribution.fragments[0]!.path).toEqual(["providers", OPENCODE_PROVIDER_ID]); + + // THE REGRESSION THIS GUARDS: the published client resolves + // `document.provider ?? document.providers`. If we had written a singular root + // into this document, `provider` would win and the user's `acme` provider would + // still be on disk but invisible to the consumer. + const resolved = publishedCommandCodeRoot(after); + expect(resolved).toBeDefined(); + expect(Object.keys(resolved!).sort()).toEqual([OPENCODE_PROVIDER_ID, "acme"].sort()); + expect(resolved!.acme).toEqual(before.providers.acme); + expect((resolved as Record }>)[OPENCODE_PROVIDER_ID]!.models["openai/gpt-5.6-sol"]).toBeDefined(); + }); + + test("a singular-root target keeps the singular root", () => { + const before = { provider: { legacy: { name: "Legacy", api: "openai-completions", baseURL: "https://legacy.test/v1", apiKey: "secret", models: {} } } }; + const contribution = buildClientContribution("commandcode", { ...CONTEXT, document: before }); + expect(contribution.fragments[0]!.path).toEqual(["provider", OPENCODE_PROVIDER_ID]); + + const after = contribution.fragments.reduce((doc, f) => setPath(doc, f.path, f.value), before); + const resolved = publishedCommandCodeRoot(after); + expect(Object.keys(resolved!).sort()).toEqual([OPENCODE_PROVIDER_ID, "legacy"].sort()); + }); + + test("a fresh target writes the singular root and the client resolves it", () => { + const doc = buildCommandCodeClientConfig(CONTEXT); + const resolved = publishedCommandCodeRoot(doc); + expect(resolved).toBeDefined(); + expect(Object.keys(resolved!)).toEqual([OPENCODE_PROVIDER_ID]); + }); + + test("both roots are declared as managed paths so disable can remove either", () => { + // `mutation-plan` refuses to publish an undeclared path; if only the singular + // root were declared, a block written under the plural root could never be + // removed again. + expect(MANAGED_PATH_TEMPLATES.commandcode).toEqual([ + ["provider", OPENCODE_PROVIDER_ID], + ["providers", OPENCODE_PROVIDER_ID], + ]); + }); + + test("the exported provider is accepted by the published credential check", () => { + const provider = buildCommandCodeClientConfig(CONTEXT).provider[OPENCODE_PROVIDER_ID]!; + // The literal "opencodex-loopback" this exporter used to write is NOT accepted. + expect(publishedAcceptsApiKey("opencodex-loopback")).toBe(false); + expect(publishedAcceptsApiKey(provider.apiKey)).toBe(true); + }); + + test("the config path ignores COMMANDCODE_HOME, which the client does not read", () => { + const relocated = "/tmp/commandcode-relocated"; + // The client resolves `HOME ?? USERPROFILE` + `/.commandcode/providers.json` + // and never consults COMMANDCODE_HOME, so honouring it would make `enable` + // report success at a path Command Code never opens. + expect(commandCodeHomeDir({ COMMANDCODE_HOME: relocated }, "/home/user")).toBe(join("/home/user", ".commandcode")); + expect(commandCodeConfigPath({ COMMANDCODE_HOME: relocated }, "/home/user")).toBe(join("/home/user", ".commandcode", "providers.json")); + // The real home still resolves the ordinary way. + expect(commandCodeConfigPath({}, homedir())).toBe(join(homedir(), ".commandcode", "providers.json")); + }); + + test("a before/after write through the real path leaves the file parseable by the client", () => { + const dir = mkdtempSync(join(tmpdir(), "cc-contract-")); + const path = join(dir, "providers.json"); + const before = { providers: { acme: { name: "Acme", api: "openai-completions", baseURL: "https://api.acme.test/v1", apiKey: "secret", models: { "acme/large": {} } } } }; + writeFileSync(path, JSON.stringify(before, null, 2), "utf8"); + + const onDisk = JSON.parse(readFileSync(path, "utf8")) as unknown; + const contribution = buildClientContribution("commandcode", { ...CONTEXT, document: onDisk }); + const after = contribution.fragments.reduce((doc, f) => setPath(doc, f.path, f.value), onDisk); + writeFileSync(path, JSON.stringify(after, null, 2), "utf8"); + + // Re-read exactly as the client does, and confirm nothing the user configured + // was lost or shadowed. + const reloaded = publishedCommandCodeRoot(JSON.parse(readFileSync(path, "utf8"))); + expect(reloaded).toBeDefined(); + expect(reloaded!.acme).toEqual(before.providers.acme); + expect(reloaded![OPENCODE_PROVIDER_ID]).toBeDefined(); + }); +}); + +describe("commandCodeProviderRoot nullish precedence (published command-code@1.66.0)", () => { + const cases: Array<[unknown, "provider" | "providers", string]> = [ + [{}, "provider", "empty document defaults to singular creation root"], + [{ provider: {} }, "provider", "valid singular root"], + [{ providers: {} }, "providers", "valid plural root"], + [{ provider: null, providers: {} }, "providers", "null singular falls through to plural"], + [{ provider: undefined, providers: {} }, "providers", "undefined singular falls through to plural"], + [{ provider: {}, providers: {} }, "provider", "both roots present prefers singular"], + [{ provider: "invalid", providers: {} }, "provider", "non-nullish string singular does not fall through"], + [{ provider: [], providers: {} }, "provider", "non-nullish array singular does not fall through"], + [{ provider: "", providers: {} }, "provider", "empty string singular does not fall through"], + [{ provider: false, providers: {} }, "provider", "boolean false singular does not fall through"], + [{ provider: 0, providers: {} }, "provider", "numeric 0 singular does not fall through"], + [{ providers: "invalid" }, "providers", "plural string root remains selected for validation"], + [{ providers: [] }, "providers", "plural array root remains selected for validation"], + [{ provider: null, providers: "invalid" }, "providers", "null singular with invalid plural selects plural for validation"], + [{ provider: null }, "provider", "null singular without plural defaults to singular"], + [{ provider: null, providers: null }, "provider", "both null defaults to singular"], + ]; + + for (const [doc, expected, label] of cases) { + test(label, () => { + expect(commandCodeProviderRoot(doc)).toBe(expected); + }); + } +}); + +describe("Command Code real integration writer root-precedence and lifecycle", () => { + let home: string; + let store: IntegrationStateStore; + + function input(overrides: Partial = {}): IntegrationWriteInput { + return { + clientId: "commandcode", + models: [ + { namespaced: "openai/gpt-5.6-sol", provider: "openai", id: "gpt-5.6-sol", contextWindow: 922_000 }, + ], + config: { port: 10100, hostname: "127.0.0.1" } as unknown as IntegrationWriteInput["config"], + port: 10100, + env: {}, + home, + store, + ...overrides, + }; + } + + function setupConfig(initialContent: string): string { + const spec = INTEGRATION_CLIENTS.commandcode; + const configPath = spec.configPath({}, home); + mkdirSync(dirname(configPath), { recursive: true }); + writeFileSync(configPath, initialContent, "utf8"); + return configPath; + } + + beforeEach(() => { + const base = mkdtempSync(join(tmpdir(), "cc-real-writer-")); + home = join(base, "home"); + const storeRoot = join(base, "store", "integrations"); + mkdirSync(home, { recursive: true }); + store = createIntegrationStateStore(storeRoot); + }); + + test("default catalog refresh replaces an enabled Command Code model roster", async () => { + const configPath = setupConfig('{"providers":{}}'); + const initial = input(); + expect(applyIntegration(initial).ok).toBe(true); + const before = JSON.parse(readFileSync(configPath, "utf8")); + expect(Object.keys(before.providers[OPENCODE_PROVIDER_ID].models)).toEqual(["openai/gpt-5.6-sol"]); + + const rosterB = [CONTEXT.models[0]!]; + const { config, port } = initial; + const outcomes = await refreshOwnedCatalogIntegrations({ models: rosterB, config, port, env: {}, home, store }); + + const after = JSON.parse(readFileSync(configPath, "utf8")); + expect(Object.keys(after.providers[OPENCODE_PROVIDER_ID].models)).toEqual(["anthropic/claude-opus-5"]); + expect(outcomes.find(outcome => outcome.client === "commandcode")) + .toEqual({ client: "commandcode", ok: true, changed: true }); + }); + + test("default catalog refresh leaves unowned Command Code bytes untouched without loading models", async () => { + const original = '{\n "providers": {"opencodex": {"models": {"personal/model": {}}}}\n}\n'; + const configPath = setupConfig(original); + let loads = 0; + const { config, port } = input(); + const outcomes = await refreshOwnedCatalogIntegrations({ + models: async () => { loads++; return CONTEXT.models; }, + config, port, env: {}, home, store, + }); + + expect(outcomes).toEqual([]); + expect(readFileSync(configPath, "utf8")).toBe(original); + expect(loads).toBe(0); + }); + + test("refuses when singular provider root is a string, leaves file untouched, appends no journal entry", async () => { + const rawContent = JSON.stringify({ provider: "invalid", providers: { acme: { models: {} } } }, null, 2); + const configPath = setupConfig(rawContent); + + const state = readIntegrationState(input()); + expect(state).toMatchObject({ state: "unsafe", reason: "blocked-container" }); + + const applyRes = await applyIntegration(input()); + expect(applyRes.ok).toBe(false); + expect(applyRes.state).toBe("unsafe"); + expect(applyRes.reason).toBe("unsafe"); + + // File must be byte-for-byte unchanged + expect(readFileSync(configPath, "utf8")).toBe(rawContent); + // Zero journal entries recorded + expect(store.listOperations("commandcode")).toHaveLength(0); + // State does not report current + expect(readIntegrationState(input()).state).not.toBe("current"); + }); + + test("refuses when singular provider root is an array, leaves file untouched, appends no journal entry", async () => { + const rawContent = JSON.stringify({ provider: [], providers: { acme: { models: {} } } }, null, 2); + const configPath = setupConfig(rawContent); + + const state = readIntegrationState(input()); + expect(state).toMatchObject({ state: "unsafe", reason: "blocked-container" }); + + const applyRes = await applyIntegration(input()); + expect(applyRes.ok).toBe(false); + expect(applyRes.state).toBe("unsafe"); + expect(applyRes.reason).toBe("unsafe"); + + expect(readFileSync(configPath, "utf8")).toBe(rawContent); + expect(store.listOperations("commandcode")).toHaveLength(0); + expect(readIntegrationState(input()).state).not.toBe("current"); + }); + + test("preserves provider: null fallback and writes under providers", async () => { + const rawContent = JSON.stringify({ + provider: null, + providers: { acme: { name: "Acme", api: "openai-completions", baseURL: "https://acme.test/v1", apiKey: false, models: {} } }, + }, null, 2); + const configPath = setupConfig(rawContent); + + const state = readIntegrationState(input()); + expect(state.state).toBe("absent"); + + const applyRes = await applyIntegration(input()); + expect(applyRes.ok).toBe(true); + expect(applyRes.state).toBe("current"); + + const onDisk = JSON.parse(readFileSync(configPath, "utf8")); + expect(onDisk.provider).toBeNull(); + expect(onDisk.providers[OPENCODE_PROVIDER_ID]).toBeDefined(); + expect(onDisk.providers.acme).toBeDefined(); + + const consumerResolved = publishedCommandCodeRoot(onDisk); + expect(consumerResolved).toBeDefined(); + expect(consumerResolved![OPENCODE_PROVIDER_ID]).toBeDefined(); + expect(consumerResolved!.acme).toBeDefined(); + }); + + test.each(["providers", "provider"] as const)("executes full lifecycle (Apply -> Disable -> Undo) on ordinary %s-root files", async root => { + const rawContent = JSON.stringify({ + [root]: { acme: { name: "Acme", api: "openai-completions", baseURL: "https://acme.test/v1", apiKey: false, models: {} } }, + }, null, 2); + const configPath = setupConfig(rawContent); + + // 1. Apply + const applyRes = await applyIntegration(input()); + expect(applyRes.ok).toBe(true); + expect(applyRes.state).toBe("current"); + + const appliedDoc = JSON.parse(readFileSync(configPath, "utf8")); + expect(appliedDoc[root][OPENCODE_PROVIDER_ID]).toBeDefined(); + expect(appliedDoc[root].acme).toBeDefined(); + expect(appliedDoc[root === "providers" ? "provider" : "providers"]).toBeUndefined(); + + // Consumer reads both + expect(publishedCommandCodeRoot(appliedDoc)![OPENCODE_PROVIDER_ID]).toBeDefined(); + expect(publishedCommandCodeRoot(appliedDoc)!.acme).toBeDefined(); + + // 2. Undo the apply directly (restores original document byte-for-byte) + const undoApplyRes = await restoreIntegration({ ...input(), opId: (applyRes as any).opId }); + expect(undoApplyRes.ok).toBe(true); + expect(readFileSync(configPath, "utf8")).toBe(rawContent); + + // Re-apply for disable test + const reapplyRes = await applyIntegration(input()); + expect(reapplyRes.ok).toBe(true); + + // 3. Disable + const disableRes = await disableIntegration(input()); + expect(disableRes.ok).toBe(true); + + const disabledDoc = JSON.parse(readFileSync(configPath, "utf8")); + expect(disabledDoc[root][OPENCODE_PROVIDER_ID]).toBeUndefined(); + expect(disabledDoc[root].acme).toBeDefined(); + expect(disabledDoc[root === "providers" ? "provider" : "providers"]).toBeUndefined(); + + // 4. Undo the disable (restores the active managed block under the original root) + const undoDisableRes = await restoreIntegration({ ...input(), opId: (disableRes as any).opId }); + expect(undoDisableRes.ok).toBe(true); + expect(readIntegrationState(input()).state).toBe("current"); + expect(undoDisableRes.state).toBe(readIntegrationState(input()).state); + + const restoredDoc = JSON.parse(readFileSync(configPath, "utf8")); + expect(restoredDoc[root][OPENCODE_PROVIDER_ID]).toBeDefined(); + expect(restoredDoc[root].acme).toBeDefined(); + expect(restoredDoc[root === "providers" ? "provider" : "providers"]).toBeUndefined(); + expect(publishedCommandCodeRoot(restoredDoc)![OPENCODE_PROVIDER_ID]).toBeDefined(); + }); + test("undoing a confirmed drift restore preserves an unparseable snapshot", () => { + const configPath = setupConfig(JSON.stringify({ providers: { acme: { models: {} } } })); + const applyRes = applyIntegration(input()); + expect(applyRes.ok).toBe(true); + if (!applyRes.ok) throw new Error("apply failed"); + + const editedText = "{ invalid JSON"; + writeFileSync(configPath, editedText, "utf8"); + const restoreRes = restoreIntegration({ ...input(), opId: applyRes.opId!, confirmDrift: true }); + expect(restoreRes.ok).toBe(true); + if (!restoreRes.ok) throw new Error("restore failed"); + + const undoRes = restoreIntegration({ ...input(), opId: restoreRes.opId! }); + expect(undoRes).toMatchObject({ ok: true, state: "conflict" }); + expect(readFileSync(configPath, "utf8")).toBe(editedText); + expect(readIntegrationState(input()).state).toBe("unsafe"); + }); +}); diff --git a/tests/clients/command-code-client.test.ts b/tests/clients/command-code-client.test.ts new file mode 100644 index 00000000000..45d2c3dc75e --- /dev/null +++ b/tests/clients/command-code-client.test.ts @@ -0,0 +1,302 @@ +import { describe, expect, test } from "bun:test"; +import { homedir } from "node:os"; +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).toBe(false); + }); + + 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 ~/.commandcode and the documented destination, with no override", () => { + expect(commandCodeHomeDir({}, "/home/u")).toBe(join("/home/u", ".commandcode")); + expect(commandCodeConfigPath({}, "/home/u")).toBe(join("/home/u", ".commandcode", "providers.json")); + // COMMANDCODE_HOME is deliberately ignored: the published client + // (`command-code@1.66.0`) resolves `HOME ?? USERPROFILE` + `/.commandcode` and + // never reads it, so honouring it here would report an enable as successful at + // a path no Command Code process opens. See commandCodeHomeDir's doc comment + // and tests/clients/command-code-client-contract.test.ts. + expect(commandCodeConfigPath({ COMMANDCODE_HOME: "/elsewhere" }, "/home/u")).toBe(join("/home/u", ".commandcode", "providers.json")); + expect(commandCodeConfigPath({ COMMANDCODE_HOME: "relative" }, "/home/u")).toBe(join("/home/u", ".commandcode", "providers.json")); + }); + + test("detects installation by ~/.commandcode, which is where the client looks", () => { + 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(join("/home/u", ".commandcode")); + }); + + 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); + }); +}); + +describe("Command Code user home resolution", () => { + const envHome = "C:\\commandcode-home"; + const userProfile = "D:\\commandcode-profile"; + + test("uses absolute HOME before USERPROFILE on Windows with the default home", () => { + const env = { HOME: envHome, USERPROFILE: userProfile }; + expect(commandCodeHomeDir(env, undefined, "win32")).toBe(join(envHome, ".commandcode")); + expect(commandCodeConfigPath(env, homedir(), "win32")).toBe(join(envHome, ".commandcode", "providers.json")); + }); + + test("uses USERPROFILE on Windows when HOME is unset", () => { + const env = { USERPROFILE: userProfile }; + expect(commandCodeHomeDir(env, homedir(), "win32")).toBe(join(userProfile, ".commandcode")); + expect(commandCodeConfigPath(env, undefined, "win32")).toBe(join(userProfile, ".commandcode", "providers.json")); + }); + + for (const HOME of ["relative", "", " \t "]) { + test(`falls back to the default home for invalid HOME ${JSON.stringify(HOME)}`, () => { + // A present but invalid HOME must not fall through to USERPROFILE. + expect(commandCodeConfigPath({ HOME, USERPROFILE: userProfile }, homedir(), "win32")) + .toBe(join(homedir(), ".commandcode", "providers.json")); + }); + } + + test("falls back to the default home when both environment homes are unset", () => { + expect(commandCodeHomeDir({}, homedir(), "win32")).toBe(join(homedir(), ".commandcode")); + }); + + test("preserves an injected home on Windows even with HOME set", () => { + const injectedHome = join(homedir(), "commandcode-sandbox"); + expect(commandCodeConfigPath({ HOME: envHome, USERPROFILE: userProfile }, injectedHome, "win32")) + .toBe(join(injectedHome, ".commandcode", "providers.json")); + }); + + test("preserves home on non-Windows platforms regardless of HOME", () => { + for (const platform of ["darwin", "linux"] as const) { + expect(commandCodeConfigPath({ HOME: envHome, USERPROFILE: userProfile }, homedir(), platform)) + .toBe(join(homedir(), ".commandcode", "providers.json")); + } + }); + + test("ignores COMMANDCODE_HOME on Windows with the default home", () => { + expect(commandCodeConfigPath({ COMMANDCODE_HOME: envHome }, homedir(), "win32")) + .toBe(join(homedir(), ".commandcode", "providers.json")); + expect(commandCodeConfigPath({ HOME: envHome, COMMANDCODE_HOME: userProfile }, homedir(), "win32")) + .toBe(join(envHome, ".commandcode", "providers.json")); + }); +}); + +// 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 66516cde100..d04310324ce 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", "droid"]); + expect(loopbackOnly).toEqual(["pi", "omp", "kimi", "gajae", "dsh", "mcode", "zcode", "commandcode", "prime", "aside", "raycast", "omo", "cline", "droid"]); }); 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 67b1dd3479b..378ea85a997 100644 --- a/tests/clients/integrations-writer.test.ts +++ b/tests/clients/integrations-writer.test.ts @@ -808,7 +808,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/clients/sync-client-integrations.test.ts b/tests/clients/sync-client-integrations.test.ts index b826161ed91..7d7caf43d2f 100644 --- a/tests/clients/sync-client-integrations.test.ts +++ b/tests/clients/sync-client-integrations.test.ts @@ -65,7 +65,7 @@ describe("ocx sync fans out to enabled native clients and owned file integration expect(fn).toContain("grokIntegrationEnabled(config)"); expect(fn).toContain("claudeDesktopIntegrationEnabled(config)"); - expect(fn).toContain('["mcode", "pi", "aside", "raycast", "omo", "cline", "droid", "opencode", "kilo"]'); + expect(fn).toContain('["mcode", "pi", "aside", "raycast", "omo", "cline", "commandcode", "droid", "opencode", "kilo"]'); expect(fn).toContain("refreshOwnedCatalogIntegrations"); // Native clients keep their catches; the owned catalog helper isolates file clients. expect(fn.match(/catch \(error\)/g)?.length).toBe(2); @@ -908,7 +908,9 @@ test("the direct ocx sync command refreshes owned clients including OpenCode and const start = src.indexOf("sync: async deps =>"); const command = src.slice(start, src.indexOf("v2: async deps =>", start)); expect(command).toContain("refreshOwnedCatalogIntegrations"); - expect(command).toContain('["mcode", "pi", "raycast", "omo", "cline", "droid", "opencode", "kilo"]'); + // Command Code joined this fan-out with its integration: a sync must refresh its + // provider block too, or the client keeps a stale catalog until the next apply. + expect(command).toContain('["mcode", "pi", "raycast", "omo", "cline", "commandcode", "droid", "opencode", "kilo"]'); expect(command).toContain("refreshAsideProfilesThroughServer"); expect(command.indexOf("syncModelsToCodex")).toBeLessThan(command.indexOf("refreshOwnedCatalogIntegrations")); expect(command).toContain('synced.status !== "refused"'); diff --git a/tests/config/client-config-export-new-clients.test.ts b/tests/config/client-config-export-new-clients.test.ts index 13979e9b4af..726dd2a226e 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", "droid"]); + expect(loopbackOnly).toEqual(["pi", "omp", "kimi", "gajae", "dsh", "mcode", "zcode", "commandcode", "prime", "aside", "raycast", "omo", "cline", "droid"]); }); 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 32f269888bb..0dd01842ff5 100644 --- a/tests/config/client-config-export.test.ts +++ b/tests/config/client-config-export.test.ts @@ -850,8 +850,8 @@ describe("hub-resolved Fast exports", () => { }); describe("EXPORT_CLIENTS registry", () => { - test("covers exactly the seventeen file-toggle clients", () => { - expect(EXPORT_CLIENT_IDS).toEqual(["opencode", "pi", "omp", "hermes", "openclaw", "kimi", "gajae", "dsh", "mcode", "zcode", "prime", "aside", "raycast", "omo", "cline", "kilo", "droid"]); + test("covers exactly the eighteen file-toggle clients", () => { + expect(EXPORT_CLIENT_IDS).toEqual(["opencode", "pi", "omp", "hermes", "openclaw", "kimi", "gajae", "dsh", "mcode", "zcode", "commandcode", "prime", "aside", "raycast", "omo", "cline", "kilo", "droid"]); 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 af44e7b3b11..b3893f7d7f8 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -329,6 +329,7 @@ "cline-client.test.ts": "clients", "cline-pass-deepseek-v4-tool-replay.test.ts": "providers", "cline-pass-provider.test.ts": "providers", "cline-pass-reasoning-efforts.test.ts": "providers", "cline-provider.test.ts": "providers", "cline-writer.test.ts": "clients", + "command-code-client-contract.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", "codebuddy-mcp-server.test.ts": "providers", "codebuddy-protocol.test.ts": "providers", "codebuddy-tool-bridge-turn.test.ts": "providers", diff --git a/tests/gui/integrations-invariants.test.ts b/tests/gui/integrations-invariants.test.ts index a9a02d2cbe5..57cbedfca4d 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(17); + expect(expected).toHaveLength(18); 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', droid: '{"theme":"dark","customModels":[{"model":"local","displayName":"Local","baseUrl":"http://127.0.0.1:11434/v1","provider":"generic-chat-completion-api"}]}\n', opencode: '{\n "provider": {\n "mine": { "npm": "keep-me" }\n }\n}\n', pi: '{\n "providers": {\n "mine": { "api": "http://keep-me" }\n }\n}\n',