@keepgoingdev/mcp-server 0.1.1 → 0.2.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
@@ -1,64 +1,945 @@
1
1
  #!/usr/bin/env node
2
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
- import { KeepGoingReader } from './storage.js';
5
- import { formatRelativeTime } from '@keepgoingdev/shared';
6
- import { registerGetMomentum } from './tools/getMomentum.js';
7
- import { registerGetSessionHistory } from './tools/getSessionHistory.js';
8
- import { registerGetReentryBriefing } from './tools/getReentryBriefing.js';
9
- import { registerResumePrompt } from './prompts/resume.js';
10
- // Handle --print-momentum CLI flag: print momentum context and exit.
11
- // Used by the Claude Code SessionStart hook (scripts/keepgoing-hook.sh).
12
- if (process.argv.includes('--print-momentum')) {
13
- // Workspace path is the first non-flag argument after the script path
14
- const wsPath = process.argv.slice(2).find(a => a !== '--print-momentum') || process.cwd();
15
- const reader = new KeepGoingReader(wsPath);
16
- if (!reader.exists()) {
17
- process.exit(0);
18
- }
19
- const lastSession = reader.getLastSession();
20
- if (!lastSession) {
21
- process.exit(0);
22
- }
23
- const touchedCount = lastSession.touchedFiles?.length ?? 0;
24
- const lines = [];
25
- lines.push(`[KeepGoing] Last checkpoint: ${formatRelativeTime(lastSession.timestamp)}`);
26
- if (lastSession.summary) {
27
- lines.push(` Summary: ${lastSession.summary}`);
28
- }
29
- if (lastSession.nextStep) {
30
- lines.push(` Next step: ${lastSession.nextStep}`);
31
- }
32
- if (lastSession.blocker) {
33
- lines.push(` Blocker: ${lastSession.blocker}`);
34
- }
35
- if (lastSession.gitBranch) {
36
- lines.push(` Branch: ${lastSession.gitBranch}`);
37
- }
38
- if (touchedCount > 0) {
39
- lines.push(` Worked on ${touchedCount} files on ${lastSession.gitBranch ?? 'unknown branch'}`);
40
- }
41
- lines.push(' Tip: Use the get_reentry_briefing tool for a full briefing');
42
- console.log(lines.join('\n'));
2
+
3
+ // src/index.ts
4
+ import path5 from "path";
5
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
+
8
+ // src/storage.ts
9
+ import fs2 from "fs";
10
+ import path2 from "path";
11
+
12
+ // ../../packages/shared/src/session.ts
13
+ import { randomUUID } from "crypto";
14
+ function generateCheckpointId() {
15
+ return randomUUID();
16
+ }
17
+ function createCheckpoint(fields) {
18
+ return {
19
+ id: generateCheckpointId(),
20
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
21
+ ...fields
22
+ };
23
+ }
24
+
25
+ // ../../packages/shared/src/timeUtils.ts
26
+ function formatRelativeTime(timestamp) {
27
+ const now = Date.now();
28
+ const then = new Date(timestamp).getTime();
29
+ const diffMs = now - then;
30
+ if (isNaN(diffMs)) {
31
+ return "unknown time";
32
+ }
33
+ if (diffMs < 0) {
34
+ return "in the future";
35
+ }
36
+ const seconds = Math.floor(diffMs / 1e3);
37
+ const minutes = Math.floor(seconds / 60);
38
+ const hours = Math.floor(minutes / 60);
39
+ const days = Math.floor(hours / 24);
40
+ const weeks = Math.floor(days / 7);
41
+ const months = Math.floor(days / 30);
42
+ const years = Math.floor(days / 365);
43
+ if (seconds < 10) {
44
+ return "just now";
45
+ } else if (seconds < 60) {
46
+ return `${seconds} seconds ago`;
47
+ } else if (minutes < 60) {
48
+ return minutes === 1 ? "1 minute ago" : `${minutes} minutes ago`;
49
+ } else if (hours < 24) {
50
+ return hours === 1 ? "1 hour ago" : `${hours} hours ago`;
51
+ } else if (days < 7) {
52
+ return days === 1 ? "1 day ago" : `${days} days ago`;
53
+ } else if (weeks < 4) {
54
+ return weeks === 1 ? "1 week ago" : `${weeks} weeks ago`;
55
+ } else if (months < 12) {
56
+ return months === 1 ? "1 month ago" : `${months} months ago`;
57
+ } else {
58
+ return years === 1 ? "1 year ago" : `${years} years ago`;
59
+ }
60
+ }
61
+
62
+ // ../../packages/shared/src/gitUtils.ts
63
+ import { execFileSync, execFile } from "child_process";
64
+ import { promisify } from "util";
65
+ var execFileAsync = promisify(execFile);
66
+ function getCurrentBranch(workspacePath2) {
67
+ try {
68
+ const result = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
69
+ cwd: workspacePath2,
70
+ encoding: "utf-8",
71
+ timeout: 5e3
72
+ });
73
+ return result.trim() || void 0;
74
+ } catch {
75
+ return void 0;
76
+ }
77
+ }
78
+ function getGitLogSince(workspacePath2, format, sinceTimestamp) {
79
+ try {
80
+ const since = sinceTimestamp || new Date(Date.now() - 24 * 60 * 60 * 1e3).toISOString();
81
+ const result = execFileSync(
82
+ "git",
83
+ ["log", `--since=${since}`, `--format=${format}`],
84
+ {
85
+ cwd: workspacePath2,
86
+ encoding: "utf-8",
87
+ timeout: 5e3
88
+ }
89
+ );
90
+ if (!result.trim()) {
91
+ return [];
92
+ }
93
+ return result.trim().split("\n").filter((line) => line.length > 0);
94
+ } catch {
95
+ return [];
96
+ }
97
+ }
98
+ function getCommitsSince(workspacePath2, sinceTimestamp) {
99
+ return getGitLogSince(workspacePath2, "%H", sinceTimestamp);
100
+ }
101
+ function getCommitMessagesSince(workspacePath2, sinceTimestamp) {
102
+ return getGitLogSince(workspacePath2, "%s", sinceTimestamp);
103
+ }
104
+ function getTouchedFiles(workspacePath2) {
105
+ try {
106
+ const result = execFileSync("git", ["status", "--porcelain"], {
107
+ cwd: workspacePath2,
108
+ encoding: "utf-8",
109
+ timeout: 5e3
110
+ });
111
+ if (!result.trim()) {
112
+ return [];
113
+ }
114
+ return result.trim().split("\n").map((line) => line.substring(3).trim()).filter((file) => file.length > 0 && !file.endsWith("/"));
115
+ } catch {
116
+ return [];
117
+ }
118
+ }
119
+
120
+ // ../../packages/shared/src/reentry.ts
121
+ var RECENT_SESSION_COUNT = 5;
122
+ function generateBriefing(lastSession, recentSessions, projectState, gitBranch, recentCommitMessages) {
123
+ if (!lastSession) {
124
+ return void 0;
125
+ }
126
+ return {
127
+ lastWorked: formatRelativeTime(lastSession.timestamp),
128
+ currentFocus: buildCurrentFocus(lastSession, projectState, gitBranch),
129
+ recentActivity: buildRecentActivity(
130
+ lastSession,
131
+ recentSessions,
132
+ recentCommitMessages
133
+ ),
134
+ suggestedNext: buildSuggestedNext(lastSession, gitBranch),
135
+ smallNextStep: buildSmallNextStep(
136
+ lastSession,
137
+ gitBranch,
138
+ recentCommitMessages
139
+ )
140
+ };
141
+ }
142
+ function getRecentSessions(allSessions, count = RECENT_SESSION_COUNT) {
143
+ return allSessions.slice(-count).reverse();
144
+ }
145
+ function buildCurrentFocus(lastSession, projectState, gitBranch) {
146
+ if (projectState.derivedCurrentFocus) {
147
+ return projectState.derivedCurrentFocus;
148
+ }
149
+ const branchFocus = inferFocusFromBranch(gitBranch);
150
+ if (branchFocus) {
151
+ return branchFocus;
152
+ }
153
+ if (lastSession.summary) {
154
+ return lastSession.summary;
155
+ }
156
+ if (lastSession.touchedFiles.length > 0) {
157
+ return inferFocusFromFiles(lastSession.touchedFiles);
158
+ }
159
+ return "Unknown, save a checkpoint to set context";
160
+ }
161
+ function buildRecentActivity(lastSession, recentSessions, recentCommitMessages) {
162
+ const parts = [];
163
+ const sessionCount = recentSessions.length;
164
+ if (sessionCount > 1) {
165
+ parts.push(`${sessionCount} recent sessions`);
166
+ } else if (sessionCount === 1) {
167
+ parts.push("1 recent session");
168
+ }
169
+ if (lastSession.summary) {
170
+ parts.push(`Last: ${lastSession.summary}`);
171
+ }
172
+ if (lastSession.touchedFiles.length > 0) {
173
+ parts.push(`${lastSession.touchedFiles.length} files touched`);
174
+ }
175
+ if (recentCommitMessages && recentCommitMessages.length > 0) {
176
+ parts.push(`${recentCommitMessages.length} recent commits`);
177
+ }
178
+ return parts.length > 0 ? parts.join(". ") : "No recent activity recorded";
179
+ }
180
+ function buildSuggestedNext(lastSession, gitBranch) {
181
+ if (lastSession.nextStep) {
182
+ return lastSession.nextStep;
183
+ }
184
+ const branchFocus = inferFocusFromBranch(gitBranch);
185
+ if (branchFocus) {
186
+ return `Continue working on ${branchFocus}`;
187
+ }
188
+ if (lastSession.touchedFiles.length > 0) {
189
+ return `Continue working on ${inferFocusFromFiles(lastSession.touchedFiles)}`;
190
+ }
191
+ return "Save a checkpoint to track your next step";
192
+ }
193
+ function buildSmallNextStep(lastSession, gitBranch, recentCommitMessages) {
194
+ const fallback = "Review last changed files to resume flow";
195
+ if (lastSession.nextStep) {
196
+ const distilled = distillToSmallStep(
197
+ lastSession.nextStep,
198
+ lastSession.touchedFiles
199
+ );
200
+ if (distilled) {
201
+ return distilled;
202
+ }
203
+ }
204
+ if (recentCommitMessages && recentCommitMessages.length > 0) {
205
+ const commitStep = deriveStepFromCommits(recentCommitMessages);
206
+ if (commitStep) {
207
+ return commitStep;
208
+ }
209
+ }
210
+ if (lastSession.touchedFiles.length > 0) {
211
+ const fileStep = deriveStepFromFiles(lastSession.touchedFiles);
212
+ if (fileStep) {
213
+ return fileStep;
214
+ }
215
+ }
216
+ const branchFocus = inferFocusFromBranch(gitBranch);
217
+ if (branchFocus) {
218
+ return `Check git status for ${branchFocus}`;
219
+ }
220
+ return fallback;
221
+ }
222
+ function distillToSmallStep(nextStep, touchedFiles) {
223
+ if (!nextStep.trim()) {
224
+ return void 0;
225
+ }
226
+ const words = nextStep.trim().split(/\s+/);
227
+ if (words.length <= 12) {
228
+ if (touchedFiles.length > 0 && !mentionsFile(nextStep)) {
229
+ const primaryFile = getPrimaryFileName(touchedFiles);
230
+ const enhanced = `${nextStep.trim()} in ${primaryFile}`;
231
+ if (enhanced.split(/\s+/).length <= 12) {
232
+ return enhanced;
233
+ }
234
+ }
235
+ return nextStep.trim();
236
+ }
237
+ return words.slice(0, 12).join(" ");
238
+ }
239
+ function deriveStepFromCommits(commitMessages) {
240
+ const lastCommit = commitMessages[0];
241
+ if (!lastCommit || !lastCommit.trim()) {
242
+ return void 0;
243
+ }
244
+ const wipPattern = /^(?:wip|work in progress|started?|begin|draft)[:\s]/i;
245
+ if (wipPattern.test(lastCommit)) {
246
+ const topic = lastCommit.replace(wipPattern, "").trim();
247
+ if (topic) {
248
+ const words = topic.split(/\s+/).slice(0, 8).join(" ");
249
+ return `Continue ${words}`;
250
+ }
251
+ }
252
+ return void 0;
253
+ }
254
+ function deriveStepFromFiles(files) {
255
+ const primaryFile = getPrimaryFileName(files);
256
+ if (files.length > 1) {
257
+ return `Open ${primaryFile} and review ${files.length} changed files`;
258
+ }
259
+ return `Open ${primaryFile} and pick up where you left off`;
260
+ }
261
+ function getPrimaryFileName(files) {
262
+ const sourceFiles = files.filter((f) => {
263
+ const lower = f.toLowerCase();
264
+ return !lower.includes("test") && !lower.includes("spec") && !lower.includes(".config") && !lower.includes("package.json") && !lower.includes("tsconfig");
265
+ });
266
+ const target = sourceFiles.length > 0 ? sourceFiles[0] : files[0];
267
+ const parts = target.replace(/\\/g, "/").split("/");
268
+ return parts[parts.length - 1];
269
+ }
270
+ function mentionsFile(text) {
271
+ return /\w+\.(?:ts|tsx|js|jsx|py|go|rs|java|rb|css|scss|html|json|yaml|yml|md|sql|sh)\b/i.test(
272
+ text
273
+ );
274
+ }
275
+ function inferFocusFromBranch(branch) {
276
+ if (!branch || branch === "main" || branch === "master" || branch === "develop" || branch === "HEAD") {
277
+ return void 0;
278
+ }
279
+ const prefixPattern = /^(?:feature|feat|fix|bugfix|hotfix|chore|refactor|docs|test|ci)\//i;
280
+ const isFix = /^(?:fix|bugfix|hotfix)\//i.test(branch);
281
+ const stripped = branch.replace(prefixPattern, "");
282
+ const cleaned = stripped.replace(/[-_/]/g, " ").replace(/^\d+\s*/, "").trim();
283
+ if (!cleaned) {
284
+ return void 0;
285
+ }
286
+ return isFix ? `${cleaned} fix` : cleaned;
287
+ }
288
+ function inferFocusFromFiles(files) {
289
+ if (files.length === 0) {
290
+ return "unknown files";
291
+ }
292
+ const dirs = files.map((f) => {
293
+ const parts = f.replace(/\\/g, "/").split("/");
294
+ return parts.length > 1 ? parts.slice(0, -1).join("/") : "";
295
+ }).filter((d) => d.length > 0);
296
+ if (dirs.length > 0) {
297
+ const counts = /* @__PURE__ */ new Map();
298
+ for (const dir of dirs) {
299
+ counts.set(dir, (counts.get(dir) ?? 0) + 1);
300
+ }
301
+ let topDir = "";
302
+ let topCount = 0;
303
+ for (const [dir, count] of counts) {
304
+ if (count > topCount) {
305
+ topDir = dir;
306
+ topCount = count;
307
+ }
308
+ }
309
+ if (topDir) {
310
+ return `files in ${topDir}`;
311
+ }
312
+ }
313
+ const names = files.slice(0, 3).map((f) => {
314
+ const parts = f.replace(/\\/g, "/").split("/");
315
+ return parts[parts.length - 1];
316
+ });
317
+ return names.join(", ");
318
+ }
319
+
320
+ // ../../packages/shared/src/storage.ts
321
+ import fs from "fs";
322
+ import path from "path";
323
+ import { randomUUID as randomUUID2 } from "crypto";
324
+ var STORAGE_DIR = ".keepgoing";
325
+ var META_FILE = "meta.json";
326
+ var SESSIONS_FILE = "sessions.json";
327
+ var STATE_FILE = "state.json";
328
+ var KeepGoingWriter = class {
329
+ storagePath;
330
+ sessionsFilePath;
331
+ stateFilePath;
332
+ metaFilePath;
333
+ constructor(workspacePath2) {
334
+ this.storagePath = path.join(workspacePath2, STORAGE_DIR);
335
+ this.sessionsFilePath = path.join(this.storagePath, SESSIONS_FILE);
336
+ this.stateFilePath = path.join(this.storagePath, STATE_FILE);
337
+ this.metaFilePath = path.join(this.storagePath, META_FILE);
338
+ }
339
+ ensureDir() {
340
+ if (!fs.existsSync(this.storagePath)) {
341
+ fs.mkdirSync(this.storagePath, { recursive: true });
342
+ }
343
+ }
344
+ saveCheckpoint(checkpoint, projectName) {
345
+ this.ensureDir();
346
+ let sessionsData;
347
+ try {
348
+ if (fs.existsSync(this.sessionsFilePath)) {
349
+ const raw = JSON.parse(fs.readFileSync(this.sessionsFilePath, "utf-8"));
350
+ if (Array.isArray(raw)) {
351
+ sessionsData = { version: 1, project: projectName, sessions: raw };
352
+ } else {
353
+ sessionsData = raw;
354
+ }
355
+ } else {
356
+ sessionsData = { version: 1, project: projectName, sessions: [] };
357
+ }
358
+ } catch {
359
+ sessionsData = { version: 1, project: projectName, sessions: [] };
360
+ }
361
+ sessionsData.sessions.push(checkpoint);
362
+ sessionsData.lastSessionId = checkpoint.id;
363
+ const MAX_SESSIONS = 200;
364
+ if (sessionsData.sessions.length > MAX_SESSIONS) {
365
+ sessionsData.sessions = sessionsData.sessions.slice(-MAX_SESSIONS);
366
+ }
367
+ fs.writeFileSync(this.sessionsFilePath, JSON.stringify(sessionsData, null, 2), "utf-8");
368
+ const state = {
369
+ lastSessionId: checkpoint.id,
370
+ lastKnownBranch: checkpoint.gitBranch,
371
+ lastActivityAt: checkpoint.timestamp
372
+ };
373
+ fs.writeFileSync(this.stateFilePath, JSON.stringify(state, null, 2), "utf-8");
374
+ let meta;
375
+ try {
376
+ if (fs.existsSync(this.metaFilePath)) {
377
+ meta = JSON.parse(fs.readFileSync(this.metaFilePath, "utf-8"));
378
+ meta.lastUpdated = checkpoint.timestamp;
379
+ } else {
380
+ meta = {
381
+ projectId: randomUUID2(),
382
+ createdAt: checkpoint.timestamp,
383
+ lastUpdated: checkpoint.timestamp
384
+ };
385
+ }
386
+ } catch {
387
+ meta = {
388
+ projectId: randomUUID2(),
389
+ createdAt: checkpoint.timestamp,
390
+ lastUpdated: checkpoint.timestamp
391
+ };
392
+ }
393
+ fs.writeFileSync(this.metaFilePath, JSON.stringify(meta, null, 2), "utf-8");
394
+ }
395
+ };
396
+
397
+ // src/storage.ts
398
+ var STORAGE_DIR2 = ".keepgoing";
399
+ var META_FILE2 = "meta.json";
400
+ var SESSIONS_FILE2 = "sessions.json";
401
+ var STATE_FILE2 = "state.json";
402
+ var KeepGoingReader = class {
403
+ storagePath;
404
+ metaFilePath;
405
+ sessionsFilePath;
406
+ stateFilePath;
407
+ constructor(workspacePath2) {
408
+ this.storagePath = path2.join(workspacePath2, STORAGE_DIR2);
409
+ this.metaFilePath = path2.join(this.storagePath, META_FILE2);
410
+ this.sessionsFilePath = path2.join(this.storagePath, SESSIONS_FILE2);
411
+ this.stateFilePath = path2.join(this.storagePath, STATE_FILE2);
412
+ }
413
+ /** Check if .keepgoing/ directory exists. */
414
+ exists() {
415
+ return fs2.existsSync(this.storagePath);
416
+ }
417
+ /** Read state.json, returns undefined if missing or corrupt. */
418
+ getState() {
419
+ return this.readJsonFile(this.stateFilePath);
420
+ }
421
+ /** Read meta.json, returns undefined if missing or corrupt. */
422
+ getMeta() {
423
+ return this.readJsonFile(this.metaFilePath);
424
+ }
425
+ /**
426
+ * Read sessions from sessions.json.
427
+ * Handles both formats:
428
+ * - Flat array: SessionCheckpoint[] (from ProjectStorage)
429
+ * - Wrapper object: ProjectSessions (from SessionStorage)
430
+ */
431
+ getSessions() {
432
+ return this.parseSessions().sessions;
433
+ }
434
+ /**
435
+ * Get the most recent session checkpoint.
436
+ * Uses state.lastSessionId if available, falls back to last in array.
437
+ */
438
+ getLastSession() {
439
+ const { sessions, wrapperLastSessionId } = this.parseSessions();
440
+ if (sessions.length === 0) {
441
+ return void 0;
442
+ }
443
+ const state = this.getState();
444
+ if (state?.lastSessionId) {
445
+ const found = sessions.find((s) => s.id === state.lastSessionId);
446
+ if (found) {
447
+ return found;
448
+ }
449
+ }
450
+ if (wrapperLastSessionId) {
451
+ const found = sessions.find((s) => s.id === wrapperLastSessionId);
452
+ if (found) {
453
+ return found;
454
+ }
455
+ }
456
+ return sessions[sessions.length - 1];
457
+ }
458
+ /**
459
+ * Returns the last N sessions, newest first.
460
+ */
461
+ getRecentSessions(count) {
462
+ return getRecentSessions(this.getSessions(), count);
463
+ }
464
+ /**
465
+ * Parses sessions.json once, returning both the session list
466
+ * and the optional lastSessionId from a ProjectSessions wrapper.
467
+ */
468
+ parseSessions() {
469
+ const raw = this.readJsonFile(
470
+ this.sessionsFilePath
471
+ );
472
+ if (!raw) {
473
+ return { sessions: [] };
474
+ }
475
+ if (Array.isArray(raw)) {
476
+ return { sessions: raw };
477
+ }
478
+ return { sessions: raw.sessions ?? [], wrapperLastSessionId: raw.lastSessionId };
479
+ }
480
+ readJsonFile(filePath) {
481
+ try {
482
+ if (!fs2.existsSync(filePath)) {
483
+ return void 0;
484
+ }
485
+ const raw = fs2.readFileSync(filePath, "utf-8");
486
+ return JSON.parse(raw);
487
+ } catch {
488
+ return void 0;
489
+ }
490
+ }
491
+ };
492
+
493
+ // src/tools/getMomentum.ts
494
+ function registerGetMomentum(server2, reader2, workspacePath2) {
495
+ server2.tool(
496
+ "get_momentum",
497
+ "Get current developer momentum: last checkpoint, next step, blockers, and branch context. Use this to understand where the developer left off.",
498
+ {},
499
+ async () => {
500
+ if (!reader2.exists()) {
501
+ return {
502
+ content: [
503
+ {
504
+ type: "text",
505
+ text: "No KeepGoing data found. The developer has not saved any checkpoints yet."
506
+ }
507
+ ]
508
+ };
509
+ }
510
+ const lastSession = reader2.getLastSession();
511
+ if (!lastSession) {
512
+ return {
513
+ content: [
514
+ {
515
+ type: "text",
516
+ text: "KeepGoing is set up but no session checkpoints exist yet."
517
+ }
518
+ ]
519
+ };
520
+ }
521
+ const state = reader2.getState();
522
+ const currentBranch = getCurrentBranch(workspacePath2);
523
+ const branchChanged = lastSession.gitBranch && currentBranch && lastSession.gitBranch !== currentBranch;
524
+ const lines = [
525
+ `## Developer Momentum`,
526
+ "",
527
+ `**Last checkpoint:** ${formatRelativeTime(lastSession.timestamp)}`,
528
+ `**Summary:** ${lastSession.summary || "No summary"}`,
529
+ `**Next step:** ${lastSession.nextStep || "Not specified"}`
530
+ ];
531
+ if (lastSession.blocker) {
532
+ lines.push(`**Blocker:** ${lastSession.blocker}`);
533
+ }
534
+ if (lastSession.projectIntent) {
535
+ lines.push(`**Project intent:** ${lastSession.projectIntent}`);
536
+ }
537
+ lines.push("");
538
+ if (currentBranch) {
539
+ lines.push(`**Current branch:** ${currentBranch}`);
540
+ }
541
+ if (branchChanged) {
542
+ lines.push(
543
+ `**Note:** Branch changed since last checkpoint (was \`${lastSession.gitBranch}\`, now \`${currentBranch}\`)`
544
+ );
545
+ }
546
+ if (lastSession.touchedFiles.length > 0) {
547
+ lines.push("");
548
+ lines.push(
549
+ `**Files touched (${lastSession.touchedFiles.length}):** ${lastSession.touchedFiles.slice(0, 10).join(", ")}`
550
+ );
551
+ if (lastSession.touchedFiles.length > 10) {
552
+ lines.push(
553
+ ` ...and ${lastSession.touchedFiles.length - 10} more`
554
+ );
555
+ }
556
+ }
557
+ if (state?.derivedCurrentFocus) {
558
+ lines.push("");
559
+ lines.push(`**Derived focus:** ${state.derivedCurrentFocus}`);
560
+ }
561
+ return {
562
+ content: [{ type: "text", text: lines.join("\n") }]
563
+ };
564
+ }
565
+ );
566
+ }
567
+
568
+ // src/tools/getSessionHistory.ts
569
+ import { z } from "zod";
570
+ function registerGetSessionHistory(server2, reader2) {
571
+ server2.tool(
572
+ "get_session_history",
573
+ "Get recent session checkpoints. Returns a chronological list of what the developer worked on.",
574
+ { limit: z.number().min(1).max(50).default(5).describe("Number of recent sessions to return (1-50, default 5)") },
575
+ async ({ limit }) => {
576
+ if (!reader2.exists()) {
577
+ return {
578
+ content: [
579
+ {
580
+ type: "text",
581
+ text: "No KeepGoing data found."
582
+ }
583
+ ]
584
+ };
585
+ }
586
+ const sessions = reader2.getRecentSessions(limit);
587
+ if (sessions.length === 0) {
588
+ return {
589
+ content: [
590
+ {
591
+ type: "text",
592
+ text: "No session checkpoints found."
593
+ }
594
+ ]
595
+ };
596
+ }
597
+ const lines = [
598
+ `## Session History (last ${sessions.length})`,
599
+ ""
600
+ ];
601
+ for (const session of sessions) {
602
+ lines.push(`### ${formatRelativeTime(session.timestamp)}`);
603
+ lines.push(`- **Summary:** ${session.summary || "No summary"}`);
604
+ lines.push(`- **Next step:** ${session.nextStep || "Not specified"}`);
605
+ if (session.blocker) {
606
+ lines.push(`- **Blocker:** ${session.blocker}`);
607
+ }
608
+ if (session.gitBranch) {
609
+ lines.push(`- **Branch:** ${session.gitBranch}`);
610
+ }
611
+ if (session.touchedFiles.length > 0) {
612
+ lines.push(
613
+ `- **Files:** ${session.touchedFiles.slice(0, 5).join(", ")}${session.touchedFiles.length > 5 ? ` (+${session.touchedFiles.length - 5} more)` : ""}`
614
+ );
615
+ }
616
+ lines.push("");
617
+ }
618
+ return {
619
+ content: [{ type: "text", text: lines.join("\n") }]
620
+ };
621
+ }
622
+ );
623
+ }
624
+
625
+ // src/tools/getReentryBriefing.ts
626
+ function registerGetReentryBriefing(server2, reader2, workspacePath2) {
627
+ server2.tool(
628
+ "get_reentry_briefing",
629
+ "Get a synthesized re-entry briefing that helps a developer understand where they left off. Includes focus, recent activity, and suggested next steps.",
630
+ {},
631
+ async () => {
632
+ if (!reader2.exists()) {
633
+ return {
634
+ content: [
635
+ {
636
+ type: "text",
637
+ text: "No KeepGoing data found. The developer has not saved any checkpoints yet."
638
+ }
639
+ ]
640
+ };
641
+ }
642
+ const lastSession = reader2.getLastSession();
643
+ const recentSessions = reader2.getRecentSessions(5);
644
+ const state = reader2.getState() ?? {};
645
+ const gitBranch = getCurrentBranch(workspacePath2);
646
+ const sinceTimestamp = lastSession?.timestamp;
647
+ const recentCommits = sinceTimestamp ? getCommitMessagesSince(workspacePath2, sinceTimestamp) : [];
648
+ const briefing = generateBriefing(
649
+ lastSession,
650
+ recentSessions,
651
+ state,
652
+ gitBranch,
653
+ recentCommits
654
+ );
655
+ if (!briefing) {
656
+ return {
657
+ content: [
658
+ {
659
+ type: "text",
660
+ text: "No session data available to generate a briefing."
661
+ }
662
+ ]
663
+ };
664
+ }
665
+ const lines = [
666
+ `## Re-entry Briefing`,
667
+ "",
668
+ `**Last worked:** ${briefing.lastWorked}`,
669
+ `**Current focus:** ${briefing.currentFocus}`,
670
+ `**Recent activity:** ${briefing.recentActivity}`,
671
+ `**Suggested next:** ${briefing.suggestedNext}`,
672
+ `**Quick start:** ${briefing.smallNextStep}`
673
+ ];
674
+ return {
675
+ content: [{ type: "text", text: lines.join("\n") }]
676
+ };
677
+ }
678
+ );
679
+ }
680
+
681
+ // src/tools/saveCheckpoint.ts
682
+ import path3 from "path";
683
+ import { z as z2 } from "zod";
684
+ function registerSaveCheckpoint(server2, reader2, workspacePath2) {
685
+ server2.tool(
686
+ "save_checkpoint",
687
+ "Save a development checkpoint. Call this after completing a task or meaningful piece of work, not just at end of session. Each checkpoint helps the next session (or developer) pick up exactly where you left off.",
688
+ {
689
+ summary: z2.string().describe("What was accomplished in this session"),
690
+ nextStep: z2.string().optional().describe("What to do next"),
691
+ blocker: z2.string().optional().describe("Any blocker preventing progress")
692
+ },
693
+ async ({ summary, nextStep, blocker }) => {
694
+ const lastSession = reader2.getLastSession();
695
+ const gitBranch = getCurrentBranch(workspacePath2);
696
+ const touchedFiles = getTouchedFiles(workspacePath2);
697
+ const commitHashes = getCommitsSince(workspacePath2, lastSession?.timestamp);
698
+ const projectName = path3.basename(workspacePath2);
699
+ const checkpoint = createCheckpoint({
700
+ summary,
701
+ nextStep: nextStep || "",
702
+ blocker,
703
+ gitBranch,
704
+ touchedFiles,
705
+ commitHashes,
706
+ workspaceRoot: workspacePath2,
707
+ source: "manual"
708
+ });
709
+ const writer = new KeepGoingWriter(workspacePath2);
710
+ writer.saveCheckpoint(checkpoint, projectName);
711
+ const lines = [
712
+ `Checkpoint saved.`,
713
+ `- **ID:** ${checkpoint.id}`,
714
+ `- **Branch:** ${gitBranch || "unknown"}`,
715
+ `- **Files tracked:** ${touchedFiles.length}`,
716
+ `- **Commits captured:** ${commitHashes.length}`
717
+ ];
718
+ return {
719
+ content: [{ type: "text", text: lines.join("\n") }]
720
+ };
721
+ }
722
+ );
723
+ }
724
+
725
+ // src/tools/setupProject.ts
726
+ import fs3 from "fs";
727
+ import path4 from "path";
728
+ import { z as z3 } from "zod";
729
+ var KEEPGOING_MARKER = "@keepgoingdev/mcp-server";
730
+ var SESSION_START_HOOK = {
731
+ matcher: "",
732
+ hooks: [
733
+ {
734
+ type: "command",
735
+ command: "npx -y @keepgoingdev/mcp-server --print-momentum"
736
+ }
737
+ ]
738
+ };
739
+ var STOP_HOOK = {
740
+ matcher: "",
741
+ hooks: [
742
+ {
743
+ type: "command",
744
+ command: "npx -y @keepgoingdev/mcp-server --save-checkpoint"
745
+ }
746
+ ]
747
+ };
748
+ var CLAUDE_MD_SECTION = `
749
+ ## KeepGoing
750
+
751
+ After completing a task or meaningful piece of work, call the \`save_checkpoint\` MCP tool with:
752
+ - \`summary\`: What you accomplished
753
+ - \`nextStep\`: What should be done next
754
+ - \`blocker\`: Any blocker (if applicable)
755
+ `;
756
+ function hasKeepGoingHook(hookEntries) {
757
+ return hookEntries.some(
758
+ (entry) => entry?.hooks?.some((h) => typeof h?.command === "string" && h.command.includes(KEEPGOING_MARKER))
759
+ );
760
+ }
761
+ function registerSetupProject(server2, workspacePath2) {
762
+ server2.tool(
763
+ "setup_project",
764
+ "Set up KeepGoing in the current project. Adds session hooks to .claude/settings.json and CLAUDE.md instructions so checkpoints are saved automatically.",
765
+ {
766
+ sessionHooks: z3.boolean().optional().default(true).describe("Add session hooks to .claude/settings.json"),
767
+ claudeMd: z3.boolean().optional().default(true).describe("Add KeepGoing instructions to CLAUDE.md")
768
+ },
769
+ async ({ sessionHooks, claudeMd }) => {
770
+ const results = [];
771
+ if (sessionHooks) {
772
+ const claudeDir = path4.join(workspacePath2, ".claude");
773
+ const settingsPath = path4.join(claudeDir, "settings.json");
774
+ let settings = {};
775
+ if (fs3.existsSync(settingsPath)) {
776
+ settings = JSON.parse(fs3.readFileSync(settingsPath, "utf-8"));
777
+ }
778
+ if (!settings.hooks) {
779
+ settings.hooks = {};
780
+ }
781
+ let hooksChanged = false;
782
+ if (!Array.isArray(settings.hooks.SessionStart)) {
783
+ settings.hooks.SessionStart = [];
784
+ }
785
+ if (!hasKeepGoingHook(settings.hooks.SessionStart)) {
786
+ settings.hooks.SessionStart.push(SESSION_START_HOOK);
787
+ hooksChanged = true;
788
+ }
789
+ if (!Array.isArray(settings.hooks.Stop)) {
790
+ settings.hooks.Stop = [];
791
+ }
792
+ if (!hasKeepGoingHook(settings.hooks.Stop)) {
793
+ settings.hooks.Stop.push(STOP_HOOK);
794
+ hooksChanged = true;
795
+ }
796
+ if (hooksChanged) {
797
+ if (!fs3.existsSync(claudeDir)) {
798
+ fs3.mkdirSync(claudeDir, { recursive: true });
799
+ }
800
+ fs3.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
801
+ results.push("**Session hooks:** Added to `.claude/settings.json`");
802
+ } else {
803
+ results.push("**Session hooks:** Already present, skipped");
804
+ }
805
+ }
806
+ if (claudeMd) {
807
+ const claudeMdPath = path4.join(workspacePath2, "CLAUDE.md");
808
+ let existing = "";
809
+ if (fs3.existsSync(claudeMdPath)) {
810
+ existing = fs3.readFileSync(claudeMdPath, "utf-8");
811
+ }
812
+ if (existing.includes("## KeepGoing")) {
813
+ results.push("**CLAUDE.md:** KeepGoing section already present, skipped");
814
+ } else {
815
+ const updated = existing + CLAUDE_MD_SECTION;
816
+ fs3.writeFileSync(claudeMdPath, updated);
817
+ results.push("**CLAUDE.md:** Added KeepGoing section");
818
+ }
819
+ }
820
+ return {
821
+ content: [{ type: "text", text: results.join("\n") }]
822
+ };
823
+ }
824
+ );
825
+ }
826
+
827
+ // src/prompts/resume.ts
828
+ function registerResumePrompt(server2) {
829
+ server2.prompt(
830
+ "resume",
831
+ "Check developer momentum and suggest what to work on next",
832
+ async () => ({
833
+ messages: [
834
+ {
835
+ role: "user",
836
+ content: {
837
+ type: "text",
838
+ text: [
839
+ "I just opened this project and want to pick up where I left off.",
840
+ "",
841
+ "Please use the KeepGoing tools to:",
842
+ "1. Check my current momentum (get_momentum)",
843
+ "2. Get a re-entry briefing (get_reentry_briefing)",
844
+ "3. Based on the results, give me a concise summary of where I left off and suggest what to work on next.",
845
+ "",
846
+ "Keep your response brief and actionable."
847
+ ].join("\n")
848
+ }
849
+ }
850
+ ]
851
+ })
852
+ );
853
+ }
854
+
855
+ // src/index.ts
856
+ if (process.argv.includes("--print-momentum")) {
857
+ const wsPath = process.argv.slice(2).find((a) => a !== "--print-momentum") || process.cwd();
858
+ const reader2 = new KeepGoingReader(wsPath);
859
+ if (!reader2.exists()) {
43
860
  process.exit(0);
861
+ }
862
+ const lastSession = reader2.getLastSession();
863
+ if (!lastSession) {
864
+ process.exit(0);
865
+ }
866
+ const touchedCount = lastSession.touchedFiles?.length ?? 0;
867
+ const lines = [];
868
+ lines.push(`[KeepGoing] Last checkpoint: ${formatRelativeTime(lastSession.timestamp)}`);
869
+ if (lastSession.summary) {
870
+ lines.push(` Summary: ${lastSession.summary}`);
871
+ }
872
+ if (lastSession.nextStep) {
873
+ lines.push(` Next step: ${lastSession.nextStep}`);
874
+ }
875
+ if (lastSession.blocker) {
876
+ lines.push(` Blocker: ${lastSession.blocker}`);
877
+ }
878
+ if (lastSession.gitBranch) {
879
+ lines.push(` Branch: ${lastSession.gitBranch}`);
880
+ }
881
+ if (touchedCount > 0) {
882
+ lines.push(` Worked on ${touchedCount} files on ${lastSession.gitBranch ?? "unknown branch"}`);
883
+ }
884
+ lines.push(" Tip: Use the get_reentry_briefing tool for a full briefing");
885
+ console.log(lines.join("\n"));
886
+ process.exit(0);
887
+ }
888
+ if (process.argv.includes("--save-checkpoint")) {
889
+ const wsPath = process.argv.slice(2).find((a) => !a.startsWith("--")) || process.cwd();
890
+ const reader2 = new KeepGoingReader(wsPath);
891
+ const lastSession = reader2.getLastSession();
892
+ if (lastSession?.timestamp) {
893
+ const ageMs = Date.now() - new Date(lastSession.timestamp).getTime();
894
+ if (ageMs < 2 * 60 * 1e3) {
895
+ process.exit(0);
896
+ }
897
+ }
898
+ const touchedFiles = getTouchedFiles(wsPath);
899
+ const commitHashes = getCommitsSince(wsPath, lastSession?.timestamp);
900
+ if (touchedFiles.length === 0 && commitHashes.length === 0) {
901
+ process.exit(0);
902
+ }
903
+ const gitBranch = getCurrentBranch(wsPath);
904
+ const commitMessages = getCommitMessagesSince(wsPath, lastSession?.timestamp);
905
+ let summary;
906
+ if (commitMessages.length > 0) {
907
+ summary = commitMessages.slice(0, 3).join("; ");
908
+ } else {
909
+ const fileNames = touchedFiles.slice(0, 5).map((f) => path5.basename(f));
910
+ summary = `Worked on ${fileNames.join(", ")}`;
911
+ if (touchedFiles.length > 5) {
912
+ summary += ` and ${touchedFiles.length - 5} more`;
913
+ }
914
+ }
915
+ const projectName = path5.basename(wsPath);
916
+ const checkpoint = createCheckpoint({
917
+ summary,
918
+ nextStep: "",
919
+ gitBranch,
920
+ touchedFiles,
921
+ commitHashes,
922
+ workspaceRoot: wsPath,
923
+ source: "auto"
924
+ });
925
+ const writer = new KeepGoingWriter(wsPath);
926
+ writer.saveCheckpoint(checkpoint, projectName);
927
+ console.log(`[KeepGoing] Auto-checkpoint saved: ${summary}`);
928
+ process.exit(0);
44
929
  }
