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.

223 lines (222 loc) 6.21 kB
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