claude-code-workflow 6.3.2 → 6.3.5

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