Skip to content

Commit 3b842ed

Browse files
author
catlog22
committed
feat(cli-executor): add streaming option and enhance output handling
- Introduced a `stream` parameter to control output streaming vs. caching. - Enhanced status determination logic to prioritize valid output over exit codes. - Updated output structure to include full stdout and stderr when not streaming. feat(cli-history-store): extend conversation turn schema and migration - Added `cached`, `stdout_full`, and `stderr_full` fields to the conversation turn schema. - Implemented database migration to add new columns if they do not exist. - Updated upsert logic to handle new fields. feat(codex-lens): implement global symbol index for fast lookups - Created `GlobalSymbolIndex` class to manage project-wide symbol indexing. - Added methods for adding, updating, and deleting symbols in the global index. - Integrated global index updates into directory indexing processes. feat(codex-lens): optimize search functionality with global index - Enhanced `ChainSearchEngine` to utilize the global symbol index for faster searches. - Added configuration option to enable/disable global symbol indexing. - Updated tests to validate global index functionality and performance.
1 parent 673e1d1 commit 3b842ed

File tree

13 files changed

+1032
-37
lines changed

13 files changed

+1032
-37
lines changed

.claude/CLAUDE.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,20 @@ Available CLI endpoints are dynamically defined by the config file:
1515
- Custom API endpoints registered via the Dashboard
1616
- Managed through the CCW Dashboard Status page
1717

18-
## Agent Execution
18+
## Tool Execution
1919

20+
### Agent Calls
2021
- **Always use `run_in_background: false`** for Task tool agent calls: `Task({ subagent_type: "xxx", prompt: "...", run_in_background: false })` to ensure synchronous execution and immediate result visibility
2122
- **TaskOutput usage**: Only use `TaskOutput({ task_id: "xxx", block: false })` + sleep loop to poll completion status. NEVER read intermediate output during agent/CLI execution - wait for final result only
2223

24+
### CLI Tool Calls (ccw cli)
25+
- **Always use `run_in_background: true`** for Bash tool when calling ccw cli:
26+
```
27+
Bash({ command: "ccw cli -p '...' --tool gemini", run_in_background: true })
28+
```
29+
- **After CLI call**: Stop immediately - let CLI execute in background, do NOT poll with TaskOutput
30+
- **View output later**: Use `ccw cli output <id>` to view cached execution results
31+
2332
## Code Diagnostics
2433

2534
- **Prefer `mcp__ide__getDiagnostics`** for code error checking over shell-based TypeScript compilation

.claude/commands/workflow/lite-plan.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ Phase 1: Task Analysis & Exploration
3838
├─ Parse input (description or .md file)
3939
├─ intelligent complexity assessment (Low/Medium/High)
4040
├─ Exploration decision (auto-detect or --explore flag)
41-
├─ ⚠️ Context protection: If file reading ≥50k chars → force cli-explore-agent
41+
├─ Context protection: If file reading ≥50k chars → force cli-explore-agent
4242
└─ Decision:
4343
├─ needsExploration=true → Launch parallel cli-explore-agents (1-4 based on complexity)
4444
└─ needsExploration=false → Skip to Phase 2/3

