From e5cdf7c083b1b220627dad711df6f6f1c746d9ad Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Sat, 13 Dec 2025 23:27:23 -0500 Subject: [PATCH 1/2] 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/2] 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