@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
194 lines (193 loc) • 7.15 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 express from "express";
import { createServer } from "http";
import { Server as SocketServer } from "socket.io";
import cors from "cors";
import { LinearTaskReader } from "../../tui/services/linear-task-reader.js";
import { SessionManager } from "../../../core/session/session-manager.js";
import { FrameManager } from "../../../core/context/index.js";
import Database from "better-sqlite3";
import { existsSync } from "fs";
import { join } from "path";
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];
}
const app = express();
const httpServer = createServer(app);
const io = new SocketServer(httpServer, {
cors: {
origin: process.env["CLIENT_URL"] || "http://localhost:3000",
methods: ["GET", "POST"]
}
});
app.use(cors());
app.use(express.json());
let taskReader;
let sessionManager;
let frameManager = null;
let db = null;
function initializeServices() {
console.log("\u{1F680} Initializing StackMemory Web Server...");
taskReader = new LinearTaskReader(process.cwd());
console.log(
`\u{1F4CB} TaskReader initialized with ${taskReader.getTasks().length} tasks`
);
sessionManager = new SessionManager({ enableMonitoring: true });
console.log("\u{1F4CA} SessionManager initialized");
const dbPath = join(process.cwd(), ".stackmemory", "context.db");
if (existsSync(dbPath)) {
try {
db = new Database(dbPath);
frameManager = new FrameManager(db, "web");
console.log("\u{1F4BE} Database and FrameManager initialized");
} catch (error) {
console.error("\u274C Failed to initialize database:", error);
}
}
}
app.get("/api/health", (req, res) => {
res.json({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
});
app.get("/api/tasks", (req, res) => {
try {
const tasks = taskReader.getTasks();
res.json({ tasks, total: tasks.length });
} catch {
res.status(500).json({ error: "Failed to fetch tasks" });
}
});
app.get("/api/tasks/active", (req, res) => {
try {
const tasks = taskReader.getActiveTasks();
res.json({ tasks, total: tasks.length });
} catch {
res.status(500).json({ error: "Failed to fetch active tasks" });
}
});
app.get("/api/tasks/by-state/:state", (req, res) => {
try {
const tasks = taskReader.getTasksByState(req.params.state);
res.json({ tasks, total: tasks.length });
} catch {
res.status(500).json({ error: "Failed to fetch tasks by state" });
}
});
app.get("/api/sessions", (req, res) => {
try {
const sessions = sessionManager?.getActiveSessions ? sessionManager.getActiveSessions() : [];
res.json({ sessions, total: sessions.length });
} catch {
res.status(500).json({ error: "Failed to fetch sessions" });
}
});
app.get("/api/frames", (req, res) => {
try {
if (!frameManager) {
res.json({ frames: [], total: 0 });
return;
}
const frames = frameManager.getAllFrames();
res.json({ frames, total: frames.length });
} catch {
res.status(500).json({ error: "Failed to fetch frames" });
}
});
app.get("/api/analytics", (req, res) => {
try {
const tasks = taskReader.getTasks();
const sessions = sessionManager?.getActiveSessions ? sessionManager.getActiveSessions() : [];
const frames = frameManager?.getAllFrames() || [];
const analytics = {
summary: {
totalTasks: tasks.length,
activeTasks: tasks.filter((t) => t.state === "In Progress").length,
completedTasks: tasks.filter((t) => t.state === "Done").length,
totalSessions: sessions.length,
totalFrames: frames.length
},
tasksByState: tasks.reduce(
(acc, task) => {
acc[task.state] = (acc[task.state] || 0) + 1;
return acc;
},
{}
),
tasksByPriority: tasks.reduce(
(acc, task) => {
const priority = task.priority || 4;
acc[priority] = (acc[priority] || 0) + 1;
return acc;
},
{}
),
recentActivity: {
tasksUpdatedToday: tasks.filter((t) => {
const updated = new Date(t.updatedAt);
const today = /* @__PURE__ */ new Date();
return updated.toDateString() === today.toDateString();
}).length,
sessionsToday: sessions.filter((s) => {
const started = new Date(s.startTime);
const today = /* @__PURE__ */ new Date();
return started.toDateString() === today.toDateString();
}).length
}
};
res.json(analytics);
} catch {
res.status(500).json({ error: "Failed to fetch analytics" });
}
});
io.on("connection", (socket) => {
console.log("\u{1F464} Client connected:", socket.id);
socket.emit("initial-data", {
tasks: taskReader.getTasks(),
sessions: sessionManager?.getActiveSessions ? sessionManager.getActiveSessions() : [],
frames: frameManager?.getAllFrames() || []
});
socket.on("refresh-tasks", () => {
socket.emit("tasks:update", taskReader.getTasks());
});
socket.on("refresh-sessions", () => {
const sessions = sessionManager?.getActiveSessions ? sessionManager.getActiveSessions() : [];
socket.emit("sessions:update", sessions);
});
socket.on("refresh-frames", () => {
socket.emit("frames:update", frameManager?.getAllFrames() || []);
});
socket.on("disconnect", () => {
console.log("\u{1F44B} Client disconnected:", socket.id);
});
});
setInterval(() => {
try {
io.emit("tasks:update", taskReader.getTasks());
const sessions = sessionManager?.getActiveSessions ? sessionManager.getActiveSessions() : [];
io.emit("sessions:update", sessions);
io.emit("frames:update", frameManager?.getAllFrames() || []);
} catch (error) {
console.error("Error in periodic update:", error);
}
}, 5e3);
const PORT = process.env["WS_PORT"] || 8080;
initializeServices();
httpServer.listen(PORT, () => {
console.log(`
\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
\u2551 StackMemory Web Dashboard Server \u2551
\u2551 Running on http://localhost:${PORT} \u2551
\u2551 WebSocket: ws://localhost:${PORT} \u2551
\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
`);
});