diff --git a/apps/desktop/public/icons/ai/codebuddy.svg b/apps/desktop/public/icons/ai/codebuddy.svg
new file mode 100644
index 0000000000..446ed6713e
--- /dev/null
+++ b/apps/desktop/public/icons/ai/codebuddy.svg
@@ -0,0 +1 @@
+
diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue
index e465db37ba..060d73096e 100644
--- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue
+++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue
@@ -2640,7 +2640,7 @@ function normalizeMaxRetries(value: number | undefined): number {
const aiDeleteConfirmOpen = ref(false);
const aiDeleteConfigId = ref(null);
-const CLI_AI_PROVIDERS = new Set(["claude-code-cli", "codex-cli", "opencode-cli", "pi-agent-cli", "cursor-cli", "grok-cli"]);
+const CLI_AI_PROVIDERS = new Set(["claude-code-cli", "codex-cli", "opencode-cli", "pi-agent-cli", "cursor-cli", "grok-cli", "codebuddy-cli"]);
const OPENCODE_CONTROL_ENV = new Set(["OPENCODE_CONFIG", "OPENCODE_CONFIG_CONTENT", "OPENCODE_CONFIG_DIR", "OPENCODE_DB", "OPENCODE_PERMISSION", "OPENCODE_DISABLE_PROJECT_CONFIG"]);
const CURSOR_CONTROL_ENV = new Set(["CURSOR_CONFIG_DIR", "CURSOR_DATA_DIR"]);
const aiProviderOptions = computed(() => Object.values(AI_PROVIDER_PRESETS).filter((provider) => !isWeb || !CLI_AI_PROVIDERS.has(provider.provider)));
@@ -2670,6 +2670,8 @@ const aiEditCursorCliPath = ref("");
const aiEditCursorCliEnvRows = ref([]);
const aiEditGrokCliPath = ref("");
const aiEditGrokCliEnvRows = ref([]);
+const aiEditCodeBuddyCliPath = ref("");
+const aiEditCodeBuddyCliEnvRows = ref([]);
const aiAnthropicMessagesMode = computed(() => aiEditApiStyle.value === "anthropic-messages");
@@ -2703,6 +2705,7 @@ const aiIsPiAgentCli = computed(() => aiEditProvider.value === "pi-agent-cli");
const aiIsOpenCodeCli = computed(() => aiEditProvider.value === "opencode-cli");
const aiIsCursorCli = computed(() => aiEditProvider.value === "cursor-cli");
const aiIsGrokCli = computed(() => aiEditProvider.value === "grok-cli");
+const aiIsCodeBuddyCli = computed(() => aiEditProvider.value === "codebuddy-cli");
const aiIsCliProvider = computed(() => CLI_AI_PROVIDERS.has(aiEditProvider.value));
const aiCliProviderLabel = computed(() => selectedAiProviderPreset.value.label);
const aiCliCommandName = computed(() => {
@@ -2711,6 +2714,7 @@ const aiCliCommandName = computed(() => {
if (aiIsOpenCodeCli.value) return "opencode";
if (aiIsCursorCli.value) return "agent";
if (aiIsGrokCli.value) return "grok";
+ if (aiIsCodeBuddyCli.value) return "codebuddy";
return "codex";
});
const aiCliLoginCommand = computed(() => {
@@ -2719,6 +2723,7 @@ const aiCliLoginCommand = computed(() => {
if (aiIsOpenCodeCli.value) return "opencode auth login";
if (aiIsCursorCli.value) return "agent login";
if (aiIsGrokCli.value) return "grok login";
+ if (aiIsCodeBuddyCli.value) return "codebuddy";
return "codex login";
});
const aiEditCliPath = computed({
@@ -2728,6 +2733,7 @@ const aiEditCliPath = computed({
if (aiIsOpenCodeCli.value) return aiEditOpenCodeCliPath.value;
if (aiIsCursorCli.value) return aiEditCursorCliPath.value;
if (aiIsGrokCli.value) return aiEditGrokCliPath.value;
+ if (aiIsCodeBuddyCli.value) return aiEditCodeBuddyCliPath.value;
return aiEditCodexCliPath.value;
},
set: (value: string) => {
@@ -2741,6 +2747,8 @@ const aiEditCliPath = computed({
aiEditCursorCliPath.value = value;
} else if (aiIsGrokCli.value) {
aiEditGrokCliPath.value = value;
+ } else if (aiIsCodeBuddyCli.value) {
+ aiEditCodeBuddyCliPath.value = value;
} else {
aiEditCodexCliPath.value = value;
}
@@ -2752,6 +2760,7 @@ const aiEditCliEnvRows = computed(() => {
if (aiIsOpenCodeCli.value) return aiEditOpenCodeCliEnvRows.value;
if (aiIsCursorCli.value) return aiEditCursorCliEnvRows.value;
if (aiIsGrokCli.value) return aiEditGrokCliEnvRows.value;
+ if (aiIsCodeBuddyCli.value) return aiEditCodeBuddyCliEnvRows.value;
return aiEditCodexCliEnvRows.value;
});
watch(aiIsCliProvider, (isCliProvider) => {
@@ -2850,6 +2859,8 @@ function removeCliEnvRow(id: string) {
aiEditCursorCliEnvRows.value = aiEditCursorCliEnvRows.value.filter((row) => row.id !== id);
} else if (aiIsGrokCli.value) {
aiEditGrokCliEnvRows.value = aiEditGrokCliEnvRows.value.filter((row) => row.id !== id);
+ } else if (aiIsCodeBuddyCli.value) {
+ aiEditCodeBuddyCliEnvRows.value = aiEditCodeBuddyCliEnvRows.value.filter((row) => row.id !== id);
} else {
aiEditCodexCliEnvRows.value = aiEditCodexCliEnvRows.value.filter((row) => row.id !== id);
}
@@ -2884,6 +2895,8 @@ function currentAiEditConfig() {
cursorCliEnv: aiIsCursorCli.value ? cliEnvFromRows(aiEditCursorCliEnvRows.value) : {},
grokCliPath: aiEditGrokCliPath.value.trim() || undefined,
grokCliEnv: aiIsGrokCli.value ? cliEnvFromRows(aiEditGrokCliEnvRows.value) : {},
+ codebuddyCliPath: aiEditCodeBuddyCliPath.value.trim() || undefined,
+ codebuddyCliEnv: aiIsCodeBuddyCli.value ? cliEnvFromRows(aiEditCodeBuddyCliEnvRows.value) : {},
};
}
@@ -2959,6 +2972,8 @@ function aiEnterEditMode(configId?: string) {
aiEditCursorCliEnvRows.value = aiEnvRowsFromConfig(config.cursorCliEnv);
aiEditGrokCliPath.value = config.grokCliPath ?? "";
aiEditGrokCliEnvRows.value = aiEnvRowsFromConfig(config.grokCliEnv);
+ aiEditCodeBuddyCliPath.value = config.codebuddyCliPath ?? "";
+ aiEditCodeBuddyCliEnvRows.value = aiEnvRowsFromConfig(config.codebuddyCliEnv);
}
} else {
aiEditConfigName.value = "";
@@ -2986,6 +3001,8 @@ function aiEnterEditMode(configId?: string) {
aiEditCursorCliEnvRows.value = [];
aiEditGrokCliPath.value = "";
aiEditGrokCliEnvRows.value = [];
+ aiEditCodeBuddyCliPath.value = "";
+ aiEditCodeBuddyCliEnvRows.value = [];
}
}
diff --git a/apps/desktop/src/composables/__tests__/useAiModelCatalog.spec.ts b/apps/desktop/src/composables/__tests__/useAiModelCatalog.spec.ts
index 5f9a95a163..7482c56a7d 100644
--- a/apps/desktop/src/composables/__tests__/useAiModelCatalog.spec.ts
+++ b/apps/desktop/src/composables/__tests__/useAiModelCatalog.spec.ts
@@ -142,6 +142,30 @@ describe("useAiModelCatalog", () => {
expect(apiMock.aiListModels).toHaveBeenCalledTimes(2);
});
+ it("tracks CodeBuddy executable and environment changes without depending on environment key order", async () => {
+ const initial: AiConfigItem = {
+ ...config(),
+ provider: "codebuddy-cli",
+ endpoint: "",
+ apiKey: "",
+ model: "default",
+ codebuddyCliPath: "/opt/homebrew/bin/codebuddy",
+ codebuddyCliEnv: { HTTPS_PROXY: "http://127.0.0.1:7890", NO_PROXY: "localhost" },
+ };
+ apiMock.aiListModels.mockResolvedValueOnce([{ id: "first" }]).mockResolvedValueOnce([{ id: "second" }]);
+
+ await expect(catalog.loadModels(initial)).resolves.toEqual([{ id: "first" }]);
+ await expect(
+ catalog.loadModels({
+ ...initial,
+ codebuddyCliEnv: { NO_PROXY: "localhost", HTTPS_PROXY: "http://127.0.0.1:7890" },
+ }),
+ ).resolves.toEqual([{ id: "first" }]);
+ await expect(catalog.loadModels({ ...initial, codebuddyCliPath: "/usr/local/bin/codebuddy" })).resolves.toEqual([{ id: "second" }]);
+
+ expect(apiMock.aiListModels).toHaveBeenCalledTimes(2);
+ });
+
it("does not let a stale request overwrite a newer provider catalog", async () => {
let resolveInitial: ((models: { id: string }[]) => void) | undefined;
apiMock.aiListModels
diff --git a/apps/desktop/src/composables/useAiModelCatalog.ts b/apps/desktop/src/composables/useAiModelCatalog.ts
index 1db9ed6dae..3c4284b60c 100644
--- a/apps/desktop/src/composables/useAiModelCatalog.ts
+++ b/apps/desktop/src/composables/useAiModelCatalog.ts
@@ -59,6 +59,7 @@ function configSignature(config: AiConfigItem): string {
piAgentCliPath: config.piAgentCliPath ?? null,
opencodeCliPath: config.opencodeCliPath ?? null,
cursorCliPath: config.cursorCliPath ?? null,
+ codebuddyCliPath: config.codebuddyCliPath ?? null,
connectionFingerprint: fingerprint(
JSON.stringify({
apiKey: config.apiKey,
@@ -69,6 +70,7 @@ function configSignature(config: AiConfigItem): string {
piAgentCliEnv: sortedRecord(config.piAgentCliEnv),
opencodeCliEnv: sortedRecord(config.opencodeCliEnv),
cursorCliEnv: sortedRecord(config.cursorCliEnv),
+ codebuddyCliEnv: sortedRecord(config.codebuddyCliEnv),
}),
),
});
diff --git a/apps/desktop/src/i18n/backend-errors.ts b/apps/desktop/src/i18n/backend-errors.ts
index 340aff57a3..562490200b 100644
--- a/apps/desktop/src/i18n/backend-errors.ts
+++ b/apps/desktop/src/i18n/backend-errors.ts
@@ -52,6 +52,16 @@ const taggedAiCliErrorKeys: Record = {
grokCliMcpStartupFailed: "ai.cliErrors.grokCliMcpStartupFailed",
grokCliCommandLineTooLong: "ai.cliErrors.grokCliCommandLineTooLong",
grokCliRunFailed: "ai.cliErrors.grokCliRunFailed",
+ codeBuddyNotInstalled: "ai.cliErrors.codeBuddyNotInstalled",
+ codeBuddyCliPathInvalid: "ai.cliErrors.codeBuddyCliPathInvalid",
+ codeBuddyEnvInvalid: "ai.cliErrors.codeBuddyEnvInvalid",
+ codeBuddyEnvReserved: "ai.cliErrors.codeBuddyEnvReserved",
+ codeBuddyNotAuthenticated: "ai.cliErrors.codeBuddyNotAuthenticated",
+ codeBuddyTimeout: "ai.cliErrors.codeBuddyTimeout",
+ codeBuddyMcpConfigInvalid: "ai.cliErrors.codeBuddyMcpConfigInvalid",
+ codeBuddyMcpStartupFailed: "ai.cliErrors.codeBuddyMcpStartupFailed",
+ codeBuddyProtocolError: "ai.cliErrors.codeBuddyProtocolError",
+ codeBuddyRunFailed: "ai.cliErrors.codeBuddyRunFailed",
cursorEnvInvalid: "ai.cliErrors.cursorEnvInvalid",
cursorEnvReserved: "ai.cliErrors.cursorEnvReserved",
cursorNotAuthenticated: "ai.cliErrors.cursorNotAuthenticated",
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts
index 7b2488fe72..106d3fe117 100644
--- a/apps/desktop/src/i18n/locales/en.ts
+++ b/apps/desktop/src/i18n/locales/en.ts
@@ -2362,6 +2362,16 @@ export default {
grokCliMcpStartupFailed: "Grok loaded the MCP configuration but could not start DBX MCP Server. Check Settings > MCP and the diagnostics below.",
grokCliCommandLineTooLong: "Windows rejected the Grok CLI command because it was too long. Update DBX and retry using the generated prompt file.",
grokCliRunFailed: "Grok CLI exited unexpectedly. Use the error code and diagnostics below to identify the failing executable or CLI output.",
+ codeBuddyNotInstalled: "CodeBuddy Code CLI was not found. Install CodeBuddy Code or set its executable path in Settings > AI.",
+ codeBuddyCliPathInvalid: "The CodeBuddy Code CLI path is invalid. Select only the codebuddy executable and configure environment variables separately.",
+ codeBuddyEnvInvalid: "A CodeBuddy environment variable name is invalid. Use names such as HTTPS_PROXY.",
+ codeBuddyEnvReserved: "A DBX-managed MCP environment variable was overridden. Remove DBX_MCP_* variables from the provider configuration.",
+ codeBuddyNotAuthenticated: "CodeBuddy Code is not authenticated. Open `codebuddy` in a terminal, sign in, and try again.",
+ codeBuddyTimeout: "CodeBuddy Code CLI did not respond before the operation timed out.",
+ codeBuddyMcpConfigInvalid: "CodeBuddy Code could not read the temporary DBX MCP configuration. Check the diagnostics below.",
+ codeBuddyMcpStartupFailed: "CodeBuddy Code loaded the MCP configuration but could not start DBX MCP Server. Check Settings > MCP and the diagnostics below.",
+ codeBuddyProtocolError: "CodeBuddy Code returned an invalid control response or JSON event stream. Check the CodeBuddy Code version and diagnostics below.",
+ codeBuddyRunFailed: "CodeBuddy Code CLI exited unexpectedly. Use the error code and diagnostics below to identify the failing executable or CLI output.",
cursorEnvInvalid: "A Cursor CLI environment variable name is invalid. Use names such as HTTPS_PROXY.",
cursorEnvReserved: "A Cursor or DBX-managed environment variable was overridden. Remove CURSOR_CONFIG_DIR, CURSOR_DATA_DIR, and DBX_MCP_* variables from the provider configuration.",
cursorNotAuthenticated: "Cursor CLI is not authenticated. Run `agent login` in a terminal and try again.",
diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts
index a9e3fb4871..c95892e955 100644
--- a/apps/desktop/src/i18n/locales/es.ts
+++ b/apps/desktop/src/i18n/locales/es.ts
@@ -2208,6 +2208,16 @@ export default withEnglishFallback({
grokCliMcpStartupFailed: "Grok cargó la configuración MCP pero no pudo iniciar DBX MCP Server. Revisa Ajustes > MCP y los diagnósticos siguientes.",
grokCliCommandLineTooLong: "Windows rechazó el comando de Grok CLI porque era demasiado largo. Actualiza DBX y vuelve a intentarlo con el archivo de prompt generado.",
grokCliRunFailed: "Grok CLI terminó de forma inesperada. Usa el código de error y los diagnósticos siguientes para identificar el ejecutable o la salida fallida.",
+ codeBuddyNotInstalled: "No se encontró CodeBuddy Code CLI. Instala CodeBuddy Code o configura la ruta del ejecutable en Ajustes > AI.",
+ codeBuddyCliPathInvalid: "La ruta de CodeBuddy Code CLI no es válida. Selecciona solo el ejecutable codebuddy y configura las variables de entorno por separado.",
+ codeBuddyEnvInvalid: "El nombre de una variable de entorno de CodeBuddy no es válido. Usa nombres como HTTPS_PROXY.",
+ codeBuddyEnvReserved: "Se sobrescribió una variable MCP gestionada por DBX. Elimina las variables DBX_MCP_* de la configuración del proveedor.",
+ codeBuddyNotAuthenticated: "CodeBuddy Code no está autenticado. Abre `codebuddy` en una terminal, inicia sesión y vuelve a intentarlo.",
+ codeBuddyTimeout: "CodeBuddy Code CLI no respondió antes de que se agotara el tiempo de la operación.",
+ codeBuddyMcpConfigInvalid: "CodeBuddy Code no pudo leer la configuración temporal de DBX MCP. Revisa los diagnósticos siguientes.",
+ codeBuddyMcpStartupFailed: "CodeBuddy Code cargó la configuración MCP pero no pudo iniciar DBX MCP Server. Revisa Ajustes > MCP y los diagnósticos siguientes.",
+ codeBuddyProtocolError: "CodeBuddy Code devolvió una respuesta de control o un flujo de eventos JSON no válido. Revisa la versión de CodeBuddy Code y los diagnósticos siguientes.",
+ codeBuddyRunFailed: "CodeBuddy Code CLI terminó de forma inesperada. Usa el código de error y los diagnósticos siguientes para identificar el ejecutable o la salida fallida.",
},
run: "Ejecutar",
readingSchema: "Leyendo esquema",
diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts
index b753546b8f..1857862c1c 100644
--- a/apps/desktop/src/i18n/locales/it.ts
+++ b/apps/desktop/src/i18n/locales/it.ts
@@ -2348,6 +2348,16 @@ export default withEnglishFallback({
grokCliMcpStartupFailed: "Grok ha caricato la configurazione MCP ma non ha potuto avviare DBX MCP Server. Controlla Impostazioni > MCP e i dettagli seguenti.",
grokCliCommandLineTooLong: "Windows ha rifiutato il comando Grok CLI perché troppo lungo. Aggiorna DBX e riprova usando il file di prompt generato.",
grokCliRunFailed: "Grok CLI è terminato in modo imprevisto. Usa il codice errore e i dettagli seguenti per identificare l'eseguibile o l'output non riuscito.",
+ codeBuddyNotInstalled: "CodeBuddy Code CLI non è stato trovato. Installa CodeBuddy Code o imposta il percorso dell'eseguibile in Impostazioni > AI.",
+ codeBuddyCliPathInvalid: "Il percorso di CodeBuddy Code CLI non è valido. Seleziona solo l'eseguibile codebuddy e configura separatamente le variabili d'ambiente.",
+ codeBuddyEnvInvalid: "Il nome di una variabile d'ambiente di CodeBuddy non è valido. Usa nomi come HTTPS_PROXY.",
+ codeBuddyEnvReserved: "È stata sovrascritta una variabile MCP gestita da DBX. Rimuovi le variabili DBX_MCP_* dalla configurazione del provider.",
+ codeBuddyNotAuthenticated: "CodeBuddy Code non è autenticato. Apri `codebuddy` in un terminale, accedi e riprova.",
+ codeBuddyTimeout: "CodeBuddy Code CLI non ha risposto prima della scadenza dell'operazione.",
+ codeBuddyMcpConfigInvalid: "CodeBuddy Code non ha potuto leggere la configurazione temporanea di DBX MCP. Controlla i dettagli seguenti.",
+ codeBuddyMcpStartupFailed: "CodeBuddy Code ha caricato la configurazione MCP ma non ha potuto avviare DBX MCP Server. Controlla Impostazioni > MCP e i dettagli seguenti.",
+ codeBuddyProtocolError: "CodeBuddy Code ha restituito una risposta di controllo o un flusso di eventi JSON non valido. Controlla la versione di CodeBuddy Code e i dettagli seguenti.",
+ codeBuddyRunFailed: "CodeBuddy Code CLI è terminato in modo imprevisto. Usa il codice errore e i dettagli seguenti per identificare l'eseguibile o l'output non riuscito.",
},
actions: {
general: "Generale",
diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts
index 0a95ebc61a..1ad959a235 100644
--- a/apps/desktop/src/i18n/locales/ja.ts
+++ b/apps/desktop/src/i18n/locales/ja.ts
@@ -2242,6 +2242,16 @@ export default withEnglishFallback({
grokCliMcpStartupFailed: "GrokはMCP設定を読み込みましたが、DBX MCP Serverを起動できません。設定 > MCPと以下の診断詳細を確認してください。",
grokCliCommandLineTooLong: "Windowsが長すぎるGrok CLIコマンドを拒否しました。DBXを更新し、生成されたプロンプトファイルで再試行してください。",
grokCliRunFailed: "Grok CLIが予期せず終了しました。エラーコードと以下の診断詳細から、失敗した実行ファイルまたはCLI出力を確認してください。",
+ codeBuddyNotInstalled: "CodeBuddy Code CLIが見つかりません。CodeBuddy Codeをインストールするか、設定 > AIで実行ファイルのパスを指定してください。",
+ codeBuddyCliPathInvalid: "CodeBuddy Code CLIのパスが無効です。codebuddy実行ファイルのみを選択し、環境変数は別に設定してください。",
+ codeBuddyEnvInvalid: "CodeBuddyの環境変数名が無効です。HTTPS_PROXYのような有効な名前を使用してください。",
+ codeBuddyEnvReserved: "DBXが管理するMCP環境変数が上書きされています。プロバイダー設定からDBX_MCP_*変数を削除してください。",
+ codeBuddyNotAuthenticated: "CodeBuddy Codeにログインしていません。ターミナルで`codebuddy`を開いてログインし、再試行してください。",
+ codeBuddyTimeout: "CodeBuddy Code CLIは操作のタイムアウト前に応答しませんでした。",
+ codeBuddyMcpConfigInvalid: "CodeBuddy Codeは一時的なDBX MCP設定を読み込めませんでした。以下の診断情報を確認してください。",
+ codeBuddyMcpStartupFailed: "CodeBuddy CodeはMCP設定を読み込みましたが、DBX MCP Serverを起動できません。設定 > MCPと以下の診断情報を確認してください。",
+ codeBuddyProtocolError: "CodeBuddy Codeが無効な制御応答またはJSONイベントストリームを返しました。CodeBuddy Codeのバージョンと以下の診断情報を確認してください。",
+ codeBuddyRunFailed: "CodeBuddy Code CLIが予期せず終了しました。エラーコードと以下の診断詳細から、失敗した実行ファイルまたはCLI出力を確認してください。",
},
run: "実行",
readingSchema: "スキーマを読み取り中",
diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts
index 36d15dac20..62a990d3e5 100644
--- a/apps/desktop/src/i18n/locales/ko.ts
+++ b/apps/desktop/src/i18n/locales/ko.ts
@@ -2226,6 +2226,16 @@ export default withEnglishFallback({
grokCliMcpStartupFailed: "Grok이 MCP 구성을 불러왔지만 DBX MCP 서버를 시작할 수 없습니다. 설정 > MCP와 아래 진단을 확인하세요.",
grokCliCommandLineTooLong: "명령이 너무 길어 Windows가 Grok CLI 명령을 거부했습니다. DBX를 업데이트하고 생성된 프롬프트 파일을 사용하여 다시 시도하세요.",
grokCliRunFailed: "Grok CLI가 예기치 않게 종료되었습니다. 아래의 오류 코드와 진단을 사용하여 실패한 실행 파일이나 CLI 출력을 파악하세요.",
+ codeBuddyNotInstalled: "CodeBuddy Code CLI를 찾을 수 없습니다. CodeBuddy Code를 설치하거나 설정 > AI에서 실행 파일 경로를 지정하세요.",
+ codeBuddyCliPathInvalid: "CodeBuddy Code CLI 경로가 잘못되었습니다. codebuddy 실행 파일만 선택하고 환경 변수는 별도로 구성하세요.",
+ codeBuddyEnvInvalid: "CodeBuddy 환경 변수 이름이 잘못되었습니다. HTTPS_PROXY 같은 유효한 이름을 사용하세요.",
+ codeBuddyEnvReserved: "DBX가 관리하는 MCP 환경 변수가 재정의되었습니다. 공급자 구성에서 DBX_MCP_* 변수를 제거하세요.",
+ codeBuddyNotAuthenticated: "CodeBuddy Code에 로그인되어 있지 않습니다. 터미널에서 `codebuddy`를 열어 로그인한 후 다시 시도하세요.",
+ codeBuddyTimeout: "CodeBuddy Code CLI가 작업 제한 시간 전에 응답하지 않았습니다.",
+ codeBuddyMcpConfigInvalid: "CodeBuddy Code가 임시 DBX MCP 구성을 읽지 못했습니다. 아래 진단을 확인하세요.",
+ codeBuddyMcpStartupFailed: "CodeBuddy Code가 MCP 구성을 불러왔지만 DBX MCP Server를 시작하지 못했습니다. 설정 > MCP와 아래 진단을 확인하세요.",
+ codeBuddyProtocolError: "CodeBuddy Code가 잘못된 제어 응답 또는 JSON 이벤트 스트림을 반환했습니다. CodeBuddy Code 버전과 아래 진단을 확인하세요.",
+ codeBuddyRunFailed: "CodeBuddy Code CLI가 예기치 않게 종료되었습니다. 아래의 오류 코드와 진단을 사용하여 실패한 실행 파일이나 CLI 출력을 파악하세요.",
},
actions: {
general: "일반",
diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts
index 07f6550ae5..c40a0aff44 100644
--- a/apps/desktop/src/i18n/locales/pt-BR.ts
+++ b/apps/desktop/src/i18n/locales/pt-BR.ts
@@ -2210,6 +2210,16 @@ export default withEnglishFallback({
grokCliMcpStartupFailed: "O Grok carregou a configuração MCP, mas não conseguiu iniciar o DBX MCP Server. Verifique Configurações > MCP e os detalhes abaixo.",
grokCliCommandLineTooLong: "O Windows rejeitou o comando do Grok CLI porque era muito longo. Atualize o DBX e tente novamente com o arquivo de prompt gerado.",
grokCliRunFailed: "Grok CLI foi encerrado inesperadamente. Use o código do erro e os detalhes abaixo para identificar o executável ou a saída com falha.",
+ codeBuddyNotInstalled: "O CodeBuddy Code CLI não foi encontrado. Instale o CodeBuddy Code ou defina o caminho do executável em Configurações > AI.",
+ codeBuddyCliPathInvalid: "O caminho do CodeBuddy Code CLI é inválido. Selecione apenas o executável codebuddy e configure as variáveis de ambiente separadamente.",
+ codeBuddyEnvInvalid: "O nome de uma variável de ambiente do CodeBuddy é inválido. Use nomes como HTTPS_PROXY.",
+ codeBuddyEnvReserved: "Uma variável MCP gerenciada pelo DBX foi sobrescrita. Remova as variáveis DBX_MCP_* da configuração do provedor.",
+ codeBuddyNotAuthenticated: "O CodeBuddy Code não está autenticado. Abra `codebuddy` em um terminal, entre na conta e tente novamente.",
+ codeBuddyTimeout: "O CodeBuddy Code CLI não respondeu antes do tempo limite da operação.",
+ codeBuddyMcpConfigInvalid: "O CodeBuddy Code não conseguiu ler a configuração temporária do DBX MCP. Verifique os detalhes abaixo.",
+ codeBuddyMcpStartupFailed: "O CodeBuddy Code carregou a configuração MCP, mas não conseguiu iniciar o DBX MCP Server. Verifique Configurações > MCP e os detalhes abaixo.",
+ codeBuddyProtocolError: "O CodeBuddy Code retornou uma resposta de controle ou um fluxo de eventos JSON inválido. Verifique a versão do CodeBuddy Code e os detalhes abaixo.",
+ codeBuddyRunFailed: "O CodeBuddy Code CLI foi encerrado inesperadamente. Use o código do erro e os detalhes abaixo para identificar o executável ou a saída com falha.",
},
run: "Executar",
readingSchema: "Lendo schema",
diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts
index da4f89aae9..a8531b3393 100644
--- a/apps/desktop/src/i18n/locales/zh-CN.ts
+++ b/apps/desktop/src/i18n/locales/zh-CN.ts
@@ -2287,6 +2287,16 @@ export default withEnglishFallback({
grokCliMcpStartupFailed: "Grok 已加载 MCP 配置,但无法启动 DBX MCP Server。请检查 设置 > MCP 与下方诊断信息。",
grokCliCommandLineTooLong: "命令行过长,Windows 拒绝执行 Grok CLI。请更新 DBX 并使用生成的 prompt 文件重试。",
grokCliRunFailed: "Grok CLI 异常退出。请根据错误代码和下方诊断详情确认失败的可执行文件或 CLI 输出。",
+ codeBuddyNotInstalled: "未找到 CodeBuddy Code CLI。请安装 CodeBuddy Code,或在 设置 > AI 中指定可执行文件路径。",
+ codeBuddyCliPathInvalid: "CodeBuddy Code CLI 路径无效。请只选择 codebuddy 可执行文件,并单独配置环境变量。",
+ codeBuddyEnvInvalid: "CodeBuddy 环境变量名称无效。请使用类似 HTTPS_PROXY 的名称。",
+ codeBuddyEnvReserved: "覆盖了 DBX 管理的 MCP 环境变量。请从提供商配置中移除 DBX_MCP_* 变量。",
+ codeBuddyNotAuthenticated: "CodeBuddy Code 尚未登录。请在终端打开 `codebuddy`,完成登录后重试。",
+ codeBuddyTimeout: "CodeBuddy Code CLI 未在操作超时前响应。",
+ codeBuddyMcpConfigInvalid: "CodeBuddy Code 无法读取临时 DBX MCP 配置。请检查下方诊断信息。",
+ codeBuddyMcpStartupFailed: "CodeBuddy Code 已加载 MCP 配置,但无法启动 DBX MCP Server。请检查 设置 > MCP 与下方诊断信息。",
+ codeBuddyProtocolError: "CodeBuddy Code 返回了无效的控制响应或 JSON 事件流。请检查 CodeBuddy Code 版本和下方诊断信息。",
+ codeBuddyRunFailed: "CodeBuddy Code CLI 异常退出。请根据错误代码和下方诊断详情确认失败的可执行文件或 CLI 输出。",
cursorEnvInvalid: "Cursor CLI 环境变量名称无效。请使用 HTTPS_PROXY 这类合法名称。",
cursorEnvReserved: "配置覆盖了由 Cursor 或 DBX 管理的环境变量。请移除 CURSOR_CONFIG_DIR、CURSOR_DATA_DIR 和 DBX_MCP_* 变量。",
cursorNotAuthenticated: "Cursor CLI 尚未登录。请在终端执行 `agent login` 后重试。",
diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts
index b2e6b9859a..d10695f9eb 100644
--- a/apps/desktop/src/i18n/locales/zh-TW.ts
+++ b/apps/desktop/src/i18n/locales/zh-TW.ts
@@ -2351,6 +2351,16 @@ export default withEnglishFallback({
grokCliMcpStartupFailed: "Grok 已載入 MCP 設定,但無法啟動 DBX MCP Server。請檢查 設定 > MCP 與下方診斷資訊。",
grokCliCommandLineTooLong: "命令列過長,Windows 拒絕執行 Grok CLI。請更新 DBX 並使用產生的 prompt 檔案重試。",
grokCliRunFailed: "Grok CLI 異常結束。請依據錯誤代碼和下方診斷詳情確認失敗的可執行檔或 CLI 輸出。",
+ codeBuddyNotInstalled: "找不到 CodeBuddy Code CLI。請安裝 CodeBuddy Code,或在 設定 > AI 中指定執行檔路徑。",
+ codeBuddyCliPathInvalid: "CodeBuddy Code CLI 路徑無效。請只選擇 codebuddy 執行檔,並另外設定環境變數。",
+ codeBuddyEnvInvalid: "CodeBuddy 環境變數名稱無效。請使用類似 HTTPS_PROXY 的名稱。",
+ codeBuddyEnvReserved: "覆寫了 DBX 管理的 MCP 環境變數。請從供應商設定中移除 DBX_MCP_* 變數。",
+ codeBuddyNotAuthenticated: "CodeBuddy Code 尚未登入。請在終端機開啟 `codebuddy`,完成登入後重試。",
+ codeBuddyTimeout: "CodeBuddy Code CLI 未在操作逾時前回應。",
+ codeBuddyMcpConfigInvalid: "CodeBuddy Code 無法讀取暫存 DBX MCP 設定。請檢查下方診斷資訊。",
+ codeBuddyMcpStartupFailed: "CodeBuddy Code 已載入 MCP 設定,但無法啟動 DBX MCP Server。請檢查 設定 > MCP 與下方診斷資訊。",
+ codeBuddyProtocolError: "CodeBuddy Code 回傳了無效的控制回應或 JSON 事件串流。請檢查 CodeBuddy Code 版本和下方診斷資訊。",
+ codeBuddyRunFailed: "CodeBuddy Code CLI 異常結束。請依據錯誤代碼和下方診斷詳情確認失敗的執行檔或 CLI 輸出。",
},
actions: {
general: "通用問答",
diff --git a/apps/desktop/src/lib/__tests__/ai/aiConfigCandidates.spec.ts b/apps/desktop/src/lib/__tests__/ai/aiConfigCandidates.spec.ts
index 34947d95d8..96b2f8c672 100644
--- a/apps/desktop/src/lib/__tests__/ai/aiConfigCandidates.spec.ts
+++ b/apps/desktop/src/lib/__tests__/ai/aiConfigCandidates.spec.ts
@@ -24,7 +24,7 @@ describe("isAiConfigModelCandidate", () => {
expect(isAiConfigModelCandidate(config({ apiKey: "" }), true)).toBe(false);
});
- it.each(["codex-cli", "claude-code-cli", "opencode-cli", "pi-agent-cli", "cursor-cli", "grok-cli"] as const)("keeps %s configs eligible without endpoint, API key, or model metadata", (provider) => {
+ it.each(["codex-cli", "claude-code-cli", "opencode-cli", "pi-agent-cli", "cursor-cli", "grok-cli", "codebuddy-cli"] as const)("keeps %s configs eligible without endpoint, API key, or model metadata", (provider) => {
expect(
isAiConfigModelCandidate(
config({
diff --git a/apps/desktop/src/lib/ai/__tests__/aiConfigOrdering.spec.ts b/apps/desktop/src/lib/ai/__tests__/aiConfigOrdering.spec.ts
index 36a0a7e3c3..5dccf21ccd 100644
--- a/apps/desktop/src/lib/ai/__tests__/aiConfigOrdering.spec.ts
+++ b/apps/desktop/src/lib/ai/__tests__/aiConfigOrdering.spec.ts
@@ -23,12 +23,13 @@ describe("orderAiConfigsForDisplay", () => {
{ id: "codex", provider: "codex-cli" },
{ id: "opencode", provider: "opencode-cli" },
{ id: "cursor", provider: "cursor-cli" },
+ { id: "codebuddy", provider: "codebuddy-cli" },
{ id: "grok", provider: "grok-cli" },
{ id: "pi", provider: "pi-agent-cli" },
{ id: "custom", provider: "custom" },
];
- expect(orderAiConfigsForDisplay(configs).map((config) => config.id)).toEqual(["claude", "openai", "gemini", "deepseek", "qwen", "minimax", "ollama", "anthropic-compatible", "openai-compatible", "claude-code-1", "codex", "opencode", "cursor", "grok", "pi", "custom"]);
+ expect(orderAiConfigsForDisplay(configs).map((config) => config.id)).toEqual(["claude", "openai", "gemini", "deepseek", "qwen", "minimax", "ollama", "anthropic-compatible", "openai-compatible", "claude-code-1", "codex", "opencode", "cursor", "codebuddy", "grok", "pi", "custom"]);
});
it("preserves creation order for configs from the same provider", () => {
diff --git a/apps/desktop/src/lib/ai/aiConfigCandidates.ts b/apps/desktop/src/lib/ai/aiConfigCandidates.ts
index c1af7bbdeb..2786e50278 100644
--- a/apps/desktop/src/lib/ai/aiConfigCandidates.ts
+++ b/apps/desktop/src/lib/ai/aiConfigCandidates.ts
@@ -1,6 +1,6 @@
import type { AiConfig } from "@/types/ai";
-const CLI_PROVIDERS = new Set(["codex-cli", "claude-code-cli", "opencode-cli", "pi-agent-cli", "cursor-cli", "grok-cli"]);
+const CLI_PROVIDERS = new Set(["codex-cli", "claude-code-cli", "opencode-cli", "pi-agent-cli", "cursor-cli", "grok-cli", "codebuddy-cli"]);
export function isAiConfigModelCandidate(config: AiConfig, requiresApiKey: boolean): boolean {
// CLI providers resolve their model and credentials externally, so keep the existing eligibility bypass.
diff --git a/apps/desktop/src/stores/__tests__/settingsStore.spec.ts b/apps/desktop/src/stores/__tests__/settingsStore.spec.ts
index 459850a7b2..9d135e847e 100644
--- a/apps/desktop/src/stores/__tests__/settingsStore.spec.ts
+++ b/apps/desktop/src/stores/__tests__/settingsStore.spec.ts
@@ -488,6 +488,22 @@ describe("settingsStore AI API key normalization", () => {
cursorCliEnv: { HTTPS_PROXY: "http://127.0.0.1:7890", EMPTY: "" },
});
});
+
+ it("normalizes CodeBuddy CLI path and environment settings", () => {
+ expect(
+ normalizeAiConfig({
+ provider: "codebuddy-cli",
+ codebuddyCliPath: " ~/.local/bin/codebuddy ",
+ codebuddyCliEnv: { HTTPS_PROXY: "http://127.0.0.1:7890", EMPTY: null as unknown as string },
+ }),
+ ).toMatchObject({
+ provider: "codebuddy-cli",
+ endpoint: "",
+ model: "default",
+ codebuddyCliPath: "~/.local/bin/codebuddy",
+ codebuddyCliEnv: { HTTPS_PROXY: "http://127.0.0.1:7890", EMPTY: "" },
+ });
+ });
});
describe("settingsStore MCP policy persistence", () => {
diff --git a/apps/desktop/src/stores/settingsStore.ts b/apps/desktop/src/stores/settingsStore.ts
index 1ffe71eb10..b02cd7603a 100644
--- a/apps/desktop/src/stores/settingsStore.ts
+++ b/apps/desktop/src/stores/settingsStore.ts
@@ -253,6 +253,16 @@ export const AI_PROVIDER_PRESETS: Record = {
authMethod: "bearer",
requiresApiKey: false,
},
+ "codebuddy-cli": {
+ label: "CodeBuddy Code",
+ iconSlug: "codebuddy",
+ provider: "codebuddy-cli",
+ endpoint: "",
+ model: "default",
+ apiStyle: "completions",
+ authMethod: "bearer",
+ requiresApiKey: false,
+ },
"grok-cli": {
label: "Grok CLI",
iconSlug: "grok",
@@ -335,6 +345,8 @@ export function normalizeAiConfig(config: Partial | null | undefined):
cursorCliEnv: normalizeAiEnv(config?.cursorCliEnv),
grokCliPath: config?.grokCliPath?.trim() || undefined,
grokCliEnv: normalizeAiEnv(config?.grokCliEnv),
+ codebuddyCliPath: config?.codebuddyCliPath?.trim() || undefined,
+ codebuddyCliEnv: normalizeAiEnv(config?.codebuddyCliEnv),
};
}
@@ -1497,7 +1509,7 @@ export const useSettingsStore = defineStore("settings", () => {
const config = aiConfigs.value.find((c) => c.id === activeModel.value!.configId);
if (!config) return false;
const preset = AI_PROVIDER_PRESETS[config.provider];
- if (config.provider === "codex-cli" || config.provider === "claude-code-cli" || config.provider === "pi-agent-cli" || config.provider === "opencode-cli" || config.provider === "cursor-cli" || config.provider === "grok-cli") return true;
+ if (config.provider === "codex-cli" || config.provider === "claude-code-cli" || config.provider === "pi-agent-cli" || config.provider === "opencode-cli" || config.provider === "cursor-cli" || config.provider === "grok-cli" || config.provider === "codebuddy-cli") return true;
return !!config.endpoint && !!activeModel.value!.modelId && (!preset.requiresApiKey || !!config.apiKey);
});
diff --git a/apps/desktop/src/types/ai.ts b/apps/desktop/src/types/ai.ts
index ff5e0f3d82..bfc65b93f6 100644
--- a/apps/desktop/src/types/ai.ts
+++ b/apps/desktop/src/types/ai.ts
@@ -1,4 +1,4 @@
-export type AiProvider = "claude" | "openai" | "gemini" | "deepseek" | "qwen" | "minimax" | "ollama" | "anthropic-compatible" | "openai-compatible" | "claude-code-cli" | "pi-agent-cli" | "codex-cli" | "opencode-cli" | "cursor-cli" | "grok-cli" | "custom";
+export type AiProvider = "claude" | "openai" | "gemini" | "deepseek" | "qwen" | "minimax" | "ollama" | "anthropic-compatible" | "openai-compatible" | "claude-code-cli" | "pi-agent-cli" | "codex-cli" | "opencode-cli" | "cursor-cli" | "grok-cli" | "codebuddy-cli" | "custom";
export type AiApiStyle = "completions" | "responses" | "anthropic-messages";
export type AiAuthMethod = "api-key" | "bearer";
export type AiEffortLevel = "low" | "medium" | "high" | "xhigh" | "max";
@@ -53,6 +53,8 @@ export interface AiConfig {
cursorCliEnv?: Record;
grokCliPath?: string | null;
grokCliEnv?: Record;
+ codebuddyCliPath?: string | null;
+ codebuddyCliEnv?: Record;
runtimeEffort?: AiEffortSelection | null;
}
diff --git a/crates/dbx-core/src/agent_loop.rs b/crates/dbx-core/src/agent_loop.rs
index 96ffdcf4f1..f67ba70bbb 100644
--- a/crates/dbx-core/src/agent_loop.rs
+++ b/crates/dbx-core/src/agent_loop.rs
@@ -196,6 +196,14 @@ pub async fn run_agent_loop(
crate::ai_grok_cli::build_grok_prompt(system_prompt, messages, agent_ctx.sql_permissions.allow_writes);
return crate::ai_grok_cli::run_grok_agent(config, &prompt, options, cancelled, on_event).await;
}
+ if matches!(config.provider, AiProvider::CodeBuddyCli) {
+ let prompt = crate::ai_codebuddy_cli::build_codebuddy_prompt(
+ system_prompt,
+ messages,
+ agent_ctx.sql_permissions.allow_writes,
+ );
+ return crate::ai_codebuddy_cli::run_codebuddy_agent(config, &prompt, options, cancelled, on_event).await;
+ }
let prompt =
crate::ai_codex_cli::build_codex_prompt(system_prompt, messages, agent_ctx.sql_permissions.allow_writes);
return crate::ai_codex_cli::run_codex_agent(config, &prompt, options, cancelled, on_event).await;
diff --git a/crates/dbx-core/src/ai.rs b/crates/dbx-core/src/ai.rs
index cace8ba0ab..9da61901b3 100644
--- a/crates/dbx-core/src/ai.rs
+++ b/crates/dbx-core/src/ai.rs
@@ -85,6 +85,8 @@ pub enum AiProvider {
CursorCli,
#[serde(rename = "grok-cli")]
GrokCli,
+ #[serde(rename = "codebuddy-cli")]
+ CodeBuddyCli,
Custom,
}
@@ -105,6 +107,7 @@ impl AiProvider {
AiProvider::OpenCodeCli => "opencode-cli",
AiProvider::CursorCli => "cursor-cli",
AiProvider::GrokCli => "grok-cli",
+ AiProvider::CodeBuddyCli => "codebuddy-cli",
AiProvider::CodexCli => "codex-cli",
AiProvider::Custom => "custom",
}
@@ -408,6 +411,10 @@ pub struct AiConfig {
pub grok_cli_path: Option,
#[serde(default)]
pub grok_cli_env: HashMap,
+ #[serde(default)]
+ pub codebuddy_cli_path: Option,
+ #[serde(default)]
+ pub codebuddy_cli_env: HashMap,
}
fn default_enable_thinking() -> bool {
@@ -426,6 +433,7 @@ pub fn is_cli_provider(provider: &AiProvider) -> bool {
| AiProvider::OpenCodeCli
| AiProvider::CursorCli
| AiProvider::GrokCli
+ | AiProvider::CodeBuddyCli
)
}
@@ -658,6 +666,7 @@ pub fn resolve_endpoint(config: &AiConfig) -> String {
| AiProvider::OpenCodeCli
| AiProvider::CursorCli
| AiProvider::GrokCli
+ | AiProvider::CodeBuddyCli
| AiProvider::Gemini => unreachable!(),
}
}
@@ -1508,6 +1517,7 @@ pub async fn list_models_core(config: &AiConfig) -> Result, Str
AiProvider::OpenCodeCli => crate::ai_opencode_cli::list_opencode_models(config).await?,
AiProvider::CursorCli => crate::ai_cursor_cli::list_cursor_models(config).await?,
AiProvider::GrokCli => crate::ai_grok_cli::list_grok_models(config).await?,
+ AiProvider::CodeBuddyCli => crate::ai_codebuddy_cli::list_codebuddy_models(config).await?,
_ => {
validate_model_list_config(config)?;
let client = build_ai_http_client(config, 30)?;
@@ -1535,7 +1545,8 @@ pub async fn list_models_core(config: &AiConfig) -> Result, Str
| AiProvider::PiAgentCli
| AiProvider::OpenCodeCli
| AiProvider::CursorCli
- | AiProvider::GrokCli => {
+ | AiProvider::GrokCli
+ | AiProvider::CodeBuddyCli => {
unreachable!()
}
}
@@ -1560,6 +1571,10 @@ pub async fn resolve_model_effort_core(config: &AiConfig, model_id: &str) -> Res
return crate::ai_opencode_cli::resolve_opencode_model_effort(config, model_id).await;
}
+ if matches!(config.provider, AiProvider::CodeBuddyCli) {
+ return crate::ai_codebuddy_cli::resolve_codebuddy_model_effort(config, model_id).await;
+ }
+
if matches!(config.provider, AiProvider::CursorCli) {
return Ok(AiEffortCapability::Unsupported);
}
@@ -2102,6 +2117,9 @@ pub async fn test_connection_core(config: &AiConfig) -> Result Result {
| AiProvider::PiAgentCli
| AiProvider::OpenCodeCli
| AiProvider::CursorCli
- | AiProvider::GrokCli => {
+ | AiProvider::GrokCli
+ | AiProvider::CodeBuddyCli => {
unreachable!()
}
AiProvider::Openai
@@ -2538,7 +2557,8 @@ pub async fn stream(
| AiProvider::PiAgentCli
| AiProvider::OpenCodeCli
| AiProvider::CursorCli
- | AiProvider::GrokCli => {
+ | AiProvider::GrokCli
+ | AiProvider::CodeBuddyCli => {
unreachable!()
}
AiProvider::Openai
@@ -4200,6 +4220,8 @@ mod tests {
cursor_cli_env: Default::default(),
grok_cli_path: None,
grok_cli_env: Default::default(),
+ codebuddy_cli_path: None,
+ codebuddy_cli_env: Default::default(),
},
system_prompt: "Be concise.".to_string(),
messages: vec![AiMessage {
@@ -4785,6 +4807,8 @@ mod tests {
assert!(config.opencode_cli_env.is_empty());
assert!(config.cursor_cli_path.is_none());
assert!(config.cursor_cli_env.is_empty());
+ assert!(config.codebuddy_cli_path.is_none());
+ assert!(config.codebuddy_cli_env.is_empty());
}
#[test]
@@ -4816,6 +4840,8 @@ mod tests {
cursor_cli_env: Default::default(),
grok_cli_path: None,
grok_cli_env: Default::default(),
+ codebuddy_cli_path: None,
+ codebuddy_cli_env: Default::default(),
};
let err = build_ai_http_client(&config, 1).unwrap_err();
@@ -4852,6 +4878,8 @@ mod tests {
cursor_cli_env: Default::default(),
grok_cli_path: None,
grok_cli_env: Default::default(),
+ codebuddy_cli_path: None,
+ codebuddy_cli_env: Default::default(),
};
build_ai_http_client(&config, 1).unwrap();
@@ -4886,6 +4914,8 @@ mod tests {
cursor_cli_env: Default::default(),
grok_cli_path: None,
grok_cli_env: Default::default(),
+ codebuddy_cli_path: None,
+ codebuddy_cli_env: Default::default(),
};
build_ai_http_client(&config, 1).unwrap();
@@ -4920,6 +4950,8 @@ mod tests {
cursor_cli_env: Default::default(),
grok_cli_path: None,
grok_cli_env: Default::default(),
+ codebuddy_cli_path: None,
+ codebuddy_cli_env: Default::default(),
};
assert_eq!(
@@ -4958,6 +4990,8 @@ mod tests {
cursor_cli_env: Default::default(),
grok_cli_path: None,
grok_cli_env: Default::default(),
+ codebuddy_cli_path: None,
+ codebuddy_cli_env: Default::default(),
};
assert_eq!(resolve_endpoint(&ollama), "http://localhost:11434/v1/chat/completions");
@@ -4993,6 +5027,8 @@ mod tests {
cursor_cli_env: Default::default(),
grok_cli_path: None,
grok_cli_env: Default::default(),
+ codebuddy_cli_path: None,
+ codebuddy_cli_env: Default::default(),
};
for provider in
@@ -5047,6 +5083,8 @@ mod tests {
cursor_cli_env: Default::default(),
grok_cli_path: None,
grok_cli_env: Default::default(),
+ codebuddy_cli_path: None,
+ codebuddy_cli_env: Default::default(),
};
assert_eq!(resolve_model_list_endpoint(&openai).unwrap(), "https://api.openai.com/v1/models");
@@ -5077,6 +5115,8 @@ mod tests {
cursor_cli_env: Default::default(),
grok_cli_path: None,
grok_cli_env: Default::default(),
+ codebuddy_cli_path: None,
+ codebuddy_cli_env: Default::default(),
};
assert_eq!(resolve_model_list_endpoint(&claude).unwrap(), "https://api.anthropic.com/v1/models");
}
@@ -5110,6 +5150,8 @@ mod tests {
cursor_cli_env: Default::default(),
grok_cli_path: None,
grok_cli_env: Default::default(),
+ codebuddy_cli_path: None,
+ codebuddy_cli_env: Default::default(),
};
assert!(uses_anthropic_messages_api(&config));
@@ -5195,6 +5237,8 @@ mod tests {
cursor_cli_env: Default::default(),
grok_cli_path: None,
grok_cli_env: Default::default(),
+ codebuddy_cli_path: None,
+ codebuddy_cli_env: Default::default(),
};
assert!(!uses_anthropic_messages_api(&config));
@@ -5240,6 +5284,8 @@ mod tests {
cursor_cli_env: Default::default(),
grok_cli_path: None,
grok_cli_env: Default::default(),
+ codebuddy_cli_path: None,
+ codebuddy_cli_env: Default::default(),
};
assert_eq!(resolve_endpoint(&config), "https://api.example.com/v1/chat/completions");
assert_eq!(resolve_model_list_endpoint(&config).unwrap(), "https://api.example.com/v1/models");
@@ -5309,6 +5355,8 @@ mod tests {
cursor_cli_env: Default::default(),
grok_cli_path: None,
grok_cli_env: Default::default(),
+ codebuddy_cli_path: None,
+ codebuddy_cli_env: Default::default(),
};
assert_eq!(resolve_endpoint(&config), "https://api.openai.com/v1/responses");
@@ -5350,6 +5398,8 @@ mod tests {
cursor_cli_env: Default::default(),
grok_cli_path: None,
grok_cli_env: Default::default(),
+ codebuddy_cli_path: None,
+ codebuddy_cli_env: Default::default(),
};
let api_key_headers = claude_headers(&config).unwrap();
@@ -5471,6 +5521,8 @@ mod tests {
cursor_cli_env: Default::default(),
grok_cli_path: None,
grok_cli_env: Default::default(),
+ codebuddy_cli_path: None,
+ codebuddy_cli_env: Default::default(),
};
assert_eq!(resolve_ollama_show_endpoint(&config).unwrap(), "http://localhost:11434/api/show");
@@ -5509,6 +5561,8 @@ mod tests {
cursor_cli_env: Default::default(),
grok_cli_path: None,
grok_cli_env: Default::default(),
+ codebuddy_cli_path: None,
+ codebuddy_cli_env: Default::default(),
};
assert_eq!(ollama_selected_model_tool_support(&config).await.unwrap(), Some(true));
@@ -5596,6 +5650,8 @@ mod tests {
cursor_cli_env: Default::default(),
grok_cli_path: None,
grok_cli_env: Default::default(),
+ codebuddy_cli_path: None,
+ codebuddy_cli_env: Default::default(),
};
let models = vec![
AiModelInfo::new("qwen3:0.6b", None),
@@ -5863,6 +5919,8 @@ mod tests {
cursor_cli_env: Default::default(),
grok_cli_path: None,
grok_cli_env: Default::default(),
+ codebuddy_cli_path: None,
+ codebuddy_cli_env: Default::default(),
};
let mut body = serde_json::json!({
@@ -6080,6 +6138,8 @@ mod tests {
cursor_cli_env: Default::default(),
grok_cli_path: None,
grok_cli_env: Default::default(),
+ codebuddy_cli_path: None,
+ codebuddy_cli_env: Default::default(),
};
let mut body = serde_json::json!({
"model": &config.model,
@@ -6123,6 +6183,8 @@ mod tests {
cursor_cli_env: Default::default(),
grok_cli_path: None,
grok_cli_env: Default::default(),
+ codebuddy_cli_path: None,
+ codebuddy_cli_env: Default::default(),
};
let mut body = serde_json::json!({ "model": &config.model });
@@ -6172,6 +6234,8 @@ mod tests {
cursor_cli_env: Default::default(),
grok_cli_path: None,
grok_cli_env: Default::default(),
+ codebuddy_cli_path: None,
+ codebuddy_cli_env: Default::default(),
};
let mut body = serde_json::json!({ "model": &config.model });
@@ -6215,6 +6279,8 @@ mod tests {
cursor_cli_env: Default::default(),
grok_cli_path: None,
grok_cli_env: Default::default(),
+ codebuddy_cli_path: None,
+ codebuddy_cli_env: Default::default(),
};
let mut body = serde_json::json!({ "model": &config.model });
@@ -6254,6 +6320,8 @@ mod tests {
cursor_cli_env: Default::default(),
grok_cli_path: None,
grok_cli_env: Default::default(),
+ codebuddy_cli_path: None,
+ codebuddy_cli_env: Default::default(),
};
let mut body = serde_json::json!({ "model": &config.model });
@@ -6324,6 +6392,8 @@ mod tests {
cursor_cli_env: Default::default(),
grok_cli_path: None,
grok_cli_env: Default::default(),
+ codebuddy_cli_path: None,
+ codebuddy_cli_env: Default::default(),
};
let request = AiCompletionRequest {
config: config.clone(),
@@ -6388,6 +6458,8 @@ mod tests {
cursor_cli_env: Default::default(),
grok_cli_path: None,
grok_cli_env: Default::default(),
+ codebuddy_cli_path: None,
+ codebuddy_cli_env: Default::default(),
};
let mut body = serde_json::json!({
"model": &config.model,
@@ -6921,6 +6993,7 @@ mod tests {
AiProvider::OpenCodeCli,
AiProvider::CursorCli,
AiProvider::GrokCli,
+ AiProvider::CodeBuddyCli,
] {
let mut config = test_config(provider.clone());
config.max_retries = None;
diff --git a/crates/dbx-core/src/ai_claude_code_cli.rs b/crates/dbx-core/src/ai_claude_code_cli.rs
index 4e937e860b..ac68d84a58 100644
--- a/crates/dbx-core/src/ai_claude_code_cli.rs
+++ b/crates/dbx-core/src/ai_claude_code_cli.rs
@@ -587,6 +587,8 @@ mod tests {
cursor_cli_env: Default::default(),
grok_cli_path: None,
grok_cli_env: Default::default(),
+ codebuddy_cli_path: None,
+ codebuddy_cli_env: Default::default(),
}
}
diff --git a/crates/dbx-core/src/ai_cli_agent.rs b/crates/dbx-core/src/ai_cli_agent.rs
index a47a43c5a9..7cd103c109 100644
--- a/crates/dbx-core/src/ai_cli_agent.rs
+++ b/crates/dbx-core/src/ai_cli_agent.rs
@@ -34,6 +34,7 @@ pub struct CliAgentCommandSpec {
pub enum CliAgentJsonlDialect {
CodexExec,
ClaudeCodePrint,
+ CodeBuddyPrint,
OpenCodeRun,
CursorPrint,
/// Grok Build headless `--output-format streaming-json` (ACP-derived NDJSON).
@@ -237,7 +238,8 @@ pub fn parse_cli_jsonl_event(line: &str, dialect: CliAgentJsonlDialect) -> Optio
fn parse_cli_jsonl_line(line: &str, dialect: CliAgentJsonlDialect) -> ParsedCliAgentEvent {
match dialect {
CliAgentJsonlDialect::CodexExec => parse_codex_jsonl_line(line),
- CliAgentJsonlDialect::ClaudeCodePrint => parse_claude_code_jsonl_line(line),
+ CliAgentJsonlDialect::ClaudeCodePrint => parse_claude_compatible_jsonl_line(line, "Claude Code CLI failed"),
+ CliAgentJsonlDialect::CodeBuddyPrint => parse_claude_compatible_jsonl_line(line, "CodeBuddy Code CLI failed"),
CliAgentJsonlDialect::OpenCodeRun => parse_open_code_jsonl_line(line),
CliAgentJsonlDialect::CursorPrint => parse_cursor_jsonl_line(line),
CliAgentJsonlDialect::GrokStreamingJson => parse_grok_streaming_json_line(line),
@@ -402,7 +404,7 @@ fn codex_error_message(value: &Value) -> String {
.to_string()
}
-fn parse_claude_code_jsonl_line(line: &str) -> ParsedCliAgentEvent {
+fn parse_claude_compatible_jsonl_line(line: &str, fallback_error: &str) -> ParsedCliAgentEvent {
let Ok(value) = serde_json::from_str::(line) else {
return ParsedCliAgentEvent::default();
};
@@ -410,9 +412,9 @@ fn parse_claude_code_jsonl_line(line: &str) -> ParsedCliAgentEvent {
match value.get("type").and_then(Value::as_str).unwrap_or_default() {
"assistant" => parse_claude_code_assistant(&value),
"user" => parse_claude_code_user(&value),
- "result" => parse_claude_code_result(&value),
+ "result" => parse_claude_code_result(&value, fallback_error),
"error" => {
- let message = claude_code_error_message(&value);
+ let message = claude_code_error_message(&value, fallback_error);
ParsedCliAgentEvent {
error: Some(message.clone()),
events: vec![AgentEvent::Error { message }],
@@ -489,10 +491,10 @@ fn parse_claude_code_user(value: &Value) -> ParsedCliAgentEvent {
ParsedCliAgentEvent { events, ..Default::default() }
}
-fn parse_claude_code_result(value: &Value) -> ParsedCliAgentEvent {
+fn parse_claude_code_result(value: &Value, fallback_error: &str) -> ParsedCliAgentEvent {
let subtype = value.get("subtype").and_then(Value::as_str).unwrap_or("success");
if subtype != "success" {
- let message = claude_code_error_message(value);
+ let message = claude_code_error_message(value, fallback_error);
return ParsedCliAgentEvent {
error: Some(message.clone()),
events: vec![AgentEvent::Error { message }],
@@ -528,14 +530,14 @@ fn claude_content_blocks(content: &Value) -> Vec {
}
}
-fn claude_code_error_message(value: &Value) -> String {
+fn claude_code_error_message(value: &Value, fallback_error: &str) -> String {
value
.get("error")
.and_then(Value::as_str)
.or_else(|| value.get("message").and_then(Value::as_str))
.or_else(|| value.get("error").and_then(|error| error.get("message")).and_then(Value::as_str))
.or_else(|| value.get("result").and_then(Value::as_str))
- .unwrap_or("Claude Code CLI failed")
+ .unwrap_or(fallback_error)
.to_string()
}
diff --git a/crates/dbx-core/src/ai_codebuddy_cli.rs b/crates/dbx-core/src/ai_codebuddy_cli.rs
new file mode 100644
index 0000000000..80c0884a4b
--- /dev/null
+++ b/crates/dbx-core/src/ai_codebuddy_cli.rs
@@ -0,0 +1,1348 @@
+use crate::agent_events::AgentEvent;
+use crate::ai::{
+ AiCapabilitySource, AiConfig, AiEffortCapability, AiEffortLevel, AiEffortOption, AiEffortSelection, AiModelInfo,
+ AiTestConnectionResult,
+};
+use crate::ai_cli_agent::{
+ build_cli_agent_prompt, cli_command, dbx_mcp_enabled_tools, dbx_mcp_scope_env, parse_cli_jsonl_event,
+ run_cli_jsonl_agent, CliAgentCommandSpec, CliAgentJsonlDialect, CliAgentProcessSpec, CliAgentRunOptions,
+};
+use serde_json::{json, Map, Value};
+use std::collections::{BTreeMap, BTreeSet};
+use std::env;
+use std::path::{Path, PathBuf};
+use std::process::Stdio;
+use std::time::{Duration, Instant};
+use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, Lines};
+use tokio::process::{Child, ChildStdin, ChildStdout};
+use tokio::sync::Notify;
+
+const CODEBUDDY_INITIALIZE_TIMEOUT: Duration = Duration::from_secs(10);
+const CODEBUDDY_ACP_TIMEOUT: Duration = Duration::from_secs(20);
+const CODEBUDDY_SETTING_SOURCES: &str = "user";
+const CODEBUDDY_EXECUTABLE_NAMES: &[&str] = &["codebuddy", "cbc"];
+
+pub type CodeBuddyRunOptions = CliAgentRunOptions;
+pub type CodeBuddyCommandSpec = CliAgentCommandSpec;
+
+struct CodeBuddyIsolatedCwd {
+ path: PathBuf,
+}
+
+impl CodeBuddyIsolatedCwd {
+ fn create() -> Result {
+ let path = env::temp_dir().join(format!("dbx-codebuddy-{}", uuid::Uuid::new_v4()));
+ std::fs::create_dir(&path).map_err(|error| {
+ format!("[codeBuddyRunFailed] Failed to create isolated CodeBuddy Code directory: {error}")
+ })?;
+ Ok(Self { path })
+ }
+
+ fn write_mcp_config(&self, config: &str) -> Result {
+ let path = self.path.join("mcp.json");
+ std::fs::write(&path, config).map_err(|error| {
+ format!("[codeBuddyRunFailed] Failed to write temporary CodeBuddy MCP configuration: {error}")
+ })?;
+ Ok(path)
+ }
+}
+
+impl Drop for CodeBuddyIsolatedCwd {
+ fn drop(&mut self) {
+ let _ = std::fs::remove_dir_all(&self.path);
+ }
+}
+
+#[derive(Debug)]
+struct CodeBuddyInitializeResult {
+ models: Vec,
+ authenticated: bool,
+ current_model_id: Option,
+}
+
+struct CodeBuddyAcpProcess {
+ child: Child,
+ stdin: ChildStdin,
+ stdout: Lines>,
+ next_request_id: u64,
+}
+
+impl CodeBuddyAcpProcess {
+ async fn request(&mut self, method: &str, params: Value) -> Result {
+ let request_id = self.next_request_id;
+ self.next_request_id += 1;
+ let mut request = serde_json::to_vec(&json!({
+ "jsonrpc": "2.0",
+ "id": request_id,
+ "method": method,
+ "params": params,
+ }))
+ .map_err(|error| format!("[codeBuddyProtocolError] Failed to serialize ACP request: {error}"))?;
+ request.push(b'\n');
+ self.stdin
+ .write_all(&request)
+ .await
+ .map_err(|error| format!("[codeBuddyRunFailed] Failed to write CodeBuddy ACP request: {error}"))?;
+ self.stdin
+ .flush()
+ .await
+ .map_err(|error| format!("[codeBuddyRunFailed] Failed to flush CodeBuddy ACP request: {error}"))?;
+
+ while let Some(line) = self
+ .stdout
+ .next_line()
+ .await
+ .map_err(|error| format!("[codeBuddyRunFailed] Failed to read CodeBuddy ACP response: {error}"))?
+ {
+ let Ok(response) = serde_json::from_str::(line.trim()) else {
+ continue;
+ };
+ if response.get("id").and_then(Value::as_u64) != Some(request_id) {
+ continue;
+ }
+ if let Some(error) = response.get("error") {
+ let message = error
+ .get("message")
+ .and_then(Value::as_str)
+ .map(sanitize_diagnostic)
+ .filter(|message| !message.is_empty())
+ .unwrap_or_else(|| "CodeBuddy ACP request failed".to_string());
+ return Err(format!("[codeBuddyProtocolError] {message}"));
+ }
+ return response
+ .get("result")
+ .cloned()
+ .ok_or_else(|| "[codeBuddyProtocolError] CodeBuddy ACP response omitted its result.".to_string());
+ }
+
+ Err("[codeBuddyProtocolError] CodeBuddy ACP process ended before returning a response.".to_string())
+ }
+
+ async fn stop(mut self) {
+ drop(self.stdin);
+ let _ = self.child.start_kill();
+ let _ = self.child.wait().await;
+ }
+}
+
+fn append_codebuddy_isolation_args(args: &mut Vec) {
+ // User settings retain CodeBuddy authentication and account preferences,
+ // while project/local instruction and hook sources stay excluded.
+ args.extend(["--setting-sources".to_string(), CODEBUDDY_SETTING_SOURCES.to_string()]);
+}
+
+fn codebuddy_program(config: &AiConfig) -> String {
+ config
+ .codebuddy_cli_path
+ .as_deref()
+ .map(str::trim)
+ .filter(|path| !path.is_empty())
+ .unwrap_or("codebuddy")
+ .to_string()
+}
+
+fn codebuddy_process_env(config: &AiConfig, command: &CodeBuddyCommandSpec) -> Result, String> {
+ let mut env = BTreeMap::from_iter(codebuddy_cli_env(config)?);
+ if let Some(dir) = command_parent_dir(command) {
+ let user_path = env.get("PATH").map(String::as_str);
+ env.insert("PATH".to_string(), merged_path_with_dir(&dir, user_path));
+ }
+ Ok(env.into_iter().collect())
+}
+
+fn command_parent_dir(command: &CodeBuddyCommandSpec) -> Option {
+ Path::new(&command.program)
+ .parent()
+ .filter(|parent| !parent.as_os_str().is_empty())
+ .map(|parent| parent.to_string_lossy().to_string())
+}
+
+fn merged_path_with_dir(dir: &str, user_path: Option<&str>) -> String {
+ let mut seen = BTreeSet::new();
+ let mut dirs = vec![PathBuf::from(dir)];
+ if let Some(path) = user_path {
+ dirs.extend(env::split_paths(path));
+ }
+ dirs.extend(common_executable_dirs());
+ let paths = dirs.into_iter().filter(|path| seen.insert(path.clone())).collect::>();
+ env::join_paths(paths).unwrap_or_default().to_string_lossy().to_string()
+}
+
+fn common_executable_dirs() -> Vec {
+ let mut dirs = Vec::new();
+ if let Ok(path) = env::var("PATH") {
+ dirs.extend(env::split_paths(&path));
+ }
+ #[cfg(windows)]
+ {
+ if let Ok(app_data) = env::var("APPDATA") {
+ dirs.push(PathBuf::from(app_data).join("npm"));
+ }
+ }
+ #[cfg(not(windows))]
+ {
+ dirs.extend([
+ PathBuf::from("/opt/homebrew/bin"),
+ PathBuf::from("/usr/local/bin"),
+ PathBuf::from("/usr/bin"),
+ PathBuf::from("/bin"),
+ PathBuf::from("/usr/sbin"),
+ PathBuf::from("/sbin"),
+ ]);
+ }
+ dirs
+}
+
+fn validate_codebuddy_program(config: &AiConfig) -> Result {
+ let program = codebuddy_program(config);
+ if starts_with_env_assignment(&program) {
+ return Err("[codeBuddyCliPathInvalid] CodeBuddy Code CLI path should contain only the executable path. Add environment variables in the CodeBuddy environment variables section.".to_string());
+ }
+ if is_path_like_program(&program) {
+ let expanded = crate::path_utils::expand_tilde(&program);
+ let path = Path::new(&expanded);
+ if path.is_dir() {
+ return CODEBUDDY_EXECUTABLE_NAMES
+ .iter()
+ .find_map(|name| launchable_program_in_dir(path, name))
+ .ok_or_else(|| {
+ "[codeBuddyCliPathInvalid] CodeBuddy Code CLI path should point to the codebuddy executable or a directory containing codebuddy."
+ .to_string()
+ });
+ }
+ return Ok(expanded);
+ }
+ Ok(program)
+}
+
+fn launchable_program_in_dir(dir: &Path, program: &str) -> Option {
+ program_path_candidates(dir, program)
+ .into_iter()
+ .find(|candidate| is_launchable_program_path(candidate) && candidate.is_file())
+ .map(|path| path.to_string_lossy().to_string())
+}
+
+#[cfg(not(windows))]
+fn program_path_candidates(dir: &Path, program: &str) -> Vec {
+ vec![dir.join(program)]
+}
+
+#[cfg(windows)]
+fn program_path_candidates(dir: &Path, program: &str) -> Vec {
+ let path = Path::new(program);
+ if path.extension().is_some() {
+ return vec![dir.join(program)];
+ }
+ [".cmd", ".exe", ".bat", ".com", ""].iter().map(|extension| dir.join(format!("{program}{extension}"))).collect()
+}
+
+#[cfg(not(windows))]
+fn is_launchable_program_path(_path: &Path) -> bool {
+ true
+}
+
+#[cfg(windows)]
+fn is_launchable_program_path(path: &Path) -> bool {
+ matches!(
+ path.extension().and_then(|extension| extension.to_str()).map(str::to_ascii_lowercase).as_deref(),
+ Some("exe" | "cmd" | "bat" | "com")
+ )
+}
+
+fn is_path_like_program(program: &str) -> bool {
+ program.contains('/') || program.contains('\\') || program.starts_with('~')
+}
+
+fn starts_with_env_assignment(program: &str) -> bool {
+ let Some(first_token) = program.split_whitespace().next() else {
+ return false;
+ };
+ let Some((key, _)) = first_token.split_once('=') else {
+ return false;
+ };
+ is_env_var_name(key)
+}
+
+fn is_env_var_name(name: &str) -> bool {
+ let mut chars = name.chars();
+ let Some(first) = chars.next() else {
+ return false;
+ };
+ (first == '_' || first.is_ascii_alphabetic()) && chars.all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
+}
+
+fn is_reserved_dbx_mcp_env_name(name: &str) -> bool {
+ name.to_ascii_uppercase().starts_with("DBX_MCP_")
+}
+
+pub fn codebuddy_cli_env(config: &AiConfig) -> Result, String> {
+ let mut env = BTreeMap::new();
+ for (key, value) in &config.codebuddy_cli_env {
+ let key = key.trim();
+ if key.is_empty() {
+ continue;
+ }
+ if !is_env_var_name(key) {
+ return Err(format!(
+ "[codeBuddyEnvInvalid] Invalid CodeBuddy environment variable name `{key}`. Use names like HTTPS_PROXY."
+ ));
+ }
+ if is_reserved_dbx_mcp_env_name(key) {
+ return Err(format!(
+ "[codeBuddyEnvReserved] `{key}` is managed by DBX for the scoped MCP server and cannot be set here."
+ ));
+ }
+ env.insert(key.to_string(), value.clone());
+ }
+ Ok(env.into_iter().collect())
+}
+
+pub fn codebuddy_enabled_tools(agent_mode: bool) -> Vec {
+ dbx_mcp_enabled_tools(agent_mode).into_iter().map(|tool| format!("mcp__dbx__{tool}")).collect()
+}
+
+fn codebuddy_mcp_config(options: &CodeBuddyRunOptions) -> String {
+ let mcp_command =
+ options.mcp_server_command.as_ref().map(|command| command.program.as_str()).unwrap_or("dbx-mcp-server");
+ let mut server = Map::new();
+ server.insert("type".to_string(), Value::String("stdio".to_string()));
+ server.insert("command".to_string(), Value::String(mcp_command.to_string()));
+ if let Some(command) = options.mcp_server_command.as_ref().filter(|command| !command.args.is_empty()) {
+ server.insert("args".to_string(), json!(command.args));
+ }
+ let env = dbx_mcp_scope_env(options)
+ .into_iter()
+ .map(|(name, value)| (name.to_string(), Value::String(value)))
+ .collect::