mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-09 02:24:11 +08:00
Add comprehensive tests for query parsing and Reciprocal Rank Fusion
- Implemented tests for the QueryParser class, covering various identifier splitting methods (CamelCase, snake_case, kebab-case), OR expansion, and FTS5 operator preservation. - Added parameterized tests to validate expected token outputs for different query formats. - Created edge case tests to ensure robustness against unusual input scenarios. - Developed tests for the Reciprocal Rank Fusion (RRF) algorithm, including score computation, weight handling, and result ranking across multiple sources. - Included tests for normalization of BM25 scores and tagging search results with source metadata.
This commit is contained in:
@@ -11,10 +11,14 @@ import { createHash } from 'crypto';
|
||||
import { existsSync, mkdirSync, renameSync, rmSync, readdirSync } from 'fs';
|
||||
|
||||
// Environment variable override for custom storage location
|
||||
const CCW_DATA_DIR = process.env.CCW_DATA_DIR;
|
||||
// Made dynamic to support testing environments
|
||||
export function getCCWHome(): string {
|
||||
return process.env.CCW_DATA_DIR || join(homedir(), '.ccw');
|
||||
}
|
||||
|
||||
// Base CCW home directory
|
||||
export const CCW_HOME = CCW_DATA_DIR || join(homedir(), '.ccw');
|
||||
// Base CCW home directory (deprecated - use getCCWHome() for dynamic access)
|
||||
// Kept for backward compatibility but will use dynamic value in tests
|
||||
export const CCW_HOME = getCCWHome();
|
||||
|
||||
/**
|
||||
* Convert project path to a human-readable folder name
|
||||
@@ -119,7 +123,7 @@ function detectHierarchyImpl(absolutePath: string): HierarchyInfo {
|
||||
const currentId = pathToFolderName(absolutePath);
|
||||
|
||||
// Get all existing project directories
|
||||
const projectsDir = join(CCW_HOME, 'projects');
|
||||
const projectsDir = join(getCCWHome(), 'projects');
|
||||
if (!existsSync(projectsDir)) {
|
||||
return { currentId, parentId: null, relativePath: '' };
|
||||
}
|
||||
@@ -243,7 +247,7 @@ function migrateToHierarchical(legacyDir: string, targetDir: string): void {
|
||||
* @param parentPath - Parent project path
|
||||
*/
|
||||
function migrateChildProjects(parentId: string, parentPath: string): void {
|
||||
const projectsDir = join(CCW_HOME, 'projects');
|
||||
const projectsDir = join(getCCWHome(), 'projects');
|
||||
if (!existsSync(projectsDir)) return;
|
||||
|
||||
const absoluteParentPath = resolve(parentPath);
|
||||
@@ -312,25 +316,25 @@ export function ensureStorageDir(dirPath: string): void {
|
||||
*/
|
||||
export const GlobalPaths = {
|
||||
/** Root CCW home directory */
|
||||
root: () => CCW_HOME,
|
||||
root: () => getCCWHome(),
|
||||
|
||||
/** Config directory */
|
||||
config: () => join(CCW_HOME, 'config'),
|
||||
config: () => join(getCCWHome(), 'config'),
|
||||
|
||||
/** Global settings file */
|
||||
settings: () => join(CCW_HOME, 'config', 'settings.json'),
|
||||
settings: () => join(getCCWHome(), 'config', 'settings.json'),
|
||||
|
||||
/** Recent project paths file */
|
||||
recentPaths: () => join(CCW_HOME, 'config', 'recent-paths.json'),
|
||||
recentPaths: () => join(getCCWHome(), 'config', 'recent-paths.json'),
|
||||
|
||||
/** Databases directory */
|
||||
databases: () => join(CCW_HOME, 'db'),
|
||||
databases: () => join(getCCWHome(), 'db'),
|
||||
|
||||
/** MCP templates database */
|
||||
mcpTemplates: () => join(CCW_HOME, 'db', 'mcp-templates.db'),
|
||||
mcpTemplates: () => join(getCCWHome(), 'db', 'mcp-templates.db'),
|
||||
|
||||
/** Logs directory */
|
||||
logs: () => join(CCW_HOME, 'logs'),
|
||||
logs: () => join(getCCWHome(), 'logs'),
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -370,7 +374,7 @@ export function getProjectPaths(projectPath: string): ProjectPaths {
|
||||
|
||||
if (hierarchy.parentId) {
|
||||
// Has parent, use hierarchical structure
|
||||
projectDir = join(CCW_HOME, 'projects', hierarchy.parentId);
|
||||
projectDir = join(getCCWHome(), 'projects', hierarchy.parentId);
|
||||
|
||||
// Build subdirectory path from relative path
|
||||
const segments = hierarchy.relativePath.split('/').filter(Boolean);
|
||||
@@ -379,7 +383,7 @@ export function getProjectPaths(projectPath: string): ProjectPaths {
|
||||
}
|
||||
|
||||
// Check if we need to migrate old flat data
|
||||
const legacyDir = join(CCW_HOME, 'projects', hierarchy.currentId);
|
||||
const legacyDir = join(getCCWHome(), 'projects', hierarchy.currentId);
|
||||
if (existsSync(legacyDir)) {
|
||||
try {
|
||||
migrateToHierarchical(legacyDir, projectDir);
|
||||
@@ -393,7 +397,7 @@ export function getProjectPaths(projectPath: string): ProjectPaths {
|
||||
}
|
||||
} else {
|
||||
// No parent, use root-level storage
|
||||
projectDir = join(CCW_HOME, 'projects', hierarchy.currentId);
|
||||
projectDir = join(getCCWHome(), 'projects', hierarchy.currentId);
|
||||
|
||||
// Check if there are child projects that need migration
|
||||
try {
|
||||
@@ -424,7 +428,7 @@ export function getProjectPaths(projectPath: string): ProjectPaths {
|
||||
* @returns Object with all project-specific paths
|
||||
*/
|
||||
export function getProjectPathsById(projectId: string): ProjectPaths {
|
||||
const projectDir = join(CCW_HOME, 'projects', projectId);
|
||||
const projectDir = join(getCCWHome(), 'projects', projectId);
|
||||
|
||||
return {
|
||||
root: projectDir,
|
||||
@@ -448,6 +452,87 @@ export const StoragePaths = {
|
||||
projectById: getProjectPathsById,
|
||||
};
|
||||
|
||||
/**
|
||||
* Information about a child project in hierarchical structure
|
||||
*/
|
||||
export interface ChildProjectInfo {
|
||||
/** Absolute path to the child project */
|
||||
projectPath: string;
|
||||
/** Relative path from parent project */
|
||||
relativePath: string;
|
||||
/** Project ID */
|
||||
projectId: string;
|
||||
/** Storage paths for this child project */
|
||||
paths: ProjectPaths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively scan for child projects in hierarchical storage structure
|
||||
* @param projectPath - Parent project path
|
||||
* @returns Array of child project information
|
||||
*/
|
||||
export function scanChildProjects(projectPath: string): ChildProjectInfo[] {
|
||||
const absolutePath = resolve(projectPath);
|
||||
const parentId = getProjectId(absolutePath);
|
||||
const parentStorageDir = join(getCCWHome(), 'projects', parentId);
|
||||
|
||||
// If parent storage doesn't exist, no children
|
||||
if (!existsSync(parentStorageDir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const children: ChildProjectInfo[] = [];
|
||||
|
||||
/**
|
||||
* Recursively scan directory for project data directories
|
||||
*/
|
||||
function scanDirectory(dir: string, relativePath: string): void {
|
||||
if (!existsSync(dir)) return;
|
||||
|
||||
try {
|
||||
const entries = readdirSync(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
|
||||
const fullPath = join(dir, entry.name);
|
||||
const currentRelPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
||||
|
||||
// Check if this directory contains project data
|
||||
const dataMarkers = ['cli-history', 'memory', 'cache', 'config'];
|
||||
const hasData = dataMarkers.some(marker => existsSync(join(fullPath, marker)));
|
||||
|
||||
if (hasData) {
|
||||
// This is a child project
|
||||
const childProjectPath = join(absolutePath, currentRelPath.replace(/\//g, sep));
|
||||
const childId = getProjectId(childProjectPath);
|
||||
|
||||
children.push({
|
||||
projectPath: childProjectPath,
|
||||
relativePath: currentRelPath,
|
||||
projectId: childId,
|
||||
paths: getProjectPaths(childProjectPath)
|
||||
});
|
||||
}
|
||||
|
||||
// Continue scanning subdirectories (skip data directories)
|
||||
if (!dataMarkers.includes(entry.name)) {
|
||||
scanDirectory(fullPath, currentRelPath);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore read errors
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`[scanChildProjects] Failed to scan ${dir}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scanDirectory(parentStorageDir, '');
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy storage paths (for backward compatibility detection)
|
||||
*/
|
||||
@@ -487,7 +572,7 @@ export function isLegacyStoragePresent(projectPath: string): boolean {
|
||||
* Get CCW home directory (for external use)
|
||||
*/
|
||||
export function getCcwHome(): string {
|
||||
return CCW_HOME;
|
||||
return getCCWHome();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -732,6 +732,215 @@ export function getMemoryStore(projectPath: string): MemoryStore {
|
||||
return storeCache.get(cacheKey)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get aggregated stats from parent and all child projects
|
||||
* @param projectPath - Parent project path
|
||||
* @returns Aggregated statistics from all projects
|
||||
*/
|
||||
export function getAggregatedStats(projectPath: string): {
|
||||
entities: number;
|
||||
prompts: number;
|
||||
conversations: number;
|
||||
total: number;
|
||||
projects: Array<{ path: string; stats: { entities: number; prompts: number; conversations: number } }>;
|
||||
} {
|
||||
const { scanChildProjects } = require('../config/storage-paths.js');
|
||||
const childProjects = scanChildProjects(projectPath);
|
||||
|
||||
const projectStats: Array<{ path: string; stats: { entities: number; prompts: number; conversations: number } }> = [];
|
||||
let totalEntities = 0;
|
||||
let totalPrompts = 0;
|
||||
let totalConversations = 0;
|
||||
|
||||
// Get parent stats
|
||||
try {
|
||||
const parentStore = getMemoryStore(projectPath);
|
||||
const db = (parentStore as any).db;
|
||||
|
||||
const entityCount = (db.prepare('SELECT COUNT(*) as count FROM entities').get() as { count: number }).count;
|
||||
const promptCount = (db.prepare('SELECT COUNT(*) as count FROM prompt_history').get() as { count: number }).count;
|
||||
const conversationCount = (db.prepare('SELECT COUNT(*) as count FROM conversations').get() as { count: number }).count;
|
||||
|
||||
projectStats.push({
|
||||
path: projectPath,
|
||||
stats: { entities: entityCount, prompts: promptCount, conversations: conversationCount }
|
||||
});
|
||||
totalEntities += entityCount;
|
||||
totalPrompts += promptCount;
|
||||
totalConversations += conversationCount;
|
||||
} catch (error) {
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`[Memory Store] Failed to get stats for parent ${projectPath}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Get child stats
|
||||
for (const child of childProjects) {
|
||||
try {
|
||||
const childStore = getMemoryStore(child.projectPath);
|
||||
const db = (childStore as any).db;
|
||||
|
||||
const entityCount = (db.prepare('SELECT COUNT(*) as count FROM entities').get() as { count: number }).count;
|
||||
const promptCount = (db.prepare('SELECT COUNT(*) as count FROM prompt_history').get() as { count: number }).count;
|
||||
const conversationCount = (db.prepare('SELECT COUNT(*) as count FROM conversations').get() as { count: number }).count;
|
||||
|
||||
projectStats.push({
|
||||
path: child.relativePath,
|
||||
stats: { entities: entityCount, prompts: promptCount, conversations: conversationCount }
|
||||
});
|
||||
totalEntities += entityCount;
|
||||
totalPrompts += promptCount;
|
||||
totalConversations += conversationCount;
|
||||
} catch (error) {
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`[Memory Store] Failed to get stats for child ${child.projectPath}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
entities: totalEntities,
|
||||
prompts: totalPrompts,
|
||||
conversations: totalConversations,
|
||||
total: totalEntities + totalPrompts + totalConversations,
|
||||
projects: projectStats
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get aggregated entities from parent and all child projects
|
||||
* @param projectPath - Parent project path
|
||||
* @param options - Query options
|
||||
* @returns Combined entities from all projects with source information
|
||||
*/
|
||||
export function getAggregatedEntities(
|
||||
projectPath: string,
|
||||
options: { type?: string; limit?: number; offset?: number } = {}
|
||||
): Array<HotEntity & { sourceProject?: string }> {
|
||||
const { scanChildProjects } = require('../config/storage-paths.js');
|
||||
const childProjects = scanChildProjects(projectPath);
|
||||
|
||||
const limit = options.limit || 50;
|
||||
const offset = options.offset || 0;
|
||||
const allEntities: Array<HotEntity & { sourceProject?: string }> = [];
|
||||
|
||||
// Get parent entities - apply LIMIT at SQL level
|
||||
try {
|
||||
const parentStore = getMemoryStore(projectPath);
|
||||
const db = (parentStore as any).db;
|
||||
|
||||
let query = 'SELECT * FROM entities';
|
||||
const params: any[] = [];
|
||||
|
||||
if (options.type) {
|
||||
query += ' WHERE type = ?';
|
||||
params.push(options.type);
|
||||
}
|
||||
|
||||
query += ' ORDER BY last_seen_at DESC LIMIT ?';
|
||||
params.push(limit);
|
||||
|
||||
const stmt = db.prepare(query);
|
||||
const parentEntities = stmt.all(...params) as Entity[];
|
||||
allEntities.push(...parentEntities.map((e: Entity) => ({ ...e, stats: {} as EntityStats, sourceProject: projectPath })));
|
||||
} catch (error) {
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`[Memory Store] Failed to get entities for parent ${projectPath}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Get child entities - apply LIMIT to each child
|
||||
for (const child of childProjects) {
|
||||
try {
|
||||
const childStore = getMemoryStore(child.projectPath);
|
||||
const db = (childStore as any).db;
|
||||
|
||||
let query = 'SELECT * FROM entities';
|
||||
const params: any[] = [];
|
||||
|
||||
if (options.type) {
|
||||
query += ' WHERE type = ?';
|
||||
params.push(options.type);
|
||||
}
|
||||
|
||||
query += ' ORDER BY last_seen_at DESC LIMIT ?';
|
||||
params.push(limit);
|
||||
|
||||
const stmt = db.prepare(query);
|
||||
const childEntities = stmt.all(...params) as Entity[];
|
||||
allEntities.push(...childEntities.map((e: Entity) => ({ ...e, stats: {} as EntityStats, sourceProject: child.relativePath })));
|
||||
} catch (error) {
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`[Memory Store] Failed to get entities for child ${child.projectPath}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by last_seen_at and apply final limit with offset
|
||||
allEntities.sort((a, b) => {
|
||||
const aTime = a.last_seen_at ? new Date(a.last_seen_at).getTime() : 0;
|
||||
const bTime = b.last_seen_at ? new Date(b.last_seen_at).getTime() : 0;
|
||||
return bTime - aTime;
|
||||
});
|
||||
|
||||
return allEntities.slice(offset, offset + limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get aggregated prompts from parent and all child projects
|
||||
* @param projectPath - Parent project path
|
||||
* @param limit - Maximum number of prompts to return
|
||||
* @returns Combined prompts from all projects with source information
|
||||
*/
|
||||
export function getAggregatedPrompts(
|
||||
projectPath: string,
|
||||
limit: number = 50
|
||||
): Array<PromptHistory & { sourceProject?: string }> {
|
||||
const { scanChildProjects } = require('../config/storage-paths.js');
|
||||
const childProjects = scanChildProjects(projectPath);
|
||||
|
||||
const allPrompts: Array<PromptHistory & { sourceProject?: string }> = [];
|
||||
|
||||
// Get parent prompts - use direct SQL query with LIMIT
|
||||
try {
|
||||
const parentStore = getMemoryStore(projectPath);
|
||||
const db = (parentStore as any).db;
|
||||
|
||||
const stmt = db.prepare('SELECT * FROM prompt_history ORDER BY timestamp DESC LIMIT ?');
|
||||
const parentPrompts = stmt.all(limit) as PromptHistory[];
|
||||
allPrompts.push(...parentPrompts.map((p: PromptHistory) => ({ ...p, sourceProject: projectPath })));
|
||||
} catch (error) {
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`[Memory Store] Failed to get prompts for parent ${projectPath}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Get child prompts - apply LIMIT to each child to reduce memory footprint
|
||||
for (const child of childProjects) {
|
||||
try {
|
||||
const childStore = getMemoryStore(child.projectPath);
|
||||
const db = (childStore as any).db;
|
||||
|
||||
const stmt = db.prepare('SELECT * FROM prompt_history ORDER BY timestamp DESC LIMIT ?');
|
||||
const childPrompts = stmt.all(limit) as PromptHistory[];
|
||||
allPrompts.push(...childPrompts.map((p: PromptHistory) => ({ ...p, sourceProject: child.relativePath })));
|
||||
} catch (error) {
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`[Memory Store] Failed to get prompts for child ${child.projectPath}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by timestamp and apply final limit
|
||||
allPrompts.sort((a, b) => {
|
||||
const aTime = a.timestamp ? new Date(a.timestamp).getTime() : 0;
|
||||
const bTime = b.timestamp ? new Date(b.timestamp).getTime() : 0;
|
||||
return bTime - aTime;
|
||||
});
|
||||
|
||||
return allPrompts.slice(0, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close all store instances
|
||||
*/
|
||||
|
||||
@@ -212,7 +212,7 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
const status = url.searchParams.get('status') || null;
|
||||
const category = url.searchParams.get('category') as 'user' | 'internal' | 'insight' | null;
|
||||
const search = url.searchParams.get('search') || null;
|
||||
const recursive = url.searchParams.get('recursive') !== 'false';
|
||||
const recursive = url.searchParams.get('recursive') === 'true';
|
||||
|
||||
getExecutionHistoryAsync(projectPath, { limit, tool, status, category, search, recursive })
|
||||
.then(history => {
|
||||
|
||||
@@ -222,21 +222,30 @@ export async function handleMemoryRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
const projectPath = url.searchParams.get('path') || initialPath;
|
||||
const limit = parseInt(url.searchParams.get('limit') || '50', 10);
|
||||
const search = url.searchParams.get('search') || null;
|
||||
const recursive = url.searchParams.get('recursive') === 'true';
|
||||
|
||||
try {
|
||||
const memoryStore = getMemoryStore(projectPath);
|
||||
let prompts;
|
||||
|
||||
if (search) {
|
||||
prompts = memoryStore.searchPrompts(search, limit);
|
||||
// Recursive mode: aggregate prompts from parent and child projects
|
||||
if (recursive && !search) {
|
||||
const { getAggregatedPrompts } = await import('../memory-store.js');
|
||||
prompts = getAggregatedPrompts(projectPath, limit);
|
||||
} else {
|
||||
// Get all recent prompts (we'll need to add this method to MemoryStore)
|
||||
const stmt = memoryStore['db'].prepare(`
|
||||
SELECT * FROM prompt_history
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT ?
|
||||
`);
|
||||
prompts = stmt.all(limit);
|
||||
// Non-recursive mode or search mode: query only current project
|
||||
const memoryStore = getMemoryStore(projectPath);
|
||||
|
||||
if (search) {
|
||||
prompts = memoryStore.searchPrompts(search, limit);
|
||||
} else {
|
||||
// Get all recent prompts (we'll need to add this method to MemoryStore)
|
||||
const stmt = memoryStore['db'].prepare(`
|
||||
SELECT * FROM prompt_history
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT ?
|
||||
`);
|
||||
prompts = stmt.all(limit);
|
||||
}
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
@@ -506,8 +515,23 @@ Return ONLY valid JSON in this exact format (no markdown, no code blocks, just p
|
||||
const projectPath = url.searchParams.get('path') || initialPath;
|
||||
const filter = url.searchParams.get('filter') || 'all'; // today, week, all
|
||||
const limit = parseInt(url.searchParams.get('limit') || '10', 10);
|
||||
const recursive = url.searchParams.get('recursive') === 'true';
|
||||
|
||||
try {
|
||||
// If requesting aggregated stats, use the aggregated function
|
||||
if (url.searchParams.has('aggregated') || recursive) {
|
||||
const { getAggregatedStats } = await import('../memory-store.js');
|
||||
const aggregatedStats = getAggregatedStats(projectPath);
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
stats: aggregatedStats,
|
||||
aggregated: true
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Original hotspot statistics (non-recursive)
|
||||
const memoryStore = getMemoryStore(projectPath);
|
||||
const hotEntities = memoryStore.getHotEntities(limit * 4);
|
||||
|
||||
|
||||
@@ -1068,3 +1068,55 @@ async function updateCcwToolsMcp(scope = 'workspace') {
|
||||
showRefreshToast(`Failed to update CCW Tools MCP: ${err.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// CCW Tools MCP for Codex
|
||||
// ========================================
|
||||
|
||||
// Get selected tools from Codex checkboxes
|
||||
function getSelectedCcwToolsCodex() {
|
||||
const checkboxes = document.querySelectorAll('.ccw-tool-checkbox-codex:checked');
|
||||
return Array.from(checkboxes).map(cb => cb.dataset.tool);
|
||||
}
|
||||
|
||||
// Select tools by category for Codex
|
||||
function selectCcwToolsCodex(type) {
|
||||
const checkboxes = document.querySelectorAll('.ccw-tool-checkbox-codex');
|
||||
const coreTools = ['write_file', 'edit_file', 'codex_lens', 'smart_search'];
|
||||
|
||||
checkboxes.forEach(cb => {
|
||||
if (type === 'all') {
|
||||
cb.checked = true;
|
||||
} else if (type === 'none') {
|
||||
cb.checked = false;
|
||||
} else if (type === 'core') {
|
||||
cb.checked = coreTools.includes(cb.dataset.tool);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Install/Update CCW Tools MCP to Codex
|
||||
async function installCcwToolsMcpToCodex() {
|
||||
const selectedTools = getSelectedCcwToolsCodex();
|
||||
|
||||
if (selectedTools.length === 0) {
|
||||
showRefreshToast('Please select at least one tool', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const ccwToolsConfig = buildCcwToolsConfig(selectedTools);
|
||||
|
||||
try {
|
||||
const isUpdate = codexMcpServers && codexMcpServers['ccw-tools'];
|
||||
const actionLabel = isUpdate ? 'Updating' : 'Installing';
|
||||
showRefreshToast(`${actionLabel} CCW Tools MCP to Codex...`, 'info');
|
||||
|
||||
await addCodexMcpServer('ccw-tools', ccwToolsConfig);
|
||||
|
||||
const resultLabel = isUpdate ? 'updated in' : 'installed to';
|
||||
showRefreshToast(`CCW Tools ${resultLabel} Codex (${selectedTools.length} tools)`, 'success');
|
||||
} catch (err) {
|
||||
console.error('Failed to install CCW Tools MCP to Codex:', err);
|
||||
showRefreshToast(`Failed to install CCW Tools MCP to Codex: ${err.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ const CCW_MCP_TOOLS = [
|
||||
{ name: 'cli_executor', desc: 'Gemini/Qwen/Codex CLI', core: false },
|
||||
];
|
||||
|
||||
// Get currently enabled tools from installed config
|
||||
// Get currently enabled tools from installed config (Claude)
|
||||
function getCcwEnabledTools() {
|
||||
const currentPath = projectPath; // Keep original format (forward slash)
|
||||
const projectData = mcpAllProjects[currentPath] || {};
|
||||
@@ -28,6 +28,18 @@ function getCcwEnabledTools() {
|
||||
return CCW_MCP_TOOLS.filter(t => t.core).map(t => t.name);
|
||||
}
|
||||
|
||||
// Get currently enabled tools from Codex config
|
||||
function getCcwEnabledToolsCodex() {
|
||||
const ccwConfig = codexMcpServers?.['ccw-tools'];
|
||||
if (ccwConfig?.env?.CCW_ENABLED_TOOLS) {
|
||||
const val = ccwConfig.env.CCW_ENABLED_TOOLS;
|
||||
if (val.toLowerCase() === 'all') return CCW_MCP_TOOLS.map(t => t.name);
|
||||
return val.split(',').map(t => t.trim());
|
||||
}
|
||||
// Default to core tools if not installed
|
||||
return CCW_MCP_TOOLS.filter(t => t.core).map(t => t.name);
|
||||
}
|
||||
|
||||
async function renderMcpManager() {
|
||||
const container = document.getElementById('mainContent');
|
||||
if (!container) return;
|
||||
@@ -120,6 +132,7 @@ async function renderMcpManager() {
|
||||
// Check if CCW Tools is already installed
|
||||
const isCcwToolsInstalled = currentProjectServerNames.includes("ccw-tools");
|
||||
const enabledTools = getCcwEnabledTools();
|
||||
const enabledToolsCodex = getCcwEnabledToolsCodex();
|
||||
|
||||
// Prepare Codex servers data
|
||||
const codexServerEntries = Object.entries(codexMcpServers || {});
|
||||
@@ -157,6 +170,60 @@ async function renderMcpManager() {
|
||||
</div>
|
||||
|
||||
${currentCliMode === 'codex' ? `
|
||||
<!-- CCW Tools MCP Server Card (Codex mode) -->
|
||||
<div class="mcp-section mb-6">
|
||||
<div class="ccw-tools-card bg-gradient-to-br from-orange-500/10 to-orange-500/5 border-2 ${codexMcpServers && codexMcpServers['ccw-tools'] ? 'border-success' : 'border-orange-500/30'} rounded-lg p-6 hover:shadow-lg transition-all">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="flex items-start gap-4 flex-1">
|
||||
<div class="shrink-0 w-12 h-12 bg-orange-500 rounded-lg flex items-center justify-center">
|
||||
<i data-lucide="wrench" class="w-6 h-6 text-white"></i>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<h3 class="text-lg font-bold text-foreground">CCW Tools MCP</h3>
|
||||
<span class="text-xs px-2 py-0.5 bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-300 rounded-full">Codex</span>
|
||||
${codexMcpServers && codexMcpServers['ccw-tools'] ? `
|
||||
<span class="inline-flex items-center gap-1 px-2 py-0.5 text-xs font-semibold rounded-full bg-success-light text-success">
|
||||
<i data-lucide="check" class="w-3 h-3"></i>
|
||||
${enabledToolsCodex.length} tools
|
||||
</span>
|
||||
` : `
|
||||
<span class="inline-flex items-center gap-1 px-2 py-0.5 text-xs font-semibold rounded-full bg-orange-500/20 text-orange-600 dark:text-orange-400">
|
||||
<i data-lucide="package" class="w-3 h-3"></i>
|
||||
${t('mcp.available')}
|
||||
</span>
|
||||
`}
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground mb-3">${t('mcp.ccwToolsDesc')}</p>
|
||||
<!-- Tool Selection Grid for Codex -->
|
||||
<div class="grid grid-cols-3 sm:grid-cols-5 gap-2 mb-3">
|
||||
${CCW_MCP_TOOLS.map(tool => `
|
||||
<label class="flex items-center gap-1.5 text-xs cursor-pointer hover:bg-muted/50 rounded px-1.5 py-1 transition-colors">
|
||||
<input type="checkbox" class="ccw-tool-checkbox-codex w-3 h-3"
|
||||
data-tool="${tool.name}"
|
||||
${enabledToolsCodex.includes(tool.name) ? 'checked' : ''}>
|
||||
<span class="${tool.core ? 'font-medium' : 'text-muted-foreground'}">${tool.desc}</span>
|
||||
</label>
|
||||
`).join('')}
|
||||
</div>
|
||||
<div class="flex items-center gap-3 text-xs">
|
||||
<button class="text-orange-500 hover:underline" onclick="selectCcwToolsCodex('core')">Core only</button>
|
||||
<button class="text-orange-500 hover:underline" onclick="selectCcwToolsCodex('all')">All</button>
|
||||
<button class="text-muted-foreground hover:underline" onclick="selectCcwToolsCodex('none')">None</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shrink-0">
|
||||
<button class="px-4 py-2 text-sm bg-orange-500 text-white rounded-lg hover:opacity-90 transition-opacity flex items-center gap-1"
|
||||
onclick="installCcwToolsMcpToCodex()">
|
||||
<i data-lucide="download" class="w-4 h-4"></i>
|
||||
${codexMcpServers && codexMcpServers['ccw-tools'] ? t('mcp.update') : t('mcp.install')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Codex MCP Servers Section -->
|
||||
<div class="mcp-section mb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
|
||||
@@ -1128,33 +1128,61 @@ export async function getExecutionHistoryAsync(baseDir: string, options: {
|
||||
}> {
|
||||
const { limit = 50, tool = null, status = null, category = null, search = null, recursive = false } = options;
|
||||
|
||||
// With centralized storage, just query the current project
|
||||
// recursive mode now searches all projects in centralized storage
|
||||
// Recursive mode: aggregate data from parent and all child projects
|
||||
if (recursive) {
|
||||
const projectIds = findProjectsWithHistory();
|
||||
const { scanChildProjects } = await import('../config/storage-paths.js');
|
||||
const childProjects = scanChildProjects(baseDir);
|
||||
|
||||
let allExecutions: (HistoryIndex['executions'][0] & { sourceDir?: string })[] = [];
|
||||
let totalCount = 0;
|
||||
|
||||
for (const projectId of projectIds) {
|
||||
try {
|
||||
// Use centralized path helper for project ID
|
||||
const projectPaths = StoragePaths.projectById(projectId);
|
||||
if (existsSync(projectPaths.historyDb)) {
|
||||
// We need to use CliHistoryStore directly for arbitrary project IDs
|
||||
const { CliHistoryStore } = await import('./cli-history-store.js');
|
||||
// CliHistoryStore expects a project path, but we have project ID
|
||||
// For now, skip cross-project queries - just query current project
|
||||
}
|
||||
} catch {
|
||||
// Skip projects with errors
|
||||
// Query parent project - apply limit at source to reduce memory footprint
|
||||
try {
|
||||
const parentStore = await getSqliteStore(baseDir);
|
||||
const parentResult = parentStore.getHistory({ limit, tool, status, category, search });
|
||||
totalCount += parentResult.total;
|
||||
|
||||
for (const exec of parentResult.executions) {
|
||||
allExecutions.push({ ...exec, sourceDir: baseDir });
|
||||
}
|
||||
} catch (error) {
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`[CLI History] Failed to query parent project ${baseDir}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// For simplicity, just query current project in recursive mode too
|
||||
const store = await getSqliteStore(baseDir);
|
||||
return store.getHistory({ limit, tool, status, category, search });
|
||||
// Query all child projects - apply limit to each child
|
||||
for (const child of childProjects) {
|
||||
try {
|
||||
const childStore = await getSqliteStore(child.projectPath);
|
||||
const childResult = childStore.getHistory({ limit, tool, status, category, search });
|
||||
totalCount += childResult.total;
|
||||
|
||||
for (const exec of childResult.executions) {
|
||||
allExecutions.push({
|
||||
...exec,
|
||||
sourceDir: child.relativePath // Show relative path for clarity
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`[CLI History] Failed to query child project ${child.projectPath}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by timestamp (newest first) and apply limit
|
||||
allExecutions.sort((a, b) => Number(b.timestamp) - Number(a.timestamp));
|
||||
const limitedExecutions = allExecutions.slice(0, limit);
|
||||
|
||||
return {
|
||||
total: totalCount,
|
||||
count: limitedExecutions.length,
|
||||
executions: limitedExecutions
|
||||
};
|
||||
}
|
||||
|
||||
// Non-recursive mode: only query current project
|
||||
const store = await getSqliteStore(baseDir);
|
||||
return store.getHistory({ limit, tool, status, category, search });
|
||||
}
|
||||
@@ -1176,26 +1204,49 @@ export function getExecutionHistory(baseDir: string, options: {
|
||||
|
||||
try {
|
||||
if (recursive) {
|
||||
const projectDirs = findProjectsWithHistory();
|
||||
const { scanChildProjects } = require('../config/storage-paths.js');
|
||||
const childProjects = scanChildProjects(baseDir);
|
||||
|
||||
let allExecutions: (HistoryIndex['executions'][0] & { sourceDir?: string })[] = [];
|
||||
let totalCount = 0;
|
||||
|
||||
for (const projectDir of projectDirs) {
|
||||
try {
|
||||
// Use baseDir as context for relative path display
|
||||
const store = getSqliteStoreSync(baseDir);
|
||||
const result = store.getHistory({ limit: 100, tool, status });
|
||||
totalCount += result.total;
|
||||
// Query parent project - apply limit at source
|
||||
try {
|
||||
const parentStore = getSqliteStoreSync(baseDir);
|
||||
const parentResult = parentStore.getHistory({ limit, tool, status });
|
||||
totalCount += parentResult.total;
|
||||
|
||||
for (const exec of result.executions) {
|
||||
allExecutions.push({ ...exec, sourceDir: projectDir });
|
||||
}
|
||||
} catch {
|
||||
// Skip projects with errors
|
||||
for (const exec of parentResult.executions) {
|
||||
allExecutions.push({ ...exec, sourceDir: baseDir });
|
||||
}
|
||||
} catch (error) {
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`[CLI History Sync] Failed to query parent project ${baseDir}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
allExecutions.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
||||
// Query all child projects - apply limit to each child
|
||||
for (const child of childProjects) {
|
||||
try {
|
||||
const childStore = getSqliteStoreSync(child.projectPath);
|
||||
const childResult = childStore.getHistory({ limit, tool, status });
|
||||
totalCount += childResult.total;
|
||||
|
||||
for (const exec of childResult.executions) {
|
||||
allExecutions.push({
|
||||
...exec,
|
||||
sourceDir: child.relativePath
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`[CLI History Sync] Failed to query child project ${child.projectPath}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by timestamp (newest first) and apply limit
|
||||
allExecutions.sort((a, b) => Number(b.timestamp) - Number(a.timestamp));
|
||||
|
||||
return {
|
||||
total: totalCount,
|
||||
|
||||
Reference in New Issue
Block a user