@keepgoingdev/mcp-server 0.3.1 → 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 +1004 -234
- 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() {
|
|
@@ -80,14 +75,12 @@ import { promisify } from "util";
|
|
|
80
75
|
var execFileAsync = promisify(execFile);
|
|
81
76
|
function findGitRoot(startPath) {
|
|
82
77
|
try {
|
|
83
|
-
const
|
|
78
|
+
const toplevel = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
84
79
|
cwd: startPath,
|
|
85
80
|
encoding: "utf-8",
|
|
86
81
|
timeout: 5e3
|
|
87
82
|
}).trim();
|
|
88
|
-
|
|
89
|
-
const absoluteGitDir = path.isAbsolute(gitCommonDir) ? gitCommonDir : path.resolve(startPath, gitCommonDir);
|
|
90
|
-
return path.dirname(absoluteGitDir);
|
|
83
|
+
return toplevel || startPath;
|
|
91
84
|
} catch {
|
|
92
85
|
return startPath;
|
|
93
86
|
}
|
|
@@ -118,10 +111,10 @@ function resolveStorageRoot(startPath) {
|
|
|
118
111
|
return startPath;
|
|
119
112
|
}
|
|
120
113
|
}
|
|
121
|
-
function getCurrentBranch(
|
|
114
|
+
function getCurrentBranch(workspacePath) {
|
|
122
115
|
try {
|
|
123
116
|
const result = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
124
|
-
cwd:
|
|
117
|
+
cwd: workspacePath,
|
|
125
118
|
encoding: "utf-8",
|
|
126
119
|
timeout: 5e3
|
|
127
120
|
});
|
|
@@ -130,14 +123,14 @@ function getCurrentBranch(workspacePath2) {
|
|
|
130
123
|
return void 0;
|
|
131
124
|
}
|
|
132
125
|
}
|
|
133
|
-
function getGitLogSince(
|
|
126
|
+
function getGitLogSince(workspacePath, format, sinceTimestamp) {
|
|
134
127
|
try {
|
|
135
128
|
const since = sinceTimestamp || new Date(Date.now() - 24 * 60 * 60 * 1e3).toISOString();
|
|
136
129
|
const result = execFileSync(
|
|
137
130
|
"git",
|
|
138
131
|
["log", `--since=${since}`, `--format=${format}`],
|
|
139
132
|
{
|
|
140
|
-
cwd:
|
|
133
|
+
cwd: workspacePath,
|
|
141
134
|
encoding: "utf-8",
|
|
142
135
|
timeout: 5e3
|
|
143
136
|
}
|
|
@@ -150,16 +143,16 @@ function getGitLogSince(workspacePath2, format, sinceTimestamp) {
|
|
|
150
143
|
return [];
|
|
151
144
|
}
|
|
152
145
|
}
|
|
153
|
-
function getCommitsSince(
|
|
154
|
-
return getGitLogSince(
|
|
146
|
+
function getCommitsSince(workspacePath, sinceTimestamp) {
|
|
147
|
+
return getGitLogSince(workspacePath, "%H", sinceTimestamp);
|
|
155
148
|
}
|
|
156
|
-
function getCommitMessagesSince(
|
|
157
|
-
return getGitLogSince(
|
|
149
|
+
function getCommitMessagesSince(workspacePath, sinceTimestamp) {
|
|
150
|
+
return getGitLogSince(workspacePath, "%s", sinceTimestamp);
|
|
158
151
|
}
|
|
159
|
-
function getHeadCommitHash(
|
|
152
|
+
function getHeadCommitHash(workspacePath) {
|
|
160
153
|
try {
|
|
161
154
|
const result = execFileSync("git", ["rev-parse", "HEAD"], {
|
|
162
|
-
cwd:
|
|
155
|
+
cwd: workspacePath,
|
|
163
156
|
encoding: "utf-8",
|
|
164
157
|
timeout: 5e3
|
|
165
158
|
});
|
|
@@ -168,10 +161,10 @@ function getHeadCommitHash(workspacePath2) {
|
|
|
168
161
|
return void 0;
|
|
169
162
|
}
|
|
170
163
|
}
|
|
171
|
-
function getTouchedFiles(
|
|
164
|
+
function getTouchedFiles(workspacePath) {
|
|
172
165
|
try {
|
|
173
166
|
const result = execFileSync("git", ["status", "--porcelain"], {
|
|
174
|
-
cwd:
|
|
167
|
+
cwd: workspacePath,
|
|
175
168
|
encoding: "utf-8",
|
|
176
169
|
timeout: 5e3
|
|
177
170
|
});
|
|
@@ -387,22 +380,34 @@ function inferFocusFromFiles(files) {
|
|
|
387
380
|
// ../../packages/shared/src/storage.ts
|
|
388
381
|
import fs from "fs";
|
|
389
382
|
import path2 from "path";
|
|
390
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
383
|
+
import { randomUUID as randomUUID2, createHash } from "crypto";
|
|
391
384
|
var STORAGE_DIR = ".keepgoing";
|
|
392
385
|
var META_FILE = "meta.json";
|
|
393
386
|
var SESSIONS_FILE = "sessions.json";
|
|
394
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
|
+
}
|
|
395
398
|
var KeepGoingWriter = class {
|
|
396
399
|
storagePath;
|
|
397
400
|
sessionsFilePath;
|
|
398
401
|
stateFilePath;
|
|
399
402
|
metaFilePath;
|
|
400
|
-
|
|
401
|
-
|
|
403
|
+
currentTasksFilePath;
|
|
404
|
+
constructor(workspacePath) {
|
|
405
|
+
const mainRoot = resolveStorageRoot(workspacePath);
|
|
402
406
|
this.storagePath = path2.join(mainRoot, STORAGE_DIR);
|
|
403
407
|
this.sessionsFilePath = path2.join(this.storagePath, SESSIONS_FILE);
|
|
404
408
|
this.stateFilePath = path2.join(this.storagePath, STATE_FILE);
|
|
405
409
|
this.metaFilePath = path2.join(this.storagePath, META_FILE);
|
|
410
|
+
this.currentTasksFilePath = path2.join(this.storagePath, CURRENT_TASKS_FILE);
|
|
406
411
|
}
|
|
407
412
|
ensureDir() {
|
|
408
413
|
if (!fs.existsSync(this.storagePath)) {
|
|
@@ -463,11 +468,212 @@ var KeepGoingWriter = class {
|
|
|
463
468
|
}
|
|
464
469
|
fs.writeFileSync(this.metaFilePath, JSON.stringify(meta, null, 2), "utf-8");
|
|
465
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
|
+
}
|
|
466
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
|
+
}
|
|
467
559
|
|
|
468
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";
|
|
469
566
|
import fs2 from "fs";
|
|
567
|
+
import os from "os";
|
|
470
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;
|
|
471
677
|
|
|
472
678
|
// ../../packages/shared/src/featureGate.ts
|
|
473
679
|
var DefaultFeatureGate = class {
|
|
@@ -487,25 +693,25 @@ var MAX_DECISIONS = 100;
|
|
|
487
693
|
var DecisionStorage = class {
|
|
488
694
|
storagePath;
|
|
489
695
|
decisionsFilePath;
|
|
490
|
-
constructor(
|
|
491
|
-
const mainRoot = resolveStorageRoot(
|
|
492
|
-
this.storagePath =
|
|
493
|
-
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);
|
|
494
700
|
}
|
|
495
701
|
ensureStorageDir() {
|
|
496
|
-
if (!
|
|
497
|
-
|
|
702
|
+
if (!fs3.existsSync(this.storagePath)) {
|
|
703
|
+
fs3.mkdirSync(this.storagePath, { recursive: true });
|
|
498
704
|
}
|
|
499
705
|
}
|
|
500
706
|
getProjectName() {
|
|
501
|
-
return
|
|
707
|
+
return path4.basename(path4.dirname(this.storagePath));
|
|
502
708
|
}
|
|
503
709
|
load() {
|
|
504
710
|
try {
|
|
505
|
-
if (!
|
|
711
|
+
if (!fs3.existsSync(this.decisionsFilePath)) {
|
|
506
712
|
return createEmptyProjectDecisions(this.getProjectName());
|
|
507
713
|
}
|
|
508
|
-
const raw =
|
|
714
|
+
const raw = fs3.readFileSync(this.decisionsFilePath, "utf-8");
|
|
509
715
|
const data = JSON.parse(raw);
|
|
510
716
|
return data;
|
|
511
717
|
} catch {
|
|
@@ -515,15 +721,14 @@ var DecisionStorage = class {
|
|
|
515
721
|
save(decisions) {
|
|
516
722
|
this.ensureStorageDir();
|
|
517
723
|
const content = JSON.stringify(decisions, null, 2);
|
|
518
|
-
|
|
724
|
+
fs3.writeFileSync(this.decisionsFilePath, content, "utf-8");
|
|
519
725
|
}
|
|
520
726
|
/**
|
|
521
|
-
* Save a decision record
|
|
727
|
+
* Save a decision record as a draft. Always persists regardless of Pro
|
|
728
|
+
* status so decisions are captured at the correct time. Returns true if
|
|
729
|
+
* saved, false on I/O error.
|
|
522
730
|
*/
|
|
523
731
|
saveDecision(decision) {
|
|
524
|
-
if (!isDecisionsEnabled()) {
|
|
525
|
-
return false;
|
|
526
|
-
}
|
|
527
732
|
const data = this.load();
|
|
528
733
|
data.decisions.push(decision);
|
|
529
734
|
data.lastDecisionId = decision.id;
|
|
@@ -766,128 +971,125 @@ function tryDetectDecision(opts) {
|
|
|
766
971
|
};
|
|
767
972
|
}
|
|
768
973
|
|
|
769
|
-
// ../../packages/shared/src/
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
return
|
|
778
|
-
}
|
|
779
|
-
function getGlobalLicensePath() {
|
|
780
|
-
return path4.join(getGlobalLicenseDir(), LICENSE_FILE);
|
|
781
|
-
}
|
|
782
|
-
function getDeviceId() {
|
|
783
|
-
const dir = getGlobalLicenseDir();
|
|
784
|
-
const filePath = path4.join(dir, DEVICE_ID_FILE);
|
|
785
|
-
try {
|
|
786
|
-
const existing = fs3.readFileSync(filePath, "utf-8").trim();
|
|
787
|
-
if (existing) return existing;
|
|
788
|
-
} catch {
|
|
789
|
-
}
|
|
790
|
-
const id = crypto.randomUUID();
|
|
791
|
-
if (!fs3.existsSync(dir)) {
|
|
792
|
-
fs3.mkdirSync(dir, { recursive: true });
|
|
793
|
-
}
|
|
794
|
-
fs3.writeFileSync(filePath, id, "utf-8");
|
|
795
|
-
return id;
|
|
796
|
-
}
|
|
797
|
-
function readLicenseCache() {
|
|
798
|
-
const licensePath = getGlobalLicensePath();
|
|
799
|
-
try {
|
|
800
|
-
if (!fs3.existsSync(licensePath)) {
|
|
801
|
-
return void 0;
|
|
802
|
-
}
|
|
803
|
-
const raw = fs3.readFileSync(licensePath, "utf-8");
|
|
804
|
-
return JSON.parse(raw);
|
|
805
|
-
} catch {
|
|
806
|
-
return void 0;
|
|
807
|
-
}
|
|
974
|
+
// ../../packages/shared/src/licenseClient.ts
|
|
975
|
+
var BASE_URL = "https://api.lemonsqueezy.com/v1/licenses";
|
|
976
|
+
var REQUEST_TIMEOUT_MS = 15e3;
|
|
977
|
+
var EXPECTED_STORE_ID = 301555;
|
|
978
|
+
var EXPECTED_PRODUCT_ID = 864311;
|
|
979
|
+
function fetchWithTimeout(url, init) {
|
|
980
|
+
const controller = new AbortController();
|
|
981
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
982
|
+
return fetch(url, { ...init, signal: controller.signal }).finally(() => clearTimeout(timer));
|
|
808
983
|
}
|
|
809
|
-
function
|
|
810
|
-
|
|
811
|
-
if (
|
|
812
|
-
|
|
984
|
+
function validateProductIdentity(meta) {
|
|
985
|
+
if (!meta) return "License response missing product metadata.";
|
|
986
|
+
if (meta.store_id !== EXPECTED_STORE_ID || meta.product_id !== EXPECTED_PRODUCT_ID) {
|
|
987
|
+
return "This license key does not belong to KeepGoing.";
|
|
813
988
|
}
|
|
814
|
-
|
|
815
|
-
fs3.writeFileSync(licensePath, JSON.stringify(cache, null, 2), "utf-8");
|
|
989
|
+
return void 0;
|
|
816
990
|
}
|
|
817
|
-
function
|
|
818
|
-
const licensePath = getGlobalLicensePath();
|
|
991
|
+
async function safeJson(res) {
|
|
819
992
|
try {
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
}
|
|
993
|
+
const text = await res.text();
|
|
994
|
+
return JSON.parse(text);
|
|
823
995
|
} catch {
|
|
996
|
+
return null;
|
|
824
997
|
}
|
|
825
998
|
}
|
|
826
|
-
function
|
|
827
|
-
return cache?.status === "active";
|
|
828
|
-
}
|
|
829
|
-
var REVALIDATION_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
|
|
830
|
-
|
|
831
|
-
// ../../packages/shared/src/licenseClient.ts
|
|
832
|
-
var BASE_URL = "https://api.lemonsqueezy.com/v1/licenses";
|
|
833
|
-
async function activateLicense(licenseKey, instanceName) {
|
|
999
|
+
async function activateLicense(licenseKey, instanceName, options) {
|
|
834
1000
|
try {
|
|
835
|
-
const res = await
|
|
1001
|
+
const res = await fetchWithTimeout(`${BASE_URL}/activate`, {
|
|
836
1002
|
method: "POST",
|
|
837
1003
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
838
1004
|
body: new URLSearchParams({ license_key: licenseKey, instance_name: instanceName })
|
|
839
1005
|
});
|
|
840
|
-
const data = await res
|
|
841
|
-
if (!res.ok || !data
|
|
842
|
-
return { valid: false, error: data
|
|
1006
|
+
const data = await safeJson(res);
|
|
1007
|
+
if (!res.ok || !data?.activated) {
|
|
1008
|
+
return { valid: false, error: data?.error || `Activation failed (${res.status})` };
|
|
1009
|
+
}
|
|
1010
|
+
if (!options?.allowTestMode && data.license_key?.test_mode) {
|
|
1011
|
+
if (data.license_key?.key && data.instance?.id) {
|
|
1012
|
+
await deactivateLicense(data.license_key.key, data.instance.id);
|
|
1013
|
+
}
|
|
1014
|
+
return { valid: false, error: "This is a test license key. Please use a production license key from your purchase confirmation." };
|
|
1015
|
+
}
|
|
1016
|
+
if (!options?.allowTestMode) {
|
|
1017
|
+
const productError = validateProductIdentity(data.meta);
|
|
1018
|
+
if (productError) {
|
|
1019
|
+
if (data.license_key?.key && data.instance?.id) {
|
|
1020
|
+
await deactivateLicense(data.license_key.key, data.instance.id);
|
|
1021
|
+
}
|
|
1022
|
+
return { valid: false, error: productError };
|
|
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
|
+
}
|
|
843
1030
|
}
|
|
844
1031
|
return {
|
|
845
1032
|
valid: true,
|
|
846
1033
|
licenseKey: data.license_key?.key,
|
|
847
1034
|
instanceId: data.instance?.id,
|
|
848
1035
|
customerName: data.meta?.customer_name,
|
|
849
|
-
productName: data.meta?.product_name
|
|
1036
|
+
productName: data.meta?.product_name,
|
|
1037
|
+
variantId: data.meta?.variant_id,
|
|
1038
|
+
variantName: data.meta?.variant_name
|
|
850
1039
|
};
|
|
851
1040
|
} catch (err) {
|
|
852
|
-
|
|
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";
|
|
1042
|
+
return { valid: false, error: message };
|
|
853
1043
|
}
|
|
854
1044
|
}
|
|
855
1045
|
async function deactivateLicense(licenseKey, instanceId) {
|
|
856
1046
|
try {
|
|
857
|
-
const res = await
|
|
1047
|
+
const res = await fetchWithTimeout(`${BASE_URL}/deactivate`, {
|
|
858
1048
|
method: "POST",
|
|
859
1049
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
860
1050
|
body: new URLSearchParams({ license_key: licenseKey, instance_id: instanceId })
|
|
861
1051
|
});
|
|
862
|
-
const data = await res
|
|
863
|
-
if (!res.ok || !data
|
|
864
|
-
return { deactivated: false, error: data
|
|
1052
|
+
const data = await safeJson(res);
|
|
1053
|
+
if (!res.ok || !data?.deactivated) {
|
|
1054
|
+
return { deactivated: false, error: data?.error || `Deactivation failed (${res.status})` };
|
|
865
1055
|
}
|
|
866
1056
|
return { deactivated: true };
|
|
867
1057
|
} catch (err) {
|
|
868
|
-
|
|
1058
|
+
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";
|
|
1059
|
+
return { deactivated: false, error: message };
|
|
869
1060
|
}
|
|
870
1061
|
}
|
|
871
1062
|
|
|
872
1063
|
// src/storage.ts
|
|
1064
|
+
import fs4 from "fs";
|
|
1065
|
+
import path5 from "path";
|
|
873
1066
|
var STORAGE_DIR3 = ".keepgoing";
|
|
874
1067
|
var META_FILE2 = "meta.json";
|
|
875
1068
|
var SESSIONS_FILE2 = "sessions.json";
|
|
876
1069
|
var DECISIONS_FILE2 = "decisions.json";
|
|
877
1070
|
var STATE_FILE2 = "state.json";
|
|
1071
|
+
var CURRENT_TASKS_FILE2 = "current-tasks.json";
|
|
878
1072
|
var KeepGoingReader = class {
|
|
1073
|
+
workspacePath;
|
|
879
1074
|
storagePath;
|
|
880
1075
|
metaFilePath;
|
|
881
1076
|
sessionsFilePath;
|
|
882
1077
|
decisionsFilePath;
|
|
883
1078
|
stateFilePath;
|
|
884
|
-
|
|
885
|
-
|
|
1079
|
+
currentTasksFilePath;
|
|
1080
|
+
_isWorktree;
|
|
1081
|
+
_cachedBranch = null;
|
|
1082
|
+
// null = not yet resolved
|
|
1083
|
+
constructor(workspacePath) {
|
|
1084
|
+
this.workspacePath = workspacePath;
|
|
1085
|
+
const mainRoot = resolveStorageRoot(workspacePath);
|
|
1086
|
+
this._isWorktree = mainRoot !== workspacePath;
|
|
886
1087
|
this.storagePath = path5.join(mainRoot, STORAGE_DIR3);
|
|
887
1088
|
this.metaFilePath = path5.join(this.storagePath, META_FILE2);
|
|
888
1089
|
this.sessionsFilePath = path5.join(this.storagePath, SESSIONS_FILE2);
|
|
889
1090
|
this.decisionsFilePath = path5.join(this.storagePath, DECISIONS_FILE2);
|
|
890
1091
|
this.stateFilePath = path5.join(this.storagePath, STATE_FILE2);
|
|
1092
|
+
this.currentTasksFilePath = path5.join(this.storagePath, CURRENT_TASKS_FILE2);
|
|
891
1093
|
}
|
|
892
1094
|
/** Check if .keepgoing/ directory exists. */
|
|
893
1095
|
exists() {
|
|
@@ -949,9 +1151,160 @@ var KeepGoingReader = class {
|
|
|
949
1151
|
const all = this.getDecisions();
|
|
950
1152
|
return all.slice(-count).reverse();
|
|
951
1153
|
}
|
|
952
|
-
/** Read
|
|
953
|
-
|
|
954
|
-
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);
|
|
1229
|
+
}
|
|
1230
|
+
/** Get the last session checkpoint for a specific branch. */
|
|
1231
|
+
getLastSessionForBranch(branch) {
|
|
1232
|
+
const sessions = this.getSessions().filter((s) => s.gitBranch === branch);
|
|
1233
|
+
return sessions.length > 0 ? sessions[sessions.length - 1] : void 0;
|
|
1234
|
+
}
|
|
1235
|
+
/** Returns the last N sessions for a specific branch, newest first. */
|
|
1236
|
+
getRecentSessionsForBranch(branch, count) {
|
|
1237
|
+
const filtered = this.getSessions().filter((s) => s.gitBranch === branch);
|
|
1238
|
+
return filtered.slice(-count).reverse();
|
|
1239
|
+
}
|
|
1240
|
+
/** Returns the last N decisions for a specific branch, newest first. */
|
|
1241
|
+
getRecentDecisionsForBranch(branch, count) {
|
|
1242
|
+
const filtered = this.getDecisions().filter((d) => d.gitBranch === branch);
|
|
1243
|
+
return filtered.slice(-count).reverse();
|
|
1244
|
+
}
|
|
1245
|
+
/** Whether the workspace is inside a git worktree. */
|
|
1246
|
+
get isWorktree() {
|
|
1247
|
+
return this._isWorktree;
|
|
1248
|
+
}
|
|
1249
|
+
/**
|
|
1250
|
+
* Returns the current git branch for this workspace.
|
|
1251
|
+
* Lazily cached: the branch is resolved once per KeepGoingReader instance.
|
|
1252
|
+
*/
|
|
1253
|
+
getCurrentBranch() {
|
|
1254
|
+
if (this._cachedBranch === null) {
|
|
1255
|
+
this._cachedBranch = getCurrentBranch(this.workspacePath);
|
|
1256
|
+
}
|
|
1257
|
+
return this._cachedBranch;
|
|
1258
|
+
}
|
|
1259
|
+
/**
|
|
1260
|
+
* Worktree-aware last session lookup.
|
|
1261
|
+
* In a worktree, scopes to the current branch with fallback to global.
|
|
1262
|
+
* Returns the session and whether it fell back to global.
|
|
1263
|
+
*/
|
|
1264
|
+
getScopedLastSession() {
|
|
1265
|
+
const branch = this.getCurrentBranch();
|
|
1266
|
+
if (this._isWorktree && branch) {
|
|
1267
|
+
const scoped = this.getLastSessionForBranch(branch);
|
|
1268
|
+
if (scoped) return { session: scoped, isFallback: false };
|
|
1269
|
+
return { session: this.getLastSession(), isFallback: true };
|
|
1270
|
+
}
|
|
1271
|
+
return { session: this.getLastSession(), isFallback: false };
|
|
1272
|
+
}
|
|
1273
|
+
/** Worktree-aware recent sessions. Scopes to current branch in a worktree. */
|
|
1274
|
+
getScopedRecentSessions(count) {
|
|
1275
|
+
const branch = this.getCurrentBranch();
|
|
1276
|
+
if (this._isWorktree && branch) {
|
|
1277
|
+
return this.getRecentSessionsForBranch(branch, count);
|
|
1278
|
+
}
|
|
1279
|
+
return this.getRecentSessions(count);
|
|
1280
|
+
}
|
|
1281
|
+
/** Worktree-aware recent decisions. Scopes to current branch in a worktree. */
|
|
1282
|
+
getScopedRecentDecisions(count) {
|
|
1283
|
+
const branch = this.getCurrentBranch();
|
|
1284
|
+
if (this._isWorktree && branch) {
|
|
1285
|
+
return this.getRecentDecisionsForBranch(branch, count);
|
|
1286
|
+
}
|
|
1287
|
+
return this.getRecentDecisions(count);
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* Resolves branch scope from an explicit `branch` parameter.
|
|
1291
|
+
* Used by tools that accept a `branch` argument (e.g. get_session_history, get_decisions).
|
|
1292
|
+
* - `"all"` returns no filter.
|
|
1293
|
+
* - An explicit branch name uses that.
|
|
1294
|
+
* - `undefined` auto-scopes to the current branch in a worktree, or all branches otherwise.
|
|
1295
|
+
*/
|
|
1296
|
+
resolveBranchScope(branch) {
|
|
1297
|
+
if (branch === "all") {
|
|
1298
|
+
return { effectiveBranch: void 0, scopeLabel: "all branches" };
|
|
1299
|
+
}
|
|
1300
|
+
if (branch) {
|
|
1301
|
+
return { effectiveBranch: branch, scopeLabel: `branch \`${branch}\`` };
|
|
1302
|
+
}
|
|
1303
|
+
const currentBranch = this.getCurrentBranch();
|
|
1304
|
+
if (this._isWorktree && currentBranch) {
|
|
1305
|
+
return { effectiveBranch: currentBranch, scopeLabel: `branch \`${currentBranch}\` (worktree)` };
|
|
1306
|
+
}
|
|
1307
|
+
return { effectiveBranch: void 0, scopeLabel: "all branches" };
|
|
955
1308
|
}
|
|
956
1309
|
/**
|
|
957
1310
|
* Parses sessions.json once, returning both the session list
|
|
@@ -990,13 +1343,13 @@ var KeepGoingReader = class {
|
|
|
990
1343
|
};
|
|
991
1344
|
|
|
992
1345
|
// src/tools/getMomentum.ts
|
|
993
|
-
function registerGetMomentum(
|
|
994
|
-
|
|
1346
|
+
function registerGetMomentum(server, reader, workspacePath) {
|
|
1347
|
+
server.tool(
|
|
995
1348
|
"get_momentum",
|
|
996
1349
|
"Get current developer momentum: last checkpoint, next step, blockers, and branch context. Use this to understand where the developer left off.",
|
|
997
1350
|
{},
|
|
998
1351
|
async () => {
|
|
999
|
-
if (!
|
|
1352
|
+
if (!reader.exists()) {
|
|
1000
1353
|
return {
|
|
1001
1354
|
content: [
|
|
1002
1355
|
{
|
|
@@ -1006,7 +1359,8 @@ function registerGetMomentum(server2, reader2, workspacePath2) {
|
|
|
1006
1359
|
]
|
|
1007
1360
|
};
|
|
1008
1361
|
}
|
|
1009
|
-
const lastSession =
|
|
1362
|
+
const { session: lastSession, isFallback } = reader.getScopedLastSession();
|
|
1363
|
+
const currentBranch = reader.getCurrentBranch();
|
|
1010
1364
|
if (!lastSession) {
|
|
1011
1365
|
return {
|
|
1012
1366
|
content: [
|
|
@@ -1017,16 +1371,24 @@ function registerGetMomentum(server2, reader2, workspacePath2) {
|
|
|
1017
1371
|
]
|
|
1018
1372
|
};
|
|
1019
1373
|
}
|
|
1020
|
-
const state =
|
|
1021
|
-
const currentBranch = getCurrentBranch(workspacePath2);
|
|
1374
|
+
const state = reader.getState();
|
|
1022
1375
|
const branchChanged = lastSession.gitBranch && currentBranch && lastSession.gitBranch !== currentBranch;
|
|
1023
1376
|
const lines = [
|
|
1024
1377
|
`## Developer Momentum`,
|
|
1025
|
-
""
|
|
1378
|
+
""
|
|
1379
|
+
];
|
|
1380
|
+
if (reader.isWorktree && currentBranch) {
|
|
1381
|
+
lines.push(`**Worktree context:** Scoped to branch \`${currentBranch}\``);
|
|
1382
|
+
if (isFallback) {
|
|
1383
|
+
lines.push(`**Note:** No checkpoints found for branch \`${currentBranch}\`. Showing last global checkpoint.`);
|
|
1384
|
+
}
|
|
1385
|
+
lines.push("");
|
|
1386
|
+
}
|
|
1387
|
+
lines.push(
|
|
1026
1388
|
`**Last checkpoint:** ${formatRelativeTime(lastSession.timestamp)}`,
|
|
1027
1389
|
`**Summary:** ${lastSession.summary || "No summary"}`,
|
|
1028
1390
|
`**Next step:** ${lastSession.nextStep || "Not specified"}`
|
|
1029
|
-
|
|
1391
|
+
);
|
|
1030
1392
|
if (lastSession.blocker) {
|
|
1031
1393
|
lines.push(`**Blocker:** ${lastSession.blocker}`);
|
|
1032
1394
|
}
|
|
@@ -1037,7 +1399,7 @@ function registerGetMomentum(server2, reader2, workspacePath2) {
|
|
|
1037
1399
|
if (currentBranch) {
|
|
1038
1400
|
lines.push(`**Current branch:** ${currentBranch}`);
|
|
1039
1401
|
}
|
|
1040
|
-
if (branchChanged) {
|
|
1402
|
+
if (branchChanged && !reader.isWorktree) {
|
|
1041
1403
|
lines.push(
|
|
1042
1404
|
`**Note:** Branch changed since last checkpoint (was \`${lastSession.gitBranch}\`, now \`${currentBranch}\`)`
|
|
1043
1405
|
);
|
|
@@ -1066,13 +1428,16 @@ function registerGetMomentum(server2, reader2, workspacePath2) {
|
|
|
1066
1428
|
|
|
1067
1429
|
// src/tools/getSessionHistory.ts
|
|
1068
1430
|
import { z } from "zod";
|
|
1069
|
-
function registerGetSessionHistory(
|
|
1070
|
-
|
|
1431
|
+
function registerGetSessionHistory(server, reader) {
|
|
1432
|
+
server.tool(
|
|
1071
1433
|
"get_session_history",
|
|
1072
1434
|
"Get recent session checkpoints. Returns a chronological list of what the developer worked on.",
|
|
1073
|
-
{
|
|
1074
|
-
|
|
1075
|
-
|
|
1435
|
+
{
|
|
1436
|
+
limit: z.number().min(1).max(50).default(5).describe("Number of recent sessions to return (1-50, default 5)"),
|
|
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.')
|
|
1438
|
+
},
|
|
1439
|
+
async ({ limit, branch }) => {
|
|
1440
|
+
if (!reader.exists()) {
|
|
1076
1441
|
return {
|
|
1077
1442
|
content: [
|
|
1078
1443
|
{
|
|
@@ -1082,19 +1447,20 @@ function registerGetSessionHistory(server2, reader2) {
|
|
|
1082
1447
|
]
|
|
1083
1448
|
};
|
|
1084
1449
|
}
|
|
1085
|
-
const
|
|
1450
|
+
const { effectiveBranch, scopeLabel } = reader.resolveBranchScope(branch);
|
|
1451
|
+
const sessions = effectiveBranch ? reader.getRecentSessionsForBranch(effectiveBranch, limit) : reader.getRecentSessions(limit);
|
|
1086
1452
|
if (sessions.length === 0) {
|
|
1087
1453
|
return {
|
|
1088
1454
|
content: [
|
|
1089
1455
|
{
|
|
1090
1456
|
type: "text",
|
|
1091
|
-
text: "No session checkpoints found."
|
|
1457
|
+
text: effectiveBranch ? `No session checkpoints found for branch \`${effectiveBranch}\`. Use branch: "all" to see all branches.` : "No session checkpoints found."
|
|
1092
1458
|
}
|
|
1093
1459
|
]
|
|
1094
1460
|
};
|
|
1095
1461
|
}
|
|
1096
1462
|
const lines = [
|
|
1097
|
-
`## Session History (last ${sessions.length})`,
|
|
1463
|
+
`## Session History (last ${sessions.length}, ${scopeLabel})`,
|
|
1098
1464
|
""
|
|
1099
1465
|
];
|
|
1100
1466
|
for (const session of sessions) {
|
|
@@ -1122,13 +1488,13 @@ function registerGetSessionHistory(server2, reader2) {
|
|
|
1122
1488
|
}
|
|
1123
1489
|
|
|
1124
1490
|
// src/tools/getReentryBriefing.ts
|
|
1125
|
-
function registerGetReentryBriefing(
|
|
1126
|
-
|
|
1491
|
+
function registerGetReentryBriefing(server, reader, workspacePath) {
|
|
1492
|
+
server.tool(
|
|
1127
1493
|
"get_reentry_briefing",
|
|
1128
1494
|
"Get a synthesized re-entry briefing that helps a developer understand where they left off. Includes focus, recent activity, and suggested next steps.",
|
|
1129
1495
|
{},
|
|
1130
1496
|
async () => {
|
|
1131
|
-
if (!
|
|
1497
|
+
if (!reader.exists()) {
|
|
1132
1498
|
return {
|
|
1133
1499
|
content: [
|
|
1134
1500
|
{
|
|
@@ -1138,12 +1504,12 @@ function registerGetReentryBriefing(server2, reader2, workspacePath2) {
|
|
|
1138
1504
|
]
|
|
1139
1505
|
};
|
|
1140
1506
|
}
|
|
1141
|
-
const
|
|
1142
|
-
const
|
|
1143
|
-
const
|
|
1144
|
-
const
|
|
1507
|
+
const gitBranch = reader.getCurrentBranch();
|
|
1508
|
+
const { session: lastSession } = reader.getScopedLastSession();
|
|
1509
|
+
const recentSessions = reader.getScopedRecentSessions(5);
|
|
1510
|
+
const state = reader.getState() ?? {};
|
|
1145
1511
|
const sinceTimestamp = lastSession?.timestamp;
|
|
1146
|
-
const recentCommits = sinceTimestamp ? getCommitMessagesSince(
|
|
1512
|
+
const recentCommits = sinceTimestamp ? getCommitMessagesSince(workspacePath, sinceTimestamp) : [];
|
|
1147
1513
|
const briefing = generateBriefing(
|
|
1148
1514
|
lastSession,
|
|
1149
1515
|
recentSessions,
|
|
@@ -1163,14 +1529,20 @@ function registerGetReentryBriefing(server2, reader2, workspacePath2) {
|
|
|
1163
1529
|
}
|
|
1164
1530
|
const lines = [
|
|
1165
1531
|
`## Re-entry Briefing`,
|
|
1166
|
-
""
|
|
1532
|
+
""
|
|
1533
|
+
];
|
|
1534
|
+
if (reader.isWorktree && gitBranch) {
|
|
1535
|
+
lines.push(`**Worktree context:** Scoped to branch \`${gitBranch}\``);
|
|
1536
|
+
lines.push("");
|
|
1537
|
+
}
|
|
1538
|
+
lines.push(
|
|
1167
1539
|
`**Last worked:** ${briefing.lastWorked}`,
|
|
1168
1540
|
`**Current focus:** ${briefing.currentFocus}`,
|
|
1169
1541
|
`**Recent activity:** ${briefing.recentActivity}`,
|
|
1170
1542
|
`**Suggested next:** ${briefing.suggestedNext}`,
|
|
1171
1543
|
`**Quick start:** ${briefing.smallNextStep}`
|
|
1172
|
-
|
|
1173
|
-
const recentDecisions =
|
|
1544
|
+
);
|
|
1545
|
+
const recentDecisions = reader.getScopedRecentDecisions(3);
|
|
1174
1546
|
if (recentDecisions.length > 0) {
|
|
1175
1547
|
lines.push("");
|
|
1176
1548
|
lines.push("### Recent decisions");
|
|
@@ -1189,8 +1561,8 @@ function registerGetReentryBriefing(server2, reader2, workspacePath2) {
|
|
|
1189
1561
|
// src/tools/saveCheckpoint.ts
|
|
1190
1562
|
import path6 from "path";
|
|
1191
1563
|
import { z as z2 } from "zod";
|
|
1192
|
-
function registerSaveCheckpoint(
|
|
1193
|
-
|
|
1564
|
+
function registerSaveCheckpoint(server, reader, workspacePath) {
|
|
1565
|
+
server.tool(
|
|
1194
1566
|
"save_checkpoint",
|
|
1195
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.",
|
|
1196
1568
|
{
|
|
@@ -1199,11 +1571,12 @@ function registerSaveCheckpoint(server2, reader2, workspacePath2) {
|
|
|
1199
1571
|
blocker: z2.string().optional().describe("Any blocker preventing progress")
|
|
1200
1572
|
},
|
|
1201
1573
|
async ({ summary, nextStep, blocker }) => {
|
|
1202
|
-
const lastSession =
|
|
1203
|
-
const gitBranch = getCurrentBranch(
|
|
1204
|
-
const touchedFiles = getTouchedFiles(
|
|
1205
|
-
const commitHashes = getCommitsSince(
|
|
1206
|
-
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 });
|
|
1207
1580
|
const checkpoint = createCheckpoint({
|
|
1208
1581
|
summary,
|
|
1209
1582
|
nextStep: nextStep || "",
|
|
@@ -1211,10 +1584,11 @@ function registerSaveCheckpoint(server2, reader2, workspacePath2) {
|
|
|
1211
1584
|
gitBranch,
|
|
1212
1585
|
touchedFiles,
|
|
1213
1586
|
commitHashes,
|
|
1214
|
-
workspaceRoot:
|
|
1215
|
-
source: "manual"
|
|
1587
|
+
workspaceRoot: workspacePath,
|
|
1588
|
+
source: "manual",
|
|
1589
|
+
sessionId
|
|
1216
1590
|
});
|
|
1217
|
-
const writer = new KeepGoingWriter(
|
|
1591
|
+
const writer = new KeepGoingWriter(workspacePath);
|
|
1218
1592
|
writer.saveCheckpoint(checkpoint, projectName);
|
|
1219
1593
|
const lines = [
|
|
1220
1594
|
`Checkpoint saved.`,
|
|
@@ -1224,11 +1598,11 @@ function registerSaveCheckpoint(server2, reader2, workspacePath2) {
|
|
|
1224
1598
|
`- **Commits captured:** ${commitHashes.length}`
|
|
1225
1599
|
];
|
|
1226
1600
|
if (commitHashes.length > 0) {
|
|
1227
|
-
const commitMessages = getCommitMessagesSince(
|
|
1228
|
-
const headHash = getHeadCommitHash(
|
|
1601
|
+
const commitMessages = getCommitMessagesSince(workspacePath, lastSession?.timestamp);
|
|
1602
|
+
const headHash = getHeadCommitHash(workspacePath);
|
|
1229
1603
|
if (commitMessages.length > 0 && headHash) {
|
|
1230
1604
|
const detected = tryDetectDecision({
|
|
1231
|
-
workspacePath
|
|
1605
|
+
workspacePath,
|
|
1232
1606
|
checkpointId: checkpoint.id,
|
|
1233
1607
|
gitBranch,
|
|
1234
1608
|
commitHash: headHash,
|
|
@@ -1249,13 +1623,16 @@ function registerSaveCheckpoint(server2, reader2, workspacePath2) {
|
|
|
1249
1623
|
|
|
1250
1624
|
// src/tools/getDecisions.ts
|
|
1251
1625
|
import { z as z3 } from "zod";
|
|
1252
|
-
function registerGetDecisions(
|
|
1253
|
-
|
|
1626
|
+
function registerGetDecisions(server, reader) {
|
|
1627
|
+
server.tool(
|
|
1254
1628
|
"get_decisions",
|
|
1255
1629
|
"Get recent decision records. Returns detected high-signal commits with their category, confidence, and rationale.",
|
|
1256
|
-
{
|
|
1257
|
-
|
|
1258
|
-
|
|
1630
|
+
{
|
|
1631
|
+
limit: z3.number().min(1).max(50).default(10).describe("Number of recent decisions to return (1-50, default 10)"),
|
|
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.')
|
|
1633
|
+
},
|
|
1634
|
+
async ({ limit, branch }) => {
|
|
1635
|
+
if (!reader.exists()) {
|
|
1259
1636
|
return {
|
|
1260
1637
|
content: [
|
|
1261
1638
|
{
|
|
@@ -1265,8 +1642,7 @@ function registerGetDecisions(server2, reader2) {
|
|
|
1265
1642
|
]
|
|
1266
1643
|
};
|
|
1267
1644
|
}
|
|
1268
|
-
|
|
1269
|
-
if (!isCachedLicenseValid(licenseCache)) {
|
|
1645
|
+
if (process.env.KEEPGOING_PRO_BYPASS !== "1" && !getLicenseForFeature("decisions")) {
|
|
1270
1646
|
return {
|
|
1271
1647
|
content: [
|
|
1272
1648
|
{
|
|
@@ -1276,19 +1652,20 @@ function registerGetDecisions(server2, reader2) {
|
|
|
1276
1652
|
]
|
|
1277
1653
|
};
|
|
1278
1654
|
}
|
|
1279
|
-
const
|
|
1655
|
+
const { effectiveBranch, scopeLabel } = reader.resolveBranchScope(branch);
|
|
1656
|
+
const decisions = effectiveBranch ? reader.getRecentDecisionsForBranch(effectiveBranch, limit) : reader.getRecentDecisions(limit);
|
|
1280
1657
|
if (decisions.length === 0) {
|
|
1281
1658
|
return {
|
|
1282
1659
|
content: [
|
|
1283
1660
|
{
|
|
1284
1661
|
type: "text",
|
|
1285
|
-
text: "No decision records found."
|
|
1662
|
+
text: effectiveBranch ? `No decision records found for branch \`${effectiveBranch}\`. Use branch: "all" to see all branches.` : "No decision records found."
|
|
1286
1663
|
}
|
|
1287
1664
|
]
|
|
1288
1665
|
};
|
|
1289
1666
|
}
|
|
1290
1667
|
const lines = [
|
|
1291
|
-
`## Decisions (last ${decisions.length})`,
|
|
1668
|
+
`## Decisions (last ${decisions.length}, ${scopeLabel})`,
|
|
1292
1669
|
""
|
|
1293
1670
|
];
|
|
1294
1671
|
for (const decision of decisions) {
|
|
@@ -1314,8 +1691,104 @@ function registerGetDecisions(server2, reader2) {
|
|
|
1314
1691
|
);
|
|
1315
1692
|
}
|
|
1316
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
|
+
|
|
1317
1789
|
// src/tools/setupProject.ts
|
|
1318
1790
|
import fs5 from "fs";
|
|
1791
|
+
import os2 from "os";
|
|
1319
1792
|
import path7 from "path";
|
|
1320
1793
|
import { z as z4 } from "zod";
|
|
1321
1794
|
var KEEPGOING_MARKER = "@keepgoingdev/mcp-server";
|
|
@@ -1337,6 +1810,15 @@ var STOP_HOOK = {
|
|
|
1337
1810
|
}
|
|
1338
1811
|
]
|
|
1339
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
|
+
};
|
|
1340
1822
|
var CLAUDE_MD_SECTION = `
|
|
1341
1823
|
## KeepGoing
|
|
1342
1824
|
|
|
@@ -1350,8 +1832,8 @@ function hasKeepGoingHook(hookEntries) {
|
|
|
1350
1832
|
(entry) => entry?.hooks?.some((h) => typeof h?.command === "string" && h.command.includes(KEEPGOING_MARKER))
|
|
1351
1833
|
);
|
|
1352
1834
|
}
|
|
1353
|
-
function registerSetupProject(
|
|
1354
|
-
|
|
1835
|
+
function registerSetupProject(server, workspacePath) {
|
|
1836
|
+
server.tool(
|
|
1355
1837
|
"setup_project",
|
|
1356
1838
|
"Set up KeepGoing in the current project. Adds session hooks to .claude/settings.json and CLAUDE.md instructions so checkpoints are saved automatically.",
|
|
1357
1839
|
{
|
|
@@ -1360,44 +1842,80 @@ function registerSetupProject(server2, workspacePath2) {
|
|
|
1360
1842
|
},
|
|
1361
1843
|
async ({ sessionHooks, claudeMd }) => {
|
|
1362
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;
|
|
1363
1852
|
if (sessionHooks) {
|
|
1364
|
-
const claudeDir = path7.join(workspacePath2, ".claude");
|
|
1365
|
-
const settingsPath = path7.join(claudeDir, "settings.json");
|
|
1366
|
-
let settings = {};
|
|
1367
|
-
if (fs5.existsSync(settingsPath)) {
|
|
1368
|
-
settings = JSON.parse(fs5.readFileSync(settingsPath, "utf-8"));
|
|
1369
|
-
}
|
|
1370
1853
|
if (!settings.hooks) {
|
|
1371
1854
|
settings.hooks = {};
|
|
1372
1855
|
}
|
|
1373
|
-
let hooksChanged = false;
|
|
1374
1856
|
if (!Array.isArray(settings.hooks.SessionStart)) {
|
|
1375
1857
|
settings.hooks.SessionStart = [];
|
|
1376
1858
|
}
|
|
1377
1859
|
if (!hasKeepGoingHook(settings.hooks.SessionStart)) {
|
|
1378
1860
|
settings.hooks.SessionStart.push(SESSION_START_HOOK);
|
|
1379
|
-
|
|
1861
|
+
settingsChanged = true;
|
|
1380
1862
|
}
|
|
1381
1863
|
if (!Array.isArray(settings.hooks.Stop)) {
|
|
1382
1864
|
settings.hooks.Stop = [];
|
|
1383
1865
|
}
|
|
1384
1866
|
if (!hasKeepGoingHook(settings.hooks.Stop)) {
|
|
1385
1867
|
settings.hooks.Stop.push(STOP_HOOK);
|
|
1386
|
-
|
|
1868
|
+
settingsChanged = true;
|
|
1387
1869
|
}
|
|
1388
|
-
if (
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
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) {
|
|
1393
1878
|
results.push("**Session hooks:** Added to `.claude/settings.json`");
|
|
1394
1879
|
} else {
|
|
1395
1880
|
results.push("**Session hooks:** Already present, skipped");
|
|
1396
1881
|
}
|
|
1397
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
|
+
}
|
|
1398
1916
|
if (claudeMd) {
|
|
1399
|
-
const dotClaudeMdPath = path7.join(
|
|
1400
|
-
const rootClaudeMdPath = path7.join(
|
|
1917
|
+
const dotClaudeMdPath = path7.join(workspacePath, ".claude", "CLAUDE.md");
|
|
1918
|
+
const rootClaudeMdPath = path7.join(workspacePath, "CLAUDE.md");
|
|
1401
1919
|
const claudeMdPath = fs5.existsSync(dotClaudeMdPath) ? dotClaudeMdPath : rootClaudeMdPath;
|
|
1402
1920
|
let existing = "";
|
|
1403
1921
|
if (fs5.existsSync(claudeMdPath)) {
|
|
@@ -1420,20 +1938,24 @@ function registerSetupProject(server2, workspacePath2) {
|
|
|
1420
1938
|
|
|
1421
1939
|
// src/tools/activateLicense.ts
|
|
1422
1940
|
import { z as z5 } from "zod";
|
|
1423
|
-
function registerActivateLicense(
|
|
1424
|
-
|
|
1941
|
+
function registerActivateLicense(server) {
|
|
1942
|
+
server.tool(
|
|
1425
1943
|
"activate_license",
|
|
1426
|
-
"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.",
|
|
1427
1945
|
{ license_key: z5.string().describe("Your KeepGoing Pro license key") },
|
|
1428
1946
|
async ({ license_key }) => {
|
|
1429
|
-
const
|
|
1430
|
-
|
|
1431
|
-
|
|
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})` : "";
|
|
1432
1954
|
return {
|
|
1433
1955
|
content: [
|
|
1434
1956
|
{
|
|
1435
1957
|
type: "text",
|
|
1436
|
-
text:
|
|
1958
|
+
text: `${label2} is already active${who2}. No action needed.`
|
|
1437
1959
|
}
|
|
1438
1960
|
]
|
|
1439
1961
|
};
|
|
@@ -1449,22 +1971,43 @@ function registerActivateLicense(server2) {
|
|
|
1449
1971
|
]
|
|
1450
1972
|
};
|
|
1451
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
|
+
}
|
|
1452
1990
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1453
|
-
|
|
1991
|
+
addLicenseEntry({
|
|
1454
1992
|
licenseKey: result.licenseKey || license_key,
|
|
1455
1993
|
instanceId: result.instanceId || getDeviceId(),
|
|
1456
1994
|
status: "active",
|
|
1457
1995
|
lastValidatedAt: now,
|
|
1458
1996
|
activatedAt: now,
|
|
1997
|
+
variantId,
|
|
1459
1998
|
customerName: result.customerName,
|
|
1460
|
-
productName: result.productName
|
|
1999
|
+
productName: result.productName,
|
|
2000
|
+
variantName: result.variantName
|
|
1461
2001
|
});
|
|
2002
|
+
const label = getVariantLabel(variantId);
|
|
2003
|
+
const features = VARIANT_FEATURE_MAP[variantId];
|
|
2004
|
+
const featureList = features ? features.join(", ") : "Pro features";
|
|
1462
2005
|
const who = result.customerName ? ` Welcome, ${result.customerName}!` : "";
|
|
1463
2006
|
return {
|
|
1464
2007
|
content: [
|
|
1465
2008
|
{
|
|
1466
2009
|
type: "text",
|
|
1467
|
-
text:
|
|
2010
|
+
text: `${label} activated successfully.${who} Enabled: ${featureList}.`
|
|
1468
2011
|
}
|
|
1469
2012
|
]
|
|
1470
2013
|
};
|
|
@@ -1473,14 +2016,18 @@ function registerActivateLicense(server2) {
|
|
|
1473
2016
|
}
|
|
1474
2017
|
|
|
1475
2018
|
// src/tools/deactivateLicense.ts
|
|
1476
|
-
|
|
1477
|
-
|
|
2019
|
+
import { z as z6 } from "zod";
|
|
2020
|
+
function registerDeactivateLicense(server) {
|
|
2021
|
+
server.tool(
|
|
1478
2022
|
"deactivate_license",
|
|
1479
2023
|
"Deactivate the KeepGoing Pro license on this device.",
|
|
1480
|
-
{
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
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) {
|
|
1484
2031
|
return {
|
|
1485
2032
|
content: [
|
|
1486
2033
|
{
|
|
@@ -1490,14 +2037,41 @@ function registerDeactivateLicense(server2) {
|
|
|
1490
2037
|
]
|
|
1491
2038
|
};
|
|
1492
2039
|
}
|
|
1493
|
-
|
|
1494
|
-
|
|
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);
|
|
1495
2069
|
if (!result.deactivated) {
|
|
1496
2070
|
return {
|
|
1497
2071
|
content: [
|
|
1498
2072
|
{
|
|
1499
2073
|
type: "text",
|
|
1500
|
-
text:
|
|
2074
|
+
text: `${label} license cleared locally, but remote deactivation failed: ${result.error ?? "unknown error"}`
|
|
1501
2075
|
}
|
|
1502
2076
|
]
|
|
1503
2077
|
};
|
|
@@ -1506,7 +2080,7 @@ function registerDeactivateLicense(server2) {
|
|
|
1506
2080
|
content: [
|
|
1507
2081
|
{
|
|
1508
2082
|
type: "text",
|
|
1509
|
-
text:
|
|
2083
|
+
text: `${label} license deactivated successfully. The activation slot has been freed.`
|
|
1510
2084
|
}
|
|
1511
2085
|
]
|
|
1512
2086
|
};
|
|
@@ -1515,8 +2089,8 @@ function registerDeactivateLicense(server2) {
|
|
|
1515
2089
|
}
|
|
1516
2090
|
|
|
1517
2091
|
// src/prompts/resume.ts
|
|
1518
|
-
function registerResumePrompt(
|
|
1519
|
-
|
|
2092
|
+
function registerResumePrompt(server) {
|
|
2093
|
+
server.prompt(
|
|
1520
2094
|
"resume",
|
|
1521
2095
|
"Check developer momentum and suggest what to work on next",
|
|
1522
2096
|
async () => ({
|
|
@@ -1542,14 +2116,76 @@ function registerResumePrompt(server2) {
|
|
|
1542
2116
|
);
|
|
1543
2117
|
}
|
|
1544
2118
|
|
|
1545
|
-
// src/
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
2119
|
+
// src/prompts/decisions.ts
|
|
2120
|
+
function registerDecisionsPrompt(server) {
|
|
2121
|
+
server.prompt(
|
|
2122
|
+
"decisions",
|
|
2123
|
+
"Review recent architectural decisions and their rationale",
|
|
2124
|
+
async () => ({
|
|
2125
|
+
messages: [
|
|
2126
|
+
{
|
|
2127
|
+
role: "user",
|
|
2128
|
+
content: {
|
|
2129
|
+
type: "text",
|
|
2130
|
+
text: [
|
|
2131
|
+
"I want to review recent architectural decisions in this project.",
|
|
2132
|
+
"",
|
|
2133
|
+
"Please use the KeepGoing tools to:",
|
|
2134
|
+
"1. Fetch recent decision records (get_decisions)",
|
|
2135
|
+
"2. Get my current branch context (get_momentum)",
|
|
2136
|
+
"3. Summarize the decisions, highlighting any that were made on the current branch",
|
|
2137
|
+
"",
|
|
2138
|
+
"Keep your response brief and organized."
|
|
2139
|
+
].join("\n")
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
]
|
|
2143
|
+
})
|
|
2144
|
+
);
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
// src/prompts/progress.ts
|
|
2148
|
+
function registerProgressPrompt(server) {
|
|
2149
|
+
server.prompt(
|
|
2150
|
+
"progress",
|
|
2151
|
+
"Summarize recent development progress across sessions",
|
|
2152
|
+
async () => ({
|
|
2153
|
+
messages: [
|
|
2154
|
+
{
|
|
2155
|
+
role: "user",
|
|
2156
|
+
content: {
|
|
2157
|
+
type: "text",
|
|
2158
|
+
text: [
|
|
2159
|
+
"I need a summary of recent development progress for this project.",
|
|
2160
|
+
"",
|
|
2161
|
+
"Please use the KeepGoing tools to:",
|
|
2162
|
+
"1. Fetch session history with a higher limit for broader coverage (get_session_history, limit: 20)",
|
|
2163
|
+
"2. Get my current branch context (get_momentum)",
|
|
2164
|
+
"3. Synthesize a progress summary grouped by branch or feature, highlighting the current branch",
|
|
2165
|
+
"",
|
|
2166
|
+
"Format the summary so it can be used in a standup or sprint review."
|
|
2167
|
+
].join("\n")
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
]
|
|
2171
|
+
})
|
|
2172
|
+
);
|
|
2173
|
+
}
|
|
2174
|
+
|
|
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()) {
|
|
1550
2186
|
process.exit(0);
|
|
1551
2187
|
}
|
|
1552
|
-
const lastSession =
|
|
2188
|
+
const { session: lastSession } = reader.getScopedLastSession();
|
|
1553
2189
|
if (!lastSession) {
|
|
1554
2190
|
process.exit(0);
|
|
1555
2191
|
}
|
|
@@ -1575,10 +2211,49 @@ if (process.argv.includes("--print-momentum")) {
|
|
|
1575
2211
|
console.log(lines.join("\n"));
|
|
1576
2212
|
process.exit(0);
|
|
1577
2213
|
}
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
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();
|
|
1582
2257
|
if (lastSession?.timestamp) {
|
|
1583
2258
|
const ageMs = Date.now() - new Date(lastSession.timestamp).getTime();
|
|
1584
2259
|
if (ageMs < 2 * 60 * 1e3) {
|
|
@@ -1603,6 +2278,7 @@ if (process.argv.includes("--save-checkpoint")) {
|
|
|
1603
2278
|
}
|
|
1604
2279
|
}
|
|
1605
2280
|
const projectName = path8.basename(resolveStorageRoot(wsPath));
|
|
2281
|
+
const sessionId = generateSessionId({ workspaceRoot: wsPath, branch: gitBranch ?? void 0, worktreePath: wsPath });
|
|
1606
2282
|
const checkpoint = createCheckpoint({
|
|
1607
2283
|
summary,
|
|
1608
2284
|
nextStep: "",
|
|
@@ -1610,12 +2286,19 @@ if (process.argv.includes("--save-checkpoint")) {
|
|
|
1610
2286
|
touchedFiles,
|
|
1611
2287
|
commitHashes,
|
|
1612
2288
|
workspaceRoot: wsPath,
|
|
1613
|
-
source: "auto"
|
|
2289
|
+
source: "auto",
|
|
2290
|
+
sessionId
|
|
1614
2291
|
});
|
|
1615
2292
|
const writer = new KeepGoingWriter(wsPath);
|
|
1616
2293
|
writer.saveCheckpoint(checkpoint, projectName);
|
|
1617
|
-
|
|
1618
|
-
|
|
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) {
|
|
1619
2302
|
const headHash = getHeadCommitHash(wsPath) || commitHashes[0];
|
|
1620
2303
|
if (headHash) {
|
|
1621
2304
|
const detected = tryDetectDecision({
|
|
@@ -1634,22 +2317,109 @@ if (process.argv.includes("--save-checkpoint")) {
|
|
|
1634
2317
|
console.log(`[KeepGoing] Auto-checkpoint saved: ${summary}`);
|
|
1635
2318
|
process.exit(0);
|
|
1636
2319
|
}
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
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
|
+
}
|
|
1655
2425
|
//# sourceMappingURL=index.js.map
|