@keepgoingdev/mcp-server 0.6.0 → 0.7.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 +434 -153
- 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
|
}
|
|
@@ -1169,23 +1236,23 @@ var DecisionStorage = class {
|
|
|
1169
1236
|
decisionsFilePath;
|
|
1170
1237
|
constructor(workspacePath) {
|
|
1171
1238
|
const mainRoot = resolveStorageRoot(workspacePath);
|
|
1172
|
-
this.storagePath =
|
|
1173
|
-
this.decisionsFilePath =
|
|
1239
|
+
this.storagePath = path6.join(mainRoot, STORAGE_DIR2);
|
|
1240
|
+
this.decisionsFilePath = path6.join(this.storagePath, DECISIONS_FILE);
|
|
1174
1241
|
}
|
|
1175
1242
|
ensureStorageDir() {
|
|
1176
|
-
if (!
|
|
1177
|
-
|
|
1243
|
+
if (!fs4.existsSync(this.storagePath)) {
|
|
1244
|
+
fs4.mkdirSync(this.storagePath, { recursive: true });
|
|
1178
1245
|
}
|
|
1179
1246
|
}
|
|
1180
1247
|
getProjectName() {
|
|
1181
|
-
return
|
|
1248
|
+
return path6.basename(path6.dirname(this.storagePath));
|
|
1182
1249
|
}
|
|
1183
1250
|
load() {
|
|
1184
1251
|
try {
|
|
1185
|
-
if (!
|
|
1252
|
+
if (!fs4.existsSync(this.decisionsFilePath)) {
|
|
1186
1253
|
return createEmptyProjectDecisions(this.getProjectName());
|
|
1187
1254
|
}
|
|
1188
|
-
const raw =
|
|
1255
|
+
const raw = fs4.readFileSync(this.decisionsFilePath, "utf-8");
|
|
1189
1256
|
const data = JSON.parse(raw);
|
|
1190
1257
|
return data;
|
|
1191
1258
|
} catch {
|
|
@@ -1195,7 +1262,7 @@ var DecisionStorage = class {
|
|
|
1195
1262
|
save(decisions) {
|
|
1196
1263
|
this.ensureStorageDir();
|
|
1197
1264
|
const content = JSON.stringify(decisions, null, 2);
|
|
1198
|
-
|
|
1265
|
+
fs4.writeFileSync(this.decisionsFilePath, content, "utf-8");
|
|
1199
1266
|
}
|
|
1200
1267
|
/**
|
|
1201
1268
|
* Save a decision record as a draft. Always persists regardless of Pro
|
|
@@ -1446,8 +1513,8 @@ function tryDetectDecision(opts) {
|
|
|
1446
1513
|
}
|
|
1447
1514
|
|
|
1448
1515
|
// ../../packages/shared/src/reader.ts
|
|
1449
|
-
import
|
|
1450
|
-
import
|
|
1516
|
+
import fs5 from "fs";
|
|
1517
|
+
import path7 from "path";
|
|
1451
1518
|
var STORAGE_DIR3 = ".keepgoing";
|
|
1452
1519
|
var META_FILE2 = "meta.json";
|
|
1453
1520
|
var SESSIONS_FILE2 = "sessions.json";
|
|
@@ -1469,16 +1536,16 @@ var KeepGoingReader = class {
|
|
|
1469
1536
|
this.workspacePath = workspacePath;
|
|
1470
1537
|
const mainRoot = resolveStorageRoot(workspacePath);
|
|
1471
1538
|
this._isWorktree = mainRoot !== workspacePath;
|
|
1472
|
-
this.storagePath =
|
|
1473
|
-
this.metaFilePath =
|
|
1474
|
-
this.sessionsFilePath =
|
|
1475
|
-
this.decisionsFilePath =
|
|
1476
|
-
this.stateFilePath =
|
|
1477
|
-
this.currentTasksFilePath =
|
|
1539
|
+
this.storagePath = path7.join(mainRoot, STORAGE_DIR3);
|
|
1540
|
+
this.metaFilePath = path7.join(this.storagePath, META_FILE2);
|
|
1541
|
+
this.sessionsFilePath = path7.join(this.storagePath, SESSIONS_FILE2);
|
|
1542
|
+
this.decisionsFilePath = path7.join(this.storagePath, DECISIONS_FILE2);
|
|
1543
|
+
this.stateFilePath = path7.join(this.storagePath, STATE_FILE2);
|
|
1544
|
+
this.currentTasksFilePath = path7.join(this.storagePath, CURRENT_TASKS_FILE2);
|
|
1478
1545
|
}
|
|
1479
1546
|
/** Check if .keepgoing/ directory exists. */
|
|
1480
1547
|
exists() {
|
|
1481
|
-
return
|
|
1548
|
+
return fs5.existsSync(this.storagePath);
|
|
1482
1549
|
}
|
|
1483
1550
|
/** Read state.json, returns undefined if missing or corrupt. */
|
|
1484
1551
|
getState() {
|
|
@@ -1716,10 +1783,10 @@ var KeepGoingReader = class {
|
|
|
1716
1783
|
}
|
|
1717
1784
|
readJsonFile(filePath) {
|
|
1718
1785
|
try {
|
|
1719
|
-
if (!
|
|
1786
|
+
if (!fs5.existsSync(filePath)) {
|
|
1720
1787
|
return void 0;
|
|
1721
1788
|
}
|
|
1722
|
-
const raw =
|
|
1789
|
+
const raw = fs5.readFileSync(filePath, "utf-8");
|
|
1723
1790
|
return JSON.parse(raw);
|
|
1724
1791
|
} catch {
|
|
1725
1792
|
return void 0;
|
|
@@ -1728,9 +1795,9 @@ var KeepGoingReader = class {
|
|
|
1728
1795
|
};
|
|
1729
1796
|
|
|
1730
1797
|
// ../../packages/shared/src/setup.ts
|
|
1731
|
-
import
|
|
1732
|
-
import
|
|
1733
|
-
import
|
|
1798
|
+
import fs6 from "fs";
|
|
1799
|
+
import os3 from "os";
|
|
1800
|
+
import path8 from "path";
|
|
1734
1801
|
var KEEPGOING_MARKER = "@keepgoingdev/mcp-server";
|
|
1735
1802
|
var SESSION_START_HOOK = {
|
|
1736
1803
|
matcher: "",
|
|
@@ -1759,36 +1826,55 @@ var POST_TOOL_USE_HOOK = {
|
|
|
1759
1826
|
}
|
|
1760
1827
|
]
|
|
1761
1828
|
};
|
|
1762
|
-
var
|
|
1829
|
+
var SESSION_END_HOOK = {
|
|
1830
|
+
matcher: "",
|
|
1831
|
+
hooks: [
|
|
1832
|
+
{
|
|
1833
|
+
type: "command",
|
|
1834
|
+
command: "npx -y @keepgoingdev/mcp-server --save-checkpoint"
|
|
1835
|
+
}
|
|
1836
|
+
]
|
|
1837
|
+
};
|
|
1838
|
+
var KEEPGOING_RULES_VERSION = 1;
|
|
1839
|
+
var KEEPGOING_RULES_CONTENT = `<!-- @keepgoingdev/mcp-server v${KEEPGOING_RULES_VERSION} -->
|
|
1763
1840
|
## KeepGoing
|
|
1764
1841
|
|
|
1765
1842
|
After completing a task or meaningful piece of work, call the \`save_checkpoint\` MCP tool with:
|
|
1766
|
-
- \`summary\`: What
|
|
1767
|
-
- \`nextStep\`: What
|
|
1843
|
+
- \`summary\`: 1-2 sentences. What changed and why \u2014 no file paths, no implementation details (those are captured from git).
|
|
1844
|
+
- \`nextStep\`: What to do next
|
|
1768
1845
|
- \`blocker\`: Any blocker (if applicable)
|
|
1769
1846
|
`;
|
|
1847
|
+
function getRulesFileVersion(content) {
|
|
1848
|
+
const match = content.match(/<!-- @keepgoingdev\/mcp-server v(\d+) -->/);
|
|
1849
|
+
return match ? parseInt(match[1], 10) : null;
|
|
1850
|
+
}
|
|
1770
1851
|
var STATUSLINE_CMD = "npx -y @keepgoingdev/mcp-server --statusline";
|
|
1852
|
+
function detectClaudeDir() {
|
|
1853
|
+
return process.env.CLAUDE_CONFIG_DIR || path8.join(os3.homedir(), ".claude");
|
|
1854
|
+
}
|
|
1771
1855
|
function hasKeepGoingHook(hookEntries) {
|
|
1772
1856
|
return hookEntries.some(
|
|
1773
1857
|
(entry) => entry?.hooks?.some((h) => typeof h?.command === "string" && h.command.includes(KEEPGOING_MARKER))
|
|
1774
1858
|
);
|
|
1775
1859
|
}
|
|
1776
|
-
function resolveScopePaths(scope, workspacePath) {
|
|
1860
|
+
function resolveScopePaths(scope, workspacePath, overrideClaudeDir) {
|
|
1777
1861
|
if (scope === "user") {
|
|
1778
|
-
const claudeDir2 =
|
|
1862
|
+
const claudeDir2 = overrideClaudeDir || detectClaudeDir();
|
|
1779
1863
|
return {
|
|
1780
1864
|
claudeDir: claudeDir2,
|
|
1781
|
-
settingsPath:
|
|
1782
|
-
claudeMdPath:
|
|
1865
|
+
settingsPath: path8.join(claudeDir2, "settings.json"),
|
|
1866
|
+
claudeMdPath: path8.join(claudeDir2, "CLAUDE.md"),
|
|
1867
|
+
rulesPath: path8.join(claudeDir2, "rules", "keepgoing.md")
|
|
1783
1868
|
};
|
|
1784
1869
|
}
|
|
1785
|
-
const claudeDir =
|
|
1786
|
-
const dotClaudeMdPath =
|
|
1787
|
-
const rootClaudeMdPath =
|
|
1870
|
+
const claudeDir = path8.join(workspacePath, ".claude");
|
|
1871
|
+
const dotClaudeMdPath = path8.join(workspacePath, ".claude", "CLAUDE.md");
|
|
1872
|
+
const rootClaudeMdPath = path8.join(workspacePath, "CLAUDE.md");
|
|
1788
1873
|
return {
|
|
1789
1874
|
claudeDir,
|
|
1790
|
-
settingsPath:
|
|
1791
|
-
claudeMdPath:
|
|
1875
|
+
settingsPath: path8.join(claudeDir, "settings.json"),
|
|
1876
|
+
claudeMdPath: fs6.existsSync(dotClaudeMdPath) ? dotClaudeMdPath : rootClaudeMdPath,
|
|
1877
|
+
rulesPath: path8.join(workspacePath, ".claude", "rules", "keepgoing.md")
|
|
1792
1878
|
};
|
|
1793
1879
|
}
|
|
1794
1880
|
function writeHooksToSettings(settings) {
|
|
@@ -1817,15 +1903,22 @@ function writeHooksToSettings(settings) {
|
|
|
1817
1903
|
settings.hooks.PostToolUse.push(POST_TOOL_USE_HOOK);
|
|
1818
1904
|
changed = true;
|
|
1819
1905
|
}
|
|
1906
|
+
if (!Array.isArray(settings.hooks.SessionEnd)) {
|
|
1907
|
+
settings.hooks.SessionEnd = [];
|
|
1908
|
+
}
|
|
1909
|
+
if (!hasKeepGoingHook(settings.hooks.SessionEnd)) {
|
|
1910
|
+
settings.hooks.SessionEnd.push(SESSION_END_HOOK);
|
|
1911
|
+
changed = true;
|
|
1912
|
+
}
|
|
1820
1913
|
return changed;
|
|
1821
1914
|
}
|
|
1822
1915
|
function checkHookConflict(scope, workspacePath) {
|
|
1823
1916
|
const otherPaths = resolveScopePaths(scope === "user" ? "project" : "user", workspacePath);
|
|
1824
|
-
if (!
|
|
1917
|
+
if (!fs6.existsSync(otherPaths.settingsPath)) {
|
|
1825
1918
|
return null;
|
|
1826
1919
|
}
|
|
1827
1920
|
try {
|
|
1828
|
-
const otherSettings = JSON.parse(
|
|
1921
|
+
const otherSettings = JSON.parse(fs6.readFileSync(otherPaths.settingsPath, "utf-8"));
|
|
1829
1922
|
const hooks = otherSettings?.hooks;
|
|
1830
1923
|
if (!hooks) return null;
|
|
1831
1924
|
const hasConflict = Array.isArray(hooks.SessionStart) && hasKeepGoingHook(hooks.SessionStart) || Array.isArray(hooks.Stop) && hasKeepGoingHook(hooks.Stop);
|
|
@@ -1844,16 +1937,20 @@ function setupProject(options) {
|
|
|
1844
1937
|
scope = "project",
|
|
1845
1938
|
sessionHooks = true,
|
|
1846
1939
|
claudeMd = true,
|
|
1847
|
-
|
|
1940
|
+
claudeDir: claudeDirOverride,
|
|
1848
1941
|
statusline
|
|
1849
1942
|
} = options;
|
|
1850
1943
|
const messages = [];
|
|
1851
1944
|
let changed = false;
|
|
1852
|
-
const { claudeDir, settingsPath, claudeMdPath } = resolveScopePaths(
|
|
1853
|
-
|
|
1945
|
+
const { claudeDir, settingsPath, claudeMdPath, rulesPath } = resolveScopePaths(
|
|
1946
|
+
scope,
|
|
1947
|
+
workspacePath,
|
|
1948
|
+
claudeDirOverride
|
|
1949
|
+
);
|
|
1950
|
+
const scopeLabel = scope === "user" ? path8.join("~", path8.relative(os3.homedir(), claudeDir), "settings.json").replace(/\\/g, "/") : ".claude/settings.json";
|
|
1854
1951
|
let settings = {};
|
|
1855
|
-
if (
|
|
1856
|
-
settings = JSON.parse(
|
|
1952
|
+
if (fs6.existsSync(settingsPath)) {
|
|
1953
|
+
settings = JSON.parse(fs6.readFileSync(settingsPath, "utf-8"));
|
|
1857
1954
|
}
|
|
1858
1955
|
let settingsChanged = false;
|
|
1859
1956
|
if (sessionHooks) {
|
|
@@ -1869,7 +1966,7 @@ function setupProject(options) {
|
|
|
1869
1966
|
messages.push(`Warning: ${conflict}`);
|
|
1870
1967
|
}
|
|
1871
1968
|
}
|
|
1872
|
-
if (scope === "project"
|
|
1969
|
+
if (scope === "project") {
|
|
1873
1970
|
const needsUpdate = settings.statusLine?.command && statusline?.isLegacy?.(settings.statusLine.command);
|
|
1874
1971
|
if (!settings.statusLine || needsUpdate) {
|
|
1875
1972
|
settings.statusLine = {
|
|
@@ -1884,29 +1981,45 @@ function setupProject(options) {
|
|
|
1884
1981
|
statusline?.cleanup?.();
|
|
1885
1982
|
}
|
|
1886
1983
|
if (settingsChanged) {
|
|
1887
|
-
if (!
|
|
1888
|
-
|
|
1984
|
+
if (!fs6.existsSync(claudeDir)) {
|
|
1985
|
+
fs6.mkdirSync(claudeDir, { recursive: true });
|
|
1889
1986
|
}
|
|
1890
|
-
|
|
1987
|
+
fs6.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
1891
1988
|
changed = true;
|
|
1892
1989
|
}
|
|
1893
1990
|
if (claudeMd) {
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1991
|
+
const rulesDir = path8.dirname(rulesPath);
|
|
1992
|
+
const rulesLabel = scope === "user" ? path8.join(path8.relative(os3.homedir(), path8.dirname(rulesPath)), "keepgoing.md").replace(/\\/g, "/") : ".claude/rules/keepgoing.md";
|
|
1993
|
+
if (fs6.existsSync(rulesPath)) {
|
|
1994
|
+
const existing = fs6.readFileSync(rulesPath, "utf-8");
|
|
1995
|
+
const existingVersion = getRulesFileVersion(existing);
|
|
1996
|
+
if (existingVersion === null) {
|
|
1997
|
+
messages.push(`Rules file: Custom file found at ${rulesLabel}, skipping`);
|
|
1998
|
+
} else if (existingVersion >= KEEPGOING_RULES_VERSION) {
|
|
1999
|
+
messages.push(`Rules file: Already up to date (v${existingVersion}), skipped`);
|
|
2000
|
+
} else {
|
|
2001
|
+
if (!fs6.existsSync(rulesDir)) {
|
|
2002
|
+
fs6.mkdirSync(rulesDir, { recursive: true });
|
|
2003
|
+
}
|
|
2004
|
+
fs6.writeFileSync(rulesPath, KEEPGOING_RULES_CONTENT);
|
|
2005
|
+
changed = true;
|
|
2006
|
+
messages.push(`Rules file: Updated v${existingVersion} \u2192 v${KEEPGOING_RULES_VERSION} at ${rulesLabel}`);
|
|
2007
|
+
}
|
|
1901
2008
|
} else {
|
|
1902
|
-
const
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
fs5.mkdirSync(mdDir, { recursive: true });
|
|
2009
|
+
const existingClaudeMd = fs6.existsSync(claudeMdPath) ? fs6.readFileSync(claudeMdPath, "utf-8") : "";
|
|
2010
|
+
if (!fs6.existsSync(rulesDir)) {
|
|
2011
|
+
fs6.mkdirSync(rulesDir, { recursive: true });
|
|
1906
2012
|
}
|
|
1907
|
-
|
|
2013
|
+
fs6.writeFileSync(rulesPath, KEEPGOING_RULES_CONTENT);
|
|
1908
2014
|
changed = true;
|
|
1909
|
-
|
|
2015
|
+
if (existingClaudeMd.includes("## KeepGoing")) {
|
|
2016
|
+
const mdLabel = scope === "user" ? "~/.claude/CLAUDE.md" : "CLAUDE.md";
|
|
2017
|
+
messages.push(
|
|
2018
|
+
`Rules file: Created ${rulesLabel} (you can now remove the ## KeepGoing section from ${mdLabel})`
|
|
2019
|
+
);
|
|
2020
|
+
} else {
|
|
2021
|
+
messages.push(`Rules file: Created ${rulesLabel}`);
|
|
2022
|
+
}
|
|
1910
2023
|
}
|
|
1911
2024
|
}
|
|
1912
2025
|
return { messages, changed };
|
|
@@ -2254,7 +2367,7 @@ function registerGetReentryBriefing(server, reader, workspacePath) {
|
|
|
2254
2367
|
}
|
|
2255
2368
|
|
|
2256
2369
|
// src/tools/saveCheckpoint.ts
|
|
2257
|
-
import
|
|
2370
|
+
import path9 from "path";
|
|
2258
2371
|
import { z as z4 } from "zod";
|
|
2259
2372
|
function registerSaveCheckpoint(server, reader, workspacePath) {
|
|
2260
2373
|
server.tool(
|
|
@@ -2270,7 +2383,7 @@ function registerSaveCheckpoint(server, reader, workspacePath) {
|
|
|
2270
2383
|
const gitBranch = getCurrentBranch(workspacePath);
|
|
2271
2384
|
const touchedFiles = getTouchedFiles(workspacePath);
|
|
2272
2385
|
const commitHashes = getCommitsSince(workspacePath, lastSession?.timestamp);
|
|
2273
|
-
const projectName =
|
|
2386
|
+
const projectName = path9.basename(resolveStorageRoot(workspacePath));
|
|
2274
2387
|
const sessionId = generateSessionId({ workspaceRoot: workspacePath, branch: gitBranch ?? void 0, worktreePath: workspacePath });
|
|
2275
2388
|
const checkpoint = createCheckpoint({
|
|
2276
2389
|
summary,
|
|
@@ -2390,7 +2503,7 @@ function registerGetDecisions(server, reader) {
|
|
|
2390
2503
|
function registerGetCurrentTask(server, reader) {
|
|
2391
2504
|
server.tool(
|
|
2392
2505
|
"get_current_task",
|
|
2393
|
-
"Get
|
|
2506
|
+
"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.",
|
|
2394
2507
|
{},
|
|
2395
2508
|
async () => {
|
|
2396
2509
|
if (!reader.exists()) {
|
|
@@ -2439,12 +2552,15 @@ function registerGetCurrentTask(server, reader) {
|
|
|
2439
2552
|
for (const task of [...activeTasks, ...finishedTasks]) {
|
|
2440
2553
|
const statusIcon = task.sessionActive ? "\u{1F7E2}" : "\u2705";
|
|
2441
2554
|
const statusLabel = task.sessionActive ? "Active" : "Finished";
|
|
2442
|
-
const sessionLabel = task.agentLabel || task.sessionId || "Session";
|
|
2555
|
+
const sessionLabel = task.sessionLabel || task.agentLabel || task.sessionId || "Session";
|
|
2443
2556
|
lines.push(`### ${statusIcon} ${sessionLabel} (${statusLabel})`);
|
|
2444
2557
|
lines.push(`- **Updated:** ${formatRelativeTime(task.updatedAt)}`);
|
|
2445
2558
|
if (task.branch) {
|
|
2446
2559
|
lines.push(`- **Branch:** ${task.branch}`);
|
|
2447
2560
|
}
|
|
2561
|
+
if (task.agentLabel && task.sessionLabel) {
|
|
2562
|
+
lines.push(`- **Agent:** ${task.agentLabel}`);
|
|
2563
|
+
}
|
|
2448
2564
|
if (task.taskSummary) {
|
|
2449
2565
|
lines.push(`- **Doing:** ${task.taskSummary}`);
|
|
2450
2566
|
}
|
|
@@ -2485,25 +2601,25 @@ function registerGetCurrentTask(server, reader) {
|
|
|
2485
2601
|
import { z as z6 } from "zod";
|
|
2486
2602
|
|
|
2487
2603
|
// src/cli/migrate.ts
|
|
2488
|
-
import
|
|
2489
|
-
import
|
|
2490
|
-
import
|
|
2604
|
+
import fs7 from "fs";
|
|
2605
|
+
import os4 from "os";
|
|
2606
|
+
import path10 from "path";
|
|
2491
2607
|
var STATUSLINE_CMD2 = "npx -y @keepgoingdev/mcp-server --statusline";
|
|
2492
2608
|
function isLegacyStatusline(command) {
|
|
2493
2609
|
return !command.includes("--statusline") && command.includes("keepgoing-statusline");
|
|
2494
2610
|
}
|
|
2495
2611
|
function migrateStatusline(wsPath) {
|
|
2496
|
-
const settingsPath =
|
|
2497
|
-
if (!
|
|
2612
|
+
const settingsPath = path10.join(wsPath, ".claude", "settings.json");
|
|
2613
|
+
if (!fs7.existsSync(settingsPath)) return void 0;
|
|
2498
2614
|
try {
|
|
2499
|
-
const settings = JSON.parse(
|
|
2615
|
+
const settings = JSON.parse(fs7.readFileSync(settingsPath, "utf-8"));
|
|
2500
2616
|
const cmd = settings.statusLine?.command;
|
|
2501
2617
|
if (!cmd || !isLegacyStatusline(cmd)) return void 0;
|
|
2502
2618
|
settings.statusLine = {
|
|
2503
2619
|
type: "command",
|
|
2504
2620
|
command: STATUSLINE_CMD2
|
|
2505
2621
|
};
|
|
2506
|
-
|
|
2622
|
+
fs7.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
2507
2623
|
cleanupLegacyScript();
|
|
2508
2624
|
return "[KeepGoing] Migrated statusline to auto-updating command (restart Claude Code to apply)";
|
|
2509
2625
|
} catch {
|
|
@@ -2511,10 +2627,10 @@ function migrateStatusline(wsPath) {
|
|
|
2511
2627
|
}
|
|
2512
2628
|
}
|
|
2513
2629
|
function cleanupLegacyScript() {
|
|
2514
|
-
const legacyScript =
|
|
2515
|
-
if (
|
|
2630
|
+
const legacyScript = path10.join(os4.homedir(), ".claude", "keepgoing-statusline.sh");
|
|
2631
|
+
if (fs7.existsSync(legacyScript)) {
|
|
2516
2632
|
try {
|
|
2517
|
-
|
|
2633
|
+
fs7.unlinkSync(legacyScript);
|
|
2518
2634
|
} catch {
|
|
2519
2635
|
}
|
|
2520
2636
|
}
|
|
@@ -2527,17 +2643,17 @@ function registerSetupProject(server, workspacePath) {
|
|
|
2527
2643
|
'Set up KeepGoing hooks and instructions. Use scope "user" for global setup (all projects) or "project" for per-project setup.',
|
|
2528
2644
|
{
|
|
2529
2645
|
sessionHooks: z6.boolean().optional().default(true).describe("Add session hooks to settings.json"),
|
|
2530
|
-
claudeMd: z6.boolean().optional().default(true).describe("Add KeepGoing instructions to
|
|
2531
|
-
scope: z6.enum(["project", "user"]).optional().default("project").describe('Where to write config: "user" for global (~/.claude/), "project" for per-project (.claude/)')
|
|
2646
|
+
claudeMd: z6.boolean().optional().default(true).describe("Add KeepGoing instructions to .claude/rules/keepgoing.md"),
|
|
2647
|
+
scope: z6.enum(["project", "user"]).optional().default("project").describe('Where to write config: "user" for global (~/.claude/), "project" for per-project (.claude/)'),
|
|
2648
|
+
claudeDir: z6.string().optional().describe("Override the Claude config directory for user scope (defaults to CLAUDE_CONFIG_DIR env var or ~/.claude)")
|
|
2532
2649
|
},
|
|
2533
|
-
async ({ sessionHooks, claudeMd, scope }) => {
|
|
2534
|
-
const hasProLicense = process.env.KEEPGOING_PRO_BYPASS === "1" || !!getLicenseForFeature("session-awareness");
|
|
2650
|
+
async ({ sessionHooks, claudeMd, scope, claudeDir }) => {
|
|
2535
2651
|
const result = setupProject({
|
|
2536
2652
|
workspacePath,
|
|
2537
2653
|
scope,
|
|
2538
2654
|
sessionHooks,
|
|
2539
2655
|
claudeMd,
|
|
2540
|
-
|
|
2656
|
+
claudeDir,
|
|
2541
2657
|
statusline: {
|
|
2542
2658
|
isLegacy: isLegacyStatusline,
|
|
2543
2659
|
cleanup: cleanupLegacyScript
|
|
@@ -2912,7 +3028,7 @@ async function handlePrintCurrent() {
|
|
|
2912
3028
|
}
|
|
2913
3029
|
|
|
2914
3030
|
// src/cli/saveCheckpoint.ts
|
|
2915
|
-
import
|
|
3031
|
+
import path11 from "path";
|
|
2916
3032
|
async function handleSaveCheckpoint() {
|
|
2917
3033
|
const wsPath = resolveWsPath();
|
|
2918
3034
|
const reader = new KeepGoingReader(wsPath);
|
|
@@ -2940,9 +3056,9 @@ async function handleSaveCheckpoint() {
|
|
|
2940
3056
|
sessionStartTime: lastSession?.timestamp ?? now,
|
|
2941
3057
|
lastActivityTime: now
|
|
2942
3058
|
});
|
|
2943
|
-
const summary = buildSmartSummary(events) ?? `Worked on ${touchedFiles.slice(0, 5).map((f) =>
|
|
3059
|
+
const summary = buildSmartSummary(events) ?? `Worked on ${touchedFiles.slice(0, 5).map((f) => path11.basename(f)).join(", ")}`;
|
|
2944
3060
|
const nextStep = buildSmartNextStep(events);
|
|
2945
|
-
const projectName =
|
|
3061
|
+
const projectName = path11.basename(resolveStorageRoot(wsPath));
|
|
2946
3062
|
const sessionId = generateSessionId({ workspaceRoot: wsPath, branch: gitBranch ?? void 0, worktreePath: wsPath });
|
|
2947
3063
|
const checkpoint = createCheckpoint({
|
|
2948
3064
|
summary,
|
|
@@ -2963,16 +3079,19 @@ async function handleSaveCheckpoint() {
|
|
|
2963
3079
|
branch: gitBranch ?? void 0,
|
|
2964
3080
|
updatedAt: checkpoint.timestamp
|
|
2965
3081
|
});
|
|
2966
|
-
if (
|
|
2967
|
-
|
|
2968
|
-
|
|
3082
|
+
if (process.env.KEEPGOING_PRO_BYPASS === "1" || getLicenseForFeature("decisions")) {
|
|
3083
|
+
for (let i = 0; i < commitHashes.length; i++) {
|
|
3084
|
+
const hash = commitHashes[i];
|
|
3085
|
+
const message = commitMessages[i];
|
|
3086
|
+
if (!hash || !message) continue;
|
|
3087
|
+
const files = getFilesChangedInCommit(wsPath, hash);
|
|
2969
3088
|
const detected = tryDetectDecision({
|
|
2970
3089
|
workspacePath: wsPath,
|
|
2971
3090
|
checkpointId: checkpoint.id,
|
|
2972
3091
|
gitBranch,
|
|
2973
|
-
commitHash:
|
|
2974
|
-
commitMessage:
|
|
2975
|
-
filesChanged:
|
|
3092
|
+
commitHash: hash,
|
|
3093
|
+
commitMessage: message,
|
|
3094
|
+
filesChanged: files
|
|
2976
3095
|
});
|
|
2977
3096
|
if (detected) {
|
|
2978
3097
|
console.log(`[KeepGoing] Decision detected: ${detected.category} (${(detected.confidence * 100).toFixed(0)}% confidence)`);
|
|
@@ -2983,6 +3102,126 @@ async function handleSaveCheckpoint() {
|
|
|
2983
3102
|
process.exit(0);
|
|
2984
3103
|
}
|
|
2985
3104
|
|
|
3105
|
+
// src/cli/transcriptUtils.ts
|
|
3106
|
+
import fs8 from "fs";
|
|
3107
|
+
var TAIL_READ_BYTES = 8192;
|
|
3108
|
+
var TOOL_VERB_MAP = {
|
|
3109
|
+
Edit: "editing",
|
|
3110
|
+
MultiEdit: "editing",
|
|
3111
|
+
Write: "editing",
|
|
3112
|
+
Read: "researching",
|
|
3113
|
+
Glob: "researching",
|
|
3114
|
+
Grep: "researching",
|
|
3115
|
+
Bash: "running",
|
|
3116
|
+
Agent: "delegating",
|
|
3117
|
+
WebFetch: "browsing",
|
|
3118
|
+
WebSearch: "browsing",
|
|
3119
|
+
TodoWrite: "planning"
|
|
3120
|
+
};
|
|
3121
|
+
function truncateAtWord(text, max) {
|
|
3122
|
+
if (text.length <= max) return text;
|
|
3123
|
+
const cut = text.slice(0, max);
|
|
3124
|
+
const lastSpace = cut.lastIndexOf(" ");
|
|
3125
|
+
return (lastSpace > max / 2 ? cut.slice(0, lastSpace) : cut.slice(0, max - 1)) + "\u2026";
|
|
3126
|
+
}
|
|
3127
|
+
var FILLER_PREFIX_RE = /^(i want to|can you|please|let['']?s|could you|help me|i need to|i['']d like to|implement the following plan[:\s]*|implement this plan[:\s]*)\s*/i;
|
|
3128
|
+
var MARKDOWN_HEADING_RE = /^#+\s+/;
|
|
3129
|
+
function extractTextFromContent(content) {
|
|
3130
|
+
if (typeof content === "string") return content;
|
|
3131
|
+
if (!Array.isArray(content)) return "";
|
|
3132
|
+
let text = "";
|
|
3133
|
+
for (const part of content) {
|
|
3134
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
3135
|
+
text += part.text + " ";
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
return text.trim();
|
|
3139
|
+
}
|
|
3140
|
+
function isUserEntry(entry) {
|
|
3141
|
+
return entry.type === "user" && entry.message?.role === "user";
|
|
3142
|
+
}
|
|
3143
|
+
function getToolUseFromEntry(entry) {
|
|
3144
|
+
const content = entry.message?.content;
|
|
3145
|
+
if (!Array.isArray(content)) return null;
|
|
3146
|
+
for (const part of content.slice().reverse()) {
|
|
3147
|
+
if (part.type === "tool_use" && typeof part.name === "string") {
|
|
3148
|
+
return part.name;
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
return null;
|
|
3152
|
+
}
|
|
3153
|
+
function isAssistantEntry(entry) {
|
|
3154
|
+
return entry.message?.role === "assistant";
|
|
3155
|
+
}
|
|
3156
|
+
function extractSessionLabel(transcriptPath) {
|
|
3157
|
+
if (!transcriptPath || !fs8.existsSync(transcriptPath)) return null;
|
|
3158
|
+
try {
|
|
3159
|
+
const raw = fs8.readFileSync(transcriptPath, "utf-8");
|
|
3160
|
+
for (const line of raw.split("\n")) {
|
|
3161
|
+
const trimmed = line.trim();
|
|
3162
|
+
if (!trimmed) continue;
|
|
3163
|
+
let entry;
|
|
3164
|
+
try {
|
|
3165
|
+
entry = JSON.parse(trimmed);
|
|
3166
|
+
} catch {
|
|
3167
|
+
continue;
|
|
3168
|
+
}
|
|
3169
|
+
if (!isUserEntry(entry)) continue;
|
|
3170
|
+
let text = extractTextFromContent(entry.message?.content);
|
|
3171
|
+
if (!text) continue;
|
|
3172
|
+
if (text.startsWith("[") || /^<[a-z][\w-]*>/.test(text)) continue;
|
|
3173
|
+
text = text.replace(/@[\w./\-]+/g, "").trim();
|
|
3174
|
+
text = text.replace(FILLER_PREFIX_RE, "").trim();
|
|
3175
|
+
text = text.replace(MARKDOWN_HEADING_RE, "").trim();
|
|
3176
|
+
text = text.replace(/\s+/g, " ").trim();
|
|
3177
|
+
if (text.length < 20) continue;
|
|
3178
|
+
if (text.length > 80) {
|
|
3179
|
+
text = text.slice(0, 80);
|
|
3180
|
+
}
|
|
3181
|
+
return text;
|
|
3182
|
+
}
|
|
3183
|
+
} catch {
|
|
3184
|
+
}
|
|
3185
|
+
return null;
|
|
3186
|
+
}
|
|
3187
|
+
function extractCurrentAction(transcriptPath) {
|
|
3188
|
+
if (!transcriptPath || !fs8.existsSync(transcriptPath)) return null;
|
|
3189
|
+
try {
|
|
3190
|
+
const stat = fs8.statSync(transcriptPath);
|
|
3191
|
+
const fileSize = stat.size;
|
|
3192
|
+
if (fileSize === 0) return null;
|
|
3193
|
+
const readSize = Math.min(fileSize, TAIL_READ_BYTES);
|
|
3194
|
+
const offset = fileSize - readSize;
|
|
3195
|
+
const buf = Buffer.alloc(readSize);
|
|
3196
|
+
const fd = fs8.openSync(transcriptPath, "r");
|
|
3197
|
+
try {
|
|
3198
|
+
fs8.readSync(fd, buf, 0, readSize, offset);
|
|
3199
|
+
} finally {
|
|
3200
|
+
fs8.closeSync(fd);
|
|
3201
|
+
}
|
|
3202
|
+
const tail = buf.toString("utf-8");
|
|
3203
|
+
const lines = tail.split("\n").reverse();
|
|
3204
|
+
for (const line of lines) {
|
|
3205
|
+
const trimmed = line.trim();
|
|
3206
|
+
if (!trimmed) continue;
|
|
3207
|
+
let entry;
|
|
3208
|
+
try {
|
|
3209
|
+
entry = JSON.parse(trimmed);
|
|
3210
|
+
} catch {
|
|
3211
|
+
continue;
|
|
3212
|
+
}
|
|
3213
|
+
if (!isAssistantEntry(entry)) continue;
|
|
3214
|
+
const toolName = getToolUseFromEntry(entry);
|
|
3215
|
+
if (toolName) {
|
|
3216
|
+
return TOOL_VERB_MAP[toolName] ?? "working";
|
|
3217
|
+
}
|
|
3218
|
+
return "done";
|
|
3219
|
+
}
|
|
3220
|
+
} catch {
|
|
3221
|
+
}
|
|
3222
|
+
return null;
|
|
3223
|
+
}
|
|
3224
|
+
|
|
2986
3225
|
// src/cli/updateTask.ts
|
|
2987
3226
|
async function handleUpdateTask() {
|
|
2988
3227
|
const args = process.argv.slice(2);
|
|
@@ -3034,7 +3273,8 @@ async function handleUpdateTaskFromHook() {
|
|
|
3034
3273
|
const writer = new KeepGoingWriter(wsPath);
|
|
3035
3274
|
const existing = writer.readCurrentTasks();
|
|
3036
3275
|
const sessionIdFromHook = hookData.session_id;
|
|
3037
|
-
const
|
|
3276
|
+
const existingSession = sessionIdFromHook ? existing.find((t) => t.sessionId === sessionIdFromHook) : void 0;
|
|
3277
|
+
const cachedBranch = existingSession?.branch;
|
|
3038
3278
|
const branch = cachedBranch ?? getCurrentBranch(wsPath) ?? void 0;
|
|
3039
3279
|
const task = {
|
|
3040
3280
|
taskSummary: fileName ? `${toolName} ${fileName}` : `Used ${toolName}`,
|
|
@@ -3046,6 +3286,10 @@ async function handleUpdateTaskFromHook() {
|
|
|
3046
3286
|
};
|
|
3047
3287
|
const sessionId = hookData.session_id || generateSessionId({ ...task, workspaceRoot: wsPath });
|
|
3048
3288
|
task.sessionId = sessionId;
|
|
3289
|
+
if (!existingSession?.sessionLabel && hookData.transcript_path) {
|
|
3290
|
+
const label = extractSessionLabel(hookData.transcript_path);
|
|
3291
|
+
if (label) task.sessionLabel = label;
|
|
3292
|
+
}
|
|
3049
3293
|
writer.upsertSession(task);
|
|
3050
3294
|
} catch {
|
|
3051
3295
|
}
|
|
@@ -3055,8 +3299,8 @@ async function handleUpdateTaskFromHook() {
|
|
|
3055
3299
|
}
|
|
3056
3300
|
|
|
3057
3301
|
// src/cli/statusline.ts
|
|
3058
|
-
import
|
|
3059
|
-
import
|
|
3302
|
+
import fs9 from "fs";
|
|
3303
|
+
import path12 from "path";
|
|
3060
3304
|
var STDIN_TIMEOUT_MS2 = 3e3;
|
|
3061
3305
|
async function handleStatusline() {
|
|
3062
3306
|
const chunks = [];
|
|
@@ -3070,36 +3314,43 @@ async function handleStatusline() {
|
|
|
3070
3314
|
clearTimeout(timeout);
|
|
3071
3315
|
try {
|
|
3072
3316
|
const raw = Buffer.concat(chunks).toString("utf-8").trim();
|
|
3073
|
-
if (!raw)
|
|
3074
|
-
process.exit(0);
|
|
3075
|
-
}
|
|
3317
|
+
if (!raw) process.exit(0);
|
|
3076
3318
|
const input = JSON.parse(raw);
|
|
3077
3319
|
const dir = input.workspace?.current_dir ?? input.cwd;
|
|
3078
|
-
if (!dir)
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3320
|
+
if (!dir) process.exit(0);
|
|
3321
|
+
const transcriptPath = input.transcript_path;
|
|
3322
|
+
const sessionId = input.session_id;
|
|
3323
|
+
let label = null;
|
|
3324
|
+
if (input.agent?.name) {
|
|
3325
|
+
label = input.agent.name;
|
|
3326
|
+
}
|
|
3327
|
+
if (!label) {
|
|
3328
|
+
try {
|
|
3329
|
+
const gitRoot = findGitRoot(dir);
|
|
3330
|
+
const tasksFile = path12.join(gitRoot, ".keepgoing", "current-tasks.json");
|
|
3331
|
+
if (fs9.existsSync(tasksFile)) {
|
|
3332
|
+
const data = JSON.parse(fs9.readFileSync(tasksFile, "utf-8"));
|
|
3333
|
+
const tasks = pruneStaleTasks(data.tasks ?? []);
|
|
3334
|
+
const match = sessionId ? tasks.find((t) => t.sessionId === sessionId) : void 0;
|
|
3335
|
+
if (match?.sessionLabel) {
|
|
3336
|
+
label = match.sessionLabel;
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
} catch {
|
|
3340
|
+
}
|
|
3091
3341
|
}
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
const summary = task.taskSummary;
|
|
3095
|
-
if (!summary) {
|
|
3096
|
-
process.exit(0);
|
|
3342
|
+
if (!label && transcriptPath) {
|
|
3343
|
+
label = extractSessionLabel(transcriptPath);
|
|
3097
3344
|
}
|
|
3098
|
-
if (
|
|
3099
|
-
|
|
3345
|
+
if (!label) process.exit(0);
|
|
3346
|
+
const action = transcriptPath ? extractCurrentAction(transcriptPath) : null;
|
|
3347
|
+
const budget = action ? 40 : 55;
|
|
3348
|
+
const displayLabel = truncateAtWord(label, budget);
|
|
3349
|
+
if (action) {
|
|
3350
|
+
process.stdout.write(`[KG] ${displayLabel} \xB7 ${action}
|
|
3100
3351
|
`);
|
|
3101
3352
|
} else {
|
|
3102
|
-
process.stdout.write(`[KG] ${
|
|
3353
|
+
process.stdout.write(`[KG] ${displayLabel}
|
|
3103
3354
|
`);
|
|
3104
3355
|
}
|
|
3105
3356
|
} catch {
|
|
@@ -3125,6 +3376,35 @@ async function handleContinueOn() {
|
|
|
3125
3376
|
process.exit(0);
|
|
3126
3377
|
}
|
|
3127
3378
|
|
|
3379
|
+
// src/cli/detectDecisions.ts
|
|
3380
|
+
async function handleDetectDecisions() {
|
|
3381
|
+
const wsPath = resolveWsPath();
|
|
3382
|
+
if (!(process.env.KEEPGOING_PRO_BYPASS === "1" || getLicenseForFeature("decisions"))) {
|
|
3383
|
+
process.exit(0);
|
|
3384
|
+
}
|
|
3385
|
+
const reader = new KeepGoingReader(wsPath);
|
|
3386
|
+
if (!reader.exists()) {
|
|
3387
|
+
process.exit(0);
|
|
3388
|
+
}
|
|
3389
|
+
const gitBranch = getCurrentBranch(wsPath);
|
|
3390
|
+
const headHash = getHeadCommitHash(wsPath);
|
|
3391
|
+
if (!headHash) process.exit(0);
|
|
3392
|
+
const commitMessage = getCommitMessageByHash(wsPath, headHash);
|
|
3393
|
+
if (!commitMessage) process.exit(0);
|
|
3394
|
+
const files = getFilesChangedInCommit(wsPath, headHash);
|
|
3395
|
+
const detected = tryDetectDecision({
|
|
3396
|
+
workspacePath: wsPath,
|
|
3397
|
+
gitBranch,
|
|
3398
|
+
commitHash: headHash,
|
|
3399
|
+
commitMessage,
|
|
3400
|
+
filesChanged: files
|
|
3401
|
+
});
|
|
3402
|
+
if (detected) {
|
|
3403
|
+
console.log(`[KeepGoing] Decision detected: ${detected.category} (${(detected.confidence * 100).toFixed(0)}% confidence)`);
|
|
3404
|
+
}
|
|
3405
|
+
process.exit(0);
|
|
3406
|
+
}
|
|
3407
|
+
|
|
3128
3408
|
// src/index.ts
|
|
3129
3409
|
var CLI_HANDLERS = {
|
|
3130
3410
|
"--print-momentum": handlePrintMomentum,
|
|
@@ -3133,7 +3413,8 @@ var CLI_HANDLERS = {
|
|
|
3133
3413
|
"--update-task-from-hook": handleUpdateTaskFromHook,
|
|
3134
3414
|
"--print-current": handlePrintCurrent,
|
|
3135
3415
|
"--statusline": handleStatusline,
|
|
3136
|
-
"--continue-on": handleContinueOn
|
|
3416
|
+
"--continue-on": handleContinueOn,
|
|
3417
|
+
"--detect-decisions": handleDetectDecisions
|
|
3137
3418
|
};
|
|
3138
3419
|
var flag = process.argv.slice(2).find((a) => a in CLI_HANDLERS);
|
|
3139
3420
|
if (flag) {
|