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.

283 lines (282 loc) 8.76 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { MetricsQueries } from "../queries/metrics-queries.js"; import { LinearClient } from "../../../integrations/linear/client.js"; import { LinearTaskManager } from "../../tasks/linear-task-manager.js"; import Database from "better-sqlite3"; import path from "path"; import fs from "fs"; import os from "os"; 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 AnalyticsService { metricsQueries; linearClient; taskStore; dbPath; projectPath; updateCallbacks = /* @__PURE__ */ new Set(); constructor(projectPath) { this.projectPath = projectPath || process.cwd(); this.dbPath = path.join(this.projectPath, ".stackmemory", "analytics.db"); this.ensureDirectoryExists(); this.metricsQueries = new MetricsQueries(this.dbPath); this.initializeTaskStore(); if (process.env["LINEAR_API_KEY"]) { this.initializeLinearIntegration(); } } initializeTaskStore() { try { const contextDbPath = path.join( this.projectPath, ".stackmemory", "context.db" ); if (fs.existsSync(contextDbPath)) { const db = new Database(contextDbPath); this.taskStore = new LinearTaskManager(this.projectPath, db); } } catch (error) { console.error("Failed to initialize task store:", error); } } ensureDirectoryExists() { const dir = path.dirname(this.dbPath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } } async initializeLinearIntegration() { try { const configPath = path.join( os.homedir(), ".stackmemory", "linear-config.json" ); if (fs.existsSync(configPath)) { const config = JSON.parse(fs.readFileSync(configPath, "utf-8")); this.linearClient = new LinearClient(config); await this.syncLinearTasks(); } } catch (error) { console.error("Failed to initialize Linear integration:", error); } } async syncLinearTasks() { await this.syncFromTaskStore(); if (this.linearClient) { try { const issues = await this.linearClient.getIssues({ limit: 100 }); for (const issue of issues) { const task = { id: issue.id, title: issue.title, state: this.mapLinearState(issue.state.type), createdAt: new Date(issue.createdAt), completedAt: issue.state.type === "completed" ? new Date(issue.updatedAt) : void 0, estimatedEffort: issue.estimate ? issue.estimate * 60 : void 0, assigneeId: issue.assignee?.id, priority: this.mapLinearPriority(issue.priority), labels: Array.isArray(issue.labels) ? issue.labels.map((l) => l.name) : issue.labels?.nodes?.map((l) => l.name) || [], blockingIssues: [] }; this.metricsQueries.upsertTask(task); } } catch (error) { console.error("Failed to sync from Linear API:", error); } } await this.notifyUpdate(); } async syncFromTaskStore() { if (!this.taskStore) return 0; try { const allTasks = this.getAllTasksFromStore(); let synced = 0; for (const task of allTasks) { const analyticsTask = { id: task.id, title: task.title, state: this.mapTaskStatus(task.status), createdAt: new Date(task.created_at * 1e3), completedAt: task.completed_at ? new Date(task.completed_at * 1e3) : void 0, estimatedEffort: task.estimated_effort, actualEffort: task.actual_effort, assigneeId: task.assignee, priority: task.priority, labels: task.tags || [], blockingIssues: task.depends_on || [] }; this.metricsQueries.upsertTask(analyticsTask); synced++; } return synced; } catch (error) { console.error("Failed to sync from task store:", error); return 0; } } getAllTasksFromStore() { if (!this.taskStore) return []; try { const contextDbPath = path.join( this.projectPath, ".stackmemory", "context.db" ); const db = new Database(contextDbPath); const rows = db.prepare( ` SELECT * FROM task_cache ORDER BY created_at DESC ` ).all(); db.close(); return rows.map((row) => ({ id: row.id, title: row.title, description: row.description, status: row.status, priority: row.priority, created_at: row.created_at, completed_at: row.completed_at, estimated_effort: row.estimated_effort, actual_effort: row.actual_effort, assignee: row.assignee, tags: JSON.parse(row.tags || "[]"), depends_on: JSON.parse(row.depends_on || "[]") })); } catch (error) { console.error("Failed to get all tasks:", error); return []; } } mapTaskStatus(status) { const statusMap = { pending: "todo", in_progress: "in_progress", completed: "completed", blocked: "blocked", cancelled: "blocked" }; return statusMap[status] || "todo"; } mapLinearState(linearState) { const stateMap = { backlog: "todo", unstarted: "todo", started: "in_progress", completed: "completed", done: "completed", canceled: "blocked" }; return stateMap[linearState.toLowerCase()] || "todo"; } mapLinearPriority(priority) { if (priority === 1) return "urgent"; if (priority === 2) return "high"; if (priority === 3) return "medium"; return "low"; } async getDashboardState(query = {}) { const timeRange = query.timeRange || this.getDefaultTimeRange(); const metrics = this.metricsQueries.getTaskMetrics({ ...query, timeRange }); const recentTasks = this.metricsQueries.getRecentTasks({ ...query, limit: 20 }); const teamMetrics = await this.getTeamMetrics(query); return { metrics, teamMetrics, recentTasks, timeRange, teamFilter: query.userIds || [], isLive: this.updateCallbacks.size > 0, lastUpdated: /* @__PURE__ */ new Date() }; } async getTeamMetrics(query) { const uniqueUserIds = /* @__PURE__ */ new Set(); const tasks = this.metricsQueries.getRecentTasks({ limit: 1e3 }); tasks.forEach((task) => { if (task.assigneeId) { uniqueUserIds.add(task.assigneeId); } }); const teamMetrics = []; const totalCompleted = tasks.filter((t) => t.state === "completed").length; for (const userId of uniqueUserIds) { const userQuery = { ...query, userIds: [userId] }; const individualMetrics = this.metricsQueries.getTaskMetrics(userQuery); teamMetrics.push({ userId, userName: await this.getUserName(userId), individualMetrics, contributionPercentage: totalCompleted > 0 ? individualMetrics.completedTasks / totalCompleted * 100 : 0, lastActive: /* @__PURE__ */ new Date() }); } return teamMetrics.sort( (a, b) => b.contributionPercentage - a.contributionPercentage ); } async getUserName(userId) { return userId; } getDefaultTimeRange() { const end = /* @__PURE__ */ new Date(); const start = /* @__PURE__ */ new Date(); start.setDate(start.getDate() - 7); return { start, end, preset: "7d" }; } subscribeToUpdates(callback) { this.updateCallbacks.add(callback); return () => { this.updateCallbacks.delete(callback); }; } async notifyUpdate() { const state = await this.getDashboardState(); this.updateCallbacks.forEach((callback) => callback(state)); } async addTask(task) { this.metricsQueries.upsertTask(task); await this.notifyUpdate(); } async updateTask(taskId, updates) { const tasks = this.metricsQueries.getRecentTasks({ limit: 1 }); const existingTask = tasks.find((t) => t.id === taskId); if (existingTask) { const updatedTask = { ...existingTask, ...updates }; this.metricsQueries.upsertTask(updatedTask); await this.notifyUpdate(); } } close() { this.metricsQueries.close(); } } export { AnalyticsService }; //# sourceMappingURL=analytics-service.js.map