From 94b03c7f75fd398282a0737c497f882aeb708724 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Tue, 17 Feb 2026 17:02:45 +0700 Subject: [PATCH 1/7] refactor(cliproxy): DRY provider lists into single source of truth - unify CLIPROXY_SUPPORTED_PROVIDERS to import from CLIPROXY_PROVIDER_IDS - replace 4 inline union types with CLIProxyProvider import - replace hardcoded provider arrays in migration-manager and proxy-routes - remove duplicate PROVIDER_DISPLAY_NAMES, use getProviderDisplayName() - sync test now imports from ui/src/lib/provider-config instead of hardcoded array Adding a new CLIProxy provider no longer requires updating 14+ hardcoded lists. --- src/cliproxy/config/generator.ts | 20 ++---------------- src/config/migration-manager.ts | 3 ++- src/config/unified-config-types.ts | 21 +++++++------------ src/types/config.ts | 4 +++- src/web-server/routes/proxy-routes.ts | 3 ++- .../backend-ui-provider-arrays-sync.test.ts | 19 ++++------------- 6 files changed, 20 insertions(+), 50 deletions(-) diff --git a/src/cliproxy/config/generator.ts b/src/cliproxy/config/generator.ts index dd1799bc..ca832297 100644 --- a/src/cliproxy/config/generator.ts +++ b/src/cliproxy/config/generator.ts @@ -6,6 +6,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { CLIProxyProvider, ProviderConfig } from '../types'; +import { getProviderDisplayName } from '../provider-capabilities'; import { getModelMappingFromConfig } from '../base-config-loader'; import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader'; import { getEffectiveApiKey, getEffectiveManagementSecret } from '../auth-token-manager'; @@ -46,34 +47,17 @@ const DEFAULT_ANTIGRAVITY_ALIASES: Array<{ name: string; alias: string; fork?: b { name: 'claude-opus-4-6-thinking', alias: 'gemini-claude-opus-4-6-thinking', fork: true }, ]; -/** Provider display names (static metadata) */ -const PROVIDER_DISPLAY_NAMES: Record = { - gemini: 'Gemini', - codex: 'Codex', - agy: 'Antigravity', - qwen: 'Qwen Code', - iflow: 'iFlow', - kiro: 'Kiro (AWS)', - ghcp: 'GitHub Copilot (OAuth)', - claude: 'Claude (Anthropic)', -}; - /** * Get provider configuration * Model mappings are loaded from config/base-{provider}.settings.json */ export function getProviderConfig(provider: CLIProxyProvider): ProviderConfig { - const displayName = PROVIDER_DISPLAY_NAMES[provider]; - if (!displayName) { - throw new Error(`Unknown provider: ${provider}`); - } - // Load models from base config file const models = getModelMappingFromConfig(provider); return { name: provider, - displayName, + displayName: getProviderDisplayName(provider), models, requiresOAuth: true, // All CLIProxy providers require OAuth }; diff --git a/src/config/migration-manager.ts b/src/config/migration-manager.ts index 9fa347d4..e14342a2 100644 --- a/src/config/migration-manager.ts +++ b/src/config/migration-manager.ts @@ -18,6 +18,7 @@ import { getCcsDir } from '../utils/config-manager'; import { expandPath } from '../utils/helpers'; import type { ProfileConfig, AccountConfig, CLIProxyVariantConfig } from './unified-config-types'; import { createEmptyUnifiedConfig } from './unified-config-types'; +import { CLIPROXY_PROVIDER_IDS } from '../cliproxy/provider-capabilities'; import { saveUnifiedConfig, hasUnifiedConfig, loadUnifiedConfig } from './unified-config-loader'; import { infoBox, warn } from '../utils/ui'; @@ -203,7 +204,7 @@ export async function migrate(dryRun = false): Promise { // 6b. Migrate built-in CLIProxy OAuth profile settings (gemini, codex, agy, qwen, iflow) // Keep settings in *.settings.json files - only record reference in config.yaml // This matches Claude's ~/.claude/settings.json pattern for user familiarity - const builtInProviders = ['gemini', 'codex', 'agy', 'qwen', 'iflow']; + const builtInProviders = [...CLIPROXY_PROVIDER_IDS]; for (const provider of builtInProviders) { const settingsFile = `${provider}.settings.json`; const settingsPath = path.join(ccsDir, settingsFile); diff --git a/src/config/unified-config-types.ts b/src/config/unified-config-types.ts index 6e6368c0..3420d406 100644 --- a/src/config/unified-config-types.ts +++ b/src/config/unified-config-types.ts @@ -10,6 +10,8 @@ */ import type { TargetType } from '../targets/target-adapter'; +import type { CLIProxyProvider } from '../cliproxy/types'; +import { CLIPROXY_PROVIDER_IDS } from '../cliproxy/provider-capabilities'; /** * Unified config version. @@ -25,18 +27,9 @@ export const UNIFIED_CONFIG_VERSION = 8; /** * Supported CLIProxy providers. - * Includes all OAuth-based providers supported by CLIProxyAPI. + * Derived from CLIPROXY_PROVIDER_IDS — single source of truth in provider-capabilities.ts. */ -export const CLIPROXY_SUPPORTED_PROVIDERS = [ - 'gemini', - 'codex', - 'agy', - 'qwen', - 'iflow', - 'kiro', - 'ghcp', - 'claude', -] as const; +export const CLIPROXY_SUPPORTED_PROVIDERS = CLIPROXY_PROVIDER_IDS; /** * Account configuration (formerly in profiles.json). @@ -80,7 +73,7 @@ export type OAuthAccounts = Record; */ export interface CLIProxyVariantConfig { /** Base provider to use */ - provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp' | 'claude'; + provider: CLIProxyProvider; /** Account nickname (references oauth_accounts) */ account?: string; /** Path to settings file (e.g., "~/.ccs/gemini-custom.settings.json") */ @@ -98,14 +91,14 @@ export interface CLIProxyVariantConfig { */ export interface CompositeTierConfig { /** Provider for this tier */ - provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp' | 'claude'; + provider: CLIProxyProvider; /** Model ID to use for this tier */ model: string; /** Account nickname (optional, references oauth_accounts) */ account?: string; /** Fallback provider+model if primary fails */ fallback?: { - provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp' | 'claude'; + provider: CLIProxyProvider; model: string; account?: string; }; diff --git a/src/types/config.ts b/src/types/config.ts index db910441..952355a9 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -3,6 +3,8 @@ * Source: ~/.ccs/config.json */ +import type { CLIProxyProvider } from '../cliproxy/types'; + /** * Profile configuration mapping * Maps profile names to settings.json paths @@ -18,7 +20,7 @@ export interface ProfilesConfig { */ export interface CLIProxyVariantConfig { /** CLIProxy provider to use */ - provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp' | 'claude'; + provider: CLIProxyProvider; /** Path to settings.json with custom model configuration (optional) */ settings?: string; /** Account identifier for multi-account support (optional, defaults to 'default') */ diff --git a/src/web-server/routes/proxy-routes.ts b/src/web-server/routes/proxy-routes.ts index b4cc3f68..a8c92f84 100644 --- a/src/web-server/routes/proxy-routes.ts +++ b/src/web-server/routes/proxy-routes.ts @@ -16,6 +16,7 @@ import { DEFAULT_CLIPROXY_SERVER_CONFIG, CliproxyServerConfig, } from '../../config/unified-config-types'; +import { CLIPROXY_PROVIDER_IDS } from '../../cliproxy/provider-capabilities'; const router = Router(); @@ -113,7 +114,7 @@ router.put('/backend', async (req: Request, res: Response) => { config.cliproxy = { backend, oauth_accounts: {}, - providers: ['gemini', 'codex', 'agy', 'qwen', 'iflow', 'kiro', 'ghcp'], + providers: [...CLIPROXY_PROVIDER_IDS], variants: {}, }; } else { diff --git a/tests/unit/cliproxy/backend-ui-provider-arrays-sync.test.ts b/tests/unit/cliproxy/backend-ui-provider-arrays-sync.test.ts index 50f48dea..15b6d489 100644 --- a/tests/unit/cliproxy/backend-ui-provider-arrays-sync.test.ts +++ b/tests/unit/cliproxy/backend-ui-provider-arrays-sync.test.ts @@ -7,34 +7,23 @@ import { describe, expect, test } from 'bun:test'; import { CLIPROXY_PROFILES } from '../../../src/auth/profile-detector'; - -// UI providers (must manually sync - this test validates the sync) -const UI_CLIPROXY_PROVIDERS = [ - 'gemini', - 'codex', - 'agy', - 'qwen', - 'iflow', - 'kiro', - 'ghcp', - 'claude', -] as const; +import { CLIPROXY_PROVIDERS } from '../../../ui/src/lib/provider-config'; describe('Provider Sync', () => { test('backend CLIPROXY_PROFILES matches UI CLIPROXY_PROVIDERS', () => { const backend = [...CLIPROXY_PROFILES].sort(); - const ui = [...UI_CLIPROXY_PROVIDERS].sort(); + const ui = [...CLIPROXY_PROVIDERS].sort(); expect(backend).toEqual(ui); }); test('both arrays have same length', () => { - expect(CLIPROXY_PROFILES.length).toBe(UI_CLIPROXY_PROVIDERS.length); + expect(CLIPROXY_PROFILES.length).toBe(CLIPROXY_PROVIDERS.length); }); test('UI array contains all backend providers', () => { for (const provider of CLIPROXY_PROFILES) { - expect(UI_CLIPROXY_PROVIDERS).toContain(provider); + expect(CLIPROXY_PROVIDERS).toContain(provider); } }); }); From 08b2a6791398912ff5ea6b3cce18b37552c1ef10 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Tue, 17 Feb 2026 17:02:55 +0700 Subject: [PATCH 2/7] feat(cliproxy): add Kimi as OAuth CLIProxy provider - add 'kimi' to CLIProxyProvider union type - register Kimi capabilities (device_code flow, moonshot alias) - add OAuth config, auth prefixes, type values, channel maps - add CLIProxy-delegated token refresh for Kimi - add Kimi model catalog (k2.5, k2-thinking, k2) - switch base-kimi.settings.json to CLIProxy mode (127.0.0.1:8317) - update comments removing kimi from settings-based profile mentions Closes #574 --- config/base-kimi.settings.json | 17 +++++---- src/auth/profile-detector.ts | 2 +- src/ccs.ts | 4 +-- src/cliproxy/auth/auth-types.ts | 13 +++++++ .../auth/provider-refreshers/index.ts | 4 ++- src/cliproxy/model-catalog.ts | 36 +++++++++++++++++++ src/cliproxy/provider-capabilities.ts | 6 ++++ src/cliproxy/types.ts | 4 ++- 8 files changed, 72 insertions(+), 14 deletions(-) diff --git a/config/base-kimi.settings.json b/config/base-kimi.settings.json index ad4b2dee..5b72a69f 100644 --- a/config/base-kimi.settings.json +++ b/config/base-kimi.settings.json @@ -1,11 +1,10 @@ { "env": { - "ANTHROPIC_BASE_URL": "https://api.kimi.com/coding/", - "ANTHROPIC_AUTH_TOKEN": "YOUR_KIMI_API_KEY_HERE", - "ANTHROPIC_MODEL": "kimi-k2-thinking-turbo", - "ANTHROPIC_DEFAULT_OPUS_MODEL": "kimi-k2-thinking-turbo", - "ANTHROPIC_DEFAULT_SONNET_MODEL": "kimi-k2-thinking-turbo", - "ANTHROPIC_DEFAULT_HAIKU_MODEL": "kimi-k2-thinking-turbo" - }, - "alwaysThinkingEnabled": true -} \ No newline at end of file + "ANTHROPIC_BASE_URL": "http://127.0.0.1:8317/api/provider/kimi", + "ANTHROPIC_AUTH_TOKEN": "ccs-internal-managed", + "ANTHROPIC_MODEL": "kimi-k2.5", + "ANTHROPIC_DEFAULT_OPUS_MODEL": "kimi-k2.5", + "ANTHROPIC_DEFAULT_SONNET_MODEL": "kimi-k2-thinking", + "ANTHROPIC_DEFAULT_HAIKU_MODEL": "kimi-k2" + } +} diff --git a/src/auth/profile-detector.ts b/src/auth/profile-detector.ts index 23570249..f700b030 100644 --- a/src/auth/profile-detector.ts +++ b/src/auth/profile-detector.ts @@ -308,7 +308,7 @@ class ProfileDetector { }; } - // Priority 3: Check settings-based profiles (glm, kimi) - LEGACY FALLBACK + // Priority 3: Check settings-based profiles (glm) - LEGACY FALLBACK if (config.profiles && config.profiles[profileName]) { return { type: 'settings', diff --git a/src/ccs.ts b/src/ccs.ts index b1df1571..1bf77d6d 100644 --- a/src/ccs.ts +++ b/src/ccs.ts @@ -792,7 +792,7 @@ async function main(): Promise { const exitCode = await executeCopilotProfile(copilotConfig, remainingArgs); process.exit(exitCode); } else if (profileInfo.type === 'settings') { - // Settings-based profiles (glm, glmt, kimi) are third-party providers + // Settings-based profiles (glm, glmt) are third-party providers // WebSearch is server-side tool - third-party providers have no access // Inject WebSearch hook into profile settings before launch ensureProfileHooks(profileInfo.name); @@ -869,7 +869,7 @@ async function main(): Promise { // GLMT FLOW: Settings-based with embedded proxy for thinking support await execClaudeWithProxy(claudeCli, profileInfo.name, remainingArgs); } else { - // EXISTING FLOW: Settings-based profile (glm, kimi) + // EXISTING FLOW: Settings-based profile (glm) // Use --settings flag (backward compatible) const expandedSettingsPath = getSettingsPath(profileInfo.name); const webSearchEnv = getWebSearchHookEnv(); diff --git a/src/cliproxy/auth/auth-types.ts b/src/cliproxy/auth/auth-types.ts index 91feaa70..acb10a43 100644 --- a/src/cliproxy/auth/auth-types.ts +++ b/src/cliproxy/auth/auth-types.ts @@ -88,6 +88,7 @@ export function toKiroManagementMethod(method: KiroAuthMethod): 'aws' | 'google' * - Kiro: Device Code Flow (polling-based, NO callback port needed) * - Qwen: Device Code Flow (polling-based, NO callback port needed) * - GHCP: Device Code Flow (polling-based, NO callback port needed) + * - Kimi: Device Code Flow (polling-based, NO callback port needed) */ export const OAUTH_CALLBACK_PORTS: Partial> = { gemini: 8085, @@ -98,6 +99,7 @@ export const OAUTH_CALLBACK_PORTS: Partial> = { // kiro: Device Code Flow - no callback port // qwen: Device Code Flow - no callback port // ghcp: Device Code Flow - no callback port + // kimi: Device Code Flow - no callback port }; /** @@ -199,6 +201,13 @@ export const OAUTH_CONFIGS: Record = { scopes: ['user:inference', 'user:profile'], authFlag: '--claude-login', }, + kimi: { + provider: 'kimi', + displayName: 'Kimi (Moonshot)', + authUrl: 'https://auth.kimi.com/api/oauth/device_authorization', + scopes: ['api'], + authFlag: '--kimi-login', + }, }; /** @@ -215,6 +224,7 @@ export const PROVIDER_AUTH_PREFIXES: Record = { kiro: ['kiro-', 'aws-', 'codewhisperer-'], ghcp: ['github-copilot-', 'copilot-', 'gh-'], claude: ['claude-', 'anthropic-'], + kimi: ['kimi-'], }; /** @@ -230,6 +240,7 @@ export const PROVIDER_TYPE_VALUES: Record = { kiro: ['kiro', 'codewhisperer'], ghcp: ['github-copilot', 'copilot'], claude: ['claude', 'anthropic'], + kimi: ['kimi'], }; /** @@ -245,6 +256,7 @@ export const CLIPROXY_CALLBACK_PROVIDER_MAP: Record = claude: 'anthropic', qwen: 'qwen', iflow: 'iflow', + kimi: 'kimi', }; /** @@ -261,6 +273,7 @@ export const CLIPROXY_AUTH_URL_PROVIDER_MAP: Record = claude: 'anthropic', qwen: 'qwen', iflow: 'iflow', + kimi: 'kimi', }; /** diff --git a/src/cliproxy/auth/provider-refreshers/index.ts b/src/cliproxy/auth/provider-refreshers/index.ts index 3c642340..abcf3ff8 100644 --- a/src/cliproxy/auth/provider-refreshers/index.ts +++ b/src/cliproxy/auth/provider-refreshers/index.ts @@ -5,7 +5,7 @@ * * Refresh responsibility: * - CCS-managed: gemini (CCS refreshes tokens directly via Google OAuth) - * - CLIProxy-delegated: codex, agy, kiro, ghcp, qwen, iflow + * - CLIProxy-delegated: codex, agy, kiro, ghcp, qwen, iflow, kimi * (CLIProxyAPIPlus handles refresh automatically in background) * - Not implemented: claude */ @@ -34,6 +34,7 @@ const CLIPROXY_DELEGATED_REFRESH: CLIProxyProvider[] = [ 'ghcp', 'qwen', 'iflow', + 'kimi', ]; /** @@ -63,6 +64,7 @@ export async function refreshToken( case 'iflow': case 'kiro': case 'ghcp': + case 'kimi': // CLIProxyAPIPlus handles refresh for these providers automatically. // No action needed from CCS — report success with delegated flag. return { success: true, delegated: true }; diff --git a/src/cliproxy/model-catalog.ts b/src/cliproxy/model-catalog.ts index e50d0ffc..c73e61bd 100644 --- a/src/cliproxy/model-catalog.ts +++ b/src/cliproxy/model-catalog.ts @@ -197,6 +197,42 @@ export const MODEL_CATALOG: Partial> = }, ], }, + kimi: { + provider: 'kimi', + displayName: 'Kimi (Moonshot)', + defaultModel: 'kimi-k2.5', + models: [ + { + id: 'kimi-k2.5', + name: 'Kimi K2.5', + description: 'Latest Moonshot coding model', + thinking: { + type: 'budget', + min: 1024, + max: 32000, + zeroAllowed: true, + dynamicAllowed: true, + }, + }, + { + id: 'kimi-k2-thinking', + name: 'Kimi K2 Thinking', + description: 'Extended reasoning model', + thinking: { + type: 'budget', + min: 1024, + max: 32000, + zeroAllowed: true, + dynamicAllowed: true, + }, + }, + { + id: 'kimi-k2', + name: 'Kimi K2', + description: 'Flagship coding model', + }, + ], + }, claude: { provider: 'claude', displayName: 'Claude (Anthropic)', diff --git a/src/cliproxy/provider-capabilities.ts b/src/cliproxy/provider-capabilities.ts index 6dec96a8..4ddd4f27 100644 --- a/src/cliproxy/provider-capabilities.ts +++ b/src/cliproxy/provider-capabilities.ts @@ -62,6 +62,12 @@ export const PROVIDER_CAPABILITIES: Record Date: Tue, 17 Feb 2026 17:03:05 +0700 Subject: [PATCH 3/7] feat(cliproxy): rename kimi API preset to km, add kimi to UI - create config/base-km.settings.json for API-key Kimi users - rename preset id 'kimi' to 'km' in CLI and UI provider presets - add 'ccs km' to help text API section, keep 'ccs kimi' for OAuth - add --km delegation flag for API-key mode, --kimi stays for OAuth - add kimi to UI CLIPROXY_PROVIDERS, assets, colors, names, device code --- config/base-km.settings.json | 10 ++++++++++ src/api/services/provider-presets.ts | 4 ++-- src/commands/help-command.ts | 6 ++++-- ui/src/lib/provider-config.ts | 6 +++++- ui/src/lib/provider-presets.ts | 5 +++-- 5 files changed, 24 insertions(+), 7 deletions(-) create mode 100644 config/base-km.settings.json diff --git a/config/base-km.settings.json b/config/base-km.settings.json new file mode 100644 index 00000000..c60892d5 --- /dev/null +++ b/config/base-km.settings.json @@ -0,0 +1,10 @@ +{ + "env": { + "ANTHROPIC_BASE_URL": "https://api.kimi.com/coding/", + "ANTHROPIC_AUTH_TOKEN": "YOUR_KIMI_API_KEY_HERE", + "ANTHROPIC_MODEL": "kimi-k2-thinking-turbo", + "ANTHROPIC_DEFAULT_OPUS_MODEL": "kimi-k2-thinking-turbo", + "ANTHROPIC_DEFAULT_SONNET_MODEL": "kimi-k2-thinking-turbo", + "ANTHROPIC_DEFAULT_HAIKU_MODEL": "kimi-k2-thinking-turbo" + } +} diff --git a/src/api/services/provider-presets.ts b/src/api/services/provider-presets.ts index c57ca5c5..7daedaa3 100644 --- a/src/api/services/provider-presets.ts +++ b/src/api/services/provider-presets.ts @@ -93,11 +93,11 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [ alwaysThinkingEnabled: true, }, { - id: 'kimi', + id: 'km', name: 'Kimi', description: 'Moonshot AI - Fast reasoning model', baseUrl: 'https://api.kimi.com/coding/', - defaultProfileName: 'kimi', + defaultProfileName: 'km', defaultModel: 'kimi-k2-thinking-turbo', apiKeyPlaceholder: 'sk-...', apiKeyHint: 'Get your API key from Moonshot AI', diff --git a/src/commands/help-command.ts b/src/commands/help-command.ts index 5971dfb5..3a4c0340 100644 --- a/src/commands/help-command.ts +++ b/src/commands/help-command.ts @@ -131,7 +131,7 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim(); ['ccs', 'Use default Claude account'], ['ccs glm', 'GLM 5 (API key required)'], ['ccs glmt', 'GLM with thinking mode'], - ['ccs kimi', 'Kimi for Coding (API key)'], + ['ccs km', 'Kimi for Coding (API key)'], ['ccs ollama', 'Local Ollama (http://localhost:11434)'], ['ccs ollama-cloud', 'Ollama Cloud (API key required)'], ['', ''], // Spacer @@ -171,6 +171,7 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim(); ['ccs codex', 'OpenAI Codex (supports -medium/-high/-xhigh model suffixes)'], ['ccs agy', 'Antigravity (Claude/Gemini models)'], ['ccs qwen', 'Qwen Code (qwen3-coder)'], + ['ccs kimi', 'Kimi (Moonshot AI K2/K2.5 models)'], ['ccs kiro', 'Kiro (AWS CodeWhisperer Claude models)'], ['ccs ghcp', 'GitHub Copilot (OAuth via CLIProxy Plus)'], ['', ''], // Spacer @@ -255,7 +256,8 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim(); printSubSection('Delegation (inside Claude Code CLI)', [ ['/ccs "task"', 'Delegate task (auto-selects profile)'], ['/ccs --glm "task"', 'Force GLM-5 for simple tasks'], - ['/ccs --kimi "task"', 'Force Kimi for long context'], + ['/ccs --kimi "task"', 'Force Kimi OAuth for long context'], + ['/ccs --km "task"', 'Force Kimi API key for long context'], ['/ccs:continue "follow-up"', 'Continue last delegation session'], ]); diff --git a/ui/src/lib/provider-config.ts b/ui/src/lib/provider-config.ts index 05e2a958..cfcf4dbb 100644 --- a/ui/src/lib/provider-config.ts +++ b/ui/src/lib/provider-config.ts @@ -19,6 +19,7 @@ export const CLIPROXY_PROVIDERS = [ 'kiro', 'ghcp', 'claude', + 'kimi', ] as const; /** Union type for CLIProxy provider IDs */ @@ -39,6 +40,7 @@ export const PROVIDER_ASSETS: Record = { kiro: '/assets/providers/kiro.png', ghcp: '/assets/providers/copilot.svg', claude: '/assets/providers/claude.svg', + kimi: '/assets/providers/kimi.svg', }; // Provider brand colors @@ -52,6 +54,7 @@ export const PROVIDER_COLORS: Record = { kiro: '#4d908e', // Dark Cyan (AWS-inspired) ghcp: '#43aa8b', // Seaweed (GitHub-inspired) claude: '#D97757', // Anthropic brand color (matches SVG) + kimi: '#FF6B35', // Moonshot AI brand orange }; // Provider display names @@ -65,6 +68,7 @@ const PROVIDER_NAMES: Record = { kiro: 'Kiro (AWS)', ghcp: 'GitHub Copilot (OAuth)', claude: 'Claude (Anthropic)', + kimi: 'Kimi (Moonshot)', }; // Map provider to display name @@ -76,7 +80,7 @@ export function getProviderDisplayName(provider: string): string { * Providers that use Device Code OAuth flow instead of Authorization Code flow. * Device Code flow requires displaying a user code for manual entry at provider's website. */ -export const DEVICE_CODE_PROVIDERS: CLIProxyProvider[] = ['ghcp', 'kiro', 'qwen']; +export const DEVICE_CODE_PROVIDERS: CLIProxyProvider[] = ['ghcp', 'kiro', 'qwen', 'kimi']; /** Check if provider uses Device Code flow */ export function isDeviceCodeProvider(provider: string): boolean { diff --git a/ui/src/lib/provider-presets.ts b/ui/src/lib/provider-presets.ts index e998901d..451f0f9d 100644 --- a/ui/src/lib/provider-presets.ts +++ b/ui/src/lib/provider-presets.ts @@ -90,11 +90,11 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [ category: 'alternative', }, { - id: 'kimi', + id: 'km', name: 'Kimi', description: 'Moonshot AI - Fast reasoning model', baseUrl: 'https://api.kimi.com/coding/', - defaultProfileName: 'kimi', + defaultProfileName: 'km', badge: 'Reasoning', icon: '/icons/kimi.svg', defaultModel: 'kimi-k2-thinking-turbo', @@ -102,6 +102,7 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [ apiKeyPlaceholder: 'sk-...', apiKeyHint: 'Get your API key from Moonshot AI', category: 'alternative', + alwaysThinkingEnabled: true, }, { id: 'foundry', From 539afea7374f2c931ab5cbb1e04e0845c57729b5 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Tue, 17 Feb 2026 17:03:11 +0700 Subject: [PATCH 4/7] style: format source and test files --- src/management/checks/image-analysis-check.ts | 5 +- tests/e2e/image-analyzer-hook.e2e.test.ts | 86 ++++++++-- tests/unit/auth/profile-detector.test.ts | 16 +- .../account-safety-quota-exhaustion.test.ts | 6 +- .../unit/cliproxy/composite-fallback.test.ts | 3 +- .../cliproxy/extended-context-config.test.ts | 4 +- .../cliproxy/management-api-client.test.ts | 8 +- tests/unit/cliproxy/port-validation.test.ts | 5 +- .../cliproxy/quota-fetcher-gemini-cli.test.ts | 4 +- .../cliproxy/quota-monitor-runtime.test.ts | 6 +- .../unit/cliproxy/remote-proxy-client.test.ts | 5 +- tests/unit/cliproxy/schema-sanitizer.test.ts | 5 +- ...ool-sanitization-proxy-integration.test.ts | 11 +- tests/unit/commands/env-command.test.ts | 24 ++- tests/unit/commands/setup-command.test.ts | 14 +- .../commands/shell-completion-command.test.ts | 4 +- tests/unit/cursor/cursor-daemon.test.ts | 54 +++--- tests/unit/cursor/cursor-protobuf.test.ts | 20 ++- tests/unit/data-aggregator.test.ts | 8 +- .../delegation/delegation-handler.test.ts | 24 +-- .../unit/delegation/headless-executor.test.ts | 5 +- tests/unit/glmt/retry-logic.test.ts | 162 +++++++++++++----- tests/unit/jsonl-parser.test.ts | 9 +- tests/unit/mcp-manager.test.ts | 2 +- tests/unit/shared-manager.test.ts | 3 +- .../unit/targets/droid-config-manager.test.ts | 138 ++++++++++----- tests/unit/targets/target-resolver.test.ts | 6 +- tests/unit/unified-config.test.ts | 11 +- tests/unit/utils/expand-path.test.ts | 92 +++++----- tests/unit/utils/signal-forwarder.test.ts | 12 +- tests/unit/utils/websearch/hook-utils.test.ts | 5 +- tests/unit/web-server/auth-middleware.test.ts | 15 +- .../web-server/cliproxy-auth-routes.test.ts | 4 +- .../web-server/cursor-settings-routes.test.ts | 5 +- 34 files changed, 486 insertions(+), 295 deletions(-) diff --git a/src/management/checks/image-analysis-check.ts b/src/management/checks/image-analysis-check.ts index 39a0f7c5..e539bff4 100644 --- a/src/management/checks/image-analysis-check.ts +++ b/src/management/checks/image-analysis-check.ts @@ -94,9 +94,8 @@ export async function runImageAnalysisCheck(results: HealthCheck): Promise * Fix image analysis configuration issues */ export async function fixImageAnalysisConfig(): Promise { - const { updateUnifiedConfig, loadOrCreateUnifiedConfig } = await import( - '../../config/unified-config-loader' - ); + const { updateUnifiedConfig, loadOrCreateUnifiedConfig } = + await import('../../config/unified-config-loader'); const config = loadOrCreateUnifiedConfig(); let fixed = false; diff --git a/tests/e2e/image-analyzer-hook.e2e.test.ts b/tests/e2e/image-analyzer-hook.e2e.test.ts index 8df3adfa..86c5c95e 100644 --- a/tests/e2e/image-analyzer-hook.e2e.test.ts +++ b/tests/e2e/image-analyzer-hook.e2e.test.ts @@ -32,7 +32,8 @@ const MOCK_PORT = 59876; // Use a unique port for mock server const CLIPROXY_API_KEY = 'test-api-key-12345'; // Default provider models for testing (matches DEFAULT_IMAGE_ANALYSIS_CONFIG) -const DEFAULT_PROVIDER_MODELS = 'agy:gemini-2.5-flash,gemini:gemini-2.5-flash,codex:gpt-5.1-codex-mini,kiro:kiro-claude-haiku-4-5,ghcp:claude-haiku-4.5,claude:claude-haiku-4-5-20251001'; +const DEFAULT_PROVIDER_MODELS = + 'agy:gemini-2.5-flash,gemini:gemini-2.5-flash,codex:gpt-5.1-codex-mini,kiro:kiro-claude-haiku-4-5,ghcp:claude-haiku-4.5,claude:claude-haiku-4-5-20251001'; const DEFAULT_PROVIDER = 'agy'; // Default test provider // ============================================================================ @@ -168,13 +169,75 @@ function invokeHook( function createTestPng(filepath: string): void { // 1x1 PNG with a red pixel (RGB: 255, 0, 0) const png = Buffer.from([ - 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, // PNG signature - 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, // IHDR chunk - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, - 0xde, 0x00, 0x00, 0x00, 0x0c, 0x49, 0x44, 0x41, // IDAT chunk (red pixel) - 0x54, 0x08, 0xd7, 0x63, 0xf8, 0xcf, 0xc0, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x18, 0xdd, 0x8d, - 0xb4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, // IEND chunk - 0x44, 0xae, 0x42, 0x60, 0x82, + 0x89, + 0x50, + 0x4e, + 0x47, + 0x0d, + 0x0a, + 0x1a, + 0x0a, // PNG signature + 0x00, + 0x00, + 0x00, + 0x0d, + 0x49, + 0x48, + 0x44, + 0x52, // IHDR chunk + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x00, + 0x01, + 0x08, + 0x02, + 0x00, + 0x00, + 0x00, + 0x90, + 0x77, + 0x53, + 0xde, + 0x00, + 0x00, + 0x00, + 0x0c, + 0x49, + 0x44, + 0x41, // IDAT chunk (red pixel) + 0x54, + 0x08, + 0xd7, + 0x63, + 0xf8, + 0xcf, + 0xc0, + 0x00, + 0x00, + 0x01, + 0x01, + 0x01, + 0x00, + 0x18, + 0xdd, + 0x8d, + 0xb4, + 0x00, + 0x00, + 0x00, + 0x00, + 0x49, + 0x45, + 0x4e, // IEND chunk + 0x44, + 0xae, + 0x42, + 0x60, + 0x82, ]); fs.writeFileSync(filepath, png); } @@ -487,15 +550,14 @@ describe('Image Analyzer Hook', () => { expect(result.code).toBe(2); const output = JSON.parse(result.stdout); expect(output.decision).toBe('block'); - expect(output.hookSpecificOutput.permissionDecisionReason).toContain( - 'CLIProxy unavailable' - ); + expect(output.hookSpecificOutput.permissionDecisionReason).toContain('CLIProxy unavailable'); }); it('should analyze PNG via mock CLIProxy and return analysis', () => { resetMockState(); mockResponse = { - content: 'This image shows a small red square, likely a single pixel or very minimal graphic.', + content: + 'This image shows a small red square, likely a single pixel or very minimal graphic.', statusCode: 200, }; diff --git a/tests/unit/auth/profile-detector.test.ts b/tests/unit/auth/profile-detector.test.ts index 52f2ae68..91cefcd0 100644 --- a/tests/unit/auth/profile-detector.test.ts +++ b/tests/unit/auth/profile-detector.test.ts @@ -100,12 +100,14 @@ describe('ProfileDetector', () => { const mockUnifiedConfig = { version: 2, profiles: { - glm: { settings: settingsPath, type: 'api' } - } + glm: { settings: settingsPath, type: 'api' }, + }, }; const isUnifiedModeSpy = spyOn(unifiedConfigLoader, 'isUnifiedMode').mockReturnValue(true); - const loadUnifiedConfigSpy = spyOn(unifiedConfigLoader, 'loadUnifiedConfig').mockReturnValue(mockUnifiedConfig as any); + const loadUnifiedConfigSpy = spyOn(unifiedConfigLoader, 'loadUnifiedConfig').mockReturnValue( + mockUnifiedConfig as any + ); try { const result = detector.detectProfileType('glm'); @@ -122,12 +124,14 @@ describe('ProfileDetector', () => { const mockUnifiedConfig = { version: 2, accounts: { - work: { created: '2025-01-01', last_used: '2025-01-02' } - } + work: { created: '2025-01-01', last_used: '2025-01-02' }, + }, }; const isUnifiedModeSpy = spyOn(unifiedConfigLoader, 'isUnifiedMode').mockReturnValue(true); - const loadUnifiedConfigSpy = spyOn(unifiedConfigLoader, 'loadUnifiedConfig').mockReturnValue(mockUnifiedConfig as any); + const loadUnifiedConfigSpy = spyOn(unifiedConfigLoader, 'loadUnifiedConfig').mockReturnValue( + mockUnifiedConfig as any + ); try { const result = detector.detectProfileType('work'); diff --git a/tests/unit/cliproxy/account-safety-quota-exhaustion.test.ts b/tests/unit/cliproxy/account-safety-quota-exhaustion.test.ts index 855832e7..a24db225 100644 --- a/tests/unit/cliproxy/account-safety-quota-exhaustion.test.ts +++ b/tests/unit/cliproxy/account-safety-quota-exhaustion.test.ts @@ -13,7 +13,11 @@ import { describe, it, expect, beforeEach, afterEach } from 'bun:test'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; -import { handleQuotaExhaustion, writeQuotaWarning, maskEmail } from '../../../src/cliproxy/account-safety'; +import { + handleQuotaExhaustion, + writeQuotaWarning, + maskEmail, +} from '../../../src/cliproxy/account-safety'; // Setup test isolation let tmpDir: string; diff --git a/tests/unit/cliproxy/composite-fallback.test.ts b/tests/unit/cliproxy/composite-fallback.test.ts index 338f6a02..33a5f5d3 100644 --- a/tests/unit/cliproxy/composite-fallback.test.ts +++ b/tests/unit/cliproxy/composite-fallback.test.ts @@ -128,8 +128,7 @@ describe('detectFailedTier', () => { }); it('should match first tier when multiple models mentioned', () => { - const stderr = - 'Tried claude-opus-4-6-thinking, then gemini-3-pro-preview, both failed'; + const stderr = 'Tried claude-opus-4-6-thinking, then gemini-3-pro-preview, both failed'; const result = detectFailedTier(stderr, tiers); expect(result).toBe('opus'); // First match }); diff --git a/tests/unit/cliproxy/extended-context-config.test.ts b/tests/unit/cliproxy/extended-context-config.test.ts index 5e3a12af..697ba35e 100644 --- a/tests/unit/cliproxy/extended-context-config.test.ts +++ b/tests/unit/cliproxy/extended-context-config.test.ts @@ -57,9 +57,7 @@ describe('shouldApplyExtendedContext', () => { }); it('returns false for Claude models without explicit flag', () => { - expect(shouldApplyExtendedContext('agy', 'claude-opus-4-5-thinking', undefined)).toBe( - false - ); + expect(shouldApplyExtendedContext('agy', 'claude-opus-4-5-thinking', undefined)).toBe(false); }); it('returns false for Claude models without explicit flag', () => { diff --git a/tests/unit/cliproxy/management-api-client.test.ts b/tests/unit/cliproxy/management-api-client.test.ts index 1816f8ef..e3b6b7b4 100644 --- a/tests/unit/cliproxy/management-api-client.test.ts +++ b/tests/unit/cliproxy/management-api-client.test.ts @@ -427,9 +427,7 @@ describe('management-api-client', () => { describe('CRUD operations', () => { it('should get claude keys', async () => { const client = new ManagementApiClient(config); - const mockKeys: ClaudeKey[] = [ - { 'api-key': 'sk-test-123', prefix: 'glm-' }, - ]; + const mockKeys: ClaudeKey[] = [{ 'api-key': 'sk-test-123', prefix: 'glm-' }]; const originalFetch = global.fetch; global.fetch = mock(() => @@ -449,9 +447,7 @@ describe('management-api-client', () => { it('should put claude keys', async () => { const client = new ManagementApiClient(config); - const mockKeys: ClaudeKey[] = [ - { 'api-key': 'sk-test-456', prefix: 'kimi-' }, - ]; + const mockKeys: ClaudeKey[] = [{ 'api-key': 'sk-test-456', prefix: 'kimi-' }]; const originalFetch = global.fetch; let requestBody: string | undefined; diff --git a/tests/unit/cliproxy/port-validation.test.ts b/tests/unit/cliproxy/port-validation.test.ts index 71d7301a..9fb39153 100644 --- a/tests/unit/cliproxy/port-validation.test.ts +++ b/tests/unit/cliproxy/port-validation.test.ts @@ -12,10 +12,7 @@ */ import { describe, it, expect } from 'bun:test'; -import { - validatePort, - CLIPROXY_DEFAULT_PORT, -} from '../../../src/cliproxy/config-generator'; +import { validatePort, CLIPROXY_DEFAULT_PORT } from '../../../src/cliproxy/config-generator'; import { resolveProxyConfig } from '../../../src/cliproxy/proxy-config-resolver'; describe('Port Validation', () => { diff --git a/tests/unit/cliproxy/quota-fetcher-gemini-cli.test.ts b/tests/unit/cliproxy/quota-fetcher-gemini-cli.test.ts index 2a9e6b6f..c879d84b 100644 --- a/tests/unit/cliproxy/quota-fetcher-gemini-cli.test.ts +++ b/tests/unit/cliproxy/quota-fetcher-gemini-cli.test.ts @@ -67,9 +67,7 @@ describe('Gemini CLI Quota Fetcher', () => { }); it('should handle camelCase API response', () => { - const rawBuckets = [ - { modelId: 'gemini-3-flash-preview', remainingFraction: 0.75 }, - ]; + const rawBuckets = [{ modelId: 'gemini-3-flash-preview', remainingFraction: 0.75 }]; const buckets = buildGeminiCliBuckets(rawBuckets); diff --git a/tests/unit/cliproxy/quota-monitor-runtime.test.ts b/tests/unit/cliproxy/quota-monitor-runtime.test.ts index 9bb57284..7afbcc45 100644 --- a/tests/unit/cliproxy/quota-monitor-runtime.test.ts +++ b/tests/unit/cliproxy/quota-monitor-runtime.test.ts @@ -11,7 +11,11 @@ import { describe, it, expect, beforeEach, afterEach } from 'bun:test'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; -import { startQuotaMonitor, stopQuotaMonitor, clearQuotaCache } from '../../../src/cliproxy/quota-manager'; +import { + startQuotaMonitor, + stopQuotaMonitor, + clearQuotaCache, +} from '../../../src/cliproxy/quota-manager'; // Setup test isolation let tmpDir: string; diff --git a/tests/unit/cliproxy/remote-proxy-client.test.ts b/tests/unit/cliproxy/remote-proxy-client.test.ts index 58defd72..e15b8d3a 100644 --- a/tests/unit/cliproxy/remote-proxy-client.test.ts +++ b/tests/unit/cliproxy/remote-proxy-client.test.ts @@ -2,7 +2,10 @@ * Unit tests for remote-proxy-client module */ import { describe, it, expect } from 'bun:test'; -import type { RemoteProxyClientConfig, RemoteProxyStatus } from '../../../src/cliproxy/remote-proxy-client'; +import type { + RemoteProxyClientConfig, + RemoteProxyStatus, +} from '../../../src/cliproxy/remote-proxy-client'; // We test the module's type exports and error handling logic // Actual HTTP calls are not mocked in this unit test - use integration tests for that diff --git a/tests/unit/cliproxy/schema-sanitizer.test.ts b/tests/unit/cliproxy/schema-sanitizer.test.ts index b5e64f38..9d555269 100644 --- a/tests/unit/cliproxy/schema-sanitizer.test.ts +++ b/tests/unit/cliproxy/schema-sanitizer.test.ts @@ -535,10 +535,7 @@ describe('sanitizeToolSchemas', () => { }); test('handles tools without input_schema', () => { - const tools = [ - { name: 'simple_tool', description: 'No schema' }, - { name: 'another_tool' }, - ]; + const tools = [{ name: 'simple_tool', description: 'No schema' }, { name: 'another_tool' }]; const result = sanitizeToolSchemas(tools); diff --git a/tests/unit/cliproxy/tool-sanitization-proxy-integration.test.ts b/tests/unit/cliproxy/tool-sanitization-proxy-integration.test.ts index 8bbfc5d0..03a9bc5f 100644 --- a/tests/unit/cliproxy/tool-sanitization-proxy-integration.test.ts +++ b/tests/unit/cliproxy/tool-sanitization-proxy-integration.test.ts @@ -111,7 +111,10 @@ describe('ToolSanitizationProxy Integration', () => { body: JSON.stringify({ model: 'test-model', tools: [ - { name: 'gitmcp__plus-pro-components__plus-pro-components', description: 'Test tool' }, + { + name: 'gitmcp__plus-pro-components__plus-pro-components', + description: 'Test tool', + }, { name: 'valid_tool', description: 'Valid tool' }, ], messages: [{ role: 'user', content: 'test' }], @@ -465,11 +468,7 @@ describe('ToolSanitizationProxy Integration', () => { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - tools: [ - { name: 'tool_a__x__x' }, - { name: 'tool_b__y__y' }, - { name: 'tool_c_valid' }, - ], + tools: [{ name: 'tool_a__x__x' }, { name: 'tool_b__y__y' }, { name: 'tool_c_valid' }], }), }); diff --git a/tests/unit/commands/env-command.test.ts b/tests/unit/commands/env-command.test.ts index aef8327d..9c2fcfd7 100644 --- a/tests/unit/commands/env-command.test.ts +++ b/tests/unit/commands/env-command.test.ts @@ -77,15 +77,11 @@ describe('env-command', () => { }); it('formats powershell export', () => { - expect(formatExportLine('powershell', 'API_KEY', 'sk-123')).toBe( - "$env:API_KEY = 'sk-123'" - ); + expect(formatExportLine('powershell', 'API_KEY', 'sk-123')).toBe("$env:API_KEY = 'sk-123'"); }); it('escapes single quotes in values', () => { - expect(formatExportLine('bash', 'VAL', "it's here")).toBe( - "export VAL='it'\\''s here'" - ); + expect(formatExportLine('bash', 'VAL', "it's here")).toBe("export VAL='it'\\''s here'"); }); it('handles empty values', () => { @@ -104,9 +100,7 @@ describe('env-command', () => { }); it('prevents backtick injection in values', () => { - expect(formatExportLine('bash', 'TOKEN', 'safe`whoami`')).toBe( - "export TOKEN='safe`whoami`'" - ); + expect(formatExportLine('bash', 'TOKEN', 'safe`whoami`')).toBe("export TOKEN='safe`whoami`'"); }); it('escapes single quotes in fish values', () => { @@ -114,9 +108,7 @@ describe('env-command', () => { }); it('escapes single quotes in powershell values', () => { - expect(formatExportLine('powershell', 'VAL', "it's here")).toBe( - "$env:VAL = 'it''s here'" - ); + expect(formatExportLine('powershell', 'VAL', "it's here")).toBe("$env:VAL = 'it''s here'"); }); }); @@ -205,11 +197,15 @@ describe('env-command', () => { }); it('returns undefined when no positional args', () => { - expect(findProfile(['--format', 'openai', '--shell', 'fish'], ['format', 'shell'])).toBeUndefined(); + expect( + findProfile(['--format', 'openai', '--shell', 'fish'], ['format', 'shell']) + ).toBeUndefined(); }); it('skips multiple flag-value pairs', () => { - expect(findProfile(['--format', 'openai', '--shell', 'fish', 'codex'], ['format', 'shell'])).toBe('codex'); + expect( + findProfile(['--format', 'openai', '--shell', 'fish', 'codex'], ['format', 'shell']) + ).toBe('codex'); }); }); }); diff --git a/tests/unit/commands/setup-command.test.ts b/tests/unit/commands/setup-command.test.ts index 8277aefb..f5e47edb 100644 --- a/tests/unit/commands/setup-command.test.ts +++ b/tests/unit/commands/setup-command.test.ts @@ -39,7 +39,8 @@ describe('isFirstTimeInstall logic', () => { createConfigJson({ profiles: { glm: '~/.ccs/glm.settings.json' } }); const legacyConfig = JSON.parse(fs.readFileSync(path.join(testDir, 'config.json'), 'utf8')); - const hasLegacyProfiles = legacyConfig.profiles && Object.keys(legacyConfig.profiles).length > 0; + const hasLegacyProfiles = + legacyConfig.profiles && Object.keys(legacyConfig.profiles).length > 0; expect(hasLegacyProfiles).toBe(true); }); @@ -48,8 +49,11 @@ describe('isFirstTimeInstall logic', () => { createConfigYaml('version: 2\nprofiles: {}\naccounts: {}'); createProfilesJson({ profiles: { work: { path: '/some/path' } } }); - const legacyProfiles = JSON.parse(fs.readFileSync(path.join(testDir, 'profiles.json'), 'utf8')); - const hasLegacyAccounts = legacyProfiles.profiles && Object.keys(legacyProfiles.profiles).length > 0; + const legacyProfiles = JSON.parse( + fs.readFileSync(path.join(testDir, 'profiles.json'), 'utf8') + ); + const hasLegacyAccounts = + legacyProfiles.profiles && Object.keys(legacyProfiles.profiles).length > 0; expect(hasLegacyAccounts).toBe(true); }); @@ -105,7 +109,9 @@ cliproxy_server: createProfilesJson({ profiles: {} }); const legacyConfig = JSON.parse(fs.readFileSync(path.join(testDir, 'config.json'), 'utf8')); - const legacyProfiles = JSON.parse(fs.readFileSync(path.join(testDir, 'profiles.json'), 'utf8')); + const legacyProfiles = JSON.parse( + fs.readFileSync(path.join(testDir, 'profiles.json'), 'utf8') + ); const hasLegacyProfiles = Object.keys(legacyConfig.profiles || {}).length > 0; const hasLegacyAccounts = Object.keys(legacyProfiles.profiles || {}).length > 0; diff --git a/tests/unit/commands/shell-completion-command.test.ts b/tests/unit/commands/shell-completion-command.test.ts index f9c93a53..4c2d71ab 100644 --- a/tests/unit/commands/shell-completion-command.test.ts +++ b/tests/unit/commands/shell-completion-command.test.ts @@ -135,6 +135,8 @@ describe('shell-completion command', () => { await expect(handleShellCompletionCommand([])).rejects.toThrow('process.exit(1)'); const plainErrorLines = errorLines.map(stripAnsi); expect(plainErrorLines.some((line) => line.includes('Error: boom'))).toBe(true); - expect(plainErrorLines.some((line) => line.includes('ccs --shell-completion --zsh'))).toBe(true); + expect(plainErrorLines.some((line) => line.includes('ccs --shell-completion --zsh'))).toBe( + true + ); }); }); diff --git a/tests/unit/cursor/cursor-daemon.test.ts b/tests/unit/cursor/cursor-daemon.test.ts index b46fa317..7990985a 100644 --- a/tests/unit/cursor/cursor-daemon.test.ts +++ b/tests/unit/cursor/cursor-daemon.test.ts @@ -141,39 +141,35 @@ describe('startDaemon', () => { expect(result.error).toContain('Invalid port'); }); - it( - 'starts and stops daemon successfully', - async () => { - const port = 10000 + Math.floor(Math.random() * 50000); - const result = await startDaemon({ port, ghost_mode: true }); - expect(result.success).toBe(true); - expect(result.pid).toBeDefined(); + it('starts and stops daemon successfully', async () => { + const port = 10000 + Math.floor(Math.random() * 50000); + const result = await startDaemon({ port, ghost_mode: true }); + expect(result.success).toBe(true); + expect(result.pid).toBeDefined(); - // Verify health - const running = await isDaemonRunning(port); - expect(running).toBe(true); + // Verify health + const running = await isDaemonRunning(port); + expect(running).toBe(true); - // Verify chat endpoint exists (requires auth, should not be 404) - const chatResponse = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - model: 'gpt-4.1', - messages: [{ role: 'user', content: 'hello' }], - }), - }); - expect(chatResponse.status).toBe(401); + // Verify chat endpoint exists (requires auth, should not be 404) + const chatResponse = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model: 'gpt-4.1', + messages: [{ role: 'user', content: 'hello' }], + }), + }); + expect(chatResponse.status).toBe(401); - // Stop - const stopResult = await stopDaemon(); - expect(stopResult.success).toBe(true); + // Stop + const stopResult = await stopDaemon(); + expect(stopResult.success).toBe(true); - // Verify stopped - const stillRunning = await isDaemonRunning(port); - expect(stillRunning).toBe(false); - }, - 35000 - ); + // Verify stopped + const stillRunning = await isDaemonRunning(port); + expect(stillRunning).toBe(false); + }, 35000); }); describe('isDaemonRunning', () => { diff --git a/tests/unit/cursor/cursor-protobuf.test.ts b/tests/unit/cursor/cursor-protobuf.test.ts index 0ea16a7c..86c2f27d 100644 --- a/tests/unit/cursor/cursor-protobuf.test.ts +++ b/tests/unit/cursor/cursor-protobuf.test.ts @@ -300,7 +300,9 @@ describe('Message Translation', () => { expect(result.messages).toHaveLength(1); expect(result.messages[0].role).toBe('user'); - expect(result.messages[0].content).toBe('[System Instructions]\nSystem instruction part 1 part 2'); + expect(result.messages[0].content).toBe( + '[System Instructions]\nSystem instruction part 1 part 2' + ); }); }); }); @@ -332,7 +334,12 @@ describe('Request Encoding', () => { }, ]; - const result = generateCursorBody([{ role: 'user', content: 'What is the weather?' }], 'gpt-4', tools, null); + const result = generateCursorBody( + [{ role: 'user', content: 'What is the weather?' }], + 'gpt-4', + tools, + null + ); expect(result).toBeInstanceOf(Uint8Array); expect(result.length).toBeGreaterThan(0); @@ -358,7 +365,9 @@ describe('Request Encoding', () => { const executor = new CursorExecutor(); // Frame header says payload is 100 bytes but only 5 bytes follow - const truncatedFrame = Buffer.from([0x00, 0x00, 0x00, 0x00, 0x64, 0x01, 0x02, 0x03, 0x04, 0x05]); + const truncatedFrame = Buffer.from([ + 0x00, 0x00, 0x00, 0x00, 0x64, 0x01, 0x02, 0x03, 0x04, 0x05, + ]); const result = executor.transformProtobufToJSON(truncatedFrame, 'gpt-4', { messages: [], @@ -644,7 +653,10 @@ describe('CursorExecutor', () => { ]; // buildCursorRequest expects (model, body, stream, credentials) - buildCursorRequest('test-model', { messages }, false, { machineId: '12345', accessToken: 'test' }); + buildCursorRequest('test-model', { messages }, false, { + machineId: '12345', + accessToken: 'test', + }); // Should have logged warning const hasWarning = consoleSpy.some((log) => log.includes('Unknown message role')); diff --git a/tests/unit/data-aggregator.test.ts b/tests/unit/data-aggregator.test.ts index 50268007..4984cc15 100644 --- a/tests/unit/data-aggregator.test.ts +++ b/tests/unit/data-aggregator.test.ts @@ -14,9 +14,7 @@ import { type RawUsageEntry } from '../../src/web-server/jsonl-parser'; // TEST FIXTURES // ============================================================================ -const createEntry = ( - overrides: Partial = {} -): RawUsageEntry => ({ +const createEntry = (overrides: Partial = {}): RawUsageEntry => ({ inputTokens: 1000, outputTokens: 500, cacheCreationTokens: 100, @@ -66,9 +64,7 @@ describe('aggregateDailyUsage', () => { expect(result[0].modelsUsed).toContain('claude-opus-4-5-20251101'); // Find sonnet breakdown - const sonnet = result[0].modelBreakdowns.find( - (b) => b.modelName === 'claude-sonnet-4-5' - ); + const sonnet = result[0].modelBreakdowns.find((b) => b.modelName === 'claude-sonnet-4-5'); expect(sonnet!.inputTokens).toBe(1500); // 1000 + 500 }); diff --git a/tests/unit/delegation/delegation-handler.test.ts b/tests/unit/delegation/delegation-handler.test.ts index 793e09e0..c5d4e044 100644 --- a/tests/unit/delegation/delegation-handler.test.ts +++ b/tests/unit/delegation/delegation-handler.test.ts @@ -124,13 +124,7 @@ describe('DelegationHandler', () => { describe('_extractOptions - agents JSON validation', () => { it('accepts valid JSON for agents', () => { - const options = handler._extractOptions([ - 'glm', - '-p', - 'test', - '--agents', - '{"name":"test"}', - ]); + const options = handler._extractOptions(['glm', '-p', 'test', '--agents', '{"name":"test"}']); expect(options.agents).toBe('{"name":"test"}'); }); @@ -153,7 +147,13 @@ describe('DelegationHandler', () => { describe('_extractOptions - betas validation', () => { it('accepts valid betas value', () => { - const options = handler._extractOptions(['glm', '-p', 'test', '--betas', 'feature1,feature2']); + const options = handler._extractOptions([ + 'glm', + '-p', + 'test', + '--betas', + 'feature1,feature2', + ]); expect(options.betas).toBe('feature1,feature2'); }); @@ -165,13 +165,7 @@ describe('DelegationHandler', () => { describe('_extractOptions - extraArgs passthrough', () => { it('passes unknown flags through to extraArgs', () => { - const options = handler._extractOptions([ - 'glm', - '-p', - 'test', - '--unknown-flag', - 'value', - ]); + const options = handler._extractOptions(['glm', '-p', 'test', '--unknown-flag', 'value']); expect(options.extraArgs).toContain('--unknown-flag'); expect(options.extraArgs).toContain('value'); }); diff --git a/tests/unit/delegation/headless-executor.test.ts b/tests/unit/delegation/headless-executor.test.ts index cb78d954..14479d27 100644 --- a/tests/unit/delegation/headless-executor.test.ts +++ b/tests/unit/delegation/headless-executor.test.ts @@ -10,10 +10,7 @@ describe('HeadlessExecutor flag construction', () => { // Since HeadlessExecutor.execute() spawns a process, we test the logic directly describe('Duplicate flag filtering', () => { - function filterExtraArgs( - extraArgs: string[], - explicitFlags: Set - ): string[] { + function filterExtraArgs(extraArgs: string[], explicitFlags: Set): string[] { const filteredExtras: string[] = []; for (let i = 0; i < extraArgs.length; i++) { if (explicitFlags.has(extraArgs[i])) { diff --git a/tests/unit/glmt/retry-logic.test.ts b/tests/unit/glmt/retry-logic.test.ts index 6523cbfa..683d2154 100644 --- a/tests/unit/glmt/retry-logic.test.ts +++ b/tests/unit/glmt/retry-logic.test.ts @@ -30,11 +30,13 @@ afterEach(() => { }); // Helper to create proxy instance with specific config -async function createTestableProxy(config: { - maxRetries?: number; - baseDelay?: number; - enabled?: boolean; -} = {}) { +async function createTestableProxy( + config: { + maxRetries?: number; + baseDelay?: number; + enabled?: boolean; + } = {} +) { // Set env vars before import if (config.maxRetries !== undefined) { process.env.GLMT_MAX_RETRIES = String(config.maxRetries); @@ -56,7 +58,11 @@ describe('GLMT Retry Logic', () => { it('should use default values when env vars not set', async () => { const proxy = await createTestableProxy(); // Access private via type assertion for testing - const config = (proxy as unknown as { retryConfig: { maxRetries: number; baseDelay: number; enabled: boolean } }).retryConfig; + const config = ( + proxy as unknown as { + retryConfig: { maxRetries: number; baseDelay: number; enabled: boolean }; + } + ).retryConfig; expect(config.maxRetries).toBe(3); expect(config.baseDelay).toBe(1000); expect(config.enabled).toBe(true); @@ -84,7 +90,11 @@ describe('GLMT Retry Logic', () => { describe('calculateRetryDelay', () => { it('should calculate exponential delay with jitter', async () => { const proxy = await createTestableProxy({ baseDelay: 1000 }); - const calcDelay = (proxy as unknown as { calculateRetryDelay: (attempt: number, retryAfter?: string) => number }).calculateRetryDelay.bind(proxy); + const calcDelay = ( + proxy as unknown as { + calculateRetryDelay: (attempt: number, retryAfter?: string) => number; + } + ).calculateRetryDelay.bind(proxy); // Attempt 0: 2^0 * 1000 = 1000 + jitter (0-500) const delay0 = calcDelay(0); @@ -104,7 +114,11 @@ describe('GLMT Retry Logic', () => { it('should honor Retry-After header in seconds', async () => { const proxy = await createTestableProxy(); - const calcDelay = (proxy as unknown as { calculateRetryDelay: (attempt: number, retryAfter?: string) => number }).calculateRetryDelay.bind(proxy); + const calcDelay = ( + proxy as unknown as { + calculateRetryDelay: (attempt: number, retryAfter?: string) => number; + } + ).calculateRetryDelay.bind(proxy); // Retry-After: 5 seconds → 5000ms const delay = calcDelay(0, '5'); @@ -113,7 +127,11 @@ describe('GLMT Retry Logic', () => { it('should ignore invalid Retry-After header and fallback to exponential', async () => { const proxy = await createTestableProxy({ baseDelay: 1000 }); - const calcDelay = (proxy as unknown as { calculateRetryDelay: (attempt: number, retryAfter?: string) => number }).calculateRetryDelay.bind(proxy); + const calcDelay = ( + proxy as unknown as { + calculateRetryDelay: (attempt: number, retryAfter?: string) => number; + } + ).calculateRetryDelay.bind(proxy); // Invalid header falls back to exponential const delay = calcDelay(0, 'invalid'); @@ -123,7 +141,11 @@ describe('GLMT Retry Logic', () => { it('should ignore zero or negative Retry-After', async () => { const proxy = await createTestableProxy({ baseDelay: 1000 }); - const calcDelay = (proxy as unknown as { calculateRetryDelay: (attempt: number, retryAfter?: string) => number }).calculateRetryDelay.bind(proxy); + const calcDelay = ( + proxy as unknown as { + calculateRetryDelay: (attempt: number, retryAfter?: string) => number; + } + ).calculateRetryDelay.bind(proxy); const delay = calcDelay(0, '0'); expect(delay).toBeGreaterThanOrEqual(1000); @@ -134,7 +156,11 @@ describe('GLMT Retry Logic', () => { describe('isRetryableError', () => { it('should return true for 429 status code', async () => { const proxy = await createTestableProxy(); - const isRetryable = (proxy as unknown as { isRetryableError: (error: Error) => { retryable: boolean; retryAfter?: string } }).isRetryableError.bind(proxy); + const isRetryable = ( + proxy as unknown as { + isRetryableError: (error: Error) => { retryable: boolean; retryAfter?: string }; + } + ).isRetryableError.bind(proxy); const result = isRetryable(new Error('Upstream error: 429 Too Many Requests')); expect(result.retryable).toBe(true); @@ -142,7 +168,11 @@ describe('GLMT Retry Logic', () => { it('should return true for rate limit message', async () => { const proxy = await createTestableProxy(); - const isRetryable = (proxy as unknown as { isRetryableError: (error: Error) => { retryable: boolean; retryAfter?: string } }).isRetryableError.bind(proxy); + const isRetryable = ( + proxy as unknown as { + isRetryableError: (error: Error) => { retryable: boolean; retryAfter?: string }; + } + ).isRetryableError.bind(proxy); const result = isRetryable(new Error('Rate limit exceeded')); expect(result.retryable).toBe(true); @@ -150,7 +180,11 @@ describe('GLMT Retry Logic', () => { it('should return false for non-retryable errors', async () => { const proxy = await createTestableProxy(); - const isRetryable = (proxy as unknown as { isRetryableError: (error: Error) => { retryable: boolean; retryAfter?: string } }).isRetryableError.bind(proxy); + const isRetryable = ( + proxy as unknown as { + isRetryableError: (error: Error) => { retryable: boolean; retryAfter?: string }; + } + ).isRetryableError.bind(proxy); expect(isRetryable(new Error('Connection refused')).retryable).toBe(false); expect(isRetryable(new Error('Timeout')).retryable).toBe(false); @@ -160,7 +194,11 @@ describe('GLMT Retry Logic', () => { it('should extract Retry-After from error message', async () => { const proxy = await createTestableProxy(); - const isRetryable = (proxy as unknown as { isRetryableError: (error: Error) => { retryable: boolean; retryAfter?: string } }).isRetryableError.bind(proxy); + const isRetryable = ( + proxy as unknown as { + isRetryableError: (error: Error) => { retryable: boolean; retryAfter?: string }; + } + ).isRetryableError.bind(proxy); const result = isRetryable(new Error('429 Too Many Requests, Retry-After: 10')); expect(result.retryable).toBe(true); @@ -174,47 +212,66 @@ describe('GLMT Retry Logic', () => { let attempts = 0; // Mock forwardToUpstream - (proxy as unknown as { forwardToUpstream: () => Promise }).forwardToUpstream = async () => { - attempts++; - return { choices: [{ message: { content: 'success' } }] }; - }; + (proxy as unknown as { forwardToUpstream: () => Promise }).forwardToUpstream = + async () => { + attempts++; + return { choices: [{ message: { content: 'success' } }] }; + }; - const forwardWithRetry = (proxy as unknown as { forwardWithRetry: (req: unknown, headers: unknown) => Promise }).forwardWithRetry.bind(proxy); + const forwardWithRetry = ( + proxy as unknown as { + forwardWithRetry: (req: unknown, headers: unknown) => Promise; + } + ).forwardWithRetry.bind(proxy); const result = await forwardWithRetry({}, {}); expect(attempts).toBe(1); - expect((result as { choices: Array<{ message: { content: string } }> }).choices[0].message.content).toBe('success'); + expect( + (result as { choices: Array<{ message: { content: string } }> }).choices[0].message.content + ).toBe('success'); }); it('should retry on 429 and succeed eventually', async () => { const proxy = await createTestableProxy({ baseDelay: 10 }); // Fast for tests let attempts = 0; - (proxy as unknown as { forwardToUpstream: () => Promise }).forwardToUpstream = async () => { - attempts++; - if (attempts < 3) { - throw new Error('Upstream error: 429 Too Many Requests'); - } - return { choices: [{ message: { content: 'success after retry' } }] }; - }; + (proxy as unknown as { forwardToUpstream: () => Promise }).forwardToUpstream = + async () => { + attempts++; + if (attempts < 3) { + throw new Error('Upstream error: 429 Too Many Requests'); + } + return { choices: [{ message: { content: 'success after retry' } }] }; + }; - const forwardWithRetry = (proxy as unknown as { forwardWithRetry: (req: unknown, headers: unknown) => Promise }).forwardWithRetry.bind(proxy); + const forwardWithRetry = ( + proxy as unknown as { + forwardWithRetry: (req: unknown, headers: unknown) => Promise; + } + ).forwardWithRetry.bind(proxy); const result = await forwardWithRetry({}, {}); expect(attempts).toBe(3); - expect((result as { choices: Array<{ message: { content: string } }> }).choices[0].message.content).toBe('success after retry'); + expect( + (result as { choices: Array<{ message: { content: string } }> }).choices[0].message.content + ).toBe('success after retry'); }); it('should fail after max retries exhausted', async () => { const proxy = await createTestableProxy({ maxRetries: 2, baseDelay: 10 }); let attempts = 0; - (proxy as unknown as { forwardToUpstream: () => Promise }).forwardToUpstream = async () => { - attempts++; - throw new Error('Upstream error: 429 Too Many Requests'); - }; + (proxy as unknown as { forwardToUpstream: () => Promise }).forwardToUpstream = + async () => { + attempts++; + throw new Error('Upstream error: 429 Too Many Requests'); + }; - const forwardWithRetry = (proxy as unknown as { forwardWithRetry: (req: unknown, headers: unknown) => Promise }).forwardWithRetry.bind(proxy); + const forwardWithRetry = ( + proxy as unknown as { + forwardWithRetry: (req: unknown, headers: unknown) => Promise; + } + ).forwardWithRetry.bind(proxy); await expect(forwardWithRetry({}, {})).rejects.toThrow('429'); expect(attempts).toBe(3); // Initial + 2 retries @@ -224,12 +281,17 @@ describe('GLMT Retry Logic', () => { const proxy = await createTestableProxy({ enabled: false }); let attempts = 0; - (proxy as unknown as { forwardToUpstream: () => Promise }).forwardToUpstream = async () => { - attempts++; - throw new Error('Upstream error: 429 Too Many Requests'); - }; + (proxy as unknown as { forwardToUpstream: () => Promise }).forwardToUpstream = + async () => { + attempts++; + throw new Error('Upstream error: 429 Too Many Requests'); + }; - const forwardWithRetry = (proxy as unknown as { forwardWithRetry: (req: unknown, headers: unknown) => Promise }).forwardWithRetry.bind(proxy); + const forwardWithRetry = ( + proxy as unknown as { + forwardWithRetry: (req: unknown, headers: unknown) => Promise; + } + ).forwardWithRetry.bind(proxy); await expect(forwardWithRetry({}, {})).rejects.toThrow('429'); expect(attempts).toBe(1); @@ -239,12 +301,17 @@ describe('GLMT Retry Logic', () => { const proxy = await createTestableProxy({ baseDelay: 10 }); let attempts = 0; - (proxy as unknown as { forwardToUpstream: () => Promise }).forwardToUpstream = async () => { - attempts++; - throw new Error('Upstream error: 500 Internal Server Error'); - }; + (proxy as unknown as { forwardToUpstream: () => Promise }).forwardToUpstream = + async () => { + attempts++; + throw new Error('Upstream error: 500 Internal Server Error'); + }; - const forwardWithRetry = (proxy as unknown as { forwardWithRetry: (req: unknown, headers: unknown) => Promise }).forwardWithRetry.bind(proxy); + const forwardWithRetry = ( + proxy as unknown as { + forwardWithRetry: (req: unknown, headers: unknown) => Promise; + } + ).forwardWithRetry.bind(proxy); await expect(forwardWithRetry({}, {})).rejects.toThrow('500'); expect(attempts).toBe(1); @@ -254,7 +321,8 @@ describe('GLMT Retry Logic', () => { describe('connection pooling', () => { it('should create https.Agent with keepAlive enabled', async () => { const proxy = await createTestableProxy(); - const agent = (proxy as unknown as { httpsAgent: { options?: { keepAlive?: boolean } } }).httpsAgent; + const agent = (proxy as unknown as { httpsAgent: { options?: { keepAlive?: boolean } } }) + .httpsAgent; expect(agent).toBeDefined(); // Agent should have keepAlive behavior (internal property) @@ -263,7 +331,9 @@ describe('GLMT Retry Logic', () => { it('should destroy agent on stop', async () => { const proxy = await createTestableProxy(); - const agent = (proxy as unknown as { httpsAgent: { destroy: () => void; destroyed?: boolean } }).httpsAgent; + const agent = ( + proxy as unknown as { httpsAgent: { destroy: () => void; destroyed?: boolean } } + ).httpsAgent; let destroyed = false; const originalDestroy = agent.destroy.bind(agent); diff --git a/tests/unit/jsonl-parser.test.ts b/tests/unit/jsonl-parser.test.ts index a6a19b1d..b3c00e71 100644 --- a/tests/unit/jsonl-parser.test.ts +++ b/tests/unit/jsonl-parser.test.ts @@ -219,14 +219,7 @@ describe('parseJsonlFile', () => { test('handles file with blank lines', async () => { const filePath = path.join(tempDir, 'blanks.jsonl'); - const content = [ - '', - VALID_ASSISTANT_ENTRY, - '', - ' ', - ASSISTANT_ENTRY_NO_CACHE, - '', - ].join('\n'); + const content = ['', VALID_ASSISTANT_ENTRY, '', ' ', ASSISTANT_ENTRY_NO_CACHE, ''].join('\n'); fs.writeFileSync(filePath, content); diff --git a/tests/unit/mcp-manager.test.ts b/tests/unit/mcp-manager.test.ts index e82362e8..bc77b9f5 100644 --- a/tests/unit/mcp-manager.test.ts +++ b/tests/unit/mcp-manager.test.ts @@ -92,7 +92,7 @@ describe('mcp-manager logic', () => { it('should not detect unrelated MCPs', () => { expect(detectWebSearchMcp({ 'my-custom-mcp': {} })).toBe(false); - expect(detectWebSearchMcp({ 'filesystem': {} })).toBe(false); + expect(detectWebSearchMcp({ filesystem: {} })).toBe(false); expect(detectWebSearchMcp({ 'github-copilot': {} })).toBe(false); }); diff --git a/tests/unit/shared-manager.test.ts b/tests/unit/shared-manager.test.ts index f9124a58..c06e2b48 100644 --- a/tests/unit/shared-manager.test.ts +++ b/tests/unit/shared-manager.test.ts @@ -63,7 +63,8 @@ describe('SharedManager', () => { 'claude-hud@claude-hud': [ { scope: 'user', - installPath: '/home/kai/.ccs/instances/ck/plugins/cache/claude-hud/claude-hud/0.0.2', + installPath: + '/home/kai/.ccs/instances/ck/plugins/cache/claude-hud/claude-hud/0.0.2', version: '0.0.2', }, ], diff --git a/tests/unit/targets/droid-config-manager.test.ts b/tests/unit/targets/droid-config-manager.test.ts index c1743d67..3df8cb5d 100644 --- a/tests/unit/targets/droid-config-manager.test.ts +++ b/tests/unit/targets/droid-config-manager.test.ts @@ -101,9 +101,7 @@ describe('droid-config-manager', () => { provider: 'anthropic', }); - const settings = JSON.parse( - fs.readFileSync(path.join(factoryDir, 'settings.json'), 'utf8') - ); + const settings = JSON.parse(fs.readFileSync(path.join(factoryDir, 'settings.json'), 'utf8')); expect(settings.customModels).toHaveLength(2); expect(settings.customModels[0].displayName).toBe('My GPT'); expect(settings.customModels[1].displayName).toBe('CCS gemini'); @@ -135,9 +133,7 @@ describe('droid-config-manager', () => { provider: 'anthropic', }); - const settings = JSON.parse( - fs.readFileSync(path.join(factoryDir, 'settings.json'), 'utf8') - ); + const settings = JSON.parse(fs.readFileSync(path.join(factoryDir, 'settings.json'), 'utf8')); expect(settings.customModels).toHaveLength(2); expect(settings.customModels[0].provider).toBe('custom-provider'); expect(settings.customModels[1].displayName).toBe('CCS gemini'); @@ -162,7 +158,10 @@ describe('droid-config-manager', () => { it('should reject symlinked temp file path', async () => { const factoryDir = path.join(tmpDir, '.factory'); fs.mkdirSync(factoryDir, { recursive: true }); - fs.writeFileSync(path.join(factoryDir, 'settings.json'), JSON.stringify({ customModels: [] })); + fs.writeFileSync( + path.join(factoryDir, 'settings.json'), + JSON.stringify({ customModels: [] }) + ); fs.symlinkSync('/tmp', path.join(factoryDir, 'settings.json.tmp')); await expect( @@ -202,9 +201,7 @@ describe('droid-config-manager', () => { provider: 'anthropic', }); - const settings = JSON.parse( - fs.readFileSync(path.join(factoryDir, 'settings.json'), 'utf8') - ); + const settings = JSON.parse(fs.readFileSync(path.join(factoryDir, 'settings.json'), 'utf8')); expect(settings.customModels).toHaveLength(1); expect(settings.customModels[0].displayName).toBe('CCS gemini'); expect(settings.customModels[0].apiKey).toBe('new-key'); @@ -255,8 +252,20 @@ describe('droid-config-manager', () => { path.join(factoryDir, 'settings.json'), JSON.stringify({ customModels: [ - { model: 'gpt-4o', displayName: 'My GPT', baseUrl: 'x', apiKey: 'y', provider: 'openai' }, - { model: 'opus', displayName: 'CCS gemini', baseUrl: 'x', apiKey: 'y', provider: 'anthropic' }, + { + model: 'gpt-4o', + displayName: 'My GPT', + baseUrl: 'x', + apiKey: 'y', + provider: 'openai', + }, + { + model: 'opus', + displayName: 'CCS gemini', + baseUrl: 'x', + apiKey: 'y', + provider: 'anthropic', + }, ], }) ); @@ -275,8 +284,20 @@ describe('droid-config-manager', () => { path.join(factoryDir, 'settings.json'), JSON.stringify({ customModels: [ - { model: 'opus', displayName: 'ccs-gemini', baseUrl: 'x', apiKey: 'y', provider: 'anthropic' }, - { model: 'gpt-4o', displayName: 'My GPT', baseUrl: 'x', apiKey: 'y', provider: 'openai' }, + { + model: 'opus', + displayName: 'ccs-gemini', + baseUrl: 'x', + apiKey: 'y', + provider: 'anthropic', + }, + { + model: 'gpt-4o', + displayName: 'My GPT', + baseUrl: 'x', + apiKey: 'y', + provider: 'openai', + }, ], }) ); @@ -351,7 +372,12 @@ describe('droid-config-manager', () => { fs.writeFileSync( path.join(factoryDir, 'settings.json'), JSON.stringify({ - customModels: [null, 123, 'bad', { displayName: 'CCS ok', model: 'x', baseUrl: 'x', apiKey: 'y', provider: 'anthropic' }], + customModels: [ + null, + 123, + 'bad', + { displayName: 'CCS ok', model: 'x', baseUrl: 'x', apiKey: 'y', provider: 'anthropic' }, + ], }) ); @@ -458,8 +484,20 @@ describe('droid-config-manager', () => { path.join(factoryDir, 'settings.json'), JSON.stringify({ customModels: [ - { model: 'gpt-4o', displayName: 'My GPT', baseUrl: 'x', apiKey: 'y', provider: 'openai' }, - { model: 'opus', displayName: 'CCS old-profile', baseUrl: 'x', apiKey: 'y', provider: 'anthropic' }, + { + model: 'gpt-4o', + displayName: 'My GPT', + baseUrl: 'x', + apiKey: 'y', + provider: 'openai', + }, + { + model: 'opus', + displayName: 'CCS old-profile', + baseUrl: 'x', + apiKey: 'y', + provider: 'anthropic', + }, ], }) ); @@ -479,8 +517,20 @@ describe('droid-config-manager', () => { path.join(factoryDir, 'settings.json'), JSON.stringify({ customModels: [ - { model: 'opus', displayName: 'ccs-gemini', baseUrl: 'x', apiKey: 'y', provider: 'anthropic' }, - { model: 'sonnet', displayName: 'ccs-codex', baseUrl: 'x', apiKey: 'y', provider: 'anthropic' }, + { + model: 'opus', + displayName: 'ccs-gemini', + baseUrl: 'x', + apiKey: 'y', + provider: 'anthropic', + }, + { + model: 'sonnet', + displayName: 'ccs-codex', + baseUrl: 'x', + apiKey: 'y', + provider: 'anthropic', + }, ], }) ); @@ -502,7 +552,13 @@ describe('droid-config-manager', () => { customModels: [ { model: 'x', displayName: 'CCS ', baseUrl: 'x', apiKey: 'y', provider: 'anthropic' }, { model: 'x', displayName: 'ccs-', baseUrl: 'x', apiKey: 'y', provider: 'anthropic' }, - { model: 'gpt-4o', displayName: 'My GPT', baseUrl: 'x', apiKey: 'y', provider: 'openai' }, + { + model: 'gpt-4o', + displayName: 'My GPT', + baseUrl: 'x', + apiKey: 'y', + provider: 'openai', + }, ], }) ); @@ -517,31 +573,27 @@ describe('droid-config-manager', () => { }); describe('concurrent writes', () => { - it( - 'should handle concurrent upserts without data loss', - async () => { - const profiles = Array.from({ length: 10 }, (_, i) => `profile-${i}`); + it('should handle concurrent upserts without data loss', async () => { + const profiles = Array.from({ length: 10 }, (_, i) => `profile-${i}`); - await Promise.all( - profiles.map((p) => - upsertCcsModel(p, { - model: 'test-model', - displayName: `CCS ${p}`, - baseUrl: 'http://localhost:8317', - apiKey: 'key', - provider: 'anthropic', - }) - ) - ); + await Promise.all( + profiles.map((p) => + upsertCcsModel(p, { + model: 'test-model', + displayName: `CCS ${p}`, + baseUrl: 'http://localhost:8317', + apiKey: 'key', + provider: 'anthropic', + }) + ) + ); - const models = await listCcsModels(); - expect(models.size).toBe(10); + const models = await listCcsModels(); + expect(models.size).toBe(10); - for (const p of profiles) { - expect(models.has(p)).toBe(true); - } - }, - 15000 - ); + for (const p of profiles) { + expect(models.has(p)).toBe(true); + } + }, 15000); }); }); diff --git a/tests/unit/targets/target-resolver.test.ts b/tests/unit/targets/target-resolver.test.ts index 03ca380a..cfed158d 100644 --- a/tests/unit/targets/target-resolver.test.ts +++ b/tests/unit/targets/target-resolver.test.ts @@ -129,9 +129,9 @@ describe('stripTargetFlag', () => { }); it('should remove repeated --target flags', () => { - expect(stripTargetFlag(['--target', 'droid', 'gemini', '--target=claude', '--verbose'])).toEqual( - ['gemini', '--verbose'] - ); + expect( + stripTargetFlag(['--target', 'droid', 'gemini', '--target=claude', '--verbose']) + ).toEqual(['gemini', '--verbose']); }); it('should throw when --target has no value', () => { diff --git a/tests/unit/unified-config.test.ts b/tests/unit/unified-config.test.ts index eaeb30dc..b4094ea4 100644 --- a/tests/unit/unified-config.test.ts +++ b/tests/unit/unified-config.test.ts @@ -19,7 +19,16 @@ import { isUnifiedConfigEnabled } from '../../src/config/feature-flags'; // Inline helper to test secret key detection (utility kept for potential reuse) function isSecretKey(key: string): boolean { const upper = key.toUpperCase(); - const secretPatterns = ['TOKEN', 'SECRET', 'API_KEY', 'APIKEY', 'PASSWORD', 'CREDENTIAL', 'AUTH', 'PRIVATE']; + const secretPatterns = [ + 'TOKEN', + 'SECRET', + 'API_KEY', + 'APIKEY', + 'PASSWORD', + 'CREDENTIAL', + 'AUTH', + 'PRIVATE', + ]; return secretPatterns.some((pattern) => upper.includes(pattern)); } diff --git a/tests/unit/utils/expand-path.test.ts b/tests/unit/utils/expand-path.test.ts index 42dab0f5..f8d759ef 100644 --- a/tests/unit/utils/expand-path.test.ts +++ b/tests/unit/utils/expand-path.test.ts @@ -1,89 +1,91 @@ -import { expect, test, describe, beforeEach, afterEach } from "bun:test"; -import * as path from "path"; -import * as os from "os"; -import { expandPath } from "../../../src/utils/helpers"; +import { expect, test, describe, beforeEach, afterEach } from 'bun:test'; +import * as path from 'path'; +import * as os from 'os'; +import { expandPath } from '../../../src/utils/helpers'; -describe("expandPath", () => { +describe('expandPath', () => { const originalEnv = { ...process.env }; const HOME = os.homedir(); beforeEach(() => { - process.env.TEST_HOME = "/custom/home"; - process.env.TEST_VAR = "foo"; + process.env.TEST_HOME = '/custom/home'; + process.env.TEST_VAR = 'foo'; }); afterEach(() => { process.env = { ...originalEnv }; }); - test("1. Tilde expansion: ~/path -> /home/user/path", () => { - expect(expandPath("~/test/file.txt")).toBe(path.join(HOME, "test/file.txt")); + test('1. Tilde expansion: ~/path -> /home/user/path', () => { + expect(expandPath('~/test/file.txt')).toBe(path.join(HOME, 'test/file.txt')); }); - test("2. Windows tilde with backslash: ~\\path", () => { + test('2. Windows tilde with backslash: ~\\path', () => { // expandPath handles both ~/ and ~\ regardless of platform - expect(expandPath("~\\test\\file.txt")).toBe(path.join(HOME, "test/file.txt")); + expect(expandPath('~\\test\\file.txt')).toBe(path.join(HOME, 'test/file.txt')); }); - test("3. Environment variable expansion: ${VAR}/path", () => { - expect(expandPath("${TEST_HOME}/file.txt")).toBe(path.normalize("/custom/home/file.txt")); + test('3. Environment variable expansion: ${VAR}/path', () => { + expect(expandPath('${TEST_HOME}/file.txt')).toBe(path.normalize('/custom/home/file.txt')); }); - test("4. Dollar sign env vars: $VAR/path", () => { - expect(expandPath("$TEST_HOME/file.txt")).toBe(path.normalize("/custom/home/file.txt")); + test('4. Dollar sign env vars: $VAR/path', () => { + expect(expandPath('$TEST_HOME/file.txt')).toBe(path.normalize('/custom/home/file.txt')); }); - test("5. Windows %VAR% expansion: %VAR%\\path (simulated)", () => { + test('5. Windows %VAR% expansion: %VAR%\\path (simulated)', () => { // We can't easily mock process.platform if it's not win32, // but the function check process.platform === 'win32' if (process.platform === 'win32') { - expect(expandPath("%TEST_HOME%\\file.txt")).toBe(path.normalize("/custom/home/file.txt")); + expect(expandPath('%TEST_HOME%\\file.txt')).toBe(path.normalize('/custom/home/file.txt')); } else { // Should remain unchanged on non-windows (but separators normalized) - expect(expandPath("%TEST_HOME%\\file.txt")).toBe(path.normalize("%TEST_HOME%/file.txt")); + expect(expandPath('%TEST_HOME%\\file.txt')).toBe(path.normalize('%TEST_HOME%/file.txt')); } }); - test("6. Mixed path separators normalization", () => { - const result = expandPath("path/to\\some/file"); - expect(result).toBe(path.normalize("path/to/some/file")); + test('6. Mixed path separators normalization', () => { + const result = expandPath('path/to\\some/file'); + expect(result).toBe(path.normalize('path/to/some/file')); }); - test("7. Nested env vars: ${HOME}/${VAR}/path", () => { - expect(expandPath("${TEST_HOME}/${TEST_VAR}/file.txt")).toBe(path.normalize("/custom/home/foo/file.txt")); + test('7. Nested env vars: ${HOME}/${VAR}/path', () => { + expect(expandPath('${TEST_HOME}/${TEST_VAR}/file.txt')).toBe( + path.normalize('/custom/home/foo/file.txt') + ); }); - test("8. Empty/null path handling", () => { - expect(expandPath("")).toBe("."); + test('8. Empty/null path handling', () => { + expect(expandPath('')).toBe('.'); }); - test("9. Already absolute paths stay unchanged", () => { - const absPath = "/absolute/path"; + test('9. Already absolute paths stay unchanged', () => { + const absPath = '/absolute/path'; expect(expandPath(absPath)).toBe(path.normalize(absPath)); }); - test("10. Undefined env vars -> empty string", () => { - expect(expandPath("${UNDEFINED_VAR}/file.txt")).toBe(path.normalize("/file.txt")); - expect(expandPath("$UNDEFINED_VAR/file.txt")).toBe(path.normalize("/file.txt")); + test('10. Undefined env vars -> empty string', () => { + expect(expandPath('${UNDEFINED_VAR}/file.txt')).toBe(path.normalize('/file.txt')); + expect(expandPath('$UNDEFINED_VAR/file.txt')).toBe(path.normalize('/file.txt')); }); - test("11. Windows drive letters stay intact", () => { + test('11. Windows drive letters stay intact', () => { // Windows drive letter paths should be preserved - const result = expandPath("C:\\Users\\test\\file.txt"); - expect(result).toContain("Users"); - expect(result).toContain("test"); + const result = expandPath('C:\\Users\\test\\file.txt'); + expect(result).toContain('Users'); + expect(result).toContain('test'); }); - test("12. Windows UNC paths handled", () => { + test('12. Windows UNC paths handled', () => { // UNC paths start with \\ - const uncPath = "\\\\server\\share\\folder"; + const uncPath = '\\\\server\\share\\folder'; const result = expandPath(uncPath); // Should normalize but preserve the structure - expect(result).toContain("server"); - expect(result).toContain("share"); + expect(result).toContain('server'); + expect(result).toContain('share'); }); - test("13. Null-like input throws TypeError", () => { + test('13. Null-like input throws TypeError', () => { // Function requires string input - documents current behavior // @ts-ignore - testing runtime edge case expect(() => expandPath(undefined as unknown as string)).toThrow(TypeError); @@ -91,14 +93,14 @@ describe("expandPath", () => { expect(() => expandPath(null as unknown as string)).toThrow(TypeError); }); - test("14. Path with spaces preserved", () => { - const pathWithSpaces = "~/My Documents/file.txt"; + test('14. Path with spaces preserved', () => { + const pathWithSpaces = '~/My Documents/file.txt'; const result = expandPath(pathWithSpaces); - expect(result).toContain("My Documents"); + expect(result).toContain('My Documents'); }); - test("15. Multiple consecutive slashes normalized", () => { - const result = expandPath("path//to///file.txt"); - expect(result).toBe(path.normalize("path/to/file.txt")); + test('15. Multiple consecutive slashes normalized', () => { + const result = expandPath('path//to///file.txt'); + expect(result).toBe(path.normalize('path/to/file.txt')); }); }); diff --git a/tests/unit/utils/signal-forwarder.test.ts b/tests/unit/utils/signal-forwarder.test.ts index 463453f8..451987f7 100644 --- a/tests/unit/utils/signal-forwarder.test.ts +++ b/tests/unit/utils/signal-forwarder.test.ts @@ -97,7 +97,9 @@ describe('signal-forwarder', () => { const child = createMockChildProcess(); const before = getSignalListenerCounts(); const onError = jest.fn(async () => {}); - const err = Object.assign(new Error('spawn failed'), { code: 'ENOENT' }) as NodeJS.ErrnoException; + const err = Object.assign(new Error('spawn failed'), { + code: 'ENOENT', + }) as NodeJS.ErrnoException; wireChildProcessSignals(child, onError); child.emit('error', err); @@ -113,7 +115,9 @@ describe('signal-forwarder', () => { const child = createMockChildProcess(); const onError = jest.fn(async () => {}); const onExit = jest.fn(); - const err = Object.assign(new Error('spawn failed'), { code: 'ENOENT' }) as NodeJS.ErrnoException; + const err = Object.assign(new Error('spawn failed'), { + code: 'ENOENT', + }) as NodeJS.ErrnoException; wireChildProcessSignals(child, onError, onExit); child.emit('error', err); @@ -134,7 +138,9 @@ describe('signal-forwarder', () => { .mockImplementation((() => undefined as never) as typeof process.exit); try { - const err = Object.assign(new Error('spawn failed'), { code: 'ENOENT' }) as NodeJS.ErrnoException; + const err = Object.assign(new Error('spawn failed'), { + code: 'ENOENT', + }) as NodeJS.ErrnoException; wireChildProcessSignals(child, onError); child.emit('error', err); await Promise.resolve(); diff --git a/tests/unit/utils/websearch/hook-utils.test.ts b/tests/unit/utils/websearch/hook-utils.test.ts index 64373365..56c5aa5f 100644 --- a/tests/unit/utils/websearch/hook-utils.test.ts +++ b/tests/unit/utils/websearch/hook-utils.test.ts @@ -1,5 +1,8 @@ import { expect, test, describe } from 'bun:test'; -import { isCcsWebSearchHook, deduplicateCcsHooks } from '../../../../src/utils/websearch/hook-utils'; +import { + isCcsWebSearchHook, + deduplicateCcsHooks, +} from '../../../../src/utils/websearch/hook-utils'; describe('isCcsWebSearchHook', () => { test('Returns true for CCS hook with forward slashes (Unix path)', () => { diff --git a/tests/unit/web-server/auth-middleware.test.ts b/tests/unit/web-server/auth-middleware.test.ts index 9c0f9191..e441b310 100644 --- a/tests/unit/web-server/auth-middleware.test.ts +++ b/tests/unit/web-server/auth-middleware.test.ts @@ -28,17 +28,13 @@ describe('Dashboard Auth', () => { describe('getDashboardAuthConfig', () => { it('returns disabled by default', async () => { - const { getDashboardAuthConfig } = await import( - '../../src/config/unified-config-loader' - ); + const { getDashboardAuthConfig } = await import('../../src/config/unified-config-loader'); const config = getDashboardAuthConfig(); expect(config.enabled).toBe(false); }); it('returns 24 hour default session timeout', async () => { - const { getDashboardAuthConfig } = await import( - '../../src/config/unified-config-loader' - ); + const { getDashboardAuthConfig } = await import('../../src/config/unified-config-loader'); const config = getDashboardAuthConfig(); expect(config.session_timeout_hours).toBe(24); }); @@ -89,12 +85,7 @@ describe('Dashboard Auth', () => { }); describe('public paths', () => { - const PUBLIC_PATHS = [ - '/api/auth/login', - '/api/auth/check', - '/api/auth/setup', - '/api/health', - ]; + const PUBLIC_PATHS = ['/api/auth/login', '/api/auth/check', '/api/auth/setup', '/api/health']; it('identifies public paths correctly', () => { const isPublicPath = (path: string) => diff --git a/tests/unit/web-server/cliproxy-auth-routes.test.ts b/tests/unit/web-server/cliproxy-auth-routes.test.ts index 1ad73d09..9982c133 100644 --- a/tests/unit/web-server/cliproxy-auth-routes.test.ts +++ b/tests/unit/web-server/cliproxy-auth-routes.test.ts @@ -3,7 +3,9 @@ import { getStartUrlUnsupportedReason } from '../../../src/web-server/routes/cli describe('cliproxy-auth-routes start-url guard', () => { it('rejects device code providers', () => { - expect(getStartUrlUnsupportedReason('kiro')).toContain("Kiro method 'aws' uses Device Code flow"); + expect(getStartUrlUnsupportedReason('kiro')).toContain( + "Kiro method 'aws' uses Device Code flow" + ); expect(getStartUrlUnsupportedReason('ghcp')).toContain("Provider 'ghcp' uses Device Code flow"); expect(getStartUrlUnsupportedReason('qwen')).toContain("Provider 'qwen' uses Device Code flow"); }); diff --git a/tests/unit/web-server/cursor-settings-routes.test.ts b/tests/unit/web-server/cursor-settings-routes.test.ts index fb163a7a..6f74edcc 100644 --- a/tests/unit/web-server/cursor-settings-routes.test.ts +++ b/tests/unit/web-server/cursor-settings-routes.test.ts @@ -14,7 +14,10 @@ process.env.CCS_HOME = TEST_CCS_DIR; // Import after setting env var import type { CursorConfig } from '../../../src/config/unified-config-types'; -import { loadOrCreateUnifiedConfig, saveUnifiedConfig } from '../../../src/config/unified-config-loader'; +import { + loadOrCreateUnifiedConfig, + saveUnifiedConfig, +} from '../../../src/config/unified-config-loader'; import { getCcsDir } from '../../../src/utils/config-manager'; describe('Cursor Settings Routes Logic', () => { From ddd5b159d21efe436aa2bf82a190d3b284abe4a8 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Tue, 17 Feb 2026 17:16:52 +0700 Subject: [PATCH 5/7] fix(cliproxy): add kimi to wizard constants and image analysis config - add kimi entry to PROVIDER_INFO and WIZARD_PROVIDER_ORDER in setup wizard - add kimi to DEFAULT_IMAGE_ANALYSIS_CONFIG.provider_models --- src/config/unified-config-types.ts | 1 + ui/src/components/setup/wizard/constants.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/config/unified-config-types.ts b/src/config/unified-config-types.ts index 3420d406..5e5e0d2d 100644 --- a/src/config/unified-config-types.ts +++ b/src/config/unified-config-types.ts @@ -666,6 +666,7 @@ export const DEFAULT_IMAGE_ANALYSIS_CONFIG: ImageAnalysisConfig = { // 'vision-model' is a generic placeholder - users can override via config.yaml qwen: 'vision-model', iflow: 'qwen3-vl-plus', + kimi: 'vision-model', }, }; diff --git a/ui/src/components/setup/wizard/constants.ts b/ui/src/components/setup/wizard/constants.ts index 96c9d633..26addd28 100644 --- a/ui/src/components/setup/wizard/constants.ts +++ b/ui/src/components/setup/wizard/constants.ts @@ -17,6 +17,7 @@ const PROVIDER_INFO: Record Date: Tue, 17 Feb 2026 17:31:27 +0700 Subject: [PATCH 6/7] fix(test): add kimi to provider tests, remove merge conflict marker - Add kimi to CLIPROXY_PROVIDER_IDS expected array - Add kimi to device_code providers expected array - Remove leftover >>>>>>> origin/dev merge conflict marker in cursor-daemon test - Fix prettier formatting in image-analysis-check.ts --- src/management/checks/image-analysis-check.ts | 5 +++-- tests/unit/cliproxy/provider-capabilities.test.ts | 3 ++- tests/unit/cursor/cursor-daemon.test.ts | 1 - 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/management/checks/image-analysis-check.ts b/src/management/checks/image-analysis-check.ts index e539bff4..39a0f7c5 100644 --- a/src/management/checks/image-analysis-check.ts +++ b/src/management/checks/image-analysis-check.ts @@ -94,8 +94,9 @@ export async function runImageAnalysisCheck(results: HealthCheck): Promise * Fix image analysis configuration issues */ export async function fixImageAnalysisConfig(): Promise { - const { updateUnifiedConfig, loadOrCreateUnifiedConfig } = - await import('../../config/unified-config-loader'); + const { updateUnifiedConfig, loadOrCreateUnifiedConfig } = await import( + '../../config/unified-config-loader' + ); const config = loadOrCreateUnifiedConfig(); let fixed = false; diff --git a/tests/unit/cliproxy/provider-capabilities.test.ts b/tests/unit/cliproxy/provider-capabilities.test.ts index c92c702f..13f3e8f7 100644 --- a/tests/unit/cliproxy/provider-capabilities.test.ts +++ b/tests/unit/cliproxy/provider-capabilities.test.ts @@ -32,6 +32,7 @@ describe('provider-capabilities', () => { 'kiro', 'ghcp', 'claude', + 'kimi', ]); }); @@ -43,7 +44,7 @@ describe('provider-capabilities', () => { }); it('returns providers by OAuth flow capability', () => { - expect(getProvidersByOAuthFlow('device_code')).toEqual(['qwen', 'kiro', 'ghcp']); + expect(getProvidersByOAuthFlow('device_code')).toEqual(['qwen', 'kiro', 'ghcp', 'kimi']); expect(getProvidersByOAuthFlow('authorization_code')).toEqual([ 'gemini', 'codex', diff --git a/tests/unit/cursor/cursor-daemon.test.ts b/tests/unit/cursor/cursor-daemon.test.ts index 34b1b1b9..1ba7e553 100644 --- a/tests/unit/cursor/cursor-daemon.test.ts +++ b/tests/unit/cursor/cursor-daemon.test.ts @@ -266,7 +266,6 @@ describe('startDaemon', () => { await stopDaemon(); } }); ->>>>>>> origin/dev }); describe('isDaemonRunning', () => { From 6961fb0ec35b326bb77ffea9f8828708d2a40cb7 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Tue, 17 Feb 2026 20:13:34 +0700 Subject: [PATCH 7/7] feat(ui): add Kimi provider logo with dark background - Add kimi.svg (Moonshot brand logo, white on dark) - Add kimi to provider-logo.tsx PROVIDER_IMAGES and PROVIDER_CONFIG - Dark background for kimi logo container (bg-gray-900) --- ui/public/assets/providers/kimi.svg | 1 + ui/src/components/cliproxy/provider-logo.tsx | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 ui/public/assets/providers/kimi.svg diff --git a/ui/public/assets/providers/kimi.svg b/ui/public/assets/providers/kimi.svg new file mode 100644 index 00000000..286f35e7 --- /dev/null +++ b/ui/public/assets/providers/kimi.svg @@ -0,0 +1 @@ +Kimi diff --git a/ui/src/components/cliproxy/provider-logo.tsx b/ui/src/components/cliproxy/provider-logo.tsx index ee762285..e2b8528b 100644 --- a/ui/src/components/cliproxy/provider-logo.tsx +++ b/ui/src/components/cliproxy/provider-logo.tsx @@ -21,6 +21,7 @@ const PROVIDER_IMAGES: Record = { kiro: '/assets/providers/kiro.png', ghcp: '/assets/providers/copilot.svg', claude: '/assets/providers/claude.svg', + kimi: '/assets/providers/kimi.svg', }; /** Provider color configuration (for fallback only - no background for image logos) */ @@ -33,8 +34,12 @@ const PROVIDER_CONFIG: Record = { iflow: { text: 'text-indigo-600', letter: 'i' }, kiro: { text: 'text-teal-600', letter: 'K' }, ghcp: { text: 'text-green-600', letter: 'C' }, + kimi: { text: 'text-orange-500', letter: 'K' }, }; +/** Providers whose logos require a dark background */ +const DARK_BG_PROVIDERS = new Set(['kimi']); + /** Size configuration */ const SIZE_CONFIG = { sm: { container: 'w-6 h-6', icon: 'w-4 h-4', text: 'text-xs' }, @@ -55,7 +60,7 @@ export function ProviderLogo({ provider, className, size = 'md' }: ProviderLogoP