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

178 lines (177 loc) 6.66 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { trace } from "./debug-trace.js"; import { logger } from "../monitoring/logger.js"; function TraceLinearAPI(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}`; const context = extractAPIContext(propertyKey, args); return trace.traceAsync("api", methodName, context, async () => { const startTime = Date.now(); try { logger.debug(`Linear API Call: ${methodName}`, context); const result = await originalMethod.apply(this, args); const duration = Date.now() - startTime; logger.info(`Linear API Success: ${methodName}`, { duration, resultType: Array.isArray(result) ? `array[${result.length}]` : typeof result, hasData: result != null }); if (duration > 1e3) { logger.warn( `Slow Linear API call: ${methodName} took ${duration}ms`, { ...context, duration } ); } return result; } catch (error) { const duration = Date.now() - startTime; logger.error(`Linear API Failed: ${methodName}`, error, { ...context, duration, errorCode: error.code, statusCode: error.statusCode, graphQLErrors: error.errors }); if (error.message?.includes("rate limit")) { logger.warn("Rate limit hit - consider implementing backoff", { method: methodName, suggestion: "Implement exponential backoff or request queuing" }); } else if (error.message?.includes("network")) { logger.warn("Network error - check connectivity", { method: methodName, suggestion: "Verify API endpoint and network connectivity" }); } else if (error.message?.includes("unauthorized")) { logger.warn("Authorization error - check API key", { method: methodName, suggestion: "Verify LINEAR_API_KEY is set and valid" }); } throw error; } }); }; } else { descriptor.value = function(...args) { const className = target.constructor.name; const methodName = `${className}.${propertyKey}`; const context = extractAPIContext(propertyKey, args); return trace.traceSync("api", methodName, context, () => { return originalMethod.apply(this, args); }); }; } return descriptor; } function extractAPIContext(methodName, args) { const context = {}; if (methodName === "createIssue" && args[0]) { context.title = args[0].title; context.teamId = args[0].teamId; context.priority = args[0].priority; } else if (methodName === "updateIssue" && args[0]) { context.issueId = args[0]; context.updates = Object.keys(args[1] || {}); } else if (methodName === "getIssue") { context.issueId = args[0]; } else if (methodName === "getIssues" && args[0]) { context.filter = args[0]; } else if (methodName === "graphql") { const query = args[0]; if (query) { const match = query.match(/(?:query|mutation)\s+(\w+)/); context.operation = match ? match[1] : "unknown"; context.queryLength = query.length; context.variables = args[1] ? Object.keys(args[1]) : []; } } return context; } function createTracedFetch(baseFetch = fetch) { return async function tracedFetch(input, init) { const url = typeof input === "string" ? input : input.toString(); const method = init?.method || "GET"; const headers = init?.headers ? { ...init.headers } : {}; if (headers.Authorization) { headers.Authorization = headers.Authorization.substring(0, 20) + "...[MASKED]"; } const context = { method, url: url.length > 100 ? url.substring(0, 100) + "..." : url, headers: Object.keys(headers), bodySize: init?.body ? JSON.stringify(init.body).length : 0 }; return trace.api(method, url, context, async () => { const startTime = Date.now(); try { const response = await baseFetch(input, init); const duration = Date.now() - startTime; logger.debug(`HTTP ${method} ${response.status}`, { url: url.substring(0, 100), status: response.status, duration, headers: { "content-type": response.headers.get("content-type"), "x-ratelimit-remaining": response.headers.get( "x-ratelimit-remaining" ), "x-ratelimit-reset": response.headers.get("x-ratelimit-reset") } }); const remaining = response.headers.get("x-ratelimit-remaining"); if (remaining && parseInt(remaining) < 10) { logger.warn(`Low rate limit remaining: ${remaining}`, { url: url.substring(0, 100), resetAt: response.headers.get("x-ratelimit-reset") }); } if (duration > 2e3) { logger.warn(`Slow HTTP response: ${duration}ms`, { method, url: url.substring(0, 100), status: response.status }); } return response; } catch (error) { const duration = Date.now() - startTime; logger.error(`HTTP ${method} failed`, error, { url: url.substring(0, 100), duration, errorType: error.constructor.name, errno: error.errno, code: error.code }); throw error; } }); }; } function wrapGraphQLClient(client) { const prototype = Object.getPrototypeOf(client); 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; TraceLinearAPI(prototype, propertyName, descriptor); Object.defineProperty(prototype, propertyName, descriptor); } return client; } export { TraceLinearAPI, createTracedFetch, wrapGraphQLClient };