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.

288 lines (287 loc) 8.33 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import express from "express"; import { AnalyticsService } from "../core/analytics-service.js"; import { WebSocketServer, WebSocket } from "ws"; class AnalyticsAPI { router; analyticsService; wss; constructor(projectPath) { this.router = express.Router(); this.analyticsService = new AnalyticsService(projectPath); this.setupRoutes(); } setupRoutes() { this.router.use(express.json()); this.router.get("/metrics", this.getMetrics.bind(this)); this.router.get("/tasks", this.getTasks.bind(this)); this.router.get("/team/:userId", this.getTeamMetrics.bind(this)); this.router.post("/tasks", this.addTask.bind(this)); this.router.put("/tasks/:taskId", this.updateTask.bind(this)); this.router.post("/sync", this.syncLinear.bind(this)); this.router.get("/export", this.exportMetrics.bind(this)); } async getMetrics(req, res) { try { const query = this.parseQuery(req.query); const dashboardState = await this.analyticsService.getDashboardState(query); res.json({ success: true, data: { metrics: dashboardState.metrics, lastUpdated: dashboardState.lastUpdated } }); } catch (error) { this.handleError(res, error); } } async getTasks(req, res) { try { const query = this.parseQuery(req.query); const dashboardState = await this.analyticsService.getDashboardState(query); res.json({ success: true, data: { tasks: dashboardState.recentTasks, total: dashboardState.metrics.totalTasks } }); } catch (error) { this.handleError(res, error); } } async getTeamMetrics(req, res) { try { const { userId } = req.params; const query = this.parseQuery(req.query); if (userId === "all") { const dashboardState = await this.analyticsService.getDashboardState(query); res.json({ success: true, data: dashboardState.teamMetrics }); } else { const dashboardState = await this.analyticsService.getDashboardState({ ...query, userIds: [userId] }); const userMetrics = dashboardState.teamMetrics.find( (m) => m.userId === userId ); if (!userMetrics) { res.status(404).json({ success: false, error: "User metrics not found" }); return; } res.json({ success: true, data: userMetrics }); } } catch (error) { this.handleError(res, error); } } async addTask(req, res) { try { const task = { ...req.body, createdAt: new Date(req.body.createdAt || Date.now()), completedAt: req.body.completedAt ? new Date(req.body.completedAt) : void 0 }; await this.analyticsService.addTask(task); res.status(201).json({ success: true, message: "Task added successfully" }); } catch (error) { this.handleError(res, error); } } async updateTask(req, res) { try { const { taskId } = req.params; const updates = req.body; if (updates.completedAt) { updates.completedAt = new Date(updates.completedAt); } await this.analyticsService.updateTask(taskId, updates); res.json({ success: true, message: "Task updated successfully" }); } catch (error) { this.handleError(res, error); } } async syncLinear(req, res) { try { await this.analyticsService.syncLinearTasks(); res.json({ success: true, message: "Linear tasks synced successfully" }); } catch (error) { this.handleError(res, error); } } async exportMetrics(req, res) { try { const query = this.parseQuery(req.query); const format = req.query.format || "json"; const dashboardState = await this.analyticsService.getDashboardState(query); if (format === "csv") { const csv = this.convertToCSV(dashboardState); res.setHeader("Content-Type", "text/csv"); res.setHeader( "Content-Disposition", 'attachment; filename="analytics-export.csv"' ); res.send(csv); } else { res.json({ success: true, data: dashboardState }); } } catch (error) { this.handleError(res, error); } } parseQuery(query) { const result = {}; if (query.start && query.end) { result.timeRange = { start: new Date(query.start), end: new Date(query.end), preset: query.preset }; } else if (query.preset) { result.timeRange = this.getPresetTimeRange(query.preset); } if (query.users) { result.userIds = Array.isArray(query.users) ? query.users : [query.users]; } if (query.states) { result.states = Array.isArray(query.states) ? query.states : [query.states]; } if (query.priorities) { result.priorities = Array.isArray(query.priorities) ? query.priorities : [query.priorities]; } if (query.labels) { result.labels = Array.isArray(query.labels) ? query.labels : [query.labels]; } if (query.limit) { result.limit = parseInt(query.limit); } if (query.offset) { result.offset = parseInt(query.offset); } return result; } getPresetTimeRange(preset) { const end = /* @__PURE__ */ new Date(); const start = /* @__PURE__ */ new Date(); switch (preset) { case "today": start.setHours(0, 0, 0, 0); break; case "7d": start.setDate(start.getDate() - 7); break; case "30d": start.setDate(start.getDate() - 30); break; case "90d": start.setDate(start.getDate() - 90); break; default: start.setDate(start.getDate() - 7); } return { start, end, preset }; } convertToCSV(dashboardState) { const tasks = dashboardState.recentTasks; if (!tasks || tasks.length === 0) return "No data"; const headers = Object.keys(tasks[0]).join(","); const rows = tasks.map( (task) => Object.values(task).map((v) => typeof v === "object" ? JSON.stringify(v) : v).join(",") ); return [headers, ...rows].join("\n"); } handleError(res, error) { console.error("Analytics API error:", error); res.status(500).json({ success: false, error: error.message || "Internal server error" }); } setupWebSocket(server) { this.wss = new WebSocketServer({ server, path: "/ws/analytics" }); this.wss.on("connection", (ws) => { console.log("WebSocket client connected to analytics"); const unsubscribe = this.analyticsService.subscribeToUpdates((state) => { if (ws.readyState === WebSocket.OPEN) { ws.send( JSON.stringify({ type: "update", data: state }) ); } }); ws.on("message", async (message) => { try { const data = JSON.parse(message); if (data.type === "subscribe") { const query = this.parseQuery(data.query || {}); const state = await this.analyticsService.getDashboardState(query); ws.send( JSON.stringify({ type: "initial", data: state }) ); } } catch (error) { ws.send( JSON.stringify({ type: "error", error: "Invalid message format" }) ); } }); ws.on("close", () => { unsubscribe(); console.log("WebSocket client disconnected"); }); ws.on("error", (error) => { console.error("WebSocket error:", error); unsubscribe(); }); }); } getRouter() { return this.router; } close() { this.analyticsService.close(); if (this.wss) { this.wss.close(); } } } export { AnalyticsAPI }; //# sourceMappingURL=analytics-api.js.map