@keepgoingdev/mcp-server 0.1.0 → 0.1.2
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/README.md +37 -0
- package/dist/index.js +636 -54
- package/dist/index.js.map +1 -1
- package/package.json +6 -5
- package/dist/index.d.ts +0 -2
- package/dist/printMomentum.d.ts +0 -9
- package/dist/printMomentum.js +0 -40
- package/dist/printMomentum.js.map +0 -1
- package/dist/prompts/resume.d.ts +0 -2
- package/dist/prompts/resume.js +0 -23
- package/dist/prompts/resume.js.map +0 -1
- package/dist/storage.d.ts +0 -40
- package/dist/storage.js +0 -101
- package/dist/storage.js.map +0 -1
- package/dist/tools/getMomentum.d.ts +0 -3
- package/dist/tools/getMomentum.js +0 -66
- package/dist/tools/getMomentum.js.map +0 -1
- package/dist/tools/getReentryBriefing.d.ts +0 -3
- package/dist/tools/getReentryBriefing.js +0 -47
- package/dist/tools/getReentryBriefing.js.map +0 -1
- package/dist/tools/getSessionHistory.d.ts +0 -3
- package/dist/tools/getSessionHistory.js +0 -50
- package/dist/tools/getSessionHistory.js.map +0 -1
- package/dist/types.d.ts +0 -43
- package/dist/types.js +0 -6
- package/dist/types.js.map +0 -1
- package/dist/utils/gitUtils.d.ts +0 -8
- package/dist/utils/gitUtils.js +0 -52
- package/dist/utils/gitUtils.js.map +0 -1
- package/dist/utils/reentry.d.ts +0 -6
- package/dist/utils/reentry.js +0 -197
- package/dist/utils/reentry.js.map +0 -1
- package/dist/utils/timeUtils.d.ts +0 -5
- package/dist/utils/timeUtils.js +0 -47
- package/dist/utils/timeUtils.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,64 +1,646 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
|
|
7
|
+
// src/storage.ts
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
|
|
11
|
+
// ../../packages/shared/src/timeUtils.ts
|
|
12
|
+
function formatRelativeTime(timestamp) {
|
|
13
|
+
const now = Date.now();
|
|
14
|
+
const then = new Date(timestamp).getTime();
|
|
15
|
+
const diffMs = now - then;
|
|
16
|
+
if (isNaN(diffMs)) {
|
|
17
|
+
return "unknown time";
|
|
18
|
+
}
|
|
19
|
+
if (diffMs < 0) {
|
|
20
|
+
return "in the future";
|
|
21
|
+
}
|
|
22
|
+
const seconds = Math.floor(diffMs / 1e3);
|
|
23
|
+
const minutes = Math.floor(seconds / 60);
|
|
24
|
+
const hours = Math.floor(minutes / 60);
|
|
25
|
+
const days = Math.floor(hours / 24);
|
|
26
|
+
const weeks = Math.floor(days / 7);
|
|
27
|
+
const months = Math.floor(days / 30);
|
|
28
|
+
const years = Math.floor(days / 365);
|
|
29
|
+
if (seconds < 10) {
|
|
30
|
+
return "just now";
|
|
31
|
+
} else if (seconds < 60) {
|
|
32
|
+
return `${seconds} seconds ago`;
|
|
33
|
+
} else if (minutes < 60) {
|
|
34
|
+
return minutes === 1 ? "1 minute ago" : `${minutes} minutes ago`;
|
|
35
|
+
} else if (hours < 24) {
|
|
36
|
+
return hours === 1 ? "1 hour ago" : `${hours} hours ago`;
|
|
37
|
+
} else if (days < 7) {
|
|
38
|
+
return days === 1 ? "1 day ago" : `${days} days ago`;
|
|
39
|
+
} else if (weeks < 4) {
|
|
40
|
+
return weeks === 1 ? "1 week ago" : `${weeks} weeks ago`;
|
|
41
|
+
} else if (months < 12) {
|
|
42
|
+
return months === 1 ? "1 month ago" : `${months} months ago`;
|
|
43
|
+
} else {
|
|
44
|
+
return years === 1 ? "1 year ago" : `${years} years ago`;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ../../packages/shared/src/gitUtils.ts
|
|
49
|
+
import { execFileSync, execFile } from "child_process";
|
|
50
|
+
import { promisify } from "util";
|
|
51
|
+
var execFileAsync = promisify(execFile);
|
|
52
|
+
function getCurrentBranch(workspacePath2) {
|
|
53
|
+
try {
|
|
54
|
+
const result = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
55
|
+
cwd: workspacePath2,
|
|
56
|
+
encoding: "utf-8",
|
|
57
|
+
timeout: 5e3
|
|
58
|
+
});
|
|
59
|
+
return result.trim() || void 0;
|
|
60
|
+
} catch {
|
|
61
|
+
return void 0;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function getGitLogSince(workspacePath2, format, sinceTimestamp) {
|
|
65
|
+
try {
|
|
66
|
+
const since = sinceTimestamp || new Date(Date.now() - 24 * 60 * 60 * 1e3).toISOString();
|
|
67
|
+
const result = execFileSync(
|
|
68
|
+
"git",
|
|
69
|
+
["log", `--since=${since}`, `--format=${format}`],
|
|
70
|
+
{
|
|
71
|
+
cwd: workspacePath2,
|
|
72
|
+
encoding: "utf-8",
|
|
73
|
+
timeout: 5e3
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
if (!result.trim()) {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
return result.trim().split("\n").filter((line) => line.length > 0);
|
|
80
|
+
} catch {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function getCommitMessagesSince(workspacePath2, sinceTimestamp) {
|
|
85
|
+
return getGitLogSince(workspacePath2, "%s", sinceTimestamp);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ../../packages/shared/src/reentry.ts
|
|
89
|
+
var RECENT_SESSION_COUNT = 5;
|
|
90
|
+
function generateBriefing(lastSession, recentSessions, projectState, gitBranch, recentCommitMessages) {
|
|
91
|
+
if (!lastSession) {
|
|
92
|
+
return void 0;
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
lastWorked: formatRelativeTime(lastSession.timestamp),
|
|
96
|
+
currentFocus: buildCurrentFocus(lastSession, projectState, gitBranch),
|
|
97
|
+
recentActivity: buildRecentActivity(
|
|
98
|
+
lastSession,
|
|
99
|
+
recentSessions,
|
|
100
|
+
recentCommitMessages
|
|
101
|
+
),
|
|
102
|
+
suggestedNext: buildSuggestedNext(lastSession, gitBranch),
|
|
103
|
+
smallNextStep: buildSmallNextStep(
|
|
104
|
+
lastSession,
|
|
105
|
+
gitBranch,
|
|
106
|
+
recentCommitMessages
|
|
107
|
+
)
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function getRecentSessions(allSessions, count = RECENT_SESSION_COUNT) {
|
|
111
|
+
return allSessions.slice(-count).reverse();
|
|
112
|
+
}
|
|
113
|
+
function buildCurrentFocus(lastSession, projectState, gitBranch) {
|
|
114
|
+
if (projectState.derivedCurrentFocus) {
|
|
115
|
+
return projectState.derivedCurrentFocus;
|
|
116
|
+
}
|
|
117
|
+
const branchFocus = inferFocusFromBranch(gitBranch);
|
|
118
|
+
if (branchFocus) {
|
|
119
|
+
return branchFocus;
|
|
120
|
+
}
|
|
121
|
+
if (lastSession.summary) {
|
|
122
|
+
return lastSession.summary;
|
|
123
|
+
}
|
|
124
|
+
if (lastSession.touchedFiles.length > 0) {
|
|
125
|
+
return inferFocusFromFiles(lastSession.touchedFiles);
|
|
126
|
+
}
|
|
127
|
+
return "Unknown, save a checkpoint to set context";
|
|
128
|
+
}
|
|
129
|
+
function buildRecentActivity(lastSession, recentSessions, recentCommitMessages) {
|
|
130
|
+
const parts = [];
|
|
131
|
+
const sessionCount = recentSessions.length;
|
|
132
|
+
if (sessionCount > 1) {
|
|
133
|
+
parts.push(`${sessionCount} recent sessions`);
|
|
134
|
+
} else if (sessionCount === 1) {
|
|
135
|
+
parts.push("1 recent session");
|
|
136
|
+
}
|
|
137
|
+
if (lastSession.summary) {
|
|
138
|
+
parts.push(`Last: ${lastSession.summary}`);
|
|
139
|
+
}
|
|
140
|
+
if (lastSession.touchedFiles.length > 0) {
|
|
141
|
+
parts.push(`${lastSession.touchedFiles.length} files touched`);
|
|
142
|
+
}
|
|
143
|
+
if (recentCommitMessages && recentCommitMessages.length > 0) {
|
|
144
|
+
parts.push(`${recentCommitMessages.length} recent commits`);
|
|
145
|
+
}
|
|
146
|
+
return parts.length > 0 ? parts.join(". ") : "No recent activity recorded";
|
|
147
|
+
}
|
|
148
|
+
function buildSuggestedNext(lastSession, gitBranch) {
|
|
149
|
+
if (lastSession.nextStep) {
|
|
150
|
+
return lastSession.nextStep;
|
|
151
|
+
}
|
|
152
|
+
const branchFocus = inferFocusFromBranch(gitBranch);
|
|
153
|
+
if (branchFocus) {
|
|
154
|
+
return `Continue working on ${branchFocus}`;
|
|
155
|
+
}
|
|
156
|
+
if (lastSession.touchedFiles.length > 0) {
|
|
157
|
+
return `Continue working on ${inferFocusFromFiles(lastSession.touchedFiles)}`;
|
|
158
|
+
}
|
|
159
|
+
return "Save a checkpoint to track your next step";
|
|
160
|
+
}
|
|
161
|
+
function buildSmallNextStep(lastSession, gitBranch, recentCommitMessages) {
|
|
162
|
+
const fallback = "Review last changed files to resume flow";
|
|
163
|
+
if (lastSession.nextStep) {
|
|
164
|
+
const distilled = distillToSmallStep(
|
|
165
|
+
lastSession.nextStep,
|
|
166
|
+
lastSession.touchedFiles
|
|
167
|
+
);
|
|
168
|
+
if (distilled) {
|
|
169
|
+
return distilled;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (recentCommitMessages && recentCommitMessages.length > 0) {
|
|
173
|
+
const commitStep = deriveStepFromCommits(recentCommitMessages);
|
|
174
|
+
if (commitStep) {
|
|
175
|
+
return commitStep;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (lastSession.touchedFiles.length > 0) {
|
|
179
|
+
const fileStep = deriveStepFromFiles(lastSession.touchedFiles);
|
|
180
|
+
if (fileStep) {
|
|
181
|
+
return fileStep;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const branchFocus = inferFocusFromBranch(gitBranch);
|
|
185
|
+
if (branchFocus) {
|
|
186
|
+
return `Check git status for ${branchFocus}`;
|
|
187
|
+
}
|
|
188
|
+
return fallback;
|
|
189
|
+
}
|
|
190
|
+
function distillToSmallStep(nextStep, touchedFiles) {
|
|
191
|
+
if (!nextStep.trim()) {
|
|
192
|
+
return void 0;
|
|
193
|
+
}
|
|
194
|
+
const words = nextStep.trim().split(/\s+/);
|
|
195
|
+
if (words.length <= 12) {
|
|
196
|
+
if (touchedFiles.length > 0 && !mentionsFile(nextStep)) {
|
|
197
|
+
const primaryFile = getPrimaryFileName(touchedFiles);
|
|
198
|
+
const enhanced = `${nextStep.trim()} in ${primaryFile}`;
|
|
199
|
+
if (enhanced.split(/\s+/).length <= 12) {
|
|
200
|
+
return enhanced;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return nextStep.trim();
|
|
204
|
+
}
|
|
205
|
+
return words.slice(0, 12).join(" ");
|
|
206
|
+
}
|
|
207
|
+
function deriveStepFromCommits(commitMessages) {
|
|
208
|
+
const lastCommit = commitMessages[0];
|
|
209
|
+
if (!lastCommit || !lastCommit.trim()) {
|
|
210
|
+
return void 0;
|
|
211
|
+
}
|
|
212
|
+
const wipPattern = /^(?:wip|work in progress|started?|begin|draft)[:\s]/i;
|
|
213
|
+
if (wipPattern.test(lastCommit)) {
|
|
214
|
+
const topic = lastCommit.replace(wipPattern, "").trim();
|
|
215
|
+
if (topic) {
|
|
216
|
+
const words = topic.split(/\s+/).slice(0, 8).join(" ");
|
|
217
|
+
return `Continue ${words}`;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return void 0;
|
|
221
|
+
}
|
|
222
|
+
function deriveStepFromFiles(files) {
|
|
223
|
+
const primaryFile = getPrimaryFileName(files);
|
|
224
|
+
if (files.length > 1) {
|
|
225
|
+
return `Open ${primaryFile} and review ${files.length} changed files`;
|
|
226
|
+
}
|
|
227
|
+
return `Open ${primaryFile} and pick up where you left off`;
|
|
228
|
+
}
|
|
229
|
+
function getPrimaryFileName(files) {
|
|
230
|
+
const sourceFiles = files.filter((f) => {
|
|
231
|
+
const lower = f.toLowerCase();
|
|
232
|
+
return !lower.includes("test") && !lower.includes("spec") && !lower.includes(".config") && !lower.includes("package.json") && !lower.includes("tsconfig");
|
|
233
|
+
});
|
|
234
|
+
const target = sourceFiles.length > 0 ? sourceFiles[0] : files[0];
|
|
235
|
+
const parts = target.replace(/\\/g, "/").split("/");
|
|
236
|
+
return parts[parts.length - 1];
|
|
237
|
+
}
|
|
238
|
+
function mentionsFile(text) {
|
|
239
|
+
return /\w+\.(?:ts|tsx|js|jsx|py|go|rs|java|rb|css|scss|html|json|yaml|yml|md|sql|sh)\b/i.test(
|
|
240
|
+
text
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
function inferFocusFromBranch(branch) {
|
|
244
|
+
if (!branch || branch === "main" || branch === "master" || branch === "develop" || branch === "HEAD") {
|
|
245
|
+
return void 0;
|
|
246
|
+
}
|
|
247
|
+
const prefixPattern = /^(?:feature|feat|fix|bugfix|hotfix|chore|refactor|docs|test|ci)\//i;
|
|
248
|
+
const isFix = /^(?:fix|bugfix|hotfix)\//i.test(branch);
|
|
249
|
+
const stripped = branch.replace(prefixPattern, "");
|
|
250
|
+
const cleaned = stripped.replace(/[-_/]/g, " ").replace(/^\d+\s*/, "").trim();
|
|
251
|
+
if (!cleaned) {
|
|
252
|
+
return void 0;
|
|
253
|
+
}
|
|
254
|
+
return isFix ? `${cleaned} fix` : cleaned;
|
|
255
|
+
}
|
|
256
|
+
function inferFocusFromFiles(files) {
|
|
257
|
+
if (files.length === 0) {
|
|
258
|
+
return "unknown files";
|
|
259
|
+
}
|
|
260
|
+
const dirs = files.map((f) => {
|
|
261
|
+
const parts = f.replace(/\\/g, "/").split("/");
|
|
262
|
+
return parts.length > 1 ? parts.slice(0, -1).join("/") : "";
|
|
263
|
+
}).filter((d) => d.length > 0);
|
|
264
|
+
if (dirs.length > 0) {
|
|
265
|
+
const counts = /* @__PURE__ */ new Map();
|
|
266
|
+
for (const dir of dirs) {
|
|
267
|
+
counts.set(dir, (counts.get(dir) ?? 0) + 1);
|
|
268
|
+
}
|
|
269
|
+
let topDir = "";
|
|
270
|
+
let topCount = 0;
|
|
271
|
+
for (const [dir, count] of counts) {
|
|
272
|
+
if (count > topCount) {
|
|
273
|
+
topDir = dir;
|
|
274
|
+
topCount = count;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (topDir) {
|
|
278
|
+
return `files in ${topDir}`;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const names = files.slice(0, 3).map((f) => {
|
|
282
|
+
const parts = f.replace(/\\/g, "/").split("/");
|
|
283
|
+
return parts[parts.length - 1];
|
|
284
|
+
});
|
|
285
|
+
return names.join(", ");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// src/storage.ts
|
|
289
|
+
var STORAGE_DIR = ".keepgoing";
|
|
290
|
+
var META_FILE = "meta.json";
|
|
291
|
+
var SESSIONS_FILE = "sessions.json";
|
|
292
|
+
var STATE_FILE = "state.json";
|
|
293
|
+
var KeepGoingReader = class {
|
|
294
|
+
storagePath;
|
|
295
|
+
metaFilePath;
|
|
296
|
+
sessionsFilePath;
|
|
297
|
+
stateFilePath;
|
|
298
|
+
constructor(workspacePath2) {
|
|
299
|
+
this.storagePath = path.join(workspacePath2, STORAGE_DIR);
|
|
300
|
+
this.metaFilePath = path.join(this.storagePath, META_FILE);
|
|
301
|
+
this.sessionsFilePath = path.join(this.storagePath, SESSIONS_FILE);
|
|
302
|
+
this.stateFilePath = path.join(this.storagePath, STATE_FILE);
|
|
303
|
+
}
|
|
304
|
+
/** Check if .keepgoing/ directory exists. */
|
|
305
|
+
exists() {
|
|
306
|
+
return fs.existsSync(this.storagePath);
|
|
307
|
+
}
|
|
308
|
+
/** Read state.json, returns undefined if missing or corrupt. */
|
|
309
|
+
getState() {
|
|
310
|
+
return this.readJsonFile(this.stateFilePath);
|
|
311
|
+
}
|
|
312
|
+
/** Read meta.json, returns undefined if missing or corrupt. */
|
|
313
|
+
getMeta() {
|
|
314
|
+
return this.readJsonFile(this.metaFilePath);
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Read sessions from sessions.json.
|
|
318
|
+
* Handles both formats:
|
|
319
|
+
* - Flat array: SessionCheckpoint[] (from ProjectStorage)
|
|
320
|
+
* - Wrapper object: ProjectSessions (from SessionStorage)
|
|
321
|
+
*/
|
|
322
|
+
getSessions() {
|
|
323
|
+
return this.parseSessions().sessions;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Get the most recent session checkpoint.
|
|
327
|
+
* Uses state.lastSessionId if available, falls back to last in array.
|
|
328
|
+
*/
|
|
329
|
+
getLastSession() {
|
|
330
|
+
const { sessions, wrapperLastSessionId } = this.parseSessions();
|
|
331
|
+
if (sessions.length === 0) {
|
|
332
|
+
return void 0;
|
|
333
|
+
}
|
|
334
|
+
const state = this.getState();
|
|
335
|
+
if (state?.lastSessionId) {
|
|
336
|
+
const found = sessions.find((s) => s.id === state.lastSessionId);
|
|
337
|
+
if (found) {
|
|
338
|
+
return found;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (wrapperLastSessionId) {
|
|
342
|
+
const found = sessions.find((s) => s.id === wrapperLastSessionId);
|
|
343
|
+
if (found) {
|
|
344
|
+
return found;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return sessions[sessions.length - 1];
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Returns the last N sessions, newest first.
|
|
351
|
+
*/
|
|
352
|
+
getRecentSessions(count) {
|
|
353
|
+
return getRecentSessions(this.getSessions(), count);
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Parses sessions.json once, returning both the session list
|
|
357
|
+
* and the optional lastSessionId from a ProjectSessions wrapper.
|
|
358
|
+
*/
|
|
359
|
+
parseSessions() {
|
|
360
|
+
const raw = this.readJsonFile(
|
|
361
|
+
this.sessionsFilePath
|
|
362
|
+
);
|
|
363
|
+
if (!raw) {
|
|
364
|
+
return { sessions: [] };
|
|
365
|
+
}
|
|
366
|
+
if (Array.isArray(raw)) {
|
|
367
|
+
return { sessions: raw };
|
|
368
|
+
}
|
|
369
|
+
return { sessions: raw.sessions ?? [], wrapperLastSessionId: raw.lastSessionId };
|
|
370
|
+
}
|
|
371
|
+
readJsonFile(filePath) {
|
|
372
|
+
try {
|
|
373
|
+
if (!fs.existsSync(filePath)) {
|
|
374
|
+
return void 0;
|
|
375
|
+
}
|
|
376
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
377
|
+
return JSON.parse(raw);
|
|
378
|
+
} catch {
|
|
379
|
+
return void 0;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
// src/tools/getMomentum.ts
|
|
385
|
+
function registerGetMomentum(server2, reader2, workspacePath2) {
|
|
386
|
+
server2.tool(
|
|
387
|
+
"get_momentum",
|
|
388
|
+
"Get current developer momentum: last checkpoint, next step, blockers, and branch context. Use this to understand where the developer left off.",
|
|
389
|
+
{},
|
|
390
|
+
async () => {
|
|
391
|
+
if (!reader2.exists()) {
|
|
392
|
+
return {
|
|
393
|
+
content: [
|
|
394
|
+
{
|
|
395
|
+
type: "text",
|
|
396
|
+
text: "No KeepGoing data found. The developer has not saved any checkpoints yet."
|
|
397
|
+
}
|
|
398
|
+
]
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
const lastSession = reader2.getLastSession();
|
|
402
|
+
if (!lastSession) {
|
|
403
|
+
return {
|
|
404
|
+
content: [
|
|
405
|
+
{
|
|
406
|
+
type: "text",
|
|
407
|
+
text: "KeepGoing is set up but no session checkpoints exist yet."
|
|
408
|
+
}
|
|
409
|
+
]
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
const state = reader2.getState();
|
|
413
|
+
const currentBranch = getCurrentBranch(workspacePath2);
|
|
414
|
+
const branchChanged = lastSession.gitBranch && currentBranch && lastSession.gitBranch !== currentBranch;
|
|
415
|
+
const lines = [
|
|
416
|
+
`## Developer Momentum`,
|
|
417
|
+
"",
|
|
418
|
+
`**Last checkpoint:** ${formatRelativeTime(lastSession.timestamp)}`,
|
|
419
|
+
`**Summary:** ${lastSession.summary || "No summary"}`,
|
|
420
|
+
`**Next step:** ${lastSession.nextStep || "Not specified"}`
|
|
421
|
+
];
|
|
422
|
+
if (lastSession.blocker) {
|
|
423
|
+
lines.push(`**Blocker:** ${lastSession.blocker}`);
|
|
424
|
+
}
|
|
425
|
+
if (lastSession.projectIntent) {
|
|
426
|
+
lines.push(`**Project intent:** ${lastSession.projectIntent}`);
|
|
427
|
+
}
|
|
428
|
+
lines.push("");
|
|
429
|
+
if (currentBranch) {
|
|
430
|
+
lines.push(`**Current branch:** ${currentBranch}`);
|
|
431
|
+
}
|
|
432
|
+
if (branchChanged) {
|
|
433
|
+
lines.push(
|
|
434
|
+
`**Note:** Branch changed since last checkpoint (was \`${lastSession.gitBranch}\`, now \`${currentBranch}\`)`
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
if (lastSession.touchedFiles.length > 0) {
|
|
438
|
+
lines.push("");
|
|
439
|
+
lines.push(
|
|
440
|
+
`**Files touched (${lastSession.touchedFiles.length}):** ${lastSession.touchedFiles.slice(0, 10).join(", ")}`
|
|
441
|
+
);
|
|
442
|
+
if (lastSession.touchedFiles.length > 10) {
|
|
443
|
+
lines.push(
|
|
444
|
+
` ...and ${lastSession.touchedFiles.length - 10} more`
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
if (state?.derivedCurrentFocus) {
|
|
449
|
+
lines.push("");
|
|
450
|
+
lines.push(`**Derived focus:** ${state.derivedCurrentFocus}`);
|
|
451
|
+
}
|
|
452
|
+
return {
|
|
453
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// src/tools/getSessionHistory.ts
|
|
460
|
+
import { z } from "zod";
|
|
461
|
+
function registerGetSessionHistory(server2, reader2) {
|
|
462
|
+
server2.tool(
|
|
463
|
+
"get_session_history",
|
|
464
|
+
"Get recent session checkpoints. Returns a chronological list of what the developer worked on.",
|
|
465
|
+
{ limit: z.number().min(1).max(50).default(5).describe("Number of recent sessions to return (1-50, default 5)") },
|
|
466
|
+
async ({ limit }) => {
|
|
467
|
+
if (!reader2.exists()) {
|
|
468
|
+
return {
|
|
469
|
+
content: [
|
|
470
|
+
{
|
|
471
|
+
type: "text",
|
|
472
|
+
text: "No KeepGoing data found."
|
|
473
|
+
}
|
|
474
|
+
]
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
const sessions = reader2.getRecentSessions(limit);
|
|
478
|
+
if (sessions.length === 0) {
|
|
479
|
+
return {
|
|
480
|
+
content: [
|
|
481
|
+
{
|
|
482
|
+
type: "text",
|
|
483
|
+
text: "No session checkpoints found."
|
|
484
|
+
}
|
|
485
|
+
]
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
const lines = [
|
|
489
|
+
`## Session History (last ${sessions.length})`,
|
|
490
|
+
""
|
|
491
|
+
];
|
|
492
|
+
for (const session of sessions) {
|
|
493
|
+
lines.push(`### ${formatRelativeTime(session.timestamp)}`);
|
|
494
|
+
lines.push(`- **Summary:** ${session.summary || "No summary"}`);
|
|
495
|
+
lines.push(`- **Next step:** ${session.nextStep || "Not specified"}`);
|
|
496
|
+
if (session.blocker) {
|
|
497
|
+
lines.push(`- **Blocker:** ${session.blocker}`);
|
|
498
|
+
}
|
|
499
|
+
if (session.gitBranch) {
|
|
500
|
+
lines.push(`- **Branch:** ${session.gitBranch}`);
|
|
501
|
+
}
|
|
502
|
+
if (session.touchedFiles.length > 0) {
|
|
503
|
+
lines.push(
|
|
504
|
+
`- **Files:** ${session.touchedFiles.slice(0, 5).join(", ")}${session.touchedFiles.length > 5 ? ` (+${session.touchedFiles.length - 5} more)` : ""}`
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
lines.push("");
|
|
508
|
+
}
|
|
509
|
+
return {
|
|
510
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// src/tools/getReentryBriefing.ts
|
|
517
|
+
function registerGetReentryBriefing(server2, reader2, workspacePath2) {
|
|
518
|
+
server2.tool(
|
|
519
|
+
"get_reentry_briefing",
|
|
520
|
+
"Get a synthesized re-entry briefing that helps a developer understand where they left off. Includes focus, recent activity, and suggested next steps.",
|
|
521
|
+
{},
|
|
522
|
+
async () => {
|
|
523
|
+
if (!reader2.exists()) {
|
|
524
|
+
return {
|
|
525
|
+
content: [
|
|
526
|
+
{
|
|
527
|
+
type: "text",
|
|
528
|
+
text: "No KeepGoing data found. The developer has not saved any checkpoints yet."
|
|
529
|
+
}
|
|
530
|
+
]
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
const lastSession = reader2.getLastSession();
|
|
534
|
+
const recentSessions = reader2.getRecentSessions(5);
|
|
535
|
+
const state = reader2.getState() ?? {};
|
|
536
|
+
const gitBranch = getCurrentBranch(workspacePath2);
|
|
537
|
+
const sinceTimestamp = lastSession?.timestamp;
|
|
538
|
+
const recentCommits = sinceTimestamp ? getCommitMessagesSince(workspacePath2, sinceTimestamp) : [];
|
|
539
|
+
const briefing = generateBriefing(
|
|
540
|
+
lastSession,
|
|
541
|
+
recentSessions,
|
|
542
|
+
state,
|
|
543
|
+
gitBranch,
|
|
544
|
+
recentCommits
|
|
545
|
+
);
|
|
546
|
+
if (!briefing) {
|
|
547
|
+
return {
|
|
548
|
+
content: [
|
|
549
|
+
{
|
|
550
|
+
type: "text",
|
|
551
|
+
text: "No session data available to generate a briefing."
|
|
552
|
+
}
|
|
553
|
+
]
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
const lines = [
|
|
557
|
+
`## Re-entry Briefing`,
|
|
558
|
+
"",
|
|
559
|
+
`**Last worked:** ${briefing.lastWorked}`,
|
|
560
|
+
`**Current focus:** ${briefing.currentFocus}`,
|
|
561
|
+
`**Recent activity:** ${briefing.recentActivity}`,
|
|
562
|
+
`**Suggested next:** ${briefing.suggestedNext}`,
|
|
563
|
+
`**Quick start:** ${briefing.smallNextStep}`
|
|
564
|
+
];
|
|
565
|
+
return {
|
|
566
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// src/prompts/resume.ts
|
|
573
|
+
function registerResumePrompt(server2) {
|
|
574
|
+
server2.prompt(
|
|
575
|
+
"resume",
|
|
576
|
+
"Check developer momentum and suggest what to work on next",
|
|
577
|
+
async () => ({
|
|
578
|
+
messages: [
|
|
579
|
+
{
|
|
580
|
+
role: "user",
|
|
581
|
+
content: {
|
|
582
|
+
type: "text",
|
|
583
|
+
text: [
|
|
584
|
+
"I just opened this project and want to pick up where I left off.",
|
|
585
|
+
"",
|
|
586
|
+
"Please use the KeepGoing tools to:",
|
|
587
|
+
"1. Check my current momentum (get_momentum)",
|
|
588
|
+
"2. Get a re-entry briefing (get_reentry_briefing)",
|
|
589
|
+
"3. Based on the results, give me a concise summary of where I left off and suggest what to work on next.",
|
|
590
|
+
"",
|
|
591
|
+
"Keep your response brief and actionable."
|
|
592
|
+
].join("\n")
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
]
|
|
596
|
+
})
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// src/index.ts
|
|
601
|
+
if (process.argv.includes("--print-momentum")) {
|
|
602
|
+
const wsPath = process.argv.slice(2).find((a) => a !== "--print-momentum") || process.cwd();
|
|
603
|
+
const reader2 = new KeepGoingReader(wsPath);
|
|
604
|
+
if (!reader2.exists()) {
|
|
605
|
+
process.exit(0);
|
|
606
|
+
}
|
|
607
|
+
const lastSession = reader2.getLastSession();
|
|
608
|
+
if (!lastSession) {
|
|
43
609
|
process.exit(0);
|
|
610
|
+
}
|
|
611
|
+
const touchedCount = lastSession.touchedFiles?.length ?? 0;
|
|
612
|
+
const lines = [];
|
|
613
|
+
lines.push(`[KeepGoing] Last checkpoint: ${formatRelativeTime(lastSession.timestamp)}`);
|
|
614
|
+
if (lastSession.summary) {
|
|
615
|
+
lines.push(` Summary: ${lastSession.summary}`);
|
|
616
|
+
}
|
|
617
|
+
if (lastSession.nextStep) {
|
|
618
|
+
lines.push(` Next step: ${lastSession.nextStep}`);
|
|
619
|
+
}
|
|
620
|
+
if (lastSession.blocker) {
|
|
621
|
+
lines.push(` Blocker: ${lastSession.blocker}`);
|
|
622
|
+
}
|
|
623
|
+
if (lastSession.gitBranch) {
|
|
624
|
+
lines.push(` Branch: ${lastSession.gitBranch}`);
|
|
625
|
+
}
|
|
626
|
+
if (touchedCount > 0) {
|
|
627
|
+
lines.push(` Worked on ${touchedCount} files on ${lastSession.gitBranch ?? "unknown branch"}`);
|
|
628
|
+
}
|
|
629
|
+
lines.push(" Tip: Use the get_reentry_briefing tool for a full briefing");
|
|
630
|
+
console.log(lines.join("\n"));
|
|
631
|
+
process.exit(0);
|
|
44
632
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const server = new McpServer({
|
|
51
|
-
name: 'keepgoing',
|
|
52
|
-
version: '0.1.0',
|
|
633
|
+
var workspacePath = process.argv[2] || process.cwd();
|
|
634
|
+
var reader = new KeepGoingReader(workspacePath);
|
|
635
|
+
var server = new McpServer({
|
|
636
|
+
name: "keepgoing",
|
|
637
|
+
version: "0.1.0"
|
|
53
638
|
});
|
|
54
|
-
// Register tools
|
|
55
639
|
registerGetMomentum(server, reader, workspacePath);
|
|
56
640
|
registerGetSessionHistory(server, reader);
|
|
57
641
|
registerGetReentryBriefing(server, reader, workspacePath);
|
|
58
|
-
// Register prompts
|
|
59
642
|
registerResumePrompt(server);
|
|
60
|
-
|
|
61
|
-
const transport = new StdioServerTransport();
|
|
643
|
+
var transport = new StdioServerTransport();
|
|
62
644
|
await server.connect(transport);
|
|
63
|
-
console.error(
|
|
645
|
+
console.error("KeepGoing MCP server started");
|
|
64
646
|
//# sourceMappingURL=index.js.map
|