From e5cdf7c083b1b220627dad711df6f6f1c746d9ad Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Sat, 13 Dec 2025 23:27:23 -0500 Subject: [PATCH 1/7] feat(cliproxy): disable logging by default and add cleanup command - Disable CLIProxy logging by default to prevent 5-12GB disk bloat - Add cliproxy.logging config option for user opt-in via ~/.ccs/config.yaml - Add `ccs cleanup` command to remove old CLIProxy logs - Bump CLIProxy config version to v3 to trigger config regeneration Closes #96 --- src/ccs.ts | 7 + src/cliproxy/config-generator.ts | 31 ++++- src/commands/cleanup-command.ts | 193 ++++++++++++++++++++++++++++ src/commands/help-command.ts | 1 + src/config/unified-config-loader.ts | 5 + src/config/unified-config-types.ts | 18 +++ 6 files changed, 252 insertions(+), 3 deletions(-) create mode 100644 src/commands/cleanup-command.ts diff --git a/src/ccs.ts b/src/ccs.ts index 3d8ae6b2..930bc247 100644 --- a/src/ccs.ts +++ b/src/ccs.ts @@ -252,6 +252,13 @@ async function main(): Promise { return; } + // Special case: cleanup command + if (firstArg === 'cleanup' || firstArg === '--cleanup') { + const { handleCleanupCommand } = await import('./commands/cleanup-command'); + await handleCleanupCommand(args.slice(1)); + return; + } + // Special case: migrate command if (firstArg === 'migrate' || firstArg === '--migrate') { const { handleMigrateCommand, printMigrateHelp } = await import('./commands/migrate-command'); diff --git a/src/cliproxy/config-generator.ts b/src/cliproxy/config-generator.ts index 6c0e0a51..3383d313 100644 --- a/src/cliproxy/config-generator.ts +++ b/src/cliproxy/config-generator.ts @@ -14,6 +14,7 @@ import { getCcsDir } from '../utils/config-manager'; import { warn } from '../utils/ui'; import { CLIProxyProvider, ProviderConfig, ProviderModelMapping } from './types'; import { getModelMappingFromConfig, getEnvVarsFromConfig } from './base-config-loader'; +import { loadOrCreateUnifiedConfig } from '../config/unified-config-loader'; /** Settings file structure for user overrides */ interface ProviderSettings { @@ -33,8 +34,9 @@ export const CCS_CONTROL_PANEL_SECRET = 'ccs'; * Config version - bump when config format changes to trigger regeneration * v1: Initial config (port, auth-dir, api-keys only) * v2: Full-featured config with dashboard, quota mgmt, simplified key + * v3: Logging disabled by default (user opt-in via ~/.ccs/config.yaml) */ -export const CLIPROXY_CONFIG_VERSION = 2; +export const CLIPROXY_CONFIG_VERSION = 3; /** Provider display names (static metadata) */ const PROVIDER_DISPLAY_NAMES: Record = { @@ -115,6 +117,18 @@ export function getBinDir(): string { return path.join(getCliproxyDir(), 'bin'); } +/** + * Get CLIProxy logging settings from user config. + * Defaults to disabled to prevent disk bloat. + */ +function getLoggingSettings(): { loggingToFile: boolean; requestLog: boolean } { + const config = loadOrCreateUnifiedConfig(); + return { + loggingToFile: config.cliproxy.logging?.enabled ?? false, + requestLog: config.cliproxy.logging?.request_log ?? false, + }; +} + /** * Generate UNIFIED config.yaml content for ALL providers * This enables concurrent usage of gemini/codex/agy without config conflicts. @@ -125,6 +139,9 @@ function generateUnifiedConfigContent(port: number = CLIPROXY_DEFAULT_PORT): str // Convert Windows backslashes to forward slashes for YAML compatibility const authDirNormalized = authDir.split(path.sep).join('/'); + // Get logging settings from user config (disabled by default) + const { loggingToFile, requestLog } = getLoggingSettings(); + // Unified config with enhanced CLIProxyAPI features const config = `# CLIProxyAPI config generated by CCS v${CLIPROXY_CONFIG_VERSION} # Supports: gemini, codex, agy, qwen, iflow (concurrent usage) @@ -143,12 +160,20 @@ debug: false # ============================================================================= # Logging # ============================================================================= +# WARNING: Logs can grow to several GB if enabled! +# To enable logging, edit ~/.ccs/config.yaml: +# cliproxy: +# logging: +# enabled: true +# request_log: true +# Then run 'ccs doctor --fix' to regenerate this config. +# Use 'ccs cleanup' to remove old logs. # Write logs to file (stored in ~/.ccs/cliproxy/logs/) -logging-to-file: true +logging-to-file: ${loggingToFile} # Log individual API requests for debugging/analytics -request-log: true +request-log: ${requestLog} # ============================================================================= # Dashboard & Management diff --git a/src/commands/cleanup-command.ts b/src/commands/cleanup-command.ts new file mode 100644 index 00000000..bae4e192 --- /dev/null +++ b/src/commands/cleanup-command.ts @@ -0,0 +1,193 @@ +/** + * Cleanup Command Handler + * + * Removes old CLIProxy logs to free up disk space. + * Logs can accumulate to several GB without user awareness. + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import { getCliproxyDir } from '../cliproxy/config-generator'; +import { info, ok, warn } from '../utils/ui'; + +/** Get the CLIProxy logs directory */ +function getLogsDir(): string { + return path.join(getCliproxyDir(), 'logs'); +} + +/** Format bytes to human-readable size */ +function formatBytes(bytes: number): string { + if (bytes === 0) return '0 B'; + const units = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${units[i]}`; +} + +/** Calculate total size of a directory */ +function getDirSize(dirPath: string): number { + if (!fs.existsSync(dirPath)) return 0; + + let totalSize = 0; + const files = fs.readdirSync(dirPath); + + for (const file of files) { + const filePath = path.join(dirPath, file); + try { + const stats = fs.lstatSync(filePath); // Use lstat to detect symlinks + if (stats.isFile() && !stats.isSymbolicLink()) { + totalSize += stats.size; + } else if (stats.isDirectory() && !stats.isSymbolicLink()) { + totalSize += getDirSize(filePath); + } + // Skip symlinks for safety + } catch { + // File may have been deleted between readdir and stat - skip + } + } + + return totalSize; +} + +/** Count files in a directory */ +function countFiles(dirPath: string): number { + if (!fs.existsSync(dirPath)) return 0; + let count = 0; + const entries = fs.readdirSync(dirPath); + + for (const entry of entries) { + const filePath = path.join(dirPath, entry); + try { + const stats = fs.lstatSync(filePath); + if (stats.isFile() && !stats.isSymbolicLink()) { + count++; + } + } catch { + // File may have been deleted - skip + } + } + return count; +} + +/** Delete all regular files in a directory (skips symlinks for safety) */ +function cleanDirectory(dirPath: string): { deleted: number; freedBytes: number } { + if (!fs.existsSync(dirPath)) return { deleted: 0, freedBytes: 0 }; + + let deleted = 0; + let freedBytes = 0; + const files = fs.readdirSync(dirPath); + + for (const file of files) { + const filePath = path.join(dirPath, file); + try { + const stats = fs.lstatSync(filePath); + + // Only delete regular files, skip symlinks for security + if (stats.isFile() && !stats.isSymbolicLink()) { + freedBytes += stats.size; + fs.unlinkSync(filePath); + deleted++; + } + } catch { + // File may have been deleted or inaccessible - skip + } + } + + return { deleted, freedBytes }; +} + +/** Print help for cleanup command */ +function printHelp(): void { + console.log(''); + console.log('Usage: ccs cleanup [options]'); + console.log(''); + console.log('Remove old CLIProxy logs to free up disk space.'); + console.log(''); + console.log('Options:'); + console.log(' --dry-run Show what would be deleted without deleting'); + console.log(' --force Skip confirmation prompt'); + console.log(' --help, -h Show this help message'); + console.log(''); + console.log('Examples:'); + console.log(' ccs cleanup Interactive cleanup with confirmation'); + console.log(' ccs cleanup --dry-run Preview cleanup without deleting'); + console.log(' ccs cleanup --force Clean without confirmation'); + console.log(''); + console.log('Note: CLIProxy logging is disabled by default.'); + console.log('To enable logging, edit ~/.ccs/config.yaml:'); + console.log(' cliproxy:'); + console.log(' logging:'); + console.log(' enabled: true'); + console.log(''); +} + +/** + * Handle cleanup command + */ +export async function handleCleanupCommand(args: string[]): Promise { + // Handle help + if (args.includes('--help') || args.includes('-h')) { + printHelp(); + return; + } + + const dryRun = args.includes('--dry-run'); + const force = args.includes('--force'); + const logsDir = getLogsDir(); + + // Check if logs directory exists + if (!fs.existsSync(logsDir)) { + console.log(info('No CLIProxy logs found.')); + return; + } + + // Calculate current size + const currentSize = getDirSize(logsDir); + const fileCount = countFiles(logsDir); + + if (fileCount === 0) { + console.log(info('No log files to clean.')); + return; + } + + console.log(''); + console.log(`CLIProxy Logs: ${logsDir}`); + console.log(` Files: ${fileCount}`); + console.log(` Size: ${formatBytes(currentSize)}`); + console.log(''); + + if (dryRun) { + console.log(info('Dry run - no files deleted.')); + console.log(`Would delete ${fileCount} files (${formatBytes(currentSize)})`); + return; + } + + // Confirm unless --force + if (!force) { + const readline = await import('readline'); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + const answer = await new Promise((resolve) => { + rl.question(`Delete ${fileCount} log files (${formatBytes(currentSize)})? [y/N] `, resolve); + }); + rl.close(); + + if (answer.toLowerCase() !== 'y') { + console.log('Cancelled.'); + return; + } + } + + // Perform cleanup + const { deleted, freedBytes } = cleanDirectory(logsDir); + console.log(ok(`Deleted ${deleted} files, freed ${formatBytes(freedBytes)}`)); + + // Suggest disabling logging if it was enabled + if (deleted > 0) { + console.log(''); + console.log(warn('Tip: CLIProxy logging is now disabled by default.')); + console.log(' Run `ccs doctor --fix` to update your config.'); + } +} diff --git a/src/commands/help-command.ts b/src/commands/help-command.ts index 23c2af18..ff724df0 100644 --- a/src/commands/help-command.ts +++ b/src/commands/help-command.ts @@ -186,6 +186,7 @@ Claude Code Profile & Model Switcher`.trim(); // Diagnostics printSubSection('Diagnostics', [ ['ccs doctor', 'Run health check and diagnostics'], + ['ccs cleanup', 'Remove old CLIProxy logs'], ['ccs config', 'Open web configuration dashboard'], ['ccs config --port 3000', 'Use specific port'], ['ccs sync', 'Sync delegation commands and skills'], diff --git a/src/config/unified-config-loader.ts b/src/config/unified-config-loader.ts index 292ec9e5..0b4152bb 100644 --- a/src/config/unified-config-loader.ts +++ b/src/config/unified-config-loader.ts @@ -105,6 +105,11 @@ function mergeWithDefaults(partial: Partial): UnifiedConfig { oauth_accounts: partial.cliproxy?.oauth_accounts ?? defaults.cliproxy.oauth_accounts, providers: defaults.cliproxy.providers, // Always use defaults for providers variants: partial.cliproxy?.variants ?? defaults.cliproxy.variants, + logging: { + enabled: partial.cliproxy?.logging?.enabled ?? defaults.cliproxy.logging?.enabled ?? false, + request_log: + partial.cliproxy?.logging?.request_log ?? defaults.cliproxy.logging?.request_log ?? false, + }, }, preferences: { ...defaults.preferences, diff --git a/src/config/unified-config-types.ts b/src/config/unified-config-types.ts index ed313298..12e62eea 100644 --- a/src/config/unified-config-types.ts +++ b/src/config/unified-config-types.ts @@ -62,6 +62,18 @@ export interface CLIProxyVariantConfig { settings?: string; } +/** + * CLIProxy logging configuration. + * Controls whether CLIProxyAPI writes logs to disk. + * Logs can grow to several GB if left enabled. + */ +export interface CLIProxyLoggingConfig { + /** Enable logging to file (default: false to prevent disk bloat) */ + enabled?: boolean; + /** Enable request logging for debugging (default: false) */ + request_log?: boolean; +} + /** * CLIProxy configuration section. */ @@ -72,6 +84,8 @@ export interface CLIProxyConfig { providers: readonly string[]; /** User-defined provider variants */ variants: Record; + /** Logging configuration (disabled by default) */ + logging?: CLIProxyLoggingConfig; } /** @@ -130,6 +144,10 @@ export function createEmptyUnifiedConfig(): UnifiedConfig { oauth_accounts: {}, providers: ['gemini', 'codex', 'agy', 'qwen', 'iflow'], variants: {}, + logging: { + enabled: false, + request_log: false, + }, }, preferences: { theme: 'system', From 5a8db2c1ee87b2a252f61759273863c0c521f27b Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Sat, 13 Dec 2025 23:31:52 -0500 Subject: [PATCH 2/7] fix(ci): prevent shell injection from PR body markdown Pass PR title and body via env vars instead of direct interpolation. Prevents backticks in markdown code blocks from being executed as shell commands. --- .github/workflows/label-pending-release.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/label-pending-release.yml b/.github/workflows/label-pending-release.yml index b427cd5a..a966d113 100644 --- a/.github/workflows/label-pending-release.yml +++ b/.github/workflows/label-pending-release.yml @@ -15,9 +15,12 @@ jobs: - name: Label linked issues as pending-release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_TITLE: ${{ github.event.pull_request.title }} + PR_BODY: ${{ github.event.pull_request.body }} run: | - # Extract issue numbers from PR title and body - PR_TEXT="${{ github.event.pull_request.title }} ${{ github.event.pull_request.body }}" + # Extract issue numbers from PR title and body (passed via env vars for safety) + # Using env vars prevents shell injection from backticks in markdown + PR_TEXT="$PR_TITLE $PR_BODY" ISSUES=$(echo "$PR_TEXT" | grep -oE "(Fixes|Closes|Resolves|Refs?) #[0-9]+" | grep -oE "#[0-9]+" | sort -u || true) if [[ -z "$ISSUES" ]]; then From 9aedbfed72861f80ace54ca3a610b849799a9716 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 14 Dec 2025 05:02:52 +0000 Subject: [PATCH 3/7] chore(release): 5.18.0-dev.1 [skip ci] --- VERSION | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 391dca82..681c7670 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -5.18.0 +5.18.0-dev.1 diff --git a/package.json b/package.json index 7d63298b..a9a74968 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kaitranntt/ccs", - "version": "5.18.0", + "version": "5.18.0-dev.1", "description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6", "keywords": [ "cli", From a6b95dbac5f97a870c7ef58701726ad9733ea75d Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Sun, 14 Dec 2025 01:07:20 -0500 Subject: [PATCH 4/7] fix(cliproxy): add SSH port forwarding instructions for headless OAuth When running OAuth in headless mode (SSH/remote), users now see clear instructions about port forwarding requirement before the OAuth URL. Previously, CLIProxyAPI's instructions were captured but not displayed. - Show [!] PORT FORWARDING REQUIRED warning - Explain that localhost:8085 callback must be reachable - Show ssh -L command with @ placeholders - Emphasize running on LOCAL machine, not remote --- src/cliproxy/auth-handler.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/cliproxy/auth-handler.ts b/src/cliproxy/auth-handler.ts index d1d8840a..0faae062 100644 --- a/src/cliproxy/auth-handler.ts +++ b/src/cliproxy/auth-handler.ts @@ -16,7 +16,7 @@ import { execSync, spawn } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; import { ProgressIndicator } from '../utils/progress-indicator'; -import { ok, fail, info, warn } from '../utils/ui'; +import { ok, fail, info, warn, color } from '../utils/ui'; import { ensureCLIProxyBinary } from './binary-manager'; import { generateConfig, getProviderAuthDir } from './config-generator'; import { CLIProxyProvider } from './types'; @@ -537,8 +537,14 @@ export async function triggerOAuth( console.log(''); if (headless) { console.log(info('Headless mode detected - manual authentication required')); - console.log(info(`${oauthConfig.displayName} will display an OAuth URL below`)); console.log(''); + console.log(warn('PORT FORWARDING REQUIRED')); + console.log(' OAuth callback uses localhost:8085 which must be reachable.'); + console.log(' Run this on your LOCAL machine (replace and ):'); + console.log(''); + console.log(` ${color('ssh -L 8085:localhost:8085 @', 'command')}`); + console.log(''); + console.log(info(`${oauthConfig.displayName} OAuth URL:`)); } else { console.log(info(`Opening browser for ${oauthConfig.displayName} authentication...`)); console.log(info('Complete the login in your browser.')); From 13d535e901e756269e12030e532e597b8c6c7b0c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 14 Dec 2025 06:08:27 +0000 Subject: [PATCH 5/7] chore(release): 5.18.0-dev.2 [skip ci] --- VERSION | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 681c7670..e3795a38 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -5.18.0-dev.1 +5.18.0-dev.2 diff --git a/package.json b/package.json index a9a74968..4a6bcc06 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kaitranntt/ccs", - "version": "5.18.0-dev.1", + "version": "5.18.0-dev.2", "description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6", "keywords": [ "cli", From 4ccde8a3f07d5ebb658213dfe9f69a7b11ec3aac Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Sun, 14 Dec 2025 01:17:22 -0500 Subject: [PATCH 6/7] fix(auth): use unified config for account profile touch in ccs.ts Account profiles in unified config mode were failing with "Profile not found" because touchProfile() only checked legacy profiles.json. Now checks unified config first with hasAccountUnified() and calls touchAccountUnified() when the profile exists there, falling back to legacy touchProfile() otherwise. Fixes #98 --- src/ccs.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/ccs.ts b/src/ccs.ts index 930bc247..15c57273 100644 --- a/src/ccs.ts +++ b/src/ccs.ts @@ -397,8 +397,12 @@ async function main(): Promise { // Ensure instance exists (lazy init if needed) const instancePath = instanceMgr.ensureInstance(profileInfo.name); - // Update last_used timestamp - registry.touchProfile(profileInfo.name); + // Update last_used timestamp (check unified config first, fallback to legacy) + if (registry.hasAccountUnified(profileInfo.name)) { + registry.touchAccountUnified(profileInfo.name); + } else { + registry.touchProfile(profileInfo.name); + } // Execute Claude with instance isolation const envVars: NodeJS.ProcessEnv = { CLAUDE_CONFIG_DIR: instancePath }; From 34abeb54364d0385845e7864775450225d3993da Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 14 Dec 2025 06:27:54 +0000 Subject: [PATCH 7/7] chore(release): 5.18.0-dev.3 [skip ci] --- VERSION | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index e3795a38..cfe61750 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -5.18.0-dev.2 +5.18.0-dev.3 diff --git a/package.json b/package.json index 4a6bcc06..fc5024d0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kaitranntt/ccs", - "version": "5.18.0-dev.2", + "version": "5.18.0-dev.3", "description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6", "keywords": [ "cli",