@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
JavaScript
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