From 58b43c791174c2afb5a9cbcd4bc95c3739b2f72f Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Thu, 19 Mar 2026 20:53:31 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20add=20Kimi=20CLI=20(Moonshot=20AI)?= =?UTF-8?q?=20support=20=E2=80=94=20connect,=20command,=20session=20tracki?= =?UTF-8?q?ng?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds initial integration for Kimi CLI (github.com/MoonshotAI/kimi-cli, 7k+ stars) so users can authenticate and run Kimi through Happy. Changes: - `happy connect kimi` — stores Moonshot API key in Happy cloud via the same vendor-token flow used by Codex/Claude/Gemini. Prompts for MOONSHOT_API_KEY (sk-...) and registers it under the 'moonshot' vendor key. Also shows in `happy connect status`. - `happy kimi` command — registered in the CLI entry point, delegates to the generic ACP runner with kimi-specific defaults. Requires kimi-cli to be installed and in PATH. - authenticateKimi.ts — interactive API key prompt with basic format validation (sk- prefix check). - KimiAuthTokens type added to connect/types.ts. - BackendFlavor union extended with 'kimi' for session metadata. - Help text updated to list kimi alongside codex/gemini. Full native kimi integration (dedicated runKimi module with ink UI, conversation history, permission handling) is left for a follow-up once the kimi-cli ACP protocol details are finalized. --- packages/happy-cli/src/commands/connect.ts | 21 +++++-- .../src/commands/connect/authenticateKimi.ts | 62 +++++++++++++++++++ .../happy-cli/src/commands/connect/types.ts | 5 ++ packages/happy-cli/src/index.ts | 50 +++++++++++++++ .../src/utils/createSessionMetadata.ts | 2 +- 5 files changed, 135 insertions(+), 5 deletions(-) create mode 100644 packages/happy-cli/src/commands/connect/authenticateKimi.ts diff --git a/packages/happy-cli/src/commands/connect.ts b/packages/happy-cli/src/commands/connect.ts index ac9311a4f2..fd867d1305 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,16 @@ 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'); process.exit(0); } else { throw new Error(`Unsupported vendor: ${vendor}`); @@ -135,10 +147,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) { 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/index.ts b/packages/happy-cli/src/index.ts index ca3c031925..f6d094e480 100644 --- a/packages/happy-cli/src/index.ts +++ b/packages/happy-cli/src/index.ts @@ -342,6 +342,55 @@ 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, resolveAcpAgentConfig } = 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(); + + 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: ['--experimental-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 +683,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/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. From 79832c9db362970f4281a87c8a564858fbb5398b Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Thu, 19 Mar 2026 21:13:54 +0800 Subject: [PATCH 2/5] fix: wire kimi into daemon, profiles, API types, and session flavor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses gaps found in gap analysis — kimi was only registered in the CLI entry point and connect command, but missing from several internal subsystems: - daemon/run.ts: add 'kimi' to agent type union, command switch, tmux agent resolution, and MOONSHOT_API_KEY token handling - agent/acp/runAcp.ts: return 'kimi' flavor instead of generic 'acp' so sessions are properly tagged - persistence.ts: add kimi to ProfileCompatibilitySchema and validateProfileForAgent so profiles can enable/disable kimi - api/api.ts: add 'moonshot' to registerVendorToken/getVendorToken type signatures - index.ts: fetch Moonshot API key from Happy cloud on startup (mirrors how Gemini fetches its OAuth token) --- packages/happy-cli/src/agent/acp/runAcp.ts | 5 ++++- packages/happy-cli/src/api/api.ts | 4 ++-- packages/happy-cli/src/daemon/run.ts | 13 +++++++++---- packages/happy-cli/src/index.ts | 14 +++++++++++++- packages/happy-cli/src/persistence.ts | 5 +++-- 5 files changed, 31 insertions(+), 10 deletions(-) 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/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 f6d094e480..735fdbd4f9 100644 --- a/packages/happy-cli/src/index.ts +++ b/packages/happy-cli/src/index.ts @@ -348,7 +348,7 @@ import { extractNoSandboxFlag } from './utils/sandboxFlags' // available in PATH. Full native integration is planned; for now // this bootstraps auth + session tracking through Happy. try { - const { runAcp, resolveAcpAgentConfig } = await import('@/agent/acp'); + const { runAcp } = await import('@/agent/acp'); let startedBy: 'daemon' | 'terminal' | undefined = undefined; const kimiArgs: string[] = []; @@ -364,6 +364,18 @@ import { extractNoSandboxFlag } from './utils/sandboxFlags' 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...'); 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]; } From 59cb32df4373151e9376d2ce4ebd1c5addb1ba3e Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Fri, 20 Mar 2026 02:36:41 +0800 Subject: [PATCH 3/5] feat: add kimi module skeleton, UI display, and app-side mode options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fills in the remaining integration gaps so kimi is a proper first-class provider rather than a thin ACP wrapper: CLI package: - kimi/constants.ts: MOONSHOT_API_KEY_ENV, KIMI_MODEL_ENV, defaults - kimi/types.ts: KimiMode interface (permission + model) - ui/ink/KimiDisplay.tsx: terminal UI component (based on Codex display pattern — message buffer, Ctrl-C double-tap exit, model label) - connect.ts: save API key to ~/.kimi/config.json on `happy connect kimi` (mirrors Gemini's local credential sync) Mobile app: - modelModeOptions.ts: add getKimiPermissionModes (default + yolo), getKimiModelModes (kimi-latest fallback), wire into getHardcodedPermissionModes / getHardcodedModelModes / getAvailablePermissionModes / getDefaultModelKey --- .../sources/components/modelModeOptions.ts | 27 ++- packages/happy-cli/src/commands/connect.ts | 36 ++++ packages/happy-cli/src/kimi/constants.ts | 14 ++ packages/happy-cli/src/kimi/types.ts | 16 ++ packages/happy-cli/src/ui/ink/KimiDisplay.tsx | 170 ++++++++++++++++++ 5 files changed, 262 insertions(+), 1 deletion(-) create mode 100644 packages/happy-cli/src/kimi/constants.ts create mode 100644 packages/happy-cli/src/kimi/types.ts create mode 100644 packages/happy-cli/src/ui/ink/KimiDisplay.tsx diff --git a/packages/happy-app/sources/components/modelModeOptions.ts b/packages/happy-app/sources/components/modelModeOptions.ts index 6d86d56079..1d146b4002 100644 --- a/packages/happy-app/sources/components/modelModeOptions.ts +++ b/packages/happy-app/sources/components/modelModeOptions.ts @@ -29,6 +29,10 @@ 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-latest', name: 'Kimi Latest', description: 'Most capable' }, +]; + export function mapMetadataOptions(options?: MetadataOption[] | null): ModeOption[] { if (!options || options.length === 0) { return []; @@ -93,6 +97,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 +116,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 +129,9 @@ export function getHardcodedModelModes(flavor: AgentFlavor, translate: Translate if (flavor === 'gemini') { return getGeminiModelModes(); } + if (flavor === 'kimi') { + return getKimiModelModes(); + } return getClaudeModelModes(); } @@ -130,7 +152,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 +191,9 @@ export function getDefaultModelKey(flavor: AgentFlavor): string { if (flavor === 'gemini') { return 'gemini-2.5-pro'; } + if (flavor === 'kimi') { + return 'kimi-latest'; + } return 'default'; } diff --git a/packages/happy-cli/src/commands/connect.ts b/packages/happy-cli/src/commands/connect.ts index fd867d1305..afe79db87a 100644 --- a/packages/happy-cli/src/commands/connect.ts +++ b/packages/happy-cli/src/commands/connect.ts @@ -123,6 +123,10 @@ async function handleConnectVendor(vendor: 'codex' | 'claude' | 'gemini' | 'kimi 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}`); @@ -229,4 +233,36 @@ function updateLocalGeminiCredentials(tokens: { // Non-critical error - server tokens will still work console.log(chalk.yellow(` āš ļø Could not update local credentials: ${error}`)); } +} + +/** + * Save Moonshot API key locally so `kimi` CLI can find it via MOONSHOT_API_KEY. + * We write a small config file to ~/.kimi/config.json — Kimi CLI checks this + * path as one of its credential sources. + */ +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/kimi/constants.ts b/packages/happy-cli/src/kimi/constants.ts new file mode 100644 index 0000000000..05e1e71388 --- /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-latest'; 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/ui/ink/KimiDisplay.tsx b/packages/happy-cli/src/ui/ink/KimiDisplay.tsx new file mode 100644 index 0000000000..5c2fa0fa3a --- /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-latest' + + 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} + + )} + + + + ) +} From 068b3f1aefae475d392154ab9667552e20edf01b Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+universeplayer@users.noreply.github.com> Date: Fri, 20 Mar 2026 02:55:22 +0800 Subject: [PATCH 4/5] fix: use real Moonshot API model IDs instead of placeholder Replace made-up 'kimi-latest' with actual model IDs from platform.moonshot.ai: kimi-k2.5 (default), kimi-k2-thinking, and kimi-k2-thinking-turbo. --- packages/happy-app/sources/components/modelModeOptions.ts | 6 ++++-- packages/happy-cli/src/kimi/constants.ts | 2 +- packages/happy-cli/src/ui/ink/KimiDisplay.tsx | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/happy-app/sources/components/modelModeOptions.ts b/packages/happy-app/sources/components/modelModeOptions.ts index 1d146b4002..b39ca13914 100644 --- a/packages/happy-app/sources/components/modelModeOptions.ts +++ b/packages/happy-app/sources/components/modelModeOptions.ts @@ -30,7 +30,9 @@ const GEMINI_MODEL_FALLBACKS: ModelMode[] = [ ]; const KIMI_MODEL_FALLBACKS: ModelMode[] = [ - { key: 'kimi-latest', name: 'Kimi Latest', description: 'Most capable' }, + { 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[] { @@ -192,7 +194,7 @@ export function getDefaultModelKey(flavor: AgentFlavor): string { return 'gemini-2.5-pro'; } if (flavor === 'kimi') { - return 'kimi-latest'; + return 'kimi-k2.5'; } return 'default'; } diff --git a/packages/happy-cli/src/kimi/constants.ts b/packages/happy-cli/src/kimi/constants.ts index 05e1e71388..7df9f282bd 100644 --- a/packages/happy-cli/src/kimi/constants.ts +++ b/packages/happy-cli/src/kimi/constants.ts @@ -11,4 +11,4 @@ export const MOONSHOT_API_KEY_ENV = 'MOONSHOT_API_KEY'; export const KIMI_MODEL_ENV = 'KIMI_MODEL'; /** Default Kimi model */ -export const DEFAULT_KIMI_MODEL = 'kimi-latest'; +export const DEFAULT_KIMI_MODEL = 'kimi-k2.5'; diff --git a/packages/happy-cli/src/ui/ink/KimiDisplay.tsx b/packages/happy-cli/src/ui/ink/KimiDisplay.tsx index 5c2fa0fa3a..c560d9db32 100644 --- a/packages/happy-cli/src/ui/ink/KimiDisplay.tsx +++ b/packages/happy-cli/src/ui/ink/KimiDisplay.tsx @@ -96,7 +96,7 @@ export const KimiDisplay: React.FC = ({ messageBuffer, logPath }).join('\n') } - const modelLabel = currentModel || 'kimi-latest' + const modelLabel = currentModel || 'kimi-k2.5' return ( From d1fe505abc5a2a659b7cf6b9c650889d569019de Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+universeplayer@users.noreply.github.com> Date: Fri, 20 Mar 2026 03:07:03 +0800 Subject: [PATCH 5/5] fix: use correct kimi-cli ACP subcommand and fix misleading comment - kimi-cli uses 'kimi acp' subcommand, not --experimental-acp (that's Gemini) - Fix comment: config.json is Happy's credential cache, not read by kimi-cli --- packages/happy-cli/src/commands/connect.ts | 6 +++--- packages/happy-cli/src/index.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/happy-cli/src/commands/connect.ts b/packages/happy-cli/src/commands/connect.ts index afe79db87a..2faa4a8c1a 100644 --- a/packages/happy-cli/src/commands/connect.ts +++ b/packages/happy-cli/src/commands/connect.ts @@ -236,9 +236,9 @@ function updateLocalGeminiCredentials(tokens: { } /** - * Save Moonshot API key locally so `kimi` CLI can find it via MOONSHOT_API_KEY. - * We write a small config file to ~/.kimi/config.json — Kimi CLI checks this - * path as one of its credential sources. + * 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 { diff --git a/packages/happy-cli/src/index.ts b/packages/happy-cli/src/index.ts index 735fdbd4f9..f4c7c47088 100644 --- a/packages/happy-cli/src/index.ts +++ b/packages/happy-cli/src/index.ts @@ -393,7 +393,7 @@ import { extractNoSandboxFlag } from './utils/sandboxFlags' startedBy, agentName: 'kimi', command: 'kimi', - args: ['--experimental-acp', ...kimiArgs], + args: ['acp', ...kimiArgs], }); } catch (error) { console.error(chalk.red('Error:'), error instanceof Error ? error.message : 'Unknown error')