@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.
223 lines (222 loc) • 6.21 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 { execSync } from "child_process";
import { pickNextLinearTask } from "./linear-task-picker.js";
function formatDuration(ms) {
const seconds = Math.floor(ms / 1e3);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
if (hours > 0) {
return `${hours}h ${minutes % 60}min`;
}
if (minutes > 0) {
return `${minutes}min`;
}
return `${seconds}s`;
}
function getCurrentBranch() {
try {
return execSync("git rev-parse --abbrev-ref HEAD", {
encoding: "utf8",
stdio: ["pipe", "pipe", "pipe"]
}).trim();
} catch {
return "unknown";
}
}
function hasUncommittedChanges() {
try {
const status = execSync("git status --porcelain", {
encoding: "utf8",
stdio: ["pipe", "pipe", "pipe"]
});
const lines = status.trim().split("\n").filter(Boolean);
return { changed: lines.length > 0, count: lines.length };
} catch {
return { changed: false, count: 0 };
}
}
function isInWorktree() {
try {
execSync("git rev-parse --is-inside-work-tree", {
encoding: "utf8",
stdio: ["pipe", "pipe", "pipe"]
});
const gitDir = execSync("git rev-parse --git-dir", {
encoding: "utf8",
stdio: ["pipe", "pipe", "pipe"]
}).trim();
return gitDir.includes(".git/worktrees/");
} catch {
return false;
}
}
function hasTestScript() {
try {
const packageJson = execSync("cat package.json", {
encoding: "utf8",
stdio: ["pipe", "pipe", "pipe"]
});
const pkg = JSON.parse(packageJson);
return !!(pkg.scripts?.test || pkg.scripts?.["test:run"]);
} catch {
return false;
}
}
async function generateSuggestions(context) {
const suggestions = [];
let keyIndex = 1;
const changes = hasUncommittedChanges();
const inWorktree = isInWorktree();
const hasTests = hasTestScript();
if (context.exitCode !== 0 && context.exitCode !== null) {
suggestions.push({
key: String(keyIndex++),
label: "Review error logs",
action: "cat ~/.claude/logs/claude-*.log | tail -50",
priority: 100
});
}
if (changes.changed) {
suggestions.push({
key: String(keyIndex++),
label: `Commit changes (${changes.count} files)`,
action: "git add -A && git commit",
priority: 90
});
const branch = getCurrentBranch();
if (branch !== "main" && branch !== "master" && branch !== "unknown") {
suggestions.push({
key: String(keyIndex++),
label: "Create PR",
action: "gh pr create --fill",
priority: 80
});
}
}
if (hasTests && changes.changed) {
suggestions.push({
key: String(keyIndex++),
label: "Run tests",
action: "npm run test:run",
priority: 85
});
}
if (inWorktree) {
suggestions.push({
key: String(keyIndex++),
label: "Merge to main",
action: "cwm",
// custom alias
priority: 70
});
}
try {
const linearTask = await pickNextLinearTask({ preferTestTasks: true });
if (linearTask) {
suggestions.push({
key: String(keyIndex++),
label: `Start: ${linearTask.identifier} - ${linearTask.title.substring(0, 40)}${linearTask.title.length > 40 ? "..." : ""}${linearTask.hasTestRequirements ? " (has tests)" : ""}`,
action: `stackmemory task start ${linearTask.id} --assign-me`,
priority: 60
});
}
} catch {
}
const durationMs = Date.now() - context.sessionStartTime;
if (durationMs > 30 * 60 * 1e3) {
suggestions.push({
key: String(keyIndex++),
label: "Take a break",
action: 'echo "Great work! Time for a coffee break."',
priority: 10
});
}
suggestions.sort((a, b) => b.priority - a.priority);
if (suggestions.length < 2) {
if (suggestions.length === 0) {
suggestions.push({
key: "1",
label: "Start new Claude session",
action: "claude-sm",
priority: 50
});
}
if (suggestions.length < 2) {
suggestions.push({
key: "2",
label: "View session logs",
action: "cat ~/.claude/logs/claude-*.log | tail -30",
priority: 40
});
}
}
suggestions.forEach((s, i) => {
s.key = String(i + 1);
});
return suggestions;
}
async function generateSessionSummary(context) {
const durationMs = Date.now() - context.sessionStartTime;
const duration = formatDuration(durationMs);
const branch = context.branch || getCurrentBranch();
let status = "success";
if (context.exitCode !== 0 && context.exitCode !== null) {
status = "error";
}
const suggestions = await generateSuggestions(context);
let linearTask;
try {
linearTask = await pickNextLinearTask({ preferTestTasks: true });
} catch {
}
return {
duration,
exitCode: context.exitCode,
branch,
status,
suggestions,
linearTask
};
}
function formatSummaryMessage(summary, sessionId) {
const statusEmoji = summary.status === "success" ? "" : "";
const exitInfo = summary.exitCode !== null ? ` | Exit: ${summary.exitCode}` : "";
const sessionInfo = sessionId ? ` | Session: ${sessionId}` : "";
let message = `Claude session complete ${statusEmoji}
`;
message += `Duration: ${summary.duration}${exitInfo}${sessionInfo}
`;
message += `Branch: ${summary.branch}
`;
if (sessionId) {
message += `View: https://claude.ai/chat/${sessionId}
`;
}
message += "\n";
if (summary.suggestions.length > 0) {
message += `What to do next:
`;
for (const s of summary.suggestions.slice(0, 4)) {
message += `${s.key}. ${s.label}
`;
}
message += `
Reply with number or custom action`;
} else {
message += `No pending actions. Nice work!`;
}
return message;
}
function getActionForKey(suggestions, key) {
const suggestion = suggestions.find((s) => s.key === key);
return suggestion?.action || null;
}
export {
formatSummaryMessage,
generateSessionSummary,
getActionForKey
};
//# sourceMappingURL=session-summary.js.map