.claude/workflows/cli-tools-usage.md

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,13 @@ RULES: $(cat ~/.claude/workflows/cli-templates/protocols/[mode]-protocol.md) $(c
6565
ccw cli -p "<PROMPT>" --tool <gemini|qwen|codex> --mode <analysis|write>
6666
```
6767

68-
**⚠️ CRITICAL**: `--mode` parameter is **MANDATORY** for all CLI executions. No defaults are assumed.
68+
**Note**: `--mode` defaults to `analysis` if not specified. Explicitly specify `--mode write` for file operations.
6969

7070
### Core Principles
7171

7272
- **Use tools early and often** - Tools are faster and more thorough
7373
- **Unified CLI** - Always use `ccw cli -p` for consistent parameter handling
74-
- **Mode is MANDATORY** - ALWAYS explicitly specify `--mode analysis|write` (no implicit defaults)
74+
- **Default mode is analysis** - Omit `--mode` for read-only operations, explicitly use `--mode write` for file modifications
7575
- **One template required** - ALWAYS reference exactly ONE template in RULES (use universal fallback if no specific match)
7676
- **Write protection** - Require EXPLICIT `--mode write` for file operations
7777
- **Use double quotes for shell expansion** - Always wrap prompts in double quotes `"..."` to enable `$(cat ...)` command substitution; NEVER use single quotes or escape characters (`\$`, `\"`, `\'`)
@@ -183,6 +183,33 @@ ASSISTANT RESPONSE: [Previous output]
183183

184184
**Tool Behavior**: Codex uses native `codex resume`; Gemini/Qwen assembles context as single prompt
185185

186+
### Streaming vs Caching
187+
188+
**Default behavior**: Non-streaming with full output caching (can retrieve later via `output` subcommand)
189+
190+
```bash
191+
ccw cli -p "..." --tool gemini # Default: output cached, no streaming
192+
ccw cli -p "..." --tool gemini --stream # Streaming: real-time output, no caching
193+
```
194+
195+
| Mode | Flag | Output | Cached |
196+
|------|------|--------|--------|
197+
| Non-streaming (default) | (none) | After completion | ✅ Yes |
198+
| Streaming | `--stream` | Real-time | ❌ No |
199+
200+
### Output Viewing
201+
202+
View cached execution output with pagination:
203+
204+
```bash
205+
ccw cli output <execution-id> # View full output
206+
ccw cli output <id> --offset 0 --limit 10000 # Paginated view
207+
ccw cli output <id> --output-type stdout # Stdout only
208+
ccw cli output <id> --raw # Raw content (for piping)
209+
```
210+
211+
**Note**: `output` subcommand views execution results. `--cache` parameter injects context into prompt - different concepts.
212+
186213
---
187214

188215
## Prompt Template

ccw/src/cli.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ export function run(argv: string[]): void {
174174
.option('--cd <path>', 'Working directory')
175175
.option('--includeDirs <dirs>', 'Additional directories (--include-directories for gemini/qwen, --add-dir for codex/claude)')
176176
.option('--timeout <ms>', 'Timeout in milliseconds', '300000')
177-
.option('--no-stream', 'Disable streaming output')
177+
.option('--stream', 'Enable streaming output (default: non-streaming with caching)')
178178
.option('--limit <n>', 'History limit')
179179
.option('--status <status>', 'Filter by status')
180180
.option('--category <category>', 'Execution category: user, internal, insight', 'user')
@@ -190,6 +190,11 @@ export function run(argv: string[]): void {
190190
.option('--memory', 'Target memory storage')
191191
.option('--storage-cache', 'Target cache storage')
192192
.option('--config', 'Target config storage')
193+
// Cache subcommand options
194+
.option('--offset <n>', 'Character offset for cache pagination', '0')
195+
.option('--output-type <type>', 'Output type: stdout, stderr, both', 'both')
196+
.option('--turn <n>', 'Turn number for cache (default: latest)')
197+
.option('--raw', 'Raw output only (no formatting)')
193198
.action((subcommand, args, options) => cliCommand(subcommand, args, options));
194199

195200
// Memory command

ccw/src/commands/cli.ts

Lines changed: 88 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
projectExists,
2525
getStorageLocationInstructions
2626
} from '../tools/storage-manager.js';
27+
import { getHistoryStore } from '../tools/cli-history-store.js';
2728

2829
// Dashboard notification settings
2930
const DASHBOARD_PORT = process.env.CCW_PORT || 3456;
@@ -74,7 +75,7 @@ interface CliExecOptions {
7475
cd?: string;
7576
includeDirs?: string;
7677
timeout?: string;
77-
noStream?: boolean;
78+
stream?: boolean; // Enable streaming (default: false, caches output)
7879
resume?: string | boolean; // true = last, string = execution ID, comma-separated for merge
7980
id?: string; // Custom execution ID (e.g., IMPL-001-step1)
8081
noNative?: boolean; // Force prompt concatenation instead of native resume
@@ -104,6 +105,14 @@ interface StorageOptions {
104105
force?: boolean;
105106
}
106107

108+
interface OutputViewOptions {
109+
offset?: string;
110+
limit?: string;
111+
outputType?: 'stdout' | 'stderr' | 'both';
112+
turn?: string;
113+
raw?: boolean;
114+
}
115+
107116
/**
108117
* Show storage information and management options
109118
*/
@@ -287,6 +296,71 @@ function showStorageHelp(): void {
287296
console.log();
288297
}
289298

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+
290364
/**
291365
* Test endpoint for debugging multi-line prompt parsing
292366
* Shows exactly how Commander.js parsed the arguments
@@ -391,7 +465,7 @@ async function statusAction(): Promise<void> {
391465
* @param {Object} options - CLI options
392466
*/
393467
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;
468+
const { prompt: optionPrompt, file, tool = 'gemini', mode = 'analysis', model, cd, includeDirs, timeout, stream, resume, id, noNative, cache, injectMode } = options;
395469

396470
// Priority: 1. --file, 2. --prompt/-p option, 3. positional argument
397471
let finalPrompt: string | undefined;
@@ -584,10 +658,10 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec
584658
custom_id: id || null
585659
});
586660

587-
// Streaming output handler
588-
const onOutput = noStream ? null : (chunk: any) => {
661+
// Streaming output handler - only active when --stream flag is passed
662+
const onOutput = stream ? (chunk: any) => {
589663
process.stdout.write(chunk.data);
590-
};
664+
} : null;
591665

592666
try {
593667
const result = await cliExecutorTool.execute({
@@ -600,11 +674,12 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec
600674
timeout: timeout ? parseInt(timeout, 10) : 300000,
601675
resume,
602676
id, // custom execution ID
603-
noNative
677+
noNative,
678+
stream: !!stream // stream=true → streaming enabled, stream=false/undefined → cache output
604679
}, onOutput);
605680

606-
// If not streaming, print output now
607-
if (noStream && result.stdout) {
681+
// If not streaming (default), print output now
682+
if (!stream && result.stdout) {
608683
console.log(result.stdout);
609684
}
610685

@@ -815,6 +890,10 @@ export async function cliCommand(
815890
await storageAction(argsArray[0], options as unknown as StorageOptions);
816891
break;
817892

893+
case 'output':
894+
await outputAction(argsArray[0], options as unknown as OutputViewOptions);
895+
break;
896+
818897
case 'test-parse':
819898
// Test endpoint to debug multi-line prompt parsing
820899
testParseAction(argsArray, options as CliExecOptions);
@@ -845,6 +924,7 @@ export async function cliCommand(
845924
console.log(chalk.gray(' storage [cmd] Manage CCW storage (info/clean/config)'));
846925
console.log(chalk.gray(' history Show execution history'));
847926
console.log(chalk.gray(' detail <id> Show execution detail'));
927+
console.log(chalk.gray(' output <id> Show execution output with pagination'));
848928
console.log(chalk.gray(' test-parse [args] Debug CLI argument parsing'));
849929
console.log();
850930
console.log(' Options:');

ccw/src/tools/cli-executor.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ const ParamsSchema = z.object({
7373
noNative: z.boolean().optional(), // Force prompt concatenation instead of native resume
7474
category: z.enum(['user', 'internal', 'insight']).default('user'), // Execution category for tracking
7575
parentExecutionId: z.string().optional(), // Parent execution ID for fork/retry scenarios
76+
stream: z.boolean().default(false), // false = cache full output (default), true = stream output via callback
7677
});
7778

7879
// Execution category types
@@ -863,24 +864,36 @@ async function executeCliTool(
863864
const endTime = Date.now();
864865
const duration = endTime - startTime;
865866

866-
// Determine status
867+
// Determine status - prioritize output content over exit code
867868
let status: 'success' | 'error' | 'timeout' = 'success';
868869
if (timedOut) {
869870
status = 'timeout';
870871
} else if (code !== 0) {
871-
// Check if HTTP 429 but results exist (Gemini quirk)
872-
if (stderr.includes('429') && stdout.trim()) {
872+
// Non-zero exit code doesn't always mean failure
873+
// Check if there's valid output (AI response) - treat as success
874+
const hasValidOutput = stdout.trim().length > 0;
875+
const hasFatalError = stderr.includes('FATAL') ||
876+
stderr.includes('Authentication failed') ||
877+
stderr.includes('API key') ||
878+
stderr.includes('rate limit exceeded');
879+
880+
if (hasValidOutput && !hasFatalError) {
881+
// Has output and no fatal errors - treat as success despite exit code
873882
status = 'success';
874883
} else {
875884
status = 'error';
876885
}
877886
}
878887

879-
// Create new turn
888+
// Create new turn - cache full output when not streaming (default)
889+
const shouldCache = !parsed.data.stream;
880890
const newTurnOutput = {
881-
stdout: stdout.substring(0, 10240), // Truncate to 10KB
882-
stderr: stderr.substring(0, 2048), // Truncate to 2KB
883-
truncated: stdout.length > 10240 || stderr.length > 2048
891+
stdout: stdout.substring(0, 10240), // Truncate preview to 10KB
892+
stderr: stderr.substring(0, 2048), // Truncate preview to 2KB
893+
truncated: stdout.length > 10240 || stderr.length > 2048,
894+
cached: shouldCache,
895+
stdout_full: shouldCache ? stdout : undefined,
896+
stderr_full: shouldCache ? stderr : undefined
884897
};
885898

886899
// Determine base turn number for merge scenarios

0 commit comments

Comments
 (0)