@keepgoingdev/mcp-server 0.5.6 → 0.6.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 +873 -446
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -189,6 +189,62 @@ function getTouchedFiles(workspacePath) {
|
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
// ../../packages/shared/src/briefingTier.ts
|
|
193
|
+
var MODEL_TIERS = {
|
|
194
|
+
// Standard: small/fast models (compact is only reachable via explicit tier or context window)
|
|
195
|
+
"claude-3-haiku": "standard",
|
|
196
|
+
"claude-3.5-haiku": "standard",
|
|
197
|
+
"claude-haiku-4-5": "standard",
|
|
198
|
+
"gpt-4o-mini": "standard",
|
|
199
|
+
"gemini-flash": "standard",
|
|
200
|
+
// Detailed: mid-tier models
|
|
201
|
+
"claude-3.5-sonnet": "detailed",
|
|
202
|
+
"claude-sonnet-4": "detailed",
|
|
203
|
+
"claude-sonnet-4-5": "detailed",
|
|
204
|
+
"claude-sonnet-4-6": "detailed",
|
|
205
|
+
"gpt-4o": "detailed",
|
|
206
|
+
"gemini-pro": "detailed",
|
|
207
|
+
// Full: large context models
|
|
208
|
+
"claude-opus-4": "full",
|
|
209
|
+
"claude-opus-4-5": "full",
|
|
210
|
+
"claude-opus-4-6": "full",
|
|
211
|
+
"o1": "full",
|
|
212
|
+
"o3": "full",
|
|
213
|
+
"gemini-ultra": "full"
|
|
214
|
+
};
|
|
215
|
+
function resolveTier(opts) {
|
|
216
|
+
if (opts?.tier) {
|
|
217
|
+
return opts.tier;
|
|
218
|
+
}
|
|
219
|
+
if (opts?.model) {
|
|
220
|
+
const fromModel = tierFromModelName(opts.model);
|
|
221
|
+
if (fromModel) return fromModel;
|
|
222
|
+
}
|
|
223
|
+
if (opts?.contextWindow !== void 0) {
|
|
224
|
+
return tierFromContextWindow(opts.contextWindow);
|
|
225
|
+
}
|
|
226
|
+
return "standard";
|
|
227
|
+
}
|
|
228
|
+
function tierFromModelName(model) {
|
|
229
|
+
const normalized = model.toLowerCase().trim();
|
|
230
|
+
if (MODEL_TIERS[normalized]) {
|
|
231
|
+
return MODEL_TIERS[normalized];
|
|
232
|
+
}
|
|
233
|
+
const entries = Object.entries(MODEL_TIERS).sort((a, b) => b[0].length - a[0].length);
|
|
234
|
+
for (const [key, tier] of entries) {
|
|
235
|
+
if (normalized.startsWith(key)) {
|
|
236
|
+
return tier;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return void 0;
|
|
240
|
+
}
|
|
241
|
+
function tierFromContextWindow(tokens) {
|
|
242
|
+
if (tokens < 16e3) return "compact";
|
|
243
|
+
if (tokens < 64e3) return "standard";
|
|
244
|
+
if (tokens < 2e5) return "detailed";
|
|
245
|
+
return "full";
|
|
246
|
+
}
|
|
247
|
+
|
|
192
248
|
// ../../packages/shared/src/reentry.ts
|
|
193
249
|
var RECENT_SESSION_COUNT = 5;
|
|
194
250
|
function generateBriefing(lastSession, recentSessions, projectState, gitBranch, recentCommitMessages) {
|
|
@@ -214,6 +270,61 @@ function generateBriefing(lastSession, recentSessions, projectState, gitBranch,
|
|
|
214
270
|
function getRecentSessions(allSessions, count = RECENT_SESSION_COUNT) {
|
|
215
271
|
return allSessions.slice(-count).reverse();
|
|
216
272
|
}
|
|
273
|
+
function generateEnrichedBriefing(opts) {
|
|
274
|
+
const tier = resolveTier({
|
|
275
|
+
tier: opts.tier,
|
|
276
|
+
model: opts.model,
|
|
277
|
+
contextWindow: opts.contextWindow
|
|
278
|
+
});
|
|
279
|
+
const core = generateBriefing(
|
|
280
|
+
opts.lastSession,
|
|
281
|
+
opts.recentSessions,
|
|
282
|
+
opts.projectState,
|
|
283
|
+
opts.gitBranch,
|
|
284
|
+
opts.recentCommits
|
|
285
|
+
);
|
|
286
|
+
if (!core) return void 0;
|
|
287
|
+
const enriched = { tier, core };
|
|
288
|
+
if (tier === "compact") {
|
|
289
|
+
return enriched;
|
|
290
|
+
}
|
|
291
|
+
enriched.blocker = opts.lastSession?.blocker;
|
|
292
|
+
enriched.gitBranch = opts.gitBranch;
|
|
293
|
+
enriched.isWorktree = opts.isWorktree;
|
|
294
|
+
if (tier === "standard") {
|
|
295
|
+
return enriched;
|
|
296
|
+
}
|
|
297
|
+
if (opts.decisions && opts.decisions.length > 0) {
|
|
298
|
+
enriched.decisions = opts.decisions.slice(0, tier === "detailed" ? 3 : 10);
|
|
299
|
+
}
|
|
300
|
+
if (opts.allTouchedFiles && opts.allTouchedFiles.length > 0) {
|
|
301
|
+
enriched.touchedFiles = tier === "detailed" ? opts.allTouchedFiles.slice(0, 10) : opts.allTouchedFiles;
|
|
302
|
+
} else if (opts.lastSession?.touchedFiles && opts.lastSession.touchedFiles.length > 0) {
|
|
303
|
+
enriched.touchedFiles = tier === "detailed" ? opts.lastSession.touchedFiles.slice(0, 10) : opts.lastSession.touchedFiles;
|
|
304
|
+
}
|
|
305
|
+
if (tier === "detailed") {
|
|
306
|
+
return enriched;
|
|
307
|
+
}
|
|
308
|
+
if (opts.allSessions && opts.allSessions.length > 0) {
|
|
309
|
+
const recent = opts.allSessions.slice(-5).reverse();
|
|
310
|
+
enriched.sessionHistory = recent.map((s) => ({
|
|
311
|
+
timestamp: s.timestamp,
|
|
312
|
+
summary: s.summary || "",
|
|
313
|
+
nextStep: s.nextStep || "",
|
|
314
|
+
branch: s.gitBranch
|
|
315
|
+
}));
|
|
316
|
+
}
|
|
317
|
+
if (opts.recentCommits && opts.recentCommits.length > 0) {
|
|
318
|
+
enriched.recentCommits = opts.recentCommits;
|
|
319
|
+
}
|
|
320
|
+
if (opts.fileConflicts && opts.fileConflicts.length > 0) {
|
|
321
|
+
enriched.fileConflicts = opts.fileConflicts;
|
|
322
|
+
}
|
|
323
|
+
if (opts.branchOverlaps && opts.branchOverlaps.length > 0) {
|
|
324
|
+
enriched.branchOverlaps = opts.branchOverlaps;
|
|
325
|
+
}
|
|
326
|
+
return enriched;
|
|
327
|
+
}
|
|
217
328
|
function buildCurrentFocus(lastSession, projectState, gitBranch) {
|
|
218
329
|
if (projectState.derivedCurrentFocus) {
|
|
219
330
|
return projectState.derivedCurrentFocus;
|
|
@@ -389,9 +500,199 @@ function inferFocusFromFiles(files) {
|
|
|
389
500
|
return names.join(", ");
|
|
390
501
|
}
|
|
391
502
|
|
|
503
|
+
// ../../packages/shared/src/briefingFormatter.ts
|
|
504
|
+
function formatEnrichedBriefing(briefing) {
|
|
505
|
+
const { tier, core } = briefing;
|
|
506
|
+
const lines = [];
|
|
507
|
+
lines.push("## Re-entry Briefing");
|
|
508
|
+
lines.push("");
|
|
509
|
+
if (briefing.isWorktree && briefing.gitBranch) {
|
|
510
|
+
lines.push(`**Worktree context:** Scoped to branch \`${briefing.gitBranch}\``);
|
|
511
|
+
lines.push("");
|
|
512
|
+
}
|
|
513
|
+
if (tier === "compact") {
|
|
514
|
+
lines.push(`**Last worked:** ${core.lastWorked}`);
|
|
515
|
+
lines.push(`**Focus:** ${core.currentFocus}`);
|
|
516
|
+
lines.push(`**Quick start:** ${core.smallNextStep}`);
|
|
517
|
+
return lines.join("\n");
|
|
518
|
+
}
|
|
519
|
+
lines.push(`**Last worked:** ${core.lastWorked}`);
|
|
520
|
+
lines.push(`**Current focus:** ${core.currentFocus}`);
|
|
521
|
+
lines.push(`**Recent activity:** ${core.recentActivity}`);
|
|
522
|
+
lines.push(`**Suggested next:** ${core.suggestedNext}`);
|
|
523
|
+
lines.push(`**Quick start:** ${core.smallNextStep}`);
|
|
524
|
+
if (briefing.blocker) {
|
|
525
|
+
lines.push(`**Blocker:** ${briefing.blocker}`);
|
|
526
|
+
}
|
|
527
|
+
if (briefing.decisions && briefing.decisions.length > 0) {
|
|
528
|
+
lines.push("");
|
|
529
|
+
lines.push("### Recent decisions");
|
|
530
|
+
for (const d of briefing.decisions) {
|
|
531
|
+
const rationale = d.rationale ? ` - ${d.rationale}` : "";
|
|
532
|
+
lines.push(`- **${d.classification.category}:** ${d.commitMessage}${rationale}`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (briefing.touchedFiles && briefing.touchedFiles.length > 0) {
|
|
536
|
+
lines.push("");
|
|
537
|
+
lines.push(`### Files touched (${briefing.touchedFiles.length})`);
|
|
538
|
+
for (const f of briefing.touchedFiles) {
|
|
539
|
+
lines.push(`- ${f}`);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (briefing.sessionHistory && briefing.sessionHistory.length > 0) {
|
|
543
|
+
lines.push("");
|
|
544
|
+
lines.push("### Session history");
|
|
545
|
+
for (const s of briefing.sessionHistory) {
|
|
546
|
+
const relTime = formatRelativeTime(s.timestamp);
|
|
547
|
+
const branch = s.branch ? ` (${s.branch})` : "";
|
|
548
|
+
lines.push(`- **${relTime}${branch}:** ${s.summary || "No summary"}. Next: ${s.nextStep || "Not specified"}`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if (briefing.recentCommits && briefing.recentCommits.length > 0) {
|
|
552
|
+
lines.push("");
|
|
553
|
+
lines.push("### Recent commits");
|
|
554
|
+
for (const msg of briefing.recentCommits.slice(0, 10)) {
|
|
555
|
+
lines.push(`- ${msg}`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
if (briefing.fileConflicts && briefing.fileConflicts.length > 0) {
|
|
559
|
+
lines.push("");
|
|
560
|
+
lines.push("### File conflicts");
|
|
561
|
+
lines.push("Multiple sessions are editing the same files:");
|
|
562
|
+
for (const c of briefing.fileConflicts) {
|
|
563
|
+
const sessionLabels = c.sessions.map((s) => s.agentLabel || s.sessionId).join(", ");
|
|
564
|
+
lines.push(`- \`${c.file}\`: ${sessionLabels}`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (briefing.branchOverlaps && briefing.branchOverlaps.length > 0) {
|
|
568
|
+
lines.push("");
|
|
569
|
+
lines.push("### Branch overlaps");
|
|
570
|
+
lines.push("Multiple sessions on the same branch:");
|
|
571
|
+
for (const o of briefing.branchOverlaps) {
|
|
572
|
+
const sessionLabels = o.sessions.map((s) => s.agentLabel || s.sessionId).join(", ");
|
|
573
|
+
lines.push(`- \`${o.branch}\`: ${sessionLabels}`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return lines.join("\n");
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// ../../packages/shared/src/continueOn.ts
|
|
580
|
+
import path2 from "path";
|
|
581
|
+
function gatherContinueOnContext(reader, workspacePath) {
|
|
582
|
+
const projectName = path2.basename(workspacePath);
|
|
583
|
+
const gitBranch = reader.getCurrentBranch();
|
|
584
|
+
if (!reader.exists()) {
|
|
585
|
+
return {
|
|
586
|
+
projectName,
|
|
587
|
+
gitBranch,
|
|
588
|
+
recentCheckpoints: [],
|
|
589
|
+
recentDecisions: [],
|
|
590
|
+
currentTasks: [],
|
|
591
|
+
recentCommitMessages: []
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
const { session: lastCheckpoint } = reader.getScopedLastSession();
|
|
595
|
+
const recentCheckpoints = reader.getScopedRecentSessions(5);
|
|
596
|
+
const recentDecisions = reader.getScopedRecentDecisions(3);
|
|
597
|
+
const currentTasks = reader.getCurrentTasks();
|
|
598
|
+
const state = reader.getState() ?? {};
|
|
599
|
+
const sinceTimestamp = lastCheckpoint?.timestamp;
|
|
600
|
+
const recentCommitMessages = sinceTimestamp ? getCommitMessagesSince(workspacePath, sinceTimestamp) : [];
|
|
601
|
+
const briefing = generateBriefing(
|
|
602
|
+
lastCheckpoint,
|
|
603
|
+
recentCheckpoints,
|
|
604
|
+
state,
|
|
605
|
+
gitBranch,
|
|
606
|
+
recentCommitMessages
|
|
607
|
+
);
|
|
608
|
+
return {
|
|
609
|
+
projectName,
|
|
610
|
+
gitBranch,
|
|
611
|
+
briefing,
|
|
612
|
+
lastCheckpoint,
|
|
613
|
+
recentCheckpoints,
|
|
614
|
+
recentDecisions,
|
|
615
|
+
currentTasks,
|
|
616
|
+
recentCommitMessages
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
function formatContinueOnPrompt(context, options) {
|
|
620
|
+
const opts = { includeCommits: true, includeFiles: true, ...options };
|
|
621
|
+
const lines = [];
|
|
622
|
+
lines.push(`# Project Context: ${context.projectName}`);
|
|
623
|
+
lines.push("");
|
|
624
|
+
lines.push("I'm continuing work on a project. Here is my development context from KeepGoing.");
|
|
625
|
+
lines.push("");
|
|
626
|
+
lines.push("## Current Status");
|
|
627
|
+
if (context.gitBranch) {
|
|
628
|
+
lines.push(`- **Branch:** ${context.gitBranch}`);
|
|
629
|
+
}
|
|
630
|
+
if (context.briefing) {
|
|
631
|
+
lines.push(`- **Last worked:** ${context.briefing.lastWorked}`);
|
|
632
|
+
lines.push(`- **Focus:** ${context.briefing.currentFocus}`);
|
|
633
|
+
} else if (context.lastCheckpoint) {
|
|
634
|
+
lines.push(`- **Last worked:** ${formatRelativeTime(context.lastCheckpoint.timestamp)}`);
|
|
635
|
+
if (context.lastCheckpoint.summary) {
|
|
636
|
+
lines.push(`- **Focus:** ${context.lastCheckpoint.summary}`);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
if (context.lastCheckpoint) {
|
|
640
|
+
lines.push("");
|
|
641
|
+
lines.push("## Last Session");
|
|
642
|
+
if (context.lastCheckpoint.summary) {
|
|
643
|
+
lines.push(`- **Summary:** ${context.lastCheckpoint.summary}`);
|
|
644
|
+
}
|
|
645
|
+
if (context.lastCheckpoint.nextStep) {
|
|
646
|
+
lines.push(`- **Next step:** ${context.lastCheckpoint.nextStep}`);
|
|
647
|
+
}
|
|
648
|
+
if (context.lastCheckpoint.blocker) {
|
|
649
|
+
lines.push(`- **Blocker:** ${context.lastCheckpoint.blocker}`);
|
|
650
|
+
}
|
|
651
|
+
if (opts.includeFiles && context.lastCheckpoint.touchedFiles.length > 0) {
|
|
652
|
+
const files = context.lastCheckpoint.touchedFiles.slice(0, 10).join(", ");
|
|
653
|
+
const extra = context.lastCheckpoint.touchedFiles.length > 10 ? ` (+${context.lastCheckpoint.touchedFiles.length - 10} more)` : "";
|
|
654
|
+
lines.push(`- **Files touched:** ${files}${extra}`);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
const activeTasks = context.currentTasks.filter((t) => t.sessionActive);
|
|
658
|
+
if (activeTasks.length > 0) {
|
|
659
|
+
lines.push("");
|
|
660
|
+
lines.push("## Active Sessions");
|
|
661
|
+
for (const task of activeTasks) {
|
|
662
|
+
const label = task.agentLabel || "Unknown agent";
|
|
663
|
+
const branch = task.branch ? ` on \`${task.branch}\`` : "";
|
|
664
|
+
lines.push(`- **${label}**${branch}: ${task.taskSummary || "Working"}`);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
if (context.recentDecisions.length > 0) {
|
|
668
|
+
lines.push("");
|
|
669
|
+
lines.push("## Recent Decisions");
|
|
670
|
+
for (const d of context.recentDecisions) {
|
|
671
|
+
lines.push(`- ${d.classification.category}: ${d.commitMessage}`);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
if (opts.includeCommits && context.recentCommitMessages.length > 0) {
|
|
675
|
+
lines.push("");
|
|
676
|
+
lines.push("## Recent Commits");
|
|
677
|
+
const commits = context.recentCommitMessages.slice(0, 10);
|
|
678
|
+
for (const msg of commits) {
|
|
679
|
+
lines.push(`- ${msg}`);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
lines.push("");
|
|
683
|
+
lines.push("---");
|
|
684
|
+
const nextStep = context.lastCheckpoint?.nextStep || context.briefing?.suggestedNext || "continue where I left off";
|
|
685
|
+
lines.push(`Please help me continue this work. My next step is: ${nextStep}.`);
|
|
686
|
+
let result = lines.join("\n");
|
|
687
|
+
if (opts.maxLength && result.length > opts.maxLength) {
|
|
688
|
+
result = result.slice(0, opts.maxLength - 3) + "...";
|
|
689
|
+
}
|
|
690
|
+
return result;
|
|
691
|
+
}
|
|
692
|
+
|
|
392
693
|
// ../../packages/shared/src/storage.ts
|
|
393
694
|
import fs from "fs";
|
|
394
|
-
import
|
|
695
|
+
import path3 from "path";
|
|
395
696
|
import { randomUUID as randomUUID2, createHash } from "crypto";
|
|
396
697
|
var STORAGE_DIR = ".keepgoing";
|
|
397
698
|
var META_FILE = "meta.json";
|
|
@@ -414,11 +715,11 @@ var KeepGoingWriter = class {
|
|
|
414
715
|
currentTasksFilePath;
|
|
415
716
|
constructor(workspacePath) {
|
|
416
717
|
const mainRoot = resolveStorageRoot(workspacePath);
|
|
417
|
-
this.storagePath =
|
|
418
|
-
this.sessionsFilePath =
|
|
419
|
-
this.stateFilePath =
|
|
420
|
-
this.metaFilePath =
|
|
421
|
-
this.currentTasksFilePath =
|
|
718
|
+
this.storagePath = path3.join(mainRoot, STORAGE_DIR);
|
|
719
|
+
this.sessionsFilePath = path3.join(this.storagePath, SESSIONS_FILE);
|
|
720
|
+
this.stateFilePath = path3.join(this.storagePath, STATE_FILE);
|
|
721
|
+
this.metaFilePath = path3.join(this.storagePath, META_FILE);
|
|
722
|
+
this.currentTasksFilePath = path3.join(this.storagePath, CURRENT_TASKS_FILE);
|
|
422
723
|
}
|
|
423
724
|
ensureDir() {
|
|
424
725
|
if (!fs.existsSync(this.storagePath)) {
|
|
@@ -732,24 +1033,24 @@ function capitalize(s) {
|
|
|
732
1033
|
|
|
733
1034
|
// ../../packages/shared/src/decisionStorage.ts
|
|
734
1035
|
import fs3 from "fs";
|
|
735
|
-
import
|
|
1036
|
+
import path5 from "path";
|
|
736
1037
|
|
|
737
1038
|
// ../../packages/shared/src/license.ts
|
|
738
1039
|
import crypto from "crypto";
|
|
739
1040
|
import fs2 from "fs";
|
|
740
1041
|
import os from "os";
|
|
741
|
-
import
|
|
1042
|
+
import path4 from "path";
|
|
742
1043
|
var LICENSE_FILE = "license.json";
|
|
743
1044
|
var DEVICE_ID_FILE = "device-id";
|
|
744
1045
|
function getGlobalLicenseDir() {
|
|
745
|
-
return
|
|
1046
|
+
return path4.join(os.homedir(), ".keepgoing");
|
|
746
1047
|
}
|
|
747
1048
|
function getGlobalLicensePath() {
|
|
748
|
-
return
|
|
1049
|
+
return path4.join(getGlobalLicenseDir(), LICENSE_FILE);
|
|
749
1050
|
}
|
|
750
1051
|
function getDeviceId() {
|
|
751
1052
|
const dir = getGlobalLicenseDir();
|
|
752
|
-
const filePath =
|
|
1053
|
+
const filePath = path4.join(dir, DEVICE_ID_FILE);
|
|
753
1054
|
try {
|
|
754
1055
|
const existing = fs2.readFileSync(filePath, "utf-8").trim();
|
|
755
1056
|
if (existing) return existing;
|
|
@@ -816,7 +1117,7 @@ function writeLicenseStore(store) {
|
|
|
816
1117
|
if (!fs2.existsSync(dirPath)) {
|
|
817
1118
|
fs2.mkdirSync(dirPath, { recursive: true });
|
|
818
1119
|
}
|
|
819
|
-
const licensePath =
|
|
1120
|
+
const licensePath = path4.join(dirPath, LICENSE_FILE);
|
|
820
1121
|
fs2.writeFileSync(licensePath, JSON.stringify(store, null, 2), "utf-8");
|
|
821
1122
|
_cachedStore = store;
|
|
822
1123
|
_cacheTimestamp = Date.now();
|
|
@@ -868,8 +1169,8 @@ var DecisionStorage = class {
|
|
|
868
1169
|
decisionsFilePath;
|
|
869
1170
|
constructor(workspacePath) {
|
|
870
1171
|
const mainRoot = resolveStorageRoot(workspacePath);
|
|
871
|
-
this.storagePath =
|
|
872
|
-
this.decisionsFilePath =
|
|
1172
|
+
this.storagePath = path5.join(mainRoot, STORAGE_DIR2);
|
|
1173
|
+
this.decisionsFilePath = path5.join(this.storagePath, DECISIONS_FILE);
|
|
873
1174
|
}
|
|
874
1175
|
ensureStorageDir() {
|
|
875
1176
|
if (!fs3.existsSync(this.storagePath)) {
|
|
@@ -877,7 +1178,7 @@ var DecisionStorage = class {
|
|
|
877
1178
|
}
|
|
878
1179
|
}
|
|
879
1180
|
getProjectName() {
|
|
880
|
-
return
|
|
1181
|
+
return path5.basename(path5.dirname(this.storagePath));
|
|
881
1182
|
}
|
|
882
1183
|
load() {
|
|
883
1184
|
try {
|
|
@@ -1144,98 +1445,9 @@ function tryDetectDecision(opts) {
|
|
|
1144
1445
|
};
|
|
1145
1446
|
}
|
|
1146
1447
|
|
|
1147
|
-
// ../../packages/shared/src/
|
|
1148
|
-
var BASE_URL = "https://api.lemonsqueezy.com/v1/licenses";
|
|
1149
|
-
var REQUEST_TIMEOUT_MS = 15e3;
|
|
1150
|
-
var EXPECTED_STORE_ID = 301555;
|
|
1151
|
-
var EXPECTED_PRODUCT_ID = 864311;
|
|
1152
|
-
function fetchWithTimeout(url, init) {
|
|
1153
|
-
const controller = new AbortController();
|
|
1154
|
-
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
1155
|
-
return fetch(url, { ...init, signal: controller.signal }).finally(() => clearTimeout(timer));
|
|
1156
|
-
}
|
|
1157
|
-
function validateProductIdentity(meta) {
|
|
1158
|
-
if (!meta) return "License response missing product metadata.";
|
|
1159
|
-
if (meta.store_id !== EXPECTED_STORE_ID || meta.product_id !== EXPECTED_PRODUCT_ID) {
|
|
1160
|
-
return "This license key does not belong to KeepGoing.";
|
|
1161
|
-
}
|
|
1162
|
-
return void 0;
|
|
1163
|
-
}
|
|
1164
|
-
async function safeJson(res) {
|
|
1165
|
-
try {
|
|
1166
|
-
const text = await res.text();
|
|
1167
|
-
return JSON.parse(text);
|
|
1168
|
-
} catch {
|
|
1169
|
-
return null;
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1172
|
-
async function activateLicense(licenseKey, instanceName, options) {
|
|
1173
|
-
try {
|
|
1174
|
-
const res = await fetchWithTimeout(`${BASE_URL}/activate`, {
|
|
1175
|
-
method: "POST",
|
|
1176
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1177
|
-
body: new URLSearchParams({ license_key: licenseKey, instance_name: instanceName })
|
|
1178
|
-
});
|
|
1179
|
-
const data = await safeJson(res);
|
|
1180
|
-
if (!res.ok || !data?.activated) {
|
|
1181
|
-
return { valid: false, error: data?.error || `Activation failed (${res.status})` };
|
|
1182
|
-
}
|
|
1183
|
-
if (!options?.allowTestMode && data.license_key?.test_mode) {
|
|
1184
|
-
if (data.license_key?.key && data.instance?.id) {
|
|
1185
|
-
await deactivateLicense(data.license_key.key, data.instance.id);
|
|
1186
|
-
}
|
|
1187
|
-
return { valid: false, error: "This is a test license key. Please use a production license key from your purchase confirmation." };
|
|
1188
|
-
}
|
|
1189
|
-
if (!options?.allowTestMode) {
|
|
1190
|
-
const productError = validateProductIdentity(data.meta);
|
|
1191
|
-
if (productError) {
|
|
1192
|
-
if (data.license_key?.key && data.instance?.id) {
|
|
1193
|
-
await deactivateLicense(data.license_key.key, data.instance.id);
|
|
1194
|
-
}
|
|
1195
|
-
return { valid: false, error: productError };
|
|
1196
|
-
}
|
|
1197
|
-
if (data.meta?.variant_id && !KNOWN_VARIANT_IDS.has(data.meta.variant_id)) {
|
|
1198
|
-
if (data.license_key?.key && data.instance?.id) {
|
|
1199
|
-
await deactivateLicense(data.license_key.key, data.instance.id);
|
|
1200
|
-
}
|
|
1201
|
-
return { valid: false, error: "This license key is for an unrecognized add-on variant. Please update KeepGoing or contact support." };
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
return {
|
|
1205
|
-
valid: true,
|
|
1206
|
-
licenseKey: data.license_key?.key,
|
|
1207
|
-
instanceId: data.instance?.id,
|
|
1208
|
-
customerName: data.meta?.customer_name,
|
|
1209
|
-
productName: data.meta?.product_name,
|
|
1210
|
-
variantId: data.meta?.variant_id,
|
|
1211
|
-
variantName: data.meta?.variant_name
|
|
1212
|
-
};
|
|
1213
|
-
} catch (err) {
|
|
1214
|
-
const message = err instanceof Error && err.name === "AbortError" ? "Request timed out. Please check your network connection and try again." : err instanceof Error ? err.message : "Network error";
|
|
1215
|
-
return { valid: false, error: message };
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
|
-
async function deactivateLicense(licenseKey, instanceId) {
|
|
1219
|
-
try {
|
|
1220
|
-
const res = await fetchWithTimeout(`${BASE_URL}/deactivate`, {
|
|
1221
|
-
method: "POST",
|
|
1222
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1223
|
-
body: new URLSearchParams({ license_key: licenseKey, instance_id: instanceId })
|
|
1224
|
-
});
|
|
1225
|
-
const data = await safeJson(res);
|
|
1226
|
-
if (!res.ok || !data?.deactivated) {
|
|
1227
|
-
return { deactivated: false, error: data?.error || `Deactivation failed (${res.status})` };
|
|
1228
|
-
}
|
|
1229
|
-
return { deactivated: true };
|
|
1230
|
-
} catch (err) {
|
|
1231
|
-
const message = err instanceof Error && err.name === "AbortError" ? "Request timed out. Please check your network connection and try again." : err instanceof Error ? err.message : "Network error";
|
|
1232
|
-
return { deactivated: false, error: message };
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
// src/storage.ts
|
|
1448
|
+
// ../../packages/shared/src/reader.ts
|
|
1237
1449
|
import fs4 from "fs";
|
|
1238
|
-
import
|
|
1450
|
+
import path6 from "path";
|
|
1239
1451
|
var STORAGE_DIR3 = ".keepgoing";
|
|
1240
1452
|
var META_FILE2 = "meta.json";
|
|
1241
1453
|
var SESSIONS_FILE2 = "sessions.json";
|
|
@@ -1257,12 +1469,12 @@ var KeepGoingReader = class {
|
|
|
1257
1469
|
this.workspacePath = workspacePath;
|
|
1258
1470
|
const mainRoot = resolveStorageRoot(workspacePath);
|
|
1259
1471
|
this._isWorktree = mainRoot !== workspacePath;
|
|
1260
|
-
this.storagePath =
|
|
1261
|
-
this.metaFilePath =
|
|
1262
|
-
this.sessionsFilePath =
|
|
1263
|
-
this.decisionsFilePath =
|
|
1264
|
-
this.stateFilePath =
|
|
1265
|
-
this.currentTasksFilePath =
|
|
1472
|
+
this.storagePath = path6.join(mainRoot, STORAGE_DIR3);
|
|
1473
|
+
this.metaFilePath = path6.join(this.storagePath, META_FILE2);
|
|
1474
|
+
this.sessionsFilePath = path6.join(this.storagePath, SESSIONS_FILE2);
|
|
1475
|
+
this.decisionsFilePath = path6.join(this.storagePath, DECISIONS_FILE2);
|
|
1476
|
+
this.stateFilePath = path6.join(this.storagePath, STATE_FILE2);
|
|
1477
|
+
this.currentTasksFilePath = path6.join(this.storagePath, CURRENT_TASKS_FILE2);
|
|
1266
1478
|
}
|
|
1267
1479
|
/** Check if .keepgoing/ directory exists. */
|
|
1268
1480
|
exists() {
|
|
@@ -1515,105 +1727,423 @@ var KeepGoingReader = class {
|
|
|
1515
1727
|
}
|
|
1516
1728
|
};
|
|
1517
1729
|
|
|
1518
|
-
// src/
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
type: "text",
|
|
1530
|
-
text: "No KeepGoing data found. The developer has not saved any checkpoints yet."
|
|
1531
|
-
}
|
|
1532
|
-
]
|
|
1533
|
-
};
|
|
1534
|
-
}
|
|
1535
|
-
const { session: lastSession, isFallback } = reader.getScopedLastSession();
|
|
1536
|
-
const currentBranch = reader.getCurrentBranch();
|
|
1537
|
-
if (!lastSession) {
|
|
1538
|
-
return {
|
|
1539
|
-
content: [
|
|
1540
|
-
{
|
|
1541
|
-
type: "text",
|
|
1542
|
-
text: "KeepGoing is set up but no session checkpoints exist yet."
|
|
1543
|
-
}
|
|
1544
|
-
]
|
|
1545
|
-
};
|
|
1546
|
-
}
|
|
1547
|
-
const state = reader.getState();
|
|
1548
|
-
const branchChanged = lastSession.gitBranch && currentBranch && lastSession.gitBranch !== currentBranch;
|
|
1549
|
-
const lines = [
|
|
1550
|
-
`## Developer Momentum`,
|
|
1551
|
-
""
|
|
1552
|
-
];
|
|
1553
|
-
if (reader.isWorktree && currentBranch) {
|
|
1554
|
-
lines.push(`**Worktree context:** Scoped to branch \`${currentBranch}\``);
|
|
1555
|
-
if (isFallback) {
|
|
1556
|
-
lines.push(`**Note:** No checkpoints found for branch \`${currentBranch}\`. Showing last global checkpoint.`);
|
|
1557
|
-
}
|
|
1558
|
-
lines.push("");
|
|
1559
|
-
}
|
|
1560
|
-
lines.push(
|
|
1561
|
-
`**Last checkpoint:** ${formatRelativeTime(lastSession.timestamp)}`,
|
|
1562
|
-
`**Summary:** ${lastSession.summary || "No summary"}`,
|
|
1563
|
-
`**Next step:** ${lastSession.nextStep || "Not specified"}`
|
|
1564
|
-
);
|
|
1565
|
-
if (lastSession.blocker) {
|
|
1566
|
-
lines.push(`**Blocker:** ${lastSession.blocker}`);
|
|
1567
|
-
}
|
|
1568
|
-
if (lastSession.projectIntent) {
|
|
1569
|
-
lines.push(`**Project intent:** ${lastSession.projectIntent}`);
|
|
1570
|
-
}
|
|
1571
|
-
lines.push("");
|
|
1572
|
-
if (currentBranch) {
|
|
1573
|
-
lines.push(`**Current branch:** ${currentBranch}`);
|
|
1574
|
-
}
|
|
1575
|
-
if (branchChanged && !reader.isWorktree) {
|
|
1576
|
-
lines.push(
|
|
1577
|
-
`**Note:** Branch changed since last checkpoint (was \`${lastSession.gitBranch}\`, now \`${currentBranch}\`)`
|
|
1578
|
-
);
|
|
1579
|
-
}
|
|
1580
|
-
if (lastSession.touchedFiles.length > 0) {
|
|
1581
|
-
lines.push("");
|
|
1582
|
-
lines.push(
|
|
1583
|
-
`**Files touched (${lastSession.touchedFiles.length}):** ${lastSession.touchedFiles.slice(0, 10).join(", ")}`
|
|
1584
|
-
);
|
|
1585
|
-
if (lastSession.touchedFiles.length > 10) {
|
|
1586
|
-
lines.push(
|
|
1587
|
-
` ...and ${lastSession.touchedFiles.length - 10} more`
|
|
1588
|
-
);
|
|
1589
|
-
}
|
|
1590
|
-
}
|
|
1591
|
-
if (state?.derivedCurrentFocus) {
|
|
1592
|
-
lines.push("");
|
|
1593
|
-
lines.push(`**Derived focus:** ${state.derivedCurrentFocus}`);
|
|
1594
|
-
}
|
|
1595
|
-
return {
|
|
1596
|
-
content: [{ type: "text", text: lines.join("\n") }]
|
|
1597
|
-
};
|
|
1730
|
+
// ../../packages/shared/src/setup.ts
|
|
1731
|
+
import fs5 from "fs";
|
|
1732
|
+
import os2 from "os";
|
|
1733
|
+
import path7 from "path";
|
|
1734
|
+
var KEEPGOING_MARKER = "@keepgoingdev/mcp-server";
|
|
1735
|
+
var SESSION_START_HOOK = {
|
|
1736
|
+
matcher: "",
|
|
1737
|
+
hooks: [
|
|
1738
|
+
{
|
|
1739
|
+
type: "command",
|
|
1740
|
+
command: "npx -y @keepgoingdev/mcp-server --print-momentum"
|
|
1598
1741
|
}
|
|
1599
|
-
|
|
1600
|
-
}
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
function registerGetSessionHistory(server, reader) {
|
|
1605
|
-
server.tool(
|
|
1606
|
-
"get_session_history",
|
|
1607
|
-
"Get recent session checkpoints. Returns a chronological list of what the developer worked on.",
|
|
1742
|
+
]
|
|
1743
|
+
};
|
|
1744
|
+
var STOP_HOOK = {
|
|
1745
|
+
matcher: "",
|
|
1746
|
+
hooks: [
|
|
1608
1747
|
{
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
}
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1748
|
+
type: "command",
|
|
1749
|
+
command: "npx -y @keepgoingdev/mcp-server --save-checkpoint"
|
|
1750
|
+
}
|
|
1751
|
+
]
|
|
1752
|
+
};
|
|
1753
|
+
var POST_TOOL_USE_HOOK = {
|
|
1754
|
+
matcher: "Edit|Write|MultiEdit",
|
|
1755
|
+
hooks: [
|
|
1756
|
+
{
|
|
1757
|
+
type: "command",
|
|
1758
|
+
command: "npx -y @keepgoingdev/mcp-server --update-task-from-hook"
|
|
1759
|
+
}
|
|
1760
|
+
]
|
|
1761
|
+
};
|
|
1762
|
+
var CLAUDE_MD_SECTION = `
|
|
1763
|
+
## KeepGoing
|
|
1764
|
+
|
|
1765
|
+
After completing a task or meaningful piece of work, call the \`save_checkpoint\` MCP tool with:
|
|
1766
|
+
- \`summary\`: What you accomplished
|
|
1767
|
+
- \`nextStep\`: What should be done next
|
|
1768
|
+
- \`blocker\`: Any blocker (if applicable)
|
|
1769
|
+
`;
|
|
1770
|
+
var STATUSLINE_CMD = "npx -y @keepgoingdev/mcp-server --statusline";
|
|
1771
|
+
function hasKeepGoingHook(hookEntries) {
|
|
1772
|
+
return hookEntries.some(
|
|
1773
|
+
(entry) => entry?.hooks?.some((h) => typeof h?.command === "string" && h.command.includes(KEEPGOING_MARKER))
|
|
1774
|
+
);
|
|
1775
|
+
}
|
|
1776
|
+
function resolveScopePaths(scope, workspacePath) {
|
|
1777
|
+
if (scope === "user") {
|
|
1778
|
+
const claudeDir2 = path7.join(os2.homedir(), ".claude");
|
|
1779
|
+
return {
|
|
1780
|
+
claudeDir: claudeDir2,
|
|
1781
|
+
settingsPath: path7.join(claudeDir2, "settings.json"),
|
|
1782
|
+
claudeMdPath: path7.join(claudeDir2, "CLAUDE.md")
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
const claudeDir = path7.join(workspacePath, ".claude");
|
|
1786
|
+
const dotClaudeMdPath = path7.join(workspacePath, ".claude", "CLAUDE.md");
|
|
1787
|
+
const rootClaudeMdPath = path7.join(workspacePath, "CLAUDE.md");
|
|
1788
|
+
return {
|
|
1789
|
+
claudeDir,
|
|
1790
|
+
settingsPath: path7.join(claudeDir, "settings.json"),
|
|
1791
|
+
claudeMdPath: fs5.existsSync(dotClaudeMdPath) ? dotClaudeMdPath : rootClaudeMdPath
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
function writeHooksToSettings(settings) {
|
|
1795
|
+
let changed = false;
|
|
1796
|
+
if (!settings.hooks) {
|
|
1797
|
+
settings.hooks = {};
|
|
1798
|
+
}
|
|
1799
|
+
if (!Array.isArray(settings.hooks.SessionStart)) {
|
|
1800
|
+
settings.hooks.SessionStart = [];
|
|
1801
|
+
}
|
|
1802
|
+
if (!hasKeepGoingHook(settings.hooks.SessionStart)) {
|
|
1803
|
+
settings.hooks.SessionStart.push(SESSION_START_HOOK);
|
|
1804
|
+
changed = true;
|
|
1805
|
+
}
|
|
1806
|
+
if (!Array.isArray(settings.hooks.Stop)) {
|
|
1807
|
+
settings.hooks.Stop = [];
|
|
1808
|
+
}
|
|
1809
|
+
if (!hasKeepGoingHook(settings.hooks.Stop)) {
|
|
1810
|
+
settings.hooks.Stop.push(STOP_HOOK);
|
|
1811
|
+
changed = true;
|
|
1812
|
+
}
|
|
1813
|
+
if (!Array.isArray(settings.hooks.PostToolUse)) {
|
|
1814
|
+
settings.hooks.PostToolUse = [];
|
|
1815
|
+
}
|
|
1816
|
+
if (!hasKeepGoingHook(settings.hooks.PostToolUse)) {
|
|
1817
|
+
settings.hooks.PostToolUse.push(POST_TOOL_USE_HOOK);
|
|
1818
|
+
changed = true;
|
|
1819
|
+
}
|
|
1820
|
+
return changed;
|
|
1821
|
+
}
|
|
1822
|
+
function checkHookConflict(scope, workspacePath) {
|
|
1823
|
+
const otherPaths = resolveScopePaths(scope === "user" ? "project" : "user", workspacePath);
|
|
1824
|
+
if (!fs5.existsSync(otherPaths.settingsPath)) {
|
|
1825
|
+
return null;
|
|
1826
|
+
}
|
|
1827
|
+
try {
|
|
1828
|
+
const otherSettings = JSON.parse(fs5.readFileSync(otherPaths.settingsPath, "utf-8"));
|
|
1829
|
+
const hooks = otherSettings?.hooks;
|
|
1830
|
+
if (!hooks) return null;
|
|
1831
|
+
const hasConflict = Array.isArray(hooks.SessionStart) && hasKeepGoingHook(hooks.SessionStart) || Array.isArray(hooks.Stop) && hasKeepGoingHook(hooks.Stop);
|
|
1832
|
+
if (hasConflict) {
|
|
1833
|
+
const otherScope = scope === "user" ? "project" : "user";
|
|
1834
|
+
const otherFile = otherPaths.settingsPath;
|
|
1835
|
+
return `KeepGoing hooks are also configured at ${otherScope} scope (${otherFile}). Having hooks at both scopes may cause them to fire twice. Consider removing the ${otherScope}-level hooks if you want to use ${scope}-level only.`;
|
|
1836
|
+
}
|
|
1837
|
+
} catch {
|
|
1838
|
+
}
|
|
1839
|
+
return null;
|
|
1840
|
+
}
|
|
1841
|
+
function setupProject(options) {
|
|
1842
|
+
const {
|
|
1843
|
+
workspacePath,
|
|
1844
|
+
scope = "project",
|
|
1845
|
+
sessionHooks = true,
|
|
1846
|
+
claudeMd = true,
|
|
1847
|
+
hasProLicense = false,
|
|
1848
|
+
statusline
|
|
1849
|
+
} = options;
|
|
1850
|
+
const messages = [];
|
|
1851
|
+
let changed = false;
|
|
1852
|
+
const { claudeDir, settingsPath, claudeMdPath } = resolveScopePaths(scope, workspacePath);
|
|
1853
|
+
const scopeLabel = scope === "user" ? "~/.claude/settings.json" : ".claude/settings.json";
|
|
1854
|
+
let settings = {};
|
|
1855
|
+
if (fs5.existsSync(settingsPath)) {
|
|
1856
|
+
settings = JSON.parse(fs5.readFileSync(settingsPath, "utf-8"));
|
|
1857
|
+
}
|
|
1858
|
+
let settingsChanged = false;
|
|
1859
|
+
if (sessionHooks) {
|
|
1860
|
+
const hooksChanged = writeHooksToSettings(settings);
|
|
1861
|
+
settingsChanged = hooksChanged;
|
|
1862
|
+
if (hooksChanged) {
|
|
1863
|
+
messages.push(`Session hooks: Added to ${scopeLabel}`);
|
|
1864
|
+
} else {
|
|
1865
|
+
messages.push("Session hooks: Already present, skipped");
|
|
1866
|
+
}
|
|
1867
|
+
const conflict = checkHookConflict(scope, workspacePath);
|
|
1868
|
+
if (conflict) {
|
|
1869
|
+
messages.push(`Warning: ${conflict}`);
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
if (scope === "project" && hasProLicense) {
|
|
1873
|
+
const needsUpdate = settings.statusLine?.command && statusline?.isLegacy?.(settings.statusLine.command);
|
|
1874
|
+
if (!settings.statusLine || needsUpdate) {
|
|
1875
|
+
settings.statusLine = {
|
|
1876
|
+
type: "command",
|
|
1877
|
+
command: STATUSLINE_CMD
|
|
1878
|
+
};
|
|
1879
|
+
settingsChanged = true;
|
|
1880
|
+
messages.push(needsUpdate ? "Statusline: Migrated to auto-updating npx command" : "Statusline: Added to .claude/settings.json");
|
|
1881
|
+
} else {
|
|
1882
|
+
messages.push("Statusline: Already configured in settings, skipped");
|
|
1883
|
+
}
|
|
1884
|
+
statusline?.cleanup?.();
|
|
1885
|
+
}
|
|
1886
|
+
if (settingsChanged) {
|
|
1887
|
+
if (!fs5.existsSync(claudeDir)) {
|
|
1888
|
+
fs5.mkdirSync(claudeDir, { recursive: true });
|
|
1889
|
+
}
|
|
1890
|
+
fs5.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
1891
|
+
changed = true;
|
|
1892
|
+
}
|
|
1893
|
+
if (claudeMd) {
|
|
1894
|
+
let existing = "";
|
|
1895
|
+
if (fs5.existsSync(claudeMdPath)) {
|
|
1896
|
+
existing = fs5.readFileSync(claudeMdPath, "utf-8");
|
|
1897
|
+
}
|
|
1898
|
+
const mdLabel = scope === "user" ? "~/.claude/CLAUDE.md" : "CLAUDE.md";
|
|
1899
|
+
if (existing.includes("## KeepGoing")) {
|
|
1900
|
+
messages.push(`CLAUDE.md: KeepGoing section already present in ${mdLabel}, skipped`);
|
|
1901
|
+
} else {
|
|
1902
|
+
const updated = existing + CLAUDE_MD_SECTION;
|
|
1903
|
+
const mdDir = path7.dirname(claudeMdPath);
|
|
1904
|
+
if (!fs5.existsSync(mdDir)) {
|
|
1905
|
+
fs5.mkdirSync(mdDir, { recursive: true });
|
|
1906
|
+
}
|
|
1907
|
+
fs5.writeFileSync(claudeMdPath, updated);
|
|
1908
|
+
changed = true;
|
|
1909
|
+
messages.push(`CLAUDE.md: Added KeepGoing section to ${mdLabel}`);
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
return { messages, changed };
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
// ../../packages/shared/src/licenseClient.ts
|
|
1916
|
+
var BASE_URL = "https://api.lemonsqueezy.com/v1/licenses";
|
|
1917
|
+
var REQUEST_TIMEOUT_MS = 15e3;
|
|
1918
|
+
var EXPECTED_STORE_ID = 301555;
|
|
1919
|
+
var EXPECTED_PRODUCT_ID = 864311;
|
|
1920
|
+
function fetchWithTimeout(url, init) {
|
|
1921
|
+
const controller = new AbortController();
|
|
1922
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
1923
|
+
return fetch(url, { ...init, signal: controller.signal }).finally(() => clearTimeout(timer));
|
|
1924
|
+
}
|
|
1925
|
+
function validateProductIdentity(meta) {
|
|
1926
|
+
if (!meta) return "License response missing product metadata.";
|
|
1927
|
+
if (meta.store_id !== EXPECTED_STORE_ID || meta.product_id !== EXPECTED_PRODUCT_ID) {
|
|
1928
|
+
return "This license key does not belong to KeepGoing.";
|
|
1929
|
+
}
|
|
1930
|
+
return void 0;
|
|
1931
|
+
}
|
|
1932
|
+
async function safeJson(res) {
|
|
1933
|
+
try {
|
|
1934
|
+
const text = await res.text();
|
|
1935
|
+
return JSON.parse(text);
|
|
1936
|
+
} catch {
|
|
1937
|
+
return null;
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
async function activateLicense(licenseKey, instanceName, options) {
|
|
1941
|
+
try {
|
|
1942
|
+
const res = await fetchWithTimeout(`${BASE_URL}/activate`, {
|
|
1943
|
+
method: "POST",
|
|
1944
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1945
|
+
body: new URLSearchParams({ license_key: licenseKey, instance_name: instanceName })
|
|
1946
|
+
});
|
|
1947
|
+
const data = await safeJson(res);
|
|
1948
|
+
if (!res.ok || !data?.activated) {
|
|
1949
|
+
return { valid: false, error: data?.error || `Activation failed (${res.status})` };
|
|
1950
|
+
}
|
|
1951
|
+
if (!options?.allowTestMode && data.license_key?.test_mode) {
|
|
1952
|
+
if (data.license_key?.key && data.instance?.id) {
|
|
1953
|
+
await deactivateLicense(data.license_key.key, data.instance.id);
|
|
1954
|
+
}
|
|
1955
|
+
return { valid: false, error: "This is a test license key. Please use a production license key from your purchase confirmation." };
|
|
1956
|
+
}
|
|
1957
|
+
if (!options?.allowTestMode) {
|
|
1958
|
+
const productError = validateProductIdentity(data.meta);
|
|
1959
|
+
if (productError) {
|
|
1960
|
+
if (data.license_key?.key && data.instance?.id) {
|
|
1961
|
+
await deactivateLicense(data.license_key.key, data.instance.id);
|
|
1962
|
+
}
|
|
1963
|
+
return { valid: false, error: productError };
|
|
1964
|
+
}
|
|
1965
|
+
if (data.meta?.variant_id && !KNOWN_VARIANT_IDS.has(data.meta.variant_id)) {
|
|
1966
|
+
if (data.license_key?.key && data.instance?.id) {
|
|
1967
|
+
await deactivateLicense(data.license_key.key, data.instance.id);
|
|
1968
|
+
}
|
|
1969
|
+
return { valid: false, error: "This license key is for an unrecognized add-on variant. Please update KeepGoing or contact support." };
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
return {
|
|
1973
|
+
valid: true,
|
|
1974
|
+
licenseKey: data.license_key?.key,
|
|
1975
|
+
instanceId: data.instance?.id,
|
|
1976
|
+
customerName: data.meta?.customer_name,
|
|
1977
|
+
productName: data.meta?.product_name,
|
|
1978
|
+
variantId: data.meta?.variant_id,
|
|
1979
|
+
variantName: data.meta?.variant_name
|
|
1980
|
+
};
|
|
1981
|
+
} catch (err) {
|
|
1982
|
+
const message = err instanceof Error && err.name === "AbortError" ? "Request timed out. Please check your network connection and try again." : err instanceof Error ? err.message : "Network error";
|
|
1983
|
+
return { valid: false, error: message };
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
async function deactivateLicense(licenseKey, instanceId) {
|
|
1987
|
+
try {
|
|
1988
|
+
const res = await fetchWithTimeout(`${BASE_URL}/deactivate`, {
|
|
1989
|
+
method: "POST",
|
|
1990
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1991
|
+
body: new URLSearchParams({ license_key: licenseKey, instance_id: instanceId })
|
|
1992
|
+
});
|
|
1993
|
+
const data = await safeJson(res);
|
|
1994
|
+
if (!res.ok || !data?.deactivated) {
|
|
1995
|
+
return { deactivated: false, error: data?.error || `Deactivation failed (${res.status})` };
|
|
1996
|
+
}
|
|
1997
|
+
return { deactivated: true };
|
|
1998
|
+
} catch (err) {
|
|
1999
|
+
const message = err instanceof Error && err.name === "AbortError" ? "Request timed out. Please check your network connection and try again." : err instanceof Error ? err.message : "Network error";
|
|
2000
|
+
return { deactivated: false, error: message };
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
// src/tools/getMomentum.ts
|
|
2005
|
+
import { z } from "zod";
|
|
2006
|
+
function registerGetMomentum(server, reader, workspacePath) {
|
|
2007
|
+
server.tool(
|
|
2008
|
+
"get_momentum",
|
|
2009
|
+
"Get current developer momentum: last checkpoint, next step, blockers, and branch context. Use this to understand where the developer left off. Pass tier or model to control detail level.",
|
|
2010
|
+
{
|
|
2011
|
+
tier: z.enum(["compact", "standard", "detailed", "full"]).optional().describe("Briefing detail level. compact (~150 tokens), standard (~400), detailed (~800), full (~1500). Default: standard."),
|
|
2012
|
+
model: z.string().optional().describe('Model name (e.g. "claude-opus-4") to auto-resolve tier. Ignored if tier is set.')
|
|
2013
|
+
},
|
|
2014
|
+
async ({ tier, model }) => {
|
|
2015
|
+
if (!reader.exists()) {
|
|
2016
|
+
return {
|
|
2017
|
+
content: [
|
|
2018
|
+
{
|
|
2019
|
+
type: "text",
|
|
2020
|
+
text: "No KeepGoing data found. The developer has not saved any checkpoints yet."
|
|
2021
|
+
}
|
|
2022
|
+
]
|
|
2023
|
+
};
|
|
2024
|
+
}
|
|
2025
|
+
if (tier || model) {
|
|
2026
|
+
const gitBranch = reader.getCurrentBranch();
|
|
2027
|
+
const { session: lastSession2 } = reader.getScopedLastSession();
|
|
2028
|
+
const recentSessions = reader.getScopedRecentSessions(5);
|
|
2029
|
+
const state2 = reader.getState() ?? {};
|
|
2030
|
+
const sinceTimestamp = lastSession2?.timestamp;
|
|
2031
|
+
const recentCommits = sinceTimestamp ? getCommitMessagesSince(workspacePath, sinceTimestamp) : [];
|
|
2032
|
+
const decisions = reader.getScopedRecentDecisions(10);
|
|
2033
|
+
const allSessions = reader.getSessions();
|
|
2034
|
+
const fileConflicts = reader.detectFileConflicts();
|
|
2035
|
+
const branchOverlaps = reader.detectBranchOverlap();
|
|
2036
|
+
const briefing = generateEnrichedBriefing({
|
|
2037
|
+
tier,
|
|
2038
|
+
model,
|
|
2039
|
+
lastSession: lastSession2,
|
|
2040
|
+
recentSessions,
|
|
2041
|
+
projectState: state2,
|
|
2042
|
+
gitBranch,
|
|
2043
|
+
recentCommits,
|
|
2044
|
+
decisions,
|
|
2045
|
+
allTouchedFiles: lastSession2?.touchedFiles,
|
|
2046
|
+
allSessions,
|
|
2047
|
+
fileConflicts,
|
|
2048
|
+
branchOverlaps,
|
|
2049
|
+
isWorktree: reader.isWorktree
|
|
2050
|
+
});
|
|
2051
|
+
if (!briefing) {
|
|
2052
|
+
return {
|
|
2053
|
+
content: [
|
|
2054
|
+
{
|
|
2055
|
+
type: "text",
|
|
2056
|
+
text: "KeepGoing is set up but no session checkpoints exist yet."
|
|
2057
|
+
}
|
|
2058
|
+
]
|
|
2059
|
+
};
|
|
2060
|
+
}
|
|
2061
|
+
return {
|
|
2062
|
+
content: [{ type: "text", text: formatEnrichedBriefing(briefing) }]
|
|
2063
|
+
};
|
|
2064
|
+
}
|
|
2065
|
+
const { session: lastSession, isFallback } = reader.getScopedLastSession();
|
|
2066
|
+
const currentBranch = reader.getCurrentBranch();
|
|
2067
|
+
if (!lastSession) {
|
|
2068
|
+
return {
|
|
2069
|
+
content: [
|
|
2070
|
+
{
|
|
2071
|
+
type: "text",
|
|
2072
|
+
text: "KeepGoing is set up but no session checkpoints exist yet."
|
|
2073
|
+
}
|
|
2074
|
+
]
|
|
2075
|
+
};
|
|
2076
|
+
}
|
|
2077
|
+
const state = reader.getState();
|
|
2078
|
+
const branchChanged = lastSession.gitBranch && currentBranch && lastSession.gitBranch !== currentBranch;
|
|
2079
|
+
const lines = [
|
|
2080
|
+
`## Developer Momentum`,
|
|
2081
|
+
""
|
|
2082
|
+
];
|
|
2083
|
+
if (reader.isWorktree && currentBranch) {
|
|
2084
|
+
lines.push(`**Worktree context:** Scoped to branch \`${currentBranch}\``);
|
|
2085
|
+
if (isFallback) {
|
|
2086
|
+
lines.push(`**Note:** No checkpoints found for branch \`${currentBranch}\`. Showing last global checkpoint.`);
|
|
2087
|
+
}
|
|
2088
|
+
lines.push("");
|
|
2089
|
+
}
|
|
2090
|
+
lines.push(
|
|
2091
|
+
`**Last checkpoint:** ${formatRelativeTime(lastSession.timestamp)}`,
|
|
2092
|
+
`**Summary:** ${lastSession.summary || "No summary"}`,
|
|
2093
|
+
`**Next step:** ${lastSession.nextStep || "Not specified"}`
|
|
2094
|
+
);
|
|
2095
|
+
if (lastSession.blocker) {
|
|
2096
|
+
lines.push(`**Blocker:** ${lastSession.blocker}`);
|
|
2097
|
+
}
|
|
2098
|
+
if (lastSession.projectIntent) {
|
|
2099
|
+
lines.push(`**Project intent:** ${lastSession.projectIntent}`);
|
|
2100
|
+
}
|
|
2101
|
+
lines.push("");
|
|
2102
|
+
if (currentBranch) {
|
|
2103
|
+
lines.push(`**Current branch:** ${currentBranch}`);
|
|
2104
|
+
}
|
|
2105
|
+
if (branchChanged && !reader.isWorktree) {
|
|
2106
|
+
lines.push(
|
|
2107
|
+
`**Note:** Branch changed since last checkpoint (was \`${lastSession.gitBranch}\`, now \`${currentBranch}\`)`
|
|
2108
|
+
);
|
|
2109
|
+
}
|
|
2110
|
+
if (lastSession.touchedFiles.length > 0) {
|
|
2111
|
+
lines.push("");
|
|
2112
|
+
lines.push(
|
|
2113
|
+
`**Files touched (${lastSession.touchedFiles.length}):** ${lastSession.touchedFiles.slice(0, 10).join(", ")}`
|
|
2114
|
+
);
|
|
2115
|
+
if (lastSession.touchedFiles.length > 10) {
|
|
2116
|
+
lines.push(
|
|
2117
|
+
` ...and ${lastSession.touchedFiles.length - 10} more`
|
|
2118
|
+
);
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
if (state?.derivedCurrentFocus) {
|
|
2122
|
+
lines.push("");
|
|
2123
|
+
lines.push(`**Derived focus:** ${state.derivedCurrentFocus}`);
|
|
2124
|
+
}
|
|
2125
|
+
return {
|
|
2126
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
2127
|
+
};
|
|
2128
|
+
}
|
|
2129
|
+
);
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
// src/tools/getSessionHistory.ts
|
|
2133
|
+
import { z as z2 } from "zod";
|
|
2134
|
+
function registerGetSessionHistory(server, reader) {
|
|
2135
|
+
server.tool(
|
|
2136
|
+
"get_session_history",
|
|
2137
|
+
"Get recent session checkpoints. Returns a chronological list of what the developer worked on.",
|
|
2138
|
+
{
|
|
2139
|
+
limit: z2.number().min(1).max(50).default(5).describe("Number of recent sessions to return (1-50, default 5)"),
|
|
2140
|
+
branch: z2.string().optional().describe('Filter to a specific branch name, or "all" to show all branches. Auto-detected from worktree context by default.')
|
|
2141
|
+
},
|
|
2142
|
+
async ({ limit, branch }) => {
|
|
2143
|
+
if (!reader.exists()) {
|
|
2144
|
+
return {
|
|
2145
|
+
content: [
|
|
2146
|
+
{
|
|
1617
2147
|
type: "text",
|
|
1618
2148
|
text: "No KeepGoing data found."
|
|
1619
2149
|
}
|
|
@@ -1661,12 +2191,16 @@ function registerGetSessionHistory(server, reader) {
|
|
|
1661
2191
|
}
|
|
1662
2192
|
|
|
1663
2193
|
// src/tools/getReentryBriefing.ts
|
|
2194
|
+
import { z as z3 } from "zod";
|
|
1664
2195
|
function registerGetReentryBriefing(server, reader, workspacePath) {
|
|
1665
2196
|
server.tool(
|
|
1666
2197
|
"get_reentry_briefing",
|
|
1667
|
-
"Get a synthesized re-entry briefing that helps a developer understand where they left off. Includes focus, recent activity, and suggested next steps.",
|
|
1668
|
-
{
|
|
1669
|
-
|
|
2198
|
+
"Get a synthesized re-entry briefing that helps a developer understand where they left off. Includes focus, recent activity, and suggested next steps. Pass tier or model to control detail level.",
|
|
2199
|
+
{
|
|
2200
|
+
tier: z3.enum(["compact", "standard", "detailed", "full"]).optional().describe("Briefing detail level. compact (~150 tokens), standard (~400), detailed (~800), full (~1500). Default: standard."),
|
|
2201
|
+
model: z3.string().optional().describe('Model name (e.g. "claude-opus-4") to auto-resolve tier. Ignored if tier is set.')
|
|
2202
|
+
},
|
|
2203
|
+
async ({ tier, model }) => {
|
|
1670
2204
|
if (!reader.exists()) {
|
|
1671
2205
|
return {
|
|
1672
2206
|
content: [
|
|
@@ -1683,13 +2217,25 @@ function registerGetReentryBriefing(server, reader, workspacePath) {
|
|
|
1683
2217
|
const state = reader.getState() ?? {};
|
|
1684
2218
|
const sinceTimestamp = lastSession?.timestamp;
|
|
1685
2219
|
const recentCommits = sinceTimestamp ? getCommitMessagesSince(workspacePath, sinceTimestamp) : [];
|
|
1686
|
-
const
|
|
2220
|
+
const decisions = reader.getScopedRecentDecisions(10);
|
|
2221
|
+
const allSessions = reader.getSessions();
|
|
2222
|
+
const fileConflicts = reader.detectFileConflicts();
|
|
2223
|
+
const branchOverlaps = reader.detectBranchOverlap();
|
|
2224
|
+
const briefing = generateEnrichedBriefing({
|
|
2225
|
+
tier,
|
|
2226
|
+
model,
|
|
1687
2227
|
lastSession,
|
|
1688
2228
|
recentSessions,
|
|
1689
|
-
state,
|
|
2229
|
+
projectState: state,
|
|
1690
2230
|
gitBranch,
|
|
1691
|
-
recentCommits
|
|
1692
|
-
|
|
2231
|
+
recentCommits,
|
|
2232
|
+
decisions,
|
|
2233
|
+
allTouchedFiles: lastSession?.touchedFiles,
|
|
2234
|
+
allSessions,
|
|
2235
|
+
fileConflicts,
|
|
2236
|
+
branchOverlaps,
|
|
2237
|
+
isWorktree: reader.isWorktree
|
|
2238
|
+
});
|
|
1693
2239
|
if (!briefing) {
|
|
1694
2240
|
return {
|
|
1695
2241
|
content: [
|
|
@@ -1700,55 +2246,31 @@ function registerGetReentryBriefing(server, reader, workspacePath) {
|
|
|
1700
2246
|
]
|
|
1701
2247
|
};
|
|
1702
2248
|
}
|
|
1703
|
-
const lines = [
|
|
1704
|
-
`## Re-entry Briefing`,
|
|
1705
|
-
""
|
|
1706
|
-
];
|
|
1707
|
-
if (reader.isWorktree && gitBranch) {
|
|
1708
|
-
lines.push(`**Worktree context:** Scoped to branch \`${gitBranch}\``);
|
|
1709
|
-
lines.push("");
|
|
1710
|
-
}
|
|
1711
|
-
lines.push(
|
|
1712
|
-
`**Last worked:** ${briefing.lastWorked}`,
|
|
1713
|
-
`**Current focus:** ${briefing.currentFocus}`,
|
|
1714
|
-
`**Recent activity:** ${briefing.recentActivity}`,
|
|
1715
|
-
`**Suggested next:** ${briefing.suggestedNext}`,
|
|
1716
|
-
`**Quick start:** ${briefing.smallNextStep}`
|
|
1717
|
-
);
|
|
1718
|
-
const recentDecisions = reader.getScopedRecentDecisions(3);
|
|
1719
|
-
if (recentDecisions.length > 0) {
|
|
1720
|
-
lines.push("");
|
|
1721
|
-
lines.push("### Recent decisions");
|
|
1722
|
-
for (const decision of recentDecisions) {
|
|
1723
|
-
const rationale = decision.rationale ? ` - ${decision.rationale}` : "";
|
|
1724
|
-
lines.push(`- **${decision.classification.category}:** ${decision.commitMessage}${rationale}`);
|
|
1725
|
-
}
|
|
1726
|
-
}
|
|
1727
2249
|
return {
|
|
1728
|
-
content: [{ type: "text", text:
|
|
2250
|
+
content: [{ type: "text", text: formatEnrichedBriefing(briefing) }]
|
|
1729
2251
|
};
|
|
1730
2252
|
}
|
|
1731
2253
|
);
|
|
1732
2254
|
}
|
|
1733
2255
|
|
|
1734
2256
|
// src/tools/saveCheckpoint.ts
|
|
1735
|
-
import
|
|
1736
|
-
import { z as
|
|
2257
|
+
import path8 from "path";
|
|
2258
|
+
import { z as z4 } from "zod";
|
|
1737
2259
|
function registerSaveCheckpoint(server, reader, workspacePath) {
|
|
1738
2260
|
server.tool(
|
|
1739
2261
|
"save_checkpoint",
|
|
1740
2262
|
"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.",
|
|
1741
2263
|
{
|
|
1742
|
-
summary:
|
|
1743
|
-
nextStep:
|
|
1744
|
-
blocker:
|
|
2264
|
+
summary: z4.string().describe("What was accomplished in this session"),
|
|
2265
|
+
nextStep: z4.string().optional().describe("What to do next"),
|
|
2266
|
+
blocker: z4.string().optional().describe("Any blocker preventing progress")
|
|
1745
2267
|
},
|
|
1746
2268
|
async ({ summary, nextStep, blocker }) => {
|
|
1747
2269
|
const lastSession = reader.getLastSession();
|
|
1748
2270
|
const gitBranch = getCurrentBranch(workspacePath);
|
|
1749
2271
|
const touchedFiles = getTouchedFiles(workspacePath);
|
|
1750
2272
|
const commitHashes = getCommitsSince(workspacePath, lastSession?.timestamp);
|
|
1751
|
-
const projectName =
|
|
2273
|
+
const projectName = path8.basename(resolveStorageRoot(workspacePath));
|
|
1752
2274
|
const sessionId = generateSessionId({ workspaceRoot: workspacePath, branch: gitBranch ?? void 0, worktreePath: workspacePath });
|
|
1753
2275
|
const checkpoint = createCheckpoint({
|
|
1754
2276
|
summary,
|
|
@@ -1795,14 +2317,14 @@ function registerSaveCheckpoint(server, reader, workspacePath) {
|
|
|
1795
2317
|
}
|
|
1796
2318
|
|
|
1797
2319
|
// src/tools/getDecisions.ts
|
|
1798
|
-
import { z as
|
|
2320
|
+
import { z as z5 } from "zod";
|
|
1799
2321
|
function registerGetDecisions(server, reader) {
|
|
1800
2322
|
server.tool(
|
|
1801
2323
|
"get_decisions",
|
|
1802
2324
|
"Get recent decision records. Returns detected high-signal commits with their category, confidence, and rationale.",
|
|
1803
2325
|
{
|
|
1804
|
-
limit:
|
|
1805
|
-
branch:
|
|
2326
|
+
limit: z5.number().min(1).max(50).default(10).describe("Number of recent decisions to return (1-50, default 10)"),
|
|
2327
|
+
branch: z5.string().optional().describe('Filter to a specific branch name, or "all" to show all branches. Auto-detected from worktree context by default.')
|
|
1806
2328
|
},
|
|
1807
2329
|
async ({ limit, branch }) => {
|
|
1808
2330
|
if (!reader.exists()) {
|
|
@@ -1960,31 +2482,28 @@ function registerGetCurrentTask(server, reader) {
|
|
|
1960
2482
|
}
|
|
1961
2483
|
|
|
1962
2484
|
// src/tools/setupProject.ts
|
|
1963
|
-
import
|
|
1964
|
-
import os3 from "os";
|
|
1965
|
-
import path8 from "path";
|
|
1966
|
-
import { z as z4 } from "zod";
|
|
2485
|
+
import { z as z6 } from "zod";
|
|
1967
2486
|
|
|
1968
2487
|
// src/cli/migrate.ts
|
|
1969
|
-
import
|
|
1970
|
-
import
|
|
1971
|
-
import
|
|
1972
|
-
var
|
|
2488
|
+
import fs6 from "fs";
|
|
2489
|
+
import os3 from "os";
|
|
2490
|
+
import path9 from "path";
|
|
2491
|
+
var STATUSLINE_CMD2 = "npx -y @keepgoingdev/mcp-server --statusline";
|
|
1973
2492
|
function isLegacyStatusline(command) {
|
|
1974
2493
|
return !command.includes("--statusline") && command.includes("keepgoing-statusline");
|
|
1975
2494
|
}
|
|
1976
2495
|
function migrateStatusline(wsPath) {
|
|
1977
|
-
const settingsPath =
|
|
1978
|
-
if (!
|
|
2496
|
+
const settingsPath = path9.join(wsPath, ".claude", "settings.json");
|
|
2497
|
+
if (!fs6.existsSync(settingsPath)) return void 0;
|
|
1979
2498
|
try {
|
|
1980
|
-
const settings = JSON.parse(
|
|
2499
|
+
const settings = JSON.parse(fs6.readFileSync(settingsPath, "utf-8"));
|
|
1981
2500
|
const cmd = settings.statusLine?.command;
|
|
1982
2501
|
if (!cmd || !isLegacyStatusline(cmd)) return void 0;
|
|
1983
2502
|
settings.statusLine = {
|
|
1984
2503
|
type: "command",
|
|
1985
|
-
command:
|
|
2504
|
+
command: STATUSLINE_CMD2
|
|
1986
2505
|
};
|
|
1987
|
-
|
|
2506
|
+
fs6.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
1988
2507
|
cleanupLegacyScript();
|
|
1989
2508
|
return "[KeepGoing] Migrated statusline to auto-updating command (restart Claude Code to apply)";
|
|
1990
2509
|
} catch {
|
|
@@ -1992,207 +2511,55 @@ function migrateStatusline(wsPath) {
|
|
|
1992
2511
|
}
|
|
1993
2512
|
}
|
|
1994
2513
|
function cleanupLegacyScript() {
|
|
1995
|
-
const legacyScript =
|
|
1996
|
-
if (
|
|
2514
|
+
const legacyScript = path9.join(os3.homedir(), ".claude", "keepgoing-statusline.sh");
|
|
2515
|
+
if (fs6.existsSync(legacyScript)) {
|
|
1997
2516
|
try {
|
|
1998
|
-
|
|
2517
|
+
fs6.unlinkSync(legacyScript);
|
|
1999
2518
|
} catch {
|
|
2000
2519
|
}
|
|
2001
2520
|
}
|
|
2002
2521
|
}
|
|
2003
2522
|
|
|
2004
2523
|
// src/tools/setupProject.ts
|
|
2005
|
-
var KEEPGOING_MARKER = "@keepgoingdev/mcp-server";
|
|
2006
|
-
var SESSION_START_HOOK = {
|
|
2007
|
-
matcher: "",
|
|
2008
|
-
hooks: [
|
|
2009
|
-
{
|
|
2010
|
-
type: "command",
|
|
2011
|
-
command: "npx -y @keepgoingdev/mcp-server --print-momentum"
|
|
2012
|
-
}
|
|
2013
|
-
]
|
|
2014
|
-
};
|
|
2015
|
-
var STOP_HOOK = {
|
|
2016
|
-
matcher: "",
|
|
2017
|
-
hooks: [
|
|
2018
|
-
{
|
|
2019
|
-
type: "command",
|
|
2020
|
-
command: "npx -y @keepgoingdev/mcp-server --save-checkpoint"
|
|
2021
|
-
}
|
|
2022
|
-
]
|
|
2023
|
-
};
|
|
2024
|
-
var POST_TOOL_USE_HOOK = {
|
|
2025
|
-
matcher: "Edit|Write|MultiEdit",
|
|
2026
|
-
hooks: [
|
|
2027
|
-
{
|
|
2028
|
-
type: "command",
|
|
2029
|
-
command: "npx -y @keepgoingdev/mcp-server --update-task-from-hook"
|
|
2030
|
-
}
|
|
2031
|
-
]
|
|
2032
|
-
};
|
|
2033
|
-
var CLAUDE_MD_SECTION = `
|
|
2034
|
-
## KeepGoing
|
|
2035
|
-
|
|
2036
|
-
After completing a task or meaningful piece of work, call the \`save_checkpoint\` MCP tool with:
|
|
2037
|
-
- \`summary\`: What you accomplished
|
|
2038
|
-
- \`nextStep\`: What should be done next
|
|
2039
|
-
- \`blocker\`: Any blocker (if applicable)
|
|
2040
|
-
`;
|
|
2041
|
-
function hasKeepGoingHook(hookEntries) {
|
|
2042
|
-
return hookEntries.some(
|
|
2043
|
-
(entry) => entry?.hooks?.some((h) => typeof h?.command === "string" && h.command.includes(KEEPGOING_MARKER))
|
|
2044
|
-
);
|
|
2045
|
-
}
|
|
2046
|
-
function resolveScopePaths(scope, workspacePath) {
|
|
2047
|
-
if (scope === "user") {
|
|
2048
|
-
const claudeDir2 = path8.join(os3.homedir(), ".claude");
|
|
2049
|
-
return {
|
|
2050
|
-
claudeDir: claudeDir2,
|
|
2051
|
-
settingsPath: path8.join(claudeDir2, "settings.json"),
|
|
2052
|
-
claudeMdPath: path8.join(claudeDir2, "CLAUDE.md")
|
|
2053
|
-
};
|
|
2054
|
-
}
|
|
2055
|
-
const claudeDir = path8.join(workspacePath, ".claude");
|
|
2056
|
-
const dotClaudeMdPath = path8.join(workspacePath, ".claude", "CLAUDE.md");
|
|
2057
|
-
const rootClaudeMdPath = path8.join(workspacePath, "CLAUDE.md");
|
|
2058
|
-
return {
|
|
2059
|
-
claudeDir,
|
|
2060
|
-
settingsPath: path8.join(claudeDir, "settings.json"),
|
|
2061
|
-
claudeMdPath: fs6.existsSync(dotClaudeMdPath) ? dotClaudeMdPath : rootClaudeMdPath
|
|
2062
|
-
};
|
|
2063
|
-
}
|
|
2064
|
-
function writeHooksToSettings(settings) {
|
|
2065
|
-
let changed = false;
|
|
2066
|
-
if (!settings.hooks) {
|
|
2067
|
-
settings.hooks = {};
|
|
2068
|
-
}
|
|
2069
|
-
if (!Array.isArray(settings.hooks.SessionStart)) {
|
|
2070
|
-
settings.hooks.SessionStart = [];
|
|
2071
|
-
}
|
|
2072
|
-
if (!hasKeepGoingHook(settings.hooks.SessionStart)) {
|
|
2073
|
-
settings.hooks.SessionStart.push(SESSION_START_HOOK);
|
|
2074
|
-
changed = true;
|
|
2075
|
-
}
|
|
2076
|
-
if (!Array.isArray(settings.hooks.Stop)) {
|
|
2077
|
-
settings.hooks.Stop = [];
|
|
2078
|
-
}
|
|
2079
|
-
if (!hasKeepGoingHook(settings.hooks.Stop)) {
|
|
2080
|
-
settings.hooks.Stop.push(STOP_HOOK);
|
|
2081
|
-
changed = true;
|
|
2082
|
-
}
|
|
2083
|
-
if (!Array.isArray(settings.hooks.PostToolUse)) {
|
|
2084
|
-
settings.hooks.PostToolUse = [];
|
|
2085
|
-
}
|
|
2086
|
-
if (!hasKeepGoingHook(settings.hooks.PostToolUse)) {
|
|
2087
|
-
settings.hooks.PostToolUse.push(POST_TOOL_USE_HOOK);
|
|
2088
|
-
changed = true;
|
|
2089
|
-
}
|
|
2090
|
-
return changed;
|
|
2091
|
-
}
|
|
2092
|
-
function checkHookConflict(scope, workspacePath) {
|
|
2093
|
-
const otherPaths = resolveScopePaths(scope === "user" ? "project" : "user", workspacePath);
|
|
2094
|
-
if (!fs6.existsSync(otherPaths.settingsPath)) {
|
|
2095
|
-
return null;
|
|
2096
|
-
}
|
|
2097
|
-
try {
|
|
2098
|
-
const otherSettings = JSON.parse(fs6.readFileSync(otherPaths.settingsPath, "utf-8"));
|
|
2099
|
-
const hooks = otherSettings?.hooks;
|
|
2100
|
-
if (!hooks) return null;
|
|
2101
|
-
const hasConflict = Array.isArray(hooks.SessionStart) && hasKeepGoingHook(hooks.SessionStart) || Array.isArray(hooks.Stop) && hasKeepGoingHook(hooks.Stop);
|
|
2102
|
-
if (hasConflict) {
|
|
2103
|
-
const otherScope = scope === "user" ? "project" : "user";
|
|
2104
|
-
const otherFile = otherPaths.settingsPath;
|
|
2105
|
-
return `**Warning:** KeepGoing hooks are also configured at ${otherScope} scope (\`${otherFile}\`). Having hooks at both scopes may cause them to fire twice. Consider removing the ${otherScope}-level hooks if you want to use ${scope}-level only.`;
|
|
2106
|
-
}
|
|
2107
|
-
} catch {
|
|
2108
|
-
}
|
|
2109
|
-
return null;
|
|
2110
|
-
}
|
|
2111
2524
|
function registerSetupProject(server, workspacePath) {
|
|
2112
2525
|
server.tool(
|
|
2113
2526
|
"setup_project",
|
|
2114
2527
|
'Set up KeepGoing hooks and instructions. Use scope "user" for global setup (all projects) or "project" for per-project setup.',
|
|
2115
2528
|
{
|
|
2116
|
-
sessionHooks:
|
|
2117
|
-
claudeMd:
|
|
2118
|
-
scope:
|
|
2529
|
+
sessionHooks: z6.boolean().optional().default(true).describe("Add session hooks to settings.json"),
|
|
2530
|
+
claudeMd: z6.boolean().optional().default(true).describe("Add KeepGoing instructions to CLAUDE.md"),
|
|
2531
|
+
scope: z6.enum(["project", "user"]).optional().default("project").describe('Where to write config: "user" for global (~/.claude/), "project" for per-project (.claude/)')
|
|
2119
2532
|
},
|
|
2120
2533
|
async ({ sessionHooks, claudeMd, scope }) => {
|
|
2121
|
-
const
|
|
2122
|
-
const
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
settingsChanged = hooksChanged;
|
|
2132
|
-
if (hooksChanged) {
|
|
2133
|
-
results.push(`**Session hooks:** Added to ${scopeLabel}`);
|
|
2134
|
-
} else {
|
|
2135
|
-
results.push("**Session hooks:** Already present, skipped");
|
|
2136
|
-
}
|
|
2137
|
-
const conflict = checkHookConflict(scope, workspacePath);
|
|
2138
|
-
if (conflict) {
|
|
2139
|
-
results.push(conflict);
|
|
2140
|
-
}
|
|
2141
|
-
}
|
|
2142
|
-
if (scope === "project") {
|
|
2143
|
-
if (process.env.KEEPGOING_PRO_BYPASS === "1" || getLicenseForFeature("session-awareness")) {
|
|
2144
|
-
const needsUpdate = settings.statusLine?.command && isLegacyStatusline(settings.statusLine.command);
|
|
2145
|
-
if (!settings.statusLine || needsUpdate) {
|
|
2146
|
-
settings.statusLine = {
|
|
2147
|
-
type: "command",
|
|
2148
|
-
command: STATUSLINE_CMD
|
|
2149
|
-
};
|
|
2150
|
-
settingsChanged = true;
|
|
2151
|
-
results.push(needsUpdate ? "**Statusline:** Migrated to auto-updating `npx` command" : "**Statusline:** Added to `.claude/settings.json`");
|
|
2152
|
-
} else {
|
|
2153
|
-
results.push("**Statusline:** `statusLine` already configured in settings, skipped");
|
|
2154
|
-
}
|
|
2155
|
-
cleanupLegacyScript();
|
|
2156
|
-
}
|
|
2157
|
-
}
|
|
2158
|
-
if (settingsChanged) {
|
|
2159
|
-
if (!fs6.existsSync(claudeDir)) {
|
|
2160
|
-
fs6.mkdirSync(claudeDir, { recursive: true });
|
|
2161
|
-
}
|
|
2162
|
-
fs6.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
2163
|
-
}
|
|
2164
|
-
if (claudeMd) {
|
|
2165
|
-
let existing = "";
|
|
2166
|
-
if (fs6.existsSync(claudeMdPath)) {
|
|
2167
|
-
existing = fs6.readFileSync(claudeMdPath, "utf-8");
|
|
2168
|
-
}
|
|
2169
|
-
const mdLabel = scope === "user" ? "`~/.claude/CLAUDE.md`" : "`CLAUDE.md`";
|
|
2170
|
-
if (existing.includes("## KeepGoing")) {
|
|
2171
|
-
results.push(`**CLAUDE.md:** KeepGoing section already present in ${mdLabel}, skipped`);
|
|
2172
|
-
} else {
|
|
2173
|
-
const updated = existing + CLAUDE_MD_SECTION;
|
|
2174
|
-
const mdDir = path8.dirname(claudeMdPath);
|
|
2175
|
-
if (!fs6.existsSync(mdDir)) {
|
|
2176
|
-
fs6.mkdirSync(mdDir, { recursive: true });
|
|
2177
|
-
}
|
|
2178
|
-
fs6.writeFileSync(claudeMdPath, updated);
|
|
2179
|
-
results.push(`**CLAUDE.md:** Added KeepGoing section to ${mdLabel}`);
|
|
2534
|
+
const hasProLicense = process.env.KEEPGOING_PRO_BYPASS === "1" || !!getLicenseForFeature("session-awareness");
|
|
2535
|
+
const result = setupProject({
|
|
2536
|
+
workspacePath,
|
|
2537
|
+
scope,
|
|
2538
|
+
sessionHooks,
|
|
2539
|
+
claudeMd,
|
|
2540
|
+
hasProLicense,
|
|
2541
|
+
statusline: {
|
|
2542
|
+
isLegacy: isLegacyStatusline,
|
|
2543
|
+
cleanup: cleanupLegacyScript
|
|
2180
2544
|
}
|
|
2181
|
-
}
|
|
2545
|
+
});
|
|
2546
|
+
const formatted = result.messages.map((msg) => {
|
|
2547
|
+
return msg.replace(/^([^:]+:)/, "**$1**");
|
|
2548
|
+
});
|
|
2182
2549
|
return {
|
|
2183
|
-
content: [{ type: "text", text:
|
|
2550
|
+
content: [{ type: "text", text: formatted.join("\n") }]
|
|
2184
2551
|
};
|
|
2185
2552
|
}
|
|
2186
2553
|
);
|
|
2187
2554
|
}
|
|
2188
2555
|
|
|
2189
2556
|
// src/tools/activateLicense.ts
|
|
2190
|
-
import { z as
|
|
2557
|
+
import { z as z7 } from "zod";
|
|
2191
2558
|
function registerActivateLicense(server) {
|
|
2192
2559
|
server.tool(
|
|
2193
2560
|
"activate_license",
|
|
2194
2561
|
"Activate a KeepGoing Pro license on this device. Unlocks add-ons like Decision Detection and Session Awareness.",
|
|
2195
|
-
{ license_key:
|
|
2562
|
+
{ license_key: z7.string().describe("Your KeepGoing Pro license key") },
|
|
2196
2563
|
async ({ license_key }) => {
|
|
2197
2564
|
const store = readLicenseStore();
|
|
2198
2565
|
const existingForKey = store.licenses.find(
|
|
@@ -2266,13 +2633,13 @@ function registerActivateLicense(server) {
|
|
|
2266
2633
|
}
|
|
2267
2634
|
|
|
2268
2635
|
// src/tools/deactivateLicense.ts
|
|
2269
|
-
import { z as
|
|
2636
|
+
import { z as z8 } from "zod";
|
|
2270
2637
|
function registerDeactivateLicense(server) {
|
|
2271
2638
|
server.tool(
|
|
2272
2639
|
"deactivate_license",
|
|
2273
2640
|
"Deactivate the KeepGoing Pro license on this device.",
|
|
2274
2641
|
{
|
|
2275
|
-
license_key:
|
|
2642
|
+
license_key: z8.string().optional().describe("Specific license key to deactivate. If omitted and only one license is active, deactivates it. If multiple are active, lists them.")
|
|
2276
2643
|
},
|
|
2277
2644
|
async ({ license_key }) => {
|
|
2278
2645
|
const store = readLicenseStore();
|
|
@@ -2338,6 +2705,48 @@ function registerDeactivateLicense(server) {
|
|
|
2338
2705
|
);
|
|
2339
2706
|
}
|
|
2340
2707
|
|
|
2708
|
+
// src/tools/continueOn.ts
|
|
2709
|
+
import { z as z9 } from "zod";
|
|
2710
|
+
function registerContinueOn(server, reader, workspacePath) {
|
|
2711
|
+
server.tool(
|
|
2712
|
+
"continue_on",
|
|
2713
|
+
"Export KeepGoing context as a formatted prompt for use in another AI tool (ChatGPT, Gemini, Copilot, etc.). Returns a markdown prompt with project status, last session, decisions, and recent commits.",
|
|
2714
|
+
{
|
|
2715
|
+
target: z9.enum(["chatgpt", "gemini", "copilot", "claude", "general"]).optional().describe("Target AI tool (currently used for future format tuning)"),
|
|
2716
|
+
include_commits: z9.boolean().default(true).describe("Include recent commit messages in the prompt"),
|
|
2717
|
+
include_files: z9.boolean().default(true).describe("Include touched file paths in the prompt")
|
|
2718
|
+
},
|
|
2719
|
+
async ({ target, include_commits, include_files }) => {
|
|
2720
|
+
if (!reader.exists()) {
|
|
2721
|
+
return {
|
|
2722
|
+
content: [{
|
|
2723
|
+
type: "text",
|
|
2724
|
+
text: "No KeepGoing data found. Save a checkpoint first to use Continue On."
|
|
2725
|
+
}]
|
|
2726
|
+
};
|
|
2727
|
+
}
|
|
2728
|
+
const context = gatherContinueOnContext(reader, workspacePath);
|
|
2729
|
+
if (!context.lastCheckpoint && !context.briefing) {
|
|
2730
|
+
return {
|
|
2731
|
+
content: [{
|
|
2732
|
+
type: "text",
|
|
2733
|
+
text: "No session data available. Save a checkpoint first."
|
|
2734
|
+
}]
|
|
2735
|
+
};
|
|
2736
|
+
}
|
|
2737
|
+
const formatOpts = {
|
|
2738
|
+
target,
|
|
2739
|
+
includeCommits: include_commits,
|
|
2740
|
+
includeFiles: include_files
|
|
2741
|
+
};
|
|
2742
|
+
const prompt = formatContinueOnPrompt(context, formatOpts);
|
|
2743
|
+
return {
|
|
2744
|
+
content: [{ type: "text", text: prompt }]
|
|
2745
|
+
};
|
|
2746
|
+
}
|
|
2747
|
+
);
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2341
2750
|
// src/prompts/resume.ts
|
|
2342
2751
|
function registerResumePrompt(server) {
|
|
2343
2752
|
server.prompt(
|
|
@@ -2503,7 +2912,7 @@ async function handlePrintCurrent() {
|
|
|
2503
2912
|
}
|
|
2504
2913
|
|
|
2505
2914
|
// src/cli/saveCheckpoint.ts
|
|
2506
|
-
import
|
|
2915
|
+
import path10 from "path";
|
|
2507
2916
|
async function handleSaveCheckpoint() {
|
|
2508
2917
|
const wsPath = resolveWsPath();
|
|
2509
2918
|
const reader = new KeepGoingReader(wsPath);
|
|
@@ -2531,9 +2940,9 @@ async function handleSaveCheckpoint() {
|
|
|
2531
2940
|
sessionStartTime: lastSession?.timestamp ?? now,
|
|
2532
2941
|
lastActivityTime: now
|
|
2533
2942
|
});
|
|
2534
|
-
const summary = buildSmartSummary(events) ?? `Worked on ${touchedFiles.slice(0, 5).map((f) =>
|
|
2943
|
+
const summary = buildSmartSummary(events) ?? `Worked on ${touchedFiles.slice(0, 5).map((f) => path10.basename(f)).join(", ")}`;
|
|
2535
2944
|
const nextStep = buildSmartNextStep(events);
|
|
2536
|
-
const projectName =
|
|
2945
|
+
const projectName = path10.basename(resolveStorageRoot(wsPath));
|
|
2537
2946
|
const sessionId = generateSessionId({ workspaceRoot: wsPath, branch: gitBranch ?? void 0, worktreePath: wsPath });
|
|
2538
2947
|
const checkpoint = createCheckpoint({
|
|
2539
2948
|
summary,
|
|
@@ -2647,7 +3056,7 @@ async function handleUpdateTaskFromHook() {
|
|
|
2647
3056
|
|
|
2648
3057
|
// src/cli/statusline.ts
|
|
2649
3058
|
import fs7 from "fs";
|
|
2650
|
-
import
|
|
3059
|
+
import path11 from "path";
|
|
2651
3060
|
var STDIN_TIMEOUT_MS2 = 3e3;
|
|
2652
3061
|
async function handleStatusline() {
|
|
2653
3062
|
const chunks = [];
|
|
@@ -2670,7 +3079,7 @@ async function handleStatusline() {
|
|
|
2670
3079
|
process.exit(0);
|
|
2671
3080
|
}
|
|
2672
3081
|
const gitRoot = findGitRoot(dir);
|
|
2673
|
-
const tasksFile =
|
|
3082
|
+
const tasksFile = path11.join(gitRoot, ".keepgoing", "current-tasks.json");
|
|
2674
3083
|
if (!fs7.existsSync(tasksFile)) {
|
|
2675
3084
|
process.exit(0);
|
|
2676
3085
|
}
|
|
@@ -2700,6 +3109,22 @@ async function handleStatusline() {
|
|
|
2700
3109
|
process.stdin.resume();
|
|
2701
3110
|
}
|
|
2702
3111
|
|
|
3112
|
+
// src/cli/continueOn.ts
|
|
3113
|
+
async function handleContinueOn() {
|
|
3114
|
+
const wsPath = resolveWsPath();
|
|
3115
|
+
const reader = new KeepGoingReader(wsPath);
|
|
3116
|
+
if (!reader.exists()) {
|
|
3117
|
+
process.exit(0);
|
|
3118
|
+
}
|
|
3119
|
+
const context = gatherContinueOnContext(reader, wsPath);
|
|
3120
|
+
if (!context.lastCheckpoint && !context.briefing) {
|
|
3121
|
+
process.exit(0);
|
|
3122
|
+
}
|
|
3123
|
+
const prompt = formatContinueOnPrompt(context);
|
|
3124
|
+
console.log(prompt);
|
|
3125
|
+
process.exit(0);
|
|
3126
|
+
}
|
|
3127
|
+
|
|
2703
3128
|
// src/index.ts
|
|
2704
3129
|
var CLI_HANDLERS = {
|
|
2705
3130
|
"--print-momentum": handlePrintMomentum,
|
|
@@ -2707,7 +3132,8 @@ var CLI_HANDLERS = {
|
|
|
2707
3132
|
"--update-task": handleUpdateTask,
|
|
2708
3133
|
"--update-task-from-hook": handleUpdateTaskFromHook,
|
|
2709
3134
|
"--print-current": handlePrintCurrent,
|
|
2710
|
-
"--statusline": handleStatusline
|
|
3135
|
+
"--statusline": handleStatusline,
|
|
3136
|
+
"--continue-on": handleContinueOn
|
|
2711
3137
|
};
|
|
2712
3138
|
var flag = process.argv.slice(2).find((a) => a in CLI_HANDLERS);
|
|
2713
3139
|
if (flag) {
|
|
@@ -2725,6 +3151,7 @@ if (flag) {
|
|
|
2725
3151
|
registerGetDecisions(server, reader);
|
|
2726
3152
|
registerGetCurrentTask(server, reader);
|
|
2727
3153
|
registerSaveCheckpoint(server, reader, workspacePath);
|
|
3154
|
+
registerContinueOn(server, reader, workspacePath);
|
|
2728
3155
|
registerSetupProject(server, workspacePath);
|
|
2729
3156
|
registerActivateLicense(server);
|
|
2730
3157
|
registerDeactivateLicense(server);
|