From 70bf0806f33bcf33e74dbe7a10b258879dc9f104 Mon Sep 17 00:00:00 2001 From: juwain Date: Mon, 4 May 2026 14:59:14 +0300 Subject: [PATCH 1/7] fix: use aarch64 for CLIProxy arm assets --- src/cliproxy/binary/platform-detector.ts | 8 ++++---- .../types/__tests__/types-backward-compat.test.ts | 4 ++-- src/cliproxy/types/platform-types.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/cliproxy/binary/platform-detector.ts b/src/cliproxy/binary/platform-detector.ts index 9c1d2f9f..1692a356 100644 --- a/src/cliproxy/binary/platform-detector.ts +++ b/src/cliproxy/binary/platform-detector.ts @@ -2,7 +2,7 @@ * Platform Detector for CLIProxyAPI Binary Downloads * * Detects OS and architecture to determine correct binary asset. - * Supports 6 platforms: darwin/linux/windows x amd64/arm64 + * Supports 6 platforms: darwin/linux/windows x amd64/aarch64 */ import { @@ -75,7 +75,7 @@ const OS_MAP: Record = { const ARCH_MAP: Record = { x64: 'amd64', - arm64: 'arm64', + arm64: 'aarch64', }; /** @@ -103,7 +103,7 @@ export function detectPlatform( if (!arch) { throw new Error( - `Unsupported CPU architecture: ${nodeArch}\n` + `Supported: x64 (amd64), arm64` + `Unsupported CPU architecture: ${nodeArch}\n` + `Supported: x64 (amd64), arm64 (aarch64)` ); } @@ -195,7 +195,7 @@ export function isPlatformSupported(): boolean { /** * Get human-readable platform description - * @returns Description string (e.g., "macOS arm64") + * @returns Description string (e.g., "macOS aarch64") */ export function getPlatformDescription(): string { try { diff --git a/src/cliproxy/types/__tests__/types-backward-compat.test.ts b/src/cliproxy/types/__tests__/types-backward-compat.test.ts index 5ee676ad..9d38736b 100644 --- a/src/cliproxy/types/__tests__/types-backward-compat.test.ts +++ b/src/cliproxy/types/__tests__/types-backward-compat.test.ts @@ -34,7 +34,7 @@ import { PLUS_ONLY_PROVIDERS } from '../../types'; describe('types.ts backward compatibility', () => { it('exports all platform types', () => { const os: SupportedOS = 'darwin'; - const arch: SupportedArch = 'arm64'; + const arch: SupportedArch = 'aarch64'; const ext: ArchiveExtension = 'tar.gz'; const info: PlatformInfo = { os, @@ -43,7 +43,7 @@ describe('types.ts backward compatibility', () => { extension: ext, }; expect(info.os).toBe('darwin'); - expect(info.arch).toBe('arm64'); + expect(info.arch).toBe('aarch64'); }); it('exports all binary types', () => { diff --git a/src/cliproxy/types/platform-types.ts b/src/cliproxy/types/platform-types.ts index 0fdd0f65..e05646c7 100644 --- a/src/cliproxy/types/platform-types.ts +++ b/src/cliproxy/types/platform-types.ts @@ -6,7 +6,7 @@ export type SupportedOS = 'darwin' | 'linux' | 'windows'; /** Supported CPU architectures */ -export type SupportedArch = 'amd64' | 'arm64'; +export type SupportedArch = 'amd64' | 'aarch64'; /** Archive extension based on platform */ export type ArchiveExtension = 'tar.gz' | 'zip'; From 4acc81c08221dbdff79319998ab1e290645ebb45 Mon Sep 17 00:00:00 2001 From: juwain Date: Mon, 4 May 2026 15:36:37 +0300 Subject: [PATCH 2/7] style: apply prettier formatting --- src/cliproxy/quota/quota-manager.ts | 5 ++--- src/management/checks/image-analysis-check.ts | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/cliproxy/quota/quota-manager.ts b/src/cliproxy/quota/quota-manager.ts index 4493aeb6..39841eb6 100644 --- a/src/cliproxy/quota/quota-manager.ts +++ b/src/cliproxy/quota/quota-manager.ts @@ -524,9 +524,8 @@ export async function preflightCheck(provider: CLIProxyProvider): Promise * Fix image analysis configuration issues */ export async function fixImageAnalysisConfig(): Promise { - const { updateConfig, loadOrCreateUnifiedConfig } = await import( - '../../config/config-loader-facade' - ); + const { updateConfig, loadOrCreateUnifiedConfig } = + await import('../../config/config-loader-facade'); const config = loadOrCreateUnifiedConfig(); let fixed = false; From 2274cef6588b01e818b60e042481d31dc2f83dd1 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Mon, 4 May 2026 10:44:20 -0400 Subject: [PATCH 3/7] fix: preserve arm64 compatibility for CLIProxy assets --- src/cliproxy/binary/platform-detector.ts | 11 ++++++++--- .../config/__tests__/backend-selection.test.js | 5 +++++ src/cliproxy/quota/quota-manager.ts | 5 +++-- .../types/__tests__/types-backward-compat.test.ts | 2 ++ src/cliproxy/types/platform-types.ts | 4 ++-- src/management/checks/image-analysis-check.ts | 5 +++-- 6 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/cliproxy/binary/platform-detector.ts b/src/cliproxy/binary/platform-detector.ts index 1692a356..521d72b4 100644 --- a/src/cliproxy/binary/platform-detector.ts +++ b/src/cliproxy/binary/platform-detector.ts @@ -78,6 +78,10 @@ const ARCH_MAP: Record = { arm64: 'aarch64', }; +export function mapNodeArchToReleaseArch(nodeArch: string): SupportedArch | undefined { + return ARCH_MAP[nodeArch]; +} + /** * Detect current platform and return binary info * @param version Optional version for binaryName (defaults to backend fallback) @@ -92,7 +96,7 @@ export function detectPlatform( const nodeArch = process.arch; const os = OS_MAP[nodePlatform]; - const arch = ARCH_MAP[nodeArch]; + const arch = mapNodeArchToReleaseArch(nodeArch); if (!os) { throw new Error( @@ -195,14 +199,15 @@ export function isPlatformSupported(): boolean { /** * Get human-readable platform description - * @returns Description string (e.g., "macOS aarch64") + * @returns Description string (e.g., "macOS ARM64") */ export function getPlatformDescription(): string { try { const platform = detectPlatform(); const osName = platform.os === 'darwin' ? 'macOS' : platform.os === 'linux' ? 'Linux' : 'Windows'; - return `${osName} ${platform.arch}`; + const archName = platform.arch === 'aarch64' ? 'ARM64' : platform.arch; + return `${osName} ${archName}`; } catch { return `${process.platform} ${process.arch} (unsupported)`; } diff --git a/src/cliproxy/config/__tests__/backend-selection.test.js b/src/cliproxy/config/__tests__/backend-selection.test.js index 5c547984..a8bcb4b3 100644 --- a/src/cliproxy/config/__tests__/backend-selection.test.js +++ b/src/cliproxy/config/__tests__/backend-selection.test.js @@ -34,6 +34,11 @@ describe('Backend Selection', () => { }); describe('detectPlatform', () => { + it('maps Node arm64 to CLIProxy aarch64 release assets', () => { + assert.strictEqual(platformDetector.mapNodeArchToReleaseArch('arm64'), 'aarch64'); + assert.strictEqual(platformDetector.mapNodeArchToReleaseArch('x64'), 'amd64'); + }); + it('generates correct binary name for original backend', () => { const info = platformDetector.detectPlatform('6.6.51', 'original'); assert(info.binaryName.startsWith('CLIProxyAPI_6.6.51_')); diff --git a/src/cliproxy/quota/quota-manager.ts b/src/cliproxy/quota/quota-manager.ts index 39841eb6..4493aeb6 100644 --- a/src/cliproxy/quota/quota-manager.ts +++ b/src/cliproxy/quota/quota-manager.ts @@ -524,8 +524,9 @@ export async function preflightCheck(provider: CLIProxyProvider): Promise { it('exports all platform types', () => { const os: SupportedOS = 'darwin'; const arch: SupportedArch = 'aarch64'; + const nodeArch: SupportedArch = 'arm64'; const ext: ArchiveExtension = 'tar.gz'; const info: PlatformInfo = { os, @@ -44,6 +45,7 @@ describe('types.ts backward compatibility', () => { }; expect(info.os).toBe('darwin'); expect(info.arch).toBe('aarch64'); + expect(nodeArch).toBe('arm64'); }); it('exports all binary types', () => { diff --git a/src/cliproxy/types/platform-types.ts b/src/cliproxy/types/platform-types.ts index e05646c7..84f27d2d 100644 --- a/src/cliproxy/types/platform-types.ts +++ b/src/cliproxy/types/platform-types.ts @@ -5,8 +5,8 @@ /** Supported operating systems */ export type SupportedOS = 'darwin' | 'linux' | 'windows'; -/** Supported CPU architectures */ -export type SupportedArch = 'amd64' | 'aarch64'; +/** Supported CPU architecture labels from Node.js and CLIProxy release assets */ +export type SupportedArch = 'amd64' | 'arm64' | 'aarch64'; /** Archive extension based on platform */ export type ArchiveExtension = 'tar.gz' | 'zip'; diff --git a/src/management/checks/image-analysis-check.ts b/src/management/checks/image-analysis-check.ts index d6d2dc05..50e9ba99 100644 --- a/src/management/checks/image-analysis-check.ts +++ b/src/management/checks/image-analysis-check.ts @@ -112,8 +112,9 @@ export async function runImageAnalysisCheck(results: HealthCheck): Promise * Fix image analysis configuration issues */ export async function fixImageAnalysisConfig(): Promise { - const { updateConfig, loadOrCreateUnifiedConfig } = - await import('../../config/config-loader-facade'); + const { updateConfig, loadOrCreateUnifiedConfig } = await import( + '../../config/config-loader-facade' + ); const config = loadOrCreateUnifiedConfig(); let fixed = false; From 4a0620d6c5d62c28935b235461e5e3a9c10e5602 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 4 May 2026 10:59:26 -0400 Subject: [PATCH 4/7] chore(release): 7.77.0-dev.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dcbc0eec..d87fc457 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kaitranntt/ccs", - "version": "7.77.0", + "version": "7.77.0-dev.1", "description": "Claude Code Switch - Instant profile switching between Claude, GLM, Kimi, and more", "keywords": [ "cli", From 9a687fc03cf704a11e804780185ba24153766fd5 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Mon, 4 May 2026 11:39:47 -0400 Subject: [PATCH 5/7] fix: preserve CLIProxy platform arch compatibility --- src/cliproxy/binary/platform-detector.ts | 26 +++++++++++------ .../__tests__/backend-selection.test.js | 28 +++++++++++++++++++ .../__tests__/types-backward-compat.test.ts | 8 +++--- 3 files changed, 49 insertions(+), 13 deletions(-) diff --git a/src/cliproxy/binary/platform-detector.ts b/src/cliproxy/binary/platform-detector.ts index 521d72b4..eeb7c9f4 100644 --- a/src/cliproxy/binary/platform-detector.ts +++ b/src/cliproxy/binary/platform-detector.ts @@ -2,7 +2,7 @@ * Platform Detector for CLIProxyAPI Binary Downloads * * Detects OS and architecture to determine correct binary asset. - * Supports 6 platforms: darwin/linux/windows x amd64/aarch64 + * Supports 6 platforms: darwin/linux/windows x amd64/arm64 */ import { @@ -64,9 +64,7 @@ export const CLIPROXY_FAULTY_RANGE = { min: '6.6.81-0', max: '6.6.88-0' }; /** @deprecated Use CLIPROXY_FALLBACK_VERSION instead */ export const CLIPROXY_VERSION = CLIPROXY_FALLBACK_VERSION; -/** - * Platform mapping from Node.js values to CLIProxyAPI naming - */ +/** Platform mapping from Node.js values to CCS public architecture labels. */ const OS_MAP: Record = { darwin: 'darwin', linux: 'linux', @@ -75,11 +73,19 @@ const OS_MAP: Record = { const ARCH_MAP: Record = { x64: 'amd64', + arm64: 'arm64', +}; + +/** CLIProxy release assets use aarch64 while Node.js reports arm64. */ +const RELEASE_ARCH_MAP: Record = { + amd64: 'amd64', arm64: 'aarch64', + aarch64: 'aarch64', }; export function mapNodeArchToReleaseArch(nodeArch: string): SupportedArch | undefined { - return ARCH_MAP[nodeArch]; + const arch = ARCH_MAP[nodeArch]; + return arch ? RELEASE_ARCH_MAP[arch] : undefined; } /** @@ -96,7 +102,8 @@ export function detectPlatform( const nodeArch = process.arch; const os = OS_MAP[nodePlatform]; - const arch = mapNodeArchToReleaseArch(nodeArch); + const arch = ARCH_MAP[nodeArch]; + const releaseArch = mapNodeArchToReleaseArch(nodeArch); if (!os) { throw new Error( @@ -105,7 +112,7 @@ export function detectPlatform( ); } - if (!arch) { + if (!arch || !releaseArch) { throw new Error( `Unsupported CPU architecture: ${nodeArch}\n` + `Supported: x64 (amd64), arm64 (aarch64)` ); @@ -114,7 +121,7 @@ export function detectPlatform( const config = BACKEND_CONFIG[backend]; const ver = version || config.fallbackVersion; const extension: ArchiveExtension = os === 'windows' ? 'zip' : 'tar.gz'; - const binaryName = `${config.binaryPrefix}_${ver}_${os}_${arch}.${extension}`; + const binaryName = `${config.binaryPrefix}_${ver}_${os}_${releaseArch}.${extension}`; return { os, @@ -206,7 +213,8 @@ export function getPlatformDescription(): string { const platform = detectPlatform(); const osName = platform.os === 'darwin' ? 'macOS' : platform.os === 'linux' ? 'Linux' : 'Windows'; - const archName = platform.arch === 'aarch64' ? 'ARM64' : platform.arch; + const archName = + platform.arch === 'arm64' || platform.arch === 'aarch64' ? 'ARM64' : platform.arch; return `${osName} ${archName}`; } catch { return `${process.platform} ${process.arch} (unsupported)`; diff --git a/src/cliproxy/config/__tests__/backend-selection.test.js b/src/cliproxy/config/__tests__/backend-selection.test.js index a8bcb4b3..b4d1ca11 100644 --- a/src/cliproxy/config/__tests__/backend-selection.test.js +++ b/src/cliproxy/config/__tests__/backend-selection.test.js @@ -9,6 +9,21 @@ describe('Backend Selection', () => { const platformDetector = require('../../../../dist/cliproxy/binary/platform-detector'); const types = require('../../../../dist/cliproxy/types'); + function withMockedProcessPlatform(platform, arch, callback) { + const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform'); + const originalArch = Object.getOwnPropertyDescriptor(process, 'arch'); + + Object.defineProperty(process, 'platform', { value: platform, configurable: true }); + Object.defineProperty(process, 'arch', { value: arch, configurable: true }); + + try { + callback(); + } finally { + Object.defineProperty(process, 'platform', originalPlatform); + Object.defineProperty(process, 'arch', originalArch); + } + } + describe('BACKEND_CONFIG', () => { it('has correct configuration for original backend', () => { const config = platformDetector.BACKEND_CONFIG.original; @@ -39,6 +54,19 @@ describe('Backend Selection', () => { assert.strictEqual(platformDetector.mapNodeArchToReleaseArch('x64'), 'amd64'); }); + it('keeps public ARM64 arch compatibility while using aarch64 release assets', () => { + withMockedProcessPlatform('darwin', 'arm64', () => { + const info = platformDetector.detectPlatform('6.10.4', 'original'); + + assert.strictEqual(info.arch, 'arm64'); + assert.strictEqual(info.binaryName, 'CLIProxyAPI_6.10.4_darwin_aarch64.tar.gz'); + assert.strictEqual( + platformDetector.getDownloadUrl('6.10.4', 'original'), + 'https://github.com/router-for-me/CLIProxyAPI/releases/download/v6.10.4/CLIProxyAPI_6.10.4_darwin_aarch64.tar.gz' + ); + }); + }); + it('generates correct binary name for original backend', () => { const info = platformDetector.detectPlatform('6.6.51', 'original'); assert(info.binaryName.startsWith('CLIProxyAPI_6.6.51_')); diff --git a/src/cliproxy/types/__tests__/types-backward-compat.test.ts b/src/cliproxy/types/__tests__/types-backward-compat.test.ts index 1710639a..95952fb7 100644 --- a/src/cliproxy/types/__tests__/types-backward-compat.test.ts +++ b/src/cliproxy/types/__tests__/types-backward-compat.test.ts @@ -34,8 +34,8 @@ import { PLUS_ONLY_PROVIDERS } from '../../types'; describe('types.ts backward compatibility', () => { it('exports all platform types', () => { const os: SupportedOS = 'darwin'; - const arch: SupportedArch = 'aarch64'; - const nodeArch: SupportedArch = 'arm64'; + const arch: SupportedArch = 'arm64'; + const releaseArch: SupportedArch = 'aarch64'; const ext: ArchiveExtension = 'tar.gz'; const info: PlatformInfo = { os, @@ -44,8 +44,8 @@ describe('types.ts backward compatibility', () => { extension: ext, }; expect(info.os).toBe('darwin'); - expect(info.arch).toBe('aarch64'); - expect(nodeArch).toBe('arm64'); + expect(info.arch).toBe('arm64'); + expect(releaseArch).toBe('aarch64'); }); it('exports all binary types', () => { From 5f05dea5acac9b07aa55b6d66215419cb26320e7 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Mon, 4 May 2026 11:51:22 -0400 Subject: [PATCH 6/7] fix: harden cursor daemon model fallback --- src/cursor/cursor-models.ts | 33 ++++++++++++++++++++++--- tests/unit/cursor/cursor-models.test.ts | 28 ++++++++++++++++++++- 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/src/cursor/cursor-models.ts b/src/cursor/cursor-models.ts index 79be4082..292ce5dc 100644 --- a/src/cursor/cursor-models.ts +++ b/src/cursor/cursor-models.ts @@ -23,6 +23,8 @@ export { DEFAULT_CURSOR_MODEL, DEFAULT_CURSOR_MODELS, detectProvider, formatMode const CURSOR_MODELS_API_ENDPOINT = 'https://api2.cursor.sh/v1/models'; const CURSOR_MODELS_CACHE_TTL_MS = 5 * 60 * 1000; +const CURSOR_MODELS_MAX_BODY_SIZE = 1024 * 1024; // 1MB limit +const CURSOR_DAEMON_MODELS_TIMEOUT_MS = 5000; let liveModelsCache: { models: CursorModel[]; @@ -81,6 +83,14 @@ function parseApiModelsResponse(payload: unknown): CursorModel[] | null { return models.length > 0 ? models : null; } +function parseContentLength(value: string | string[] | undefined): number | null { + const rawValue = Array.isArray(value) ? value[0] : value; + if (!rawValue) return null; + + const parsed = Number.parseInt(rawValue, 10); + return Number.isFinite(parsed) && parsed >= 0 ? parsed : null; +} + function getCachedLiveModels(nowMs: number = Date.now()): CursorModel[] | null { if (!liveModelsCache) return null; if (liveModelsCache.expiresAtMs <= nowMs) { @@ -195,24 +205,39 @@ export async function fetchModelsFromDaemon(port: number): Promise { - const MAX_BODY_SIZE = 1024 * 1024; // 1MB limit + const contentLength = parseContentLength(res.headers['content-length']); + if (contentLength !== null && contentLength > CURSOR_MODELS_MAX_BODY_SIZE) { + debugLog( + 'Cursor daemon /v1/models content-length exceeded 1MB; falling back to defaults' + ); + res.destroy(); + req.destroy(); + safeResolve(DEFAULT_CURSOR_MODELS); + return; + } + let data = ''; + let bodySize = 0; res.on('data', (chunk) => { - data += chunk; - if (data.length > MAX_BODY_SIZE) { + if (resolved) return; + bodySize += typeof chunk === 'string' ? Buffer.byteLength(chunk) : chunk.length; + if (bodySize > CURSOR_MODELS_MAX_BODY_SIZE) { debugLog('Cursor daemon /v1/models body exceeded 1MB; falling back to defaults'); res.destroy(); req.destroy(); safeResolve(DEFAULT_CURSOR_MODELS); return; } + + data += chunk; }); res.on('end', () => { + if (resolved) return; try { const response = JSON.parse(data) as { data?: Array<{ id?: unknown }> }; if (Array.isArray(response.data)) { diff --git a/tests/unit/cursor/cursor-models.test.ts b/tests/unit/cursor/cursor-models.test.ts index dddea9e8..14569c52 100644 --- a/tests/unit/cursor/cursor-models.test.ts +++ b/tests/unit/cursor/cursor-models.test.ts @@ -170,7 +170,7 @@ describe('fetchModelsFromDaemon', () => { it( 'falls back to defaults when daemon response exceeds max body size', async () => { - const oversizedPayload = 'x'.repeat(1024 * 1024 + 1024); + const oversizedPayload = Buffer.alloc(1024 * 1024 + 1024, 'x'); const server = http.createServer((_req, res) => { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(oversizedPayload); @@ -187,11 +187,37 @@ describe('fetchModelsFromDaemon', () => { expect(models).toEqual(DEFAULT_CURSOR_MODELS); } finally { server.closeAllConnections?.(); + server.closeIdleConnections?.(); await new Promise((resolve) => server.close(() => resolve())); } }, 30000 ); + + it('falls back to defaults when daemon advertises an oversized response', async () => { + const server = http.createServer((_req, res) => { + res.writeHead(200, { + 'Content-Type': 'application/json', + 'Content-Length': String(1024 * 1024 + 1), + }); + res.flushHeaders(); + }); + + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); + const address = server.address(); + if (!address || typeof address === 'string') { + throw new Error('Unable to resolve test server port'); + } + + try { + const models = await fetchModelsFromDaemon(address.port); + expect(models).toEqual(DEFAULT_CURSOR_MODELS); + } finally { + server.closeAllConnections?.(); + server.closeIdleConnections?.(); + await new Promise((resolve) => server.close(() => resolve())); + } + }); }); describe('fetchModelsFromCursorApi', () => { From 6089e1b178f6161fa87109b0636627decd40acee Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 4 May 2026 11:59:13 -0400 Subject: [PATCH 7/7] chore(release): 7.77.0-dev.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d87fc457..edefe896 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kaitranntt/ccs", - "version": "7.77.0-dev.1", + "version": "7.77.0-dev.2", "description": "Claude Code Switch - Instant profile switching between Claude, GLM, Kimi, and more", "keywords": [ "cli",