claude-code-workflow 6.3.2 → 6.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/.claude/CLAUDE.md +9 -1
  2. package/.claude/commands/workflow/lite-plan.md +1 -1
  3. package/.claude/workflows/cli-tools-usage.md +515 -516
  4. package/ccw/dist/cli.d.ts.map +1 -1
  5. package/ccw/dist/cli.js +6 -1
  6. package/ccw/dist/cli.js.map +1 -1
  7. package/ccw/dist/commands/cli.d.ts +1 -1
  8. package/ccw/dist/commands/cli.d.ts.map +1 -1
  9. package/ccw/dist/commands/cli.js +71 -7
  10. package/ccw/dist/commands/cli.js.map +1 -1
  11. package/ccw/dist/tools/cli-executor.d.ts.map +1 -1
  12. package/ccw/dist/tools/cli-executor.js +19 -7
  13. package/ccw/dist/tools/cli-executor.js.map +1 -1
  14. package/ccw/dist/tools/cli-history-store.d.ts +33 -0
  15. package/ccw/dist/tools/cli-history-store.d.ts.map +1 -1
  16. package/ccw/dist/tools/cli-history-store.js +89 -5
  17. package/ccw/dist/tools/cli-history-store.js.map +1 -1
  18. package/ccw/src/cli.ts +263 -258
  19. package/ccw/src/commands/cli.ts +967 -884
  20. package/ccw/src/tools/cli-executor.ts +20 -7
  21. package/ccw/src/tools/cli-history-store.ts +125 -5
  22. package/codex-lens/src/codexlens/__pycache__/config.cpython-313.pyc +0 -0
  23. package/codex-lens/src/codexlens/config.py +3 -0
  24. package/codex-lens/src/codexlens/search/__pycache__/chain_search.cpython-313.pyc +0 -0
  25. package/codex-lens/src/codexlens/search/__pycache__/hybrid_search.cpython-313.pyc +0 -0
  26. package/codex-lens/src/codexlens/search/__pycache__/ranking.cpython-313.pyc +0 -0
  27. package/codex-lens/src/codexlens/search/chain_search.py +71 -1
  28. package/codex-lens/src/codexlens/search/ranking.py +274 -274
  29. package/codex-lens/src/codexlens/semantic/__pycache__/chunker.cpython-313.pyc +0 -0
  30. package/codex-lens/src/codexlens/storage/__pycache__/dir_index.cpython-313.pyc +0 -0
  31. package/codex-lens/src/codexlens/storage/__pycache__/global_index.cpython-313.pyc +0 -0
  32. package/codex-lens/src/codexlens/storage/__pycache__/index_tree.cpython-313.pyc +0 -0
  33. package/codex-lens/src/codexlens/storage/dir_index.py +1888 -1850
  34. package/codex-lens/src/codexlens/storage/global_index.py +365 -0
  35. package/codex-lens/src/codexlens/storage/index_tree.py +83 -10
  36. package/package.json +1 -1
