mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-04 01:40:45 +08:00
- Updated embedding_manager.py to include backend parameter in model configuration. - Modified model_manager.py to utilize cache_name for ONNX models. - Refactored hybrid_search.py to improve embedder initialization based on backend type. - Added backend column to vector_store.py for better model configuration management. - Implemented migration for existing database to include backend information. - Enhanced API settings implementation with comprehensive provider and endpoint management. - Introduced LiteLLM integration guide detailing configuration and usage. - Added examples for LiteLLM usage in TypeScript.
885 lines
33 KiB
TypeScript
885 lines
33 KiB
TypeScript
/**
|
|
* CLI Command - Unified CLI tool executor command
|
|
* Provides interface for executing Gemini, Qwen, and Codex
|
|
*/
|
|
|
|
import chalk from 'chalk';
|
|
import http from 'http';
|
|
import {
|
|
cliExecutorTool,
|
|
getCliToolsStatus,
|
|
getExecutionHistory,
|
|
getExecutionHistoryAsync,
|
|
getExecutionDetail,
|
|
getConversationDetail
|
|
} from '../tools/cli-executor.js';
|
|
import {
|
|
getStorageStats,
|
|
getStorageConfig,
|
|
cleanProjectStorage,
|
|
cleanAllStorage,
|
|
formatBytes,
|
|
formatTimeAgo,
|
|
resolveProjectId,
|
|
projectExists,
|
|
getStorageLocationInstructions
|
|
} from '../tools/storage-manager.js';
|
|
|
|
// Dashboard notification settings
|
|
const DASHBOARD_PORT = process.env.CCW_PORT || 3456;
|
|
|
|
/**
|
|
* Notify dashboard of CLI execution events (fire and forget)
|
|
*/
|
|
function notifyDashboard(data: Record<string, unknown>): void {
|
|
const payload = JSON.stringify({
|
|
type: 'cli_execution',
|
|
...data,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
const req = http.request({
|
|
hostname: 'localhost',
|
|
port: Number(DASHBOARD_PORT),
|
|
path: '/api/hook',
|
|
method: 'POST',
|
|
timeout: 2000, // 2 second timeout to prevent hanging
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Content-Length': Buffer.byteLength(payload)
|
|
}
|
|
});
|
|
|
|
// Fire and forget - don't block process exit
|
|
req.on('socket', (socket) => {
|
|
socket.unref(); // Allow process to exit even if socket is open
|
|
});
|
|
req.on('error', (err) => {
|
|
if (process.env.DEBUG) console.error('[Dashboard] CLI notification failed:', err.message);
|
|
});
|
|
req.on('timeout', () => {
|
|
req.destroy();
|
|
if (process.env.DEBUG) console.error('[Dashboard] CLI notification timed out');
|
|
});
|
|
req.write(payload);
|
|
req.end();
|
|
}
|
|
|
|
interface CliExecOptions {
|
|
prompt?: string; // Prompt via --prompt/-p option (preferred for multi-line)
|
|
file?: string; // Read prompt from file
|
|
tool?: string;
|
|
mode?: string;
|
|
model?: string;
|
|
cd?: string;
|
|
includeDirs?: string;
|
|
timeout?: string;
|
|
noStream?: boolean;
|
|
resume?: string | boolean; // true = last, string = execution ID, comma-separated for merge
|
|
id?: string; // Custom execution ID (e.g., IMPL-001-step1)
|
|
noNative?: boolean; // Force prompt concatenation instead of native resume
|
|
cache?: string | boolean; // Cache: true = auto from CONTEXT, string = comma-separated patterns/content
|
|
injectMode?: 'none' | 'full' | 'progressive'; // Inject mode for cached content
|
|
}
|
|
|
|
/** Cache configuration parsed from --cache */
|
|
interface CacheConfig {
|
|
patterns?: string[]; // @patterns to pack (items starting with @)
|
|
content?: string; // Additional text content (items not starting with @)
|
|
}
|
|
|
|
interface HistoryOptions {
|
|
limit?: string;
|
|
tool?: string;
|
|
status?: string;
|
|
}
|
|
|
|
interface StorageOptions {
|
|
all?: boolean;
|
|
project?: string;
|
|
cliHistory?: boolean;
|
|
memory?: boolean;
|
|
storageCache?: boolean;
|
|
config?: boolean;
|
|
force?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Show storage information and management options
|
|
*/
|
|
async function storageAction(subAction: string | undefined, options: StorageOptions): Promise<void> {
|
|
switch (subAction) {
|
|
case 'info':
|
|
case undefined:
|
|
await showStorageInfo();
|
|
break;
|
|
case 'clean':
|
|
await cleanStorage(options);
|
|
break;
|
|
case 'config':
|
|
showStorageConfig();
|
|
break;
|
|
default:
|
|
showStorageHelp();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show storage information
|
|
*/
|
|
async function showStorageInfo(): Promise<void> {
|
|
console.log(chalk.bold.cyan('\n CCW Storage Information\n'));
|
|
|
|
const config = getStorageConfig();
|
|
const stats = getStorageStats();
|
|
|
|
// Configuration
|
|
console.log(chalk.bold.white(' Location:'));
|
|
console.log(` ${chalk.cyan(stats.rootPath)}`);
|
|
if (config.isCustom) {
|
|
console.log(chalk.gray(` (Custom: CCW_DATA_DIR=${config.envVar})`));
|
|
}
|
|
console.log();
|
|
|
|
// Summary
|
|
console.log(chalk.bold.white(' Summary:'));
|
|
console.log(` Total Size: ${chalk.yellow(formatBytes(stats.totalSize))}`);
|
|
console.log(` Projects: ${chalk.yellow(stats.projectCount.toString())}`);
|
|
console.log(` Global DB: ${stats.globalDb.exists ? chalk.green(formatBytes(stats.globalDb.size)) : chalk.gray('Not created')}`);
|
|
console.log();
|
|
|
|
// Projects breakdown
|
|
if (stats.projects.length > 0) {
|
|
console.log(chalk.bold.white(' Projects:'));
|
|
console.log(chalk.gray(' ID Size History Last Used'));
|
|
console.log(chalk.gray(' ─────────────────────────────────────────────────────'));
|
|
|
|
for (const project of stats.projects) {
|
|
const historyInfo = project.cliHistory.recordCount !== undefined
|
|
? `${project.cliHistory.recordCount} records`
|
|
: (project.cliHistory.exists ? 'Yes' : '-');
|
|
|
|
console.log(
|
|
` ${chalk.dim(project.projectId)} ` +
|
|
`${formatBytes(project.totalSize).padStart(8)} ` +
|
|
`${historyInfo.padStart(10)} ` +
|
|
`${chalk.gray(formatTimeAgo(project.lastModified))}`
|
|
);
|
|
}
|
|
console.log();
|
|
}
|
|
|
|
// Usage tips
|
|
console.log(chalk.gray(' Commands:'));
|
|
console.log(chalk.gray(' ccw cli storage clean Clean all storage'));
|
|
console.log(chalk.gray(' ccw cli storage clean --project <path> Clean specific project'));
|
|
console.log(chalk.gray(' ccw cli storage config Show location config'));
|
|
console.log();
|
|
}
|
|
|
|
/**
|
|
* Clean storage
|
|
*/
|
|
async function cleanStorage(options: StorageOptions): Promise<void> {
|
|
const { all, project, force, cliHistory, memory, storageCache, config } = options;
|
|
|
|
// Determine what to clean
|
|
const cleanTypes = {
|
|
cliHistory: cliHistory || (!cliHistory && !memory && !storageCache && !config),
|
|
memory: memory || (!cliHistory && !memory && !storageCache && !config),
|
|
cache: storageCache || (!cliHistory && !memory && !storageCache && !config),
|
|
config: config || false, // Config requires explicit flag
|
|
all: !cliHistory && !memory && !storageCache && !config
|
|
};
|
|
|
|
if (project) {
|
|
// Clean specific project
|
|
const projectId = resolveProjectId(project);
|
|
|
|
if (!projectExists(projectId)) {
|
|
console.log(chalk.yellow(`\n No storage found for project: ${project}`));
|
|
console.log(chalk.gray(` (Project ID: ${projectId})\n`));
|
|
return;
|
|
}
|
|
|
|
if (!force) {
|
|
console.log(chalk.bold.yellow('\n Warning: This will delete storage for project:'));
|
|
console.log(` Path: ${project}`);
|
|
console.log(` ID: ${projectId}`);
|
|
console.log(chalk.gray('\n Use --force to confirm deletion.\n'));
|
|
return;
|
|
}
|
|
|
|
console.log(chalk.bold.cyan('\n Cleaning project storage...\n'));
|
|
const result = cleanProjectStorage(projectId, cleanTypes);
|
|
|
|
if (result.success) {
|
|
console.log(chalk.green(` ✓ Cleaned ${formatBytes(result.freedBytes)}`));
|
|
} else {
|
|
console.log(chalk.red(' ✗ Cleanup completed with errors:'));
|
|
for (const err of result.errors) {
|
|
console.log(chalk.red(` - ${err}`));
|
|
}
|
|
}
|
|
} else {
|
|
// Clean all storage
|
|
const stats = getStorageStats();
|
|
|
|
if (stats.projectCount === 0) {
|
|
console.log(chalk.yellow('\n No storage to clean.\n'));
|
|
return;
|
|
}
|
|
|
|
if (!force) {
|
|
console.log(chalk.bold.yellow('\n Warning: This will delete ALL CCW storage:'));
|
|
console.log(` Location: ${stats.rootPath}`);
|
|
console.log(` Projects: ${stats.projectCount}`);
|
|
console.log(` Size: ${formatBytes(stats.totalSize)}`);
|
|
console.log(chalk.gray('\n Use --force to confirm deletion.\n'));
|
|
return;
|
|
}
|
|
|
|
console.log(chalk.bold.cyan('\n Cleaning all storage...\n'));
|
|
const result = cleanAllStorage(cleanTypes);
|
|
|
|
if (result.success) {
|
|
console.log(chalk.green(` ✓ Cleaned ${result.projectsCleaned} projects, freed ${formatBytes(result.freedBytes)}`));
|
|
} else {
|
|
console.log(chalk.yellow(` ⚠ Cleaned ${result.projectsCleaned} projects with some errors:`));
|
|
for (const err of result.errors) {
|
|
console.log(chalk.red(` - ${err}`));
|
|
}
|
|
}
|
|
}
|
|
console.log();
|
|
}
|
|
|
|
/**
|
|
* Show storage configuration
|
|
*/
|
|
function showStorageConfig(): void {
|
|
console.log(getStorageLocationInstructions());
|
|
}
|
|
|
|
/**
|
|
* Show storage help
|
|
*/
|
|
function showStorageHelp(): void {
|
|
console.log(chalk.bold.cyan('\n CCW Storage Management\n'));
|
|
console.log(' Subcommands:');
|
|
console.log(chalk.gray(' info Show storage information (default)'));
|
|
console.log(chalk.gray(' clean Clean storage'));
|
|
console.log(chalk.gray(' config Show configuration instructions'));
|
|
console.log();
|
|
console.log(' Clean Options:');
|
|
console.log(chalk.gray(' --project <path> Clean specific project storage'));
|
|
console.log(chalk.gray(' --force Confirm deletion'));
|
|
console.log(chalk.gray(' --cli-history Clean only CLI history'));
|
|
console.log(chalk.gray(' --memory Clean only memory store'));
|
|
console.log(chalk.gray(' --cache Clean only cache'));
|
|
console.log(chalk.gray(' --config Clean config (requires explicit flag)'));
|
|
console.log();
|
|
console.log(' Examples:');
|
|
console.log(chalk.gray(' ccw cli storage # Show storage info'));
|
|
console.log(chalk.gray(' ccw cli storage clean --force # Clean all storage'));
|
|
console.log(chalk.gray(' ccw cli storage clean --project . --force # Clean current project'));
|
|
console.log(chalk.gray(' ccw cli storage config # Show config instructions'));
|
|
console.log();
|
|
}
|
|
|
|
/**
|
|
* Test endpoint for debugging multi-line prompt parsing
|
|
* Shows exactly how Commander.js parsed the arguments
|
|
*/
|
|
function testParseAction(args: string[], options: CliExecOptions): void {
|
|
console.log(chalk.bold.cyan('\n ═══════════════════════════════════════════════'));
|
|
console.log(chalk.bold.cyan(' │ CLI PARSE TEST ENDPOINT │'));
|
|
console.log(chalk.bold.cyan(' ═══════════════════════════════════════════════\n'));
|
|
|
|
// Show args array parsing
|
|
console.log(chalk.bold.yellow('📦 Positional Arguments (args[]):'));
|
|
console.log(chalk.gray(' Length: ') + chalk.white(args.length));
|
|
if (args.length === 0) {
|
|
console.log(chalk.gray(' (empty)'));
|
|
} else {
|
|
args.forEach((arg, i) => {
|
|
console.log(chalk.gray(` [${i}]: `) + chalk.green(`"${arg}"`));
|
|
// Show if multiline
|
|
if (arg.includes('\n')) {
|
|
console.log(chalk.yellow(` ↳ Contains ${arg.split('\n').length} lines`));
|
|
}
|
|
});
|
|
}
|
|
|
|
console.log();
|
|
|
|
// Show options parsing
|
|
console.log(chalk.bold.yellow('⚙️ Options:'));
|
|
const optionEntries = Object.entries(options).filter(([_, v]) => v !== undefined);
|
|
if (optionEntries.length === 0) {
|
|
console.log(chalk.gray(' (none)'));
|
|
} else {
|
|
optionEntries.forEach(([key, value]) => {
|
|
const displayValue = typeof value === 'string' && value.includes('\n')
|
|
? `"${value.substring(0, 50)}..." (${value.split('\n').length} lines)`
|
|
: JSON.stringify(value);
|
|
console.log(chalk.gray(` --${key}: `) + chalk.cyan(displayValue));
|
|
});
|
|
}
|
|
|
|
console.log();
|
|
|
|
// Show what would be used as prompt
|
|
console.log(chalk.bold.yellow('🎯 Final Prompt Resolution:'));
|
|
const { prompt: optionPrompt, file } = options;
|
|
|
|
if (file) {
|
|
console.log(chalk.gray(' Source: ') + chalk.magenta('--file/-f option'));
|
|
console.log(chalk.gray(' File: ') + chalk.cyan(file));
|
|
} else if (optionPrompt) {
|
|
console.log(chalk.gray(' Source: ') + chalk.magenta('--prompt/-p option'));
|
|
console.log(chalk.gray(' Value: ') + chalk.green(`"${optionPrompt.substring(0, 100)}${optionPrompt.length > 100 ? '...' : ''}"`));
|
|
if (optionPrompt.includes('\n')) {
|
|
console.log(chalk.yellow(` ↳ Multiline: ${optionPrompt.split('\n').length} lines`));
|
|
}
|
|
} else if (args[0]) {
|
|
console.log(chalk.gray(' Source: ') + chalk.magenta('positional argument (args[0])'));
|
|
console.log(chalk.gray(' Value: ') + chalk.green(`"${args[0].substring(0, 100)}${args[0].length > 100 ? '...' : ''}"`));
|
|
if (args[0].includes('\n')) {
|
|
console.log(chalk.yellow(` ↳ Multiline: ${args[0].split('\n').length} lines`));
|
|
}
|
|
} else {
|
|
console.log(chalk.red(' No prompt found!'));
|
|
}
|
|
|
|
console.log();
|
|
|
|
// Show raw debug info
|
|
console.log(chalk.bold.yellow('🔍 Raw Debug Info:'));
|
|
console.log(chalk.gray(' process.argv:'));
|
|
process.argv.forEach((arg, i) => {
|
|
console.log(chalk.gray(` [${i}]: `) + chalk.dim(arg.length > 60 ? arg.substring(0, 60) + '...' : arg));
|
|
});
|
|
|
|
console.log(chalk.bold.cyan('\n ═══════════════════════════════════════════════\n'));
|
|
}
|
|
|
|
/**
|
|
* Show CLI tool status
|
|
*/
|
|
async function statusAction(): Promise<void> {
|
|
console.log(chalk.bold.cyan('\n CLI Tools Status\n'));
|
|
|
|
const status = await getCliToolsStatus();
|
|
|
|
for (const [tool, info] of Object.entries(status)) {
|
|
const statusIcon = info.available ? chalk.green('●') : chalk.red('○');
|
|
const statusText = info.available ? chalk.green('Available') : chalk.red('Not Found');
|
|
|
|
console.log(` ${statusIcon} ${chalk.bold.white(tool.padEnd(10))} ${statusText}`);
|
|
if (info.available && info.path) {
|
|
console.log(chalk.gray(` ${info.path}`));
|
|
}
|
|
}
|
|
|
|
console.log();
|
|
}
|
|
|
|
/**
|
|
* Execute a CLI tool
|
|
* @param {string} prompt - Prompt to execute
|
|
* @param {Object} options - CLI options
|
|
*/
|
|
async function execAction(positionalPrompt: string | undefined, options: CliExecOptions): Promise<void> {
|
|
const { prompt: optionPrompt, file, tool = 'gemini', mode = 'analysis', model, cd, includeDirs, timeout, noStream, resume, id, noNative, cache, injectMode } = options;
|
|
|
|
// Priority: 1. --file, 2. --prompt/-p option, 3. positional argument
|
|
let finalPrompt: string | undefined;
|
|
|
|
if (file) {
|
|
// Read from file
|
|
const { readFileSync, existsSync } = await import('fs');
|
|
const { resolve } = await import('path');
|
|
const filePath = resolve(file);
|
|
if (!existsSync(filePath)) {
|
|
console.error(chalk.red(`Error: File not found: ${filePath}`));
|
|
process.exit(1);
|
|
}
|
|
finalPrompt = readFileSync(filePath, 'utf8').trim();
|
|
if (!finalPrompt) {
|
|
console.error(chalk.red('Error: File is empty'));
|
|
process.exit(1);
|
|
}
|
|
} else if (optionPrompt) {
|
|
// Use --prompt/-p option (preferred for multi-line)
|
|
finalPrompt = optionPrompt;
|
|
} else {
|
|
// Fall back to positional argument
|
|
finalPrompt = positionalPrompt;
|
|
}
|
|
|
|
// Prompt is required unless resuming
|
|
if (!finalPrompt && !resume) {
|
|
console.error(chalk.red('Error: Prompt is required'));
|
|
console.error(chalk.gray('Usage: ccw cli -p "<prompt>" --tool gemini'));
|
|
console.error(chalk.gray(' or: ccw cli -f prompt.txt --tool codex'));
|
|
console.error(chalk.gray(' or: ccw cli --resume --tool gemini'));
|
|
process.exit(1);
|
|
}
|
|
|
|
const prompt_to_use = finalPrompt || '';
|
|
|
|
// Handle cache option: pack @patterns and/or content
|
|
let cacheSessionId: string | undefined;
|
|
let actualPrompt = prompt_to_use;
|
|
|
|
if (cache) {
|
|
const { handler: contextCacheHandler } = await import('../tools/context-cache.js');
|
|
|
|
// Parse cache config from comma-separated string
|
|
// Items starting with @ are patterns, others are text content
|
|
let cacheConfig: CacheConfig = {};
|
|
|
|
if (cache === true) {
|
|
// --cache without value: auto-extract from CONTEXT field
|
|
const contextMatch = prompt_to_use.match(/CONTEXT:\s*([^\n]+)/i);
|
|
if (contextMatch) {
|
|
const contextLine = contextMatch[1];
|
|
const patternMatches = contextLine.matchAll(/@[^\s|]+/g);
|
|
cacheConfig.patterns = Array.from(patternMatches).map(m => m[0]);
|
|
}
|
|
} else if (typeof cache === 'string') {
|
|
// Parse comma-separated items: @patterns and text content
|
|
const items = cache.split(',').map(s => s.trim()).filter(Boolean);
|
|
const patterns: string[] = [];
|
|
const contentParts: string[] = [];
|
|
|
|
for (const item of items) {
|
|
if (item.startsWith('@')) {
|
|
patterns.push(item);
|
|
} else {
|
|
contentParts.push(item);
|
|
}
|
|
}
|
|
|
|
if (patterns.length > 0) {
|
|
cacheConfig.patterns = patterns;
|
|
}
|
|
if (contentParts.length > 0) {
|
|
cacheConfig.content = contentParts.join('\n');
|
|
}
|
|
}
|
|
|
|
// Also extract patterns from CONTEXT if not provided
|
|
if ((!cacheConfig.patterns || cacheConfig.patterns.length === 0) && prompt_to_use) {
|
|
const contextMatch = prompt_to_use.match(/CONTEXT:\s*([^\n]+)/i);
|
|
if (contextMatch) {
|
|
const contextLine = contextMatch[1];
|
|
const patternMatches = contextLine.matchAll(/@[^\s|]+/g);
|
|
cacheConfig.patterns = Array.from(patternMatches).map(m => m[0]);
|
|
}
|
|
}
|
|
|
|
// Pack if we have patterns or content
|
|
if ((cacheConfig.patterns && cacheConfig.patterns.length > 0) || cacheConfig.content) {
|
|
const patternCount = cacheConfig.patterns?.length || 0;
|
|
const hasContent = !!cacheConfig.content;
|
|
console.log(chalk.gray(` Caching: ${patternCount} pattern(s)${hasContent ? ' + text content' : ''}...`));
|
|
|
|
const cacheResult = await contextCacheHandler({
|
|
operation: 'pack',
|
|
patterns: cacheConfig.patterns,
|
|
content: cacheConfig.content,
|
|
cwd: cd || process.cwd(),
|
|
include_dirs: includeDirs ? includeDirs.split(',') : undefined,
|
|
});
|
|
|
|
if (cacheResult.success && cacheResult.result) {
|
|
const packResult = cacheResult.result as { session_id: string; files_packed: number; total_bytes: number };
|
|
cacheSessionId = packResult.session_id;
|
|
console.log(chalk.gray(` Cached: ${packResult.files_packed} files, ${packResult.total_bytes} bytes`));
|
|
console.log(chalk.gray(` Session: ${cacheSessionId}`));
|
|
|
|
// Determine inject mode:
|
|
// --inject-mode explicitly set > tool default (codex=full, others=none)
|
|
const effectiveInjectMode = injectMode ?? (tool === 'codex' ? 'full' : 'none');
|
|
|
|
if (effectiveInjectMode !== 'none' && cacheSessionId) {
|
|
if (effectiveInjectMode === 'full') {
|
|
// Read full cache content
|
|
const readResult = await contextCacheHandler({
|
|
operation: 'read',
|
|
session_id: cacheSessionId,
|
|
offset: 0,
|
|
limit: 1024 * 1024, // 1MB max
|
|
});
|
|
|
|
if (readResult.success && readResult.result) {
|
|
const { content: cachedContent, total_bytes } = readResult.result as { content: string; total_bytes: number };
|
|
console.log(chalk.gray(` Injecting ${total_bytes} bytes (full mode)...`));
|
|
actualPrompt = `=== CACHED CONTEXT (${packResult.files_packed} files) ===\n${cachedContent}\n\n=== USER PROMPT ===\n${prompt_to_use}`;
|
|
}
|
|
} else if (effectiveInjectMode === 'progressive') {
|
|
// Progressive mode: read first page only (64KB default)
|
|
const pageLimit = 65536;
|
|
const readResult = await contextCacheHandler({
|
|
operation: 'read',
|
|
session_id: cacheSessionId,
|
|
offset: 0,
|
|
limit: pageLimit,
|
|
});
|
|
|
|
if (readResult.success && readResult.result) {
|
|
const { content: cachedContent, total_bytes, has_more, next_offset } = readResult.result as {
|
|
content: string; total_bytes: number; has_more: boolean; next_offset: number | null
|
|
};
|
|
console.log(chalk.gray(` Injecting ${cachedContent.length}/${total_bytes} bytes (progressive mode)...`));
|
|
|
|
const moreInfo = has_more
|
|
? `\n[... ${total_bytes - cachedContent.length} more bytes available via: context_cache(operation="read", session_id="${cacheSessionId}", offset=${next_offset}) ...]`
|
|
: '';
|
|
|
|
actualPrompt = `=== CACHED CONTEXT (${packResult.files_packed} files, progressive) ===\n${cachedContent}${moreInfo}\n\n=== USER PROMPT ===\n${prompt_to_use}`;
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log();
|
|
} else {
|
|
console.log(chalk.yellow(` Cache warning: ${cacheResult.error}`));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse resume IDs for merge scenario
|
|
const resumeIds = resume && typeof resume === 'string' ? resume.split(',').map(s => s.trim()).filter(Boolean) : [];
|
|
const isMerge = resumeIds.length > 1;
|
|
|
|
// Show execution mode
|
|
let resumeInfo = '';
|
|
if (isMerge) {
|
|
resumeInfo = ` merging ${resumeIds.length} conversations`;
|
|
} else if (resume) {
|
|
resumeInfo = typeof resume === 'string' ? ` resuming ${resume}` : ' resuming last';
|
|
}
|
|
const nativeMode = noNative ? ' (prompt-concat)' : '';
|
|
const idInfo = id ? ` [${id}]` : '';
|
|
console.log(chalk.cyan(`\n Executing ${tool} (${mode} mode${resumeInfo}${nativeMode})${idInfo}...\n`));
|
|
|
|
// Show merge details
|
|
if (isMerge) {
|
|
console.log(chalk.gray(' Merging conversations:'));
|
|
for (const rid of resumeIds) {
|
|
console.log(chalk.gray(` • ${rid}`));
|
|
}
|
|
console.log();
|
|
}
|
|
|
|
// Notify dashboard: execution started
|
|
notifyDashboard({
|
|
event: 'started',
|
|
tool,
|
|
mode,
|
|
prompt_preview: prompt_to_use.substring(0, 100) + (prompt_to_use.length > 100 ? '...' : ''),
|
|
custom_id: id || null
|
|
});
|
|
|
|
// Streaming output handler
|
|
const onOutput = noStream ? null : (chunk: any) => {
|
|
process.stdout.write(chunk.data);
|
|
};
|
|
|
|
try {
|
|
const result = await cliExecutorTool.execute({
|
|
tool,
|
|
prompt: actualPrompt,
|
|
mode,
|
|
model,
|
|
cd,
|
|
includeDirs,
|
|
timeout: timeout ? parseInt(timeout, 10) : 300000,
|
|
resume,
|
|
id, // custom execution ID
|
|
noNative
|
|
}, onOutput);
|
|
|
|
// If not streaming, print output now
|
|
if (noStream && result.stdout) {
|
|
console.log(result.stdout);
|
|
}
|
|
|
|
// Print summary with execution ID and turn info
|
|
console.log();
|
|
if (result.success) {
|
|
const turnInfo = result.conversation.turn_count > 1
|
|
? ` (turn ${result.conversation.turn_count})`
|
|
: '';
|
|
console.log(chalk.green(` ✓ Completed in ${(result.execution.duration_ms / 1000).toFixed(1)}s${turnInfo}`));
|
|
console.log(chalk.gray(` ID: ${result.execution.id}`));
|
|
if (isMerge && !id) {
|
|
// Merge without custom ID: updated all source conversations
|
|
console.log(chalk.gray(` Updated ${resumeIds.length} conversations: ${resumeIds.join(', ')}`));
|
|
} else if (isMerge && id) {
|
|
// Merge with custom ID: created new merged conversation
|
|
console.log(chalk.gray(` Created merged conversation from ${resumeIds.length} sources`));
|
|
}
|
|
if (result.conversation.turn_count > 1) {
|
|
console.log(chalk.gray(` Total: ${result.conversation.turn_count} turns, ${(result.conversation.total_duration_ms / 1000).toFixed(1)}s`));
|
|
}
|
|
console.log(chalk.dim(` Continue: ccw cli -p "..." --resume ${result.execution.id}`));
|
|
|
|
// Notify dashboard: execution completed
|
|
notifyDashboard({
|
|
event: 'completed',
|
|
tool,
|
|
mode,
|
|
execution_id: result.execution.id,
|
|
success: true,
|
|
duration_ms: result.execution.duration_ms,
|
|
turn_count: result.conversation.turn_count
|
|
});
|
|
|
|
// Ensure clean exit after successful execution
|
|
process.exit(0);
|
|
} else {
|
|
console.log(chalk.red(` ✗ Failed (${result.execution.status})`));
|
|
console.log(chalk.gray(` ID: ${result.execution.id}`));
|
|
if (result.stderr) {
|
|
console.error(chalk.red(result.stderr));
|
|
}
|
|
|
|
// Notify dashboard: execution failccw cli -p
|
|
notifyDashboard({
|
|
event: 'completed',
|
|
tool,
|
|
mode,
|
|
execution_id: result.execution.id,
|
|
success: false,
|
|
status: result.execution.status,
|
|
duration_ms: result.execution.duration_ms
|
|
});
|
|
|
|
process.exit(1);
|
|
}
|
|
} catch (error) {
|
|
const err = error as Error;
|
|
console.error(chalk.red(` Error: ${err.message}`));
|
|
|
|
// Notify dashboard: execution error
|
|
notifyDashboard({
|
|
event: 'error',
|
|
tool,
|
|
mode,
|
|
error: err.message
|
|
});
|
|
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show execution history
|
|
* @param {Object} options - CLI options
|
|
*/
|
|
async function historyAction(options: HistoryOptions): Promise<void> {
|
|
const { limit = '20', tool, status } = options;
|
|
|
|
console.log(chalk.bold.cyan('\n CLI Execution History\n'));
|
|
|
|
const history = await getExecutionHistoryAsync(process.cwd(), { limit: parseInt(limit, 10), tool, status });
|
|
|
|
if (history.executions.length === 0) {
|
|
console.log(chalk.gray(' No executions found.\n'));
|
|
return;
|
|
}
|
|
|
|
console.log(chalk.gray(` Total executions: ${history.total}\n`));
|
|
|
|
for (const exec of history.executions) {
|
|
const statusIcon = exec.status === 'success' ? chalk.green('●') :
|
|
exec.status === 'timeout' ? chalk.yellow('●') : chalk.red('●');
|
|
const duration = exec.duration_ms >= 1000
|
|
? `${(exec.duration_ms / 1000).toFixed(1)}s`
|
|
: `${exec.duration_ms}ms`;
|
|
|
|
const timeAgo = getTimeAgo(new Date(exec.updated_at || exec.timestamp));
|
|
const turnInfo = exec.turn_count && exec.turn_count > 1 ? chalk.cyan(` [${exec.turn_count} turns]`) : '';
|
|
|
|
console.log(` ${statusIcon} ${chalk.bold.white(exec.tool.padEnd(8))} ${chalk.gray(timeAgo.padEnd(12))} ${chalk.gray(duration.padEnd(8))}${turnInfo}`);
|
|
console.log(chalk.gray(` ${exec.prompt_preview}`));
|
|
console.log(chalk.dim(` ID: ${exec.id}`));
|
|
console.log();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show conversation detail with all turns
|
|
* @param {string} conversationId - Conversation ID
|
|
*/
|
|
async function detailAction(conversationId: string | undefined): Promise<void> {
|
|
if (!conversationId) {
|
|
console.error(chalk.red('Error: Conversation ID is required'));
|
|
console.error(chalk.gray('Usage: ccw cli detail <conversation-id>'));
|
|
process.exit(1);
|
|
}
|
|
|
|
const conversation = getConversationDetail(process.cwd(), conversationId);
|
|
|
|
if (!conversation) {
|
|
console.error(chalk.red(`Error: Conversation not found: ${conversationId}`));
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(chalk.bold.cyan('\n Conversation Detail\n'));
|
|
console.log(` ${chalk.gray('ID:')} ${conversation.id}`);
|
|
console.log(` ${chalk.gray('Tool:')} ${conversation.tool}`);
|
|
console.log(` ${chalk.gray('Model:')} ${conversation.model}`);
|
|
console.log(` ${chalk.gray('Mode:')} ${conversation.mode}`);
|
|
console.log(` ${chalk.gray('Status:')} ${conversation.latest_status}`);
|
|
console.log(` ${chalk.gray('Turns:')} ${conversation.turn_count}`);
|
|
console.log(` ${chalk.gray('Duration:')} ${(conversation.total_duration_ms / 1000).toFixed(1)}s total`);
|
|
console.log(` ${chalk.gray('Created:')} ${conversation.created_at}`);
|
|
if (conversation.turn_count > 1) {
|
|
console.log(` ${chalk.gray('Updated:')} ${conversation.updated_at}`);
|
|
}
|
|
|
|
// Show all turns
|
|
for (const turn of conversation.turns) {
|
|
console.log(chalk.bold.cyan(`\n ═══ Turn ${turn.turn} ═══`));
|
|
console.log(chalk.gray(` ${turn.timestamp} | ${turn.status} | ${(turn.duration_ms / 1000).toFixed(1)}s`));
|
|
|
|
console.log(chalk.bold.white('\n Prompt:'));
|
|
console.log(chalk.gray(' ' + turn.prompt.split('\n').join('\n ')));
|
|
|
|
if (turn.output.stdout) {
|
|
console.log(chalk.bold.white('\n Output:'));
|
|
console.log(turn.output.stdout);
|
|
}
|
|
|
|
if (turn.output.stderr) {
|
|
console.log(chalk.bold.red('\n Errors:'));
|
|
console.log(turn.output.stderr);
|
|
}
|
|
|
|
if (turn.output.truncated) {
|
|
console.log(chalk.yellow('\n Note: Output was truncated due to size.'));
|
|
}
|
|
}
|
|
|
|
console.log(chalk.dim(`\n Continue: ccw cli -p "..." --resume ${conversation.id}`));
|
|
console.log();
|
|
}
|
|
|
|
/**
|
|
* Get human-readable time ago string
|
|
* @param {Date} date
|
|
* @returns {string}
|
|
*/
|
|
function getTimeAgo(date: Date): string {
|
|
const seconds = Math.floor((new Date().getTime() - date.getTime()) / 1000);
|
|
|
|
if (seconds < 60) return 'just now';
|
|
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
|
|
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
|
|
if (seconds < 604800) return `${Math.floor(seconds / 86400)}d ago`;
|
|
return date.toLocaleDateString();
|
|
}
|
|
|
|
/**ccw cli -p
|
|
* CLI command entry point
|
|
* @param {string} subcommand - Subcommand (status, exec, history, detail)
|
|
* @param {string[]} args - Arguments array
|
|
* @param {Object} options - CLI options
|
|
*/
|
|
export async function cliCommand(
|
|
subcommand: string,
|
|
args: string | string[],
|
|
options: CliExecOptions | HistoryOptions
|
|
): Promise<void> {
|
|
const argsArray = Array.isArray(args) ? args : (args ? [args] : []);
|
|
|
|
switch (subcommand) {
|
|
case 'status':
|
|
await statusAction();
|
|
break;
|
|
|
|
case 'history':
|
|
await historyAction(options as HistoryOptions);
|
|
break;
|
|
|
|
case 'detail':
|
|
await detailAction(argsArray[0]);
|
|
break;
|
|
|
|
case 'storage':
|
|
await storageAction(argsArray[0], options as unknown as StorageOptions);
|
|
break;
|
|
|
|
case 'test-parse':
|
|
// Test endpoint to debug multi-line prompt parsing
|
|
testParseAction(argsArray, options as CliExecOptions);
|
|
break;
|
|
|
|
default: {
|
|
const execOptions = options as CliExecOptions;
|
|
// Auto-exec if: has -p/--prompt, has -f/--file, has --resume, or subcommand looks like a prompt
|
|
const hasPromptOption = !!execOptions.prompt;
|
|
const hasFileOption = !!execOptions.file;
|
|
const hasResume = execOptions.resume !== undefined;
|
|
const subcommandIsPrompt = subcommand && !subcommand.startsWith('-');
|
|
|
|
if (hasPromptOption || hasFileOption || hasResume || subcommandIsPrompt) {
|
|
// Treat as exec: use subcommand as positional prompt if no -p/-f option
|
|
const positionalPrompt = subcommandIsPrompt ? subcommand : undefined;
|
|
await execAction(positionalPrompt, execOptions);
|
|
} else {
|
|
// Show help
|
|
console.log(chalk.bold.cyan('\n CCW CLI Tool Executor\n'));
|
|
console.log(' Unified interface for Gemini, Qwen, and Codex CLI tools.\n');
|
|
console.log(' Usage:');
|
|
console.log(chalk.gray(' ccw cli -p "<prompt>" --tool <tool> Execute with prompt'));
|
|
console.log(chalk.gray(' ccw cli -f prompt.txt --tool <tool> Execute from file'));
|
|
console.log();
|
|
console.log(' Subcommands:');
|
|
console.log(chalk.gray(' status Check CLI tools availability'));
|
|
console.log(chalk.gray(' storage [cmd] Manage CCW storage (info/clean/config)'));
|
|
console.log(chalk.gray(' history Show execution history'));
|
|
console.log(chalk.gray(' detail <id> Show execution detail'));
|
|
console.log(chalk.gray(' test-parse [args] Debug CLI argument parsing'));
|
|
console.log();
|
|
console.log(' Options:');
|
|
console.log(chalk.gray(' -p, --prompt <text> Prompt text'));
|
|
console.log(chalk.gray(' -f, --file <file> Read prompt from file'));
|
|
console.log(chalk.gray(' --tool <tool> Tool: gemini, qwen, codex (default: gemini)'));
|
|
console.log(chalk.gray(' --mode <mode> Mode: analysis, write, auto (default: analysis)'));
|
|
console.log(chalk.gray(' --model <model> Model override'));
|
|
console.log(chalk.gray(' --cd <path> Working directory'));
|
|
console.log(chalk.gray(' --includeDirs <dirs> Additional directories'));
|
|
console.log(chalk.gray(' --timeout <ms> Timeout (default: 0=disabled)'));
|
|
console.log(chalk.gray(' --resume [id] Resume previous session'));
|
|
console.log(chalk.gray(' --cache <items> Cache: comma-separated @patterns and text'));
|
|
console.log(chalk.gray(' --inject-mode <m> Inject mode: none, full, progressive'));
|
|
console.log();
|
|
console.log(' Cache format:');
|
|
console.log(chalk.gray(' --cache "@src/**/*.ts,@CLAUDE.md" # @patterns to pack'));
|
|
console.log(chalk.gray(' --cache "@src/**/*,extra context" # patterns + text content'));
|
|
console.log(chalk.gray(' --cache # auto from CONTEXT field'));
|
|
console.log();
|
|
console.log(' Inject modes:');
|
|
console.log(chalk.gray(' none: cache only, no injection (default for gemini/qwen)'));
|
|
console.log(chalk.gray(' full: inject all cached content (default for codex)'));
|
|
console.log(chalk.gray(' progressive: inject first 64KB with MCP continuation hint'));
|
|
console.log();
|
|
console.log(' Examples:');
|
|
console.log(chalk.gray(' ccw cli -p "Analyze auth module" --tool gemini'));
|
|
console.log(chalk.gray(' ccw cli -f prompt.txt --tool codex --mode write'));
|
|
console.log(chalk.gray(' ccw cli -p "$(cat template.md)" --tool gemini'));
|
|
console.log(chalk.gray(' ccw cli --resume --tool gemini'));
|
|
console.log(chalk.gray(' ccw cli -p "..." --cache "@src/**/*.ts" --tool codex'));
|
|
console.log(chalk.gray(' ccw cli -p "..." --cache "@src/**/*" --inject-mode progressive --tool gemini'));
|
|
console.log();
|
|
}
|
|
}
|
|
}
|
|
}
|