mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-09 02:24:11 +08:00
feat: Enhance navigation and cleanup for graph explorer view
- Added a cleanup function to reset the state when navigating away from the graph explorer. - Updated navigation logic to call the cleanup function before switching views. - Improved internationalization by adding new translations for graph-related terms. - Adjusted icon sizes for better UI consistency in the graph explorer. - Implemented impact analysis button functionality in the graph explorer. - Refactored CLI tool configuration to use updated model names. - Enhanced CLI executor to handle prompts correctly for codex commands. - Introduced code relationship storage for better visualization in the index tree. - Added support for parsing Markdown and plain text files in the symbol parser. - Updated tests to reflect changes in language detection logic.
This commit is contained in:
@@ -27,7 +27,7 @@ export type CliToolName = 'gemini' | 'qwen' | 'codex';
|
||||
export const PREDEFINED_MODELS: Record<CliToolName, string[]> = {
|
||||
gemini: ['gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-2.0-flash', 'gemini-1.5-pro', 'gemini-1.5-flash'],
|
||||
qwen: ['coder-model', 'vision-model', 'qwen2.5-coder-32b'],
|
||||
codex: ['gpt5-codex', 'gpt-4.1', 'o4-mini', 'o3']
|
||||
codex: ['gpt-5.2', 'gpt-4.1', 'o4-mini', 'o3']
|
||||
};
|
||||
|
||||
export const DEFAULT_CONFIG: CliConfig = {
|
||||
@@ -45,8 +45,8 @@ export const DEFAULT_CONFIG: CliConfig = {
|
||||
},
|
||||
codex: {
|
||||
enabled: true,
|
||||
primaryModel: 'gpt5-codex',
|
||||
secondaryModel: 'gpt5-codex'
|
||||
primaryModel: 'gpt-5.2',
|
||||
secondaryModel: 'gpt-5.2'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -319,6 +319,8 @@ function buildCommand(params: {
|
||||
break;
|
||||
|
||||
case 'codex':
|
||||
// Codex does NOT support stdin - prompt must be passed as command line argument
|
||||
useStdin = false;
|
||||
// Native resume: codex resume <uuid> [prompt] or --last
|
||||
if (nativeResume?.enabled) {
|
||||
args.push('resume');
|
||||
@@ -343,6 +345,10 @@ function buildCommand(params: {
|
||||
args.push('--add-dir', addDir);
|
||||
}
|
||||
}
|
||||
// Add prompt as positional argument for resume
|
||||
if (prompt) {
|
||||
args.push(prompt);
|
||||
}
|
||||
} else {
|
||||
// Standard exec mode
|
||||
args.push('exec');
|
||||
@@ -362,6 +368,10 @@ function buildCommand(params: {
|
||||
args.push('--add-dir', addDir);
|
||||
}
|
||||
}
|
||||
// Add prompt as positional argument (codex exec "prompt")
|
||||
if (prompt) {
|
||||
args.push(prompt);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -379,9 +389,11 @@ function buildCommand(params: {
|
||||
if (model) {
|
||||
args.push('--model', model);
|
||||
}
|
||||
// Permission modes for write/auto
|
||||
// Permission modes: write/auto → bypassPermissions, analysis → default
|
||||
if (mode === 'write' || mode === 'auto') {
|
||||
args.push('--dangerously-skip-permissions');
|
||||
args.push('--permission-mode', 'bypassPermissions');
|
||||
} else {
|
||||
args.push('--permission-mode', 'default');
|
||||
}
|
||||
// Output format for better parsing
|
||||
args.push('--output-format', 'text');
|
||||
@@ -570,7 +582,7 @@ async function executeCliTool(
|
||||
|
||||
// Determine working directory early (needed for conversation lookup)
|
||||
const workingDir = cd || process.cwd();
|
||||
const historyDir = ensureHistoryDir(workingDir);
|
||||
ensureHistoryDir(workingDir); // Ensure history directory exists
|
||||
|
||||
// Get SQLite store for native session lookup
|
||||
const store = await getSqliteStore(workingDir);
|
||||
@@ -722,16 +734,8 @@ async function executeCliTool(
|
||||
}
|
||||
}
|
||||
|
||||
// Determine effective model (use config's primaryModel if not explicitly provided)
|
||||
let effectiveModel = model;
|
||||
if (!effectiveModel) {
|
||||
try {
|
||||
effectiveModel = getPrimaryModel(workingDir, tool);
|
||||
} catch {
|
||||
// Config not available, use default (let the CLI tool use its own default)
|
||||
effectiveModel = undefined;
|
||||
}
|
||||
}
|
||||
// Only pass model if explicitly provided - let CLI tools use their own defaults
|
||||
const effectiveModel = model;
|
||||
|
||||
// Build command
|
||||
const { command, args, useStdin } = buildCommand({
|
||||
@@ -864,7 +868,7 @@ async function executeCliTool(
|
||||
// Save all source conversations
|
||||
try {
|
||||
for (const conv of savedConversations) {
|
||||
saveConversation(historyDir, conv);
|
||||
saveConversation(workingDir, conv);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[CLI Executor] Failed to save merged histories:', (err as Error).message);
|
||||
@@ -906,7 +910,7 @@ async function executeCliTool(
|
||||
};
|
||||
// Save merged conversation
|
||||
try {
|
||||
saveConversation(historyDir, conversation);
|
||||
saveConversation(workingDir, conversation);
|
||||
} catch (err) {
|
||||
console.error('[CLI Executor] Failed to save merged conversation:', (err as Error).message);
|
||||
}
|
||||
@@ -937,7 +941,7 @@ async function executeCliTool(
|
||||
};
|
||||
// Try to save conversation to history
|
||||
try {
|
||||
saveConversation(historyDir, conversation);
|
||||
saveConversation(workingDir, conversation);
|
||||
} catch (err) {
|
||||
// Non-fatal: continue even if history save fails
|
||||
console.error('[CLI Executor] Failed to save history:', (err as Error).message);
|
||||
@@ -945,7 +949,8 @@ async function executeCliTool(
|
||||
}
|
||||
|
||||
// Track native session after execution (async, non-blocking)
|
||||
trackNewSession(tool, new Date(startTime), workingDir)
|
||||
// Pass prompt for precise matching in parallel execution scenarios
|
||||
trackNewSession(tool, new Date(startTime), workingDir, prompt)
|
||||
.then((nativeSession) => {
|
||||
if (nativeSession) {
|
||||
// Save native session mapping
|
||||
@@ -1211,8 +1216,8 @@ export function getExecutionHistory(baseDir: string, options: {
|
||||
* Get conversation detail by ID (returns ConversationRecord)
|
||||
*/
|
||||
export function getConversationDetail(baseDir: string, conversationId: string): ConversationRecord | null {
|
||||
const paths = StoragePaths.project(baseDir);
|
||||
return loadConversation(paths.cliHistory, conversationId);
|
||||
// Pass baseDir directly - loadConversation will resolve the correct storage path
|
||||
return loadConversation(baseDir, conversationId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,7 +7,7 @@ import Database from 'better-sqlite3';
|
||||
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, rmdirSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { parseSessionFile, formatConversation, extractConversationPairs, type ParsedSession, type ParsedTurn } from './session-content-parser.js';
|
||||
import { StoragePaths, ensureStorageDir } from '../config/storage-paths.js';
|
||||
import { StoragePaths, ensureStorageDir, getProjectId } from '../config/storage-paths.js';
|
||||
|
||||
// Types
|
||||
export interface ConversationTurn {
|
||||
@@ -239,10 +239,12 @@ export class CliHistoryStore {
|
||||
*/
|
||||
private migrateSchema(): void {
|
||||
try {
|
||||
// Check if category column exists
|
||||
// Check if columns exist
|
||||
const tableInfo = this.db.prepare('PRAGMA table_info(conversations)').all() as Array<{ name: string }>;
|
||||
const hasCategory = tableInfo.some(col => col.name === 'category');
|
||||
const hasParentExecutionId = tableInfo.some(col => col.name === 'parent_execution_id');
|
||||
const hasProjectRoot = tableInfo.some(col => col.name === 'project_root');
|
||||
const hasRelativePath = tableInfo.some(col => col.name === 'relative_path');
|
||||
|
||||
if (!hasCategory) {
|
||||
console.log('[CLI History] Migrating database: adding category column...');
|
||||
@@ -270,6 +272,28 @@ export class CliHistoryStore {
|
||||
}
|
||||
console.log('[CLI History] Migration complete: parent_execution_id column added');
|
||||
}
|
||||
|
||||
// Add hierarchical storage support columns
|
||||
if (!hasProjectRoot) {
|
||||
console.log('[CLI History] Migrating database: adding project_root column for hierarchical storage...');
|
||||
this.db.exec(`
|
||||
ALTER TABLE conversations ADD COLUMN project_root TEXT;
|
||||
`);
|
||||
try {
|
||||
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_conversations_project_root ON conversations(project_root);`);
|
||||
} catch (indexErr) {
|
||||
console.warn('[CLI History] Project root index creation warning:', (indexErr as Error).message);
|
||||
}
|
||||
console.log('[CLI History] Migration complete: project_root column added');
|
||||
}
|
||||
|
||||
if (!hasRelativePath) {
|
||||
console.log('[CLI History] Migrating database: adding relative_path column for hierarchical storage...');
|
||||
this.db.exec(`
|
||||
ALTER TABLE conversations ADD COLUMN relative_path TEXT;
|
||||
`);
|
||||
console.log('[CLI History] Migration complete: relative_path column added');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[CLI History] Migration error:', (err as Error).message);
|
||||
// Don't throw - allow the store to continue working with existing schema
|
||||
@@ -1115,17 +1139,21 @@ export class CliHistoryStore {
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance cache
|
||||
// Singleton instance cache - keyed by normalized project ID for consistency
|
||||
const storeCache = new Map<string, CliHistoryStore>();
|
||||
|
||||
/**
|
||||
* Get or create a store instance for a directory
|
||||
* Uses normalized project ID as cache key to handle path casing differences
|
||||
*/
|
||||
export function getHistoryStore(baseDir: string): CliHistoryStore {
|
||||
if (!storeCache.has(baseDir)) {
|
||||
storeCache.set(baseDir, new CliHistoryStore(baseDir));
|
||||
// Use getProjectId to normalize path for consistent cache key
|
||||
const cacheKey = getProjectId(baseDir);
|
||||
|
||||
if (!storeCache.has(cacheKey)) {
|
||||
storeCache.set(cacheKey, new CliHistoryStore(baseDir));
|
||||
}
|
||||
return storeCache.get(baseDir)!;
|
||||
return storeCache.get(cacheKey)!;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -70,18 +70,60 @@ abstract class SessionDiscoverer {
|
||||
|
||||
/**
|
||||
* Track new session created during execution
|
||||
* @param beforeTimestamp - Filter sessions created after this time
|
||||
* @param workingDir - Project working directory
|
||||
* @param prompt - Optional prompt content for precise matching (fallback)
|
||||
*/
|
||||
async trackNewSession(
|
||||
beforeTimestamp: Date,
|
||||
workingDir: string
|
||||
workingDir: string,
|
||||
prompt?: string
|
||||
): Promise<NativeSession | null> {
|
||||
const sessions = this.getSessions({
|
||||
workingDir,
|
||||
afterTimestamp: beforeTimestamp,
|
||||
limit: 1
|
||||
limit: 10 // Get more candidates for prompt matching
|
||||
});
|
||||
return sessions.length > 0 ? sessions[0] : null;
|
||||
|
||||
if (sessions.length === 0) return null;
|
||||
|
||||
// If only one session or no prompt provided, return the latest
|
||||
if (sessions.length === 1 || !prompt) {
|
||||
return sessions[0];
|
||||
}
|
||||
|
||||
// Try to match by prompt content (fallback for parallel execution)
|
||||
const matched = this.matchSessionByPrompt(sessions, prompt);
|
||||
return matched || sessions[0]; // Fallback to latest if no match
|
||||
}
|
||||
|
||||
/**
|
||||
* Match session by prompt content
|
||||
* Searches for the prompt in session's user messages
|
||||
*/
|
||||
matchSessionByPrompt(sessions: NativeSession[], prompt: string): NativeSession | null {
|
||||
// Normalize prompt for comparison (first 200 chars)
|
||||
const promptPrefix = prompt.substring(0, 200).trim();
|
||||
if (!promptPrefix) return null;
|
||||
|
||||
for (const session of sessions) {
|
||||
try {
|
||||
const userMessage = this.extractFirstUserMessage(session.filePath);
|
||||
if (userMessage && userMessage.includes(promptPrefix)) {
|
||||
return session;
|
||||
}
|
||||
} catch {
|
||||
// Skip sessions that can't be read
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract first user message from session file
|
||||
* Override in subclass for tool-specific format
|
||||
*/
|
||||
abstract extractFirstUserMessage(filePath: string): string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -157,6 +199,23 @@ class GeminiSessionDiscoverer extends SessionDiscoverer {
|
||||
const sessions = this.getSessions();
|
||||
return sessions.find(s => s.sessionId === sessionId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract first user message from Gemini session file
|
||||
* Format: { "messages": [{ "type": "user", "content": "..." }] }
|
||||
*/
|
||||
extractFirstUserMessage(filePath: string): string | null {
|
||||
try {
|
||||
const content = JSON.parse(readFileSync(filePath, 'utf8'));
|
||||
if (content.messages && Array.isArray(content.messages)) {
|
||||
const userMsg = content.messages.find((m: { type: string }) => m.type === 'user');
|
||||
return userMsg?.content || null;
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -330,6 +389,46 @@ class QwenSessionDiscoverer extends SessionDiscoverer {
|
||||
const sessions = this.getSessions();
|
||||
return sessions.find(s => s.sessionId === sessionId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract first user message from Qwen session file
|
||||
* New format (.jsonl): { type: "user", message: { role: "user", parts: [{ text: "..." }] } }
|
||||
* Legacy format (.json): { "messages": [{ "type": "user", "content": "..." }] }
|
||||
*/
|
||||
extractFirstUserMessage(filePath: string): string | null {
|
||||
try {
|
||||
const content = readFileSync(filePath, 'utf8');
|
||||
|
||||
// Check if JSONL (new format) or JSON (legacy)
|
||||
if (filePath.endsWith('.jsonl')) {
|
||||
// JSONL format - find first user message
|
||||
const lines = content.split('\n').filter(l => l.trim());
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const entry = JSON.parse(line);
|
||||
// New Qwen format: { type: "user", message: { parts: [{ text: "..." }] } }
|
||||
if (entry.type === 'user' && entry.message?.parts?.[0]?.text) {
|
||||
return entry.message.parts[0].text;
|
||||
}
|
||||
// Alternative format
|
||||
if (entry.role === 'user' && entry.content) {
|
||||
return entry.content;
|
||||
}
|
||||
} catch { /* skip invalid lines */ }
|
||||
}
|
||||
} else {
|
||||
// Legacy JSON format
|
||||
const data = JSON.parse(content);
|
||||
if (data.messages && Array.isArray(data.messages)) {
|
||||
const userMsg = data.messages.find((m: { type: string }) => m.type === 'user');
|
||||
return userMsg?.content || null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -430,6 +529,32 @@ class CodexSessionDiscoverer extends SessionDiscoverer {
|
||||
const sessions = this.getSessions();
|
||||
return sessions.find(s => s.sessionId === sessionId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract first user message from Codex session file (.jsonl)
|
||||
* Format: {"type":"event_msg","payload":{"type":"user_message","message":"..."}}
|
||||
*/
|
||||
extractFirstUserMessage(filePath: string): string | null {
|
||||
try {
|
||||
const content = readFileSync(filePath, 'utf8');
|
||||
const lines = content.split('\n').filter(l => l.trim());
|
||||
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const entry = JSON.parse(line);
|
||||
// Look for user_message event
|
||||
if (entry.type === 'event_msg' &&
|
||||
entry.payload?.type === 'user_message' &&
|
||||
entry.payload?.message) {
|
||||
return entry.payload.message;
|
||||
}
|
||||
} catch { /* skip invalid lines */ }
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -462,15 +587,17 @@ class ClaudeSessionDiscoverer extends SessionDiscoverer {
|
||||
}
|
||||
|
||||
for (const projectHash of projectDirs) {
|
||||
const sessionsDir = join(this.basePath, projectHash, 'sessions');
|
||||
if (!existsSync(sessionsDir)) continue;
|
||||
// Claude Code stores session files directly in project folder (not in 'sessions' subdirectory)
|
||||
// e.g., ~/.claude/projects/D--Claude-dms3/<uuid>.jsonl
|
||||
const projectDir = join(this.basePath, projectHash);
|
||||
if (!existsSync(projectDir)) continue;
|
||||
|
||||
const sessionFiles = readdirSync(sessionsDir)
|
||||
const sessionFiles = readdirSync(projectDir)
|
||||
.filter(f => f.endsWith('.jsonl') || f.endsWith('.json'))
|
||||
.map(f => ({
|
||||
name: f,
|
||||
path: join(sessionsDir, f),
|
||||
stat: statSync(join(sessionsDir, f))
|
||||
path: join(projectDir, f),
|
||||
stat: statSync(join(projectDir, f))
|
||||
}))
|
||||
.sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs);
|
||||
|
||||
@@ -521,6 +648,35 @@ class ClaudeSessionDiscoverer extends SessionDiscoverer {
|
||||
const sessions = this.getSessions();
|
||||
return sessions.find(s => s.sessionId === sessionId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract first user message from Claude Code session file (.jsonl)
|
||||
* Format: {"type":"user","message":{"role":"user","content":"..."},"isMeta":false,...}
|
||||
*/
|
||||
extractFirstUserMessage(filePath: string): string | null {
|
||||
try {
|
||||
const content = readFileSync(filePath, 'utf8');
|
||||
const lines = content.split('\n').filter(l => l.trim());
|
||||
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const entry = JSON.parse(line);
|
||||
// Claude Code format: type="user", message.role="user", message.content="..."
|
||||
// Skip meta messages and command messages
|
||||
if (entry.type === 'user' &&
|
||||
entry.message?.role === 'user' &&
|
||||
entry.message?.content &&
|
||||
!entry.isMeta &&
|
||||
!entry.message.content.startsWith('<command-')) {
|
||||
return entry.message.content;
|
||||
}
|
||||
} catch { /* skip invalid lines */ }
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton discoverers
|
||||
@@ -564,15 +720,20 @@ export function findNativeSessionById(
|
||||
|
||||
/**
|
||||
* Track new session created during execution
|
||||
* @param tool - CLI tool name (gemini, qwen, codex, claude)
|
||||
* @param beforeTimestamp - Filter sessions created after this time
|
||||
* @param workingDir - Project working directory
|
||||
* @param prompt - Optional prompt for precise matching in parallel execution
|
||||
*/
|
||||
export async function trackNewSession(
|
||||
tool: string,
|
||||
beforeTimestamp: Date,
|
||||
workingDir: string
|
||||
workingDir: string,
|
||||
prompt?: string
|
||||
): Promise<NativeSession | null> {
|
||||
const discoverer = discoverers[tool];
|
||||
if (!discoverer) return null;
|
||||
return discoverer.trackNewSession(beforeTimestamp, workingDir);
|
||||
return discoverer.trackNewSession(beforeTimestamp, workingDir, prompt);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -137,47 +137,112 @@ function getDbRecordCount(dbPath: string, tableName: string): number {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get storage statistics for a specific project by ID
|
||||
* Check if a directory is a project data directory
|
||||
* A project data directory contains at least one of: cli-history, memory, cache, config
|
||||
*/
|
||||
export function getProjectStorageStats(projectId: string): ProjectStorageStats {
|
||||
const paths = StoragePaths.projectById(projectId);
|
||||
function isProjectDataDirectory(dirPath: string): boolean {
|
||||
const dataMarkers = ['cli-history', 'memory', 'cache', 'config'];
|
||||
return dataMarkers.some(marker => existsSync(join(dirPath, marker)));
|
||||
}
|
||||
|
||||
const cliHistorySize = getDirSize(paths.cliHistory);
|
||||
const memorySize = getDirSize(paths.memory);
|
||||
const cacheSize = getDirSize(paths.cache);
|
||||
const configSize = getDirSize(paths.config);
|
||||
/**
|
||||
* Get storage statistics for a specific project by path
|
||||
* @param projectId - Project ID (can be hierarchical like "parent/child")
|
||||
* @param projectDir - Actual directory path in storage
|
||||
*/
|
||||
function getProjectStats(projectId: string, projectDir: string): ProjectStorageStats {
|
||||
const cliHistoryDir = join(projectDir, 'cli-history');
|
||||
const memoryDir = join(projectDir, 'memory');
|
||||
const cacheDir = join(projectDir, 'cache');
|
||||
const configDir = join(projectDir, 'config');
|
||||
|
||||
const cliHistorySize = getDirSize(cliHistoryDir);
|
||||
const memorySize = getDirSize(memoryDir);
|
||||
const cacheSize = getDirSize(cacheDir);
|
||||
const configSize = getDirSize(configDir);
|
||||
|
||||
let recordCount: number | undefined;
|
||||
if (existsSync(paths.historyDb)) {
|
||||
recordCount = getDbRecordCount(paths.historyDb, 'conversations');
|
||||
const historyDb = join(cliHistoryDir, 'history.db');
|
||||
if (existsSync(historyDb)) {
|
||||
recordCount = getDbRecordCount(historyDb, 'conversations');
|
||||
}
|
||||
|
||||
return {
|
||||
projectId,
|
||||
totalSize: cliHistorySize + memorySize + cacheSize + configSize,
|
||||
cliHistory: {
|
||||
exists: existsSync(paths.cliHistory),
|
||||
exists: existsSync(cliHistoryDir),
|
||||
size: cliHistorySize,
|
||||
recordCount
|
||||
},
|
||||
memory: {
|
||||
exists: existsSync(paths.memory),
|
||||
exists: existsSync(memoryDir),
|
||||
size: memorySize
|
||||
},
|
||||
cache: {
|
||||
exists: existsSync(paths.cache),
|
||||
exists: existsSync(cacheDir),
|
||||
size: cacheSize
|
||||
},
|
||||
config: {
|
||||
exists: existsSync(paths.config),
|
||||
exists: existsSync(configDir),
|
||||
size: configSize
|
||||
},
|
||||
lastModified: getLatestModTime(paths.root)
|
||||
lastModified: getLatestModTime(projectDir)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get storage statistics for a specific project by ID (legacy)
|
||||
*/
|
||||
export function getProjectStorageStats(projectId: string): ProjectStorageStats {
|
||||
const paths = StoragePaths.projectById(projectId);
|
||||
return getProjectStats(projectId, paths.root);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively scan project directory for hierarchical structure
|
||||
* @param basePath - Base directory to scan
|
||||
* @param relativePath - Relative path from projects root
|
||||
* @param results - Array to accumulate results
|
||||
*/
|
||||
function scanProjectDirectory(
|
||||
basePath: string,
|
||||
relativePath: string,
|
||||
results: ProjectStorageStats[]
|
||||
): void {
|
||||
if (!existsSync(basePath)) return;
|
||||
|
||||
try {
|
||||
const entries = readdirSync(basePath, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
|
||||
const fullPath = join(basePath, entry.name);
|
||||
const currentRelPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
||||
|
||||
// Check if this is a project data directory
|
||||
if (isProjectDataDirectory(fullPath)) {
|
||||
const projectId = currentRelPath;
|
||||
const stats = getProjectStats(projectId, fullPath);
|
||||
results.push(stats);
|
||||
}
|
||||
|
||||
// Recursively scan subdirectories (excluding data directories)
|
||||
const dataDirs = ['cli-history', 'memory', 'cache', 'config'];
|
||||
if (!dataDirs.includes(entry.name)) {
|
||||
scanProjectDirectory(fullPath, currentRelPath, results);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore read errors
|
||||
if (process.env.DEBUG) console.error(`[Storage] Failed to scan ${basePath}: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all storage statistics
|
||||
* Supports hierarchical project structure
|
||||
*/
|
||||
export function getStorageStats(): StorageStats {
|
||||
const rootPath = CCW_HOME;
|
||||
@@ -187,19 +252,10 @@ export function getStorageStats(): StorageStats {
|
||||
const mcpTemplatesPath = StoragePaths.global.mcpTemplates();
|
||||
const globalDbSize = getFileSize(mcpTemplatesPath);
|
||||
|
||||
// Projects
|
||||
// Projects - use recursive scanning for hierarchical structure
|
||||
const projects: ProjectStorageStats[] = [];
|
||||
if (existsSync(projectsDir)) {
|
||||
try {
|
||||
const entries = readdirSync(projectsDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
projects.push(getProjectStorageStats(entry.name));
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore read errors
|
||||
}
|
||||
scanProjectDirectory(projectsDir, '', projects);
|
||||
}
|
||||
|
||||
// Sort by last modified (most recent first)
|
||||
|
||||
Reference in New Issue
Block a user