From a34eeb63bf69d280738b1d5a53f931cf3f6ebef1 Mon Sep 17 00:00:00 2001 From: catlog22 Date: Sun, 18 Jan 2026 11:10:05 +0800 Subject: [PATCH] feat(cli): add CLI prompt simulation and testing scripts - Introduced `simulate-cli-prompt.js` to simulate various prompt formats and display the final content passed to the CLI. - Added `test-shell-prompt.js` to test actual shell execution of different prompt formats, demonstrating correct vs incorrect multi-line prompt handling. - Created comprehensive tests in `cli-prompt-parsing.test.ts` to validate prompt parsing, including single-line, multi-line, special characters, and template concatenation. - Implemented edge case handling for empty lines, long prompts, and Unicode characters. --- .claude/workflows/cli-tools-usage.md | 51 ++- ccw/bin/ccw-native.js | 32 ++ ccw/bin/ccw-prompt-test.js | 283 ++++++++++++++ ccw/scripts/simulate-cli-prompt.js | 214 +++++++++++ ccw/scripts/test-shell-prompt.js | 159 ++++++++ ccw/src/commands/cli.ts | 44 ++- ccw/tests/cli-prompt-parsing.test.ts | 539 +++++++++++++++++++++++++++ ccw/tsconfig.tsbuildinfo | 1 - 8 files changed, 1303 insertions(+), 20 deletions(-) create mode 100644 ccw/bin/ccw-native.js create mode 100644 ccw/bin/ccw-prompt-test.js create mode 100644 ccw/scripts/simulate-cli-prompt.js create mode 100644 ccw/scripts/test-shell-prompt.js create mode 100644 ccw/tests/cli-prompt-parsing.test.ts delete mode 100644 ccw/tsconfig.tsbuildinfo diff --git a/.claude/workflows/cli-tools-usage.md b/.claude/workflows/cli-tools-usage.md index 7f3a8251..fbc1aa3c 100644 --- a/.claude/workflows/cli-tools-usage.md +++ b/.claude/workflows/cli-tools-usage.md @@ -389,51 +389,84 @@ ASSISTANT RESPONSE: [Previous output] ### Command Examples +> **⚠️ IMPORTANT: Multi-line prompts in shell** +> +> Direct multi-line strings in quotes **WILL BREAK** in bash/shell: +> ```bash +> # ❌ WRONG - shell cannot parse multi-line in quotes +> ccw cli -p "PURPOSE: ... +> TASK: ..." +> ``` +> +> **Correct methods:** +> 1. **Stdin pipe (NEW):** `echo "..." | ccw cli --tool gemini` or heredoc +> 2. **File-based:** `ccw cli -f prompt.txt --tool gemini` +> 3. **Single line:** Use `|` as logical separator between fields + #### Task-Type Specific Templates -**Analysis Task** (Security Audit): +**Analysis Task** (Security Audit) - File method: ```bash -ccw cli -p " +# Create prompt file +cat > /tmp/security-audit.txt << 'EOF' PURPOSE: Identify OWASP Top 10 vulnerabilities in authentication module to pass security audit; success = all critical/high issues documented with remediation TASK: • Scan for injection flaws (SQL, command, LDAP) • Check authentication bypass vectors • Evaluate session management • Assess sensitive data exposure MODE: analysis CONTEXT: @src/auth/**/* @src/middleware/auth.ts | Memory: Using bcrypt for passwords, JWT for sessions EXPECTED: Security report with: severity matrix, file:line references, CVE mappings where applicable, remediation code snippets prioritized by risk CONSTRAINTS: Focus on authentication | Ignore test files -" --tool gemini --mode analysis --rule analysis-assess-security-risks --cd src/auth +EOF + +# Execute +ccw cli -f /tmp/security-audit.txt --tool gemini --mode analysis --rule analysis-assess-security-risks --cd src/auth +``` + +**Analysis Task** (Security Audit) - Single line method: +```bash +ccw cli -p "PURPOSE: Identify OWASP Top 10 vulnerabilities in auth module; success = critical issues documented | TASK: • Scan injection flaws • Check auth bypass • Evaluate session management | MODE: analysis | CONTEXT: @src/auth/**/* | Memory: bcrypt, JWT | EXPECTED: Security report with severity matrix | CONSTRAINTS: Focus on auth" --tool gemini --mode analysis --rule analysis-assess-security-risks ``` **Implementation Task** (New Feature): ```bash -ccw cli -p "PURPOSE: Implement rate limiting for API endpoints to prevent abuse; must be configurable per-endpoint; backward compatible with existing clients +# Create prompt file +cat > /tmp/rate-limit.txt << 'EOF' +PURPOSE: Implement rate limiting for API endpoints to prevent abuse; must be configurable per-endpoint; backward compatible with existing clients TASK: • Create rate limiter middleware with sliding window • Implement per-route configuration • Add Redis backend for distributed state • Include bypass for internal services MODE: write CONTEXT: @src/middleware/**/* @src/config/**/* | Memory: Using Express.js, Redis already configured, existing middleware pattern in auth.ts EXPECTED: Production-ready code with: TypeScript types, unit tests, integration test, configuration example, migration guide CONSTRAINTS: Follow existing middleware patterns | No breaking changes -" --tool gemini --mode write --rule development-implement-feature +EOF + +ccw cli -f /tmp/rate-limit.txt --tool gemini --mode write --rule development-implement-feature ``` **Bug Fix Task**: ```bash -ccw cli -p "PURPOSE: Fix memory leak in WebSocket connection handler causing server OOM after 24h; root cause must be identified before any fix +cat > /tmp/memory-leak.txt << 'EOF' +PURPOSE: Fix memory leak in WebSocket connection handler causing server OOM after 24h; root cause must be identified before any fix TASK: • Trace connection lifecycle from open to close • Identify event listener accumulation • Check cleanup on disconnect • Verify garbage collection eligibility MODE: analysis CONTEXT: @src/websocket/**/* @src/services/connection-manager.ts | Memory: Using ws library, ~5000 concurrent connections in production EXPECTED: Root cause analysis with: memory profile, leak source (file:line), fix recommendation with code, verification steps CONSTRAINTS: Focus on resource cleanup -" --tool gemini --mode analysis --rule analysis-diagnose-bug-root-cause --cd src +EOF + +ccw cli -f /tmp/memory-leak.txt --tool gemini --mode analysis --rule analysis-diagnose-bug-root-cause --cd src ``` **Refactoring Task**: ```bash -ccw cli -p "PURPOSE: Refactor payment processing to use strategy pattern for multi-gateway support; no functional changes; all existing tests must pass +cat > /tmp/refactor-payment.txt << 'EOF' +PURPOSE: Refactor payment processing to use strategy pattern for multi-gateway support; no functional changes; all existing tests must pass TASK: • Extract gateway interface from current implementation • Create strategy classes for Stripe, PayPal • Implement factory for gateway selection • Migrate existing code to use strategies MODE: write CONTEXT: @src/payments/**/* @src/types/payment.ts | Memory: Currently only Stripe, adding PayPal next sprint, must support future gateways EXPECTED: Refactored code with: strategy interface, concrete implementations, factory class, updated tests, migration checklist CONSTRAINTS: Preserve all existing behavior | Tests must pass -" --tool gemini --mode write --rule development-refactor-codebase +EOF + +ccw cli -f /tmp/refactor-payment.txt --tool gemini --mode write --rule development-refactor-codebase ``` **Code Review Task** (codex review mode): diff --git a/ccw/bin/ccw-native.js b/ccw/bin/ccw-native.js new file mode 100644 index 00000000..14ad139e --- /dev/null +++ b/ccw/bin/ccw-native.js @@ -0,0 +1,32 @@ +#!/usr/bin/env node +/** + * CCW Native Wrapper + * + * 替代 npm 生成的 shell wrapper,直接用 Node.js 处理参数传递, + * 避免 Git Bash + Windows 的多行参数问题。 + */ + +import { spawn } from 'child_process'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// 目标脚本路径 +const targetScript = join(__dirname, 'ccw.js'); + +// 直接传递所有参数,不经过 shell +const child = spawn(process.execPath, [targetScript, ...process.argv.slice(2)], { + stdio: 'inherit', + shell: false, // 关键:不使用 shell,避免参数被 shell 解析 +}); + +child.on('close', (code) => { + process.exit(code || 0); +}); + +child.on('error', (err) => { + console.error('Failed to start ccw:', err.message); + process.exit(1); +}); diff --git a/ccw/bin/ccw-prompt-test.js b/ccw/bin/ccw-prompt-test.js new file mode 100644 index 00000000..59a2b146 --- /dev/null +++ b/ccw/bin/ccw-prompt-test.js @@ -0,0 +1,283 @@ +#!/usr/bin/env node +/** + * CCW Prompt Test Endpoint + * + * 独立的提示词解析测试工具,用于调试不同格式的提示词传递。 + * + * Usage: + * node ccw/bin/ccw-prompt-test.js -p "prompt" # 测试 -p 参数 + * node ccw/bin/ccw-prompt-test.js -f file.txt # 测试文件读取 + * echo "prompt" | node ccw/bin/ccw-prompt-test.js # 测试 stdin + * node ccw/bin/ccw-prompt-test.js --raw # 原始 argv 输出 + */ + +import { readFileSync, existsSync } from 'fs'; +import { resolve } from 'path'; + +// ANSI colors +const colors = { + reset: '\x1b[0m', + bold: '\x1b[1m', + dim: '\x1b[2m', + red: '\x1b[31m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + magenta: '\x1b[35m', + cyan: '\x1b[36m', + white: '\x1b[37m', + gray: '\x1b[90m', +}; + +const c = (color, text) => `${colors[color]}${text}${colors.reset}`; + +/** + * Parse command line arguments manually (no dependencies) + */ +function parseArgs(argv) { + const args = argv.slice(2); + const result = { + prompt: undefined, + file: undefined, + tool: 'gemini', + mode: 'analysis', + raw: false, + help: false, + positional: [], + }; + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + + if (arg === '--help' || arg === '-h') { + result.help = true; + } else if (arg === '--raw') { + result.raw = true; + } else if (arg === '-p' || arg === '--prompt') { + result.prompt = args[++i]; + } else if (arg === '-f' || arg === '--file') { + result.file = args[++i]; + } else if (arg === '--tool') { + result.tool = args[++i]; + } else if (arg === '--mode') { + result.mode = args[++i]; + } else if (!arg.startsWith('-')) { + result.positional.push(arg); + } + } + + return result; +} + +/** + * Read from stdin if available (non-TTY) + */ +function readStdin() { + if (process.stdin.isTTY) { + return null; + } + try { + return readFileSync(0, 'utf8').trim(); + } catch { + return null; + } +} + +/** + * Analyze prompt content + */ +function analyzePrompt(prompt) { + if (!prompt) return null; + + const analysis = { + length: prompt.length, + lines: prompt.split('\n').length, + hasNewlines: prompt.includes('\n'), + hasAtPatterns: /@[^\s]+/.test(prompt), + atPatterns: [], + hasBullets: /[•●○■□▪▫]/.test(prompt), + hasMemory: /Memory:/i.test(prompt), + sections: [], + }; + + // Extract @ patterns + const atMatches = prompt.matchAll(/@[^\s|]+/g); + analysis.atPatterns = Array.from(atMatches).map(m => m[0]); + + // Detect sections + const sectionPatterns = ['PURPOSE', 'TASK', 'MODE', 'CONTEXT', 'EXPECTED', 'CONSTRAINTS']; + for (const section of sectionPatterns) { + if (new RegExp(`${section}:`, 'i').test(prompt)) { + analysis.sections.push(section); + } + } + + return analysis; +} + +/** + * Print box + */ +function printBox(title, content, color = 'cyan') { + const width = 70; + const line = '─'.repeat(width); + console.log(c(color, `┌${line}┐`)); + console.log(c(color, '│') + c('bold', ` ${title}`.padEnd(width)) + c(color, '│')); + console.log(c(color, `├${line}┤`)); + + const lines = content.split('\n'); + for (const l of lines) { + const truncated = l.length > width - 2 ? l.substring(0, width - 5) + '...' : l; + console.log(c(color, '│') + ` ${truncated}`.padEnd(width) + c(color, '│')); + } + console.log(c(color, `└${line}┘`)); +} + +/** + * Main + */ +function main() { + const parsed = parseArgs(process.argv); + + // Help + if (parsed.help) { + console.log(` +${c('bold', 'CCW Prompt Test Endpoint')} + +${c('cyan', 'Usage:')} + node ccw/bin/ccw-prompt-test.js -p "prompt" Test -p argument + node ccw/bin/ccw-prompt-test.js -f file.txt Test file input + echo "prompt" | node ccw/bin/ccw-prompt-test.js Test stdin pipe + node ccw/bin/ccw-prompt-test.js --raw Show raw argv only + +${c('cyan', 'Options:')} + -p, --prompt Prompt text + -f, --file Read prompt from file + --tool Tool name (default: gemini) + --mode Mode (default: analysis) + --raw Show only raw process.argv + -h, --help Show this help + +${c('cyan', 'Multi-line prompt methods:')} + ${c('green', '1. Stdin pipe (recommended):')} + echo "PURPOSE: Test + TASK: Step 1" | node ccw/bin/ccw-prompt-test.js + + ${c('green', '2. File input:')} + node ccw/bin/ccw-prompt-test.js -f prompt.txt + + ${c('green', '3. Heredoc:')} + node ccw/bin/ccw-prompt-test.js << 'EOF' + PURPOSE: Test + TASK: Step 1 + EOF +`); + return; + } + + // Raw mode + if (parsed.raw) { + console.log(c('bold', '\nRaw process.argv:')); + process.argv.forEach((arg, i) => { + console.log(` [${i}]: ${JSON.stringify(arg)}`); + }); + return; + } + + console.log(c('bold', '\n═══════════════════════════════════════════════════════════════════════')); + console.log(c('bold', ' CCW PROMPT TEST ENDPOINT')); + console.log(c('bold', '═══════════════════════════════════════════════════════════════════════\n')); + + // 1. Raw argv + console.log(c('yellow', '📦 1. RAW PROCESS.ARGV:')); + console.log(c('gray', ` Total: ${process.argv.length} arguments`)); + process.argv.forEach((arg, i) => { + const display = arg.length > 60 ? arg.substring(0, 57) + '...' : arg; + const hasNewline = arg.includes('\n'); + console.log(c('gray', ` [${i}]: `) + c(hasNewline ? 'green' : 'white', JSON.stringify(display))); + if (hasNewline) { + console.log(c('green', ` ↳ Contains ${arg.split('\n').length} lines (newlines preserved!)`)); + } + }); + console.log(); + + // 2. Parsed options + console.log(c('yellow', '📋 2. PARSED OPTIONS:')); + console.log(c('gray', ' --prompt: ') + (parsed.prompt ? c('green', JSON.stringify(parsed.prompt.substring(0, 50) + (parsed.prompt.length > 50 ? '...' : ''))) : c('dim', '(not set)'))); + console.log(c('gray', ' --file: ') + (parsed.file ? c('cyan', parsed.file) : c('dim', '(not set)'))); + console.log(c('gray', ' --tool: ') + c('white', parsed.tool)); + console.log(c('gray', ' --mode: ') + c('white', parsed.mode)); + console.log(c('gray', ' stdin.isTTY: ') + c(process.stdin.isTTY ? 'yellow' : 'green', String(process.stdin.isTTY))); + console.log(); + + // 3. Resolve final prompt + console.log(c('yellow', '🎯 3. PROMPT RESOLUTION:')); + let finalPrompt = null; + let source = null; + + // Priority: file > stdin > -p > positional + if (parsed.file) { + source = 'file'; + const filePath = resolve(parsed.file); + if (existsSync(filePath)) { + finalPrompt = readFileSync(filePath, 'utf8').trim(); + console.log(c('gray', ' Source: ') + c('magenta', `--file (${filePath})`)); + } else { + console.log(c('red', ` Error: File not found: ${filePath}`)); + } + } else { + const stdinContent = readStdin(); + if (stdinContent) { + source = 'stdin'; + finalPrompt = stdinContent; + console.log(c('gray', ' Source: ') + c('green', 'stdin (piped input)')); + } else if (parsed.prompt) { + source = '-p option'; + finalPrompt = parsed.prompt; + console.log(c('gray', ' Source: ') + c('cyan', '--prompt/-p option')); + } else if (parsed.positional.length > 0) { + source = 'positional'; + finalPrompt = parsed.positional.join(' '); + console.log(c('gray', ' Source: ') + c('yellow', 'positional argument')); + } else { + console.log(c('red', ' No prompt found!')); + } + } + console.log(); + + // 4. Prompt analysis + if (finalPrompt) { + const analysis = analyzePrompt(finalPrompt); + + console.log(c('yellow', '📊 4. PROMPT ANALYSIS:')); + console.log(c('gray', ' Length: ') + c('white', `${analysis.length} chars`)); + console.log(c('gray', ' Lines: ') + c('white', String(analysis.lines))); + console.log(c('gray', ' Has newlines: ') + c(analysis.hasNewlines ? 'green' : 'yellow', analysis.hasNewlines ? '✓ Yes' : '✗ No')); + console.log(c('gray', ' Has @ patterns: ') + c(analysis.hasAtPatterns ? 'green' : 'dim', analysis.hasAtPatterns ? '✓ Yes' : '✗ No')); + if (analysis.atPatterns.length > 0) { + console.log(c('gray', ' @ patterns:')); + analysis.atPatterns.forEach(p => console.log(c('blue', ` • ${p}`))); + } + console.log(c('gray', ' Has bullets: ') + c(analysis.hasBullets ? 'green' : 'dim', analysis.hasBullets ? '✓ Yes' : '✗ No')); + console.log(c('gray', ' Has Memory: ') + c(analysis.hasMemory ? 'green' : 'dim', analysis.hasMemory ? '✓ Yes' : '✗ No')); + if (analysis.sections.length > 0) { + console.log(c('gray', ' Sections: ') + c('cyan', analysis.sections.join(', '))); + } + console.log(); + + // 5. Final prompt content + console.log(c('yellow', '📄 5. FINAL PROMPT CONTENT:')); + printBox(`${source} → ${analysis.length} chars, ${analysis.lines} lines`, finalPrompt, 'green'); + console.log(); + + // 6. Simulated CLI command + console.log(c('yellow', '🚀 6. SIMULATED CLI EXECUTION:')); + console.log(c('gray', ' Would execute:')); + console.log(c('cyan', ` ${parsed.tool} cli --mode ${parsed.mode}`)); + console.log(c('gray', ' With prompt of ') + c('green', `${analysis.length} chars, ${analysis.lines} lines`)); + } + + console.log(c('bold', '\n═══════════════════════════════════════════════════════════════════════\n')); +} + +main(); diff --git a/ccw/scripts/simulate-cli-prompt.js b/ccw/scripts/simulate-cli-prompt.js new file mode 100644 index 00000000..a15aac8a --- /dev/null +++ b/ccw/scripts/simulate-cli-prompt.js @@ -0,0 +1,214 @@ +#!/usr/bin/env node +/** + * CLI Prompt Simulation Script + * + * Simulates different prompt formats and outputs the final content passed to CLI. + * Usage: node ccw/scripts/simulate-cli-prompt.js + */ + +import chalk from 'chalk'; + +// Test cases for different prompt formats +const testCases = [ + { + name: 'Single-line prompt', + input: { + prompt: 'Analyze the authentication module for security issues', + tool: 'gemini', + mode: 'analysis', + }, + }, + { + name: 'Single-line with quotes', + input: { + prompt: 'Fix the error: "Cannot read property \'id\' of undefined"', + tool: 'gemini', + mode: 'analysis', + }, + }, + { + name: 'Multi-line structured prompt', + input: { + prompt: `PURPOSE: Identify security vulnerabilities +TASK: • Scan injection flaws • Check auth bypass +MODE: analysis +CONTEXT: @src/auth/**/* +EXPECTED: Security report`, + tool: 'gemini', + mode: 'analysis', + }, + }, + { + name: '@ patterns with glob wildcards', + input: { + prompt: `CONTEXT: @src/**/*.{ts,tsx} @!node_modules/** @!dist/** +TASK: Analyze TypeScript files`, + tool: 'gemini', + mode: 'analysis', + }, + }, + { + name: '@ patterns with Memory section', + input: { + prompt: `PURPOSE: Security audit +CONTEXT: @src/auth/**/* @src/middleware/auth.ts | Memory: Using bcrypt for passwords, JWT for sessions +EXPECTED: Vulnerability report`, + tool: 'gemini', + mode: 'analysis', + }, + }, + { + name: 'Full template format (from cli-tools-usage.md)', + input: { + prompt: `PURPOSE: Identify OWASP Top 10 vulnerabilities in authentication module to pass security audit; success = all critical/high issues documented with remediation +TASK: • Scan for injection flaws (SQL, command, LDAP) • Check authentication bypass vectors • Evaluate session management • Assess sensitive data exposure +MODE: analysis +CONTEXT: @src/auth/**/* @src/middleware/auth.ts | Memory: Using bcrypt for passwords, JWT for sessions +EXPECTED: Security report with: severity matrix, file:line references, CVE mappings where applicable, remediation code snippets prioritized by risk +CONSTRAINTS: Focus on authentication | Ignore test files`, + tool: 'gemini', + mode: 'analysis', + }, + }, + { + name: 'Special characters and Unicode', + input: { + prompt: `TASK: • 分析代码 • Check → errors • Fix ✗ issues +EXPECTED: Report with ✓ checkmarks`, + tool: 'gemini', + mode: 'analysis', + }, + }, + { + name: 'Code-like content', + input: { + prompt: `Fix: const result = arr.filter(x => x > 0).map(x => x * 2); +Error at line 42: TypeError: Cannot read property 'length' of null`, + tool: 'gemini', + mode: 'write', + }, + }, + { + name: 'Shell-like patterns', + input: { + prompt: `Run: npm run build && npm test | grep "passed" +Expected output: All tests passed`, + tool: 'gemini', + mode: 'analysis', + }, + }, +]; + +/** + * Simulate prompt processing (mirrors cli.ts logic) + */ +function simulatePromptProcessing(input) { + const { prompt, tool, mode, rule = 'universal-rigorous-style' } = input; + + // Step 1: Get base prompt + let finalPrompt = prompt; + + // Step 2: Extract @ patterns from CONTEXT (for cache simulation) + const contextMatch = prompt.match(/CONTEXT:\s*([^\n]+)/i); + let extractedPatterns = []; + if (contextMatch) { + const contextLine = contextMatch[1]; + const patternMatches = contextLine.matchAll(/@[^\s|]+/g); + extractedPatterns = Array.from(patternMatches).map(m => m[0]); + } + + // Step 3: Simulate template concatenation + const mockSystemRules = `[SYSTEM RULES - ${mode} mode protocol loaded]`; + const mockRoles = `[ROLES - ${rule} template loaded]`; + + const parts = [finalPrompt]; + parts.push(`\n=== SYSTEM RULES ===\n${mockSystemRules}`); + parts.push(`\n=== ROLES ===\n${mockRoles}`); + + return { + originalPrompt: prompt, + finalPrompt: parts.join('\n'), + extractedPatterns, + metadata: { + tool, + mode, + rule, + originalLength: prompt.length, + finalLength: parts.join('\n').length, + lineCount: parts.join('\n').split('\n').length, + hasMultiline: prompt.includes('\n'), + hasAtPatterns: extractedPatterns.length > 0, + }, + }; +} + +/** + * Display test result + */ +function displayResult(testCase, result) { + console.log(chalk.bold.cyan('\n' + '═'.repeat(70))); + console.log(chalk.bold.white(`📋 Test: ${testCase.name}`)); + console.log(chalk.cyan('═'.repeat(70))); + + // Input section + console.log(chalk.bold.yellow('\n📥 INPUT:')); + console.log(chalk.gray(' Tool: ') + chalk.green(testCase.input.tool)); + console.log(chalk.gray(' Mode: ') + chalk.green(testCase.input.mode)); + console.log(chalk.gray(' Prompt:')); + console.log(chalk.white(' ┌' + '─'.repeat(66) + '┐')); + testCase.input.prompt.split('\n').forEach(line => { + const truncated = line.length > 64 ? line.substring(0, 61) + '...' : line; + console.log(chalk.white(' │ ') + chalk.cyan(truncated.padEnd(64)) + chalk.white(' │')); + }); + console.log(chalk.white(' └' + '─'.repeat(66) + '┘')); + + // Metadata section + console.log(chalk.bold.yellow('\n📊 METADATA:')); + console.log(chalk.gray(' Original length: ') + chalk.magenta(result.metadata.originalLength + ' chars')); + console.log(chalk.gray(' Final length: ') + chalk.magenta(result.metadata.finalLength + ' chars')); + console.log(chalk.gray(' Line count: ') + chalk.magenta(result.metadata.lineCount)); + console.log(chalk.gray(' Has multiline: ') + (result.metadata.hasMultiline ? chalk.green('✓') : chalk.red('✗'))); + console.log(chalk.gray(' Has @ patterns: ') + (result.metadata.hasAtPatterns ? chalk.green('✓') : chalk.red('✗'))); + + if (result.extractedPatterns.length > 0) { + console.log(chalk.gray(' Extracted patterns:')); + result.extractedPatterns.forEach(p => { + console.log(chalk.gray(' • ') + chalk.blue(p)); + }); + } + + // Final prompt section + console.log(chalk.bold.yellow('\n📤 FINAL PROMPT (passed to CLI):')); + console.log(chalk.white(' ┌' + '─'.repeat(66) + '┐')); + result.finalPrompt.split('\n').slice(0, 15).forEach(line => { + const truncated = line.length > 64 ? line.substring(0, 61) + '...' : line; + console.log(chalk.white(' │ ') + chalk.green(truncated.padEnd(64)) + chalk.white(' │')); + }); + if (result.finalPrompt.split('\n').length > 15) { + console.log(chalk.white(' │ ') + chalk.dim(`... (${result.finalPrompt.split('\n').length - 15} more lines)`.padEnd(64)) + chalk.white(' │')); + } + console.log(chalk.white(' └' + '─'.repeat(66) + '┘')); +} + +/** + * Main execution + */ +function main() { + console.log(chalk.bold.magenta('\n' + '█'.repeat(70))); + console.log(chalk.bold.white(' CLI PROMPT SIMULATION - Testing Different Prompt Formats')); + console.log(chalk.bold.magenta('█'.repeat(70))); + + console.log(chalk.gray('\nThis script simulates how different prompt formats are processed')); + console.log(chalk.gray('and shows the final content passed to the CLI executor.\n')); + + for (const testCase of testCases) { + const result = simulatePromptProcessing(testCase.input); + displayResult(testCase, result); + } + + console.log(chalk.bold.magenta('\n' + '█'.repeat(70))); + console.log(chalk.bold.white(` Completed ${testCases.length} simulations`)); + console.log(chalk.bold.magenta('█'.repeat(70) + '\n')); +} + +main(); diff --git a/ccw/scripts/test-shell-prompt.js b/ccw/scripts/test-shell-prompt.js new file mode 100644 index 00000000..44854800 --- /dev/null +++ b/ccw/scripts/test-shell-prompt.js @@ -0,0 +1,159 @@ +#!/usr/bin/env node +/** + * CLI Shell Prompt Test Script + * + * Tests actual shell execution of different prompt formats. + * Demonstrates correct vs incorrect multi-line prompt handling. + */ + +import { execSync, exec } from 'child_process'; +import { writeFileSync, unlinkSync, mkdtempSync } from 'fs'; +import { join } from 'path'; +import { tmpdir } from 'os'; +import chalk from 'chalk'; + +const tmpDir = mkdtempSync(join(tmpdir(), 'ccw-shell-test-')); + +/** + * Execute ccw cli test-parse and capture output + */ +function testParse(command, description) { + console.log(chalk.bold.cyan(`\n${'─'.repeat(70)}`)); + console.log(chalk.bold.white(`📋 ${description}`)); + console.log(chalk.gray(`Command: ${command.substring(0, 100)}${command.length > 100 ? '...' : ''}`)); + console.log(chalk.cyan('─'.repeat(70))); + + try { + const result = execSync(command, { + encoding: 'utf8', + cwd: process.cwd(), + shell: true, + timeout: 10000, + }); + + // Extract key info from output + const promptMatch = result.match(/Value: "([^"]+)"/); + const sourceMatch = result.match(/Source: ([^\n]+)/); + + if (promptMatch) { + console.log(chalk.green('✓ Parsed prompt: ') + chalk.yellow(promptMatch[1].substring(0, 60))); + } + if (sourceMatch) { + console.log(chalk.gray(' Source: ') + sourceMatch[1]); + } + + // Check if prompt was truncated or split + if (result.includes('Positional Arguments') && result.includes('[0]:')) { + const posMatch = result.match(/\[0\]: "([^"]+)"/); + if (posMatch) { + console.log(chalk.red('⚠ WARNING: Part of prompt leaked to positional args: ') + chalk.yellow(posMatch[1])); + } + } + + return { success: true, output: result }; + } catch (error) { + console.log(chalk.red('✗ Error: ') + error.message); + return { success: false, error }; + } +} + +/** + * Main test suite + */ +function main() { + console.log(chalk.bold.magenta('\n' + '█'.repeat(70))); + console.log(chalk.bold.white(' SHELL PROMPT FORMAT TESTS')); + console.log(chalk.bold.magenta('█'.repeat(70))); + + // ============================================ + // INCORRECT METHODS (will fail or parse wrong) + // ============================================ + console.log(chalk.bold.red('\n\n⛔ INCORRECT METHODS (will fail):')); + + // Method 1: Direct multi-line in double quotes (WRONG) + testParse( + `ccw cli test-parse -p "PURPOSE: Test +TASK: Step 1" --tool gemini`, + '❌ Direct multi-line in double quotes (WRONG)' + ); + + // ============================================ + // CORRECT METHODS + // ============================================ + console.log(chalk.bold.green('\n\n✅ CORRECT METHODS:')); + + // Method 1: Single line (works) + testParse( + `ccw cli test-parse -p "PURPOSE: Test | TASK: Step 1" --tool gemini`, + '✓ Single line with pipe separator' + ); + + // Method 2: File-based (-f option) - RECOMMENDED + const promptFile = join(tmpDir, 'prompt.txt'); + writeFileSync(promptFile, `PURPOSE: Identify vulnerabilities +TASK: • Scan injection flaws • Check auth bypass +MODE: analysis +CONTEXT: @src/auth/**/* +EXPECTED: Security report`); + + testParse( + `ccw cli test-parse -f "${promptFile}" --tool gemini`, + '✓ File-based prompt (-f option) - RECOMMENDED' + ); + + // Method 3: $'...' syntax with literal \n (bash only) + testParse( + `ccw cli test-parse -p $'PURPOSE: Test\\nTASK: Step 1\\nMODE: analysis' --tool gemini`, + "✓ $'...' syntax with \\n (bash only)" + ); + + // Method 4: Heredoc via stdin (if supported) + // Note: ccw cli currently doesn't support stdin, but showing the pattern + console.log(chalk.bold.cyan(`\n${'─'.repeat(70)}`)); + console.log(chalk.bold.white(`📋 ✓ Heredoc pattern (for reference)`)); + console.log(chalk.gray(`Command: cat <<'EOF' > prompt.txt && ccw cli -f prompt.txt ...`)); + console.log(chalk.cyan('─'.repeat(70))); + console.log(chalk.green('✓ Create file with heredoc, then use -f option')); + + // Method 5: Single line with escaped newlines in content + testParse( + `ccw cli test-parse -p "PURPOSE: Test | TASK: Step 1 | MODE: analysis | CONTEXT: @src/**/*" --tool gemini`, + '✓ Single line with | as logical separator' + ); + + // Cleanup + try { + unlinkSync(promptFile); + } catch {} + + // Summary + console.log(chalk.bold.magenta('\n\n' + '█'.repeat(70))); + console.log(chalk.bold.white(' SUMMARY: Recommended Approaches')); + console.log(chalk.bold.magenta('█'.repeat(70))); + + console.log(` +${chalk.bold.green('For multi-line prompts, use ONE of these methods:')} + +${chalk.bold('1. File-based (RECOMMENDED):')} + ${chalk.cyan('ccw cli -f prompt.txt --tool gemini --mode analysis')} + +${chalk.bold("2. $'...' syntax (bash only):")} + ${chalk.cyan("ccw cli -p $'PURPOSE: ...\\nTASK: ...\\nMODE: ...' --tool gemini")} + +${chalk.bold('3. Heredoc + file:')} + ${chalk.cyan(`cat <<'EOF' > /tmp/prompt.txt +PURPOSE: ... +TASK: ... +EOF +ccw cli -f /tmp/prompt.txt --tool gemini`)} + +${chalk.bold('4. Single line with separators:')} + ${chalk.cyan('ccw cli -p "PURPOSE: ... | TASK: ... | MODE: ..." --tool gemini')} + +${chalk.bold.red('AVOID:')} + ${chalk.red('ccw cli -p "PURPOSE: ...')} + ${chalk.red('TASK: ..." # Multi-line in quotes - WILL BREAK')} +`); +} + +main(); diff --git a/ccw/src/commands/cli.ts b/ccw/src/commands/cli.ts index 491ea249..3ad82870 100644 --- a/ccw/src/commands/cli.ts +++ b/ccw/src/commands/cli.ts @@ -441,6 +441,11 @@ function testParseAction(args: string[], options: CliExecOptions): void { console.log(chalk.bold.cyan(' │ CLI PARSE TEST ENDPOINT │')); console.log(chalk.bold.cyan(' ═══════════════════════════════════════════════\n')); + // Debug: show raw options.prompt with JSON.stringify to reveal hidden characters + console.log(chalk.bold.yellow('🔬 RAW OPTIONS.PROMPT (JSON):')); + console.log(chalk.cyan(' ' + JSON.stringify(options.prompt))); + console.log(); + // Show args array parsing console.log(chalk.bold.yellow('📦 Positional Arguments (args[]):')); console.log(chalk.gray(' Length: ') + chalk.white(args.length)); @@ -550,7 +555,7 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec console.log(chalk.yellow(' Debug mode enabled\n')); } - // Priority: 1. --file, 2. --prompt/-p option, 3. positional argument + // Priority: 1. --file, 2. stdin (piped), 3. --prompt/-p option, 4. positional argument // Note: On Windows, quoted arguments like -p "say hello" may be split into // -p "say" and positional "hello". We merge them back together. let finalPrompt: string | undefined; @@ -569,13 +574,30 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec console.error(chalk.red('Error: File is empty')); process.exit(1); } - } else if (optionPrompt) { - // Use --prompt/-p option (preferred for multi-line) - // Merge with positional argument if Windows split the quoted string - finalPrompt = positionalPrompt ? `${optionPrompt} ${positionalPrompt}` : optionPrompt; - } else { - // Fall back to positional argument - finalPrompt = positionalPrompt; + } else if (!process.stdin.isTTY) { + // Read from stdin (piped input) - enables: echo "prompt" | ccw cli --tool gemini + // This bypasses Windows shell multi-line argument limitations + const { readFileSync } = await import('fs'); + try { + finalPrompt = readFileSync(0, 'utf8').trim(); // fd 0 = stdin + if (debug) { + console.log(chalk.gray(` Read ${finalPrompt.length} chars from stdin`)); + } + } catch { + // stdin not available or empty, fall through to other methods + } + } + + // If no stdin input, try --prompt/-p option or positional argument + if (!finalPrompt) { + if (optionPrompt) { + // Use --prompt/-p option (preferred for multi-line) + // Merge with positional argument if Windows split the quoted string + finalPrompt = positionalPrompt ? `${optionPrompt} ${positionalPrompt}` : optionPrompt; + } else { + // Fall back to positional argument + finalPrompt = positionalPrompt; + } } // Prompt is required unless resuming OR using review mode with target flags @@ -1257,15 +1279,16 @@ export async function cliCommand( default: { const execOptions = options as CliExecOptions; // Auto-exec if: has -p/--prompt, has -f/--file, has --resume, subcommand looks like a prompt, - // or review mode with target flags (--uncommitted, --base, --commit) + // review mode with target flags (--uncommitted, --base, --commit), or stdin has piped input const hasPromptOption = !!execOptions.prompt; const hasFileOption = !!execOptions.file; const hasResume = execOptions.resume !== undefined; const subcommandIsPrompt = subcommand && !subcommand.startsWith('-'); const hasReviewTarget = execOptions.mode === 'review' && (execOptions.uncommitted || execOptions.base || execOptions.commit); + const hasStdinInput = !process.stdin.isTTY; // piped input detected - if (hasPromptOption || hasFileOption || hasResume || subcommandIsPrompt || hasReviewTarget) { + if (hasPromptOption || hasFileOption || hasResume || subcommandIsPrompt || hasReviewTarget || hasStdinInput) { // Treat as exec: use subcommand as positional prompt if no -p/-f option let positionalPrompt = subcommandIsPrompt ? subcommand : undefined; @@ -1284,6 +1307,7 @@ export async function cliCommand( console.log(' Usage:'); console.log(chalk.gray(' ccw cli -f prompt.txt --tool Execute from file (recommended for multi-line)')); console.log(chalk.gray(' ccw cli -p "" --tool Execute with prompt (single-line)')); + console.log(chalk.gray(' echo "prompt" | ccw cli --tool Execute from stdin (pipe)')); console.log(); console.log(' Subcommands:'); console.log(chalk.gray(' status Check CLI tools availability')); diff --git a/ccw/tests/cli-prompt-parsing.test.ts b/ccw/tests/cli-prompt-parsing.test.ts new file mode 100644 index 00000000..6fb383ff --- /dev/null +++ b/ccw/tests/cli-prompt-parsing.test.ts @@ -0,0 +1,539 @@ +/** + * CLI Prompt Parsing Tests + * + * Tests different prompt formats and verifies the final prompt passed to CLI executor. + * Covers: single-line, multi-line, @ symbols, special characters, template concatenation. + */ + +import { after, afterEach, before, describe, it, mock } from 'node:test'; +import assert from 'node:assert/strict'; +import http from 'node:http'; +import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +const TEST_CCW_HOME = mkdtempSync(join(tmpdir(), 'ccw-prompt-parse-')); +process.env.CCW_DATA_DIR = TEST_CCW_HOME; + +const cliCommandPath = new URL('../dist/commands/cli.js', import.meta.url).href; +const cliExecutorPath = new URL('../dist/tools/cli-executor.js', import.meta.url).href; +const historyStorePath = new URL('../dist/tools/cli-history-store.js', import.meta.url).href; + +function stubHttpRequest(): void { + mock.method(http, 'request', () => { + const req = { + on(event: string, handler: (arg?: any) => void) { + if (event === 'socket') handler({ unref() {} }); + return req; + }, + write() {}, + end() {}, + destroy() {}, + }; + return req as any; + }); +} + +function createMockExecutor(calls: any[]) { + return async (params: any) => { + calls.push({ + prompt: params.prompt, + tool: params.tool, + mode: params.mode, + model: params.model, + promptLength: params.prompt?.length, + hasNewlines: params.prompt?.includes('\n'), + lineCount: params.prompt?.split('\n').length, + }); + return { + success: true, + stdout: 'ok', + stderr: '', + execution: { id: 'EXEC-TEST', duration_ms: 1, status: 'success' }, + conversation: { turn_count: 1, total_duration_ms: 1 }, + }; + }; +} + +describe('CLI Prompt Parsing', async () => { + let cliModule: any; + let cliExecutorModule: any; + let historyStoreModule: any; + + before(async () => { + cliModule = await import(cliCommandPath); + cliExecutorModule = await import(cliExecutorPath); + historyStoreModule = await import(historyStorePath); + }); + + afterEach(() => { + mock.restoreAll(); + }); + + after(() => { + try { + historyStoreModule?.closeAllStores?.(); + } catch { /* ignore */ } + rmSync(TEST_CCW_HOME, { recursive: true, force: true }); + }); + + describe('Single-line prompts', () => { + it('passes simple single-line prompt via -p option', async () => { + stubHttpRequest(); + mock.method(console, 'log', () => {}); + mock.method(console, 'error', () => {}); + mock.method(process as any, 'exit', () => {}); + + const calls: any[] = []; + mock.method(cliExecutorModule.cliExecutorTool, 'execute', createMockExecutor(calls)); + + await cliModule.cliCommand('exec', [], { + prompt: 'Simple single line prompt', + tool: 'gemini', + }); + await new Promise(r => setTimeout(r, 100)); + + assert.equal(calls.length, 1); + // Prompt contains original + template rules + assert.ok(calls[0].prompt.includes('Simple single line prompt')); + assert.equal(calls[0].hasNewlines, true); // Templates add newlines + console.log('\n📝 Single-line prompt final length:', calls[0].promptLength); + }); + + it('passes prompt with quotes', async () => { + stubHttpRequest(); + mock.method(console, 'log', () => {}); + mock.method(console, 'error', () => {}); + mock.method(process as any, 'exit', () => {}); + + const calls: any[] = []; + mock.method(cliExecutorModule.cliExecutorTool, 'execute', createMockExecutor(calls)); + + await cliModule.cliCommand('exec', [], { + prompt: 'Say "Hello World" to me', + tool: 'gemini', + }); + await new Promise(r => setTimeout(r, 100)); + + assert.equal(calls.length, 1); + assert.ok(calls[0].prompt.includes('Say "Hello World" to me')); + console.log('\n📝 Quoted prompt preserved:', calls[0].prompt.substring(0, 100)); + }); + }); + + describe('Multi-line prompts', () => { + it('passes multi-line prompt via -p option', async () => { + stubHttpRequest(); + mock.method(console, 'log', () => {}); + mock.method(console, 'error', () => {}); + mock.method(process as any, 'exit', () => {}); + + const calls: any[] = []; + mock.method(cliExecutorModule.cliExecutorTool, 'execute', createMockExecutor(calls)); + + const multiLinePrompt = `PURPOSE: Test multi-line prompt +TASK: • Step 1 • Step 2 • Step 3 +MODE: analysis +CONTEXT: @src/**/* +EXPECTED: Test output`; + + await cliModule.cliCommand('exec', [], { + prompt: multiLinePrompt, + tool: 'gemini', + }); + await new Promise(r => setTimeout(r, 100)); + + assert.equal(calls.length, 1); + assert.ok(calls[0].prompt.includes('PURPOSE: Test multi-line prompt')); + assert.ok(calls[0].prompt.includes('TASK: • Step 1 • Step 2 • Step 3')); + assert.ok(calls[0].prompt.includes('CONTEXT: @src/**/*')); + console.log('\n📝 Multi-line prompt lines:', calls[0].lineCount); + }); + + it('reads multi-line prompt from file via -f option', async () => { + stubHttpRequest(); + mock.method(console, 'log', () => {}); + mock.method(console, 'error', () => {}); + mock.method(process as any, 'exit', () => {}); + + const calls: any[] = []; + mock.method(cliExecutorModule.cliExecutorTool, 'execute', createMockExecutor(calls)); + + // Create temp prompt file + const promptFile = join(TEST_CCW_HOME, 'test-prompt.txt'); + const fileContent = `PURPOSE: File-based prompt test +TASK: • Read from file +MODE: analysis +CONTEXT: @**/*.ts | Memory: test context +EXPECTED: Success`; + writeFileSync(promptFile, fileContent); + + await cliModule.cliCommand('exec', [], { + file: promptFile, + tool: 'gemini', + }); + await new Promise(r => setTimeout(r, 100)); + + assert.equal(calls.length, 1); + assert.ok(calls[0].prompt.includes('PURPOSE: File-based prompt test')); + assert.ok(calls[0].prompt.includes('Memory: test context')); + console.log('\n📝 File prompt loaded, lines:', calls[0].lineCount); + }); + }); + + describe('@ symbol patterns', () => { + it('preserves @ patterns in CONTEXT field', async () => { + stubHttpRequest(); + mock.method(console, 'log', () => {}); + mock.method(console, 'error', () => {}); + mock.method(process as any, 'exit', () => {}); + + const calls: any[] = []; + mock.method(cliExecutorModule.cliExecutorTool, 'execute', createMockExecutor(calls)); + + const promptWithPatterns = `PURPOSE: Test @ patterns +CONTEXT: @src/auth/**/*.ts @src/middleware/*.ts @shared/utils/security.ts +EXPECTED: Pattern preservation`; + + await cliModule.cliCommand('exec', [], { + prompt: promptWithPatterns, + tool: 'gemini', + }); + await new Promise(r => setTimeout(r, 100)); + + assert.equal(calls.length, 1); + assert.ok(calls[0].prompt.includes('@src/auth/**/*.ts')); + assert.ok(calls[0].prompt.includes('@src/middleware/*.ts')); + assert.ok(calls[0].prompt.includes('@shared/utils/security.ts')); + console.log('\n📝 @ patterns preserved in final prompt'); + }); + + it('preserves @ patterns with glob wildcards', async () => { + stubHttpRequest(); + mock.method(console, 'log', () => {}); + mock.method(console, 'error', () => {}); + mock.method(process as any, 'exit', () => {}); + + const calls: any[] = []; + mock.method(cliExecutorModule.cliExecutorTool, 'execute', createMockExecutor(calls)); + + await cliModule.cliCommand('exec', [], { + prompt: 'CONTEXT: @**/*.{ts,tsx} @!node_modules/** @!dist/**', + tool: 'gemini', + }); + await new Promise(r => setTimeout(r, 100)); + + assert.equal(calls.length, 1); + assert.ok(calls[0].prompt.includes('@**/*.{ts,tsx}')); + assert.ok(calls[0].prompt.includes('@!node_modules/**')); + console.log('\n📝 Complex glob patterns preserved'); + }); + + it('preserves @ patterns with Memory section', async () => { + stubHttpRequest(); + mock.method(console, 'log', () => {}); + mock.method(console, 'error', () => {}); + mock.method(process as any, 'exit', () => {}); + + const calls: any[] = []; + mock.method(cliExecutorModule.cliExecutorTool, 'execute', createMockExecutor(calls)); + + await cliModule.cliCommand('exec', [], { + prompt: 'CONTEXT: @src/**/* | Memory: Using bcrypt for passwords, JWT for sessions', + tool: 'gemini', + }); + await new Promise(r => setTimeout(r, 100)); + + assert.equal(calls.length, 1); + assert.ok(calls[0].prompt.includes('@src/**/*')); + assert.ok(calls[0].prompt.includes('Memory: Using bcrypt for passwords')); + console.log('\n📝 @ pattern + Memory preserved'); + }); + }); + + describe('Special characters', () => { + it('preserves bullet points and special characters', async () => { + stubHttpRequest(); + mock.method(console, 'log', () => {}); + mock.method(console, 'error', () => {}); + mock.method(process as any, 'exit', () => {}); + + const calls: any[] = []; + mock.method(cliExecutorModule.cliExecutorTool, 'execute', createMockExecutor(calls)); + + await cliModule.cliCommand('exec', [], { + prompt: 'TASK: • First item • Second item • Third item ✓ ✗', + tool: 'gemini', + }); + await new Promise(r => setTimeout(r, 100)); + + assert.equal(calls.length, 1); + assert.ok(calls[0].prompt.includes('• First item')); + assert.ok(calls[0].prompt.includes('✓')); + console.log('\n📝 Special characters preserved'); + }); + + it('preserves code-like content', async () => { + stubHttpRequest(); + mock.method(console, 'log', () => {}); + mock.method(console, 'error', () => {}); + mock.method(process as any, 'exit', () => {}); + + const calls: any[] = []; + mock.method(cliExecutorModule.cliExecutorTool, 'execute', createMockExecutor(calls)); + + await cliModule.cliCommand('exec', [], { + prompt: 'Fix: const x = arr.filter(i => i > 0).map(i => i * 2);', + tool: 'gemini', + }); + await new Promise(r => setTimeout(r, 100)); + + assert.equal(calls.length, 1); + assert.ok(calls[0].prompt.includes('const x = arr.filter')); + assert.ok(calls[0].prompt.includes('=>')); + console.log('\n📝 Code-like content preserved'); + }); + + it('preserves shell-like patterns', async () => { + stubHttpRequest(); + mock.method(console, 'log', () => {}); + mock.method(console, 'error', () => {}); + mock.method(process as any, 'exit', () => {}); + + const calls: any[] = []; + mock.method(cliExecutorModule.cliExecutorTool, 'execute', createMockExecutor(calls)); + + await cliModule.cliCommand('exec', [], { + prompt: 'Run: npm run build && npm test | grep "passed"', + tool: 'gemini', + }); + await new Promise(r => setTimeout(r, 100)); + + assert.equal(calls.length, 1); + assert.ok(calls[0].prompt.includes('npm run build && npm test')); + assert.ok(calls[0].prompt.includes('| grep')); + console.log('\n📝 Shell-like patterns preserved'); + }); + }); + + describe('Template concatenation', () => { + it('concatenates system rules and roles to prompt', async () => { + stubHttpRequest(); + mock.method(console, 'log', () => {}); + mock.method(console, 'error', () => {}); + mock.method(process as any, 'exit', () => {}); + + const calls: any[] = []; + mock.method(cliExecutorModule.cliExecutorTool, 'execute', createMockExecutor(calls)); + + await cliModule.cliCommand('exec', [], { + prompt: 'Test prompt', + tool: 'gemini', + rule: 'universal-rigorous-style', + }); + await new Promise(r => setTimeout(r, 100)); + + assert.equal(calls.length, 1); + // Should have SYSTEM RULES and ROLES sections + assert.ok(calls[0].prompt.includes('Test prompt')); + assert.ok(calls[0].prompt.includes('=== SYSTEM RULES ===') || calls[0].promptLength > 100); + console.log('\n📝 Template concatenated, total length:', calls[0].promptLength); + }); + + it('preserves user prompt structure with templates', async () => { + stubHttpRequest(); + mock.method(console, 'log', () => {}); + mock.method(console, 'error', () => {}); + mock.method(process as any, 'exit', () => {}); + + const calls: any[] = []; + mock.method(cliExecutorModule.cliExecutorTool, 'execute', createMockExecutor(calls)); + + const structuredPrompt = `PURPOSE: Security audit +TASK: • Scan injection flaws • Check auth bypass +MODE: analysis +CONTEXT: @src/auth/**/* +EXPECTED: Security report +CONSTRAINTS: Focus on auth`; + + await cliModule.cliCommand('exec', [], { + prompt: structuredPrompt, + tool: 'gemini', + mode: 'analysis', + }); + await new Promise(r => setTimeout(r, 100)); + + assert.equal(calls.length, 1); + // User prompt should come first + const promptStart = calls[0].prompt.indexOf('PURPOSE: Security audit'); + const rulesStart = calls[0].prompt.indexOf('=== SYSTEM RULES ==='); + if (rulesStart > 0) { + assert.ok(promptStart < rulesStart, 'User prompt should precede system rules'); + } + console.log('\n📝 User prompt preserved at start'); + }); + }); + + describe('Edge cases', () => { + it('handles empty lines in prompt', async () => { + stubHttpRequest(); + mock.method(console, 'log', () => {}); + mock.method(console, 'error', () => {}); + mock.method(process as any, 'exit', () => {}); + + const calls: any[] = []; + mock.method(cliExecutorModule.cliExecutorTool, 'execute', createMockExecutor(calls)); + + await cliModule.cliCommand('exec', [], { + prompt: 'Line 1\n\nLine 3\n\n\nLine 6', + tool: 'gemini', + }); + await new Promise(r => setTimeout(r, 100)); + + assert.equal(calls.length, 1); + assert.ok(calls[0].prompt.includes('Line 1')); + assert.ok(calls[0].prompt.includes('Line 3')); + assert.ok(calls[0].prompt.includes('Line 6')); + console.log('\n📝 Empty lines handled'); + }); + + it('handles very long single-line prompt', async () => { + stubHttpRequest(); + mock.method(console, 'log', () => {}); + mock.method(console, 'error', () => {}); + mock.method(process as any, 'exit', () => {}); + + const calls: any[] = []; + mock.method(cliExecutorModule.cliExecutorTool, 'execute', createMockExecutor(calls)); + + const longPrompt = 'A'.repeat(10000); + await cliModule.cliCommand('exec', [], { + prompt: longPrompt, + tool: 'gemini', + }); + await new Promise(r => setTimeout(r, 100)); + + assert.equal(calls.length, 1); + assert.ok(calls[0].prompt.includes('A'.repeat(100))); + console.log('\n📝 Long prompt handled, length:', calls[0].promptLength); + }); + + it('handles Unicode characters', async () => { + stubHttpRequest(); + mock.method(console, 'log', () => {}); + mock.method(console, 'error', () => {}); + mock.method(process as any, 'exit', () => {}); + + const calls: any[] = []; + mock.method(cliExecutorModule.cliExecutorTool, 'execute', createMockExecutor(calls)); + + await cliModule.cliCommand('exec', [], { + prompt: '中文测试 日本語テスト 한국어테스트 🚀🔥💡', + tool: 'gemini', + }); + await new Promise(r => setTimeout(r, 100)); + + assert.equal(calls.length, 1); + assert.ok(calls[0].prompt.includes('中文测试')); + assert.ok(calls[0].prompt.includes('日本語テスト')); + assert.ok(calls[0].prompt.includes('🚀')); + console.log('\n📝 Unicode preserved'); + }); + }); + + describe('Full template prompt', () => { + it('preserves complete CLI tools usage template format', async () => { + stubHttpRequest(); + mock.method(console, 'log', () => {}); + mock.method(console, 'error', () => {}); + mock.method(process as any, 'exit', () => {}); + + const calls: any[] = []; + mock.method(cliExecutorModule.cliExecutorTool, 'execute', createMockExecutor(calls)); + + const fullTemplate = `PURPOSE: Identify OWASP Top 10 vulnerabilities in authentication module to pass security audit; success = all critical/high issues documented with remediation +TASK: • Scan for injection flaws (SQL, command, LDAP) • Check authentication bypass vectors • Evaluate session management • Assess sensitive data exposure +MODE: analysis +CONTEXT: @src/auth/**/* @src/middleware/auth.ts | Memory: Using bcrypt for passwords, JWT for sessions +EXPECTED: Security report with: severity matrix, file:line references, CVE mappings where applicable, remediation code snippets prioritized by risk +CONSTRAINTS: Focus on authentication | Ignore test files`; + + await cliModule.cliCommand('exec', [], { + prompt: fullTemplate, + tool: 'gemini', + mode: 'analysis', + }); + await new Promise(r => setTimeout(r, 100)); + + assert.equal(calls.length, 1); + // Verify all sections preserved + assert.ok(calls[0].prompt.includes('PURPOSE:')); + assert.ok(calls[0].prompt.includes('TASK:')); + assert.ok(calls[0].prompt.includes('MODE:')); + assert.ok(calls[0].prompt.includes('CONTEXT:')); + assert.ok(calls[0].prompt.includes('EXPECTED:')); + assert.ok(calls[0].prompt.includes('CONSTRAINTS:')); + // Verify specific content + assert.ok(calls[0].prompt.includes('OWASP Top 10')); + assert.ok(calls[0].prompt.includes('@src/auth/**/*')); + assert.ok(calls[0].prompt.includes('Memory: Using bcrypt')); + assert.ok(calls[0].prompt.includes('severity matrix')); + + console.log('\n📝 Full template format preserved'); + console.log(' Final prompt length:', calls[0].promptLength); + console.log(' Line count:', calls[0].lineCount); + }); + }); +}); + +describe('Prompt Output Visualization', () => { + let cliModule: any; + let cliExecutorModule: any; + + before(async () => { + cliModule = await import(cliCommandPath); + cliExecutorModule = await import(cliExecutorPath); + }); + + afterEach(() => { + mock.restoreAll(); + }); + + it('demonstrates final prompt structure (visual output)', async () => { + stubHttpRequest(); + mock.method(console, 'log', () => {}); + mock.method(console, 'error', () => {}); + mock.method(process as any, 'exit', () => {}); + + const capturedPrompts: string[] = []; + mock.method(cliExecutorModule.cliExecutorTool, 'execute', async (params: any) => { + capturedPrompts.push(params.prompt); + return { + success: true, + stdout: 'ok', + stderr: '', + execution: { id: 'EXEC-VIS', duration_ms: 1, status: 'success' }, + conversation: { turn_count: 1, total_duration_ms: 1 }, + }; + }); + + await cliModule.cliCommand('exec', [], { + prompt: 'Test visualization prompt', + tool: 'gemini', + }); + await new Promise(r => setTimeout(r, 100)); + + // Output visualization + console.log('\n' + '='.repeat(60)); + console.log('📋 FINAL PROMPT PASSED TO CLI:'); + console.log('='.repeat(60)); + if (capturedPrompts[0]) { + const preview = capturedPrompts[0].substring(0, 500); + console.log(preview); + if (capturedPrompts[0].length > 500) { + console.log(`\n... [${capturedPrompts[0].length - 500} more characters]`); + } + } + console.log('='.repeat(60)); + }); +}); diff --git a/ccw/tsconfig.tsbuildinfo b/ccw/tsconfig.tsbuildinfo deleted file mode 100644 index 5e559b2b..00000000 --- a/ccw/tsconfig.tsbuildinfo +++ /dev/null @@ -1 +0,0 @@ -{"root":["./src/cli.ts","./src/index.ts","./src/commands/cli.ts","./src/commands/core-memory.ts","./src/commands/hook.ts","./src/commands/install.ts","./src/commands/issue.ts","./src/commands/list.ts","./src/commands/memory.ts","./src/commands/serve.ts","./src/commands/session-path-resolver.ts","./src/commands/session.ts","./src/commands/stop.ts","./src/commands/tool.ts","./src/commands/uninstall.ts","./src/commands/upgrade.ts","./src/commands/view.ts","./src/config/litellm-api-config-manager.ts","./src/config/provider-models.ts","./src/config/storage-paths.ts","./src/core/cache-manager.ts","./src/core/claude-freshness.ts","./src/core/core-memory-store.ts","./src/core/dashboard-generator-patch.ts","./src/core/dashboard-generator.ts","./src/core/data-aggregator.ts","./src/core/history-importer.ts","./src/core/lite-scanner-complete.ts","./src/core/lite-scanner.ts","./src/core/manifest.ts","./src/core/memory-embedder-bridge.ts","./src/core/memory-store.ts","./src/core/server.ts","./src/core/session-clustering-service.ts","./src/core/session-scanner.ts","./src/core/websocket.ts","./src/core/routes/ccw-routes.ts","./src/core/routes/claude-routes.ts","./src/core/routes/cli-routes.ts","./src/core/routes/codexlens-routes.ts","./src/core/routes/core-memory-routes.ts","./src/core/routes/discovery-routes.ts","./src/core/routes/files-routes.ts","./src/core/routes/graph-routes.ts","./src/core/routes/help-routes.ts","./src/core/routes/hooks-routes.ts","./src/core/routes/issue-routes.ts","./src/core/routes/litellm-api-routes.ts","./src/core/routes/litellm-routes.ts","./src/core/routes/mcp-routes.ts","./src/core/routes/mcp-templates-db.ts","./src/core/routes/memory-routes.ts","./src/core/routes/nav-status-routes.ts","./src/core/routes/rules-routes.ts","./src/core/routes/session-routes.ts","./src/core/routes/skills-routes.ts","./src/core/routes/status-routes.ts","./src/core/routes/system-routes.ts","./src/mcp-server/index.ts","./src/tools/classify-folders.ts","./src/tools/claude-cli-tools.ts","./src/tools/cli-config-manager.ts","./src/tools/cli-executor.ts","./src/tools/cli-history-store.ts","./src/tools/codex-lens.ts","./src/tools/context-cache-store.ts","./src/tools/context-cache.ts","./src/tools/convert-tokens-to-css.ts","./src/tools/core-memory.ts","./src/tools/detect-changed-modules.ts","./src/tools/discover-design-files.ts","./src/tools/edit-file.ts","./src/tools/generate-module-docs.ts","./src/tools/get-modules-by-depth.ts","./src/tools/index.ts","./src/tools/litellm-client.ts","./src/tools/litellm-executor.ts","./src/tools/native-session-discovery.ts","./src/tools/notifier.ts","./src/tools/pattern-parser.ts","./src/tools/read-file.ts","./src/tools/resume-strategy.ts","./src/tools/session-content-parser.ts","./src/tools/session-manager.ts","./src/tools/smart-context.ts","./src/tools/smart-search.ts","./src/tools/storage-manager.ts","./src/tools/ui-generate-preview.js","./src/tools/ui-instantiate-prototypes.js","./src/tools/update-module-claude.js","./src/tools/write-file.ts","./src/types/config.ts","./src/types/index.ts","./src/types/litellm-api-config.ts","./src/types/session.ts","./src/types/tool.ts","./src/utils/browser-launcher.ts","./src/utils/file-utils.ts","./src/utils/path-resolver.ts","./src/utils/path-validator.ts","./src/utils/python-utils.ts","./src/utils/ui.ts"],"version":"5.9.3"} \ No newline at end of file