45
- // Default: start MCP server
46
- // Workspace path can be passed as an argument, otherwise defaults to CWD.
47
- // MCP hosts (Claude Code, etc.) typically launch the server with the project root as CWD.
48
- const workspacePath = process.argv[2] || process.cwd();
49
- const reader = new KeepGoingReader(workspacePath);
50
- const server = new McpServer({
51
- name: 'keepgoing',
52
- version: '0.1.0',
930
+ var workspacePath = process.argv[2] || process.cwd();
931
+ var reader = new KeepGoingReader(workspacePath);
932
+ var server = new McpServer({
933
+ name: "keepgoing",
934
+ version: "0.1.0"
53
935
  });
54
- // Register tools
55
936
  registerGetMomentum(server, reader, workspacePath);
56
937
  registerGetSessionHistory(server, reader);
57
938
  registerGetReentryBriefing(server, reader, workspacePath);
58
- // Register prompts
939
+ registerSaveCheckpoint(server, reader, workspacePath);
940
+ registerSetupProject(server, workspacePath);
59
941
  registerResumePrompt(server);
60
- // Connect via stdio
61
- const transport = new StdioServerTransport();
942
+ var transport = new StdioServerTransport();
62
943
  await server.connect(transport);
63
- console.error('KeepGoing MCP server started');
944
+ console.error("KeepGoing MCP server started");
64
945
  //# sourceMappingURL=index.js.map