feat(claude-flow): Init claude-flow v3, pretrain on repo, update CLAUDE.md
- Run npx @claude-flow/cli@latest init --force: 115 files created (agents, commands, helpers, skills, settings, MCP config) - Initialize memory.db (147 KB): 84 files analyzed, 30 patterns extracted, 46 trajectories evaluated via 4-step RETRIEVE/JUDGE/DISTILL/CONSOLIDATE - Run pretraining with MoE model: hyperbolic Poincaré embeddings, 3 contradictions resolved, all-MiniLM-L6-v2 ONNX embedding index - Include .claude/memory.db and .claude-flow/metrics/learning.json in repo for team sharing (semantic search available to all contributors) - Update CLAUDE.md: add wifi-densepose project context, key crates, ruvector integration map, correct build/test commands for this repo, ADR cross-reference (ADR-014 through ADR-017) https://claude.ai/code/session_01BSBAQJ34SLkiJy4A8SoiL4
This commit is contained in:
350
.claude/helpers/auto-memory-hook.mjs
Executable file
350
.claude/helpers/auto-memory-hook.mjs
Executable file
@@ -0,0 +1,350 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Auto Memory Bridge Hook (ADR-048/049)
|
||||
*
|
||||
* Wires AutoMemoryBridge + LearningBridge + MemoryGraph into Claude Code
|
||||
* session lifecycle. Called by settings.json SessionStart/SessionEnd hooks.
|
||||
*
|
||||
* Usage:
|
||||
* node auto-memory-hook.mjs import # SessionStart: import auto memory files into backend
|
||||
* node auto-memory-hook.mjs sync # SessionEnd: sync insights back to MEMORY.md
|
||||
* node auto-memory-hook.mjs status # Show bridge status
|
||||
*/
|
||||
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const PROJECT_ROOT = join(__dirname, '../..');
|
||||
const DATA_DIR = join(PROJECT_ROOT, '.claude-flow', 'data');
|
||||
const STORE_PATH = join(DATA_DIR, 'auto-memory-store.json');
|
||||
|
||||
// Colors
|
||||
const GREEN = '\x1b[0;32m';
|
||||
const CYAN = '\x1b[0;36m';
|
||||
const DIM = '\x1b[2m';
|
||||
const RESET = '\x1b[0m';
|
||||
|
||||
const log = (msg) => console.log(`${CYAN}[AutoMemory] ${msg}${RESET}`);
|
||||
const success = (msg) => console.log(`${GREEN}[AutoMemory] ✓ ${msg}${RESET}`);
|
||||
const dim = (msg) => console.log(` ${DIM}${msg}${RESET}`);
|
||||
|
||||
// Ensure data dir
|
||||
if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true });
|
||||
|
||||
// ============================================================================
|
||||
// Simple JSON File Backend (implements IMemoryBackend interface)
|
||||
// ============================================================================
|
||||
|
||||
class JsonFileBackend {
|
||||
constructor(filePath) {
|
||||
this.filePath = filePath;
|
||||
this.entries = new Map();
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
if (existsSync(this.filePath)) {
|
||||
try {
|
||||
const data = JSON.parse(readFileSync(this.filePath, 'utf-8'));
|
||||
if (Array.isArray(data)) {
|
||||
for (const entry of data) this.entries.set(entry.id, entry);
|
||||
}
|
||||
} catch { /* start fresh */ }
|
||||
}
|
||||
}
|
||||
|
||||
async shutdown() { this._persist(); }
|
||||
async store(entry) { this.entries.set(entry.id, entry); this._persist(); }
|
||||
async get(id) { return this.entries.get(id) ?? null; }
|
||||
async getByKey(key, ns) {
|
||||
for (const e of this.entries.values()) {
|
||||
if (e.key === key && (!ns || e.namespace === ns)) return e;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
async update(id, updates) {
|
||||
const e = this.entries.get(id);
|
||||
if (!e) return null;
|
||||
if (updates.metadata) Object.assign(e.metadata, updates.metadata);
|
||||
if (updates.content !== undefined) e.content = updates.content;
|
||||
if (updates.tags) e.tags = updates.tags;
|
||||
e.updatedAt = Date.now();
|
||||
this._persist();
|
||||
return e;
|
||||
}
|
||||
async delete(id) { return this.entries.delete(id); }
|
||||
async query(opts) {
|
||||
let results = [...this.entries.values()];
|
||||
if (opts?.namespace) results = results.filter(e => e.namespace === opts.namespace);
|
||||
if (opts?.type) results = results.filter(e => e.type === opts.type);
|
||||
if (opts?.limit) results = results.slice(0, opts.limit);
|
||||
return results;
|
||||
}
|
||||
async search() { return []; } // No vector search in JSON backend
|
||||
async bulkInsert(entries) { for (const e of entries) this.entries.set(e.id, e); this._persist(); }
|
||||
async bulkDelete(ids) { let n = 0; for (const id of ids) { if (this.entries.delete(id)) n++; } this._persist(); return n; }
|
||||
async count() { return this.entries.size; }
|
||||
async listNamespaces() {
|
||||
const ns = new Set();
|
||||
for (const e of this.entries.values()) ns.add(e.namespace || 'default');
|
||||
return [...ns];
|
||||
}
|
||||
async clearNamespace(ns) {
|
||||
let n = 0;
|
||||
for (const [id, e] of this.entries) {
|
||||
if (e.namespace === ns) { this.entries.delete(id); n++; }
|
||||
}
|
||||
this._persist();
|
||||
return n;
|
||||
}
|
||||
async getStats() {
|
||||
return {
|
||||
totalEntries: this.entries.size,
|
||||
entriesByNamespace: {},
|
||||
entriesByType: { semantic: 0, episodic: 0, procedural: 0, working: 0, cache: 0 },
|
||||
memoryUsage: 0, avgQueryTime: 0, avgSearchTime: 0,
|
||||
};
|
||||
}
|
||||
async healthCheck() {
|
||||
return {
|
||||
status: 'healthy',
|
||||
components: {
|
||||
storage: { status: 'healthy', latency: 0 },
|
||||
index: { status: 'healthy', latency: 0 },
|
||||
cache: { status: 'healthy', latency: 0 },
|
||||
},
|
||||
timestamp: Date.now(), issues: [], recommendations: [],
|
||||
};
|
||||
}
|
||||
|
||||
_persist() {
|
||||
try {
|
||||
writeFileSync(this.filePath, JSON.stringify([...this.entries.values()], null, 2), 'utf-8');
|
||||
} catch { /* best effort */ }
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Resolve memory package path (local dev or npm installed)
|
||||
// ============================================================================
|
||||
|
||||
async function loadMemoryPackage() {
|
||||
// Strategy 1: Local dev (built dist)
|
||||
const localDist = join(PROJECT_ROOT, 'v3/@claude-flow/memory/dist/index.js');
|
||||
if (existsSync(localDist)) {
|
||||
try {
|
||||
return await import(`file://${localDist}`);
|
||||
} catch { /* fall through */ }
|
||||
}
|
||||
|
||||
// Strategy 2: npm installed @claude-flow/memory
|
||||
try {
|
||||
return await import('@claude-flow/memory');
|
||||
} catch { /* fall through */ }
|
||||
|
||||
// Strategy 3: Installed via @claude-flow/cli which includes memory
|
||||
const cliMemory = join(PROJECT_ROOT, 'node_modules/@claude-flow/memory/dist/index.js');
|
||||
if (existsSync(cliMemory)) {
|
||||
try {
|
||||
return await import(`file://${cliMemory}`);
|
||||
} catch { /* fall through */ }
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Read config from .claude-flow/config.yaml
|
||||
// ============================================================================
|
||||
|
||||
function readConfig() {
|
||||
const configPath = join(PROJECT_ROOT, '.claude-flow', 'config.yaml');
|
||||
const defaults = {
|
||||
learningBridge: { enabled: true, sonaMode: 'balanced', confidenceDecayRate: 0.005, accessBoostAmount: 0.03, consolidationThreshold: 10 },
|
||||
memoryGraph: { enabled: true, pageRankDamping: 0.85, maxNodes: 5000, similarityThreshold: 0.8 },
|
||||
agentScopes: { enabled: true, defaultScope: 'project' },
|
||||
};
|
||||
|
||||
if (!existsSync(configPath)) return defaults;
|
||||
|
||||
try {
|
||||
const yaml = readFileSync(configPath, 'utf-8');
|
||||
// Simple YAML parser for the memory section
|
||||
const getBool = (key) => {
|
||||
const match = yaml.match(new RegExp(`${key}:\\s*(true|false)`, 'i'));
|
||||
return match ? match[1] === 'true' : undefined;
|
||||
};
|
||||
|
||||
const lbEnabled = getBool('learningBridge[\\s\\S]*?enabled');
|
||||
if (lbEnabled !== undefined) defaults.learningBridge.enabled = lbEnabled;
|
||||
|
||||
const mgEnabled = getBool('memoryGraph[\\s\\S]*?enabled');
|
||||
if (mgEnabled !== undefined) defaults.memoryGraph.enabled = mgEnabled;
|
||||
|
||||
const asEnabled = getBool('agentScopes[\\s\\S]*?enabled');
|
||||
if (asEnabled !== undefined) defaults.agentScopes.enabled = asEnabled;
|
||||
|
||||
return defaults;
|
||||
} catch {
|
||||
return defaults;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Commands
|
||||
// ============================================================================
|
||||
|
||||
async function doImport() {
|
||||
log('Importing auto memory files into bridge...');
|
||||
|
||||
const memPkg = await loadMemoryPackage();
|
||||
if (!memPkg || !memPkg.AutoMemoryBridge) {
|
||||
dim('Memory package not available — skipping auto memory import');
|
||||
return;
|
||||
}
|
||||
|
||||
const config = readConfig();
|
||||
const backend = new JsonFileBackend(STORE_PATH);
|
||||
await backend.initialize();
|
||||
|
||||
const bridgeConfig = {
|
||||
workingDir: PROJECT_ROOT,
|
||||
syncMode: 'on-session-end',
|
||||
};
|
||||
|
||||
// Wire learning if enabled and available
|
||||
if (config.learningBridge.enabled && memPkg.LearningBridge) {
|
||||
bridgeConfig.learning = {
|
||||
sonaMode: config.learningBridge.sonaMode,
|
||||
confidenceDecayRate: config.learningBridge.confidenceDecayRate,
|
||||
accessBoostAmount: config.learningBridge.accessBoostAmount,
|
||||
consolidationThreshold: config.learningBridge.consolidationThreshold,
|
||||
};
|
||||
}
|
||||
|
||||
// Wire graph if enabled and available
|
||||
if (config.memoryGraph.enabled && memPkg.MemoryGraph) {
|
||||
bridgeConfig.graph = {
|
||||
pageRankDamping: config.memoryGraph.pageRankDamping,
|
||||
maxNodes: config.memoryGraph.maxNodes,
|
||||
similarityThreshold: config.memoryGraph.similarityThreshold,
|
||||
};
|
||||
}
|
||||
|
||||
const bridge = new memPkg.AutoMemoryBridge(backend, bridgeConfig);
|
||||
|
||||
try {
|
||||
const result = await bridge.importFromAutoMemory();
|
||||
success(`Imported ${result.imported} entries (${result.skipped} skipped)`);
|
||||
dim(`├─ Backend entries: ${await backend.count()}`);
|
||||
dim(`├─ Learning: ${config.learningBridge.enabled ? 'active' : 'disabled'}`);
|
||||
dim(`├─ Graph: ${config.memoryGraph.enabled ? 'active' : 'disabled'}`);
|
||||
dim(`└─ Agent scopes: ${config.agentScopes.enabled ? 'active' : 'disabled'}`);
|
||||
} catch (err) {
|
||||
dim(`Import failed (non-critical): ${err.message}`);
|
||||
}
|
||||
|
||||
await backend.shutdown();
|
||||
}
|
||||
|
||||
async function doSync() {
|
||||
log('Syncing insights to auto memory files...');
|
||||
|
||||
const memPkg = await loadMemoryPackage();
|
||||
if (!memPkg || !memPkg.AutoMemoryBridge) {
|
||||
dim('Memory package not available — skipping sync');
|
||||
return;
|
||||
}
|
||||
|
||||
const config = readConfig();
|
||||
const backend = new JsonFileBackend(STORE_PATH);
|
||||
await backend.initialize();
|
||||
|
||||
const entryCount = await backend.count();
|
||||
if (entryCount === 0) {
|
||||
dim('No entries to sync');
|
||||
await backend.shutdown();
|
||||
return;
|
||||
}
|
||||
|
||||
const bridgeConfig = {
|
||||
workingDir: PROJECT_ROOT,
|
||||
syncMode: 'on-session-end',
|
||||
};
|
||||
|
||||
if (config.learningBridge.enabled && memPkg.LearningBridge) {
|
||||
bridgeConfig.learning = {
|
||||
sonaMode: config.learningBridge.sonaMode,
|
||||
confidenceDecayRate: config.learningBridge.confidenceDecayRate,
|
||||
consolidationThreshold: config.learningBridge.consolidationThreshold,
|
||||
};
|
||||
}
|
||||
|
||||
if (config.memoryGraph.enabled && memPkg.MemoryGraph) {
|
||||
bridgeConfig.graph = {
|
||||
pageRankDamping: config.memoryGraph.pageRankDamping,
|
||||
maxNodes: config.memoryGraph.maxNodes,
|
||||
};
|
||||
}
|
||||
|
||||
const bridge = new memPkg.AutoMemoryBridge(backend, bridgeConfig);
|
||||
|
||||
try {
|
||||
const syncResult = await bridge.syncToAutoMemory();
|
||||
success(`Synced ${syncResult.synced} entries to auto memory`);
|
||||
dim(`├─ Categories updated: ${syncResult.categories?.join(', ') || 'none'}`);
|
||||
dim(`└─ Backend entries: ${entryCount}`);
|
||||
|
||||
// Curate MEMORY.md index with graph-aware ordering
|
||||
await bridge.curateIndex();
|
||||
success('Curated MEMORY.md index');
|
||||
} catch (err) {
|
||||
dim(`Sync failed (non-critical): ${err.message}`);
|
||||
}
|
||||
|
||||
if (bridge.destroy) bridge.destroy();
|
||||
await backend.shutdown();
|
||||
}
|
||||
|
||||
async function doStatus() {
|
||||
const memPkg = await loadMemoryPackage();
|
||||
const config = readConfig();
|
||||
|
||||
console.log('\n=== Auto Memory Bridge Status ===\n');
|
||||
console.log(` Package: ${memPkg ? '✅ Available' : '❌ Not found'}`);
|
||||
console.log(` Store: ${existsSync(STORE_PATH) ? '✅ ' + STORE_PATH : '⏸ Not initialized'}`);
|
||||
console.log(` LearningBridge: ${config.learningBridge.enabled ? '✅ Enabled' : '⏸ Disabled'}`);
|
||||
console.log(` MemoryGraph: ${config.memoryGraph.enabled ? '✅ Enabled' : '⏸ Disabled'}`);
|
||||
console.log(` AgentScopes: ${config.agentScopes.enabled ? '✅ Enabled' : '⏸ Disabled'}`);
|
||||
|
||||
if (existsSync(STORE_PATH)) {
|
||||
try {
|
||||
const data = JSON.parse(readFileSync(STORE_PATH, 'utf-8'));
|
||||
console.log(` Entries: ${Array.isArray(data) ? data.length : 0}`);
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Main
|
||||
// ============================================================================
|
||||
|
||||
const command = process.argv[2] || 'status';
|
||||
|
||||
try {
|
||||
switch (command) {
|
||||
case 'import': await doImport(); break;
|
||||
case 'sync': await doSync(); break;
|
||||
case 'status': await doStatus(); break;
|
||||
default:
|
||||
console.log('Usage: auto-memory-hook.mjs <import|sync|status>');
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (err) {
|
||||
// Hooks must never crash Claude Code - fail silently
|
||||
dim(`Error (non-critical): ${err.message}`);
|
||||
}
|
||||
@@ -57,7 +57,7 @@ is_running() {
|
||||
|
||||
# Start the swarm monitor daemon
|
||||
start_swarm_monitor() {
|
||||
local interval="${1:-3}"
|
||||
local interval="${1:-30}"
|
||||
|
||||
if is_running "$SWARM_MONITOR_PID"; then
|
||||
log "Swarm monitor already running (PID: $(cat "$SWARM_MONITOR_PID"))"
|
||||
@@ -78,7 +78,7 @@ start_swarm_monitor() {
|
||||
|
||||
# Start the metrics update daemon
|
||||
start_metrics_daemon() {
|
||||
local interval="${1:-30}" # Default 30 seconds for V3 sync
|
||||
local interval="${1:-60}" # Default 60 seconds - less frequent updates
|
||||
|
||||
if is_running "$METRICS_DAEMON_PID"; then
|
||||
log "Metrics daemon already running (PID: $(cat "$METRICS_DAEMON_PID"))"
|
||||
@@ -126,8 +126,8 @@ stop_daemon() {
|
||||
# Start all daemons
|
||||
start_all() {
|
||||
log "Starting all Claude Flow daemons..."
|
||||
start_swarm_monitor "${1:-3}"
|
||||
start_metrics_daemon "${2:-5}"
|
||||
start_swarm_monitor "${1:-30}"
|
||||
start_metrics_daemon "${2:-60}"
|
||||
|
||||
# Initial metrics update
|
||||
"$SCRIPT_DIR/swarm-monitor.sh" check > /dev/null 2>&1
|
||||
@@ -207,22 +207,22 @@ show_status() {
|
||||
# Main command handling
|
||||
case "${1:-status}" in
|
||||
"start")
|
||||
start_all "${2:-3}" "${3:-5}"
|
||||
start_all "${2:-30}" "${3:-60}"
|
||||
;;
|
||||
"stop")
|
||||
stop_all
|
||||
;;
|
||||
"restart")
|
||||
restart_all "${2:-3}" "${3:-5}"
|
||||
restart_all "${2:-30}" "${3:-60}"
|
||||
;;
|
||||
"status")
|
||||
show_status
|
||||
;;
|
||||
"start-swarm")
|
||||
start_swarm_monitor "${2:-3}"
|
||||
start_swarm_monitor "${2:-30}"
|
||||
;;
|
||||
"start-metrics")
|
||||
start_metrics_daemon "${2:-5}"
|
||||
start_metrics_daemon "${2:-60}"
|
||||
;;
|
||||
"help"|"-h"|"--help")
|
||||
echo "Claude Flow V3 Daemon Manager"
|
||||
@@ -239,8 +239,8 @@ case "${1:-status}" in
|
||||
echo " help Show this help"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 start # Start with defaults (3s swarm, 5s metrics)"
|
||||
echo " $0 start 2 3 # Start with 2s swarm, 3s metrics intervals"
|
||||
echo " $0 start # Start with defaults (30s swarm, 60s metrics)"
|
||||
echo " $0 start 10 30 # Start with 10s swarm, 30s metrics intervals"
|
||||
echo " $0 status # Show current status"
|
||||
echo " $0 stop # Stop all daemons"
|
||||
;;
|
||||
|
||||
232
.claude/helpers/hook-handler.cjs
Normal file
232
.claude/helpers/hook-handler.cjs
Normal file
@@ -0,0 +1,232 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Claude Flow Hook Handler (Cross-Platform)
|
||||
* Dispatches hook events to the appropriate helper modules.
|
||||
*
|
||||
* Usage: node hook-handler.cjs <command> [args...]
|
||||
*
|
||||
* Commands:
|
||||
* route - Route a task to optimal agent (reads PROMPT from env/stdin)
|
||||
* pre-bash - Validate command safety before execution
|
||||
* post-edit - Record edit outcome for learning
|
||||
* session-restore - Restore previous session state
|
||||
* session-end - End session and persist state
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const helpersDir = __dirname;
|
||||
|
||||
// Safe require with stdout suppression - the helper modules have CLI
|
||||
// sections that run unconditionally on require(), so we mute console
|
||||
// during the require to prevent noisy output.
|
||||
function safeRequire(modulePath) {
|
||||
try {
|
||||
if (fs.existsSync(modulePath)) {
|
||||
const origLog = console.log;
|
||||
const origError = console.error;
|
||||
console.log = () => {};
|
||||
console.error = () => {};
|
||||
try {
|
||||
const mod = require(modulePath);
|
||||
return mod;
|
||||
} finally {
|
||||
console.log = origLog;
|
||||
console.error = origError;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// silently fail
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const router = safeRequire(path.join(helpersDir, 'router.js'));
|
||||
const session = safeRequire(path.join(helpersDir, 'session.js'));
|
||||
const memory = safeRequire(path.join(helpersDir, 'memory.js'));
|
||||
const intelligence = safeRequire(path.join(helpersDir, 'intelligence.cjs'));
|
||||
|
||||
// Get the command from argv
|
||||
const [,, command, ...args] = process.argv;
|
||||
|
||||
// Get prompt from environment variable (set by Claude Code hooks)
|
||||
const prompt = process.env.PROMPT || process.env.TOOL_INPUT_command || args.join(' ') || '';
|
||||
|
||||
const handlers = {
|
||||
'route': () => {
|
||||
// Inject ranked intelligence context before routing
|
||||
if (intelligence && intelligence.getContext) {
|
||||
try {
|
||||
const ctx = intelligence.getContext(prompt);
|
||||
if (ctx) console.log(ctx);
|
||||
} catch (e) { /* non-fatal */ }
|
||||
}
|
||||
if (router && router.routeTask) {
|
||||
const result = router.routeTask(prompt);
|
||||
// Format output for Claude Code hook consumption
|
||||
const output = [
|
||||
`[INFO] Routing task: ${prompt.substring(0, 80) || '(no prompt)'}`,
|
||||
'',
|
||||
'Routing Method',
|
||||
' - Method: keyword',
|
||||
' - Backend: keyword matching',
|
||||
` - Latency: ${(Math.random() * 0.5 + 0.1).toFixed(3)}ms`,
|
||||
' - Matched Pattern: keyword-fallback',
|
||||
'',
|
||||
'Semantic Matches:',
|
||||
' bugfix-task: 15.0%',
|
||||
' devops-task: 14.0%',
|
||||
' testing-task: 13.0%',
|
||||
'',
|
||||
'+------------------- Primary Recommendation -------------------+',
|
||||
`| Agent: ${result.agent.padEnd(53)}|`,
|
||||
`| Confidence: ${(result.confidence * 100).toFixed(1)}%${' '.repeat(44)}|`,
|
||||
`| Reason: ${result.reason.substring(0, 53).padEnd(53)}|`,
|
||||
'+--------------------------------------------------------------+',
|
||||
'',
|
||||
'Alternative Agents',
|
||||
'+------------+------------+-------------------------------------+',
|
||||
'| Agent Type | Confidence | Reason |',
|
||||
'+------------+------------+-------------------------------------+',
|
||||
'| researcher | 60.0% | Alternative agent for researcher... |',
|
||||
'| tester | 50.0% | Alternative agent for tester cap... |',
|
||||
'+------------+------------+-------------------------------------+',
|
||||
'',
|
||||
'Estimated Metrics',
|
||||
' - Success Probability: 70.0%',
|
||||
' - Estimated Duration: 10-30 min',
|
||||
' - Complexity: LOW',
|
||||
];
|
||||
console.log(output.join('\n'));
|
||||
} else {
|
||||
console.log('[INFO] Router not available, using default routing');
|
||||
}
|
||||
},
|
||||
|
||||
'pre-bash': () => {
|
||||
// Basic command safety check
|
||||
const cmd = prompt.toLowerCase();
|
||||
const dangerous = ['rm -rf /', 'format c:', 'del /s /q c:\\', ':(){:|:&};:'];
|
||||
for (const d of dangerous) {
|
||||
if (cmd.includes(d)) {
|
||||
console.error(`[BLOCKED] Dangerous command detected: ${d}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
console.log('[OK] Command validated');
|
||||
},
|
||||
|
||||
'post-edit': () => {
|
||||
// Record edit for session metrics
|
||||
if (session && session.metric) {
|
||||
try { session.metric('edits'); } catch (e) { /* no active session */ }
|
||||
}
|
||||
// Record edit for intelligence consolidation
|
||||
if (intelligence && intelligence.recordEdit) {
|
||||
try {
|
||||
const file = process.env.TOOL_INPUT_file_path || args[0] || '';
|
||||
intelligence.recordEdit(file);
|
||||
} catch (e) { /* non-fatal */ }
|
||||
}
|
||||
console.log('[OK] Edit recorded');
|
||||
},
|
||||
|
||||
'session-restore': () => {
|
||||
if (session) {
|
||||
// Try restore first, fall back to start
|
||||
const existing = session.restore && session.restore();
|
||||
if (!existing) {
|
||||
session.start && session.start();
|
||||
}
|
||||
} else {
|
||||
// Minimal session restore output
|
||||
const sessionId = `session-${Date.now()}`;
|
||||
console.log(`[INFO] Restoring session: %SESSION_ID%`);
|
||||
console.log('');
|
||||
console.log(`[OK] Session restored from %SESSION_ID%`);
|
||||
console.log(`New session ID: ${sessionId}`);
|
||||
console.log('');
|
||||
console.log('Restored State');
|
||||
console.log('+----------------+-------+');
|
||||
console.log('| Item | Count |');
|
||||
console.log('+----------------+-------+');
|
||||
console.log('| Tasks | 0 |');
|
||||
console.log('| Agents | 0 |');
|
||||
console.log('| Memory Entries | 0 |');
|
||||
console.log('+----------------+-------+');
|
||||
}
|
||||
// Initialize intelligence graph after session restore
|
||||
if (intelligence && intelligence.init) {
|
||||
try {
|
||||
const result = intelligence.init();
|
||||
if (result && result.nodes > 0) {
|
||||
console.log(`[INTELLIGENCE] Loaded ${result.nodes} patterns, ${result.edges} edges`);
|
||||
}
|
||||
} catch (e) { /* non-fatal */ }
|
||||
}
|
||||
},
|
||||
|
||||
'session-end': () => {
|
||||
// Consolidate intelligence before ending session
|
||||
if (intelligence && intelligence.consolidate) {
|
||||
try {
|
||||
const result = intelligence.consolidate();
|
||||
if (result && result.entries > 0) {
|
||||
console.log(`[INTELLIGENCE] Consolidated: ${result.entries} entries, ${result.edges} edges${result.newEntries > 0 ? `, ${result.newEntries} new` : ''}, PageRank recomputed`);
|
||||
}
|
||||
} catch (e) { /* non-fatal */ }
|
||||
}
|
||||
if (session && session.end) {
|
||||
session.end();
|
||||
} else {
|
||||
console.log('[OK] Session ended');
|
||||
}
|
||||
},
|
||||
|
||||
'pre-task': () => {
|
||||
if (session && session.metric) {
|
||||
try { session.metric('tasks'); } catch (e) { /* no active session */ }
|
||||
}
|
||||
// Route the task if router is available
|
||||
if (router && router.routeTask && prompt) {
|
||||
const result = router.routeTask(prompt);
|
||||
console.log(`[INFO] Task routed to: ${result.agent} (confidence: ${result.confidence})`);
|
||||
} else {
|
||||
console.log('[OK] Task started');
|
||||
}
|
||||
},
|
||||
|
||||
'post-task': () => {
|
||||
// Implicit success feedback for intelligence
|
||||
if (intelligence && intelligence.feedback) {
|
||||
try {
|
||||
intelligence.feedback(true);
|
||||
} catch (e) { /* non-fatal */ }
|
||||
}
|
||||
console.log('[OK] Task completed');
|
||||
},
|
||||
|
||||
'stats': () => {
|
||||
if (intelligence && intelligence.stats) {
|
||||
intelligence.stats(args.includes('--json'));
|
||||
} else {
|
||||
console.log('[WARN] Intelligence module not available. Run session-restore first.');
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Execute the handler
|
||||
if (command && handlers[command]) {
|
||||
try {
|
||||
handlers[command]();
|
||||
} catch (e) {
|
||||
// Hooks should never crash Claude Code - fail silently
|
||||
console.log(`[WARN] Hook ${command} encountered an error: ${e.message}`);
|
||||
}
|
||||
} else if (command) {
|
||||
// Unknown command - pass through without error
|
||||
console.log(`[OK] Hook: ${command}`);
|
||||
} else {
|
||||
console.log('Usage: hook-handler.cjs <route|pre-bash|post-edit|session-restore|session-end|pre-task|post-task|stats>');
|
||||
}
|
||||
916
.claude/helpers/intelligence.cjs
Normal file
916
.claude/helpers/intelligence.cjs
Normal file
@@ -0,0 +1,916 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Intelligence Layer (ADR-050)
|
||||
*
|
||||
* Closes the intelligence loop by wiring PageRank-ranked memory into
|
||||
* the hook system. Pure CJS — no ESM imports of @claude-flow/memory.
|
||||
*
|
||||
* Data files (all under .claude-flow/data/):
|
||||
* auto-memory-store.json — written by auto-memory-hook.mjs
|
||||
* graph-state.json — serialized graph (nodes + edges + pageRanks)
|
||||
* ranked-context.json — pre-computed ranked entries for fast lookup
|
||||
* pending-insights.jsonl — append-only edit/task log
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const DATA_DIR = path.join(process.cwd(), '.claude-flow', 'data');
|
||||
const STORE_PATH = path.join(DATA_DIR, 'auto-memory-store.json');
|
||||
const GRAPH_PATH = path.join(DATA_DIR, 'graph-state.json');
|
||||
const RANKED_PATH = path.join(DATA_DIR, 'ranked-context.json');
|
||||
const PENDING_PATH = path.join(DATA_DIR, 'pending-insights.jsonl');
|
||||
const SESSION_DIR = path.join(process.cwd(), '.claude-flow', 'sessions');
|
||||
const SESSION_FILE = path.join(SESSION_DIR, 'current.json');
|
||||
|
||||
// ── Stop words for trigram matching ──────────────────────────────────────────
|
||||
|
||||
const STOP_WORDS = new Set([
|
||||
'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
|
||||
'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
|
||||
'should', 'may', 'might', 'shall', 'can', 'to', 'of', 'in', 'for',
|
||||
'on', 'with', 'at', 'by', 'from', 'as', 'into', 'through', 'during',
|
||||
'before', 'after', 'and', 'but', 'or', 'nor', 'not', 'so', 'yet',
|
||||
'both', 'either', 'neither', 'each', 'every', 'all', 'any', 'few',
|
||||
'more', 'most', 'other', 'some', 'such', 'no', 'only', 'own', 'same',
|
||||
'than', 'too', 'very', 'just', 'because', 'if', 'when', 'which',
|
||||
'who', 'whom', 'this', 'that', 'these', 'those', 'it', 'its',
|
||||
]);
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function ensureDataDir() {
|
||||
if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
function readJSON(filePath) {
|
||||
try {
|
||||
if (fs.existsSync(filePath)) return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
||||
} catch { /* corrupt file — start fresh */ }
|
||||
return null;
|
||||
}
|
||||
|
||||
function writeJSON(filePath, data) {
|
||||
ensureDataDir();
|
||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
||||
}
|
||||
|
||||
function tokenize(text) {
|
||||
if (!text) return [];
|
||||
return text.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-]/g, ' ')
|
||||
.split(/\s+/)
|
||||
.filter(w => w.length > 2 && !STOP_WORDS.has(w));
|
||||
}
|
||||
|
||||
function trigrams(words) {
|
||||
const t = new Set();
|
||||
for (const w of words) {
|
||||
for (let i = 0; i <= w.length - 3; i++) t.add(w.slice(i, i + 3));
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
function jaccardSimilarity(setA, setB) {
|
||||
if (setA.size === 0 && setB.size === 0) return 0;
|
||||
let intersection = 0;
|
||||
for (const item of setA) { if (setB.has(item)) intersection++; }
|
||||
return intersection / (setA.size + setB.size - intersection);
|
||||
}
|
||||
|
||||
// ── Session state helpers ────────────────────────────────────────────────────
|
||||
|
||||
function sessionGet(key) {
|
||||
try {
|
||||
if (!fs.existsSync(SESSION_FILE)) return null;
|
||||
const session = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf-8'));
|
||||
return key ? (session.context || {})[key] : session.context;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
function sessionSet(key, value) {
|
||||
try {
|
||||
if (!fs.existsSync(SESSION_DIR)) fs.mkdirSync(SESSION_DIR, { recursive: true });
|
||||
let session = {};
|
||||
if (fs.existsSync(SESSION_FILE)) {
|
||||
session = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf-8'));
|
||||
}
|
||||
if (!session.context) session.context = {};
|
||||
session.context[key] = value;
|
||||
session.updatedAt = new Date().toISOString();
|
||||
fs.writeFileSync(SESSION_FILE, JSON.stringify(session, null, 2), 'utf-8');
|
||||
} catch { /* best effort */ }
|
||||
}
|
||||
|
||||
// ── PageRank ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function computePageRank(nodes, edges, damping, maxIter) {
|
||||
damping = damping || 0.85;
|
||||
maxIter = maxIter || 30;
|
||||
|
||||
const ids = Object.keys(nodes);
|
||||
const n = ids.length;
|
||||
if (n === 0) return {};
|
||||
|
||||
// Build adjacency: outgoing edges per node
|
||||
const outLinks = {};
|
||||
const inLinks = {};
|
||||
for (const id of ids) { outLinks[id] = []; inLinks[id] = []; }
|
||||
for (const edge of edges) {
|
||||
if (outLinks[edge.sourceId]) outLinks[edge.sourceId].push(edge.targetId);
|
||||
if (inLinks[edge.targetId]) inLinks[edge.targetId].push(edge.sourceId);
|
||||
}
|
||||
|
||||
// Initialize ranks
|
||||
const ranks = {};
|
||||
for (const id of ids) ranks[id] = 1 / n;
|
||||
|
||||
// Power iteration (with dangling node redistribution)
|
||||
for (let iter = 0; iter < maxIter; iter++) {
|
||||
const newRanks = {};
|
||||
let diff = 0;
|
||||
|
||||
// Collect rank from dangling nodes (no outgoing edges)
|
||||
let danglingSum = 0;
|
||||
for (const id of ids) {
|
||||
if (outLinks[id].length === 0) danglingSum += ranks[id];
|
||||
}
|
||||
|
||||
for (const id of ids) {
|
||||
let sum = 0;
|
||||
for (const src of inLinks[id]) {
|
||||
const outCount = outLinks[src].length;
|
||||
if (outCount > 0) sum += ranks[src] / outCount;
|
||||
}
|
||||
// Dangling rank distributed evenly + teleport
|
||||
newRanks[id] = (1 - damping) / n + damping * (sum + danglingSum / n);
|
||||
diff += Math.abs(newRanks[id] - ranks[id]);
|
||||
}
|
||||
|
||||
for (const id of ids) ranks[id] = newRanks[id];
|
||||
if (diff < 1e-6) break; // converged
|
||||
}
|
||||
|
||||
return ranks;
|
||||
}
|
||||
|
||||
// ── Edge building ────────────────────────────────────────────────────────────
|
||||
|
||||
function buildEdges(entries) {
|
||||
const edges = [];
|
||||
const byCategory = {};
|
||||
|
||||
for (const entry of entries) {
|
||||
const cat = entry.category || entry.namespace || 'default';
|
||||
if (!byCategory[cat]) byCategory[cat] = [];
|
||||
byCategory[cat].push(entry);
|
||||
}
|
||||
|
||||
// Temporal edges: entries from same sourceFile
|
||||
const byFile = {};
|
||||
for (const entry of entries) {
|
||||
const file = (entry.metadata && entry.metadata.sourceFile) || null;
|
||||
if (file) {
|
||||
if (!byFile[file]) byFile[file] = [];
|
||||
byFile[file].push(entry);
|
||||
}
|
||||
}
|
||||
for (const file of Object.keys(byFile)) {
|
||||
const group = byFile[file];
|
||||
for (let i = 0; i < group.length - 1; i++) {
|
||||
edges.push({
|
||||
sourceId: group[i].id,
|
||||
targetId: group[i + 1].id,
|
||||
type: 'temporal',
|
||||
weight: 0.5,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Similarity edges within categories (Jaccard > 0.3)
|
||||
for (const cat of Object.keys(byCategory)) {
|
||||
const group = byCategory[cat];
|
||||
for (let i = 0; i < group.length; i++) {
|
||||
const triA = trigrams(tokenize(group[i].content || group[i].summary || ''));
|
||||
for (let j = i + 1; j < group.length; j++) {
|
||||
const triB = trigrams(tokenize(group[j].content || group[j].summary || ''));
|
||||
const sim = jaccardSimilarity(triA, triB);
|
||||
if (sim > 0.3) {
|
||||
edges.push({
|
||||
sourceId: group[i].id,
|
||||
targetId: group[j].id,
|
||||
type: 'similar',
|
||||
weight: sim,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return edges;
|
||||
}
|
||||
|
||||
// ── Bootstrap from MEMORY.md files ───────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* If auto-memory-store.json is empty, bootstrap by parsing MEMORY.md and
|
||||
* topic files from the auto-memory directory. This removes the dependency
|
||||
* on @claude-flow/memory for the initial seed.
|
||||
*/
|
||||
function bootstrapFromMemoryFiles() {
|
||||
const entries = [];
|
||||
const cwd = process.cwd();
|
||||
|
||||
// Search for auto-memory directories
|
||||
const candidates = [
|
||||
// Claude Code auto-memory (project-scoped)
|
||||
path.join(require('os').homedir(), '.claude', 'projects'),
|
||||
// Local project memory
|
||||
path.join(cwd, '.claude-flow', 'memory'),
|
||||
path.join(cwd, '.claude', 'memory'),
|
||||
];
|
||||
|
||||
// Find MEMORY.md in project-scoped dirs
|
||||
for (const base of candidates) {
|
||||
if (!fs.existsSync(base)) continue;
|
||||
|
||||
// For the projects dir, scan subdirectories for memory/
|
||||
if (base.endsWith('projects')) {
|
||||
try {
|
||||
const projectDirs = fs.readdirSync(base);
|
||||
for (const pdir of projectDirs) {
|
||||
const memDir = path.join(base, pdir, 'memory');
|
||||
if (fs.existsSync(memDir)) {
|
||||
parseMemoryDir(memDir, entries);
|
||||
}
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
} else if (fs.existsSync(base)) {
|
||||
parseMemoryDir(base, entries);
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
function parseMemoryDir(dir, entries) {
|
||||
try {
|
||||
const files = fs.readdirSync(dir).filter(f => f.endsWith('.md'));
|
||||
for (const file of files) {
|
||||
const filePath = path.join(dir, file);
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
if (!content.trim()) continue;
|
||||
|
||||
// Parse markdown sections as separate entries
|
||||
const sections = content.split(/^##?\s+/m).filter(Boolean);
|
||||
for (const section of sections) {
|
||||
const lines = section.trim().split('\n');
|
||||
const title = lines[0].trim();
|
||||
const body = lines.slice(1).join('\n').trim();
|
||||
if (!body || body.length < 10) continue;
|
||||
|
||||
const id = `mem-${file.replace('.md', '')}-${title.replace(/[^a-z0-9]/gi, '-').toLowerCase().slice(0, 30)}`;
|
||||
entries.push({
|
||||
id,
|
||||
key: title.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 50),
|
||||
content: body.slice(0, 500),
|
||||
summary: title,
|
||||
namespace: file === 'MEMORY.md' ? 'core' : file.replace('.md', ''),
|
||||
type: 'semantic',
|
||||
metadata: { sourceFile: filePath, bootstrapped: true },
|
||||
createdAt: Date.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch { /* skip unreadable dirs */ }
|
||||
}
|
||||
|
||||
// ── Exported functions ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* init() — Called from session-restore. Budget: <200ms.
|
||||
* Reads auto-memory-store.json, builds graph, computes PageRank, writes caches.
|
||||
* If store is empty, bootstraps from MEMORY.md files directly.
|
||||
*/
|
||||
function init() {
|
||||
ensureDataDir();
|
||||
|
||||
// Check if graph-state.json is fresh (within 60s of store)
|
||||
const graphState = readJSON(GRAPH_PATH);
|
||||
let store = readJSON(STORE_PATH);
|
||||
|
||||
// Bootstrap from MEMORY.md files if store is empty
|
||||
if (!store || !Array.isArray(store) || store.length === 0) {
|
||||
const bootstrapped = bootstrapFromMemoryFiles();
|
||||
if (bootstrapped.length > 0) {
|
||||
store = bootstrapped;
|
||||
writeJSON(STORE_PATH, store);
|
||||
} else {
|
||||
return { nodes: 0, edges: 0, message: 'No memory entries to index' };
|
||||
}
|
||||
}
|
||||
|
||||
// Skip rebuild if graph is fresh and store hasn't changed
|
||||
if (graphState && graphState.nodeCount === store.length) {
|
||||
const age = Date.now() - (graphState.updatedAt || 0);
|
||||
if (age < 60000) {
|
||||
return {
|
||||
nodes: graphState.nodeCount || Object.keys(graphState.nodes || {}).length,
|
||||
edges: (graphState.edges || []).length,
|
||||
message: 'Graph cache hit',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Build nodes
|
||||
const nodes = {};
|
||||
for (const entry of store) {
|
||||
const id = entry.id || entry.key || `entry-${Math.random().toString(36).slice(2, 8)}`;
|
||||
nodes[id] = {
|
||||
id,
|
||||
category: entry.namespace || entry.type || 'default',
|
||||
confidence: (entry.metadata && entry.metadata.confidence) || 0.5,
|
||||
accessCount: (entry.metadata && entry.metadata.accessCount) || 0,
|
||||
createdAt: entry.createdAt || Date.now(),
|
||||
};
|
||||
// Ensure entry has id for edge building
|
||||
entry.id = id;
|
||||
}
|
||||
|
||||
// Build edges
|
||||
const edges = buildEdges(store);
|
||||
|
||||
// Compute PageRank
|
||||
const pageRanks = computePageRank(nodes, edges, 0.85, 30);
|
||||
|
||||
// Write graph state
|
||||
const graph = {
|
||||
version: 1,
|
||||
updatedAt: Date.now(),
|
||||
nodeCount: Object.keys(nodes).length,
|
||||
nodes,
|
||||
edges,
|
||||
pageRanks,
|
||||
};
|
||||
writeJSON(GRAPH_PATH, graph);
|
||||
|
||||
// Build ranked context for fast lookup
|
||||
const rankedEntries = store.map(entry => {
|
||||
const id = entry.id;
|
||||
const content = entry.content || entry.value || '';
|
||||
const summary = entry.summary || entry.key || '';
|
||||
const words = tokenize(content + ' ' + summary);
|
||||
return {
|
||||
id,
|
||||
content,
|
||||
summary,
|
||||
category: entry.namespace || entry.type || 'default',
|
||||
confidence: nodes[id] ? nodes[id].confidence : 0.5,
|
||||
pageRank: pageRanks[id] || 0,
|
||||
accessCount: nodes[id] ? nodes[id].accessCount : 0,
|
||||
words,
|
||||
};
|
||||
}).sort((a, b) => {
|
||||
const scoreA = 0.6 * a.pageRank + 0.4 * a.confidence;
|
||||
const scoreB = 0.6 * b.pageRank + 0.4 * b.confidence;
|
||||
return scoreB - scoreA;
|
||||
});
|
||||
|
||||
const ranked = {
|
||||
version: 1,
|
||||
computedAt: Date.now(),
|
||||
entries: rankedEntries,
|
||||
};
|
||||
writeJSON(RANKED_PATH, ranked);
|
||||
|
||||
return {
|
||||
nodes: Object.keys(nodes).length,
|
||||
edges: edges.length,
|
||||
message: 'Graph built and ranked',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* getContext(prompt) — Called from route. Budget: <15ms.
|
||||
* Matches prompt to ranked entries, returns top-5 formatted context.
|
||||
*/
|
||||
function getContext(prompt) {
|
||||
if (!prompt) return null;
|
||||
|
||||
const ranked = readJSON(RANKED_PATH);
|
||||
if (!ranked || !ranked.entries || ranked.entries.length === 0) return null;
|
||||
|
||||
const promptWords = tokenize(prompt);
|
||||
if (promptWords.length === 0) return null;
|
||||
const promptTrigrams = trigrams(promptWords);
|
||||
|
||||
const ALPHA = 0.6; // content match weight
|
||||
const MIN_THRESHOLD = 0.05;
|
||||
const TOP_K = 5;
|
||||
|
||||
// Score each entry
|
||||
const scored = [];
|
||||
for (const entry of ranked.entries) {
|
||||
const entryTrigrams = trigrams(entry.words || []);
|
||||
const contentMatch = jaccardSimilarity(promptTrigrams, entryTrigrams);
|
||||
const score = ALPHA * contentMatch + (1 - ALPHA) * (entry.pageRank || 0);
|
||||
if (score >= MIN_THRESHOLD) {
|
||||
scored.push({ ...entry, score });
|
||||
}
|
||||
}
|
||||
|
||||
if (scored.length === 0) return null;
|
||||
|
||||
// Sort by score descending, take top-K
|
||||
scored.sort((a, b) => b.score - a.score);
|
||||
const topEntries = scored.slice(0, TOP_K);
|
||||
|
||||
// Boost previously matched patterns (implicit success: user continued working)
|
||||
const prevMatched = sessionGet('lastMatchedPatterns');
|
||||
|
||||
// Store NEW matched IDs in session state for feedback
|
||||
const matchedIds = topEntries.map(e => e.id);
|
||||
sessionSet('lastMatchedPatterns', matchedIds);
|
||||
|
||||
// Only boost previous if they differ from current (avoid double-boosting)
|
||||
if (prevMatched && Array.isArray(prevMatched)) {
|
||||
const newSet = new Set(matchedIds);
|
||||
const toBoost = prevMatched.filter(id => !newSet.has(id));
|
||||
if (toBoost.length > 0) boostConfidence(toBoost, 0.03);
|
||||
}
|
||||
|
||||
// Format output
|
||||
const lines = ['[INTELLIGENCE] Relevant patterns for this task:'];
|
||||
for (let i = 0; i < topEntries.length; i++) {
|
||||
const e = topEntries[i];
|
||||
const display = (e.summary || e.content || '').slice(0, 80);
|
||||
const accessed = e.accessCount || 0;
|
||||
lines.push(` * (${e.score.toFixed(2)}) ${display} [rank #${i + 1}, ${accessed}x accessed]`);
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* recordEdit(file) — Called from post-edit. Budget: <2ms.
|
||||
* Appends to pending-insights.jsonl.
|
||||
*/
|
||||
function recordEdit(file) {
|
||||
ensureDataDir();
|
||||
const entry = JSON.stringify({
|
||||
type: 'edit',
|
||||
file: file || 'unknown',
|
||||
timestamp: Date.now(),
|
||||
sessionId: sessionGet('sessionId') || null,
|
||||
});
|
||||
fs.appendFileSync(PENDING_PATH, entry + '\n', 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* feedback(success) — Called from post-task. Budget: <10ms.
|
||||
* Boosts or decays confidence for last-matched patterns.
|
||||
*/
|
||||
function feedback(success) {
|
||||
const matchedIds = sessionGet('lastMatchedPatterns');
|
||||
if (!matchedIds || !Array.isArray(matchedIds)) return;
|
||||
|
||||
const amount = success ? 0.05 : -0.02;
|
||||
boostConfidence(matchedIds, amount);
|
||||
}
|
||||
|
||||
function boostConfidence(ids, amount) {
|
||||
const ranked = readJSON(RANKED_PATH);
|
||||
if (!ranked || !ranked.entries) return;
|
||||
|
||||
let changed = false;
|
||||
for (const entry of ranked.entries) {
|
||||
if (ids.includes(entry.id)) {
|
||||
entry.confidence = Math.max(0, Math.min(1, (entry.confidence || 0.5) + amount));
|
||||
entry.accessCount = (entry.accessCount || 0) + 1;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) writeJSON(RANKED_PATH, ranked);
|
||||
|
||||
// Also update graph-state confidence
|
||||
const graph = readJSON(GRAPH_PATH);
|
||||
if (graph && graph.nodes) {
|
||||
for (const id of ids) {
|
||||
if (graph.nodes[id]) {
|
||||
graph.nodes[id].confidence = Math.max(0, Math.min(1, (graph.nodes[id].confidence || 0.5) + amount));
|
||||
graph.nodes[id].accessCount = (graph.nodes[id].accessCount || 0) + 1;
|
||||
}
|
||||
}
|
||||
writeJSON(GRAPH_PATH, graph);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* consolidate() — Called from session-end. Budget: <500ms.
|
||||
* Processes pending insights, rebuilds edges, recomputes PageRank.
|
||||
*/
|
||||
function consolidate() {
|
||||
ensureDataDir();
|
||||
|
||||
const store = readJSON(STORE_PATH);
|
||||
if (!store || !Array.isArray(store)) {
|
||||
return { entries: 0, edges: 0, newEntries: 0, message: 'No store to consolidate' };
|
||||
}
|
||||
|
||||
// 1. Process pending insights
|
||||
let newEntries = 0;
|
||||
if (fs.existsSync(PENDING_PATH)) {
|
||||
const lines = fs.readFileSync(PENDING_PATH, 'utf-8').trim().split('\n').filter(Boolean);
|
||||
const editCounts = {};
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const insight = JSON.parse(line);
|
||||
if (insight.file) {
|
||||
editCounts[insight.file] = (editCounts[insight.file] || 0) + 1;
|
||||
}
|
||||
} catch { /* skip malformed */ }
|
||||
}
|
||||
|
||||
// Create entries for frequently-edited files (3+ edits)
|
||||
for (const [file, count] of Object.entries(editCounts)) {
|
||||
if (count >= 3) {
|
||||
const exists = store.some(e =>
|
||||
(e.metadata && e.metadata.sourceFile === file && e.metadata.autoGenerated)
|
||||
);
|
||||
if (!exists) {
|
||||
store.push({
|
||||
id: `insight-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
||||
key: `frequent-edit-${path.basename(file)}`,
|
||||
content: `File ${file} was edited ${count} times this session — likely a hot path worth monitoring.`,
|
||||
summary: `Frequently edited: ${path.basename(file)} (${count}x)`,
|
||||
namespace: 'insights',
|
||||
type: 'procedural',
|
||||
metadata: { sourceFile: file, editCount: count, autoGenerated: true },
|
||||
createdAt: Date.now(),
|
||||
});
|
||||
newEntries++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear pending
|
||||
fs.writeFileSync(PENDING_PATH, '', 'utf-8');
|
||||
}
|
||||
|
||||
// 2. Confidence decay for unaccessed entries
|
||||
const graph = readJSON(GRAPH_PATH);
|
||||
if (graph && graph.nodes) {
|
||||
const now = Date.now();
|
||||
for (const id of Object.keys(graph.nodes)) {
|
||||
const node = graph.nodes[id];
|
||||
const hoursSinceCreation = (now - (node.createdAt || now)) / (1000 * 60 * 60);
|
||||
if (node.accessCount === 0 && hoursSinceCreation > 24) {
|
||||
node.confidence = Math.max(0.05, (node.confidence || 0.5) - 0.005 * Math.floor(hoursSinceCreation / 24));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Rebuild edges with updated store
|
||||
for (const entry of store) {
|
||||
if (!entry.id) entry.id = `entry-${Math.random().toString(36).slice(2, 8)}`;
|
||||
}
|
||||
const edges = buildEdges(store);
|
||||
|
||||
// 4. Build updated nodes
|
||||
const nodes = {};
|
||||
for (const entry of store) {
|
||||
nodes[entry.id] = {
|
||||
id: entry.id,
|
||||
category: entry.namespace || entry.type || 'default',
|
||||
confidence: (graph && graph.nodes && graph.nodes[entry.id])
|
||||
? graph.nodes[entry.id].confidence
|
||||
: (entry.metadata && entry.metadata.confidence) || 0.5,
|
||||
accessCount: (graph && graph.nodes && graph.nodes[entry.id])
|
||||
? graph.nodes[entry.id].accessCount
|
||||
: (entry.metadata && entry.metadata.accessCount) || 0,
|
||||
createdAt: entry.createdAt || Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
// 5. Recompute PageRank
|
||||
const pageRanks = computePageRank(nodes, edges, 0.85, 30);
|
||||
|
||||
// 6. Write updated graph
|
||||
writeJSON(GRAPH_PATH, {
|
||||
version: 1,
|
||||
updatedAt: Date.now(),
|
||||
nodeCount: Object.keys(nodes).length,
|
||||
nodes,
|
||||
edges,
|
||||
pageRanks,
|
||||
});
|
||||
|
||||
// 7. Write updated ranked context
|
||||
const rankedEntries = store.map(entry => {
|
||||
const id = entry.id;
|
||||
const content = entry.content || entry.value || '';
|
||||
const summary = entry.summary || entry.key || '';
|
||||
const words = tokenize(content + ' ' + summary);
|
||||
return {
|
||||
id,
|
||||
content,
|
||||
summary,
|
||||
category: entry.namespace || entry.type || 'default',
|
||||
confidence: nodes[id] ? nodes[id].confidence : 0.5,
|
||||
pageRank: pageRanks[id] || 0,
|
||||
accessCount: nodes[id] ? nodes[id].accessCount : 0,
|
||||
words,
|
||||
};
|
||||
}).sort((a, b) => {
|
||||
const scoreA = 0.6 * a.pageRank + 0.4 * a.confidence;
|
||||
const scoreB = 0.6 * b.pageRank + 0.4 * b.confidence;
|
||||
return scoreB - scoreA;
|
||||
});
|
||||
|
||||
writeJSON(RANKED_PATH, {
|
||||
version: 1,
|
||||
computedAt: Date.now(),
|
||||
entries: rankedEntries,
|
||||
});
|
||||
|
||||
// 8. Persist updated store (with new insight entries)
|
||||
if (newEntries > 0) writeJSON(STORE_PATH, store);
|
||||
|
||||
// 9. Save snapshot for delta tracking
|
||||
const updatedGraph = readJSON(GRAPH_PATH);
|
||||
const updatedRanked = readJSON(RANKED_PATH);
|
||||
saveSnapshot(updatedGraph, updatedRanked);
|
||||
|
||||
return {
|
||||
entries: store.length,
|
||||
edges: edges.length,
|
||||
newEntries,
|
||||
message: 'Consolidated',
|
||||
};
|
||||
}
|
||||
|
||||
// ── Snapshot for delta tracking ─────────────────────────────────────────────
|
||||
|
||||
const SNAPSHOT_PATH = path.join(DATA_DIR, 'intelligence-snapshot.json');
|
||||
|
||||
function saveSnapshot(graph, ranked) {
|
||||
const snap = {
|
||||
timestamp: Date.now(),
|
||||
nodes: graph ? Object.keys(graph.nodes || {}).length : 0,
|
||||
edges: graph ? (graph.edges || []).length : 0,
|
||||
pageRankSum: 0,
|
||||
confidences: [],
|
||||
accessCounts: [],
|
||||
topPatterns: [],
|
||||
};
|
||||
|
||||
if (graph && graph.pageRanks) {
|
||||
for (const v of Object.values(graph.pageRanks)) snap.pageRankSum += v;
|
||||
}
|
||||
if (graph && graph.nodes) {
|
||||
for (const n of Object.values(graph.nodes)) {
|
||||
snap.confidences.push(n.confidence || 0.5);
|
||||
snap.accessCounts.push(n.accessCount || 0);
|
||||
}
|
||||
}
|
||||
if (ranked && ranked.entries) {
|
||||
snap.topPatterns = ranked.entries.slice(0, 10).map(e => ({
|
||||
id: e.id,
|
||||
summary: (e.summary || '').slice(0, 60),
|
||||
confidence: e.confidence || 0.5,
|
||||
pageRank: e.pageRank || 0,
|
||||
accessCount: e.accessCount || 0,
|
||||
}));
|
||||
}
|
||||
|
||||
// Keep history: append to array, cap at 50
|
||||
let history = readJSON(SNAPSHOT_PATH);
|
||||
if (!Array.isArray(history)) history = [];
|
||||
history.push(snap);
|
||||
if (history.length > 50) history = history.slice(-50);
|
||||
writeJSON(SNAPSHOT_PATH, history);
|
||||
}
|
||||
|
||||
/**
|
||||
* stats() — Diagnostic report showing intelligence health and improvement.
|
||||
* Can be called as: node intelligence.cjs stats [--json]
|
||||
*/
|
||||
function stats(outputJson) {
|
||||
const graph = readJSON(GRAPH_PATH);
|
||||
const ranked = readJSON(RANKED_PATH);
|
||||
const history = readJSON(SNAPSHOT_PATH) || [];
|
||||
const pending = fs.existsSync(PENDING_PATH)
|
||||
? fs.readFileSync(PENDING_PATH, 'utf-8').trim().split('\n').filter(Boolean).length
|
||||
: 0;
|
||||
|
||||
// Current state
|
||||
const nodes = graph ? Object.keys(graph.nodes || {}).length : 0;
|
||||
const edges = graph ? (graph.edges || []).length : 0;
|
||||
const density = nodes > 1 ? (2 * edges) / (nodes * (nodes - 1)) : 0;
|
||||
|
||||
// Confidence distribution
|
||||
const confidences = [];
|
||||
const accessCounts = [];
|
||||
if (graph && graph.nodes) {
|
||||
for (const n of Object.values(graph.nodes)) {
|
||||
confidences.push(n.confidence || 0.5);
|
||||
accessCounts.push(n.accessCount || 0);
|
||||
}
|
||||
}
|
||||
confidences.sort((a, b) => a - b);
|
||||
const confMin = confidences.length ? confidences[0] : 0;
|
||||
const confMax = confidences.length ? confidences[confidences.length - 1] : 0;
|
||||
const confMean = confidences.length ? confidences.reduce((s, c) => s + c, 0) / confidences.length : 0;
|
||||
const confMedian = confidences.length ? confidences[Math.floor(confidences.length / 2)] : 0;
|
||||
|
||||
// Access stats
|
||||
const totalAccess = accessCounts.reduce((s, c) => s + c, 0);
|
||||
const accessedCount = accessCounts.filter(c => c > 0).length;
|
||||
|
||||
// PageRank stats
|
||||
let prSum = 0, prMax = 0, prMaxId = '';
|
||||
if (graph && graph.pageRanks) {
|
||||
for (const [id, pr] of Object.entries(graph.pageRanks)) {
|
||||
prSum += pr;
|
||||
if (pr > prMax) { prMax = pr; prMaxId = id; }
|
||||
}
|
||||
}
|
||||
|
||||
// Top patterns by composite score
|
||||
const topPatterns = (ranked && ranked.entries || []).slice(0, 10).map((e, i) => ({
|
||||
rank: i + 1,
|
||||
summary: (e.summary || '').slice(0, 60),
|
||||
confidence: (e.confidence || 0.5).toFixed(3),
|
||||
pageRank: (e.pageRank || 0).toFixed(4),
|
||||
accessed: e.accessCount || 0,
|
||||
score: (0.6 * (e.pageRank || 0) + 0.4 * (e.confidence || 0.5)).toFixed(4),
|
||||
}));
|
||||
|
||||
// Edge type breakdown
|
||||
const edgeTypes = {};
|
||||
if (graph && graph.edges) {
|
||||
for (const e of graph.edges) {
|
||||
edgeTypes[e.type || 'unknown'] = (edgeTypes[e.type || 'unknown'] || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Delta from previous snapshot
|
||||
let delta = null;
|
||||
if (history.length >= 2) {
|
||||
const prev = history[history.length - 2];
|
||||
const curr = history[history.length - 1];
|
||||
const elapsed = (curr.timestamp - prev.timestamp) / 1000;
|
||||
const prevConfMean = prev.confidences.length
|
||||
? prev.confidences.reduce((s, c) => s + c, 0) / prev.confidences.length : 0;
|
||||
const currConfMean = curr.confidences.length
|
||||
? curr.confidences.reduce((s, c) => s + c, 0) / curr.confidences.length : 0;
|
||||
const prevAccess = prev.accessCounts.reduce((s, c) => s + c, 0);
|
||||
const currAccess = curr.accessCounts.reduce((s, c) => s + c, 0);
|
||||
|
||||
delta = {
|
||||
elapsed: elapsed < 3600 ? `${Math.round(elapsed / 60)}m` : `${(elapsed / 3600).toFixed(1)}h`,
|
||||
nodes: curr.nodes - prev.nodes,
|
||||
edges: curr.edges - prev.edges,
|
||||
confidenceMean: currConfMean - prevConfMean,
|
||||
totalAccess: currAccess - prevAccess,
|
||||
};
|
||||
}
|
||||
|
||||
// Trend over all history
|
||||
let trend = null;
|
||||
if (history.length >= 3) {
|
||||
const first = history[0];
|
||||
const last = history[history.length - 1];
|
||||
const sessions = history.length;
|
||||
const firstConfMean = first.confidences.length
|
||||
? first.confidences.reduce((s, c) => s + c, 0) / first.confidences.length : 0;
|
||||
const lastConfMean = last.confidences.length
|
||||
? last.confidences.reduce((s, c) => s + c, 0) / last.confidences.length : 0;
|
||||
trend = {
|
||||
sessions,
|
||||
nodeGrowth: last.nodes - first.nodes,
|
||||
edgeGrowth: last.edges - first.edges,
|
||||
confidenceDrift: lastConfMean - firstConfMean,
|
||||
direction: lastConfMean > firstConfMean ? 'improving' :
|
||||
lastConfMean < firstConfMean ? 'declining' : 'stable',
|
||||
};
|
||||
}
|
||||
|
||||
const report = {
|
||||
graph: { nodes, edges, density: +density.toFixed(4) },
|
||||
confidence: {
|
||||
min: +confMin.toFixed(3), max: +confMax.toFixed(3),
|
||||
mean: +confMean.toFixed(3), median: +confMedian.toFixed(3),
|
||||
},
|
||||
access: { total: totalAccess, patternsAccessed: accessedCount, patternsNeverAccessed: nodes - accessedCount },
|
||||
pageRank: { sum: +prSum.toFixed(4), topNode: prMaxId, topNodeRank: +prMax.toFixed(4) },
|
||||
edgeTypes,
|
||||
pendingInsights: pending,
|
||||
snapshots: history.length,
|
||||
topPatterns,
|
||||
delta,
|
||||
trend,
|
||||
};
|
||||
|
||||
if (outputJson) {
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
return report;
|
||||
}
|
||||
|
||||
// Human-readable output
|
||||
const bar = '+' + '-'.repeat(62) + '+';
|
||||
console.log(bar);
|
||||
console.log('|' + ' Intelligence Diagnostics (ADR-050)'.padEnd(62) + '|');
|
||||
console.log(bar);
|
||||
console.log('');
|
||||
|
||||
console.log(' Graph');
|
||||
console.log(` Nodes: ${nodes}`);
|
||||
console.log(` Edges: ${edges} (${Object.entries(edgeTypes).map(([t,c]) => `${c} ${t}`).join(', ') || 'none'})`);
|
||||
console.log(` Density: ${(density * 100).toFixed(1)}%`);
|
||||
console.log('');
|
||||
|
||||
console.log(' Confidence');
|
||||
console.log(` Min: ${confMin.toFixed(3)}`);
|
||||
console.log(` Max: ${confMax.toFixed(3)}`);
|
||||
console.log(` Mean: ${confMean.toFixed(3)}`);
|
||||
console.log(` Median: ${confMedian.toFixed(3)}`);
|
||||
console.log('');
|
||||
|
||||
console.log(' Access');
|
||||
console.log(` Total accesses: ${totalAccess}`);
|
||||
console.log(` Patterns used: ${accessedCount}/${nodes}`);
|
||||
console.log(` Never accessed: ${nodes - accessedCount}`);
|
||||
console.log(` Pending insights: ${pending}`);
|
||||
console.log('');
|
||||
|
||||
console.log(' PageRank');
|
||||
console.log(` Sum: ${prSum.toFixed(4)} (should be ~1.0)`);
|
||||
console.log(` Top node: ${prMaxId || '(none)'} (${prMax.toFixed(4)})`);
|
||||
console.log('');
|
||||
|
||||
if (topPatterns.length > 0) {
|
||||
console.log(' Top Patterns (by composite score)');
|
||||
console.log(' ' + '-'.repeat(60));
|
||||
for (const p of topPatterns) {
|
||||
console.log(` #${p.rank} ${p.summary}`);
|
||||
console.log(` conf=${p.confidence} pr=${p.pageRank} score=${p.score} accessed=${p.accessed}x`);
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
if (delta) {
|
||||
console.log(` Last Delta (${delta.elapsed} ago)`);
|
||||
const sign = v => v > 0 ? `+${v}` : `${v}`;
|
||||
console.log(` Nodes: ${sign(delta.nodes)}`);
|
||||
console.log(` Edges: ${sign(delta.edges)}`);
|
||||
console.log(` Confidence: ${delta.confidenceMean >= 0 ? '+' : ''}${delta.confidenceMean.toFixed(4)}`);
|
||||
console.log(` Accesses: ${sign(delta.totalAccess)}`);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
if (trend) {
|
||||
console.log(` Trend (${trend.sessions} snapshots)`);
|
||||
console.log(` Node growth: ${trend.nodeGrowth >= 0 ? '+' : ''}${trend.nodeGrowth}`);
|
||||
console.log(` Edge growth: ${trend.edgeGrowth >= 0 ? '+' : ''}${trend.edgeGrowth}`);
|
||||
console.log(` Confidence drift: ${trend.confidenceDrift >= 0 ? '+' : ''}${trend.confidenceDrift.toFixed(4)}`);
|
||||
console.log(` Direction: ${trend.direction.toUpperCase()}`);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
if (!delta && !trend) {
|
||||
console.log(' No history yet — run more sessions to see deltas and trends.');
|
||||
console.log('');
|
||||
}
|
||||
|
||||
console.log(bar);
|
||||
return report;
|
||||
}
|
||||
|
||||
module.exports = { init, getContext, recordEdit, feedback, consolidate, stats };
|
||||
|
||||
// ── CLI entrypoint ──────────────────────────────────────────────────────────
|
||||
if (require.main === module) {
|
||||
const cmd = process.argv[2];
|
||||
const jsonFlag = process.argv.includes('--json');
|
||||
|
||||
const cmds = {
|
||||
init: () => { const r = init(); console.log(JSON.stringify(r)); },
|
||||
stats: () => { stats(jsonFlag); },
|
||||
consolidate: () => { const r = consolidate(); console.log(JSON.stringify(r)); },
|
||||
};
|
||||
|
||||
if (cmd && cmds[cmd]) {
|
||||
cmds[cmd]();
|
||||
} else {
|
||||
console.log('Usage: intelligence.cjs <stats|init|consolidate> [--json]');
|
||||
console.log('');
|
||||
console.log(' stats Show intelligence diagnostics and trends');
|
||||
console.log(' stats --json Output as JSON for programmatic use');
|
||||
console.log(' init Build graph and rank entries');
|
||||
console.log(' consolidate Process pending insights and recompute');
|
||||
}
|
||||
}
|
||||
@@ -100,6 +100,14 @@ const commands = {
|
||||
return session;
|
||||
},
|
||||
|
||||
get: (key) => {
|
||||
if (!fs.existsSync(SESSION_FILE)) return null;
|
||||
try {
|
||||
const session = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf-8'));
|
||||
return key ? (session.context || {})[key] : session.context;
|
||||
} catch { return null; }
|
||||
},
|
||||
|
||||
metric: (name) => {
|
||||
if (!fs.existsSync(SESSION_FILE)) {
|
||||
return null;
|
||||
|
||||
@@ -1,32 +1,31 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Claude Flow V3 Statusline Generator
|
||||
* Claude Flow V3 Statusline Generator (Optimized)
|
||||
* Displays real-time V3 implementation progress and system status
|
||||
*
|
||||
* Usage: node statusline.cjs [--json] [--compact]
|
||||
*
|
||||
* IMPORTANT: This file uses .cjs extension to work in ES module projects.
|
||||
* The require() syntax is intentional for CommonJS compatibility.
|
||||
* Performance notes:
|
||||
* - Single git execSync call (combines branch + status + upstream)
|
||||
* - No recursive file reading (only stat/readdir, never read test contents)
|
||||
* - No ps aux calls (uses process.memoryUsage() + file-based metrics)
|
||||
* - Strict 2s timeout on all execSync calls
|
||||
* - Shared settings cache across functions
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
const os = require('os');
|
||||
|
||||
// Configuration
|
||||
const CONFIG = {
|
||||
enabled: true,
|
||||
showProgress: true,
|
||||
showSecurity: true,
|
||||
showSwarm: true,
|
||||
showHooks: true,
|
||||
showPerformance: true,
|
||||
refreshInterval: 5000,
|
||||
maxAgents: 15,
|
||||
topology: 'hierarchical-mesh',
|
||||
};
|
||||
|
||||
const CWD = process.cwd();
|
||||
|
||||
// ANSI colors
|
||||
const c = {
|
||||
reset: '\x1b[0m',
|
||||
@@ -47,270 +46,709 @@ const c = {
|
||||
brightWhite: '\x1b[1;37m',
|
||||
};
|
||||
|
||||
// Get user info
|
||||
function getUserInfo() {
|
||||
let name = 'user';
|
||||
let gitBranch = '';
|
||||
let modelName = 'Opus 4.5';
|
||||
|
||||
// Safe execSync with strict timeout (returns empty string on failure)
|
||||
function safeExec(cmd, timeoutMs = 2000) {
|
||||
try {
|
||||
name = execSync('git config user.name 2>/dev/null || echo "user"', { encoding: 'utf-8' }).trim();
|
||||
gitBranch = execSync('git branch --show-current 2>/dev/null || echo ""', { encoding: 'utf-8' }).trim();
|
||||
} catch (e) {
|
||||
// Ignore errors
|
||||
return execSync(cmd, {
|
||||
encoding: 'utf-8',
|
||||
timeout: timeoutMs,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
}).trim();
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
|
||||
return { name, gitBranch, modelName };
|
||||
}
|
||||
|
||||
// Get learning stats from memory database
|
||||
function getLearningStats() {
|
||||
const memoryPaths = [
|
||||
path.join(process.cwd(), '.swarm', 'memory.db'),
|
||||
path.join(process.cwd(), '.claude', 'memory.db'),
|
||||
path.join(process.cwd(), 'data', 'memory.db'),
|
||||
];
|
||||
// Safe JSON file reader (returns null on failure)
|
||||
function readJSON(filePath) {
|
||||
try {
|
||||
if (fs.existsSync(filePath)) {
|
||||
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
return null;
|
||||
}
|
||||
|
||||
let patterns = 0;
|
||||
let sessions = 0;
|
||||
let trajectories = 0;
|
||||
// Safe file stat (returns null on failure)
|
||||
function safeStat(filePath) {
|
||||
try {
|
||||
return fs.statSync(filePath);
|
||||
} catch { /* ignore */ }
|
||||
return null;
|
||||
}
|
||||
|
||||
// Try to read from sqlite database
|
||||
for (const dbPath of memoryPaths) {
|
||||
if (fs.existsSync(dbPath)) {
|
||||
try {
|
||||
// Count entries in memory file (rough estimate from file size)
|
||||
const stats = fs.statSync(dbPath);
|
||||
const sizeKB = stats.size / 1024;
|
||||
// Estimate: ~2KB per pattern on average
|
||||
patterns = Math.floor(sizeKB / 2);
|
||||
sessions = Math.max(1, Math.floor(patterns / 10));
|
||||
trajectories = Math.floor(patterns / 5);
|
||||
break;
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
// Shared settings cache — read once, used by multiple functions
|
||||
let _settingsCache = undefined;
|
||||
function getSettings() {
|
||||
if (_settingsCache !== undefined) return _settingsCache;
|
||||
_settingsCache = readJSON(path.join(CWD, '.claude', 'settings.json'))
|
||||
|| readJSON(path.join(CWD, '.claude', 'settings.local.json'))
|
||||
|| null;
|
||||
return _settingsCache;
|
||||
}
|
||||
|
||||
// ─── Data Collection (all pure-Node.js or single-exec) ──────────
|
||||
|
||||
// Get all git info in ONE shell call
|
||||
function getGitInfo() {
|
||||
const result = {
|
||||
name: 'user', gitBranch: '', modified: 0, untracked: 0,
|
||||
staged: 0, ahead: 0, behind: 0,
|
||||
};
|
||||
|
||||
// Single shell: get user.name, branch, porcelain status, and upstream diff
|
||||
const script = [
|
||||
'git config user.name 2>/dev/null || echo user',
|
||||
'echo "---SEP---"',
|
||||
'git branch --show-current 2>/dev/null',
|
||||
'echo "---SEP---"',
|
||||
'git status --porcelain 2>/dev/null',
|
||||
'echo "---SEP---"',
|
||||
'git rev-list --left-right --count HEAD...@{upstream} 2>/dev/null || echo "0 0"',
|
||||
].join('; ');
|
||||
|
||||
const raw = safeExec("sh -c '" + script + "'", 3000);
|
||||
if (!raw) return result;
|
||||
|
||||
const parts = raw.split('---SEP---').map(s => s.trim());
|
||||
if (parts.length >= 4) {
|
||||
result.name = parts[0] || 'user';
|
||||
result.gitBranch = parts[1] || '';
|
||||
|
||||
// Parse porcelain status
|
||||
if (parts[2]) {
|
||||
for (const line of parts[2].split('\n')) {
|
||||
if (!line || line.length < 2) continue;
|
||||
const x = line[0], y = line[1];
|
||||
if (x === '?' && y === '?') { result.untracked++; continue; }
|
||||
if (x !== ' ' && x !== '?') result.staged++;
|
||||
if (y !== ' ' && y !== '?') result.modified++;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse ahead/behind
|
||||
const ab = (parts[3] || '0 0').split(/\s+/);
|
||||
result.ahead = parseInt(ab[0]) || 0;
|
||||
result.behind = parseInt(ab[1]) || 0;
|
||||
}
|
||||
|
||||
// Also check for 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
|
||||
return result;
|
||||
}
|
||||
|
||||
// Detect model name from Claude config (pure file reads, no exec)
|
||||
function getModelName() {
|
||||
try {
|
||||
const claudeConfig = readJSON(path.join(os.homedir(), '.claude.json'));
|
||||
if (claudeConfig && claudeConfig.projects) {
|
||||
for (const [projectPath, projectConfig] of Object.entries(claudeConfig.projects)) {
|
||||
if (CWD === projectPath || CWD.startsWith(projectPath + '/')) {
|
||||
const usage = projectConfig.lastModelUsage;
|
||||
if (usage) {
|
||||
const ids = Object.keys(usage);
|
||||
if (ids.length > 0) {
|
||||
let modelId = ids[ids.length - 1];
|
||||
let latest = 0;
|
||||
for (const id of ids) {
|
||||
const ts = usage[id] && usage[id].lastUsedAt ? new Date(usage[id].lastUsedAt).getTime() : 0;
|
||||
if (ts > latest) { latest = ts; modelId = id; }
|
||||
}
|
||||
if (modelId.includes('opus')) return 'Opus 4.6';
|
||||
if (modelId.includes('sonnet')) return 'Sonnet 4.6';
|
||||
if (modelId.includes('haiku')) return 'Haiku 4.5';
|
||||
return modelId.split('-').slice(1, 3).join(' ');
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
|
||||
// Fallback: settings.json model field
|
||||
const settings = getSettings();
|
||||
if (settings && settings.model) {
|
||||
const m = settings.model;
|
||||
if (m.includes('opus')) return 'Opus 4.6';
|
||||
if (m.includes('sonnet')) return 'Sonnet 4.6';
|
||||
if (m.includes('haiku')) return 'Haiku 4.5';
|
||||
}
|
||||
return 'Claude Code';
|
||||
}
|
||||
|
||||
// Get learning stats from memory database (pure stat calls)
|
||||
function getLearningStats() {
|
||||
const memoryPaths = [
|
||||
path.join(CWD, '.swarm', 'memory.db'),
|
||||
path.join(CWD, '.claude-flow', 'memory.db'),
|
||||
path.join(CWD, '.claude', 'memory.db'),
|
||||
path.join(CWD, 'data', 'memory.db'),
|
||||
path.join(CWD, '.agentdb', 'memory.db'),
|
||||
];
|
||||
|
||||
for (const dbPath of memoryPaths) {
|
||||
const stat = safeStat(dbPath);
|
||||
if (stat) {
|
||||
const sizeKB = stat.size / 1024;
|
||||
const patterns = Math.floor(sizeKB / 2);
|
||||
return {
|
||||
patterns,
|
||||
sessions: Math.max(1, Math.floor(patterns / 10)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { patterns, sessions, trajectories };
|
||||
// Check session files count
|
||||
let sessions = 0;
|
||||
try {
|
||||
const sessDir = path.join(CWD, '.claude', 'sessions');
|
||||
if (fs.existsSync(sessDir)) {
|
||||
sessions = fs.readdirSync(sessDir).filter(f => f.endsWith('.json')).length;
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
|
||||
return { patterns: 0, sessions };
|
||||
}
|
||||
|
||||
// Get V3 progress from learning state (grows as system learns)
|
||||
// V3 progress from metrics files (pure file reads)
|
||||
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));
|
||||
|
||||
const dddData = readJSON(path.join(CWD, '.claude-flow', 'metrics', 'ddd-progress.json'));
|
||||
let dddProgress = dddData ? (dddData.progress || 0) : 0;
|
||||
let domainsCompleted = Math.min(5, Math.floor(dddProgress / 20));
|
||||
|
||||
if (dddProgress === 0 && learning.patterns > 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;
|
||||
dddProgress = Math.floor((domainsCompleted / totalDomains) * 100);
|
||||
}
|
||||
|
||||
return {
|
||||
domainsCompleted,
|
||||
totalDomains,
|
||||
dddProgress,
|
||||
domainsCompleted, totalDomains, dddProgress,
|
||||
patternsLearned: learning.patterns,
|
||||
sessionsCompleted: learning.sessions
|
||||
sessionsCompleted: learning.sessions,
|
||||
};
|
||||
}
|
||||
|
||||
// Get security status based on actual scans
|
||||
// Security status (pure file reads)
|
||||
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
|
||||
}
|
||||
const auditData = readJSON(path.join(CWD, '.claude-flow', 'security', 'audit-status.json'));
|
||||
if (auditData) {
|
||||
return {
|
||||
status: auditData.status || 'PENDING',
|
||||
cvesFixed: auditData.cvesFixed || 0,
|
||||
totalCves: auditData.totalCves || 3,
|
||||
};
|
||||
}
|
||||
|
||||
// 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
|
||||
let cvesFixed = 0;
|
||||
try {
|
||||
const scanDir = path.join(CWD, '.claude', 'security-scans');
|
||||
if (fs.existsSync(scanDir)) {
|
||||
cvesFixed = Math.min(totalCves, fs.readdirSync(scanDir).filter(f => f.endsWith('.json')).length);
|
||||
}
|
||||
}
|
||||
|
||||
const status = cvesFixed >= totalCves ? 'CLEAN' : cvesFixed > 0 ? 'IN_PROGRESS' : 'PENDING';
|
||||
} catch { /* ignore */ }
|
||||
|
||||
return {
|
||||
status,
|
||||
status: cvesFixed >= totalCves ? 'CLEAN' : cvesFixed > 0 ? 'IN_PROGRESS' : 'PENDING',
|
||||
cvesFixed,
|
||||
totalCves,
|
||||
};
|
||||
}
|
||||
|
||||
// Get swarm status
|
||||
// Swarm status (pure file reads, NO ps aux)
|
||||
function getSwarmStatus() {
|
||||
let activeAgents = 0;
|
||||
let coordinationActive = false;
|
||||
|
||||
try {
|
||||
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
|
||||
const activityData = readJSON(path.join(CWD, '.claude-flow', 'metrics', 'swarm-activity.json'));
|
||||
if (activityData && activityData.swarm) {
|
||||
return {
|
||||
activeAgents: activityData.swarm.agent_count || 0,
|
||||
maxAgents: CONFIG.maxAgents,
|
||||
coordinationActive: activityData.swarm.coordination_active || activityData.swarm.active || false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
activeAgents,
|
||||
maxAgents: CONFIG.maxAgents,
|
||||
coordinationActive,
|
||||
};
|
||||
const progressData = readJSON(path.join(CWD, '.claude-flow', 'metrics', 'v3-progress.json'));
|
||||
if (progressData && progressData.swarm) {
|
||||
return {
|
||||
activeAgents: progressData.swarm.activeAgents || progressData.swarm.agent_count || 0,
|
||||
maxAgents: progressData.swarm.totalAgents || CONFIG.maxAgents,
|
||||
coordinationActive: progressData.swarm.active || (progressData.swarm.activeAgents > 0),
|
||||
};
|
||||
}
|
||||
|
||||
return { activeAgents: 0, maxAgents: CONFIG.maxAgents, coordinationActive: false };
|
||||
}
|
||||
|
||||
// Get system metrics (dynamic based on actual state)
|
||||
// System metrics (uses process.memoryUsage() — no shell spawn)
|
||||
function getSystemMetrics() {
|
||||
let memoryMB = 0;
|
||||
let subAgents = 0;
|
||||
|
||||
try {
|
||||
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 memoryMB = Math.floor(process.memoryUsage().heapUsed / 1024 / 1024);
|
||||
const learning = getLearningStats();
|
||||
const agentdb = getAgentDBStats();
|
||||
|
||||
// Intelligence % based on learned patterns (0 patterns = 0%, 1000+ = 100%)
|
||||
const intelligencePct = Math.min(100, Math.floor((learning.patterns / 10) * 1));
|
||||
// Intelligence from learning.json
|
||||
const learningData = readJSON(path.join(CWD, '.claude-flow', 'metrics', 'learning.json'));
|
||||
let intelligencePct = 0;
|
||||
let contextPct = 0;
|
||||
|
||||
// Context % based on session history (0 sessions = 0%, grows with usage)
|
||||
const contextPct = Math.min(100, Math.floor(learning.sessions * 5));
|
||||
|
||||
// Count active sub-agents from process list
|
||||
try {
|
||||
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
|
||||
if (learningData && learningData.intelligence && learningData.intelligence.score !== undefined) {
|
||||
intelligencePct = Math.min(100, Math.floor(learningData.intelligence.score));
|
||||
} else {
|
||||
const fromPatterns = learning.patterns > 0 ? Math.min(100, Math.floor(learning.patterns / 10)) : 0;
|
||||
const fromVectors = agentdb.vectorCount > 0 ? Math.min(100, Math.floor(agentdb.vectorCount / 100)) : 0;
|
||||
intelligencePct = Math.max(fromPatterns, fromVectors);
|
||||
}
|
||||
|
||||
return {
|
||||
memoryMB,
|
||||
contextPct,
|
||||
intelligencePct,
|
||||
subAgents,
|
||||
};
|
||||
// Maturity fallback (pure fs checks, no git exec)
|
||||
if (intelligencePct === 0) {
|
||||
let score = 0;
|
||||
if (fs.existsSync(path.join(CWD, '.claude'))) score += 15;
|
||||
const srcDirs = ['src', 'lib', 'app', 'packages', 'v3'];
|
||||
for (const d of srcDirs) { if (fs.existsSync(path.join(CWD, d))) { score += 15; break; } }
|
||||
const testDirs = ['tests', 'test', '__tests__', 'spec'];
|
||||
for (const d of testDirs) { if (fs.existsSync(path.join(CWD, d))) { score += 10; break; } }
|
||||
const cfgFiles = ['package.json', 'tsconfig.json', 'pyproject.toml', 'Cargo.toml', 'go.mod'];
|
||||
for (const f of cfgFiles) { if (fs.existsSync(path.join(CWD, f))) { score += 5; break; } }
|
||||
intelligencePct = Math.min(100, score);
|
||||
}
|
||||
|
||||
if (learningData && learningData.sessions && learningData.sessions.total !== undefined) {
|
||||
contextPct = Math.min(100, learningData.sessions.total * 5);
|
||||
} else {
|
||||
contextPct = Math.min(100, Math.floor(learning.sessions * 5));
|
||||
}
|
||||
|
||||
// Sub-agents from file metrics (no ps aux)
|
||||
let subAgents = 0;
|
||||
const activityData = readJSON(path.join(CWD, '.claude-flow', 'metrics', 'swarm-activity.json'));
|
||||
if (activityData && activityData.processes && activityData.processes.estimated_agents) {
|
||||
subAgents = activityData.processes.estimated_agents;
|
||||
}
|
||||
|
||||
return { memoryMB, contextPct, intelligencePct, subAgents };
|
||||
}
|
||||
|
||||
// Generate progress bar
|
||||
// ADR status (count files only — don't read contents)
|
||||
function getADRStatus() {
|
||||
const complianceData = readJSON(path.join(CWD, '.claude-flow', 'metrics', 'adr-compliance.json'));
|
||||
if (complianceData) {
|
||||
const checks = complianceData.checks || {};
|
||||
const total = Object.keys(checks).length;
|
||||
const impl = Object.values(checks).filter(c => c.compliant).length;
|
||||
return { count: total, implemented: impl, compliance: complianceData.compliance || 0 };
|
||||
}
|
||||
|
||||
// Fallback: just count ADR files (don't read them)
|
||||
const adrPaths = [
|
||||
path.join(CWD, 'v3', 'implementation', 'adrs'),
|
||||
path.join(CWD, 'docs', 'adrs'),
|
||||
path.join(CWD, '.claude-flow', 'adrs'),
|
||||
];
|
||||
|
||||
for (const adrPath of adrPaths) {
|
||||
try {
|
||||
if (fs.existsSync(adrPath)) {
|
||||
const files = fs.readdirSync(adrPath).filter(f =>
|
||||
f.endsWith('.md') && (f.startsWith('ADR-') || f.startsWith('adr-') || /^\d{4}-/.test(f))
|
||||
);
|
||||
const implemented = Math.floor(files.length * 0.7);
|
||||
const compliance = files.length > 0 ? Math.floor((implemented / files.length) * 100) : 0;
|
||||
return { count: files.length, implemented, compliance };
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
return { count: 0, implemented: 0, compliance: 0 };
|
||||
}
|
||||
|
||||
// Hooks status (shared settings cache)
|
||||
function getHooksStatus() {
|
||||
let enabled = 0;
|
||||
const total = 17;
|
||||
const settings = getSettings();
|
||||
|
||||
if (settings && settings.hooks) {
|
||||
for (const category of Object.keys(settings.hooks)) {
|
||||
const h = settings.hooks[category];
|
||||
if (Array.isArray(h) && h.length > 0) enabled++;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const hooksDir = path.join(CWD, '.claude', 'hooks');
|
||||
if (fs.existsSync(hooksDir)) {
|
||||
const hookFiles = fs.readdirSync(hooksDir).filter(f => f.endsWith('.js') || f.endsWith('.sh')).length;
|
||||
enabled = Math.max(enabled, hookFiles);
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
|
||||
return { enabled, total };
|
||||
}
|
||||
|
||||
// AgentDB stats (pure stat calls)
|
||||
function getAgentDBStats() {
|
||||
let vectorCount = 0;
|
||||
let dbSizeKB = 0;
|
||||
let namespaces = 0;
|
||||
let hasHnsw = false;
|
||||
|
||||
const dbFiles = [
|
||||
path.join(CWD, '.swarm', 'memory.db'),
|
||||
path.join(CWD, '.claude-flow', 'memory.db'),
|
||||
path.join(CWD, '.claude', 'memory.db'),
|
||||
path.join(CWD, 'data', 'memory.db'),
|
||||
];
|
||||
|
||||
for (const f of dbFiles) {
|
||||
const stat = safeStat(f);
|
||||
if (stat) {
|
||||
dbSizeKB = stat.size / 1024;
|
||||
vectorCount = Math.floor(dbSizeKB / 2);
|
||||
namespaces = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (vectorCount === 0) {
|
||||
const dbDirs = [
|
||||
path.join(CWD, '.claude-flow', 'agentdb'),
|
||||
path.join(CWD, '.swarm', 'agentdb'),
|
||||
path.join(CWD, '.agentdb'),
|
||||
];
|
||||
for (const dir of dbDirs) {
|
||||
try {
|
||||
if (fs.existsSync(dir) && fs.statSync(dir).isDirectory()) {
|
||||
const files = fs.readdirSync(dir);
|
||||
namespaces = files.filter(f => f.endsWith('.db') || f.endsWith('.sqlite')).length;
|
||||
for (const file of files) {
|
||||
const stat = safeStat(path.join(dir, file));
|
||||
if (stat && stat.isFile()) dbSizeKB += stat.size / 1024;
|
||||
}
|
||||
vectorCount = Math.floor(dbSizeKB / 2);
|
||||
break;
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
const hnswPaths = [
|
||||
path.join(CWD, '.swarm', 'hnsw.index'),
|
||||
path.join(CWD, '.claude-flow', 'hnsw.index'),
|
||||
];
|
||||
for (const p of hnswPaths) {
|
||||
const stat = safeStat(p);
|
||||
if (stat) {
|
||||
hasHnsw = true;
|
||||
vectorCount = Math.max(vectorCount, Math.floor(stat.size / 512));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { vectorCount, dbSizeKB: Math.floor(dbSizeKB), namespaces, hasHnsw };
|
||||
}
|
||||
|
||||
// Test stats (count files only — NO reading file contents)
|
||||
function getTestStats() {
|
||||
let testFiles = 0;
|
||||
|
||||
function countTestFiles(dir, depth) {
|
||||
if (depth === undefined) depth = 0;
|
||||
if (depth > 2) return;
|
||||
try {
|
||||
if (!fs.existsSync(dir)) return;
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
|
||||
countTestFiles(path.join(dir, entry.name), depth + 1);
|
||||
} else if (entry.isFile()) {
|
||||
const n = entry.name;
|
||||
if (n.includes('.test.') || n.includes('.spec.') || n.includes('_test.') || n.includes('_spec.')) {
|
||||
testFiles++;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
var testDirNames = ['tests', 'test', '__tests__', 'v3/__tests__'];
|
||||
for (var i = 0; i < testDirNames.length; i++) {
|
||||
countTestFiles(path.join(CWD, testDirNames[i]));
|
||||
}
|
||||
countTestFiles(path.join(CWD, 'src'));
|
||||
|
||||
return { testFiles, testCases: testFiles * 4 };
|
||||
}
|
||||
|
||||
// Integration status (shared settings + file checks)
|
||||
function getIntegrationStatus() {
|
||||
const mcpServers = { total: 0, enabled: 0 };
|
||||
const settings = getSettings();
|
||||
|
||||
if (settings && settings.mcpServers && typeof settings.mcpServers === 'object') {
|
||||
const servers = Object.keys(settings.mcpServers);
|
||||
mcpServers.total = servers.length;
|
||||
mcpServers.enabled = settings.enabledMcpjsonServers
|
||||
? settings.enabledMcpjsonServers.filter(s => servers.includes(s)).length
|
||||
: servers.length;
|
||||
}
|
||||
|
||||
if (mcpServers.total === 0) {
|
||||
const mcpConfig = readJSON(path.join(CWD, '.mcp.json'))
|
||||
|| readJSON(path.join(os.homedir(), '.claude', 'mcp.json'));
|
||||
if (mcpConfig && mcpConfig.mcpServers) {
|
||||
const s = Object.keys(mcpConfig.mcpServers);
|
||||
mcpServers.total = s.length;
|
||||
mcpServers.enabled = s.length;
|
||||
}
|
||||
}
|
||||
|
||||
const hasDatabase = ['.swarm/memory.db', '.claude-flow/memory.db', 'data/memory.db']
|
||||
.some(p => fs.existsSync(path.join(CWD, p)));
|
||||
const hasApi = !!(process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY);
|
||||
|
||||
return { mcpServers, hasDatabase, hasApi };
|
||||
}
|
||||
|
||||
// Session stats (pure file reads)
|
||||
function getSessionStats() {
|
||||
var sessionPaths = ['.claude-flow/session.json', '.claude/session.json'];
|
||||
for (var i = 0; i < sessionPaths.length; i++) {
|
||||
const data = readJSON(path.join(CWD, sessionPaths[i]));
|
||||
if (data && data.startTime) {
|
||||
const diffMs = Date.now() - new Date(data.startTime).getTime();
|
||||
const mins = Math.floor(diffMs / 60000);
|
||||
const duration = mins < 60 ? mins + 'm' : Math.floor(mins / 60) + 'h' + (mins % 60) + 'm';
|
||||
return { duration: duration };
|
||||
}
|
||||
}
|
||||
return { duration: '' };
|
||||
}
|
||||
|
||||
// ─── Rendering ──────────────────────────────────────────────────
|
||||
|
||||
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) + ']';
|
||||
return '[' + '\u25CF'.repeat(filled) + '\u25CB'.repeat(width - filled) + ']';
|
||||
}
|
||||
|
||||
// Generate full statusline
|
||||
function generateStatusline() {
|
||||
const user = getUserInfo();
|
||||
const git = getGitInfo();
|
||||
// Prefer model name from Claude Code stdin data, fallback to file-based detection
|
||||
const modelName = getModelFromStdin() || getModelName();
|
||||
const ctxInfo = getContextFromStdin();
|
||||
const costInfo = getCostFromStdin();
|
||||
const progress = getV3Progress();
|
||||
const security = getSecurityStatus();
|
||||
const swarm = getSwarmStatus();
|
||||
const system = getSystemMetrics();
|
||||
const adrs = getADRStatus();
|
||||
const hooks = getHooksStatus();
|
||||
const agentdb = getAgentDBStats();
|
||||
const tests = getTestStats();
|
||||
const session = getSessionStats();
|
||||
const integration = getIntegrationStatus();
|
||||
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
|
||||
let header = c.bold + c.brightPurple + '\u258A Claude Flow V3 ' + c.reset;
|
||||
header += (swarm.coordinationActive ? c.brightCyan : c.dim) + '\u25CF ' + c.brightCyan + git.name + c.reset;
|
||||
if (git.gitBranch) {
|
||||
header += ' ' + c.dim + '\u2502' + c.reset + ' ' + c.brightBlue + '\u23C7 ' + git.gitBranch + c.reset;
|
||||
const changes = git.modified + git.staged + git.untracked;
|
||||
if (changes > 0) {
|
||||
let ind = '';
|
||||
if (git.staged > 0) ind += c.brightGreen + '+' + git.staged + c.reset;
|
||||
if (git.modified > 0) ind += c.brightYellow + '~' + git.modified + c.reset;
|
||||
if (git.untracked > 0) ind += c.dim + '?' + git.untracked + c.reset;
|
||||
header += ' ' + ind;
|
||||
}
|
||||
if (git.ahead > 0) header += ' ' + c.brightGreen + '\u2191' + git.ahead + c.reset;
|
||||
if (git.behind > 0) header += ' ' + c.brightRed + '\u2193' + git.behind + c.reset;
|
||||
}
|
||||
header += ' ' + c.dim + '\u2502' + c.reset + ' ' + c.purple + modelName + c.reset;
|
||||
// Show session duration from Claude Code stdin if available, else from local files
|
||||
const duration = costInfo ? costInfo.duration : session.duration;
|
||||
if (duration) header += ' ' + c.dim + '\u2502' + c.reset + ' ' + c.cyan + '\u23F1 ' + duration + c.reset;
|
||||
// Show context usage from Claude Code stdin if available
|
||||
if (ctxInfo && ctxInfo.usedPct > 0) {
|
||||
const ctxColor = ctxInfo.usedPct >= 90 ? c.brightRed : ctxInfo.usedPct >= 70 ? c.brightYellow : c.brightGreen;
|
||||
header += ' ' + c.dim + '\u2502' + c.reset + ' ' + ctxColor + '\u25CF ' + ctxInfo.usedPct + '% ctx' + c.reset;
|
||||
}
|
||||
// Show cost from Claude Code stdin if available
|
||||
if (costInfo && costInfo.costUsd > 0) {
|
||||
header += ' ' + c.dim + '\u2502' + c.reset + ' ' + c.brightYellow + '$' + costInfo.costUsd.toFixed(2) + c.reset;
|
||||
}
|
||||
header += ` ${c.dim}│${c.reset} ${c.purple}${user.modelName}${c.reset}`;
|
||||
lines.push(header);
|
||||
|
||||
// Separator
|
||||
lines.push(`${c.dim}─────────────────────────────────────────────────────${c.reset}`);
|
||||
lines.push(c.dim + '\u2500'.repeat(53) + c.reset);
|
||||
|
||||
// Line 1: DDD Domain Progress
|
||||
// Line 1: DDD Domains
|
||||
const domainsColor = progress.domainsCompleted >= 3 ? c.brightGreen : progress.domainsCompleted > 0 ? c.yellow : c.red;
|
||||
let perfIndicator;
|
||||
if (agentdb.hasHnsw && agentdb.vectorCount > 0) {
|
||||
const speedup = agentdb.vectorCount > 10000 ? '12500x' : agentdb.vectorCount > 1000 ? '150x' : '10x';
|
||||
perfIndicator = c.brightGreen + '\u26A1 HNSW ' + speedup + c.reset;
|
||||
} else if (progress.patternsLearned > 0) {
|
||||
const pk = progress.patternsLearned >= 1000 ? (progress.patternsLearned / 1000).toFixed(1) + 'k' : String(progress.patternsLearned);
|
||||
perfIndicator = c.brightYellow + '\uD83D\uDCDA ' + pk + ' patterns' + c.reset;
|
||||
} else {
|
||||
perfIndicator = c.dim + '\u26A1 target: 150x-12500x' + c.reset;
|
||||
}
|
||||
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}`
|
||||
c.brightCyan + '\uD83C\uDFD7\uFE0F DDD Domains' + c.reset + ' ' + progressBar(progress.domainsCompleted, progress.totalDomains) + ' ' +
|
||||
domainsColor + progress.domainsCompleted + c.reset + '/' + c.brightWhite + progress.totalDomains + c.reset + ' ' + perfIndicator
|
||||
);
|
||||
|
||||
// Line 2: Swarm + CVE + Memory + Context + Intelligence
|
||||
const swarmIndicator = swarm.coordinationActive ? `${c.brightGreen}◉${c.reset}` : `${c.dim}○${c.reset}`;
|
||||
// Line 2: Swarm + Hooks + CVE + Memory + Intelligence
|
||||
const swarmInd = swarm.coordinationActive ? c.brightGreen + '\u25C9' + c.reset : c.dim + '\u25CB' + 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;
|
||||
const secIcon = security.status === 'CLEAN' ? '\uD83D\uDFE2' : security.status === 'IN_PROGRESS' ? '\uD83D\uDFE1' : '\uD83D\uDD34';
|
||||
const secColor = security.status === 'CLEAN' ? c.brightGreen : security.status === 'IN_PROGRESS' ? c.brightYellow : c.brightRed;
|
||||
const hooksColor = hooks.enabled > 0 ? c.brightGreen : c.dim;
|
||||
const intellColor = system.intelligencePct >= 80 ? c.brightGreen : system.intelligencePct >= 40 ? c.brightYellow : c.dim;
|
||||
|
||||
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}`
|
||||
c.brightYellow + '\uD83E\uDD16 Swarm' + c.reset + ' ' + swarmInd + ' [' + agentsColor + String(swarm.activeAgents).padStart(2) + c.reset + '/' + c.brightWhite + swarm.maxAgents + c.reset + '] ' +
|
||||
c.brightPurple + '\uD83D\uDC65 ' + system.subAgents + c.reset + ' ' +
|
||||
c.brightBlue + '\uD83E\uDE9D ' + hooksColor + hooks.enabled + c.reset + '/' + c.brightWhite + hooks.total + c.reset + ' ' +
|
||||
secIcon + ' ' + secColor + 'CVE ' + security.cvesFixed + c.reset + '/' + c.brightWhite + security.totalCves + c.reset + ' ' +
|
||||
c.brightCyan + '\uD83D\uDCBE ' + system.memoryMB + 'MB' + c.reset + ' ' +
|
||||
intellColor + '\uD83E\uDDE0 ' + String(system.intelligencePct).padStart(3) + '%' + c.reset
|
||||
);
|
||||
|
||||
// Line 3: Architecture status
|
||||
// Line 3: Architecture
|
||||
const dddColor = progress.dddProgress >= 50 ? c.brightGreen : progress.dddProgress > 0 ? c.yellow : c.red;
|
||||
const adrColor = adrs.count > 0 ? (adrs.implemented === adrs.count ? c.brightGreen : c.yellow) : c.dim;
|
||||
const adrDisplay = adrs.compliance > 0 ? adrColor + '\u25CF' + adrs.compliance + '%' + c.reset : adrColor + '\u25CF' + adrs.implemented + '/' + adrs.count + c.reset;
|
||||
|
||||
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}`
|
||||
c.brightPurple + '\uD83D\uDD27 Architecture' + c.reset + ' ' +
|
||||
c.cyan + 'ADRs' + c.reset + ' ' + adrDisplay + ' ' + c.dim + '\u2502' + c.reset + ' ' +
|
||||
c.cyan + 'DDD' + c.reset + ' ' + dddColor + '\u25CF' + String(progress.dddProgress).padStart(3) + '%' + c.reset + ' ' + c.dim + '\u2502' + c.reset + ' ' +
|
||||
c.cyan + 'Security' + c.reset + ' ' + secColor + '\u25CF' + security.status + c.reset
|
||||
);
|
||||
|
||||
// Line 4: AgentDB, Tests, Integration
|
||||
const hnswInd = agentdb.hasHnsw ? c.brightGreen + '\u26A1' + c.reset : '';
|
||||
const sizeDisp = agentdb.dbSizeKB >= 1024 ? (agentdb.dbSizeKB / 1024).toFixed(1) + 'MB' : agentdb.dbSizeKB + 'KB';
|
||||
const vectorColor = agentdb.vectorCount > 0 ? c.brightGreen : c.dim;
|
||||
const testColor = tests.testFiles > 0 ? c.brightGreen : c.dim;
|
||||
|
||||
let integStr = '';
|
||||
if (integration.mcpServers.total > 0) {
|
||||
const mcpCol = integration.mcpServers.enabled === integration.mcpServers.total ? c.brightGreen :
|
||||
integration.mcpServers.enabled > 0 ? c.brightYellow : c.red;
|
||||
integStr += c.cyan + 'MCP' + c.reset + ' ' + mcpCol + '\u25CF' + integration.mcpServers.enabled + '/' + integration.mcpServers.total + c.reset;
|
||||
}
|
||||
if (integration.hasDatabase) integStr += (integStr ? ' ' : '') + c.brightGreen + '\u25C6' + c.reset + 'DB';
|
||||
if (integration.hasApi) integStr += (integStr ? ' ' : '') + c.brightGreen + '\u25C6' + c.reset + 'API';
|
||||
if (!integStr) integStr = c.dim + '\u25CF none' + c.reset;
|
||||
|
||||
lines.push(
|
||||
c.brightCyan + '\uD83D\uDCCA AgentDB' + c.reset + ' ' +
|
||||
c.cyan + 'Vectors' + c.reset + ' ' + vectorColor + '\u25CF' + agentdb.vectorCount + hnswInd + c.reset + ' ' + c.dim + '\u2502' + c.reset + ' ' +
|
||||
c.cyan + 'Size' + c.reset + ' ' + c.brightWhite + sizeDisp + c.reset + ' ' + c.dim + '\u2502' + c.reset + ' ' +
|
||||
c.cyan + 'Tests' + c.reset + ' ' + testColor + '\u25CF' + tests.testFiles + c.reset + ' ' + c.dim + '(~' + tests.testCases + ' cases)' + c.reset + ' ' + c.dim + '\u2502' + c.reset + ' ' +
|
||||
integStr
|
||||
);
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
// Generate JSON data
|
||||
// JSON output
|
||||
function generateJSON() {
|
||||
const git = getGitInfo();
|
||||
return {
|
||||
user: getUserInfo(),
|
||||
user: { name: git.name, gitBranch: git.gitBranch, modelName: getModelName() },
|
||||
v3Progress: getV3Progress(),
|
||||
security: getSecurityStatus(),
|
||||
swarm: getSwarmStatus(),
|
||||
system: getSystemMetrics(),
|
||||
performance: {
|
||||
flashAttentionTarget: '2.49x-7.47x',
|
||||
searchImprovement: '150x-12,500x',
|
||||
memoryReduction: '50-75%',
|
||||
},
|
||||
adrs: getADRStatus(),
|
||||
hooks: getHooksStatus(),
|
||||
agentdb: getAgentDBStats(),
|
||||
tests: getTestStats(),
|
||||
git: { modified: git.modified, untracked: git.untracked, staged: git.staged, ahead: git.ahead, behind: git.behind },
|
||||
lastUpdated: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
// Main
|
||||
// ─── Stdin reader (Claude Code pipes session JSON) ──────────────
|
||||
|
||||
// Claude Code sends session JSON via stdin (model, context, cost, etc.)
|
||||
// Read it synchronously so the script works both:
|
||||
// 1. When invoked by Claude Code (stdin has JSON)
|
||||
// 2. When invoked manually from terminal (stdin is empty/tty)
|
||||
let _stdinData = null;
|
||||
function getStdinData() {
|
||||
if (_stdinData !== undefined && _stdinData !== null) return _stdinData;
|
||||
try {
|
||||
// Check if stdin is a TTY (manual run) — skip reading
|
||||
if (process.stdin.isTTY) { _stdinData = null; return null; }
|
||||
// Read stdin synchronously via fd 0
|
||||
const chunks = [];
|
||||
const buf = Buffer.alloc(4096);
|
||||
let bytesRead;
|
||||
try {
|
||||
while ((bytesRead = fs.readSync(0, buf, 0, buf.length, null)) > 0) {
|
||||
chunks.push(buf.slice(0, bytesRead));
|
||||
}
|
||||
} catch { /* EOF or read error */ }
|
||||
const raw = Buffer.concat(chunks).toString('utf-8').trim();
|
||||
if (raw && raw.startsWith('{')) {
|
||||
_stdinData = JSON.parse(raw);
|
||||
} else {
|
||||
_stdinData = null;
|
||||
}
|
||||
} catch {
|
||||
_stdinData = null;
|
||||
}
|
||||
return _stdinData;
|
||||
}
|
||||
|
||||
// Override model detection to prefer stdin data from Claude Code
|
||||
function getModelFromStdin() {
|
||||
const data = getStdinData();
|
||||
if (data && data.model && data.model.display_name) return data.model.display_name;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get context window info from Claude Code session
|
||||
function getContextFromStdin() {
|
||||
const data = getStdinData();
|
||||
if (data && data.context_window) {
|
||||
return {
|
||||
usedPct: Math.floor(data.context_window.used_percentage || 0),
|
||||
remainingPct: Math.floor(data.context_window.remaining_percentage || 100),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get cost info from Claude Code session
|
||||
function getCostFromStdin() {
|
||||
const data = getStdinData();
|
||||
if (data && data.cost) {
|
||||
const durationMs = data.cost.total_duration_ms || 0;
|
||||
const mins = Math.floor(durationMs / 60000);
|
||||
const secs = Math.floor((durationMs % 60000) / 1000);
|
||||
return {
|
||||
costUsd: data.cost.total_cost_usd || 0,
|
||||
duration: mins > 0 ? mins + 'm' + secs + 's' : secs + 's',
|
||||
linesAdded: data.cost.total_lines_added || 0,
|
||||
linesRemoved: data.cost.total_lines_removed || 0,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ─── Main ───────────────────────────────────────────────────────
|
||||
if (process.argv.includes('--json')) {
|
||||
console.log(JSON.stringify(generateJSON(), null, 2));
|
||||
} else if (process.argv.includes('--compact')) {
|
||||
|
||||
@@ -18,7 +18,7 @@ const CONFIG = {
|
||||
showSwarm: true,
|
||||
showHooks: true,
|
||||
showPerformance: true,
|
||||
refreshInterval: 5000,
|
||||
refreshInterval: 30000,
|
||||
maxAgents: 15,
|
||||
topology: 'hierarchical-mesh',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user