UNPKG

@stackmemoryai/stackmemory

Version:

Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.

418 lines (417 loc) 13.5 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { performance } from "perf_hooks"; import { logger } from "../monitoring/logger.js"; import * as fs from "fs"; import * as path from "path"; import { v4 as uuidv4 } from "uuid"; function getEnv(key, defaultValue) { const value = process.env[key]; if (value === void 0) { if (defaultValue !== void 0) return defaultValue; throw new Error(`Environment variable ${key} is required`); } return value; } function getOptionalEnv(key) { return process.env[key]; } class TraceContext { static instance; config; currentTrace = null; traceStack = []; allTraces = []; outputFile; startTime = Date.now(); sensitivePatterns = [ /api[_-]?key/i, /token/i, /secret/i, /password/i, /bearer/i, /authorization/i, /client[_-]?id/i, /client[_-]?secret/i ]; constructor() { this.config = this.loadConfig(); if (this.config.output === "file" || this.config.output === "both") { this.initializeOutputFile(); } } static getInstance() { if (!TraceContext.instance) { TraceContext.instance = new TraceContext(); } return TraceContext.instance; } loadConfig() { return { enabled: process.env["DEBUG_TRACE"] === "true" || process.env["STACKMEMORY_DEBUG"] === "true", verbosity: process.env["TRACE_VERBOSITY"] || "full", output: process.env["TRACE_OUTPUT"] || "console", includeParams: process.env["TRACE_PARAMS"] !== "false", includeResults: process.env["TRACE_RESULTS"] !== "false", maskSensitive: process.env["TRACE_MASK_SENSITIVE"] !== "false", performanceThreshold: parseInt( process.env["TRACE_PERF_THRESHOLD"] || "100" ), maxDepth: parseInt(process.env["TRACE_MAX_DEPTH"] || "20"), captureMemory: process.env["TRACE_MEMORY"] === "true" }; } initializeOutputFile() { const traceDir = path.join( process.env["HOME"] || ".", ".stackmemory", "traces" ); if (!fs.existsSync(traceDir)) { fs.mkdirSync(traceDir, { recursive: true }); } const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-"); this.outputFile = path.join(traceDir, `trace-${timestamp}.jsonl`); } maskSensitiveData(obj) { if (!this.config.maskSensitive) return obj; if (typeof obj !== "object" || obj === null) return obj; const masked = Array.isArray(obj) ? [...obj] : { ...obj }; for (const key in masked) { if (typeof key === "string") { const isSensitive = this.sensitivePatterns.some( (pattern) => pattern.test(key) ); if (isSensitive) { masked[key] = "[MASKED]"; } else if (typeof masked[key] === "object") { masked[key] = this.maskSensitiveData(masked[key]); } else if (typeof masked[key] === "string" && masked[key].length > 20) { if (/^[a-zA-Z0-9_-]{20,}$/.test(masked[key])) { masked[key] = masked[key].substring(0, 8) + "...[MASKED]"; } } } } return masked; } captureMemory() { if (!this.config.captureMemory) return void 0; return process.memoryUsage(); } formatDuration(ms) { if (ms < 1e3) return `${ms.toFixed(0)}ms`; if (ms < 6e4) return `${(ms / 1e3).toFixed(2)}s`; return `${(ms / 6e4).toFixed(2)}m`; } formatMemory(bytes) { const mb = bytes / 1024 / 1024; return `${mb.toFixed(2)}MB`; } getIndent(depth) { return " ".repeat(depth); } formatTraceEntry(entry, includeChildren = true) { const indent = this.getIndent(entry.depth); const duration = entry.duration ? ` [${this.formatDuration(entry.duration)}]` : ""; const memory = entry.memory?.delta ? ` (\u0394mem: ${this.formatMemory(entry.memory.delta.heapUsed)})` : ""; let output = `${indent}\u2192 [${entry.type.toUpperCase()}:${entry.id.substring(0, 8)}] ${entry.name}${duration}${memory}`; if (entry.error) { output += ` ${indent} \u2717 ERROR: ${entry.error.message || entry.error}`; if (entry.error.stack && this.config.verbosity === "full") { output += ` ${indent} Stack: ${entry.error.stack.split("\n")[1]?.trim()}`; } } if (this.config.includeParams && entry.params && Object.keys(entry.params).length > 0) { const maskedParams = this.maskSensitiveData(entry.params); output += ` ${indent} \u25B8 Params: ${JSON.stringify(maskedParams, null, 2).replace(/\n/g, "\n" + indent + " ")}`; } if (this.config.includeResults && entry.result !== void 0 && !entry.error) { const maskedResult = this.maskSensitiveData(entry.result); const resultStr = JSON.stringify(maskedResult, null, 2); if (resultStr.length < 200) { output += ` ${indent} \u25C2 Result: ${resultStr.replace(/\n/g, "\n" + indent + " ")}`; } else { output += ` ${indent} \u25C2 Result: [${typeof maskedResult}] ${resultStr.substring(0, 100)}...`; } } if (entry.duration && entry.duration > this.config.performanceThreshold) { output += ` ${indent} WARNING: Exceeded ${this.config.performanceThreshold}ms threshold`; } if (includeChildren && entry.children.length > 0) { for (const child of entry.children) { output += "\n" + this.formatTraceEntry(child, true); } } if (entry.endTime && entry.depth > 0) { output += ` ${indent}\u2190 [${entry.type.toUpperCase()}:${entry.id.substring(0, 8)}] completed`; } return output; } outputTrace(entry) { if (!this.config.enabled) return; const formatted = this.formatTraceEntry(entry, false); if (this.config.output === "console" || this.config.output === "both") { console.log(formatted); } if ((this.config.output === "file" || this.config.output === "both") && this.outputFile) { const jsonLine = JSON.stringify({ ...entry, formatted, timestamp: (/* @__PURE__ */ new Date()).toISOString() }) + "\n"; fs.appendFileSync(this.outputFile, jsonLine); } } startTrace(type, name, params, metadata) { if (!this.config.enabled) return ""; const id = uuidv4(); const parentId = this.currentTrace?.id; const depth = this.traceStack.length; if (depth > this.config.maxDepth) { return id; } const entry = { id, parentId, type, name, startTime: performance.now(), depth, params: this.config.includeParams ? params : void 0, metadata, children: [], memory: this.captureMemory() ? { before: this.captureMemory() } : void 0 }; if (this.currentTrace) { this.currentTrace.children.push(entry); } else { this.allTraces.push(entry); } this.traceStack.push(entry); this.currentTrace = entry; this.outputTrace(entry); return id; } endTrace(id, result, error) { if (!this.config.enabled) return; const index = this.traceStack.findIndex((t) => t.id === id); if (index === -1) return; const entry = this.traceStack[index]; entry.endTime = performance.now(); entry.duration = entry.endTime - entry.startTime; entry.result = this.config.includeResults && !error ? result : void 0; entry.error = error; if (entry.memory?.before) { entry.memory.after = this.captureMemory(); if (entry.memory.after) { entry.memory.delta = { rss: entry.memory.after.rss - entry.memory.before.rss, heapUsed: entry.memory.after.heapUsed - entry.memory.before.heapUsed }; } } this.outputTrace(entry); this.traceStack.splice(index); this.currentTrace = this.traceStack[this.traceStack.length - 1] || null; } async traceAsync(type, name, params, fn) { const id = this.startTrace(type, name, params); try { const result = await fn(); this.endTrace(id, result); return result; } catch (error) { this.endTrace(id, void 0, error); throw error; } } traceSync(type, name, params, fn) { const id = this.startTrace(type, name, params); try { const result = fn(); this.endTrace(id, result); return result; } catch (error) { this.endTrace(id, void 0, error); throw error; } } async command(name, options, fn) { return this.traceAsync("command", name, options, fn); } async step(name, fn) { return this.traceAsync("step", name, void 0, fn); } async query(sql, params, fn) { return this.traceAsync("query", sql.substring(0, 50), params, fn); } async api(method, url, body, fn) { return this.traceAsync("api", `${method} ${url}`, { body }, fn); } getExecutionSummary() { if (!this.config.enabled) return "Tracing disabled"; const totalDuration = Date.now() - this.startTime; const errorCount = this.countErrors(this.allTraces); const slowCount = this.countSlowOperations(this.allTraces); let summary = ` ${"=".repeat(80)} `; summary += `EXECUTION SUMMARY `; summary += `${"=".repeat(80)} `; summary += `Total Duration: ${this.formatDuration(totalDuration)} `; summary += `Total Operations: ${this.countOperations(this.allTraces)} `; summary += `Errors: ${errorCount} `; summary += `Slow Operations (>${this.config.performanceThreshold}ms): ${slowCount} `; if (this.config.captureMemory) { const memUsage = process.memoryUsage(); summary += `Final Memory: RSS=${this.formatMemory(memUsage.rss)}, Heap=${this.formatMemory(memUsage.heapUsed)} `; } if (this.outputFile) { summary += `Trace Log: ${this.outputFile} `; } summary += `${"=".repeat(80)}`; return summary; } countOperations(traces) { let count = traces.length; for (const trace2 of traces) { count += this.countOperations(trace2.children); } return count; } countErrors(traces) { let count = 0; for (const trace2 of traces) { if (trace2.error) count++; count += this.countErrors(trace2.children); } return count; } countSlowOperations(traces) { let count = 0; for (const trace2 of traces) { if (trace2.duration && trace2.duration > this.config.performanceThreshold) count++; count += this.countSlowOperations(trace2.children); } return count; } getLastError() { const findLastError = (traces) => { for (let i = traces.length - 1; i >= 0; i--) { const trace2 = traces[i]; if (trace2.error) return trace2; const childError = findLastError(trace2.children); if (childError) return childError; } return null; }; return findLastError(this.allTraces); } exportTraces() { return this.allTraces; } reset() { this.currentTrace = null; this.traceStack = []; this.allTraces = []; this.startTime = Date.now(); } } const trace = TraceContext.getInstance(); function Trace(type = "function") { return function(target, propertyKey, descriptor) { const originalMethod = descriptor.value; const isAsync = originalMethod.constructor.name === "AsyncFunction"; if (isAsync) { descriptor.value = async function(...args) { const className = target.constructor.name; const methodName = `${className}.${propertyKey}`; return trace.traceAsync(type, methodName, args, async () => { return originalMethod.apply(this, args); }); }; } else { descriptor.value = function(...args) { const className = target.constructor.name; const methodName = `${className}.${propertyKey}`; return trace.traceSync(type, methodName, args, () => { return originalMethod.apply(this, args); }); }; } return descriptor; }; } function TraceClass(type = "function") { return function(constructor) { const prototype = constructor.prototype; const propertyNames = Object.getOwnPropertyNames(prototype); for (const propertyName of propertyNames) { if (propertyName === "constructor") continue; const descriptor = Object.getOwnPropertyDescriptor( prototype, propertyName ); if (!descriptor || typeof descriptor.value !== "function") continue; Trace(type)(prototype, propertyName, descriptor); Object.defineProperty(prototype, propertyName, descriptor); } return constructor; }; } function TraceCritical(target, propertyKey, descriptor) { const originalMethod = descriptor.value; descriptor.value = async function(...args) { const className = target.constructor.name; const methodName = `${className}.${propertyKey} [CRITICAL]`; const contextBefore = { memory: process.memoryUsage(), timestamp: (/* @__PURE__ */ new Date()).toISOString(), args: trace["maskSensitiveData"](args) }; try { return await trace.traceAsync( "function", methodName, contextBefore, async () => { return originalMethod.apply(this, args); } ); } catch (error) { logger.error(`Critical operation failed: ${methodName}`, error, { context: contextBefore, stack: error.stack }); throw error; } }; return descriptor; } export { Trace, TraceClass, TraceContext, TraceCritical, trace }; //# sourceMappingURL=debug-trace.js.map