@@ -1,884 +1,967 @@
1
- /**
2
- * CLI Command - Unified CLI tool executor command
3
- * Provides interface for executing Gemini, Qwen, and Codex
4
- */
5
-
6
- import chalk from 'chalk';
7
- import http from 'http';
8
- import {
9
- cliExecutorTool,
10
- getCliToolsStatus,
11
- getExecutionHistory,
12
- getExecutionHistoryAsync,
13
- getExecutionDetail,
14
- getConversationDetail
15
- } from '../tools/cli-executor.js';
16
- import {
17
- getStorageStats,
18
- getStorageConfig,
19
- cleanProjectStorage,
20
- cleanAllStorage,
21
- formatBytes,
22
- formatTimeAgo,
23
- resolveProjectId,
24
- projectExists,
25
- getStorageLocationInstructions
26
- } from '../tools/storage-manager.js';
27
-
28
- // Dashboard notification settings
29
- const DASHBOARD_PORT = process.env.CCW_PORT || 3456;
30
-
31
- /**
32
- * Notify dashboard of CLI execution events (fire and forget)
33
- */
34
- function notifyDashboard(data: Record<string, unknown>): void {
35
- const payload = JSON.stringify({
36
- type: 'cli_execution',
37
- ...data,
38
- timestamp: new Date().toISOString()
39
- });
40
-
41
- const req = http.request({
42
- hostname: 'localhost',
43
- port: Number(DASHBOARD_PORT),
44
- path: '/api/hook',
45
- method: 'POST',
46
- timeout: 2000, // 2 second timeout to prevent hanging
47
- headers: {
48
- 'Content-Type': 'application/json',
49
- 'Content-Length': Buffer.byteLength(payload)
50
- }
51
- });
52
-
53
- // Fire and forget - don't block process exit
54
- req.on('socket', (socket) => {
55
- socket.unref(); // Allow process to exit even if socket is open
56
- });
57
- req.on('error', (err) => {
58
- if (process.env.DEBUG) console.error('[Dashboard] CLI notification failed:', err.message);
59
- });
60
- req.on('timeout', () => {
61
- req.destroy();
62
- if (process.env.DEBUG) console.error('[Dashboard] CLI notification timed out');
63
- });
64
- req.write(payload);
65
- req.end();
66
- }
67
-
68
- interface CliExecOptions {
69
- prompt?: string; // Prompt via --prompt/-p option (preferred for multi-line)
70
- file?: string; // Read prompt from file
71
- tool?: string;
72
- mode?: string;
73
- model?: string;
74
- cd?: string;
75
- includeDirs?: string;
76
- timeout?: string;
77
- noStream?: boolean;
78
- resume?: string | boolean; // true = last, string = execution ID, comma-separated for merge
79
- id?: string; // Custom execution ID (e.g., IMPL-001-step1)
80
- noNative?: boolean; // Force prompt concatenation instead of native resume
81
- cache?: string | boolean; // Cache: true = auto from CONTEXT, string = comma-separated patterns/content
82
- injectMode?: 'none' | 'full' | 'progressive'; // Inject mode for cached content
83
- }
84
-
85
- /** Cache configuration parsed from --cache */
86
- interface CacheConfig {
87
- patterns?: string[]; // @patterns to pack (items starting with @)
88
- content?: string; // Additional text content (items not starting with @)
89
- }
90
-
91
- interface HistoryOptions {
92
- limit?: string;
93
- tool?: string;
94
- status?: string;
95
- }
96
-
97
- interface StorageOptions {
98
- all?: boolean;
99
- project?: string;
100
- cliHistory?: boolean;
101
- memory?: boolean;
102
- storageCache?: boolean;
103
- config?: boolean;
104
- force?: boolean;
105
- }
106
-
107
- /**
108
- * Show storage information and management options
109
- */
110
- async function storageAction(subAction: string | undefined, options: StorageOptions): Promise<void> {
111
- switch (subAction) {
112
- case 'info':
113
- case undefined:
114
- await showStorageInfo();
115
- break;
116
- case 'clean':
117
- await cleanStorage(options);
118
- break;
119
- case 'config':
120
- showStorageConfig();
121
- break;
122
- default:
123
- showStorageHelp();
124
- }
125
- }
126
-
127
- /**
128
- * Show storage information
129
- */
130
- async function showStorageInfo(): Promise<void> {
131
- console.log(chalk.bold.cyan('\n CCW Storage Information\n'));
132
-
133
- const config = getStorageConfig();
134
- const stats = getStorageStats();
135
-
136
- // Configuration
137
- console.log(chalk.bold.white(' Location:'));
138
- console.log(` ${chalk.cyan(stats.rootPath)}`);
139
- if (config.isCustom) {
140
- console.log(chalk.gray(` (Custom: CCW_DATA_DIR=${config.envVar})`));
141
- }
142
- console.log();
143
-
144
- // Summary
145
- console.log(chalk.bold.white(' Summary:'));
146
- console.log(` Total Size: ${chalk.yellow(formatBytes(stats.totalSize))}`);
147
- console.log(` Projects: ${chalk.yellow(stats.projectCount.toString())}`);
148
- console.log(` Global DB: ${stats.globalDb.exists ? chalk.green(formatBytes(stats.globalDb.size)) : chalk.gray('Not created')}`);
149
- console.log();
150
-
151
- // Projects breakdown
152
- if (stats.projects.length > 0) {
153
- console.log(chalk.bold.white(' Projects:'));
154
- console.log(chalk.gray(' ID Size History Last Used'));
155
- console.log(chalk.gray(' ─────────────────────────────────────────────────────'));
156
-
157
- for (const project of stats.projects) {
158
- const historyInfo = project.cliHistory.recordCount !== undefined
159
- ? `${project.cliHistory.recordCount} records`
160
- : (project.cliHistory.exists ? 'Yes' : '-');
161
-
162
- console.log(
163
- ` ${chalk.dim(project.projectId)} ` +
164
- `${formatBytes(project.totalSize).padStart(8)} ` +
165
- `${historyInfo.padStart(10)} ` +
166
- `${chalk.gray(formatTimeAgo(project.lastModified))}`
167
- );
168
- }
169
- console.log();
170
- }
171
-
172
- // Usage tips
173
- console.log(chalk.gray(' Commands:'));
174
- console.log(chalk.gray(' ccw cli storage clean Clean all storage'));
175
- console.log(chalk.gray(' ccw cli storage clean --project <path> Clean specific project'));
176
- console.log(chalk.gray(' ccw cli storage config Show location config'));
177
- console.log();
178
- }
179
-
180
- /**
181
- * Clean storage
182
- */
183
- async function cleanStorage(options: StorageOptions): Promise<void> {
184
- const { all, project, force, cliHistory, memory, storageCache, config } = options;
185
-
186
- // Determine what to clean
187
- const cleanTypes = {
188
- cliHistory: cliHistory || (!cliHistory && !memory && !storageCache && !config),
189
- memory: memory || (!cliHistory && !memory && !storageCache && !config),
190
- cache: storageCache || (!cliHistory && !memory && !storageCache && !config),
191
- config: config || false, // Config requires explicit flag
192
- all: !cliHistory && !memory && !storageCache && !config
193
- };
194
-
195
- if (project) {
196
- // Clean specific project
197
- const projectId = resolveProjectId(project);
198
-
199
- if (!projectExists(projectId)) {
200
- console.log(chalk.yellow(`\n No storage found for project: ${project}`));
201
- console.log(chalk.gray(` (Project ID: ${projectId})\n`));
202
- return;
203
- }
204
-
205
- if (!force) {
206
- console.log(chalk.bold.yellow('\n Warning: This will delete storage for project:'));
207
- console.log(` Path: ${project}`);
208
- console.log(` ID: ${projectId}`);
209
- console.log(chalk.gray('\n Use --force to confirm deletion.\n'));
210
- return;
211
- }
212
-
213
- console.log(chalk.bold.cyan('\n Cleaning project storage...\n'));
214
- const result = cleanProjectStorage(projectId, cleanTypes);
215
-
216
- if (result.success) {
217
- console.log(chalk.green(` ✓ Cleaned ${formatBytes(result.freedBytes)}`));
218
- } else {
219
- console.log(chalk.red(' ✗ Cleanup completed with errors:'));
220
- for (const err of result.errors) {
221
- console.log(chalk.red(` - ${err}`));
222
- }
223
- }
224
- } else {
225
- // Clean all storage
226
- const stats = getStorageStats();
227
-
228
- if (stats.projectCount === 0) {
229
- console.log(chalk.yellow('\n No storage to clean.\n'));
230
- return;
231
- }
232
-
233
- if (!force) {
234
- console.log(chalk.bold.yellow('\n Warning: This will delete ALL CCW storage:'));
235
- console.log(` Location: ${stats.rootPath}`);
236
- console.log(` Projects: ${stats.projectCount}`);
237
- console.log(` Size: ${formatBytes(stats.totalSize)}`);
238
- console.log(chalk.gray('\n Use --force to confirm deletion.\n'));
239
- return;
240
- }
241
-
242
- console.log(chalk.bold.cyan('\n Cleaning all storage...\n'));
243
- const result = cleanAllStorage(cleanTypes);
244
-
245
- if (result.success) {
246
- console.log(chalk.green(` ✓ Cleaned ${result.projectsCleaned} projects, freed ${formatBytes(result.freedBytes)}`));
247
- } else {
248
- console.log(chalk.yellow(` ⚠ Cleaned ${result.projectsCleaned} projects with some errors:`));
249
- for (const err of result.errors) {
250
- console.log(chalk.red(` - ${err}`));
251
- }
252
- }
253
- }
254
- console.log();
255
- }
256
-
257
- /**
258
- * Show storage configuration
259
- */
260
- function showStorageConfig(): void {
261
- console.log(getStorageLocationInstructions());
262
- }
263
-
264
- /**
265
- * Show storage help
266
- */
267
- function showStorageHelp(): void {
268
- console.log(chalk.bold.cyan('\n CCW Storage Management\n'));
269
- console.log(' Subcommands:');
270
- console.log(chalk.gray(' info Show storage information (default)'));
271
- console.log(chalk.gray(' clean Clean storage'));
272
- console.log(chalk.gray(' config Show configuration instructions'));
273
- console.log();
274
- console.log(' Clean Options:');
275
- console.log(chalk.gray(' --project <path> Clean specific project storage'));
276
- console.log(chalk.gray(' --force Confirm deletion'));
277
- console.log(chalk.gray(' --cli-history Clean only CLI history'));
278
- console.log(chalk.gray(' --memory Clean only memory store'));
279
- console.log(chalk.gray(' --cache Clean only cache'));
280
- console.log(chalk.gray(' --config Clean config (requires explicit flag)'));
281
- console.log();
282
- console.log(' Examples:');
283
- console.log(chalk.gray(' ccw cli storage # Show storage info'));
284
- console.log(chalk.gray(' ccw cli storage clean --force # Clean all storage'));
285
- console.log(chalk.gray(' ccw cli storage clean --project . --force # Clean current project'));
286
- console.log(chalk.gray(' ccw cli storage config # Show config instructions'));
287
- console.log();
288
- }
289
-
290
- /**
291
- * Test endpoint for debugging multi-line prompt parsing
292
- * Shows exactly how Commander.js parsed the arguments
293
- */
294
- function testParseAction(args: string[], options: CliExecOptions): void {
295
- console.log(chalk.bold.cyan('\n ═══════════════════════════════════════════════'));
296
- console.log(chalk.bold.cyan(' │ CLI PARSE TEST ENDPOINT │'));
297
- console.log(chalk.bold.cyan(' ═══════════════════════════════════════════════\n'));
298
-
299
- // Show args array parsing
300
- console.log(chalk.bold.yellow('📦 Positional Arguments (args[]):'));
301
- console.log(chalk.gray(' Length: ') + chalk.white(args.length));
302
- if (args.length === 0) {
303
- console.log(chalk.gray(' (empty)'));
304
- } else {
305
- args.forEach((arg, i) => {
306
- console.log(chalk.gray(` [${i}]: `) + chalk.green(`"${arg}"`));
307
- // Show if multiline
308
- if (arg.includes('\n')) {
309
- console.log(chalk.yellow(` ↳ Contains ${arg.split('\n').length} lines`));
310
- }
311
- });
312
- }
313
-
314
- console.log();
315
-
316
- // Show options parsing
317
- console.log(chalk.bold.yellow('⚙️ Options:'));
318
- const optionEntries = Object.entries(options).filter(([_, v]) => v !== undefined);
319
- if (optionEntries.length === 0) {
320
- console.log(chalk.gray(' (none)'));
321
- } else {
322
- optionEntries.forEach(([key, value]) => {
323
- const displayValue = typeof value === 'string' && value.includes('\n')
324
- ? `"${value.substring(0, 50)}..." (${value.split('\n').length} lines)`
325
- : JSON.stringify(value);
326
- console.log(chalk.gray(` --${key}: `) + chalk.cyan(displayValue));
327
- });
328
- }
329
-
330
- console.log();
331
-
332
- // Show what would be used as prompt
333
- console.log(chalk.bold.yellow('🎯 Final Prompt Resolution:'));
334
- const { prompt: optionPrompt, file } = options;
335
-
336
- if (file) {
337
- console.log(chalk.gray(' Source: ') + chalk.magenta('--file/-f option'));
338
- console.log(chalk.gray(' File: ') + chalk.cyan(file));
339
- } else if (optionPrompt) {
340
- console.log(chalk.gray(' Source: ') + chalk.magenta('--prompt/-p option'));
341
- console.log(chalk.gray(' Value: ') + chalk.green(`"${optionPrompt.substring(0, 100)}${optionPrompt.length > 100 ? '...' : ''}"`));
342
- if (optionPrompt.includes('\n')) {
343
- console.log(chalk.yellow(` ↳ Multiline: ${optionPrompt.split('\n').length} lines`));
344
- }
345
- } else if (args[0]) {
346
- console.log(chalk.gray(' Source: ') + chalk.magenta('positional argument (args[0])'));
347
- console.log(chalk.gray(' Value: ') + chalk.green(`"${args[0].substring(0, 100)}${args[0].length > 100 ? '...' : ''}"`));
348
- if (args[0].includes('\n')) {
349
- console.log(chalk.yellow(` ↳ Multiline: ${args[0].split('\n').length} lines`));
350
- }
351
- } else {
352
- console.log(chalk.red(' No prompt found!'));
353
- }
354
-
355
- console.log();
356
-
357
- // Show raw debug info
358
- console.log(chalk.bold.yellow('🔍 Raw Debug Info:'));
359
- console.log(chalk.gray(' process.argv:'));
360
- process.argv.forEach((arg, i) => {
361
- console.log(chalk.gray(` [${i}]: `) + chalk.dim(arg.length > 60 ? arg.substring(0, 60) + '...' : arg));
362
- });
363
-
364
- console.log(chalk.bold.cyan('\n ═══════════════════════════════════════════════\n'));
365
- }
366
-
367
- /**
368
- * Show CLI tool status
369
- */
370
- async function statusAction(): Promise<void> {
371
- console.log(chalk.bold.cyan('\n CLI Tools Status\n'));
372
-
373
- const status = await getCliToolsStatus();
374
-
375
- for (const [tool, info] of Object.entries(status)) {
376
- const statusIcon = info.available ? chalk.green('●') : chalk.red('○');
377
- const statusText = info.available ? chalk.green('Available') : chalk.red('Not Found');
378
-
379
- console.log(` ${statusIcon} ${chalk.bold.white(tool.padEnd(10))} ${statusText}`);
380
- if (info.available && info.path) {
381
- console.log(chalk.gray(` ${info.path}`));
382
- }
383
- }
384
-
385
- console.log();
386
- }
387
-
388
- /**
389
- * Execute a CLI tool
390
- * @param {string} prompt - Prompt to execute
391
- * @param {Object} options - CLI options
392
- */
393
- async function execAction(positionalPrompt: string | undefined, options: CliExecOptions): Promise<void> {
394
- const { prompt: optionPrompt, file, tool = 'gemini', mode = 'analysis', model, cd, includeDirs, timeout, noStream, resume, id, noNative, cache, injectMode } = options;
395
-
396
- // Priority: 1. --file, 2. --prompt/-p option, 3. positional argument
397
- let finalPrompt: string | undefined;
398
-
399
- if (file) {
400
- // Read from file
401
- const { readFileSync, existsSync } = await import('fs');
402
- const { resolve } = await import('path');
403
- const filePath = resolve(file);
404
- if (!existsSync(filePath)) {
405
- console.error(chalk.red(`Error: File not found: ${filePath}`));
406
- process.exit(1);
407
- }
408
- finalPrompt = readFileSync(filePath, 'utf8').trim();
409
- if (!finalPrompt) {
410
- console.error(chalk.red('Error: File is empty'));
411
- process.exit(1);
412
- }
413
- } else if (optionPrompt) {
414
- // Use --prompt/-p option (preferred for multi-line)
415
- finalPrompt = optionPrompt;
416
- } else {
417
- // Fall back to positional argument
418
- finalPrompt = positionalPrompt;
419
- }
420
-
421
- // Prompt is required unless resuming
422
- if (!finalPrompt && !resume) {
423
- console.error(chalk.red('Error: Prompt is required'));
424
- console.error(chalk.gray('Usage: ccw cli -p "<prompt>" --tool gemini'));
425
- console.error(chalk.gray(' or: ccw cli -f prompt.txt --tool codex'));
426
- console.error(chalk.gray(' or: ccw cli --resume --tool gemini'));
427
- process.exit(1);
428
- }
429
-
430
- const prompt_to_use = finalPrompt || '';
431
-
432
- // Handle cache option: pack @patterns and/or content
433
- let cacheSessionId: string | undefined;
434
- let actualPrompt = prompt_to_use;
435
-
436
- if (cache) {
437
- const { handler: contextCacheHandler } = await import('../tools/context-cache.js');
438
-
439
- // Parse cache config from comma-separated string
440
- // Items starting with @ are patterns, others are text content
441
- let cacheConfig: CacheConfig = {};
442
-
443
- if (cache === true) {
444
- // --cache without value: auto-extract from CONTEXT field
445
- const contextMatch = prompt_to_use.match(/CONTEXT:\s*([^\n]+)/i);
446
- if (contextMatch) {
447
- const contextLine = contextMatch[1];
448
- const patternMatches = contextLine.matchAll(/@[^\s|]+/g);
449
- cacheConfig.patterns = Array.from(patternMatches).map(m => m[0]);
450
- }
451
- } else if (typeof cache === 'string') {
452
- // Parse comma-separated items: @patterns and text content
453
- const items = cache.split(',').map(s => s.trim()).filter(Boolean);
454
- const patterns: string[] = [];
455
- const contentParts: string[] = [];
456
-
457
- for (const item of items) {
458
- if (item.startsWith('@')) {
459
- patterns.push(item);
460
- } else {
461
- contentParts.push(item);
462
- }
463
- }
464
-
465
- if (patterns.length > 0) {
466
- cacheConfig.patterns = patterns;
467
- }
468
- if (contentParts.length > 0) {
469
- cacheConfig.content = contentParts.join('\n');
470
- }
471
- }
472
-
473
- // Also extract patterns from CONTEXT if not provided
474
- if ((!cacheConfig.patterns || cacheConfig.patterns.length === 0) && prompt_to_use) {
475
- const contextMatch = prompt_to_use.match(/CONTEXT:\s*([^\n]+)/i);
476
- if (contextMatch) {
477
- const contextLine = contextMatch[1];
478
- const patternMatches = contextLine.matchAll(/@[^\s|]+/g);
479
- cacheConfig.patterns = Array.from(patternMatches).map(m => m[0]);
480
- }
481
- }
482
-
483
- // Pack if we have patterns or content
484
- if ((cacheConfig.patterns && cacheConfig.patterns.length > 0) || cacheConfig.content) {
485
- const patternCount = cacheConfig.patterns?.length || 0;
486
- const hasContent = !!cacheConfig.content;
487
- console.log(chalk.gray(` Caching: ${patternCount} pattern(s)${hasContent ? ' + text content' : ''}...`));
488
-
489
- const cacheResult = await contextCacheHandler({
490
- operation: 'pack',
491
- patterns: cacheConfig.patterns,
492
- content: cacheConfig.content,
493
- cwd: cd || process.cwd(),
494
- include_dirs: includeDirs ? includeDirs.split(',') : undefined,
495
- });
496
-
497
- if (cacheResult.success && cacheResult.result) {
498
- const packResult = cacheResult.result as { session_id: string; files_packed: number; total_bytes: number };
499
- cacheSessionId = packResult.session_id;
500
- console.log(chalk.gray(` Cached: ${packResult.files_packed} files, ${packResult.total_bytes} bytes`));
501
- console.log(chalk.gray(` Session: ${cacheSessionId}`));
502
-
503
- // Determine inject mode:
504
- // --inject-mode explicitly set > tool default (codex=full, others=none)
505
- const effectiveInjectMode = injectMode ?? (tool === 'codex' ? 'full' : 'none');
506
-
507
- if (effectiveInjectMode !== 'none' && cacheSessionId) {
508
- if (effectiveInjectMode === 'full') {
509
- // Read full cache content
510
- const readResult = await contextCacheHandler({
511
- operation: 'read',
512
- session_id: cacheSessionId,
513
- offset: 0,
514
- limit: 1024 * 1024, // 1MB max
515
- });
516
-
517
- if (readResult.success && readResult.result) {
518
- const { content: cachedContent, total_bytes } = readResult.result as { content: string; total_bytes: number };
519
- console.log(chalk.gray(` Injecting ${total_bytes} bytes (full mode)...`));
520
- actualPrompt = `=== CACHED CONTEXT (${packResult.files_packed} files) ===\n${cachedContent}\n\n=== USER PROMPT ===\n${prompt_to_use}`;
521
- }
522
- } else if (effectiveInjectMode === 'progressive') {
523
- // Progressive mode: read first page only (64KB default)
524
- const pageLimit = 65536;
525
- const readResult = await contextCacheHandler({
526
- operation: 'read',
527
- session_id: cacheSessionId,
528
- offset: 0,
529
- limit: pageLimit,
530
- });
531
-
532
- if (readResult.success && readResult.result) {
533
- const { content: cachedContent, total_bytes, has_more, next_offset } = readResult.result as {
534
- content: string; total_bytes: number; has_more: boolean; next_offset: number | null
535
- };
536
- console.log(chalk.gray(` Injecting ${cachedContent.length}/${total_bytes} bytes (progressive mode)...`));
537
-
538
- const moreInfo = has_more
539
- ? `\n[... ${total_bytes - cachedContent.length} more bytes available via: context_cache(operation="read", session_id="${cacheSessionId}", offset=${next_offset}) ...]`
540
- : '';
541
-
542
- actualPrompt = `=== CACHED CONTEXT (${packResult.files_packed} files, progressive) ===\n${cachedContent}${moreInfo}\n\n=== USER PROMPT ===\n${prompt_to_use}`;
543
- }
544
- }
545
- }
546
-
547
- console.log();
548
- } else {
549
- console.log(chalk.yellow(` Cache warning: ${cacheResult.error}`));
550
- }
551
- }
552
- }
553
-
554
- // Parse resume IDs for merge scenario
555
- const resumeIds = resume && typeof resume === 'string' ? resume.split(',').map(s => s.trim()).filter(Boolean) : [];
556
- const isMerge = resumeIds.length > 1;
557
-
558
- // Show execution mode
559
- let resumeInfo = '';
560
- if (isMerge) {
561
- resumeInfo = ` merging ${resumeIds.length} conversations`;
562
- } else if (resume) {
563
- resumeInfo = typeof resume === 'string' ? ` resuming ${resume}` : ' resuming last';
564
- }
565
- const nativeMode = noNative ? ' (prompt-concat)' : '';
566
- const idInfo = id ? ` [${id}]` : '';
567
- console.log(chalk.cyan(`\n Executing ${tool} (${mode} mode${resumeInfo}${nativeMode})${idInfo}...\n`));
568
-
569
- // Show merge details
570
- if (isMerge) {
571
- console.log(chalk.gray(' Merging conversations:'));
572
- for (const rid of resumeIds) {
573
- console.log(chalk.gray(` • ${rid}`));
574
- }
575
- console.log();
576
- }
577
-
578
- // Notify dashboard: execution started
579
- notifyDashboard({
580
- event: 'started',
581
- tool,
582
- mode,
583
- prompt_preview: prompt_to_use.substring(0, 100) + (prompt_to_use.length > 100 ? '...' : ''),
584
- custom_id: id || null
585
- });
586
-
587
- // Streaming output handler
588
- const onOutput = noStream ? null : (chunk: any) => {
589
- process.stdout.write(chunk.data);
590
- };
591
-
592
- try {
593
- const result = await cliExecutorTool.execute({
594
- tool,
595
- prompt: actualPrompt,
596
- mode,
597
- model,
598
- cd,
599
- includeDirs,
600
- timeout: timeout ? parseInt(timeout, 10) : 300000,
601
- resume,
602
- id, // custom execution ID
603
- noNative
604
- }, onOutput);
605
-
606
- // If not streaming, print output now
607
- if (noStream && result.stdout) {
608
- console.log(result.stdout);
609
- }
610
-
611
- // Print summary with execution ID and turn info
612
- console.log();
613
- if (result.success) {
614
- const turnInfo = result.conversation.turn_count > 1
615
- ? ` (turn ${result.conversation.turn_count})`
616
- : '';
617
- console.log(chalk.green(` ✓ Completed in ${(result.execution.duration_ms / 1000).toFixed(1)}s${turnInfo}`));
618
- console.log(chalk.gray(` ID: ${result.execution.id}`));
619
- if (isMerge && !id) {
620
- // Merge without custom ID: updated all source conversations
621
- console.log(chalk.gray(` Updated ${resumeIds.length} conversations: ${resumeIds.join(', ')}`));
622
- } else if (isMerge && id) {
623
- // Merge with custom ID: created new merged conversation
624
- console.log(chalk.gray(` Created merged conversation from ${resumeIds.length} sources`));
625
- }
626
- if (result.conversation.turn_count > 1) {
627
- console.log(chalk.gray(` Total: ${result.conversation.turn_count} turns, ${(result.conversation.total_duration_ms / 1000).toFixed(1)}s`));
628
- }
629
- console.log(chalk.dim(` Continue: ccw cli -p "..." --resume ${result.execution.id}`));
630
-
631
- // Notify dashboard: execution completed
632
- notifyDashboard({
633
- event: 'completed',
634
- tool,
635
- mode,
636
- execution_id: result.execution.id,
637
- success: true,
638
- duration_ms: result.execution.duration_ms,
639
- turn_count: result.conversation.turn_count
640
- });
641
-
642
- // Ensure clean exit after successful execution
643
- process.exit(0);
644
- } else {
645
- console.log(chalk.red(` Failed (${result.execution.status})`));
646
- console.log(chalk.gray(` ID: ${result.execution.id}`));
647
- if (result.stderr) {
648
- console.error(chalk.red(result.stderr));
649
- }
650
-
651
- // Notify dashboard: execution failccw cli -p
652
- notifyDashboard({
653
- event: 'completed',
654
- tool,
655
- mode,
656
- execution_id: result.execution.id,
657
- success: false,
658
- status: result.execution.status,
659
- duration_ms: result.execution.duration_ms
660
- });
661
-
662
- process.exit(1);
663
- }
664
- } catch (error) {
665
- const err = error as Error;
666
- console.error(chalk.red(` Error: ${err.message}`));
667
-
668
- // Notify dashboard: execution error
669
- notifyDashboard({
670
- event: 'error',
671
- tool,
672
- mode,
673
- error: err.message
674
- });
675
-
676
- process.exit(1);
677
- }
678
- }
679
-
680
- /**
681
- * Show execution history
682
- * @param {Object} options - CLI options
683
- */
684
- async function historyAction(options: HistoryOptions): Promise<void> {
685
- const { limit = '20', tool, status } = options;
686
-
687
- console.log(chalk.bold.cyan('\n CLI Execution History\n'));
688
-
689
- const history = await getExecutionHistoryAsync(process.cwd(), { limit: parseInt(limit, 10), tool, status });
690
-
691
- if (history.executions.length === 0) {
692
- console.log(chalk.gray(' No executions found.\n'));
693
- return;
694
- }
695
-
696
- console.log(chalk.gray(` Total executions: ${history.total}\n`));
697
-
698
- for (const exec of history.executions) {
699
- const statusIcon = exec.status === 'success' ? chalk.green('●') :
700
- exec.status === 'timeout' ? chalk.yellow('●') : chalk.red('●');
701
- const duration = exec.duration_ms >= 1000
702
- ? `${(exec.duration_ms / 1000).toFixed(1)}s`
703
- : `${exec.duration_ms}ms`;
704
-
705
- const timeAgo = getTimeAgo(new Date(exec.updated_at || exec.timestamp));
706
- const turnInfo = exec.turn_count && exec.turn_count > 1 ? chalk.cyan(` [${exec.turn_count} turns]`) : '';
707
-
708
- console.log(` ${statusIcon} ${chalk.bold.white(exec.tool.padEnd(8))} ${chalk.gray(timeAgo.padEnd(12))} ${chalk.gray(duration.padEnd(8))}${turnInfo}`);
709
- console.log(chalk.gray(` ${exec.prompt_preview}`));
710
- console.log(chalk.dim(` ID: ${exec.id}`));
711
- console.log();
712
- }
713
- }
714
-
715
- /**
716
- * Show conversation detail with all turns
717
- * @param {string} conversationId - Conversation ID
718
- */
719
- async function detailAction(conversationId: string | undefined): Promise<void> {
720
- if (!conversationId) {
721
- console.error(chalk.red('Error: Conversation ID is required'));
722
- console.error(chalk.gray('Usage: ccw cli detail <conversation-id>'));
723
- process.exit(1);
724
- }
725
-
726
- const conversation = getConversationDetail(process.cwd(), conversationId);
727
-
728
- if (!conversation) {
729
- console.error(chalk.red(`Error: Conversation not found: ${conversationId}`));
730
- process.exit(1);
731
- }
732
-
733
- console.log(chalk.bold.cyan('\n Conversation Detail\n'));
734
- console.log(` ${chalk.gray('ID:')} ${conversation.id}`);
735
- console.log(` ${chalk.gray('Tool:')} ${conversation.tool}`);
736
- console.log(` ${chalk.gray('Model:')} ${conversation.model}`);
737
- console.log(` ${chalk.gray('Mode:')} ${conversation.mode}`);
738
- console.log(` ${chalk.gray('Status:')} ${conversation.latest_status}`);
739
- console.log(` ${chalk.gray('Turns:')} ${conversation.turn_count}`);
740
- console.log(` ${chalk.gray('Duration:')} ${(conversation.total_duration_ms / 1000).toFixed(1)}s total`);
741
- console.log(` ${chalk.gray('Created:')} ${conversation.created_at}`);
742
- if (conversation.turn_count > 1) {
743
- console.log(` ${chalk.gray('Updated:')} ${conversation.updated_at}`);
744
- }
745
-
746
- // Show all turns
747
- for (const turn of conversation.turns) {
748
- console.log(chalk.bold.cyan(`\n ═══ Turn ${turn.turn} ═══`));
749
- console.log(chalk.gray(` ${turn.timestamp} | ${turn.status} | ${(turn.duration_ms / 1000).toFixed(1)}s`));
750
-
751
- console.log(chalk.bold.white('\n Prompt:'));
752
- console.log(chalk.gray(' ' + turn.prompt.split('\n').join('\n ')));
753
-
754
- if (turn.output.stdout) {
755
- console.log(chalk.bold.white('\n Output:'));
756
- console.log(turn.output.stdout);
757
- }
758
-
759
- if (turn.output.stderr) {
760
- console.log(chalk.bold.red('\n Errors:'));
761
- console.log(turn.output.stderr);
762
- }
763
-
764
- if (turn.output.truncated) {
765
- console.log(chalk.yellow('\n Note: Output was truncated due to size.'));
766
- }
767
- }
768
-
769
- console.log(chalk.dim(`\n Continue: ccw cli -p "..." --resume ${conversation.id}`));
770
- console.log();
771
- }
772
-
773
- /**
774
- * Get human-readable time ago string
775
- * @param {Date} date
776
- * @returns {string}
777
- */
778
- function getTimeAgo(date: Date): string {
779
- const seconds = Math.floor((new Date().getTime() - date.getTime()) / 1000);
780
-
781
- if (seconds < 60) return 'just now';
782
- if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
783
- if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
784
- if (seconds < 604800) return `${Math.floor(seconds / 86400)}d ago`;
785
- return date.toLocaleDateString();
786
- }
787
-
788
- /**ccw cli -p
789
- * CLI command entry point
790
- * @param {string} subcommand - Subcommand (status, exec, history, detail)
791
- * @param {string[]} args - Arguments array
792
- * @param {Object} options - CLI options
793
- */
794
- export async function cliCommand(
795
- subcommand: string,
796
- args: string | string[],
797
- options: CliExecOptions | HistoryOptions
798
- ): Promise<void> {
799
- const argsArray = Array.isArray(args) ? args : (args ? [args] : []);
800
-
801
- switch (subcommand) {
802
- case 'status':
803
- await statusAction();
804
- break;
805
-
806
- case 'history':
807
- await historyAction(options as HistoryOptions);
808
- break;
809
-
810
- case 'detail':
811
- await detailAction(argsArray[0]);
812
- break;
813
-
814
- case 'storage':
815
- await storageAction(argsArray[0], options as unknown as StorageOptions);
816
- break;
817
-
818
- case 'test-parse':
819
- // Test endpoint to debug multi-line prompt parsing
820
- testParseAction(argsArray, options as CliExecOptions);
821
- break;
822
-
823
- default: {
824
- const execOptions = options as CliExecOptions;
825
- // Auto-exec if: has -p/--prompt, has -f/--file, has --resume, or subcommand looks like a prompt
826
- const hasPromptOption = !!execOptions.prompt;
827
- const hasFileOption = !!execOptions.file;
828
- const hasResume = execOptions.resume !== undefined;
829
- const subcommandIsPrompt = subcommand && !subcommand.startsWith('-');
830
-
831
- if (hasPromptOption || hasFileOption || hasResume || subcommandIsPrompt) {
832
- // Treat as exec: use subcommand as positional prompt if no -p/-f option
833
- const positionalPrompt = subcommandIsPrompt ? subcommand : undefined;
834
- await execAction(positionalPrompt, execOptions);
835
- } else {
836
- // Show help
837
- console.log(chalk.bold.cyan('\n CCW CLI Tool Executor\n'));
838
- console.log(' Unified interface for Gemini, Qwen, and Codex CLI tools.\n');
839
- console.log(' Usage:');
840
- console.log(chalk.gray(' ccw cli -p "<prompt>" --tool <tool> Execute with prompt'));
841
- console.log(chalk.gray(' ccw cli -f prompt.txt --tool <tool> Execute from file'));
842
- console.log();
843
- console.log(' Subcommands:');
844
- console.log(chalk.gray(' status Check CLI tools availability'));
845
- console.log(chalk.gray(' storage [cmd] Manage CCW storage (info/clean/config)'));
846
- console.log(chalk.gray(' history Show execution history'));
847
- console.log(chalk.gray(' detail <id> Show execution detail'));
848
- console.log(chalk.gray(' test-parse [args] Debug CLI argument parsing'));
849
- console.log();
850
- console.log(' Options:');
851
- console.log(chalk.gray(' -p, --prompt <text> Prompt text'));
852
- console.log(chalk.gray(' -f, --file <file> Read prompt from file'));
853
- console.log(chalk.gray(' --tool <tool> Tool: gemini, qwen, codex (default: gemini)'));
854
- console.log(chalk.gray(' --mode <mode> Mode: analysis, write, auto (default: analysis)'));
855
- console.log(chalk.gray(' --model <model> Model override'));
856
- console.log(chalk.gray(' --cd <path> Working directory'));
857
- console.log(chalk.gray(' --includeDirs <dirs> Additional directories'));
858
- console.log(chalk.gray(' --timeout <ms> Timeout (default: 0=disabled)'));
859
- console.log(chalk.gray(' --resume [id] Resume previous session'));
860
- console.log(chalk.gray(' --cache <items> Cache: comma-separated @patterns and text'));
861
- console.log(chalk.gray(' --inject-mode <m> Inject mode: none, full, progressive'));
862
- console.log();
863
- console.log(' Cache format:');
864
- console.log(chalk.gray(' --cache "@src/**/*.ts,@CLAUDE.md" # @patterns to pack'));
865
- console.log(chalk.gray(' --cache "@src/**/*,extra context" # patterns + text content'));
866
- console.log(chalk.gray(' --cache # auto from CONTEXT field'));
867
- console.log();
868
- console.log(' Inject modes:');
869
- console.log(chalk.gray(' none: cache only, no injection (default for gemini/qwen)'));
870
- console.log(chalk.gray(' full: inject all cached content (default for codex)'));
871
- console.log(chalk.gray(' progressive: inject first 64KB with MCP continuation hint'));
872
- console.log();
873
- console.log(' Examples:');
874
- console.log(chalk.gray(' ccw cli -p "Analyze auth module" --tool gemini'));
875
- console.log(chalk.gray(' ccw cli -f prompt.txt --tool codex --mode write'));
876
- console.log(chalk.gray(' ccw cli -p "$(cat template.md)" --tool gemini'));
877
- console.log(chalk.gray(' ccw cli --resume --tool gemini'));
878
- console.log(chalk.gray(' ccw cli -p "..." --cache "@src/**/*.ts" --tool codex'));
879
- console.log(chalk.gray(' ccw cli -p "..." --cache "@src/**/*" --inject-mode progressive --tool gemini'));
880
- console.log();
881
- }
882
- }
883
- }
884
- }
1
+ /**
2
+ * CLI Command - Unified CLI tool executor command
3
+ * Provides interface for executing Gemini, Qwen, and Codex
4
+ */
5
+
6
+ import chalk from 'chalk';
7
+ import http from 'http';
8
+ import {
9
+ cliExecutorTool,
10
+ getCliToolsStatus,
11
+ getExecutionHistory,
12
+ getExecutionHistoryAsync,
13
+ getExecutionDetail,
14
+ getConversationDetail
15
+ } from '../tools/cli-executor.js';
16
+ import {
17
+ getStorageStats,
18
+ getStorageConfig,
19
+ cleanProjectStorage,
20
+ cleanAllStorage,
21
+ formatBytes,
22
+ formatTimeAgo,
23
+ resolveProjectId,
24
+ projectExists,
25
+ getStorageLocationInstructions
26
+ } from '../tools/storage-manager.js';
27
+ import { getHistoryStore } from '../tools/cli-history-store.js';
28
+
29
+ // Dashboard notification settings
30
+ const DASHBOARD_PORT = process.env.CCW_PORT || 3456;
31
+
32
+ /**
33
+ * Notify dashboard of CLI execution events (fire and forget)
34
+ */
35
+ function notifyDashboard(data: Record<string, unknown>): void {
36
+ const payload = JSON.stringify({
37
+ type: 'cli_execution',
38
+ ...data,
39
+ timestamp: new Date().toISOString()
40
+ });
41
+
42
+ const req = http.request({
43
+ hostname: 'localhost',
44
+ port: Number(DASHBOARD_PORT),
45
+ path: '/api/hook',
46
+ method: 'POST',
47
+ timeout: 2000, // 2 second timeout to prevent hanging
48
+ headers: {
49
+ 'Content-Type': 'application/json',
50
+ 'Content-Length': Buffer.byteLength(payload)
51
+ }
52
+ });
53
+
54
+ // Fire and forget - don't block process exit
55
+ req.on('socket', (socket) => {
56
+ socket.unref(); // Allow process to exit even if socket is open
57
+ });
58
+ req.on('error', (err) => {
59
+ if (process.env.DEBUG) console.error('[Dashboard] CLI notification failed:', err.message);
60
+ });
61
+ req.on('timeout', () => {
62
+ req.destroy();
63
+ if (process.env.DEBUG) console.error('[Dashboard] CLI notification timed out');
64
+ });
65
+ req.write(payload);
66
+ req.end();
67
+ }
68
+
69
+ interface CliExecOptions {
70
+ prompt?: string; // Prompt via --prompt/-p option (preferred for multi-line)
71
+ file?: string; // Read prompt from file
72
+ tool?: string;
73
+ mode?: string;
74
+ model?: string;
75
+ cd?: string;
76
+ includeDirs?: string;
77
+ timeout?: string;
78
+ stream?: boolean; // Enable streaming (default: false, caches output)
79
+ resume?: string | boolean; // true = last, string = execution ID, comma-separated for merge
80
+ id?: string; // Custom execution ID (e.g., IMPL-001-step1)
81
+ noNative?: boolean; // Force prompt concatenation instead of native resume
82
+ cache?: string | boolean; // Cache: true = auto from CONTEXT, string = comma-separated patterns/content
83
+ injectMode?: 'none' | 'full' | 'progressive'; // Inject mode for cached content
84
+ }
85
+
86
+ /** Cache configuration parsed from --cache */
87
+ interface CacheConfig {
88
+ patterns?: string[]; // @patterns to pack (items starting with @)
89
+ content?: string; // Additional text content (items not starting with @)
90
+ }
91
+
92
+ interface HistoryOptions {
93
+ limit?: string;
94
+ tool?: string;
95
+ status?: string;
96
+ }
97
+
98
+ interface StorageOptions {
99
+ all?: boolean;
100
+ project?: string;
101
+ cliHistory?: boolean;
102
+ memory?: boolean;
103
+ storageCache?: boolean;
104
+ config?: boolean;
105
+ force?: boolean;
106
+ }
107
+
108
+ interface OutputViewOptions {
109
+ offset?: string;
110
+ limit?: string;
111
+ outputType?: 'stdout' | 'stderr' | 'both';
112
+ turn?: string;
113
+ raw?: boolean;
114
+ }
115
+
116
+ /**
117
+ * Show storage information and management options
118
+ */
119
+ async function storageAction(subAction: string | undefined, options: StorageOptions): Promise<void> {
120
+ switch (subAction) {
121
+ case 'info':
122
+ case undefined:
123
+ await showStorageInfo();
124
+ break;
125
+ case 'clean':
126
+ await cleanStorage(options);
127
+ break;
128
+ case 'config':
129
+ showStorageConfig();
130
+ break;
131
+ default:
132
+ showStorageHelp();
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Show storage information
138
+ */
139
+ async function showStorageInfo(): Promise<void> {
140
+ console.log(chalk.bold.cyan('\n CCW Storage Information\n'));
141
+
142
+ const config = getStorageConfig();
143
+ const stats = getStorageStats();
144
+
145
+ // Configuration
146
+ console.log(chalk.bold.white(' Location:'));
147
+ console.log(` ${chalk.cyan(stats.rootPath)}`);
148
+ if (config.isCustom) {
149
+ console.log(chalk.gray(` (Custom: CCW_DATA_DIR=${config.envVar})`));
150
+ }
151
+ console.log();
152
+
153
+ // Summary
154
+ console.log(chalk.bold.white(' Summary:'));
155
+ console.log(` Total Size: ${chalk.yellow(formatBytes(stats.totalSize))}`);
156
+ console.log(` Projects: ${chalk.yellow(stats.projectCount.toString())}`);
157
+ console.log(` Global DB: ${stats.globalDb.exists ? chalk.green(formatBytes(stats.globalDb.size)) : chalk.gray('Not created')}`);
158
+ console.log();
159
+
160
+ // Projects breakdown
161
+ if (stats.projects.length > 0) {
162
+ console.log(chalk.bold.white(' Projects:'));
163
+ console.log(chalk.gray(' ID Size History Last Used'));
164
+ console.log(chalk.gray(' ─────────────────────────────────────────────────────'));
165
+
166
+ for (const project of stats.projects) {
167
+ const historyInfo = project.cliHistory.recordCount !== undefined
168
+ ? `${project.cliHistory.recordCount} records`
169
+ : (project.cliHistory.exists ? 'Yes' : '-');
170
+
171
+ console.log(
172
+ ` ${chalk.dim(project.projectId)} ` +
173
+ `${formatBytes(project.totalSize).padStart(8)} ` +
174
+ `${historyInfo.padStart(10)} ` +
175
+ `${chalk.gray(formatTimeAgo(project.lastModified))}`
176
+ );
177
+ }
178
+ console.log();
179
+ }
180
+
181
+ // Usage tips
182
+ console.log(chalk.gray(' Commands:'));
183
+ console.log(chalk.gray(' ccw cli storage clean Clean all storage'));
184
+ console.log(chalk.gray(' ccw cli storage clean --project <path> Clean specific project'));
185
+ console.log(chalk.gray(' ccw cli storage config Show location config'));
186
+ console.log();
187
+ }
188
+
189
+ /**
190
+ * Clean storage
191
+ */
192
+ async function cleanStorage(options: StorageOptions): Promise<void> {
193
+ const { all, project, force, cliHistory, memory, storageCache, config } = options;
194
+
195
+ // Determine what to clean
196
+ const cleanTypes = {
197
+ cliHistory: cliHistory || (!cliHistory && !memory && !storageCache && !config),
198
+ memory: memory || (!cliHistory && !memory && !storageCache && !config),
199
+ cache: storageCache || (!cliHistory && !memory && !storageCache && !config),
200
+ config: config || false, // Config requires explicit flag
201
+ all: !cliHistory && !memory && !storageCache && !config
202
+ };
203
+
204
+ if (project) {
205
+ // Clean specific project
206
+ const projectId = resolveProjectId(project);
207
+
208
+ if (!projectExists(projectId)) {
209
+ console.log(chalk.yellow(`\n No storage found for project: ${project}`));
210
+ console.log(chalk.gray(` (Project ID: ${projectId})\n`));
211
+ return;
212
+ }
213
+
214
+ if (!force) {
215
+ console.log(chalk.bold.yellow('\n Warning: This will delete storage for project:'));
216
+ console.log(` Path: ${project}`);
217
+ console.log(` ID: ${projectId}`);
218
+ console.log(chalk.gray('\n Use --force to confirm deletion.\n'));
219
+ return;
220
+ }
221
+
222
+ console.log(chalk.bold.cyan('\n Cleaning project storage...\n'));
223
+ const result = cleanProjectStorage(projectId, cleanTypes);
224
+
225
+ if (result.success) {
226
+ console.log(chalk.green(` ✓ Cleaned ${formatBytes(result.freedBytes)}`));
227
+ } else {
228
+ console.log(chalk.red(' ✗ Cleanup completed with errors:'));
229
+ for (const err of result.errors) {
230
+ console.log(chalk.red(` - ${err}`));
231
+ }
232
+ }
233
+ } else {
234
+ // Clean all storage
235
+ const stats = getStorageStats();
236
+
237
+ if (stats.projectCount === 0) {
238
+ console.log(chalk.yellow('\n No storage to clean.\n'));
239
+ return;
240
+ }
241
+
242
+ if (!force) {
243
+ console.log(chalk.bold.yellow('\n Warning: This will delete ALL CCW storage:'));
244
+ console.log(` Location: ${stats.rootPath}`);
245
+ console.log(` Projects: ${stats.projectCount}`);
246
+ console.log(` Size: ${formatBytes(stats.totalSize)}`);
247
+ console.log(chalk.gray('\n Use --force to confirm deletion.\n'));
248
+ return;
249
+ }
250
+
251
+ console.log(chalk.bold.cyan('\n Cleaning all storage...\n'));
252
+ const result = cleanAllStorage(cleanTypes);
253
+
254
+ if (result.success) {
255
+ console.log(chalk.green(` ✓ Cleaned ${result.projectsCleaned} projects, freed ${formatBytes(result.freedBytes)}`));
256
+ } else {
257
+ console.log(chalk.yellow(` ⚠ Cleaned ${result.projectsCleaned} projects with some errors:`));
258
+ for (const err of result.errors) {
259
+ console.log(chalk.red(` - ${err}`));
260
+ }
261
+ }
262
+ }
263
+ console.log();
264
+ }
265
+
266
+ /**
267
+ * Show storage configuration
268
+ */
269
+ function showStorageConfig(): void {
270
+ console.log(getStorageLocationInstructions());
271
+ }
272
+
273
+ /**
274
+ * Show storage help
275
+ */
276
+ function showStorageHelp(): void {
277
+ console.log(chalk.bold.cyan('\n CCW Storage Management\n'));
278
+ console.log(' Subcommands:');
279
+ console.log(chalk.gray(' info Show storage information (default)'));
280
+ console.log(chalk.gray(' clean Clean storage'));
281
+ console.log(chalk.gray(' config Show configuration instructions'));
282
+ console.log();
283
+ console.log(' Clean Options:');
284
+ console.log(chalk.gray(' --project <path> Clean specific project storage'));
285
+ console.log(chalk.gray(' --force Confirm deletion'));
286
+ console.log(chalk.gray(' --cli-history Clean only CLI history'));
287
+ console.log(chalk.gray(' --memory Clean only memory store'));
288
+ console.log(chalk.gray(' --cache Clean only cache'));
289
+ console.log(chalk.gray(' --config Clean config (requires explicit flag)'));
290
+ console.log();
291
+ console.log(' Examples:');
292
+ console.log(chalk.gray(' ccw cli storage # Show storage info'));
293
+ console.log(chalk.gray(' ccw cli storage clean --force # Clean all storage'));
294
+ console.log(chalk.gray(' ccw cli storage clean --project . --force # Clean current project'));
295
+ console.log(chalk.gray(' ccw cli storage config # Show config instructions'));
296
+ console.log();
297
+ }
298
+
299
+ /**
300
+ * Show cached output for a conversation with pagination
301
+ */
302
+ async function outputAction(conversationId: string | undefined, options: OutputViewOptions): Promise<void> {
303
+ if (!conversationId) {
304
+ console.error(chalk.red('Error: Conversation ID is required'));
305
+ console.error(chalk.gray('Usage: ccw cli output <conversation-id> [--offset N] [--limit N]'));
306
+ process.exit(1);
307
+ }
308
+
309
+ const store = getHistoryStore(process.cwd());
310
+ const result = store.getCachedOutput(
311
+ conversationId,
312
+ options.turn ? parseInt(options.turn) : undefined,
313
+ {
314
+ offset: parseInt(options.offset || '0'),
315
+ limit: parseInt(options.limit || '10000'),
316
+ outputType: options.outputType || 'both'
317
+ }
318
+ );
319
+
320
+ if (!result) {
321
+ console.error(chalk.red(`Error: Execution not found: ${conversationId}`));
322
+ process.exit(1);
323
+ }
324
+
325
+ if (options.raw) {
326
+ // Raw output only (for piping)
327
+ if (result.stdout) console.log(result.stdout.content);
328
+ return;
329
+ }
330
+
331
+ // Formatted output
332
+ console.log(chalk.bold.cyan('Execution Output\n'));
333
+ console.log(` ${chalk.gray('ID:')} ${result.conversationId}`);
334
+ console.log(` ${chalk.gray('Turn:')} ${result.turnNumber}`);
335
+ console.log(` ${chalk.gray('Cached:')} ${result.cached ? chalk.green('Yes') : chalk.yellow('No')}`);
336
+ console.log(` ${chalk.gray('Status:')} ${result.status}`);
337
+ console.log(` ${chalk.gray('Time:')} ${result.timestamp}`);
338
+ console.log();
339
+
340
+ if (result.stdout) {
341
+ console.log(` ${chalk.gray('Stdout:')} (${result.stdout.totalBytes} bytes, offset ${result.stdout.offset})`);
342
+ console.log(chalk.gray(' ' + '-'.repeat(60)));
343
+ console.log(result.stdout.content);
344
+ console.log(chalk.gray(' ' + '-'.repeat(60)));
345
+ if (result.stdout.hasMore) {
346
+ console.log(chalk.yellow(` ... ${result.stdout.totalBytes - result.stdout.offset - result.stdout.content.length} more bytes available`));
347
+ console.log(chalk.gray(` Use --offset ${result.stdout.offset + result.stdout.content.length} to continue`));
348
+ }
349
+ console.log();
350
+ }
351
+
352
+ if (result.stderr && result.stderr.content) {
353
+ console.log(` ${chalk.gray('Stderr:')} (${result.stderr.totalBytes} bytes, offset ${result.stderr.offset})`);
354
+ console.log(chalk.gray(' ' + '-'.repeat(60)));
355
+ console.log(result.stderr.content);
356
+ console.log(chalk.gray(' ' + '-'.repeat(60)));
357
+ if (result.stderr.hasMore) {
358
+ console.log(chalk.yellow(` ... ${result.stderr.totalBytes - result.stderr.offset - result.stderr.content.length} more bytes available`));
359
+ }
360
+ console.log();
361
+ }
362
+ }
363
+
364
+ /**
365
+ * Test endpoint for debugging multi-line prompt parsing
366
+ * Shows exactly how Commander.js parsed the arguments
367
+ */
368
+ function testParseAction(args: string[], options: CliExecOptions): void {
369
+ console.log(chalk.bold.cyan('\n ═══════════════════════════════════════════════'));
370
+ console.log(chalk.bold.cyan(' │ CLI PARSE TEST ENDPOINT │'));
371
+ console.log(chalk.bold.cyan(' ═══════════════════════════════════════════════\n'));
372
+
373
+ // Show args array parsing
374
+ console.log(chalk.bold.yellow('📦 Positional Arguments (args[]):'));
375
+ console.log(chalk.gray(' Length: ') + chalk.white(args.length));
376
+ if (args.length === 0) {
377
+ console.log(chalk.gray(' (empty)'));
378
+ } else {
379
+ args.forEach((arg, i) => {
380
+ console.log(chalk.gray(` [${i}]: `) + chalk.green(`"${arg}"`));
381
+ // Show if multiline
382
+ if (arg.includes('\n')) {
383
+ console.log(chalk.yellow(` ↳ Contains ${arg.split('\n').length} lines`));
384
+ }
385
+ });
386
+ }
387
+
388
+ console.log();
389
+
390
+ // Show options parsing
391
+ console.log(chalk.bold.yellow('⚙️ Options:'));
392
+ const optionEntries = Object.entries(options).filter(([_, v]) => v !== undefined);
393
+ if (optionEntries.length === 0) {
394
+ console.log(chalk.gray(' (none)'));
395
+ } else {
396
+ optionEntries.forEach(([key, value]) => {
397
+ const displayValue = typeof value === 'string' && value.includes('\n')
398
+ ? `"${value.substring(0, 50)}..." (${value.split('\n').length} lines)`
399
+ : JSON.stringify(value);
400
+ console.log(chalk.gray(` --${key}: `) + chalk.cyan(displayValue));
401
+ });
402
+ }
403
+
404
+ console.log();
405
+
406
+ // Show what would be used as prompt
407
+ console.log(chalk.bold.yellow('🎯 Final Prompt Resolution:'));
408
+ const { prompt: optionPrompt, file } = options;
409
+
410
+ if (file) {
411
+ console.log(chalk.gray(' Source: ') + chalk.magenta('--file/-f option'));
412
+ console.log(chalk.gray(' File: ') + chalk.cyan(file));
413
+ } else if (optionPrompt) {
414
+ console.log(chalk.gray(' Source: ') + chalk.magenta('--prompt/-p option'));
415
+ console.log(chalk.gray(' Value: ') + chalk.green(`"${optionPrompt.substring(0, 100)}${optionPrompt.length > 100 ? '...' : ''}"`));
416
+ if (optionPrompt.includes('\n')) {
417
+ console.log(chalk.yellow(` ↳ Multiline: ${optionPrompt.split('\n').length} lines`));
418
+ }
419
+ } else if (args[0]) {
420
+ console.log(chalk.gray(' Source: ') + chalk.magenta('positional argument (args[0])'));
421
+ console.log(chalk.gray(' Value: ') + chalk.green(`"${args[0].substring(0, 100)}${args[0].length > 100 ? '...' : ''}"`));
422
+ if (args[0].includes('\n')) {
423
+ console.log(chalk.yellow(` ↳ Multiline: ${args[0].split('\n').length} lines`));
424
+ }
425
+ } else {
426
+ console.log(chalk.red(' No prompt found!'));
427
+ }
428
+
429
+ console.log();
430
+
431
+ // Show raw debug info
432
+ console.log(chalk.bold.yellow('🔍 Raw Debug Info:'));
433
+ console.log(chalk.gray(' process.argv:'));
434
+ process.argv.forEach((arg, i) => {
435
+ console.log(chalk.gray(` [${i}]: `) + chalk.dim(arg.length > 60 ? arg.substring(0, 60) + '...' : arg));
436
+ });
437
+
438
+ console.log(chalk.bold.cyan('\n ═══════════════════════════════════════════════\n'));
439
+ }
440
+
441
+ /**
442
+ * Show CLI tool status
443
+ */
444
+ async function statusAction(): Promise<void> {
445
+ console.log(chalk.bold.cyan('\n CLI Tools Status\n'));
446
+
447
+ const status = await getCliToolsStatus();
448
+
449
+ for (const [tool, info] of Object.entries(status)) {
450
+ const statusIcon = info.available ? chalk.green('●') : chalk.red('○');
451
+ const statusText = info.available ? chalk.green('Available') : chalk.red('Not Found');
452
+
453
+ console.log(` ${statusIcon} ${chalk.bold.white(tool.padEnd(10))} ${statusText}`);
454
+ if (info.available && info.path) {
455
+ console.log(chalk.gray(` ${info.path}`));
456
+ }
457
+ }
458
+
459
+ console.log();
460
+ }
461
+
462
+ /**
463
+ * Execute a CLI tool
464
+ * @param {string} prompt - Prompt to execute
465
+ * @param {Object} options - CLI options
466
+ */
467
+ async function execAction(positionalPrompt: string | undefined, options: CliExecOptions): Promise<void> {
468
+ const { prompt: optionPrompt, file, tool = 'gemini', mode = 'analysis', model, cd, includeDirs, timeout, stream, resume, id, noNative, cache, injectMode } = options;
469
+
470
+ // Priority: 1. --file, 2. --prompt/-p option, 3. positional argument
471
+ let finalPrompt: string | undefined;
472
+
473
+ if (file) {
474
+ // Read from file
475
+ const { readFileSync, existsSync } = await import('fs');
476
+ const { resolve } = await import('path');
477
+ const filePath = resolve(file);
478
+ if (!existsSync(filePath)) {
479
+ console.error(chalk.red(`Error: File not found: ${filePath}`));
480
+ process.exit(1);
481
+ }
482
+ finalPrompt = readFileSync(filePath, 'utf8').trim();
483
+ if (!finalPrompt) {
484
+ console.error(chalk.red('Error: File is empty'));
485
+ process.exit(1);
486
+ }
487
+ } else if (optionPrompt) {
488
+ // Use --prompt/-p option (preferred for multi-line)
489
+ finalPrompt = optionPrompt;
490
+ } else {
491
+ // Fall back to positional argument
492
+ finalPrompt = positionalPrompt;
493
+ }
494
+
495
+ // Prompt is required unless resuming
496
+ if (!finalPrompt && !resume) {
497
+ console.error(chalk.red('Error: Prompt is required'));
498
+ console.error(chalk.gray('Usage: ccw cli -p "<prompt>" --tool gemini'));
499
+ console.error(chalk.gray(' or: ccw cli -f prompt.txt --tool codex'));
500
+ console.error(chalk.gray(' or: ccw cli --resume --tool gemini'));
501
+ process.exit(1);
502
+ }
503
+
504
+ const prompt_to_use = finalPrompt || '';
505
+
506
+ // Handle cache option: pack @patterns and/or content
507
+ let cacheSessionId: string | undefined;
508
+ let actualPrompt = prompt_to_use;
509
+
510
+ if (cache) {
511
+ const { handler: contextCacheHandler } = await import('../tools/context-cache.js');
512
+
513
+ // Parse cache config from comma-separated string
514
+ // Items starting with @ are patterns, others are text content
515
+ let cacheConfig: CacheConfig = {};
516
+
517
+ if (cache === true) {
518
+ // --cache without value: auto-extract from CONTEXT field
519
+ const contextMatch = prompt_to_use.match(/CONTEXT:\s*([^\n]+)/i);
520
+ if (contextMatch) {
521
+ const contextLine = contextMatch[1];
522
+ const patternMatches = contextLine.matchAll(/@[^\s|]+/g);
523
+ cacheConfig.patterns = Array.from(patternMatches).map(m => m[0]);
524
+ }
525
+ } else if (typeof cache === 'string') {
526
+ // Parse comma-separated items: @patterns and text content
527
+ const items = cache.split(',').map(s => s.trim()).filter(Boolean);
528
+ const patterns: string[] = [];
529
+ const contentParts: string[] = [];
530
+
531
+ for (const item of items) {
532
+ if (item.startsWith('@')) {
533
+ patterns.push(item);
534
+ } else {
535
+ contentParts.push(item);
536
+ }
537
+ }
538
+
539
+ if (patterns.length > 0) {
540
+ cacheConfig.patterns = patterns;
541
+ }
542
+ if (contentParts.length > 0) {
543
+ cacheConfig.content = contentParts.join('\n');
544
+ }
545
+ }
546
+
547
+ // Also extract patterns from CONTEXT if not provided
548
+ if ((!cacheConfig.patterns || cacheConfig.patterns.length === 0) && prompt_to_use) {
549
+ const contextMatch = prompt_to_use.match(/CONTEXT:\s*([^\n]+)/i);
550
+ if (contextMatch) {
551
+ const contextLine = contextMatch[1];
552
+ const patternMatches = contextLine.matchAll(/@[^\s|]+/g);
553
+ cacheConfig.patterns = Array.from(patternMatches).map(m => m[0]);
554
+ }
555
+ }
556
+
557
+ // Pack if we have patterns or content
558
+ if ((cacheConfig.patterns && cacheConfig.patterns.length > 0) || cacheConfig.content) {
559
+ const patternCount = cacheConfig.patterns?.length || 0;
560
+ const hasContent = !!cacheConfig.content;
561
+ console.log(chalk.gray(` Caching: ${patternCount} pattern(s)${hasContent ? ' + text content' : ''}...`));
562
+
563
+ const cacheResult = await contextCacheHandler({
564
+ operation: 'pack',
565
+ patterns: cacheConfig.patterns,
566
+ content: cacheConfig.content,
567
+ cwd: cd || process.cwd(),
568
+ include_dirs: includeDirs ? includeDirs.split(',') : undefined,
569
+ });
570
+
571
+ if (cacheResult.success && cacheResult.result) {
572
+ const packResult = cacheResult.result as { session_id: string; files_packed: number; total_bytes: number };
573
+ cacheSessionId = packResult.session_id;
574
+ console.log(chalk.gray(` Cached: ${packResult.files_packed} files, ${packResult.total_bytes} bytes`));
575
+ console.log(chalk.gray(` Session: ${cacheSessionId}`));
576
+
577
+ // Determine inject mode:
578
+ // --inject-mode explicitly set > tool default (codex=full, others=none)
579
+ const effectiveInjectMode = injectMode ?? (tool === 'codex' ? 'full' : 'none');
580
+
581
+ if (effectiveInjectMode !== 'none' && cacheSessionId) {
582
+ if (effectiveInjectMode === 'full') {
583
+ // Read full cache content
584
+ const readResult = await contextCacheHandler({
585
+ operation: 'read',
586
+ session_id: cacheSessionId,
587
+ offset: 0,
588
+ limit: 1024 * 1024, // 1MB max
589
+ });
590
+
591
+ if (readResult.success && readResult.result) {
592
+ const { content: cachedContent, total_bytes } = readResult.result as { content: string; total_bytes: number };
593
+ console.log(chalk.gray(` Injecting ${total_bytes} bytes (full mode)...`));
594
+ actualPrompt = `=== CACHED CONTEXT (${packResult.files_packed} files) ===\n${cachedContent}\n\n=== USER PROMPT ===\n${prompt_to_use}`;
595
+ }
596
+ } else if (effectiveInjectMode === 'progressive') {
597
+ // Progressive mode: read first page only (64KB default)
598
+ const pageLimit = 65536;
599
+ const readResult = await contextCacheHandler({
600
+ operation: 'read',
601
+ session_id: cacheSessionId,
602
+ offset: 0,
603
+ limit: pageLimit,
604
+ });
605
+
606
+ if (readResult.success && readResult.result) {
607
+ const { content: cachedContent, total_bytes, has_more, next_offset } = readResult.result as {
608
+ content: string; total_bytes: number; has_more: boolean; next_offset: number | null
609
+ };
610
+ console.log(chalk.gray(` Injecting ${cachedContent.length}/${total_bytes} bytes (progressive mode)...`));
611
+
612
+ const moreInfo = has_more
613
+ ? `\n[... ${total_bytes - cachedContent.length} more bytes available via: context_cache(operation="read", session_id="${cacheSessionId}", offset=${next_offset}) ...]`
614
+ : '';
615
+
616
+ actualPrompt = `=== CACHED CONTEXT (${packResult.files_packed} files, progressive) ===\n${cachedContent}${moreInfo}\n\n=== USER PROMPT ===\n${prompt_to_use}`;
617
+ }
618
+ }
619
+ }
620
+
621
+ console.log();
622
+ } else {
623
+ console.log(chalk.yellow(` Cache warning: ${cacheResult.error}`));
624
+ }
625
+ }
626
+ }
627
+
628
+ // Parse resume IDs for merge scenario
629
+ const resumeIds = resume && typeof resume === 'string' ? resume.split(',').map(s => s.trim()).filter(Boolean) : [];
630
+ const isMerge = resumeIds.length > 1;
631
+
632
+ // Show execution mode
633
+ let resumeInfo = '';
634
+ if (isMerge) {
635
+ resumeInfo = ` merging ${resumeIds.length} conversations`;
636
+ } else if (resume) {
637
+ resumeInfo = typeof resume === 'string' ? ` resuming ${resume}` : ' resuming last';
638
+ }
639
+ const nativeMode = noNative ? ' (prompt-concat)' : '';
640
+ const idInfo = id ? ` [${id}]` : '';
641
+ console.log(chalk.cyan(`\n Executing ${tool} (${mode} mode${resumeInfo}${nativeMode})${idInfo}...\n`));
642
+
643
+ // Show merge details
644
+ if (isMerge) {
645
+ console.log(chalk.gray(' Merging conversations:'));
646
+ for (const rid of resumeIds) {
647
+ console.log(chalk.gray(` • ${rid}`));
648
+ }
649
+ console.log();
650
+ }
651
+
652
+ // Notify dashboard: execution started
653
+ notifyDashboard({
654
+ event: 'started',
655
+ tool,
656
+ mode,
657
+ prompt_preview: prompt_to_use.substring(0, 100) + (prompt_to_use.length > 100 ? '...' : ''),
658
+ custom_id: id || null
659
+ });
660
+
661
+ // Streaming output handler - only active when --stream flag is passed
662
+ const onOutput = stream ? (chunk: any) => {
663
+ process.stdout.write(chunk.data);
664
+ } : null;
665
+
666
+ try {
667
+ const result = await cliExecutorTool.execute({
668
+ tool,
669
+ prompt: actualPrompt,
670
+ mode,
671
+ model,
672
+ cd,
673
+ includeDirs,
674
+ timeout: timeout ? parseInt(timeout, 10) : 300000,
675
+ resume,
676
+ id, // custom execution ID
677
+ noNative,
678
+ stream: !!stream // stream=true → streaming enabled, stream=false/undefined → cache output
679
+ }, onOutput);
680
+
681
+ // If not streaming (default), print output now
682
+ if (!stream && result.stdout) {
683
+ console.log(result.stdout);
684
+ }
685
+
686
+ // Print summary with execution ID and turn info
687
+ console.log();
688
+ if (result.success) {
689
+ const turnInfo = result.conversation.turn_count > 1
690
+ ? ` (turn ${result.conversation.turn_count})`
691
+ : '';
692
+ console.log(chalk.green(` Completed in ${(result.execution.duration_ms / 1000).toFixed(1)}s${turnInfo}`));
693
+ console.log(chalk.gray(` ID: ${result.execution.id}`));
694
+ if (isMerge && !id) {
695
+ // Merge without custom ID: updated all source conversations
696
+ console.log(chalk.gray(` Updated ${resumeIds.length} conversations: ${resumeIds.join(', ')}`));
697
+ } else if (isMerge && id) {
698
+ // Merge with custom ID: created new merged conversation
699
+ console.log(chalk.gray(` Created merged conversation from ${resumeIds.length} sources`));
700
+ }
701
+ if (result.conversation.turn_count > 1) {
702
+ console.log(chalk.gray(` Total: ${result.conversation.turn_count} turns, ${(result.conversation.total_duration_ms / 1000).toFixed(1)}s`));
703
+ }
704
+ console.log(chalk.dim(` Continue: ccw cli -p "..." --resume ${result.execution.id}`));
705
+ if (!stream) {
706
+ console.log(chalk.dim(` Output (optional): ccw cli output ${result.execution.id}`));
707
+ }
708
+
709
+ // Notify dashboard: execution completed
710
+ notifyDashboard({
711
+ event: 'completed',
712
+ tool,
713
+ mode,
714
+ execution_id: result.execution.id,
715
+ success: true,
716
+ duration_ms: result.execution.duration_ms,
717
+ turn_count: result.conversation.turn_count
718
+ });
719
+
720
+ // Ensure clean exit after successful execution
721
+ process.exit(0);
722
+ } else {
723
+ console.log(chalk.red(` ✗ Failed (${result.execution.status})`));
724
+ console.log(chalk.gray(` ID: ${result.execution.id}`));
725
+ if (result.stderr) {
726
+ console.error(chalk.red(result.stderr));
727
+ }
728
+
729
+ // Notify dashboard: execution failccw cli -p
730
+ notifyDashboard({
731
+ event: 'completed',
732
+ tool,
733
+ mode,
734
+ execution_id: result.execution.id,
735
+ success: false,
736
+ status: result.execution.status,
737
+ duration_ms: result.execution.duration_ms
738
+ });
739
+
740
+ process.exit(1);
741
+ }
742
+ } catch (error) {
743
+ const err = error as Error;
744
+ console.error(chalk.red(` Error: ${err.message}`));
745
+
746
+ // Notify dashboard: execution error
747
+ notifyDashboard({
748
+ event: 'error',
749
+ tool,
750
+ mode,
751
+ error: err.message
752
+ });
753
+
754
+ process.exit(1);
755
+ }
756
+ }
757
+
758
+ /**
759
+ * Show execution history
760
+ * @param {Object} options - CLI options
761
+ */
762
+ async function historyAction(options: HistoryOptions): Promise<void> {
763
+ const { limit = '20', tool, status } = options;
764
+
765
+ console.log(chalk.bold.cyan('\n CLI Execution History\n'));
766
+
767
+ const history = await getExecutionHistoryAsync(process.cwd(), { limit: parseInt(limit, 10), tool, status });
768
+
769
+ if (history.executions.length === 0) {
770
+ console.log(chalk.gray(' No executions found.\n'));
771
+ return;
772
+ }
773
+
774
+ console.log(chalk.gray(` Total executions: ${history.total}\n`));
775
+
776
+ for (const exec of history.executions) {
777
+ const statusIcon = exec.status === 'success' ? chalk.green('●') :
778
+ exec.status === 'timeout' ? chalk.yellow('●') : chalk.red('●');
779
+ const duration = exec.duration_ms >= 1000
780
+ ? `${(exec.duration_ms / 1000).toFixed(1)}s`
781
+ : `${exec.duration_ms}ms`;
782
+
783
+ const timeAgo = getTimeAgo(new Date(exec.updated_at || exec.timestamp));
784
+ const turnInfo = exec.turn_count && exec.turn_count > 1 ? chalk.cyan(` [${exec.turn_count} turns]`) : '';
785
+
786
+ console.log(` ${statusIcon} ${chalk.bold.white(exec.tool.padEnd(8))} ${chalk.gray(timeAgo.padEnd(12))} ${chalk.gray(duration.padEnd(8))}${turnInfo}`);
787
+ console.log(chalk.gray(` ${exec.prompt_preview}`));
788
+ console.log(chalk.dim(` ID: ${exec.id}`));
789
+ console.log();
790
+ }
791
+ }
792
+
793
+ /**
794
+ * Show conversation detail with all turns
795
+ * @param {string} conversationId - Conversation ID
796
+ */
797
+ async function detailAction(conversationId: string | undefined): Promise<void> {
798
+ if (!conversationId) {
799
+ console.error(chalk.red('Error: Conversation ID is required'));
800
+ console.error(chalk.gray('Usage: ccw cli detail <conversation-id>'));
801
+ process.exit(1);
802
+ }
803
+
804
+ const conversation = getConversationDetail(process.cwd(), conversationId);
805
+
806
+ if (!conversation) {
807
+ console.error(chalk.red(`Error: Conversation not found: ${conversationId}`));
808
+ process.exit(1);
809
+ }
810
+
811
+ console.log(chalk.bold.cyan('\n Conversation Detail\n'));
812
+ console.log(` ${chalk.gray('ID:')} ${conversation.id}`);
813
+ console.log(` ${chalk.gray('Tool:')} ${conversation.tool}`);
814
+ console.log(` ${chalk.gray('Model:')} ${conversation.model}`);
815
+ console.log(` ${chalk.gray('Mode:')} ${conversation.mode}`);
816
+ console.log(` ${chalk.gray('Status:')} ${conversation.latest_status}`);
817
+ console.log(` ${chalk.gray('Turns:')} ${conversation.turn_count}`);
818
+ console.log(` ${chalk.gray('Duration:')} ${(conversation.total_duration_ms / 1000).toFixed(1)}s total`);
819
+ console.log(` ${chalk.gray('Created:')} ${conversation.created_at}`);
820
+ if (conversation.turn_count > 1) {
821
+ console.log(` ${chalk.gray('Updated:')} ${conversation.updated_at}`);
822
+ }
823
+
824
+ // Show all turns
825
+ for (const turn of conversation.turns) {
826
+ console.log(chalk.bold.cyan(`\n ═══ Turn ${turn.turn} ═══`));
827
+ console.log(chalk.gray(` ${turn.timestamp} | ${turn.status} | ${(turn.duration_ms / 1000).toFixed(1)}s`));
828
+
829
+ console.log(chalk.bold.white('\n Prompt:'));
830
+ console.log(chalk.gray(' ' + turn.prompt.split('\n').join('\n ')));
831
+
832
+ if (turn.output.stdout) {
833
+ console.log(chalk.bold.white('\n Output:'));
834
+ console.log(turn.output.stdout);
835
+ }
836
+
837
+ if (turn.output.stderr) {
838
+ console.log(chalk.bold.red('\n Errors:'));
839
+ console.log(turn.output.stderr);
840
+ }
841
+
842
+ if (turn.output.truncated) {
843
+ console.log(chalk.yellow('\n Note: Output was truncated due to size.'));
844
+ }
845
+ }
846
+
847
+ console.log(chalk.dim(`\n Continue: ccw cli -p "..." --resume ${conversation.id}`));
848
+ console.log();
849
+ }
850
+
851
+ /**
852
+ * Get human-readable time ago string
853
+ * @param {Date} date
854
+ * @returns {string}
855
+ */
856
+ function getTimeAgo(date: Date): string {
857
+ const seconds = Math.floor((new Date().getTime() - date.getTime()) / 1000);
858
+
859
+ if (seconds < 60) return 'just now';
860
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
861
+ if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
862
+ if (seconds < 604800) return `${Math.floor(seconds / 86400)}d ago`;
863
+ return date.toLocaleDateString();
864
+ }
865
+
866
+ /**ccw cli -p
867
+ * CLI command entry point
868
+ * @param {string} subcommand - Subcommand (status, exec, history, detail)
869
+ * @param {string[]} args - Arguments array
870
+ * @param {Object} options - CLI options
871
+ */
872
+ export async function cliCommand(
873
+ subcommand: string,
874
+ args: string | string[],
875
+ options: CliExecOptions | HistoryOptions
876
+ ): Promise<void> {
877
+ const argsArray = Array.isArray(args) ? args : (args ? [args] : []);
878
+
879
+ switch (subcommand) {
880
+ case 'status':
881
+ await statusAction();
882
+ break;
883
+
884
+ case 'history':
885
+ await historyAction(options as HistoryOptions);
886
+ break;
887
+
888
+ case 'detail':
889
+ await detailAction(argsArray[0]);
890
+ break;
891
+
892
+ case 'storage':
893
+ await storageAction(argsArray[0], options as unknown as StorageOptions);
894
+ break;
895
+
896
+ case 'output':
897
+ await outputAction(argsArray[0], options as unknown as OutputViewOptions);
898
+ break;
899
+
900
+ case 'test-parse':
901
+ // Test endpoint to debug multi-line prompt parsing
902
+ testParseAction(argsArray, options as CliExecOptions);
903
+ break;
904
+
905
+ default: {
906
+ const execOptions = options as CliExecOptions;
907
+ // Auto-exec if: has -p/--prompt, has -f/--file, has --resume, or subcommand looks like a prompt
908
+ const hasPromptOption = !!execOptions.prompt;
909
+ const hasFileOption = !!execOptions.file;
910
+ const hasResume = execOptions.resume !== undefined;
911
+ const subcommandIsPrompt = subcommand && !subcommand.startsWith('-');
912
+
913
+ if (hasPromptOption || hasFileOption || hasResume || subcommandIsPrompt) {
914
+ // Treat as exec: use subcommand as positional prompt if no -p/-f option
915
+ const positionalPrompt = subcommandIsPrompt ? subcommand : undefined;
916
+ await execAction(positionalPrompt, execOptions);
917
+ } else {
918
+ // Show help
919
+ console.log(chalk.bold.cyan('\n CCW CLI Tool Executor\n'));
920
+ console.log(' Unified interface for Gemini, Qwen, and Codex CLI tools.\n');
921
+ console.log(' Usage:');
922
+ console.log(chalk.gray(' ccw cli -p "<prompt>" --tool <tool> Execute with prompt'));
923
+ console.log(chalk.gray(' ccw cli -f prompt.txt --tool <tool> Execute from file'));
924
+ console.log();
925
+ console.log(' Subcommands:');
926
+ console.log(chalk.gray(' status Check CLI tools availability'));
927
+ console.log(chalk.gray(' storage [cmd] Manage CCW storage (info/clean/config)'));
928
+ console.log(chalk.gray(' history Show execution history'));
929
+ console.log(chalk.gray(' detail <id> Show execution detail'));
930
+ console.log(chalk.gray(' output <id> Show execution output with pagination'));
931
+ console.log(chalk.gray(' test-parse [args] Debug CLI argument parsing'));
932
+ console.log();
933
+ console.log(' Options:');
934
+ console.log(chalk.gray(' -p, --prompt <text> Prompt text'));
935
+ console.log(chalk.gray(' -f, --file <file> Read prompt from file'));
936
+ console.log(chalk.gray(' --tool <tool> Tool: gemini, qwen, codex (default: gemini)'));
937
+ console.log(chalk.gray(' --mode <mode> Mode: analysis, write, auto (default: analysis)'));
938
+ console.log(chalk.gray(' --model <model> Model override'));
939
+ console.log(chalk.gray(' --cd <path> Working directory'));
940
+ console.log(chalk.gray(' --includeDirs <dirs> Additional directories'));
941
+ console.log(chalk.gray(' --timeout <ms> Timeout (default: 0=disabled)'));
942
+ console.log(chalk.gray(' --resume [id] Resume previous session'));
943
+ console.log(chalk.gray(' --cache <items> Cache: comma-separated @patterns and text'));
944
+ console.log(chalk.gray(' --inject-mode <m> Inject mode: none, full, progressive'));
945
+ console.log();
946
+ console.log(' Cache format:');
947
+ console.log(chalk.gray(' --cache "@src/**/*.ts,@CLAUDE.md" # @patterns to pack'));
948
+ console.log(chalk.gray(' --cache "@src/**/*,extra context" # patterns + text content'));
949
+ console.log(chalk.gray(' --cache # auto from CONTEXT field'));
950
+ console.log();
951
+ console.log(' Inject modes:');
952
+ console.log(chalk.gray(' none: cache only, no injection (default for gemini/qwen)'));
953
+ console.log(chalk.gray(' full: inject all cached content (default for codex)'));
954
+ console.log(chalk.gray(' progressive: inject first 64KB with MCP continuation hint'));
955
+ console.log();
956
+ console.log(' Examples:');
957
+ console.log(chalk.gray(' ccw cli -p "Analyze auth module" --tool gemini'));
958
+ console.log(chalk.gray(' ccw cli -f prompt.txt --tool codex --mode write'));
959
+ console.log(chalk.gray(' ccw cli -p "$(cat template.md)" --tool gemini'));
960
+ console.log(chalk.gray(' ccw cli --resume --tool gemini'));
961
+ console.log(chalk.gray(' ccw cli -p "..." --cache "@src/**/*.ts" --tool codex'));
962
+ console.log(chalk.gray(' ccw cli -p "..." --cache "@src/**/*" --inject-mode progressive --tool gemini'));
963
+ console.log();
964
+ }
965
+ }
966
+ }
967
+ }