feat(ai): support opencode 2 cli without pure flag

This commit is contained in:
lxk955
2026-10-17 17:46:06 +08:00
committed by GitHub
parent 0f745f73d0
commit 9174c6e265
34 changed files with 437 additions and 16 deletions
@@ -6221,6 +6221,7 @@ const aiEditPiAgentCliPath = ref("");
const aiEditPiAgentCliEnvRows = ref<AiEnvRow[]>([]);
const aiEditOpenCodeCliPath = ref("");
const aiEditOpenCodeCliEnvRows = ref<AiEnvRow[]>([]);
const aiEditOpenCodeCliVersion = ref("auto");
const aiEditCursorCliPath = ref("");
const aiEditCursorCliEnvRows = ref<AiEnvRow[]>([]);
const aiEditGrokCliPath = ref("");
@@ -6511,6 +6512,7 @@ function currentAiEditConfig() {
piAgentCliEnv: aiIsPiAgentCli.value ? cliEnvFromRows(aiEditPiAgentCliEnvRows.value) : {},
opencodeCliPath: aiEditOpenCodeCliPath.value.trim() || undefined,
opencodeCliEnv: aiIsOpenCodeCli.value ? cliEnvFromRows(aiEditOpenCodeCliEnvRows.value) : {},
opencodeCliVersion: aiIsOpenCodeCli.value ? aiEditOpenCodeCliVersion.value || undefined : undefined,
cursorCliPath: aiEditCursorCliPath.value.trim() || undefined,
cursorCliEnv: aiIsCursorCli.value ? cliEnvFromRows(aiEditCursorCliEnvRows.value) : {},
grokCliPath: aiEditGrokCliPath.value.trim() || undefined,
@@ -6552,6 +6554,7 @@ function aiSelectProvider(presetId: string) {
aiEditSkipTlsVerify.value = false;
aiEditEnableThinking.value = true;
aiEditReasoningLevel.value = "default";
aiEditOpenCodeCliVersion.value = "auto";
if (CLI_AI_PROVIDERS.has(provider)) void ensureCliMcpStatus();
}
@@ -6603,6 +6606,7 @@ function aiEnterEditMode(configId?: string) {
aiEditPiAgentCliEnvRows.value = aiEnvRowsFromConfig(config.piAgentCliEnv);
aiEditOpenCodeCliPath.value = config.opencodeCliPath ?? "";
aiEditOpenCodeCliEnvRows.value = aiEnvRowsFromConfig(config.opencodeCliEnv);
aiEditOpenCodeCliVersion.value = config.opencodeCliVersion ?? "auto";
aiEditCursorCliPath.value = config.cursorCliPath ?? "";
aiEditCursorCliEnvRows.value = aiEnvRowsFromConfig(config.cursorCliEnv);
aiEditGrokCliPath.value = config.grokCliPath ?? "";
@@ -6638,6 +6642,7 @@ function aiEnterEditMode(configId?: string) {
aiEditPiAgentCliEnvRows.value = [];
aiEditOpenCodeCliPath.value = "";
aiEditOpenCodeCliEnvRows.value = [];
aiEditOpenCodeCliVersion.value = "auto";
aiEditCursorCliPath.value = "";
aiEditCursorCliEnvRows.value = [];
aiEditGrokCliPath.value = "";
@@ -11752,6 +11757,26 @@ LIMIT 100;</pre>
</div>
</div>
<!-- OpenCode CLI Version -->
<div v-if="!aiIsCcSwitchProvider && aiIsOpenCodeCli" class="grid grid-cols-3 items-start gap-3">
<Label class="pt-2 text-right text-xs">{{ t("ai.openCodeCliVersion") }}</Label>
<div class="col-span-2 space-y-1.5">
<Select v-model="aiEditOpenCodeCliVersion">
<SelectTrigger class="h-8 text-xs">
<SelectValue :placeholder="t('ai.openCodeCliVersionAuto')" />
</SelectTrigger>
<SelectContent>
<SelectItem value="auto">{{ t("ai.openCodeCliVersionAuto") }}</SelectItem>
<SelectItem value="v2">{{ t("ai.openCodeCliVersionV2") }}</SelectItem>
<SelectItem value="v1">{{ t("ai.openCodeCliVersionV1") }}</SelectItem>
</SelectContent>
</Select>
<p class="text-[11px] text-muted-foreground">
{{ t("ai.openCodeCliVersionHint") }}
</p>
</div>
</div>
<!-- CLI Env -->
<div v-if="!aiIsCcSwitchProvider && aiIsCliProvider" class="grid grid-cols-3 items-start gap-3">
<Label class="pt-2 text-right text-xs">{{ t("ai.cliEnv") }}</Label>
@@ -115,7 +115,10 @@ describe("useAiModelCatalog", () => {
).resolves.toEqual([{ id: "first" }]);
await expect(catalog.loadModels({ ...initial, opencodeCliPath: "/usr/local/bin/opencode" })).resolves.toEqual([{ id: "second" }]);
expect(apiMock.aiListModels).toHaveBeenCalledTimes(2);
apiMock.aiListModels.mockResolvedValueOnce([{ id: "third" }]);
await expect(catalog.loadModels({ ...initial, opencodeCliVersion: "v2" })).resolves.toEqual([{ id: "third" }]);
expect(apiMock.aiListModels).toHaveBeenCalledTimes(3);
});
it("tracks Cursor executable and environment changes without depending on environment key order", async () => {
@@ -59,6 +59,7 @@ function configSignature(config: AiConfigItem): string {
claudeCodeCliPath: config.claudeCodeCliPath ?? null,
piAgentCliPath: config.piAgentCliPath ?? null,
opencodeCliPath: config.opencodeCliPath ?? null,
opencodeCliVersion: config.opencodeCliVersion ?? null,
cursorCliPath: config.cursorCliPath ?? null,
codebuddyCliPath: config.codebuddyCliPath ?? null,
qoderCliPath: config.qoderCliPath ?? null,
@@ -71,6 +72,7 @@ function configSignature(config: AiConfigItem): string {
claudeCodeCliEnv: sortedRecord(config.claudeCodeCliEnv),
piAgentCliEnv: sortedRecord(config.piAgentCliEnv),
opencodeCliEnv: sortedRecord(config.opencodeCliEnv),
opencodeCliVersion: config.opencodeCliVersion ?? "",
cursorCliEnv: sortedRecord(config.cursorCliEnv),
codebuddyCliEnv: sortedRecord(config.codebuddyCliEnv),
qoderCliEnv: sortedRecord(config.qoderCliEnv),
+5
View File
@@ -3328,6 +3328,11 @@ export default withEnglishFallback({
cliPath: "{provider} yolu",
cliPathHint: "PATH-dən {command} əmrindən istifadə etmək üçün boş saxlayın. {loginCommand} ilə ayrıca daxil olun.",
cliPathEnvError: "{provider} yolu yalnız icra faylının yolunu ehtiva etməlidir. Mühit dəyişənlərini aşağıda əlavə edin.",
openCodeCliVersion: "OpenCode CLI versiyası",
openCodeCliVersionAuto: "Avtomatik aşkarlama",
openCodeCliVersionV2: "OpenCode 2 (v2.x)",
openCodeCliVersionV1: "OpenCode 1 (v1.x)",
openCodeCliVersionHint: "OpenCode 2 köhnə --pure parametrini ləğv edir. Avtomatik aşkarlama icra faylının adını, CLI versiyasını və xəta çıxışını yoxlayır.",
cliEnv: "Mühit dəyişənləri",
cliEnvAdd: "Dəyişən əlavə et",
cliEnvHint: "{provider} üçün ötürülən istəyə bağlı dəyişənlər, məsələn, HTTPS_PROXY, NO_PROXY və ya SSL_CERT_FILE.",
+5
View File
@@ -4208,6 +4208,11 @@ export default {
cliPath: "{provider} Path",
cliPathHint: "Leave empty to use {command} from PATH. Sign in separately with {loginCommand}.",
cliPathEnvError: "{provider} path should contain only the executable path. Add environment variables below.",
openCodeCliVersion: "OpenCode CLI Version",
openCodeCliVersionAuto: "Auto-detect",
openCodeCliVersionV2: "OpenCode 2 (v2.x)",
openCodeCliVersionV1: "OpenCode 1 (v1.x)",
openCodeCliVersionHint: "OpenCode 2 removes the legacy --pure flag. Auto-detection checks the executable name, CLI version, and error output.",
cliEnv: "Environment variables",
cliEnvAdd: "Add variable",
cliEnvHint: "Optional variables passed to {provider}, such as HTTPS_PROXY, NO_PROXY, or SSL_CERT_FILE.",
+5
View File
@@ -3739,6 +3739,11 @@ export default withEnglishFallback({
cliPath: "{provider} Path",
cliPathHint: "Leave empty to use {command} from PATH. Sign in separately with {loginCommand}.",
cliPathEnvError: "{provider} path should contain only the executable path. Add environment variables below.",
openCodeCliVersion: "Versión de OpenCode CLI",
openCodeCliVersionAuto: "Detección automática",
openCodeCliVersionV2: "OpenCode 2 (v2.x)",
openCodeCliVersionV1: "OpenCode 1 (v1.x)",
openCodeCliVersionHint: "OpenCode 2 elimina el parámetro --pure heredado. La detección automática comprueba el nombre del ejecutable, la versión y la salida de error.",
cliEnv: "Environment variables",
cliEnvAdd: "Add variable",
cliEnvHint: "Optional variables passed to {provider}, such as HTTPS_PROXY, NO_PROXY, or SSL_CERT_FILE.",
+5
View File
@@ -3655,6 +3655,11 @@ export default withEnglishFallback({
cliPath: "Path {provider}",
cliPathHint: "Biarkan kosong untuk menggunakan {command} dari PATH. Masuk secara terpisah dengan {loginCommand}.",
cliPathEnvError: "Path {provider} hanya boleh berisi path eksekutabel. Tambahkan variabel lingkungan di bawah.",
openCodeCliVersion: "Versi OpenCode CLI",
openCodeCliVersionAuto: "Deteksi otomatis",
openCodeCliVersionV2: "OpenCode 2 (v2.x)",
openCodeCliVersionV1: "OpenCode 1 (v1.x)",
openCodeCliVersionHint: "OpenCode 2 menghapus flag lama --pure. Deteksi otomatis memeriksa nama eksekutabel, versi CLI, dan keluaran galat.",
cliEnv: "Variabel lingkungan",
cliEnvAdd: "Tambah variabel",
cliEnvHint: "Variabel opsional yang dikirim ke {provider}, seperti HTTPS_PROXY, NO_PROXY, atau SSL_CERT_FILE.",
+5
View File
@@ -3918,6 +3918,11 @@ export default withEnglishFallback({
cliPath: "{provider} Path",
cliPathHint: "Leave empty to use {command} from PATH. Sign in separately with {loginCommand}.",
cliPathEnvError: "{provider} path should contain only the executable path. Add environment variables below.",
openCodeCliVersion: "Versione OpenCode CLI",
openCodeCliVersionAuto: "Rileva automaticamente",
openCodeCliVersionV2: "OpenCode 2 (v2.x)",
openCodeCliVersionV1: "OpenCode 1 (v1.x)",
openCodeCliVersionHint: "OpenCode 2 rimuove il flag legacy --pure. Il rilevamento automatico verifica il nome dell'eseguibile, la versione CLI e l'output di errore.",
cliEnv: "Environment variables",
cliEnvAdd: "Add variable",
cliEnvHint: "Optional variables passed to {provider}, such as HTTPS_PROXY, NO_PROXY, or SSL_CERT_FILE.",
+5
View File
@@ -3608,6 +3608,11 @@ export default withEnglishFallback({
cliPath: "{provider} Path",
cliPathHint: "Leave empty to use {command} from PATH. Sign in separately with {loginCommand}.",
cliPathEnvError: "{provider} path should contain only the executable path. Add environment variables below.",
openCodeCliVersion: "OpenCode CLI バージョン",
openCodeCliVersionAuto: "自動検出",
openCodeCliVersionV2: "OpenCode 2 (v2.x)",
openCodeCliVersionV1: "OpenCode 1 (v1.x)",
openCodeCliVersionHint: "OpenCode 2 では従来の --pure フラグが削除されています。自動検出で実行可能ファイル名、CLI バージョン、エラー出力を確認します。",
cliEnv: "Environment variables",
cliEnvAdd: "Add variable",
cliEnvHint: "Optional variables passed to {provider}, such as HTTPS_PROXY, NO_PROXY, or SSL_CERT_FILE.",
+5
View File
@@ -3757,6 +3757,11 @@ export default withEnglishFallback({
cliPath: "{provider} 경로",
cliPathHint: "PATH의 {command}를 사용하려면 비워 두세요. {loginCommand}로 별도로 로그인하세요.",
cliPathEnvError: "{provider} 경로에는 실행 파일 경로만 포함해야 합니다. 환경 변수는 아래에 추가하세요.",
openCodeCliVersion: "OpenCode CLI 버전",
openCodeCliVersionAuto: "자동 감지",
openCodeCliVersionV2: "OpenCode 2 (v2.x)",
openCodeCliVersionV1: "OpenCode 1 (v1.x)",
openCodeCliVersionHint: "OpenCode 2에서는 기존의 --pure 플래그가 제거되었습니다. 자동 감지는 실행 파일 이름, CLI 버전 및 오류 출력을 확인합니다.",
cliEnv: "환경 변수",
cliEnvAdd: "변수 추가",
cliEnvHint: "{provider}에 전달할 선택적 변수 (예: HTTPS_PROXY, NO_PROXY, SSL_CERT_FILE).",
+5
View File
@@ -3585,6 +3585,11 @@ export default withEnglishFallback({
cliPath: "{provider} Path",
cliPathHint: "Leave empty to use {command} from PATH. Sign in separately with {loginCommand}.",
cliPathEnvError: "{provider} path should contain only the executable path. Add environment variables below.",
openCodeCliVersion: "Versão do OpenCode CLI",
openCodeCliVersionAuto: "Detectar automaticamente",
openCodeCliVersionV2: "OpenCode 2 (v2.x)",
openCodeCliVersionV1: "OpenCode 1 (v1.x)",
openCodeCliVersionHint: "O OpenCode 2 remove a flag legada --pure. A detecção automática verifica o nome do executável, versão da CLI e saída de erro.",
cliEnv: "Environment variables",
cliEnvAdd: "Add variable",
cliEnvHint: "Optional variables passed to {provider}, such as HTTPS_PROXY, NO_PROXY, or SSL_CERT_FILE.",
+5
View File
@@ -3921,6 +3921,11 @@ export default withEnglishFallback({
cliPath: "Путь к {provider}",
cliPathHint: "Оставьте пустым, чтобы использовать {command} из PATH. Войдите отдельно командой {loginCommand}.",
cliPathEnvError: "Путь к {provider} должен содержать только путь к исполняемому файлу. Добавьте переменные окружения ниже.",
openCodeCliVersion: "Версия OpenCode CLI",
openCodeCliVersionAuto: "Автоопределение",
openCodeCliVersionV2: "OpenCode 2 (v2.x)",
openCodeCliVersionV1: "OpenCode 1 (v1.x)",
openCodeCliVersionHint: "В OpenCode 2 удален устаревший флаг --pure. Автоопределение проверяет имя исполняемого файла, версию CLI и вывод ошибок.",
cliEnv: "Переменные окружения",
cliEnvAdd: "Добавить переменную",
cliEnvHint: "Необязательные переменные, передаваемые {provider}, например HTTPS_PROXY, NO_PROXY или SSL_CERT_FILE.",
+5
View File
@@ -4121,6 +4121,11 @@ export default withEnglishFallback({
cliPath: "Path ของ {provider}",
cliPathHint: "ปล่อยว่างเพื่อใช้ {command} จาก PATH ลงชื่อเข้าใช้แยกต่างหากด้วย {loginCommand}",
cliPathEnvError: "Path ของ {provider} ควรมีเฉพาะ Path ของ Executable เพิ่ม Environment variables ด้านล่าง",
openCodeCliVersion: "เวอร์ชัน OpenCode CLI",
openCodeCliVersionAuto: "ตรวจหาอัตโนมัติ",
openCodeCliVersionV2: "OpenCode 2 (v2.x)",
openCodeCliVersionV1: "OpenCode 1 (v1.x)",
openCodeCliVersionHint: "OpenCode 2 ยกเลิกการใช้ flag --pure เดิม การตรวจหาอัตโนมัติจะตรวจสอบชื่อไฟล์ executable เวอร์ชัน CLI และข้อผิดพลาด",
cliEnv: "Environment variables",
cliEnvAdd: "เพิ่มตัวแปร",
cliEnvHint: "ตัวแปรที่ส่งไปยัง {provider} (ไม่บังคับ) เช่น HTTPS_PROXY, NO_PROXY หรือ SSL_CERT_FILE",
+5
View File
@@ -3299,6 +3299,11 @@ export default withEnglishFallback({
cliPath: "{provider} Yolu",
cliPathHint: "PATH üzerindeki {command} komutunu kullanmak için boş bırakın. {loginCommand} ile ayrıca oturum açın.",
cliPathEnvError: "{provider} yolu yalnızca çalıştırılabilir dosyanın yolunu içermelidir. Ortam değişkenlerini aşağıya ekleyin.",
openCodeCliVersion: "OpenCode CLI Sürümü",
openCodeCliVersionAuto: "Otomatik algıla",
openCodeCliVersionV2: "OpenCode 2 (v2.x)",
openCodeCliVersionV1: "OpenCode 1 (v1.x)",
openCodeCliVersionHint: "OpenCode 2 eski --pure bayrağını kaldırır. Otomatik algılama çalıştırılabilir dosya adını, CLI sürümünü ve hata çıktısını denetler.",
cliEnv: "Ortam değişkenleri",
cliEnvAdd: "Değişken ekle",
cliEnvHint: "{provider} sağlayıcısına iletilen isteğe bağlı değişkenler; örneğin HTTPS_PROXY, NO_PROXY veya SSL_CERT_FILE.",
+5
View File
@@ -4138,6 +4138,11 @@ export default withEnglishFallback({
cliPath: "{provider} 路径",
cliPathHint: "留空则使用 PATH 中的 {command}。请先在终端执行 {loginCommand} 登录。",
cliPathEnvError: "{provider} 路径只能填写可执行文件路径。请在下方添加环境变量。",
openCodeCliVersion: "OpenCode CLI 版本",
openCodeCliVersionAuto: "自动检测",
openCodeCliVersionV2: "OpenCode 2 (v2.x)",
openCodeCliVersionV1: "OpenCode 1 (v1.x)",
openCodeCliVersionHint: "OpenCode 2 移除了旧版的 --pure 参数。自动检测会检查可执行文件名、CLI 版本及报错输出。",
cliEnv: "环境变量",
cliEnvAdd: "添加变量",
cliEnvHint: "传递给 {provider} 的可选变量,例如 HTTPS_PROXY、NO_PROXY 或 SSL_CERT_FILE。",
+5
View File
@@ -3893,6 +3893,11 @@ export default withEnglishFallback({
cliPath: "{provider} 路径",
cliPathHint: "留空则使用 PATH 中的 {command}。请先在终端执行 {loginCommand} 登录。",
cliPathEnvError: "{provider} 路径只能填写可执行文件路径。请在下方添加环境变量。",
openCodeCliVersion: "OpenCode CLI 版本",
openCodeCliVersionAuto: "自動檢測",
openCodeCliVersionV2: "OpenCode 2 (v2.x)",
openCodeCliVersionV1: "OpenCode 1 (v1.x)",
openCodeCliVersionHint: "OpenCode 2 移除了舊版的 --pure 參數。自動檢測會檢查執行檔名稱、CLI 版本及報錯輸出。",
cliEnv: "环境变量",
cliEnvAdd: "添加变量",
cliEnvHint: "传递给 {provider} 的可选变量,例如 HTTPS_PROXY、NO_PROXY 或 SSL_CERT_FILE。",
@@ -1454,6 +1454,7 @@ describe("settingsStore AI API key normalization", () => {
provider: "opencode-cli",
opencodeCliPath: " /opt/homebrew/bin/opencode ",
opencodeCliEnv: { HTTPS_PROXY: "http://127.0.0.1:7890", EMPTY: null as unknown as string },
opencodeCliVersion: " v2 ",
}),
).toMatchObject({
provider: "opencode-cli",
@@ -1461,6 +1462,7 @@ describe("settingsStore AI API key normalization", () => {
model: "default",
opencodeCliPath: "/opt/homebrew/bin/opencode",
opencodeCliEnv: { HTTPS_PROXY: "http://127.0.0.1:7890", EMPTY: "" },
opencodeCliVersion: "v2",
});
});
+1
View File
@@ -728,6 +728,7 @@ export function normalizeAiConfig(config: Partial<AiConfig> | null | undefined):
piAgentCliEnv: normalizeAiEnv(config?.piAgentCliEnv),
opencodeCliPath: config?.opencodeCliPath?.trim() || undefined,
opencodeCliEnv: normalizeAiEnv(config?.opencodeCliEnv),
opencodeCliVersion: config?.opencodeCliVersion?.trim() || undefined,
cursorCliPath: config?.cursorCliPath?.trim() || undefined,
cursorCliEnv: normalizeAiEnv(config?.cursorCliEnv),
grokCliPath: config?.grokCliPath?.trim() || undefined,
+1
View File
@@ -75,6 +75,7 @@ export interface AiConfig {
piAgentCliEnv?: Record<string, string>;
opencodeCliPath?: string | null;
opencodeCliEnv?: Record<string, string>;
opencodeCliVersion?: string | null;
cursorCliPath?: string | null;
cursorCliEnv?: Record<string, string>;
grokCliPath?: string | null;
+29 -1
View File
@@ -99,7 +99,7 @@ pub enum AiProvider {
ClaudeCodeCli,
#[serde(rename = "pi-agent-cli")]
PiAgentCli,
#[serde(rename = "opencode-cli")]
#[serde(rename = "opencode-cli", alias = "opencode2-cli")]
OpenCodeCli,
#[serde(rename = "cursor-cli")]
CursorCli,
@@ -459,6 +459,8 @@ pub struct AiConfig {
#[serde(default)]
pub opencode_cli_env: HashMap<String, String>,
#[serde(default)]
pub opencode_cli_version: Option<String>,
#[serde(default)]
pub cursor_cli_path: Option<String>,
#[serde(default)]
pub cursor_cli_env: HashMap<String, String>,
@@ -5634,6 +5636,7 @@ mod tests {
pi_agent_cli_env: Default::default(),
opencode_cli_path: None,
opencode_cli_env: Default::default(),
opencode_cli_version: None,
cursor_cli_path: None,
cursor_cli_env: Default::default(),
grok_cli_path: None,
@@ -6271,6 +6274,7 @@ mod tests {
assert!(config.pi_agent_cli_env.is_empty());
assert!(config.opencode_cli_path.is_none());
assert!(config.opencode_cli_env.is_empty());
assert!(config.opencode_cli_version.is_none());
assert!(config.cursor_cli_path.is_none());
assert!(config.cursor_cli_env.is_empty());
assert!(config.codebuddy_cli_path.is_none());
@@ -6307,6 +6311,7 @@ mod tests {
pi_agent_cli_env: Default::default(),
opencode_cli_path: None,
opencode_cli_env: Default::default(),
opencode_cli_version: None,
cursor_cli_path: None,
cursor_cli_env: Default::default(),
grok_cli_path: None,
@@ -6350,6 +6355,7 @@ mod tests {
pi_agent_cli_env: Default::default(),
opencode_cli_path: None,
opencode_cli_env: Default::default(),
opencode_cli_version: None,
cursor_cli_path: None,
cursor_cli_env: Default::default(),
grok_cli_path: None,
@@ -6408,6 +6414,7 @@ mod tests {
pi_agent_cli_env: Default::default(),
opencode_cli_path: None,
opencode_cli_env: Default::default(),
opencode_cli_version: None,
cursor_cli_path: None,
cursor_cli_env: Default::default(),
grok_cli_path: None,
@@ -6449,6 +6456,7 @@ mod tests {
pi_agent_cli_env: Default::default(),
opencode_cli_path: None,
opencode_cli_env: Default::default(),
opencode_cli_version: None,
cursor_cli_path: None,
cursor_cli_env: Default::default(),
grok_cli_path: None,
@@ -6494,6 +6502,7 @@ mod tests {
pi_agent_cli_env: Default::default(),
opencode_cli_path: None,
opencode_cli_env: Default::default(),
opencode_cli_version: None,
cursor_cli_path: None,
cursor_cli_env: Default::default(),
grok_cli_path: None,
@@ -6536,6 +6545,7 @@ mod tests {
pi_agent_cli_env: Default::default(),
opencode_cli_path: None,
opencode_cli_env: Default::default(),
opencode_cli_version: None,
cursor_cli_path: None,
cursor_cli_env: Default::default(),
grok_cli_path: None,
@@ -6597,6 +6607,7 @@ mod tests {
pi_agent_cli_env: Default::default(),
opencode_cli_path: None,
opencode_cli_env: Default::default(),
opencode_cli_version: None,
cursor_cli_path: None,
cursor_cli_env: Default::default(),
grok_cli_path: None,
@@ -6634,6 +6645,7 @@ mod tests {
pi_agent_cli_env: Default::default(),
opencode_cli_path: None,
opencode_cli_env: Default::default(),
opencode_cli_version: None,
cursor_cli_path: None,
cursor_cli_env: Default::default(),
grok_cli_path: None,
@@ -6674,6 +6686,7 @@ mod tests {
pi_agent_cli_env: Default::default(),
opencode_cli_path: None,
opencode_cli_env: Default::default(),
opencode_cli_version: None,
cursor_cli_path: None,
cursor_cli_env: Default::default(),
grok_cli_path: None,
@@ -6781,6 +6794,7 @@ mod tests {
pi_agent_cli_env: Default::default(),
opencode_cli_path: None,
opencode_cli_env: Default::default(),
opencode_cli_version: None,
cursor_cli_path: None,
cursor_cli_env: Default::default(),
grok_cli_path: None,
@@ -6869,6 +6883,7 @@ mod tests {
pi_agent_cli_env: Default::default(),
opencode_cli_path: None,
opencode_cli_env: Default::default(),
opencode_cli_version: None,
cursor_cli_path: None,
cursor_cli_env: Default::default(),
grok_cli_path: None,
@@ -6984,6 +6999,7 @@ mod tests {
pi_agent_cli_env: Default::default(),
opencode_cli_path: None,
opencode_cli_env: Default::default(),
opencode_cli_version: None,
cursor_cli_path: None,
cursor_cli_env: Default::default(),
grok_cli_path: None,
@@ -7034,6 +7050,7 @@ mod tests {
pi_agent_cli_env: Default::default(),
opencode_cli_path: None,
opencode_cli_env: Default::default(),
opencode_cli_version: None,
cursor_cli_path: None,
cursor_cli_env: Default::default(),
grok_cli_path: None,
@@ -7206,6 +7223,7 @@ mod tests {
pi_agent_cli_env: Default::default(),
opencode_cli_path: None,
opencode_cli_env: Default::default(),
opencode_cli_version: None,
cursor_cli_path: None,
cursor_cli_env: Default::default(),
grok_cli_path: None,
@@ -7252,6 +7270,7 @@ mod tests {
pi_agent_cli_env: Default::default(),
opencode_cli_path: None,
opencode_cli_env: Default::default(),
opencode_cli_version: None,
cursor_cli_path: None,
cursor_cli_env: Default::default(),
grok_cli_path: None,
@@ -7346,6 +7365,7 @@ mod tests {
pi_agent_cli_env: Default::default(),
opencode_cli_path: None,
opencode_cli_env: Default::default(),
opencode_cli_version: None,
cursor_cli_path: None,
cursor_cli_env: Default::default(),
grok_cli_path: None,
@@ -7695,6 +7715,7 @@ mod tests {
pi_agent_cli_env: Default::default(),
opencode_cli_path: None,
opencode_cli_env: Default::default(),
opencode_cli_version: None,
cursor_cli_path: None,
cursor_cli_env: Default::default(),
grok_cli_path: None,
@@ -7928,6 +7949,7 @@ mod tests {
pi_agent_cli_env: Default::default(),
opencode_cli_path: None,
opencode_cli_env: Default::default(),
opencode_cli_version: None,
cursor_cli_path: None,
cursor_cli_env: Default::default(),
grok_cli_path: None,
@@ -7981,6 +8003,7 @@ mod tests {
pi_agent_cli_env: Default::default(),
opencode_cli_path: None,
opencode_cli_env: Default::default(),
opencode_cli_version: None,
cursor_cli_path: None,
cursor_cli_env: Default::default(),
grok_cli_path: None,
@@ -8037,6 +8060,7 @@ mod tests {
pi_agent_cli_env: Default::default(),
opencode_cli_path: None,
opencode_cli_env: Default::default(),
opencode_cli_version: None,
cursor_cli_path: None,
cursor_cli_env: Default::default(),
grok_cli_path: None,
@@ -8087,6 +8111,7 @@ mod tests {
pi_agent_cli_env: Default::default(),
opencode_cli_path: None,
opencode_cli_env: Default::default(),
opencode_cli_version: None,
cursor_cli_path: None,
cursor_cli_env: Default::default(),
grok_cli_path: None,
@@ -8481,6 +8506,7 @@ mod tests {
pi_agent_cli_env: Default::default(),
opencode_cli_path: None,
opencode_cli_env: Default::default(),
opencode_cli_version: None,
cursor_cli_path: None,
cursor_cli_env: Default::default(),
grok_cli_path: None,
@@ -8558,6 +8584,7 @@ mod tests {
pi_agent_cli_env: Default::default(),
opencode_cli_path: None,
opencode_cli_env: Default::default(),
opencode_cli_version: None,
cursor_cli_path: None,
cursor_cli_env: Default::default(),
grok_cli_path: None,
@@ -8631,6 +8658,7 @@ mod tests {
pi_agent_cli_env: Default::default(),
opencode_cli_path: None,
opencode_cli_env: Default::default(),
opencode_cli_version: None,
cursor_cli_path: None,
cursor_cli_env: Default::default(),
grok_cli_path: None,
@@ -586,6 +586,7 @@ mod tests {
pi_agent_cli_env: Default::default(),
opencode_cli_path: None,
opencode_cli_env: Default::default(),
opencode_cli_version: None,
cursor_cli_path: None,
cursor_cli_env: Default::default(),
grok_cli_path: None,
@@ -941,6 +941,7 @@ mod tests {
pi_agent_cli_env: Default::default(),
opencode_cli_path: None,
opencode_cli_env: Default::default(),
opencode_cli_version: None,
cursor_cli_path: None,
cursor_cli_env: Default::default(),
grok_cli_path: None,
@@ -1132,6 +1132,7 @@ mod tests {
pi_agent_cli_env: Default::default(),
opencode_cli_path: None,
opencode_cli_env: Default::default(),
opencode_cli_version: None,
cursor_cli_path: None,
cursor_cli_env: Default::default(),
grok_cli_path: None,
@@ -423,6 +423,7 @@ mod tests {
pi_agent_cli_env: Default::default(),
opencode_cli_path: None,
opencode_cli_env: Default::default(),
opencode_cli_version: None,
cursor_cli_path: None,
cursor_cli_env: Default::default(),
grok_cli_path: None,
+1
View File
@@ -482,6 +482,7 @@ mod tests {
pi_agent_cli_env: Default::default(),
opencode_cli_path: None,
opencode_cli_env: Default::default(),
opencode_cli_version: None,
cursor_cli_path: None,
cursor_cli_env: Default::default(),
grok_cli_path: None,
@@ -803,6 +803,7 @@ mod tests {
pi_agent_cli_env: Default::default(),
opencode_cli_path: None,
opencode_cli_env: Default::default(),
opencode_cli_version: None,
cursor_cli_path: None,
cursor_cli_env: Default::default(),
grok_cli_path: None,
+277 -13
View File
@@ -5,11 +5,12 @@ use crate::ai_cli_agent::{
CliAgentCommandSpec, CliAgentJsonlDialect, CliAgentProcessSpec, CliAgentRunOptions,
};
use serde_json::{json, Map, Value};
use std::collections::{BTreeMap, BTreeSet};
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::env;
use std::path::{Path, PathBuf};
use std::sync::{Arc, LazyLock};
use std::time::{Duration, Instant};
use tokio::sync::Notify;
use tokio::sync::{Notify, RwLock};
const OPENCODE_MODEL_DISCOVERY_TIMEOUT: Duration = Duration::from_secs(10);
const OPENCODE_CONTROL_ENV: &[&str] = &[
@@ -24,6 +25,103 @@ const OPENCODE_CONTROL_ENV: &[&str] = &[
pub type OpenCodeRunOptions = CliAgentRunOptions;
pub type OpenCodeCommandSpec = CliAgentCommandSpec;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OpenCodeVersion {
V1,
V2,
}
static OPENCODE_DETECTED_V2: LazyLock<RwLock<HashMap<String, bool>>> = LazyLock::new(|| RwLock::new(HashMap::new()));
pub fn resolve_configured_opencode_version(config: &AiConfig) -> Option<OpenCodeVersion> {
if let Some(version) = &config.opencode_cli_version {
let trimmed = version.trim().to_ascii_lowercase();
if trimmed == "v2" || trimmed == "2" || trimmed == "opencode2" {
return Some(OpenCodeVersion::V2);
}
if trimmed == "v1" || trimmed == "1" || trimmed == "opencode1" {
return Some(OpenCodeVersion::V1);
}
}
None
}
pub fn is_opencode_v2_program_name(program: &str) -> bool {
let lower = program.to_ascii_lowercase();
let file_name = Path::new(&lower).file_name().and_then(|n| n.to_str()).unwrap_or(&lower);
file_name.contains("opencode2")
|| file_name.contains("opencode-2")
|| file_name.contains("opencode-v2")
|| file_name.contains("opencode_v2")
}
pub fn parse_opencode_version_is_v2(output: &str) -> bool {
let lower = output.to_ascii_lowercase();
for token in lower.split_whitespace() {
let cleaned = token.trim_start_matches('v');
if let Some(major_str) = cleaned.split('.').next() {
if let Ok(major) = major_str.parse::<u32>() {
if major >= 2 {
return true;
}
}
}
}
false
}
pub async fn record_opencode_version_v2(program: &str) {
OPENCODE_DETECTED_V2.write().await.insert(program.to_string(), true);
}
pub async fn detect_opencode_version(program: &str) -> OpenCodeVersion {
if is_opencode_v2_program_name(program) {
return OpenCodeVersion::V2;
}
if let Some(&is_v2) = OPENCODE_DETECTED_V2.read().await.get(program) {
return if is_v2 { OpenCodeVersion::V2 } else { OpenCodeVersion::V1 };
}
let mut process = cli_command(program);
process.arg("--version");
let is_v2 = match tokio::time::timeout(Duration::from_millis(1500), process.output()).await {
Ok(Ok(output)) if output.status.success() => {
let stdout = String::from_utf8_lossy(&output.stdout);
parse_opencode_version_is_v2(&stdout)
}
_ => false,
};
OPENCODE_DETECTED_V2.write().await.insert(program.to_string(), is_v2);
if is_v2 {
OpenCodeVersion::V2
} else {
OpenCodeVersion::V1
}
}
pub fn resolve_opencode_version_fast(config: &AiConfig) -> OpenCodeVersion {
if let Some(version) = resolve_configured_opencode_version(config) {
return version;
}
let program = opencode_program(config);
if is_opencode_v2_program_name(&program) {
return OpenCodeVersion::V2;
}
if let Ok(guard) = OPENCODE_DETECTED_V2.try_read() {
if let Some(&is_v2) = guard.get(&program) {
return if is_v2 { OpenCodeVersion::V2 } else { OpenCodeVersion::V1 };
}
}
OpenCodeVersion::V1
}
pub fn is_unrecognized_pure_flag(message: &str) -> bool {
let lower = message.to_ascii_lowercase();
lower.contains("--pure")
&& (lower.contains("unrecognized") || lower.contains("unknown") || lower.contains("unexpected"))
}
struct OpenCodeIsolatedCwd {
path: PathBuf,
}
@@ -216,21 +314,48 @@ fn opencode_selection_args(config: &AiConfig) -> Vec<String> {
}
pub fn build_opencode_command(config: &AiConfig) -> OpenCodeCommandSpec {
let version = resolve_opencode_version_fast(config);
build_opencode_command_for_version(config, version)
}
pub fn build_opencode_command_for_version(config: &AiConfig, version: OpenCodeVersion) -> OpenCodeCommandSpec {
let mut command = OpenCodeCommandSpec { program: opencode_program(config), args: Vec::new() };
command.args.extend(["run".to_string(), "--format".to_string(), "json".to_string(), "--pure".to_string()]);
command.args.extend(["run".to_string(), "--format".to_string(), "json".to_string()]);
if version == OpenCodeVersion::V1 {
command.args.push("--pure".to_string());
}
command.args.extend(opencode_selection_args(config));
command
}
fn resolved_models_command(config: &AiConfig) -> Result<OpenCodeCommandSpec, String> {
pub fn resolved_models_command(config: &AiConfig) -> Result<OpenCodeCommandSpec, String> {
let version = resolve_opencode_version_fast(config);
resolved_models_command_for_version(config, version)
}
pub fn resolved_models_command_for_version(
config: &AiConfig,
version: OpenCodeVersion,
) -> Result<OpenCodeCommandSpec, String> {
let mut command = resolve_opencode_command(config)?;
command.args.extend(["models".to_string(), "--verbose".to_string(), "--pure".to_string()]);
command.args.extend(["models".to_string(), "--verbose".to_string()]);
if version == OpenCodeVersion::V1 {
command.args.push("--pure".to_string());
}
Ok(command)
}
fn resolved_run_command(config: &AiConfig) -> Result<OpenCodeCommandSpec, String> {
pub fn resolved_run_command(config: &AiConfig) -> Result<OpenCodeCommandSpec, String> {
let version = resolve_opencode_version_fast(config);
resolved_run_command_for_version(config, version)
}
pub fn resolved_run_command_for_version(
config: &AiConfig,
version: OpenCodeVersion,
) -> Result<OpenCodeCommandSpec, String> {
let resolved = resolve_opencode_command(config)?;
let mut command = build_opencode_command(config);
let mut command = build_opencode_command_for_version(config, version);
command.program = resolved.program;
command.args.splice(0..0, resolved.args);
Ok(command)
@@ -241,7 +366,13 @@ pub fn build_opencode_prompt(system_prompt: &str, messages: &[crate::ai::AiMessa
}
pub async fn list_opencode_models(config: &AiConfig) -> Result<Vec<AiModelInfo>, String> {
let command = resolved_models_command(config)?;
let mut version = if let Some(configured) = resolve_configured_opencode_version(config) {
configured
} else {
let program = opencode_program(config);
detect_opencode_version(&program).await
};
let command = resolved_models_command_for_version(config, version)?;
let runtime = OpenCodeIsolatedCwd::create()?;
let mut process = cli_command(&command.program);
process.args(&command.args);
@@ -258,7 +389,34 @@ pub async fn list_opencode_models(config: &AiConfig) -> Result<Vec<AiModelInfo>,
.map_err(|error| classify_opencode_spawn_error(&error.to_string()))?;
if !output.status.success() {
return Err(classify_opencode_run_error(&combined_output(&output.stderr, &output.stdout)));
let err_output = combined_output(&output.stderr, &output.stdout);
if version == OpenCodeVersion::V1 && is_unrecognized_pure_flag(&err_output) {
let program = opencode_program(config);
record_opencode_version_v2(&program).await;
version = OpenCodeVersion::V2;
let retry_command = resolved_models_command_for_version(config, version)?;
let mut retry_process = cli_command(&retry_command.program);
retry_process.args(&retry_command.args);
for key in OPENCODE_CONTROL_ENV {
retry_process.env_remove(key);
}
retry_process
.envs(opencode_process_env(config, opencode_runtime_config(None))?)
.current_dir(&runtime.path)
.kill_on_drop(true);
let retry_output = tokio::time::timeout(OPENCODE_MODEL_DISCOVERY_TIMEOUT, retry_process.output())
.await
.map_err(|_| "[openCodeTimeout] OpenCode model discovery timed out".to_string())?
.map_err(|error| classify_opencode_spawn_error(&error.to_string()))?;
if !retry_output.status.success() {
return Err(classify_opencode_run_error(&combined_output(&retry_output.stderr, &retry_output.stdout)));
}
return parse_opencode_models(&String::from_utf8_lossy(&retry_output.stdout)).ok_or_else(|| {
"[openCodeNotAuthenticated] OpenCode returned no configured models. Sign in or configure a provider in OpenCode, then retry."
.to_string()
});
}
return Err(classify_opencode_run_error(&err_output));
}
parse_opencode_models(&String::from_utf8_lossy(&output.stdout)).ok_or_else(|| {
"[openCodeNotAuthenticated] OpenCode returned no configured models. Sign in or configure a provider in OpenCode, then retry."
@@ -413,10 +571,22 @@ pub async fn run_opencode_agent(
cancelled: &Notify,
on_event: impl Fn(AgentEvent) + Send + Sync + 'static,
) -> Result<String, String> {
let version = if let Some(configured) = resolve_configured_opencode_version(config) {
configured
} else {
let program = opencode_program(config);
detect_opencode_version(&program).await
};
let runtime = OpenCodeIsolatedCwd::create()?;
let result = run_cli_jsonl_agent(
let command = resolved_run_command_for_version(config, version)?;
let on_event_arc = Arc::new(on_event);
let on_event_fn = {
let on_event_arc = Arc::clone(&on_event_arc);
move |event| on_event_arc(event)
};
let run_res = run_cli_jsonl_agent(
CliAgentProcessSpec {
command: resolved_run_command(config)?,
command,
env: opencode_process_env(config, opencode_runtime_config(Some(&options)))?,
env_remove: OPENCODE_CONTROL_ENV.iter().map(|value| (*value).to_string()).collect(),
current_dir: Some(runtime.path.clone()),
@@ -426,9 +596,38 @@ pub async fn run_opencode_agent(
classify_run_error: classify_opencode_run_error,
},
cancelled,
on_event,
on_event_fn,
)
.await?;
.await;
let result = match run_res {
Err(err) if version == OpenCodeVersion::V1 && is_unrecognized_pure_flag(&err) => {
let program = opencode_program(config);
record_opencode_version_v2(&program).await;
let v2_command = resolved_run_command_for_version(config, OpenCodeVersion::V2)?;
let on_event_fn2 = {
let on_event_arc = Arc::clone(&on_event_arc);
move |event| on_event_arc(event)
};
run_cli_jsonl_agent(
CliAgentProcessSpec {
command: v2_command,
env: opencode_process_env(config, opencode_runtime_config(Some(&options)))?,
env_remove: OPENCODE_CONTROL_ENV.iter().map(|value| (*value).to_string()).collect(),
current_dir: Some(runtime.path.clone()),
stdin: Some(prompt.to_string()),
dialect: CliAgentJsonlDialect::OpenCodeRun,
classify_spawn_error: classify_opencode_spawn_error,
classify_run_error: classify_opencode_run_error,
},
cancelled,
on_event_fn2,
)
.await?
}
other => other?,
};
if result.trim().is_empty() {
Err("[openCodeProtocolError] OpenCode completed without a text response".to_string())
} else {
@@ -469,6 +668,7 @@ mod tests {
pi_agent_cli_env: Default::default(),
opencode_cli_path: None,
opencode_cli_env: Default::default(),
opencode_cli_version: None,
cursor_cli_path: None,
cursor_cli_env: Default::default(),
grok_cli_path: None,
@@ -528,6 +728,70 @@ mod tests {
assert_eq!(command.args, ["run", "--format", "json", "--pure"]);
}
#[test]
fn builds_run_command_for_opencode2_executable() {
let mut config = config("default");
config.opencode_cli_path = Some("/usr/local/bin/opencode2".to_string());
let command = build_opencode_command(&config);
assert_eq!(command.program, "/usr/local/bin/opencode2");
assert_eq!(command.args, ["run", "--format", "json"]);
}
#[test]
fn builds_run_command_for_opencode2_configured_version() {
let mut config = config("default");
config.opencode_cli_version = Some("v2".to_string());
let command = build_opencode_command(&config);
assert_eq!(command.program, "opencode");
assert_eq!(command.args, ["run", "--format", "json"]);
}
#[test]
fn models_command_omits_pure_flag_for_v2() {
let mut config = config("default");
config.opencode_cli_version = Some("v2".to_string());
let command = resolved_models_command(&config).unwrap();
assert_eq!(command.program, "opencode");
assert_eq!(command.args, ["models", "--verbose"]);
}
#[test]
fn parses_opencode_version_string() {
assert!(parse_opencode_version_is_v2("opencode v2.0.1"));
assert!(parse_opencode_version_is_v2("opencode 2.0.0"));
assert!(parse_opencode_version_is_v2("2.1.0"));
assert!(parse_opencode_version_is_v2("v2.0.0-beta.1"));
assert!(!parse_opencode_version_is_v2("opencode v1.4.2"));
assert!(!parse_opencode_version_is_v2("opencode 0.9.1"));
assert!(!parse_opencode_version_is_v2(""));
}
#[test]
fn detects_opencode_v2_from_program_name() {
assert!(is_opencode_v2_program_name("opencode2"));
assert!(is_opencode_v2_program_name("opencode-2"));
assert!(is_opencode_v2_program_name("opencode-v2"));
assert!(is_opencode_v2_program_name("/usr/local/bin/opencode2"));
assert!(is_opencode_v2_program_name(r"C:\Program Files\OpenCode\opencode2.exe"));
assert!(!is_opencode_v2_program_name("opencode"));
assert!(!is_opencode_v2_program_name("/usr/bin/opencode"));
}
#[test]
fn detects_unrecognized_pure_flag_error() {
assert!(is_unrecognized_pure_flag("Unrecognized flag: --pure in command opencode run"));
assert!(is_unrecognized_pure_flag("error: unknown flag: --pure"));
assert!(is_unrecognized_pure_flag("ERROR\n Unrecognized flag: --pure"));
assert!(!is_unrecognized_pure_flag("connection failed"));
assert!(!is_unrecognized_pure_flag("unknown model"));
}
#[test]
fn connection_check_uses_model_discovery_without_a_model_request() {
let command = resolved_models_command(&config("default")).unwrap();
@@ -877,6 +877,7 @@ mod tests {
pi_agent_cli_env: HashMap::new(),
opencode_cli_path: None,
opencode_cli_env: HashMap::new(),
opencode_cli_version: None,
cursor_cli_path: None,
cursor_cli_env: HashMap::new(),
grok_cli_path: None,
@@ -545,6 +545,7 @@ mod tests {
pi_agent_cli_env: Default::default(),
opencode_cli_path: None,
opencode_cli_env: Default::default(),
opencode_cli_version: None,
cursor_cli_path: None,
cursor_cli_env: Default::default(),
grok_cli_path: None,
@@ -3476,6 +3476,7 @@ mod tests {
pi_agent_cli_env: Default::default(),
opencode_cli_path: None,
opencode_cli_env: Default::default(),
opencode_cli_version: None,
cursor_cli_path: None,
cursor_cli_env: Default::default(),
grok_cli_path: None,
@@ -14229,6 +14229,7 @@ mod tests {
pi_agent_cli_env: std::collections::HashMap::new(),
opencode_cli_path: None,
opencode_cli_env: std::collections::HashMap::new(),
opencode_cli_version: None,
cursor_cli_path: None,
cursor_cli_env: std::collections::HashMap::new(),
grok_cli_path: None,
+2
View File
@@ -607,6 +607,7 @@ mod tests {
pi_agent_cli_env: Default::default(),
opencode_cli_path: None,
opencode_cli_env: Default::default(),
opencode_cli_version: None,
cursor_cli_path: None,
cursor_cli_env: Default::default(),
grok_cli_path: None,
@@ -716,6 +717,7 @@ mod tests {
pi_agent_cli_env: Default::default(),
opencode_cli_path: None,
opencode_cli_env: Default::default(),
opencode_cli_version: None,
cursor_cli_path: None,
cursor_cli_env: Default::default(),
grok_cli_path: None,
+2
View File
@@ -1141,9 +1141,11 @@ test("normalizes legacy AI config and fills provider defaults", () => {
provider: "opencode-cli",
opencodeCliPath: " /opt/homebrew/bin/opencode ",
opencodeCliEnv: { HTTPS_PROXY: "http://proxy:9800" },
opencodeCliVersion: " v2 ",
} as any);
assert.equal(openCode.opencodeCliPath, "/opt/homebrew/bin/opencode");
assert.deepEqual(openCode.opencodeCliEnv, { HTTPS_PROXY: "http://proxy:9800" });
assert.equal(openCode.opencodeCliVersion, "v2");
assert.equal(openCode.model, "default");
const grokCli = normalizeAiConfig({
provider: "grok-cli",
+17 -1
View File
@@ -482,7 +482,22 @@ fn resolve_cli_provider_config(mut config: AiConfig) -> AiConfig {
AiProvider::CodexCli => (&mut config.codex_cli_path, "codex"),
AiProvider::ClaudeCodeCli => (&mut config.claude_code_cli_path, "claude"),
AiProvider::PiAgentCli => (&mut config.pi_agent_cli_path, "pi"),
AiProvider::OpenCodeCli => (&mut config.opencode_cli_path, "opencode"),
AiProvider::OpenCodeCli => {
let default_cmd = if matches!(config.opencode_cli_version.as_deref(), Some("v2" | "2")) {
if super::mcp::locate_command("opencode2").is_some() {
"opencode2"
} else {
"opencode"
}
} else if super::mcp::locate_command("opencode").is_none()
&& super::mcp::locate_command("opencode2").is_some()
{
"opencode2"
} else {
"opencode"
};
(&mut config.opencode_cli_path, default_cmd)
}
AiProvider::CursorCli => (&mut config.cursor_cli_path, "agent"),
AiProvider::GrokCli => (&mut config.grok_cli_path, "grok"),
AiProvider::CodeBuddyCli => (&mut config.codebuddy_cli_path, "codebuddy"),
@@ -739,6 +754,7 @@ mod tests {
pi_agent_cli_env: Default::default(),
opencode_cli_path: None,
opencode_cli_env: Default::default(),
opencode_cli_version: None,
cursor_cli_path: None,
cursor_cli_env: Default::default(),
grok_cli_path: None,