diff --git a/packages/happy-app/sources/components/modelModeOptions.ts b/packages/happy-app/sources/components/modelModeOptions.ts index 6d86d56079..b39ca13914 100644 --- a/packages/happy-app/sources/components/modelModeOptions.ts +++ b/packages/happy-app/sources/components/modelModeOptions.ts @@ -29,6 +29,12 @@ const GEMINI_MODEL_FALLBACKS: ModelMode[] = [ { key: 'gemini-2.5-flash-lite', name: 'Gemini 2.5 Flash Lite', description: 'Fastest' }, ]; +const KIMI_MODEL_FALLBACKS: ModelMode[] = [ + { key: 'kimi-k2.5', name: 'Kimi K2.5', description: 'Most capable' }, + { key: 'kimi-k2-thinking', name: 'Kimi K2 Thinking', description: 'Deep reasoning' }, + { key: 'kimi-k2-thinking-turbo', name: 'Kimi K2 Thinking Turbo', description: 'Fast reasoning' }, +]; + export function mapMetadataOptions(options?: MetadataOption[] | null): ModeOption[] { if (!options || options.length === 0) { return []; @@ -93,6 +99,18 @@ export function getGeminiModelModes(): ModelMode[] { return GEMINI_MODEL_FALLBACKS; } +export function getKimiPermissionModes(translate: Translate): PermissionMode[] { + // Kimi uses the same permission model as Gemini (default / read-only / yolo) + return [ + { key: 'default', name: translate('agentInput.geminiPermissionMode.default'), description: null }, + { key: 'yolo', name: translate('agentInput.geminiPermissionMode.yolo'), description: null }, + ]; +} + +export function getKimiModelModes(): ModelMode[] { + return KIMI_MODEL_FALLBACKS; +} + export function getHardcodedPermissionModes(flavor: AgentFlavor, translate: Translate): PermissionMode[] { if (flavor === 'codex') { return getCodexPermissionModes(translate); @@ -100,6 +118,9 @@ export function getHardcodedPermissionModes(flavor: AgentFlavor, translate: Tran if (flavor === 'gemini') { return getGeminiPermissionModes(translate); } + if (flavor === 'kimi') { + return getKimiPermissionModes(translate); + } return getClaudePermissionModes(translate); } @@ -110,6 +131,9 @@ export function getHardcodedModelModes(flavor: AgentFlavor, translate: Translate if (flavor === 'gemini') { return getGeminiModelModes(); } + if (flavor === 'kimi') { + return getKimiModelModes(); + } return getClaudeModelModes(); } @@ -130,7 +154,7 @@ export function getAvailablePermissionModes( metadata: Metadata | null | undefined, translate: Translate, ): PermissionMode[] { - if (flavor === 'claude' || flavor === 'codex') { + if (flavor === 'claude' || flavor === 'codex' || flavor === 'kimi') { return hackModes(getHardcodedPermissionModes(flavor, translate)); } @@ -169,6 +193,9 @@ export function getDefaultModelKey(flavor: AgentFlavor): string { if (flavor === 'gemini') { return 'gemini-2.5-pro'; } + if (flavor === 'kimi') { + return 'kimi-k2.5'; + } return 'default'; } diff --git a/packages/happy-cli/src/agent/acp/runAcp.ts b/packages/happy-cli/src/agent/acp/runAcp.ts index 469c8d3cc6..f5158554f3 100644 --- a/packages/happy-cli/src/agent/acp/runAcp.ts +++ b/packages/happy-cli/src/agent/acp/runAcp.ts @@ -435,10 +435,13 @@ type PendingTurn = { timeout: NodeJS.Timeout; }; -function resolveSessionFlavor(agentName: string): 'gemini' | 'opencode' | 'acp' { +function resolveSessionFlavor(agentName: string): 'gemini' | 'kimi' | 'opencode' | 'acp' { if (agentName === 'gemini') { return 'gemini'; } + if (agentName === 'kimi') { + return 'kimi'; + } if (agentName === 'opencode') { return 'opencode'; } diff --git a/packages/happy-cli/src/api/api.ts b/packages/happy-cli/src/api/api.ts index fc38118043..b648bca21e 100644 --- a/packages/happy-cli/src/api/api.ts +++ b/packages/happy-cli/src/api/api.ts @@ -289,7 +289,7 @@ export class ApiClient { * Register a vendor API token with the server * The token is sent as a JSON string - server handles encryption */ - async registerVendorToken(vendor: 'openai' | 'anthropic' | 'gemini', apiKey: any): Promise { + async registerVendorToken(vendor: 'openai' | 'anthropic' | 'gemini' | 'moonshot', apiKey: any): Promise { try { const response = await axios.post( `${configuration.serverUrl}/v1/connect/${vendor}/register`, @@ -320,7 +320,7 @@ export class ApiClient { * Get vendor API token from the server * Returns the token if it exists, null otherwise */ - async getVendorToken(vendor: 'openai' | 'anthropic' | 'gemini'): Promise { + async getVendorToken(vendor: 'openai' | 'anthropic' | 'gemini' | 'moonshot'): Promise { try { const response = await axios.get( `${configuration.serverUrl}/v1/connect/${vendor}/token`, diff --git a/packages/happy-cli/src/commands/connect.ts b/packages/happy-cli/src/commands/connect.ts index ac9311a4f2..2faa4a8c1a 100644 --- a/packages/happy-cli/src/commands/connect.ts +++ b/packages/happy-cli/src/commands/connect.ts @@ -7,6 +7,7 @@ import { ApiClient } from '@/api/api'; import { authenticateCodex } from './connect/authenticateCodex'; import { authenticateClaude } from './connect/authenticateClaude'; import { authenticateGemini } from './connect/authenticateGemini'; +import { authenticateKimi } from './connect/authenticateKimi'; import { decodeJwtPayload } from './connect/utils'; /** @@ -36,6 +37,9 @@ export async function handleConnectCommand(args: string[]): Promise { case 'gemini': await handleConnectVendor('gemini', 'Gemini'); break; + case 'kimi': + await handleConnectVendor('kimi', 'Kimi'); + break; case 'status': await handleConnectStatus(); break; @@ -54,6 +58,7 @@ ${chalk.bold('Usage:')} happy connect codex Store your Codex API key in Happy cloud happy connect claude Store your Anthropic API key in Happy cloud happy connect gemini Store your Gemini API key in Happy cloud + happy connect kimi Store your Moonshot API key in Happy cloud happy connect status Show connection status for all vendors happy connect help Show this help message @@ -66,6 +71,7 @@ ${chalk.bold('Examples:')} happy connect codex happy connect claude happy connect gemini + happy connect kimi happy connect status ${chalk.bold('Notes:')} @@ -75,7 +81,7 @@ ${chalk.bold('Notes:')} `); } -async function handleConnectVendor(vendor: 'codex' | 'claude' | 'gemini', displayName: string): Promise { +async function handleConnectVendor(vendor: 'codex' | 'claude' | 'gemini' | 'kimi', displayName: string): Promise { console.log(chalk.bold(`\nšŸ”Œ Connecting ${displayName} to Happy cloud\n`)); // Check if authenticated @@ -107,10 +113,20 @@ async function handleConnectVendor(vendor: 'codex' | 'claude' | 'gemini', displa const geminiAuthTokens = await authenticateGemini(); await api.registerVendorToken('gemini', { oauth: geminiAuthTokens }); console.log('āœ… Gemini token registered with server'); - + // Also update local Gemini config to keep tokens in sync updateLocalGeminiCredentials(geminiAuthTokens); - + + process.exit(0); + } else if (vendor === 'kimi') { + console.log('šŸš€ Registering Kimi/Moonshot token with server'); + const kimiAuthTokens = await authenticateKimi(); + await api.registerVendorToken('moonshot', { oauth: kimiAuthTokens }); + console.log('āœ… Kimi token registered with server'); + + // Also save API key locally so kimi CLI can pick it up + updateLocalKimiCredentials(kimiAuthTokens.access_token); + process.exit(0); } else { throw new Error(`Unsupported vendor: ${vendor}`); @@ -135,10 +151,11 @@ async function handleConnectStatus(): Promise { const api = await ApiClient.create(credentials); // Check each vendor - const vendors: Array<{ key: 'openai' | 'anthropic' | 'gemini'; name: string; display: string }> = [ + const vendors: Array<{ key: 'openai' | 'anthropic' | 'gemini' | 'moonshot'; name: string; display: string }> = [ { key: 'gemini', name: 'Gemini', display: 'Google Gemini' }, { key: 'openai', name: 'Codex', display: 'OpenAI Codex' }, { key: 'anthropic', name: 'Claude', display: 'Anthropic Claude' }, + { key: 'moonshot', name: 'Kimi', display: 'Moonshot Kimi' }, ]; for (const vendor of vendors) { @@ -216,4 +233,36 @@ function updateLocalGeminiCredentials(tokens: { // Non-critical error - server tokens will still work console.log(chalk.yellow(` āš ļø Could not update local credentials: ${error}`)); } +} + +/** + * Cache Moonshot API key locally so Happy can pass it to kimi-cli via + * MOONSHOT_API_KEY env var on next session start. This is Happy's own + * credential store — kimi-cli itself uses ~/.kimi/config.toml. + */ +function updateLocalKimiCredentials(apiKey: string): void { + try { + const kimiDir = join(homedir(), '.kimi'); + const configPath = join(kimiDir, 'config.json'); + + if (!existsSync(kimiDir)) { + mkdirSync(kimiDir, { recursive: true }); + } + + // Read existing config or start fresh + let config: Record = {}; + if (existsSync(configPath)) { + try { + config = JSON.parse(require('fs').readFileSync(configPath, 'utf-8')); + } catch { + // corrupt file — overwrite + } + } + + config.api_key = apiKey; + writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8'); + console.log(chalk.gray(` Updated local credentials: ${configPath}`)); + } catch (error) { + console.log(chalk.yellow(` āš ļø Could not update local credentials: ${error}`)); + } } \ No newline at end of file diff --git a/packages/happy-cli/src/commands/connect/authenticateKimi.ts b/packages/happy-cli/src/commands/connect/authenticateKimi.ts new file mode 100644 index 0000000000..a206d1eac0 --- /dev/null +++ b/packages/happy-cli/src/commands/connect/authenticateKimi.ts @@ -0,0 +1,62 @@ +/** + * Kimi/Moonshot authentication helper + * + * Kimi CLI authenticates via MOONSHOT_API_KEY. This module prompts the user + * for their API key and returns it in a token-compatible format so it can + * be stored in Happy cloud alongside other vendor tokens. + */ + +import * as readline from 'readline'; + +export interface KimiAuthTokens { + access_token: string; + token_type: string; +} + +/** + * Prompt the user for their Moonshot API key. + * + * Kimi/Moonshot doesn't use OAuth — the CLI reads MOONSHOT_API_KEY + * from the environment. We wrap it in the same token shape used by other + * vendors so the Happy cloud can store and relay it uniformly. + */ +export async function authenticateKimi(): Promise { + console.log('šŸ”‘ Kimi uses a Moonshot API key for authentication.'); + console.log(' Get your key from: https://platform.moonshot.cn/console/api-keys'); + console.log(''); + + const apiKey = await promptForInput('Enter your Moonshot API key: '); + + if (!apiKey || !apiKey.trim()) { + throw new Error('No API key provided'); + } + + const trimmed = apiKey.trim(); + + // Basic format check — Moonshot keys start with "sk-" + if (!trimmed.startsWith('sk-')) { + console.log('āš ļø Warning: Moonshot API keys usually start with "sk-".'); + console.log(' Continuing anyway — double-check the key if authentication fails.'); + } + + console.log(''); + console.log('šŸŽ‰ API key received!'); + + return { + access_token: trimmed, + token_type: 'Bearer', + }; +} + +function promptForInput(question: string): Promise { + return new Promise((resolve) => { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + rl.question(question, (answer) => { + rl.close(); + resolve(answer); + }); + }); +} diff --git a/packages/happy-cli/src/commands/connect/types.ts b/packages/happy-cli/src/commands/connect/types.ts index 2a24e5ea88..1e137a6f14 100644 --- a/packages/happy-cli/src/commands/connect/types.ts +++ b/packages/happy-cli/src/commands/connect/types.ts @@ -23,6 +23,11 @@ export interface PKCECodes { challenge: string; } +export interface KimiAuthTokens { + access_token: string; + token_type: string; +} + export interface ClaudeAuthTokens { raw: any; token: string; diff --git a/packages/happy-cli/src/daemon/run.ts b/packages/happy-cli/src/daemon/run.ts index 75889d14e9..3882504c77 100644 --- a/packages/happy-cli/src/daemon/run.ts +++ b/packages/happy-cli/src/daemon/run.ts @@ -36,7 +36,7 @@ export const initialMachineMetadata: MachineMetadata = { // Get environment variables for a profile, filtered for agent compatibility async function getProfileEnvironmentVariablesForAgent( profileId: string, - agentType: 'claude' | 'codex' | 'gemini' + agentType: 'claude' | 'codex' | 'gemini' | 'kimi' ): Promise> { try { const settings = await readSettings(); @@ -285,6 +285,8 @@ export async function startDaemon(): Promise { // Set the environment variable for Codex authEnv.CODEX_HOME = codexHomeDir.name; + } else if (options.agent === 'kimi') { + authEnv.MOONSHOT_API_KEY = options.token; } else { // Assuming claude authEnv.CLAUDE_CODE_OAUTH_TOKEN = options.token; } @@ -386,8 +388,8 @@ export async function startDaemon(): Promise { // Construct command for the CLI const cliPath = join(projectPath(), 'dist', 'index.mjs'); - // Determine agent command - support claude, codex, and gemini - const agent = options.agent === 'gemini' ? 'gemini' : (options.agent === 'codex' ? 'codex' : 'claude'); + // Determine agent command - support claude, codex, gemini, and kimi + const agent = options.agent === 'gemini' ? 'gemini' : (options.agent === 'kimi' ? 'kimi' : (options.agent === 'codex' ? 'codex' : 'claude')); const fullCommand = `node --no-warnings --no-deprecation ${cliPath} ${agent} --happy-starting-mode remote --started-by daemon`; // Spawn in tmux with environment variables @@ -470,7 +472,7 @@ export async function startDaemon(): Promise { if (!useTmux) { logger.debug(`[DAEMON RUN] Using regular process spawning`); - // Construct arguments for the CLI - support claude, codex, and gemini + // Construct arguments for the CLI - support claude, codex, gemini, and kimi let agentCommand: string; switch (options.agent) { case 'claude': @@ -483,6 +485,9 @@ export async function startDaemon(): Promise { case 'gemini': agentCommand = 'gemini'; break; + case 'kimi': + agentCommand = 'kimi'; + break; default: return { type: 'error', diff --git a/packages/happy-cli/src/index.ts b/packages/happy-cli/src/index.ts index ca3c031925..f4c7c47088 100644 --- a/packages/happy-cli/src/index.ts +++ b/packages/happy-cli/src/index.ts @@ -342,6 +342,67 @@ import { extractNoSandboxFlag } from './utils/sandboxFlags' process.exit(1) } return; + } else if (subcommand === 'kimi') { + // Handle kimi command — delegates to the generic ACP runner. + // Kimi CLI (github.com/MoonshotAI/kimi-cli) must be installed and + // available in PATH. Full native integration is planned; for now + // this bootstraps auth + session tracking through Happy. + try { + const { runAcp } = await import('@/agent/acp'); + + let startedBy: 'daemon' | 'terminal' | undefined = undefined; + const kimiArgs: string[] = []; + for (let i = 1; i < args.length; i++) { + if (args[i] === '--started-by') { + startedBy = args[++i] as 'daemon' | 'terminal'; + continue; + } + kimiArgs.push(args[i]); + } + + const { + credentials + } = await authAndSetupMachineIfNeeded(); + + // Try to fetch Moonshot API key from Happy cloud (via 'happy connect kimi') + try { + const api = await ApiClient.create(credentials); + const vendorToken = await api.getVendorToken('moonshot'); + if (vendorToken?.oauth?.access_token && !process.env.MOONSHOT_API_KEY) { + process.env.MOONSHOT_API_KEY = vendorToken.oauth.access_token; + logger.debug('[Kimi] Using API key from Happy cloud'); + } + } catch (error) { + logger.debug('[Kimi] Failed to fetch cloud token:', error); + } + + logger.debug('Ensuring Happy background service is running & matches our version...'); + if (!(await isDaemonRunningCurrentlyInstalledHappyVersion())) { + logger.debug('Starting Happy background service...'); + const daemonProcess = spawnHappyCLI(['daemon', 'start-sync'], { + detached: true, + stdio: 'ignore', + env: process.env + }); + daemonProcess.unref(); + await new Promise(resolve => setTimeout(resolve, 200)); + } + + await runAcp({ + credentials, + startedBy, + agentName: 'kimi', + command: 'kimi', + args: ['acp', ...kimiArgs], + }); + } catch (error) { + console.error(chalk.red('Error:'), error instanceof Error ? error.message : 'Unknown error') + if (process.env.DEBUG) { + console.error(error) + } + process.exit(1) + } + return; } else if (subcommand === 'acp') { try { const { runAcp, resolveAcpAgentConfig } = await import('@/agent/acp'); @@ -634,6 +695,7 @@ ${chalk.bold('Usage:')} happy auth Manage authentication happy codex Start Codex mode happy gemini Start Gemini mode (ACP) + happy kimi Start Kimi mode (ACP) happy acp Start a generic ACP-compatible agent happy connect Connect AI vendor API keys happy sandbox Configure and manage OS-level sandboxing diff --git a/packages/happy-cli/src/kimi/constants.ts b/packages/happy-cli/src/kimi/constants.ts new file mode 100644 index 0000000000..7df9f282bd --- /dev/null +++ b/packages/happy-cli/src/kimi/constants.ts @@ -0,0 +1,14 @@ +/** + * Kimi Constants + * + * Environment variable names and defaults for the Kimi CLI integration. + */ + +/** Environment variable name for Moonshot API key */ +export const MOONSHOT_API_KEY_ENV = 'MOONSHOT_API_KEY'; + +/** Environment variable name for Kimi model selection */ +export const KIMI_MODEL_ENV = 'KIMI_MODEL'; + +/** Default Kimi model */ +export const DEFAULT_KIMI_MODEL = 'kimi-k2.5'; diff --git a/packages/happy-cli/src/kimi/types.ts b/packages/happy-cli/src/kimi/types.ts new file mode 100644 index 0000000000..d6c998526b --- /dev/null +++ b/packages/happy-cli/src/kimi/types.ts @@ -0,0 +1,16 @@ +/** + * Kimi Types + * + * Type definitions for Kimi CLI integration. + */ + +import type { PermissionMode } from '@/api/types'; + +/** + * Mode configuration for Kimi messages + */ +export interface KimiMode { + permissionMode: PermissionMode; + model?: string; + originalUserMessage?: string; +} diff --git a/packages/happy-cli/src/persistence.ts b/packages/happy-cli/src/persistence.ts index 1b32282dea..2a21b6b64b 100644 --- a/packages/happy-cli/src/persistence.ts +++ b/packages/happy-cli/src/persistence.ts @@ -59,6 +59,7 @@ const ProfileCompatibilitySchema = z.object({ claude: z.boolean().default(true), codex: z.boolean().default(true), gemini: z.boolean().default(true), + kimi: z.boolean().default(true), }); export const SandboxConfigSchema = z.object({ @@ -108,7 +109,7 @@ export const AIBackendProfileSchema = z.object({ defaultModelMode: z.string().optional(), // Compatibility metadata - compatibility: ProfileCompatibilitySchema.default({ claude: true, codex: true, gemini: true }), + compatibility: ProfileCompatibilitySchema.default({ claude: true, codex: true, gemini: true, kimi: true }), // Built-in profile indicator isBuiltIn: z.boolean().default(false), @@ -122,7 +123,7 @@ export const AIBackendProfileSchema = z.object({ export type AIBackendProfile = z.infer; // Helper functions matching the happy app exactly -export function validateProfileForAgent(profile: AIBackendProfile, agent: 'claude' | 'codex' | 'gemini'): boolean { +export function validateProfileForAgent(profile: AIBackendProfile, agent: 'claude' | 'codex' | 'gemini' | 'kimi'): boolean { return profile.compatibility[agent]; } diff --git a/packages/happy-cli/src/ui/ink/KimiDisplay.tsx b/packages/happy-cli/src/ui/ink/KimiDisplay.tsx new file mode 100644 index 0000000000..c560d9db32 --- /dev/null +++ b/packages/happy-cli/src/ui/ink/KimiDisplay.tsx @@ -0,0 +1,170 @@ +import React, { useState, useEffect, useRef, useCallback } from 'react' +import { Box, Text, useStdout, useInput } from 'ink' +import { MessageBuffer, type BufferedMessage } from './messageBuffer' + +interface KimiDisplayProps { + messageBuffer: MessageBuffer + logPath?: string + currentModel?: string + onExit?: () => void +} + +export const KimiDisplay: React.FC = ({ messageBuffer, logPath, currentModel, onExit }) => { + const [messages, setMessages] = useState([]) + const [confirmationMode, setConfirmationMode] = useState(false) + const [actionInProgress, setActionInProgress] = useState(false) + const confirmationTimeoutRef = useRef(null) + const { stdout } = useStdout() + const terminalWidth = stdout.columns || 80 + const terminalHeight = stdout.rows || 24 + + useEffect(() => { + setMessages(messageBuffer.getMessages()) + + const unsubscribe = messageBuffer.onUpdate((newMessages) => { + setMessages(newMessages) + }) + + return () => { + unsubscribe() + if (confirmationTimeoutRef.current) { + clearTimeout(confirmationTimeoutRef.current) + } + } + }, [messageBuffer]) + + const resetConfirmation = useCallback(() => { + setConfirmationMode(false) + if (confirmationTimeoutRef.current) { + clearTimeout(confirmationTimeoutRef.current) + confirmationTimeoutRef.current = null + } + }, []) + + const setConfirmationWithTimeout = useCallback(() => { + setConfirmationMode(true) + if (confirmationTimeoutRef.current) { + clearTimeout(confirmationTimeoutRef.current) + } + confirmationTimeoutRef.current = setTimeout(() => { + resetConfirmation() + }, 15000) + }, [resetConfirmation]) + + useInput(useCallback(async (input, key) => { + if (actionInProgress) return + + if (key.ctrl && input === 'c') { + if (confirmationMode) { + resetConfirmation() + setActionInProgress(true) + await new Promise(resolve => setTimeout(resolve, 100)) + onExit?.() + } else { + setConfirmationWithTimeout() + } + return + } + + if (confirmationMode) { + resetConfirmation() + } + }, [confirmationMode, actionInProgress, onExit, setConfirmationWithTimeout, resetConfirmation])) + + const getMessageColor = (type: BufferedMessage['type']): string => { + switch (type) { + case 'user': return 'magenta' + case 'assistant': return 'cyan' + case 'system': return 'blue' + case 'tool': return 'yellow' + case 'result': return 'green' + case 'status': return 'gray' + default: return 'white' + } + } + + const formatMessage = (msg: BufferedMessage): string => { + const lines = msg.content.split('\n') + const maxLineLength = terminalWidth - 10 + return lines.map(line => { + if (line.length <= maxLineLength) return line + const chunks: string[] = [] + for (let i = 0; i < line.length; i += maxLineLength) { + chunks.push(line.slice(i, i + maxLineLength)) + } + return chunks.join('\n') + }).join('\n') + } + + const modelLabel = currentModel || 'kimi-k2.5' + + return ( + + + + šŸŒ™ Kimi Agent Messages ({modelLabel}) + {'─'.repeat(Math.min(terminalWidth - 4, 60))} + + + + {messages.length === 0 ? ( + Waiting for messages... + ) : ( + messages.slice(-Math.max(1, terminalHeight - 10)).map((msg) => ( + + + {formatMessage(msg)} + + + )) + )} + + + + + + {actionInProgress ? ( + + Exiting agent... + + ) : confirmationMode ? ( + + āš ļø Press Ctrl-C again to exit the agent + + ) : ( + <> + + šŸŒ™ Kimi Agent Running • Ctrl-C to exit + + + )} + {process.env.DEBUG && logPath && ( + + Debug logs: {logPath} + + )} + + + + ) +} diff --git a/packages/happy-cli/src/utils/createSessionMetadata.ts b/packages/happy-cli/src/utils/createSessionMetadata.ts index b4e05a3d3e..1cda3cc66a 100644 --- a/packages/happy-cli/src/utils/createSessionMetadata.ts +++ b/packages/happy-cli/src/utils/createSessionMetadata.ts @@ -19,7 +19,7 @@ import packageJson from '../../package.json'; /** * Backend flavor identifier for session metadata. */ -export type BackendFlavor = 'claude' | 'codex' | 'gemini' | 'opencode' | 'acp'; +export type BackendFlavor = 'claude' | 'codex' | 'gemini' | 'kimi' | 'opencode' | 'acp'; /** * Options for creating session metadata.