@keepgoingdev/mcp-server 0.7.1 → 0.8.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 +1050 -616
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -6,6 +6,16 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
6
6
|
|
|
7
7
|
// ../../packages/shared/src/session.ts
|
|
8
8
|
import { randomUUID } from "crypto";
|
|
9
|
+
|
|
10
|
+
// ../../packages/shared/src/sanitize.ts
|
|
11
|
+
var AGENT_TAG_PATTERN = /<\/?(?:teammate-message|system-reminder|command-name|tool_use)\b[^>]*>/g;
|
|
12
|
+
function stripAgentTags(text) {
|
|
13
|
+
if (!text) return "";
|
|
14
|
+
const stripped = text.replace(AGENT_TAG_PATTERN, "");
|
|
15
|
+
return stripped.trim();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ../../packages/shared/src/session.ts
|
|
9
19
|
function generateCheckpointId() {
|
|
10
20
|
return randomUUID();
|
|
11
21
|
}
|
|
@@ -13,7 +23,10 @@ function createCheckpoint(fields) {
|
|
|
13
23
|
return {
|
|
14
24
|
id: generateCheckpointId(),
|
|
15
25
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16
|
-
...fields
|
|
26
|
+
...fields,
|
|
27
|
+
summary: stripAgentTags(fields.summary),
|
|
28
|
+
nextStep: stripAgentTags(fields.nextStep),
|
|
29
|
+
blocker: fields.blocker ? stripAgentTags(fields.blocker) : fields.blocker
|
|
17
30
|
};
|
|
18
31
|
}
|
|
19
32
|
function createDecisionRecord(fields) {
|
|
@@ -78,7 +91,8 @@ function findGitRoot(startPath) {
|
|
|
78
91
|
const toplevel = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
79
92
|
cwd: startPath,
|
|
80
93
|
encoding: "utf-8",
|
|
81
|
-
timeout: 5e3
|
|
94
|
+
timeout: 5e3,
|
|
95
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
82
96
|
}).trim();
|
|
83
97
|
return toplevel || startPath;
|
|
84
98
|
} catch {
|
|
@@ -95,12 +109,14 @@ function resolveStorageRoot(startPath) {
|
|
|
95
109
|
const toplevel = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
96
110
|
cwd: startPath,
|
|
97
111
|
encoding: "utf-8",
|
|
98
|
-
timeout: 5e3
|
|
112
|
+
timeout: 5e3,
|
|
113
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
99
114
|
}).trim();
|
|
100
115
|
const commonDir = execFileSync("git", ["rev-parse", "--git-common-dir"], {
|
|
101
116
|
cwd: startPath,
|
|
102
117
|
encoding: "utf-8",
|
|
103
|
-
timeout: 5e3
|
|
118
|
+
timeout: 5e3,
|
|
119
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
104
120
|
}).trim();
|
|
105
121
|
const absoluteCommonDir = path.resolve(toplevel, commonDir);
|
|
106
122
|
const mainRoot = path.dirname(absoluteCommonDir);
|
|
@@ -703,22 +719,160 @@ function formatContinueOnPrompt(context, options) {
|
|
|
703
719
|
return result;
|
|
704
720
|
}
|
|
705
721
|
|
|
706
|
-
// ../../packages/shared/src/
|
|
707
|
-
import
|
|
708
|
-
import
|
|
709
|
-
import { randomUUID as randomUUID2, createHash } from "crypto";
|
|
722
|
+
// ../../packages/shared/src/reader.ts
|
|
723
|
+
import fs4 from "fs";
|
|
724
|
+
import path6 from "path";
|
|
710
725
|
|
|
711
|
-
// ../../packages/shared/src/
|
|
726
|
+
// ../../packages/shared/src/license.ts
|
|
727
|
+
import crypto from "crypto";
|
|
712
728
|
import fs from "fs";
|
|
713
729
|
import os from "os";
|
|
714
730
|
import path3 from "path";
|
|
715
|
-
var
|
|
716
|
-
var
|
|
731
|
+
var LICENSE_FILE = "license.json";
|
|
732
|
+
var DEVICE_ID_FILE = "device-id";
|
|
733
|
+
function getGlobalLicenseDir() {
|
|
734
|
+
return path3.join(os.homedir(), ".keepgoing");
|
|
735
|
+
}
|
|
736
|
+
function getGlobalLicensePath() {
|
|
737
|
+
return path3.join(getGlobalLicenseDir(), LICENSE_FILE);
|
|
738
|
+
}
|
|
739
|
+
function getDeviceId() {
|
|
740
|
+
const dir = getGlobalLicenseDir();
|
|
741
|
+
const filePath = path3.join(dir, DEVICE_ID_FILE);
|
|
742
|
+
try {
|
|
743
|
+
const existing = fs.readFileSync(filePath, "utf-8").trim();
|
|
744
|
+
if (existing) return existing;
|
|
745
|
+
} catch {
|
|
746
|
+
}
|
|
747
|
+
const id = crypto.randomUUID();
|
|
748
|
+
if (!fs.existsSync(dir)) {
|
|
749
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
750
|
+
}
|
|
751
|
+
fs.writeFileSync(filePath, id, "utf-8");
|
|
752
|
+
return id;
|
|
753
|
+
}
|
|
754
|
+
var DECISION_DETECTION_VARIANT_ID = 1361527;
|
|
755
|
+
var SESSION_AWARENESS_VARIANT_ID = 1366510;
|
|
756
|
+
var TEST_DECISION_DETECTION_VARIANT_ID = 1345647;
|
|
757
|
+
var TEST_SESSION_AWARENESS_VARIANT_ID = 1365992;
|
|
758
|
+
var VARIANT_FEATURE_MAP = {
|
|
759
|
+
[DECISION_DETECTION_VARIANT_ID]: ["decisions"],
|
|
760
|
+
[SESSION_AWARENESS_VARIANT_ID]: ["session-awareness"],
|
|
761
|
+
[TEST_DECISION_DETECTION_VARIANT_ID]: ["decisions"],
|
|
762
|
+
[TEST_SESSION_AWARENESS_VARIANT_ID]: ["session-awareness"]
|
|
763
|
+
// Future bundle: [BUNDLE_VARIANT_ID]: ['decisions', 'session-awareness'],
|
|
764
|
+
};
|
|
765
|
+
var KNOWN_VARIANT_IDS = new Set(Object.keys(VARIANT_FEATURE_MAP).map(Number));
|
|
766
|
+
function getVariantLabel(variantId) {
|
|
767
|
+
const features = VARIANT_FEATURE_MAP[variantId];
|
|
768
|
+
if (!features) return "Unknown Add-on";
|
|
769
|
+
if (features.includes("decisions") && features.includes("session-awareness")) return "Pro Bundle";
|
|
770
|
+
if (features.includes("decisions")) return "Decision Detection";
|
|
771
|
+
if (features.includes("session-awareness")) return "Session Awareness";
|
|
772
|
+
return "Pro Add-on";
|
|
773
|
+
}
|
|
774
|
+
var _cachedStore;
|
|
775
|
+
var _cacheTimestamp = 0;
|
|
776
|
+
var LICENSE_CACHE_TTL_MS = 2e3;
|
|
777
|
+
function readLicenseStore() {
|
|
778
|
+
const now = Date.now();
|
|
779
|
+
if (_cachedStore && now - _cacheTimestamp < LICENSE_CACHE_TTL_MS) {
|
|
780
|
+
return _cachedStore;
|
|
781
|
+
}
|
|
782
|
+
const licensePath = getGlobalLicensePath();
|
|
783
|
+
let store;
|
|
784
|
+
try {
|
|
785
|
+
if (!fs.existsSync(licensePath)) {
|
|
786
|
+
store = { version: 2, licenses: [] };
|
|
787
|
+
} else {
|
|
788
|
+
const raw = fs.readFileSync(licensePath, "utf-8");
|
|
789
|
+
const data = JSON.parse(raw);
|
|
790
|
+
if (data?.version === 2 && Array.isArray(data.licenses)) {
|
|
791
|
+
store = data;
|
|
792
|
+
} else {
|
|
793
|
+
store = { version: 2, licenses: [] };
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
} catch {
|
|
797
|
+
store = { version: 2, licenses: [] };
|
|
798
|
+
}
|
|
799
|
+
_cachedStore = store;
|
|
800
|
+
_cacheTimestamp = now;
|
|
801
|
+
return store;
|
|
802
|
+
}
|
|
803
|
+
function writeLicenseStore(store) {
|
|
804
|
+
const dirPath = getGlobalLicenseDir();
|
|
805
|
+
if (!fs.existsSync(dirPath)) {
|
|
806
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
807
|
+
}
|
|
808
|
+
const licensePath = path3.join(dirPath, LICENSE_FILE);
|
|
809
|
+
fs.writeFileSync(licensePath, JSON.stringify(store, null, 2), "utf-8");
|
|
810
|
+
_cachedStore = store;
|
|
811
|
+
_cacheTimestamp = Date.now();
|
|
812
|
+
}
|
|
813
|
+
function addLicenseEntry(entry) {
|
|
814
|
+
const store = readLicenseStore();
|
|
815
|
+
const idx = store.licenses.findIndex((l) => l.licenseKey === entry.licenseKey);
|
|
816
|
+
if (idx >= 0) {
|
|
817
|
+
store.licenses[idx] = entry;
|
|
818
|
+
} else {
|
|
819
|
+
store.licenses.push(entry);
|
|
820
|
+
}
|
|
821
|
+
writeLicenseStore(store);
|
|
822
|
+
}
|
|
823
|
+
function removeLicenseEntry(licenseKey) {
|
|
824
|
+
const store = readLicenseStore();
|
|
825
|
+
store.licenses = store.licenses.filter((l) => l.licenseKey !== licenseKey);
|
|
826
|
+
writeLicenseStore(store);
|
|
827
|
+
}
|
|
828
|
+
function getActiveLicenses() {
|
|
829
|
+
return readLicenseStore().licenses.filter((l) => l.status === "active");
|
|
830
|
+
}
|
|
831
|
+
function getLicenseForFeature(feature) {
|
|
832
|
+
const active = getActiveLicenses();
|
|
833
|
+
return active.find((l) => {
|
|
834
|
+
const features = VARIANT_FEATURE_MAP[l.variantId];
|
|
835
|
+
return features?.includes(feature);
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
function getAllLicensesNeedingRevalidation() {
|
|
839
|
+
return getActiveLicenses().filter((l) => needsRevalidation(l));
|
|
840
|
+
}
|
|
841
|
+
var REVALIDATION_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
|
|
842
|
+
function needsRevalidation(entry) {
|
|
843
|
+
const lastValidated = new Date(entry.lastValidatedAt).getTime();
|
|
844
|
+
return Date.now() - lastValidated > REVALIDATION_THRESHOLD_MS;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// ../../packages/shared/src/storage.ts
|
|
848
|
+
import fs3 from "fs";
|
|
849
|
+
import path5 from "path";
|
|
850
|
+
import { randomUUID as randomUUID2, createHash } from "crypto";
|
|
851
|
+
|
|
852
|
+
// ../../packages/shared/src/registry.ts
|
|
853
|
+
import fs2 from "fs";
|
|
854
|
+
import os2 from "os";
|
|
855
|
+
import path4 from "path";
|
|
856
|
+
var KEEPGOING_DIR = path4.join(os2.homedir(), ".keepgoing");
|
|
857
|
+
var KNOWN_PROJECTS_FILE = path4.join(KEEPGOING_DIR, "known-projects.json");
|
|
858
|
+
var TRAY_CONFIG_FILE = path4.join(KEEPGOING_DIR, "tray-config.json");
|
|
717
859
|
var STALE_PROJECT_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
860
|
+
function readTrayConfigProjects() {
|
|
861
|
+
try {
|
|
862
|
+
if (fs2.existsSync(TRAY_CONFIG_FILE)) {
|
|
863
|
+
const raw = JSON.parse(fs2.readFileSync(TRAY_CONFIG_FILE, "utf-8"));
|
|
864
|
+
if (raw && Array.isArray(raw.projects)) {
|
|
865
|
+
return raw.projects.filter((p) => typeof p === "string");
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
} catch {
|
|
869
|
+
}
|
|
870
|
+
return [];
|
|
871
|
+
}
|
|
718
872
|
function readKnownProjects() {
|
|
719
873
|
try {
|
|
720
|
-
if (
|
|
721
|
-
const raw = JSON.parse(
|
|
874
|
+
if (fs2.existsSync(KNOWN_PROJECTS_FILE)) {
|
|
875
|
+
const raw = JSON.parse(fs2.readFileSync(KNOWN_PROJECTS_FILE, "utf-8"));
|
|
722
876
|
if (raw && Array.isArray(raw.projects)) {
|
|
723
877
|
return raw;
|
|
724
878
|
}
|
|
@@ -728,18 +882,18 @@ function readKnownProjects() {
|
|
|
728
882
|
return { version: 1, projects: [] };
|
|
729
883
|
}
|
|
730
884
|
function writeKnownProjects(data) {
|
|
731
|
-
if (!
|
|
732
|
-
|
|
885
|
+
if (!fs2.existsSync(KEEPGOING_DIR)) {
|
|
886
|
+
fs2.mkdirSync(KEEPGOING_DIR, { recursive: true });
|
|
733
887
|
}
|
|
734
888
|
const tmpFile = KNOWN_PROJECTS_FILE + ".tmp";
|
|
735
|
-
|
|
736
|
-
|
|
889
|
+
fs2.writeFileSync(tmpFile, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
890
|
+
fs2.renameSync(tmpFile, KNOWN_PROJECTS_FILE);
|
|
737
891
|
}
|
|
738
892
|
function registerProject(projectPath, projectName) {
|
|
739
893
|
try {
|
|
740
894
|
const data = readKnownProjects();
|
|
741
895
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
742
|
-
const name = projectName ||
|
|
896
|
+
const name = projectName || path4.basename(projectPath);
|
|
743
897
|
const existingIdx = data.projects.findIndex((p) => p.path === projectPath);
|
|
744
898
|
if (existingIdx >= 0) {
|
|
745
899
|
data.projects[existingIdx].lastSeen = now;
|
|
@@ -778,23 +932,23 @@ var KeepGoingWriter = class {
|
|
|
778
932
|
currentTasksFilePath;
|
|
779
933
|
constructor(workspacePath) {
|
|
780
934
|
const mainRoot = resolveStorageRoot(workspacePath);
|
|
781
|
-
this.storagePath =
|
|
782
|
-
this.sessionsFilePath =
|
|
783
|
-
this.stateFilePath =
|
|
784
|
-
this.metaFilePath =
|
|
785
|
-
this.currentTasksFilePath =
|
|
935
|
+
this.storagePath = path5.join(mainRoot, STORAGE_DIR);
|
|
936
|
+
this.sessionsFilePath = path5.join(this.storagePath, SESSIONS_FILE);
|
|
937
|
+
this.stateFilePath = path5.join(this.storagePath, STATE_FILE);
|
|
938
|
+
this.metaFilePath = path5.join(this.storagePath, META_FILE);
|
|
939
|
+
this.currentTasksFilePath = path5.join(this.storagePath, CURRENT_TASKS_FILE);
|
|
786
940
|
}
|
|
787
941
|
ensureDir() {
|
|
788
|
-
if (!
|
|
789
|
-
|
|
942
|
+
if (!fs3.existsSync(this.storagePath)) {
|
|
943
|
+
fs3.mkdirSync(this.storagePath, { recursive: true });
|
|
790
944
|
}
|
|
791
945
|
}
|
|
792
946
|
saveCheckpoint(checkpoint, projectName) {
|
|
793
947
|
this.ensureDir();
|
|
794
948
|
let sessionsData;
|
|
795
949
|
try {
|
|
796
|
-
if (
|
|
797
|
-
const raw = JSON.parse(
|
|
950
|
+
if (fs3.existsSync(this.sessionsFilePath)) {
|
|
951
|
+
const raw = JSON.parse(fs3.readFileSync(this.sessionsFilePath, "utf-8"));
|
|
798
952
|
if (Array.isArray(raw)) {
|
|
799
953
|
sessionsData = { version: 1, project: projectName, sessions: raw };
|
|
800
954
|
} else {
|
|
@@ -812,13 +966,13 @@ var KeepGoingWriter = class {
|
|
|
812
966
|
if (sessionsData.sessions.length > MAX_SESSIONS) {
|
|
813
967
|
sessionsData.sessions = sessionsData.sessions.slice(-MAX_SESSIONS);
|
|
814
968
|
}
|
|
815
|
-
|
|
969
|
+
fs3.writeFileSync(this.sessionsFilePath, JSON.stringify(sessionsData, null, 2), "utf-8");
|
|
816
970
|
const state = {
|
|
817
971
|
lastSessionId: checkpoint.id,
|
|
818
972
|
lastKnownBranch: checkpoint.gitBranch,
|
|
819
973
|
lastActivityAt: checkpoint.timestamp
|
|
820
974
|
};
|
|
821
|
-
|
|
975
|
+
fs3.writeFileSync(this.stateFilePath, JSON.stringify(state, null, 2), "utf-8");
|
|
822
976
|
this.updateMeta(checkpoint.timestamp);
|
|
823
977
|
const mainRoot = resolveStorageRoot(this.storagePath);
|
|
824
978
|
registerProject(mainRoot, projectName);
|
|
@@ -826,8 +980,8 @@ var KeepGoingWriter = class {
|
|
|
826
980
|
updateMeta(timestamp) {
|
|
827
981
|
let meta;
|
|
828
982
|
try {
|
|
829
|
-
if (
|
|
830
|
-
meta = JSON.parse(
|
|
983
|
+
if (fs3.existsSync(this.metaFilePath)) {
|
|
984
|
+
meta = JSON.parse(fs3.readFileSync(this.metaFilePath, "utf-8"));
|
|
831
985
|
meta.lastUpdated = timestamp;
|
|
832
986
|
} else {
|
|
833
987
|
meta = {
|
|
@@ -843,7 +997,28 @@ var KeepGoingWriter = class {
|
|
|
843
997
|
lastUpdated: timestamp
|
|
844
998
|
};
|
|
845
999
|
}
|
|
846
|
-
|
|
1000
|
+
fs3.writeFileSync(this.metaFilePath, JSON.stringify(meta, null, 2), "utf-8");
|
|
1001
|
+
}
|
|
1002
|
+
// ---------------------------------------------------------------------------
|
|
1003
|
+
// Activity signals API
|
|
1004
|
+
// ---------------------------------------------------------------------------
|
|
1005
|
+
/**
|
|
1006
|
+
* Writes activity signals to state.json for momentum computation.
|
|
1007
|
+
* Performs a shallow merge: provided fields overwrite existing ones,
|
|
1008
|
+
* fields not provided are preserved.
|
|
1009
|
+
*/
|
|
1010
|
+
writeActivitySignal(signal) {
|
|
1011
|
+
this.ensureDir();
|
|
1012
|
+
let state = {};
|
|
1013
|
+
try {
|
|
1014
|
+
if (fs3.existsSync(this.stateFilePath)) {
|
|
1015
|
+
state = JSON.parse(fs3.readFileSync(this.stateFilePath, "utf-8"));
|
|
1016
|
+
}
|
|
1017
|
+
} catch {
|
|
1018
|
+
state = {};
|
|
1019
|
+
}
|
|
1020
|
+
state.activitySignals = { ...state.activitySignals, ...signal };
|
|
1021
|
+
fs3.writeFileSync(this.stateFilePath, JSON.stringify(state, null, 2), "utf-8");
|
|
847
1022
|
}
|
|
848
1023
|
// ---------------------------------------------------------------------------
|
|
849
1024
|
// Multi-session API
|
|
@@ -851,8 +1026,8 @@ var KeepGoingWriter = class {
|
|
|
851
1026
|
/** Read all current tasks from current-tasks.json. Auto-prunes stale sessions. */
|
|
852
1027
|
readCurrentTasks() {
|
|
853
1028
|
try {
|
|
854
|
-
if (
|
|
855
|
-
const raw = JSON.parse(
|
|
1029
|
+
if (fs3.existsSync(this.currentTasksFilePath)) {
|
|
1030
|
+
const raw = JSON.parse(fs3.readFileSync(this.currentTasksFilePath, "utf-8"));
|
|
856
1031
|
const tasks = Array.isArray(raw) ? raw : raw.tasks ?? [];
|
|
857
1032
|
return this.pruneStale(tasks);
|
|
858
1033
|
}
|
|
@@ -873,6 +1048,8 @@ var KeepGoingWriter = class {
|
|
|
873
1048
|
/** Core upsert logic: merges the update into current-tasks.json and returns the pruned task list. */
|
|
874
1049
|
upsertSessionCore(update) {
|
|
875
1050
|
this.ensureDir();
|
|
1051
|
+
if (update.taskSummary) update.taskSummary = stripAgentTags(update.taskSummary);
|
|
1052
|
+
if (update.sessionLabel) update.sessionLabel = stripAgentTags(update.sessionLabel);
|
|
876
1053
|
const sessionId = update.sessionId || generateSessionId(update);
|
|
877
1054
|
const tasks = this.readAllTasksRaw();
|
|
878
1055
|
const existingIdx = tasks.findIndex((t) => t.sessionId === sessionId);
|
|
@@ -907,8 +1084,8 @@ var KeepGoingWriter = class {
|
|
|
907
1084
|
// ---------------------------------------------------------------------------
|
|
908
1085
|
readAllTasksRaw() {
|
|
909
1086
|
try {
|
|
910
|
-
if (
|
|
911
|
-
const raw = JSON.parse(
|
|
1087
|
+
if (fs3.existsSync(this.currentTasksFilePath)) {
|
|
1088
|
+
const raw = JSON.parse(fs3.readFileSync(this.currentTasksFilePath, "utf-8"));
|
|
912
1089
|
return Array.isArray(raw) ? [...raw] : [...raw.tasks ?? []];
|
|
913
1090
|
}
|
|
914
1091
|
} catch {
|
|
@@ -920,7 +1097,7 @@ var KeepGoingWriter = class {
|
|
|
920
1097
|
}
|
|
921
1098
|
writeTasksFile(tasks) {
|
|
922
1099
|
const data = { version: 1, tasks };
|
|
923
|
-
|
|
1100
|
+
fs3.writeFileSync(this.currentTasksFilePath, JSON.stringify(data, null, 2), "utf-8");
|
|
924
1101
|
}
|
|
925
1102
|
};
|
|
926
1103
|
function generateSessionId(context) {
|
|
@@ -936,54 +1113,334 @@ function generateSessionId(context) {
|
|
|
936
1113
|
return `ses_${hash}`;
|
|
937
1114
|
}
|
|
938
1115
|
|
|
939
|
-
// ../../packages/shared/src/
|
|
940
|
-
var
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1116
|
+
// ../../packages/shared/src/reader.ts
|
|
1117
|
+
var STORAGE_DIR2 = ".keepgoing";
|
|
1118
|
+
var META_FILE2 = "meta.json";
|
|
1119
|
+
var SESSIONS_FILE2 = "sessions.json";
|
|
1120
|
+
var DECISIONS_FILE = "decisions.json";
|
|
1121
|
+
var STATE_FILE2 = "state.json";
|
|
1122
|
+
var CURRENT_TASKS_FILE2 = "current-tasks.json";
|
|
1123
|
+
var KeepGoingReader = class {
|
|
1124
|
+
workspacePath;
|
|
1125
|
+
storagePath;
|
|
1126
|
+
metaFilePath;
|
|
1127
|
+
sessionsFilePath;
|
|
1128
|
+
decisionsFilePath;
|
|
1129
|
+
stateFilePath;
|
|
1130
|
+
currentTasksFilePath;
|
|
1131
|
+
_isWorktree;
|
|
1132
|
+
_cachedBranch = null;
|
|
1133
|
+
// null = not yet resolved
|
|
1134
|
+
constructor(workspacePath) {
|
|
1135
|
+
this.workspacePath = workspacePath;
|
|
1136
|
+
const mainRoot = resolveStorageRoot(workspacePath);
|
|
1137
|
+
this._isWorktree = mainRoot !== workspacePath;
|
|
1138
|
+
this.storagePath = path6.join(mainRoot, STORAGE_DIR2);
|
|
1139
|
+
this.metaFilePath = path6.join(this.storagePath, META_FILE2);
|
|
1140
|
+
this.sessionsFilePath = path6.join(this.storagePath, SESSIONS_FILE2);
|
|
1141
|
+
this.decisionsFilePath = path6.join(this.storagePath, DECISIONS_FILE);
|
|
1142
|
+
this.stateFilePath = path6.join(this.storagePath, STATE_FILE2);
|
|
1143
|
+
this.currentTasksFilePath = path6.join(this.storagePath, CURRENT_TASKS_FILE2);
|
|
1144
|
+
}
|
|
1145
|
+
/** Check if .keepgoing/ directory exists. */
|
|
1146
|
+
exists() {
|
|
1147
|
+
return fs4.existsSync(this.storagePath);
|
|
1148
|
+
}
|
|
1149
|
+
/** Read state.json, returns undefined if missing or corrupt. */
|
|
1150
|
+
getState() {
|
|
1151
|
+
return this.readJsonFile(this.stateFilePath);
|
|
1152
|
+
}
|
|
1153
|
+
/** Read meta.json, returns undefined if missing or corrupt. */
|
|
1154
|
+
getMeta() {
|
|
1155
|
+
return this.readJsonFile(this.metaFilePath);
|
|
1156
|
+
}
|
|
1157
|
+
/**
|
|
1158
|
+
* Read sessions from sessions.json.
|
|
1159
|
+
* Handles both formats:
|
|
1160
|
+
* - Flat array: SessionCheckpoint[] (from ProjectStorage)
|
|
1161
|
+
* - Wrapper object: ProjectSessions (from SessionStorage)
|
|
1162
|
+
*/
|
|
1163
|
+
getSessions() {
|
|
1164
|
+
return this.parseSessions().sessions;
|
|
1165
|
+
}
|
|
1166
|
+
/**
|
|
1167
|
+
* Get the most recent session checkpoint.
|
|
1168
|
+
* Uses state.lastSessionId if available, falls back to last in array.
|
|
1169
|
+
*/
|
|
1170
|
+
getLastSession() {
|
|
1171
|
+
const { sessions, wrapperLastSessionId } = this.parseSessions();
|
|
1172
|
+
if (sessions.length === 0) {
|
|
1173
|
+
return void 0;
|
|
1174
|
+
}
|
|
1175
|
+
const state = this.getState();
|
|
1176
|
+
if (state?.lastSessionId) {
|
|
1177
|
+
const found = sessions.find((s) => s.id === state.lastSessionId);
|
|
1178
|
+
if (found) {
|
|
1179
|
+
return found;
|
|
973
1180
|
}
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1181
|
+
}
|
|
1182
|
+
if (wrapperLastSessionId) {
|
|
1183
|
+
const found = sessions.find((s) => s.id === wrapperLastSessionId);
|
|
1184
|
+
if (found) {
|
|
1185
|
+
return found;
|
|
978
1186
|
}
|
|
979
|
-
groups.get("other").push(msg.trim());
|
|
980
1187
|
}
|
|
1188
|
+
return sessions[sessions.length - 1];
|
|
981
1189
|
}
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1190
|
+
/**
|
|
1191
|
+
* Returns the last N sessions, newest first.
|
|
1192
|
+
*/
|
|
1193
|
+
getRecentSessions(count) {
|
|
1194
|
+
return getRecentSessions(this.getSessions(), count);
|
|
1195
|
+
}
|
|
1196
|
+
/** Read all decisions from decisions.json. */
|
|
1197
|
+
getDecisions() {
|
|
1198
|
+
return this.parseDecisions().decisions;
|
|
1199
|
+
}
|
|
1200
|
+
/** Returns the last N decisions, newest first. */
|
|
1201
|
+
getRecentDecisions(count) {
|
|
1202
|
+
const all = this.getDecisions();
|
|
1203
|
+
return all.slice(-count).reverse();
|
|
1204
|
+
}
|
|
1205
|
+
/** Read the multi-license store from `~/.keepgoing/license.json`. */
|
|
1206
|
+
getLicenseStore() {
|
|
1207
|
+
return readLicenseStore();
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Read all current tasks from current-tasks.json.
|
|
1211
|
+
* Automatically filters out stale finished sessions (> 2 hours).
|
|
1212
|
+
*/
|
|
1213
|
+
getCurrentTasks() {
|
|
1214
|
+
const multiRaw = this.readJsonFile(this.currentTasksFilePath);
|
|
1215
|
+
if (multiRaw) {
|
|
1216
|
+
const tasks = Array.isArray(multiRaw) ? multiRaw : multiRaw.tasks ?? [];
|
|
1217
|
+
return this.pruneStale(tasks);
|
|
1218
|
+
}
|
|
1219
|
+
return [];
|
|
1220
|
+
}
|
|
1221
|
+
/** Get only active sessions (sessionActive=true and within stale threshold). */
|
|
1222
|
+
getActiveTasks() {
|
|
1223
|
+
return this.getCurrentTasks().filter((t) => t.sessionActive);
|
|
1224
|
+
}
|
|
1225
|
+
/** Get a specific session by ID. */
|
|
1226
|
+
getTaskBySessionId(sessionId) {
|
|
1227
|
+
return this.getCurrentTasks().find((t) => t.sessionId === sessionId);
|
|
1228
|
+
}
|
|
1229
|
+
/**
|
|
1230
|
+
* Detect files being edited by multiple sessions simultaneously.
|
|
1231
|
+
* Returns pairs of session IDs and the conflicting file paths.
|
|
1232
|
+
*/
|
|
1233
|
+
detectFileConflicts() {
|
|
1234
|
+
const activeTasks = this.getActiveTasks();
|
|
1235
|
+
if (activeTasks.length < 2) return [];
|
|
1236
|
+
const fileToSessions = /* @__PURE__ */ new Map();
|
|
1237
|
+
for (const task of activeTasks) {
|
|
1238
|
+
if (task.lastFileEdited && task.sessionId) {
|
|
1239
|
+
const existing = fileToSessions.get(task.lastFileEdited) ?? [];
|
|
1240
|
+
existing.push({
|
|
1241
|
+
sessionId: task.sessionId,
|
|
1242
|
+
agentLabel: task.agentLabel,
|
|
1243
|
+
branch: task.branch
|
|
1244
|
+
});
|
|
1245
|
+
fileToSessions.set(task.lastFileEdited, existing);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
const conflicts = [];
|
|
1249
|
+
for (const [file, sessions] of fileToSessions) {
|
|
1250
|
+
if (sessions.length > 1) {
|
|
1251
|
+
conflicts.push({ file, sessions });
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
return conflicts;
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Detect sessions on the same branch (possible duplicate work).
|
|
1258
|
+
*/
|
|
1259
|
+
detectBranchOverlap() {
|
|
1260
|
+
const activeTasks = this.getActiveTasks();
|
|
1261
|
+
if (activeTasks.length < 2) return [];
|
|
1262
|
+
const branchToSessions = /* @__PURE__ */ new Map();
|
|
1263
|
+
for (const task of activeTasks) {
|
|
1264
|
+
if (task.branch && task.sessionId) {
|
|
1265
|
+
const existing = branchToSessions.get(task.branch) ?? [];
|
|
1266
|
+
existing.push({ sessionId: task.sessionId, agentLabel: task.agentLabel });
|
|
1267
|
+
branchToSessions.set(task.branch, existing);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
const overlaps = [];
|
|
1271
|
+
for (const [branch, sessions] of branchToSessions) {
|
|
1272
|
+
if (sessions.length > 1) {
|
|
1273
|
+
overlaps.push({ branch, sessions });
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
return overlaps;
|
|
1277
|
+
}
|
|
1278
|
+
pruneStale(tasks) {
|
|
1279
|
+
return pruneStaleTasks(tasks);
|
|
1280
|
+
}
|
|
1281
|
+
/** Get the last session checkpoint for a specific branch. */
|
|
1282
|
+
getLastSessionForBranch(branch) {
|
|
1283
|
+
const sessions = this.getSessions().filter((s) => s.gitBranch === branch);
|
|
1284
|
+
return sessions.length > 0 ? sessions[sessions.length - 1] : void 0;
|
|
1285
|
+
}
|
|
1286
|
+
/** Returns the last N sessions for a specific branch, newest first. */
|
|
1287
|
+
getRecentSessionsForBranch(branch, count) {
|
|
1288
|
+
const filtered = this.getSessions().filter((s) => s.gitBranch === branch);
|
|
1289
|
+
return filtered.slice(-count).reverse();
|
|
1290
|
+
}
|
|
1291
|
+
/** Returns the last N decisions for a specific branch, newest first. */
|
|
1292
|
+
getRecentDecisionsForBranch(branch, count) {
|
|
1293
|
+
const filtered = this.getDecisions().filter((d) => d.gitBranch === branch);
|
|
1294
|
+
return filtered.slice(-count).reverse();
|
|
1295
|
+
}
|
|
1296
|
+
/** Whether the workspace is inside a git worktree. */
|
|
1297
|
+
get isWorktree() {
|
|
1298
|
+
return this._isWorktree;
|
|
1299
|
+
}
|
|
1300
|
+
/**
|
|
1301
|
+
* Returns the current git branch for this workspace.
|
|
1302
|
+
* Lazily cached: the branch is resolved once per KeepGoingReader instance.
|
|
1303
|
+
*/
|
|
1304
|
+
getCurrentBranch() {
|
|
1305
|
+
if (this._cachedBranch === null) {
|
|
1306
|
+
this._cachedBranch = getCurrentBranch(this.workspacePath);
|
|
1307
|
+
}
|
|
1308
|
+
return this._cachedBranch;
|
|
1309
|
+
}
|
|
1310
|
+
/**
|
|
1311
|
+
* Worktree-aware last session lookup.
|
|
1312
|
+
* In a worktree, scopes to the current branch with fallback to global.
|
|
1313
|
+
* Returns the session and whether it fell back to global.
|
|
1314
|
+
*/
|
|
1315
|
+
getScopedLastSession() {
|
|
1316
|
+
const branch = this.getCurrentBranch();
|
|
1317
|
+
if (this._isWorktree && branch) {
|
|
1318
|
+
const scoped = this.getLastSessionForBranch(branch);
|
|
1319
|
+
if (scoped) return { session: scoped, isFallback: false };
|
|
1320
|
+
return { session: this.getLastSession(), isFallback: true };
|
|
1321
|
+
}
|
|
1322
|
+
return { session: this.getLastSession(), isFallback: false };
|
|
1323
|
+
}
|
|
1324
|
+
/** Worktree-aware recent sessions. Scopes to current branch in a worktree. */
|
|
1325
|
+
getScopedRecentSessions(count) {
|
|
1326
|
+
const branch = this.getCurrentBranch();
|
|
1327
|
+
if (this._isWorktree && branch) {
|
|
1328
|
+
return this.getRecentSessionsForBranch(branch, count);
|
|
1329
|
+
}
|
|
1330
|
+
return this.getRecentSessions(count);
|
|
1331
|
+
}
|
|
1332
|
+
/** Worktree-aware recent decisions. Scopes to current branch in a worktree. */
|
|
1333
|
+
getScopedRecentDecisions(count) {
|
|
1334
|
+
const branch = this.getCurrentBranch();
|
|
1335
|
+
if (this._isWorktree && branch) {
|
|
1336
|
+
return this.getRecentDecisionsForBranch(branch, count);
|
|
1337
|
+
}
|
|
1338
|
+
return this.getRecentDecisions(count);
|
|
1339
|
+
}
|
|
1340
|
+
/**
|
|
1341
|
+
* Resolves branch scope from an explicit `branch` parameter.
|
|
1342
|
+
* Used by tools that accept a `branch` argument (e.g. get_session_history, get_decisions).
|
|
1343
|
+
* - `"all"` returns no filter.
|
|
1344
|
+
* - An explicit branch name uses that.
|
|
1345
|
+
* - `undefined` auto-scopes to the current branch in a worktree, or all branches otherwise.
|
|
1346
|
+
*/
|
|
1347
|
+
resolveBranchScope(branch) {
|
|
1348
|
+
if (branch === "all") {
|
|
1349
|
+
return { effectiveBranch: void 0, scopeLabel: "all branches" };
|
|
1350
|
+
}
|
|
1351
|
+
if (branch) {
|
|
1352
|
+
return { effectiveBranch: branch, scopeLabel: `branch \`${branch}\`` };
|
|
1353
|
+
}
|
|
1354
|
+
const currentBranch = this.getCurrentBranch();
|
|
1355
|
+
if (this._isWorktree && currentBranch) {
|
|
1356
|
+
return { effectiveBranch: currentBranch, scopeLabel: `branch \`${currentBranch}\` (worktree)` };
|
|
1357
|
+
}
|
|
1358
|
+
return { effectiveBranch: void 0, scopeLabel: "all branches" };
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* Parses sessions.json once, returning both the session list
|
|
1362
|
+
* and the optional lastSessionId from a ProjectSessions wrapper.
|
|
1363
|
+
*/
|
|
1364
|
+
parseSessions() {
|
|
1365
|
+
const raw = this.readJsonFile(
|
|
1366
|
+
this.sessionsFilePath
|
|
1367
|
+
);
|
|
1368
|
+
if (!raw) {
|
|
1369
|
+
return { sessions: [] };
|
|
1370
|
+
}
|
|
1371
|
+
if (Array.isArray(raw)) {
|
|
1372
|
+
return { sessions: raw };
|
|
1373
|
+
}
|
|
1374
|
+
return { sessions: raw.sessions ?? [], wrapperLastSessionId: raw.lastSessionId };
|
|
1375
|
+
}
|
|
1376
|
+
parseDecisions() {
|
|
1377
|
+
const raw = this.readJsonFile(this.decisionsFilePath);
|
|
1378
|
+
if (!raw) {
|
|
1379
|
+
return { decisions: [] };
|
|
1380
|
+
}
|
|
1381
|
+
return { decisions: raw.decisions ?? [], lastDecisionId: raw.lastDecisionId };
|
|
1382
|
+
}
|
|
1383
|
+
readJsonFile(filePath) {
|
|
1384
|
+
try {
|
|
1385
|
+
if (!fs4.existsSync(filePath)) {
|
|
1386
|
+
return void 0;
|
|
1387
|
+
}
|
|
1388
|
+
const raw = fs4.readFileSync(filePath, "utf-8");
|
|
1389
|
+
return JSON.parse(raw);
|
|
1390
|
+
} catch {
|
|
1391
|
+
return void 0;
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
};
|
|
1395
|
+
|
|
1396
|
+
// ../../packages/shared/src/smartSummary.ts
|
|
1397
|
+
var PREFIX_VERBS = {
|
|
1398
|
+
feat: "Added",
|
|
1399
|
+
fix: "Fixed",
|
|
1400
|
+
refactor: "Refactored",
|
|
1401
|
+
docs: "Updated docs for",
|
|
1402
|
+
test: "Added tests for",
|
|
1403
|
+
chore: "Updated",
|
|
1404
|
+
style: "Styled",
|
|
1405
|
+
perf: "Optimized",
|
|
1406
|
+
ci: "Updated CI for",
|
|
1407
|
+
build: "Updated build for",
|
|
1408
|
+
revert: "Reverted"
|
|
1409
|
+
};
|
|
1410
|
+
var NOISE_PATTERNS = [
|
|
1411
|
+
"node_modules",
|
|
1412
|
+
"package-lock.json",
|
|
1413
|
+
"yarn.lock",
|
|
1414
|
+
"pnpm-lock.yaml",
|
|
1415
|
+
".gitignore",
|
|
1416
|
+
".DS_Store",
|
|
1417
|
+
"dist/",
|
|
1418
|
+
"out/",
|
|
1419
|
+
"build/"
|
|
1420
|
+
];
|
|
1421
|
+
function categorizeCommits(messages) {
|
|
1422
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1423
|
+
for (const msg of messages) {
|
|
1424
|
+
const match = msg.match(/^(\w+)(?:\([^)]*\))?[!]?:\s*(.+)/);
|
|
1425
|
+
if (match) {
|
|
1426
|
+
const prefix = match[1].toLowerCase();
|
|
1427
|
+
const body = match[2].trim();
|
|
1428
|
+
if (!groups.has(prefix)) {
|
|
1429
|
+
groups.set(prefix, []);
|
|
1430
|
+
}
|
|
1431
|
+
groups.get(prefix).push(body);
|
|
1432
|
+
} else {
|
|
1433
|
+
if (!groups.has("other")) {
|
|
1434
|
+
groups.set("other", []);
|
|
1435
|
+
}
|
|
1436
|
+
groups.get("other").push(msg.trim());
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
return groups;
|
|
1440
|
+
}
|
|
1441
|
+
function inferWorkAreas(files) {
|
|
1442
|
+
const areas = /* @__PURE__ */ new Map();
|
|
1443
|
+
for (const file of files) {
|
|
987
1444
|
if (NOISE_PATTERNS.some((p) => file.includes(p))) {
|
|
988
1445
|
continue;
|
|
989
1446
|
}
|
|
@@ -1098,131 +1555,222 @@ function capitalize(s) {
|
|
|
1098
1555
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
1099
1556
|
}
|
|
1100
1557
|
|
|
1101
|
-
// ../../packages/shared/src/
|
|
1102
|
-
import
|
|
1103
|
-
import
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
|
|
1558
|
+
// ../../packages/shared/src/contextSnapshot.ts
|
|
1559
|
+
import fs5 from "fs";
|
|
1560
|
+
import path7 from "path";
|
|
1561
|
+
function formatCompactRelativeTime(date) {
|
|
1562
|
+
const diffMs = Date.now() - date.getTime();
|
|
1563
|
+
if (isNaN(diffMs)) return "?";
|
|
1564
|
+
if (diffMs < 0) return "now";
|
|
1565
|
+
const seconds = Math.floor(diffMs / 1e3);
|
|
1566
|
+
const minutes = Math.floor(seconds / 60);
|
|
1567
|
+
const hours = Math.floor(minutes / 60);
|
|
1568
|
+
const days = Math.floor(hours / 24);
|
|
1569
|
+
const weeks = Math.floor(days / 7);
|
|
1570
|
+
if (seconds < 60) return "now";
|
|
1571
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
1572
|
+
if (hours < 24) return `${hours}h ago`;
|
|
1573
|
+
if (days < 7) return `${days}d ago`;
|
|
1574
|
+
return `${weeks}w ago`;
|
|
1117
1575
|
}
|
|
1118
|
-
function
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1576
|
+
function computeMomentum(timestamp, signals) {
|
|
1577
|
+
if (signals) {
|
|
1578
|
+
if (signals.lastGitOpAt) {
|
|
1579
|
+
const gitOpDiffMs = Date.now() - new Date(signals.lastGitOpAt).getTime();
|
|
1580
|
+
if (!isNaN(gitOpDiffMs) && gitOpDiffMs >= 0 && gitOpDiffMs < 30 * 60 * 1e3) {
|
|
1581
|
+
return "hot";
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
if (signals.recentCommitCount !== void 0 && signals.recentCommitCount >= 2) {
|
|
1585
|
+
return "hot";
|
|
1586
|
+
}
|
|
1125
1587
|
}
|
|
1126
|
-
const
|
|
1127
|
-
if (
|
|
1128
|
-
|
|
1588
|
+
const diffMs = Date.now() - new Date(timestamp).getTime();
|
|
1589
|
+
if (isNaN(diffMs) || diffMs < 0) return "hot";
|
|
1590
|
+
const minutes = diffMs / (1e3 * 60);
|
|
1591
|
+
if (minutes < 30) return "hot";
|
|
1592
|
+
if (minutes < 240) return "warm";
|
|
1593
|
+
return "cold";
|
|
1594
|
+
}
|
|
1595
|
+
function inferFocusFromBranch2(branch) {
|
|
1596
|
+
if (!branch || branch === "main" || branch === "master" || branch === "develop" || branch === "HEAD") {
|
|
1597
|
+
return void 0;
|
|
1129
1598
|
}
|
|
1130
|
-
|
|
1131
|
-
|
|
1599
|
+
const prefixPattern = /^(?:feature|feat|fix|bugfix|hotfix|chore|refactor|docs|test|ci)\//i;
|
|
1600
|
+
const isFix = /^(?:fix|bugfix|hotfix)\//i.test(branch);
|
|
1601
|
+
const stripped = branch.replace(prefixPattern, "");
|
|
1602
|
+
const cleaned = stripped.replace(/[-_/]/g, " ").replace(/^\d+\s*/, "").trim();
|
|
1603
|
+
if (!cleaned) return void 0;
|
|
1604
|
+
return isFix ? `${cleaned} fix` : cleaned;
|
|
1132
1605
|
}
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
return "Pro Add-on";
|
|
1606
|
+
function cleanCommitMessage(message, maxLen = 60) {
|
|
1607
|
+
if (!message) return "";
|
|
1608
|
+
const match = message.match(/^(?:\w+)(?:\([^)]*\))?[!]?:\s*(.+)/);
|
|
1609
|
+
const body = match ? match[1].trim() : message.trim();
|
|
1610
|
+
if (!body) return "";
|
|
1611
|
+
const capitalized = body.charAt(0).toUpperCase() + body.slice(1);
|
|
1612
|
+
if (capitalized.length <= maxLen) return capitalized;
|
|
1613
|
+
return capitalized.slice(0, maxLen - 3) + "...";
|
|
1614
|
+
}
|
|
1615
|
+
function buildDoing(checkpoint, branch, recentCommitMessages) {
|
|
1616
|
+
if (checkpoint?.summary) return checkpoint.summary;
|
|
1617
|
+
const branchFocus = inferFocusFromBranch2(branch ?? checkpoint?.gitBranch);
|
|
1618
|
+
if (branchFocus) return branchFocus;
|
|
1619
|
+
if (recentCommitMessages && recentCommitMessages.length > 0) {
|
|
1620
|
+
const cleaned = cleanCommitMessage(recentCommitMessages[0]);
|
|
1621
|
+
if (cleaned) return cleaned;
|
|
1622
|
+
}
|
|
1623
|
+
return "Unknown";
|
|
1152
1624
|
}
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
if (_cachedStore && now - _cacheTimestamp < LICENSE_CACHE_TTL_MS) {
|
|
1159
|
-
return _cachedStore;
|
|
1625
|
+
function buildWhere(checkpoint, branch) {
|
|
1626
|
+
const effectiveBranch = branch ?? checkpoint?.gitBranch;
|
|
1627
|
+
const parts = [];
|
|
1628
|
+
if (effectiveBranch) {
|
|
1629
|
+
parts.push(effectiveBranch);
|
|
1160
1630
|
}
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1631
|
+
if (checkpoint?.touchedFiles && checkpoint.touchedFiles.length > 0) {
|
|
1632
|
+
const fileNames = checkpoint.touchedFiles.slice(0, 2).map((f) => {
|
|
1633
|
+
const segments = f.replace(/\\/g, "/").split("/");
|
|
1634
|
+
return segments[segments.length - 1];
|
|
1635
|
+
});
|
|
1636
|
+
parts.push(fileNames.join(", "));
|
|
1637
|
+
}
|
|
1638
|
+
return parts.join(" \xB7 ") || "unknown";
|
|
1639
|
+
}
|
|
1640
|
+
function detectFocusArea(files) {
|
|
1641
|
+
if (!files || files.length === 0) return void 0;
|
|
1642
|
+
const areas = inferWorkAreas(files);
|
|
1643
|
+
if (areas.length === 0) return void 0;
|
|
1644
|
+
if (areas.length === 1) return areas[0];
|
|
1645
|
+
const topArea = areas[0];
|
|
1646
|
+
const areaCounts = /* @__PURE__ */ new Map();
|
|
1647
|
+
for (const file of files) {
|
|
1648
|
+
const parts = file.split("/").filter(Boolean);
|
|
1649
|
+
let area;
|
|
1650
|
+
if (parts.length <= 1) {
|
|
1651
|
+
area = "root";
|
|
1652
|
+
} else if (parts[0] === "apps" || parts[0] === "packages") {
|
|
1653
|
+
area = parts.length > 1 ? parts[1] : parts[0];
|
|
1654
|
+
} else if (parts[0] === "src") {
|
|
1655
|
+
area = parts.length > 1 ? parts[1] : "src";
|
|
1166
1656
|
} else {
|
|
1167
|
-
|
|
1168
|
-
const data = JSON.parse(raw);
|
|
1169
|
-
if (data?.version === 2 && Array.isArray(data.licenses)) {
|
|
1170
|
-
store = data;
|
|
1171
|
-
} else {
|
|
1172
|
-
store = { version: 2, licenses: [] };
|
|
1173
|
-
}
|
|
1657
|
+
area = parts[0];
|
|
1174
1658
|
}
|
|
1175
|
-
|
|
1176
|
-
store = { version: 2, licenses: [] };
|
|
1659
|
+
areaCounts.set(area, (areaCounts.get(area) ?? 0) + 1);
|
|
1177
1660
|
}
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
const dirPath = getGlobalLicenseDir();
|
|
1184
|
-
if (!fs3.existsSync(dirPath)) {
|
|
1185
|
-
fs3.mkdirSync(dirPath, { recursive: true });
|
|
1661
|
+
let topCount = 0;
|
|
1662
|
+
for (const [areaKey, count] of areaCounts) {
|
|
1663
|
+
if (topArea.toLowerCase().includes(areaKey.toLowerCase()) || areaKey.toLowerCase().includes(topArea.toLowerCase().split(" ")[0])) {
|
|
1664
|
+
topCount = Math.max(topCount, count);
|
|
1665
|
+
}
|
|
1186
1666
|
}
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
_cachedStore = store;
|
|
1190
|
-
_cacheTimestamp = Date.now();
|
|
1191
|
-
}
|
|
1192
|
-
function addLicenseEntry(entry) {
|
|
1193
|
-
const store = readLicenseStore();
|
|
1194
|
-
const idx = store.licenses.findIndex((l) => l.licenseKey === entry.licenseKey);
|
|
1195
|
-
if (idx >= 0) {
|
|
1196
|
-
store.licenses[idx] = entry;
|
|
1197
|
-
} else {
|
|
1198
|
-
store.licenses.push(entry);
|
|
1667
|
+
if (topCount === 0) {
|
|
1668
|
+
topCount = Math.max(...areaCounts.values());
|
|
1199
1669
|
}
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
const store = readLicenseStore();
|
|
1204
|
-
store.licenses = store.licenses.filter((l) => l.licenseKey !== licenseKey);
|
|
1205
|
-
writeLicenseStore(store);
|
|
1206
|
-
}
|
|
1207
|
-
function getActiveLicenses() {
|
|
1208
|
-
return readLicenseStore().licenses.filter((l) => l.status === "active");
|
|
1670
|
+
const ratio = topCount / files.length;
|
|
1671
|
+
if (ratio >= 0.6) return topArea;
|
|
1672
|
+
return void 0;
|
|
1209
1673
|
}
|
|
1210
|
-
function
|
|
1211
|
-
const
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1674
|
+
function generateContextSnapshot(projectRoot) {
|
|
1675
|
+
const reader = new KeepGoingReader(projectRoot);
|
|
1676
|
+
if (!reader.exists()) return null;
|
|
1677
|
+
const lastSession = reader.getLastSession();
|
|
1678
|
+
const state = reader.getState();
|
|
1679
|
+
if (!lastSession && !state) return null;
|
|
1680
|
+
const timestamp = state?.lastActivityAt ?? lastSession?.timestamp;
|
|
1681
|
+
if (!timestamp) return null;
|
|
1682
|
+
const branch = state?.lastKnownBranch ?? lastSession?.gitBranch;
|
|
1683
|
+
let activeAgents = 0;
|
|
1684
|
+
try {
|
|
1685
|
+
const activeTasks = reader.getActiveTasks();
|
|
1686
|
+
activeAgents = activeTasks.length;
|
|
1687
|
+
} catch {
|
|
1688
|
+
}
|
|
1689
|
+
const doing = buildDoing(lastSession, branch);
|
|
1690
|
+
const next = lastSession?.nextStep ?? "";
|
|
1691
|
+
const where = buildWhere(lastSession, branch);
|
|
1692
|
+
const when = formatCompactRelativeTime(new Date(timestamp));
|
|
1693
|
+
const momentum = computeMomentum(timestamp, state?.activitySignals);
|
|
1694
|
+
const blocker = lastSession?.blocker;
|
|
1695
|
+
const snapshot = {
|
|
1696
|
+
doing,
|
|
1697
|
+
next,
|
|
1698
|
+
where,
|
|
1699
|
+
when,
|
|
1700
|
+
momentum,
|
|
1701
|
+
lastActivityAt: timestamp
|
|
1702
|
+
};
|
|
1703
|
+
if (blocker) snapshot.blocker = blocker;
|
|
1704
|
+
if (activeAgents > 0) snapshot.activeAgents = activeAgents;
|
|
1705
|
+
const focusArea = detectFocusArea(lastSession?.touchedFiles ?? []);
|
|
1706
|
+
if (focusArea) snapshot.focusArea = focusArea;
|
|
1707
|
+
return snapshot;
|
|
1708
|
+
}
|
|
1709
|
+
function formatCrossProjectLine(entry) {
|
|
1710
|
+
const s = entry.snapshot;
|
|
1711
|
+
const icon = s.momentum === "hot" ? "\u26A1" : s.momentum === "warm" ? "\u{1F536}" : "\u{1F4A4}";
|
|
1712
|
+
const parts = [];
|
|
1713
|
+
parts.push(`${icon} ${entry.name}: ${s.doing}`);
|
|
1714
|
+
if (s.next) {
|
|
1715
|
+
parts.push(`\u2192 ${s.next}`);
|
|
1716
|
+
}
|
|
1717
|
+
let line = parts.join(" ");
|
|
1718
|
+
line += ` (${s.when})`;
|
|
1719
|
+
if (s.blocker) {
|
|
1720
|
+
line += ` \u26D4 ${s.blocker}`;
|
|
1721
|
+
}
|
|
1722
|
+
return line;
|
|
1723
|
+
}
|
|
1724
|
+
var MOMENTUM_RANK = { hot: 0, warm: 1, cold: 2 };
|
|
1725
|
+
function generateCrossProjectSummary() {
|
|
1726
|
+
const registry = readKnownProjects();
|
|
1727
|
+
const trayPaths = readTrayConfigProjects();
|
|
1728
|
+
const seenPaths = new Set(registry.projects.map((p) => p.path));
|
|
1729
|
+
const allEntries = [...registry.projects];
|
|
1730
|
+
for (const tp of trayPaths) {
|
|
1731
|
+
if (!seenPaths.has(tp)) {
|
|
1732
|
+
seenPaths.add(tp);
|
|
1733
|
+
allEntries.push({ path: tp, name: path7.basename(tp) });
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
const projects = [];
|
|
1737
|
+
const seenRoots = /* @__PURE__ */ new Set();
|
|
1738
|
+
for (const entry of allEntries) {
|
|
1739
|
+
if (!fs5.existsSync(entry.path)) continue;
|
|
1740
|
+
const snapshot = generateContextSnapshot(entry.path);
|
|
1741
|
+
if (!snapshot) continue;
|
|
1742
|
+
let resolvedRoot;
|
|
1743
|
+
try {
|
|
1744
|
+
resolvedRoot = fs5.realpathSync(entry.path);
|
|
1745
|
+
} catch {
|
|
1746
|
+
resolvedRoot = entry.path;
|
|
1747
|
+
}
|
|
1748
|
+
if (seenRoots.has(resolvedRoot)) continue;
|
|
1749
|
+
seenRoots.add(resolvedRoot);
|
|
1750
|
+
projects.push({
|
|
1751
|
+
name: entry.name,
|
|
1752
|
+
path: entry.path,
|
|
1753
|
+
snapshot
|
|
1754
|
+
});
|
|
1755
|
+
}
|
|
1756
|
+
projects.sort((a, b) => {
|
|
1757
|
+
const rankA = MOMENTUM_RANK[a.snapshot.momentum ?? "cold"];
|
|
1758
|
+
const rankB = MOMENTUM_RANK[b.snapshot.momentum ?? "cold"];
|
|
1759
|
+
if (rankA !== rankB) return rankA - rankB;
|
|
1760
|
+
const timeA = a.snapshot.lastActivityAt ? new Date(a.snapshot.lastActivityAt).getTime() : 0;
|
|
1761
|
+
const timeB = b.snapshot.lastActivityAt ? new Date(b.snapshot.lastActivityAt).getTime() : 0;
|
|
1762
|
+
return timeB - timeA;
|
|
1215
1763
|
});
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
}
|
|
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;
|
|
1764
|
+
return {
|
|
1765
|
+
projects,
|
|
1766
|
+
generated: (/* @__PURE__ */ new Date()).toISOString()
|
|
1767
|
+
};
|
|
1224
1768
|
}
|
|
1225
1769
|
|
|
1770
|
+
// ../../packages/shared/src/decisionStorage.ts
|
|
1771
|
+
import fs6 from "fs";
|
|
1772
|
+
import path8 from "path";
|
|
1773
|
+
|
|
1226
1774
|
// ../../packages/shared/src/featureGate.ts
|
|
1227
1775
|
var DefaultFeatureGate = class {
|
|
1228
1776
|
isEnabled(_feature) {
|
|
@@ -1235,31 +1783,31 @@ function isDecisionsEnabled() {
|
|
|
1235
1783
|
}
|
|
1236
1784
|
|
|
1237
1785
|
// ../../packages/shared/src/decisionStorage.ts
|
|
1238
|
-
var
|
|
1239
|
-
var
|
|
1786
|
+
var STORAGE_DIR3 = ".keepgoing";
|
|
1787
|
+
var DECISIONS_FILE2 = "decisions.json";
|
|
1240
1788
|
var MAX_DECISIONS = 100;
|
|
1241
1789
|
var DecisionStorage = class {
|
|
1242
1790
|
storagePath;
|
|
1243
1791
|
decisionsFilePath;
|
|
1244
1792
|
constructor(workspacePath) {
|
|
1245
1793
|
const mainRoot = resolveStorageRoot(workspacePath);
|
|
1246
|
-
this.storagePath =
|
|
1247
|
-
this.decisionsFilePath =
|
|
1794
|
+
this.storagePath = path8.join(mainRoot, STORAGE_DIR3);
|
|
1795
|
+
this.decisionsFilePath = path8.join(this.storagePath, DECISIONS_FILE2);
|
|
1248
1796
|
}
|
|
1249
1797
|
ensureStorageDir() {
|
|
1250
|
-
if (!
|
|
1251
|
-
|
|
1798
|
+
if (!fs6.existsSync(this.storagePath)) {
|
|
1799
|
+
fs6.mkdirSync(this.storagePath, { recursive: true });
|
|
1252
1800
|
}
|
|
1253
1801
|
}
|
|
1254
1802
|
getProjectName() {
|
|
1255
|
-
return
|
|
1803
|
+
return path8.basename(path8.dirname(this.storagePath));
|
|
1256
1804
|
}
|
|
1257
1805
|
load() {
|
|
1258
1806
|
try {
|
|
1259
|
-
if (!
|
|
1807
|
+
if (!fs6.existsSync(this.decisionsFilePath)) {
|
|
1260
1808
|
return createEmptyProjectDecisions(this.getProjectName());
|
|
1261
1809
|
}
|
|
1262
|
-
const raw =
|
|
1810
|
+
const raw = fs6.readFileSync(this.decisionsFilePath, "utf-8");
|
|
1263
1811
|
const data = JSON.parse(raw);
|
|
1264
1812
|
return data;
|
|
1265
1813
|
} catch {
|
|
@@ -1269,7 +1817,7 @@ var DecisionStorage = class {
|
|
|
1269
1817
|
save(decisions) {
|
|
1270
1818
|
this.ensureStorageDir();
|
|
1271
1819
|
const content = JSON.stringify(decisions, null, 2);
|
|
1272
|
-
|
|
1820
|
+
fs6.writeFileSync(this.decisionsFilePath, content, "utf-8");
|
|
1273
1821
|
}
|
|
1274
1822
|
/**
|
|
1275
1823
|
* Save a decision record as a draft. Always persists regardless of Pro
|
|
@@ -1442,369 +1990,87 @@ function classifyCommit(commit) {
|
|
|
1442
1990
|
const matchedTypes = [];
|
|
1443
1991
|
if (type && HIGH_SIGNAL_TYPES.has(type)) {
|
|
1444
1992
|
matchedTypes.push(`type:${type}`);
|
|
1445
|
-
confidence += 0.35;
|
|
1446
|
-
reasons.push(`conventional commit type '${type}' is high-signal`);
|
|
1447
|
-
}
|
|
1448
|
-
if (scope && HIGH_SIGNAL_TYPES.has(scope)) {
|
|
1449
|
-
matchedTypes.push(`scope:${scope}`);
|
|
1450
|
-
confidence += 0.25;
|
|
1451
|
-
reasons.push(`conventional commit scope '${scope}' is high-signal`);
|
|
1452
|
-
}
|
|
1453
|
-
if (parsed.breaking) {
|
|
1454
|
-
confidence += 0.4;
|
|
1455
|
-
reasons.push("breaking change indicated by ! marker");
|
|
1456
|
-
}
|
|
1457
|
-
const matchedPaths = [];
|
|
1458
|
-
let bestTier = null;
|
|
1459
|
-
for (const file of filesChanged) {
|
|
1460
|
-
const pm = matchHighSignalPath(file);
|
|
1461
|
-
if (pm && !matchedPaths.includes(pm.label)) {
|
|
1462
|
-
matchedPaths.push(pm.label);
|
|
1463
|
-
if (bestTier !== "infra") {
|
|
1464
|
-
bestTier = pm.tier;
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
}
|
|
1468
|
-
if (matchedPaths.length > 0) {
|
|
1469
|
-
confidence += bestTier === "infra" ? 0.4 : 0.2;
|
|
1470
|
-
reasons.push(`commit touched: ${matchedPaths.slice(0, 3).join(", ")}`);
|
|
1471
|
-
}
|
|
1472
|
-
if (filesChanged.length > 0 && filesChanged.every((f) => NEGATIVE_PATH_PATTERNS.some((p) => p.test(f)))) {
|
|
1473
|
-
confidence -= 0.5;
|
|
1474
|
-
reasons.push("all changed files are low-signal (lock files, generated code, dist)");
|
|
1475
|
-
}
|
|
1476
|
-
if (filesChanged.length > 0 && filesChanged.every((f) => /favicon/i.test(f) || /\.(svg|png|ico)$/i.test(f))) {
|
|
1477
|
-
confidence -= 0.5;
|
|
1478
|
-
reasons.push("all changed files are UI assets (favicon, SVG, PNG)");
|
|
1479
|
-
}
|
|
1480
|
-
if (filesChanged.length > 0 && filesChanged.every((f) => /\.css$/i.test(f)) && /tailwind/i.test(messageLower)) {
|
|
1481
|
-
confidence -= 0.5;
|
|
1482
|
-
reasons.push("Tailwind generated CSS change (low signal)");
|
|
1483
|
-
}
|
|
1484
|
-
const isDecisionCandidate = confidence >= 0.4;
|
|
1485
|
-
const keywordLabels = matchedKeywords.map((kw) => kw.source.replace(/\\b/g, ""));
|
|
1486
|
-
const category = isDecisionCandidate ? inferCategory(keywordLabels, matchedTypes, matchedPaths) : "unknown";
|
|
1487
|
-
return {
|
|
1488
|
-
isDecisionCandidate,
|
|
1489
|
-
confidence: Math.max(0, Math.min(1, confidence)),
|
|
1490
|
-
reasons,
|
|
1491
|
-
category
|
|
1492
|
-
};
|
|
1493
|
-
}
|
|
1494
|
-
function tryDetectDecision(opts) {
|
|
1495
|
-
if (!isDecisionsEnabled()) {
|
|
1496
|
-
return void 0;
|
|
1497
|
-
}
|
|
1498
|
-
const classification = classifyCommit({
|
|
1499
|
-
message: opts.commitMessage,
|
|
1500
|
-
filesChanged: opts.filesChanged
|
|
1501
|
-
});
|
|
1502
|
-
if (!classification.isDecisionCandidate) {
|
|
1503
|
-
return void 0;
|
|
1504
|
-
}
|
|
1505
|
-
const decision = createDecisionRecord({
|
|
1506
|
-
checkpointId: opts.checkpointId,
|
|
1507
|
-
gitBranch: opts.gitBranch,
|
|
1508
|
-
commitHash: opts.commitHash,
|
|
1509
|
-
commitMessage: opts.commitMessage,
|
|
1510
|
-
filesChanged: opts.filesChanged,
|
|
1511
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1512
|
-
classification
|
|
1513
|
-
});
|
|
1514
|
-
const storage = new DecisionStorage(opts.workspacePath);
|
|
1515
|
-
storage.saveDecision(decision);
|
|
1516
|
-
return {
|
|
1517
|
-
category: classification.category,
|
|
1518
|
-
confidence: classification.confidence
|
|
1519
|
-
};
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
// ../../packages/shared/src/reader.ts
|
|
1523
|
-
import fs5 from "fs";
|
|
1524
|
-
import path7 from "path";
|
|
1525
|
-
var STORAGE_DIR3 = ".keepgoing";
|
|
1526
|
-
var META_FILE2 = "meta.json";
|
|
1527
|
-
var SESSIONS_FILE2 = "sessions.json";
|
|
1528
|
-
var DECISIONS_FILE2 = "decisions.json";
|
|
1529
|
-
var STATE_FILE2 = "state.json";
|
|
1530
|
-
var CURRENT_TASKS_FILE2 = "current-tasks.json";
|
|
1531
|
-
var KeepGoingReader = class {
|
|
1532
|
-
workspacePath;
|
|
1533
|
-
storagePath;
|
|
1534
|
-
metaFilePath;
|
|
1535
|
-
sessionsFilePath;
|
|
1536
|
-
decisionsFilePath;
|
|
1537
|
-
stateFilePath;
|
|
1538
|
-
currentTasksFilePath;
|
|
1539
|
-
_isWorktree;
|
|
1540
|
-
_cachedBranch = null;
|
|
1541
|
-
// null = not yet resolved
|
|
1542
|
-
constructor(workspacePath) {
|
|
1543
|
-
this.workspacePath = workspacePath;
|
|
1544
|
-
const mainRoot = resolveStorageRoot(workspacePath);
|
|
1545
|
-
this._isWorktree = mainRoot !== workspacePath;
|
|
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);
|
|
1552
|
-
}
|
|
1553
|
-
/** Check if .keepgoing/ directory exists. */
|
|
1554
|
-
exists() {
|
|
1555
|
-
return fs5.existsSync(this.storagePath);
|
|
1556
|
-
}
|
|
1557
|
-
/** Read state.json, returns undefined if missing or corrupt. */
|
|
1558
|
-
getState() {
|
|
1559
|
-
return this.readJsonFile(this.stateFilePath);
|
|
1560
|
-
}
|
|
1561
|
-
/** Read meta.json, returns undefined if missing or corrupt. */
|
|
1562
|
-
getMeta() {
|
|
1563
|
-
return this.readJsonFile(this.metaFilePath);
|
|
1564
|
-
}
|
|
1565
|
-
/**
|
|
1566
|
-
* Read sessions from sessions.json.
|
|
1567
|
-
* Handles both formats:
|
|
1568
|
-
* - Flat array: SessionCheckpoint[] (from ProjectStorage)
|
|
1569
|
-
* - Wrapper object: ProjectSessions (from SessionStorage)
|
|
1570
|
-
*/
|
|
1571
|
-
getSessions() {
|
|
1572
|
-
return this.parseSessions().sessions;
|
|
1573
|
-
}
|
|
1574
|
-
/**
|
|
1575
|
-
* Get the most recent session checkpoint.
|
|
1576
|
-
* Uses state.lastSessionId if available, falls back to last in array.
|
|
1577
|
-
*/
|
|
1578
|
-
getLastSession() {
|
|
1579
|
-
const { sessions, wrapperLastSessionId } = this.parseSessions();
|
|
1580
|
-
if (sessions.length === 0) {
|
|
1581
|
-
return void 0;
|
|
1582
|
-
}
|
|
1583
|
-
const state = this.getState();
|
|
1584
|
-
if (state?.lastSessionId) {
|
|
1585
|
-
const found = sessions.find((s) => s.id === state.lastSessionId);
|
|
1586
|
-
if (found) {
|
|
1587
|
-
return found;
|
|
1588
|
-
}
|
|
1589
|
-
}
|
|
1590
|
-
if (wrapperLastSessionId) {
|
|
1591
|
-
const found = sessions.find((s) => s.id === wrapperLastSessionId);
|
|
1592
|
-
if (found) {
|
|
1593
|
-
return found;
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1596
|
-
return sessions[sessions.length - 1];
|
|
1597
|
-
}
|
|
1598
|
-
/**
|
|
1599
|
-
* Returns the last N sessions, newest first.
|
|
1600
|
-
*/
|
|
1601
|
-
getRecentSessions(count) {
|
|
1602
|
-
return getRecentSessions(this.getSessions(), count);
|
|
1603
|
-
}
|
|
1604
|
-
/** Read all decisions from decisions.json. */
|
|
1605
|
-
getDecisions() {
|
|
1606
|
-
return this.parseDecisions().decisions;
|
|
1607
|
-
}
|
|
1608
|
-
/** Returns the last N decisions, newest first. */
|
|
1609
|
-
getRecentDecisions(count) {
|
|
1610
|
-
const all = this.getDecisions();
|
|
1611
|
-
return all.slice(-count).reverse();
|
|
1612
|
-
}
|
|
1613
|
-
/** Read the multi-license store from `~/.keepgoing/license.json`. */
|
|
1614
|
-
getLicenseStore() {
|
|
1615
|
-
return readLicenseStore();
|
|
1616
|
-
}
|
|
1617
|
-
/**
|
|
1618
|
-
* Read all current tasks from current-tasks.json.
|
|
1619
|
-
* Automatically filters out stale finished sessions (> 2 hours).
|
|
1620
|
-
*/
|
|
1621
|
-
getCurrentTasks() {
|
|
1622
|
-
const multiRaw = this.readJsonFile(this.currentTasksFilePath);
|
|
1623
|
-
if (multiRaw) {
|
|
1624
|
-
const tasks = Array.isArray(multiRaw) ? multiRaw : multiRaw.tasks ?? [];
|
|
1625
|
-
return this.pruneStale(tasks);
|
|
1626
|
-
}
|
|
1627
|
-
return [];
|
|
1628
|
-
}
|
|
1629
|
-
/** Get only active sessions (sessionActive=true and within stale threshold). */
|
|
1630
|
-
getActiveTasks() {
|
|
1631
|
-
return this.getCurrentTasks().filter((t) => t.sessionActive);
|
|
1632
|
-
}
|
|
1633
|
-
/** Get a specific session by ID. */
|
|
1634
|
-
getTaskBySessionId(sessionId) {
|
|
1635
|
-
return this.getCurrentTasks().find((t) => t.sessionId === sessionId);
|
|
1636
|
-
}
|
|
1637
|
-
/**
|
|
1638
|
-
* Detect files being edited by multiple sessions simultaneously.
|
|
1639
|
-
* Returns pairs of session IDs and the conflicting file paths.
|
|
1640
|
-
*/
|
|
1641
|
-
detectFileConflicts() {
|
|
1642
|
-
const activeTasks = this.getActiveTasks();
|
|
1643
|
-
if (activeTasks.length < 2) return [];
|
|
1644
|
-
const fileToSessions = /* @__PURE__ */ new Map();
|
|
1645
|
-
for (const task of activeTasks) {
|
|
1646
|
-
if (task.lastFileEdited && task.sessionId) {
|
|
1647
|
-
const existing = fileToSessions.get(task.lastFileEdited) ?? [];
|
|
1648
|
-
existing.push({
|
|
1649
|
-
sessionId: task.sessionId,
|
|
1650
|
-
agentLabel: task.agentLabel,
|
|
1651
|
-
branch: task.branch
|
|
1652
|
-
});
|
|
1653
|
-
fileToSessions.set(task.lastFileEdited, existing);
|
|
1654
|
-
}
|
|
1655
|
-
}
|
|
1656
|
-
const conflicts = [];
|
|
1657
|
-
for (const [file, sessions] of fileToSessions) {
|
|
1658
|
-
if (sessions.length > 1) {
|
|
1659
|
-
conflicts.push({ file, sessions });
|
|
1660
|
-
}
|
|
1661
|
-
}
|
|
1662
|
-
return conflicts;
|
|
1663
|
-
}
|
|
1664
|
-
/**
|
|
1665
|
-
* Detect sessions on the same branch (possible duplicate work).
|
|
1666
|
-
*/
|
|
1667
|
-
detectBranchOverlap() {
|
|
1668
|
-
const activeTasks = this.getActiveTasks();
|
|
1669
|
-
if (activeTasks.length < 2) return [];
|
|
1670
|
-
const branchToSessions = /* @__PURE__ */ new Map();
|
|
1671
|
-
for (const task of activeTasks) {
|
|
1672
|
-
if (task.branch && task.sessionId) {
|
|
1673
|
-
const existing = branchToSessions.get(task.branch) ?? [];
|
|
1674
|
-
existing.push({ sessionId: task.sessionId, agentLabel: task.agentLabel });
|
|
1675
|
-
branchToSessions.set(task.branch, existing);
|
|
1676
|
-
}
|
|
1677
|
-
}
|
|
1678
|
-
const overlaps = [];
|
|
1679
|
-
for (const [branch, sessions] of branchToSessions) {
|
|
1680
|
-
if (sessions.length > 1) {
|
|
1681
|
-
overlaps.push({ branch, sessions });
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
return overlaps;
|
|
1685
|
-
}
|
|
1686
|
-
pruneStale(tasks) {
|
|
1687
|
-
return pruneStaleTasks(tasks);
|
|
1688
|
-
}
|
|
1689
|
-
/** Get the last session checkpoint for a specific branch. */
|
|
1690
|
-
getLastSessionForBranch(branch) {
|
|
1691
|
-
const sessions = this.getSessions().filter((s) => s.gitBranch === branch);
|
|
1692
|
-
return sessions.length > 0 ? sessions[sessions.length - 1] : void 0;
|
|
1693
|
-
}
|
|
1694
|
-
/** Returns the last N sessions for a specific branch, newest first. */
|
|
1695
|
-
getRecentSessionsForBranch(branch, count) {
|
|
1696
|
-
const filtered = this.getSessions().filter((s) => s.gitBranch === branch);
|
|
1697
|
-
return filtered.slice(-count).reverse();
|
|
1698
|
-
}
|
|
1699
|
-
/** Returns the last N decisions for a specific branch, newest first. */
|
|
1700
|
-
getRecentDecisionsForBranch(branch, count) {
|
|
1701
|
-
const filtered = this.getDecisions().filter((d) => d.gitBranch === branch);
|
|
1702
|
-
return filtered.slice(-count).reverse();
|
|
1703
|
-
}
|
|
1704
|
-
/** Whether the workspace is inside a git worktree. */
|
|
1705
|
-
get isWorktree() {
|
|
1706
|
-
return this._isWorktree;
|
|
1707
|
-
}
|
|
1708
|
-
/**
|
|
1709
|
-
* Returns the current git branch for this workspace.
|
|
1710
|
-
* Lazily cached: the branch is resolved once per KeepGoingReader instance.
|
|
1711
|
-
*/
|
|
1712
|
-
getCurrentBranch() {
|
|
1713
|
-
if (this._cachedBranch === null) {
|
|
1714
|
-
this._cachedBranch = getCurrentBranch(this.workspacePath);
|
|
1715
|
-
}
|
|
1716
|
-
return this._cachedBranch;
|
|
1993
|
+
confidence += 0.35;
|
|
1994
|
+
reasons.push(`conventional commit type '${type}' is high-signal`);
|
|
1717
1995
|
}
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
*/
|
|
1723
|
-
getScopedLastSession() {
|
|
1724
|
-
const branch = this.getCurrentBranch();
|
|
1725
|
-
if (this._isWorktree && branch) {
|
|
1726
|
-
const scoped = this.getLastSessionForBranch(branch);
|
|
1727
|
-
if (scoped) return { session: scoped, isFallback: false };
|
|
1728
|
-
return { session: this.getLastSession(), isFallback: true };
|
|
1729
|
-
}
|
|
1730
|
-
return { session: this.getLastSession(), isFallback: false };
|
|
1996
|
+
if (scope && HIGH_SIGNAL_TYPES.has(scope)) {
|
|
1997
|
+
matchedTypes.push(`scope:${scope}`);
|
|
1998
|
+
confidence += 0.25;
|
|
1999
|
+
reasons.push(`conventional commit scope '${scope}' is high-signal`);
|
|
1731
2000
|
}
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
if (this._isWorktree && branch) {
|
|
1736
|
-
return this.getRecentSessionsForBranch(branch, count);
|
|
1737
|
-
}
|
|
1738
|
-
return this.getRecentSessions(count);
|
|
2001
|
+
if (parsed.breaking) {
|
|
2002
|
+
confidence += 0.4;
|
|
2003
|
+
reasons.push("breaking change indicated by ! marker");
|
|
1739
2004
|
}
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
2005
|
+
const matchedPaths = [];
|
|
2006
|
+
let bestTier = null;
|
|
2007
|
+
for (const file of filesChanged) {
|
|
2008
|
+
const pm = matchHighSignalPath(file);
|
|
2009
|
+
if (pm && !matchedPaths.includes(pm.label)) {
|
|
2010
|
+
matchedPaths.push(pm.label);
|
|
2011
|
+
if (bestTier !== "infra") {
|
|
2012
|
+
bestTier = pm.tier;
|
|
2013
|
+
}
|
|
1745
2014
|
}
|
|
1746
|
-
return this.getRecentDecisions(count);
|
|
1747
2015
|
}
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
* - `"all"` returns no filter.
|
|
1752
|
-
* - An explicit branch name uses that.
|
|
1753
|
-
* - `undefined` auto-scopes to the current branch in a worktree, or all branches otherwise.
|
|
1754
|
-
*/
|
|
1755
|
-
resolveBranchScope(branch) {
|
|
1756
|
-
if (branch === "all") {
|
|
1757
|
-
return { effectiveBranch: void 0, scopeLabel: "all branches" };
|
|
1758
|
-
}
|
|
1759
|
-
if (branch) {
|
|
1760
|
-
return { effectiveBranch: branch, scopeLabel: `branch \`${branch}\`` };
|
|
1761
|
-
}
|
|
1762
|
-
const currentBranch = this.getCurrentBranch();
|
|
1763
|
-
if (this._isWorktree && currentBranch) {
|
|
1764
|
-
return { effectiveBranch: currentBranch, scopeLabel: `branch \`${currentBranch}\` (worktree)` };
|
|
1765
|
-
}
|
|
1766
|
-
return { effectiveBranch: void 0, scopeLabel: "all branches" };
|
|
2016
|
+
if (matchedPaths.length > 0) {
|
|
2017
|
+
confidence += bestTier === "infra" ? 0.4 : 0.2;
|
|
2018
|
+
reasons.push(`commit touched: ${matchedPaths.slice(0, 3).join(", ")}`);
|
|
1767
2019
|
}
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
*/
|
|
1772
|
-
parseSessions() {
|
|
1773
|
-
const raw = this.readJsonFile(
|
|
1774
|
-
this.sessionsFilePath
|
|
1775
|
-
);
|
|
1776
|
-
if (!raw) {
|
|
1777
|
-
return { sessions: [] };
|
|
1778
|
-
}
|
|
1779
|
-
if (Array.isArray(raw)) {
|
|
1780
|
-
return { sessions: raw };
|
|
1781
|
-
}
|
|
1782
|
-
return { sessions: raw.sessions ?? [], wrapperLastSessionId: raw.lastSessionId };
|
|
2020
|
+
if (filesChanged.length > 0 && filesChanged.every((f) => NEGATIVE_PATH_PATTERNS.some((p) => p.test(f)))) {
|
|
2021
|
+
confidence -= 0.5;
|
|
2022
|
+
reasons.push("all changed files are low-signal (lock files, generated code, dist)");
|
|
1783
2023
|
}
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
return { decisions: [] };
|
|
1788
|
-
}
|
|
1789
|
-
return { decisions: raw.decisions ?? [], lastDecisionId: raw.lastDecisionId };
|
|
2024
|
+
if (filesChanged.length > 0 && filesChanged.every((f) => /favicon/i.test(f) || /\.(svg|png|ico)$/i.test(f))) {
|
|
2025
|
+
confidence -= 0.5;
|
|
2026
|
+
reasons.push("all changed files are UI assets (favicon, SVG, PNG)");
|
|
1790
2027
|
}
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
return void 0;
|
|
1795
|
-
}
|
|
1796
|
-
const raw = fs5.readFileSync(filePath, "utf-8");
|
|
1797
|
-
return JSON.parse(raw);
|
|
1798
|
-
} catch {
|
|
1799
|
-
return void 0;
|
|
1800
|
-
}
|
|
2028
|
+
if (filesChanged.length > 0 && filesChanged.every((f) => /\.css$/i.test(f)) && /tailwind/i.test(messageLower)) {
|
|
2029
|
+
confidence -= 0.5;
|
|
2030
|
+
reasons.push("Tailwind generated CSS change (low signal)");
|
|
1801
2031
|
}
|
|
1802
|
-
|
|
2032
|
+
const isDecisionCandidate = confidence >= 0.4;
|
|
2033
|
+
const keywordLabels = matchedKeywords.map((kw) => kw.source.replace(/\\b/g, ""));
|
|
2034
|
+
const category = isDecisionCandidate ? inferCategory(keywordLabels, matchedTypes, matchedPaths) : "unknown";
|
|
2035
|
+
return {
|
|
2036
|
+
isDecisionCandidate,
|
|
2037
|
+
confidence: Math.max(0, Math.min(1, confidence)),
|
|
2038
|
+
reasons,
|
|
2039
|
+
category
|
|
2040
|
+
};
|
|
2041
|
+
}
|
|
2042
|
+
function tryDetectDecision(opts) {
|
|
2043
|
+
if (!isDecisionsEnabled()) {
|
|
2044
|
+
return void 0;
|
|
2045
|
+
}
|
|
2046
|
+
const classification = classifyCommit({
|
|
2047
|
+
message: opts.commitMessage,
|
|
2048
|
+
filesChanged: opts.filesChanged
|
|
2049
|
+
});
|
|
2050
|
+
if (!classification.isDecisionCandidate) {
|
|
2051
|
+
return void 0;
|
|
2052
|
+
}
|
|
2053
|
+
const decision = createDecisionRecord({
|
|
2054
|
+
checkpointId: opts.checkpointId,
|
|
2055
|
+
gitBranch: opts.gitBranch,
|
|
2056
|
+
commitHash: opts.commitHash,
|
|
2057
|
+
commitMessage: opts.commitMessage,
|
|
2058
|
+
filesChanged: opts.filesChanged,
|
|
2059
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2060
|
+
classification
|
|
2061
|
+
});
|
|
2062
|
+
const storage = new DecisionStorage(opts.workspacePath);
|
|
2063
|
+
storage.saveDecision(decision);
|
|
2064
|
+
return {
|
|
2065
|
+
category: classification.category,
|
|
2066
|
+
confidence: classification.confidence
|
|
2067
|
+
};
|
|
2068
|
+
}
|
|
1803
2069
|
|
|
1804
2070
|
// ../../packages/shared/src/setup.ts
|
|
1805
|
-
import
|
|
2071
|
+
import fs7 from "fs";
|
|
1806
2072
|
import os3 from "os";
|
|
1807
|
-
import
|
|
2073
|
+
import path9 from "path";
|
|
1808
2074
|
var KEEPGOING_MARKER = "@keepgoingdev/mcp-server";
|
|
1809
2075
|
var SESSION_START_HOOK = {
|
|
1810
2076
|
matcher: "",
|
|
@@ -1842,12 +2108,14 @@ var SESSION_END_HOOK = {
|
|
|
1842
2108
|
}
|
|
1843
2109
|
]
|
|
1844
2110
|
};
|
|
1845
|
-
var KEEPGOING_RULES_VERSION =
|
|
2111
|
+
var KEEPGOING_RULES_VERSION = 2;
|
|
1846
2112
|
var KEEPGOING_RULES_CONTENT = `<!-- @keepgoingdev/mcp-server v${KEEPGOING_RULES_VERSION} -->
|
|
1847
2113
|
## KeepGoing
|
|
1848
2114
|
|
|
2115
|
+
When you see KeepGoing momentum data in your session context (from a SessionStart hook), share a brief welcome with the user that includes: what was last worked on, the suggested next step, and any blockers.
|
|
2116
|
+
|
|
1849
2117
|
After completing a task or meaningful piece of work, call the \`save_checkpoint\` MCP tool with:
|
|
1850
|
-
- \`summary\`: 1-2 sentences. What changed and why
|
|
2118
|
+
- \`summary\`: 1-2 sentences. What changed and why, no file paths, no implementation details (those are captured from git).
|
|
1851
2119
|
- \`nextStep\`: What to do next
|
|
1852
2120
|
- \`blocker\`: Any blocker (if applicable)
|
|
1853
2121
|
`;
|
|
@@ -1857,7 +2125,7 @@ function getRulesFileVersion(content) {
|
|
|
1857
2125
|
}
|
|
1858
2126
|
var STATUSLINE_CMD = "npx -y @keepgoingdev/mcp-server --statusline";
|
|
1859
2127
|
function detectClaudeDir() {
|
|
1860
|
-
return process.env.CLAUDE_CONFIG_DIR ||
|
|
2128
|
+
return process.env.CLAUDE_CONFIG_DIR || path9.join(os3.homedir(), ".claude");
|
|
1861
2129
|
}
|
|
1862
2130
|
function hasKeepGoingHook(hookEntries) {
|
|
1863
2131
|
return hookEntries.some(
|
|
@@ -1869,19 +2137,19 @@ function resolveScopePaths(scope, workspacePath, overrideClaudeDir) {
|
|
|
1869
2137
|
const claudeDir2 = overrideClaudeDir || detectClaudeDir();
|
|
1870
2138
|
return {
|
|
1871
2139
|
claudeDir: claudeDir2,
|
|
1872
|
-
settingsPath:
|
|
1873
|
-
claudeMdPath:
|
|
1874
|
-
rulesPath:
|
|
2140
|
+
settingsPath: path9.join(claudeDir2, "settings.json"),
|
|
2141
|
+
claudeMdPath: path9.join(claudeDir2, "CLAUDE.md"),
|
|
2142
|
+
rulesPath: path9.join(claudeDir2, "rules", "keepgoing.md")
|
|
1875
2143
|
};
|
|
1876
2144
|
}
|
|
1877
|
-
const claudeDir =
|
|
1878
|
-
const dotClaudeMdPath =
|
|
1879
|
-
const rootClaudeMdPath =
|
|
2145
|
+
const claudeDir = path9.join(workspacePath, ".claude");
|
|
2146
|
+
const dotClaudeMdPath = path9.join(workspacePath, ".claude", "CLAUDE.md");
|
|
2147
|
+
const rootClaudeMdPath = path9.join(workspacePath, "CLAUDE.md");
|
|
1880
2148
|
return {
|
|
1881
2149
|
claudeDir,
|
|
1882
|
-
settingsPath:
|
|
1883
|
-
claudeMdPath:
|
|
1884
|
-
rulesPath:
|
|
2150
|
+
settingsPath: path9.join(claudeDir, "settings.json"),
|
|
2151
|
+
claudeMdPath: fs7.existsSync(dotClaudeMdPath) ? dotClaudeMdPath : rootClaudeMdPath,
|
|
2152
|
+
rulesPath: path9.join(workspacePath, ".claude", "rules", "keepgoing.md")
|
|
1885
2153
|
};
|
|
1886
2154
|
}
|
|
1887
2155
|
function writeHooksToSettings(settings) {
|
|
@@ -1921,11 +2189,11 @@ function writeHooksToSettings(settings) {
|
|
|
1921
2189
|
}
|
|
1922
2190
|
function checkHookConflict(scope, workspacePath) {
|
|
1923
2191
|
const otherPaths = resolveScopePaths(scope === "user" ? "project" : "user", workspacePath);
|
|
1924
|
-
if (!
|
|
2192
|
+
if (!fs7.existsSync(otherPaths.settingsPath)) {
|
|
1925
2193
|
return null;
|
|
1926
2194
|
}
|
|
1927
2195
|
try {
|
|
1928
|
-
const otherSettings = JSON.parse(
|
|
2196
|
+
const otherSettings = JSON.parse(fs7.readFileSync(otherPaths.settingsPath, "utf-8"));
|
|
1929
2197
|
const hooks = otherSettings?.hooks;
|
|
1930
2198
|
if (!hooks) return null;
|
|
1931
2199
|
const hasConflict = Array.isArray(hooks.SessionStart) && hasKeepGoingHook(hooks.SessionStart) || Array.isArray(hooks.Stop) && hasKeepGoingHook(hooks.Stop);
|
|
@@ -1954,10 +2222,10 @@ function setupProject(options) {
|
|
|
1954
2222
|
workspacePath,
|
|
1955
2223
|
claudeDirOverride
|
|
1956
2224
|
);
|
|
1957
|
-
const scopeLabel = scope === "user" ?
|
|
2225
|
+
const scopeLabel = scope === "user" ? path9.join("~", path9.relative(os3.homedir(), claudeDir), "settings.json").replace(/\\/g, "/") : ".claude/settings.json";
|
|
1958
2226
|
let settings = {};
|
|
1959
|
-
if (
|
|
1960
|
-
settings = JSON.parse(
|
|
2227
|
+
if (fs7.existsSync(settingsPath)) {
|
|
2228
|
+
settings = JSON.parse(fs7.readFileSync(settingsPath, "utf-8"));
|
|
1961
2229
|
}
|
|
1962
2230
|
let settingsChanged = false;
|
|
1963
2231
|
if (sessionHooks) {
|
|
@@ -1973,7 +2241,7 @@ function setupProject(options) {
|
|
|
1973
2241
|
messages.push(`Warning: ${conflict}`);
|
|
1974
2242
|
}
|
|
1975
2243
|
}
|
|
1976
|
-
|
|
2244
|
+
{
|
|
1977
2245
|
const needsUpdate = settings.statusLine?.command && statusline?.isLegacy?.(settings.statusLine.command);
|
|
1978
2246
|
if (!settings.statusLine || needsUpdate) {
|
|
1979
2247
|
settings.statusLine = {
|
|
@@ -1981,43 +2249,43 @@ function setupProject(options) {
|
|
|
1981
2249
|
command: STATUSLINE_CMD
|
|
1982
2250
|
};
|
|
1983
2251
|
settingsChanged = true;
|
|
1984
|
-
messages.push(needsUpdate ? "Statusline: Migrated to auto-updating npx command" :
|
|
2252
|
+
messages.push(needsUpdate ? "Statusline: Migrated to auto-updating npx command" : `Statusline: Added to ${scopeLabel}`);
|
|
1985
2253
|
} else {
|
|
1986
2254
|
messages.push("Statusline: Already configured in settings, skipped");
|
|
1987
2255
|
}
|
|
1988
2256
|
statusline?.cleanup?.();
|
|
1989
2257
|
}
|
|
1990
2258
|
if (settingsChanged) {
|
|
1991
|
-
if (!
|
|
1992
|
-
|
|
2259
|
+
if (!fs7.existsSync(claudeDir)) {
|
|
2260
|
+
fs7.mkdirSync(claudeDir, { recursive: true });
|
|
1993
2261
|
}
|
|
1994
|
-
|
|
2262
|
+
fs7.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
1995
2263
|
changed = true;
|
|
1996
2264
|
}
|
|
1997
2265
|
if (claudeMd) {
|
|
1998
|
-
const rulesDir =
|
|
1999
|
-
const rulesLabel = scope === "user" ?
|
|
2000
|
-
if (
|
|
2001
|
-
const existing =
|
|
2266
|
+
const rulesDir = path9.dirname(rulesPath);
|
|
2267
|
+
const rulesLabel = scope === "user" ? path9.join(path9.relative(os3.homedir(), path9.dirname(rulesPath)), "keepgoing.md").replace(/\\/g, "/") : ".claude/rules/keepgoing.md";
|
|
2268
|
+
if (fs7.existsSync(rulesPath)) {
|
|
2269
|
+
const existing = fs7.readFileSync(rulesPath, "utf-8");
|
|
2002
2270
|
const existingVersion = getRulesFileVersion(existing);
|
|
2003
2271
|
if (existingVersion === null) {
|
|
2004
2272
|
messages.push(`Rules file: Custom file found at ${rulesLabel}, skipping`);
|
|
2005
2273
|
} else if (existingVersion >= KEEPGOING_RULES_VERSION) {
|
|
2006
2274
|
messages.push(`Rules file: Already up to date (v${existingVersion}), skipped`);
|
|
2007
2275
|
} else {
|
|
2008
|
-
if (!
|
|
2009
|
-
|
|
2276
|
+
if (!fs7.existsSync(rulesDir)) {
|
|
2277
|
+
fs7.mkdirSync(rulesDir, { recursive: true });
|
|
2010
2278
|
}
|
|
2011
|
-
|
|
2279
|
+
fs7.writeFileSync(rulesPath, KEEPGOING_RULES_CONTENT);
|
|
2012
2280
|
changed = true;
|
|
2013
2281
|
messages.push(`Rules file: Updated v${existingVersion} \u2192 v${KEEPGOING_RULES_VERSION} at ${rulesLabel}`);
|
|
2014
2282
|
}
|
|
2015
2283
|
} else {
|
|
2016
|
-
const existingClaudeMd =
|
|
2017
|
-
if (!
|
|
2018
|
-
|
|
2284
|
+
const existingClaudeMd = fs7.existsSync(claudeMdPath) ? fs7.readFileSync(claudeMdPath, "utf-8") : "";
|
|
2285
|
+
if (!fs7.existsSync(rulesDir)) {
|
|
2286
|
+
fs7.mkdirSync(rulesDir, { recursive: true });
|
|
2019
2287
|
}
|
|
2020
|
-
|
|
2288
|
+
fs7.writeFileSync(rulesPath, KEEPGOING_RULES_CONTENT);
|
|
2021
2289
|
changed = true;
|
|
2022
2290
|
if (existingClaudeMd.includes("## KeepGoing")) {
|
|
2023
2291
|
const mdLabel = scope === "user" ? "~/.claude/CLAUDE.md" : "CLAUDE.md";
|
|
@@ -2453,7 +2721,7 @@ function registerGetReentryBriefing(server, reader, workspacePath) {
|
|
|
2453
2721
|
}
|
|
2454
2722
|
|
|
2455
2723
|
// src/tools/saveCheckpoint.ts
|
|
2456
|
-
import
|
|
2724
|
+
import path10 from "path";
|
|
2457
2725
|
import { z as z4 } from "zod";
|
|
2458
2726
|
function registerSaveCheckpoint(server, reader, workspacePath) {
|
|
2459
2727
|
server.tool(
|
|
@@ -2465,11 +2733,14 @@ function registerSaveCheckpoint(server, reader, workspacePath) {
|
|
|
2465
2733
|
blocker: z4.string().optional().describe("Any blocker preventing progress")
|
|
2466
2734
|
},
|
|
2467
2735
|
async ({ summary, nextStep, blocker }) => {
|
|
2736
|
+
summary = stripAgentTags(summary);
|
|
2737
|
+
nextStep = nextStep ? stripAgentTags(nextStep) : nextStep;
|
|
2738
|
+
blocker = blocker ? stripAgentTags(blocker) : blocker;
|
|
2468
2739
|
const lastSession = reader.getLastSession();
|
|
2469
2740
|
const gitBranch = getCurrentBranch(workspacePath);
|
|
2470
2741
|
const touchedFiles = getTouchedFiles(workspacePath);
|
|
2471
2742
|
const commitHashes = getCommitsSince(workspacePath, lastSession?.timestamp);
|
|
2472
|
-
const projectName =
|
|
2743
|
+
const projectName = path10.basename(resolveStorageRoot(workspacePath));
|
|
2473
2744
|
const sessionId = generateSessionId({ workspaceRoot: workspacePath, branch: gitBranch ?? void 0, worktreePath: workspacePath });
|
|
2474
2745
|
const checkpoint = createCheckpoint({
|
|
2475
2746
|
summary,
|
|
@@ -2687,25 +2958,25 @@ function registerGetCurrentTask(server, reader) {
|
|
|
2687
2958
|
import { z as z6 } from "zod";
|
|
2688
2959
|
|
|
2689
2960
|
// src/cli/migrate.ts
|
|
2690
|
-
import
|
|
2961
|
+
import fs8 from "fs";
|
|
2691
2962
|
import os4 from "os";
|
|
2692
|
-
import
|
|
2963
|
+
import path11 from "path";
|
|
2693
2964
|
var STATUSLINE_CMD2 = "npx -y @keepgoingdev/mcp-server --statusline";
|
|
2694
2965
|
function isLegacyStatusline(command) {
|
|
2695
2966
|
return !command.includes("--statusline") && command.includes("keepgoing-statusline");
|
|
2696
2967
|
}
|
|
2697
2968
|
function migrateStatusline(wsPath) {
|
|
2698
|
-
const settingsPath =
|
|
2699
|
-
if (!
|
|
2969
|
+
const settingsPath = path11.join(wsPath, ".claude", "settings.json");
|
|
2970
|
+
if (!fs8.existsSync(settingsPath)) return void 0;
|
|
2700
2971
|
try {
|
|
2701
|
-
const settings = JSON.parse(
|
|
2972
|
+
const settings = JSON.parse(fs8.readFileSync(settingsPath, "utf-8"));
|
|
2702
2973
|
const cmd = settings.statusLine?.command;
|
|
2703
2974
|
if (!cmd || !isLegacyStatusline(cmd)) return void 0;
|
|
2704
2975
|
settings.statusLine = {
|
|
2705
2976
|
type: "command",
|
|
2706
2977
|
command: STATUSLINE_CMD2
|
|
2707
2978
|
};
|
|
2708
|
-
|
|
2979
|
+
fs8.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
2709
2980
|
cleanupLegacyScript();
|
|
2710
2981
|
return "[KeepGoing] Migrated statusline to auto-updating command (restart Claude Code to apply)";
|
|
2711
2982
|
} catch {
|
|
@@ -2713,10 +2984,10 @@ function migrateStatusline(wsPath) {
|
|
|
2713
2984
|
}
|
|
2714
2985
|
}
|
|
2715
2986
|
function cleanupLegacyScript() {
|
|
2716
|
-
const legacyScript =
|
|
2717
|
-
if (
|
|
2987
|
+
const legacyScript = path11.join(os4.homedir(), ".claude", "keepgoing-statusline.sh");
|
|
2988
|
+
if (fs8.existsSync(legacyScript)) {
|
|
2718
2989
|
try {
|
|
2719
|
-
|
|
2990
|
+
fs8.unlinkSync(legacyScript);
|
|
2720
2991
|
} catch {
|
|
2721
2992
|
}
|
|
2722
2993
|
}
|
|
@@ -2949,6 +3220,87 @@ function registerContinueOn(server, reader, workspacePath) {
|
|
|
2949
3220
|
);
|
|
2950
3221
|
}
|
|
2951
3222
|
|
|
3223
|
+
// src/tools/getContextSnapshot.ts
|
|
3224
|
+
import { z as z10 } from "zod";
|
|
3225
|
+
function registerGetContextSnapshot(server, reader, workspacePath) {
|
|
3226
|
+
server.tool(
|
|
3227
|
+
"get_context_snapshot",
|
|
3228
|
+
"Get a compact context snapshot: what you were doing, what is next, and momentum. Use this for quick orientation without a full briefing.",
|
|
3229
|
+
{
|
|
3230
|
+
format: z10.enum(["text", "json"]).optional().describe('Output format. "text" (default) returns a formatted single line. "json" returns the structured snapshot object.')
|
|
3231
|
+
},
|
|
3232
|
+
async ({ format }) => {
|
|
3233
|
+
const snapshot = generateContextSnapshot(workspacePath);
|
|
3234
|
+
if (!snapshot) {
|
|
3235
|
+
return {
|
|
3236
|
+
content: [
|
|
3237
|
+
{
|
|
3238
|
+
type: "text",
|
|
3239
|
+
text: "No KeepGoing data found. The developer has not saved any checkpoints yet."
|
|
3240
|
+
}
|
|
3241
|
+
]
|
|
3242
|
+
};
|
|
3243
|
+
}
|
|
3244
|
+
if (format === "json") {
|
|
3245
|
+
return {
|
|
3246
|
+
content: [{ type: "text", text: JSON.stringify(snapshot, null, 2) }]
|
|
3247
|
+
};
|
|
3248
|
+
}
|
|
3249
|
+
const icon = snapshot.momentum === "hot" ? "\u26A1" : snapshot.momentum === "warm" ? "\u{1F536}" : "\u{1F4A4}";
|
|
3250
|
+
const parts = [];
|
|
3251
|
+
parts.push(`${icon} ${snapshot.doing}`);
|
|
3252
|
+
if (snapshot.next) {
|
|
3253
|
+
parts.push(`\u2192 ${snapshot.next}`);
|
|
3254
|
+
}
|
|
3255
|
+
let line = parts.join(" ");
|
|
3256
|
+
line += ` (${snapshot.when})`;
|
|
3257
|
+
if (snapshot.blocker) {
|
|
3258
|
+
line += ` \u26D4 ${snapshot.blocker}`;
|
|
3259
|
+
}
|
|
3260
|
+
if (snapshot.activeAgents && snapshot.activeAgents > 0) {
|
|
3261
|
+
line += ` [${snapshot.activeAgents} active agent${snapshot.activeAgents > 1 ? "s" : ""}]`;
|
|
3262
|
+
}
|
|
3263
|
+
return {
|
|
3264
|
+
content: [{ type: "text", text: line }]
|
|
3265
|
+
};
|
|
3266
|
+
}
|
|
3267
|
+
);
|
|
3268
|
+
}
|
|
3269
|
+
|
|
3270
|
+
// src/tools/getWhatsHot.ts
|
|
3271
|
+
import { z as z11 } from "zod";
|
|
3272
|
+
function registerGetWhatsHot(server) {
|
|
3273
|
+
server.tool(
|
|
3274
|
+
"get_whats_hot",
|
|
3275
|
+
"Get a summary of activity across all registered projects, sorted by momentum. Shows what the developer is working on across their entire portfolio.",
|
|
3276
|
+
{
|
|
3277
|
+
format: z11.enum(["text", "json"]).optional().describe('Output format. "text" (default) returns formatted lines. "json" returns the structured summary object.')
|
|
3278
|
+
},
|
|
3279
|
+
async ({ format }) => {
|
|
3280
|
+
const summary = generateCrossProjectSummary();
|
|
3281
|
+
if (summary.projects.length === 0) {
|
|
3282
|
+
return {
|
|
3283
|
+
content: [
|
|
3284
|
+
{
|
|
3285
|
+
type: "text",
|
|
3286
|
+
text: "No projects with activity found. Projects are registered automatically when checkpoints are saved."
|
|
3287
|
+
}
|
|
3288
|
+
]
|
|
3289
|
+
};
|
|
3290
|
+
}
|
|
3291
|
+
if (format === "json") {
|
|
3292
|
+
return {
|
|
3293
|
+
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
3294
|
+
};
|
|
3295
|
+
}
|
|
3296
|
+
const lines = summary.projects.map((entry) => formatCrossProjectLine(entry));
|
|
3297
|
+
return {
|
|
3298
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
3299
|
+
};
|
|
3300
|
+
}
|
|
3301
|
+
);
|
|
3302
|
+
}
|
|
3303
|
+
|
|
2952
3304
|
// src/prompts/resume.ts
|
|
2953
3305
|
function registerResumePrompt(server) {
|
|
2954
3306
|
server.prompt(
|
|
@@ -3114,7 +3466,7 @@ async function handlePrintCurrent() {
|
|
|
3114
3466
|
}
|
|
3115
3467
|
|
|
3116
3468
|
// src/cli/saveCheckpoint.ts
|
|
3117
|
-
import
|
|
3469
|
+
import path12 from "path";
|
|
3118
3470
|
async function handleSaveCheckpoint() {
|
|
3119
3471
|
const wsPath = resolveWsPath();
|
|
3120
3472
|
const reader = new KeepGoingReader(wsPath);
|
|
@@ -3142,9 +3494,9 @@ async function handleSaveCheckpoint() {
|
|
|
3142
3494
|
sessionStartTime: lastSession?.timestamp ?? now,
|
|
3143
3495
|
lastActivityTime: now
|
|
3144
3496
|
});
|
|
3145
|
-
const summary = buildSmartSummary(events) ?? `Worked on ${touchedFiles.slice(0, 5).map((f) =>
|
|
3497
|
+
const summary = buildSmartSummary(events) ?? `Worked on ${touchedFiles.slice(0, 5).map((f) => path12.basename(f)).join(", ")}`;
|
|
3146
3498
|
const nextStep = buildSmartNextStep(events);
|
|
3147
|
-
const projectName =
|
|
3499
|
+
const projectName = path12.basename(resolveStorageRoot(wsPath));
|
|
3148
3500
|
const sessionId = generateSessionId({ workspaceRoot: wsPath, branch: gitBranch ?? void 0, worktreePath: wsPath });
|
|
3149
3501
|
const checkpoint = createCheckpoint({
|
|
3150
3502
|
summary,
|
|
@@ -3189,8 +3541,9 @@ async function handleSaveCheckpoint() {
|
|
|
3189
3541
|
}
|
|
3190
3542
|
|
|
3191
3543
|
// src/cli/transcriptUtils.ts
|
|
3192
|
-
import
|
|
3193
|
-
var TAIL_READ_BYTES =
|
|
3544
|
+
import fs9 from "fs";
|
|
3545
|
+
var TAIL_READ_BYTES = 32768;
|
|
3546
|
+
var LATEST_LABEL_READ_BYTES = 65536;
|
|
3194
3547
|
var TOOL_VERB_MAP = {
|
|
3195
3548
|
Edit: "editing",
|
|
3196
3549
|
MultiEdit: "editing",
|
|
@@ -3202,7 +3555,12 @@ var TOOL_VERB_MAP = {
|
|
|
3202
3555
|
Agent: "delegating",
|
|
3203
3556
|
WebFetch: "browsing",
|
|
3204
3557
|
WebSearch: "browsing",
|
|
3205
|
-
TodoWrite: "planning"
|
|
3558
|
+
TodoWrite: "planning",
|
|
3559
|
+
AskUserQuestion: "discussing",
|
|
3560
|
+
EnterPlanMode: "planning",
|
|
3561
|
+
ExitPlanMode: "planning",
|
|
3562
|
+
TaskCreate: "planning",
|
|
3563
|
+
TaskUpdate: "planning"
|
|
3206
3564
|
};
|
|
3207
3565
|
function truncateAtWord(text, max) {
|
|
3208
3566
|
if (text.length <= max) return text;
|
|
@@ -3240,9 +3598,9 @@ function isAssistantEntry(entry) {
|
|
|
3240
3598
|
return entry.message?.role === "assistant";
|
|
3241
3599
|
}
|
|
3242
3600
|
function extractSessionLabel(transcriptPath) {
|
|
3243
|
-
if (!transcriptPath || !
|
|
3601
|
+
if (!transcriptPath || !fs9.existsSync(transcriptPath)) return null;
|
|
3244
3602
|
try {
|
|
3245
|
-
const raw =
|
|
3603
|
+
const raw = fs9.readFileSync(transcriptPath, "utf-8");
|
|
3246
3604
|
for (const line of raw.split("\n")) {
|
|
3247
3605
|
const trimmed = line.trim();
|
|
3248
3606
|
if (!trimmed) continue;
|
|
@@ -3255,7 +3613,55 @@ function extractSessionLabel(transcriptPath) {
|
|
|
3255
3613
|
if (!isUserEntry(entry)) continue;
|
|
3256
3614
|
let text = extractTextFromContent(entry.message?.content);
|
|
3257
3615
|
if (!text) continue;
|
|
3258
|
-
if (text.startsWith("[") || /^<[a-z][\w-]
|
|
3616
|
+
if (text.startsWith("[") || /^<[a-z][\w-]*[\s>]/.test(text)) continue;
|
|
3617
|
+
text = stripAgentTags(text);
|
|
3618
|
+
if (!text) continue;
|
|
3619
|
+
text = text.replace(/@[\w./\-]+/g, "").trim();
|
|
3620
|
+
text = text.replace(FILLER_PREFIX_RE, "").trim();
|
|
3621
|
+
text = text.replace(MARKDOWN_HEADING_RE, "").trim();
|
|
3622
|
+
text = text.replace(/\s+/g, " ").trim();
|
|
3623
|
+
if (text.length < 20) continue;
|
|
3624
|
+
if (text.length > 80) {
|
|
3625
|
+
text = text.slice(0, 80);
|
|
3626
|
+
}
|
|
3627
|
+
return text;
|
|
3628
|
+
}
|
|
3629
|
+
} catch {
|
|
3630
|
+
}
|
|
3631
|
+
return null;
|
|
3632
|
+
}
|
|
3633
|
+
function extractLatestUserLabel(transcriptPath) {
|
|
3634
|
+
if (!transcriptPath || !fs9.existsSync(transcriptPath)) return null;
|
|
3635
|
+
try {
|
|
3636
|
+
const stat = fs9.statSync(transcriptPath);
|
|
3637
|
+
const fileSize = stat.size;
|
|
3638
|
+
if (fileSize === 0) return null;
|
|
3639
|
+
const readSize = Math.min(fileSize, LATEST_LABEL_READ_BYTES);
|
|
3640
|
+
const offset = fileSize - readSize;
|
|
3641
|
+
const buf = Buffer.alloc(readSize);
|
|
3642
|
+
const fd = fs9.openSync(transcriptPath, "r");
|
|
3643
|
+
try {
|
|
3644
|
+
fs9.readSync(fd, buf, 0, readSize, offset);
|
|
3645
|
+
} finally {
|
|
3646
|
+
fs9.closeSync(fd);
|
|
3647
|
+
}
|
|
3648
|
+
const tail = buf.toString("utf-8");
|
|
3649
|
+
const lines = tail.split("\n").reverse();
|
|
3650
|
+
for (const line of lines) {
|
|
3651
|
+
const trimmed = line.trim();
|
|
3652
|
+
if (!trimmed) continue;
|
|
3653
|
+
let entry;
|
|
3654
|
+
try {
|
|
3655
|
+
entry = JSON.parse(trimmed);
|
|
3656
|
+
} catch {
|
|
3657
|
+
continue;
|
|
3658
|
+
}
|
|
3659
|
+
if (!isUserEntry(entry)) continue;
|
|
3660
|
+
let text = extractTextFromContent(entry.message?.content);
|
|
3661
|
+
if (!text) continue;
|
|
3662
|
+
if (text.startsWith("[") || /^<[a-z][\w-]*[\s>]/.test(text)) continue;
|
|
3663
|
+
text = stripAgentTags(text);
|
|
3664
|
+
if (!text) continue;
|
|
3259
3665
|
text = text.replace(/@[\w./\-]+/g, "").trim();
|
|
3260
3666
|
text = text.replace(FILLER_PREFIX_RE, "").trim();
|
|
3261
3667
|
text = text.replace(MARKDOWN_HEADING_RE, "").trim();
|
|
@@ -3271,19 +3677,19 @@ function extractSessionLabel(transcriptPath) {
|
|
|
3271
3677
|
return null;
|
|
3272
3678
|
}
|
|
3273
3679
|
function extractCurrentAction(transcriptPath) {
|
|
3274
|
-
if (!transcriptPath || !
|
|
3680
|
+
if (!transcriptPath || !fs9.existsSync(transcriptPath)) return null;
|
|
3275
3681
|
try {
|
|
3276
|
-
const stat =
|
|
3682
|
+
const stat = fs9.statSync(transcriptPath);
|
|
3277
3683
|
const fileSize = stat.size;
|
|
3278
3684
|
if (fileSize === 0) return null;
|
|
3279
3685
|
const readSize = Math.min(fileSize, TAIL_READ_BYTES);
|
|
3280
3686
|
const offset = fileSize - readSize;
|
|
3281
3687
|
const buf = Buffer.alloc(readSize);
|
|
3282
|
-
const fd =
|
|
3688
|
+
const fd = fs9.openSync(transcriptPath, "r");
|
|
3283
3689
|
try {
|
|
3284
|
-
|
|
3690
|
+
fs9.readSync(fd, buf, 0, readSize, offset);
|
|
3285
3691
|
} finally {
|
|
3286
|
-
|
|
3692
|
+
fs9.closeSync(fd);
|
|
3287
3693
|
}
|
|
3288
3694
|
const tail = buf.toString("utf-8");
|
|
3289
3695
|
const lines = tail.split("\n").reverse();
|
|
@@ -3318,6 +3724,8 @@ async function handleUpdateTask() {
|
|
|
3318
3724
|
if (payloadStr) {
|
|
3319
3725
|
try {
|
|
3320
3726
|
const payload = JSON.parse(payloadStr);
|
|
3727
|
+
if (payload.taskSummary) payload.taskSummary = stripAgentTags(payload.taskSummary);
|
|
3728
|
+
if (payload.sessionLabel) payload.sessionLabel = stripAgentTags(payload.sessionLabel);
|
|
3321
3729
|
const writer = new KeepGoingWriter(wsPath);
|
|
3322
3730
|
const branch = payload.branch ?? getCurrentBranch(wsPath) ?? void 0;
|
|
3323
3731
|
const task = {
|
|
@@ -3385,8 +3793,8 @@ async function handleUpdateTaskFromHook() {
|
|
|
3385
3793
|
}
|
|
3386
3794
|
|
|
3387
3795
|
// src/cli/statusline.ts
|
|
3388
|
-
import
|
|
3389
|
-
import
|
|
3796
|
+
import fs10 from "fs";
|
|
3797
|
+
import path13 from "path";
|
|
3390
3798
|
var STDIN_TIMEOUT_MS2 = 3e3;
|
|
3391
3799
|
async function handleStatusline() {
|
|
3392
3800
|
const chunks = [];
|
|
@@ -3410,12 +3818,15 @@ async function handleStatusline() {
|
|
|
3410
3818
|
if (input.agent?.name) {
|
|
3411
3819
|
label = input.agent.name;
|
|
3412
3820
|
}
|
|
3821
|
+
if (!label && transcriptPath) {
|
|
3822
|
+
label = extractLatestUserLabel(transcriptPath);
|
|
3823
|
+
}
|
|
3413
3824
|
if (!label) {
|
|
3414
3825
|
try {
|
|
3415
3826
|
const gitRoot = findGitRoot(dir);
|
|
3416
|
-
const tasksFile =
|
|
3417
|
-
if (
|
|
3418
|
-
const data = JSON.parse(
|
|
3827
|
+
const tasksFile = path13.join(gitRoot, ".keepgoing", "current-tasks.json");
|
|
3828
|
+
if (fs10.existsSync(tasksFile)) {
|
|
3829
|
+
const data = JSON.parse(fs10.readFileSync(tasksFile, "utf-8"));
|
|
3419
3830
|
const tasks = pruneStaleTasks(data.tasks ?? []);
|
|
3420
3831
|
const match = sessionId ? tasks.find((t) => t.sessionId === sessionId) : void 0;
|
|
3421
3832
|
if (match?.sessionLabel) {
|
|
@@ -3428,7 +3839,28 @@ async function handleStatusline() {
|
|
|
3428
3839
|
if (!label && transcriptPath) {
|
|
3429
3840
|
label = extractSessionLabel(transcriptPath);
|
|
3430
3841
|
}
|
|
3431
|
-
if (!label)
|
|
3842
|
+
if (!label) {
|
|
3843
|
+
try {
|
|
3844
|
+
const gitRoot = findGitRoot(dir);
|
|
3845
|
+
const reader = new KeepGoingReader(gitRoot);
|
|
3846
|
+
if (reader.exists()) {
|
|
3847
|
+
const recent = reader.getScopedRecentSessions(10);
|
|
3848
|
+
const last = recent.find((s) => s.source !== "auto") ?? recent[0];
|
|
3849
|
+
if (last) {
|
|
3850
|
+
const ago = formatRelativeTime(last.timestamp);
|
|
3851
|
+
const summary = last.summary ? truncateAtWord(last.summary, 40) : null;
|
|
3852
|
+
const next = last.nextStep ? truncateAtWord(last.nextStep, 30) : null;
|
|
3853
|
+
const parts = [`[KG] ${ago}`];
|
|
3854
|
+
if (summary) parts.push(summary);
|
|
3855
|
+
if (next) parts.push(`\u2192 ${next}`);
|
|
3856
|
+
process.stdout.write(`${parts.join(" \xB7 ")}
|
|
3857
|
+
`);
|
|
3858
|
+
}
|
|
3859
|
+
}
|
|
3860
|
+
} catch {
|
|
3861
|
+
}
|
|
3862
|
+
process.exit(0);
|
|
3863
|
+
}
|
|
3432
3864
|
const action = transcriptPath ? extractCurrentAction(transcriptPath) : null;
|
|
3433
3865
|
const budget = action ? 40 : 55;
|
|
3434
3866
|
const displayLabel = truncateAtWord(label, budget);
|
|
@@ -3519,6 +3951,8 @@ if (flag) {
|
|
|
3519
3951
|
registerGetCurrentTask(server, reader);
|
|
3520
3952
|
registerSaveCheckpoint(server, reader, workspacePath);
|
|
3521
3953
|
registerContinueOn(server, reader, workspacePath);
|
|
3954
|
+
registerGetContextSnapshot(server, reader, workspacePath);
|
|
3955
|
+
registerGetWhatsHot(server);
|
|
3522
3956
|
registerSetupProject(server, workspacePath);
|
|
3523
3957
|
registerActivateLicense(server);
|
|
3524
3958
|
registerDeactivateLicense(server);
|