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