From b8c807b2f9d5dce9192934ca47899b1465834225 Mon Sep 17 00:00:00 2001 From: catlog22 Date: Sat, 24 Jan 2026 21:17:21 +0800 Subject: [PATCH] feat: add parent/child directory lookup for ccw cli output - Implement findProjectWithExecution() to search upward through parent directories - Add automatic project path discovery in outputAction - Support explicit --project parameter for manual path specification - Improve error messages with search scope indication - Display project path in formatted output - Enable cross-directory execution without working directory dependency --- ccw/src/commands/cli.ts | 41 +++++++++++-- ccw/src/tools/cli-history-store.ts | 94 +++++++++++++++++++++++++++++- 2 files changed, 128 insertions(+), 7 deletions(-) diff --git a/ccw/src/commands/cli.ts b/ccw/src/commands/cli.ts index bac9111a..709dcfa1 100644 --- a/ccw/src/commands/cli.ts +++ b/ccw/src/commands/cli.ts @@ -28,7 +28,7 @@ import { projectExists, getStorageLocationInstructions } from '../tools/storage-manager.js'; -import { getHistoryStore } from '../tools/cli-history-store.js'; +import { getHistoryStore, findProjectWithExecution } from '../tools/cli-history-store.js'; import { createSpinner } from '../utils/ui.js'; import { loadClaudeCliSettings } from '../tools/claude-cli-tools.js'; @@ -163,6 +163,7 @@ interface OutputViewOptions { turn?: string; raw?: boolean; final?: boolean; // Only output final result with usage hint + project?: string; // Optional project path for lookup } /** @@ -355,16 +356,21 @@ function showStorageHelp(): void { /** * Show cached output for a conversation with pagination + * Supports automatic discovery of project path from current directory or parents */ async function outputAction(conversationId: string | undefined, options: OutputViewOptions): Promise { if (!conversationId) { console.error(chalk.red('Error: Conversation ID is required')); - console.error(chalk.gray('Usage: ccw cli output [--offset N] [--limit N]')); + console.error(chalk.gray('Usage: ccw cli output [--offset N] [--limit N] [--project ]')); process.exit(1); } - const store = getHistoryStore(process.cwd()); - const result = store.getCachedOutput( + // Determine project path to use + let projectPath = options.project || process.cwd(); + let store = getHistoryStore(projectPath); + + // Try to get result from specified/current directory + let result = store.getCachedOutput( conversationId, options.turn ? parseInt(options.turn) : undefined, { @@ -374,8 +380,31 @@ async function outputAction(conversationId: string | undefined, options: OutputV } ); + // If not found and no explicit project specified, try to find it + if (!result && !options.project) { + const found = findProjectWithExecution(conversationId, process.cwd()); + if (found) { + projectPath = found.projectPath; + store = getHistoryStore(projectPath); + result = store.getCachedOutput( + conversationId, + options.turn ? parseInt(options.turn) : undefined, + { + offset: parseInt(options.offset || '0'), + limit: parseInt(options.limit || '10000'), + outputType: options.outputType || 'both' + } + ); + } + } + if (!result) { + const hint = options.project + ? `in project: ${options.project}` + : 'in current directory or parent directories'; console.error(chalk.red(`Error: Execution not found: ${conversationId}`)); + console.error(chalk.gray(` Searched ${hint}`)); + console.error(chalk.gray('Usage: ccw cli output [--project ]')); process.exit(1); } @@ -395,9 +424,10 @@ async function outputAction(conversationId: string | undefined, options: OutputV 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 hint)')); + console.log(chalk.dim(' --raw Raw output (no formatting)')); console.log(chalk.dim(' --offset Start from byte offset')); console.log(chalk.dim(' --limit Limit output bytes')); + console.log(chalk.dim(' --project

Specify project path explicitly')); console.log(chalk.dim(` --resume ccw cli -p "..." --resume ${conversationId}`)); return; } @@ -409,6 +439,7 @@ async function outputAction(conversationId: string | undefined, options: OutputV 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(); if (result.stdout) { diff --git a/ccw/src/tools/cli-history-store.ts b/ccw/src/tools/cli-history-store.ts index 7a5d560a..89bf22c3 100644 --- a/ccw/src/tools/cli-history-store.ts +++ b/ccw/src/tools/cli-history-store.ts @@ -5,9 +5,9 @@ import Database from 'better-sqlite3'; import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, rmdirSync } from 'fs'; -import { join } from 'path'; +import { join, dirname, resolve } from 'path'; import { parseSessionFile, formatConversation, extractConversationPairs, type ParsedSession, type ParsedTurn } from './session-content-parser.js'; -import { StoragePaths, ensureStorageDir, getProjectId } from '../config/storage-paths.js'; +import { StoragePaths, ensureStorageDir, getProjectId, getCCWHome } from '../config/storage-paths.js'; import type { CliOutputUnit } from './cli-output-converter.js'; // Types @@ -1404,5 +1404,95 @@ export function closeAllStores(): void { storeCache.clear(); } +/** + * Find project path that contains the given execution + * Searches upward through parent directories and all registered projects + * @param conversationId - Execution ID to search for + * @param startDir - Starting directory (default: process.cwd()) + * @returns Object with projectPath and projectId if found, null otherwise + */ +export function findProjectWithExecution( + conversationId: string, + startDir: string = process.cwd() +): { projectPath: string; projectId: string } | null { + // Strategy 1: Search upward in parent directories + let currentPath = resolve(startDir); + const visited = new Set(); + + while (true) { + // Avoid infinite loops + if (visited.has(currentPath)) break; + visited.add(currentPath); + + const projectId = getProjectId(currentPath); + const paths = StoragePaths.project(currentPath); + + // Check if database exists for this path + if (existsSync(paths.historyDb)) { + try { + const store = getHistoryStore(currentPath); + const result = store.getCachedOutput(conversationId); + if (result) { + return { projectPath: currentPath, projectId }; + } + } catch { + // Database might be locked or corrupted, continue searching + } + } + + // Move to parent directory + const parentPath = dirname(currentPath); + if (parentPath === currentPath) { + // Reached filesystem root + break; + } + currentPath = parentPath; + } + + // Strategy 2: Search in all registered projects (global search) + // This covers cases where execution might be in a completely different project tree + const projectsDir = join(getCCWHome(), 'projects'); + if (existsSync(projectsDir)) { + try { + const entries = readdirSync(projectsDir, { withFileTypes: true }); + + for (const entry of entries) { + if (!entry.isDirectory()) continue; + + const projectId = entry.name; + const historyDb = join(projectsDir, projectId, 'cli-history', 'history.db'); + + if (!existsSync(historyDb)) continue; + + try { + // Open and query this database directly + const db = new Database(historyDb, { readonly: true }); + const turn = db.prepare(` + SELECT * FROM turns + WHERE conversation_id = ? + ORDER BY turn_number DESC + LIMIT 1 + `).get(conversationId); + + db.close(); + + if (turn) { + // Found in this project - return the projectId + // Note: projectPath is set to projectId since we don't have the original path stored + return { projectPath: projectId, projectId }; + } + } catch { + // Skip this database (might be corrupted or locked) + continue; + } + } + } catch { + // Failed to read projects directory + } + } + + return null; +} + // Re-export types from session-content-parser export type { ParsedSession, ParsedTurn } from './session-content-parser.js';