mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-10 02:24:35 +08:00
feat: add navigation status routes and update badge aggregation logic
This commit is contained in:
@@ -1785,18 +1785,65 @@ except Exception as e:
|
||||
|
||||
// Map settings to env var format for defaults
|
||||
const settingsDefaults: Record<string, string> = {};
|
||||
|
||||
// Embedding settings
|
||||
if (settings.embedding?.backend) {
|
||||
settingsDefaults['CODEXLENS_EMBEDDING_BACKEND'] = settings.embedding.backend;
|
||||
}
|
||||
if (settings.embedding?.model) {
|
||||
settingsDefaults['CODEXLENS_EMBEDDING_MODEL'] = settings.embedding.model;
|
||||
settingsDefaults['LITELLM_EMBEDDING_MODEL'] = settings.embedding.model;
|
||||
}
|
||||
if (settings.embedding?.use_gpu !== undefined) {
|
||||
settingsDefaults['CODEXLENS_USE_GPU'] = String(settings.embedding.use_gpu);
|
||||
}
|
||||
if (settings.embedding?.strategy) {
|
||||
settingsDefaults['CODEXLENS_EMBEDDING_STRATEGY'] = settings.embedding.strategy;
|
||||
}
|
||||
if (settings.embedding?.cooldown !== undefined) {
|
||||
settingsDefaults['CODEXLENS_EMBEDDING_COOLDOWN'] = String(settings.embedding.cooldown);
|
||||
}
|
||||
|
||||
// Reranker settings
|
||||
if (settings.reranker?.backend) {
|
||||
// Map 'api' to 'litellm' for UI consistency
|
||||
settingsDefaults['CODEXLENS_RERANKER_BACKEND'] = settings.reranker.backend === 'api' ? 'litellm' : settings.reranker.backend;
|
||||
settingsDefaults['CODEXLENS_RERANKER_BACKEND'] = settings.reranker.backend;
|
||||
}
|
||||
if (settings.reranker?.model) {
|
||||
settingsDefaults['CODEXLENS_RERANKER_MODEL'] = settings.reranker.model;
|
||||
settingsDefaults['LITELLM_RERANKER_MODEL'] = settings.reranker.model;
|
||||
}
|
||||
if (settings.reranker?.enabled !== undefined) {
|
||||
settingsDefaults['CODEXLENS_RERANKER_ENABLED'] = String(settings.reranker.enabled);
|
||||
}
|
||||
if (settings.reranker?.top_k !== undefined) {
|
||||
settingsDefaults['CODEXLENS_RERANKER_TOP_K'] = String(settings.reranker.top_k);
|
||||
}
|
||||
|
||||
// API/Concurrency settings
|
||||
if (settings.api?.max_workers !== undefined) {
|
||||
settingsDefaults['CODEXLENS_API_MAX_WORKERS'] = String(settings.api.max_workers);
|
||||
}
|
||||
if (settings.api?.batch_size !== undefined) {
|
||||
settingsDefaults['CODEXLENS_API_BATCH_SIZE'] = String(settings.api.batch_size);
|
||||
}
|
||||
|
||||
// Cascade search settings
|
||||
if (settings.cascade?.strategy) {
|
||||
settingsDefaults['CODEXLENS_CASCADE_STRATEGY'] = settings.cascade.strategy;
|
||||
}
|
||||
if (settings.cascade?.coarse_k !== undefined) {
|
||||
settingsDefaults['CODEXLENS_CASCADE_COARSE_K'] = String(settings.cascade.coarse_k);
|
||||
}
|
||||
if (settings.cascade?.fine_k !== undefined) {
|
||||
settingsDefaults['CODEXLENS_CASCADE_FINE_K'] = String(settings.cascade.fine_k);
|
||||
}
|
||||
|
||||
// LLM settings
|
||||
if (settings.llm?.enabled !== undefined) {
|
||||
settingsDefaults['CODEXLENS_LLM_ENABLED'] = String(settings.llm.enabled);
|
||||
}
|
||||
if (settings.llm?.batch_size !== undefined) {
|
||||
settingsDefaults['CODEXLENS_LLM_BATCH_SIZE'] = String(settings.llm.batch_size);
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
@@ -1956,10 +2003,57 @@ except Exception as e:
|
||||
|
||||
await writeFile(envPath, lines.join('\n'), 'utf-8');
|
||||
|
||||
// Also update settings.json with mapped values
|
||||
const settingsPath = join(homedir(), '.codexlens', 'settings.json');
|
||||
let settings: Record<string, any> = {};
|
||||
try {
|
||||
const settingsContent = await readFile(settingsPath, 'utf-8');
|
||||
settings = JSON.parse(settingsContent);
|
||||
} catch (e) {
|
||||
// File doesn't exist, create default structure
|
||||
settings = { embedding: {}, reranker: {}, api: {}, cascade: {}, llm: {} };
|
||||
}
|
||||
|
||||
// Map env vars to settings.json structure
|
||||
const envToSettings: Record<string, { path: string[], transform?: (v: string) => any }> = {
|
||||
'CODEXLENS_EMBEDDING_BACKEND': { path: ['embedding', 'backend'] },
|
||||
'CODEXLENS_EMBEDDING_MODEL': { path: ['embedding', 'model'] },
|
||||
'CODEXLENS_USE_GPU': { path: ['embedding', 'use_gpu'], transform: v => v === 'true' },
|
||||
'CODEXLENS_EMBEDDING_STRATEGY': { path: ['embedding', 'strategy'] },
|
||||
'CODEXLENS_EMBEDDING_COOLDOWN': { path: ['embedding', 'cooldown'], transform: v => parseFloat(v) },
|
||||
'CODEXLENS_RERANKER_BACKEND': { path: ['reranker', 'backend'] },
|
||||
'CODEXLENS_RERANKER_MODEL': { path: ['reranker', 'model'] },
|
||||
'CODEXLENS_RERANKER_ENABLED': { path: ['reranker', 'enabled'], transform: v => v === 'true' },
|
||||
'CODEXLENS_RERANKER_TOP_K': { path: ['reranker', 'top_k'], transform: v => parseInt(v, 10) },
|
||||
'CODEXLENS_API_MAX_WORKERS': { path: ['api', 'max_workers'], transform: v => parseInt(v, 10) },
|
||||
'CODEXLENS_API_BATCH_SIZE': { path: ['api', 'batch_size'], transform: v => parseInt(v, 10) },
|
||||
'CODEXLENS_CASCADE_STRATEGY': { path: ['cascade', 'strategy'] },
|
||||
'CODEXLENS_CASCADE_COARSE_K': { path: ['cascade', 'coarse_k'], transform: v => parseInt(v, 10) },
|
||||
'CODEXLENS_CASCADE_FINE_K': { path: ['cascade', 'fine_k'], transform: v => parseInt(v, 10) },
|
||||
'CODEXLENS_LLM_ENABLED': { path: ['llm', 'enabled'], transform: v => v === 'true' },
|
||||
'CODEXLENS_LLM_BATCH_SIZE': { path: ['llm', 'batch_size'], transform: v => parseInt(v, 10) },
|
||||
'LITELLM_EMBEDDING_MODEL': { path: ['embedding', 'model'] },
|
||||
'LITELLM_RERANKER_MODEL': { path: ['reranker', 'model'] }
|
||||
};
|
||||
|
||||
// Apply env vars to settings
|
||||
for (const [envKey, value] of Object.entries(env)) {
|
||||
const mapping = envToSettings[envKey];
|
||||
if (mapping && value) {
|
||||
const [section, key] = mapping.path;
|
||||
if (!settings[section]) settings[section] = {};
|
||||
settings[section][key] = mapping.transform ? mapping.transform(value) : value;
|
||||
}
|
||||
}
|
||||
|
||||
// Write updated settings
|
||||
await writeFile(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Environment configuration saved',
|
||||
path: envPath
|
||||
message: 'Environment and settings configuration saved',
|
||||
path: envPath,
|
||||
settingsPath
|
||||
};
|
||||
} catch (err) {
|
||||
return { success: false, error: err.message, status: 500 };
|
||||
|
||||
240
ccw/src/core/routes/nav-status-routes.ts
Normal file
240
ccw/src/core/routes/nav-status-routes.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
// @ts-nocheck
|
||||
/**
|
||||
* Navigation Status Routes Module
|
||||
* Aggregated status endpoint for navigation bar badge updates
|
||||
*
|
||||
* API Endpoints:
|
||||
* - GET /api/nav-status - Get aggregated navigation bar status (counts for all badges)
|
||||
*/
|
||||
import type { IncomingMessage, ServerResponse } from 'http';
|
||||
import { existsSync, readFileSync, readdirSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { homedir } from 'os';
|
||||
|
||||
export interface RouteContext {
|
||||
pathname: string;
|
||||
url: URL;
|
||||
req: IncomingMessage;
|
||||
res: ServerResponse;
|
||||
initialPath: string;
|
||||
}
|
||||
|
||||
// ========== Count Helper Functions ==========
|
||||
|
||||
/**
|
||||
* Count issues from JSONL file
|
||||
*/
|
||||
function countIssues(projectPath: string): number {
|
||||
const issuesPath = join(projectPath, '.workflow', 'issues', 'issues.jsonl');
|
||||
if (!existsSync(issuesPath)) return 0;
|
||||
try {
|
||||
const content = readFileSync(issuesPath, 'utf8');
|
||||
return content.split('\n').filter(line => line.trim()).length;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Count discoveries from index or directory scan
|
||||
*/
|
||||
function countDiscoveries(projectPath: string): number {
|
||||
const discoveriesDir = join(projectPath, '.workflow', 'issues', 'discoveries');
|
||||
const indexPath = join(discoveriesDir, 'index.json');
|
||||
|
||||
// Try index.json first
|
||||
if (existsSync(indexPath)) {
|
||||
try {
|
||||
const index = JSON.parse(readFileSync(indexPath, 'utf8'));
|
||||
return index.discoveries?.length || 0;
|
||||
} catch { /* fall through */ }
|
||||
}
|
||||
|
||||
// Fallback: scan directory
|
||||
if (!existsSync(discoveriesDir)) return 0;
|
||||
try {
|
||||
const entries = readdirSync(discoveriesDir, { withFileTypes: true });
|
||||
return entries.filter(e => e.isDirectory() && e.name.startsWith('DSC-')).length;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Count skills from project and user directories
|
||||
*/
|
||||
function countSkills(projectPath: string): { project: number; user: number; total: number } {
|
||||
let project = 0, user = 0;
|
||||
|
||||
// Project skills
|
||||
const projectSkillsDir = join(projectPath, '.claude', 'skills');
|
||||
if (existsSync(projectSkillsDir)) {
|
||||
try {
|
||||
const entries = readdirSync(projectSkillsDir, { withFileTypes: true });
|
||||
project = entries.filter(e =>
|
||||
e.isDirectory() && existsSync(join(projectSkillsDir, e.name, 'SKILL.md'))
|
||||
).length;
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
// User skills
|
||||
const userSkillsDir = join(homedir(), '.claude', 'skills');
|
||||
if (existsSync(userSkillsDir)) {
|
||||
try {
|
||||
const entries = readdirSync(userSkillsDir, { withFileTypes: true });
|
||||
user = entries.filter(e =>
|
||||
e.isDirectory() && existsSync(join(userSkillsDir, e.name, 'SKILL.md'))
|
||||
).length;
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
return { project, user, total: project + user };
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively count rules in a directory
|
||||
*/
|
||||
function countRulesInDir(dirPath: string): number {
|
||||
if (!existsSync(dirPath)) return 0;
|
||||
let count = 0;
|
||||
try {
|
||||
const entries = readdirSync(dirPath, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.isFile() && entry.name.endsWith('.md')) {
|
||||
count++;
|
||||
} else if (entry.isDirectory()) {
|
||||
count += countRulesInDir(join(dirPath, entry.name));
|
||||
}
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count rules from project and user directories
|
||||
*/
|
||||
function countRules(projectPath: string): { project: number; user: number; total: number } {
|
||||
const project = countRulesInDir(join(projectPath, '.claude', 'rules'));
|
||||
const user = countRulesInDir(join(homedir(), '.claude', 'rules'));
|
||||
return { project, user, total: project + user };
|
||||
}
|
||||
|
||||
/**
|
||||
* Count CLAUDE.md files
|
||||
*/
|
||||
function countClaudeFiles(projectPath: string): number {
|
||||
let count = 0;
|
||||
const EXCLUDES = ['.git', 'node_modules', 'dist', 'build', '.venv', 'venv', '__pycache__', 'coverage', '.workflow'];
|
||||
|
||||
// User main
|
||||
if (existsSync(join(homedir(), '.claude', 'CLAUDE.md'))) count++;
|
||||
|
||||
// Project main
|
||||
if (existsSync(join(projectPath, '.claude', 'CLAUDE.md'))) count++;
|
||||
|
||||
// Root CLAUDE.md
|
||||
if (existsSync(join(projectPath, 'CLAUDE.md'))) count++;
|
||||
|
||||
// Module-level (scan project subdirectories for CLAUDE.md files)
|
||||
function scanDir(dir: string, depth: number = 0) {
|
||||
if (depth > 3) return; // Limit recursion depth
|
||||
try {
|
||||
const entries = readdirSync(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory() && !EXCLUDES.includes(entry.name) && !entry.name.startsWith('.')) {
|
||||
const subDir = join(dir, entry.name);
|
||||
if (existsSync(join(subDir, 'CLAUDE.md'))) count++;
|
||||
scanDir(subDir, depth + 1);
|
||||
}
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
scanDir(projectPath);
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count hooks from settings object
|
||||
*/
|
||||
function countHooksFromSettings(settings: any): number {
|
||||
if (!settings?.hooks) return 0;
|
||||
let count = 0;
|
||||
for (const event of Object.keys(settings.hooks)) {
|
||||
const hookList = settings.hooks[event];
|
||||
count += Array.isArray(hookList) ? hookList.length : 1;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count hooks from global and project settings
|
||||
*/
|
||||
function countHooks(projectPath: string): { global: number; project: number; total: number } {
|
||||
let global = 0, project = 0;
|
||||
|
||||
// Global settings
|
||||
const globalSettingsPath = join(homedir(), '.claude', 'settings.json');
|
||||
if (existsSync(globalSettingsPath)) {
|
||||
try {
|
||||
const settings = JSON.parse(readFileSync(globalSettingsPath, 'utf8'));
|
||||
global = countHooksFromSettings(settings);
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
// Project settings
|
||||
const projectSettingsPath = join(projectPath, '.claude', 'settings.json');
|
||||
if (existsSync(projectSettingsPath)) {
|
||||
try {
|
||||
const settings = JSON.parse(readFileSync(projectSettingsPath, 'utf8'));
|
||||
project = countHooksFromSettings(settings);
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
return { global, project, total: global + project };
|
||||
}
|
||||
|
||||
// ========== Route Handler ==========
|
||||
|
||||
export async function handleNavStatusRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
const { pathname, url, res, initialPath } = ctx;
|
||||
|
||||
// GET /api/nav-status - Aggregated navigation badge status
|
||||
if (pathname === '/api/nav-status' && ctx.req.method === 'GET') {
|
||||
try {
|
||||
const projectPath = url.searchParams.get('path') || initialPath;
|
||||
|
||||
// Execute all counts (synchronous file reads wrapped in Promise.resolve for consistency)
|
||||
const [issues, discoveries, skills, rules, claude, hooks] = await Promise.all([
|
||||
Promise.resolve(countIssues(projectPath)),
|
||||
Promise.resolve(countDiscoveries(projectPath)),
|
||||
Promise.resolve(countSkills(projectPath)),
|
||||
Promise.resolve(countRules(projectPath)),
|
||||
Promise.resolve(countClaudeFiles(projectPath)),
|
||||
Promise.resolve(countHooks(projectPath))
|
||||
]);
|
||||
|
||||
const response = {
|
||||
issues: { count: issues },
|
||||
discoveries: { count: discoveries },
|
||||
skills: { count: skills.total, project: skills.project, user: skills.user },
|
||||
rules: { count: rules.total, project: rules.project, user: rules.user },
|
||||
claude: { count: claude },
|
||||
hooks: { count: hooks.total, global: hooks.global, project: hooks.project },
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(response));
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[Nav Status] Error:', error);
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (error as Error).message }));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import { handleClaudeRoutes } from './routes/claude-routes.js';
|
||||
import { handleHelpRoutes } from './routes/help-routes.js';
|
||||
import { handleLiteLLMRoutes } from './routes/litellm-routes.js';
|
||||
import { handleLiteLLMApiRoutes } from './routes/litellm-api-routes.js';
|
||||
import { handleNavStatusRoutes } from './routes/nav-status-routes.js';
|
||||
|
||||
// Import WebSocket handling
|
||||
import { handleWebSocketUpgrade, broadcastToClients } from './websocket.js';
|
||||
@@ -287,6 +288,11 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
|
||||
if (await handleStatusRoutes(routeContext)) return;
|
||||
}
|
||||
|
||||
// Navigation status routes (/api/nav-status) - Aggregated badge counts
|
||||
if (pathname === '/api/nav-status') {
|
||||
if (await handleNavStatusRoutes(routeContext)) return;
|
||||
}
|
||||
|
||||
// CLI routes (/api/cli/*)
|
||||
if (pathname.startsWith('/api/cli/')) {
|
||||
if (await handleCliRoutes(routeContext)) return;
|
||||
|
||||
Reference in New Issue
Block a user