UNPKG

@stackmemoryai/stackmemory

Version:

Lossless, project-scoped memory for AI coding tools. Durable context across sessions with 56 MCP tools, FTS5 search, conductor orchestrator, loop/watch monitoring, snapshot capture, pre-flight overlap checks, Claude/Codex/OpenCode wrappers, Linear sync, a

119 lines (118 loc) 4.19 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { Command } from "commander"; import { existsSync } from "fs"; import { join } from "path"; function createStatsCommand() { const stats = new Command("stats").description("Show telemetry statistics").argument("[category]", "Category to show (edits)", "edits").option("-d, --days <n>", "Number of days to show trend", "7").option("--json", "Output as JSON", false).action(async (category, options) => { if (category !== "edits") { console.log(`Unknown stats category: ${category}`); console.log("Available: edits"); return; } const projectRoot = process.cwd(); const dbPath = join(projectRoot, ".stackmemory", "context.db"); if (!existsSync(dbPath)) { console.log('No data. Run "stackmemory init" first.'); return; } const { default: Database } = await import("better-sqlite3"); const db = new Database(dbPath); const tableExists = db.prepare( "SELECT name FROM sqlite_master WHERE type='table' AND name='edit_telemetry'" ).get(); if (!tableExists) { db.close(); console.log("No edit telemetry data yet."); console.log("Edit telemetry is collected via the PostToolUse hook."); return; } const days = parseInt(options.days, 10) || 7; const cutoff = Math.floor(Date.now() / 1e3) - days * 86400; const byTool = db.prepare( `SELECT tool_name, COUNT(*) as total, SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as successes, SUM(CASE WHEN success = 0 THEN 1 ELSE 0 END) as failures FROM edit_telemetry WHERE timestamp >= ? GROUP BY tool_name ORDER BY total DESC` ).all(cutoff); const topFails = db.prepare( `SELECT file_path, COUNT(*) as fail_count FROM edit_telemetry WHERE success = 0 AND timestamp >= ? AND file_path IS NOT NULL GROUP BY file_path ORDER BY fail_count DESC LIMIT 10` ).all(cutoff); const errorTypes = db.prepare( `SELECT error_type, COUNT(*) as count FROM edit_telemetry WHERE success = 0 AND timestamp >= ? AND error_type IS NOT NULL GROUP BY error_type ORDER BY count DESC` ).all(cutoff); const trend = db.prepare( `SELECT date(timestamp, 'unixepoch') as day, COUNT(*) as total, SUM(CASE WHEN success = 0 THEN 1 ELSE 0 END) as failures FROM edit_telemetry WHERE timestamp >= ? GROUP BY day ORDER BY day DESC` ).all(cutoff); db.close(); if (options.json) { console.log( JSON.stringify({ byTool, topFails, errorTypes, trend }, null, 2) ); return; } console.log(` Edit Telemetry (last ${days} days)`); console.log("\u2500".repeat(50)); if (byTool.length === 0) { console.log("No edit telemetry data in this period."); return; } console.log("\nSuccess Rate by Tool:"); for (const row of byTool) { const rate = row.total > 0 ? Math.round(row.successes / row.total * 100) : 0; console.log( ` ${row.tool_name.padEnd(20)} ${rate}% (${row.successes}/${row.total})` ); } if (topFails.length > 0) { console.log("\nTop Failure Files:"); for (const row of topFails) { console.log(` ${row.file_path} (${row.fail_count}x)`); } } if (errorTypes.length > 0) { console.log("\nError Types:"); for (const row of errorTypes) { console.log(` ${row.error_type.padEnd(30)} ${row.count}x`); } } if (trend.length > 0) { console.log("\nDaily Trend:"); for (const row of trend) { const failRate = row.total > 0 ? Math.round(row.failures / row.total * 100) : 0; console.log( ` ${row.day} ${row.total} edits, ${row.failures} failures (${failRate}%)` ); } } console.log(""); }); return stats; } export { createStatsCommand };