Files
CrmClinicas/.claude/helpers/statusline.cjs
Consultoria AS 79b5d86325 feat: CRM Clinicas SaaS - MVP completo
- Auth: Login/Register con creacion de clinica
- Dashboard: KPIs reales, graficas recharts
- Pacientes: CRUD completo con busqueda
- Agenda: FullCalendar, drag-and-drop, vista recepcion
- Expediente: Notas SOAP, signos vitales, CIE-10
- Facturacion: Facturas con IVA, campos CFDI SAT
- Inventario: Productos, stock, movimientos, alertas
- Configuracion: Clinica, equipo, catalogo servicios
- Supabase self-hosted: 18 tablas con RLS multi-tenant
- Docker + Nginx para produccion

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-03 07:04:14 +00:00

578 lines
22 KiB
JavaScript

#!/usr/bin/env node
/**
* Claude Flow V3 Statusline Generator
* Displays real-time V3 implementation progress and system status
*
* Usage: node statusline.cjs [options]
*
* Options:
* (default) Safe multi-line output with collision zone avoidance
* --single Single-line output (completely avoids collision)
* --unsafe Legacy multi-line without collision avoidance
* --legacy Alias for --unsafe
* --json JSON output with pretty printing
* --compact JSON output without formatting
*
* Collision Zone Fix (Issue #985):
* Claude Code writes its internal status (e.g., "7s • 1p") at absolute
* terminal coordinates (columns 15-25 on second-to-last line). The safe
* mode pads the collision line with spaces to push content past column 25.
*
* IMPORTANT: This file uses .cjs extension to work in ES module projects.
* The require() syntax is intentional for CommonJS compatibility.
*/
/* eslint-disable @typescript-eslint/no-var-requires */
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
// Configuration
const CONFIG = {
enabled: true,
showProgress: true,
showSecurity: true,
showSwarm: true,
showHooks: true,
showPerformance: true,
refreshInterval: 5000,
maxAgents: 15,
topology: 'hierarchical-mesh',
};
// Cross-platform helpers
const isWindows = process.platform === 'win32';
const nullDevice = isWindows ? 'NUL' : '/dev/null';
// ANSI colors
const c = {
reset: '\x1b[0m',
bold: '\x1b[1m',
dim: '\x1b[2m',
red: '\x1b[0;31m',
green: '\x1b[0;32m',
yellow: '\x1b[0;33m',
blue: '\x1b[0;34m',
purple: '\x1b[0;35m',
cyan: '\x1b[0;36m',
brightRed: '\x1b[1;31m',
brightGreen: '\x1b[1;32m',
brightYellow: '\x1b[1;33m',
brightBlue: '\x1b[1;34m',
brightPurple: '\x1b[1;35m',
brightCyan: '\x1b[1;36m',
brightWhite: '\x1b[1;37m',
};
// Get user info
function getUserInfo() {
let name = 'user';
let gitBranch = '';
let modelName = 'Unknown';
try {
const gitUserCmd = isWindows
? 'git config user.name 2>NUL || echo user'
: 'git config user.name 2>/dev/null || echo "user"';
const gitBranchCmd = isWindows
? 'git branch --show-current 2>NUL || echo.'
: 'git branch --show-current 2>/dev/null || echo ""';
name = execSync(gitUserCmd, { encoding: 'utf-8' }).trim();
gitBranch = execSync(gitBranchCmd, { encoding: 'utf-8' }).trim();
if (gitBranch === '.') gitBranch = ''; // Windows echo. outputs a dot
} catch (e) {
// Ignore errors
}
// Auto-detect model from Claude Code's config
try {
const homedir = require('os').homedir();
const claudeConfigPath = path.join(homedir, '.claude.json');
if (fs.existsSync(claudeConfigPath)) {
const claudeConfig = JSON.parse(fs.readFileSync(claudeConfigPath, 'utf-8'));
// Try to find lastModelUsage - check current dir and parent dirs
let lastModelUsage = null;
const cwd = process.cwd();
if (claudeConfig.projects) {
// Try exact match first, then check if cwd starts with any project path
for (const [projectPath, projectConfig] of Object.entries(claudeConfig.projects)) {
if (cwd === projectPath || cwd.startsWith(projectPath + '/')) {
lastModelUsage = projectConfig.lastModelUsage;
break;
}
}
}
if (lastModelUsage) {
const modelIds = Object.keys(lastModelUsage);
if (modelIds.length > 0) {
// Take the last model (most recently added to the object)
// Or find the one with most tokens (most actively used this session)
let modelId = modelIds[modelIds.length - 1];
if (modelIds.length > 1) {
// If multiple models, pick the one with most total tokens
let maxTokens = 0;
for (const id of modelIds) {
const usage = lastModelUsage[id];
const total = (usage.inputTokens || 0) + (usage.outputTokens || 0);
if (total > maxTokens) {
maxTokens = total;
modelId = id;
}
}
}
// Parse model ID to human-readable name
if (modelId.includes('opus')) modelName = 'Opus 4.5';
else if (modelId.includes('sonnet')) modelName = 'Sonnet 4';
else if (modelId.includes('haiku')) modelName = 'Haiku 4.5';
else modelName = modelId.split('-').slice(1, 3).join(' ');
}
}
}
} catch (e) {
// Fallback to Unknown if can't read config
}
return { name, gitBranch, modelName };
}
// Get learning stats from intelligence loop data (ADR-050)
function getLearningStats() {
let patterns = 0;
let sessions = 0;
let trajectories = 0;
let edges = 0;
let confidenceMean = 0;
let accessedCount = 0;
let trend = 'STABLE';
// PRIMARY: Read from intelligence loop data files
const dataDir = path.join(process.cwd(), '.claude-flow', 'data');
// 1. graph-state.json — authoritative node/edge counts
const graphPath = path.join(dataDir, 'graph-state.json');
if (fs.existsSync(graphPath)) {
try {
const graph = JSON.parse(fs.readFileSync(graphPath, 'utf-8'));
patterns = graph.nodes ? Object.keys(graph.nodes).length : 0;
edges = Array.isArray(graph.edges) ? graph.edges.length : 0;
} catch (e) { /* ignore */ }
}
// 2. ranked-context.json — confidence and access data
const rankedPath = path.join(dataDir, 'ranked-context.json');
if (fs.existsSync(rankedPath)) {
try {
const ranked = JSON.parse(fs.readFileSync(rankedPath, 'utf-8'));
if (ranked.entries && ranked.entries.length > 0) {
patterns = Math.max(patterns, ranked.entries.length);
let confSum = 0;
let accCount = 0;
for (let i = 0; i < ranked.entries.length; i++) {
confSum += (ranked.entries[i].confidence || 0);
if ((ranked.entries[i].accessCount || 0) > 0) accCount++;
}
confidenceMean = confSum / ranked.entries.length;
accessedCount = accCount;
}
} catch (e) { /* ignore */ }
}
// 3. intelligence-snapshot.json — trend history
const snapshotPath = path.join(dataDir, 'intelligence-snapshot.json');
if (fs.existsSync(snapshotPath)) {
try {
const snapshot = JSON.parse(fs.readFileSync(snapshotPath, 'utf-8'));
if (snapshot.history && snapshot.history.length >= 2) {
const first = snapshot.history[0];
const last = snapshot.history[snapshot.history.length - 1];
const confDrift = (last.confidenceMean || 0) - (first.confidenceMean || 0);
trend = confDrift > 0.01 ? 'IMPROVING' : confDrift < -0.01 ? 'DECLINING' : 'STABLE';
sessions = Math.max(sessions, snapshot.history.length);
}
} catch (e) { /* ignore */ }
}
// 4. auto-memory-store.json — fallback entry count
if (patterns === 0) {
const autoMemPath = path.join(dataDir, 'auto-memory-store.json');
if (fs.existsSync(autoMemPath)) {
try {
const data = JSON.parse(fs.readFileSync(autoMemPath, 'utf-8'));
patterns = Array.isArray(data) ? data.length : (data.entries ? data.entries.length : 0);
} catch (e) { /* ignore */ }
}
}
// FALLBACK: Legacy memory.db file-size estimation
if (patterns === 0) {
const memoryPaths = [
path.join(process.cwd(), '.swarm', 'memory.db'),
path.join(process.cwd(), '.claude', 'memory.db'),
path.join(process.cwd(), 'data', 'memory.db'),
];
for (let j = 0; j < memoryPaths.length; j++) {
if (fs.existsSync(memoryPaths[j])) {
try {
const dbStats = fs.statSync(memoryPaths[j]);
patterns = Math.floor(dbStats.size / 1024 / 2);
break;
} catch (e) { /* ignore */ }
}
}
}
// Session count from session files
const sessionsPath = path.join(process.cwd(), '.claude', 'sessions');
if (fs.existsSync(sessionsPath)) {
try {
const sessionFiles = fs.readdirSync(sessionsPath).filter(f => f.endsWith('.json'));
sessions = Math.max(sessions, sessionFiles.length);
} catch (e) { /* ignore */ }
}
trajectories = Math.floor(patterns / 5);
return { patterns, sessions, trajectories, edges, confidenceMean, accessedCount, trend };
}
// Get V3 progress from learning state (grows as system learns)
function getV3Progress() {
const learning = getLearningStats();
// DDD progress based on actual learned patterns
// New install: 0 patterns = 0/5 domains, 0% DDD
// As patterns grow: 10+ patterns = 1 domain, 50+ = 2, 100+ = 3, 200+ = 4, 500+ = 5
let domainsCompleted = 0;
if (learning.patterns >= 500) domainsCompleted = 5;
else if (learning.patterns >= 200) domainsCompleted = 4;
else if (learning.patterns >= 100) domainsCompleted = 3;
else if (learning.patterns >= 50) domainsCompleted = 2;
else if (learning.patterns >= 10) domainsCompleted = 1;
const totalDomains = 5;
const dddProgress = Math.min(100, Math.floor((domainsCompleted / totalDomains) * 100));
return {
domainsCompleted,
totalDomains,
dddProgress,
patternsLearned: learning.patterns,
sessionsCompleted: learning.sessions
};
}
// Get security status based on actual scans
function getSecurityStatus() {
// Check for security scan results in memory
const scanResultsPath = path.join(process.cwd(), '.claude', 'security-scans');
let cvesFixed = 0;
const totalCves = 3;
if (fs.existsSync(scanResultsPath)) {
try {
const scans = fs.readdirSync(scanResultsPath).filter(f => f.endsWith('.json'));
// Each successful scan file = 1 CVE addressed
cvesFixed = Math.min(totalCves, scans.length);
} catch (e) {
// Ignore
}
}
// Also check .swarm/security for audit results
const auditPath = path.join(process.cwd(), '.swarm', 'security');
if (fs.existsSync(auditPath)) {
try {
const audits = fs.readdirSync(auditPath).filter(f => f.includes('audit'));
cvesFixed = Math.min(totalCves, Math.max(cvesFixed, audits.length));
} catch (e) {
// Ignore
}
}
const status = cvesFixed >= totalCves ? 'CLEAN' : cvesFixed > 0 ? 'IN_PROGRESS' : 'PENDING';
return {
status,
cvesFixed,
totalCves,
};
}
// Get swarm status
function getSwarmStatus() {
let activeAgents = 0;
let coordinationActive = false;
try {
if (isWindows) {
// Windows: use tasklist and findstr
const ps = execSync('tasklist 2>NUL | findstr /I "agentic-flow" 2>NUL | find /C /V "" 2>NUL || echo 0', { encoding: 'utf-8' });
activeAgents = Math.max(0, parseInt(ps.trim()) || 0);
} else {
const ps = execSync('ps aux 2>/dev/null | grep -c agentic-flow || echo "0"', { encoding: 'utf-8' });
activeAgents = Math.max(0, parseInt(ps.trim()) - 1);
}
coordinationActive = activeAgents > 0;
} catch (e) {
// Ignore errors - default to 0 agents
}
return {
activeAgents,
maxAgents: CONFIG.maxAgents,
coordinationActive,
};
}
// Get system metrics (dynamic based on actual state)
function getSystemMetrics() {
let memoryMB = 0;
let subAgents = 0;
try {
if (isWindows) {
// Windows: use tasklist for memory info, fallback to process.memoryUsage
// tasklist memory column is complex to parse, use Node.js API instead
memoryMB = Math.floor(process.memoryUsage().heapUsed / 1024 / 1024);
} else {
const mem = execSync('ps aux | grep -E "(node|agentic|claude)" | grep -v grep | awk \'{sum += $6} END {print int(sum/1024)}\'', { encoding: 'utf-8' });
memoryMB = parseInt(mem.trim()) || 0;
}
} catch (e) {
// Fallback
memoryMB = Math.floor(process.memoryUsage().heapUsed / 1024 / 1024);
}
// Get learning stats for intelligence %
const learning = getLearningStats();
// Intelligence % from REAL intelligence loop data (ADR-050)
// Composite: 40% confidence mean + 30% access ratio + 30% pattern density
let intelligencePct = 0;
if (learning.confidenceMean > 0 || (learning.patterns > 0 && learning.accessedCount > 0)) {
const confScore = Math.min(100, Math.floor(learning.confidenceMean * 100));
const accessRatio = learning.patterns > 0 ? (learning.accessedCount / learning.patterns) : 0;
const accessScore = Math.min(100, Math.floor(accessRatio * 100));
const densityScore = Math.min(100, Math.floor(learning.patterns / 5));
intelligencePct = Math.floor(confScore * 0.4 + accessScore * 0.3 + densityScore * 0.3);
}
// Fallback: legacy pattern count
if (intelligencePct === 0 && learning.patterns > 0) {
intelligencePct = Math.min(100, Math.floor(learning.patterns / 10));
}
// Context % based on session history
const contextPct = Math.min(100, Math.floor(learning.sessions * 5));
// Count active sub-agents from process list
try {
if (isWindows) {
// Windows: use tasklist and findstr for agent counting
const agents = execSync('tasklist 2>NUL | findstr /I "claude-flow" 2>NUL | find /C /V "" 2>NUL || echo 0', { encoding: 'utf-8' });
subAgents = Math.max(0, parseInt(agents.trim()) || 0);
} else {
const agents = execSync('ps aux 2>/dev/null | grep -c "claude-flow.*agent" || echo "0"', { encoding: 'utf-8' });
subAgents = Math.max(0, parseInt(agents.trim()) - 1);
}
} catch (e) {
// Ignore - default to 0
}
return {
memoryMB,
contextPct,
intelligencePct,
subAgents,
};
}
// Generate progress bar
function progressBar(current, total) {
const width = 5;
const filled = Math.round((current / total) * width);
const empty = width - filled;
return '[' + '\u25CF'.repeat(filled) + '\u25CB'.repeat(empty) + ']';
}
// Generate full statusline
function generateStatusline() {
const user = getUserInfo();
const progress = getV3Progress();
const security = getSecurityStatus();
const swarm = getSwarmStatus();
const system = getSystemMetrics();
const lines = [];
// Header Line
let header = `${c.bold}${c.brightPurple}▊ Claude Flow V3 ${c.reset}`;
header += `${swarm.coordinationActive ? c.brightCyan : c.dim}${c.brightCyan}${user.name}${c.reset}`;
if (user.gitBranch) {
header += ` ${c.dim}${c.reset} ${c.brightBlue}${user.gitBranch}${c.reset}`;
}
header += ` ${c.dim}${c.reset} ${c.purple}${user.modelName}${c.reset}`;
lines.push(header);
// Separator
lines.push(`${c.dim}─────────────────────────────────────────────────────${c.reset}`);
// Line 1: DDD Domain Progress
const domainsColor = progress.domainsCompleted >= 3 ? c.brightGreen : progress.domainsCompleted > 0 ? c.yellow : c.red;
lines.push(
`${c.brightCyan}🏗️ DDD Domains${c.reset} ${progressBar(progress.domainsCompleted, progress.totalDomains)} ` +
`${domainsColor}${progress.domainsCompleted}${c.reset}/${c.brightWhite}${progress.totalDomains}${c.reset} ` +
`${c.brightYellow}⚡ 1.0x${c.reset} ${c.dim}${c.reset} ${c.brightYellow}2.49x-7.47x${c.reset}`
);
// Line 2: Swarm + CVE + Memory + Context + Intelligence
const swarmIndicator = swarm.coordinationActive ? `${c.brightGreen}${c.reset}` : `${c.dim}${c.reset}`;
const agentsColor = swarm.activeAgents > 0 ? c.brightGreen : c.red;
let securityIcon = security.status === 'CLEAN' ? '🟢' : security.status === 'IN_PROGRESS' ? '🟡' : '🔴';
let securityColor = security.status === 'CLEAN' ? c.brightGreen : security.status === 'IN_PROGRESS' ? c.brightYellow : c.brightRed;
lines.push(
`${c.brightYellow}🤖 Swarm${c.reset} ${swarmIndicator} [${agentsColor}${String(swarm.activeAgents).padStart(2)}${c.reset}/${c.brightWhite}${swarm.maxAgents}${c.reset}] ` +
`${c.brightPurple}👥 ${system.subAgents}${c.reset} ` +
`${securityIcon} ${securityColor}CVE ${security.cvesFixed}${c.reset}/${c.brightWhite}${security.totalCves}${c.reset} ` +
`${c.brightCyan}💾 ${system.memoryMB}MB${c.reset} ` +
`${c.brightGreen}📂 ${String(system.contextPct).padStart(3)}%${c.reset} ` +
`${c.dim}🧠 ${String(system.intelligencePct).padStart(3)}%${c.reset}`
);
// Line 3: Architecture status
const dddColor = progress.dddProgress >= 50 ? c.brightGreen : progress.dddProgress > 0 ? c.yellow : c.red;
lines.push(
`${c.brightPurple}🔧 Architecture${c.reset} ` +
`${c.cyan}DDD${c.reset} ${dddColor}${String(progress.dddProgress).padStart(3)}%${c.reset} ${c.dim}${c.reset} ` +
`${c.cyan}Security${c.reset} ${securityColor}${security.status}${c.reset} ${c.dim}${c.reset} ` +
`${c.cyan}Memory${c.reset} ${c.brightGreen}●AgentDB${c.reset} ${c.dim}${c.reset} ` +
`${c.cyan}Integration${c.reset} ${swarm.coordinationActive ? c.brightCyan : c.dim}${c.reset}`
);
return lines.join('\n');
}
// Generate JSON data
function generateJSON() {
return {
user: getUserInfo(),
v3Progress: getV3Progress(),
security: getSecurityStatus(),
swarm: getSwarmStatus(),
system: getSystemMetrics(),
performance: {
flashAttentionTarget: '2.49x-7.47x',
searchImprovement: '150x-12,500x',
memoryReduction: '50-75%',
},
lastUpdated: new Date().toISOString(),
};
}
/**
* Generate single-line output for Claude Code compatibility
* This avoids the collision zone issue entirely by using one line
* @see https://github.com/ruvnet/claude-flow/issues/985
*/
function generateSingleLine() {
if (!CONFIG.enabled) return '';
const user = getUserInfo();
const progress = getV3Progress();
const security = getSecurityStatus();
const swarm = getSwarmStatus();
const system = getSystemMetrics();
const swarmIndicator = swarm.coordinationActive ? '●' : '○';
const securityStatus = security.status === 'CLEAN' ? '✓' :
security.cvesFixed > 0 ? '~' : '✗';
return `${c.brightPurple}CF-V3${c.reset} ${c.dim}|${c.reset} ` +
`${c.cyan}D:${progress.domainsCompleted}/${progress.totalDomains}${c.reset} ${c.dim}|${c.reset} ` +
`${c.yellow}S:${swarmIndicator}${swarm.activeAgents}/${swarm.maxAgents}${c.reset} ${c.dim}|${c.reset} ` +
`${security.status === 'CLEAN' ? c.green : c.red}CVE:${securityStatus}${security.cvesFixed}/${security.totalCves}${c.reset} ${c.dim}|${c.reset} ` +
`${c.dim}🧠${system.intelligencePct}%${c.reset}`;
}
/**
* Generate safe multi-line statusline that avoids Claude Code collision zone
* The collision zone is columns 15-25 on the second-to-last line.
* We pad that line with spaces to push content past column 25.
* @see https://github.com/ruvnet/claude-flow/issues/985
*/
function generateSafeStatusline() {
if (!CONFIG.enabled) return '';
const user = getUserInfo();
const progress = getV3Progress();
const security = getSecurityStatus();
const swarm = getSwarmStatus();
const system = getSystemMetrics();
const lines = [];
// Header Line
let header = `${c.bold}${c.brightPurple}▊ Claude Flow V3 ${c.reset}`;
header += `${swarm.coordinationActive ? c.brightCyan : c.dim}${c.brightCyan}${user.name}${c.reset}`;
if (user.gitBranch) {
header += ` ${c.dim}${c.reset} ${c.brightBlue}${user.gitBranch}${c.reset}`;
}
header += ` ${c.dim}${c.reset} ${c.purple}${user.modelName}${c.reset}`;
lines.push(header);
// Separator
lines.push(`${c.dim}─────────────────────────────────────────────────────${c.reset}`);
// Line 1: DDD Domain Progress
const domainsColor = progress.domainsCompleted >= 3 ? c.brightGreen : progress.domainsCompleted > 0 ? c.yellow : c.red;
lines.push(
`${c.brightCyan}🏗️ DDD Domains${c.reset} ${progressBar(progress.domainsCompleted, progress.totalDomains)} ` +
`${domainsColor}${progress.domainsCompleted}${c.reset}/${c.brightWhite}${progress.totalDomains}${c.reset} ` +
`${c.brightYellow}⚡ 1.0x${c.reset} ${c.dim}${c.reset} ${c.brightYellow}2.49x-7.47x${c.reset}`
);
// Line 2 (COLLISION LINE): Swarm status with 24 spaces padding after emoji
// The emoji (🤖) is 2 columns. 24 spaces pushes content to column 26, past the collision zone (15-25)
const swarmIndicator = swarm.coordinationActive ? `${c.brightGreen}${c.reset}` : `${c.dim}${c.reset}`;
const agentsColor = swarm.activeAgents > 0 ? c.brightGreen : c.red;
let securityIcon = security.status === 'CLEAN' ? '🟢' : security.status === 'IN_PROGRESS' ? '🟡' : '🔴';
let securityColor = security.status === 'CLEAN' ? c.brightGreen : security.status === 'IN_PROGRESS' ? c.brightYellow : c.brightRed;
// CRITICAL: 24 spaces after 🤖 (emoji is 2 cols, so 2+24=26, past collision zone cols 15-25)
lines.push(
`${c.brightYellow}🤖${c.reset} ` + // 24 spaces padding
`${swarmIndicator} [${agentsColor}${String(swarm.activeAgents).padStart(2)}${c.reset}/${c.brightWhite}${swarm.maxAgents}${c.reset}] ` +
`${c.brightPurple}👥 ${system.subAgents}${c.reset} ` +
`${securityIcon} ${securityColor}CVE ${security.cvesFixed}${c.reset}/${c.brightWhite}${security.totalCves}${c.reset} ` +
`${c.brightCyan}💾 ${system.memoryMB}MB${c.reset} ` +
`${c.dim}🧠 ${system.intelligencePct}%${c.reset}`
);
// Line 3: Architecture status (this is the last line, not in collision zone)
const dddColor = progress.dddProgress >= 50 ? c.brightGreen : progress.dddProgress > 0 ? c.yellow : c.red;
lines.push(
`${c.brightPurple}🔧 Architecture${c.reset} ` +
`${c.cyan}DDD${c.reset} ${dddColor}${String(progress.dddProgress).padStart(3)}%${c.reset} ${c.dim}${c.reset} ` +
`${c.cyan}Security${c.reset} ${securityColor}${security.status}${c.reset} ${c.dim}${c.reset} ` +
`${c.cyan}Memory${c.reset} ${c.brightGreen}●AgentDB${c.reset} ${c.dim}${c.reset} ` +
`${c.cyan}Integration${c.reset} ${swarm.coordinationActive ? c.brightCyan : c.dim}${c.reset}`
);
return lines.join('\n');
}
// Main
if (process.argv.includes('--json')) {
console.log(JSON.stringify(generateJSON(), null, 2));
} else if (process.argv.includes('--compact')) {
console.log(JSON.stringify(generateJSON()));
} else if (process.argv.includes('--single')) {
// Single-line mode - completely avoids collision zone
console.log(generateSingleLine());
} else if (process.argv.includes('--unsafe') || process.argv.includes('--legacy')) {
// Legacy mode - original multi-line without collision avoidance
console.log(generateStatusline());
} else {
// Default: Safe multi-line mode with collision zone avoidance
// Use --unsafe or --legacy to get the original behavior
console.log(generateSafeStatusline());
}