@keepgoingdev/mcp-server 0.6.1 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +376 -138
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -149,6 +149,18 @@ function getCommitsSince(workspacePath, sinceTimestamp) {
|
|
|
149
149
|
function getCommitMessagesSince(workspacePath, sinceTimestamp) {
|
|
150
150
|
return getGitLogSince(workspacePath, "%s", sinceTimestamp);
|
|
151
151
|
}
|
|
152
|
+
function getCommitMessageByHash(workspacePath, commitHash) {
|
|
153
|
+
try {
|
|
154
|
+
const result = execFileSync("git", ["log", "-1", "--format=%s", commitHash], {
|
|
155
|
+
cwd: workspacePath,
|
|
156
|
+
encoding: "utf-8",
|
|
157
|
+
timeout: 5e3
|
|
158
|
+
});
|
|
159
|
+
return result.trim() || void 0;
|
|
160
|
+
} catch {
|
|
161
|
+
return void 0;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
152
164
|
function getFilesChangedInCommit(workspacePath, commitHash) {
|
|
153
165
|
try {
|
|
154
166
|
const result = execFileSync("git", ["diff-tree", "--no-commit-id", "--name-only", "-r", commitHash], {
|
|
@@ -350,7 +362,8 @@ function buildRecentActivity(lastSession, recentSessions, recentCommitMessages)
|
|
|
350
362
|
parts.push("1 recent session");
|
|
351
363
|
}
|
|
352
364
|
if (lastSession.summary) {
|
|
353
|
-
|
|
365
|
+
const brief = lastSession.summary.length > 120 ? lastSession.summary.slice(0, 117) + "..." : lastSession.summary;
|
|
366
|
+
parts.push(`Last: ${brief}`);
|
|
354
367
|
}
|
|
355
368
|
if (lastSession.touchedFiles.length > 0) {
|
|
356
369
|
parts.push(`${lastSession.touchedFiles.length} files touched`);
|
|
@@ -691,9 +704,59 @@ function formatContinueOnPrompt(context, options) {
|
|
|
691
704
|
}
|
|
692
705
|
|
|
693
706
|
// ../../packages/shared/src/storage.ts
|
|
707
|
+
import fs2 from "fs";
|
|
708
|
+
import path4 from "path";
|
|
709
|
+
import { randomUUID as randomUUID2, createHash } from "crypto";
|
|
710
|
+
|
|
711
|
+
// ../../packages/shared/src/registry.ts
|
|
694
712
|
import fs from "fs";
|
|
713
|
+
import os from "os";
|
|
695
714
|
import path3 from "path";
|
|
696
|
-
|
|
715
|
+
var KEEPGOING_DIR = path3.join(os.homedir(), ".keepgoing");
|
|
716
|
+
var KNOWN_PROJECTS_FILE = path3.join(KEEPGOING_DIR, "known-projects.json");
|
|
717
|
+
var STALE_PROJECT_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
718
|
+
function readKnownProjects() {
|
|
719
|
+
try {
|
|
720
|
+
if (fs.existsSync(KNOWN_PROJECTS_FILE)) {
|
|
721
|
+
const raw = JSON.parse(fs.readFileSync(KNOWN_PROJECTS_FILE, "utf-8"));
|
|
722
|
+
if (raw && Array.isArray(raw.projects)) {
|
|
723
|
+
return raw;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
} catch {
|
|
727
|
+
}
|
|
728
|
+
return { version: 1, projects: [] };
|
|
729
|
+
}
|
|
730
|
+
function writeKnownProjects(data) {
|
|
731
|
+
if (!fs.existsSync(KEEPGOING_DIR)) {
|
|
732
|
+
fs.mkdirSync(KEEPGOING_DIR, { recursive: true });
|
|
733
|
+
}
|
|
734
|
+
const tmpFile = KNOWN_PROJECTS_FILE + ".tmp";
|
|
735
|
+
fs.writeFileSync(tmpFile, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
736
|
+
fs.renameSync(tmpFile, KNOWN_PROJECTS_FILE);
|
|
737
|
+
}
|
|
738
|
+
function registerProject(projectPath, projectName) {
|
|
739
|
+
try {
|
|
740
|
+
const data = readKnownProjects();
|
|
741
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
742
|
+
const name = projectName || path3.basename(projectPath);
|
|
743
|
+
const existingIdx = data.projects.findIndex((p) => p.path === projectPath);
|
|
744
|
+
if (existingIdx >= 0) {
|
|
745
|
+
data.projects[existingIdx].lastSeen = now;
|
|
746
|
+
if (projectName) {
|
|
747
|
+
data.projects[existingIdx].name = name;
|
|
748
|
+
}
|
|
749
|
+
} else {
|
|
750
|
+
data.projects.push({ path: projectPath, name, lastSeen: now });
|
|
751
|
+
}
|
|
752
|
+
const cutoff = Date.now() - STALE_PROJECT_MS;
|
|
753
|
+
data.projects = data.projects.filter((p) => new Date(p.lastSeen).getTime() > cutoff);
|
|
754
|
+
writeKnownProjects(data);
|
|
755
|
+
} catch {
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// ../../packages/shared/src/storage.ts
|
|
697
760
|
var STORAGE_DIR = ".keepgoing";
|
|
698
761
|
var META_FILE = "meta.json";
|
|
699
762
|
var SESSIONS_FILE = "sessions.json";
|
|
@@ -715,23 +778,23 @@ var KeepGoingWriter = class {
|
|
|
715
778
|
currentTasksFilePath;
|
|
716
779
|
constructor(workspacePath) {
|
|
717
780
|
const mainRoot = resolveStorageRoot(workspacePath);
|
|
718
|
-
this.storagePath =
|
|
719
|
-
this.sessionsFilePath =
|
|
720
|
-
this.stateFilePath =
|
|
721
|
-
this.metaFilePath =
|
|
722
|
-
this.currentTasksFilePath =
|
|
781
|
+
this.storagePath = path4.join(mainRoot, STORAGE_DIR);
|
|
782
|
+
this.sessionsFilePath = path4.join(this.storagePath, SESSIONS_FILE);
|
|
783
|
+
this.stateFilePath = path4.join(this.storagePath, STATE_FILE);
|
|
784
|
+
this.metaFilePath = path4.join(this.storagePath, META_FILE);
|
|
785
|
+
this.currentTasksFilePath = path4.join(this.storagePath, CURRENT_TASKS_FILE);
|
|
723
786
|
}
|
|
724
787
|
ensureDir() {
|
|
725
|
-
if (!
|
|
726
|
-
|
|
788
|
+
if (!fs2.existsSync(this.storagePath)) {
|
|
789
|
+
fs2.mkdirSync(this.storagePath, { recursive: true });
|
|
727
790
|
}
|
|
728
791
|
}
|
|
729
792
|
saveCheckpoint(checkpoint, projectName) {
|
|
730
793
|
this.ensureDir();
|
|
731
794
|
let sessionsData;
|
|
732
795
|
try {
|
|
733
|
-
if (
|
|
734
|
-
const raw = JSON.parse(
|
|
796
|
+
if (fs2.existsSync(this.sessionsFilePath)) {
|
|
797
|
+
const raw = JSON.parse(fs2.readFileSync(this.sessionsFilePath, "utf-8"));
|
|
735
798
|
if (Array.isArray(raw)) {
|
|
736
799
|
sessionsData = { version: 1, project: projectName, sessions: raw };
|
|
737
800
|
} else {
|
|
@@ -749,20 +812,22 @@ var KeepGoingWriter = class {
|
|
|
749
812
|
if (sessionsData.sessions.length > MAX_SESSIONS) {
|
|
750
813
|
sessionsData.sessions = sessionsData.sessions.slice(-MAX_SESSIONS);
|
|
751
814
|
}
|
|
752
|
-
|
|
815
|
+
fs2.writeFileSync(this.sessionsFilePath, JSON.stringify(sessionsData, null, 2), "utf-8");
|
|
753
816
|
const state = {
|
|
754
817
|
lastSessionId: checkpoint.id,
|
|
755
818
|
lastKnownBranch: checkpoint.gitBranch,
|
|
756
819
|
lastActivityAt: checkpoint.timestamp
|
|
757
820
|
};
|
|
758
|
-
|
|
821
|
+
fs2.writeFileSync(this.stateFilePath, JSON.stringify(state, null, 2), "utf-8");
|
|
759
822
|
this.updateMeta(checkpoint.timestamp);
|
|
823
|
+
const mainRoot = resolveStorageRoot(this.storagePath);
|
|
824
|
+
registerProject(mainRoot, projectName);
|
|
760
825
|
}
|
|
761
826
|
updateMeta(timestamp) {
|
|
762
827
|
let meta;
|
|
763
828
|
try {
|
|
764
|
-
if (
|
|
765
|
-
meta = JSON.parse(
|
|
829
|
+
if (fs2.existsSync(this.metaFilePath)) {
|
|
830
|
+
meta = JSON.parse(fs2.readFileSync(this.metaFilePath, "utf-8"));
|
|
766
831
|
meta.lastUpdated = timestamp;
|
|
767
832
|
} else {
|
|
768
833
|
meta = {
|
|
@@ -778,7 +843,7 @@ var KeepGoingWriter = class {
|
|
|
778
843
|
lastUpdated: timestamp
|
|
779
844
|
};
|
|
780
845
|
}
|
|
781
|
-
|
|
846
|
+
fs2.writeFileSync(this.metaFilePath, JSON.stringify(meta, null, 2), "utf-8");
|
|
782
847
|
}
|
|
783
848
|
// ---------------------------------------------------------------------------
|
|
784
849
|
// Multi-session API
|
|
@@ -786,8 +851,8 @@ var KeepGoingWriter = class {
|
|
|
786
851
|
/** Read all current tasks from current-tasks.json. Auto-prunes stale sessions. */
|
|
787
852
|
readCurrentTasks() {
|
|
788
853
|
try {
|
|
789
|
-
if (
|
|
790
|
-
const raw = JSON.parse(
|
|
854
|
+
if (fs2.existsSync(this.currentTasksFilePath)) {
|
|
855
|
+
const raw = JSON.parse(fs2.readFileSync(this.currentTasksFilePath, "utf-8"));
|
|
791
856
|
const tasks = Array.isArray(raw) ? raw : raw.tasks ?? [];
|
|
792
857
|
return this.pruneStale(tasks);
|
|
793
858
|
}
|
|
@@ -802,6 +867,8 @@ var KeepGoingWriter = class {
|
|
|
802
867
|
upsertSession(update) {
|
|
803
868
|
this.ensureDir();
|
|
804
869
|
this.upsertSessionCore(update);
|
|
870
|
+
const mainRoot = resolveStorageRoot(this.storagePath);
|
|
871
|
+
registerProject(mainRoot);
|
|
805
872
|
}
|
|
806
873
|
/** Core upsert logic: merges the update into current-tasks.json and returns the pruned task list. */
|
|
807
874
|
upsertSessionCore(update) {
|
|
@@ -840,8 +907,8 @@ var KeepGoingWriter = class {
|
|
|
840
907
|
// ---------------------------------------------------------------------------
|
|
841
908
|
readAllTasksRaw() {
|
|
842
909
|
try {
|
|
843
|
-
if (
|
|
844
|
-
const raw = JSON.parse(
|
|
910
|
+
if (fs2.existsSync(this.currentTasksFilePath)) {
|
|
911
|
+
const raw = JSON.parse(fs2.readFileSync(this.currentTasksFilePath, "utf-8"));
|
|
845
912
|
return Array.isArray(raw) ? [...raw] : [...raw.tasks ?? []];
|
|
846
913
|
}
|
|
847
914
|
} catch {
|
|
@@ -853,7 +920,7 @@ var KeepGoingWriter = class {
|
|
|
853
920
|
}
|
|
854
921
|
writeTasksFile(tasks) {
|
|
855
922
|
const data = { version: 1, tasks };
|
|
856
|
-
|
|
923
|
+
fs2.writeFileSync(this.currentTasksFilePath, JSON.stringify(data, null, 2), "utf-8");
|
|
857
924
|
}
|
|
858
925
|
};
|
|
859
926
|
function generateSessionId(context) {
|
|
@@ -1032,35 +1099,35 @@ function capitalize(s) {
|
|
|
1032
1099
|
}
|
|
1033
1100
|
|
|
1034
1101
|
// ../../packages/shared/src/decisionStorage.ts
|
|
1035
|
-
import
|
|
1036
|
-
import
|
|
1102
|
+
import fs4 from "fs";
|
|
1103
|
+
import path6 from "path";
|
|
1037
1104
|
|
|
1038
1105
|
// ../../packages/shared/src/license.ts
|
|
1039
1106
|
import crypto from "crypto";
|
|
1040
|
-
import
|
|
1041
|
-
import
|
|
1042
|
-
import
|
|
1107
|
+
import fs3 from "fs";
|
|
1108
|
+
import os2 from "os";
|
|
1109
|
+
import path5 from "path";
|
|
1043
1110
|
var LICENSE_FILE = "license.json";
|
|
1044
1111
|
var DEVICE_ID_FILE = "device-id";
|
|
1045
1112
|
function getGlobalLicenseDir() {
|
|
1046
|
-
return
|
|
1113
|
+
return path5.join(os2.homedir(), ".keepgoing");
|
|
1047
1114
|
}
|
|
1048
1115
|
function getGlobalLicensePath() {
|
|
1049
|
-
return
|
|
1116
|
+
return path5.join(getGlobalLicenseDir(), LICENSE_FILE);
|
|
1050
1117
|
}
|
|
1051
1118
|
function getDeviceId() {
|
|
1052
1119
|
const dir = getGlobalLicenseDir();
|
|
1053
|
-
const filePath =
|
|
1120
|
+
const filePath = path5.join(dir, DEVICE_ID_FILE);
|
|
1054
1121
|
try {
|
|
1055
|
-
const existing =
|
|
1122
|
+
const existing = fs3.readFileSync(filePath, "utf-8").trim();
|
|
1056
1123
|
if (existing) return existing;
|
|
1057
1124
|
} catch {
|
|
1058
1125
|
}
|
|
1059
1126
|
const id = crypto.randomUUID();
|
|
1060
|
-
if (!
|
|
1061
|
-
|
|
1127
|
+
if (!fs3.existsSync(dir)) {
|
|
1128
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
1062
1129
|
}
|
|
1063
|
-
|
|
1130
|
+
fs3.writeFileSync(filePath, id, "utf-8");
|
|
1064
1131
|
return id;
|
|
1065
1132
|
}
|
|
1066
1133
|
var DECISION_DETECTION_VARIANT_ID = 1361527;
|
|
@@ -1094,10 +1161,10 @@ function readLicenseStore() {
|
|
|
1094
1161
|
const licensePath = getGlobalLicensePath();
|
|
1095
1162
|
let store;
|
|
1096
1163
|
try {
|
|
1097
|
-
if (!
|
|
1164
|
+
if (!fs3.existsSync(licensePath)) {
|
|
1098
1165
|
store = { version: 2, licenses: [] };
|
|
1099
1166
|
} else {
|
|
1100
|
-
const raw =
|
|
1167
|
+
const raw = fs3.readFileSync(licensePath, "utf-8");
|
|
1101
1168
|
const data = JSON.parse(raw);
|
|
1102
1169
|
if (data?.version === 2 && Array.isArray(data.licenses)) {
|
|
1103
1170
|
store = data;
|
|
@@ -1114,11 +1181,11 @@ function readLicenseStore() {
|
|
|
1114
1181
|
}
|
|
1115
1182
|
function writeLicenseStore(store) {
|
|
1116
1183
|
const dirPath = getGlobalLicenseDir();
|
|
1117
|
-
if (!
|
|
1118
|
-
|
|
1184
|
+
if (!fs3.existsSync(dirPath)) {
|
|
1185
|
+
fs3.mkdirSync(dirPath, { recursive: true });
|
|
1119
1186
|
}
|
|
1120
|
-
const licensePath =
|
|
1121
|
-
|
|
1187
|
+
const licensePath = path5.join(dirPath, LICENSE_FILE);
|
|
1188
|
+
fs3.writeFileSync(licensePath, JSON.stringify(store, null, 2), "utf-8");
|
|
1122
1189
|
_cachedStore = store;
|
|
1123
1190
|
_cacheTimestamp = Date.now();
|
|
1124
1191
|
}
|
|
@@ -1147,7 +1214,14 @@ function getLicenseForFeature(feature) {
|
|
|
1147
1214
|
return features?.includes(feature);
|
|
1148
1215
|
});
|
|
1149
1216
|
}
|
|
1217
|
+
function getAllLicensesNeedingRevalidation() {
|
|
1218
|
+
return getActiveLicenses().filter((l) => needsRevalidation(l));
|
|
1219
|
+
}
|
|
1150
1220
|
var REVALIDATION_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
|
|
1221
|
+
function needsRevalidation(entry) {
|
|
1222
|
+
const lastValidated = new Date(entry.lastValidatedAt).getTime();
|
|
1223
|
+
return Date.now() - lastValidated > REVALIDATION_THRESHOLD_MS;
|
|
1224
|
+
}
|
|
1151
1225
|
|
|
1152
1226
|
// ../../packages/shared/src/featureGate.ts
|
|
1153
1227
|
var DefaultFeatureGate = class {
|
|
@@ -1169,23 +1243,23 @@ var DecisionStorage = class {
|
|
|
1169
1243
|
decisionsFilePath;
|
|
1170
1244
|
constructor(workspacePath) {
|
|
1171
1245
|
const mainRoot = resolveStorageRoot(workspacePath);
|
|
1172
|
-
this.storagePath =
|
|
1173
|
-
this.decisionsFilePath =
|
|
1246
|
+
this.storagePath = path6.join(mainRoot, STORAGE_DIR2);
|
|
1247
|
+
this.decisionsFilePath = path6.join(this.storagePath, DECISIONS_FILE);
|
|
1174
1248
|
}
|
|
1175
1249
|
ensureStorageDir() {
|
|
1176
|
-
if (!
|
|
1177
|
-
|
|
1250
|
+
if (!fs4.existsSync(this.storagePath)) {
|
|
1251
|
+
fs4.mkdirSync(this.storagePath, { recursive: true });
|
|
1178
1252
|
}
|
|
1179
1253
|
}
|
|
1180
1254
|
getProjectName() {
|
|
1181
|
-
return
|
|
1255
|
+
return path6.basename(path6.dirname(this.storagePath));
|
|
1182
1256
|
}
|
|
1183
1257
|
load() {
|
|
1184
1258
|
try {
|
|
1185
|
-
if (!
|
|
1259
|
+
if (!fs4.existsSync(this.decisionsFilePath)) {
|
|
1186
1260
|
return createEmptyProjectDecisions(this.getProjectName());
|
|
1187
1261
|
}
|
|
1188
|
-
const raw =
|
|
1262
|
+
const raw = fs4.readFileSync(this.decisionsFilePath, "utf-8");
|
|
1189
1263
|
const data = JSON.parse(raw);
|
|
1190
1264
|
return data;
|
|
1191
1265
|
} catch {
|
|
@@ -1195,7 +1269,7 @@ var DecisionStorage = class {
|
|
|
1195
1269
|
save(decisions) {
|
|
1196
1270
|
this.ensureStorageDir();
|
|
1197
1271
|
const content = JSON.stringify(decisions, null, 2);
|
|
1198
|
-
|
|
1272
|
+
fs4.writeFileSync(this.decisionsFilePath, content, "utf-8");
|
|
1199
1273
|
}
|
|
1200
1274
|
/**
|
|
1201
1275
|
* Save a decision record as a draft. Always persists regardless of Pro
|
|
@@ -1446,8 +1520,8 @@ function tryDetectDecision(opts) {
|
|
|
1446
1520
|
}
|
|
1447
1521
|
|
|
1448
1522
|
// ../../packages/shared/src/reader.ts
|
|
1449
|
-
import
|
|
1450
|
-
import
|
|
1523
|
+
import fs5 from "fs";
|
|
1524
|
+
import path7 from "path";
|
|
1451
1525
|
var STORAGE_DIR3 = ".keepgoing";
|
|
1452
1526
|
var META_FILE2 = "meta.json";
|
|
1453
1527
|
var SESSIONS_FILE2 = "sessions.json";
|
|
@@ -1469,16 +1543,16 @@ var KeepGoingReader = class {
|
|
|
1469
1543
|
this.workspacePath = workspacePath;
|
|
1470
1544
|
const mainRoot = resolveStorageRoot(workspacePath);
|
|
1471
1545
|
this._isWorktree = mainRoot !== workspacePath;
|
|
1472
|
-
this.storagePath =
|
|
1473
|
-
this.metaFilePath =
|
|
1474
|
-
this.sessionsFilePath =
|
|
1475
|
-
this.decisionsFilePath =
|
|
1476
|
-
this.stateFilePath =
|
|
1477
|
-
this.currentTasksFilePath =
|
|
1546
|
+
this.storagePath = path7.join(mainRoot, STORAGE_DIR3);
|
|
1547
|
+
this.metaFilePath = path7.join(this.storagePath, META_FILE2);
|
|
1548
|
+
this.sessionsFilePath = path7.join(this.storagePath, SESSIONS_FILE2);
|
|
1549
|
+
this.decisionsFilePath = path7.join(this.storagePath, DECISIONS_FILE2);
|
|
1550
|
+
this.stateFilePath = path7.join(this.storagePath, STATE_FILE2);
|
|
1551
|
+
this.currentTasksFilePath = path7.join(this.storagePath, CURRENT_TASKS_FILE2);
|
|
1478
1552
|
}
|
|
1479
1553
|
/** Check if .keepgoing/ directory exists. */
|
|
1480
1554
|
exists() {
|
|
1481
|
-
return
|
|
1555
|
+
return fs5.existsSync(this.storagePath);
|
|
1482
1556
|
}
|
|
1483
1557
|
/** Read state.json, returns undefined if missing or corrupt. */
|
|
1484
1558
|
getState() {
|
|
@@ -1716,10 +1790,10 @@ var KeepGoingReader = class {
|
|
|
1716
1790
|
}
|
|
1717
1791
|
readJsonFile(filePath) {
|
|
1718
1792
|
try {
|
|
1719
|
-
if (!
|
|
1793
|
+
if (!fs5.existsSync(filePath)) {
|
|
1720
1794
|
return void 0;
|
|
1721
1795
|
}
|
|
1722
|
-
const raw =
|
|
1796
|
+
const raw = fs5.readFileSync(filePath, "utf-8");
|
|
1723
1797
|
return JSON.parse(raw);
|
|
1724
1798
|
} catch {
|
|
1725
1799
|
return void 0;
|
|
@@ -1728,9 +1802,9 @@ var KeepGoingReader = class {
|
|
|
1728
1802
|
};
|
|
1729
1803
|
|
|
1730
1804
|
// ../../packages/shared/src/setup.ts
|
|
1731
|
-
import
|
|
1732
|
-
import
|
|
1733
|
-
import
|
|
1805
|
+
import fs6 from "fs";
|
|
1806
|
+
import os3 from "os";
|
|
1807
|
+
import path8 from "path";
|
|
1734
1808
|
var KEEPGOING_MARKER = "@keepgoingdev/mcp-server";
|
|
1735
1809
|
var SESSION_START_HOOK = {
|
|
1736
1810
|
matcher: "",
|
|
@@ -1759,36 +1833,55 @@ var POST_TOOL_USE_HOOK = {
|
|
|
1759
1833
|
}
|
|
1760
1834
|
]
|
|
1761
1835
|
};
|
|
1762
|
-
var
|
|
1836
|
+
var SESSION_END_HOOK = {
|
|
1837
|
+
matcher: "",
|
|
1838
|
+
hooks: [
|
|
1839
|
+
{
|
|
1840
|
+
type: "command",
|
|
1841
|
+
command: "npx -y @keepgoingdev/mcp-server --save-checkpoint"
|
|
1842
|
+
}
|
|
1843
|
+
]
|
|
1844
|
+
};
|
|
1845
|
+
var KEEPGOING_RULES_VERSION = 1;
|
|
1846
|
+
var KEEPGOING_RULES_CONTENT = `<!-- @keepgoingdev/mcp-server v${KEEPGOING_RULES_VERSION} -->
|
|
1763
1847
|
## KeepGoing
|
|
1764
1848
|
|
|
1765
1849
|
After completing a task or meaningful piece of work, call the \`save_checkpoint\` MCP tool with:
|
|
1766
|
-
- \`summary\`: What
|
|
1767
|
-
- \`nextStep\`: What
|
|
1850
|
+
- \`summary\`: 1-2 sentences. What changed and why \u2014 no file paths, no implementation details (those are captured from git).
|
|
1851
|
+
- \`nextStep\`: What to do next
|
|
1768
1852
|
- \`blocker\`: Any blocker (if applicable)
|
|
1769
1853
|
`;
|
|
1854
|
+
function getRulesFileVersion(content) {
|
|
1855
|
+
const match = content.match(/<!-- @keepgoingdev\/mcp-server v(\d+) -->/);
|
|
1856
|
+
return match ? parseInt(match[1], 10) : null;
|
|
1857
|
+
}
|
|
1770
1858
|
var STATUSLINE_CMD = "npx -y @keepgoingdev/mcp-server --statusline";
|
|
1859
|
+
function detectClaudeDir() {
|
|
1860
|
+
return process.env.CLAUDE_CONFIG_DIR || path8.join(os3.homedir(), ".claude");
|
|
1861
|
+
}
|
|
1771
1862
|
function hasKeepGoingHook(hookEntries) {
|
|
1772
1863
|
return hookEntries.some(
|
|
1773
1864
|
(entry) => entry?.hooks?.some((h) => typeof h?.command === "string" && h.command.includes(KEEPGOING_MARKER))
|
|
1774
1865
|
);
|
|
1775
1866
|
}
|
|
1776
|
-
function resolveScopePaths(scope, workspacePath) {
|
|
1867
|
+
function resolveScopePaths(scope, workspacePath, overrideClaudeDir) {
|
|
1777
1868
|
if (scope === "user") {
|
|
1778
|
-
const claudeDir2 =
|
|
1869
|
+
const claudeDir2 = overrideClaudeDir || detectClaudeDir();
|
|
1779
1870
|
return {
|
|
1780
1871
|
claudeDir: claudeDir2,
|
|
1781
|
-
settingsPath:
|
|
1782
|
-
claudeMdPath:
|
|
1872
|
+
settingsPath: path8.join(claudeDir2, "settings.json"),
|
|
1873
|
+
claudeMdPath: path8.join(claudeDir2, "CLAUDE.md"),
|
|
1874
|
+
rulesPath: path8.join(claudeDir2, "rules", "keepgoing.md")
|
|
1783
1875
|
};
|
|
1784
1876
|
}
|
|
1785
|
-
const claudeDir =
|
|
1786
|
-
const dotClaudeMdPath =
|
|
1787
|
-
const rootClaudeMdPath =
|
|
1877
|
+
const claudeDir = path8.join(workspacePath, ".claude");
|
|
1878
|
+
const dotClaudeMdPath = path8.join(workspacePath, ".claude", "CLAUDE.md");
|
|
1879
|
+
const rootClaudeMdPath = path8.join(workspacePath, "CLAUDE.md");
|
|
1788
1880
|
return {
|
|
1789
1881
|
claudeDir,
|
|
1790
|
-
settingsPath:
|
|
1791
|
-
claudeMdPath:
|
|
1882
|
+
settingsPath: path8.join(claudeDir, "settings.json"),
|
|
1883
|
+
claudeMdPath: fs6.existsSync(dotClaudeMdPath) ? dotClaudeMdPath : rootClaudeMdPath,
|
|
1884
|
+
rulesPath: path8.join(workspacePath, ".claude", "rules", "keepgoing.md")
|
|
1792
1885
|
};
|
|
1793
1886
|
}
|
|
1794
1887
|
function writeHooksToSettings(settings) {
|
|
@@ -1817,15 +1910,22 @@ function writeHooksToSettings(settings) {
|
|
|
1817
1910
|
settings.hooks.PostToolUse.push(POST_TOOL_USE_HOOK);
|
|
1818
1911
|
changed = true;
|
|
1819
1912
|
}
|
|
1913
|
+
if (!Array.isArray(settings.hooks.SessionEnd)) {
|
|
1914
|
+
settings.hooks.SessionEnd = [];
|
|
1915
|
+
}
|
|
1916
|
+
if (!hasKeepGoingHook(settings.hooks.SessionEnd)) {
|
|
1917
|
+
settings.hooks.SessionEnd.push(SESSION_END_HOOK);
|
|
1918
|
+
changed = true;
|
|
1919
|
+
}
|
|
1820
1920
|
return changed;
|
|
1821
1921
|
}
|
|
1822
1922
|
function checkHookConflict(scope, workspacePath) {
|
|
1823
1923
|
const otherPaths = resolveScopePaths(scope === "user" ? "project" : "user", workspacePath);
|
|
1824
|
-
if (!
|
|
1924
|
+
if (!fs6.existsSync(otherPaths.settingsPath)) {
|
|
1825
1925
|
return null;
|
|
1826
1926
|
}
|
|
1827
1927
|
try {
|
|
1828
|
-
const otherSettings = JSON.parse(
|
|
1928
|
+
const otherSettings = JSON.parse(fs6.readFileSync(otherPaths.settingsPath, "utf-8"));
|
|
1829
1929
|
const hooks = otherSettings?.hooks;
|
|
1830
1930
|
if (!hooks) return null;
|
|
1831
1931
|
const hasConflict = Array.isArray(hooks.SessionStart) && hasKeepGoingHook(hooks.SessionStart) || Array.isArray(hooks.Stop) && hasKeepGoingHook(hooks.Stop);
|
|
@@ -1844,15 +1944,20 @@ function setupProject(options) {
|
|
|
1844
1944
|
scope = "project",
|
|
1845
1945
|
sessionHooks = true,
|
|
1846
1946
|
claudeMd = true,
|
|
1947
|
+
claudeDir: claudeDirOverride,
|
|
1847
1948
|
statusline
|
|
1848
1949
|
} = options;
|
|
1849
1950
|
const messages = [];
|
|
1850
1951
|
let changed = false;
|
|
1851
|
-
const { claudeDir, settingsPath, claudeMdPath } = resolveScopePaths(
|
|
1852
|
-
|
|
1952
|
+
const { claudeDir, settingsPath, claudeMdPath, rulesPath } = resolveScopePaths(
|
|
1953
|
+
scope,
|
|
1954
|
+
workspacePath,
|
|
1955
|
+
claudeDirOverride
|
|
1956
|
+
);
|
|
1957
|
+
const scopeLabel = scope === "user" ? path8.join("~", path8.relative(os3.homedir(), claudeDir), "settings.json").replace(/\\/g, "/") : ".claude/settings.json";
|
|
1853
1958
|
let settings = {};
|
|
1854
|
-
if (
|
|
1855
|
-
settings = JSON.parse(
|
|
1959
|
+
if (fs6.existsSync(settingsPath)) {
|
|
1960
|
+
settings = JSON.parse(fs6.readFileSync(settingsPath, "utf-8"));
|
|
1856
1961
|
}
|
|
1857
1962
|
let settingsChanged = false;
|
|
1858
1963
|
if (sessionHooks) {
|
|
@@ -1883,29 +1988,45 @@ function setupProject(options) {
|
|
|
1883
1988
|
statusline?.cleanup?.();
|
|
1884
1989
|
}
|
|
1885
1990
|
if (settingsChanged) {
|
|
1886
|
-
if (!
|
|
1887
|
-
|
|
1991
|
+
if (!fs6.existsSync(claudeDir)) {
|
|
1992
|
+
fs6.mkdirSync(claudeDir, { recursive: true });
|
|
1888
1993
|
}
|
|
1889
|
-
|
|
1994
|
+
fs6.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
1890
1995
|
changed = true;
|
|
1891
1996
|
}
|
|
1892
1997
|
if (claudeMd) {
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1998
|
+
const rulesDir = path8.dirname(rulesPath);
|
|
1999
|
+
const rulesLabel = scope === "user" ? path8.join(path8.relative(os3.homedir(), path8.dirname(rulesPath)), "keepgoing.md").replace(/\\/g, "/") : ".claude/rules/keepgoing.md";
|
|
2000
|
+
if (fs6.existsSync(rulesPath)) {
|
|
2001
|
+
const existing = fs6.readFileSync(rulesPath, "utf-8");
|
|
2002
|
+
const existingVersion = getRulesFileVersion(existing);
|
|
2003
|
+
if (existingVersion === null) {
|
|
2004
|
+
messages.push(`Rules file: Custom file found at ${rulesLabel}, skipping`);
|
|
2005
|
+
} else if (existingVersion >= KEEPGOING_RULES_VERSION) {
|
|
2006
|
+
messages.push(`Rules file: Already up to date (v${existingVersion}), skipped`);
|
|
2007
|
+
} else {
|
|
2008
|
+
if (!fs6.existsSync(rulesDir)) {
|
|
2009
|
+
fs6.mkdirSync(rulesDir, { recursive: true });
|
|
2010
|
+
}
|
|
2011
|
+
fs6.writeFileSync(rulesPath, KEEPGOING_RULES_CONTENT);
|
|
2012
|
+
changed = true;
|
|
2013
|
+
messages.push(`Rules file: Updated v${existingVersion} \u2192 v${KEEPGOING_RULES_VERSION} at ${rulesLabel}`);
|
|
2014
|
+
}
|
|
1900
2015
|
} else {
|
|
1901
|
-
const
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
fs5.mkdirSync(mdDir, { recursive: true });
|
|
2016
|
+
const existingClaudeMd = fs6.existsSync(claudeMdPath) ? fs6.readFileSync(claudeMdPath, "utf-8") : "";
|
|
2017
|
+
if (!fs6.existsSync(rulesDir)) {
|
|
2018
|
+
fs6.mkdirSync(rulesDir, { recursive: true });
|
|
1905
2019
|
}
|
|
1906
|
-
|
|
2020
|
+
fs6.writeFileSync(rulesPath, KEEPGOING_RULES_CONTENT);
|
|
1907
2021
|
changed = true;
|
|
1908
|
-
|
|
2022
|
+
if (existingClaudeMd.includes("## KeepGoing")) {
|
|
2023
|
+
const mdLabel = scope === "user" ? "~/.claude/CLAUDE.md" : "CLAUDE.md";
|
|
2024
|
+
messages.push(
|
|
2025
|
+
`Rules file: Created ${rulesLabel} (you can now remove the ## KeepGoing section from ${mdLabel})`
|
|
2026
|
+
);
|
|
2027
|
+
} else {
|
|
2028
|
+
messages.push(`Rules file: Created ${rulesLabel}`);
|
|
2029
|
+
}
|
|
1909
2030
|
}
|
|
1910
2031
|
}
|
|
1911
2032
|
return { messages, changed };
|
|
@@ -1982,6 +2103,39 @@ async function activateLicense(licenseKey, instanceName, options) {
|
|
|
1982
2103
|
return { valid: false, error: message };
|
|
1983
2104
|
}
|
|
1984
2105
|
}
|
|
2106
|
+
async function validateLicense(licenseKey, instanceId, options) {
|
|
2107
|
+
try {
|
|
2108
|
+
const res = await fetchWithTimeout(`${BASE_URL}/validate`, {
|
|
2109
|
+
method: "POST",
|
|
2110
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
2111
|
+
body: new URLSearchParams({ license_key: licenseKey, instance_id: instanceId })
|
|
2112
|
+
});
|
|
2113
|
+
const data = await safeJson(res);
|
|
2114
|
+
if (!res.ok || !data?.valid) {
|
|
2115
|
+
return { valid: false, isNetworkError: false, error: data?.error || `Validation failed (${res.status})` };
|
|
2116
|
+
}
|
|
2117
|
+
if (!options?.allowTestMode && data.license_key?.test_mode) {
|
|
2118
|
+
return { valid: false, isNetworkError: false, error: "This is a test license key. Please use a production license key from your purchase confirmation." };
|
|
2119
|
+
}
|
|
2120
|
+
if (!options?.allowTestMode) {
|
|
2121
|
+
const productError = validateProductIdentity(data.meta);
|
|
2122
|
+
if (productError) {
|
|
2123
|
+
return { valid: false, isNetworkError: false, error: productError };
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
return {
|
|
2127
|
+
valid: true,
|
|
2128
|
+
licenseKey: data.license_key?.key,
|
|
2129
|
+
customerName: data.meta?.customer_name,
|
|
2130
|
+
productName: data.meta?.product_name,
|
|
2131
|
+
variantId: data.meta?.variant_id,
|
|
2132
|
+
variantName: data.meta?.variant_name
|
|
2133
|
+
};
|
|
2134
|
+
} catch (err) {
|
|
2135
|
+
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";
|
|
2136
|
+
return { valid: false, isNetworkError: true, error: message };
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
1985
2139
|
async function deactivateLicense(licenseKey, instanceId) {
|
|
1986
2140
|
try {
|
|
1987
2141
|
const res = await fetchWithTimeout(`${BASE_URL}/deactivate`, {
|
|
@@ -2000,6 +2154,52 @@ async function deactivateLicense(licenseKey, instanceId) {
|
|
|
2000
2154
|
}
|
|
2001
2155
|
}
|
|
2002
2156
|
|
|
2157
|
+
// ../../packages/shared/src/licenseRevalidation.ts
|
|
2158
|
+
async function revalidateStaleLicenses(options) {
|
|
2159
|
+
const stale = getAllLicensesNeedingRevalidation();
|
|
2160
|
+
const result = { checked: stale.length, refreshed: 0, revoked: 0, skippedNetworkError: 0 };
|
|
2161
|
+
if (stale.length === 0) return result;
|
|
2162
|
+
const updates = /* @__PURE__ */ new Map();
|
|
2163
|
+
for (const entry of stale) {
|
|
2164
|
+
const vResult = await validateLicense(entry.licenseKey, entry.instanceId, {
|
|
2165
|
+
allowTestMode: options?.allowTestMode
|
|
2166
|
+
});
|
|
2167
|
+
if (vResult.valid) {
|
|
2168
|
+
const patch = {
|
|
2169
|
+
lastValidatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2170
|
+
};
|
|
2171
|
+
if (vResult.customerName) patch.customerName = vResult.customerName;
|
|
2172
|
+
updates.set(entry.licenseKey, patch);
|
|
2173
|
+
result.refreshed++;
|
|
2174
|
+
} else if (vResult.isNetworkError) {
|
|
2175
|
+
result.skippedNetworkError++;
|
|
2176
|
+
} else {
|
|
2177
|
+
updates.set(entry.licenseKey, { status: "inactive" });
|
|
2178
|
+
result.revoked++;
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
if (updates.size > 0) {
|
|
2182
|
+
const store = readLicenseStore();
|
|
2183
|
+
for (const [key, patch] of updates) {
|
|
2184
|
+
const idx = store.licenses.findIndex((l) => l.licenseKey === key);
|
|
2185
|
+
if (idx >= 0) {
|
|
2186
|
+
Object.assign(store.licenses[idx], patch);
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
writeLicenseStore(store);
|
|
2190
|
+
}
|
|
2191
|
+
return result;
|
|
2192
|
+
}
|
|
2193
|
+
async function getLicenseForFeatureWithRevalidation(feature, options) {
|
|
2194
|
+
const license = getLicenseForFeature(feature);
|
|
2195
|
+
if (!license) return void 0;
|
|
2196
|
+
if (needsRevalidation(license)) {
|
|
2197
|
+
await revalidateStaleLicenses(options);
|
|
2198
|
+
return getLicenseForFeature(feature);
|
|
2199
|
+
}
|
|
2200
|
+
return license;
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2003
2203
|
// src/tools/getMomentum.ts
|
|
2004
2204
|
import { z } from "zod";
|
|
2005
2205
|
function registerGetMomentum(server, reader, workspacePath) {
|
|
@@ -2253,7 +2453,7 @@ function registerGetReentryBriefing(server, reader, workspacePath) {
|
|
|
2253
2453
|
}
|
|
2254
2454
|
|
|
2255
2455
|
// src/tools/saveCheckpoint.ts
|
|
2256
|
-
import
|
|
2456
|
+
import path9 from "path";
|
|
2257
2457
|
import { z as z4 } from "zod";
|
|
2258
2458
|
function registerSaveCheckpoint(server, reader, workspacePath) {
|
|
2259
2459
|
server.tool(
|
|
@@ -2269,7 +2469,7 @@ function registerSaveCheckpoint(server, reader, workspacePath) {
|
|
|
2269
2469
|
const gitBranch = getCurrentBranch(workspacePath);
|
|
2270
2470
|
const touchedFiles = getTouchedFiles(workspacePath);
|
|
2271
2471
|
const commitHashes = getCommitsSince(workspacePath, lastSession?.timestamp);
|
|
2272
|
-
const projectName =
|
|
2472
|
+
const projectName = path9.basename(resolveStorageRoot(workspacePath));
|
|
2273
2473
|
const sessionId = generateSessionId({ workspaceRoot: workspacePath, branch: gitBranch ?? void 0, worktreePath: workspacePath });
|
|
2274
2474
|
const checkpoint = createCheckpoint({
|
|
2275
2475
|
summary,
|
|
@@ -2336,7 +2536,7 @@ function registerGetDecisions(server, reader) {
|
|
|
2336
2536
|
]
|
|
2337
2537
|
};
|
|
2338
2538
|
}
|
|
2339
|
-
if (process.env.KEEPGOING_PRO_BYPASS !== "1" && !
|
|
2539
|
+
if (process.env.KEEPGOING_PRO_BYPASS !== "1" && !await getLicenseForFeatureWithRevalidation("decisions")) {
|
|
2340
2540
|
return {
|
|
2341
2541
|
content: [
|
|
2342
2542
|
{
|
|
@@ -2389,7 +2589,7 @@ function registerGetDecisions(server, reader) {
|
|
|
2389
2589
|
function registerGetCurrentTask(server, reader) {
|
|
2390
2590
|
server.tool(
|
|
2391
2591
|
"get_current_task",
|
|
2392
|
-
"Get
|
|
2592
|
+
"Get a bird's eye view of all active Claude sessions. See what each session is working on, which branch it is on, and when it last did something. Useful when running multiple parallel sessions across worktrees.",
|
|
2393
2593
|
{},
|
|
2394
2594
|
async () => {
|
|
2395
2595
|
if (!reader.exists()) {
|
|
@@ -2402,7 +2602,7 @@ function registerGetCurrentTask(server, reader) {
|
|
|
2402
2602
|
]
|
|
2403
2603
|
};
|
|
2404
2604
|
}
|
|
2405
|
-
if (process.env.KEEPGOING_PRO_BYPASS !== "1" && !
|
|
2605
|
+
if (process.env.KEEPGOING_PRO_BYPASS !== "1" && !await getLicenseForFeatureWithRevalidation("session-awareness")) {
|
|
2406
2606
|
return {
|
|
2407
2607
|
content: [
|
|
2408
2608
|
{
|
|
@@ -2438,12 +2638,15 @@ function registerGetCurrentTask(server, reader) {
|
|
|
2438
2638
|
for (const task of [...activeTasks, ...finishedTasks]) {
|
|
2439
2639
|
const statusIcon = task.sessionActive ? "\u{1F7E2}" : "\u2705";
|
|
2440
2640
|
const statusLabel = task.sessionActive ? "Active" : "Finished";
|
|
2441
|
-
const sessionLabel = task.agentLabel || task.sessionId || "Session";
|
|
2641
|
+
const sessionLabel = task.sessionLabel || task.agentLabel || task.sessionId || "Session";
|
|
2442
2642
|
lines.push(`### ${statusIcon} ${sessionLabel} (${statusLabel})`);
|
|
2443
2643
|
lines.push(`- **Updated:** ${formatRelativeTime(task.updatedAt)}`);
|
|
2444
2644
|
if (task.branch) {
|
|
2445
2645
|
lines.push(`- **Branch:** ${task.branch}`);
|
|
2446
2646
|
}
|
|
2647
|
+
if (task.agentLabel && task.sessionLabel) {
|
|
2648
|
+
lines.push(`- **Agent:** ${task.agentLabel}`);
|
|
2649
|
+
}
|
|
2447
2650
|
if (task.taskSummary) {
|
|
2448
2651
|
lines.push(`- **Doing:** ${task.taskSummary}`);
|
|
2449
2652
|
}
|
|
@@ -2484,25 +2687,25 @@ function registerGetCurrentTask(server, reader) {
|
|
|
2484
2687
|
import { z as z6 } from "zod";
|
|
2485
2688
|
|
|
2486
2689
|
// src/cli/migrate.ts
|
|
2487
|
-
import
|
|
2488
|
-
import
|
|
2489
|
-
import
|
|
2690
|
+
import fs7 from "fs";
|
|
2691
|
+
import os4 from "os";
|
|
2692
|
+
import path10 from "path";
|
|
2490
2693
|
var STATUSLINE_CMD2 = "npx -y @keepgoingdev/mcp-server --statusline";
|
|
2491
2694
|
function isLegacyStatusline(command) {
|
|
2492
2695
|
return !command.includes("--statusline") && command.includes("keepgoing-statusline");
|
|
2493
2696
|
}
|
|
2494
2697
|
function migrateStatusline(wsPath) {
|
|
2495
|
-
const settingsPath =
|
|
2496
|
-
if (!
|
|
2698
|
+
const settingsPath = path10.join(wsPath, ".claude", "settings.json");
|
|
2699
|
+
if (!fs7.existsSync(settingsPath)) return void 0;
|
|
2497
2700
|
try {
|
|
2498
|
-
const settings = JSON.parse(
|
|
2701
|
+
const settings = JSON.parse(fs7.readFileSync(settingsPath, "utf-8"));
|
|
2499
2702
|
const cmd = settings.statusLine?.command;
|
|
2500
2703
|
if (!cmd || !isLegacyStatusline(cmd)) return void 0;
|
|
2501
2704
|
settings.statusLine = {
|
|
2502
2705
|
type: "command",
|
|
2503
2706
|
command: STATUSLINE_CMD2
|
|
2504
2707
|
};
|
|
2505
|
-
|
|
2708
|
+
fs7.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
2506
2709
|
cleanupLegacyScript();
|
|
2507
2710
|
return "[KeepGoing] Migrated statusline to auto-updating command (restart Claude Code to apply)";
|
|
2508
2711
|
} catch {
|
|
@@ -2510,10 +2713,10 @@ function migrateStatusline(wsPath) {
|
|
|
2510
2713
|
}
|
|
2511
2714
|
}
|
|
2512
2715
|
function cleanupLegacyScript() {
|
|
2513
|
-
const legacyScript =
|
|
2514
|
-
if (
|
|
2716
|
+
const legacyScript = path10.join(os4.homedir(), ".claude", "keepgoing-statusline.sh");
|
|
2717
|
+
if (fs7.existsSync(legacyScript)) {
|
|
2515
2718
|
try {
|
|
2516
|
-
|
|
2719
|
+
fs7.unlinkSync(legacyScript);
|
|
2517
2720
|
} catch {
|
|
2518
2721
|
}
|
|
2519
2722
|
}
|
|
@@ -2526,15 +2729,17 @@ function registerSetupProject(server, workspacePath) {
|
|
|
2526
2729
|
'Set up KeepGoing hooks and instructions. Use scope "user" for global setup (all projects) or "project" for per-project setup.',
|
|
2527
2730
|
{
|
|
2528
2731
|
sessionHooks: z6.boolean().optional().default(true).describe("Add session hooks to settings.json"),
|
|
2529
|
-
claudeMd: z6.boolean().optional().default(true).describe("Add KeepGoing instructions to
|
|
2530
|
-
scope: z6.enum(["project", "user"]).optional().default("project").describe('Where to write config: "user" for global (~/.claude/), "project" for per-project (.claude/)')
|
|
2732
|
+
claudeMd: z6.boolean().optional().default(true).describe("Add KeepGoing instructions to .claude/rules/keepgoing.md"),
|
|
2733
|
+
scope: z6.enum(["project", "user"]).optional().default("project").describe('Where to write config: "user" for global (~/.claude/), "project" for per-project (.claude/)'),
|
|
2734
|
+
claudeDir: z6.string().optional().describe("Override the Claude config directory for user scope (defaults to CLAUDE_CONFIG_DIR env var or ~/.claude)")
|
|
2531
2735
|
},
|
|
2532
|
-
async ({ sessionHooks, claudeMd, scope }) => {
|
|
2736
|
+
async ({ sessionHooks, claudeMd, scope, claudeDir }) => {
|
|
2533
2737
|
const result = setupProject({
|
|
2534
2738
|
workspacePath,
|
|
2535
2739
|
scope,
|
|
2536
2740
|
sessionHooks,
|
|
2537
2741
|
claudeMd,
|
|
2742
|
+
claudeDir,
|
|
2538
2743
|
statusline: {
|
|
2539
2744
|
isLegacy: isLegacyStatusline,
|
|
2540
2745
|
cleanup: cleanupLegacyScript
|
|
@@ -2872,7 +3077,7 @@ async function handlePrintMomentum() {
|
|
|
2872
3077
|
process.exit(0);
|
|
2873
3078
|
}
|
|
2874
3079
|
async function handlePrintCurrent() {
|
|
2875
|
-
if (process.env.KEEPGOING_PRO_BYPASS !== "1" && !
|
|
3080
|
+
if (process.env.KEEPGOING_PRO_BYPASS !== "1" && !await getLicenseForFeatureWithRevalidation("session-awareness")) {
|
|
2876
3081
|
process.exit(0);
|
|
2877
3082
|
}
|
|
2878
3083
|
const wsPath = resolveWsPath();
|
|
@@ -2909,7 +3114,7 @@ async function handlePrintCurrent() {
|
|
|
2909
3114
|
}
|
|
2910
3115
|
|
|
2911
3116
|
// src/cli/saveCheckpoint.ts
|
|
2912
|
-
import
|
|
3117
|
+
import path11 from "path";
|
|
2913
3118
|
async function handleSaveCheckpoint() {
|
|
2914
3119
|
const wsPath = resolveWsPath();
|
|
2915
3120
|
const reader = new KeepGoingReader(wsPath);
|
|
@@ -2937,9 +3142,9 @@ async function handleSaveCheckpoint() {
|
|
|
2937
3142
|
sessionStartTime: lastSession?.timestamp ?? now,
|
|
2938
3143
|
lastActivityTime: now
|
|
2939
3144
|
});
|
|
2940
|
-
const summary = buildSmartSummary(events) ?? `Worked on ${touchedFiles.slice(0, 5).map((f) =>
|
|
3145
|
+
const summary = buildSmartSummary(events) ?? `Worked on ${touchedFiles.slice(0, 5).map((f) => path11.basename(f)).join(", ")}`;
|
|
2941
3146
|
const nextStep = buildSmartNextStep(events);
|
|
2942
|
-
const projectName =
|
|
3147
|
+
const projectName = path11.basename(resolveStorageRoot(wsPath));
|
|
2943
3148
|
const sessionId = generateSessionId({ workspaceRoot: wsPath, branch: gitBranch ?? void 0, worktreePath: wsPath });
|
|
2944
3149
|
const checkpoint = createCheckpoint({
|
|
2945
3150
|
summary,
|
|
@@ -2960,16 +3165,19 @@ async function handleSaveCheckpoint() {
|
|
|
2960
3165
|
branch: gitBranch ?? void 0,
|
|
2961
3166
|
updatedAt: checkpoint.timestamp
|
|
2962
3167
|
});
|
|
2963
|
-
if (
|
|
2964
|
-
|
|
2965
|
-
|
|
3168
|
+
if (process.env.KEEPGOING_PRO_BYPASS === "1" || await getLicenseForFeatureWithRevalidation("decisions")) {
|
|
3169
|
+
for (let i = 0; i < commitHashes.length; i++) {
|
|
3170
|
+
const hash = commitHashes[i];
|
|
3171
|
+
const message = commitMessages[i];
|
|
3172
|
+
if (!hash || !message) continue;
|
|
3173
|
+
const files = getFilesChangedInCommit(wsPath, hash);
|
|
2966
3174
|
const detected = tryDetectDecision({
|
|
2967
3175
|
workspacePath: wsPath,
|
|
2968
3176
|
checkpointId: checkpoint.id,
|
|
2969
3177
|
gitBranch,
|
|
2970
|
-
commitHash:
|
|
2971
|
-
commitMessage:
|
|
2972
|
-
filesChanged:
|
|
3178
|
+
commitHash: hash,
|
|
3179
|
+
commitMessage: message,
|
|
3180
|
+
filesChanged: files
|
|
2973
3181
|
});
|
|
2974
3182
|
if (detected) {
|
|
2975
3183
|
console.log(`[KeepGoing] Decision detected: ${detected.category} (${(detected.confidence * 100).toFixed(0)}% confidence)`);
|
|
@@ -2981,7 +3189,7 @@ async function handleSaveCheckpoint() {
|
|
|
2981
3189
|
}
|
|
2982
3190
|
|
|
2983
3191
|
// src/cli/transcriptUtils.ts
|
|
2984
|
-
import
|
|
3192
|
+
import fs8 from "fs";
|
|
2985
3193
|
var TAIL_READ_BYTES = 8192;
|
|
2986
3194
|
var TOOL_VERB_MAP = {
|
|
2987
3195
|
Edit: "editing",
|
|
@@ -3032,9 +3240,9 @@ function isAssistantEntry(entry) {
|
|
|
3032
3240
|
return entry.message?.role === "assistant";
|
|
3033
3241
|
}
|
|
3034
3242
|
function extractSessionLabel(transcriptPath) {
|
|
3035
|
-
if (!transcriptPath || !
|
|
3243
|
+
if (!transcriptPath || !fs8.existsSync(transcriptPath)) return null;
|
|
3036
3244
|
try {
|
|
3037
|
-
const raw =
|
|
3245
|
+
const raw = fs8.readFileSync(transcriptPath, "utf-8");
|
|
3038
3246
|
for (const line of raw.split("\n")) {
|
|
3039
3247
|
const trimmed = line.trim();
|
|
3040
3248
|
if (!trimmed) continue;
|
|
@@ -3063,19 +3271,19 @@ function extractSessionLabel(transcriptPath) {
|
|
|
3063
3271
|
return null;
|
|
3064
3272
|
}
|
|
3065
3273
|
function extractCurrentAction(transcriptPath) {
|
|
3066
|
-
if (!transcriptPath || !
|
|
3274
|
+
if (!transcriptPath || !fs8.existsSync(transcriptPath)) return null;
|
|
3067
3275
|
try {
|
|
3068
|
-
const stat =
|
|
3276
|
+
const stat = fs8.statSync(transcriptPath);
|
|
3069
3277
|
const fileSize = stat.size;
|
|
3070
3278
|
if (fileSize === 0) return null;
|
|
3071
3279
|
const readSize = Math.min(fileSize, TAIL_READ_BYTES);
|
|
3072
3280
|
const offset = fileSize - readSize;
|
|
3073
3281
|
const buf = Buffer.alloc(readSize);
|
|
3074
|
-
const fd =
|
|
3282
|
+
const fd = fs8.openSync(transcriptPath, "r");
|
|
3075
3283
|
try {
|
|
3076
|
-
|
|
3284
|
+
fs8.readSync(fd, buf, 0, readSize, offset);
|
|
3077
3285
|
} finally {
|
|
3078
|
-
|
|
3286
|
+
fs8.closeSync(fd);
|
|
3079
3287
|
}
|
|
3080
3288
|
const tail = buf.toString("utf-8");
|
|
3081
3289
|
const lines = tail.split("\n").reverse();
|
|
@@ -3177,8 +3385,8 @@ async function handleUpdateTaskFromHook() {
|
|
|
3177
3385
|
}
|
|
3178
3386
|
|
|
3179
3387
|
// src/cli/statusline.ts
|
|
3180
|
-
import
|
|
3181
|
-
import
|
|
3388
|
+
import fs9 from "fs";
|
|
3389
|
+
import path12 from "path";
|
|
3182
3390
|
var STDIN_TIMEOUT_MS2 = 3e3;
|
|
3183
3391
|
async function handleStatusline() {
|
|
3184
3392
|
const chunks = [];
|
|
@@ -3205,9 +3413,9 @@ async function handleStatusline() {
|
|
|
3205
3413
|
if (!label) {
|
|
3206
3414
|
try {
|
|
3207
3415
|
const gitRoot = findGitRoot(dir);
|
|
3208
|
-
const tasksFile =
|
|
3209
|
-
if (
|
|
3210
|
-
const data = JSON.parse(
|
|
3416
|
+
const tasksFile = path12.join(gitRoot, ".keepgoing", "current-tasks.json");
|
|
3417
|
+
if (fs9.existsSync(tasksFile)) {
|
|
3418
|
+
const data = JSON.parse(fs9.readFileSync(tasksFile, "utf-8"));
|
|
3211
3419
|
const tasks = pruneStaleTasks(data.tasks ?? []);
|
|
3212
3420
|
const match = sessionId ? tasks.find((t) => t.sessionId === sessionId) : void 0;
|
|
3213
3421
|
if (match?.sessionLabel) {
|
|
@@ -3254,6 +3462,35 @@ async function handleContinueOn() {
|
|
|
3254
3462
|
process.exit(0);
|
|
3255
3463
|
}
|
|
3256
3464
|
|
|
3465
|
+
// src/cli/detectDecisions.ts
|
|
3466
|
+
async function handleDetectDecisions() {
|
|
3467
|
+
const wsPath = resolveWsPath();
|
|
3468
|
+
if (!(process.env.KEEPGOING_PRO_BYPASS === "1" || await getLicenseForFeatureWithRevalidation("decisions"))) {
|
|
3469
|
+
process.exit(0);
|
|
3470
|
+
}
|
|
3471
|
+
const reader = new KeepGoingReader(wsPath);
|
|
3472
|
+
if (!reader.exists()) {
|
|
3473
|
+
process.exit(0);
|
|
3474
|
+
}
|
|
3475
|
+
const gitBranch = getCurrentBranch(wsPath);
|
|
3476
|
+
const headHash = getHeadCommitHash(wsPath);
|
|
3477
|
+
if (!headHash) process.exit(0);
|
|
3478
|
+
const commitMessage = getCommitMessageByHash(wsPath, headHash);
|
|
3479
|
+
if (!commitMessage) process.exit(0);
|
|
3480
|
+
const files = getFilesChangedInCommit(wsPath, headHash);
|
|
3481
|
+
const detected = tryDetectDecision({
|
|
3482
|
+
workspacePath: wsPath,
|
|
3483
|
+
gitBranch,
|
|
3484
|
+
commitHash: headHash,
|
|
3485
|
+
commitMessage,
|
|
3486
|
+
filesChanged: files
|
|
3487
|
+
});
|
|
3488
|
+
if (detected) {
|
|
3489
|
+
console.log(`[KeepGoing] Decision detected: ${detected.category} (${(detected.confidence * 100).toFixed(0)}% confidence)`);
|
|
3490
|
+
}
|
|
3491
|
+
process.exit(0);
|
|
3492
|
+
}
|
|
3493
|
+
|
|
3257
3494
|
// src/index.ts
|
|
3258
3495
|
var CLI_HANDLERS = {
|
|
3259
3496
|
"--print-momentum": handlePrintMomentum,
|
|
@@ -3262,7 +3499,8 @@ var CLI_HANDLERS = {
|
|
|
3262
3499
|
"--update-task-from-hook": handleUpdateTaskFromHook,
|
|
3263
3500
|
"--print-current": handlePrintCurrent,
|
|
3264
3501
|
"--statusline": handleStatusline,
|
|
3265
|
-
"--continue-on": handleContinueOn
|
|
3502
|
+
"--continue-on": handleContinueOn,
|
|
3503
|
+
"--detect-decisions": handleDetectDecisions
|
|
3266
3504
|
};
|
|
3267
3505
|
var flag = process.argv.slice(2).find((a) => a in CLI_HANDLERS);
|
|
3268
3506
|
if (flag) {
|