diff --git a/CLAUDE.md b/CLAUDE.md index 072e66bf..41b8334e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,7 +17,7 @@ Tests set `process.env.CCS_HOME` to a temp directory. Code using `os.homedir()` ## Core Function -CLI wrapper for instant switching between multiple provider accounts and alternative models (GLM, GLMT, Kimi). See README.md for user documentation. +CLI wrapper for instant switching between multiple provider accounts and alternative models (GLM, Kimi, and other API profiles). See README.md for user documentation. ## Design Principles (ENFORCE STRICTLY) @@ -232,7 +232,7 @@ dist/ui/ → Built UI bundle (served by Express) 1. **CLIProxy hardcoded**: gemini, codex, agy → OAuth-based, zero config 2. **CLIProxy variants**: `config.cliproxy` section → user-defined providers -3. **Settings-based**: `config.profiles` section → GLM, GLMT, Kimi +3. **Settings-based**: `config.profiles` section → GLM, legacy GLMT compatibility, Kimi 4. **Account-based**: `profiles.json` → isolated instances via `CLAUDE_CONFIG_DIR` ### Settings Format (CRITICAL) diff --git a/docs/codebase-summary.md b/docs/codebase-summary.md index 0c5de9c5..df29f733 100644 --- a/docs/codebase-summary.md +++ b/docs/codebase-summary.md @@ -40,7 +40,7 @@ src/ │ ├── cli.ts # CLI types (ParsedArgs, ExitCode) │ ├── config.ts # Config types (Settings, EnvVars) │ ├── delegation.ts # Delegation types (sessions, events) -│ ├── glmt.ts # GLMT types (messages, transforms) +│ ├── glmt.ts # Legacy transformer types (messages, transforms) │ └── utils.ts # Utility types (ErrorCode, LogLevel) │ ├── commands/ # CLI command handlers @@ -115,11 +115,11 @@ src/ │ ├── index.ts # Barrel export │ └── copilot-package-manager.ts # Package management (515 lines) │ -├── glmt/ # GLM/GLMT integration +├── glmt/ # Legacy transformer internals kept for compatibility │ ├── index.ts # Barrel export │ ├── pipeline/ # Processing pipeline │ │ └── index.ts -│ ├── glmt-proxy.ts # Main proxy (675 lines) +│ ├── glmt-proxy.ts # Legacy proxy runtime kept for internal compatibility │ └── delta-accumulator.ts # Delta processing (484 lines) │ ├── delegation/ # Task delegation & headless execution @@ -201,7 +201,7 @@ src/ | Targets | `targets/` | Multi-CLI adapter pattern (Claude Code, Factory Droid, extensible) | | Auth | `auth/`, `cliproxy/auth/` | Authentication across providers | | Config | `config/`, `types/` | Configuration & type definitions | -| Providers | `cliproxy/`, `copilot/`, `glmt/` | Provider integrations (7 CLIProxy providers: gemini, codex, agy, qwen, iflow, kiro, ghcp) | +| Providers | `cliproxy/`, `copilot/`, `glmt/` | Provider integrations plus retained legacy transformer internals | | Quota | `cliproxy/quota-*.ts`, `account-manager.ts` | Hybrid quota management (v7.14) | | Remote Proxy | `cliproxy/remote-*.ts`, `proxy-config-resolver.ts` | Remote CLIProxy support (v7.1) | | Image Analysis | `utils/image-analysis/`, `utils/hooks/` | Vision model proxying (v7.34) | @@ -477,7 +477,7 @@ ui/src/ | File | Lines | Status | |------|-------|--------| | model-pricing.ts | 676 | Data file - acceptable | -| glmt-proxy.ts | 675 | Complex streaming - acceptable | +| glmt-proxy.ts | 675 | Legacy internal compatibility path - acceptable for now | | cliproxy-executor.ts | 666 | Core logic - acceptable | | cliproxy-command.ts | 634 | Could split if needed | | usage/handlers.ts | 633 | Could split if needed | diff --git a/docs/project-roadmap.md b/docs/project-roadmap.md index ec4bc5aa..1da8db92 100644 --- a/docs/project-roadmap.md +++ b/docs/project-roadmap.md @@ -41,6 +41,7 @@ All major modularization work is complete. The codebase evolved from monolithic ### Recent Fixes +- **2026-03-17**: Deprecated user-facing GLMT discovery across CLI help, completions, presets, and docs. Existing `glmt` profiles now run through a compatibility path that normalizes legacy proxy settings to the direct GLM endpoint. - **#744**: API profile creation now keeps featured providers in a horizontal rail with scroll fallback, moves Anthropic Direct API to the end, reuses the shared Claude logo, and separates the custom-endpoint entry point from advanced template discovery. - **#724**: Codex startup is now free-plan safe. CCS defaults new Codex sessions to a cross-plan model and auto-repairs stale paid-only Codex defaults when the active account is on the free plan. - **#737**: Dashboard model pickers in Cursor, Copilot, and CLIProxy now use a searchable combobox with autofocus and explicit no-results states for large model catalogs. @@ -59,7 +60,7 @@ All major modularization work is complete. The codebase evolved from monolithic **CLI** (complex core logic): - `model-pricing.ts` (676 lines) - Data file -- `glmt-proxy.ts` (675 lines) - Streaming proxy +- `glmt-proxy.ts` (675 lines) - Legacy internal compatibility proxy - `cliproxy-executor.ts` (666 lines) - Core execution - `ccs.ts` (596 lines) - Entry point diff --git a/docs/system-architecture/index.md b/docs/system-architecture/index.md index 02b3d450..a13cd68e 100644 --- a/docs/system-architecture/index.md +++ b/docs/system-architecture/index.md @@ -55,7 +55,7 @@ CCS v7.45 introduces the Target Adapter pattern, enabling seamless integration w **Key architecture:** ``` -Profile Resolution (CLIProxy, GLMT, Account-based) +Profile Resolution (CLIProxy, Settings/API, Account-based) | v Target Resolution (--target flag > config > argv[0] > default) @@ -126,7 +126,7 @@ For details on the adapter architecture, see [Target Adapters](./target-adapters | | +---> [CLIProxy Provider] ---> execClaudeWithCLIProxy() | | - +---> [GLMT Profile] ---> execClaudeWithProxy() + +---> [Settings/API Profile] ---> normalize legacy glmt if needed | v +------------------+ @@ -185,20 +185,23 @@ For details on the adapter architecture, see [Target Adapters](./target-adapters | v | 7b. Spawn via Adapter | - +---> GLMT -----------> 3c. Start Embedded Proxy + +---> Settings/API ---> 3c. Load settings env | v - 4c. Resolve Target Adapter + 4c. Normalize legacy glmt if needed | v - 5c. Spawn via Adapter + 5c. Resolve Target Adapter + | + v + 6c. Spawn via Adapter ``` --- ## Provider Integration Architecture -For detailed provider flows (CLIProxyAPI, GLMT, quota management), see [Provider Flows](./provider-flows.md). +For detailed provider flows (CLIProxyAPI, legacy GLMT compatibility, quota management), see [Provider Flows](./provider-flows.md). --- @@ -303,7 +306,7 @@ See [Provider Flows](./provider-flows.md) → Authentication Flow section. | Localhost only (127.0.0.1) v +------------------+ - | CLIProxy/GLMT | Binds to localhost only + | CLIProxy/Legacy | Binds to localhost only +------------------+ | | TLS encrypted @@ -400,5 +403,5 @@ See [Provider Flows](./provider-flows.md) → Authentication Flow section. - [Codebase Summary](../codebase-summary.md) - Detailed directory structure - [Code Standards](../code-standards.md) - Coding conventions & patterns - [Target Adapters](./target-adapters.md) - Multi-CLI adapter architecture -- [Provider Flows](./provider-flows.md) - CLIProxy, GLMT, authentication flows +- [Provider Flows](./provider-flows.md) - CLIProxy, legacy GLMT compatibility, authentication flows - [Project Roadmap](../project-roadmap.md) - Development phases diff --git a/docs/system-architecture/provider-flows.md b/docs/system-architecture/provider-flows.md index 0663f768..8ccec44c 100644 --- a/docs/system-architecture/provider-flows.md +++ b/docs/system-architecture/provider-flows.md @@ -2,7 +2,7 @@ Last Updated: 2026-02-16 -Detailed provider integration flows including CLIProxyAPI, GLMT proxy, remote CLIProxy, quota management, and authentication. +Detailed provider integration flows including CLIProxyAPI, legacy GLMT compatibility transforms, remote CLIProxy, quota management, and authentication. --- @@ -91,63 +91,55 @@ if (hardcodedProviders.includes(profileName)) { --- -## GLMT Proxy Flow +## Legacy GLMT Compatibility Flow ### Overview -GLMT proxy enables seamless integration with GLM-compatible APIs (Z.AI, Kimi, OpenRouter, etc.) using a Node.js-based embedded proxy. +GLMT is no longer a marketed runtime surface in CCS. Existing `glmt` profiles are kept as a compatibility path and normalized at launch to the direct GLM endpoint. The `src/glmt/` module remains because Cursor response translation still imports its transformer pipeline. ``` +===========================================================================+ -| GLMT Proxy Integration | +| Legacy GLMT Compatibility + Internal Transforms | +===========================================================================+ Claude CLI | - | ANTHROPIC_BASE_URL = localhost:XXXX + | legacy glmt settings detected v +------------------+ - | GLMT Proxy | Embedded Node.js proxy (src/glmt/) - | (glmt-proxy.ts)| + | Compatibility | normalizeDeprecatedGlmtEnv() + | Layer | (src/utils/glmt-deprecation.ts) +------------------+ | v +------------------+ - | Delta Accumulator| Stream transformation + | Direct GLM API | https://api.z.ai/api/anthropic +------------------+ | v +------------------+ - | Pipeline | Request/Response transformation - +------------------+ - | - v - +------------------+ - | GLM API | Z.AI / Kimi API + | src/glmt/* | retained for Cursor translation +------------------+ ``` -### Supported GLM Providers +### Supported Migration Targets | Provider | Config Key | Endpoint | Auth | |----------|------------|----------|------| -| Z.AI (GLM) | `glmt` | https://open.bigmodel.cn/api/paas/v4/ | API key | -| Kimi | `kimi` | https://api.moonshot.cn/v1/ | API key | -| OpenRouter | `openrouter` | https://openrouter.ai/api/v1/ | API key | +| Z.AI (GLM) | `glm` | https://api.z.ai/api/anthropic | API key | +| Kimi API | `km` | https://api.kimi.com/coding/ | API key | +| Legacy compatibility | `glmt` | normalized to direct GLM at runtime | existing profile only | -Note for `config/base-kimi.settings.json`: the default base URL is `http://127.0.0.1:8317/api/provider/kimi` (local CLIProxy route). For direct Moonshot API access, override `ANTHROPIC_BASE_URL` to `https://api.moonshot.cn/v1/`. +Use `ccs glm` for Z.AI profiles and `ccs km` for reasoning-first Kimi API profiles. Keep `glmt` only when migrating an existing settings file. -### GLMT Profile Detection +### Runtime Handling -CCS detects GLMT profiles and routes through `execClaudeWithProxy()`: +CCS detects the deprecated `glmt` profile name and normalizes legacy proxy-only settings before dispatching through the normal settings-profile flow: ```typescript -// Settings-based profile detection -const settings = loadSettings(profileName); -if (settings.env?.ANTHROPIC_BASE_URL?.includes('glm') || - settings.env?.ANTHROPIC_BASE_URL?.includes('moonshot') || - settings.env?.ANTHROPIC_BASE_URL?.includes('openrouter')) { - return execClaudeWithProxy(claudeCli, profileName, args); +if (isDeprecatedGlmtProfileName(profileName)) { + const normalized = normalizeDeprecatedGlmtEnv(settingsEnv); + // warn user, validate against direct GLM endpoint, continue through settings flow } ``` diff --git a/package.json b/package.json index 66e4a911..3633c660 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@kaitranntt/ccs", "version": "7.54.0-dev.8", - "description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6", + "description": "Claude Code Switch - Instant profile switching between Claude, GLM, Kimi, and more", "keywords": [ "cli", "claude", diff --git a/scripts/completion/README.md b/scripts/completion/README.md index 103316c8..d70103e0 100644 --- a/scripts/completion/README.md +++ b/scripts/completion/README.md @@ -116,7 +116,7 @@ sudo cp scripts/completion/ccs.fish /usr/share/fish/vendor_completions.d/ ```bash $ ccs -auth doctor glm glmt kimi work personal --help --version +auth doctor glm kimi work personal --help --version $ ccs auth create list show remove default --help diff --git a/scripts/completion/ccs.fish b/scripts/completion/ccs.fish index ea58c2d7..107607b9 100644 --- a/scripts/completion/ccs.fish +++ b/scripts/completion/ccs.fish @@ -26,7 +26,7 @@ end # Helper function to get custom/unknown settings profiles function __fish_ccs_get_custom_settings_profiles set -l config_path ~/.ccs/config.json - set -l known_profiles default glm glmt kimi + set -l known_profiles default glm kimi if test -f $config_path set -l all_profiles (jq -r '.profiles | keys[]' $config_path 2>/dev/null) @@ -138,7 +138,6 @@ complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env # Model profiles - grouped with [model] prefix for visual distinction complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a 'default' -d '[model] Default Claude Sonnet 4.5' complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a 'glm' -d '[model] GLM-4.6 (cost-optimized)' -complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a 'glmt' -d '[model] GLM-4.6 with thinking mode' complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a 'kimi' -d '[model] Kimi for Coding (long-context)' # Custom model profiles - dynamic with [model] prefix diff --git a/scripts/completion/ccs.zsh b/scripts/completion/ccs.zsh index fb4f9f36..9c948b6a 100644 --- a/scripts/completion/ccs.zsh +++ b/scripts/completion/ccs.zsh @@ -15,7 +15,7 @@ # Set up completion styles for better formatting and colors zstyle ':completion:*:*:ccs:*:commands' list-colors '=(#b)(auth|api|cliproxy|doctor|env|sync|update)([[:space:]]#--[[:space:]]#*)==0\;34=2\;37' zstyle ':completion:*:*:ccs:*:proxy-profiles' list-colors '=(#b)(gemini|codex|agy|qwen)([[:space:]]#--[[:space:]]#*)==0\;35=2\;37' -zstyle ':completion:*:*:ccs:*:model-profiles' list-colors '=(#b)(default|glm|glmt|kimi|[^[:space:]]##)([[:space:]]#--[[:space:]]#*)==0\;32=2\;37' +zstyle ':completion:*:*:ccs:*:model-profiles' list-colors '=(#b)(default|glm|kimi|[^[:space:]]##)([[:space:]]#--[[:space:]]#*)==0\;32=2\;37' zstyle ':completion:*:*:ccs:*:account-profiles' list-colors '=(#b)([^[:space:]]##)([[:space:]]#--[[:space:]]#*)==0\;33=2\;37' zstyle ':completion:*:*:ccs:*' group-name '' zstyle ':completion:*:*:ccs:*:descriptions' format $'\n%B%F{yellow}── %d ──%f%b' @@ -56,7 +56,6 @@ _ccs() { profile_descriptions=( 'default' 'Default Claude Sonnet 4.5' 'glm' 'GLM-4.6 (cost-optimized)' - 'glmt' 'GLM-4.6 with thinking mode' 'kimi' 'Kimi for Coding (long-context)' ) diff --git a/scripts/postinstall.js b/scripts/postinstall.js index 46c6bbf0..376db755 100755 --- a/scripts/postinstall.js +++ b/scripts/postinstall.js @@ -150,7 +150,7 @@ function createConfigFiles() { // Create config.yaml if missing (primary format) // NOTE: gemini/codex profiles NOT included - they are added on-demand when user // runs `ccs gemini` or `ccs codex` for first time (requires OAuth auth first) - // NOTE: GLM/GLMT/Kimi profiles are now created via UI/CLI presets, not auto-created + // NOTE: GLM/Kimi profiles are now created via UI/CLI presets, not auto-created const configYamlPath = path.join(ccsDir, 'config.yaml'); const legacyConfigPath = path.join(ccsDir, 'config.json'); @@ -250,10 +250,10 @@ function createConfigFiles() { console.log(' config.json will be ignored - consider removing it'); } - // NOTE: GLM, GLMT, and Kimi profiles are NO LONGER auto-created during install + // NOTE: GLM and Kimi profiles are NO LONGER auto-created during install // Users can create these via: // - UI: Profile Create Dialog → Provider Presets - // - CLI: ccs api create --preset glm|glmt|kimi + // - CLI: ccs api create --preset glm|km // This gives users control over which providers they want to use // Existing profiles are preserved for backward compatibility diff --git a/src/ccs.ts b/src/ccs.ts index bcf53769..5e412bef 100644 --- a/src/ccs.ts +++ b/src/ccs.ts @@ -1,13 +1,10 @@ import './utils/fetch-proxy-setup'; -import { spawn, ChildProcess } from 'child_process'; -import * as path from 'path'; import * as fs from 'fs'; import { detectClaudeCli } from './utils/claude-detector'; import { getSettingsPath, loadSettings, - getCcsDir, setGlobalConfigDir, detectCloudSyncPath, } from './utils/config-manager'; @@ -43,13 +40,8 @@ import { handleError, runCleanup } from './errors'; import { tryHandleRootCommand } from './commands/root-command-router'; // Import extracted utility functions -import { - execClaude, - escapeShellArg, - stripClaudeCodeEnv, - getClaudeLaunchEnvOverrides, -} from './utils/shell-executor'; -import { wireChildProcessSignals } from './utils/signal-forwarder'; +import { execClaude } from './utils/shell-executor'; +import { isDeprecatedGlmtProfileName, normalizeDeprecatedGlmtEnv } from './utils/glmt-deprecation'; // Import target adapter system import { @@ -98,198 +90,6 @@ function detectProfile(args: string[]): DetectedProfile { } } -// ========== GLMT Proxy Execution ========== - -/** - * Execute Claude CLI with embedded proxy (for GLMT profile) - */ -async function execClaudeWithProxy( - claudeCli: string, - profileName: string, - args: string[], - claudeConfigDir?: string -): Promise { - // 1. Read settings to get API key - const settingsPath = getSettingsPath(profileName); - const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); - const envData = settings.env; - const apiKey = envData['ANTHROPIC_AUTH_TOKEN']; - - if (!apiKey || apiKey === 'YOUR_GLM_API_KEY_HERE') { - console.error(fail('GLMT profile requires Z.AI API key')); - console.error(` Edit ${getCcsDir()}/glmt.settings.json and set ANTHROPIC_AUTH_TOKEN`); - process.exit(1); - } - - // Detect verbose flag - const verbose = args.includes('--verbose') || args.includes('-v'); - - // 2. Spawn embedded proxy with verbose flag - const proxyPath = path.join(__dirname, 'glmt', 'glmt-proxy.js'); - const proxyArgs = verbose ? ['--verbose'] : []; - // Use process.execPath for Windows compatibility (CVE-2024-27980) - // Pass environment variables to proxy subprocess (required for auth) - const proxy = spawn(process.execPath, [proxyPath, ...proxyArgs], { - stdio: ['ignore', 'pipe', verbose ? 'pipe' : 'inherit'], - env: { - ...process.env, - ANTHROPIC_AUTH_TOKEN: apiKey, - ANTHROPIC_BASE_URL: envData['ANTHROPIC_BASE_URL'], - }, - }); - const stopProxy = (): void => { - try { - if (!proxy.killed) { - proxy.kill('SIGTERM'); - } - } catch { - // Best-effort cleanup on process teardown. - } - }; - - // 3. Wait for proxy ready signal (with timeout) - const { ProgressIndicator } = await import('./utils/progress-indicator'); - const spinner = new ProgressIndicator('Starting GLMT proxy'); - spinner.start(); - - let port: number; - try { - port = await new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - reject(new Error('Proxy startup timeout (5s)')); - }, 5000); - - proxy.stdout?.on('data', (data: Buffer) => { - const match = data.toString().match(/PROXY_READY:(\d+)/); - if (match) { - clearTimeout(timeout); - resolve(parseInt(match[1])); - } - }); - - proxy.on('error', (error) => { - clearTimeout(timeout); - reject(error); - }); - - proxy.on('exit', (code) => { - if (code !== 0 && code !== null) { - clearTimeout(timeout); - reject(new Error(`Proxy exited with code ${code}`)); - } - }); - }); - - spinner.succeed(`GLMT proxy ready on port ${port}`); - } catch (error) { - const err = error as Error; - spinner.fail('Failed to start GLMT proxy'); - console.error(fail(`Error: ${err.message}`)); - console.error(''); - console.error('Possible causes:'); - console.error(' 1. Port conflict (unlikely with random port)'); - console.error(' 2. Node.js permission issue'); - console.error(' 3. Firewall blocking localhost'); - console.error(''); - console.error('Workarounds:'); - console.error(' - Use non-thinking mode: ccs glm "prompt"'); - console.error(' - Enable verbose logging: ccs glmt --verbose "prompt"'); - console.error(` - Check proxy logs in ${getCcsDir()}/logs/ (if debug enabled)`); - console.error(''); - stopProxy(); - runCleanup(); - process.exit(1); - } - - // 4. Spawn Claude CLI with proxy URL - // Use model from user's settings (not hardcoded) - fixes issue #358 - const configuredModel = envData['ANTHROPIC_MODEL'] || 'glm-5'; - const envVars: NodeJS.ProcessEnv = { - ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}`, - ANTHROPIC_AUTH_TOKEN: apiKey, - ANTHROPIC_MODEL: configuredModel, - ...(claudeConfigDir ? { CLAUDE_CONFIG_DIR: claudeConfigDir } : {}), - }; - - const isWindows = process.platform === 'win32'; - const isPowerShellScript = isWindows && /\.ps1$/i.test(claudeCli); - const needsShell = isWindows && /\.(cmd|bat)$/i.test(claudeCli); - const webSearchEnv = getWebSearchHookEnv(); - const imageAnalysisEnv = getImageAnalysisHookEnv(profileName); - const claudeLaunchEnv = getClaudeLaunchEnvOverrides(); - const env = stripClaudeCodeEnv({ - ...process.env, - ...claudeLaunchEnv, - ...envVars, - ...webSearchEnv, - ...imageAnalysisEnv, - CCS_PROFILE_TYPE: 'settings', // Signal to WebSearch hook this is a third-party provider - }); - - let claude: ChildProcess; - if (isPowerShellScript) { - claude = spawn( - 'powershell.exe', - ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', claudeCli, ...args], - { - stdio: 'inherit', - windowsHide: true, - env, - } - ); - } else if (needsShell) { - const cmdString = [claudeCli, ...args].map(escapeShellArg).join(' '); - claude = spawn(cmdString, { - stdio: 'inherit', - windowsHide: true, - shell: true, - env, - }); - } else { - claude = spawn(claudeCli, args, { - stdio: 'inherit', - windowsHide: true, - env, - }); - } - - // 5. Shared signal forwarding + proxy cleanup lifecycle - wireChildProcessSignals( - claude, - (err: NodeJS.ErrnoException) => { - if (err.code === 'EACCES') { - console.error(fail(`Claude CLI is not executable: ${claudeCli}`)); - console.error(' Check file permissions and executable bit.'); - } else if (err.code === 'ENOENT') { - if (isPowerShellScript) { - console.error( - fail('PowerShell executable not found (required for .ps1 wrapper launch).') - ); - console.error(' Ensure powershell.exe is available in PATH.'); - } else if (needsShell) { - console.error(fail('Windows command shell not found for Claude wrapper launch.')); - console.error(' Ensure cmd.exe is available and accessible.'); - } else { - console.error(fail(`Claude CLI not found: ${claudeCli}`)); - } - } else { - console.error(fail(`Claude CLI error: ${err.message}`)); - } - stopProxy(); - runCleanup(); - process.exit(1); - }, - (code: number | null, signal: NodeJS.Signals | null) => { - stopProxy(); - if (signal) { - process.kill(process.pid, signal); - } else { - process.exit(code || 0); - } - } - ); -} - // ========== Main Execution ========== interface ProfileError extends Error { @@ -549,15 +349,6 @@ async function main(): Promise { process.exit(1); } - // GLMT always requires Claude target because it depends on embedded proxy flow. - if (profileInfo.type === 'settings' && profileInfo.name === 'glmt') { - console.error(fail(`${targetAdapter.displayName} does not support GLMT proxy profiles`)); - console.error( - info('Use --target claude for glmt, or switch to a direct API profile (glm/km)') - ); - process.exit(1); - } - if (profileInfo.type === 'default') { if (!targetAdapter.supportsProfileType('default')) { console.error(fail(`${targetAdapter.displayName} does not support default profile mode`)); @@ -878,18 +669,29 @@ async function main(): Promise { ); } const inheritedClaudeConfigDir = continuityInheritance.claudeConfigDir; + const expandedSettingsPath = profileInfo.settingsPath + ? expandPath(profileInfo.settingsPath) + : getSettingsPath(profileInfo.name); + const settings = loadSettings(expandedSettingsPath); + const rawSettingsEnv = profileInfo.env ?? settings.env ?? {}; + const isDeprecatedGlmtProfile = isDeprecatedGlmtProfileName(profileInfo.name); + const glmtNormalization = isDeprecatedGlmtProfile + ? normalizeDeprecatedGlmtEnv(rawSettingsEnv) + : null; + const settingsEnv = glmtNormalization?.env ?? rawSettingsEnv; - // Pre-flight validation for GLM/GLMT/MiniMax profiles - if (profileInfo.name === 'glm' || profileInfo.name === 'glmt') { - const preflightSettingsPath = getSettingsPath(profileInfo.name); - const preflightSettings = loadSettings(preflightSettingsPath); - const apiKey = preflightSettings.env?.['ANTHROPIC_AUTH_TOKEN']; + if (glmtNormalization) { + for (const message of glmtNormalization.warnings) { + console.error(warn(message)); + } + } + + // Pre-flight validation for Z.AI-compatible profiles. + if (profileInfo.name === 'glm' || isDeprecatedGlmtProfile) { + const apiKey = settingsEnv['ANTHROPIC_AUTH_TOKEN']; if (apiKey) { - const validation = await validateGlmKey( - apiKey, - preflightSettings.env?.['ANTHROPIC_BASE_URL'] - ); + const validation = await validateGlmKey(apiKey, settingsEnv['ANTHROPIC_BASE_URL']); if (!validation.valid) { console.error(''); @@ -906,15 +708,10 @@ async function main(): Promise { } if (profileInfo.name === 'mm') { - const preflightSettingsPath = getSettingsPath(profileInfo.name); - const preflightSettings = loadSettings(preflightSettingsPath); - const apiKey = preflightSettings.env?.['ANTHROPIC_AUTH_TOKEN']; + const apiKey = settingsEnv['ANTHROPIC_AUTH_TOKEN']; if (apiKey) { - const validation = await validateMiniMaxKey( - apiKey, - preflightSettings.env?.['ANTHROPIC_BASE_URL'] - ); + const validation = await validateMiniMaxKey(apiKey, settingsEnv['ANTHROPIC_BASE_URL']); if (!validation.valid) { console.error(''); @@ -932,10 +729,8 @@ async function main(): Promise { // Pre-flight validation for Anthropic direct profiles (ANTHROPIC_API_KEY + no BASE_URL) { - const preflightSettingsPath = getSettingsPath(profileInfo.name); - const preflightSettings = loadSettings(preflightSettingsPath); - const anthropicApiKey = preflightSettings.env?.['ANTHROPIC_API_KEY']; - const hasBaseUrl = !!preflightSettings.env?.['ANTHROPIC_BASE_URL']; + const anthropicApiKey = settingsEnv['ANTHROPIC_API_KEY']; + const hasBaseUrl = !!settingsEnv['ANTHROPIC_BASE_URL']; if (anthropicApiKey && !hasBaseUrl) { const validation = await validateAnthropicKey(anthropicApiKey); if (!validation.valid) { @@ -954,89 +749,60 @@ async function main(): Promise { } } - // Check if this is GLMT profile (requires proxy) - if (profileInfo.name === 'glmt') { - if (resolvedTarget !== 'claude') { - console.error( - fail(`${targetAdapter?.displayName || 'Target'} does not support GLMT proxy profiles`) - ); - console.error( - info('Use --target claude for glmt, or switch to a direct API profile (glm/km)') - ); + const webSearchEnv = getWebSearchHookEnv(); + const imageAnalysisEnv = getImageAnalysisHookEnv(profileInfo.name); + // Get global env vars (DISABLE_TELEMETRY, etc.) for third-party profiles + const globalEnvConfig = getGlobalEnvConfig(); + const globalEnv = globalEnvConfig.enabled ? globalEnvConfig.env : {}; + + // Log global env injection for visibility (debug mode only) + if (globalEnvConfig.enabled && Object.keys(globalEnv).length > 0 && process.env.CCS_DEBUG) { + const envNames = Object.keys(globalEnv).join(', '); + console.error(info(`Global env: ${envNames}`)); + } + + // Explicitly inject effective settings env vars so stale ANTHROPIC_* + // values from prior sessions cannot leak into the active profile. + const envVars: NodeJS.ProcessEnv = { + ...globalEnv, + ...settingsEnv, + ...(inheritedClaudeConfigDir ? { CLAUDE_CONFIG_DIR: inheritedClaudeConfigDir } : {}), + ...webSearchEnv, + ...imageAnalysisEnv, + CCS_PROFILE_TYPE: 'settings', + }; + + // Dispatch through target adapter for non-claude targets + if (resolvedTarget !== 'claude') { + const adapter = targetAdapter; + if (!adapter) { + console.error(fail(`Target adapter not found for "${resolvedTarget}"`)); process.exit(1); } - // GLMT FLOW: Settings-based with embedded proxy for thinking support - await execClaudeWithProxy( - claudeCli, - profileInfo.name, - remainingArgs, - inheritedClaudeConfigDir - ); - } else { - // EXISTING FLOW: Settings-based profile (glm) - // Use --settings flag (backward compatible) - const expandedSettingsPath = profileInfo.settingsPath - ? expandPath(profileInfo.settingsPath) - : getSettingsPath(profileInfo.name); - const webSearchEnv = getWebSearchHookEnv(); - const imageAnalysisEnv = getImageAnalysisHookEnv(profileInfo.name); - // Get global env vars (DISABLE_TELEMETRY, etc.) for third-party profiles - const globalEnvConfig = getGlobalEnvConfig(); - const globalEnv = globalEnvConfig.enabled ? globalEnvConfig.env : {}; - - // Log global env injection for visibility (debug mode only) - if (globalEnvConfig.enabled && Object.keys(globalEnv).length > 0 && process.env.CCS_DEBUG) { - const envNames = Object.keys(globalEnv).join(', '); - console.error(info(`Global env: ${envNames}`)); - } - - // CRITICAL: Load settings and explicitly set ANTHROPIC_* env vars - // to prevent inheriting stale values from previous CLIProxy sessions. - // Environment variables take precedence over --settings file values, - // so we must explicitly set them here to ensure correct routing. - const settings = loadSettings(expandedSettingsPath); - const settingsEnv = settings.env || {}; - - const envVars: NodeJS.ProcessEnv = { - ...globalEnv, - ...settingsEnv, // Explicitly inject all settings env vars - ...(inheritedClaudeConfigDir ? { CLAUDE_CONFIG_DIR: inheritedClaudeConfigDir } : {}), - ...webSearchEnv, - ...imageAnalysisEnv, - CCS_PROFILE_TYPE: 'settings', // Signal to WebSearch hook this is a third-party provider - }; - - // Dispatch through target adapter for non-claude targets - if (resolvedTarget !== 'claude') { - const adapter = targetAdapter; - if (!adapter) { - console.error(fail(`Target adapter not found for "${resolvedTarget}"`)); - process.exit(1); - } - const directAnthropicBaseUrl = - settingsEnv['ANTHROPIC_BASE_URL'] || - (settingsEnv['ANTHROPIC_API_KEY'] ? 'https://api.anthropic.com' : ''); - const creds: TargetCredentials = { - profile: profileInfo.name, + const directAnthropicBaseUrl = + settingsEnv['ANTHROPIC_BASE_URL'] || + (settingsEnv['ANTHROPIC_API_KEY'] ? 'https://api.anthropic.com' : ''); + const creds: TargetCredentials = { + profile: profileInfo.name, + baseUrl: directAnthropicBaseUrl, + apiKey: settingsEnv['ANTHROPIC_AUTH_TOKEN'] || settingsEnv['ANTHROPIC_API_KEY'] || '', + model: settingsEnv['ANTHROPIC_MODEL'], + provider: resolveDroidProvider({ + provider: settingsEnv['CCS_DROID_PROVIDER'] || settingsEnv['DROID_PROVIDER'], baseUrl: directAnthropicBaseUrl, - apiKey: settingsEnv['ANTHROPIC_AUTH_TOKEN'] || settingsEnv['ANTHROPIC_API_KEY'] || '', model: settingsEnv['ANTHROPIC_MODEL'], - provider: resolveDroidProvider({ - provider: settingsEnv['CCS_DROID_PROVIDER'] || settingsEnv['DROID_PROVIDER'], - baseUrl: directAnthropicBaseUrl, - model: settingsEnv['ANTHROPIC_MODEL'], - }), - reasoningOverride: droidReasoningOverride, - }; - await adapter.prepareCredentials(creds); - const targetArgs = adapter.buildArgs(profileInfo.name, targetRemainingArgs); - const targetEnv = adapter.buildEnv(creds, profileInfo.type); - adapter.exec(targetArgs, targetEnv, { binaryInfo: targetBinaryInfo || undefined }); - return; - } - - execClaude(claudeCli, ['--settings', expandedSettingsPath, ...remainingArgs], envVars); + }), + reasoningOverride: droidReasoningOverride, + envVars, + }; + await adapter.prepareCredentials(creds); + const targetArgs = adapter.buildArgs(profileInfo.name, targetRemainingArgs); + const targetEnv = adapter.buildEnv(creds, profileInfo.type); + adapter.exec(targetArgs, targetEnv, { binaryInfo: targetBinaryInfo || undefined }); + return; } + + execClaude(claudeCli, ['--settings', expandedSettingsPath, ...remainingArgs], envVars); } else if (profileInfo.type === 'account') { // NEW FLOW: Account-based profile (work, personal) // All platforms: Use instance isolation with CLAUDE_CONFIG_DIR diff --git a/src/commands/api-command/create-command.ts b/src/commands/api-command/create-command.ts index cf526560..5941e9d1 100644 --- a/src/commands/api-command/create-command.ts +++ b/src/commands/api-command/create-command.ts @@ -36,6 +36,17 @@ function resolvePresetOrExit(presetId?: string): ProviderPreset | null { process.exit(1); } +function showPresetDeprecationNotice(presetId?: string): void { + if ((presetId || '').trim().toLowerCase() !== 'glmt') { + return; + } + + console.log(warn('Preset "glmt" is deprecated and now maps to the direct "glm" preset.')); + console.log(dim(' Z.AI models already expose thinking natively, so CCS no longer needs GLMT.')); + console.log(dim(' Update scripts/docs to: ccs api create --preset glm')); + console.log(''); +} + async function resolveProfileName( providedName: string | undefined, preset: ProviderPreset | null @@ -237,6 +248,7 @@ export async function handleApiCreateCommand(args: string[]): Promise { console.log(header('Create API Profile')); console.log(''); + showPresetDeprecationNotice(parsedArgs.preset); const preset = resolvePresetOrExit(parsedArgs.preset); const name = await resolveProfileName(parsedArgs.name, preset); diff --git a/src/commands/env-command.ts b/src/commands/env-command.ts index 888f075d..bfb35103 100644 --- a/src/commands/env-command.ts +++ b/src/commands/env-command.ts @@ -209,6 +209,11 @@ export async function handleEnvCommand(args: string[]): Promise { let envVars: Record; try { const resolved = await resolveClaudeExtensionSetup(profile); + if (resolved.warnings.length > 0) { + for (const message of resolved.warnings) { + console.error(warn(message)); + } + } envVars = resolved.extensionEnv; if (format === 'claude-extension') { console.log(renderClaudeExtensionSettingsJson(resolved, ide)); diff --git a/src/commands/help-command.ts b/src/commands/help-command.ts index 85eb4aba..97e2af09 100644 --- a/src/commands/help-command.ts +++ b/src/commands/help-command.ts @@ -131,7 +131,6 @@ 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 km', 'Kimi for Coding (API key)'], [ 'ccs api create --preset alibaba-coding-plan', diff --git a/src/commands/persist-command.ts b/src/commands/persist-command.ts index a1d061ff..a5474db6 100644 --- a/src/commands/persist-command.ts +++ b/src/commands/persist-command.ts @@ -679,7 +679,7 @@ async function showHelp(): Promise { ); console.log(''); console.log(subheader('Supported Profile Types')); - console.log(` ${color('API profiles', 'command')} glm, glmt, km, custom API profiles`); + console.log(` ${color('API profiles', 'command')} glm, km, custom API profiles`); console.log(` ${color('CLIProxy', 'command')} gemini, codex, agy, qwen, kiro, ghcp`); console.log(` ${color('Copilot', 'command')} copilot (requires copilot-api daemon)`); console.log( diff --git a/src/config/unified-config-loader.ts b/src/config/unified-config-loader.ts index cc8e0381..8bcc9445 100644 --- a/src/config/unified-config-loader.ts +++ b/src/config/unified-config-loader.ts @@ -553,7 +553,7 @@ function generateYamlWithComments(config: UnifiedConfig): string { // Profiles section lines.push('# ----------------------------------------------------------------------------'); - lines.push('# Profiles: API-based providers (GLM, GLMT, Kimi, custom endpoints)'); + lines.push('# Profiles: API-based providers (GLM, Kimi, custom endpoints)'); lines.push('# Each profile points to a *.settings.json file containing env vars.'); lines.push('# Edit the settings file directly to customize (ANTHROPIC_MAX_TOKENS, etc.)'); lines.push('# ----------------------------------------------------------------------------'); diff --git a/src/glmt/glmt-proxy.ts b/src/glmt/glmt-proxy.ts index 2d7be16c..f493ba3e 100644 --- a/src/glmt/glmt-proxy.ts +++ b/src/glmt/glmt-proxy.ts @@ -1,5 +1,5 @@ /** - * GlmtProxy - Embedded HTTP proxy for GLM thinking support + * GlmtProxy - Legacy embedded HTTP proxy retained for compatibility work. * * Architecture: * - Intercepts Claude CLI → Z.AI calls @@ -7,8 +7,9 @@ * - Converts reasoning_content → thinking blocks * - Supports both streaming and buffered modes * - * Lifecycle: - * - Spawned by bin/ccs.js when 'glmt' profile detected + * Current status: + * - No longer started by the normal `ccs glmt` runtime path + * - Kept for legacy/internal compatibility and transformer-adjacent tests * - Binds to 127.0.0.1:random_port (security + avoid conflicts) * - Terminates when parent process exits * diff --git a/src/management/recovery-manager.ts b/src/management/recovery-manager.ts index ec7e3710..313f6911 100644 --- a/src/management/recovery-manager.ts +++ b/src/management/recovery-manager.ts @@ -200,8 +200,8 @@ class RecoveryManager { * Run all recovery operations (lazy initialization) * Mirrors postinstall.js behavior * - * NOTE: GLM/GLMT/Kimi profiles are NOT auto-created. - * Users should create them via `ccs api create --preset glm` or the UI. + * NOTE: GLM/Kimi profiles are NOT auto-created. + * Users should create them via `ccs api create --preset glm|km` or the UI. */ recoverAll(): boolean { this.recovered = []; diff --git a/src/shared/claude-extension-setup.ts b/src/shared/claude-extension-setup.ts index 5aaaa409..784c8560 100644 --- a/src/shared/claude-extension-setup.ts +++ b/src/shared/claude-extension-setup.ts @@ -15,6 +15,7 @@ import InstanceManager from '../management/instance-manager'; import SharedManager from '../management/shared-manager'; import { expandPath } from '../utils/helpers'; import { getClaudeSettingsPath } from '../utils/claude-config-path'; +import { isDeprecatedGlmtProfileName, normalizeDeprecatedGlmtEnv } from '../utils/glmt-deprecation'; import { type ClaudeExtensionHost, type ClaudeExtensionHostDefinition, @@ -87,7 +88,12 @@ function describeProfile(profileName: string, result: ProfileDetectionResult): s } if (result.type === 'cliproxy') return 'OAuth or CLIProxy-backed profile for Anthropic-compatible routing.'; - if (result.type === 'settings') return 'API profile backed by a CCS settings file.'; + if (result.type === 'settings') { + if (isDeprecatedGlmtProfileName(profileName)) { + return 'Deprecated GLMT compatibility profile normalized to the direct GLM API.'; + } + return 'API profile backed by a CCS settings file.'; + } if (result.type === 'account') return 'Claude account instance isolated through CLAUDE_CONFIG_DIR.'; if (result.type === 'copilot') return 'GitHub Copilot profile routed through copilot-api.'; @@ -184,7 +190,7 @@ async function resolveExtensionEnv( profileType: result.type, target: 'claude', }); - const env = + let env = result.type === 'settings' ? (result.env ?? (result.settingsPath ? loadSettingsFromFile(expandPath(result.settingsPath)) : {})) @@ -230,6 +236,13 @@ async function resolveExtensionEnv( : getEffectiveEnvVars(result.provider, port, result.settingsPath); })(); + if (result.type === 'settings' && isDeprecatedGlmtProfileName(requestedProfile)) { + const normalized = normalizeDeprecatedGlmtEnv(sortEnvRecord(env)); + env = normalized.env; + warnings.push(...normalized.warnings); + notes.push('Create or migrate to a glm profile when convenient to remove legacy GLMT config.'); + } + if (!requestedIsDefault && continuity.claudeConfigDir && !env.CLAUDE_CONFIG_DIR) { env.CLAUDE_CONFIG_DIR = continuity.claudeConfigDir; notes.push( diff --git a/src/shared/provider-preset-catalog.ts b/src/shared/provider-preset-catalog.ts index 42c6e7be..e6996a04 100644 --- a/src/shared/provider-preset-catalog.ts +++ b/src/shared/provider-preset-catalog.ts @@ -14,7 +14,6 @@ export const PROVIDER_PRESET_IDS = [ 'llamacpp', 'anthropic', 'glm', - 'glmt', 'km', 'foundry', 'mm', @@ -54,6 +53,7 @@ export const OPENROUTER_BASE_URL = 'https://openrouter.ai/api'; * Keep this minimal and explicit to avoid hidden implicit behavior. */ export const PROVIDER_PRESET_ALIASES: Readonly> = Object.freeze({ + glmt: 'glm', kimi: 'km', alibaba: 'alibaba-coding-plan', acp: 'alibaba-coding-plan', @@ -137,7 +137,7 @@ const RAW_PROVIDER_PRESET_DEFINITIONS: readonly ProviderPresetDefinition[] = [ { id: 'glm', name: 'GLM', - description: 'Claude via Z.AI', + description: 'Direct Z.AI Anthropic-compatible API profile', baseUrl: 'https://api.z.ai/api/anthropic', defaultProfileName: 'glm', defaultModel: 'glm-5', @@ -148,29 +148,6 @@ const RAW_PROVIDER_PRESET_DEFINITIONS: readonly ProviderPresetDefinition[] = [ badge: 'Z.AI', icon: '/icons/zai.svg', }, - { - id: 'glmt', - name: 'GLMT', - description: 'GLM with Thinking mode support', - baseUrl: 'https://api.z.ai/api/coding/paas/v4/chat/completions', - defaultProfileName: 'glmt', - defaultModel: 'glm-5', - apiKeyPlaceholder: 'ghp_...', - apiKeyHint: 'Same API key as GLM', - category: 'alternative', - requiresApiKey: true, - extraEnv: { - ANTHROPIC_TEMPERATURE: '0.2', - ANTHROPIC_MAX_TOKENS: '65536', - MAX_THINKING_TOKENS: '32768', - ENABLE_STREAMING: 'true', - ANTHROPIC_SAFE_MODE: 'false', - API_TIMEOUT_MS: '3000000', - }, - alwaysThinkingEnabled: true, - badge: 'Thinking', - icon: '/icons/zai.svg', - }, { id: 'km', name: 'Kimi', diff --git a/src/types/index.ts b/src/types/index.ts index 494883be..f88f447d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -32,7 +32,7 @@ export type { DelegationEvent, } from './delegation'; -// GLMT types +// Legacy GLMT transformer types export type { AnthropicMessage, ContentBlock, diff --git a/src/utils/config-manager.ts b/src/utils/config-manager.ts index 28b09105..7b8ce3cb 100644 --- a/src/utils/config-manager.ts +++ b/src/utils/config-manager.ts @@ -337,7 +337,7 @@ export function getSettingsPath(profile: string): string { /** * Get display name for a profile by reading ANTHROPIC_MODEL from settings - * @param profile - Profile name (glm, glmt, kimi, custom, etc.) + * @param profile - Profile name (glm, km, glmt compatibility, custom, etc.) * @returns Formatted display name (e.g., 'GLM-4.7', 'Kimi', 'Custom-Model') */ export function getModelDisplayName(profile: string): string { diff --git a/src/utils/glmt-deprecation.ts b/src/utils/glmt-deprecation.ts new file mode 100644 index 00000000..8235aea8 --- /dev/null +++ b/src/utils/glmt-deprecation.ts @@ -0,0 +1,81 @@ +const LEGACY_GLMT_PROFILE = 'glmt'; +const LEGACY_GLMT_BASE_URL = 'https://api.z.ai/api/coding/paas/v4/chat/completions'; +const DIRECT_GLM_BASE_URL = 'https://api.z.ai/api/anthropic'; + +const GLMT_PROXY_ONLY_ENV_KEYS = [ + 'API_TIMEOUT_MS', + 'ANTHROPIC_SAFE_MODE', + 'ENABLE_STREAMING', + 'MAX_THINKING_TOKENS', +] as const; + +export interface GlmtNormalizationResult { + env: Record; + warnings: string[]; + migrated: boolean; +} + +export function isDeprecatedGlmtProfileName(profileName: string | null | undefined): boolean { + return (profileName || '').trim().toLowerCase() === LEGACY_GLMT_PROFILE; +} + +export function isLegacyGlmtBaseUrl(baseUrl: string | null | undefined): boolean { + const normalized = (baseUrl || '').trim().toLowerCase().replace(/\/+$/, ''); + if (!normalized) { + return false; + } + + return ( + normalized === LEGACY_GLMT_BASE_URL || + normalized.includes('/api/coding/paas/v4') || + normalized.endsWith('/chat/completions') + ); +} + +export function normalizeDeprecatedGlmtEnv(env: Record): GlmtNormalizationResult { + const normalizedEnv = { ...env }; + let migrated = false; + + if ( + !normalizedEnv['ANTHROPIC_BASE_URL'] || + isLegacyGlmtBaseUrl(normalizedEnv['ANTHROPIC_BASE_URL']) + ) { + normalizedEnv['ANTHROPIC_BASE_URL'] = DIRECT_GLM_BASE_URL; + migrated = true; + } + + for (const key of GLMT_PROXY_ONLY_ENV_KEYS) { + if (key in normalizedEnv) { + delete normalizedEnv[key]; + migrated = true; + } + } + + return { + env: normalizedEnv, + warnings: buildGlmtCompatibilityWarnings(migrated), + migrated, + }; +} + +export function buildGlmtCompatibilityWarnings(migrated: boolean): string[] { + const warnings = [ + 'GLMT is deprecated and kept only as a compatibility path.', + 'Use ccs glm for Z.AI API profiles.', + 'Use ccs km for reasoning-first Kimi API profiles.', + ]; + + if (migrated) { + warnings.splice( + 1, + 0, + 'CCS normalized legacy GLMT proxy settings to the direct GLM endpoint for this run.' + ); + } + + return warnings; +} + +export function getDirectGlmBaseUrl(): string { + return DIRECT_GLM_BASE_URL; +} diff --git a/tests/README.md b/tests/README.md index 3abf42e6..c5e4acf4 100644 --- a/tests/README.md +++ b/tests/README.md @@ -5,7 +5,7 @@ ``` tests/ ├── unit/ # Module unit tests (Mocha) -│ ├── glmt/ # GLMT transformer tests +│ ├── glmt/ # Legacy GLMT transformer/internal compatibility tests │ └── delegation/ # Delegation module tests ├── npm/ # npm package tests (Mocha) ├── native/ # Native installation tests (bash/PowerShell) @@ -32,7 +32,7 @@ bun run test:native # Native Unix tests (bash) ### Unit Tests (`unit/`) Module-level tests using Mocha framework: -- `unit/glmt/` - GLMT transformer, SSE parser, delta accumulator +- `unit/glmt/` - Legacy transformer internals kept for Cursor translation compatibility - `unit/delegation/` - Permission mode, session manager, result formatter ### npm Tests (`npm/`) @@ -53,7 +53,7 @@ Integration and smoke coverage for scenarios that exercise multiple layers: - Shell and standalone probe scripts remain on-demand for targeted debugging - `cursor-daemon-lifecycle.test.ts` - local daemon process + HTTP smoke coverage - `image-analyzer-hook.test.ts` - hook integration coverage -- `glmt-integration-test.sh` - GLMT integration probe +- `glmt-integration-test.sh` - legacy GLMT compatibility smoke probe - `symlink-chain-test.sh` - Symlink chain handling - `ux-integration-test.sh` - CLI UX integration diff --git a/tests/integration/glmt-integration-test.sh b/tests/integration/glmt-integration-test.sh index 84cf7ba2..2dfb0239 100755 --- a/tests/integration/glmt-integration-test.sh +++ b/tests/integration/glmt-integration-test.sh @@ -1,170 +1,84 @@ #!/usr/bin/env bash # -# GLMT Integration Test Suite -# Tests proxy startup, configuration, and basic functionality +# Legacy GLMT compatibility smoke probe +# Verifies that user-facing GLMT marketing is gone while internal transformer +# modules still exist for compatibility and Cursor translation. # set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -CCS_DIR="$(dirname "$SCRIPT_DIR")" - -echo "=== GLMT Integration Test Suite ===" -echo "" - -# Colors -GREEN='\033[0;32m' -RED='\033[0;31m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color +REPO_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")" PASSED=0 FAILED=0 test_pass() { - echo -e "${GREEN}✓${NC} $1" - ((PASSED++)) + echo "[OK] $1" + PASSED=$((PASSED + 1)) } test_fail() { - echo -e "${RED}✗${NC} $1" - ((FAILED++)) + echo "[X] $1" + FAILED=$((FAILED + 1)) } test_info() { - echo -e "${YELLOW}ℹ${NC} $1" + echo "[i] $1" } -# Test 1: Check GLMT profile exists in config -echo "Test 1: GLMT profile configuration" -if grep -q '"glmt"' ~/.ccs/config.json; then - test_pass "GLMT profile found in config.json" -else - test_fail "GLMT profile NOT found in config.json" -fi +echo "=== Legacy GLMT Compatibility Smoke Test ===" +echo -# Test 2: Check GLMT settings file exists -echo "Test 2: GLMT settings file" -if [ -f ~/.ccs/glmt.settings.json ]; then - test_pass "GLMT settings file exists" - - # Check if API key is configured - if grep -q "YOUR_GLM_API_KEY_HERE" ~/.ccs/glmt.settings.json; then - test_info "API key not configured (still has placeholder)" - else - test_pass "API key configured" - fi -else - test_fail "GLMT settings file NOT found" -fi - -# Test 3: Check transformer module -echo "Test 3: Transformer module" -if [ -f "$CCS_DIR/bin/glmt-transformer.js" ]; then - test_pass "Transformer module exists" - - # Test syntax - if node --check "$CCS_DIR/bin/glmt-transformer.js" 2>/dev/null; then - test_pass "Transformer syntax valid" - else - test_fail "Transformer syntax invalid" - fi -else - test_fail "Transformer module NOT found" -fi - -# Test 4: Check proxy module -echo "Test 4: Proxy module" -if [ -f "$CCS_DIR/bin/glmt-proxy.js" ]; then - test_pass "Proxy module exists" - - # Test syntax - if node --check "$CCS_DIR/bin/glmt-proxy.js" 2>/dev/null; then - test_pass "Proxy syntax valid" - else - test_fail "Proxy syntax invalid" - fi -else - test_fail "Proxy module NOT found" -fi - -# Test 5: Test proxy startup -echo "Test 5: Proxy startup test" -test_info "Starting proxy in background..." - -# Start proxy -node "$CCS_DIR/bin/glmt-proxy.js" & -PROXY_PID=$! - -# Wait for PROXY_READY signal (with timeout) -TIMEOUT=5 -PORT="" -for i in $(seq 1 $TIMEOUT); do - if ps -p $PROXY_PID > /dev/null 2>&1; then - sleep 0.2 - # Check if proxy outputted anything (we can't easily capture it in background) - if [ $i -eq $TIMEOUT ]; then - test_fail "Proxy started but PROXY_READY signal not captured (this is OK - proxy works)" - PORT="unknown" - fi - else - test_fail "Proxy failed to start or exited immediately" - break - fi +echo "Test 1: Internal transformer sources remain available" +for file in \ + "$REPO_DIR/src/glmt/glmt-transformer.ts" \ + "$REPO_DIR/src/glmt/delta-accumulator.ts" \ + "$REPO_DIR/src/glmt/sse-parser.ts" \ + "$REPO_DIR/src/cursor/cursor-anthropic-response.ts"; do + if [ -f "$file" ]; then + test_pass "$(basename "$file") exists" + else + test_fail "Missing internal compatibility file: $file" + fi done -if ps -p $PROXY_PID > /dev/null 2>&1; then - test_pass "Proxy process is running (PID: $PROXY_PID)" - - # Kill proxy - kill $PROXY_PID 2>/dev/null || true - sleep 0.5 - - if ! ps -p $PROXY_PID > /dev/null 2>&1; then - test_pass "Proxy terminated gracefully" - else - test_fail "Proxy did not terminate (killing forcefully)" - kill -9 $PROXY_PID 2>/dev/null || true - fi -fi - -# Test 6: Unit tests -echo "Test 6: Unit tests" -if [ -f "$CCS_DIR/tests/glmt-transformer.test.js" ]; then - test_info "Running transformer unit tests..." - if node "$CCS_DIR/tests/glmt-transformer.test.js" | grep -q "Passed: 12/12"; then - test_pass "All 12 unit tests passed" - else - test_fail "Some unit tests failed" - fi +echo +echo "Test 2: Root help no longer advertises ccs glmt" +if command -v ccs >/dev/null 2>&1; then + if ccs --help | grep -q "ccs glmt"; then + test_fail "Root help still advertises ccs glmt" + else + test_pass "Root help hides ccs glmt" + fi else - test_fail "Unit test file NOT found" + test_info "ccs binary not found in PATH; skipping live help probe" fi -# Test 7: Help text -echo "Test 7: Help text verification" -if ccs --help | grep -q "ccs glmt"; then - test_pass "GLMT appears in help text" +echo +echo "Test 3: Legacy glmt settings file inspection" +if [ -f "$HOME/.ccs/glmt.settings.json" ]; then + test_pass "Legacy glmt.settings.json detected" + if grep -q "api/coding/paas/v4/chat/completions" "$HOME/.ccs/glmt.settings.json"; then + test_info "Legacy proxy endpoint still present in settings; CCS should normalize it at runtime" + else + test_info "Settings already point at a direct endpoint or custom override" + fi else - test_fail "GLMT NOT in help text" + test_info "No legacy glmt.settings.json found; nothing to migrate locally" fi -# Summary -echo "" -echo "=== Test Summary ===" -echo -e "Passed: ${GREEN}$PASSED${NC}" -echo -e "Failed: ${RED}$FAILED${NC}" -echo "" +echo +echo "=== Summary ===" +echo "Passed: $PASSED" +echo "Failed: $FAILED" +echo -if [ $FAILED -eq 0 ]; then - echo -e "${GREEN}All tests passed!${NC}" - echo "" - echo "Next steps:" - echo " 1. Test with Claude Code: ccs glmt \"What is 2+2?\"" - echo " 2. Test thinking tags: ccs glmt \" Explain recursion\"" - echo " 3. Check thinking blocks appear in Claude Code UI" - exit 0 -else - echo -e "${RED}Some tests failed. Please review above.${NC}" - exit 1 +if [ "$FAILED" -eq 0 ]; then + echo "[OK] Legacy compatibility surface looks consistent." + echo "[i] Preferred profiles: ccs glm for Z.AI, ccs km for reasoning-first Kimi." + exit 0 fi + +echo "[X] Review the failures above." +exit 1 diff --git a/tests/npm/cli.test.js b/tests/npm/cli.test.js index a89072ac..d32055b4 100644 --- a/tests/npm/cli.test.js +++ b/tests/npm/cli.test.js @@ -77,8 +77,9 @@ describe('npm CLI', () => { }); describe('Profile handling', () => { - // Note: GLM/GLMT/Kimi profiles are no longer auto-created (v6.0) - // Users create these via UI presets or CLI: ccs api create --preset glm + // Note: GLM/Kimi profiles are no longer auto-created (v6.0). + // Legacy GLMT files may still exist, but new supported API profiles are created + // via UI presets or CLI: ccs api create --preset glm it('shows helpful error for non-existent profile', function() { try { diff --git a/tests/npm/postinstall.test.js b/tests/npm/postinstall.test.js index 136df681..f7f56700 100644 --- a/tests/npm/postinstall.test.js +++ b/tests/npm/postinstall.test.js @@ -46,8 +46,9 @@ describe('npm postinstall', () => { env: { ...process.env, CCS_HOME: testEnv.testHome } }); - // GLM/GLMT/Kimi profiles are NO LONGER auto-created during install - // Users create these via UI presets or CLI: ccs api create --preset glm + // GLM/Kimi profiles are NO LONGER auto-created during install. + // Legacy glmt.settings.json files may still exist from older setups. + // Users create supported API profiles via UI presets or CLI: ccs api create --preset glm assert(!testEnv.fileExists('glm.settings.json'), 'glm.settings.json should NOT be auto-created'); assert(!testEnv.fileExists('glmt.settings.json'), 'glmt.settings.json should NOT be auto-created'); assert(!testEnv.fileExists('kimi.settings.json'), 'kimi.settings.json should NOT be auto-created'); @@ -110,7 +111,7 @@ describe('npm postinstall', () => { // Verify existing file still exists and new files are created assert(testEnv.fileExists('existing.txt'), 'Existing files should be preserved'); assert(testEnv.fileExists('config.yaml'), 'config.yaml should be created'); - // GLM/GLMT/Kimi are no longer auto-created + // GLM/Kimi are no longer auto-created. Legacy GLMT files remain untouched if present. assert(!testEnv.fileExists('glm.settings.json'), 'glm.settings.json should NOT be auto-created'); }); diff --git a/tests/unit/api/provider-presets.test.ts b/tests/unit/api/provider-presets.test.ts index e4518e07..752f0d13 100644 --- a/tests/unit/api/provider-presets.test.ts +++ b/tests/unit/api/provider-presets.test.ts @@ -42,6 +42,12 @@ describe('provider-presets', () => { expect(preset?.id).toBe('km'); }); + it('resolves legacy glmt preset alias to glm', () => { + const preset = getPresetById('glmt'); + expect(preset?.id).toBe('glm'); + expect(preset?.baseUrl).toBe('https://api.z.ai/api/anthropic'); + }); + it('resolves preset id with extra whitespace', () => { const preset = getPresetById(' km '); expect(preset?.id).toBe('km'); @@ -56,6 +62,11 @@ describe('provider-presets', () => { expect(isValidPresetId('kimi')).toBe(true); }); + it('keeps glmt out of the canonical preset catalog while preserving alias compatibility', () => { + expect(PROVIDER_PRESETS.some((preset) => preset.id === 'glmt')).toBe(false); + expect(isValidPresetId('glmt')).toBe(true); + }); + it('uses non-reserved default profile name for qwen API preset', () => { const preset = getPresetById('qwen'); expect(preset?.defaultProfileName).toBe('qwen-api'); diff --git a/tests/unit/commands/help-command-parity.test.ts b/tests/unit/commands/help-command-parity.test.ts index 2b3f6740..2ed57bd1 100644 --- a/tests/unit/commands/help-command-parity.test.ts +++ b/tests/unit/commands/help-command-parity.test.ts @@ -41,6 +41,19 @@ describe('help command parity', () => { expect(rendered.includes('http://127.0.0.1:8080')).toBe(true); }); + test('root help no longer markets glmt as a supported profile', async () => { + const lines: string[] = []; + console.log = (...args: unknown[]) => { + lines.push(args.map((arg) => String(arg)).join(' ')); + }; + + await handleHelpCommand(); + + const rendered = stripAnsi(lines.join('\n')); + expect(rendered.includes('ccs glmt')).toBe(false); + expect(rendered.includes('ccs glm')).toBe(true); + }); + test('root help documents Claude IDE extension setup surfaces', async () => { const lines: string[] = []; console.log = (...args: unknown[]) => { diff --git a/tests/unit/glmt/test-thinking-multi-message.js b/tests/unit/glmt/test-thinking-multi-message.js index d909fad7..ed7d62f0 100644 --- a/tests/unit/glmt/test-thinking-multi-message.js +++ b/tests/unit/glmt/test-thinking-multi-message.js @@ -1,214 +1,17 @@ #!/usr/bin/env node 'use strict'; -/** - * Test Script: Multi-message thinking block behavior - * - * Simulates 3 consecutive messages to test if thinking blocks - * appear in all messages or only the first one. - * - * Usage: CCS_DEBUG=1 node test-thinking-multi-message.js - */ - -const { spawn } = require('child_process'); -const path = require('path'); -const fs = require('fs'); - -const ccsPath = path.join(__dirname, 'bin', 'ccs.js'); -const logDir = path.join(require('os').homedir(), '.ccs', 'logs'); - -// Ensure logs directory exists -if (!fs.existsSync(logDir)) { - fs.mkdirSync(logDir, { recursive: true }); -} - console.log('='.repeat(60)); -console.log('GLMT Multi-Message Thinking Block Test'); +console.log('Legacy GLMT Multi-Message Probe'); console.log('='.repeat(60)); console.log(''); -console.log('Test scenario: 3 consecutive messages with thinking enabled'); -console.log('Expected: Thinking blocks appear in ALL 3 messages'); -console.log('Actual: User reports thinking only in first message'); +console.log('This manual script targeted the retired `ccs glmt` runtime path.'); +console.log('CCS now routes supported Z.AI usage through `ccs glm`, and thinking is'); +console.log('handled natively by current upstream models instead of the old GLMT proxy.'); console.log(''); -console.log('Log directory:', logDir); +console.log('Use one of these instead:'); +console.log(' 1. tests/integration/glmt-integration-test.sh'); +console.log(' 2. ccs glm ""'); +console.log(' 3. internal transformer tests under tests/unit/glmt/'); console.log(''); - -// Test messages -const messages = [ - 'Message 1: Calculate 15! (factorial)', - 'Message 2: What is the square root of 2 to 10 decimal places?', - 'Message 3: Explain the Pythagorean theorem' -]; - -// Track results -const results = { - message1: { thinking: false, error: null }, - message2: { thinking: false, error: null }, - message3: { thinking: false, error: null } -}; - -async function runMessage(messageIndex) { - const message = messages[messageIndex]; - const messageKey = `message${messageIndex + 1}`; - - console.log('-'.repeat(60)); - console.log(`Testing Message ${messageIndex + 1}/${messages.length}`); - console.log(`Prompt: "${message}"`); - console.log('-'.repeat(60)); - - return new Promise((resolve, reject) => { - const startTime = Date.now(); - - // Clear old logs for this test - const beforeFiles = fs.readdirSync(logDir).filter(f => f.endsWith('.json')); - - // Use process.execPath for Windows compatibility (CVE-2024-27980) - const child = spawn(process.execPath, [ccsPath, 'glmt', '--verbose', message], { - stdio: ['ignore', 'pipe', 'pipe'], - env: { - ...process.env, - CCS_DEBUG: '1' - } - }); - - let stdout = ''; - let stderr = ''; - - child.stdout.on('data', (data) => { - const text = data.toString(); - stdout += text; - - // Check for thinking indicator - if (text.includes('∴ Thinking') || text.includes('Thinking…')) { - results[messageKey].thinking = true; - console.log('[✓] Thinking block detected in stdout'); - } - }); - - child.stderr.on('data', (data) => { - stderr += data.toString(); - }); - - child.on('close', (code) => { - const duration = Date.now() - startTime; - - console.log(''); - console.log(`Process exited with code ${code} after ${duration}ms`); - - // Check logs - const afterFiles = fs.readdirSync(logDir).filter(f => f.endsWith('.json')); - const newFiles = afterFiles.filter(f => !beforeFiles.includes(f)); - - console.log(`New log files: ${newFiles.length}`); - - // Check for reasoning_content in response logs - const responseFiles = newFiles.filter(f => f.includes('response-openai')); - console.log(`Response log files: ${responseFiles.length}`); - - if (responseFiles.length > 0) { - const latestResponse = responseFiles.sort().pop(); - const responsePath = path.join(logDir, latestResponse); - console.log(`Latest response log: ${latestResponse}`); - - try { - const responseData = JSON.parse(fs.readFileSync(responsePath, 'utf8')); - const reasoningContent = responseData.choices?.[0]?.message?.reasoning_content; - - if (reasoningContent) { - const length = reasoningContent.length; - const lines = reasoningContent.split('\n').length; - console.log(`[✓] reasoning_content found: ${length} chars, ${lines} lines`); - results[messageKey].thinking = true; - } else { - console.log('[X] No reasoning_content in response'); - results[messageKey].thinking = false; - } - } catch (e) { - console.log(`[!] Error reading response log: ${e.message}`); - results[messageKey].error = e.message; - } - } else { - console.log('[X] No response logs found'); - results[messageKey].error = 'No response logs'; - } - - console.log(''); - - if (code === 0) { - resolve(); - } else { - results[messageKey].error = `Exit code ${code}`; - reject(new Error(`Process exited with code ${code}`)); - } - }); - - child.on('error', (error) => { - console.error(`[X] Process error: ${error.message}`); - results[messageKey].error = error.message; - reject(error); - }); - }); -} - -async function main() { - try { - // Run messages sequentially - for (let i = 0; i < messages.length; i++) { - await runMessage(i); - - // Wait a bit between messages - if (i < messages.length - 1) { - console.log('Waiting 2s before next message...'); - console.log(''); - await new Promise(resolve => setTimeout(resolve, 2000)); - } - } - - // Final summary - console.log('='.repeat(60)); - console.log('TEST RESULTS'); - console.log('='.repeat(60)); - console.log(''); - - for (let i = 1; i <= 3; i++) { - const key = `message${i}`; - const result = results[key]; - const status = result.thinking ? '[✓ PASS]' : '[X FAIL]'; - console.log(`${status} Message ${i}: Thinking = ${result.thinking}`); - if (result.error) { - console.log(` Error: ${result.error}`); - } - } - - console.log(''); - - const passCount = Object.values(results).filter(r => r.thinking).length; - const failCount = 3 - passCount; - - console.log(`Summary: ${passCount}/3 messages showed thinking blocks`); - console.log(''); - - if (failCount > 0) { - console.log('[!] ISSUE CONFIRMED: Some messages missing thinking blocks'); - console.log(''); - console.log('Next steps:'); - console.log(' 1. Analyze request logs to verify reasoning params'); - console.log(' 2. Check if transformer is being called correctly'); - console.log(' 3. Verify state management (accumulator/parser)'); - console.log(''); - process.exit(1); - } else { - console.log('[✓] ALL TESTS PASSED: Thinking blocks appear in all messages'); - console.log(''); - process.exit(0); - } - - } catch (error) { - console.error(''); - console.error('[X] Test failed:', error.message); - console.error(''); - process.exit(1); - } -} - -main(); +process.exit(0); diff --git a/tests/unit/utils/glmt-deprecation.test.ts b/tests/unit/utils/glmt-deprecation.test.ts new file mode 100644 index 00000000..d003a9e7 --- /dev/null +++ b/tests/unit/utils/glmt-deprecation.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from 'bun:test'; +import { + buildGlmtCompatibilityWarnings, + isDeprecatedGlmtProfileName, + isLegacyGlmtBaseUrl, + normalizeDeprecatedGlmtEnv, +} from '../../../src/utils/glmt-deprecation'; + +describe('glmt deprecation helpers', () => { + it('detects the deprecated glmt profile name case-insensitively', () => { + expect(isDeprecatedGlmtProfileName('glmt')).toBe(true); + expect(isDeprecatedGlmtProfileName('GLMT')).toBe(true); + expect(isDeprecatedGlmtProfileName('glm')).toBe(false); + }); + + it('detects legacy GLMT proxy base URLs', () => { + expect(isLegacyGlmtBaseUrl('https://api.z.ai/api/coding/paas/v4/chat/completions')).toBe( + true + ); + expect(isLegacyGlmtBaseUrl('https://api.z.ai/api/coding/paas/v4/chat/completions/')).toBe( + true + ); + expect(isLegacyGlmtBaseUrl('https://api.z.ai/api/anthropic')).toBe(false); + }); + + it('normalizes legacy GLMT proxy settings to the direct GLM endpoint', () => { + const result = normalizeDeprecatedGlmtEnv({ + ANTHROPIC_BASE_URL: 'https://api.z.ai/api/coding/paas/v4/chat/completions', + ANTHROPIC_AUTH_TOKEN: 'ghp_test', + ANTHROPIC_MODEL: 'glm-5', + ENABLE_STREAMING: 'true', + MAX_THINKING_TOKENS: '32768', + API_TIMEOUT_MS: '3000000', + }); + + expect(result.migrated).toBe(true); + expect(result.env['ANTHROPIC_BASE_URL']).toBe('https://api.z.ai/api/anthropic'); + expect(result.env['ENABLE_STREAMING']).toBeUndefined(); + expect(result.env['MAX_THINKING_TOKENS']).toBeUndefined(); + expect(result.env['API_TIMEOUT_MS']).toBeUndefined(); + expect(result.warnings).toContain( + 'CCS normalized legacy GLMT proxy settings to the direct GLM endpoint for this run.' + ); + }); + + it('keeps already-direct GLM settings intact apart from deprecation messaging', () => { + const result = normalizeDeprecatedGlmtEnv({ + ANTHROPIC_BASE_URL: 'https://api.z.ai/api/anthropic', + ANTHROPIC_AUTH_TOKEN: 'ghp_test', + ANTHROPIC_MODEL: 'glm-5', + }); + + expect(result.migrated).toBe(false); + expect(result.env['ANTHROPIC_BASE_URL']).toBe('https://api.z.ai/api/anthropic'); + expect(result.warnings).toEqual(buildGlmtCompatibilityWarnings(false)); + }); +}); diff --git a/ui/src/components/profiles/profile-create-dialog.tsx b/ui/src/components/profiles/profile-create-dialog.tsx index d08f0c98..0debffa0 100644 --- a/ui/src/components/profiles/profile-create-dialog.tsx +++ b/ui/src/components/profiles/profile-create-dialog.tsx @@ -237,7 +237,7 @@ export function ProfileCreateDialog({ }; // Check for common URL mistakes - only for truly custom URLs - // Presets (OpenRouter, GLM, GLMT, Kimi) have vetted URLs that may require full paths + // Presets (OpenRouter, GLM, Kimi) have vetted URLs that may require full paths useEffect(() => { // Only warn for custom URLs, not preset-selected ones const isCustomUrl = selectedPreset === CUSTOM_PRESET_ID; diff --git a/ui/tests/unit/ui/lib/provider-presets.test.ts b/ui/tests/unit/ui/lib/provider-presets.test.ts index a8566369..346abb3c 100644 --- a/ui/tests/unit/ui/lib/provider-presets.test.ts +++ b/ui/tests/unit/ui/lib/provider-presets.test.ts @@ -28,6 +28,10 @@ describe('resolvePresetApiKeyValue', () => { }); describe('provider preset metadata', () => { + it('maps legacy glmt preset requests to glm', () => { + expect(getPresetById('glmt')?.id).toBe('glm'); + }); + it('keeps Anthropic direct last in the recommended order', () => { const recommendedPresetIds = getPresetsByCategory('recommended').map((preset) => preset.id); expect(recommendedPresetIds.at(-1)).toBe('anthropic');