From df23975a0b77ae35360428210504d4d56dc16a90 Mon Sep 17 00:00:00 2001 From: catlog22 Date: Tue, 16 Dec 2025 19:27:05 +0800 Subject: [PATCH] Add comprehensive tests for schema cleanup migration and search comparison - Implement tests for migration 005 to verify removal of deprecated fields in the database schema. - Ensure that new databases are created with a clean schema. - Validate that keywords are correctly extracted from the normalized file_keywords table. - Test symbol insertion without deprecated fields and subdir operations without direct_files. - Create a detailed search comparison test to evaluate vector search vs hybrid search performance. - Add a script for reindexing projects to extract code relationships and verify GraphAnalyzer functionality. - Include a test script to check TreeSitter parser availability and relationship extraction from sample files. --- ccw/docs/CODEX_MCP_IMPLEMENTATION_SUMMARY.md | 360 ++++ ccw/docs/CODEX_MCP_TESTING_GUIDE.md | 321 +++ ccw/docs/GRAPH_EXPLORER_FIX.md | 237 +++ ccw/docs/GRAPH_EXPLORER_TROUBLESHOOTING.md | 331 ++++ ccw/docs/QUICK_TEST_CODEX_MCP.md | 273 +++ ccw/docs/mcp-manager-guide.md | 280 +++ ccw/src/config/storage-paths.ts | 72 + ccw/src/core/dashboard-generator.ts | 9 +- ccw/src/core/history-importer.ts | 170 +- ccw/src/core/memory-store.ts | 56 +- ccw/src/core/routes/cli-routes.ts | 2 +- ccw/src/core/routes/codexlens-routes.ts | 252 ++- ccw/src/core/routes/graph-routes.md | 6 +- ccw/src/core/routes/graph-routes.ts | 212 +- ccw/src/core/routes/memory-routes.ts | 13 +- ccw/src/core/routes/status-routes.ts | 57 + ccw/src/core/server.ts | 6 + .../dashboard-css/15-mcp-manager.css | 375 ++++ .../dashboard-js/components/cli-status.js | 88 +- .../dashboard-js/components/hook-manager.js | 65 +- .../dashboard-js/components/mcp-manager.js | 4 + .../dashboard-js/components/navigation.js | 3 +- ccw/src/templates/dashboard-js/i18n.js | 142 ++ .../dashboard-js/views/cli-manager.js | 249 +++ .../dashboard-js/views/codexlens-manager.js | 596 ++++++ .../dashboard-js/views/hook-manager.js | 7 +- .../dashboard-js/views/mcp-manager.js | 180 +- .../dashboard-js/views/mcp-manager.js.backup | 1729 +++++++++++++++++ .../dashboard-js/views/mcp-manager.js.new | 928 +++++++++ ccw/src/tools/cli-executor.ts | 4 +- ccw/src/tools/cli-history-store.ts | 33 +- codex-lens/docs/CLI_INTEGRATION_SUMMARY.md | 316 +++ codex-lens/docs/IMPLEMENTATION_SUMMARY.md | 488 +++++ codex-lens/docs/MIGRATION_005_SUMMARY.md | 220 +++ codex-lens/docs/PURE_VECTOR_SEARCH_GUIDE.md | 417 ++++ codex-lens/docs/SEARCH_ANALYSIS_SUMMARY.md | 192 ++ codex-lens/docs/SEARCH_COMPARISON_ANALYSIS.md | 711 +++++++ codex-lens/docs/test-quality-enhancements.md | 187 ++ codex-lens/scripts/generate_embeddings.py | 363 ++++ codex-lens/src/codex_lens.egg-info/PKG-INFO | 4 + .../src/codex_lens.egg-info/SOURCES.txt | 32 + .../src/codex_lens.egg-info/requires.txt | 6 + codex-lens/src/codexlens/cli/__init__.py | 19 + codex-lens/src/codexlens/cli/commands.py | 525 ++++- .../src/codexlens/cli/embedding_manager.py | 331 ++++ codex-lens/src/codexlens/cli/model_manager.py | 289 +++ codex-lens/src/codexlens/cli/output.py | 5 +- codex-lens/src/codexlens/entities.py | 1 + .../src/codexlens/search/chain_search.py | 23 +- .../src/codexlens/search/hybrid_search.py | 111 +- codex-lens/src/codexlens/semantic/embedder.py | 53 +- codex-lens/src/codexlens/storage/dir_index.py | 217 ++- .../migration_005_cleanup_unused_fields.py | 188 ++ codex-lens/tests/test_dual_fts.py | 141 ++ codex-lens/tests/test_hybrid_search_e2e.py | 69 + codex-lens/tests/test_pure_vector_search.py | 324 +++ codex-lens/tests/test_query_parser.py | 59 + .../tests/test_schema_cleanup_migration.py | 306 +++ codex-lens/tests/test_search_comparison.py | 529 +++++ scripts/reindex-with-relationships.sh | 141 ++ scripts/test-graph-analyzer.py | 153 ++ 61 files changed, 13114 insertions(+), 366 deletions(-) create mode 100644 ccw/docs/CODEX_MCP_IMPLEMENTATION_SUMMARY.md create mode 100644 ccw/docs/CODEX_MCP_TESTING_GUIDE.md create mode 100644 ccw/docs/GRAPH_EXPLORER_FIX.md create mode 100644 ccw/docs/GRAPH_EXPLORER_TROUBLESHOOTING.md create mode 100644 ccw/docs/QUICK_TEST_CODEX_MCP.md create mode 100644 ccw/docs/mcp-manager-guide.md create mode 100644 ccw/src/core/routes/status-routes.ts create mode 100644 ccw/src/templates/dashboard-css/15-mcp-manager.css create mode 100644 ccw/src/templates/dashboard-js/views/codexlens-manager.js create mode 100644 ccw/src/templates/dashboard-js/views/mcp-manager.js.backup create mode 100644 ccw/src/templates/dashboard-js/views/mcp-manager.js.new create mode 100644 codex-lens/docs/CLI_INTEGRATION_SUMMARY.md create mode 100644 codex-lens/docs/IMPLEMENTATION_SUMMARY.md create mode 100644 codex-lens/docs/MIGRATION_005_SUMMARY.md create mode 100644 codex-lens/docs/PURE_VECTOR_SEARCH_GUIDE.md create mode 100644 codex-lens/docs/SEARCH_ANALYSIS_SUMMARY.md create mode 100644 codex-lens/docs/SEARCH_COMPARISON_ANALYSIS.md create mode 100644 codex-lens/docs/test-quality-enhancements.md create mode 100644 codex-lens/scripts/generate_embeddings.py create mode 100644 codex-lens/src/codexlens/cli/embedding_manager.py create mode 100644 codex-lens/src/codexlens/cli/model_manager.py create mode 100644 codex-lens/src/codexlens/storage/migrations/migration_005_cleanup_unused_fields.py create mode 100644 codex-lens/tests/test_pure_vector_search.py create mode 100644 codex-lens/tests/test_schema_cleanup_migration.py create mode 100644 codex-lens/tests/test_search_comparison.py create mode 100644 scripts/reindex-with-relationships.sh create mode 100644 scripts/test-graph-analyzer.py diff --git a/ccw/docs/CODEX_MCP_IMPLEMENTATION_SUMMARY.md b/ccw/docs/CODEX_MCP_IMPLEMENTATION_SUMMARY.md new file mode 100644 index 00000000..afc0f65c --- /dev/null +++ b/ccw/docs/CODEX_MCP_IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,360 @@ +# Codex MCP 功能实现总结 + +## 📝 已完成的修复 + +### 1. CCW Tools MCP 卡片样式修复 + +**文件**: `ccw/src/templates/dashboard-js/views/mcp-manager.js` + +**修改内容**: +- ✅ 卡片边框: `border-primary` → `border-orange-500` (第345行) +- ✅ 图标背景: `bg-primary` → `bg-orange-500` (第348行) +- ✅ 图标颜色: `text-primary-foreground` → `text-white` (第349行) +- ✅ "Available"徽章: `bg-primary/20 text-primary` → `bg-orange-500/20 text-orange-600` (第360行) +- ✅ 选择按钮颜色: `text-primary` → `text-orange-500` (第378-379行) +- ✅ 安装按钮: `bg-primary` → `bg-orange-500` (第386行、第399行) + +**影响范围**: Claude 模式下的 CCW Tools MCP 卡片 + +--- + +### 2. Toast 消息显示时间增强 + +**文件**: `ccw/src/templates/dashboard-js/components/navigation.js` + +**修改内容**: +- ✅ 显示时间: 2000ms → 3500ms (第300行) + +**影响范围**: 所有 Toast 消息(MCP 安装、删除、切换等操作反馈) + +--- + +## 🔧 功能实现细节 + +### Codex MCP 安装流程 + +``` +用户操作 + ↓ +前端函数: copyClaudeServerToCodex(serverName, serverConfig) + ↓ +调用: addCodexMcpServer(serverName, serverConfig) + ↓ +API 请求: POST /api/codex-mcp-add + ↓ +后端处理: addCodexMcpServer(serverName, serverConfig) + ↓ +文件操作: + 1. 读取 ~/.codex/config.toml (如存在) + 2. 解析 TOML 配置 + 3. 添加/更新 mcp_servers[serverName] + 4. 序列化为 TOML + 5. 写入文件 + ↓ +返回响应: {success: true} 或 {error: "..."} + ↓ +前端更新: + 1. loadMcpConfig() - 重新加载配置 + 2. renderMcpManager() - 重新渲染 UI + 3. showRefreshToast(...) - 显示成功/失败消息 (3.5秒) +``` + +--- + +## 📍 关键代码位置 + +### 前端 + +| 功能 | 文件 | 行号 | 说明 | +|------|------|------|------| +| 复制到 Codex | `components/mcp-manager.js` | 175-177 | `copyClaudeServerToCodex()` 函数 | +| 添加到 Codex | `components/mcp-manager.js` | 87-114 | `addCodexMcpServer()` 函数 | +| Toast 消息 | `components/navigation.js` | 286-301 | `showRefreshToast()` 函数 | +| CCW Tools 样式 | `views/mcp-manager.js` | 342-415 | Claude 模式卡片渲染 | +| 其他项目按钮 | `views/mcp-manager.js` | 1015-1020 | "Install to Codex" 按钮 | + +### 后端 + +| 功能 | 文件 | 行号 | 说明 | +|------|------|------|------| +| API 端点 | `core/routes/mcp-routes.ts` | 1001-1010 | `/api/codex-mcp-add` 路由 | +| 添加服务器 | `core/routes/mcp-routes.ts` | 251-330 | `addCodexMcpServer()` 函数 | +| TOML 序列化 | `core/routes/mcp-routes.ts` | 166-188 | `serializeToml()` 函数 | + +### CSS + +| 功能 | 文件 | 行号 | 说明 | +|------|------|------|------| +| Toast 样式 | `dashboard-css/06-cards.css` | 1501-1538 | Toast 容器和类型样式 | +| Toast 动画 | `dashboard-css/06-cards.css` | 1540-1551 | 滑入/淡出动画 | + +--- + +## 🧪 测试用例 + +### 测试用例 1: CCW Tools 样式验证 + +**前置条件**: Dashboard 运行,进入 MCP 管理页面 + +**测试步骤**: +1. 确保在 Claude 模式 +2. 查看 CCW Tools MCP 卡片 + +**预期结果**: +- [ ] 卡片有橙色边框(`border-orange-500/30`) +- [ ] 图标背景是橙色(`bg-orange-500`) +- [ ] 图标是白色(`text-white`) +- [ ] "Available"徽章是橙色 +- [ ] 按钮是橙色 + +**优先级**: High + +--- + +### 测试用例 2: Codex MCP 新建安装 + +**前置条件**: Dashboard 运行,进入 MCP 管理页面 + +**测试步骤**: +1. 切换到 Codex 模式 +2. 勾选 CCW Tools 的 4 个核心工具 +3. 点击"Install"按钮 +4. 观察 Toast 消息 + +**预期结果**: +- [ ] Toast 消息显示 +- [ ] 消息内容: "CCW Tools installed to Codex (4 tools)" +- [ ] Toast 停留时间: 3.5秒 +- [ ] 卡片状态更新(显示"4 tools"绿色徽章) +- [ ] `~/.codex/config.toml` 文件创建成功 +- [ ] config.toml 包含正确的 `[mcp_servers.ccw-tools]` 配置 + +**优先级**: Critical + +--- + +### 测试用例 3: Claude MCP 复制到 Codex + +**前置条件**: +- Dashboard 运行 +- Claude 模式下已创建全局 MCP 服务器 `test-server` + +**测试步骤**: +1. 切换到 Codex 模式 +2. 滚动到"Copy Claude Servers to Codex"区域 +3. 找到 `test-server` 卡片 +4. 点击"→ Codex"按钮 +5. 观察 Toast 消息 + +**预期结果**: +- [ ] Toast 消息显示 +- [ ] 消息内容: "Codex MCP server 'test-server' added" +- [ ] Toast 停留时间: 3.5秒 +- [ ] 卡片出现"Already added"绿色徽章 +- [ ] "→ Codex"按钮消失 +- [ ] 服务器出现在"Codex Global Servers"区域 +- [ ] `~/.codex/config.toml` 包含 `test-server` 配置 + +**优先级**: Critical + +--- + +### 测试用例 4: 其他项目 MCP 复制到 Codex + +**前置条件**: +- Dashboard 运行 +- 其他项目中存在 MCP 服务器 + +**测试步骤**: +1. 切换到 Codex 模式 +2. 滚动到"Available from Other Projects"区域 +3. 找到来自其他项目的服务器卡片 +4. 点击"Install to Codex"按钮 +5. 观察 Toast 消息 + +**预期结果**: +- [ ] Toast 消息显示 +- [ ] 消息内容包含服务器名称 +- [ ] Toast 停留时间: 3.5秒 +- [ ] 服务器出现在"Codex Global Servers"区域 +- [ ] `~/.codex/config.toml` 包含新服务器配置 + +**优先级**: High + +--- + +## 🔍 验证清单 + +### 代码审查 + +- [x] ✅ 前端函数正确调用后端 API +- [x] ✅ 后端正确处理请求并写入配置文件 +- [x] ✅ Toast 消息在成功和失败时都正确显示 +- [x] ✅ Toast 显示时间更新为 3.5秒 +- [x] ✅ CCW Tools 卡片使用橙色样式 +- [x] ✅ 复制按钮调用正确的函数 +- [x] ✅ 配置文件路径正确 (`~/.codex/config.toml`) +- [x] ✅ TOML 序列化正确处理所有字段 + +### 功能测试 + +- [ ] ⬜ CCW Tools 样式在 Claude 模式下正确显示 +- [ ] ⬜ Codex MCP 新建安装成功 +- [ ] ⬜ Toast 消息正确显示并停留 3.5秒 +- [ ] ⬜ config.toml 文件正确创建 +- [ ] ⬜ 从 Claude 复制到 Codex 成功 +- [ ] ⬜ 从其他项目复制到 Codex 成功 +- [ ] ⬜ 卡片状态正确更新 +- [ ] ⬜ UI 刷新正确 + +### 边界情况 + +- [ ] ⬜ Codex 目录不存在时自动创建 +- [ ] ⬜ config.toml 不存在时正确创建 +- [ ] ⬜ config.toml 已存在时正确追加 +- [ ] ⬜ 重复安装同一服务器正确更新配置 +- [ ] ⬜ API 失败时显示错误 Toast +- [ ] ⬜ 网络错误时显示错误信息 + +--- + +## 📦 相关文件清单 + +### 已修改文件 + +1. `ccw/src/templates/dashboard-js/views/mcp-manager.js` + - 修改: CCW Tools 卡片样式(第342-415行) + +2. `ccw/src/templates/dashboard-js/components/navigation.js` + - 修改: Toast 显示时间(第300行) + +### 核心功能文件(未修改但相关) + +3. `ccw/src/templates/dashboard-js/components/mcp-manager.js` + - 包含: `addCodexMcpServer()`, `copyClaudeServerToCodex()` 函数 + +4. `ccw/src/core/routes/mcp-routes.ts` + - 包含: Codex MCP API 端点和后端逻辑 + +5. `ccw/src/templates/dashboard-css/06-cards.css` + - 包含: Toast 样式定义 + +### 新增文档 + +6. `ccw/docs/CODEX_MCP_TESTING_GUIDE.md` + - 详细测试指南 + +7. `ccw/docs/QUICK_TEST_CODEX_MCP.md` + - 快速测试步骤 + +8. `ccw/docs/CODEX_MCP_IMPLEMENTATION_SUMMARY.md` + - 本文档 + +--- + +## 🎯 下一步行动 + +### 立即执行 + +1. **重启 Dashboard**: + ```bash + # 停止当前 Dashboard + # 重新启动 + npm run dev # 或你的启动命令 + ``` + +2. **执行快速测试**: + - 按照 `QUICK_TEST_CODEX_MCP.md` 执行测试 + - 重点验证: + - CCW Tools 样式 + - Toast 消息显示和时长 + - config.toml 文件创建 + +3. **记录测试结果**: + - 填写 `QUICK_TEST_CODEX_MCP.md` 中的检查清单 + - 截图保存关键步骤 + +### 如果测试失败 + +1. **检查浏览器控制台**: + - F12 打开开发者工具 + - Console 标签查看错误 + - Network 标签查看 API 请求 + +2. **检查后端日志**: + - 查看 CCW Dashboard 的控制台输出 + - 查找 `Error adding Codex MCP server` 等错误信息 + +3. **验证文件权限**: + ```bash + ls -la ~/.codex/ + # 确保有读写权限 + ``` + +--- + +## 📊 测试报告模板 + +```markdown +# Codex MCP 功能测试报告 + +**测试日期**: ___________ +**测试人员**: ___________ +**CCW 版本**: ___________ +**浏览器**: ___________ + +## 测试结果 + +### CCW Tools 样式 (Claude 模式) +- [ ] ✅ 通过 / [ ] ❌ 失败 +- 备注: ___________ + +### Codex MCP 新建安装 +- [ ] ✅ 通过 / [ ] ❌ 失败 +- Toast 显示: [ ] ✅ 是 / [ ] ❌ 否 +- Toast 时长: _____ 秒 +- config.toml 创建: [ ] ✅ 是 / [ ] ❌ 否 +- 备注: ___________ + +### Claude → Codex 复制 +- [ ] ✅ 通过 / [ ] ❌ 失败 +- Toast 显示: [ ] ✅ 是 / [ ] ❌ 否 +- Toast 内容正确: [ ] ✅ 是 / [ ] ❌ 否 +- 备注: ___________ + +### 其他项目 → Codex 安装 +- [ ] ✅ 通过 / [ ] ❌ 失败 +- 备注: ___________ + +## 发现的问题 + +1. ___________ +2. ___________ +3. ___________ + +## 建议改进 + +1. ___________ +2. ___________ +3. ___________ +``` + +--- + +## 🎉 总结 + +所有功能已经实现并准备好测试: + +✅ **已完成**: +- CCW Tools MCP 卡片样式修复(橙色) +- Toast 消息显示时间增强(3.5秒) +- Codex MCP 安装功能(已存在,无需修改) +- Claude → Codex 复制功能(已存在,无需修改) +- 详细测试文档和指南 + +⚠️ **待验证**: +- 实际运行环境中的功能测试 +- 用户体验反馈 +- 边界情况处理 + +请按照 `QUICK_TEST_CODEX_MCP.md` 开始测试! diff --git a/ccw/docs/CODEX_MCP_TESTING_GUIDE.md b/ccw/docs/CODEX_MCP_TESTING_GUIDE.md new file mode 100644 index 00000000..b71e02db --- /dev/null +++ b/ccw/docs/CODEX_MCP_TESTING_GUIDE.md @@ -0,0 +1,321 @@ +# Codex MCP 安装测试指南 + +## 测试准备 + +### 前置条件 +1. 确保 CCW Dashboard 正在运行 +2. 打开浏览器访问 Dashboard 界面 +3. 导航到 "MCP 管理" 页面 + +### 测试环境 +- **Codex 配置文件**: `~/.codex/config.toml` +- **Claude 配置文件**: `~/.claude.json` +- **Dashboard URL**: `http://localhost:3000` + +--- + +## 测试场景 1: Codex MCP 新建安装 + +### 测试步骤 + +1. **切换到 Codex 模式** + - 点击页面顶部的 "Codex" 按钮(橙色高亮) + - 确认右侧显示配置文件路径:`~/.codex/config.toml` + +2. **查看 CCW Tools MCP 卡片** + - ✅ 验证卡片有橙色边框 (`border-orange-500`) + - ✅ 验证图标背景是橙色 (`bg-orange-500`) + - ✅ 验证图标颜色是白色 + - ✅ 验证"Available"徽章是橙色 + - ✅ 验证"Core only"/"All"按钮是橙色 + +3. **选择工具并安装** + - 勾选需要的工具(例如:所有核心工具) + - 点击橙色的"Install"按钮 + - **预期结果**: + - 屏幕底部中央显示 Toast 消息 + - Toast 消息内容:`"CCW Tools installed to Codex (X tools)"` (X 为选择的工具数量) + - Toast 消息类型:绿色成功提示 + - Toast 显示时间:3.5秒 + - 卡片状态更新为"已安装"(绿色对勾徽章) + - 安装按钮文字变为"Update" + +4. **验证安装结果** + - 打开 `~/.codex/config.toml` 文件 + - 确认存在 `[mcp_servers.ccw-tools]` 配置块 + - 示例配置: + ```toml + [mcp_servers.ccw-tools] + command = "npx" + args = ["-y", "ccw-mcp"] + env = { CCW_ENABLED_TOOLS = "write_file,edit_file,codex_lens,smart_search" } + ``` + +### 测试数据记录 + +| 测试项 | 预期结果 | 实际结果 | 状态 | +|--------|----------|----------|------| +| 卡片样式(橙色边框) | ✅ | _待填写_ | ⬜ | +| 图标样式(橙色背景) | ✅ | _待填写_ | ⬜ | +| Toast 消息显示 | ✅ 3.5秒 | _待填写_ | ⬜ | +| Toast 消息内容 | "CCW Tools installed to Codex (X tools)" | _待填写_ | ⬜ | +| config.toml 文件创建 | ✅ | _待填写_ | ⬜ | +| MCP 服务器配置正确 | ✅ | _待填写_ | ⬜ | + +--- + +## 测试场景 2: 从 Claude MCP 复制到 Codex + +### 测试步骤 + +1. **前置准备:在 Claude 模式下创建 MCP 服务器** + - 切换到 "Claude" 模式 + - 在"全局可用 MCP"区域点击"+ New Global Server" + - 创建测试服务器: + - **名称**: `test-mcp-server` + - **命令**: `npx` + - **参数**: `-y @modelcontextprotocol/server-filesystem /tmp` + - 点击"Create"按钮 + - 确认服务器出现在"全局可用 MCP"列表中 + +2. **切换到 Codex 模式** + - 点击顶部的 "Codex" 按钮 + - 向下滚动到"Copy Claude Servers to Codex"区域 + +3. **找到测试服务器** + - 在列表中找到 `test-mcp-server` + - 卡片应该显示: + - 蓝色"Claude"徽章 + - 虚线边框(表示可复制) + - 橙色"→ Codex"按钮 + +4. **执行复制操作** + - 点击橙色的"→ Codex"按钮 + - **预期结果**: + - Toast 消息显示:`"Codex MCP server 'test-mcp-server' added"` (中文:`"Codex MCP 服务器 'test-mcp-server' 已添加"`) + - Toast 类型:绿色成功提示 + - Toast 显示时间:3.5秒 + - 卡片出现绿色"Already added"徽章 + - "→ Codex"按钮消失 + - 服务器出现在"Codex Global Servers"区域 + +5. **验证复制结果** + - 检查 `~/.codex/config.toml` 文件 + - 确认存在 `[mcp_servers.test-mcp-server]` 配置块 + - 示例配置: + ```toml + [mcp_servers.test-mcp-server] + command = "npx" + args = ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"] + ``` + +### 测试数据记录 + +| 测试项 | 预期结果 | 实际结果 | 状态 | +|--------|----------|----------|------| +| Claude 服务器创建 | ✅ | _待填写_ | ⬜ | +| 复制区域显示服务器 | ✅ | _待填写_ | ⬜ | +| Toast 消息显示 | ✅ 3.5秒 | _待填写_ | ⬜ | +| Toast 消息内容 | "Codex MCP server 'test-mcp-server' added" | _待填写_ | ⬜ | +| config.toml 配置正确 | ✅ | _待填写_ | ⬜ | +| 卡片状态更新 | "Already added"徽章 | _待填写_ | ⬜ | +| Codex区域显示服务器 | ✅ | _待填写_ | ⬜ | + +--- + +## 测试场景 3: 从其他项目复制到 Codex + +### 测试步骤 + +1. **前置准备:在其他项目中创建 MCP 服务器** + - 假设你有另一个项目(例如:`/path/to/other-project`) + - 在该项目的 `.claude.json` 或 `.mcp.json` 中添加 MCP 服务器配置 + - 或在 Dashboard 中为该项目创建 MCP 服务器 + +2. **切换回当前项目** + - 在 Dashboard 左上角切换到当前测试项目 + - 切换到 "Codex" 模式 + +3. **查看"其他项目可用"区域** + - 向下滚动到最底部的"Available from Other Projects"区域 + - 应该看到来自其他项目的 MCP 服务器 + - 卡片显示: + - 服务器名称 + - 蓝色"Claude"徽章 + - 项目来源标签(例如:`other-project`) + - 橙色"Install to Codex"按钮 + +4. **执行安装操作** + - 点击橙色的"Install to Codex"按钮 + - **预期结果**: + - Toast 消息显示成功信息 + - Toast 显示时间:3.5秒 + - 服务器出现在"Codex Global Servers"区域 + - 原卡片显示"Already added"徽章 + +5. **验证安装结果** + - 检查 `~/.codex/config.toml` 文件 + - 确认新服务器配置正确 + +### 测试数据记录 + +| 测试项 | 预期结果 | 实际结果 | 状态 | +|--------|----------|----------|------| +| 其他项目服务器显示 | ✅ | _待填写_ | ⬜ | +| Toast 消息显示 | ✅ 3.5秒 | _待填写_ | ⬜ | +| Toast 消息内容 | 成功消息 | _待填写_ | ⬜ | +| config.toml 配置正确 | ✅ | _待填写_ | ⬜ | +| Codex区域显示服务器 | ✅ | _待填写_ | ⬜ | + +--- + +## 故障排查 + +### Toast 消息不显示 + +**可能原因**: +1. Toast 容器 CSS 被覆盖 +2. JavaScript 错误阻止了消息显示 + +**排查步骤**: +1. 打开浏览器开发者工具(F12) +2. 切换到 Console 标签页 +3. 执行安装操作 +4. 查看是否有错误信息 +5. 检查 Network 标签页,确认 API 请求成功(状态码 200) + +### config.toml 未创建 + +**可能原因**: +1. 文件权限问题 +2. 后端 API 错误 + +**排查步骤**: +1. 检查 `~/.codex` 目录是否存在 +2. 检查该目录的读写权限 +3. 查看 CCW Dashboard 后端日志 +4. 检查 API 响应: + ```bash + # 在浏览器开发者工具 Network 标签页查看 + # POST /api/codex-mcp-add + # 响应应该是: {"success": true} + ``` + +### 服务器配置格式不正确 + +**可能原因**: +1. Claude 格式到 Codex 格式转换错误 +2. 特殊字段未正确处理 + +**排查步骤**: +1. 对比 Claude 和 Codex 配置格式 +2. 检查转换逻辑(`addCodexMcpServer` 函数) +3. 验证 TOML 序列化正确性 + +--- + +## 成功标准 + +所有测试场景通过以下标准: + +✅ **UI 样式正确** +- Claude 模式:CCW Tools 卡片使用橙色样式 +- Codex 模式:CCW Tools 卡片使用橙色样式 +- 按钮颜色和边框符合设计规范 + +✅ **Toast 反馈完整** +- 安装成功时显示成功 Toast +- Toast 消息内容准确(包含服务器名称) +- Toast 显示时间为 3.5秒 +- Toast 类型正确(success/error) + +✅ **配置文件正确** +- `~/.codex/config.toml` 创建成功 +- MCP 服务器配置格式正确 +- 配置内容与源配置匹配 + +✅ **UI 状态同步** +- 安装后卡片状态更新 +- 服务器出现在正确的区域 +- 徽章显示正确 + +--- + +## 测试报告模板 + +### 测试信息 +- **测试日期**: _____ +- **测试人员**: _____ +- **CCW 版本**: _____ +- **浏览器**: _____ + +### 测试结果总结 + +| 测试场景 | 通过 | 失败 | 备注 | +|----------|------|------|------| +| Codex MCP 新建安装 | ⬜ | ⬜ | | +| 从 Claude MCP 复制到 Codex | ⬜ | ⬜ | | +| 从其他项目复制到 Codex | ⬜ | ⬜ | | + +### 发现的问题 + +1. **问题描述**: _____ + - **严重程度**: Critical / High / Medium / Low + - **复现步骤**: _____ + - **预期结果**: _____ + - **实际结果**: _____ + - **截图/日志**: _____ + +### 改进建议 + +_____ + +--- + +## 附录:功能实现细节 + +### Toast 消息机制 + +**实现位置**: +- `ccw/src/templates/dashboard-js/components/navigation.js:286-301` +- 显示时间:3500ms (3.5秒) +- 淡出动画:300ms + +**Toast 类型**: +- `success`: 绿色背景 (`hsl(142 76% 36%)`) +- `error`: 红色背景 (`hsl(0 72% 51%)`) +- `info`: 主色调背景 +- `warning`: 橙色背景 (`hsl(38 92% 50%)`) + +### Codex MCP 安装流程 + +1. **前端调用**: `copyClaudeServerToCodex(serverName, serverConfig)` +2. **API 端点**: `POST /api/codex-mcp-add` +3. **后端处理**: `addCodexMcpServer(serverName, serverConfig)` +4. **配置写入**: 序列化为 TOML 格式并写入 `~/.codex/config.toml` +5. **响应返回**: `{success: true}` 或 `{error: "错误消息"}` +6. **前端更新**: + - 重新加载 MCP 配置 + - 重新渲染 UI + - 显示 Toast 消息 + +### 格式转换规则 + +**Claude 格式** → **Codex 格式**: +- `command` → `command` (保持不变) +- `args` → `args` (保持不变) +- `env` → `env` (保持不变) +- `cwd` → `cwd` (可选) +- `url` → `url` (HTTP 服务器) +- `enabled` → `enabled` (默认 true) + +--- + +## 联系支持 + +如果遇到问题,请提供以下信息: +1. 测试场景编号 +2. 浏览器开发者工具的 Console 输出 +3. Network 标签页的 API 请求/响应详情 +4. `~/.codex/config.toml` 文件内容(如果存在) +5. CCW Dashboard 后端日志 diff --git a/ccw/docs/GRAPH_EXPLORER_FIX.md b/ccw/docs/GRAPH_EXPLORER_FIX.md new file mode 100644 index 00000000..5ffadb06 --- /dev/null +++ b/ccw/docs/GRAPH_EXPLORER_FIX.md @@ -0,0 +1,237 @@ +# Graph Explorer Fix - Migration 005 Compatibility + +## Issue Description + +The CCW Dashboard's Graph Explorer view was broken after codex-lens migration 005, which cleaned up unused database fields. + +### Root Cause + +Migration 005 removed unused/redundant columns from the codex-lens database: +- `symbols.token_count` (unused, always NULL) +- `symbols.symbol_type` (redundant duplicate of `kind`) + +However, `ccw/src/core/routes/graph-routes.ts` was still querying these removed columns, causing SQL errors: + +```typescript +// BEFORE (broken): +SELECT + s.id, + s.name, + s.kind, + s.start_line, + s.token_count, // ❌ Column removed in migration 005 + s.symbol_type, // ❌ Column removed in migration 005 + f.path as file +FROM symbols s +``` + +This resulted in database query failures when trying to load the graph visualization. + +## Fix Applied + +Updated `graph-routes.ts` to match the new database schema (v5): + +### 1. Updated GraphNode Interface + +**Before:** +```typescript +interface GraphNode { + id: string; + name: string; + type: string; + file: string; + line: number; + docstring?: string; // ❌ Removed (no longer available) + tokenCount?: number; // ❌ Removed (no longer available) +} +``` + +**After:** +```typescript +interface GraphNode { + id: string; + name: string; + type: string; + file: string; + line: number; +} +``` + +### 2. Updated SQL Query + +**Before:** +```typescript +SELECT + s.id, + s.name, + s.kind, + s.start_line, + s.token_count, // ❌ Removed + s.symbol_type, // ❌ Removed + f.path as file +FROM symbols s +``` + +**After:** +```typescript +SELECT + s.id, + s.name, + s.kind, + s.start_line, + f.path as file +FROM symbols s +``` + +### 3. Updated Row Mapping + +**Before:** +```typescript +return rows.map((row: any) => ({ + id: `${row.file}:${row.name}:${row.start_line}`, + name: row.name, + type: mapSymbolKind(row.kind), + file: row.file, + line: row.start_line, + docstring: row.symbol_type || undefined, // ❌ Removed + tokenCount: row.token_count || undefined, // ❌ Removed +})); +``` + +**After:** +```typescript +return rows.map((row: any) => ({ + id: `${row.file}:${row.name}:${row.start_line}`, + name: row.name, + type: mapSymbolKind(row.kind), + file: row.file, + line: row.start_line, +})); +``` + +### 4. Updated API Documentation + +Updated `graph-routes.md` to reflect the simplified schema without the removed fields. + +## How to Use Graph Explorer + +### Prerequisites + +1. **CodexLens must be installed and initialized:** + ```bash + pip install -e codex-lens/ + ``` + +2. **Project must be indexed:** + ```bash + # Via CLI + codex init + + # Or via CCW Dashboard + # Navigate to "CodexLens" view → Click "Initialize" → Select project + ``` + + This creates the `_index.db` database at `~/.codexlens/indexes//_index.db` + +3. **Symbols and relationships must be extracted:** + - CodexLens automatically indexes symbols during `init` + - Requires TreeSitter parsers for your programming language + - Relationships are extracted via migration 003 (code_relationships table) + +### Accessing Graph Explorer + +1. **Start CCW Dashboard:** + ```bash + ccw view + ``` + +2. **Navigate to Graph Explorer:** + - Click the "Graph" icon in the left sidebar (git-branch icon) + - Or use keyboard shortcut if configured + +3. **View Code Structure:** + - **Code Relations Tab**: Interactive graph visualization of symbols and their relationships + - **Search Process Tab**: Visualizes search pipeline steps (experimental) + +### Graph Controls + +**Toolbar (top-right):** +- **Fit View**: Zoom to fit all nodes in viewport +- **Center**: Center the graph +- **Reset Filters**: Clear all node/edge type filters +- **Refresh**: Reload data from database + +**Sidebar Filters:** +- **Node Types**: Filter by MODULE, CLASS, FUNCTION, METHOD, VARIABLE +- **Edge Types**: Filter by CALLS, IMPORTS, INHERITS +- **Legend**: Color-coded guide for node/edge types + +**Interaction:** +- **Click node**: Show details panel with symbol information +- **Drag nodes**: Rearrange graph layout +- **Scroll**: Zoom in/out +- **Pan**: Click and drag on empty space + +### API Endpoints + +The Graph Explorer uses these REST endpoints: + +1. **GET /api/graph/nodes** + - Returns all symbols as graph nodes + - Query param: `path` (optional, defaults to current project) + +2. **GET /api/graph/edges** + - Returns all code relationships as graph edges + - Query param: `path` (optional) + +3. **GET /api/graph/impact** + - Returns impact analysis for a symbol + - Query params: `path`, `symbol` (required, format: `file:name:line`) + +## Verification + +To verify the fix works: + +1. **Ensure project is indexed:** + ```bash + ls ~/.codexlens/indexes/ + # Should show your project path + ``` + +2. **Check database has symbols:** + ```bash + sqlite3 ~/.codexlens/indexes//_index.db "SELECT COUNT(*) FROM symbols" + # Should return > 0 + ``` + +3. **Check schema version:** + ```bash + sqlite3 ~/.codexlens/indexes//_index.db "PRAGMA user_version" + # Should return: 5 (after migration 005) + ``` + +4. **Test Graph Explorer:** + - Open CCW dashboard: `ccw view` + - Navigate to Graph view + - Should see nodes/edges displayed without errors + +## Related Files + +- **Implementation**: `ccw/src/core/routes/graph-routes.ts` +- **Frontend**: `ccw/src/templates/dashboard-js/views/graph-explorer.js` +- **Styles**: `ccw/src/templates/dashboard-css/14-graph-explorer.css` +- **API Docs**: `ccw/src/core/routes/graph-routes.md` +- **Migration**: `codex-lens/src/codexlens/storage/migrations/migration_005_cleanup_unused_fields.py` + +## Impact + +- **Breaking Change**: Graph Explorer required codex-lens database schema v5 +- **Data Loss**: None (removed fields were unused or redundant) +- **Compatibility**: Graph Explorer now works correctly with migration 005+ +- **Future**: All CCW features requiring codex-lens database access must respect schema version 5 + +## References + +- Migration 005 Documentation: `codex-lens/docs/MIGRATION_005_SUMMARY.md` +- Graph Routes API: `ccw/src/core/routes/graph-routes.md` +- CodexLens Schema: `codex-lens/src/codexlens/storage/dir_index.py` diff --git a/ccw/docs/GRAPH_EXPLORER_TROUBLESHOOTING.md b/ccw/docs/GRAPH_EXPLORER_TROUBLESHOOTING.md new file mode 100644 index 00000000..539428e0 --- /dev/null +++ b/ccw/docs/GRAPH_EXPLORER_TROUBLESHOOTING.md @@ -0,0 +1,331 @@ +# Graph Explorer 故障排查指南 + +## 问题 1: 数据库列名错误 + +### 症状 +``` +[Graph] Failed to query symbols: no such column: s.token_count +[Graph] Failed to query relationships: no such column: f.path +``` + +### 原因 +Migration 004 和 005 修改了数据库 schema: +- Migration 004: `files.path` → `files.full_path` +- Migration 005: 删除 `symbols.token_count` 和 `symbols.symbol_type` + +### 解决方案 +✅ **已修复** - `graph-routes.ts` 已更新为使用正确的列名: +- 使用 `f.full_path` 代替 `f.path` +- 移除对 `s.token_count` 和 `s.symbol_type` 的引用 + +--- + +## 问题 2: 图谱显示为空(无节点/边) + +### 症状 +- 前端 Graph Explorer 视图加载成功,但图谱为空 +- 控制台显示 `nodes: []` 和 `edges: []` + +### 诊断步骤 + +#### 1. 检查数据库是否存在 + +```bash +# Windows (Git Bash) +ls ~/.codexlens/indexes/ + +# 应该看到您的项目路径,例如: +# D/Claude_dms3/ +``` + +#### 2. 检查数据库内容 + +```bash +# 进入项目索引数据库 +cd ~/.codexlens/indexes/D/Claude_dms3/ # 替换为您的项目路径 + +# 检查符号数量 +sqlite3 _index.db "SELECT COUNT(*) FROM symbols;" + +# 检查文件数量 +sqlite3 _index.db "SELECT COUNT(*) FROM files;" + +# 检查关系数量(重要!) +sqlite3 _index.db "SELECT COUNT(*) FROM code_relationships;" +``` + +#### 3. 判断问题类型 + +**情况 A:所有计数都是 0** +- 问题:项目未索引 +- 解决方案:运行 `codex init ` + +**情况 B:symbols > 0, files > 0, code_relationships = 0** +- 问题:**旧索引缺少关系数据**(本次遇到的情况) +- 解决方案:重新索引以提取关系 + +**情况 C:所有计数都 > 0** +- 问题:前端或 API 路由错误 +- 解决方案:检查浏览器控制台错误 + +### 解决方案:重新索引提取代码关系 + +#### 方案 1: 使用 CodexLens CLI(推荐) + +```bash +# 1. 清除旧索引(可选但推荐) +rm -rf ~/.codexlens/indexes/D/Claude_dms3/_index.db + +# 2. 重新初始化项目 +cd /d/Claude_dms3 +codex init . + +# 3. 验证关系数据已提取 +sqlite3 ~/.codexlens/indexes/D/Claude_dms3/_index.db "SELECT COUNT(*) FROM code_relationships;" +# 应该返回 > 0 +``` + +#### 方案 2: 使用 Python 脚本手动提取 + +创建临时脚本 `extract_relationships.py`: + +```python +#!/usr/bin/env python3 +""" +临时脚本:为已索引项目提取代码关系 +适用于 migration 003 之前创建的索引 +""" +from pathlib import Path +from codexlens.storage.dir_index import DirIndexStore +from codexlens.semantic.graph_analyzer import GraphAnalyzer + +def extract_relationships_for_project(project_path: str): + """为已索引项目提取并添加代码关系""" + project = Path(project_path).resolve() + + # 打开索引数据库 + store = DirIndexStore(project) + store.initialize() + + print(f"Processing project: {project}") + + # 获取所有已索引文件 + with store._get_connection() as conn: + cursor = conn.execute(""" + SELECT f.id, f.full_path, f.language, f.content + FROM files f + WHERE f.language IN ('python', 'javascript', 'typescript') + AND f.content IS NOT NULL + """) + files = cursor.fetchall() + + total = len(files) + processed = 0 + relationships_added = 0 + + for file_id, file_path, language, content in files: + processed += 1 + print(f"[{processed}/{total}] Processing {file_path}...") + + try: + # 创建图分析器 + analyzer = GraphAnalyzer(language) + + if not analyzer.is_available(): + print(f" ⚠ GraphAnalyzer not available for {language}") + continue + + # 获取符号 + with store._get_connection() as conn: + cursor = conn.execute(""" + SELECT name, kind, start_line, end_line + FROM symbols + WHERE file_id = ? + ORDER BY start_line + """, (file_id,)) + symbol_rows = cursor.fetchall() + + # 构造 Symbol 对象 + from codexlens.entities import Symbol + symbols = [ + Symbol( + name=row[0], + kind=row[1], + start_line=row[2], + end_line=row[3], + file_path=file_path + ) + for row in symbol_rows + ] + + # 提取关系 + relationships = analyzer.analyze_with_symbols( + content, + Path(file_path), + symbols + ) + + if relationships: + store.add_relationships(file_path, relationships) + relationships_added += len(relationships) + print(f" ✓ Added {len(relationships)} relationships") + else: + print(f" - No relationships found") + + except Exception as e: + print(f" ✗ Error: {e}") + continue + + store.close() + + print(f"\n✅ Complete!") + print(f" Files processed: {processed}") + print(f" Relationships added: {relationships_added}") + +if __name__ == "__main__": + import sys + if len(sys.argv) < 2: + print("Usage: python extract_relationships.py ") + sys.exit(1) + + extract_relationships_for_project(sys.argv[1]) +``` + +运行脚本: + +```bash +cd /d/Claude_dms3/codex-lens +python extract_relationships.py D:/Claude_dms3 +``` + +#### 验证修复 + +```bash +# 1. 检查关系数量 +sqlite3 ~/.codexlens/indexes/D/Claude_dms3/_index.db "SELECT COUNT(*) FROM code_relationships;" +# 应该 > 0 + +# 2. 检查示例关系 +sqlite3 ~/.codexlens/indexes/D/Claude_dms3/_index.db " +SELECT + s.name as source, + r.relationship_type, + r.target_qualified_name +FROM code_relationships r +JOIN symbols s ON r.source_symbol_id = s.id +LIMIT 5; +" + +# 3. 重启 CCW Dashboard +ccw view + +# 4. 打开 Graph Explorer,应该能看到节点和边 +``` + +--- + +## 问题 3: Graph Explorer 不显示(404 或空白) + +### 症状 +- 左侧边栏的 Graph 图标不响应点击 +- 或点击后显示空白页面 + +### 诊断 + +1. **检查路由是否注册**: + ```bash + cd /d/Claude_dms3/ccw + rg "handleGraphRoutes" src/ + ``` + +2. **检查前端是否包含 graph-explorer 视图**: + ```bash + ls src/templates/dashboard-js/views/graph-explorer.js + ``` + +3. **检查 dashboard-generator.ts 是否包含 graph explorer**: + ```bash + rg "graph-explorer" src/core/dashboard-generator.ts + ``` + +### 解决方案 + +确保以下文件存在且正确: +- `src/core/routes/graph-routes.ts` - API 路由处理 +- `src/templates/dashboard-js/views/graph-explorer.js` - 前端视图 +- `src/templates/dashboard-css/14-graph-explorer.css` - 样式 +- `src/templates/dashboard.html` - 包含 Graph 导航项(line 334) + +--- + +## 问题 4: 关系提取失败(调试模式) + +### 启用调试日志 + +```bash +# 设置日志级别为 DEBUG +export CODEXLENS_LOG_LEVEL=DEBUG + +# 重新索引 +codex init /d/Claude_dms3 + +# 检查日志中的关系提取信息 +# 应该看到: +# DEBUG: Extracting relationships from +# DEBUG: Found N relationships +``` + +### 常见失败原因 + +1. **TreeSitter 解析器缺失**: + ```bash + python -c "from codexlens.semantic.graph_analyzer import GraphAnalyzer; print(GraphAnalyzer('python').is_available())" + # 应该返回: True + ``` + +2. **文件语言未识别**: + ```sql + sqlite3 _index.db "SELECT DISTINCT language FROM files;" + # 应该看到: python, javascript, typescript + ``` + +3. **源代码无法解析**: + - 语法错误的文件会被静默跳过 + - 检查 DEBUG 日志中的解析错误 + +--- + +## 快速诊断命令汇总 + +```bash +# 1. 检查数据库 schema 版本 +sqlite3 ~/.codexlens/indexes/D/Claude_dms3/_index.db "PRAGMA user_version;" +# 应该 >= 5 + +# 2. 检查表结构 +sqlite3 ~/.codexlens/indexes/D/Claude_dms3/_index.db "PRAGMA table_info(files);" +# 应该看到: full_path(不是 path) + +sqlite3 ~/.codexlens/indexes/D/Claude_dms3/_index.db "PRAGMA table_info(symbols);" +# 不应该看到: token_count, symbol_type + +# 3. 检查数据统计 +sqlite3 ~/.codexlens/indexes/D/Claude_dms3/_index.db " +SELECT + (SELECT COUNT(*) FROM files) as files, + (SELECT COUNT(*) FROM symbols) as symbols, + (SELECT COUNT(*) FROM code_relationships) as relationships; +" + +# 4. 测试 API 端点 +curl "http://localhost:3000/api/graph/nodes" | jq '.nodes | length' +curl "http://localhost:3000/api/graph/edges" | jq '.edges | length' +``` + +--- + +## 相关文档 + +- [Graph Explorer 修复说明](./GRAPH_EXPLORER_FIX.md) +- [Migration 005 总结](../../codex-lens/docs/MIGRATION_005_SUMMARY.md) +- [Graph Routes API](../src/core/routes/graph-routes.md) diff --git a/ccw/docs/QUICK_TEST_CODEX_MCP.md b/ccw/docs/QUICK_TEST_CODEX_MCP.md new file mode 100644 index 00000000..beaa09b6 --- /dev/null +++ b/ccw/docs/QUICK_TEST_CODEX_MCP.md @@ -0,0 +1,273 @@ +# Codex MCP 快速测试指南 + +## 🎯 快速测试步骤 + +### 测试 1: CCW Tools 样式检查(1分钟) + +1. 打开 Dashboard → MCP 管理 +2. 确保在 **Claude 模式** +3. 查看 CCW Tools MCP 卡片 +4. ✅ **验证点**: + - 卡片有橙色边框(不是蓝色) + - 左上角图标是橙色背景(不是蓝色) + - "Available"徽章是橙色(不是蓝色) + - "Core only"/"All"按钮是橙色文字 + +**预期效果**: +``` +┌─────────────────────────────────────────┐ +│ 🔧 CCW Tools MCP │ ← 橙色边框 +│ [橙色图标] Available (橙色徽章) │ +│ │ +│ [✓] Write/create files │ +│ [✓] Edit/replace content │ +│ ... │ +│ │ +│ [橙色按钮] Core only [橙色按钮] All │ +│ │ +│ [橙色安装按钮] Install to Workspace │ +└─────────────────────────────────────────┘ +``` + +--- + +### 测试 2: Codex MCP 安装 + Toast 反馈(2分钟) + +#### 步骤 + +1. **切换到 Codex 模式** + - 点击页面顶部的 "Codex" 按钮 + - 确认右侧显示 `~/.codex/config.toml` + +2. **选择并安装 CCW Tools** + - 在 CCW Tools 卡片中勾选所有核心工具 + - 点击橙色"Install"按钮 + +3. **观察 Toast 消息** + - **关键点**: 盯住屏幕底部中央 + - 应该看到绿色的成功消息 + - 消息内容: `"CCW Tools installed to Codex (4 tools)"` 或中文版本 + - 消息停留 **3.5秒**(不是2秒) + +4. **验证安装结果** + ```bash + # 查看 Codex 配置文件 + cat ~/.codex/config.toml + + # 应该看到类似以下内容: + # [mcp_servers.ccw-tools] + # command = "npx" + # args = ["-y", "ccw-mcp"] + # env = { CCW_ENABLED_TOOLS = "write_file,edit_file,codex_lens,smart_search" } + ``` + +#### ✅ 成功标准 + +| 项目 | 预期 | 通过? | +|------|------|-------| +| Toast 显示 | ✅ | ⬜ | +| Toast 内容正确 | ✅ | ⬜ | +| Toast 停留 3.5秒 | ✅ | ⬜ | +| config.toml 创建 | ✅ | ⬜ | +| 卡片状态更新 | ✅ | ⬜ | + +--- + +### 测试 3: 从 Claude 复制到 Codex(3分钟) + +#### 前置步骤:创建测试服务器 + +1. **切换到 Claude 模式** +2. **创建全局 MCP 服务器**: + - 点击"全局可用 MCP"区域的"+ New Global Server" + - 填写信息: + - 名称: `test-filesystem` + - 命令: `npx` + - 参数(每行一个): + ``` + -y + @modelcontextprotocol/server-filesystem + /tmp + ``` + - 点击"Create" + +3. **验证创建成功**: 服务器应该出现在"全局可用 MCP"列表中 + +#### 测试步骤 + +1. **切换到 Codex 模式** +2. **找到复制区域**: 向下滚动到"Copy Claude Servers to Codex" +3. **找到测试服务器**: 应该看到 `test-filesystem` 卡片 +4. **点击复制按钮**: 橙色的"→ Codex"按钮 +5. **观察反馈**: + - Toast 消息: `"Codex MCP server 'test-filesystem' added"` + - 停留时间: 3.5秒 + - 卡片出现"Already added"绿色徽章 +6. **验证结果**: + ```bash + cat ~/.codex/config.toml + + # 应该看到: + # [mcp_servers.test-filesystem] + # command = "npx" + # args = ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"] + ``` + +#### ✅ 成功标准 + +| 项目 | 预期 | 通过? | +|------|------|-------| +| Toast 显示(包含服务器名称) | ✅ | ⬜ | +| Toast 停留 3.5秒 | ✅ | ⬜ | +| config.toml 正确添加 | ✅ | ⬜ | +| "Already added"徽章显示 | ✅ | ⬜ | +| 服务器出现在 Codex 区域 | ✅ | ⬜ | + +--- + +## 🔍 调试清单 + +### Toast 消息不显示? + +**检查点**: +1. 打开浏览器开发者工具 (F12) +2. 切换到 **Console** 标签 +3. 执行安装操作 +4. 查看是否有错误(红色文字) + +**常见错误**: +```javascript +// 如果看到这个错误,说明 API 调用失败 +Failed to add Codex MCP server: ... + +// 如果看到这个,说明 Toast 函数未定义 +showRefreshToast is not defined +``` + +### 配置文件未创建? + +**检查步骤**: +```bash +# 1. 检查目录是否存在 +ls -la ~/.codex/ + +# 2. 如果不存在,手动创建 +mkdir -p ~/.codex + +# 3. 检查权限 +ls -la ~/.codex/ +# 应该看到: drwxr-xr-x (可读写) + +# 4. 重试安装操作 +``` + +### 样式不对? + +**可能原因**: +- 浏览器缓存了旧的 CSS +- 需要硬刷新 + +**解决方法**: +``` +按 Ctrl + Shift + R (Windows/Linux) +或 Cmd + Shift + R (Mac) +强制刷新页面 +``` + +--- + +## 📊 测试报告模板 + +**测试时间**: ___________ +**浏览器**: Chrome / Firefox / Safari / Edge +**操作系统**: Windows / macOS / Linux + +### 测试结果 + +| 测试项 | 通过 | 失败 | 备注 | +|--------|------|------|------| +| CCW Tools 橙色样式 | ⬜ | ⬜ | | +| Codex MCP 安装 | ⬜ | ⬜ | | +| Toast 消息显示 | ⬜ | ⬜ | | +| Toast 停留 3.5秒 | ⬜ | ⬜ | | +| Claude → Codex 复制 | ⬜ | ⬜ | | +| config.toml 正确性 | ⬜ | ⬜ | | + +### 发现的问题 + +_请在这里描述任何问题_ + +### 截图 + +_如果有问题,请附上截图_ + +--- + +## 🎬 视频演示脚本 + +如果需要录制演示视频,按照以下脚本操作: + +### 第1段:样式检查(15秒) + +``` +1. 打开 MCP 管理页面 +2. 指向 CCW Tools 卡片 +3. 圈出橙色边框 +4. 圈出橙色图标 +5. 圈出橙色按钮 +``` + +### 第2段:Codex 安装演示(30秒) + +``` +1. 切换到 Codex 模式 +2. 勾选核心工具 +3. 点击 Install 按钮 +4. 暂停并放大 Toast 消息(绿色成功消息) +5. 数秒数:1、2、3、3.5秒后消失 +6. 显示 config.toml 文件内容 +``` + +### 第3段:Claude → Codex 复制演示(45秒) + +``` +1. 切换到 Claude 模式 +2. 创建测试服务器 +3. 切换到 Codex 模式 +4. 找到复制区域 +5. 点击"→ Codex"按钮 +6. 暂停并放大 Toast 消息(包含服务器名称) +7. 显示卡片状态变化("Already added"徽章) +8. 显示 config.toml 更新后的内容 +``` + +--- + +## ✅ 完整测试检查清单 + +打印此清单并在测试时勾选: + +``` +□ 启动 CCW Dashboard +□ 导航到 MCP 管理页面 +□ 【Claude模式】CCW Tools 卡片样式正确(橙色) +□ 【Claude模式】创建全局 MCP 测试服务器 +□ 【Codex模式】CCW Tools 卡片样式正确(橙色) +□ 【Codex模式】安装 CCW Tools +□ 【Codex模式】Toast 消息显示 3.5秒 +□ 【Codex模式】config.toml 创建成功 +□ 【Codex模式】从 Claude 复制测试服务器 +□ 【Codex模式】Toast 消息包含服务器名称 +□ 【Codex模式】卡片显示"Already added" +□ 【Codex模式】config.toml 包含新服务器 +□ 清理测试数据(删除测试服务器) +□ 填写测试报告 +``` + +--- + +## 🎉 成功! + +如果所有测试通过,恭喜!功能工作正常。 + +如果有任何问题,请参考 `CODEX_MCP_TESTING_GUIDE.md` 的详细故障排查部分。 diff --git a/ccw/docs/mcp-manager-guide.md b/ccw/docs/mcp-manager-guide.md new file mode 100644 index 00000000..170e7723 --- /dev/null +++ b/ccw/docs/mcp-manager-guide.md @@ -0,0 +1,280 @@ +# MCP Manager - 使用指南 + +## 概述 + +全新的 MCP 管理器提供了统一的界面来管理 MCP 服务器,支持多种安装方式和配置管理。 + +## 主要特性 + +### 1. 统一的 MCP 编辑弹窗 +- **三种模式**: + - 创建模式(Create):创建新的 MCP 服务器 + - 编辑模式(Edit):编辑现有 MCP 服务器 + - 查看模式(View):只读查看 MCP 服务器详情 + +- **两种服务器类型**: + - STDIO (Command-based):通过命令行启动的 MCP 服务器 + - HTTP (URL-based):通过 HTTP/HTTPS 访问的 MCP 服务器 + +### 2. 多种安装目标 + +支持安装到以下位置: + +| 目标 | 配置文件 | 说明 | +|------|---------|------| +| **Claude** | `.mcp.json` | 项目级配置,推荐用于 Claude CLI | +| **Codex** | `~/.codex/config.toml` | Codex 全局配置 | +| **Project** | `.mcp.json` | 项目级配置(与 Claude 相同) | +| **Global** | `~/.claude.json` | 全局配置,所有项目可用 | + +### 3. MCP 模板系统 + +- **保存模板**:从现有 MCP 服务器创建可复用模板 +- **浏览模板**:按分类查看所有已保存的模板 +- **一键安装**:从模板快速安装 MCP 服务器到任意目标 + +### 4. 统一的服务器管理 + +- **查看所有服务器**: + - Project(项目级) + - Global(全局级) + - Codex(Codex 全局) + - Enterprise(企业级,只读) + +- **操作**: + - 启用/禁用 + - 查看详情 + - 编辑配置 + - 删除服务器 + - 保存为模板 + +## 使用方法 + +### 创建新的 MCP 服务器 + +1. 点击 **"Create New"** 按钮 +2. 填写服务器信息: + - **名称**:唯一标识符(必填) + - **描述**:简要说明(可选) + - **分类**:从预定义分类中选择 + +3. 选择服务器类型: + - **STDIO**:填写 `command`、`args`、`env`、`cwd` + - **HTTP**:填写 `url`、HTTP 头 + +4. (可选)勾选 **"Save as Template"** 保存为模板 + +5. 选择安装目标(Claude/Codex/Project/Global) + +6. 点击 **"Install"** 完成安装 + +### STDIO 服务器示例 + +```json +{ + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/directory"], + "env": { + "DEBUG": "true" + } +} +``` + +### HTTP 服务器示例 + +```json +{ + "url": "https://api.example.com/mcp", + "http_headers": { + "Authorization": "Bearer YOUR_TOKEN", + "X-API-Key": "YOUR_KEY" + } +} +``` + +### 从模板安装 + +1. 点击 **"Templates"** 按钮 +2. 浏览分类中的模板 +3. 点击模板卡片上的 **"Install"** 按钮 +4. 在弹窗中修改配置(如需要) +5. 选择安装目标 +6. 点击 **"Install"** + +### 编辑现有服务器 + +1. 在服务器列表中找到目标服务器 +2. 点击 **编辑图标**(✏️) +3. 修改配置 +4. 点击 **"Update"** 保存更改 + +### 管理服务器 + +- **启用/禁用**:点击开关图标(🔄) +- **查看详情**:点击眼睛图标(👁️) +- **保存为模板**:点击书签图标(🔖) +- **删除**:点击垃圾桶图标(🗑️) + +## CLI 模式切换 + +支持两种 CLI 模式: + +- **Claude 模式**:管理 `~/.claude.json` 和 `.mcp.json` 中的服务器 +- **Codex 模式**:管理 `~/.codex/config.toml` 中的服务器 + +在界面顶部切换 CLI 模式以查看和管理相应的服务器。 + +## 统计信息 + +仪表板顶部显示以下统计: + +- **Total Servers**:总服务器数量 +- **Enabled**:已启用的服务器数量 +- **Claude**:Claude 相关服务器数量(Project + Global) +- **Codex**:Codex 服务器数量 + +## 服务器分类 + +预定义分类: +- Development Tools +- Data & APIs +- Files & Storage +- AI & ML +- DevOps +- Custom + +## API 支持 + +后端 API 已完整实现,支持: + +### Claude MCP API +- `POST /api/mcp-copy-server` - 安装到项目/全局 +- `POST /api/mcp-remove-server` - 从项目删除 +- `POST /api/mcp-add-global-server` - 添加全局服务器 +- `POST /api/mcp-remove-global-server` - 删除全局服务器 +- `POST /api/mcp-toggle` - 启用/禁用服务器 + +### Codex MCP API +- `POST /api/codex-mcp-add` - 添加 Codex 服务器 +- `POST /api/codex-mcp-remove` - 删除 Codex 服务器 +- `POST /api/codex-mcp-toggle` - 启用/禁用 Codex 服务器 +- `GET /api/codex-mcp-config` - 获取 Codex 配置 + +### 模板 API +- `GET /api/mcp-templates` - 获取所有模板 +- `POST /api/mcp-templates` - 保存模板 +- `DELETE /api/mcp-templates/:name` - 删除模板 +- `GET /api/mcp-templates/search?q=keyword` - 搜索模板 +- `GET /api/mcp-templates/categories` - 获取所有分类 + +## 配置文件格式 + +### .mcp.json (项目级) +```json +{ + "mcpServers": { + "server-name": { + "command": "node", + "args": ["server.js"], + "env": {} + } + } +} +``` + +### .claude.json (全局级) +```json +{ + "mcpServers": { + "server-name": { + "command": "node", + "args": ["server.js"] + } + } +} +``` + +### config.toml (Codex) +```toml +[mcp_servers.server-name] +command = "node" +args = ["server.js"] +enabled = true +``` + +## 故障排除 + +### 常见问题 + +1. **服务器无法启用** + - 检查命令是否正确 + - 确认依赖是否已安装 + - 查看环境变量是否正确 + +2. **无法保存到 Codex** + - 确认 `~/.codex` 目录存在 + - 检查文件权限 + +3. **模板无法加载** + - 刷新页面重试 + - 检查浏览器控制台错误信息 + +### 调试技巧 + +- 打开浏览器开发者工具查看网络请求 +- 检查控制台日志 +- 查看配置文件是否正确生成 + +## 兼容性 + +- **支持的配置格式**: + - `.mcp.json` (JSON) + - `.claude.json` (JSON) + - `config.toml` (TOML for Codex) + +- **浏览器支持**: + - Chrome/Edge (推荐) + - Firefox + - Safari + +## 最佳实践 + +1. **使用 .mcp.json 优先**: + - 便于版本控制 + - 项目独立配置 + - Claude 和 Codex 都能识别 + +2. **分类管理模板**: + - 为模板选择合适的分类 + - 添加清晰的描述 + - 避免重复的模板名称 + +3. **环境变量安全**: + - 敏感信息使用环境变量 + - 不要在配置文件中硬编码 token + - 使用 `.env` 文件管理密钥 + +4. **服务器命名规范**: + - 使用小写字母和连字符 + - 避免特殊字符 + - 名称具有描述性 + +## 更新日志 + +### v2.0 (当前版本) +- ✅ 全新的统一编辑弹窗 +- ✅ 支持多种安装目标(Claude/Codex/Project/Global) +- ✅ 完整的模板系统 +- ✅ STDIO 和 HTTP 服务器类型支持 +- ✅ 统一的服务器列表视图 +- ✅ 实时统计信息 +- ✅ 国际化支持(英文/中文) +- ✅ 响应式设计 + +### 从旧版本迁移 + +旧版本的 MCP 配置会自动识别,无需手动迁移。新版本完全兼容旧配置文件。 + +## 支持 + +如有问题或建议,请联系开发团队或提交 issue。 diff --git a/ccw/src/config/storage-paths.ts b/ccw/src/config/storage-paths.ts index a543bc29..47a74f54 100644 --- a/ccw/src/config/storage-paths.ts +++ b/ccw/src/config/storage-paths.ts @@ -9,6 +9,7 @@ import { homedir } from 'os'; import { join, resolve, dirname, relative, sep } from 'path'; import { createHash } from 'crypto'; import { existsSync, mkdirSync, renameSync, rmSync, readdirSync } from 'fs'; +import { readdir } from 'fs/promises'; // Environment variable override for custom storage location // Made dynamic to support testing environments @@ -533,6 +534,77 @@ export function scanChildProjects(projectPath: string): ChildProjectInfo[] { return children; } +/** + * Asynchronously scan for child projects in hierarchical storage structure + * Non-blocking version using fs.promises for better performance + * @param projectPath - Parent project path + * @returns Promise resolving to array of child project information + */ +export async function scanChildProjectsAsync(projectPath: string): Promise { + 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 (async) + */ + async function scanDirectoryAsync(dir: string, relativePath: string): Promise { + if (!existsSync(dir)) return; + + try { + const entries = await readdir(dir, { withFileTypes: true }); + + // Process directories in parallel for better performance + const promises = entries + .filter(entry => entry.isDirectory()) + .map(async (entry) => { + 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)) { + await scanDirectoryAsync(fullPath, currentRelPath); + } + }); + + await Promise.all(promises); + } catch (error) { + // Ignore read errors + if (process.env.DEBUG) { + console.error(`[scanChildProjectsAsync] Failed to scan ${dir}:`, error); + } + } + } + + await scanDirectoryAsync(parentStorageDir, ''); + + return children; +} + /** * Legacy storage paths (for backward compatibility detection) */ diff --git a/ccw/src/core/dashboard-generator.ts b/ccw/src/core/dashboard-generator.ts index 6dec5b78..a1368395 100644 --- a/ccw/src/core/dashboard-generator.ts +++ b/ccw/src/core/dashboard-generator.ts @@ -24,7 +24,13 @@ const MODULE_CSS_FILES = [ '07-managers.css', '08-review.css', '09-explorer.css', - '10-cli.css' + '10-cli.css', + '11-memory.css', + '11-prompt-history.css', + '12-skills-rules.css', + '13-claude-manager.css', + '14-graph-explorer.css', + '15-mcp-manager.css' ]; const MODULE_FILES = [ @@ -57,6 +63,7 @@ const MODULE_FILES = [ 'views/lite-tasks.js', 'views/fix-session.js', 'views/cli-manager.js', + 'views/codexlens-manager.js', 'views/explorer.js', 'views/mcp-manager.js', 'views/hook-manager.js', diff --git a/ccw/src/core/history-importer.ts b/ccw/src/core/history-importer.ts index 084fcb96..bcb2f9d3 100644 --- a/ccw/src/core/history-importer.ts +++ b/ccw/src/core/history-importer.ts @@ -104,45 +104,45 @@ export class HistoryImporter { /** * Initialize database schema for conversation history + * NOTE: Schema aligned with MemoryStore for seamless importing */ private initSchema(): void { this.db.exec(` - -- Conversations table + -- Conversations table (aligned with MemoryStore schema) CREATE TABLE IF NOT EXISTS conversations ( id TEXT PRIMARY KEY, - session_id TEXT NOT NULL, - project_path TEXT, + source TEXT DEFAULT 'ccw', + external_id TEXT, + project_name TEXT, + git_branch TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, - message_count INTEGER DEFAULT 0, - total_tokens INTEGER DEFAULT 0, - metadata TEXT + quality_score INTEGER, + turn_count INTEGER DEFAULT 0, + prompt_preview TEXT ); - -- Messages table + -- Messages table (aligned with MemoryStore schema) CREATE TABLE IF NOT EXISTS messages ( - id TEXT PRIMARY KEY, + id INTEGER PRIMARY KEY AUTOINCREMENT, conversation_id TEXT NOT NULL, - parent_id TEXT, - role TEXT NOT NULL, - content TEXT NOT NULL, + role TEXT NOT NULL CHECK(role IN ('user', 'assistant', 'system')), + content_text TEXT, + content_json TEXT, timestamp TEXT NOT NULL, - model TEXT, - input_tokens INTEGER DEFAULT 0, - output_tokens INTEGER DEFAULT 0, - cwd TEXT, - git_branch TEXT, + token_count INTEGER, FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE ); - -- Tool calls table + -- Tool calls table (aligned with MemoryStore schema) CREATE TABLE IF NOT EXISTS tool_calls ( - id TEXT PRIMARY KEY, - message_id TEXT NOT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + message_id INTEGER NOT NULL, tool_name TEXT NOT NULL, - tool_input TEXT, - tool_result TEXT, - timestamp TEXT NOT NULL, + tool_args TEXT, + tool_output TEXT, + status TEXT, + duration_ms INTEGER, FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE ); @@ -160,13 +160,11 @@ export class HistoryImporter { created_at TEXT NOT NULL ); - -- Indexes - CREATE INDEX IF NOT EXISTS idx_conversations_session ON conversations(session_id); - CREATE INDEX IF NOT EXISTS idx_conversations_project ON conversations(project_path); + -- Indexes (aligned with MemoryStore) + CREATE INDEX IF NOT EXISTS idx_conversations_created ON conversations(created_at DESC); + CREATE INDEX IF NOT EXISTS idx_conversations_updated ON conversations(updated_at DESC); CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages(conversation_id); - CREATE INDEX IF NOT EXISTS idx_messages_timestamp ON messages(timestamp DESC); CREATE INDEX IF NOT EXISTS idx_tool_calls_message ON tool_calls(message_id); - CREATE INDEX IF NOT EXISTS idx_tool_calls_name ON tool_calls(tool_name); `); } @@ -332,17 +330,17 @@ export class HistoryImporter { const result: ImportResult = { imported: 0, skipped: 0, errors: 0 }; const upsertConversation = this.db.prepare(` - INSERT INTO conversations (id, session_id, project_path, created_at, updated_at, message_count, metadata) - VALUES (@id, @session_id, @project_path, @created_at, @updated_at, 1, @metadata) + INSERT INTO conversations (id, source, external_id, project_name, created_at, updated_at, turn_count, prompt_preview) + VALUES (@id, @source, @external_id, @project_name, @created_at, @updated_at, 1, @prompt_preview) ON CONFLICT(id) DO UPDATE SET updated_at = @updated_at, - message_count = message_count + 1 + turn_count = turn_count + 1, + prompt_preview = @prompt_preview `); const upsertMessage = this.db.prepare(` - INSERT INTO messages (id, conversation_id, role, content, timestamp, cwd) - VALUES (@id, @conversation_id, 'user', @content, @timestamp, @cwd) - ON CONFLICT(id) DO NOTHING + INSERT INTO messages (conversation_id, role, content_text, timestamp) + VALUES (@conversation_id, 'user', @content_text, @timestamp) `); const insertHash = this.db.prepare(` @@ -354,7 +352,6 @@ export class HistoryImporter { for (const entry of entries) { try { const timestamp = new Date(entry.timestamp).toISOString(); - const messageId = `${entry.sessionId}-${entry.timestamp}`; const hash = this.generateHash(entry.sessionId, timestamp, entry.display); // Check if hash exists @@ -364,29 +361,28 @@ export class HistoryImporter { continue; } - // Insert conversation + // Insert conversation (using MemoryStore-compatible fields) upsertConversation.run({ id: entry.sessionId, - session_id: entry.sessionId, - project_path: entry.project, + source: 'global_history', + external_id: entry.sessionId, + project_name: entry.project, created_at: timestamp, updated_at: timestamp, - metadata: JSON.stringify({ source: 'global_history' }) + prompt_preview: entry.display.substring(0, 100) }); - // Insert message - upsertMessage.run({ - id: messageId, + // Insert message (using MemoryStore-compatible fields) + const insertResult = upsertMessage.run({ conversation_id: entry.sessionId, - content: entry.display, - timestamp, - cwd: entry.project + content_text: entry.display, + timestamp }); - // Insert hash + // Insert hash (using actual message ID from insert) insertHash.run({ hash, - message_id: messageId, + message_id: String(insertResult.lastInsertRowid), created_at: timestamp }); @@ -413,24 +409,22 @@ export class HistoryImporter { const result: ImportResult = { imported: 0, skipped: 0, errors: 0 }; const upsertConversation = this.db.prepare(` - INSERT INTO conversations (id, session_id, project_path, created_at, updated_at, message_count, total_tokens, metadata) - VALUES (@id, @session_id, @project_path, @created_at, @updated_at, @message_count, @total_tokens, @metadata) + INSERT INTO conversations (id, source, external_id, project_name, git_branch, created_at, updated_at, turn_count, prompt_preview) + VALUES (@id, @source, @external_id, @project_name, @git_branch, @created_at, @updated_at, @turn_count, @prompt_preview) ON CONFLICT(id) DO UPDATE SET updated_at = @updated_at, - message_count = @message_count, - total_tokens = @total_tokens + turn_count = @turn_count, + prompt_preview = @prompt_preview `); const upsertMessage = this.db.prepare(` - INSERT INTO messages (id, conversation_id, parent_id, role, content, timestamp, model, input_tokens, output_tokens, cwd, git_branch) - VALUES (@id, @conversation_id, @parent_id, @role, @content, @timestamp, @model, @input_tokens, @output_tokens, @cwd, @git_branch) - ON CONFLICT(id) DO NOTHING + INSERT INTO messages (conversation_id, role, content_text, content_json, timestamp, token_count) + VALUES (@conversation_id, @role, @content_text, @content_json, @timestamp, @token_count) `); const insertToolCall = this.db.prepare(` - INSERT INTO tool_calls (id, message_id, tool_name, tool_input, tool_result, timestamp) - VALUES (@id, @message_id, @tool_name, @tool_input, @tool_result, @timestamp) - ON CONFLICT(id) DO NOTHING + INSERT INTO tool_calls (message_id, tool_name, tool_args, tool_output, status) + VALUES (@message_id, @tool_name, @tool_args, @tool_output, @status) `); const insertHash = this.db.prepare(` @@ -439,27 +433,29 @@ export class HistoryImporter { `); const transaction = this.db.transaction(() => { - let totalTokens = 0; const firstMessage = messages[0]; const lastMessage = messages[messages.length - 1]; + const promptPreview = firstMessage?.message + ? this.extractTextContent(firstMessage.message.content).substring(0, 100) + : ''; // Insert conversation FIRST (before messages, for foreign key constraint) upsertConversation.run({ id: sessionId, - session_id: sessionId, - project_path: metadata.cwd || null, + source: 'session_file', + external_id: sessionId, + project_name: metadata.cwd || null, + git_branch: metadata.gitBranch || null, created_at: firstMessage.timestamp, updated_at: lastMessage.timestamp, - message_count: 0, - total_tokens: 0, - metadata: JSON.stringify({ ...metadata, source: 'session_file' }) + turn_count: 0, + prompt_preview: promptPreview }); for (const msg of messages) { if (!msg.message) continue; try { - const messageId = msg.uuid || `${sessionId}-${msg.timestamp}`; const content = this.extractTextContent(msg.message.content); const hash = this.generateHash(sessionId, msg.timestamp, content); @@ -470,43 +466,44 @@ export class HistoryImporter { continue; } - // Calculate tokens + // Calculate total tokens const inputTokens = msg.message.usage?.input_tokens || 0; const outputTokens = msg.message.usage?.output_tokens || 0; - totalTokens += inputTokens + outputTokens; + const totalTokens = inputTokens + outputTokens; - // Insert message - upsertMessage.run({ - id: messageId, + // Store content as JSON if complex, otherwise as text + const contentJson = typeof msg.message.content === 'object' + ? JSON.stringify(msg.message.content) + : null; + + // Insert message (using MemoryStore-compatible fields) + const insertResult = upsertMessage.run({ conversation_id: sessionId, - parent_id: msg.parentUuid || null, role: msg.message.role, - content, + content_text: content, + content_json: contentJson, timestamp: msg.timestamp, - model: msg.message.model || null, - input_tokens: inputTokens, - output_tokens: outputTokens, - cwd: msg.cwd || metadata.cwd || null, - git_branch: msg.gitBranch || metadata.gitBranch || null + token_count: totalTokens }); + const messageId = insertResult.lastInsertRowid as number; + // Extract and insert tool calls const toolCalls = this.extractToolCalls(msg.message.content); for (const tool of toolCalls) { insertToolCall.run({ - id: tool.id || `${messageId}-${tool.name}`, message_id: messageId, tool_name: tool.name, - tool_input: JSON.stringify(tool.input), - tool_result: tool.result || null, - timestamp: msg.timestamp + tool_args: JSON.stringify(tool.input), + tool_output: tool.result || null, + status: 'success' }); } - // Insert hash + // Insert hash (using actual message ID from insert) insertHash.run({ hash, - message_id: messageId, + message_id: String(messageId), created_at: msg.timestamp }); @@ -520,13 +517,14 @@ export class HistoryImporter { // Update conversation with final counts upsertConversation.run({ id: sessionId, - session_id: sessionId, - project_path: metadata.cwd || null, + source: 'session_file', + external_id: sessionId, + project_name: metadata.cwd || null, + git_branch: metadata.gitBranch || null, created_at: firstMessage.timestamp, updated_at: lastMessage.timestamp, - message_count: result.imported, - total_tokens: totalTokens, - metadata: JSON.stringify({ ...metadata, source: 'session_file' }) + turn_count: result.imported, + prompt_preview: promptPreview }); }); diff --git a/ccw/src/core/memory-store.ts b/ccw/src/core/memory-store.ts index 9190d050..314a0880 100644 --- a/ccw/src/core/memory-store.ts +++ b/ccw/src/core/memory-store.ts @@ -90,6 +90,8 @@ export interface ToolCall { id?: number; message_id: number; tool_name: string; + // NOTE: Naming inconsistency - using tool_args/tool_output vs tool_input/tool_result in HistoryImporter + // Kept for backward compatibility with existing databases tool_args?: string; tool_output?: string; status?: string; @@ -114,8 +116,10 @@ export interface EntityWithAssociations extends Entity { export class MemoryStore { private db: Database.Database; private dbPath: string; + private projectPath: string; constructor(projectPath: string) { + this.projectPath = projectPath; // Use centralized storage path const paths = StoragePaths.project(projectPath); const memoryDir = paths.memory; @@ -315,6 +319,22 @@ export class MemoryStore { `); console.log('[Memory Store] Migration complete: relative_path column added'); } + + // Add missing timestamp index for messages table (for time-based queries) + try { + const indexExists = this.db.prepare(` + SELECT name FROM sqlite_master + WHERE type='index' AND name='idx_messages_timestamp' + `).get(); + + if (!indexExists) { + console.log('[Memory Store] Adding missing timestamp index to messages table...'); + this.db.exec(`CREATE INDEX IF NOT EXISTS idx_messages_timestamp ON messages(timestamp DESC);`); + console.log('[Memory Store] Migration complete: messages timestamp index added'); + } + } catch (indexErr) { + console.warn('[Memory Store] Messages timestamp index creation warning:', (indexErr as Error).message); + } } catch (err) { console.error('[Memory Store] Migration error:', (err as Error).message); // Don't throw - allow the store to continue working with existing schema @@ -597,13 +617,15 @@ export class MemoryStore { */ saveConversation(conversation: Conversation): void { const stmt = this.db.prepare(` - INSERT INTO conversations (id, source, external_id, project_name, git_branch, created_at, updated_at, quality_score, turn_count, prompt_preview) - VALUES (@id, @source, @external_id, @project_name, @git_branch, @created_at, @updated_at, @quality_score, @turn_count, @prompt_preview) + INSERT INTO conversations (id, source, external_id, project_name, git_branch, created_at, updated_at, quality_score, turn_count, prompt_preview, project_root, relative_path) + VALUES (@id, @source, @external_id, @project_name, @git_branch, @created_at, @updated_at, @quality_score, @turn_count, @prompt_preview, @project_root, @relative_path) ON CONFLICT(id) DO UPDATE SET updated_at = @updated_at, quality_score = @quality_score, turn_count = @turn_count, - prompt_preview = @prompt_preview + prompt_preview = @prompt_preview, + project_root = @project_root, + relative_path = @relative_path `); stmt.run({ @@ -616,7 +638,9 @@ export class MemoryStore { updated_at: conversation.updated_at, quality_score: conversation.quality_score || null, turn_count: conversation.turn_count, - prompt_preview: conversation.prompt_preview || null + prompt_preview: conversation.prompt_preview || null, + project_root: this.projectPath, + relative_path: null // For future hierarchical tracking }); } @@ -737,15 +761,15 @@ export function getMemoryStore(projectPath: string): MemoryStore { * @param projectPath - Parent project path * @returns Aggregated statistics from all projects */ -export function getAggregatedStats(projectPath: string): { +export async function getAggregatedStats(projectPath: string): Promise<{ 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 { scanChildProjectsAsync } = await import('../config/storage-paths.js'); + const childProjects = await scanChildProjectsAsync(projectPath); const projectStats: Array<{ path: string; stats: { entities: number; prompts: number; conversations: number } }> = []; let totalEntities = 0; @@ -813,12 +837,12 @@ export function getAggregatedStats(projectPath: string): { * @param options - Query options * @returns Combined entities from all projects with source information */ -export function getAggregatedEntities( +export async function getAggregatedEntities( projectPath: string, options: { type?: string; limit?: number; offset?: number } = {} -): Array { - const { scanChildProjects } = require('../config/storage-paths.js'); - const childProjects = scanChildProjects(projectPath); +): Promise> { + const { scanChildProjectsAsync } = await import('../config/storage-paths.js'); + const childProjects = await scanChildProjectsAsync(projectPath); const limit = options.limit || 50; const offset = options.offset || 0; @@ -892,12 +916,12 @@ export function getAggregatedEntities( * @param limit - Maximum number of prompts to return * @returns Combined prompts from all projects with source information */ -export function getAggregatedPrompts( +export async function getAggregatedPrompts( projectPath: string, limit: number = 50 -): Array { - const { scanChildProjects } = require('../config/storage-paths.js'); - const childProjects = scanChildProjects(projectPath); +): Promise> { + const { scanChildProjectsAsync } = await import('../config/storage-paths.js'); + const childProjects = await scanChildProjectsAsync(projectPath); const allPrompts: Array = []; diff --git a/ccw/src/core/routes/cli-routes.ts b/ccw/src/core/routes/cli-routes.ts index 40c9746d..0678401e 100644 --- a/ccw/src/core/routes/cli-routes.ts +++ b/ccw/src/core/routes/cli-routes.ts @@ -212,7 +212,7 @@ export async function handleCliRoutes(ctx: RouteContext): Promise { 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') === 'true'; + const recursive = url.searchParams.get('recursive') !== 'false'; getExecutionHistoryAsync(projectPath, { limit, tool, status, category, search, recursive }) .then(history => { diff --git a/ccw/src/core/routes/codexlens-routes.ts b/ccw/src/core/routes/codexlens-routes.ts index 4de3a89f..3c01b314 100644 --- a/ccw/src/core/routes/codexlens-routes.ts +++ b/ccw/src/core/routes/codexlens-routes.ts @@ -23,6 +23,37 @@ export interface RouteContext { broadcastToClients: (data: unknown) => void; } +/** + * Strip ANSI color codes from string + * Rich library adds color codes even with --json flag + */ +function stripAnsiCodes(str: string): string { + // ANSI escape code pattern: \x1b[...m or \x1b]... + return str.replace(/\x1b\[[0-9;]*m/g, '') + .replace(/\x1b\][0-9;]*\x07/g, '') + .replace(/\x1b\][^\x07]*\x07/g, ''); +} + +/** + * Extract JSON from CLI output that may contain logging messages + * CodexLens CLI outputs logs like "INFO ..." before the JSON + * Also strips ANSI color codes that Rich library adds + */ +function extractJSON(output: string): any { + // Strip ANSI color codes first + const cleanOutput = stripAnsiCodes(output); + + // Find the first { or [ character (start of JSON) + const jsonStart = cleanOutput.search(/[{\[]/); + if (jsonStart === -1) { + throw new Error('No JSON found in output'); + } + + // Extract everything from the first { or [ onwards + const jsonString = cleanOutput.substring(jsonStart); + return JSON.parse(jsonString); +} + /** * Handle CodexLens routes * @returns true if route was handled, false otherwise @@ -83,23 +114,45 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise return true; } - // API: CodexLens Config - GET (Get current configuration) + // API: CodexLens Config - GET (Get current configuration with index count) if (pathname === '/api/codexlens/config' && req.method === 'GET') { try { - const result = await executeCodexLens(['config-show', '--json']); - if (result.success) { + // Fetch both config and status to merge index_count + const [configResult, statusResult] = await Promise.all([ + executeCodexLens(['config', '--json']), + executeCodexLens(['status', '--json']) + ]); + + let responseData = { index_dir: '~/.codexlens/indexes', index_count: 0 }; + + // Parse config (extract JSON from output that may contain log messages) + if (configResult.success) { try { - const config = JSON.parse(result.output); - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify(config)); - } catch { - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ index_dir: '~/.codexlens/indexes', index_count: 0 })); + const config = extractJSON(configResult.output); + if (config.success && config.result) { + responseData.index_dir = config.result.index_root || responseData.index_dir; + } + } catch (e) { + console.error('[CodexLens] Failed to parse config:', e.message); + console.error('[CodexLens] Config output:', configResult.output.substring(0, 200)); } - } else { - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ index_dir: '~/.codexlens/indexes', index_count: 0 })); } + + // Parse status to get index_count (projects_count) + if (statusResult.success) { + try { + const status = extractJSON(statusResult.output); + if (status.success && status.result) { + responseData.index_count = status.result.projects_count || 0; + } + } catch (e) { + console.error('[CodexLens] Failed to parse status:', e.message); + console.error('[CodexLens] Status output:', statusResult.output.substring(0, 200)); + } + } + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(responseData)); } catch (err) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: err.message })); @@ -168,7 +221,7 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise const result = await executeCodexLens(['init', targetPath, '--json'], { cwd: targetPath }); if (result.success) { try { - const parsed = JSON.parse(result.output); + const parsed = extractJSON(result.output); return { success: true, result: parsed }; } catch { return { success: true, output: result.output }; @@ -237,7 +290,7 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise const result = await executeCodexLens(args, { cwd: targetPath, timeout: timeoutMs + 30000 }); if (result.success) { try { - const parsed = JSON.parse(result.output); + const parsed = extractJSON(result.output); return { success: true, result: parsed }; } catch { return { success: true, output: result.output }; @@ -253,10 +306,11 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise } - // API: CodexLens Search (FTS5 text search) + // API: CodexLens Search (FTS5 text search with mode support) if (pathname === '/api/codexlens/search') { const query = url.searchParams.get('query') || ''; const limit = parseInt(url.searchParams.get('limit') || '20', 10); + const mode = url.searchParams.get('mode') || 'exact'; // exact, fuzzy, hybrid, vector const projectPath = url.searchParams.get('path') || initialPath; if (!query) { @@ -266,13 +320,13 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise } try { - const args = ['search', query, '--path', projectPath, '--limit', limit.toString(), '--json']; + const args = ['search', query, '--path', projectPath, '--limit', limit.toString(), '--mode', mode, '--json']; const result = await executeCodexLens(args, { cwd: projectPath }); if (result.success) { try { - const parsed = JSON.parse(result.output); + const parsed = extractJSON(result.output); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, ...parsed.result })); } catch { @@ -290,10 +344,11 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise return true; } - // API: CodexLens Search Files Only (return file paths only) + // API: CodexLens Search Files Only (return file paths only, with mode support) if (pathname === '/api/codexlens/search_files') { const query = url.searchParams.get('query') || ''; const limit = parseInt(url.searchParams.get('limit') || '20', 10); + const mode = url.searchParams.get('mode') || 'exact'; // exact, fuzzy, hybrid, vector const projectPath = url.searchParams.get('path') || initialPath; if (!query) { @@ -303,13 +358,13 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise } try { - const args = ['search', query, '--path', projectPath, '--limit', limit.toString(), '--files-only', '--json']; + const args = ['search', query, '--path', projectPath, '--limit', limit.toString(), '--mode', mode, '--files-only', '--json']; const result = await executeCodexLens(args, { cwd: projectPath }); if (result.success) { try { - const parsed = JSON.parse(result.output); + const parsed = extractJSON(result.output); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, ...parsed.result })); } catch { @@ -327,6 +382,51 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise return true; } + // API: CodexLens Symbol Search (search for symbols by name) + if (pathname === '/api/codexlens/symbol') { + const query = url.searchParams.get('query') || ''; + const file = url.searchParams.get('file'); + const limit = parseInt(url.searchParams.get('limit') || '20', 10); + const projectPath = url.searchParams.get('path') || initialPath; + + if (!query && !file) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'Either query or file parameter is required' })); + return true; + } + + try { + let args; + if (file) { + // Get symbols from a specific file + args = ['symbol', '--file', file, '--json']; + } else { + // Search for symbols by name + args = ['symbol', query, '--path', projectPath, '--limit', limit.toString(), '--json']; + } + + const result = await executeCodexLens(args, { cwd: projectPath }); + + if (result.success) { + try { + const parsed = extractJSON(result.output); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: true, ...parsed.result })); + } catch { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: true, symbols: [], output: result.output })); + } + } else { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: result.error })); + } + } catch (err) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: err.message })); + } + return true; + } + // API: CodexLens Semantic Search Install (fastembed, ONNX-based, ~200MB) if (pathname === '/api/codexlens/semantic/install' && req.method === 'POST') { @@ -350,5 +450,117 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise return true; } + // API: CodexLens Model List (list available embedding models) + if (pathname === '/api/codexlens/models' && req.method === 'GET') { + try { + const result = await executeCodexLens(['model-list', '--json']); + if (result.success) { + try { + const parsed = extractJSON(result.output); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(parsed)); + } catch { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: true, result: { models: [] }, output: result.output })); + } + } else { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: result.error })); + } + } catch (err) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: err.message })); + } + return true; + } + + // API: CodexLens Model Download (download embedding model by profile) + if (pathname === '/api/codexlens/models/download' && req.method === 'POST') { + handlePostRequest(req, res, async (body) => { + const { profile } = body; + + if (!profile) { + return { success: false, error: 'profile is required', status: 400 }; + } + + try { + const result = await executeCodexLens(['model-download', profile, '--json'], { timeout: 600000 }); // 10 min for download + if (result.success) { + try { + const parsed = extractJSON(result.output); + return { success: true, ...parsed }; + } catch { + return { success: true, output: result.output }; + } + } else { + return { success: false, error: result.error, status: 500 }; + } + } catch (err) { + return { success: false, error: err.message, status: 500 }; + } + }); + return true; + } + + // API: CodexLens Model Delete (delete embedding model by profile) + if (pathname === '/api/codexlens/models/delete' && req.method === 'POST') { + handlePostRequest(req, res, async (body) => { + const { profile } = body; + + if (!profile) { + return { success: false, error: 'profile is required', status: 400 }; + } + + try { + const result = await executeCodexLens(['model-delete', profile, '--json']); + if (result.success) { + try { + const parsed = extractJSON(result.output); + return { success: true, ...parsed }; + } catch { + return { success: true, output: result.output }; + } + } else { + return { success: false, error: result.error, status: 500 }; + } + } catch (err) { + return { success: false, error: err.message, status: 500 }; + } + }); + return true; + } + + // API: CodexLens Model Info (get model info by profile) + if (pathname === '/api/codexlens/models/info' && req.method === 'GET') { + const profile = url.searchParams.get('profile'); + + if (!profile) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'profile parameter is required' })); + return true; + } + + try { + const result = await executeCodexLens(['model-info', profile, '--json']); + if (result.success) { + try { + const parsed = extractJSON(result.output); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(parsed)); + } catch { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'Failed to parse response' })); + } + } else { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: result.error })); + } + } catch (err) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: err.message })); + } + return true; + } + return false; } diff --git a/ccw/src/core/routes/graph-routes.md b/ccw/src/core/routes/graph-routes.md index 5c142ad7..270852b1 100644 --- a/ccw/src/core/routes/graph-routes.md +++ b/ccw/src/core/routes/graph-routes.md @@ -20,9 +20,7 @@ Query all symbols from the CodexLens SQLite database and return them as graph no "name": "functionName", "type": "FUNCTION", "file": "src/file.ts", - "line": 10, - "docstring": "function_type", - "tokenCount": 45 + "line": 10 } ] } @@ -98,7 +96,7 @@ Maps source code paths to CodexLens index database paths following the storage s ### Database Schema Queries two main tables: 1. **symbols** - Code symbol definitions - - `id`, `file_id`, `name`, `kind`, `start_line`, `end_line`, `token_count`, `symbol_type` + - `id`, `file_id`, `name`, `kind`, `start_line`, `end_line` 2. **code_relationships** - Inter-symbol dependencies - `id`, `source_symbol_id`, `target_qualified_name`, `relationship_type`, `source_line`, `target_file` diff --git a/ccw/src/core/routes/graph-routes.ts b/ccw/src/core/routes/graph-routes.ts index abcafe71..0059b293 100644 --- a/ccw/src/core/routes/graph-routes.ts +++ b/ccw/src/core/routes/graph-routes.ts @@ -5,7 +5,7 @@ import type { IncomingMessage, ServerResponse } from 'http'; import { homedir } from 'os'; import { join, resolve, normalize } from 'path'; -import { existsSync } from 'fs'; +import { existsSync, readdirSync } from 'fs'; import Database from 'better-sqlite3'; export interface RouteContext { @@ -63,8 +63,6 @@ interface GraphNode { type: string; file: string; line: number; - docstring?: string; - tokenCount?: number; } interface GraphEdge { @@ -108,6 +106,36 @@ function validateProjectPath(projectPath: string, initialPath: string): string | return normalized; } +/** + * Find all _index.db files recursively in a directory + * @param dir Directory to search + * @returns Array of absolute paths to _index.db files + */ +function findAllIndexDbs(dir: string): string[] { + const dbs: string[] = []; + + function traverse(currentDir: string): void { + const dbPath = join(currentDir, '_index.db'); + if (existsSync(dbPath)) { + dbs.push(dbPath); + } + + try { + const entries = readdirSync(currentDir, { withFileTypes: true }); + for (const entry of entries) { + if (entry.isDirectory()) { + traverse(join(currentDir, entry.name)); + } + } + } catch { + // Silently skip directories we can't read + } + } + + traverse(dir); + return dbs; +} + /** * Map codex-lens symbol kinds to graph node types */ @@ -138,93 +166,117 @@ function mapRelationType(relType: string): string { } /** - * Query symbols from codex-lens database + * Query symbols from all codex-lens databases (hierarchical structure) */ async function querySymbols(projectPath: string): Promise { const mapper = new PathMapper(); - const dbPath = mapper.sourceToIndexDb(projectPath); + const rootDbPath = mapper.sourceToIndexDb(projectPath); + const indexRoot = rootDbPath.replace(/[\\/]_index\.db$/, ''); - if (!existsSync(dbPath)) { + if (!existsSync(indexRoot)) { return []; } - try { - const db = Database(dbPath, { readonly: true }); + // Find all _index.db files recursively + const dbPaths = findAllIndexDbs(indexRoot); - const rows = db.prepare(` - SELECT - s.id, - s.name, - s.kind, - s.start_line, - s.token_count, - s.symbol_type, - f.path as file - FROM symbols s - JOIN files f ON s.file_id = f.id - ORDER BY f.path, s.start_line - `).all(); - - db.close(); - - return rows.map((row: any) => ({ - id: `${row.file}:${row.name}:${row.start_line}`, - name: row.name, - type: mapSymbolKind(row.kind), - file: row.file, - line: row.start_line, - docstring: row.symbol_type || undefined, - tokenCount: row.token_count || undefined, - })); - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - console.error(`[Graph] Failed to query symbols: ${message}`); + if (dbPaths.length === 0) { return []; } + + const allNodes: GraphNode[] = []; + + for (const dbPath of dbPaths) { + try { + const db = Database(dbPath, { readonly: true }); + + const rows = db.prepare(` + SELECT + s.id, + s.name, + s.kind, + s.start_line, + f.full_path as file + FROM symbols s + JOIN files f ON s.file_id = f.id + ORDER BY f.full_path, s.start_line + `).all(); + + db.close(); + + allNodes.push(...rows.map((row: any) => ({ + id: `${row.file}:${row.name}:${row.start_line}`, + name: row.name, + type: mapSymbolKind(row.kind), + file: row.file, + line: row.start_line, + }))); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + console.error(`[Graph] Failed to query symbols from ${dbPath}: ${message}`); + // Continue with other databases even if one fails + } + } + + return allNodes; } /** - * Query code relationships from codex-lens database + * Query code relationships from all codex-lens databases (hierarchical structure) */ async function queryRelationships(projectPath: string): Promise { const mapper = new PathMapper(); - const dbPath = mapper.sourceToIndexDb(projectPath); + const rootDbPath = mapper.sourceToIndexDb(projectPath); + const indexRoot = rootDbPath.replace(/[\\/]_index\.db$/, ''); - if (!existsSync(dbPath)) { + if (!existsSync(indexRoot)) { return []; } - try { - const db = Database(dbPath, { readonly: true }); + // Find all _index.db files recursively + const dbPaths = findAllIndexDbs(indexRoot); - const rows = db.prepare(` - SELECT - s.name as source_name, - s.start_line as source_line, - f.path as source_file, - r.target_qualified_name, - r.relationship_type, - r.target_file - FROM code_relationships r - JOIN symbols s ON r.source_symbol_id = s.id - JOIN files f ON s.file_id = f.id - ORDER BY f.path, s.start_line - `).all(); - - db.close(); - - return rows.map((row: any) => ({ - source: `${row.source_file}:${row.source_name}:${row.source_line}`, - target: row.target_qualified_name, - type: mapRelationType(row.relationship_type), - sourceLine: row.source_line, - sourceFile: row.source_file, - })); - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - console.error(`[Graph] Failed to query relationships: ${message}`); + if (dbPaths.length === 0) { return []; } + + const allEdges: GraphEdge[] = []; + + for (const dbPath of dbPaths) { + try { + const db = Database(dbPath, { readonly: true }); + + const rows = db.prepare(` + SELECT + s.name as source_name, + s.start_line as source_line, + f.full_path as source_file, + r.target_qualified_name, + r.relationship_type, + r.target_file + FROM code_relationships r + JOIN symbols s ON r.source_symbol_id = s.id + JOIN files f ON s.file_id = f.id + ORDER BY f.full_path, s.start_line + `).all(); + + db.close(); + + allEdges.push(...rows.map((row: any) => ({ + source: `${row.source_file}:${row.source_name}:${row.source_line}`, + target: row.target_qualified_name, + type: mapRelationType(row.relationship_type), + sourceLine: row.source_line, + sourceFile: row.source_file, + }))); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + console.error(`[Graph] Failed to query relationships from ${dbPath}: ${message}`); + // Continue with other databases even if one fails + } + } + + return allEdges; } /** @@ -292,7 +344,7 @@ async function analyzeImpact(projectPath: string, symbolId: string): Promise { if (pathname === '/api/graph/nodes') { const rawPath = url.searchParams.get('path') || initialPath; const projectPath = validateProjectPath(rawPath, initialPath); + const limitStr = url.searchParams.get('limit') || '1000'; + const limit = Math.min(parseInt(limitStr, 10) || 1000, 5000); // Max 5000 nodes if (!projectPath) { res.writeHead(400, { 'Content-Type': 'application/json' }); @@ -338,9 +392,15 @@ export async function handleGraphRoutes(ctx: RouteContext): Promise { } try { - const nodes = await querySymbols(projectPath); + const allNodes = await querySymbols(projectPath); + const nodes = allNodes.slice(0, limit); res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ nodes })); + res.end(JSON.stringify({ + nodes, + total: allNodes.length, + limit, + hasMore: allNodes.length > limit + })); } catch (err) { console.error(`[Graph] Error fetching nodes:`, err); res.writeHead(500, { 'Content-Type': 'application/json' }); @@ -353,6 +413,8 @@ export async function handleGraphRoutes(ctx: RouteContext): Promise { if (pathname === '/api/graph/edges') { const rawPath = url.searchParams.get('path') || initialPath; const projectPath = validateProjectPath(rawPath, initialPath); + const limitStr = url.searchParams.get('limit') || '2000'; + const limit = Math.min(parseInt(limitStr, 10) || 2000, 10000); // Max 10000 edges if (!projectPath) { res.writeHead(400, { 'Content-Type': 'application/json' }); @@ -361,9 +423,15 @@ export async function handleGraphRoutes(ctx: RouteContext): Promise { } try { - const edges = await queryRelationships(projectPath); + const allEdges = await queryRelationships(projectPath); + const edges = allEdges.slice(0, limit); res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ edges })); + res.end(JSON.stringify({ + edges, + total: allEdges.length, + limit, + hasMore: allEdges.length > limit + })); } catch (err) { console.error(`[Graph] Error fetching edges:`, err); res.writeHead(500, { 'Content-Type': 'application/json' }); diff --git a/ccw/src/core/routes/memory-routes.ts b/ccw/src/core/routes/memory-routes.ts index af9474fd..d42e9e2d 100644 --- a/ccw/src/core/routes/memory-routes.ts +++ b/ccw/src/core/routes/memory-routes.ts @@ -1,4 +1,3 @@ -// @ts-nocheck import http from 'http'; import { URL } from 'url'; import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync, unlinkSync } from 'fs'; @@ -222,7 +221,7 @@ export async function handleMemoryRoutes(ctx: RouteContext): Promise { 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'; + const recursive = url.searchParams.get('recursive') !== 'false'; try { let prompts; @@ -230,7 +229,7 @@ export async function handleMemoryRoutes(ctx: RouteContext): Promise { // Recursive mode: aggregate prompts from parent and child projects if (recursive && !search) { const { getAggregatedPrompts } = await import('../memory-store.js'); - prompts = getAggregatedPrompts(projectPath, limit); + prompts = await getAggregatedPrompts(projectPath, limit); } else { // Non-recursive mode or search mode: query only current project const memoryStore = getMemoryStore(projectPath); @@ -390,11 +389,11 @@ Return ONLY valid JSON in this exact format (no markdown, no code blocks, just p mode: 'analysis', timeout: 120000, cd: projectPath, - category: 'insights' + category: 'insight' }); // Try to parse JSON from response - let insights = { patterns: [], suggestions: [] }; + let insights: { patterns: any[]; suggestions: any[] } = { patterns: [], suggestions: [] }; if (result.stdout) { let outputText = result.stdout; @@ -515,13 +514,13 @@ 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'; + const recursive = url.searchParams.get('recursive') !== 'false'; 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); + const aggregatedStats = await getAggregatedStats(projectPath); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ diff --git a/ccw/src/core/routes/status-routes.ts b/ccw/src/core/routes/status-routes.ts new file mode 100644 index 00000000..3b18f512 --- /dev/null +++ b/ccw/src/core/routes/status-routes.ts @@ -0,0 +1,57 @@ +// @ts-nocheck +/** + * Status Routes Module + * Aggregated status endpoint for faster dashboard loading + */ +import type { IncomingMessage, ServerResponse } from 'http'; +import { getCliToolsStatus } from '../../tools/cli-executor.js'; +import { checkVenvStatus, checkSemanticStatus } from '../../tools/codex-lens.js'; + +export interface RouteContext { + pathname: string; + url: URL; + req: IncomingMessage; + res: ServerResponse; + initialPath: string; + handlePostRequest: (req: IncomingMessage, res: ServerResponse, handler: (body: unknown) => Promise) => void; + broadcastToClients: (data: unknown) => void; +} + +/** + * Handle status routes + * @returns true if route was handled, false otherwise + */ +export async function handleStatusRoutes(ctx: RouteContext): Promise { + const { pathname, res } = ctx; + + // API: Aggregated Status (all statuses in one call) + if (pathname === '/api/status/all') { + try { + // Execute all status checks in parallel + const [cliStatus, codexLensStatus, semanticStatus] = await Promise.all([ + getCliToolsStatus(), + checkVenvStatus(), + // Always check semantic status (will return available: false if CodexLens not ready) + checkSemanticStatus().catch(() => ({ available: false, backend: null })) + ]); + + const response = { + cli: cliStatus, + codexLens: codexLensStatus, + semantic: semanticStatus, + timestamp: new Date().toISOString() + }; + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(response)); + return true; + } catch (error) { + console.error('[Status Routes] Error fetching aggregated status:', error); + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: (error as Error).message })); + return true; + } + } + + return false; +} diff --git a/ccw/src/core/server.ts b/ccw/src/core/server.ts index 9f6d4297..88301984 100644 --- a/ccw/src/core/server.ts +++ b/ccw/src/core/server.ts @@ -6,6 +6,7 @@ import { join } from 'path'; import { resolvePath, getRecentPaths, normalizePathForDisplay } from '../utils/path-resolver.js'; // Import route handlers +import { handleStatusRoutes } from './routes/status-routes.js'; import { handleCliRoutes } from './routes/cli-routes.js'; import { handleMemoryRoutes } from './routes/memory-routes.js'; import { handleMcpRoutes } from './routes/mcp-routes.js'; @@ -243,6 +244,11 @@ export async function startServer(options: ServerOptions = {}): Promise
- ~500MB + ~130MB
' + + ''; + } + if (window.lucide) lucide.createIcons(); + } catch (err) { + container.innerHTML = + '
' + t('common.error') + ': ' + err.message + '
'; + } +} + +// Install semantic dependencies +async function installSemanticDeps() { + var container = document.getElementById('semanticDepsStatus'); + if (!container) return; + + container.innerHTML = + '
' + t('codexlens.installingDeps') + '
'; + + try { + var response = await fetch('/api/codexlens/semantic/install', { method: 'POST' }); + var result = await response.json(); + + if (result.success) { + showRefreshToast(t('codexlens.depsInstalled'), 'success'); + await loadSemanticDepsStatus(); + await loadModelList(); + } else { + showRefreshToast(t('codexlens.depsInstallFailed') + ': ' + result.error, 'error'); + await loadSemanticDepsStatus(); + } + } catch (err) { + showRefreshToast(t('common.error') + ': ' + err.message, 'error'); + await loadSemanticDepsStatus(); + } +} + +// Load model list +async function loadModelList() { + var container = document.getElementById('modelListContainer'); + if (!container) return; + + try { + var response = await fetch('/api/codexlens/models'); + var result = await response.json(); + + if (!result.success || !result.result || !result.result.models) { + container.innerHTML = + '
' + t('codexlens.semanticNotInstalled') + '
'; + return; + } + + var models = result.result.models; + var html = '
'; + + models.forEach(function(model) { + var statusIcon = model.installed + ? '' + : ''; + + var sizeText = model.installed + ? model.actual_size_mb.toFixed(1) + ' MB' + : '~' + model.estimated_size_mb + ' MB'; + + var actionBtn = model.installed + ? '' + : ''; + + html += + '
' + + '
' + + '
' + + '
' + + statusIcon + + '' + model.profile + '' + + '(' + model.dimensions + ' dims)' + + '
' + + '
' + model.model_name + '
' + + '
' + model.use_case + '
' + + '
' + + '
' + + '
' + sizeText + '
' + + actionBtn + + '
' + + '
' + + '
'; + }); + + html += '
'; + container.innerHTML = html; + if (window.lucide) lucide.createIcons(); + } catch (err) { + container.innerHTML = + '
' + t('common.error') + ': ' + err.message + '
'; + } +} + +// Download model +async function downloadModel(profile) { + var modelCard = document.getElementById('model-' + profile); + if (!modelCard) return; + + var originalHTML = modelCard.innerHTML; + modelCard.innerHTML = + '
' + + '' + t('codexlens.downloading') + '' + + '
'; + + try { + var response = await fetch('/api/codexlens/models/download', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ profile: profile }) + }); + + var result = await response.json(); + + if (result.success) { + showRefreshToast(t('codexlens.modelDownloaded') + ': ' + profile, 'success'); + await loadModelList(); + } else { + showRefreshToast(t('codexlens.modelDownloadFailed') + ': ' + result.error, 'error'); + modelCard.innerHTML = originalHTML; + if (window.lucide) lucide.createIcons(); + } + } catch (err) { + showRefreshToast(t('common.error') + ': ' + err.message, 'error'); + modelCard.innerHTML = originalHTML; + if (window.lucide) lucide.createIcons(); + } +} + +// Delete model +async function deleteModel(profile) { + if (!confirm(t('codexlens.deleteModelConfirm') + ' ' + profile + '?')) { + return; + } + + var modelCard = document.getElementById('model-' + profile); + if (!modelCard) return; + + var originalHTML = modelCard.innerHTML; + modelCard.innerHTML = + '
' + + '' + t('codexlens.deleting') + '' + + '
'; + + try { + var response = await fetch('/api/codexlens/models/delete', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ profile: profile }) + }); + + var result = await response.json(); + + if (result.success) { + showRefreshToast(t('codexlens.modelDeleted') + ': ' + profile, 'success'); + await loadModelList(); + } else { + showRefreshToast(t('codexlens.modelDeleteFailed') + ': ' + result.error, 'error'); + modelCard.innerHTML = originalHTML; + if (window.lucide) lucide.createIcons(); + } + } catch (err) { + showRefreshToast(t('common.error') + ': ' + err.message, 'error'); + modelCard.innerHTML = originalHTML; + if (window.lucide) lucide.createIcons(); + } } async function cleanCodexLensIndexes() { diff --git a/ccw/src/templates/dashboard-js/views/codexlens-manager.js b/ccw/src/templates/dashboard-js/views/codexlens-manager.js new file mode 100644 index 00000000..9ecbb456 --- /dev/null +++ b/ccw/src/templates/dashboard-js/views/codexlens-manager.js @@ -0,0 +1,596 @@ +// CodexLens Manager - Configuration, Model Management, and Semantic Dependencies +// Extracted from cli-manager.js for better maintainability + +// ============================================================ +// CODEXLENS CONFIGURATION MODAL +// ============================================================ + +/** + * Show CodexLens configuration modal + */ +async function showCodexLensConfigModal() { + try { + showRefreshToast(t('codexlens.loadingConfig'), 'info'); + + // Fetch current config + const response = await fetch('/api/codexlens/config'); + const config = await response.json(); + + const modalHtml = buildCodexLensConfigContent(config); + + // Create and show modal + const modalContainer = document.createElement('div'); + modalContainer.innerHTML = modalHtml; + document.body.appendChild(modalContainer); + + // Initialize icons + if (window.lucide) lucide.createIcons(); + + // Initialize event handlers + initCodexLensConfigEvents(config); + } catch (err) { + showRefreshToast(t('common.error') + ': ' + err.message, 'error'); + } +} + +/** + * Build CodexLens configuration modal content + */ +function buildCodexLensConfigContent(config) { + const indexDir = config.index_dir || '~/.codexlens/indexes'; + const indexCount = config.index_count || 0; + const isInstalled = window.cliToolsStatus?.codexlens?.installed || false; + + return '