@keepgoingdev/mcp-server 0.7.2 → 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 +993 -638
- 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) {
|
|
@@ -706,22 +719,160 @@ function formatContinueOnPrompt(context, options) {
|
|
|
706
719
|
return result;
|
|
707
720
|
}
|
|
708
721
|
|
|
709
|
-
// ../../packages/shared/src/
|
|
710
|
-
import
|
|
711
|
-
import
|
|
712
|
-
import { randomUUID as randomUUID2, createHash } from "crypto";
|
|
722
|
+
// ../../packages/shared/src/reader.ts
|
|
723
|
+
import fs4 from "fs";
|
|
724
|
+
import path6 from "path";
|
|
713
725
|
|
|
714
|
-
// ../../packages/shared/src/
|
|
726
|
+
// ../../packages/shared/src/license.ts
|
|
727
|
+
import crypto from "crypto";
|
|
715
728
|
import fs from "fs";
|
|
716
729
|
import os from "os";
|
|
717
730
|
import path3 from "path";
|
|
718
|
-
var
|
|
719
|
-
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");
|
|
720
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
|
+
}
|
|
721
872
|
function readKnownProjects() {
|
|
722
873
|
try {
|
|
723
|
-
if (
|
|
724
|
-
const raw = JSON.parse(
|
|
874
|
+
if (fs2.existsSync(KNOWN_PROJECTS_FILE)) {
|
|
875
|
+
const raw = JSON.parse(fs2.readFileSync(KNOWN_PROJECTS_FILE, "utf-8"));
|
|
725
876
|
if (raw && Array.isArray(raw.projects)) {
|
|
726
877
|
return raw;
|
|
727
878
|
}
|
|
@@ -731,18 +882,18 @@ function readKnownProjects() {
|
|
|
731
882
|
return { version: 1, projects: [] };
|
|
732
883
|
}
|
|
733
884
|
function writeKnownProjects(data) {
|
|
734
|
-
if (!
|
|
735
|
-
|
|
885
|
+
if (!fs2.existsSync(KEEPGOING_DIR)) {
|
|
886
|
+
fs2.mkdirSync(KEEPGOING_DIR, { recursive: true });
|
|
736
887
|
}
|
|
737
888
|
const tmpFile = KNOWN_PROJECTS_FILE + ".tmp";
|
|
738
|
-
|
|
739
|
-
|
|
889
|
+
fs2.writeFileSync(tmpFile, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
890
|
+
fs2.renameSync(tmpFile, KNOWN_PROJECTS_FILE);
|
|
740
891
|
}
|
|
741
892
|
function registerProject(projectPath, projectName) {
|
|
742
893
|
try {
|
|
743
894
|
const data = readKnownProjects();
|
|
744
895
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
745
|
-
const name = projectName ||
|
|
896
|
+
const name = projectName || path4.basename(projectPath);
|
|
746
897
|
const existingIdx = data.projects.findIndex((p) => p.path === projectPath);
|
|
747
898
|
if (existingIdx >= 0) {
|
|
748
899
|
data.projects[existingIdx].lastSeen = now;
|
|
@@ -781,23 +932,23 @@ var KeepGoingWriter = class {
|
|
|
781
932
|
currentTasksFilePath;
|
|
782
933
|
constructor(workspacePath) {
|
|
783
934
|
const mainRoot = resolveStorageRoot(workspacePath);
|
|
784
|
-
this.storagePath =
|
|
785
|
-
this.sessionsFilePath =
|
|
786
|
-
this.stateFilePath =
|
|
787
|
-
this.metaFilePath =
|
|
788
|
-
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);
|
|
789
940
|
}
|
|
790
941
|
ensureDir() {
|
|
791
|
-
if (!
|
|
792
|
-
|
|
942
|
+
if (!fs3.existsSync(this.storagePath)) {
|
|
943
|
+
fs3.mkdirSync(this.storagePath, { recursive: true });
|
|
793
944
|
}
|
|
794
945
|
}
|
|
795
946
|
saveCheckpoint(checkpoint, projectName) {
|
|
796
947
|
this.ensureDir();
|
|
797
948
|
let sessionsData;
|
|
798
949
|
try {
|
|
799
|
-
if (
|
|
800
|
-
const raw = JSON.parse(
|
|
950
|
+
if (fs3.existsSync(this.sessionsFilePath)) {
|
|
951
|
+
const raw = JSON.parse(fs3.readFileSync(this.sessionsFilePath, "utf-8"));
|
|
801
952
|
if (Array.isArray(raw)) {
|
|
802
953
|
sessionsData = { version: 1, project: projectName, sessions: raw };
|
|
803
954
|
} else {
|
|
@@ -815,13 +966,13 @@ var KeepGoingWriter = class {
|
|
|
815
966
|
if (sessionsData.sessions.length > MAX_SESSIONS) {
|
|
816
967
|
sessionsData.sessions = sessionsData.sessions.slice(-MAX_SESSIONS);
|
|
817
968
|
}
|
|
818
|
-
|
|
969
|
+
fs3.writeFileSync(this.sessionsFilePath, JSON.stringify(sessionsData, null, 2), "utf-8");
|
|
819
970
|
const state = {
|
|
820
971
|
lastSessionId: checkpoint.id,
|
|
821
972
|
lastKnownBranch: checkpoint.gitBranch,
|
|
822
973
|
lastActivityAt: checkpoint.timestamp
|
|
823
974
|
};
|
|
824
|
-
|
|
975
|
+
fs3.writeFileSync(this.stateFilePath, JSON.stringify(state, null, 2), "utf-8");
|
|
825
976
|
this.updateMeta(checkpoint.timestamp);
|
|
826
977
|
const mainRoot = resolveStorageRoot(this.storagePath);
|
|
827
978
|
registerProject(mainRoot, projectName);
|
|
@@ -829,8 +980,8 @@ var KeepGoingWriter = class {
|
|
|
829
980
|
updateMeta(timestamp) {
|
|
830
981
|
let meta;
|
|
831
982
|
try {
|
|
832
|
-
if (
|
|
833
|
-
meta = JSON.parse(
|
|
983
|
+
if (fs3.existsSync(this.metaFilePath)) {
|
|
984
|
+
meta = JSON.parse(fs3.readFileSync(this.metaFilePath, "utf-8"));
|
|
834
985
|
meta.lastUpdated = timestamp;
|
|
835
986
|
} else {
|
|
836
987
|
meta = {
|
|
@@ -846,7 +997,28 @@ var KeepGoingWriter = class {
|
|
|
846
997
|
lastUpdated: timestamp
|
|
847
998
|
};
|
|
848
999
|
}
|
|
849
|
-
|
|
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");
|
|
850
1022
|
}
|
|
851
1023
|
// ---------------------------------------------------------------------------
|
|
852
1024
|
// Multi-session API
|
|
@@ -854,8 +1026,8 @@ var KeepGoingWriter = class {
|
|
|
854
1026
|
/** Read all current tasks from current-tasks.json. Auto-prunes stale sessions. */
|
|
855
1027
|
readCurrentTasks() {
|
|
856
1028
|
try {
|
|
857
|
-
if (
|
|
858
|
-
const raw = JSON.parse(
|
|
1029
|
+
if (fs3.existsSync(this.currentTasksFilePath)) {
|
|
1030
|
+
const raw = JSON.parse(fs3.readFileSync(this.currentTasksFilePath, "utf-8"));
|
|
859
1031
|
const tasks = Array.isArray(raw) ? raw : raw.tasks ?? [];
|
|
860
1032
|
return this.pruneStale(tasks);
|
|
861
1033
|
}
|
|
@@ -876,6 +1048,8 @@ var KeepGoingWriter = class {
|
|
|
876
1048
|
/** Core upsert logic: merges the update into current-tasks.json and returns the pruned task list. */
|
|
877
1049
|
upsertSessionCore(update) {
|
|
878
1050
|
this.ensureDir();
|
|
1051
|
+
if (update.taskSummary) update.taskSummary = stripAgentTags(update.taskSummary);
|
|
1052
|
+
if (update.sessionLabel) update.sessionLabel = stripAgentTags(update.sessionLabel);
|
|
879
1053
|
const sessionId = update.sessionId || generateSessionId(update);
|
|
880
1054
|
const tasks = this.readAllTasksRaw();
|
|
881
1055
|
const existingIdx = tasks.findIndex((t) => t.sessionId === sessionId);
|
|
@@ -910,8 +1084,8 @@ var KeepGoingWriter = class {
|
|
|
910
1084
|
// ---------------------------------------------------------------------------
|
|
911
1085
|
readAllTasksRaw() {
|
|
912
1086
|
try {
|
|
913
|
-
if (
|
|
914
|
-
const raw = JSON.parse(
|
|
1087
|
+
if (fs3.existsSync(this.currentTasksFilePath)) {
|
|
1088
|
+
const raw = JSON.parse(fs3.readFileSync(this.currentTasksFilePath, "utf-8"));
|
|
915
1089
|
return Array.isArray(raw) ? [...raw] : [...raw.tasks ?? []];
|
|
916
1090
|
}
|
|
917
1091
|
} catch {
|
|
@@ -923,7 +1097,7 @@ var KeepGoingWriter = class {
|
|
|
923
1097
|
}
|
|
924
1098
|
writeTasksFile(tasks) {
|
|
925
1099
|
const data = { version: 1, tasks };
|
|
926
|
-
|
|
1100
|
+
fs3.writeFileSync(this.currentTasksFilePath, JSON.stringify(data, null, 2), "utf-8");
|
|
927
1101
|
}
|
|
928
1102
|
};
|
|
929
1103
|
function generateSessionId(context) {
|
|
@@ -939,57 +1113,337 @@ function generateSessionId(context) {
|
|
|
939
1113
|
return `ses_${hash}`;
|
|
940
1114
|
}
|
|
941
1115
|
|
|
942
|
-
// ../../packages/shared/src/
|
|
943
|
-
var
|
|
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
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
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;
|
|
976
1180
|
}
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1181
|
+
}
|
|
1182
|
+
if (wrapperLastSessionId) {
|
|
1183
|
+
const found = sessions.find((s) => s.id === wrapperLastSessionId);
|
|
1184
|
+
if (found) {
|
|
1185
|
+
return found;
|
|
981
1186
|
}
|
|
982
|
-
groups.get("other").push(msg.trim());
|
|
983
1187
|
}
|
|
1188
|
+
return sessions[sessions.length - 1];
|
|
984
1189
|
}
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
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) {
|
|
1444
|
+
if (NOISE_PATTERNS.some((p) => file.includes(p))) {
|
|
1445
|
+
continue;
|
|
1446
|
+
}
|
|
993
1447
|
const parts = file.split("/").filter(Boolean);
|
|
994
1448
|
let area;
|
|
995
1449
|
if (parts.length >= 2 && (parts[0] === "apps" || parts[0] === "packages")) {
|
|
@@ -1101,130 +1555,221 @@ function capitalize(s) {
|
|
|
1101
1555
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
1102
1556
|
}
|
|
1103
1557
|
|
|
1104
|
-
// ../../packages/shared/src/
|
|
1105
|
-
import
|
|
1106
|
-
import
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
|
|
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`;
|
|
1120
1575
|
}
|
|
1121
|
-
function
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
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
|
+
}
|
|
1128
1587
|
}
|
|
1129
|
-
const
|
|
1130
|
-
if (
|
|
1131
|
-
|
|
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;
|
|
1132
1598
|
}
|
|
1133
|
-
|
|
1134
|
-
|
|
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;
|
|
1135
1605
|
}
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
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";
|
|
1155
1624
|
}
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
if (_cachedStore && now - _cacheTimestamp < LICENSE_CACHE_TTL_MS) {
|
|
1162
|
-
return _cachedStore;
|
|
1625
|
+
function buildWhere(checkpoint, branch) {
|
|
1626
|
+
const effectiveBranch = branch ?? checkpoint?.gitBranch;
|
|
1627
|
+
const parts = [];
|
|
1628
|
+
if (effectiveBranch) {
|
|
1629
|
+
parts.push(effectiveBranch);
|
|
1163
1630
|
}
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
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";
|
|
1169
1656
|
} else {
|
|
1170
|
-
|
|
1171
|
-
const data = JSON.parse(raw);
|
|
1172
|
-
if (data?.version === 2 && Array.isArray(data.licenses)) {
|
|
1173
|
-
store = data;
|
|
1174
|
-
} else {
|
|
1175
|
-
store = { version: 2, licenses: [] };
|
|
1176
|
-
}
|
|
1657
|
+
area = parts[0];
|
|
1177
1658
|
}
|
|
1178
|
-
|
|
1179
|
-
store = { version: 2, licenses: [] };
|
|
1659
|
+
areaCounts.set(area, (areaCounts.get(area) ?? 0) + 1);
|
|
1180
1660
|
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
const dirPath = getGlobalLicenseDir();
|
|
1187
|
-
if (!fs3.existsSync(dirPath)) {
|
|
1188
|
-
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
|
+
}
|
|
1189
1666
|
}
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
_cachedStore = store;
|
|
1193
|
-
_cacheTimestamp = Date.now();
|
|
1194
|
-
}
|
|
1195
|
-
function addLicenseEntry(entry) {
|
|
1196
|
-
const store = readLicenseStore();
|
|
1197
|
-
const idx = store.licenses.findIndex((l) => l.licenseKey === entry.licenseKey);
|
|
1198
|
-
if (idx >= 0) {
|
|
1199
|
-
store.licenses[idx] = entry;
|
|
1200
|
-
} else {
|
|
1201
|
-
store.licenses.push(entry);
|
|
1667
|
+
if (topCount === 0) {
|
|
1668
|
+
topCount = Math.max(...areaCounts.values());
|
|
1202
1669
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
const store = readLicenseStore();
|
|
1207
|
-
store.licenses = store.licenses.filter((l) => l.licenseKey !== licenseKey);
|
|
1208
|
-
writeLicenseStore(store);
|
|
1209
|
-
}
|
|
1210
|
-
function getActiveLicenses() {
|
|
1211
|
-
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;
|
|
1212
1673
|
}
|
|
1213
|
-
function
|
|
1214
|
-
const
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
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;
|
|
1218
1763
|
});
|
|
1764
|
+
return {
|
|
1765
|
+
projects,
|
|
1766
|
+
generated: (/* @__PURE__ */ new Date()).toISOString()
|
|
1767
|
+
};
|
|
1219
1768
|
}
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
function needsRevalidation(entry) {
|
|
1225
|
-
const lastValidated = new Date(entry.lastValidatedAt).getTime();
|
|
1226
|
-
return Date.now() - lastValidated > REVALIDATION_THRESHOLD_MS;
|
|
1227
|
-
}
|
|
1769
|
+
|
|
1770
|
+
// ../../packages/shared/src/decisionStorage.ts
|
|
1771
|
+
import fs6 from "fs";
|
|
1772
|
+
import path8 from "path";
|
|
1228
1773
|
|
|
1229
1774
|
// ../../packages/shared/src/featureGate.ts
|
|
1230
1775
|
var DefaultFeatureGate = class {
|
|
@@ -1238,31 +1783,31 @@ function isDecisionsEnabled() {
|
|
|
1238
1783
|
}
|
|
1239
1784
|
|
|
1240
1785
|
// ../../packages/shared/src/decisionStorage.ts
|
|
1241
|
-
var
|
|
1242
|
-
var
|
|
1786
|
+
var STORAGE_DIR3 = ".keepgoing";
|
|
1787
|
+
var DECISIONS_FILE2 = "decisions.json";
|
|
1243
1788
|
var MAX_DECISIONS = 100;
|
|
1244
1789
|
var DecisionStorage = class {
|
|
1245
1790
|
storagePath;
|
|
1246
1791
|
decisionsFilePath;
|
|
1247
1792
|
constructor(workspacePath) {
|
|
1248
1793
|
const mainRoot = resolveStorageRoot(workspacePath);
|
|
1249
|
-
this.storagePath =
|
|
1250
|
-
this.decisionsFilePath =
|
|
1794
|
+
this.storagePath = path8.join(mainRoot, STORAGE_DIR3);
|
|
1795
|
+
this.decisionsFilePath = path8.join(this.storagePath, DECISIONS_FILE2);
|
|
1251
1796
|
}
|
|
1252
1797
|
ensureStorageDir() {
|
|
1253
|
-
if (!
|
|
1254
|
-
|
|
1798
|
+
if (!fs6.existsSync(this.storagePath)) {
|
|
1799
|
+
fs6.mkdirSync(this.storagePath, { recursive: true });
|
|
1255
1800
|
}
|
|
1256
1801
|
}
|
|
1257
1802
|
getProjectName() {
|
|
1258
|
-
return
|
|
1803
|
+
return path8.basename(path8.dirname(this.storagePath));
|
|
1259
1804
|
}
|
|
1260
1805
|
load() {
|
|
1261
1806
|
try {
|
|
1262
|
-
if (!
|
|
1807
|
+
if (!fs6.existsSync(this.decisionsFilePath)) {
|
|
1263
1808
|
return createEmptyProjectDecisions(this.getProjectName());
|
|
1264
1809
|
}
|
|
1265
|
-
const raw =
|
|
1810
|
+
const raw = fs6.readFileSync(this.decisionsFilePath, "utf-8");
|
|
1266
1811
|
const data = JSON.parse(raw);
|
|
1267
1812
|
return data;
|
|
1268
1813
|
} catch {
|
|
@@ -1272,7 +1817,7 @@ var DecisionStorage = class {
|
|
|
1272
1817
|
save(decisions) {
|
|
1273
1818
|
this.ensureStorageDir();
|
|
1274
1819
|
const content = JSON.stringify(decisions, null, 2);
|
|
1275
|
-
|
|
1820
|
+
fs6.writeFileSync(this.decisionsFilePath, content, "utf-8");
|
|
1276
1821
|
}
|
|
1277
1822
|
/**
|
|
1278
1823
|
* Save a decision record as a draft. Always persists regardless of Pro
|
|
@@ -1417,397 +1962,115 @@ function inferCategory(matchedKeywords, matchedTypes, matchedPaths) {
|
|
|
1417
1962
|
if (/auth|oidc|oauth|redirect|origin|trusted/.test(all)) {
|
|
1418
1963
|
return "auth";
|
|
1419
1964
|
}
|
|
1420
|
-
if (/migrat|postgres|db|supabase/.test(all)) {
|
|
1421
|
-
return "migration";
|
|
1422
|
-
}
|
|
1423
|
-
if (/ci|workflow|build|deploy/.test(all)) {
|
|
1424
|
-
return "deploy";
|
|
1425
|
-
}
|
|
1426
|
-
if (/fly|docker|k8s|terraform|infra|ops/.test(all)) {
|
|
1427
|
-
return "infra";
|
|
1428
|
-
}
|
|
1429
|
-
return "unknown";
|
|
1430
|
-
}
|
|
1431
|
-
function classifyCommit(commit) {
|
|
1432
|
-
const { message, filesChanged } = commit;
|
|
1433
|
-
const messageLower = message.toLowerCase();
|
|
1434
|
-
const parsed = parseConventionalCommit(message);
|
|
1435
|
-
const type = commit.type ?? parsed.type;
|
|
1436
|
-
const scope = commit.scope ?? parsed.scope;
|
|
1437
|
-
const reasons = [];
|
|
1438
|
-
let confidence = 0;
|
|
1439
|
-
const matchedKeywords = MESSAGE_KEYWORDS.filter((kw) => kw.test(messageLower));
|
|
1440
|
-
if (matchedKeywords.length > 0) {
|
|
1441
|
-
confidence += 0.3;
|
|
1442
|
-
const labels = matchedKeywords.slice(0, 3).map((kw) => kw.source.replace(/\\b/g, ""));
|
|
1443
|
-
reasons.push(`commit message contains: ${labels.join(", ")}`);
|
|
1444
|
-
}
|
|
1445
|
-
const matchedTypes = [];
|
|
1446
|
-
if (type && HIGH_SIGNAL_TYPES.has(type)) {
|
|
1447
|
-
matchedTypes.push(`type:${type}`);
|
|
1448
|
-
confidence += 0.35;
|
|
1449
|
-
reasons.push(`conventional commit type '${type}' is high-signal`);
|
|
1450
|
-
}
|
|
1451
|
-
if (scope && HIGH_SIGNAL_TYPES.has(scope)) {
|
|
1452
|
-
matchedTypes.push(`scope:${scope}`);
|
|
1453
|
-
confidence += 0.25;
|
|
1454
|
-
reasons.push(`conventional commit scope '${scope}' is high-signal`);
|
|
1455
|
-
}
|
|
1456
|
-
if (parsed.breaking) {
|
|
1457
|
-
confidence += 0.4;
|
|
1458
|
-
reasons.push("breaking change indicated by ! marker");
|
|
1459
|
-
}
|
|
1460
|
-
const matchedPaths = [];
|
|
1461
|
-
let bestTier = null;
|
|
1462
|
-
for (const file of filesChanged) {
|
|
1463
|
-
const pm = matchHighSignalPath(file);
|
|
1464
|
-
if (pm && !matchedPaths.includes(pm.label)) {
|
|
1465
|
-
matchedPaths.push(pm.label);
|
|
1466
|
-
if (bestTier !== "infra") {
|
|
1467
|
-
bestTier = pm.tier;
|
|
1468
|
-
}
|
|
1469
|
-
}
|
|
1470
|
-
}
|
|
1471
|
-
if (matchedPaths.length > 0) {
|
|
1472
|
-
confidence += bestTier === "infra" ? 0.4 : 0.2;
|
|
1473
|
-
reasons.push(`commit touched: ${matchedPaths.slice(0, 3).join(", ")}`);
|
|
1474
|
-
}
|
|
1475
|
-
if (filesChanged.length > 0 && filesChanged.every((f) => NEGATIVE_PATH_PATTERNS.some((p) => p.test(f)))) {
|
|
1476
|
-
confidence -= 0.5;
|
|
1477
|
-
reasons.push("all changed files are low-signal (lock files, generated code, dist)");
|
|
1478
|
-
}
|
|
1479
|
-
if (filesChanged.length > 0 && filesChanged.every((f) => /favicon/i.test(f) || /\.(svg|png|ico)$/i.test(f))) {
|
|
1480
|
-
confidence -= 0.5;
|
|
1481
|
-
reasons.push("all changed files are UI assets (favicon, SVG, PNG)");
|
|
1482
|
-
}
|
|
1483
|
-
if (filesChanged.length > 0 && filesChanged.every((f) => /\.css$/i.test(f)) && /tailwind/i.test(messageLower)) {
|
|
1484
|
-
confidence -= 0.5;
|
|
1485
|
-
reasons.push("Tailwind generated CSS change (low signal)");
|
|
1486
|
-
}
|
|
1487
|
-
const isDecisionCandidate = confidence >= 0.4;
|
|
1488
|
-
const keywordLabels = matchedKeywords.map((kw) => kw.source.replace(/\\b/g, ""));
|
|
1489
|
-
const category = isDecisionCandidate ? inferCategory(keywordLabels, matchedTypes, matchedPaths) : "unknown";
|
|
1490
|
-
return {
|
|
1491
|
-
isDecisionCandidate,
|
|
1492
|
-
confidence: Math.max(0, Math.min(1, confidence)),
|
|
1493
|
-
reasons,
|
|
1494
|
-
category
|
|
1495
|
-
};
|
|
1496
|
-
}
|
|
1497
|
-
function tryDetectDecision(opts) {
|
|
1498
|
-
if (!isDecisionsEnabled()) {
|
|
1499
|
-
return void 0;
|
|
1500
|
-
}
|
|
1501
|
-
const classification = classifyCommit({
|
|
1502
|
-
message: opts.commitMessage,
|
|
1503
|
-
filesChanged: opts.filesChanged
|
|
1504
|
-
});
|
|
1505
|
-
if (!classification.isDecisionCandidate) {
|
|
1506
|
-
return void 0;
|
|
1507
|
-
}
|
|
1508
|
-
const decision = createDecisionRecord({
|
|
1509
|
-
checkpointId: opts.checkpointId,
|
|
1510
|
-
gitBranch: opts.gitBranch,
|
|
1511
|
-
commitHash: opts.commitHash,
|
|
1512
|
-
commitMessage: opts.commitMessage,
|
|
1513
|
-
filesChanged: opts.filesChanged,
|
|
1514
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1515
|
-
classification
|
|
1516
|
-
});
|
|
1517
|
-
const storage = new DecisionStorage(opts.workspacePath);
|
|
1518
|
-
storage.saveDecision(decision);
|
|
1519
|
-
return {
|
|
1520
|
-
category: classification.category,
|
|
1521
|
-
confidence: classification.confidence
|
|
1522
|
-
};
|
|
1523
|
-
}
|
|
1524
|
-
|
|
1525
|
-
// ../../packages/shared/src/reader.ts
|
|
1526
|
-
import fs5 from "fs";
|
|
1527
|
-
import path7 from "path";
|
|
1528
|
-
var STORAGE_DIR3 = ".keepgoing";
|
|
1529
|
-
var META_FILE2 = "meta.json";
|
|
1530
|
-
var SESSIONS_FILE2 = "sessions.json";
|
|
1531
|
-
var DECISIONS_FILE2 = "decisions.json";
|
|
1532
|
-
var STATE_FILE2 = "state.json";
|
|
1533
|
-
var CURRENT_TASKS_FILE2 = "current-tasks.json";
|
|
1534
|
-
var KeepGoingReader = class {
|
|
1535
|
-
workspacePath;
|
|
1536
|
-
storagePath;
|
|
1537
|
-
metaFilePath;
|
|
1538
|
-
sessionsFilePath;
|
|
1539
|
-
decisionsFilePath;
|
|
1540
|
-
stateFilePath;
|
|
1541
|
-
currentTasksFilePath;
|
|
1542
|
-
_isWorktree;
|
|
1543
|
-
_cachedBranch = null;
|
|
1544
|
-
// null = not yet resolved
|
|
1545
|
-
constructor(workspacePath) {
|
|
1546
|
-
this.workspacePath = workspacePath;
|
|
1547
|
-
const mainRoot = resolveStorageRoot(workspacePath);
|
|
1548
|
-
this._isWorktree = mainRoot !== workspacePath;
|
|
1549
|
-
this.storagePath = path7.join(mainRoot, STORAGE_DIR3);
|
|
1550
|
-
this.metaFilePath = path7.join(this.storagePath, META_FILE2);
|
|
1551
|
-
this.sessionsFilePath = path7.join(this.storagePath, SESSIONS_FILE2);
|
|
1552
|
-
this.decisionsFilePath = path7.join(this.storagePath, DECISIONS_FILE2);
|
|
1553
|
-
this.stateFilePath = path7.join(this.storagePath, STATE_FILE2);
|
|
1554
|
-
this.currentTasksFilePath = path7.join(this.storagePath, CURRENT_TASKS_FILE2);
|
|
1555
|
-
}
|
|
1556
|
-
/** Check if .keepgoing/ directory exists. */
|
|
1557
|
-
exists() {
|
|
1558
|
-
return fs5.existsSync(this.storagePath);
|
|
1559
|
-
}
|
|
1560
|
-
/** Read state.json, returns undefined if missing or corrupt. */
|
|
1561
|
-
getState() {
|
|
1562
|
-
return this.readJsonFile(this.stateFilePath);
|
|
1563
|
-
}
|
|
1564
|
-
/** Read meta.json, returns undefined if missing or corrupt. */
|
|
1565
|
-
getMeta() {
|
|
1566
|
-
return this.readJsonFile(this.metaFilePath);
|
|
1567
|
-
}
|
|
1568
|
-
/**
|
|
1569
|
-
* Read sessions from sessions.json.
|
|
1570
|
-
* Handles both formats:
|
|
1571
|
-
* - Flat array: SessionCheckpoint[] (from ProjectStorage)
|
|
1572
|
-
* - Wrapper object: ProjectSessions (from SessionStorage)
|
|
1573
|
-
*/
|
|
1574
|
-
getSessions() {
|
|
1575
|
-
return this.parseSessions().sessions;
|
|
1576
|
-
}
|
|
1577
|
-
/**
|
|
1578
|
-
* Get the most recent session checkpoint.
|
|
1579
|
-
* Uses state.lastSessionId if available, falls back to last in array.
|
|
1580
|
-
*/
|
|
1581
|
-
getLastSession() {
|
|
1582
|
-
const { sessions, wrapperLastSessionId } = this.parseSessions();
|
|
1583
|
-
if (sessions.length === 0) {
|
|
1584
|
-
return void 0;
|
|
1585
|
-
}
|
|
1586
|
-
const state = this.getState();
|
|
1587
|
-
if (state?.lastSessionId) {
|
|
1588
|
-
const found = sessions.find((s) => s.id === state.lastSessionId);
|
|
1589
|
-
if (found) {
|
|
1590
|
-
return found;
|
|
1591
|
-
}
|
|
1592
|
-
}
|
|
1593
|
-
if (wrapperLastSessionId) {
|
|
1594
|
-
const found = sessions.find((s) => s.id === wrapperLastSessionId);
|
|
1595
|
-
if (found) {
|
|
1596
|
-
return found;
|
|
1597
|
-
}
|
|
1598
|
-
}
|
|
1599
|
-
return sessions[sessions.length - 1];
|
|
1600
|
-
}
|
|
1601
|
-
/**
|
|
1602
|
-
* Returns the last N sessions, newest first.
|
|
1603
|
-
*/
|
|
1604
|
-
getRecentSessions(count) {
|
|
1605
|
-
return getRecentSessions(this.getSessions(), count);
|
|
1606
|
-
}
|
|
1607
|
-
/** Read all decisions from decisions.json. */
|
|
1608
|
-
getDecisions() {
|
|
1609
|
-
return this.parseDecisions().decisions;
|
|
1610
|
-
}
|
|
1611
|
-
/** Returns the last N decisions, newest first. */
|
|
1612
|
-
getRecentDecisions(count) {
|
|
1613
|
-
const all = this.getDecisions();
|
|
1614
|
-
return all.slice(-count).reverse();
|
|
1615
|
-
}
|
|
1616
|
-
/** Read the multi-license store from `~/.keepgoing/license.json`. */
|
|
1617
|
-
getLicenseStore() {
|
|
1618
|
-
return readLicenseStore();
|
|
1619
|
-
}
|
|
1620
|
-
/**
|
|
1621
|
-
* Read all current tasks from current-tasks.json.
|
|
1622
|
-
* Automatically filters out stale finished sessions (> 2 hours).
|
|
1623
|
-
*/
|
|
1624
|
-
getCurrentTasks() {
|
|
1625
|
-
const multiRaw = this.readJsonFile(this.currentTasksFilePath);
|
|
1626
|
-
if (multiRaw) {
|
|
1627
|
-
const tasks = Array.isArray(multiRaw) ? multiRaw : multiRaw.tasks ?? [];
|
|
1628
|
-
return this.pruneStale(tasks);
|
|
1629
|
-
}
|
|
1630
|
-
return [];
|
|
1631
|
-
}
|
|
1632
|
-
/** Get only active sessions (sessionActive=true and within stale threshold). */
|
|
1633
|
-
getActiveTasks() {
|
|
1634
|
-
return this.getCurrentTasks().filter((t) => t.sessionActive);
|
|
1635
|
-
}
|
|
1636
|
-
/** Get a specific session by ID. */
|
|
1637
|
-
getTaskBySessionId(sessionId) {
|
|
1638
|
-
return this.getCurrentTasks().find((t) => t.sessionId === sessionId);
|
|
1639
|
-
}
|
|
1640
|
-
/**
|
|
1641
|
-
* Detect files being edited by multiple sessions simultaneously.
|
|
1642
|
-
* Returns pairs of session IDs and the conflicting file paths.
|
|
1643
|
-
*/
|
|
1644
|
-
detectFileConflicts() {
|
|
1645
|
-
const activeTasks = this.getActiveTasks();
|
|
1646
|
-
if (activeTasks.length < 2) return [];
|
|
1647
|
-
const fileToSessions = /* @__PURE__ */ new Map();
|
|
1648
|
-
for (const task of activeTasks) {
|
|
1649
|
-
if (task.lastFileEdited && task.sessionId) {
|
|
1650
|
-
const existing = fileToSessions.get(task.lastFileEdited) ?? [];
|
|
1651
|
-
existing.push({
|
|
1652
|
-
sessionId: task.sessionId,
|
|
1653
|
-
agentLabel: task.agentLabel,
|
|
1654
|
-
branch: task.branch
|
|
1655
|
-
});
|
|
1656
|
-
fileToSessions.set(task.lastFileEdited, existing);
|
|
1657
|
-
}
|
|
1658
|
-
}
|
|
1659
|
-
const conflicts = [];
|
|
1660
|
-
for (const [file, sessions] of fileToSessions) {
|
|
1661
|
-
if (sessions.length > 1) {
|
|
1662
|
-
conflicts.push({ file, sessions });
|
|
1663
|
-
}
|
|
1664
|
-
}
|
|
1665
|
-
return conflicts;
|
|
1666
|
-
}
|
|
1667
|
-
/**
|
|
1668
|
-
* Detect sessions on the same branch (possible duplicate work).
|
|
1669
|
-
*/
|
|
1670
|
-
detectBranchOverlap() {
|
|
1671
|
-
const activeTasks = this.getActiveTasks();
|
|
1672
|
-
if (activeTasks.length < 2) return [];
|
|
1673
|
-
const branchToSessions = /* @__PURE__ */ new Map();
|
|
1674
|
-
for (const task of activeTasks) {
|
|
1675
|
-
if (task.branch && task.sessionId) {
|
|
1676
|
-
const existing = branchToSessions.get(task.branch) ?? [];
|
|
1677
|
-
existing.push({ sessionId: task.sessionId, agentLabel: task.agentLabel });
|
|
1678
|
-
branchToSessions.set(task.branch, existing);
|
|
1679
|
-
}
|
|
1680
|
-
}
|
|
1681
|
-
const overlaps = [];
|
|
1682
|
-
for (const [branch, sessions] of branchToSessions) {
|
|
1683
|
-
if (sessions.length > 1) {
|
|
1684
|
-
overlaps.push({ branch, sessions });
|
|
1685
|
-
}
|
|
1686
|
-
}
|
|
1687
|
-
return overlaps;
|
|
1688
|
-
}
|
|
1689
|
-
pruneStale(tasks) {
|
|
1690
|
-
return pruneStaleTasks(tasks);
|
|
1965
|
+
if (/migrat|postgres|db|supabase/.test(all)) {
|
|
1966
|
+
return "migration";
|
|
1691
1967
|
}
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
const sessions = this.getSessions().filter((s) => s.gitBranch === branch);
|
|
1695
|
-
return sessions.length > 0 ? sessions[sessions.length - 1] : void 0;
|
|
1968
|
+
if (/ci|workflow|build|deploy/.test(all)) {
|
|
1969
|
+
return "deploy";
|
|
1696
1970
|
}
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
const filtered = this.getSessions().filter((s) => s.gitBranch === branch);
|
|
1700
|
-
return filtered.slice(-count).reverse();
|
|
1971
|
+
if (/fly|docker|k8s|terraform|infra|ops/.test(all)) {
|
|
1972
|
+
return "infra";
|
|
1701
1973
|
}
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1974
|
+
return "unknown";
|
|
1975
|
+
}
|
|
1976
|
+
function classifyCommit(commit) {
|
|
1977
|
+
const { message, filesChanged } = commit;
|
|
1978
|
+
const messageLower = message.toLowerCase();
|
|
1979
|
+
const parsed = parseConventionalCommit(message);
|
|
1980
|
+
const type = commit.type ?? parsed.type;
|
|
1981
|
+
const scope = commit.scope ?? parsed.scope;
|
|
1982
|
+
const reasons = [];
|
|
1983
|
+
let confidence = 0;
|
|
1984
|
+
const matchedKeywords = MESSAGE_KEYWORDS.filter((kw) => kw.test(messageLower));
|
|
1985
|
+
if (matchedKeywords.length > 0) {
|
|
1986
|
+
confidence += 0.3;
|
|
1987
|
+
const labels = matchedKeywords.slice(0, 3).map((kw) => kw.source.replace(/\\b/g, ""));
|
|
1988
|
+
reasons.push(`commit message contains: ${labels.join(", ")}`);
|
|
1706
1989
|
}
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1990
|
+
const matchedTypes = [];
|
|
1991
|
+
if (type && HIGH_SIGNAL_TYPES.has(type)) {
|
|
1992
|
+
matchedTypes.push(`type:${type}`);
|
|
1993
|
+
confidence += 0.35;
|
|
1994
|
+
reasons.push(`conventional commit type '${type}' is high-signal`);
|
|
1710
1995
|
}
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
getCurrentBranch() {
|
|
1716
|
-
if (this._cachedBranch === null) {
|
|
1717
|
-
this._cachedBranch = getCurrentBranch(this.workspacePath);
|
|
1718
|
-
}
|
|
1719
|
-
return this._cachedBranch;
|
|
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`);
|
|
1720
2000
|
}
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
* Returns the session and whether it fell back to global.
|
|
1725
|
-
*/
|
|
1726
|
-
getScopedLastSession() {
|
|
1727
|
-
const branch = this.getCurrentBranch();
|
|
1728
|
-
if (this._isWorktree && branch) {
|
|
1729
|
-
const scoped = this.getLastSessionForBranch(branch);
|
|
1730
|
-
if (scoped) return { session: scoped, isFallback: false };
|
|
1731
|
-
return { session: this.getLastSession(), isFallback: true };
|
|
1732
|
-
}
|
|
1733
|
-
return { session: this.getLastSession(), isFallback: false };
|
|
2001
|
+
if (parsed.breaking) {
|
|
2002
|
+
confidence += 0.4;
|
|
2003
|
+
reasons.push("breaking change indicated by ! marker");
|
|
1734
2004
|
}
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
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
|
+
}
|
|
1740
2014
|
}
|
|
1741
|
-
return this.getRecentSessions(count);
|
|
1742
2015
|
}
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
if (this._isWorktree && branch) {
|
|
1747
|
-
return this.getRecentDecisionsForBranch(branch, count);
|
|
1748
|
-
}
|
|
1749
|
-
return this.getRecentDecisions(count);
|
|
2016
|
+
if (matchedPaths.length > 0) {
|
|
2017
|
+
confidence += bestTier === "infra" ? 0.4 : 0.2;
|
|
2018
|
+
reasons.push(`commit touched: ${matchedPaths.slice(0, 3).join(", ")}`);
|
|
1750
2019
|
}
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
* - `"all"` returns no filter.
|
|
1755
|
-
* - An explicit branch name uses that.
|
|
1756
|
-
* - `undefined` auto-scopes to the current branch in a worktree, or all branches otherwise.
|
|
1757
|
-
*/
|
|
1758
|
-
resolveBranchScope(branch) {
|
|
1759
|
-
if (branch === "all") {
|
|
1760
|
-
return { effectiveBranch: void 0, scopeLabel: "all branches" };
|
|
1761
|
-
}
|
|
1762
|
-
if (branch) {
|
|
1763
|
-
return { effectiveBranch: branch, scopeLabel: `branch \`${branch}\`` };
|
|
1764
|
-
}
|
|
1765
|
-
const currentBranch = this.getCurrentBranch();
|
|
1766
|
-
if (this._isWorktree && currentBranch) {
|
|
1767
|
-
return { effectiveBranch: currentBranch, scopeLabel: `branch \`${currentBranch}\` (worktree)` };
|
|
1768
|
-
}
|
|
1769
|
-
return { effectiveBranch: void 0, scopeLabel: "all branches" };
|
|
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)");
|
|
1770
2023
|
}
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
*/
|
|
1775
|
-
parseSessions() {
|
|
1776
|
-
const raw = this.readJsonFile(
|
|
1777
|
-
this.sessionsFilePath
|
|
1778
|
-
);
|
|
1779
|
-
if (!raw) {
|
|
1780
|
-
return { sessions: [] };
|
|
1781
|
-
}
|
|
1782
|
-
if (Array.isArray(raw)) {
|
|
1783
|
-
return { sessions: raw };
|
|
1784
|
-
}
|
|
1785
|
-
return { sessions: raw.sessions ?? [], wrapperLastSessionId: raw.lastSessionId };
|
|
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)");
|
|
1786
2027
|
}
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
return { decisions: [] };
|
|
1791
|
-
}
|
|
1792
|
-
return { decisions: raw.decisions ?? [], lastDecisionId: raw.lastDecisionId };
|
|
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)");
|
|
1793
2031
|
}
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
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;
|
|
1804
2045
|
}
|
|
1805
|
-
|
|
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
|
+
}
|
|
1806
2069
|
|
|
1807
2070
|
// ../../packages/shared/src/setup.ts
|
|
1808
|
-
import
|
|
2071
|
+
import fs7 from "fs";
|
|
1809
2072
|
import os3 from "os";
|
|
1810
|
-
import
|
|
2073
|
+
import path9 from "path";
|
|
1811
2074
|
var KEEPGOING_MARKER = "@keepgoingdev/mcp-server";
|
|
1812
2075
|
var SESSION_START_HOOK = {
|
|
1813
2076
|
matcher: "",
|
|
@@ -1862,7 +2125,7 @@ function getRulesFileVersion(content) {
|
|
|
1862
2125
|
}
|
|
1863
2126
|
var STATUSLINE_CMD = "npx -y @keepgoingdev/mcp-server --statusline";
|
|
1864
2127
|
function detectClaudeDir() {
|
|
1865
|
-
return process.env.CLAUDE_CONFIG_DIR ||
|
|
2128
|
+
return process.env.CLAUDE_CONFIG_DIR || path9.join(os3.homedir(), ".claude");
|
|
1866
2129
|
}
|
|
1867
2130
|
function hasKeepGoingHook(hookEntries) {
|
|
1868
2131
|
return hookEntries.some(
|
|
@@ -1874,19 +2137,19 @@ function resolveScopePaths(scope, workspacePath, overrideClaudeDir) {
|
|
|
1874
2137
|
const claudeDir2 = overrideClaudeDir || detectClaudeDir();
|
|
1875
2138
|
return {
|
|
1876
2139
|
claudeDir: claudeDir2,
|
|
1877
|
-
settingsPath:
|
|
1878
|
-
claudeMdPath:
|
|
1879
|
-
rulesPath:
|
|
2140
|
+
settingsPath: path9.join(claudeDir2, "settings.json"),
|
|
2141
|
+
claudeMdPath: path9.join(claudeDir2, "CLAUDE.md"),
|
|
2142
|
+
rulesPath: path9.join(claudeDir2, "rules", "keepgoing.md")
|
|
1880
2143
|
};
|
|
1881
2144
|
}
|
|
1882
|
-
const claudeDir =
|
|
1883
|
-
const dotClaudeMdPath =
|
|
1884
|
-
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");
|
|
1885
2148
|
return {
|
|
1886
2149
|
claudeDir,
|
|
1887
|
-
settingsPath:
|
|
1888
|
-
claudeMdPath:
|
|
1889
|
-
rulesPath:
|
|
2150
|
+
settingsPath: path9.join(claudeDir, "settings.json"),
|
|
2151
|
+
claudeMdPath: fs7.existsSync(dotClaudeMdPath) ? dotClaudeMdPath : rootClaudeMdPath,
|
|
2152
|
+
rulesPath: path9.join(workspacePath, ".claude", "rules", "keepgoing.md")
|
|
1890
2153
|
};
|
|
1891
2154
|
}
|
|
1892
2155
|
function writeHooksToSettings(settings) {
|
|
@@ -1926,11 +2189,11 @@ function writeHooksToSettings(settings) {
|
|
|
1926
2189
|
}
|
|
1927
2190
|
function checkHookConflict(scope, workspacePath) {
|
|
1928
2191
|
const otherPaths = resolveScopePaths(scope === "user" ? "project" : "user", workspacePath);
|
|
1929
|
-
if (!
|
|
2192
|
+
if (!fs7.existsSync(otherPaths.settingsPath)) {
|
|
1930
2193
|
return null;
|
|
1931
2194
|
}
|
|
1932
2195
|
try {
|
|
1933
|
-
const otherSettings = JSON.parse(
|
|
2196
|
+
const otherSettings = JSON.parse(fs7.readFileSync(otherPaths.settingsPath, "utf-8"));
|
|
1934
2197
|
const hooks = otherSettings?.hooks;
|
|
1935
2198
|
if (!hooks) return null;
|
|
1936
2199
|
const hasConflict = Array.isArray(hooks.SessionStart) && hasKeepGoingHook(hooks.SessionStart) || Array.isArray(hooks.Stop) && hasKeepGoingHook(hooks.Stop);
|
|
@@ -1959,10 +2222,10 @@ function setupProject(options) {
|
|
|
1959
2222
|
workspacePath,
|
|
1960
2223
|
claudeDirOverride
|
|
1961
2224
|
);
|
|
1962
|
-
const scopeLabel = scope === "user" ?
|
|
2225
|
+
const scopeLabel = scope === "user" ? path9.join("~", path9.relative(os3.homedir(), claudeDir), "settings.json").replace(/\\/g, "/") : ".claude/settings.json";
|
|
1963
2226
|
let settings = {};
|
|
1964
|
-
if (
|
|
1965
|
-
settings = JSON.parse(
|
|
2227
|
+
if (fs7.existsSync(settingsPath)) {
|
|
2228
|
+
settings = JSON.parse(fs7.readFileSync(settingsPath, "utf-8"));
|
|
1966
2229
|
}
|
|
1967
2230
|
let settingsChanged = false;
|
|
1968
2231
|
if (sessionHooks) {
|
|
@@ -1993,36 +2256,36 @@ function setupProject(options) {
|
|
|
1993
2256
|
statusline?.cleanup?.();
|
|
1994
2257
|
}
|
|
1995
2258
|
if (settingsChanged) {
|
|
1996
|
-
if (!
|
|
1997
|
-
|
|
2259
|
+
if (!fs7.existsSync(claudeDir)) {
|
|
2260
|
+
fs7.mkdirSync(claudeDir, { recursive: true });
|
|
1998
2261
|
}
|
|
1999
|
-
|
|
2262
|
+
fs7.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
2000
2263
|
changed = true;
|
|
2001
2264
|
}
|
|
2002
2265
|
if (claudeMd) {
|
|
2003
|
-
const rulesDir =
|
|
2004
|
-
const rulesLabel = scope === "user" ?
|
|
2005
|
-
if (
|
|
2006
|
-
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");
|
|
2007
2270
|
const existingVersion = getRulesFileVersion(existing);
|
|
2008
2271
|
if (existingVersion === null) {
|
|
2009
2272
|
messages.push(`Rules file: Custom file found at ${rulesLabel}, skipping`);
|
|
2010
2273
|
} else if (existingVersion >= KEEPGOING_RULES_VERSION) {
|
|
2011
2274
|
messages.push(`Rules file: Already up to date (v${existingVersion}), skipped`);
|
|
2012
2275
|
} else {
|
|
2013
|
-
if (!
|
|
2014
|
-
|
|
2276
|
+
if (!fs7.existsSync(rulesDir)) {
|
|
2277
|
+
fs7.mkdirSync(rulesDir, { recursive: true });
|
|
2015
2278
|
}
|
|
2016
|
-
|
|
2279
|
+
fs7.writeFileSync(rulesPath, KEEPGOING_RULES_CONTENT);
|
|
2017
2280
|
changed = true;
|
|
2018
2281
|
messages.push(`Rules file: Updated v${existingVersion} \u2192 v${KEEPGOING_RULES_VERSION} at ${rulesLabel}`);
|
|
2019
2282
|
}
|
|
2020
2283
|
} else {
|
|
2021
|
-
const existingClaudeMd =
|
|
2022
|
-
if (!
|
|
2023
|
-
|
|
2284
|
+
const existingClaudeMd = fs7.existsSync(claudeMdPath) ? fs7.readFileSync(claudeMdPath, "utf-8") : "";
|
|
2285
|
+
if (!fs7.existsSync(rulesDir)) {
|
|
2286
|
+
fs7.mkdirSync(rulesDir, { recursive: true });
|
|
2024
2287
|
}
|
|
2025
|
-
|
|
2288
|
+
fs7.writeFileSync(rulesPath, KEEPGOING_RULES_CONTENT);
|
|
2026
2289
|
changed = true;
|
|
2027
2290
|
if (existingClaudeMd.includes("## KeepGoing")) {
|
|
2028
2291
|
const mdLabel = scope === "user" ? "~/.claude/CLAUDE.md" : "CLAUDE.md";
|
|
@@ -2458,7 +2721,7 @@ function registerGetReentryBriefing(server, reader, workspacePath) {
|
|
|
2458
2721
|
}
|
|
2459
2722
|
|
|
2460
2723
|
// src/tools/saveCheckpoint.ts
|
|
2461
|
-
import
|
|
2724
|
+
import path10 from "path";
|
|
2462
2725
|
import { z as z4 } from "zod";
|
|
2463
2726
|
function registerSaveCheckpoint(server, reader, workspacePath) {
|
|
2464
2727
|
server.tool(
|
|
@@ -2470,11 +2733,14 @@ function registerSaveCheckpoint(server, reader, workspacePath) {
|
|
|
2470
2733
|
blocker: z4.string().optional().describe("Any blocker preventing progress")
|
|
2471
2734
|
},
|
|
2472
2735
|
async ({ summary, nextStep, blocker }) => {
|
|
2736
|
+
summary = stripAgentTags(summary);
|
|
2737
|
+
nextStep = nextStep ? stripAgentTags(nextStep) : nextStep;
|
|
2738
|
+
blocker = blocker ? stripAgentTags(blocker) : blocker;
|
|
2473
2739
|
const lastSession = reader.getLastSession();
|
|
2474
2740
|
const gitBranch = getCurrentBranch(workspacePath);
|
|
2475
2741
|
const touchedFiles = getTouchedFiles(workspacePath);
|
|
2476
2742
|
const commitHashes = getCommitsSince(workspacePath, lastSession?.timestamp);
|
|
2477
|
-
const projectName =
|
|
2743
|
+
const projectName = path10.basename(resolveStorageRoot(workspacePath));
|
|
2478
2744
|
const sessionId = generateSessionId({ workspaceRoot: workspacePath, branch: gitBranch ?? void 0, worktreePath: workspacePath });
|
|
2479
2745
|
const checkpoint = createCheckpoint({
|
|
2480
2746
|
summary,
|
|
@@ -2692,25 +2958,25 @@ function registerGetCurrentTask(server, reader) {
|
|
|
2692
2958
|
import { z as z6 } from "zod";
|
|
2693
2959
|
|
|
2694
2960
|
// src/cli/migrate.ts
|
|
2695
|
-
import
|
|
2961
|
+
import fs8 from "fs";
|
|
2696
2962
|
import os4 from "os";
|
|
2697
|
-
import
|
|
2963
|
+
import path11 from "path";
|
|
2698
2964
|
var STATUSLINE_CMD2 = "npx -y @keepgoingdev/mcp-server --statusline";
|
|
2699
2965
|
function isLegacyStatusline(command) {
|
|
2700
2966
|
return !command.includes("--statusline") && command.includes("keepgoing-statusline");
|
|
2701
2967
|
}
|
|
2702
2968
|
function migrateStatusline(wsPath) {
|
|
2703
|
-
const settingsPath =
|
|
2704
|
-
if (!
|
|
2969
|
+
const settingsPath = path11.join(wsPath, ".claude", "settings.json");
|
|
2970
|
+
if (!fs8.existsSync(settingsPath)) return void 0;
|
|
2705
2971
|
try {
|
|
2706
|
-
const settings = JSON.parse(
|
|
2972
|
+
const settings = JSON.parse(fs8.readFileSync(settingsPath, "utf-8"));
|
|
2707
2973
|
const cmd = settings.statusLine?.command;
|
|
2708
2974
|
if (!cmd || !isLegacyStatusline(cmd)) return void 0;
|
|
2709
2975
|
settings.statusLine = {
|
|
2710
2976
|
type: "command",
|
|
2711
2977
|
command: STATUSLINE_CMD2
|
|
2712
2978
|
};
|
|
2713
|
-
|
|
2979
|
+
fs8.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
2714
2980
|
cleanupLegacyScript();
|
|
2715
2981
|
return "[KeepGoing] Migrated statusline to auto-updating command (restart Claude Code to apply)";
|
|
2716
2982
|
} catch {
|
|
@@ -2718,10 +2984,10 @@ function migrateStatusline(wsPath) {
|
|
|
2718
2984
|
}
|
|
2719
2985
|
}
|
|
2720
2986
|
function cleanupLegacyScript() {
|
|
2721
|
-
const legacyScript =
|
|
2722
|
-
if (
|
|
2987
|
+
const legacyScript = path11.join(os4.homedir(), ".claude", "keepgoing-statusline.sh");
|
|
2988
|
+
if (fs8.existsSync(legacyScript)) {
|
|
2723
2989
|
try {
|
|
2724
|
-
|
|
2990
|
+
fs8.unlinkSync(legacyScript);
|
|
2725
2991
|
} catch {
|
|
2726
2992
|
}
|
|
2727
2993
|
}
|
|
@@ -2954,6 +3220,87 @@ function registerContinueOn(server, reader, workspacePath) {
|
|
|
2954
3220
|
);
|
|
2955
3221
|
}
|
|
2956
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
|
+
|
|
2957
3304
|
// src/prompts/resume.ts
|
|
2958
3305
|
function registerResumePrompt(server) {
|
|
2959
3306
|
server.prompt(
|
|
@@ -3119,7 +3466,7 @@ async function handlePrintCurrent() {
|
|
|
3119
3466
|
}
|
|
3120
3467
|
|
|
3121
3468
|
// src/cli/saveCheckpoint.ts
|
|
3122
|
-
import
|
|
3469
|
+
import path12 from "path";
|
|
3123
3470
|
async function handleSaveCheckpoint() {
|
|
3124
3471
|
const wsPath = resolveWsPath();
|
|
3125
3472
|
const reader = new KeepGoingReader(wsPath);
|
|
@@ -3147,9 +3494,9 @@ async function handleSaveCheckpoint() {
|
|
|
3147
3494
|
sessionStartTime: lastSession?.timestamp ?? now,
|
|
3148
3495
|
lastActivityTime: now
|
|
3149
3496
|
});
|
|
3150
|
-
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(", ")}`;
|
|
3151
3498
|
const nextStep = buildSmartNextStep(events);
|
|
3152
|
-
const projectName =
|
|
3499
|
+
const projectName = path12.basename(resolveStorageRoot(wsPath));
|
|
3153
3500
|
const sessionId = generateSessionId({ workspaceRoot: wsPath, branch: gitBranch ?? void 0, worktreePath: wsPath });
|
|
3154
3501
|
const checkpoint = createCheckpoint({
|
|
3155
3502
|
summary,
|
|
@@ -3194,7 +3541,7 @@ async function handleSaveCheckpoint() {
|
|
|
3194
3541
|
}
|
|
3195
3542
|
|
|
3196
3543
|
// src/cli/transcriptUtils.ts
|
|
3197
|
-
import
|
|
3544
|
+
import fs9 from "fs";
|
|
3198
3545
|
var TAIL_READ_BYTES = 32768;
|
|
3199
3546
|
var LATEST_LABEL_READ_BYTES = 65536;
|
|
3200
3547
|
var TOOL_VERB_MAP = {
|
|
@@ -3251,9 +3598,9 @@ function isAssistantEntry(entry) {
|
|
|
3251
3598
|
return entry.message?.role === "assistant";
|
|
3252
3599
|
}
|
|
3253
3600
|
function extractSessionLabel(transcriptPath) {
|
|
3254
|
-
if (!transcriptPath || !
|
|
3601
|
+
if (!transcriptPath || !fs9.existsSync(transcriptPath)) return null;
|
|
3255
3602
|
try {
|
|
3256
|
-
const raw =
|
|
3603
|
+
const raw = fs9.readFileSync(transcriptPath, "utf-8");
|
|
3257
3604
|
for (const line of raw.split("\n")) {
|
|
3258
3605
|
const trimmed = line.trim();
|
|
3259
3606
|
if (!trimmed) continue;
|
|
@@ -3266,7 +3613,9 @@ function extractSessionLabel(transcriptPath) {
|
|
|
3266
3613
|
if (!isUserEntry(entry)) continue;
|
|
3267
3614
|
let text = extractTextFromContent(entry.message?.content);
|
|
3268
3615
|
if (!text) continue;
|
|
3269
|
-
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;
|
|
3270
3619
|
text = text.replace(/@[\w./\-]+/g, "").trim();
|
|
3271
3620
|
text = text.replace(FILLER_PREFIX_RE, "").trim();
|
|
3272
3621
|
text = text.replace(MARKDOWN_HEADING_RE, "").trim();
|
|
@@ -3282,19 +3631,19 @@ function extractSessionLabel(transcriptPath) {
|
|
|
3282
3631
|
return null;
|
|
3283
3632
|
}
|
|
3284
3633
|
function extractLatestUserLabel(transcriptPath) {
|
|
3285
|
-
if (!transcriptPath || !
|
|
3634
|
+
if (!transcriptPath || !fs9.existsSync(transcriptPath)) return null;
|
|
3286
3635
|
try {
|
|
3287
|
-
const stat =
|
|
3636
|
+
const stat = fs9.statSync(transcriptPath);
|
|
3288
3637
|
const fileSize = stat.size;
|
|
3289
3638
|
if (fileSize === 0) return null;
|
|
3290
3639
|
const readSize = Math.min(fileSize, LATEST_LABEL_READ_BYTES);
|
|
3291
3640
|
const offset = fileSize - readSize;
|
|
3292
3641
|
const buf = Buffer.alloc(readSize);
|
|
3293
|
-
const fd =
|
|
3642
|
+
const fd = fs9.openSync(transcriptPath, "r");
|
|
3294
3643
|
try {
|
|
3295
|
-
|
|
3644
|
+
fs9.readSync(fd, buf, 0, readSize, offset);
|
|
3296
3645
|
} finally {
|
|
3297
|
-
|
|
3646
|
+
fs9.closeSync(fd);
|
|
3298
3647
|
}
|
|
3299
3648
|
const tail = buf.toString("utf-8");
|
|
3300
3649
|
const lines = tail.split("\n").reverse();
|
|
@@ -3310,7 +3659,9 @@ function extractLatestUserLabel(transcriptPath) {
|
|
|
3310
3659
|
if (!isUserEntry(entry)) continue;
|
|
3311
3660
|
let text = extractTextFromContent(entry.message?.content);
|
|
3312
3661
|
if (!text) continue;
|
|
3313
|
-
if (text.startsWith("[") || /^<[a-z][\w-]
|
|
3662
|
+
if (text.startsWith("[") || /^<[a-z][\w-]*[\s>]/.test(text)) continue;
|
|
3663
|
+
text = stripAgentTags(text);
|
|
3664
|
+
if (!text) continue;
|
|
3314
3665
|
text = text.replace(/@[\w./\-]+/g, "").trim();
|
|
3315
3666
|
text = text.replace(FILLER_PREFIX_RE, "").trim();
|
|
3316
3667
|
text = text.replace(MARKDOWN_HEADING_RE, "").trim();
|
|
@@ -3326,19 +3677,19 @@ function extractLatestUserLabel(transcriptPath) {
|
|
|
3326
3677
|
return null;
|
|
3327
3678
|
}
|
|
3328
3679
|
function extractCurrentAction(transcriptPath) {
|
|
3329
|
-
if (!transcriptPath || !
|
|
3680
|
+
if (!transcriptPath || !fs9.existsSync(transcriptPath)) return null;
|
|
3330
3681
|
try {
|
|
3331
|
-
const stat =
|
|
3682
|
+
const stat = fs9.statSync(transcriptPath);
|
|
3332
3683
|
const fileSize = stat.size;
|
|
3333
3684
|
if (fileSize === 0) return null;
|
|
3334
3685
|
const readSize = Math.min(fileSize, TAIL_READ_BYTES);
|
|
3335
3686
|
const offset = fileSize - readSize;
|
|
3336
3687
|
const buf = Buffer.alloc(readSize);
|
|
3337
|
-
const fd =
|
|
3688
|
+
const fd = fs9.openSync(transcriptPath, "r");
|
|
3338
3689
|
try {
|
|
3339
|
-
|
|
3690
|
+
fs9.readSync(fd, buf, 0, readSize, offset);
|
|
3340
3691
|
} finally {
|
|
3341
|
-
|
|
3692
|
+
fs9.closeSync(fd);
|
|
3342
3693
|
}
|
|
3343
3694
|
const tail = buf.toString("utf-8");
|
|
3344
3695
|
const lines = tail.split("\n").reverse();
|
|
@@ -3373,6 +3724,8 @@ async function handleUpdateTask() {
|
|
|
3373
3724
|
if (payloadStr) {
|
|
3374
3725
|
try {
|
|
3375
3726
|
const payload = JSON.parse(payloadStr);
|
|
3727
|
+
if (payload.taskSummary) payload.taskSummary = stripAgentTags(payload.taskSummary);
|
|
3728
|
+
if (payload.sessionLabel) payload.sessionLabel = stripAgentTags(payload.sessionLabel);
|
|
3376
3729
|
const writer = new KeepGoingWriter(wsPath);
|
|
3377
3730
|
const branch = payload.branch ?? getCurrentBranch(wsPath) ?? void 0;
|
|
3378
3731
|
const task = {
|
|
@@ -3440,8 +3793,8 @@ async function handleUpdateTaskFromHook() {
|
|
|
3440
3793
|
}
|
|
3441
3794
|
|
|
3442
3795
|
// src/cli/statusline.ts
|
|
3443
|
-
import
|
|
3444
|
-
import
|
|
3796
|
+
import fs10 from "fs";
|
|
3797
|
+
import path13 from "path";
|
|
3445
3798
|
var STDIN_TIMEOUT_MS2 = 3e3;
|
|
3446
3799
|
async function handleStatusline() {
|
|
3447
3800
|
const chunks = [];
|
|
@@ -3471,9 +3824,9 @@ async function handleStatusline() {
|
|
|
3471
3824
|
if (!label) {
|
|
3472
3825
|
try {
|
|
3473
3826
|
const gitRoot = findGitRoot(dir);
|
|
3474
|
-
const tasksFile =
|
|
3475
|
-
if (
|
|
3476
|
-
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"));
|
|
3477
3830
|
const tasks = pruneStaleTasks(data.tasks ?? []);
|
|
3478
3831
|
const match = sessionId ? tasks.find((t) => t.sessionId === sessionId) : void 0;
|
|
3479
3832
|
if (match?.sessionLabel) {
|
|
@@ -3598,6 +3951,8 @@ if (flag) {
|
|
|
3598
3951
|
registerGetCurrentTask(server, reader);
|
|
3599
3952
|
registerSaveCheckpoint(server, reader, workspacePath);
|
|
3600
3953
|
registerContinueOn(server, reader, workspacePath);
|
|
3954
|
+
registerGetContextSnapshot(server, reader, workspacePath);
|
|
3955
|
+
registerGetWhatsHot(server);
|
|
3601
3956
|
registerSetupProject(server, workspacePath);
|
|
3602
3957
|
registerActivateLicense(server);
|
|
3603
3958
|
registerDeactivateLicense(server);
|