mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-11 17:21:03 +08:00
feat: Implement DeepWiki generator and CLI integration
- Added `deepwiki_generator.py` for generating documentation from source code. - Integrated symbol extraction and markdown generation for supported file types. - Implemented database migration for legacy timestamp formats in DeepWikiStore. - Enhanced debug logging for better traceability during conversation and store operations. - Updated dependencies in `PKG-INFO` and `requires.txt` for compatibility. - Added new tests for the DeepWiki generator and storage functionalities. - Refactored existing code for improved readability and maintainability.
This commit is contained in:
@@ -201,7 +201,7 @@ export function run(argv: string[]): void {
|
||||
.option('--status <status>', 'Filter by status')
|
||||
.option('--category <category>', 'Execution category: user, internal, insight', 'user')
|
||||
.option('--resume [id]', 'Resume previous session (empty=last, or execution ID, or comma-separated IDs for merge)')
|
||||
.option('--id <id>', 'Custom execution ID (e.g., IMPL-001-step1)')
|
||||
.option('--id <id>', 'Execution ID (recommended, auto-generated if omitted)')
|
||||
.option('--no-native', 'Force prompt concatenation instead of native resume')
|
||||
.option('--cache [items]', 'Cache: comma-separated @patterns and text content')
|
||||
.option('--inject-mode <mode>', 'Inject mode: none, full, progressive (default: codex=full, others=none)')
|
||||
@@ -226,7 +226,10 @@ export function run(argv: string[]): void {
|
||||
.option('--output-type <type>', 'Output type: stdout, stderr, both', 'both')
|
||||
.option('--turn <n>', 'Turn number for cache (default: latest)')
|
||||
.option('--raw', 'Raw output only (no formatting)')
|
||||
.option('--final', 'Output final result only with usage hint')
|
||||
.option('--final', 'Output final result only (legacy, now default)')
|
||||
.option('--verbose', 'Show full metadata in output view')
|
||||
.option('--timeout <seconds>', 'Timeout for watch command')
|
||||
.option('--all', 'Show all executions in show command')
|
||||
.option('--to-file <path>', 'Save output to file')
|
||||
.action((subcommand, args, options) => cliCommand(subcommand, args, options));
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { CliOutputUnit } from '../tools/cli-output-converter.js';
|
||||
import { SmartContentFormatter } from '../tools/cli-output-converter.js';
|
||||
import {
|
||||
cliExecutorTool,
|
||||
generateExecutionId,
|
||||
getCliToolsStatus,
|
||||
getExecutionHistory,
|
||||
getExecutionHistoryAsync,
|
||||
@@ -181,6 +182,7 @@ interface OutputViewOptions {
|
||||
turn?: string;
|
||||
raw?: boolean;
|
||||
final?: boolean; // Only output final result with usage hint
|
||||
verbose?: boolean; // Show full metadata (original default behavior)
|
||||
project?: string; // Optional project path for lookup
|
||||
}
|
||||
|
||||
@@ -432,56 +434,57 @@ async function outputAction(conversationId: string | undefined, options: OutputV
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.final) {
|
||||
// Final result only with usage hint
|
||||
// Prefer finalOutput (agent_message only) > parsedOutput (filtered) > raw stdout
|
||||
const outputContent = result.finalOutput?.content || result.parsedOutput?.content || result.stdout?.content;
|
||||
if (outputContent) {
|
||||
console.log(outputContent);
|
||||
}
|
||||
if (options.verbose) {
|
||||
// Verbose: full metadata + output (original default behavior)
|
||||
console.log(chalk.bold.cyan('Execution Output\n'));
|
||||
console.log(` ${chalk.gray('ID:')} ${result.conversationId}`);
|
||||
console.log(` ${chalk.gray('Turn:')} ${result.turnNumber}`);
|
||||
console.log(` ${chalk.gray('Cached:')} ${result.cached ? chalk.green('Yes') : chalk.yellow('No')}`);
|
||||
console.log(` ${chalk.gray('Status:')} ${result.status}`);
|
||||
console.log(` ${chalk.gray('Time:')} ${result.timestamp}`);
|
||||
console.log(` ${chalk.gray('Project:')} ${chalk.cyan(projectPath)}`);
|
||||
console.log();
|
||||
console.log(chalk.gray('─'.repeat(60)));
|
||||
console.log(chalk.dim(`Usage: ccw cli output ${conversationId} [options]`));
|
||||
console.log(chalk.dim(' --raw Raw output (no formatting)'));
|
||||
console.log(chalk.dim(' --offset <n> Start from byte offset'));
|
||||
console.log(chalk.dim(' --limit <n> Limit output bytes'));
|
||||
console.log(chalk.dim(' --project <p> Specify project path explicitly'));
|
||||
console.log(chalk.dim(` --resume ccw cli -p "..." --resume ${conversationId}`));
|
||||
|
||||
if (result.stdout) {
|
||||
console.log(` ${chalk.gray('Stdout:')} (${result.stdout.totalBytes} bytes, offset ${result.stdout.offset})`);
|
||||
console.log(chalk.gray(' ' + '-'.repeat(60)));
|
||||
console.log(result.stdout.content);
|
||||
console.log(chalk.gray(' ' + '-'.repeat(60)));
|
||||
if (result.stdout.hasMore) {
|
||||
console.log(chalk.yellow(` ... ${result.stdout.totalBytes - result.stdout.offset - result.stdout.content.length} more bytes available`));
|
||||
console.log(chalk.gray(` Use --offset ${result.stdout.offset + result.stdout.content.length} to continue`));
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
if (result.stderr && result.stderr.content) {
|
||||
console.log(` ${chalk.gray('Stderr:')} (${result.stderr.totalBytes} bytes, offset ${result.stderr.offset})`);
|
||||
console.log(chalk.gray(' ' + '-'.repeat(60)));
|
||||
console.log(result.stderr.content);
|
||||
console.log(chalk.gray(' ' + '-'.repeat(60)));
|
||||
if (result.stderr.hasMore) {
|
||||
console.log(chalk.yellow(` ... ${result.stderr.totalBytes - result.stderr.offset - result.stderr.content.length} more bytes available`));
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Formatted output
|
||||
console.log(chalk.bold.cyan('Execution Output\n'));
|
||||
console.log(` ${chalk.gray('ID:')} ${result.conversationId}`);
|
||||
console.log(` ${chalk.gray('Turn:')} ${result.turnNumber}`);
|
||||
console.log(` ${chalk.gray('Cached:')} ${result.cached ? chalk.green('Yes') : chalk.yellow('No')}`);
|
||||
console.log(` ${chalk.gray('Status:')} ${result.status}`);
|
||||
console.log(` ${chalk.gray('Time:')} ${result.timestamp}`);
|
||||
console.log(` ${chalk.gray('Project:')} ${chalk.cyan(projectPath)}`);
|
||||
// Default: final result only (equivalent to --final)
|
||||
// Prefer finalOutput (agent_message only) > parsedOutput (filtered) > raw stdout
|
||||
const outputContent = result.finalOutput?.content || result.parsedOutput?.content || result.stdout?.content;
|
||||
if (outputContent) {
|
||||
console.log(outputContent);
|
||||
}
|
||||
console.log();
|
||||
|
||||
if (result.stdout) {
|
||||
console.log(` ${chalk.gray('Stdout:')} (${result.stdout.totalBytes} bytes, offset ${result.stdout.offset})`);
|
||||
console.log(chalk.gray(' ' + '-'.repeat(60)));
|
||||
console.log(result.stdout.content);
|
||||
console.log(chalk.gray(' ' + '-'.repeat(60)));
|
||||
if (result.stdout.hasMore) {
|
||||
console.log(chalk.yellow(` ... ${result.stdout.totalBytes - result.stdout.offset - result.stdout.content.length} more bytes available`));
|
||||
console.log(chalk.gray(` Use --offset ${result.stdout.offset + result.stdout.content.length} to continue`));
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
if (result.stderr && result.stderr.content) {
|
||||
console.log(` ${chalk.gray('Stderr:')} (${result.stderr.totalBytes} bytes, offset ${result.stderr.offset})`);
|
||||
console.log(chalk.gray(' ' + '-'.repeat(60)));
|
||||
console.log(result.stderr.content);
|
||||
console.log(chalk.gray(' ' + '-'.repeat(60)));
|
||||
if (result.stderr.hasMore) {
|
||||
console.log(chalk.yellow(` ... ${result.stderr.totalBytes - result.stderr.offset - result.stderr.content.length} more bytes available`));
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
console.log(chalk.gray('\u2500'.repeat(60)));
|
||||
console.log(chalk.dim(`Usage: ccw cli output ${conversationId} [options]`));
|
||||
console.log(chalk.dim(' --verbose Show full metadata'));
|
||||
console.log(chalk.dim(' --raw Raw output (no formatting)'));
|
||||
console.log(chalk.dim(' --offset <n> Start from byte offset'));
|
||||
console.log(chalk.dim(' --limit <n> Limit output bytes'));
|
||||
console.log(chalk.dim(' --project <p> Specify project path explicitly'));
|
||||
console.log(chalk.dim(` --resume ccw cli -p "..." --resume ${conversationId}`));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -922,8 +925,8 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Generate execution ID for streaming (use custom ID or timestamp-based)
|
||||
const executionId = id || `${Date.now()}-${tool}`;
|
||||
// Generate execution ID for streaming (use custom ID or auto-generated readable ID)
|
||||
const executionId = id || generateExecutionId(tool);
|
||||
const startTime = Date.now();
|
||||
const modelInfo = model ? ` @${model}` : '';
|
||||
const spinnerBaseText = `Executing ${tool}${modelInfo} (${mode} mode${resumeInfo}${nativeMode})${idInfo}...`;
|
||||
@@ -989,9 +992,9 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec
|
||||
mode
|
||||
});
|
||||
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`[CLI] Generated executionId: ${executionId}`);
|
||||
}
|
||||
// Always output execution ID to stderr for programmatic capture
|
||||
// Callers can: ccw cli -p "..." 2>&1 | grep CCW_EXEC_ID
|
||||
console.error(`[CCW_EXEC_ID=${executionId}]`);
|
||||
|
||||
// Buffer to accumulate output when both --stream and --to-file are specified
|
||||
let streamBuffer = '';
|
||||
@@ -1306,6 +1309,214 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show all executions — active (running) + recent completed
|
||||
* Combines live dashboard state with SQLite history
|
||||
*/
|
||||
async function showAction(options: { all?: boolean }): Promise<void> {
|
||||
console.log(chalk.bold.cyan('\n CLI Executions\n'));
|
||||
|
||||
// 1. Try to fetch active executions from dashboard
|
||||
let activeExecs: Array<{
|
||||
id: string; tool: string; mode: string; status: string;
|
||||
prompt: string; startTime: number; isComplete?: boolean;
|
||||
}> = [];
|
||||
|
||||
try {
|
||||
const data = await new Promise<string>((resolve, reject) => {
|
||||
const req = http.request({
|
||||
hostname: 'localhost',
|
||||
port: Number(DASHBOARD_PORT),
|
||||
path: '/api/cli/active',
|
||||
method: 'GET',
|
||||
timeout: 2000,
|
||||
agent: false,
|
||||
headers: { 'Connection': 'close' }
|
||||
}, (res) => {
|
||||
let body = '';
|
||||
res.on('data', (chunk: Buffer) => { body += chunk.toString(); });
|
||||
res.on('end', () => resolve(body));
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
||||
req.end();
|
||||
});
|
||||
const parsed = JSON.parse(data);
|
||||
activeExecs = Array.isArray(parsed) ? parsed : (parsed.executions || []);
|
||||
} catch {
|
||||
// Dashboard not available — show only history
|
||||
}
|
||||
|
||||
// 2. Get recent history from SQLite
|
||||
const historyLimit = options.all ? 100 : 20;
|
||||
const history = await getExecutionHistoryAsync(process.cwd(), { limit: historyLimit, recursive: true });
|
||||
|
||||
// 3. Build unified list: active first, then history (de-duped)
|
||||
const seenIds = new Set<string>();
|
||||
const rows: Array<{
|
||||
id: string; tool: string; mode: string; status: string;
|
||||
prompt: string; time: string; duration: string;
|
||||
}> = [];
|
||||
|
||||
// Active executions (running)
|
||||
for (const exec of activeExecs) {
|
||||
if (exec.status === 'running') {
|
||||
seenIds.add(exec.id);
|
||||
const elapsed = Math.floor((Date.now() - exec.startTime) / 1000);
|
||||
rows.push({
|
||||
id: exec.id,
|
||||
tool: exec.tool,
|
||||
mode: exec.mode,
|
||||
status: 'running',
|
||||
prompt: (exec.prompt || '').replace(/\n/g, ' ').substring(0, 50),
|
||||
time: `${elapsed}s ago`,
|
||||
duration: `${elapsed}s...`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// History executions
|
||||
for (const exec of history.executions) {
|
||||
if (seenIds.has(exec.id)) continue;
|
||||
seenIds.add(exec.id);
|
||||
const duration = exec.duration_ms >= 1000
|
||||
? `${(exec.duration_ms / 1000).toFixed(1)}s`
|
||||
: `${exec.duration_ms}ms`;
|
||||
const timeAgo = getTimeAgo(new Date(exec.updated_at || exec.timestamp));
|
||||
rows.push({
|
||||
id: exec.id,
|
||||
tool: exec.tool,
|
||||
mode: exec.mode || 'analysis',
|
||||
status: exec.status,
|
||||
prompt: exec.prompt_preview.replace(/\n/g, ' ').substring(0, 50),
|
||||
time: timeAgo,
|
||||
duration,
|
||||
});
|
||||
}
|
||||
|
||||
if (rows.length === 0) {
|
||||
console.log(chalk.gray(' No executions found.\n'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. Render table
|
||||
console.log(chalk.gray(' Status Tool Mode Duration Time ID'));
|
||||
console.log(chalk.gray(' ' + '\u2500'.repeat(80)));
|
||||
|
||||
for (const row of rows) {
|
||||
const statusIcon = row.status === 'running' ? chalk.blue('\u25CF') :
|
||||
row.status === 'success' || row.status === 'completed' ? chalk.green('\u25CF') :
|
||||
row.status === 'timeout' ? chalk.yellow('\u25CF') : chalk.red('\u25CF');
|
||||
console.log(` ${statusIcon} ${chalk.bold.white(row.tool.padEnd(8))} ${chalk.gray(row.mode.padEnd(9))} ${chalk.gray(row.duration.padEnd(9))} ${chalk.gray(row.time.padEnd(11))} ${chalk.dim(row.id)}`);
|
||||
if (row.prompt) {
|
||||
console.log(chalk.gray(` ${row.prompt}${row.prompt.length >= 50 ? '...' : ''}`));
|
||||
}
|
||||
}
|
||||
|
||||
console.log();
|
||||
console.log(chalk.gray(' ' + '\u2500'.repeat(80)));
|
||||
console.log(chalk.dim(' Output: ccw cli output <id>'));
|
||||
console.log(chalk.dim(' Watch: ccw cli watch <id>'));
|
||||
console.log(chalk.dim(' Detail: ccw cli detail <id>'));
|
||||
console.log();
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch a running execution — stream output to stderr until completion
|
||||
* Exits with code 0 (success), 1 (error), or 2 (timeout)
|
||||
*/
|
||||
async function watchAction(watchId: string | undefined, options: { timeout?: string }): Promise<void> {
|
||||
if (!watchId) {
|
||||
console.error(chalk.red('Error: Execution ID is required'));
|
||||
console.error(chalk.gray('Usage: ccw cli watch <id> [--timeout 120]'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const timeoutMs = options.timeout ? parseInt(options.timeout, 10) * 1000 : 0;
|
||||
const startTime = Date.now();
|
||||
|
||||
process.stderr.write(chalk.cyan(`Watching execution: ${watchId}\n`));
|
||||
|
||||
// Track output position for incremental display
|
||||
let lastOutputLen = 0;
|
||||
|
||||
const poll = async (): Promise<number> => {
|
||||
// Check timeout
|
||||
if (timeoutMs > 0 && (Date.now() - startTime) > timeoutMs) {
|
||||
process.stderr.write(chalk.yellow('\nWatch timed out.\n'));
|
||||
return 2;
|
||||
}
|
||||
|
||||
try {
|
||||
// Fetch active execution state from dashboard
|
||||
const data = await new Promise<string>((resolve, reject) => {
|
||||
const req = http.request({
|
||||
hostname: 'localhost',
|
||||
port: Number(DASHBOARD_PORT),
|
||||
path: '/api/cli/active',
|
||||
method: 'GET',
|
||||
timeout: 3000,
|
||||
agent: false,
|
||||
headers: { 'Connection': 'close' }
|
||||
}, (res) => {
|
||||
let body = '';
|
||||
res.on('data', (chunk: Buffer) => { body += chunk.toString(); });
|
||||
res.on('end', () => resolve(body));
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
||||
req.end();
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(data);
|
||||
const executions = Array.isArray(parsed) ? parsed : (parsed.executions || []);
|
||||
const exec = executions.find((e: { id: string }) => e.id === watchId);
|
||||
|
||||
if (exec) {
|
||||
// Show incremental output
|
||||
const fullOutput = exec.output || '';
|
||||
if (fullOutput.length > lastOutputLen) {
|
||||
process.stderr.write(fullOutput.slice(lastOutputLen));
|
||||
lastOutputLen = fullOutput.length;
|
||||
}
|
||||
|
||||
if (exec.status === 'running') {
|
||||
// Still running — wait and poll again
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
return poll();
|
||||
}
|
||||
|
||||
// Completed
|
||||
process.stderr.write(chalk.green(`\nExecution ${exec.status === 'completed' || exec.status === 'success' ? 'completed' : 'failed'}.\n`));
|
||||
return (exec.status === 'completed' || exec.status === 'success') ? 0 : 1;
|
||||
}
|
||||
} catch {
|
||||
// Dashboard not available
|
||||
}
|
||||
|
||||
// Not found in active — check SQLite history
|
||||
const store = getHistoryStore(process.cwd());
|
||||
const result = store.getCachedOutput(watchId);
|
||||
if (result) {
|
||||
process.stderr.write(chalk.gray(`\nExecution already completed (status: ${result.status}).\n`));
|
||||
process.stderr.write(chalk.dim(`Use: ccw cli output ${watchId}\n`));
|
||||
return result.status === 'success' ? 0 : 1;
|
||||
}
|
||||
|
||||
// Not found anywhere — may still be starting, wait and retry a few times
|
||||
if ((Date.now() - startTime) < 10000) {
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
return poll();
|
||||
}
|
||||
|
||||
process.stderr.write(chalk.red(`\nExecution not found: ${watchId}\n`));
|
||||
return 1;
|
||||
};
|
||||
|
||||
const exitCode = await poll();
|
||||
process.exit(exitCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show execution history
|
||||
* @param {Object} options - CLI options
|
||||
@@ -1455,6 +1666,14 @@ export async function cliCommand(
|
||||
await historyAction(options as HistoryOptions);
|
||||
break;
|
||||
|
||||
case 'show':
|
||||
await showAction(options as unknown as { all?: boolean });
|
||||
break;
|
||||
|
||||
case 'watch':
|
||||
await watchAction(argsArray[0], options as unknown as { timeout?: string });
|
||||
break;
|
||||
|
||||
case 'detail':
|
||||
await detailAction(argsArray[0]);
|
||||
break;
|
||||
@@ -1506,11 +1725,13 @@ export async function cliCommand(
|
||||
console.log(chalk.gray(' echo "prompt" | ccw cli --tool <tool> Execute from stdin (pipe)'));
|
||||
console.log();
|
||||
console.log(' Subcommands:');
|
||||
console.log(chalk.gray(' show List all executions (active + recent)'));
|
||||
console.log(chalk.gray(' watch <id> Stream execution output (stderr, exits on completion)'));
|
||||
console.log(chalk.gray(' output <id> Get final execution result'));
|
||||
console.log(chalk.gray(' status Check CLI tools availability'));
|
||||
console.log(chalk.gray(' storage [cmd] Manage CCW storage (info/clean/config)'));
|
||||
console.log(chalk.gray(' history Show execution history'));
|
||||
console.log(chalk.gray(' detail <id> Show execution detail'));
|
||||
console.log(chalk.gray(' output <id> Show execution output with pagination'));
|
||||
console.log(chalk.gray(' detail <id> Show execution detail (legacy, use show/output)'));
|
||||
console.log(chalk.gray(' test-parse [args] Debug CLI argument parsing'));
|
||||
console.log();
|
||||
console.log(' Options:');
|
||||
@@ -1523,7 +1744,7 @@ export async function cliCommand(
|
||||
console.log(chalk.gray(' --effort <level> Effort level for claude (low, medium, high)'));
|
||||
console.log(chalk.gray(' --cd <path> Working directory'));
|
||||
console.log(chalk.gray(' --includeDirs <dirs> Additional directories'));
|
||||
// --timeout removed - controlled by external caller (bash timeout)
|
||||
console.log(chalk.gray(' --id <id> Execution ID (recommended, auto-generated if omitted)'));
|
||||
console.log(chalk.gray(' --resume [id] Resume previous session'));
|
||||
console.log(chalk.gray(' --cache <items> Cache: comma-separated @patterns and text'));
|
||||
console.log(chalk.gray(' --inject-mode <m> Inject mode: none, full, progressive'));
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
deleteExecutionAsync,
|
||||
batchDeleteExecutionsAsync,
|
||||
executeCliTool,
|
||||
generateExecutionId,
|
||||
getNativeSessionContent,
|
||||
getFormattedNativeConversation,
|
||||
getEnrichedConversation,
|
||||
@@ -940,7 +941,7 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
const executionId = `${Date.now()}-${tool}`;
|
||||
const executionId = generateExecutionId(tool);
|
||||
|
||||
// Store active execution for state recovery
|
||||
// Check map size limit before creating new execution
|
||||
|
||||
@@ -11,13 +11,22 @@
|
||||
|
||||
import type { RouteContext } from './types.js';
|
||||
import { getDeepWikiService } from '../../services/deepwiki-service.js';
|
||||
import { handleDeepWikiRoutes as handleDeepWikiPostRoutes } from '../../services/deepwiki-service.js';
|
||||
|
||||
/**
|
||||
* Handle DeepWiki routes
|
||||
* @returns true if route was handled, false otherwise
|
||||
*/
|
||||
export async function handleDeepWikiRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
const { pathname, url, res } = ctx;
|
||||
const { pathname, url, res, req } = ctx;
|
||||
|
||||
// POST endpoints implemented in the DeepWiki service module.
|
||||
if (
|
||||
(pathname === '/api/deepwiki/symbols-for-paths' || pathname === '/api/deepwiki/stale-files') &&
|
||||
req.method === 'POST'
|
||||
) {
|
||||
return handleDeepWikiPostRoutes({ pathname, req, res });
|
||||
}
|
||||
|
||||
// GET /api/deepwiki/files - List all documented files
|
||||
if (pathname === '/api/deepwiki/files') {
|
||||
|
||||
@@ -17,6 +17,7 @@ import { getLaunchConfig } from './cli-launch-registry.js';
|
||||
import { assembleInstruction, type InstructionType } from './cli-instruction-assembler.js';
|
||||
import { loadEndpointSettings } from '../../config/cli-settings-manager.js';
|
||||
import { getToolConfig } from '../../tools/claude-cli-tools.js';
|
||||
import { generateExecutionId } from '../../tools/cli-executor.js';
|
||||
|
||||
export interface CliSession {
|
||||
sessionKey: string;
|
||||
@@ -532,7 +533,7 @@ export class CliSessionManager {
|
||||
|
||||
const executionId = resumeKey
|
||||
? `${resumeKey}-${Date.now()}`
|
||||
: `exec-${Date.now()}-${randomBytes(3).toString('hex')}`;
|
||||
: generateExecutionId(options.tool);
|
||||
|
||||
let command: string;
|
||||
|
||||
|
||||
@@ -35,22 +35,31 @@ import {
|
||||
saveConversation
|
||||
} from './cli-executor-state.js';
|
||||
|
||||
// Track all running child processes for cleanup on interruption (multi-process support)
|
||||
const runningChildProcesses = new Set<ChildProcess>();
|
||||
|
||||
// Debug logging for parallel execution testing
|
||||
// Debug logging for history save investigation (Iteration 4)
|
||||
const DEBUG_SESSION_ID = 'DBG-parallel-ccw-cli-test-2026-03-07';
|
||||
const DEBUG_LOG_PATH = path.join(process.cwd(), '.workflow', '.debug', DEBUG_SESSION_ID, 'debug.log');
|
||||
const DEBUG_LOG_PATH = path.join(process.cwd(), '.workflow', '.debug', DEBUG_SESSION_ID, 'debug-save.log');
|
||||
|
||||
// Ensure debug log directory exists
|
||||
try {
|
||||
const debugDir = path.dirname(DEBUG_LOG_PATH);
|
||||
if (!fs.existsSync(debugDir)) {
|
||||
fs.mkdirSync(debugDir, { recursive: true });
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore directory creation errors
|
||||
}
|
||||
|
||||
function writeDebugLog(event: string, data: Record<string, any>): void {
|
||||
try {
|
||||
const logEntry = JSON.stringify({ event, ...data, timestamp: new Date().toISOString() }) + '\n';
|
||||
fs.appendFileSync(DEBUG_LOG_PATH, logEntry, 'utf8');
|
||||
} catch (err) {
|
||||
// Silently ignore logging errors to avoid disrupting execution
|
||||
// Silently ignore logging errors
|
||||
}
|
||||
}
|
||||
|
||||
// Track all running child processes for cleanup on interruption (multi-process support)
|
||||
const runningChildProcesses = new Set<ChildProcess>();
|
||||
|
||||
/**
|
||||
* Kill all running CLI child processes
|
||||
@@ -58,16 +67,14 @@ function writeDebugLog(event: string, data: Record<string, any>): void {
|
||||
*/
|
||||
export function killAllCliProcesses(): boolean {
|
||||
if (runningChildProcesses.size === 0) return false;
|
||||
writeDebugLog('KILL_ALL_START', { initial_set_size: runningChildProcesses.size });
|
||||
|
||||
const processesToKill = Array.from(runningChildProcesses);
|
||||
debugLog('KILL', `Killing ${processesToKill.length} child process(es)`, { pids: processesToKill.map(p => p.pid) });
|
||||
writeDebugLog('KILL_ALL_COPY', { pids_to_kill: processesToKill.map(p => p.pid) });
|
||||
|
||||
// 1. SIGTERM for graceful shutdown
|
||||
for (const child of processesToKill) {
|
||||
if (!child.killed) {
|
||||
try { child.kill('SIGTERM'); } catch (e: any) { writeDebugLog('KILL_SIGTERM_ERROR', { pid: child.pid, error: e.message }); }
|
||||
try { child.kill('SIGTERM'); } catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,13 +82,12 @@ export function killAllCliProcesses(): boolean {
|
||||
const killTimeout = setTimeout(() => {
|
||||
for (const child of processesToKill) {
|
||||
if (!child.killed) {
|
||||
try { child.kill('SIGKILL'); } catch (e: any) { writeDebugLog('KILL_SIGKILL_ERROR', { pid: child.pid, error: e.message }); }
|
||||
try { child.kill('SIGKILL'); } catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
}, 2000);
|
||||
killTimeout.unref();
|
||||
|
||||
writeDebugLog('KILL_ALL_CLEAR', { set_size_before: runningChildProcesses.size, pids_in_set: Array.from(runningChildProcesses).map(p => p.pid) });
|
||||
runningChildProcesses.clear();
|
||||
return true;
|
||||
}
|
||||
@@ -257,7 +263,6 @@ async function executeClaudeWithSettings(params: ClaudeWithSettingsParams): Prom
|
||||
|
||||
// Track child process for cleanup (multi-process support)
|
||||
runningChildProcesses.add(child);
|
||||
writeDebugLog('PROCESS_ADD', { pid: child.pid, set_size_after: runningChildProcesses.size, function: 'executeClaudeWithSettings' });
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
@@ -297,7 +302,6 @@ async function executeClaudeWithSettings(params: ClaudeWithSettingsParams): Prom
|
||||
|
||||
child.on('close', (code) => {
|
||||
runningChildProcesses.delete(child);
|
||||
writeDebugLog('PROCESS_DELETE', { pid: child.pid, exit_code: code, set_size_after: runningChildProcesses.size, function: 'executeClaudeWithSettings', handler: 'close' });
|
||||
|
||||
const endTime = Date.now();
|
||||
const duration = endTime - startTime;
|
||||
@@ -338,10 +342,8 @@ async function executeClaudeWithSettings(params: ClaudeWithSettingsParams): Prom
|
||||
|
||||
// Save to history
|
||||
try {
|
||||
writeDebugLog('SAVE_CONVERSATION_START', { conversationId: conversation.id, pid: child.pid, function: 'executeClaudeWithSettings' });
|
||||
saveConversation(workingDir, conversation);
|
||||
} catch (err) {
|
||||
writeDebugLog('SAVE_CONVERSATION_ERROR', { conversationId: conversation.id, pid: child.pid, error: (err as Error).message, stack: (err as Error).stack, function: 'executeClaudeWithSettings' });
|
||||
console.error('[CLI Executor] Failed to save CLI封装 history:', (err as Error).message);
|
||||
}
|
||||
|
||||
@@ -356,7 +358,6 @@ async function executeClaudeWithSettings(params: ClaudeWithSettingsParams): Prom
|
||||
|
||||
child.on('error', (error) => {
|
||||
runningChildProcesses.delete(child);
|
||||
writeDebugLog('PROCESS_DELETE', { pid: child.pid, set_size_after: runningChildProcesses.size, function: 'executeClaudeWithSettings', handler: 'error' });
|
||||
reject(new Error(`Failed to spawn claude: ${error.message}`));
|
||||
});
|
||||
});
|
||||
@@ -391,6 +392,30 @@ type BuiltinCliTool = typeof BUILTIN_CLI_TOOLS[number];
|
||||
*/
|
||||
export type TransactionId = string;
|
||||
|
||||
/**
|
||||
* Generate a readable execution ID for CLI executions
|
||||
* Format: {prefix}-{HHmmss}-{rand4} → e.g. gem-143022-x7k2
|
||||
* @param tool - CLI tool name (gemini, qwen, codex, claude, opencode, litellm, etc.)
|
||||
* @returns Short, human-readable execution ID
|
||||
*/
|
||||
export function generateExecutionId(tool: string): string {
|
||||
const prefixMap: Record<string, string> = {
|
||||
gemini: 'gem',
|
||||
qwen: 'qwn',
|
||||
codex: 'cdx',
|
||||
claude: 'cld',
|
||||
opencode: 'opc',
|
||||
litellm: 'llm',
|
||||
};
|
||||
const prefix = prefixMap[tool] || tool.slice(0, 3);
|
||||
const now = new Date();
|
||||
const time = [now.getHours(), now.getMinutes(), now.getSeconds()]
|
||||
.map(n => String(n).padStart(2, '0'))
|
||||
.join('');
|
||||
const rand = Math.random().toString(36).slice(2, 6);
|
||||
return `${prefix}-${time}-${rand}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique transaction ID for the current execution
|
||||
* @param conversationId - CCW conversation ID
|
||||
@@ -526,7 +551,7 @@ async function executeCliTool(
|
||||
const duration = endTime - startTime;
|
||||
|
||||
const execution: ExecutionRecord = {
|
||||
id: customId || `${Date.now()}-litellm`,
|
||||
id: customId || generateExecutionId('litellm'),
|
||||
timestamp: new Date(startTime).toISOString(),
|
||||
tool: 'litellm',
|
||||
model: result.model,
|
||||
@@ -676,7 +701,7 @@ async function executeCliTool(
|
||||
const duration = endTime - startTime;
|
||||
|
||||
const execution: ExecutionRecord = {
|
||||
id: customId || `${Date.now()}-litellm`,
|
||||
id: customId || generateExecutionId('litellm'),
|
||||
timestamp: new Date(startTime).toISOString(),
|
||||
tool: toolName,
|
||||
model: result.model, // Use effective model from result (reflects any override)
|
||||
@@ -789,11 +814,11 @@ async function executeCliTool(
|
||||
existingConversation = loadConversation(workingDir, conversationId);
|
||||
} else {
|
||||
// No previous conversation, create new
|
||||
conversationId = `${Date.now()}-${tool}`;
|
||||
conversationId = generateExecutionId(tool);
|
||||
}
|
||||
} else {
|
||||
// New conversation with auto-generated ID
|
||||
conversationId = `${Date.now()}-${tool}`;
|
||||
conversationId = generateExecutionId(tool);
|
||||
}
|
||||
|
||||
// Generate transaction ID for concurrent session disambiguation
|
||||
@@ -1019,7 +1044,6 @@ async function executeCliTool(
|
||||
|
||||
// Track child process for cleanup on interruption (multi-process support)
|
||||
runningChildProcesses.add(child);
|
||||
writeDebugLog('PROCESS_ADD', { pid: child.pid, set_size_after: runningChildProcesses.size, function: 'executeCliTool', tool });
|
||||
|
||||
debugLog('SPAWN', `Process spawned`, { pid: child.pid });
|
||||
|
||||
@@ -1071,7 +1095,6 @@ async function executeCliTool(
|
||||
child.on('close', async (code) => {
|
||||
// Remove from running processes
|
||||
runningChildProcesses.delete(child);
|
||||
writeDebugLog('PROCESS_DELETE', { pid: child.pid, exit_code: code, set_size_after: runningChildProcesses.size, function: 'executeCliTool', handler: 'close', tool });
|
||||
|
||||
// Flush remaining buffer from parser
|
||||
const remainingUnits = parser.flush();
|
||||
@@ -1200,11 +1223,9 @@ async function executeCliTool(
|
||||
// Save all source conversations
|
||||
try {
|
||||
for (const conv of savedConversations) {
|
||||
writeDebugLog('SAVE_CONVERSATION_START', { conversationId: conv.id, pid: child.pid, function: 'executeCliTool', context: 'merge-loop', tool });
|
||||
saveConversation(workingDir, conv);
|
||||
}
|
||||
} catch (err) {
|
||||
writeDebugLog('SAVE_CONVERSATION_ERROR', { pid: child.pid, error: (err as Error).message, stack: (err as Error).stack, function: 'executeCliTool', context: 'merge-loop', tool });
|
||||
console.error('[CLI Executor] Failed to save merged histories:', (err as Error).message);
|
||||
}
|
||||
} else if (isMerge && mergeResult && customId) {
|
||||
@@ -1244,10 +1265,8 @@ async function executeCliTool(
|
||||
};
|
||||
// Save merged conversation
|
||||
try {
|
||||
writeDebugLog('SAVE_CONVERSATION_START', { conversationId: conversation.id, pid: child.pid, function: 'executeCliTool', context: 'merge-with-id', tool });
|
||||
saveConversation(workingDir, conversation);
|
||||
} catch (err) {
|
||||
writeDebugLog('SAVE_CONVERSATION_ERROR', { conversationId: conversation.id, pid: child.pid, error: (err as Error).message, stack: (err as Error).stack, function: 'executeCliTool', context: 'merge-with-id', tool });
|
||||
console.error('[CLI Executor] Failed to save merged conversation:', (err as Error).message);
|
||||
}
|
||||
} else {
|
||||
@@ -1277,10 +1296,11 @@ async function executeCliTool(
|
||||
};
|
||||
// Try to save conversation to history
|
||||
try {
|
||||
writeDebugLog('SAVE_CONVERSATION_START', { conversationId: conversation.id, pid: child.pid, function: 'executeCliTool', context: 'normal', tool });
|
||||
writeDebugLog('BEFORE_SAVE_CONV', { conversationId: conversation.id, workingDir, tool });
|
||||
saveConversation(workingDir, conversation);
|
||||
writeDebugLog('AFTER_SAVE_CONV', { conversationId: conversation.id, workingDir, tool });
|
||||
} catch (err) {
|
||||
writeDebugLog('SAVE_CONVERSATION_ERROR', { conversationId: conversation.id, pid: child.pid, error: (err as Error).message, stack: (err as Error).stack, function: 'executeCliTool', context: 'normal', tool });
|
||||
writeDebugLog('SAVE_CONV_OUTER_ERROR', { conversationId: conversation.id, workingDir, tool, error: (err as Error).message, stack: (err as Error).stack });
|
||||
// Non-fatal: continue even if history save fails
|
||||
console.error('[CLI Executor] Failed to save history:', (err as Error).message);
|
||||
}
|
||||
@@ -1341,7 +1361,6 @@ async function executeCliTool(
|
||||
child.on('error', (error) => {
|
||||
// Remove from running processes
|
||||
runningChildProcesses.delete(child);
|
||||
writeDebugLog('PROCESS_DELETE', { pid: child.pid, set_size_after: runningChildProcesses.size, function: 'executeCliTool', handler: 'error', tool });
|
||||
|
||||
errorLog('SPAWN', `Failed to spawn process`, error, {
|
||||
tool,
|
||||
|
||||
@@ -6,6 +6,31 @@
|
||||
import type { HistoryIndexEntry } from './cli-history-store.js';
|
||||
import { StoragePaths, ensureStorageDir } from '../config/storage-paths.js';
|
||||
import type { CliOutputUnit } from './cli-output-converter.js';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
// Debug logging for history save investigation (Iteration 4)
|
||||
const DEBUG_SESSION_ID = 'DBG-parallel-ccw-cli-test-2026-03-07';
|
||||
const DEBUG_LOG_PATH = path.join(process.cwd(), '.workflow', '.debug', DEBUG_SESSION_ID, 'debug-save.log');
|
||||
|
||||
// Ensure debug log directory exists
|
||||
try {
|
||||
const debugDir = path.dirname(DEBUG_LOG_PATH);
|
||||
if (!fs.existsSync(debugDir)) {
|
||||
fs.mkdirSync(debugDir, { recursive: true });
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore directory creation errors
|
||||
}
|
||||
|
||||
function writeDebugLog(event: string, data: Record<string, any>): void {
|
||||
try {
|
||||
const logEntry = JSON.stringify({ event, ...data, timestamp: new Date().toISOString() }) + '\n';
|
||||
fs.appendFileSync(DEBUG_LOG_PATH, logEntry, 'utf8');
|
||||
} catch (err) {
|
||||
// Silently ignore logging errors
|
||||
}
|
||||
}
|
||||
|
||||
// Lazy-loaded SQLite store module
|
||||
let sqliteStoreModule: typeof import('./cli-history-store.js') | null = null;
|
||||
@@ -14,8 +39,10 @@ let sqliteStoreModule: typeof import('./cli-history-store.js') | null = null;
|
||||
* Get or initialize SQLite store (async)
|
||||
*/
|
||||
export async function getSqliteStore(baseDir: string) {
|
||||
writeDebugLog('GET_STORE', { baseDir, baseDirType: typeof baseDir, moduleInitialized: sqliteStoreModule !== null });
|
||||
if (!sqliteStoreModule) {
|
||||
sqliteStoreModule = await import('./cli-history-store.js');
|
||||
writeDebugLog('MODULE_LOADED', { baseDir });
|
||||
}
|
||||
return sqliteStoreModule.getHistoryStore(baseDir);
|
||||
}
|
||||
@@ -136,15 +163,20 @@ async function saveConversationAsync(baseDir: string, conversation: Conversation
|
||||
* @param baseDir - Project base directory (NOT historyDir)
|
||||
*/
|
||||
export function saveConversation(baseDir: string, conversation: ConversationRecord): void {
|
||||
writeDebugLog('SAVE_CONV_START', { baseDir, conversationId: conversation.id, moduleInitialized: sqliteStoreModule !== null });
|
||||
try {
|
||||
const store = getSqliteStoreSync(baseDir);
|
||||
writeDebugLog('SAVE_CONV_SYNC', { baseDir, conversationId: conversation.id });
|
||||
// Fire and forget - don't block on async save in sync context
|
||||
store.saveConversation(conversation).catch(err => {
|
||||
writeDebugLog('SAVE_CONV_ERROR', { baseDir, conversationId: conversation.id, error: err.message, stack: err.stack });
|
||||
console.error('[CLI Executor] Failed to save conversation:', err.message);
|
||||
});
|
||||
} catch {
|
||||
} catch (err) {
|
||||
writeDebugLog('SAVE_CONV_FALLBACK_ASYNC', { baseDir, conversationId: conversation.id, error: (err as Error).message });
|
||||
// If sync not available, queue for async save
|
||||
saveConversationAsync(baseDir, conversation).catch(err => {
|
||||
writeDebugLog('SAVE_CONV_ASYNC_ERROR', { baseDir, conversationId: conversation.id, error: err.message, stack: err.stack });
|
||||
console.error('[CLI Executor] Failed to save conversation:', err.message);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,13 +4,36 @@
|
||||
*/
|
||||
|
||||
import Database from 'better-sqlite3';
|
||||
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, rmdirSync } from 'fs';
|
||||
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, rmdirSync, appendFileSync } from 'fs';
|
||||
import { join, dirname, resolve } from 'path';
|
||||
import { parseSessionFile, formatConversation, extractConversationPairs, type ParsedSession, type ParsedTurn } from './session-content-parser.js';
|
||||
import { getDiscoverer, getNativeSessions } from './native-session-discovery.js';
|
||||
import { StoragePaths, ensureStorageDir, getProjectId, getCCWHome } from '../config/storage-paths.js';
|
||||
import type { CliOutputUnit } from './cli-output-converter.js';
|
||||
|
||||
// Debug logging for history save investigation (Iteration 4)
|
||||
const DEBUG_SESSION_ID = 'DBG-parallel-ccw-cli-test-2026-03-07';
|
||||
const DEBUG_LOG_PATH = join(process.cwd(), '.workflow', '.debug', DEBUG_SESSION_ID, 'debug-save.log');
|
||||
|
||||
// Ensure debug log directory exists
|
||||
try {
|
||||
const debugDir = dirname(DEBUG_LOG_PATH);
|
||||
if (!existsSync(debugDir)) {
|
||||
mkdirSync(debugDir, { recursive: true });
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore directory creation errors
|
||||
}
|
||||
|
||||
function writeDebugLog(event: string, data: Record<string, any>): void {
|
||||
try {
|
||||
const logEntry = JSON.stringify({ event, ...data, timestamp: new Date().toISOString() }) + '\n';
|
||||
appendFileSync(DEBUG_LOG_PATH, logEntry, 'utf8');
|
||||
} catch (err) {
|
||||
// Silently ignore logging errors
|
||||
}
|
||||
}
|
||||
|
||||
// Types
|
||||
export interface ConversationTurn {
|
||||
turn: number;
|
||||
@@ -110,22 +133,29 @@ export class CliHistoryStore {
|
||||
private projectPath: string;
|
||||
|
||||
constructor(baseDir: string) {
|
||||
writeDebugLog('STORE_CONSTRUCT_START', { baseDir });
|
||||
this.projectPath = baseDir;
|
||||
|
||||
// Use centralized storage path
|
||||
const paths = StoragePaths.project(baseDir);
|
||||
const historyDir = paths.cliHistory;
|
||||
writeDebugLog('STORAGE_PATHS', { baseDir, historyDir, historyDb: paths.historyDb });
|
||||
ensureStorageDir(historyDir);
|
||||
|
||||
this.dbPath = paths.historyDb;
|
||||
writeDebugLog('DB_INSTANCE_CREATE', { dbPath: this.dbPath });
|
||||
this.db = new Database(this.dbPath);
|
||||
writeDebugLog('DB_INSTANCE_CREATED', { dbPath: this.dbPath });
|
||||
this.db.pragma('journal_mode = WAL');
|
||||
this.db.pragma('synchronous = NORMAL');
|
||||
this.db.pragma('busy_timeout = 10000'); // Wait up to 10 seconds for locks (increased for write-heavy scenarios)
|
||||
this.db.pragma('wal_autocheckpoint = 1000'); // Optimize WAL checkpointing
|
||||
|
||||
writeDebugLog('INIT_SCHEMA_START', { dbPath: this.dbPath });
|
||||
this.initSchema();
|
||||
writeDebugLog('INIT_SCHEMA_COMPLETE', { dbPath: this.dbPath });
|
||||
this.migrateFromJson(historyDir);
|
||||
writeDebugLog('STORE_CONSTRUCT_COMPLETE', { baseDir, dbPath: this.dbPath });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -936,11 +966,7 @@ export class CliHistoryStore {
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO native_session_mapping (ccw_id, tool, native_session_id, native_session_path, project_hash, transaction_id, created_at)
|
||||
VALUES (@ccw_id, @tool, @native_session_id, @native_session_path, @project_hash, @transaction_id, @created_at)
|
||||
ON CONFLICT(ccw_id) DO UPDATE SET
|
||||
native_session_id = @native_session_id,
|
||||
native_session_path = @native_session_path,
|
||||
project_hash = @project_hash,
|
||||
transaction_id = @transaction_id
|
||||
ON CONFLICT(tool, native_session_id) DO NOTHING
|
||||
`);
|
||||
|
||||
await this.withRetry(() => stmt.run({
|
||||
|
||||
Reference in New Issue
Block a user