@keepgoingdev/mcp-server 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -339,7 +339,8 @@ function generateEnrichedBriefing(opts) {
339
339
  timestamp: s.timestamp,
340
340
  summary: s.summary || "",
341
341
  nextStep: s.nextStep || "",
342
- branch: s.gitBranch
342
+ branch: s.gitBranch,
343
+ sessionPhase: s.sessionPhase
343
344
  }));
344
345
  }
345
346
  if (opts.recentCommits && opts.recentCommits.length > 0) {
@@ -372,10 +373,13 @@ function buildCurrentFocus(lastSession, projectState, gitBranch) {
372
373
  function buildRecentActivity(lastSession, recentSessions, recentCommitMessages) {
373
374
  const parts = [];
374
375
  const sessionCount = recentSessions.length;
376
+ const planCount = recentSessions.filter((s) => s.sessionPhase === "planning").length;
375
377
  if (sessionCount > 1) {
376
- parts.push(`${sessionCount} recent sessions`);
378
+ const planSuffix = planCount > 0 ? ` (${planCount} plan-only)` : "";
379
+ parts.push(`${sessionCount} recent sessions${planSuffix}`);
377
380
  } else if (sessionCount === 1) {
378
- parts.push("1 recent session");
381
+ const planSuffix = planCount === 1 ? " (plan-only)" : "";
382
+ parts.push(`1 recent session${planSuffix}`);
379
383
  }
380
384
  if (lastSession.summary) {
381
385
  const brief = lastSession.summary.length > 120 ? lastSession.summary.slice(0, 117) + "..." : lastSession.summary;
@@ -574,7 +578,8 @@ function formatEnrichedBriefing(briefing) {
574
578
  for (const s of briefing.sessionHistory) {
575
579
  const relTime = formatRelativeTime(s.timestamp);
576
580
  const branch = s.branch ? ` (${s.branch})` : "";
577
- lines.push(`- **${relTime}${branch}:** ${s.summary || "No summary"}. Next: ${s.nextStep || "Not specified"}`);
581
+ const planTag = s.sessionPhase === "planning" ? " (plan)" : "";
582
+ lines.push(`- **${relTime}${branch}${planTag}:** ${s.summary || "No summary"}. Next: ${s.nextStep || "Not specified"}`);
578
583
  }
579
584
  }
580
585
  if (briefing.recentCommits && briefing.recentCommits.length > 0) {
@@ -2099,6 +2104,15 @@ var POST_TOOL_USE_HOOK = {
2099
2104
  }
2100
2105
  ]
2101
2106
  };
2107
+ var PLAN_MODE_HOOK = {
2108
+ matcher: "Read|Grep|Glob|Bash|WebSearch",
2109
+ hooks: [
2110
+ {
2111
+ type: "command",
2112
+ command: "npx -y @keepgoingdev/mcp-server --heartbeat"
2113
+ }
2114
+ ]
2115
+ };
2102
2116
  var SESSION_END_HOOK = {
2103
2117
  matcher: "",
2104
2118
  hooks: [
@@ -2108,7 +2122,7 @@ var SESSION_END_HOOK = {
2108
2122
  }
2109
2123
  ]
2110
2124
  };
2111
- var KEEPGOING_RULES_VERSION = 2;
2125
+ var KEEPGOING_RULES_VERSION = 3;
2112
2126
  var KEEPGOING_RULES_CONTENT = `<!-- @keepgoingdev/mcp-server v${KEEPGOING_RULES_VERSION} -->
2113
2127
  ## KeepGoing
2114
2128
 
@@ -2118,6 +2132,8 @@ After completing a task or meaningful piece of work, call the \`save_checkpoint\
2118
2132
  - \`summary\`: 1-2 sentences. What changed and why, no file paths, no implementation details (those are captured from git).
2119
2133
  - \`nextStep\`: What to do next
2120
2134
  - \`blocker\`: Any blocker (if applicable)
2135
+
2136
+ When working in plan mode (investigating, designing, iterating on an approach before any edits), call \`save_checkpoint\` when you reach a significant milestone or conclusion. Use the summary to capture what was investigated and decided. This preserves planning context for future sessions.
2121
2137
  `;
2122
2138
  function getRulesFileVersion(content) {
2123
2139
  const match = content.match(/<!-- @keepgoingdev\/mcp-server v(\d+) -->/);
@@ -2178,6 +2194,13 @@ function writeHooksToSettings(settings) {
2178
2194
  settings.hooks.PostToolUse.push(POST_TOOL_USE_HOOK);
2179
2195
  changed = true;
2180
2196
  }
2197
+ const hasHeartbeat = settings.hooks.PostToolUse.some(
2198
+ (entry) => entry?.hooks?.some((h) => typeof h?.command === "string" && h.command.includes("--heartbeat"))
2199
+ );
2200
+ if (!hasHeartbeat) {
2201
+ settings.hooks.PostToolUse.push(PLAN_MODE_HOOK);
2202
+ changed = true;
2203
+ }
2181
2204
  if (!Array.isArray(settings.hooks.SessionEnd)) {
2182
2205
  settings.hooks.SessionEnd = [];
2183
2206
  }
@@ -2741,7 +2764,11 @@ function registerSaveCheckpoint(server, reader, workspacePath) {
2741
2764
  const touchedFiles = getTouchedFiles(workspacePath);
2742
2765
  const commitHashes = getCommitsSince(workspacePath, lastSession?.timestamp);
2743
2766
  const projectName = path10.basename(resolveStorageRoot(workspacePath));
2767
+ const writer = new KeepGoingWriter(workspacePath);
2744
2768
  const sessionId = generateSessionId({ workspaceRoot: workspacePath, branch: gitBranch ?? void 0, worktreePath: workspacePath });
2769
+ const existingTasks = writer.readCurrentTasks();
2770
+ const existingSession = existingTasks.find((t) => t.sessionId === sessionId);
2771
+ const sessionPhase = existingSession?.sessionPhase;
2745
2772
  const checkpoint = createCheckpoint({
2746
2773
  summary,
2747
2774
  nextStep: nextStep || "",
@@ -2751,10 +2778,19 @@ function registerSaveCheckpoint(server, reader, workspacePath) {
2751
2778
  commitHashes,
2752
2779
  workspaceRoot: workspacePath,
2753
2780
  source: "manual",
2754
- sessionId
2781
+ sessionId,
2782
+ ...sessionPhase ? { sessionPhase } : {},
2783
+ ...sessionPhase === "planning" ? { tags: ["plan"] } : {}
2755
2784
  });
2756
- const writer = new KeepGoingWriter(workspacePath);
2757
2785
  writer.saveCheckpoint(checkpoint, projectName);
2786
+ writer.upsertSession({
2787
+ sessionId,
2788
+ sessionActive: true,
2789
+ branch: gitBranch ?? void 0,
2790
+ updatedAt: checkpoint.timestamp,
2791
+ taskSummary: summary,
2792
+ nextStep: nextStep || void 0
2793
+ });
2758
2794
  const lines = [
2759
2795
  `Checkpoint saved.`,
2760
2796
  `- **ID:** ${checkpoint.id}`,
@@ -2907,8 +2943,9 @@ function registerGetCurrentTask(server, reader) {
2907
2943
  lines.push("");
2908
2944
  }
2909
2945
  for (const task of [...activeTasks, ...finishedTasks]) {
2946
+ const phaseLabel = task.sessionPhase === "planning" ? "Planning" : void 0;
2910
2947
  const statusIcon = task.sessionActive ? "\u{1F7E2}" : "\u2705";
2911
- const statusLabel = task.sessionActive ? "Active" : "Finished";
2948
+ const statusLabel = task.sessionActive ? phaseLabel || "Active" : "Finished";
2912
2949
  const sessionLabel = task.sessionLabel || task.agentLabel || task.sessionId || "Session";
2913
2950
  lines.push(`### ${statusIcon} ${sessionLabel} (${statusLabel})`);
2914
2951
  lines.push(`- **Updated:** ${formatRelativeTime(task.updatedAt)}`);
@@ -3470,6 +3507,7 @@ import path12 from "path";
3470
3507
  async function handleSaveCheckpoint() {
3471
3508
  const wsPath = resolveWsPath();
3472
3509
  const reader = new KeepGoingReader(wsPath);
3510
+ const writer = new KeepGoingWriter(wsPath);
3473
3511
  const { session: lastSession } = reader.getScopedLastSession();
3474
3512
  if (lastSession?.timestamp) {
3475
3513
  const ageMs = Date.now() - new Date(lastSession.timestamp).getTime();
@@ -3479,10 +3517,40 @@ async function handleSaveCheckpoint() {
3479
3517
  }
3480
3518
  const touchedFiles = getTouchedFiles(wsPath);
3481
3519
  const commitHashes = getCommitsSince(wsPath, lastSession?.timestamp);
3482
- if (touchedFiles.length === 0 && commitHashes.length === 0) {
3520
+ const gitBranch = getCurrentBranch(wsPath);
3521
+ const sessionId = generateSessionId({ workspaceRoot: wsPath, branch: gitBranch ?? void 0, worktreePath: wsPath });
3522
+ const existingTasks = writer.readCurrentTasks();
3523
+ const existingSession = existingTasks.find((t) => t.sessionId === sessionId);
3524
+ const isPlanning = existingSession?.sessionPhase === "planning";
3525
+ if (touchedFiles.length === 0 && commitHashes.length === 0 && !isPlanning) {
3526
+ process.exit(0);
3527
+ }
3528
+ const projectName = path12.basename(resolveStorageRoot(wsPath));
3529
+ if (touchedFiles.length === 0 && commitHashes.length === 0 && isPlanning) {
3530
+ const summary2 = existingSession?.sessionLabel || existingSession?.taskSummary || "Planning session";
3531
+ const checkpoint2 = createCheckpoint({
3532
+ summary: summary2,
3533
+ nextStep: existingSession?.nextStep || "",
3534
+ gitBranch,
3535
+ touchedFiles: [],
3536
+ commitHashes: [],
3537
+ workspaceRoot: wsPath,
3538
+ source: "auto",
3539
+ sessionId,
3540
+ sessionPhase: "planning",
3541
+ tags: ["plan"]
3542
+ });
3543
+ writer.saveCheckpoint(checkpoint2, projectName);
3544
+ writer.upsertSession({
3545
+ sessionId,
3546
+ sessionActive: false,
3547
+ nextStep: checkpoint2.nextStep || void 0,
3548
+ branch: gitBranch ?? void 0,
3549
+ updatedAt: checkpoint2.timestamp
3550
+ });
3551
+ console.log(`[KeepGoing] Plan checkpoint saved: ${summary2}`);
3483
3552
  process.exit(0);
3484
3553
  }
3485
- const gitBranch = getCurrentBranch(wsPath);
3486
3554
  const commitMessages = getCommitMessagesSince(wsPath, lastSession?.timestamp);
3487
3555
  const now = (/* @__PURE__ */ new Date()).toISOString();
3488
3556
  const events = buildSessionEvents({
@@ -3496,8 +3564,7 @@ async function handleSaveCheckpoint() {
3496
3564
  });
3497
3565
  const summary = buildSmartSummary(events) ?? `Worked on ${touchedFiles.slice(0, 5).map((f) => path12.basename(f)).join(", ")}`;
3498
3566
  const nextStep = buildSmartNextStep(events);
3499
- const projectName = path12.basename(resolveStorageRoot(wsPath));
3500
- const sessionId = generateSessionId({ workspaceRoot: wsPath, branch: gitBranch ?? void 0, worktreePath: wsPath });
3567
+ const sessionPhase = existingSession?.sessionPhase;
3501
3568
  const checkpoint = createCheckpoint({
3502
3569
  summary,
3503
3570
  nextStep,
@@ -3506,9 +3573,10 @@ async function handleSaveCheckpoint() {
3506
3573
  commitHashes,
3507
3574
  workspaceRoot: wsPath,
3508
3575
  source: "auto",
3509
- sessionId
3576
+ sessionId,
3577
+ ...sessionPhase ? { sessionPhase } : {},
3578
+ ...sessionPhase === "planning" ? { tags: ["plan"] } : {}
3510
3579
  });
3511
- const writer = new KeepGoingWriter(wsPath);
3512
3580
  writer.saveCheckpoint(checkpoint, projectName);
3513
3581
  writer.upsertSession({
3514
3582
  sessionId,
@@ -3776,7 +3844,8 @@ async function handleUpdateTaskFromHook() {
3776
3844
  branch,
3777
3845
  worktreePath: wsPath,
3778
3846
  sessionActive: true,
3779
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3847
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3848
+ sessionPhase: "active"
3780
3849
  };
3781
3850
  const sessionId = hookData.session_id || generateSessionId({ ...task, workspaceRoot: wsPath });
3782
3851
  task.sessionId = sessionId;
@@ -3923,6 +3992,59 @@ async function handleDetectDecisions() {
3923
3992
  process.exit(0);
3924
3993
  }
3925
3994
 
3995
+ // src/cli/heartbeat.ts
3996
+ var STDIN_TIMEOUT_MS3 = 3e3;
3997
+ var THROTTLE_MS = 3e4;
3998
+ async function handleHeartbeat() {
3999
+ const wsPath = resolveWsPath();
4000
+ const chunks = [];
4001
+ const timeout = setTimeout(() => process.exit(0), STDIN_TIMEOUT_MS3);
4002
+ process.stdin.on("error", () => {
4003
+ clearTimeout(timeout);
4004
+ process.exit(0);
4005
+ });
4006
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
4007
+ process.stdin.on("end", () => {
4008
+ clearTimeout(timeout);
4009
+ try {
4010
+ const raw = Buffer.concat(chunks).toString("utf-8").trim();
4011
+ if (!raw) {
4012
+ process.exit(0);
4013
+ }
4014
+ const hookData = JSON.parse(raw);
4015
+ const writer = new KeepGoingWriter(wsPath);
4016
+ const existing = writer.readCurrentTasks();
4017
+ const sessionIdFromHook = hookData.session_id;
4018
+ const sessionId = sessionIdFromHook || generateSessionId({ workspaceRoot: wsPath, worktreePath: wsPath, branch: getCurrentBranch(wsPath) ?? void 0 });
4019
+ const existingSession = existing.find((t) => t.sessionId === sessionId);
4020
+ if (existingSession?.updatedAt) {
4021
+ const ageMs = Date.now() - new Date(existingSession.updatedAt).getTime();
4022
+ if (ageMs < THROTTLE_MS) {
4023
+ process.exit(0);
4024
+ }
4025
+ }
4026
+ const sessionPhase = existingSession?.sessionPhase === "active" ? "active" : "planning";
4027
+ const branch = existingSession?.branch ?? getCurrentBranch(wsPath) ?? void 0;
4028
+ const task = {
4029
+ sessionId,
4030
+ sessionActive: true,
4031
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4032
+ sessionPhase,
4033
+ worktreePath: wsPath,
4034
+ branch
4035
+ };
4036
+ if (!existingSession?.sessionLabel && hookData.transcript_path) {
4037
+ const label = extractSessionLabel(hookData.transcript_path);
4038
+ if (label) task.sessionLabel = label;
4039
+ }
4040
+ writer.upsertSession(task);
4041
+ } catch {
4042
+ }
4043
+ process.exit(0);
4044
+ });
4045
+ process.stdin.resume();
4046
+ }
4047
+
3926
4048
  // src/index.ts
3927
4049
  var CLI_HANDLERS = {
3928
4050
  "--print-momentum": handlePrintMomentum,
@@ -3932,7 +4054,8 @@ var CLI_HANDLERS = {
3932
4054
  "--print-current": handlePrintCurrent,
3933
4055
  "--statusline": handleStatusline,
3934
4056
  "--continue-on": handleContinueOn,
3935
- "--detect-decisions": handleDetectDecisions
4057
+ "--detect-decisions": handleDetectDecisions,
4058
+ "--heartbeat": handleHeartbeat
3936
4059
  };
3937
4060
  var flag = process.argv.slice(2).find((a) => a in CLI_HANDLERS);
3938
4061
  if (flag) {