@keepgoingdev/mcp-server 0.4.0 → 0.5.1
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 +786 -220
- package/dist/index.js.map +1 -1
- package/dist/statusline.sh +51 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import path8 from "path";
|
|
5
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
6
|
|
|
8
|
-
// src/storage.ts
|
|
9
|
-
import fs4 from "fs";
|
|
10
|
-
import path5 from "path";
|
|
11
|
-
|
|
12
7
|
// ../../packages/shared/src/session.ts
|
|
13
8
|
import { randomUUID } from "crypto";
|
|
14
9
|
function generateCheckpointId() {
|
|
@@ -116,10 +111,10 @@ function resolveStorageRoot(startPath) {
|
|
|
116
111
|
return startPath;
|
|
117
112
|
}
|
|
118
113
|
}
|
|
119
|
-
function getCurrentBranch(
|
|
114
|
+
function getCurrentBranch(workspacePath) {
|
|
120
115
|
try {
|
|
121
116
|
const result = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
122
|
-
cwd:
|
|
117
|
+
cwd: workspacePath,
|
|
123
118
|
encoding: "utf-8",
|
|
124
119
|
timeout: 5e3
|
|
125
120
|
});
|
|
@@ -128,14 +123,14 @@ function getCurrentBranch(workspacePath2) {
|
|
|
128
123
|
return void 0;
|
|
129
124
|
}
|
|
130
125
|
}
|
|
131
|
-
function getGitLogSince(
|
|
126
|
+
function getGitLogSince(workspacePath, format, sinceTimestamp) {
|
|
132
127
|
try {
|
|
133
128
|
const since = sinceTimestamp || new Date(Date.now() - 24 * 60 * 60 * 1e3).toISOString();
|
|
134
129
|
const result = execFileSync(
|
|
135
130
|
"git",
|
|
136
131
|
["log", `--since=${since}`, `--format=${format}`],
|
|
137
132
|
{
|
|
138
|
-
cwd:
|
|
133
|
+
cwd: workspacePath,
|
|
139
134
|
encoding: "utf-8",
|
|
140
135
|
timeout: 5e3
|
|
141
136
|
}
|
|
@@ -148,16 +143,16 @@ function getGitLogSince(workspacePath2, format, sinceTimestamp) {
|
|
|
148
143
|
return [];
|
|
149
144
|
}
|
|
150
145
|
}
|
|
151
|
-
function getCommitsSince(
|
|
152
|
-
return getGitLogSince(
|
|
146
|
+
function getCommitsSince(workspacePath, sinceTimestamp) {
|
|
147
|
+
return getGitLogSince(workspacePath, "%H", sinceTimestamp);
|
|
153
148
|
}
|
|
154
|
-
function getCommitMessagesSince(
|
|
155
|
-
return getGitLogSince(
|
|
149
|
+
function getCommitMessagesSince(workspacePath, sinceTimestamp) {
|
|
150
|
+
return getGitLogSince(workspacePath, "%s", sinceTimestamp);
|
|
156
151
|
}
|
|
157
|
-
function getHeadCommitHash(
|
|
152
|
+
function getHeadCommitHash(workspacePath) {
|
|
158
153
|
try {
|
|
159
154
|
const result = execFileSync("git", ["rev-parse", "HEAD"], {
|
|
160
|
-
cwd:
|
|
155
|
+
cwd: workspacePath,
|
|
161
156
|
encoding: "utf-8",
|
|
162
157
|
timeout: 5e3
|
|
163
158
|
});
|
|
@@ -166,10 +161,10 @@ function getHeadCommitHash(workspacePath2) {
|
|
|
166
161
|
return void 0;
|
|
167
162
|
}
|
|
168
163
|
}
|
|
169
|
-
function getTouchedFiles(
|
|
164
|
+
function getTouchedFiles(workspacePath) {
|
|
170
165
|
try {
|
|
171
166
|
const result = execFileSync("git", ["status", "--porcelain"], {
|
|
172
|
-
cwd:
|
|
167
|
+
cwd: workspacePath,
|
|
173
168
|
encoding: "utf-8",
|
|
174
169
|
timeout: 5e3
|
|
175
170
|
});
|
|
@@ -385,22 +380,33 @@ function inferFocusFromFiles(files) {
|
|
|
385
380
|
// ../../packages/shared/src/storage.ts
|
|
386
381
|
import fs from "fs";
|
|
387
382
|
import path2 from "path";
|
|
388
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
383
|
+
import { randomUUID as randomUUID2, createHash } from "crypto";
|
|
389
384
|
var STORAGE_DIR = ".keepgoing";
|
|
390
385
|
var META_FILE = "meta.json";
|
|
391
386
|
var SESSIONS_FILE = "sessions.json";
|
|
392
387
|
var STATE_FILE = "state.json";
|
|
388
|
+
var CURRENT_TASKS_FILE = "current-tasks.json";
|
|
389
|
+
var STALE_SESSION_MS = 2 * 60 * 60 * 1e3;
|
|
390
|
+
function pruneStaleTasks(tasks) {
|
|
391
|
+
const now = Date.now();
|
|
392
|
+
return tasks.filter((t) => {
|
|
393
|
+
const updatedAt = new Date(t.updatedAt).getTime();
|
|
394
|
+
return !isNaN(updatedAt) && now - updatedAt < STALE_SESSION_MS;
|
|
395
|
+
});
|
|
396
|
+
}
|
|
393
397
|
var KeepGoingWriter = class {
|
|
394
398
|
storagePath;
|
|
395
399
|
sessionsFilePath;
|
|
396
400
|
stateFilePath;
|
|
397
401
|
metaFilePath;
|
|
398
|
-
|
|
399
|
-
|
|
402
|
+
currentTasksFilePath;
|
|
403
|
+
constructor(workspacePath) {
|
|
404
|
+
const mainRoot = resolveStorageRoot(workspacePath);
|
|
400
405
|
this.storagePath = path2.join(mainRoot, STORAGE_DIR);
|
|
401
406
|
this.sessionsFilePath = path2.join(this.storagePath, SESSIONS_FILE);
|
|
402
407
|
this.stateFilePath = path2.join(this.storagePath, STATE_FILE);
|
|
403
408
|
this.metaFilePath = path2.join(this.storagePath, META_FILE);
|
|
409
|
+
this.currentTasksFilePath = path2.join(this.storagePath, CURRENT_TASKS_FILE);
|
|
404
410
|
}
|
|
405
411
|
ensureDir() {
|
|
406
412
|
if (!fs.existsSync(this.storagePath)) {
|
|
@@ -461,11 +467,212 @@ var KeepGoingWriter = class {
|
|
|
461
467
|
}
|
|
462
468
|
fs.writeFileSync(this.metaFilePath, JSON.stringify(meta, null, 2), "utf-8");
|
|
463
469
|
}
|
|
470
|
+
// ---------------------------------------------------------------------------
|
|
471
|
+
// Multi-session API
|
|
472
|
+
// ---------------------------------------------------------------------------
|
|
473
|
+
/** Read all current tasks from current-tasks.json. Auto-prunes stale sessions. */
|
|
474
|
+
readCurrentTasks() {
|
|
475
|
+
try {
|
|
476
|
+
if (fs.existsSync(this.currentTasksFilePath)) {
|
|
477
|
+
const raw = JSON.parse(fs.readFileSync(this.currentTasksFilePath, "utf-8"));
|
|
478
|
+
const tasks = Array.isArray(raw) ? raw : raw.tasks ?? [];
|
|
479
|
+
return this.pruneStale(tasks);
|
|
480
|
+
}
|
|
481
|
+
} catch {
|
|
482
|
+
}
|
|
483
|
+
return [];
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Upsert a session task by sessionId into current-tasks.json.
|
|
487
|
+
* If no sessionId is present on the task, generates one.
|
|
488
|
+
*/
|
|
489
|
+
upsertSession(update) {
|
|
490
|
+
this.ensureDir();
|
|
491
|
+
this.upsertSessionCore(update);
|
|
492
|
+
}
|
|
493
|
+
/** Core upsert logic: merges the update into current-tasks.json and returns the pruned task list. */
|
|
494
|
+
upsertSessionCore(update) {
|
|
495
|
+
this.ensureDir();
|
|
496
|
+
const sessionId = update.sessionId || generateSessionId(update);
|
|
497
|
+
const tasks = this.readAllTasksRaw();
|
|
498
|
+
const existingIdx = tasks.findIndex((t) => t.sessionId === sessionId);
|
|
499
|
+
let merged;
|
|
500
|
+
if (existingIdx >= 0) {
|
|
501
|
+
const existing = tasks[existingIdx];
|
|
502
|
+
merged = { ...existing, ...update, sessionId };
|
|
503
|
+
tasks[existingIdx] = merged;
|
|
504
|
+
} else {
|
|
505
|
+
merged = { ...update, sessionId };
|
|
506
|
+
tasks.push(merged);
|
|
507
|
+
}
|
|
508
|
+
const pruned = this.pruneStale(tasks);
|
|
509
|
+
this.writeTasksFile(pruned);
|
|
510
|
+
return pruned;
|
|
511
|
+
}
|
|
512
|
+
/** Remove a specific session by ID. */
|
|
513
|
+
removeSession(sessionId) {
|
|
514
|
+
const tasks = this.readAllTasksRaw().filter((t) => t.sessionId !== sessionId);
|
|
515
|
+
this.writeTasksFile(tasks);
|
|
516
|
+
}
|
|
517
|
+
/** Get all active sessions (sessionActive=true and within stale threshold). */
|
|
518
|
+
getActiveSessions() {
|
|
519
|
+
return this.readCurrentTasks().filter((t) => t.sessionActive);
|
|
520
|
+
}
|
|
521
|
+
/** Get a specific session by ID. */
|
|
522
|
+
getSession(sessionId) {
|
|
523
|
+
return this.readCurrentTasks().find((t) => t.sessionId === sessionId);
|
|
524
|
+
}
|
|
525
|
+
// ---------------------------------------------------------------------------
|
|
526
|
+
// Private helpers
|
|
527
|
+
// ---------------------------------------------------------------------------
|
|
528
|
+
readAllTasksRaw() {
|
|
529
|
+
try {
|
|
530
|
+
if (fs.existsSync(this.currentTasksFilePath)) {
|
|
531
|
+
const raw = JSON.parse(fs.readFileSync(this.currentTasksFilePath, "utf-8"));
|
|
532
|
+
return Array.isArray(raw) ? [...raw] : [...raw.tasks ?? []];
|
|
533
|
+
}
|
|
534
|
+
} catch {
|
|
535
|
+
}
|
|
536
|
+
return [];
|
|
537
|
+
}
|
|
538
|
+
pruneStale(tasks) {
|
|
539
|
+
return pruneStaleTasks(tasks);
|
|
540
|
+
}
|
|
541
|
+
writeTasksFile(tasks) {
|
|
542
|
+
const data = { version: 1, tasks };
|
|
543
|
+
fs.writeFileSync(this.currentTasksFilePath, JSON.stringify(data, null, 2), "utf-8");
|
|
544
|
+
}
|
|
464
545
|
};
|
|
546
|
+
function generateSessionId(context) {
|
|
547
|
+
const parts = [
|
|
548
|
+
context.worktreePath || context.workspaceRoot || "",
|
|
549
|
+
context.agentLabel || "",
|
|
550
|
+
context.branch || ""
|
|
551
|
+
].filter(Boolean);
|
|
552
|
+
if (parts.length === 0) {
|
|
553
|
+
return randomUUID2();
|
|
554
|
+
}
|
|
555
|
+
const hash = createHash("sha256").update(parts.join("|")).digest("hex").slice(0, 12);
|
|
556
|
+
return `ses_${hash}`;
|
|
557
|
+
}
|
|
465
558
|
|
|
466
559
|
// ../../packages/shared/src/decisionStorage.ts
|
|
560
|
+
import fs3 from "fs";
|
|
561
|
+
import path4 from "path";
|
|
562
|
+
|
|
563
|
+
// ../../packages/shared/src/license.ts
|
|
564
|
+
import crypto from "crypto";
|
|
467
565
|
import fs2 from "fs";
|
|
566
|
+
import os from "os";
|
|
468
567
|
import path3 from "path";
|
|
568
|
+
var LICENSE_FILE = "license.json";
|
|
569
|
+
var DEVICE_ID_FILE = "device-id";
|
|
570
|
+
function getGlobalLicenseDir() {
|
|
571
|
+
return path3.join(os.homedir(), ".keepgoing");
|
|
572
|
+
}
|
|
573
|
+
function getGlobalLicensePath() {
|
|
574
|
+
return path3.join(getGlobalLicenseDir(), LICENSE_FILE);
|
|
575
|
+
}
|
|
576
|
+
function getDeviceId() {
|
|
577
|
+
const dir = getGlobalLicenseDir();
|
|
578
|
+
const filePath = path3.join(dir, DEVICE_ID_FILE);
|
|
579
|
+
try {
|
|
580
|
+
const existing = fs2.readFileSync(filePath, "utf-8").trim();
|
|
581
|
+
if (existing) return existing;
|
|
582
|
+
} catch {
|
|
583
|
+
}
|
|
584
|
+
const id = crypto.randomUUID();
|
|
585
|
+
if (!fs2.existsSync(dir)) {
|
|
586
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
587
|
+
}
|
|
588
|
+
fs2.writeFileSync(filePath, id, "utf-8");
|
|
589
|
+
return id;
|
|
590
|
+
}
|
|
591
|
+
var DECISION_DETECTION_VARIANT_ID = 1361527;
|
|
592
|
+
var SESSION_AWARENESS_VARIANT_ID = 1366510;
|
|
593
|
+
var TEST_DECISION_DETECTION_VARIANT_ID = 1345647;
|
|
594
|
+
var TEST_SESSION_AWARENESS_VARIANT_ID = 1365992;
|
|
595
|
+
var VARIANT_FEATURE_MAP = {
|
|
596
|
+
[DECISION_DETECTION_VARIANT_ID]: ["decisions"],
|
|
597
|
+
[SESSION_AWARENESS_VARIANT_ID]: ["session-awareness"],
|
|
598
|
+
[TEST_DECISION_DETECTION_VARIANT_ID]: ["decisions"],
|
|
599
|
+
[TEST_SESSION_AWARENESS_VARIANT_ID]: ["session-awareness"]
|
|
600
|
+
// Future bundle: [BUNDLE_VARIANT_ID]: ['decisions', 'session-awareness'],
|
|
601
|
+
};
|
|
602
|
+
var KNOWN_VARIANT_IDS = new Set(Object.keys(VARIANT_FEATURE_MAP).map(Number));
|
|
603
|
+
function getVariantLabel(variantId) {
|
|
604
|
+
const features = VARIANT_FEATURE_MAP[variantId];
|
|
605
|
+
if (!features) return "Unknown Add-on";
|
|
606
|
+
if (features.includes("decisions") && features.includes("session-awareness")) return "Pro Bundle";
|
|
607
|
+
if (features.includes("decisions")) return "Decision Detection";
|
|
608
|
+
if (features.includes("session-awareness")) return "Session Awareness";
|
|
609
|
+
return "Pro Add-on";
|
|
610
|
+
}
|
|
611
|
+
var _cachedStore;
|
|
612
|
+
var _cacheTimestamp = 0;
|
|
613
|
+
var LICENSE_CACHE_TTL_MS = 2e3;
|
|
614
|
+
function readLicenseStore() {
|
|
615
|
+
const now = Date.now();
|
|
616
|
+
if (_cachedStore && now - _cacheTimestamp < LICENSE_CACHE_TTL_MS) {
|
|
617
|
+
return _cachedStore;
|
|
618
|
+
}
|
|
619
|
+
const licensePath = getGlobalLicensePath();
|
|
620
|
+
let store;
|
|
621
|
+
try {
|
|
622
|
+
if (!fs2.existsSync(licensePath)) {
|
|
623
|
+
store = { version: 2, licenses: [] };
|
|
624
|
+
} else {
|
|
625
|
+
const raw = fs2.readFileSync(licensePath, "utf-8");
|
|
626
|
+
const data = JSON.parse(raw);
|
|
627
|
+
if (data?.version === 2 && Array.isArray(data.licenses)) {
|
|
628
|
+
store = data;
|
|
629
|
+
} else {
|
|
630
|
+
store = { version: 2, licenses: [] };
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
} catch {
|
|
634
|
+
store = { version: 2, licenses: [] };
|
|
635
|
+
}
|
|
636
|
+
_cachedStore = store;
|
|
637
|
+
_cacheTimestamp = now;
|
|
638
|
+
return store;
|
|
639
|
+
}
|
|
640
|
+
function writeLicenseStore(store) {
|
|
641
|
+
const dirPath = getGlobalLicenseDir();
|
|
642
|
+
if (!fs2.existsSync(dirPath)) {
|
|
643
|
+
fs2.mkdirSync(dirPath, { recursive: true });
|
|
644
|
+
}
|
|
645
|
+
const licensePath = path3.join(dirPath, LICENSE_FILE);
|
|
646
|
+
fs2.writeFileSync(licensePath, JSON.stringify(store, null, 2), "utf-8");
|
|
647
|
+
_cachedStore = store;
|
|
648
|
+
_cacheTimestamp = Date.now();
|
|
649
|
+
}
|
|
650
|
+
function addLicenseEntry(entry) {
|
|
651
|
+
const store = readLicenseStore();
|
|
652
|
+
const idx = store.licenses.findIndex((l) => l.licenseKey === entry.licenseKey);
|
|
653
|
+
if (idx >= 0) {
|
|
654
|
+
store.licenses[idx] = entry;
|
|
655
|
+
} else {
|
|
656
|
+
store.licenses.push(entry);
|
|
657
|
+
}
|
|
658
|
+
writeLicenseStore(store);
|
|
659
|
+
}
|
|
660
|
+
function removeLicenseEntry(licenseKey) {
|
|
661
|
+
const store = readLicenseStore();
|
|
662
|
+
store.licenses = store.licenses.filter((l) => l.licenseKey !== licenseKey);
|
|
663
|
+
writeLicenseStore(store);
|
|
664
|
+
}
|
|
665
|
+
function getActiveLicenses() {
|
|
666
|
+
return readLicenseStore().licenses.filter((l) => l.status === "active");
|
|
667
|
+
}
|
|
668
|
+
function getLicenseForFeature(feature) {
|
|
669
|
+
const active = getActiveLicenses();
|
|
670
|
+
return active.find((l) => {
|
|
671
|
+
const features = VARIANT_FEATURE_MAP[l.variantId];
|
|
672
|
+
return features?.includes(feature);
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
var REVALIDATION_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
|
|
469
676
|
|
|
470
677
|
// ../../packages/shared/src/featureGate.ts
|
|
471
678
|
var DefaultFeatureGate = class {
|
|
@@ -485,25 +692,25 @@ var MAX_DECISIONS = 100;
|
|
|
485
692
|
var DecisionStorage = class {
|
|
486
693
|
storagePath;
|
|
487
694
|
decisionsFilePath;
|
|
488
|
-
constructor(
|
|
489
|
-
const mainRoot = resolveStorageRoot(
|
|
490
|
-
this.storagePath =
|
|
491
|
-
this.decisionsFilePath =
|
|
695
|
+
constructor(workspacePath) {
|
|
696
|
+
const mainRoot = resolveStorageRoot(workspacePath);
|
|
697
|
+
this.storagePath = path4.join(mainRoot, STORAGE_DIR2);
|
|
698
|
+
this.decisionsFilePath = path4.join(this.storagePath, DECISIONS_FILE);
|
|
492
699
|
}
|
|
493
700
|
ensureStorageDir() {
|
|
494
|
-
if (!
|
|
495
|
-
|
|
701
|
+
if (!fs3.existsSync(this.storagePath)) {
|
|
702
|
+
fs3.mkdirSync(this.storagePath, { recursive: true });
|
|
496
703
|
}
|
|
497
704
|
}
|
|
498
705
|
getProjectName() {
|
|
499
|
-
return
|
|
706
|
+
return path4.basename(path4.dirname(this.storagePath));
|
|
500
707
|
}
|
|
501
708
|
load() {
|
|
502
709
|
try {
|
|
503
|
-
if (!
|
|
710
|
+
if (!fs3.existsSync(this.decisionsFilePath)) {
|
|
504
711
|
return createEmptyProjectDecisions(this.getProjectName());
|
|
505
712
|
}
|
|
506
|
-
const raw =
|
|
713
|
+
const raw = fs3.readFileSync(this.decisionsFilePath, "utf-8");
|
|
507
714
|
const data = JSON.parse(raw);
|
|
508
715
|
return data;
|
|
509
716
|
} catch {
|
|
@@ -513,7 +720,7 @@ var DecisionStorage = class {
|
|
|
513
720
|
save(decisions) {
|
|
514
721
|
this.ensureStorageDir();
|
|
515
722
|
const content = JSON.stringify(decisions, null, 2);
|
|
516
|
-
|
|
723
|
+
fs3.writeFileSync(this.decisionsFilePath, content, "utf-8");
|
|
517
724
|
}
|
|
518
725
|
/**
|
|
519
726
|
* Save a decision record as a draft. Always persists regardless of Pro
|
|
@@ -763,68 +970,6 @@ function tryDetectDecision(opts) {
|
|
|
763
970
|
};
|
|
764
971
|
}
|
|
765
972
|
|
|
766
|
-
// ../../packages/shared/src/license.ts
|
|
767
|
-
import crypto from "crypto";
|
|
768
|
-
import fs3 from "fs";
|
|
769
|
-
import os from "os";
|
|
770
|
-
import path4 from "path";
|
|
771
|
-
var LICENSE_FILE = "license.json";
|
|
772
|
-
var DEVICE_ID_FILE = "device-id";
|
|
773
|
-
function getGlobalLicenseDir() {
|
|
774
|
-
return path4.join(os.homedir(), ".keepgoing");
|
|
775
|
-
}
|
|
776
|
-
function getGlobalLicensePath() {
|
|
777
|
-
return path4.join(getGlobalLicenseDir(), LICENSE_FILE);
|
|
778
|
-
}
|
|
779
|
-
function getDeviceId() {
|
|
780
|
-
const dir = getGlobalLicenseDir();
|
|
781
|
-
const filePath = path4.join(dir, DEVICE_ID_FILE);
|
|
782
|
-
try {
|
|
783
|
-
const existing = fs3.readFileSync(filePath, "utf-8").trim();
|
|
784
|
-
if (existing) return existing;
|
|
785
|
-
} catch {
|
|
786
|
-
}
|
|
787
|
-
const id = crypto.randomUUID();
|
|
788
|
-
if (!fs3.existsSync(dir)) {
|
|
789
|
-
fs3.mkdirSync(dir, { recursive: true });
|
|
790
|
-
}
|
|
791
|
-
fs3.writeFileSync(filePath, id, "utf-8");
|
|
792
|
-
return id;
|
|
793
|
-
}
|
|
794
|
-
function readLicenseCache() {
|
|
795
|
-
const licensePath = getGlobalLicensePath();
|
|
796
|
-
try {
|
|
797
|
-
if (!fs3.existsSync(licensePath)) {
|
|
798
|
-
return void 0;
|
|
799
|
-
}
|
|
800
|
-
const raw = fs3.readFileSync(licensePath, "utf-8");
|
|
801
|
-
return JSON.parse(raw);
|
|
802
|
-
} catch {
|
|
803
|
-
return void 0;
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
function writeLicenseCache(cache) {
|
|
807
|
-
const dirPath = getGlobalLicenseDir();
|
|
808
|
-
if (!fs3.existsSync(dirPath)) {
|
|
809
|
-
fs3.mkdirSync(dirPath, { recursive: true });
|
|
810
|
-
}
|
|
811
|
-
const licensePath = path4.join(dirPath, LICENSE_FILE);
|
|
812
|
-
fs3.writeFileSync(licensePath, JSON.stringify(cache, null, 2), "utf-8");
|
|
813
|
-
}
|
|
814
|
-
function deleteLicenseCache() {
|
|
815
|
-
const licensePath = getGlobalLicensePath();
|
|
816
|
-
try {
|
|
817
|
-
if (fs3.existsSync(licensePath)) {
|
|
818
|
-
fs3.unlinkSync(licensePath);
|
|
819
|
-
}
|
|
820
|
-
} catch {
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
function isCachedLicenseValid(cache) {
|
|
824
|
-
return cache?.status === "active";
|
|
825
|
-
}
|
|
826
|
-
var REVALIDATION_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
|
|
827
|
-
|
|
828
973
|
// ../../packages/shared/src/licenseClient.ts
|
|
829
974
|
var BASE_URL = "https://api.lemonsqueezy.com/v1/licenses";
|
|
830
975
|
var REQUEST_TIMEOUT_MS = 15e3;
|
|
@@ -875,13 +1020,21 @@ async function activateLicense(licenseKey, instanceName, options) {
|
|
|
875
1020
|
}
|
|
876
1021
|
return { valid: false, error: productError };
|
|
877
1022
|
}
|
|
1023
|
+
if (data.meta?.variant_id && !KNOWN_VARIANT_IDS.has(data.meta.variant_id)) {
|
|
1024
|
+
if (data.license_key?.key && data.instance?.id) {
|
|
1025
|
+
await deactivateLicense(data.license_key.key, data.instance.id);
|
|
1026
|
+
}
|
|
1027
|
+
return { valid: false, error: "This license key is for an unrecognized add-on variant. Please update KeepGoing or contact support." };
|
|
1028
|
+
}
|
|
878
1029
|
}
|
|
879
1030
|
return {
|
|
880
1031
|
valid: true,
|
|
881
1032
|
licenseKey: data.license_key?.key,
|
|
882
1033
|
instanceId: data.instance?.id,
|
|
883
1034
|
customerName: data.meta?.customer_name,
|
|
884
|
-
productName: data.meta?.product_name
|
|
1035
|
+
productName: data.meta?.product_name,
|
|
1036
|
+
variantId: data.meta?.variant_id,
|
|
1037
|
+
variantName: data.meta?.variant_name
|
|
885
1038
|
};
|
|
886
1039
|
} catch (err) {
|
|
887
1040
|
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";
|
|
@@ -907,11 +1060,14 @@ async function deactivateLicense(licenseKey, instanceId) {
|
|
|
907
1060
|
}
|
|
908
1061
|
|
|
909
1062
|
// src/storage.ts
|
|
1063
|
+
import fs4 from "fs";
|
|
1064
|
+
import path5 from "path";
|
|
910
1065
|
var STORAGE_DIR3 = ".keepgoing";
|
|
911
1066
|
var META_FILE2 = "meta.json";
|
|
912
1067
|
var SESSIONS_FILE2 = "sessions.json";
|
|
913
1068
|
var DECISIONS_FILE2 = "decisions.json";
|
|
914
1069
|
var STATE_FILE2 = "state.json";
|
|
1070
|
+
var CURRENT_TASKS_FILE2 = "current-tasks.json";
|
|
915
1071
|
var KeepGoingReader = class {
|
|
916
1072
|
workspacePath;
|
|
917
1073
|
storagePath;
|
|
@@ -919,18 +1075,20 @@ var KeepGoingReader = class {
|
|
|
919
1075
|
sessionsFilePath;
|
|
920
1076
|
decisionsFilePath;
|
|
921
1077
|
stateFilePath;
|
|
1078
|
+
currentTasksFilePath;
|
|
922
1079
|
_isWorktree;
|
|
923
1080
|
_cachedBranch = null;
|
|
924
1081
|
// null = not yet resolved
|
|
925
|
-
constructor(
|
|
926
|
-
this.workspacePath =
|
|
927
|
-
const mainRoot = resolveStorageRoot(
|
|
928
|
-
this._isWorktree = mainRoot !==
|
|
1082
|
+
constructor(workspacePath) {
|
|
1083
|
+
this.workspacePath = workspacePath;
|
|
1084
|
+
const mainRoot = resolveStorageRoot(workspacePath);
|
|
1085
|
+
this._isWorktree = mainRoot !== workspacePath;
|
|
929
1086
|
this.storagePath = path5.join(mainRoot, STORAGE_DIR3);
|
|
930
1087
|
this.metaFilePath = path5.join(this.storagePath, META_FILE2);
|
|
931
1088
|
this.sessionsFilePath = path5.join(this.storagePath, SESSIONS_FILE2);
|
|
932
1089
|
this.decisionsFilePath = path5.join(this.storagePath, DECISIONS_FILE2);
|
|
933
1090
|
this.stateFilePath = path5.join(this.storagePath, STATE_FILE2);
|
|
1091
|
+
this.currentTasksFilePath = path5.join(this.storagePath, CURRENT_TASKS_FILE2);
|
|
934
1092
|
}
|
|
935
1093
|
/** Check if .keepgoing/ directory exists. */
|
|
936
1094
|
exists() {
|
|
@@ -992,9 +1150,81 @@ var KeepGoingReader = class {
|
|
|
992
1150
|
const all = this.getDecisions();
|
|
993
1151
|
return all.slice(-count).reverse();
|
|
994
1152
|
}
|
|
995
|
-
/** Read
|
|
996
|
-
|
|
997
|
-
return
|
|
1153
|
+
/** Read the multi-license store from `~/.keepgoing/license.json`. */
|
|
1154
|
+
getLicenseStore() {
|
|
1155
|
+
return readLicenseStore();
|
|
1156
|
+
}
|
|
1157
|
+
/**
|
|
1158
|
+
* Read all current tasks from current-tasks.json.
|
|
1159
|
+
* Automatically filters out stale finished sessions (> 2 hours).
|
|
1160
|
+
*/
|
|
1161
|
+
getCurrentTasks() {
|
|
1162
|
+
const multiRaw = this.readJsonFile(this.currentTasksFilePath);
|
|
1163
|
+
if (multiRaw) {
|
|
1164
|
+
const tasks = Array.isArray(multiRaw) ? multiRaw : multiRaw.tasks ?? [];
|
|
1165
|
+
return this.pruneStale(tasks);
|
|
1166
|
+
}
|
|
1167
|
+
return [];
|
|
1168
|
+
}
|
|
1169
|
+
/** Get only active sessions (sessionActive=true and within stale threshold). */
|
|
1170
|
+
getActiveTasks() {
|
|
1171
|
+
return this.getCurrentTasks().filter((t) => t.sessionActive);
|
|
1172
|
+
}
|
|
1173
|
+
/** Get a specific session by ID. */
|
|
1174
|
+
getTaskBySessionId(sessionId) {
|
|
1175
|
+
return this.getCurrentTasks().find((t) => t.sessionId === sessionId);
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Detect files being edited by multiple sessions simultaneously.
|
|
1179
|
+
* Returns pairs of session IDs and the conflicting file paths.
|
|
1180
|
+
*/
|
|
1181
|
+
detectFileConflicts() {
|
|
1182
|
+
const activeTasks = this.getActiveTasks();
|
|
1183
|
+
if (activeTasks.length < 2) return [];
|
|
1184
|
+
const fileToSessions = /* @__PURE__ */ new Map();
|
|
1185
|
+
for (const task of activeTasks) {
|
|
1186
|
+
if (task.lastFileEdited && task.sessionId) {
|
|
1187
|
+
const existing = fileToSessions.get(task.lastFileEdited) ?? [];
|
|
1188
|
+
existing.push({
|
|
1189
|
+
sessionId: task.sessionId,
|
|
1190
|
+
agentLabel: task.agentLabel,
|
|
1191
|
+
branch: task.branch
|
|
1192
|
+
});
|
|
1193
|
+
fileToSessions.set(task.lastFileEdited, existing);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
const conflicts = [];
|
|
1197
|
+
for (const [file, sessions] of fileToSessions) {
|
|
1198
|
+
if (sessions.length > 1) {
|
|
1199
|
+
conflicts.push({ file, sessions });
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
return conflicts;
|
|
1203
|
+
}
|
|
1204
|
+
/**
|
|
1205
|
+
* Detect sessions on the same branch (possible duplicate work).
|
|
1206
|
+
*/
|
|
1207
|
+
detectBranchOverlap() {
|
|
1208
|
+
const activeTasks = this.getActiveTasks();
|
|
1209
|
+
if (activeTasks.length < 2) return [];
|
|
1210
|
+
const branchToSessions = /* @__PURE__ */ new Map();
|
|
1211
|
+
for (const task of activeTasks) {
|
|
1212
|
+
if (task.branch && task.sessionId) {
|
|
1213
|
+
const existing = branchToSessions.get(task.branch) ?? [];
|
|
1214
|
+
existing.push({ sessionId: task.sessionId, agentLabel: task.agentLabel });
|
|
1215
|
+
branchToSessions.set(task.branch, existing);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
const overlaps = [];
|
|
1219
|
+
for (const [branch, sessions] of branchToSessions) {
|
|
1220
|
+
if (sessions.length > 1) {
|
|
1221
|
+
overlaps.push({ branch, sessions });
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
return overlaps;
|
|
1225
|
+
}
|
|
1226
|
+
pruneStale(tasks) {
|
|
1227
|
+
return pruneStaleTasks(tasks);
|
|
998
1228
|
}
|
|
999
1229
|
/** Get the last session checkpoint for a specific branch. */
|
|
1000
1230
|
getLastSessionForBranch(branch) {
|
|
@@ -1112,13 +1342,13 @@ var KeepGoingReader = class {
|
|
|
1112
1342
|
};
|
|
1113
1343
|
|
|
1114
1344
|
// src/tools/getMomentum.ts
|
|
1115
|
-
function registerGetMomentum(
|
|
1116
|
-
|
|
1345
|
+
function registerGetMomentum(server, reader, workspacePath) {
|
|
1346
|
+
server.tool(
|
|
1117
1347
|
"get_momentum",
|
|
1118
1348
|
"Get current developer momentum: last checkpoint, next step, blockers, and branch context. Use this to understand where the developer left off.",
|
|
1119
1349
|
{},
|
|
1120
1350
|
async () => {
|
|
1121
|
-
if (!
|
|
1351
|
+
if (!reader.exists()) {
|
|
1122
1352
|
return {
|
|
1123
1353
|
content: [
|
|
1124
1354
|
{
|
|
@@ -1128,8 +1358,8 @@ function registerGetMomentum(server2, reader2, workspacePath2) {
|
|
|
1128
1358
|
]
|
|
1129
1359
|
};
|
|
1130
1360
|
}
|
|
1131
|
-
const { session: lastSession, isFallback } =
|
|
1132
|
-
const currentBranch =
|
|
1361
|
+
const { session: lastSession, isFallback } = reader.getScopedLastSession();
|
|
1362
|
+
const currentBranch = reader.getCurrentBranch();
|
|
1133
1363
|
if (!lastSession) {
|
|
1134
1364
|
return {
|
|
1135
1365
|
content: [
|
|
@@ -1140,13 +1370,13 @@ function registerGetMomentum(server2, reader2, workspacePath2) {
|
|
|
1140
1370
|
]
|
|
1141
1371
|
};
|
|
1142
1372
|
}
|
|
1143
|
-
const state =
|
|
1373
|
+
const state = reader.getState();
|
|
1144
1374
|
const branchChanged = lastSession.gitBranch && currentBranch && lastSession.gitBranch !== currentBranch;
|
|
1145
1375
|
const lines = [
|
|
1146
1376
|
`## Developer Momentum`,
|
|
1147
1377
|
""
|
|
1148
1378
|
];
|
|
1149
|
-
if (
|
|
1379
|
+
if (reader.isWorktree && currentBranch) {
|
|
1150
1380
|
lines.push(`**Worktree context:** Scoped to branch \`${currentBranch}\``);
|
|
1151
1381
|
if (isFallback) {
|
|
1152
1382
|
lines.push(`**Note:** No checkpoints found for branch \`${currentBranch}\`. Showing last global checkpoint.`);
|
|
@@ -1168,7 +1398,7 @@ function registerGetMomentum(server2, reader2, workspacePath2) {
|
|
|
1168
1398
|
if (currentBranch) {
|
|
1169
1399
|
lines.push(`**Current branch:** ${currentBranch}`);
|
|
1170
1400
|
}
|
|
1171
|
-
if (branchChanged && !
|
|
1401
|
+
if (branchChanged && !reader.isWorktree) {
|
|
1172
1402
|
lines.push(
|
|
1173
1403
|
`**Note:** Branch changed since last checkpoint (was \`${lastSession.gitBranch}\`, now \`${currentBranch}\`)`
|
|
1174
1404
|
);
|
|
@@ -1197,8 +1427,8 @@ function registerGetMomentum(server2, reader2, workspacePath2) {
|
|
|
1197
1427
|
|
|
1198
1428
|
// src/tools/getSessionHistory.ts
|
|
1199
1429
|
import { z } from "zod";
|
|
1200
|
-
function registerGetSessionHistory(
|
|
1201
|
-
|
|
1430
|
+
function registerGetSessionHistory(server, reader) {
|
|
1431
|
+
server.tool(
|
|
1202
1432
|
"get_session_history",
|
|
1203
1433
|
"Get recent session checkpoints. Returns a chronological list of what the developer worked on.",
|
|
1204
1434
|
{
|
|
@@ -1206,7 +1436,7 @@ function registerGetSessionHistory(server2, reader2) {
|
|
|
1206
1436
|
branch: z.string().optional().describe('Filter to a specific branch name, or "all" to show all branches. Auto-detected from worktree context by default.')
|
|
1207
1437
|
},
|
|
1208
1438
|
async ({ limit, branch }) => {
|
|
1209
|
-
if (!
|
|
1439
|
+
if (!reader.exists()) {
|
|
1210
1440
|
return {
|
|
1211
1441
|
content: [
|
|
1212
1442
|
{
|
|
@@ -1216,8 +1446,8 @@ function registerGetSessionHistory(server2, reader2) {
|
|
|
1216
1446
|
]
|
|
1217
1447
|
};
|
|
1218
1448
|
}
|
|
1219
|
-
const { effectiveBranch, scopeLabel } =
|
|
1220
|
-
const sessions = effectiveBranch ?
|
|
1449
|
+
const { effectiveBranch, scopeLabel } = reader.resolveBranchScope(branch);
|
|
1450
|
+
const sessions = effectiveBranch ? reader.getRecentSessionsForBranch(effectiveBranch, limit) : reader.getRecentSessions(limit);
|
|
1221
1451
|
if (sessions.length === 0) {
|
|
1222
1452
|
return {
|
|
1223
1453
|
content: [
|
|
@@ -1257,13 +1487,13 @@ function registerGetSessionHistory(server2, reader2) {
|
|
|
1257
1487
|
}
|
|
1258
1488
|
|
|
1259
1489
|
// src/tools/getReentryBriefing.ts
|
|
1260
|
-
function registerGetReentryBriefing(
|
|
1261
|
-
|
|
1490
|
+
function registerGetReentryBriefing(server, reader, workspacePath) {
|
|
1491
|
+
server.tool(
|
|
1262
1492
|
"get_reentry_briefing",
|
|
1263
1493
|
"Get a synthesized re-entry briefing that helps a developer understand where they left off. Includes focus, recent activity, and suggested next steps.",
|
|
1264
1494
|
{},
|
|
1265
1495
|
async () => {
|
|
1266
|
-
if (!
|
|
1496
|
+
if (!reader.exists()) {
|
|
1267
1497
|
return {
|
|
1268
1498
|
content: [
|
|
1269
1499
|
{
|
|
@@ -1273,12 +1503,12 @@ function registerGetReentryBriefing(server2, reader2, workspacePath2) {
|
|
|
1273
1503
|
]
|
|
1274
1504
|
};
|
|
1275
1505
|
}
|
|
1276
|
-
const gitBranch =
|
|
1277
|
-
const { session: lastSession } =
|
|
1278
|
-
const recentSessions =
|
|
1279
|
-
const state =
|
|
1506
|
+
const gitBranch = reader.getCurrentBranch();
|
|
1507
|
+
const { session: lastSession } = reader.getScopedLastSession();
|
|
1508
|
+
const recentSessions = reader.getScopedRecentSessions(5);
|
|
1509
|
+
const state = reader.getState() ?? {};
|
|
1280
1510
|
const sinceTimestamp = lastSession?.timestamp;
|
|
1281
|
-
const recentCommits = sinceTimestamp ? getCommitMessagesSince(
|
|
1511
|
+
const recentCommits = sinceTimestamp ? getCommitMessagesSince(workspacePath, sinceTimestamp) : [];
|
|
1282
1512
|
const briefing = generateBriefing(
|
|
1283
1513
|
lastSession,
|
|
1284
1514
|
recentSessions,
|
|
@@ -1300,7 +1530,7 @@ function registerGetReentryBriefing(server2, reader2, workspacePath2) {
|
|
|
1300
1530
|
`## Re-entry Briefing`,
|
|
1301
1531
|
""
|
|
1302
1532
|
];
|
|
1303
|
-
if (
|
|
1533
|
+
if (reader.isWorktree && gitBranch) {
|
|
1304
1534
|
lines.push(`**Worktree context:** Scoped to branch \`${gitBranch}\``);
|
|
1305
1535
|
lines.push("");
|
|
1306
1536
|
}
|
|
@@ -1311,7 +1541,7 @@ function registerGetReentryBriefing(server2, reader2, workspacePath2) {
|
|
|
1311
1541
|
`**Suggested next:** ${briefing.suggestedNext}`,
|
|
1312
1542
|
`**Quick start:** ${briefing.smallNextStep}`
|
|
1313
1543
|
);
|
|
1314
|
-
const recentDecisions =
|
|
1544
|
+
const recentDecisions = reader.getScopedRecentDecisions(3);
|
|
1315
1545
|
if (recentDecisions.length > 0) {
|
|
1316
1546
|
lines.push("");
|
|
1317
1547
|
lines.push("### Recent decisions");
|
|
@@ -1330,8 +1560,8 @@ function registerGetReentryBriefing(server2, reader2, workspacePath2) {
|
|
|
1330
1560
|
// src/tools/saveCheckpoint.ts
|
|
1331
1561
|
import path6 from "path";
|
|
1332
1562
|
import { z as z2 } from "zod";
|
|
1333
|
-
function registerSaveCheckpoint(
|
|
1334
|
-
|
|
1563
|
+
function registerSaveCheckpoint(server, reader, workspacePath) {
|
|
1564
|
+
server.tool(
|
|
1335
1565
|
"save_checkpoint",
|
|
1336
1566
|
"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.",
|
|
1337
1567
|
{
|
|
@@ -1340,11 +1570,12 @@ function registerSaveCheckpoint(server2, reader2, workspacePath2) {
|
|
|
1340
1570
|
blocker: z2.string().optional().describe("Any blocker preventing progress")
|
|
1341
1571
|
},
|
|
1342
1572
|
async ({ summary, nextStep, blocker }) => {
|
|
1343
|
-
const lastSession =
|
|
1344
|
-
const gitBranch = getCurrentBranch(
|
|
1345
|
-
const touchedFiles = getTouchedFiles(
|
|
1346
|
-
const commitHashes = getCommitsSince(
|
|
1347
|
-
const projectName = path6.basename(resolveStorageRoot(
|
|
1573
|
+
const lastSession = reader.getLastSession();
|
|
1574
|
+
const gitBranch = getCurrentBranch(workspacePath);
|
|
1575
|
+
const touchedFiles = getTouchedFiles(workspacePath);
|
|
1576
|
+
const commitHashes = getCommitsSince(workspacePath, lastSession?.timestamp);
|
|
1577
|
+
const projectName = path6.basename(resolveStorageRoot(workspacePath));
|
|
1578
|
+
const sessionId = generateSessionId({ workspaceRoot: workspacePath, branch: gitBranch ?? void 0, worktreePath: workspacePath });
|
|
1348
1579
|
const checkpoint = createCheckpoint({
|
|
1349
1580
|
summary,
|
|
1350
1581
|
nextStep: nextStep || "",
|
|
@@ -1352,10 +1583,11 @@ function registerSaveCheckpoint(server2, reader2, workspacePath2) {
|
|
|
1352
1583
|
gitBranch,
|
|
1353
1584
|
touchedFiles,
|
|
1354
1585
|
commitHashes,
|
|
1355
|
-
workspaceRoot:
|
|
1356
|
-
source: "manual"
|
|
1586
|
+
workspaceRoot: workspacePath,
|
|
1587
|
+
source: "manual",
|
|
1588
|
+
sessionId
|
|
1357
1589
|
});
|
|
1358
|
-
const writer = new KeepGoingWriter(
|
|
1590
|
+
const writer = new KeepGoingWriter(workspacePath);
|
|
1359
1591
|
writer.saveCheckpoint(checkpoint, projectName);
|
|
1360
1592
|
const lines = [
|
|
1361
1593
|
`Checkpoint saved.`,
|
|
@@ -1365,11 +1597,11 @@ function registerSaveCheckpoint(server2, reader2, workspacePath2) {
|
|
|
1365
1597
|
`- **Commits captured:** ${commitHashes.length}`
|
|
1366
1598
|
];
|
|
1367
1599
|
if (commitHashes.length > 0) {
|
|
1368
|
-
const commitMessages = getCommitMessagesSince(
|
|
1369
|
-
const headHash = getHeadCommitHash(
|
|
1600
|
+
const commitMessages = getCommitMessagesSince(workspacePath, lastSession?.timestamp);
|
|
1601
|
+
const headHash = getHeadCommitHash(workspacePath);
|
|
1370
1602
|
if (commitMessages.length > 0 && headHash) {
|
|
1371
1603
|
const detected = tryDetectDecision({
|
|
1372
|
-
workspacePath
|
|
1604
|
+
workspacePath,
|
|
1373
1605
|
checkpointId: checkpoint.id,
|
|
1374
1606
|
gitBranch,
|
|
1375
1607
|
commitHash: headHash,
|
|
@@ -1390,8 +1622,8 @@ function registerSaveCheckpoint(server2, reader2, workspacePath2) {
|
|
|
1390
1622
|
|
|
1391
1623
|
// src/tools/getDecisions.ts
|
|
1392
1624
|
import { z as z3 } from "zod";
|
|
1393
|
-
function registerGetDecisions(
|
|
1394
|
-
|
|
1625
|
+
function registerGetDecisions(server, reader) {
|
|
1626
|
+
server.tool(
|
|
1395
1627
|
"get_decisions",
|
|
1396
1628
|
"Get recent decision records. Returns detected high-signal commits with their category, confidence, and rationale.",
|
|
1397
1629
|
{
|
|
@@ -1399,7 +1631,7 @@ function registerGetDecisions(server2, reader2) {
|
|
|
1399
1631
|
branch: z3.string().optional().describe('Filter to a specific branch name, or "all" to show all branches. Auto-detected from worktree context by default.')
|
|
1400
1632
|
},
|
|
1401
1633
|
async ({ limit, branch }) => {
|
|
1402
|
-
if (!
|
|
1634
|
+
if (!reader.exists()) {
|
|
1403
1635
|
return {
|
|
1404
1636
|
content: [
|
|
1405
1637
|
{
|
|
@@ -1409,8 +1641,7 @@ function registerGetDecisions(server2, reader2) {
|
|
|
1409
1641
|
]
|
|
1410
1642
|
};
|
|
1411
1643
|
}
|
|
1412
|
-
|
|
1413
|
-
if (!isCachedLicenseValid(licenseCache)) {
|
|
1644
|
+
if (process.env.KEEPGOING_PRO_BYPASS !== "1" && !getLicenseForFeature("decisions")) {
|
|
1414
1645
|
return {
|
|
1415
1646
|
content: [
|
|
1416
1647
|
{
|
|
@@ -1420,8 +1651,8 @@ function registerGetDecisions(server2, reader2) {
|
|
|
1420
1651
|
]
|
|
1421
1652
|
};
|
|
1422
1653
|
}
|
|
1423
|
-
const { effectiveBranch, scopeLabel } =
|
|
1424
|
-
const decisions = effectiveBranch ?
|
|
1654
|
+
const { effectiveBranch, scopeLabel } = reader.resolveBranchScope(branch);
|
|
1655
|
+
const decisions = effectiveBranch ? reader.getRecentDecisionsForBranch(effectiveBranch, limit) : reader.getRecentDecisions(limit);
|
|
1425
1656
|
if (decisions.length === 0) {
|
|
1426
1657
|
return {
|
|
1427
1658
|
content: [
|
|
@@ -1459,8 +1690,104 @@ function registerGetDecisions(server2, reader2) {
|
|
|
1459
1690
|
);
|
|
1460
1691
|
}
|
|
1461
1692
|
|
|
1693
|
+
// src/tools/getCurrentTask.ts
|
|
1694
|
+
function registerGetCurrentTask(server, reader) {
|
|
1695
|
+
server.tool(
|
|
1696
|
+
"get_current_task",
|
|
1697
|
+
"Get current live session tasks. Shows all active AI agent sessions, what each is doing, last files edited, and next steps. Supports multiple concurrent sessions.",
|
|
1698
|
+
{},
|
|
1699
|
+
async () => {
|
|
1700
|
+
if (!reader.exists()) {
|
|
1701
|
+
return {
|
|
1702
|
+
content: [
|
|
1703
|
+
{
|
|
1704
|
+
type: "text",
|
|
1705
|
+
text: "No KeepGoing data found."
|
|
1706
|
+
}
|
|
1707
|
+
]
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
1710
|
+
if (process.env.KEEPGOING_PRO_BYPASS !== "1" && !getLicenseForFeature("session-awareness")) {
|
|
1711
|
+
return {
|
|
1712
|
+
content: [
|
|
1713
|
+
{
|
|
1714
|
+
type: "text",
|
|
1715
|
+
text: "Session Awareness requires a license. Use the activate_license tool, run `keepgoing activate <key>` in your terminal, or visit https://keepgoing.dev/add-ons to purchase."
|
|
1716
|
+
}
|
|
1717
|
+
]
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1720
|
+
const tasks = reader.getCurrentTasks();
|
|
1721
|
+
if (tasks.length === 0) {
|
|
1722
|
+
return {
|
|
1723
|
+
content: [
|
|
1724
|
+
{
|
|
1725
|
+
type: "text",
|
|
1726
|
+
text: "No current task data found. The agent has not started writing session data yet."
|
|
1727
|
+
}
|
|
1728
|
+
]
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
const activeTasks = tasks.filter((t) => t.sessionActive);
|
|
1732
|
+
const finishedTasks = tasks.filter((t) => !t.sessionActive);
|
|
1733
|
+
const lines = [];
|
|
1734
|
+
const totalActive = activeTasks.length;
|
|
1735
|
+
const totalFinished = finishedTasks.length;
|
|
1736
|
+
if (totalActive > 0 || totalFinished > 0) {
|
|
1737
|
+
const parts = [];
|
|
1738
|
+
if (totalActive > 0) parts.push(`${totalActive} active`);
|
|
1739
|
+
if (totalFinished > 0) parts.push(`${totalFinished} finished`);
|
|
1740
|
+
lines.push(`## Live Sessions (${parts.join(", ")})`);
|
|
1741
|
+
lines.push("");
|
|
1742
|
+
}
|
|
1743
|
+
for (const task of [...activeTasks, ...finishedTasks]) {
|
|
1744
|
+
const statusIcon = task.sessionActive ? "\u{1F7E2}" : "\u2705";
|
|
1745
|
+
const statusLabel = task.sessionActive ? "Active" : "Finished";
|
|
1746
|
+
const sessionLabel = task.agentLabel || task.sessionId || "Session";
|
|
1747
|
+
lines.push(`### ${statusIcon} ${sessionLabel} (${statusLabel})`);
|
|
1748
|
+
lines.push(`- **Updated:** ${formatRelativeTime(task.updatedAt)}`);
|
|
1749
|
+
if (task.branch) {
|
|
1750
|
+
lines.push(`- **Branch:** ${task.branch}`);
|
|
1751
|
+
}
|
|
1752
|
+
if (task.taskSummary) {
|
|
1753
|
+
lines.push(`- **Doing:** ${task.taskSummary}`);
|
|
1754
|
+
}
|
|
1755
|
+
if (task.lastFileEdited) {
|
|
1756
|
+
lines.push(`- **Last file:** ${task.lastFileEdited}`);
|
|
1757
|
+
}
|
|
1758
|
+
if (task.nextStep) {
|
|
1759
|
+
lines.push(`- **Next step:** ${task.nextStep}`);
|
|
1760
|
+
}
|
|
1761
|
+
lines.push("");
|
|
1762
|
+
}
|
|
1763
|
+
const conflicts = reader.detectFileConflicts();
|
|
1764
|
+
if (conflicts.length > 0) {
|
|
1765
|
+
lines.push("### \u26A0\uFE0F Potential Conflicts");
|
|
1766
|
+
for (const conflict of conflicts) {
|
|
1767
|
+
const sessionLabels = conflict.sessions.map((s) => s.agentLabel || s.sessionId || "unknown").join(", ");
|
|
1768
|
+
lines.push(`- **${conflict.file}** is being edited by: ${sessionLabels}`);
|
|
1769
|
+
}
|
|
1770
|
+
lines.push("");
|
|
1771
|
+
}
|
|
1772
|
+
const overlaps = reader.detectBranchOverlap();
|
|
1773
|
+
if (overlaps.length > 0) {
|
|
1774
|
+
lines.push("### \u2139\uFE0F Branch Overlap");
|
|
1775
|
+
for (const overlap of overlaps) {
|
|
1776
|
+
const sessionLabels = overlap.sessions.map((s) => s.agentLabel || s.sessionId || "unknown").join(", ");
|
|
1777
|
+
lines.push(`- **${overlap.branch}**: ${sessionLabels} (possible duplicate work)`);
|
|
1778
|
+
}
|
|
1779
|
+
lines.push("");
|
|
1780
|
+
}
|
|
1781
|
+
return {
|
|
1782
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
);
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1462
1788
|
// src/tools/setupProject.ts
|
|
1463
1789
|
import fs5 from "fs";
|
|
1790
|
+
import os2 from "os";
|
|
1464
1791
|
import path7 from "path";
|
|
1465
1792
|
import { z as z4 } from "zod";
|
|
1466
1793
|
var KEEPGOING_MARKER = "@keepgoingdev/mcp-server";
|
|
@@ -1482,6 +1809,15 @@ var STOP_HOOK = {
|
|
|
1482
1809
|
}
|
|
1483
1810
|
]
|
|
1484
1811
|
};
|
|
1812
|
+
var POST_TOOL_USE_HOOK = {
|
|
1813
|
+
matcher: "Edit|Write|MultiEdit",
|
|
1814
|
+
hooks: [
|
|
1815
|
+
{
|
|
1816
|
+
type: "command",
|
|
1817
|
+
command: "npx -y @keepgoingdev/mcp-server --update-task-from-hook"
|
|
1818
|
+
}
|
|
1819
|
+
]
|
|
1820
|
+
};
|
|
1485
1821
|
var CLAUDE_MD_SECTION = `
|
|
1486
1822
|
## KeepGoing
|
|
1487
1823
|
|
|
@@ -1495,8 +1831,8 @@ function hasKeepGoingHook(hookEntries) {
|
|
|
1495
1831
|
(entry) => entry?.hooks?.some((h) => typeof h?.command === "string" && h.command.includes(KEEPGOING_MARKER))
|
|
1496
1832
|
);
|
|
1497
1833
|
}
|
|
1498
|
-
function registerSetupProject(
|
|
1499
|
-
|
|
1834
|
+
function registerSetupProject(server, workspacePath) {
|
|
1835
|
+
server.tool(
|
|
1500
1836
|
"setup_project",
|
|
1501
1837
|
"Set up KeepGoing in the current project. Adds session hooks to .claude/settings.json and CLAUDE.md instructions so checkpoints are saved automatically.",
|
|
1502
1838
|
{
|
|
@@ -1505,44 +1841,80 @@ function registerSetupProject(server2, workspacePath2) {
|
|
|
1505
1841
|
},
|
|
1506
1842
|
async ({ sessionHooks, claudeMd }) => {
|
|
1507
1843
|
const results = [];
|
|
1844
|
+
const claudeDir = path7.join(workspacePath, ".claude");
|
|
1845
|
+
const settingsPath = path7.join(claudeDir, "settings.json");
|
|
1846
|
+
let settings = {};
|
|
1847
|
+
if (fs5.existsSync(settingsPath)) {
|
|
1848
|
+
settings = JSON.parse(fs5.readFileSync(settingsPath, "utf-8"));
|
|
1849
|
+
}
|
|
1850
|
+
let settingsChanged = false;
|
|
1508
1851
|
if (sessionHooks) {
|
|
1509
|
-
const claudeDir = path7.join(workspacePath2, ".claude");
|
|
1510
|
-
const settingsPath = path7.join(claudeDir, "settings.json");
|
|
1511
|
-
let settings = {};
|
|
1512
|
-
if (fs5.existsSync(settingsPath)) {
|
|
1513
|
-
settings = JSON.parse(fs5.readFileSync(settingsPath, "utf-8"));
|
|
1514
|
-
}
|
|
1515
1852
|
if (!settings.hooks) {
|
|
1516
1853
|
settings.hooks = {};
|
|
1517
1854
|
}
|
|
1518
|
-
let hooksChanged = false;
|
|
1519
1855
|
if (!Array.isArray(settings.hooks.SessionStart)) {
|
|
1520
1856
|
settings.hooks.SessionStart = [];
|
|
1521
1857
|
}
|
|
1522
1858
|
if (!hasKeepGoingHook(settings.hooks.SessionStart)) {
|
|
1523
1859
|
settings.hooks.SessionStart.push(SESSION_START_HOOK);
|
|
1524
|
-
|
|
1860
|
+
settingsChanged = true;
|
|
1525
1861
|
}
|
|
1526
1862
|
if (!Array.isArray(settings.hooks.Stop)) {
|
|
1527
1863
|
settings.hooks.Stop = [];
|
|
1528
1864
|
}
|
|
1529
1865
|
if (!hasKeepGoingHook(settings.hooks.Stop)) {
|
|
1530
1866
|
settings.hooks.Stop.push(STOP_HOOK);
|
|
1531
|
-
|
|
1867
|
+
settingsChanged = true;
|
|
1532
1868
|
}
|
|
1533
|
-
if (
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1869
|
+
if (!Array.isArray(settings.hooks.PostToolUse)) {
|
|
1870
|
+
settings.hooks.PostToolUse = [];
|
|
1871
|
+
}
|
|
1872
|
+
if (!hasKeepGoingHook(settings.hooks.PostToolUse)) {
|
|
1873
|
+
settings.hooks.PostToolUse.push(POST_TOOL_USE_HOOK);
|
|
1874
|
+
settingsChanged = true;
|
|
1875
|
+
}
|
|
1876
|
+
if (settingsChanged) {
|
|
1538
1877
|
results.push("**Session hooks:** Added to `.claude/settings.json`");
|
|
1539
1878
|
} else {
|
|
1540
1879
|
results.push("**Session hooks:** Already present, skipped");
|
|
1541
1880
|
}
|
|
1542
1881
|
}
|
|
1882
|
+
if (process.env.KEEPGOING_PRO_BYPASS === "1" || getLicenseForFeature("session-awareness")) {
|
|
1883
|
+
const statuslineSrc = path7.resolve(
|
|
1884
|
+
new URL(".", import.meta.url).pathname,
|
|
1885
|
+
"statusline.sh"
|
|
1886
|
+
);
|
|
1887
|
+
const claudeHome = path7.join(os2.homedir(), ".claude");
|
|
1888
|
+
const statuslineDest = path7.join(claudeHome, "keepgoing-statusline.sh");
|
|
1889
|
+
if (fs5.existsSync(statuslineSrc)) {
|
|
1890
|
+
if (!fs5.existsSync(claudeHome)) {
|
|
1891
|
+
fs5.mkdirSync(claudeHome, { recursive: true });
|
|
1892
|
+
}
|
|
1893
|
+
fs5.copyFileSync(statuslineSrc, statuslineDest);
|
|
1894
|
+
fs5.chmodSync(statuslineDest, 493);
|
|
1895
|
+
if (!settings.statusLine) {
|
|
1896
|
+
settings.statusLine = {
|
|
1897
|
+
type: "command",
|
|
1898
|
+
command: statuslineDest
|
|
1899
|
+
};
|
|
1900
|
+
settingsChanged = true;
|
|
1901
|
+
results.push("**Statusline:** Installed `keepgoing-statusline.sh` and added to `.claude/settings.json`");
|
|
1902
|
+
} else {
|
|
1903
|
+
results.push("**Statusline:** `statusLine` already configured in settings, skipped");
|
|
1904
|
+
}
|
|
1905
|
+
} else {
|
|
1906
|
+
results.push("**Statusline:** Script not found in package, skipped");
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
if (settingsChanged) {
|
|
1910
|
+
if (!fs5.existsSync(claudeDir)) {
|
|
1911
|
+
fs5.mkdirSync(claudeDir, { recursive: true });
|
|
1912
|
+
}
|
|
1913
|
+
fs5.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
1914
|
+
}
|
|
1543
1915
|
if (claudeMd) {
|
|
1544
|
-
const dotClaudeMdPath = path7.join(
|
|
1545
|
-
const rootClaudeMdPath = path7.join(
|
|
1916
|
+
const dotClaudeMdPath = path7.join(workspacePath, ".claude", "CLAUDE.md");
|
|
1917
|
+
const rootClaudeMdPath = path7.join(workspacePath, "CLAUDE.md");
|
|
1546
1918
|
const claudeMdPath = fs5.existsSync(dotClaudeMdPath) ? dotClaudeMdPath : rootClaudeMdPath;
|
|
1547
1919
|
let existing = "";
|
|
1548
1920
|
if (fs5.existsSync(claudeMdPath)) {
|
|
@@ -1565,20 +1937,24 @@ function registerSetupProject(server2, workspacePath2) {
|
|
|
1565
1937
|
|
|
1566
1938
|
// src/tools/activateLicense.ts
|
|
1567
1939
|
import { z as z5 } from "zod";
|
|
1568
|
-
function registerActivateLicense(
|
|
1569
|
-
|
|
1940
|
+
function registerActivateLicense(server) {
|
|
1941
|
+
server.tool(
|
|
1570
1942
|
"activate_license",
|
|
1571
|
-
"Activate a KeepGoing Pro license on this device. Unlocks Decision Detection and
|
|
1943
|
+
"Activate a KeepGoing Pro license on this device. Unlocks add-ons like Decision Detection and Session Awareness.",
|
|
1572
1944
|
{ license_key: z5.string().describe("Your KeepGoing Pro license key") },
|
|
1573
1945
|
async ({ license_key }) => {
|
|
1574
|
-
const
|
|
1575
|
-
|
|
1576
|
-
|
|
1946
|
+
const store = readLicenseStore();
|
|
1947
|
+
const existingForKey = store.licenses.find(
|
|
1948
|
+
(l) => l.status === "active" && l.licenseKey === license_key
|
|
1949
|
+
);
|
|
1950
|
+
if (existingForKey) {
|
|
1951
|
+
const label2 = getVariantLabel(existingForKey.variantId);
|
|
1952
|
+
const who2 = existingForKey.customerName ? ` (${existingForKey.customerName})` : "";
|
|
1577
1953
|
return {
|
|
1578
1954
|
content: [
|
|
1579
1955
|
{
|
|
1580
1956
|
type: "text",
|
|
1581
|
-
text:
|
|
1957
|
+
text: `${label2} is already active${who2}. No action needed.`
|
|
1582
1958
|
}
|
|
1583
1959
|
]
|
|
1584
1960
|
};
|
|
@@ -1594,22 +1970,43 @@ function registerActivateLicense(server2) {
|
|
|
1594
1970
|
]
|
|
1595
1971
|
};
|
|
1596
1972
|
}
|
|
1973
|
+
const variantId = result.variantId;
|
|
1974
|
+
const existingForVariant = store.licenses.find(
|
|
1975
|
+
(l) => l.status === "active" && l.variantId === variantId
|
|
1976
|
+
);
|
|
1977
|
+
if (existingForVariant) {
|
|
1978
|
+
const label2 = getVariantLabel(variantId);
|
|
1979
|
+
const who2 = existingForVariant.customerName ? ` (${existingForVariant.customerName})` : "";
|
|
1980
|
+
return {
|
|
1981
|
+
content: [
|
|
1982
|
+
{
|
|
1983
|
+
type: "text",
|
|
1984
|
+
text: `${label2} is already active${who2}. No action needed.`
|
|
1985
|
+
}
|
|
1986
|
+
]
|
|
1987
|
+
};
|
|
1988
|
+
}
|
|
1597
1989
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1598
|
-
|
|
1990
|
+
addLicenseEntry({
|
|
1599
1991
|
licenseKey: result.licenseKey || license_key,
|
|
1600
1992
|
instanceId: result.instanceId || getDeviceId(),
|
|
1601
1993
|
status: "active",
|
|
1602
1994
|
lastValidatedAt: now,
|
|
1603
1995
|
activatedAt: now,
|
|
1996
|
+
variantId,
|
|
1604
1997
|
customerName: result.customerName,
|
|
1605
|
-
productName: result.productName
|
|
1998
|
+
productName: result.productName,
|
|
1999
|
+
variantName: result.variantName
|
|
1606
2000
|
});
|
|
2001
|
+
const label = getVariantLabel(variantId);
|
|
2002
|
+
const features = VARIANT_FEATURE_MAP[variantId];
|
|
2003
|
+
const featureList = features ? features.join(", ") : "Pro features";
|
|
1607
2004
|
const who = result.customerName ? ` Welcome, ${result.customerName}!` : "";
|
|
1608
2005
|
return {
|
|
1609
2006
|
content: [
|
|
1610
2007
|
{
|
|
1611
2008
|
type: "text",
|
|
1612
|
-
text:
|
|
2009
|
+
text: `${label} activated successfully.${who} Enabled: ${featureList}.`
|
|
1613
2010
|
}
|
|
1614
2011
|
]
|
|
1615
2012
|
};
|
|
@@ -1618,14 +2015,18 @@ function registerActivateLicense(server2) {
|
|
|
1618
2015
|
}
|
|
1619
2016
|
|
|
1620
2017
|
// src/tools/deactivateLicense.ts
|
|
1621
|
-
|
|
1622
|
-
|
|
2018
|
+
import { z as z6 } from "zod";
|
|
2019
|
+
function registerDeactivateLicense(server) {
|
|
2020
|
+
server.tool(
|
|
1623
2021
|
"deactivate_license",
|
|
1624
2022
|
"Deactivate the KeepGoing Pro license on this device.",
|
|
1625
|
-
{
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
2023
|
+
{
|
|
2024
|
+
license_key: z6.string().optional().describe("Specific license key to deactivate. If omitted and only one license is active, deactivates it. If multiple are active, lists them.")
|
|
2025
|
+
},
|
|
2026
|
+
async ({ license_key }) => {
|
|
2027
|
+
const store = readLicenseStore();
|
|
2028
|
+
const activeLicenses = store.licenses.filter((l) => l.status === "active");
|
|
2029
|
+
if (activeLicenses.length === 0) {
|
|
1629
2030
|
return {
|
|
1630
2031
|
content: [
|
|
1631
2032
|
{
|
|
@@ -1635,14 +2036,41 @@ function registerDeactivateLicense(server2) {
|
|
|
1635
2036
|
]
|
|
1636
2037
|
};
|
|
1637
2038
|
}
|
|
1638
|
-
|
|
1639
|
-
|
|
2039
|
+
let target;
|
|
2040
|
+
if (license_key) {
|
|
2041
|
+
target = activeLicenses.find((l) => l.licenseKey === license_key);
|
|
2042
|
+
if (!target) {
|
|
2043
|
+
return {
|
|
2044
|
+
content: [
|
|
2045
|
+
{
|
|
2046
|
+
type: "text",
|
|
2047
|
+
text: `No active license found with key "${license_key}".`
|
|
2048
|
+
}
|
|
2049
|
+
]
|
|
2050
|
+
};
|
|
2051
|
+
}
|
|
2052
|
+
} else if (activeLicenses.length === 1) {
|
|
2053
|
+
target = activeLicenses[0];
|
|
2054
|
+
} else {
|
|
2055
|
+
const lines = ["Multiple active licenses found. Please specify which to deactivate using the license_key parameter:", ""];
|
|
2056
|
+
for (const l of activeLicenses) {
|
|
2057
|
+
const label2 = getVariantLabel(l.variantId);
|
|
2058
|
+
const who = l.customerName ? ` (${l.customerName})` : "";
|
|
2059
|
+
lines.push(`- ${label2}${who}: ${l.licenseKey}`);
|
|
2060
|
+
}
|
|
2061
|
+
return {
|
|
2062
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
2063
|
+
};
|
|
2064
|
+
}
|
|
2065
|
+
const result = await deactivateLicense(target.licenseKey, target.instanceId);
|
|
2066
|
+
removeLicenseEntry(target.licenseKey);
|
|
2067
|
+
const label = getVariantLabel(target.variantId);
|
|
1640
2068
|
if (!result.deactivated) {
|
|
1641
2069
|
return {
|
|
1642
2070
|
content: [
|
|
1643
2071
|
{
|
|
1644
2072
|
type: "text",
|
|
1645
|
-
text:
|
|
2073
|
+
text: `${label} license cleared locally, but remote deactivation failed: ${result.error ?? "unknown error"}`
|
|
1646
2074
|
}
|
|
1647
2075
|
]
|
|
1648
2076
|
};
|
|
@@ -1651,7 +2079,7 @@ function registerDeactivateLicense(server2) {
|
|
|
1651
2079
|
content: [
|
|
1652
2080
|
{
|
|
1653
2081
|
type: "text",
|
|
1654
|
-
text:
|
|
2082
|
+
text: `${label} license deactivated successfully. The activation slot has been freed.`
|
|
1655
2083
|
}
|
|
1656
2084
|
]
|
|
1657
2085
|
};
|
|
@@ -1660,8 +2088,8 @@ function registerDeactivateLicense(server2) {
|
|
|
1660
2088
|
}
|
|
1661
2089
|
|
|
1662
2090
|
// src/prompts/resume.ts
|
|
1663
|
-
function registerResumePrompt(
|
|
1664
|
-
|
|
2091
|
+
function registerResumePrompt(server) {
|
|
2092
|
+
server.prompt(
|
|
1665
2093
|
"resume",
|
|
1666
2094
|
"Check developer momentum and suggest what to work on next",
|
|
1667
2095
|
async () => ({
|
|
@@ -1688,8 +2116,8 @@ function registerResumePrompt(server2) {
|
|
|
1688
2116
|
}
|
|
1689
2117
|
|
|
1690
2118
|
// src/prompts/decisions.ts
|
|
1691
|
-
function registerDecisionsPrompt(
|
|
1692
|
-
|
|
2119
|
+
function registerDecisionsPrompt(server) {
|
|
2120
|
+
server.prompt(
|
|
1693
2121
|
"decisions",
|
|
1694
2122
|
"Review recent architectural decisions and their rationale",
|
|
1695
2123
|
async () => ({
|
|
@@ -1716,8 +2144,8 @@ function registerDecisionsPrompt(server2) {
|
|
|
1716
2144
|
}
|
|
1717
2145
|
|
|
1718
2146
|
// src/prompts/progress.ts
|
|
1719
|
-
function registerProgressPrompt(
|
|
1720
|
-
|
|
2147
|
+
function registerProgressPrompt(server) {
|
|
2148
|
+
server.prompt(
|
|
1721
2149
|
"progress",
|
|
1722
2150
|
"Summarize recent development progress across sessions",
|
|
1723
2151
|
async () => ({
|
|
@@ -1743,14 +2171,20 @@ function registerProgressPrompt(server2) {
|
|
|
1743
2171
|
);
|
|
1744
2172
|
}
|
|
1745
2173
|
|
|
1746
|
-
// src/
|
|
1747
|
-
|
|
1748
|
-
const
|
|
1749
|
-
|
|
1750
|
-
|
|
2174
|
+
// src/cli/util.ts
|
|
2175
|
+
function resolveWsPath(args = process.argv.slice(2)) {
|
|
2176
|
+
const explicit = args.find((a) => !a.startsWith("--"));
|
|
2177
|
+
return findGitRoot(explicit || process.cwd());
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
// src/cli/print.ts
|
|
2181
|
+
async function handlePrintMomentum() {
|
|
2182
|
+
const wsPath = resolveWsPath();
|
|
2183
|
+
const reader = new KeepGoingReader(wsPath);
|
|
2184
|
+
if (!reader.exists()) {
|
|
1751
2185
|
process.exit(0);
|
|
1752
2186
|
}
|
|
1753
|
-
const { session: lastSession } =
|
|
2187
|
+
const { session: lastSession } = reader.getScopedLastSession();
|
|
1754
2188
|
if (!lastSession) {
|
|
1755
2189
|
process.exit(0);
|
|
1756
2190
|
}
|
|
@@ -1776,10 +2210,49 @@ if (process.argv.includes("--print-momentum")) {
|
|
|
1776
2210
|
console.log(lines.join("\n"));
|
|
1777
2211
|
process.exit(0);
|
|
1778
2212
|
}
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
2213
|
+
async function handlePrintCurrent() {
|
|
2214
|
+
if (process.env.KEEPGOING_PRO_BYPASS !== "1" && !getLicenseForFeature("session-awareness")) {
|
|
2215
|
+
process.exit(0);
|
|
2216
|
+
}
|
|
2217
|
+
const wsPath = resolveWsPath();
|
|
2218
|
+
const reader = new KeepGoingReader(wsPath);
|
|
2219
|
+
const tasks = reader.getCurrentTasks();
|
|
2220
|
+
if (tasks.length === 0) {
|
|
2221
|
+
process.exit(0);
|
|
2222
|
+
}
|
|
2223
|
+
const activeTasks = tasks.filter((t) => t.sessionActive);
|
|
2224
|
+
const finishedTasks = tasks.filter((t) => !t.sessionActive);
|
|
2225
|
+
if (tasks.length > 1) {
|
|
2226
|
+
const parts = [];
|
|
2227
|
+
if (activeTasks.length > 0) parts.push(`${activeTasks.length} active`);
|
|
2228
|
+
if (finishedTasks.length > 0) parts.push(`${finishedTasks.length} finished`);
|
|
2229
|
+
console.log(`[KeepGoing] Sessions: ${parts.join(", ")}`);
|
|
2230
|
+
}
|
|
2231
|
+
for (const task of [...activeTasks, ...finishedTasks]) {
|
|
2232
|
+
const prefix = task.sessionActive ? "[KeepGoing] Current task:" : "[KeepGoing] \u2705 Last task:";
|
|
2233
|
+
const sessionLabel = task.agentLabel || task.sessionId || "";
|
|
2234
|
+
const labelSuffix = sessionLabel ? ` (${sessionLabel})` : "";
|
|
2235
|
+
const lines = [`${prefix} ${formatRelativeTime(task.updatedAt)}${labelSuffix}`];
|
|
2236
|
+
if (task.branch) {
|
|
2237
|
+
lines.push(` Branch: ${task.branch}`);
|
|
2238
|
+
}
|
|
2239
|
+
if (task.taskSummary) {
|
|
2240
|
+
lines.push(` Doing: ${task.taskSummary}`);
|
|
2241
|
+
}
|
|
2242
|
+
if (task.nextStep) {
|
|
2243
|
+
lines.push(` Next: ${task.nextStep}`);
|
|
2244
|
+
}
|
|
2245
|
+
console.log(lines.join("\n"));
|
|
2246
|
+
}
|
|
2247
|
+
process.exit(0);
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
// src/cli/saveCheckpoint.ts
|
|
2251
|
+
import path8 from "path";
|
|
2252
|
+
async function handleSaveCheckpoint() {
|
|
2253
|
+
const wsPath = resolveWsPath();
|
|
2254
|
+
const reader = new KeepGoingReader(wsPath);
|
|
2255
|
+
const { session: lastSession } = reader.getScopedLastSession();
|
|
1783
2256
|
if (lastSession?.timestamp) {
|
|
1784
2257
|
const ageMs = Date.now() - new Date(lastSession.timestamp).getTime();
|
|
1785
2258
|
if (ageMs < 2 * 60 * 1e3) {
|
|
@@ -1804,6 +2277,7 @@ if (process.argv.includes("--save-checkpoint")) {
|
|
|
1804
2277
|
}
|
|
1805
2278
|
}
|
|
1806
2279
|
const projectName = path8.basename(resolveStorageRoot(wsPath));
|
|
2280
|
+
const sessionId = generateSessionId({ workspaceRoot: wsPath, branch: gitBranch ?? void 0, worktreePath: wsPath });
|
|
1807
2281
|
const checkpoint = createCheckpoint({
|
|
1808
2282
|
summary,
|
|
1809
2283
|
nextStep: "",
|
|
@@ -1811,12 +2285,19 @@ if (process.argv.includes("--save-checkpoint")) {
|
|
|
1811
2285
|
touchedFiles,
|
|
1812
2286
|
commitHashes,
|
|
1813
2287
|
workspaceRoot: wsPath,
|
|
1814
|
-
source: "auto"
|
|
2288
|
+
source: "auto",
|
|
2289
|
+
sessionId
|
|
1815
2290
|
});
|
|
1816
2291
|
const writer = new KeepGoingWriter(wsPath);
|
|
1817
2292
|
writer.saveCheckpoint(checkpoint, projectName);
|
|
1818
|
-
|
|
1819
|
-
|
|
2293
|
+
writer.upsertSession({
|
|
2294
|
+
sessionId,
|
|
2295
|
+
sessionActive: false,
|
|
2296
|
+
nextStep: checkpoint.nextStep || void 0,
|
|
2297
|
+
branch: gitBranch ?? void 0,
|
|
2298
|
+
updatedAt: checkpoint.timestamp
|
|
2299
|
+
});
|
|
2300
|
+
if ((process.env.KEEPGOING_PRO_BYPASS === "1" || getLicenseForFeature("decisions")) && commitMessages.length > 0) {
|
|
1820
2301
|
const headHash = getHeadCommitHash(wsPath) || commitHashes[0];
|
|
1821
2302
|
if (headHash) {
|
|
1822
2303
|
const detected = tryDetectDecision({
|
|
@@ -1835,24 +2316,109 @@ if (process.argv.includes("--save-checkpoint")) {
|
|
|
1835
2316
|
console.log(`[KeepGoing] Auto-checkpoint saved: ${summary}`);
|
|
1836
2317
|
process.exit(0);
|
|
1837
2318
|
}
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
2319
|
+
|
|
2320
|
+
// src/cli/updateTask.ts
|
|
2321
|
+
async function handleUpdateTask() {
|
|
2322
|
+
const args = process.argv.slice(2);
|
|
2323
|
+
const flagIndex = args.indexOf("--update-task");
|
|
2324
|
+
const payloadStr = args[flagIndex + 1];
|
|
2325
|
+
const wsArgs = args.filter((a, i) => !a.startsWith("--") && i !== flagIndex + 1);
|
|
2326
|
+
const wsPath = resolveWsPath(wsArgs.length > 0 ? wsArgs : void 0);
|
|
2327
|
+
if (payloadStr) {
|
|
2328
|
+
try {
|
|
2329
|
+
const payload = JSON.parse(payloadStr);
|
|
2330
|
+
const writer = new KeepGoingWriter(wsPath);
|
|
2331
|
+
const branch = payload.branch ?? getCurrentBranch(wsPath) ?? void 0;
|
|
2332
|
+
const task = {
|
|
2333
|
+
...payload,
|
|
2334
|
+
branch,
|
|
2335
|
+
worktreePath: wsPath,
|
|
2336
|
+
sessionActive: payload.sessionActive !== false,
|
|
2337
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2338
|
+
};
|
|
2339
|
+
const sessionId = payload.sessionId || generateSessionId({ ...task, workspaceRoot: wsPath });
|
|
2340
|
+
task.sessionId = sessionId;
|
|
2341
|
+
writer.upsertSession(task);
|
|
2342
|
+
} catch {
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
process.exit(0);
|
|
2346
|
+
}
|
|
2347
|
+
var STDIN_TIMEOUT_MS = 5e3;
|
|
2348
|
+
async function handleUpdateTaskFromHook() {
|
|
2349
|
+
const wsPath = resolveWsPath();
|
|
2350
|
+
const chunks = [];
|
|
2351
|
+
const timeout = setTimeout(() => process.exit(0), STDIN_TIMEOUT_MS);
|
|
2352
|
+
process.stdin.on("error", () => {
|
|
2353
|
+
clearTimeout(timeout);
|
|
2354
|
+
process.exit(0);
|
|
2355
|
+
});
|
|
2356
|
+
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
2357
|
+
process.stdin.on("end", () => {
|
|
2358
|
+
clearTimeout(timeout);
|
|
2359
|
+
try {
|
|
2360
|
+
const raw = Buffer.concat(chunks).toString("utf-8").trim();
|
|
2361
|
+
if (!raw) {
|
|
2362
|
+
process.exit(0);
|
|
2363
|
+
}
|
|
2364
|
+
const hookData = JSON.parse(raw);
|
|
2365
|
+
const toolName = hookData.tool_name ?? "Edit";
|
|
2366
|
+
const filePath = hookData.tool_input?.file_path ?? hookData.tool_input?.path ?? "";
|
|
2367
|
+
const fileName = filePath ? filePath.split("/").pop() ?? filePath : "";
|
|
2368
|
+
const writer = new KeepGoingWriter(wsPath);
|
|
2369
|
+
const existing = writer.readCurrentTasks();
|
|
2370
|
+
const cachedBranch = existing.find((t) => t.sessionActive && t.worktreePath === wsPath)?.branch;
|
|
2371
|
+
const branch = cachedBranch ?? getCurrentBranch(wsPath) ?? void 0;
|
|
2372
|
+
const task = {
|
|
2373
|
+
taskSummary: fileName ? `${toolName} ${fileName}` : `Used ${toolName}`,
|
|
2374
|
+
lastFileEdited: filePath || void 0,
|
|
2375
|
+
branch,
|
|
2376
|
+
worktreePath: wsPath,
|
|
2377
|
+
sessionActive: true,
|
|
2378
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2379
|
+
};
|
|
2380
|
+
const sessionId = hookData.session_id || generateSessionId({ ...task, workspaceRoot: wsPath });
|
|
2381
|
+
task.sessionId = sessionId;
|
|
2382
|
+
writer.upsertSession(task);
|
|
2383
|
+
} catch {
|
|
2384
|
+
}
|
|
2385
|
+
process.exit(0);
|
|
2386
|
+
});
|
|
2387
|
+
process.stdin.resume();
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
// src/index.ts
|
|
2391
|
+
var CLI_HANDLERS = {
|
|
2392
|
+
"--print-momentum": handlePrintMomentum,
|
|
2393
|
+
"--save-checkpoint": handleSaveCheckpoint,
|
|
2394
|
+
"--update-task": handleUpdateTask,
|
|
2395
|
+
"--update-task-from-hook": handleUpdateTaskFromHook,
|
|
2396
|
+
"--print-current": handlePrintCurrent
|
|
2397
|
+
};
|
|
2398
|
+
var flag = process.argv.slice(2).find((a) => a in CLI_HANDLERS);
|
|
2399
|
+
if (flag) {
|
|
2400
|
+
await CLI_HANDLERS[flag]();
|
|
2401
|
+
} else {
|
|
2402
|
+
const workspacePath = findGitRoot(process.argv[2] || process.cwd());
|
|
2403
|
+
const reader = new KeepGoingReader(workspacePath);
|
|
2404
|
+
const server = new McpServer({
|
|
2405
|
+
name: "keepgoing",
|
|
2406
|
+
version: "0.1.0"
|
|
2407
|
+
});
|
|
2408
|
+
registerGetMomentum(server, reader, workspacePath);
|
|
2409
|
+
registerGetSessionHistory(server, reader);
|
|
2410
|
+
registerGetReentryBriefing(server, reader, workspacePath);
|
|
2411
|
+
registerGetDecisions(server, reader);
|
|
2412
|
+
registerGetCurrentTask(server, reader);
|
|
2413
|
+
registerSaveCheckpoint(server, reader, workspacePath);
|
|
2414
|
+
registerSetupProject(server, workspacePath);
|
|
2415
|
+
registerActivateLicense(server);
|
|
2416
|
+
registerDeactivateLicense(server);
|
|
2417
|
+
registerResumePrompt(server);
|
|
2418
|
+
registerDecisionsPrompt(server);
|
|
2419
|
+
registerProgressPrompt(server);
|
|
2420
|
+
const transport = new StdioServerTransport();
|
|
2421
|
+
await server.connect(transport);
|
|
2422
|
+
console.error("KeepGoing MCP server started");
|
|
2423
|
+
}
|
|
1858
2424
|
//# sourceMappingURL=index.js.map
|