@keepgoingdev/mcp-server 0.7.2 → 0.9.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 +1107 -629
- 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) {
|
|
@@ -326,7 +339,8 @@ function generateEnrichedBriefing(opts) {
|
|
|
326
339
|
timestamp: s.timestamp,
|
|
327
340
|
summary: s.summary || "",
|
|
328
341
|
nextStep: s.nextStep || "",
|
|
329
|
-
branch: s.gitBranch
|
|
342
|
+
branch: s.gitBranch,
|
|
343
|
+
sessionPhase: s.sessionPhase
|
|
330
344
|
}));
|
|
331
345
|
}
|
|
332
346
|
if (opts.recentCommits && opts.recentCommits.length > 0) {
|
|
@@ -359,10 +373,13 @@ function buildCurrentFocus(lastSession, projectState, gitBranch) {
|
|
|
359
373
|
function buildRecentActivity(lastSession, recentSessions, recentCommitMessages) {
|
|
360
374
|
const parts = [];
|
|
361
375
|
const sessionCount = recentSessions.length;
|
|
376
|
+
const planCount = recentSessions.filter((s) => s.sessionPhase === "planning").length;
|
|
362
377
|
if (sessionCount > 1) {
|
|
363
|
-
|
|
378
|
+
const planSuffix = planCount > 0 ? ` (${planCount} plan-only)` : "";
|
|
379
|
+
parts.push(`${sessionCount} recent sessions${planSuffix}`);
|
|
364
380
|
} else if (sessionCount === 1) {
|
|
365
|
-
|
|
381
|
+
const planSuffix = planCount === 1 ? " (plan-only)" : "";
|
|
382
|
+
parts.push(`1 recent session${planSuffix}`);
|
|
366
383
|
}
|
|
367
384
|
if (lastSession.summary) {
|
|
368
385
|
const brief = lastSession.summary.length > 120 ? lastSession.summary.slice(0, 117) + "..." : lastSession.summary;
|
|
@@ -561,7 +578,8 @@ function formatEnrichedBriefing(briefing) {
|
|
|
561
578
|
for (const s of briefing.sessionHistory) {
|
|
562
579
|
const relTime = formatRelativeTime(s.timestamp);
|
|
563
580
|
const branch = s.branch ? ` (${s.branch})` : "";
|
|
564
|
-
|
|
581
|
+
const planTag = s.sessionPhase === "planning" ? " (plan)" : "";
|
|
582
|
+
lines.push(`- **${relTime}${branch}${planTag}:** ${s.summary || "No summary"}. Next: ${s.nextStep || "Not specified"}`);
|
|
565
583
|
}
|
|
566
584
|
}
|
|
567
585
|
if (briefing.recentCommits && briefing.recentCommits.length > 0) {
|
|
@@ -706,22 +724,160 @@ function formatContinueOnPrompt(context, options) {
|
|
|
706
724
|
return result;
|
|
707
725
|
}
|
|
708
726
|
|
|
709
|
-
// ../../packages/shared/src/
|
|
710
|
-
import
|
|
711
|
-
import
|
|
712
|
-
import { randomUUID as randomUUID2, createHash } from "crypto";
|
|
727
|
+
// ../../packages/shared/src/reader.ts
|
|
728
|
+
import fs4 from "fs";
|
|
729
|
+
import path6 from "path";
|
|
713
730
|
|
|
714
|
-
// ../../packages/shared/src/
|
|
731
|
+
// ../../packages/shared/src/license.ts
|
|
732
|
+
import crypto from "crypto";
|
|
715
733
|
import fs from "fs";
|
|
716
734
|
import os from "os";
|
|
717
735
|
import path3 from "path";
|
|
718
|
-
var
|
|
719
|
-
var
|
|
736
|
+
var LICENSE_FILE = "license.json";
|
|
737
|
+
var DEVICE_ID_FILE = "device-id";
|
|
738
|
+
function getGlobalLicenseDir() {
|
|
739
|
+
return path3.join(os.homedir(), ".keepgoing");
|
|
740
|
+
}
|
|
741
|
+
function getGlobalLicensePath() {
|
|
742
|
+
return path3.join(getGlobalLicenseDir(), LICENSE_FILE);
|
|
743
|
+
}
|
|
744
|
+
function getDeviceId() {
|
|
745
|
+
const dir = getGlobalLicenseDir();
|
|
746
|
+
const filePath = path3.join(dir, DEVICE_ID_FILE);
|
|
747
|
+
try {
|
|
748
|
+
const existing = fs.readFileSync(filePath, "utf-8").trim();
|
|
749
|
+
if (existing) return existing;
|
|
750
|
+
} catch {
|
|
751
|
+
}
|
|
752
|
+
const id = crypto.randomUUID();
|
|
753
|
+
if (!fs.existsSync(dir)) {
|
|
754
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
755
|
+
}
|
|
756
|
+
fs.writeFileSync(filePath, id, "utf-8");
|
|
757
|
+
return id;
|
|
758
|
+
}
|
|
759
|
+
var DECISION_DETECTION_VARIANT_ID = 1361527;
|
|
760
|
+
var SESSION_AWARENESS_VARIANT_ID = 1366510;
|
|
761
|
+
var TEST_DECISION_DETECTION_VARIANT_ID = 1345647;
|
|
762
|
+
var TEST_SESSION_AWARENESS_VARIANT_ID = 1365992;
|
|
763
|
+
var VARIANT_FEATURE_MAP = {
|
|
764
|
+
[DECISION_DETECTION_VARIANT_ID]: ["decisions"],
|
|
765
|
+
[SESSION_AWARENESS_VARIANT_ID]: ["session-awareness"],
|
|
766
|
+
[TEST_DECISION_DETECTION_VARIANT_ID]: ["decisions"],
|
|
767
|
+
[TEST_SESSION_AWARENESS_VARIANT_ID]: ["session-awareness"]
|
|
768
|
+
// Future bundle: [BUNDLE_VARIANT_ID]: ['decisions', 'session-awareness'],
|
|
769
|
+
};
|
|
770
|
+
var KNOWN_VARIANT_IDS = new Set(Object.keys(VARIANT_FEATURE_MAP).map(Number));
|
|
771
|
+
function getVariantLabel(variantId) {
|
|
772
|
+
const features = VARIANT_FEATURE_MAP[variantId];
|
|
773
|
+
if (!features) return "Unknown Add-on";
|
|
774
|
+
if (features.includes("decisions") && features.includes("session-awareness")) return "Pro Bundle";
|
|
775
|
+
if (features.includes("decisions")) return "Decision Detection";
|
|
776
|
+
if (features.includes("session-awareness")) return "Session Awareness";
|
|
777
|
+
return "Pro Add-on";
|
|
778
|
+
}
|
|
779
|
+
var _cachedStore;
|
|
780
|
+
var _cacheTimestamp = 0;
|
|
781
|
+
var LICENSE_CACHE_TTL_MS = 2e3;
|
|
782
|
+
function readLicenseStore() {
|
|
783
|
+
const now = Date.now();
|
|
784
|
+
if (_cachedStore && now - _cacheTimestamp < LICENSE_CACHE_TTL_MS) {
|
|
785
|
+
return _cachedStore;
|
|
786
|
+
}
|
|
787
|
+
const licensePath = getGlobalLicensePath();
|
|
788
|
+
let store;
|
|
789
|
+
try {
|
|
790
|
+
if (!fs.existsSync(licensePath)) {
|
|
791
|
+
store = { version: 2, licenses: [] };
|
|
792
|
+
} else {
|
|
793
|
+
const raw = fs.readFileSync(licensePath, "utf-8");
|
|
794
|
+
const data = JSON.parse(raw);
|
|
795
|
+
if (data?.version === 2 && Array.isArray(data.licenses)) {
|
|
796
|
+
store = data;
|
|
797
|
+
} else {
|
|
798
|
+
store = { version: 2, licenses: [] };
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
} catch {
|
|
802
|
+
store = { version: 2, licenses: [] };
|
|
803
|
+
}
|
|
804
|
+
_cachedStore = store;
|
|
805
|
+
_cacheTimestamp = now;
|
|
806
|
+
return store;
|
|
807
|
+
}
|
|
808
|
+
function writeLicenseStore(store) {
|
|
809
|
+
const dirPath = getGlobalLicenseDir();
|
|
810
|
+
if (!fs.existsSync(dirPath)) {
|
|
811
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
812
|
+
}
|
|
813
|
+
const licensePath = path3.join(dirPath, LICENSE_FILE);
|
|
814
|
+
fs.writeFileSync(licensePath, JSON.stringify(store, null, 2), "utf-8");
|
|
815
|
+
_cachedStore = store;
|
|
816
|
+
_cacheTimestamp = Date.now();
|
|
817
|
+
}
|
|
818
|
+
function addLicenseEntry(entry) {
|
|
819
|
+
const store = readLicenseStore();
|
|
820
|
+
const idx = store.licenses.findIndex((l) => l.licenseKey === entry.licenseKey);
|
|
821
|
+
if (idx >= 0) {
|
|
822
|
+
store.licenses[idx] = entry;
|
|
823
|
+
} else {
|
|
824
|
+
store.licenses.push(entry);
|
|
825
|
+
}
|
|
826
|
+
writeLicenseStore(store);
|
|
827
|
+
}
|
|
828
|
+
function removeLicenseEntry(licenseKey) {
|
|
829
|
+
const store = readLicenseStore();
|
|
830
|
+
store.licenses = store.licenses.filter((l) => l.licenseKey !== licenseKey);
|
|
831
|
+
writeLicenseStore(store);
|
|
832
|
+
}
|
|
833
|
+
function getActiveLicenses() {
|
|
834
|
+
return readLicenseStore().licenses.filter((l) => l.status === "active");
|
|
835
|
+
}
|
|
836
|
+
function getLicenseForFeature(feature) {
|
|
837
|
+
const active = getActiveLicenses();
|
|
838
|
+
return active.find((l) => {
|
|
839
|
+
const features = VARIANT_FEATURE_MAP[l.variantId];
|
|
840
|
+
return features?.includes(feature);
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
function getAllLicensesNeedingRevalidation() {
|
|
844
|
+
return getActiveLicenses().filter((l) => needsRevalidation(l));
|
|
845
|
+
}
|
|
846
|
+
var REVALIDATION_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
|
|
847
|
+
function needsRevalidation(entry) {
|
|
848
|
+
const lastValidated = new Date(entry.lastValidatedAt).getTime();
|
|
849
|
+
return Date.now() - lastValidated > REVALIDATION_THRESHOLD_MS;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// ../../packages/shared/src/storage.ts
|
|
853
|
+
import fs3 from "fs";
|
|
854
|
+
import path5 from "path";
|
|
855
|
+
import { randomUUID as randomUUID2, createHash } from "crypto";
|
|
856
|
+
|
|
857
|
+
// ../../packages/shared/src/registry.ts
|
|
858
|
+
import fs2 from "fs";
|
|
859
|
+
import os2 from "os";
|
|
860
|
+
import path4 from "path";
|
|
861
|
+
var KEEPGOING_DIR = path4.join(os2.homedir(), ".keepgoing");
|
|
862
|
+
var KNOWN_PROJECTS_FILE = path4.join(KEEPGOING_DIR, "known-projects.json");
|
|
863
|
+
var TRAY_CONFIG_FILE = path4.join(KEEPGOING_DIR, "tray-config.json");
|
|
720
864
|
var STALE_PROJECT_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
865
|
+
function readTrayConfigProjects() {
|
|
866
|
+
try {
|
|
867
|
+
if (fs2.existsSync(TRAY_CONFIG_FILE)) {
|
|
868
|
+
const raw = JSON.parse(fs2.readFileSync(TRAY_CONFIG_FILE, "utf-8"));
|
|
869
|
+
if (raw && Array.isArray(raw.projects)) {
|
|
870
|
+
return raw.projects.filter((p) => typeof p === "string");
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
} catch {
|
|
874
|
+
}
|
|
875
|
+
return [];
|
|
876
|
+
}
|
|
721
877
|
function readKnownProjects() {
|
|
722
878
|
try {
|
|
723
|
-
if (
|
|
724
|
-
const raw = JSON.parse(
|
|
879
|
+
if (fs2.existsSync(KNOWN_PROJECTS_FILE)) {
|
|
880
|
+
const raw = JSON.parse(fs2.readFileSync(KNOWN_PROJECTS_FILE, "utf-8"));
|
|
725
881
|
if (raw && Array.isArray(raw.projects)) {
|
|
726
882
|
return raw;
|
|
727
883
|
}
|
|
@@ -731,18 +887,18 @@ function readKnownProjects() {
|
|
|
731
887
|
return { version: 1, projects: [] };
|
|
732
888
|
}
|
|
733
889
|
function writeKnownProjects(data) {
|
|
734
|
-
if (!
|
|
735
|
-
|
|
890
|
+
if (!fs2.existsSync(KEEPGOING_DIR)) {
|
|
891
|
+
fs2.mkdirSync(KEEPGOING_DIR, { recursive: true });
|
|
736
892
|
}
|
|
737
893
|
const tmpFile = KNOWN_PROJECTS_FILE + ".tmp";
|
|
738
|
-
|
|
739
|
-
|
|
894
|
+
fs2.writeFileSync(tmpFile, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
895
|
+
fs2.renameSync(tmpFile, KNOWN_PROJECTS_FILE);
|
|
740
896
|
}
|
|
741
897
|
function registerProject(projectPath, projectName) {
|
|
742
898
|
try {
|
|
743
899
|
const data = readKnownProjects();
|
|
744
900
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
745
|
-
const name = projectName ||
|
|
901
|
+
const name = projectName || path4.basename(projectPath);
|
|
746
902
|
const existingIdx = data.projects.findIndex((p) => p.path === projectPath);
|
|
747
903
|
if (existingIdx >= 0) {
|
|
748
904
|
data.projects[existingIdx].lastSeen = now;
|
|
@@ -781,23 +937,23 @@ var KeepGoingWriter = class {
|
|
|
781
937
|
currentTasksFilePath;
|
|
782
938
|
constructor(workspacePath) {
|
|
783
939
|
const mainRoot = resolveStorageRoot(workspacePath);
|
|
784
|
-
this.storagePath =
|
|
785
|
-
this.sessionsFilePath =
|
|
786
|
-
this.stateFilePath =
|
|
787
|
-
this.metaFilePath =
|
|
788
|
-
this.currentTasksFilePath =
|
|
940
|
+
this.storagePath = path5.join(mainRoot, STORAGE_DIR);
|
|
941
|
+
this.sessionsFilePath = path5.join(this.storagePath, SESSIONS_FILE);
|
|
942
|
+
this.stateFilePath = path5.join(this.storagePath, STATE_FILE);
|
|
943
|
+
this.metaFilePath = path5.join(this.storagePath, META_FILE);
|
|
944
|
+
this.currentTasksFilePath = path5.join(this.storagePath, CURRENT_TASKS_FILE);
|
|
789
945
|
}
|
|
790
946
|
ensureDir() {
|
|
791
|
-
if (!
|
|
792
|
-
|
|
947
|
+
if (!fs3.existsSync(this.storagePath)) {
|
|
948
|
+
fs3.mkdirSync(this.storagePath, { recursive: true });
|
|
793
949
|
}
|
|
794
950
|
}
|
|
795
951
|
saveCheckpoint(checkpoint, projectName) {
|
|
796
952
|
this.ensureDir();
|
|
797
953
|
let sessionsData;
|
|
798
954
|
try {
|
|
799
|
-
if (
|
|
800
|
-
const raw = JSON.parse(
|
|
955
|
+
if (fs3.existsSync(this.sessionsFilePath)) {
|
|
956
|
+
const raw = JSON.parse(fs3.readFileSync(this.sessionsFilePath, "utf-8"));
|
|
801
957
|
if (Array.isArray(raw)) {
|
|
802
958
|
sessionsData = { version: 1, project: projectName, sessions: raw };
|
|
803
959
|
} else {
|
|
@@ -815,13 +971,13 @@ var KeepGoingWriter = class {
|
|
|
815
971
|
if (sessionsData.sessions.length > MAX_SESSIONS) {
|
|
816
972
|
sessionsData.sessions = sessionsData.sessions.slice(-MAX_SESSIONS);
|
|
817
973
|
}
|
|
818
|
-
|
|
974
|
+
fs3.writeFileSync(this.sessionsFilePath, JSON.stringify(sessionsData, null, 2), "utf-8");
|
|
819
975
|
const state = {
|
|
820
976
|
lastSessionId: checkpoint.id,
|
|
821
977
|
lastKnownBranch: checkpoint.gitBranch,
|
|
822
978
|
lastActivityAt: checkpoint.timestamp
|
|
823
979
|
};
|
|
824
|
-
|
|
980
|
+
fs3.writeFileSync(this.stateFilePath, JSON.stringify(state, null, 2), "utf-8");
|
|
825
981
|
this.updateMeta(checkpoint.timestamp);
|
|
826
982
|
const mainRoot = resolveStorageRoot(this.storagePath);
|
|
827
983
|
registerProject(mainRoot, projectName);
|
|
@@ -829,8 +985,8 @@ var KeepGoingWriter = class {
|
|
|
829
985
|
updateMeta(timestamp) {
|
|
830
986
|
let meta;
|
|
831
987
|
try {
|
|
832
|
-
if (
|
|
833
|
-
meta = JSON.parse(
|
|
988
|
+
if (fs3.existsSync(this.metaFilePath)) {
|
|
989
|
+
meta = JSON.parse(fs3.readFileSync(this.metaFilePath, "utf-8"));
|
|
834
990
|
meta.lastUpdated = timestamp;
|
|
835
991
|
} else {
|
|
836
992
|
meta = {
|
|
@@ -846,7 +1002,28 @@ var KeepGoingWriter = class {
|
|
|
846
1002
|
lastUpdated: timestamp
|
|
847
1003
|
};
|
|
848
1004
|
}
|
|
849
|
-
|
|
1005
|
+
fs3.writeFileSync(this.metaFilePath, JSON.stringify(meta, null, 2), "utf-8");
|
|
1006
|
+
}
|
|
1007
|
+
// ---------------------------------------------------------------------------
|
|
1008
|
+
// Activity signals API
|
|
1009
|
+
// ---------------------------------------------------------------------------
|
|
1010
|
+
/**
|
|
1011
|
+
* Writes activity signals to state.json for momentum computation.
|
|
1012
|
+
* Performs a shallow merge: provided fields overwrite existing ones,
|
|
1013
|
+
* fields not provided are preserved.
|
|
1014
|
+
*/
|
|
1015
|
+
writeActivitySignal(signal) {
|
|
1016
|
+
this.ensureDir();
|
|
1017
|
+
let state = {};
|
|
1018
|
+
try {
|
|
1019
|
+
if (fs3.existsSync(this.stateFilePath)) {
|
|
1020
|
+
state = JSON.parse(fs3.readFileSync(this.stateFilePath, "utf-8"));
|
|
1021
|
+
}
|
|
1022
|
+
} catch {
|
|
1023
|
+
state = {};
|
|
1024
|
+
}
|
|
1025
|
+
state.activitySignals = { ...state.activitySignals, ...signal };
|
|
1026
|
+
fs3.writeFileSync(this.stateFilePath, JSON.stringify(state, null, 2), "utf-8");
|
|
850
1027
|
}
|
|
851
1028
|
// ---------------------------------------------------------------------------
|
|
852
1029
|
// Multi-session API
|
|
@@ -854,8 +1031,8 @@ var KeepGoingWriter = class {
|
|
|
854
1031
|
/** Read all current tasks from current-tasks.json. Auto-prunes stale sessions. */
|
|
855
1032
|
readCurrentTasks() {
|
|
856
1033
|
try {
|
|
857
|
-
if (
|
|
858
|
-
const raw = JSON.parse(
|
|
1034
|
+
if (fs3.existsSync(this.currentTasksFilePath)) {
|
|
1035
|
+
const raw = JSON.parse(fs3.readFileSync(this.currentTasksFilePath, "utf-8"));
|
|
859
1036
|
const tasks = Array.isArray(raw) ? raw : raw.tasks ?? [];
|
|
860
1037
|
return this.pruneStale(tasks);
|
|
861
1038
|
}
|
|
@@ -876,6 +1053,8 @@ var KeepGoingWriter = class {
|
|
|
876
1053
|
/** Core upsert logic: merges the update into current-tasks.json and returns the pruned task list. */
|
|
877
1054
|
upsertSessionCore(update) {
|
|
878
1055
|
this.ensureDir();
|
|
1056
|
+
if (update.taskSummary) update.taskSummary = stripAgentTags(update.taskSummary);
|
|
1057
|
+
if (update.sessionLabel) update.sessionLabel = stripAgentTags(update.sessionLabel);
|
|
879
1058
|
const sessionId = update.sessionId || generateSessionId(update);
|
|
880
1059
|
const tasks = this.readAllTasksRaw();
|
|
881
1060
|
const existingIdx = tasks.findIndex((t) => t.sessionId === sessionId);
|
|
@@ -910,8 +1089,8 @@ var KeepGoingWriter = class {
|
|
|
910
1089
|
// ---------------------------------------------------------------------------
|
|
911
1090
|
readAllTasksRaw() {
|
|
912
1091
|
try {
|
|
913
|
-
if (
|
|
914
|
-
const raw = JSON.parse(
|
|
1092
|
+
if (fs3.existsSync(this.currentTasksFilePath)) {
|
|
1093
|
+
const raw = JSON.parse(fs3.readFileSync(this.currentTasksFilePath, "utf-8"));
|
|
915
1094
|
return Array.isArray(raw) ? [...raw] : [...raw.tasks ?? []];
|
|
916
1095
|
}
|
|
917
1096
|
} catch {
|
|
@@ -923,7 +1102,7 @@ var KeepGoingWriter = class {
|
|
|
923
1102
|
}
|
|
924
1103
|
writeTasksFile(tasks) {
|
|
925
1104
|
const data = { version: 1, tasks };
|
|
926
|
-
|
|
1105
|
+
fs3.writeFileSync(this.currentTasksFilePath, JSON.stringify(data, null, 2), "utf-8");
|
|
927
1106
|
}
|
|
928
1107
|
};
|
|
929
1108
|
function generateSessionId(context) {
|
|
@@ -939,57 +1118,337 @@ function generateSessionId(context) {
|
|
|
939
1118
|
return `ses_${hash}`;
|
|
940
1119
|
}
|
|
941
1120
|
|
|
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
|
-
|
|
1121
|
+
// ../../packages/shared/src/reader.ts
|
|
1122
|
+
var STORAGE_DIR2 = ".keepgoing";
|
|
1123
|
+
var META_FILE2 = "meta.json";
|
|
1124
|
+
var SESSIONS_FILE2 = "sessions.json";
|
|
1125
|
+
var DECISIONS_FILE = "decisions.json";
|
|
1126
|
+
var STATE_FILE2 = "state.json";
|
|
1127
|
+
var CURRENT_TASKS_FILE2 = "current-tasks.json";
|
|
1128
|
+
var KeepGoingReader = class {
|
|
1129
|
+
workspacePath;
|
|
1130
|
+
storagePath;
|
|
1131
|
+
metaFilePath;
|
|
1132
|
+
sessionsFilePath;
|
|
1133
|
+
decisionsFilePath;
|
|
1134
|
+
stateFilePath;
|
|
1135
|
+
currentTasksFilePath;
|
|
1136
|
+
_isWorktree;
|
|
1137
|
+
_cachedBranch = null;
|
|
1138
|
+
// null = not yet resolved
|
|
1139
|
+
constructor(workspacePath) {
|
|
1140
|
+
this.workspacePath = workspacePath;
|
|
1141
|
+
const mainRoot = resolveStorageRoot(workspacePath);
|
|
1142
|
+
this._isWorktree = mainRoot !== workspacePath;
|
|
1143
|
+
this.storagePath = path6.join(mainRoot, STORAGE_DIR2);
|
|
1144
|
+
this.metaFilePath = path6.join(this.storagePath, META_FILE2);
|
|
1145
|
+
this.sessionsFilePath = path6.join(this.storagePath, SESSIONS_FILE2);
|
|
1146
|
+
this.decisionsFilePath = path6.join(this.storagePath, DECISIONS_FILE);
|
|
1147
|
+
this.stateFilePath = path6.join(this.storagePath, STATE_FILE2);
|
|
1148
|
+
this.currentTasksFilePath = path6.join(this.storagePath, CURRENT_TASKS_FILE2);
|
|
1149
|
+
}
|
|
1150
|
+
/** Check if .keepgoing/ directory exists. */
|
|
1151
|
+
exists() {
|
|
1152
|
+
return fs4.existsSync(this.storagePath);
|
|
1153
|
+
}
|
|
1154
|
+
/** Read state.json, returns undefined if missing or corrupt. */
|
|
1155
|
+
getState() {
|
|
1156
|
+
return this.readJsonFile(this.stateFilePath);
|
|
1157
|
+
}
|
|
1158
|
+
/** Read meta.json, returns undefined if missing or corrupt. */
|
|
1159
|
+
getMeta() {
|
|
1160
|
+
return this.readJsonFile(this.metaFilePath);
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* Read sessions from sessions.json.
|
|
1164
|
+
* Handles both formats:
|
|
1165
|
+
* - Flat array: SessionCheckpoint[] (from ProjectStorage)
|
|
1166
|
+
* - Wrapper object: ProjectSessions (from SessionStorage)
|
|
1167
|
+
*/
|
|
1168
|
+
getSessions() {
|
|
1169
|
+
return this.parseSessions().sessions;
|
|
1170
|
+
}
|
|
1171
|
+
/**
|
|
1172
|
+
* Get the most recent session checkpoint.
|
|
1173
|
+
* Uses state.lastSessionId if available, falls back to last in array.
|
|
1174
|
+
*/
|
|
1175
|
+
getLastSession() {
|
|
1176
|
+
const { sessions, wrapperLastSessionId } = this.parseSessions();
|
|
1177
|
+
if (sessions.length === 0) {
|
|
1178
|
+
return void 0;
|
|
1179
|
+
}
|
|
1180
|
+
const state = this.getState();
|
|
1181
|
+
if (state?.lastSessionId) {
|
|
1182
|
+
const found = sessions.find((s) => s.id === state.lastSessionId);
|
|
1183
|
+
if (found) {
|
|
1184
|
+
return found;
|
|
976
1185
|
}
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1186
|
+
}
|
|
1187
|
+
if (wrapperLastSessionId) {
|
|
1188
|
+
const found = sessions.find((s) => s.id === wrapperLastSessionId);
|
|
1189
|
+
if (found) {
|
|
1190
|
+
return found;
|
|
981
1191
|
}
|
|
982
|
-
groups.get("other").push(msg.trim());
|
|
983
1192
|
}
|
|
1193
|
+
return sessions[sessions.length - 1];
|
|
984
1194
|
}
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1195
|
+
/**
|
|
1196
|
+
* Returns the last N sessions, newest first.
|
|
1197
|
+
*/
|
|
1198
|
+
getRecentSessions(count) {
|
|
1199
|
+
return getRecentSessions(this.getSessions(), count);
|
|
1200
|
+
}
|
|
1201
|
+
/** Read all decisions from decisions.json. */
|
|
1202
|
+
getDecisions() {
|
|
1203
|
+
return this.parseDecisions().decisions;
|
|
1204
|
+
}
|
|
1205
|
+
/** Returns the last N decisions, newest first. */
|
|
1206
|
+
getRecentDecisions(count) {
|
|
1207
|
+
const all = this.getDecisions();
|
|
1208
|
+
return all.slice(-count).reverse();
|
|
1209
|
+
}
|
|
1210
|
+
/** Read the multi-license store from `~/.keepgoing/license.json`. */
|
|
1211
|
+
getLicenseStore() {
|
|
1212
|
+
return readLicenseStore();
|
|
1213
|
+
}
|
|
1214
|
+
/**
|
|
1215
|
+
* Read all current tasks from current-tasks.json.
|
|
1216
|
+
* Automatically filters out stale finished sessions (> 2 hours).
|
|
1217
|
+
*/
|
|
1218
|
+
getCurrentTasks() {
|
|
1219
|
+
const multiRaw = this.readJsonFile(this.currentTasksFilePath);
|
|
1220
|
+
if (multiRaw) {
|
|
1221
|
+
const tasks = Array.isArray(multiRaw) ? multiRaw : multiRaw.tasks ?? [];
|
|
1222
|
+
return this.pruneStale(tasks);
|
|
1223
|
+
}
|
|
1224
|
+
return [];
|
|
1225
|
+
}
|
|
1226
|
+
/** Get only active sessions (sessionActive=true and within stale threshold). */
|
|
1227
|
+
getActiveTasks() {
|
|
1228
|
+
return this.getCurrentTasks().filter((t) => t.sessionActive);
|
|
1229
|
+
}
|
|
1230
|
+
/** Get a specific session by ID. */
|
|
1231
|
+
getTaskBySessionId(sessionId) {
|
|
1232
|
+
return this.getCurrentTasks().find((t) => t.sessionId === sessionId);
|
|
1233
|
+
}
|
|
1234
|
+
/**
|
|
1235
|
+
* Detect files being edited by multiple sessions simultaneously.
|
|
1236
|
+
* Returns pairs of session IDs and the conflicting file paths.
|
|
1237
|
+
*/
|
|
1238
|
+
detectFileConflicts() {
|
|
1239
|
+
const activeTasks = this.getActiveTasks();
|
|
1240
|
+
if (activeTasks.length < 2) return [];
|
|
1241
|
+
const fileToSessions = /* @__PURE__ */ new Map();
|
|
1242
|
+
for (const task of activeTasks) {
|
|
1243
|
+
if (task.lastFileEdited && task.sessionId) {
|
|
1244
|
+
const existing = fileToSessions.get(task.lastFileEdited) ?? [];
|
|
1245
|
+
existing.push({
|
|
1246
|
+
sessionId: task.sessionId,
|
|
1247
|
+
agentLabel: task.agentLabel,
|
|
1248
|
+
branch: task.branch
|
|
1249
|
+
});
|
|
1250
|
+
fileToSessions.set(task.lastFileEdited, existing);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
const conflicts = [];
|
|
1254
|
+
for (const [file, sessions] of fileToSessions) {
|
|
1255
|
+
if (sessions.length > 1) {
|
|
1256
|
+
conflicts.push({ file, sessions });
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
return conflicts;
|
|
1260
|
+
}
|
|
1261
|
+
/**
|
|
1262
|
+
* Detect sessions on the same branch (possible duplicate work).
|
|
1263
|
+
*/
|
|
1264
|
+
detectBranchOverlap() {
|
|
1265
|
+
const activeTasks = this.getActiveTasks();
|
|
1266
|
+
if (activeTasks.length < 2) return [];
|
|
1267
|
+
const branchToSessions = /* @__PURE__ */ new Map();
|
|
1268
|
+
for (const task of activeTasks) {
|
|
1269
|
+
if (task.branch && task.sessionId) {
|
|
1270
|
+
const existing = branchToSessions.get(task.branch) ?? [];
|
|
1271
|
+
existing.push({ sessionId: task.sessionId, agentLabel: task.agentLabel });
|
|
1272
|
+
branchToSessions.set(task.branch, existing);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
const overlaps = [];
|
|
1276
|
+
for (const [branch, sessions] of branchToSessions) {
|
|
1277
|
+
if (sessions.length > 1) {
|
|
1278
|
+
overlaps.push({ branch, sessions });
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
return overlaps;
|
|
1282
|
+
}
|
|
1283
|
+
pruneStale(tasks) {
|
|
1284
|
+
return pruneStaleTasks(tasks);
|
|
1285
|
+
}
|
|
1286
|
+
/** Get the last session checkpoint for a specific branch. */
|
|
1287
|
+
getLastSessionForBranch(branch) {
|
|
1288
|
+
const sessions = this.getSessions().filter((s) => s.gitBranch === branch);
|
|
1289
|
+
return sessions.length > 0 ? sessions[sessions.length - 1] : void 0;
|
|
1290
|
+
}
|
|
1291
|
+
/** Returns the last N sessions for a specific branch, newest first. */
|
|
1292
|
+
getRecentSessionsForBranch(branch, count) {
|
|
1293
|
+
const filtered = this.getSessions().filter((s) => s.gitBranch === branch);
|
|
1294
|
+
return filtered.slice(-count).reverse();
|
|
1295
|
+
}
|
|
1296
|
+
/** Returns the last N decisions for a specific branch, newest first. */
|
|
1297
|
+
getRecentDecisionsForBranch(branch, count) {
|
|
1298
|
+
const filtered = this.getDecisions().filter((d) => d.gitBranch === branch);
|
|
1299
|
+
return filtered.slice(-count).reverse();
|
|
1300
|
+
}
|
|
1301
|
+
/** Whether the workspace is inside a git worktree. */
|
|
1302
|
+
get isWorktree() {
|
|
1303
|
+
return this._isWorktree;
|
|
1304
|
+
}
|
|
1305
|
+
/**
|
|
1306
|
+
* Returns the current git branch for this workspace.
|
|
1307
|
+
* Lazily cached: the branch is resolved once per KeepGoingReader instance.
|
|
1308
|
+
*/
|
|
1309
|
+
getCurrentBranch() {
|
|
1310
|
+
if (this._cachedBranch === null) {
|
|
1311
|
+
this._cachedBranch = getCurrentBranch(this.workspacePath);
|
|
1312
|
+
}
|
|
1313
|
+
return this._cachedBranch;
|
|
1314
|
+
}
|
|
1315
|
+
/**
|
|
1316
|
+
* Worktree-aware last session lookup.
|
|
1317
|
+
* In a worktree, scopes to the current branch with fallback to global.
|
|
1318
|
+
* Returns the session and whether it fell back to global.
|
|
1319
|
+
*/
|
|
1320
|
+
getScopedLastSession() {
|
|
1321
|
+
const branch = this.getCurrentBranch();
|
|
1322
|
+
if (this._isWorktree && branch) {
|
|
1323
|
+
const scoped = this.getLastSessionForBranch(branch);
|
|
1324
|
+
if (scoped) return { session: scoped, isFallback: false };
|
|
1325
|
+
return { session: this.getLastSession(), isFallback: true };
|
|
1326
|
+
}
|
|
1327
|
+
return { session: this.getLastSession(), isFallback: false };
|
|
1328
|
+
}
|
|
1329
|
+
/** Worktree-aware recent sessions. Scopes to current branch in a worktree. */
|
|
1330
|
+
getScopedRecentSessions(count) {
|
|
1331
|
+
const branch = this.getCurrentBranch();
|
|
1332
|
+
if (this._isWorktree && branch) {
|
|
1333
|
+
return this.getRecentSessionsForBranch(branch, count);
|
|
1334
|
+
}
|
|
1335
|
+
return this.getRecentSessions(count);
|
|
1336
|
+
}
|
|
1337
|
+
/** Worktree-aware recent decisions. Scopes to current branch in a worktree. */
|
|
1338
|
+
getScopedRecentDecisions(count) {
|
|
1339
|
+
const branch = this.getCurrentBranch();
|
|
1340
|
+
if (this._isWorktree && branch) {
|
|
1341
|
+
return this.getRecentDecisionsForBranch(branch, count);
|
|
1342
|
+
}
|
|
1343
|
+
return this.getRecentDecisions(count);
|
|
1344
|
+
}
|
|
1345
|
+
/**
|
|
1346
|
+
* Resolves branch scope from an explicit `branch` parameter.
|
|
1347
|
+
* Used by tools that accept a `branch` argument (e.g. get_session_history, get_decisions).
|
|
1348
|
+
* - `"all"` returns no filter.
|
|
1349
|
+
* - An explicit branch name uses that.
|
|
1350
|
+
* - `undefined` auto-scopes to the current branch in a worktree, or all branches otherwise.
|
|
1351
|
+
*/
|
|
1352
|
+
resolveBranchScope(branch) {
|
|
1353
|
+
if (branch === "all") {
|
|
1354
|
+
return { effectiveBranch: void 0, scopeLabel: "all branches" };
|
|
1355
|
+
}
|
|
1356
|
+
if (branch) {
|
|
1357
|
+
return { effectiveBranch: branch, scopeLabel: `branch \`${branch}\`` };
|
|
1358
|
+
}
|
|
1359
|
+
const currentBranch = this.getCurrentBranch();
|
|
1360
|
+
if (this._isWorktree && currentBranch) {
|
|
1361
|
+
return { effectiveBranch: currentBranch, scopeLabel: `branch \`${currentBranch}\` (worktree)` };
|
|
1362
|
+
}
|
|
1363
|
+
return { effectiveBranch: void 0, scopeLabel: "all branches" };
|
|
1364
|
+
}
|
|
1365
|
+
/**
|
|
1366
|
+
* Parses sessions.json once, returning both the session list
|
|
1367
|
+
* and the optional lastSessionId from a ProjectSessions wrapper.
|
|
1368
|
+
*/
|
|
1369
|
+
parseSessions() {
|
|
1370
|
+
const raw = this.readJsonFile(
|
|
1371
|
+
this.sessionsFilePath
|
|
1372
|
+
);
|
|
1373
|
+
if (!raw) {
|
|
1374
|
+
return { sessions: [] };
|
|
1375
|
+
}
|
|
1376
|
+
if (Array.isArray(raw)) {
|
|
1377
|
+
return { sessions: raw };
|
|
1378
|
+
}
|
|
1379
|
+
return { sessions: raw.sessions ?? [], wrapperLastSessionId: raw.lastSessionId };
|
|
1380
|
+
}
|
|
1381
|
+
parseDecisions() {
|
|
1382
|
+
const raw = this.readJsonFile(this.decisionsFilePath);
|
|
1383
|
+
if (!raw) {
|
|
1384
|
+
return { decisions: [] };
|
|
1385
|
+
}
|
|
1386
|
+
return { decisions: raw.decisions ?? [], lastDecisionId: raw.lastDecisionId };
|
|
1387
|
+
}
|
|
1388
|
+
readJsonFile(filePath) {
|
|
1389
|
+
try {
|
|
1390
|
+
if (!fs4.existsSync(filePath)) {
|
|
1391
|
+
return void 0;
|
|
1392
|
+
}
|
|
1393
|
+
const raw = fs4.readFileSync(filePath, "utf-8");
|
|
1394
|
+
return JSON.parse(raw);
|
|
1395
|
+
} catch {
|
|
1396
|
+
return void 0;
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
};
|
|
1400
|
+
|
|
1401
|
+
// ../../packages/shared/src/smartSummary.ts
|
|
1402
|
+
var PREFIX_VERBS = {
|
|
1403
|
+
feat: "Added",
|
|
1404
|
+
fix: "Fixed",
|
|
1405
|
+
refactor: "Refactored",
|
|
1406
|
+
docs: "Updated docs for",
|
|
1407
|
+
test: "Added tests for",
|
|
1408
|
+
chore: "Updated",
|
|
1409
|
+
style: "Styled",
|
|
1410
|
+
perf: "Optimized",
|
|
1411
|
+
ci: "Updated CI for",
|
|
1412
|
+
build: "Updated build for",
|
|
1413
|
+
revert: "Reverted"
|
|
1414
|
+
};
|
|
1415
|
+
var NOISE_PATTERNS = [
|
|
1416
|
+
"node_modules",
|
|
1417
|
+
"package-lock.json",
|
|
1418
|
+
"yarn.lock",
|
|
1419
|
+
"pnpm-lock.yaml",
|
|
1420
|
+
".gitignore",
|
|
1421
|
+
".DS_Store",
|
|
1422
|
+
"dist/",
|
|
1423
|
+
"out/",
|
|
1424
|
+
"build/"
|
|
1425
|
+
];
|
|
1426
|
+
function categorizeCommits(messages) {
|
|
1427
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1428
|
+
for (const msg of messages) {
|
|
1429
|
+
const match = msg.match(/^(\w+)(?:\([^)]*\))?[!]?:\s*(.+)/);
|
|
1430
|
+
if (match) {
|
|
1431
|
+
const prefix = match[1].toLowerCase();
|
|
1432
|
+
const body = match[2].trim();
|
|
1433
|
+
if (!groups.has(prefix)) {
|
|
1434
|
+
groups.set(prefix, []);
|
|
1435
|
+
}
|
|
1436
|
+
groups.get(prefix).push(body);
|
|
1437
|
+
} else {
|
|
1438
|
+
if (!groups.has("other")) {
|
|
1439
|
+
groups.set("other", []);
|
|
1440
|
+
}
|
|
1441
|
+
groups.get("other").push(msg.trim());
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
return groups;
|
|
1445
|
+
}
|
|
1446
|
+
function inferWorkAreas(files) {
|
|
1447
|
+
const areas = /* @__PURE__ */ new Map();
|
|
1448
|
+
for (const file of files) {
|
|
1449
|
+
if (NOISE_PATTERNS.some((p) => file.includes(p))) {
|
|
1450
|
+
continue;
|
|
1451
|
+
}
|
|
993
1452
|
const parts = file.split("/").filter(Boolean);
|
|
994
1453
|
let area;
|
|
995
1454
|
if (parts.length >= 2 && (parts[0] === "apps" || parts[0] === "packages")) {
|
|
@@ -1101,131 +1560,222 @@ function capitalize(s) {
|
|
|
1101
1560
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
1102
1561
|
}
|
|
1103
1562
|
|
|
1104
|
-
// ../../packages/shared/src/
|
|
1105
|
-
import
|
|
1106
|
-
import
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1563
|
+
// ../../packages/shared/src/contextSnapshot.ts
|
|
1564
|
+
import fs5 from "fs";
|
|
1565
|
+
import path7 from "path";
|
|
1566
|
+
function formatCompactRelativeTime(date) {
|
|
1567
|
+
const diffMs = Date.now() - date.getTime();
|
|
1568
|
+
if (isNaN(diffMs)) return "?";
|
|
1569
|
+
if (diffMs < 0) return "now";
|
|
1570
|
+
const seconds = Math.floor(diffMs / 1e3);
|
|
1571
|
+
const minutes = Math.floor(seconds / 60);
|
|
1572
|
+
const hours = Math.floor(minutes / 60);
|
|
1573
|
+
const days = Math.floor(hours / 24);
|
|
1574
|
+
const weeks = Math.floor(days / 7);
|
|
1575
|
+
if (seconds < 60) return "now";
|
|
1576
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
1577
|
+
if (hours < 24) return `${hours}h ago`;
|
|
1578
|
+
if (days < 7) return `${days}d ago`;
|
|
1579
|
+
return `${weeks}w ago`;
|
|
1120
1580
|
}
|
|
1121
|
-
function
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1581
|
+
function computeMomentum(timestamp, signals) {
|
|
1582
|
+
if (signals) {
|
|
1583
|
+
if (signals.lastGitOpAt) {
|
|
1584
|
+
const gitOpDiffMs = Date.now() - new Date(signals.lastGitOpAt).getTime();
|
|
1585
|
+
if (!isNaN(gitOpDiffMs) && gitOpDiffMs >= 0 && gitOpDiffMs < 30 * 60 * 1e3) {
|
|
1586
|
+
return "hot";
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
if (signals.recentCommitCount !== void 0 && signals.recentCommitCount >= 2) {
|
|
1590
|
+
return "hot";
|
|
1591
|
+
}
|
|
1128
1592
|
}
|
|
1129
|
-
const
|
|
1130
|
-
if (
|
|
1131
|
-
|
|
1593
|
+
const diffMs = Date.now() - new Date(timestamp).getTime();
|
|
1594
|
+
if (isNaN(diffMs) || diffMs < 0) return "hot";
|
|
1595
|
+
const minutes = diffMs / (1e3 * 60);
|
|
1596
|
+
if (minutes < 30) return "hot";
|
|
1597
|
+
if (minutes < 240) return "warm";
|
|
1598
|
+
return "cold";
|
|
1599
|
+
}
|
|
1600
|
+
function inferFocusFromBranch2(branch) {
|
|
1601
|
+
if (!branch || branch === "main" || branch === "master" || branch === "develop" || branch === "HEAD") {
|
|
1602
|
+
return void 0;
|
|
1132
1603
|
}
|
|
1133
|
-
|
|
1134
|
-
|
|
1604
|
+
const prefixPattern = /^(?:feature|feat|fix|bugfix|hotfix|chore|refactor|docs|test|ci)\//i;
|
|
1605
|
+
const isFix = /^(?:fix|bugfix|hotfix)\//i.test(branch);
|
|
1606
|
+
const stripped = branch.replace(prefixPattern, "");
|
|
1607
|
+
const cleaned = stripped.replace(/[-_/]/g, " ").replace(/^\d+\s*/, "").trim();
|
|
1608
|
+
if (!cleaned) return void 0;
|
|
1609
|
+
return isFix ? `${cleaned} fix` : cleaned;
|
|
1135
1610
|
}
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
return "Pro Add-on";
|
|
1611
|
+
function cleanCommitMessage(message, maxLen = 60) {
|
|
1612
|
+
if (!message) return "";
|
|
1613
|
+
const match = message.match(/^(?:\w+)(?:\([^)]*\))?[!]?:\s*(.+)/);
|
|
1614
|
+
const body = match ? match[1].trim() : message.trim();
|
|
1615
|
+
if (!body) return "";
|
|
1616
|
+
const capitalized = body.charAt(0).toUpperCase() + body.slice(1);
|
|
1617
|
+
if (capitalized.length <= maxLen) return capitalized;
|
|
1618
|
+
return capitalized.slice(0, maxLen - 3) + "...";
|
|
1619
|
+
}
|
|
1620
|
+
function buildDoing(checkpoint, branch, recentCommitMessages) {
|
|
1621
|
+
if (checkpoint?.summary) return checkpoint.summary;
|
|
1622
|
+
const branchFocus = inferFocusFromBranch2(branch ?? checkpoint?.gitBranch);
|
|
1623
|
+
if (branchFocus) return branchFocus;
|
|
1624
|
+
if (recentCommitMessages && recentCommitMessages.length > 0) {
|
|
1625
|
+
const cleaned = cleanCommitMessage(recentCommitMessages[0]);
|
|
1626
|
+
if (cleaned) return cleaned;
|
|
1627
|
+
}
|
|
1628
|
+
return "Unknown";
|
|
1155
1629
|
}
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
if (_cachedStore && now - _cacheTimestamp < LICENSE_CACHE_TTL_MS) {
|
|
1162
|
-
return _cachedStore;
|
|
1630
|
+
function buildWhere(checkpoint, branch) {
|
|
1631
|
+
const effectiveBranch = branch ?? checkpoint?.gitBranch;
|
|
1632
|
+
const parts = [];
|
|
1633
|
+
if (effectiveBranch) {
|
|
1634
|
+
parts.push(effectiveBranch);
|
|
1163
1635
|
}
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1636
|
+
if (checkpoint?.touchedFiles && checkpoint.touchedFiles.length > 0) {
|
|
1637
|
+
const fileNames = checkpoint.touchedFiles.slice(0, 2).map((f) => {
|
|
1638
|
+
const segments = f.replace(/\\/g, "/").split("/");
|
|
1639
|
+
return segments[segments.length - 1];
|
|
1640
|
+
});
|
|
1641
|
+
parts.push(fileNames.join(", "));
|
|
1642
|
+
}
|
|
1643
|
+
return parts.join(" \xB7 ") || "unknown";
|
|
1644
|
+
}
|
|
1645
|
+
function detectFocusArea(files) {
|
|
1646
|
+
if (!files || files.length === 0) return void 0;
|
|
1647
|
+
const areas = inferWorkAreas(files);
|
|
1648
|
+
if (areas.length === 0) return void 0;
|
|
1649
|
+
if (areas.length === 1) return areas[0];
|
|
1650
|
+
const topArea = areas[0];
|
|
1651
|
+
const areaCounts = /* @__PURE__ */ new Map();
|
|
1652
|
+
for (const file of files) {
|
|
1653
|
+
const parts = file.split("/").filter(Boolean);
|
|
1654
|
+
let area;
|
|
1655
|
+
if (parts.length <= 1) {
|
|
1656
|
+
area = "root";
|
|
1657
|
+
} else if (parts[0] === "apps" || parts[0] === "packages") {
|
|
1658
|
+
area = parts.length > 1 ? parts[1] : parts[0];
|
|
1659
|
+
} else if (parts[0] === "src") {
|
|
1660
|
+
area = parts.length > 1 ? parts[1] : "src";
|
|
1169
1661
|
} 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
|
-
}
|
|
1662
|
+
area = parts[0];
|
|
1177
1663
|
}
|
|
1178
|
-
|
|
1179
|
-
store = { version: 2, licenses: [] };
|
|
1664
|
+
areaCounts.set(area, (areaCounts.get(area) ?? 0) + 1);
|
|
1180
1665
|
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
const dirPath = getGlobalLicenseDir();
|
|
1187
|
-
if (!fs3.existsSync(dirPath)) {
|
|
1188
|
-
fs3.mkdirSync(dirPath, { recursive: true });
|
|
1666
|
+
let topCount = 0;
|
|
1667
|
+
for (const [areaKey, count] of areaCounts) {
|
|
1668
|
+
if (topArea.toLowerCase().includes(areaKey.toLowerCase()) || areaKey.toLowerCase().includes(topArea.toLowerCase().split(" ")[0])) {
|
|
1669
|
+
topCount = Math.max(topCount, count);
|
|
1670
|
+
}
|
|
1189
1671
|
}
|
|
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);
|
|
1672
|
+
if (topCount === 0) {
|
|
1673
|
+
topCount = Math.max(...areaCounts.values());
|
|
1202
1674
|
}
|
|
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");
|
|
1675
|
+
const ratio = topCount / files.length;
|
|
1676
|
+
if (ratio >= 0.6) return topArea;
|
|
1677
|
+
return void 0;
|
|
1212
1678
|
}
|
|
1213
|
-
function
|
|
1214
|
-
const
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1679
|
+
function generateContextSnapshot(projectRoot) {
|
|
1680
|
+
const reader = new KeepGoingReader(projectRoot);
|
|
1681
|
+
if (!reader.exists()) return null;
|
|
1682
|
+
const lastSession = reader.getLastSession();
|
|
1683
|
+
const state = reader.getState();
|
|
1684
|
+
if (!lastSession && !state) return null;
|
|
1685
|
+
const timestamp = state?.lastActivityAt ?? lastSession?.timestamp;
|
|
1686
|
+
if (!timestamp) return null;
|
|
1687
|
+
const branch = state?.lastKnownBranch ?? lastSession?.gitBranch;
|
|
1688
|
+
let activeAgents = 0;
|
|
1689
|
+
try {
|
|
1690
|
+
const activeTasks = reader.getActiveTasks();
|
|
1691
|
+
activeAgents = activeTasks.length;
|
|
1692
|
+
} catch {
|
|
1693
|
+
}
|
|
1694
|
+
const doing = buildDoing(lastSession, branch);
|
|
1695
|
+
const next = lastSession?.nextStep ?? "";
|
|
1696
|
+
const where = buildWhere(lastSession, branch);
|
|
1697
|
+
const when = formatCompactRelativeTime(new Date(timestamp));
|
|
1698
|
+
const momentum = computeMomentum(timestamp, state?.activitySignals);
|
|
1699
|
+
const blocker = lastSession?.blocker;
|
|
1700
|
+
const snapshot = {
|
|
1701
|
+
doing,
|
|
1702
|
+
next,
|
|
1703
|
+
where,
|
|
1704
|
+
when,
|
|
1705
|
+
momentum,
|
|
1706
|
+
lastActivityAt: timestamp
|
|
1707
|
+
};
|
|
1708
|
+
if (blocker) snapshot.blocker = blocker;
|
|
1709
|
+
if (activeAgents > 0) snapshot.activeAgents = activeAgents;
|
|
1710
|
+
const focusArea = detectFocusArea(lastSession?.touchedFiles ?? []);
|
|
1711
|
+
if (focusArea) snapshot.focusArea = focusArea;
|
|
1712
|
+
return snapshot;
|
|
1713
|
+
}
|
|
1714
|
+
function formatCrossProjectLine(entry) {
|
|
1715
|
+
const s = entry.snapshot;
|
|
1716
|
+
const icon = s.momentum === "hot" ? "\u26A1" : s.momentum === "warm" ? "\u{1F536}" : "\u{1F4A4}";
|
|
1717
|
+
const parts = [];
|
|
1718
|
+
parts.push(`${icon} ${entry.name}: ${s.doing}`);
|
|
1719
|
+
if (s.next) {
|
|
1720
|
+
parts.push(`\u2192 ${s.next}`);
|
|
1721
|
+
}
|
|
1722
|
+
let line = parts.join(" ");
|
|
1723
|
+
line += ` (${s.when})`;
|
|
1724
|
+
if (s.blocker) {
|
|
1725
|
+
line += ` \u26D4 ${s.blocker}`;
|
|
1726
|
+
}
|
|
1727
|
+
return line;
|
|
1728
|
+
}
|
|
1729
|
+
var MOMENTUM_RANK = { hot: 0, warm: 1, cold: 2 };
|
|
1730
|
+
function generateCrossProjectSummary() {
|
|
1731
|
+
const registry = readKnownProjects();
|
|
1732
|
+
const trayPaths = readTrayConfigProjects();
|
|
1733
|
+
const seenPaths = new Set(registry.projects.map((p) => p.path));
|
|
1734
|
+
const allEntries = [...registry.projects];
|
|
1735
|
+
for (const tp of trayPaths) {
|
|
1736
|
+
if (!seenPaths.has(tp)) {
|
|
1737
|
+
seenPaths.add(tp);
|
|
1738
|
+
allEntries.push({ path: tp, name: path7.basename(tp) });
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
const projects = [];
|
|
1742
|
+
const seenRoots = /* @__PURE__ */ new Set();
|
|
1743
|
+
for (const entry of allEntries) {
|
|
1744
|
+
if (!fs5.existsSync(entry.path)) continue;
|
|
1745
|
+
const snapshot = generateContextSnapshot(entry.path);
|
|
1746
|
+
if (!snapshot) continue;
|
|
1747
|
+
let resolvedRoot;
|
|
1748
|
+
try {
|
|
1749
|
+
resolvedRoot = fs5.realpathSync(entry.path);
|
|
1750
|
+
} catch {
|
|
1751
|
+
resolvedRoot = entry.path;
|
|
1752
|
+
}
|
|
1753
|
+
if (seenRoots.has(resolvedRoot)) continue;
|
|
1754
|
+
seenRoots.add(resolvedRoot);
|
|
1755
|
+
projects.push({
|
|
1756
|
+
name: entry.name,
|
|
1757
|
+
path: entry.path,
|
|
1758
|
+
snapshot
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
projects.sort((a, b) => {
|
|
1762
|
+
const rankA = MOMENTUM_RANK[a.snapshot.momentum ?? "cold"];
|
|
1763
|
+
const rankB = MOMENTUM_RANK[b.snapshot.momentum ?? "cold"];
|
|
1764
|
+
if (rankA !== rankB) return rankA - rankB;
|
|
1765
|
+
const timeA = a.snapshot.lastActivityAt ? new Date(a.snapshot.lastActivityAt).getTime() : 0;
|
|
1766
|
+
const timeB = b.snapshot.lastActivityAt ? new Date(b.snapshot.lastActivityAt).getTime() : 0;
|
|
1767
|
+
return timeB - timeA;
|
|
1218
1768
|
});
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
}
|
|
1223
|
-
var REVALIDATION_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
|
|
1224
|
-
function needsRevalidation(entry) {
|
|
1225
|
-
const lastValidated = new Date(entry.lastValidatedAt).getTime();
|
|
1226
|
-
return Date.now() - lastValidated > REVALIDATION_THRESHOLD_MS;
|
|
1769
|
+
return {
|
|
1770
|
+
projects,
|
|
1771
|
+
generated: (/* @__PURE__ */ new Date()).toISOString()
|
|
1772
|
+
};
|
|
1227
1773
|
}
|
|
1228
1774
|
|
|
1775
|
+
// ../../packages/shared/src/decisionStorage.ts
|
|
1776
|
+
import fs6 from "fs";
|
|
1777
|
+
import path8 from "path";
|
|
1778
|
+
|
|
1229
1779
|
// ../../packages/shared/src/featureGate.ts
|
|
1230
1780
|
var DefaultFeatureGate = class {
|
|
1231
1781
|
isEnabled(_feature) {
|
|
@@ -1238,31 +1788,31 @@ function isDecisionsEnabled() {
|
|
|
1238
1788
|
}
|
|
1239
1789
|
|
|
1240
1790
|
// ../../packages/shared/src/decisionStorage.ts
|
|
1241
|
-
var
|
|
1242
|
-
var
|
|
1791
|
+
var STORAGE_DIR3 = ".keepgoing";
|
|
1792
|
+
var DECISIONS_FILE2 = "decisions.json";
|
|
1243
1793
|
var MAX_DECISIONS = 100;
|
|
1244
1794
|
var DecisionStorage = class {
|
|
1245
1795
|
storagePath;
|
|
1246
1796
|
decisionsFilePath;
|
|
1247
1797
|
constructor(workspacePath) {
|
|
1248
1798
|
const mainRoot = resolveStorageRoot(workspacePath);
|
|
1249
|
-
this.storagePath =
|
|
1250
|
-
this.decisionsFilePath =
|
|
1799
|
+
this.storagePath = path8.join(mainRoot, STORAGE_DIR3);
|
|
1800
|
+
this.decisionsFilePath = path8.join(this.storagePath, DECISIONS_FILE2);
|
|
1251
1801
|
}
|
|
1252
1802
|
ensureStorageDir() {
|
|
1253
|
-
if (!
|
|
1254
|
-
|
|
1803
|
+
if (!fs6.existsSync(this.storagePath)) {
|
|
1804
|
+
fs6.mkdirSync(this.storagePath, { recursive: true });
|
|
1255
1805
|
}
|
|
1256
1806
|
}
|
|
1257
1807
|
getProjectName() {
|
|
1258
|
-
return
|
|
1808
|
+
return path8.basename(path8.dirname(this.storagePath));
|
|
1259
1809
|
}
|
|
1260
1810
|
load() {
|
|
1261
1811
|
try {
|
|
1262
|
-
if (!
|
|
1812
|
+
if (!fs6.existsSync(this.decisionsFilePath)) {
|
|
1263
1813
|
return createEmptyProjectDecisions(this.getProjectName());
|
|
1264
1814
|
}
|
|
1265
|
-
const raw =
|
|
1815
|
+
const raw = fs6.readFileSync(this.decisionsFilePath, "utf-8");
|
|
1266
1816
|
const data = JSON.parse(raw);
|
|
1267
1817
|
return data;
|
|
1268
1818
|
} catch {
|
|
@@ -1272,7 +1822,7 @@ var DecisionStorage = class {
|
|
|
1272
1822
|
save(decisions) {
|
|
1273
1823
|
this.ensureStorageDir();
|
|
1274
1824
|
const content = JSON.stringify(decisions, null, 2);
|
|
1275
|
-
|
|
1825
|
+
fs6.writeFileSync(this.decisionsFilePath, content, "utf-8");
|
|
1276
1826
|
}
|
|
1277
1827
|
/**
|
|
1278
1828
|
* Save a decision record as a draft. Always persists regardless of Pro
|
|
@@ -1448,366 +1998,84 @@ function classifyCommit(commit) {
|
|
|
1448
1998
|
confidence += 0.35;
|
|
1449
1999
|
reasons.push(`conventional commit type '${type}' is high-signal`);
|
|
1450
2000
|
}
|
|
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);
|
|
1691
|
-
}
|
|
1692
|
-
/** Get the last session checkpoint for a specific branch. */
|
|
1693
|
-
getLastSessionForBranch(branch) {
|
|
1694
|
-
const sessions = this.getSessions().filter((s) => s.gitBranch === branch);
|
|
1695
|
-
return sessions.length > 0 ? sessions[sessions.length - 1] : void 0;
|
|
1696
|
-
}
|
|
1697
|
-
/** Returns the last N sessions for a specific branch, newest first. */
|
|
1698
|
-
getRecentSessionsForBranch(branch, count) {
|
|
1699
|
-
const filtered = this.getSessions().filter((s) => s.gitBranch === branch);
|
|
1700
|
-
return filtered.slice(-count).reverse();
|
|
1701
|
-
}
|
|
1702
|
-
/** Returns the last N decisions for a specific branch, newest first. */
|
|
1703
|
-
getRecentDecisionsForBranch(branch, count) {
|
|
1704
|
-
const filtered = this.getDecisions().filter((d) => d.gitBranch === branch);
|
|
1705
|
-
return filtered.slice(-count).reverse();
|
|
1706
|
-
}
|
|
1707
|
-
/** Whether the workspace is inside a git worktree. */
|
|
1708
|
-
get isWorktree() {
|
|
1709
|
-
return this._isWorktree;
|
|
1710
|
-
}
|
|
1711
|
-
/**
|
|
1712
|
-
* Returns the current git branch for this workspace.
|
|
1713
|
-
* Lazily cached: the branch is resolved once per KeepGoingReader instance.
|
|
1714
|
-
*/
|
|
1715
|
-
getCurrentBranch() {
|
|
1716
|
-
if (this._cachedBranch === null) {
|
|
1717
|
-
this._cachedBranch = getCurrentBranch(this.workspacePath);
|
|
1718
|
-
}
|
|
1719
|
-
return this._cachedBranch;
|
|
1720
|
-
}
|
|
1721
|
-
/**
|
|
1722
|
-
* Worktree-aware last session lookup.
|
|
1723
|
-
* In a worktree, scopes to the current branch with fallback to global.
|
|
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 };
|
|
1734
|
-
}
|
|
1735
|
-
/** Worktree-aware recent sessions. Scopes to current branch in a worktree. */
|
|
1736
|
-
getScopedRecentSessions(count) {
|
|
1737
|
-
const branch = this.getCurrentBranch();
|
|
1738
|
-
if (this._isWorktree && branch) {
|
|
1739
|
-
return this.getRecentSessionsForBranch(branch, count);
|
|
1740
|
-
}
|
|
1741
|
-
return this.getRecentSessions(count);
|
|
2001
|
+
if (scope && HIGH_SIGNAL_TYPES.has(scope)) {
|
|
2002
|
+
matchedTypes.push(`scope:${scope}`);
|
|
2003
|
+
confidence += 0.25;
|
|
2004
|
+
reasons.push(`conventional commit scope '${scope}' is high-signal`);
|
|
1742
2005
|
}
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
if (this._isWorktree && branch) {
|
|
1747
|
-
return this.getRecentDecisionsForBranch(branch, count);
|
|
1748
|
-
}
|
|
1749
|
-
return this.getRecentDecisions(count);
|
|
2006
|
+
if (parsed.breaking) {
|
|
2007
|
+
confidence += 0.4;
|
|
2008
|
+
reasons.push("breaking change indicated by ! marker");
|
|
1750
2009
|
}
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
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)` };
|
|
2010
|
+
const matchedPaths = [];
|
|
2011
|
+
let bestTier = null;
|
|
2012
|
+
for (const file of filesChanged) {
|
|
2013
|
+
const pm = matchHighSignalPath(file);
|
|
2014
|
+
if (pm && !matchedPaths.includes(pm.label)) {
|
|
2015
|
+
matchedPaths.push(pm.label);
|
|
2016
|
+
if (bestTier !== "infra") {
|
|
2017
|
+
bestTier = pm.tier;
|
|
2018
|
+
}
|
|
1768
2019
|
}
|
|
1769
|
-
return { effectiveBranch: void 0, scopeLabel: "all branches" };
|
|
1770
2020
|
}
|
|
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 };
|
|
2021
|
+
if (matchedPaths.length > 0) {
|
|
2022
|
+
confidence += bestTier === "infra" ? 0.4 : 0.2;
|
|
2023
|
+
reasons.push(`commit touched: ${matchedPaths.slice(0, 3).join(", ")}`);
|
|
1786
2024
|
}
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
return { decisions: [] };
|
|
1791
|
-
}
|
|
1792
|
-
return { decisions: raw.decisions ?? [], lastDecisionId: raw.lastDecisionId };
|
|
2025
|
+
if (filesChanged.length > 0 && filesChanged.every((f) => NEGATIVE_PATH_PATTERNS.some((p) => p.test(f)))) {
|
|
2026
|
+
confidence -= 0.5;
|
|
2027
|
+
reasons.push("all changed files are low-signal (lock files, generated code, dist)");
|
|
1793
2028
|
}
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
return void 0;
|
|
1798
|
-
}
|
|
1799
|
-
const raw = fs5.readFileSync(filePath, "utf-8");
|
|
1800
|
-
return JSON.parse(raw);
|
|
1801
|
-
} catch {
|
|
1802
|
-
return void 0;
|
|
1803
|
-
}
|
|
2029
|
+
if (filesChanged.length > 0 && filesChanged.every((f) => /favicon/i.test(f) || /\.(svg|png|ico)$/i.test(f))) {
|
|
2030
|
+
confidence -= 0.5;
|
|
2031
|
+
reasons.push("all changed files are UI assets (favicon, SVG, PNG)");
|
|
1804
2032
|
}
|
|
1805
|
-
|
|
2033
|
+
if (filesChanged.length > 0 && filesChanged.every((f) => /\.css$/i.test(f)) && /tailwind/i.test(messageLower)) {
|
|
2034
|
+
confidence -= 0.5;
|
|
2035
|
+
reasons.push("Tailwind generated CSS change (low signal)");
|
|
2036
|
+
}
|
|
2037
|
+
const isDecisionCandidate = confidence >= 0.4;
|
|
2038
|
+
const keywordLabels = matchedKeywords.map((kw) => kw.source.replace(/\\b/g, ""));
|
|
2039
|
+
const category = isDecisionCandidate ? inferCategory(keywordLabels, matchedTypes, matchedPaths) : "unknown";
|
|
2040
|
+
return {
|
|
2041
|
+
isDecisionCandidate,
|
|
2042
|
+
confidence: Math.max(0, Math.min(1, confidence)),
|
|
2043
|
+
reasons,
|
|
2044
|
+
category
|
|
2045
|
+
};
|
|
2046
|
+
}
|
|
2047
|
+
function tryDetectDecision(opts) {
|
|
2048
|
+
if (!isDecisionsEnabled()) {
|
|
2049
|
+
return void 0;
|
|
2050
|
+
}
|
|
2051
|
+
const classification = classifyCommit({
|
|
2052
|
+
message: opts.commitMessage,
|
|
2053
|
+
filesChanged: opts.filesChanged
|
|
2054
|
+
});
|
|
2055
|
+
if (!classification.isDecisionCandidate) {
|
|
2056
|
+
return void 0;
|
|
2057
|
+
}
|
|
2058
|
+
const decision = createDecisionRecord({
|
|
2059
|
+
checkpointId: opts.checkpointId,
|
|
2060
|
+
gitBranch: opts.gitBranch,
|
|
2061
|
+
commitHash: opts.commitHash,
|
|
2062
|
+
commitMessage: opts.commitMessage,
|
|
2063
|
+
filesChanged: opts.filesChanged,
|
|
2064
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2065
|
+
classification
|
|
2066
|
+
});
|
|
2067
|
+
const storage = new DecisionStorage(opts.workspacePath);
|
|
2068
|
+
storage.saveDecision(decision);
|
|
2069
|
+
return {
|
|
2070
|
+
category: classification.category,
|
|
2071
|
+
confidence: classification.confidence
|
|
2072
|
+
};
|
|
2073
|
+
}
|
|
1806
2074
|
|
|
1807
2075
|
// ../../packages/shared/src/setup.ts
|
|
1808
|
-
import
|
|
2076
|
+
import fs7 from "fs";
|
|
1809
2077
|
import os3 from "os";
|
|
1810
|
-
import
|
|
2078
|
+
import path9 from "path";
|
|
1811
2079
|
var KEEPGOING_MARKER = "@keepgoingdev/mcp-server";
|
|
1812
2080
|
var SESSION_START_HOOK = {
|
|
1813
2081
|
matcher: "",
|
|
@@ -1836,6 +2104,15 @@ var POST_TOOL_USE_HOOK = {
|
|
|
1836
2104
|
}
|
|
1837
2105
|
]
|
|
1838
2106
|
};
|
|
2107
|
+
var PLAN_MODE_HOOK = {
|
|
2108
|
+
matcher: "Read|Grep|Glob|Bash|WebSearch",
|
|
2109
|
+
hooks: [
|
|
2110
|
+
{
|
|
2111
|
+
type: "command",
|
|
2112
|
+
command: "npx -y @keepgoingdev/mcp-server --heartbeat"
|
|
2113
|
+
}
|
|
2114
|
+
]
|
|
2115
|
+
};
|
|
1839
2116
|
var SESSION_END_HOOK = {
|
|
1840
2117
|
matcher: "",
|
|
1841
2118
|
hooks: [
|
|
@@ -1845,7 +2122,7 @@ var SESSION_END_HOOK = {
|
|
|
1845
2122
|
}
|
|
1846
2123
|
]
|
|
1847
2124
|
};
|
|
1848
|
-
var KEEPGOING_RULES_VERSION =
|
|
2125
|
+
var KEEPGOING_RULES_VERSION = 3;
|
|
1849
2126
|
var KEEPGOING_RULES_CONTENT = `<!-- @keepgoingdev/mcp-server v${KEEPGOING_RULES_VERSION} -->
|
|
1850
2127
|
## KeepGoing
|
|
1851
2128
|
|
|
@@ -1855,6 +2132,8 @@ After completing a task or meaningful piece of work, call the \`save_checkpoint\
|
|
|
1855
2132
|
- \`summary\`: 1-2 sentences. What changed and why, no file paths, no implementation details (those are captured from git).
|
|
1856
2133
|
- \`nextStep\`: What to do next
|
|
1857
2134
|
- \`blocker\`: Any blocker (if applicable)
|
|
2135
|
+
|
|
2136
|
+
When working in plan mode (investigating, designing, iterating on an approach before any edits), call \`save_checkpoint\` when you reach a significant milestone or conclusion. Use the summary to capture what was investigated and decided. This preserves planning context for future sessions.
|
|
1858
2137
|
`;
|
|
1859
2138
|
function getRulesFileVersion(content) {
|
|
1860
2139
|
const match = content.match(/<!-- @keepgoingdev\/mcp-server v(\d+) -->/);
|
|
@@ -1862,7 +2141,7 @@ function getRulesFileVersion(content) {
|
|
|
1862
2141
|
}
|
|
1863
2142
|
var STATUSLINE_CMD = "npx -y @keepgoingdev/mcp-server --statusline";
|
|
1864
2143
|
function detectClaudeDir() {
|
|
1865
|
-
return process.env.CLAUDE_CONFIG_DIR ||
|
|
2144
|
+
return process.env.CLAUDE_CONFIG_DIR || path9.join(os3.homedir(), ".claude");
|
|
1866
2145
|
}
|
|
1867
2146
|
function hasKeepGoingHook(hookEntries) {
|
|
1868
2147
|
return hookEntries.some(
|
|
@@ -1874,19 +2153,19 @@ function resolveScopePaths(scope, workspacePath, overrideClaudeDir) {
|
|
|
1874
2153
|
const claudeDir2 = overrideClaudeDir || detectClaudeDir();
|
|
1875
2154
|
return {
|
|
1876
2155
|
claudeDir: claudeDir2,
|
|
1877
|
-
settingsPath:
|
|
1878
|
-
claudeMdPath:
|
|
1879
|
-
rulesPath:
|
|
2156
|
+
settingsPath: path9.join(claudeDir2, "settings.json"),
|
|
2157
|
+
claudeMdPath: path9.join(claudeDir2, "CLAUDE.md"),
|
|
2158
|
+
rulesPath: path9.join(claudeDir2, "rules", "keepgoing.md")
|
|
1880
2159
|
};
|
|
1881
2160
|
}
|
|
1882
|
-
const claudeDir =
|
|
1883
|
-
const dotClaudeMdPath =
|
|
1884
|
-
const rootClaudeMdPath =
|
|
2161
|
+
const claudeDir = path9.join(workspacePath, ".claude");
|
|
2162
|
+
const dotClaudeMdPath = path9.join(workspacePath, ".claude", "CLAUDE.md");
|
|
2163
|
+
const rootClaudeMdPath = path9.join(workspacePath, "CLAUDE.md");
|
|
1885
2164
|
return {
|
|
1886
2165
|
claudeDir,
|
|
1887
|
-
settingsPath:
|
|
1888
|
-
claudeMdPath:
|
|
1889
|
-
rulesPath:
|
|
2166
|
+
settingsPath: path9.join(claudeDir, "settings.json"),
|
|
2167
|
+
claudeMdPath: fs7.existsSync(dotClaudeMdPath) ? dotClaudeMdPath : rootClaudeMdPath,
|
|
2168
|
+
rulesPath: path9.join(workspacePath, ".claude", "rules", "keepgoing.md")
|
|
1890
2169
|
};
|
|
1891
2170
|
}
|
|
1892
2171
|
function writeHooksToSettings(settings) {
|
|
@@ -1915,6 +2194,13 @@ function writeHooksToSettings(settings) {
|
|
|
1915
2194
|
settings.hooks.PostToolUse.push(POST_TOOL_USE_HOOK);
|
|
1916
2195
|
changed = true;
|
|
1917
2196
|
}
|
|
2197
|
+
const hasHeartbeat = settings.hooks.PostToolUse.some(
|
|
2198
|
+
(entry) => entry?.hooks?.some((h) => typeof h?.command === "string" && h.command.includes("--heartbeat"))
|
|
2199
|
+
);
|
|
2200
|
+
if (!hasHeartbeat) {
|
|
2201
|
+
settings.hooks.PostToolUse.push(PLAN_MODE_HOOK);
|
|
2202
|
+
changed = true;
|
|
2203
|
+
}
|
|
1918
2204
|
if (!Array.isArray(settings.hooks.SessionEnd)) {
|
|
1919
2205
|
settings.hooks.SessionEnd = [];
|
|
1920
2206
|
}
|
|
@@ -1926,11 +2212,11 @@ function writeHooksToSettings(settings) {
|
|
|
1926
2212
|
}
|
|
1927
2213
|
function checkHookConflict(scope, workspacePath) {
|
|
1928
2214
|
const otherPaths = resolveScopePaths(scope === "user" ? "project" : "user", workspacePath);
|
|
1929
|
-
if (!
|
|
2215
|
+
if (!fs7.existsSync(otherPaths.settingsPath)) {
|
|
1930
2216
|
return null;
|
|
1931
2217
|
}
|
|
1932
2218
|
try {
|
|
1933
|
-
const otherSettings = JSON.parse(
|
|
2219
|
+
const otherSettings = JSON.parse(fs7.readFileSync(otherPaths.settingsPath, "utf-8"));
|
|
1934
2220
|
const hooks = otherSettings?.hooks;
|
|
1935
2221
|
if (!hooks) return null;
|
|
1936
2222
|
const hasConflict = Array.isArray(hooks.SessionStart) && hasKeepGoingHook(hooks.SessionStart) || Array.isArray(hooks.Stop) && hasKeepGoingHook(hooks.Stop);
|
|
@@ -1959,10 +2245,10 @@ function setupProject(options) {
|
|
|
1959
2245
|
workspacePath,
|
|
1960
2246
|
claudeDirOverride
|
|
1961
2247
|
);
|
|
1962
|
-
const scopeLabel = scope === "user" ?
|
|
2248
|
+
const scopeLabel = scope === "user" ? path9.join("~", path9.relative(os3.homedir(), claudeDir), "settings.json").replace(/\\/g, "/") : ".claude/settings.json";
|
|
1963
2249
|
let settings = {};
|
|
1964
|
-
if (
|
|
1965
|
-
settings = JSON.parse(
|
|
2250
|
+
if (fs7.existsSync(settingsPath)) {
|
|
2251
|
+
settings = JSON.parse(fs7.readFileSync(settingsPath, "utf-8"));
|
|
1966
2252
|
}
|
|
1967
2253
|
let settingsChanged = false;
|
|
1968
2254
|
if (sessionHooks) {
|
|
@@ -1993,36 +2279,36 @@ function setupProject(options) {
|
|
|
1993
2279
|
statusline?.cleanup?.();
|
|
1994
2280
|
}
|
|
1995
2281
|
if (settingsChanged) {
|
|
1996
|
-
if (!
|
|
1997
|
-
|
|
2282
|
+
if (!fs7.existsSync(claudeDir)) {
|
|
2283
|
+
fs7.mkdirSync(claudeDir, { recursive: true });
|
|
1998
2284
|
}
|
|
1999
|
-
|
|
2285
|
+
fs7.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
2000
2286
|
changed = true;
|
|
2001
2287
|
}
|
|
2002
2288
|
if (claudeMd) {
|
|
2003
|
-
const rulesDir =
|
|
2004
|
-
const rulesLabel = scope === "user" ?
|
|
2005
|
-
if (
|
|
2006
|
-
const existing =
|
|
2289
|
+
const rulesDir = path9.dirname(rulesPath);
|
|
2290
|
+
const rulesLabel = scope === "user" ? path9.join(path9.relative(os3.homedir(), path9.dirname(rulesPath)), "keepgoing.md").replace(/\\/g, "/") : ".claude/rules/keepgoing.md";
|
|
2291
|
+
if (fs7.existsSync(rulesPath)) {
|
|
2292
|
+
const existing = fs7.readFileSync(rulesPath, "utf-8");
|
|
2007
2293
|
const existingVersion = getRulesFileVersion(existing);
|
|
2008
2294
|
if (existingVersion === null) {
|
|
2009
2295
|
messages.push(`Rules file: Custom file found at ${rulesLabel}, skipping`);
|
|
2010
2296
|
} else if (existingVersion >= KEEPGOING_RULES_VERSION) {
|
|
2011
2297
|
messages.push(`Rules file: Already up to date (v${existingVersion}), skipped`);
|
|
2012
2298
|
} else {
|
|
2013
|
-
if (!
|
|
2014
|
-
|
|
2299
|
+
if (!fs7.existsSync(rulesDir)) {
|
|
2300
|
+
fs7.mkdirSync(rulesDir, { recursive: true });
|
|
2015
2301
|
}
|
|
2016
|
-
|
|
2302
|
+
fs7.writeFileSync(rulesPath, KEEPGOING_RULES_CONTENT);
|
|
2017
2303
|
changed = true;
|
|
2018
2304
|
messages.push(`Rules file: Updated v${existingVersion} \u2192 v${KEEPGOING_RULES_VERSION} at ${rulesLabel}`);
|
|
2019
2305
|
}
|
|
2020
2306
|
} else {
|
|
2021
|
-
const existingClaudeMd =
|
|
2022
|
-
if (!
|
|
2023
|
-
|
|
2307
|
+
const existingClaudeMd = fs7.existsSync(claudeMdPath) ? fs7.readFileSync(claudeMdPath, "utf-8") : "";
|
|
2308
|
+
if (!fs7.existsSync(rulesDir)) {
|
|
2309
|
+
fs7.mkdirSync(rulesDir, { recursive: true });
|
|
2024
2310
|
}
|
|
2025
|
-
|
|
2311
|
+
fs7.writeFileSync(rulesPath, KEEPGOING_RULES_CONTENT);
|
|
2026
2312
|
changed = true;
|
|
2027
2313
|
if (existingClaudeMd.includes("## KeepGoing")) {
|
|
2028
2314
|
const mdLabel = scope === "user" ? "~/.claude/CLAUDE.md" : "CLAUDE.md";
|
|
@@ -2458,7 +2744,7 @@ function registerGetReentryBriefing(server, reader, workspacePath) {
|
|
|
2458
2744
|
}
|
|
2459
2745
|
|
|
2460
2746
|
// src/tools/saveCheckpoint.ts
|
|
2461
|
-
import
|
|
2747
|
+
import path10 from "path";
|
|
2462
2748
|
import { z as z4 } from "zod";
|
|
2463
2749
|
function registerSaveCheckpoint(server, reader, workspacePath) {
|
|
2464
2750
|
server.tool(
|
|
@@ -2470,12 +2756,19 @@ function registerSaveCheckpoint(server, reader, workspacePath) {
|
|
|
2470
2756
|
blocker: z4.string().optional().describe("Any blocker preventing progress")
|
|
2471
2757
|
},
|
|
2472
2758
|
async ({ summary, nextStep, blocker }) => {
|
|
2759
|
+
summary = stripAgentTags(summary);
|
|
2760
|
+
nextStep = nextStep ? stripAgentTags(nextStep) : nextStep;
|
|
2761
|
+
blocker = blocker ? stripAgentTags(blocker) : blocker;
|
|
2473
2762
|
const lastSession = reader.getLastSession();
|
|
2474
2763
|
const gitBranch = getCurrentBranch(workspacePath);
|
|
2475
2764
|
const touchedFiles = getTouchedFiles(workspacePath);
|
|
2476
2765
|
const commitHashes = getCommitsSince(workspacePath, lastSession?.timestamp);
|
|
2477
|
-
const projectName =
|
|
2766
|
+
const projectName = path10.basename(resolveStorageRoot(workspacePath));
|
|
2767
|
+
const writer = new KeepGoingWriter(workspacePath);
|
|
2478
2768
|
const sessionId = generateSessionId({ workspaceRoot: workspacePath, branch: gitBranch ?? void 0, worktreePath: workspacePath });
|
|
2769
|
+
const existingTasks = writer.readCurrentTasks();
|
|
2770
|
+
const existingSession = existingTasks.find((t) => t.sessionId === sessionId);
|
|
2771
|
+
const sessionPhase = existingSession?.sessionPhase;
|
|
2479
2772
|
const checkpoint = createCheckpoint({
|
|
2480
2773
|
summary,
|
|
2481
2774
|
nextStep: nextStep || "",
|
|
@@ -2485,10 +2778,19 @@ function registerSaveCheckpoint(server, reader, workspacePath) {
|
|
|
2485
2778
|
commitHashes,
|
|
2486
2779
|
workspaceRoot: workspacePath,
|
|
2487
2780
|
source: "manual",
|
|
2488
|
-
sessionId
|
|
2781
|
+
sessionId,
|
|
2782
|
+
...sessionPhase ? { sessionPhase } : {},
|
|
2783
|
+
...sessionPhase === "planning" ? { tags: ["plan"] } : {}
|
|
2489
2784
|
});
|
|
2490
|
-
const writer = new KeepGoingWriter(workspacePath);
|
|
2491
2785
|
writer.saveCheckpoint(checkpoint, projectName);
|
|
2786
|
+
writer.upsertSession({
|
|
2787
|
+
sessionId,
|
|
2788
|
+
sessionActive: true,
|
|
2789
|
+
branch: gitBranch ?? void 0,
|
|
2790
|
+
updatedAt: checkpoint.timestamp,
|
|
2791
|
+
taskSummary: summary,
|
|
2792
|
+
nextStep: nextStep || void 0
|
|
2793
|
+
});
|
|
2492
2794
|
const lines = [
|
|
2493
2795
|
`Checkpoint saved.`,
|
|
2494
2796
|
`- **ID:** ${checkpoint.id}`,
|
|
@@ -2641,8 +2943,9 @@ function registerGetCurrentTask(server, reader) {
|
|
|
2641
2943
|
lines.push("");
|
|
2642
2944
|
}
|
|
2643
2945
|
for (const task of [...activeTasks, ...finishedTasks]) {
|
|
2946
|
+
const phaseLabel = task.sessionPhase === "planning" ? "Planning" : void 0;
|
|
2644
2947
|
const statusIcon = task.sessionActive ? "\u{1F7E2}" : "\u2705";
|
|
2645
|
-
const statusLabel = task.sessionActive ? "Active" : "Finished";
|
|
2948
|
+
const statusLabel = task.sessionActive ? phaseLabel || "Active" : "Finished";
|
|
2646
2949
|
const sessionLabel = task.sessionLabel || task.agentLabel || task.sessionId || "Session";
|
|
2647
2950
|
lines.push(`### ${statusIcon} ${sessionLabel} (${statusLabel})`);
|
|
2648
2951
|
lines.push(`- **Updated:** ${formatRelativeTime(task.updatedAt)}`);
|
|
@@ -2692,25 +2995,25 @@ function registerGetCurrentTask(server, reader) {
|
|
|
2692
2995
|
import { z as z6 } from "zod";
|
|
2693
2996
|
|
|
2694
2997
|
// src/cli/migrate.ts
|
|
2695
|
-
import
|
|
2998
|
+
import fs8 from "fs";
|
|
2696
2999
|
import os4 from "os";
|
|
2697
|
-
import
|
|
3000
|
+
import path11 from "path";
|
|
2698
3001
|
var STATUSLINE_CMD2 = "npx -y @keepgoingdev/mcp-server --statusline";
|
|
2699
3002
|
function isLegacyStatusline(command) {
|
|
2700
3003
|
return !command.includes("--statusline") && command.includes("keepgoing-statusline");
|
|
2701
3004
|
}
|
|
2702
3005
|
function migrateStatusline(wsPath) {
|
|
2703
|
-
const settingsPath =
|
|
2704
|
-
if (!
|
|
3006
|
+
const settingsPath = path11.join(wsPath, ".claude", "settings.json");
|
|
3007
|
+
if (!fs8.existsSync(settingsPath)) return void 0;
|
|
2705
3008
|
try {
|
|
2706
|
-
const settings = JSON.parse(
|
|
3009
|
+
const settings = JSON.parse(fs8.readFileSync(settingsPath, "utf-8"));
|
|
2707
3010
|
const cmd = settings.statusLine?.command;
|
|
2708
3011
|
if (!cmd || !isLegacyStatusline(cmd)) return void 0;
|
|
2709
3012
|
settings.statusLine = {
|
|
2710
3013
|
type: "command",
|
|
2711
3014
|
command: STATUSLINE_CMD2
|
|
2712
3015
|
};
|
|
2713
|
-
|
|
3016
|
+
fs8.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
2714
3017
|
cleanupLegacyScript();
|
|
2715
3018
|
return "[KeepGoing] Migrated statusline to auto-updating command (restart Claude Code to apply)";
|
|
2716
3019
|
} catch {
|
|
@@ -2718,10 +3021,10 @@ function migrateStatusline(wsPath) {
|
|
|
2718
3021
|
}
|
|
2719
3022
|
}
|
|
2720
3023
|
function cleanupLegacyScript() {
|
|
2721
|
-
const legacyScript =
|
|
2722
|
-
if (
|
|
3024
|
+
const legacyScript = path11.join(os4.homedir(), ".claude", "keepgoing-statusline.sh");
|
|
3025
|
+
if (fs8.existsSync(legacyScript)) {
|
|
2723
3026
|
try {
|
|
2724
|
-
|
|
3027
|
+
fs8.unlinkSync(legacyScript);
|
|
2725
3028
|
} catch {
|
|
2726
3029
|
}
|
|
2727
3030
|
}
|
|
@@ -2954,6 +3257,87 @@ function registerContinueOn(server, reader, workspacePath) {
|
|
|
2954
3257
|
);
|
|
2955
3258
|
}
|
|
2956
3259
|
|
|
3260
|
+
// src/tools/getContextSnapshot.ts
|
|
3261
|
+
import { z as z10 } from "zod";
|
|
3262
|
+
function registerGetContextSnapshot(server, reader, workspacePath) {
|
|
3263
|
+
server.tool(
|
|
3264
|
+
"get_context_snapshot",
|
|
3265
|
+
"Get a compact context snapshot: what you were doing, what is next, and momentum. Use this for quick orientation without a full briefing.",
|
|
3266
|
+
{
|
|
3267
|
+
format: z10.enum(["text", "json"]).optional().describe('Output format. "text" (default) returns a formatted single line. "json" returns the structured snapshot object.')
|
|
3268
|
+
},
|
|
3269
|
+
async ({ format }) => {
|
|
3270
|
+
const snapshot = generateContextSnapshot(workspacePath);
|
|
3271
|
+
if (!snapshot) {
|
|
3272
|
+
return {
|
|
3273
|
+
content: [
|
|
3274
|
+
{
|
|
3275
|
+
type: "text",
|
|
3276
|
+
text: "No KeepGoing data found. The developer has not saved any checkpoints yet."
|
|
3277
|
+
}
|
|
3278
|
+
]
|
|
3279
|
+
};
|
|
3280
|
+
}
|
|
3281
|
+
if (format === "json") {
|
|
3282
|
+
return {
|
|
3283
|
+
content: [{ type: "text", text: JSON.stringify(snapshot, null, 2) }]
|
|
3284
|
+
};
|
|
3285
|
+
}
|
|
3286
|
+
const icon = snapshot.momentum === "hot" ? "\u26A1" : snapshot.momentum === "warm" ? "\u{1F536}" : "\u{1F4A4}";
|
|
3287
|
+
const parts = [];
|
|
3288
|
+
parts.push(`${icon} ${snapshot.doing}`);
|
|
3289
|
+
if (snapshot.next) {
|
|
3290
|
+
parts.push(`\u2192 ${snapshot.next}`);
|
|
3291
|
+
}
|
|
3292
|
+
let line = parts.join(" ");
|
|
3293
|
+
line += ` (${snapshot.when})`;
|
|
3294
|
+
if (snapshot.blocker) {
|
|
3295
|
+
line += ` \u26D4 ${snapshot.blocker}`;
|
|
3296
|
+
}
|
|
3297
|
+
if (snapshot.activeAgents && snapshot.activeAgents > 0) {
|
|
3298
|
+
line += ` [${snapshot.activeAgents} active agent${snapshot.activeAgents > 1 ? "s" : ""}]`;
|
|
3299
|
+
}
|
|
3300
|
+
return {
|
|
3301
|
+
content: [{ type: "text", text: line }]
|
|
3302
|
+
};
|
|
3303
|
+
}
|
|
3304
|
+
);
|
|
3305
|
+
}
|
|
3306
|
+
|
|
3307
|
+
// src/tools/getWhatsHot.ts
|
|
3308
|
+
import { z as z11 } from "zod";
|
|
3309
|
+
function registerGetWhatsHot(server) {
|
|
3310
|
+
server.tool(
|
|
3311
|
+
"get_whats_hot",
|
|
3312
|
+
"Get a summary of activity across all registered projects, sorted by momentum. Shows what the developer is working on across their entire portfolio.",
|
|
3313
|
+
{
|
|
3314
|
+
format: z11.enum(["text", "json"]).optional().describe('Output format. "text" (default) returns formatted lines. "json" returns the structured summary object.')
|
|
3315
|
+
},
|
|
3316
|
+
async ({ format }) => {
|
|
3317
|
+
const summary = generateCrossProjectSummary();
|
|
3318
|
+
if (summary.projects.length === 0) {
|
|
3319
|
+
return {
|
|
3320
|
+
content: [
|
|
3321
|
+
{
|
|
3322
|
+
type: "text",
|
|
3323
|
+
text: "No projects with activity found. Projects are registered automatically when checkpoints are saved."
|
|
3324
|
+
}
|
|
3325
|
+
]
|
|
3326
|
+
};
|
|
3327
|
+
}
|
|
3328
|
+
if (format === "json") {
|
|
3329
|
+
return {
|
|
3330
|
+
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
|
|
3331
|
+
};
|
|
3332
|
+
}
|
|
3333
|
+
const lines = summary.projects.map((entry) => formatCrossProjectLine(entry));
|
|
3334
|
+
return {
|
|
3335
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
3336
|
+
};
|
|
3337
|
+
}
|
|
3338
|
+
);
|
|
3339
|
+
}
|
|
3340
|
+
|
|
2957
3341
|
// src/prompts/resume.ts
|
|
2958
3342
|
function registerResumePrompt(server) {
|
|
2959
3343
|
server.prompt(
|
|
@@ -3119,10 +3503,11 @@ async function handlePrintCurrent() {
|
|
|
3119
3503
|
}
|
|
3120
3504
|
|
|
3121
3505
|
// src/cli/saveCheckpoint.ts
|
|
3122
|
-
import
|
|
3506
|
+
import path12 from "path";
|
|
3123
3507
|
async function handleSaveCheckpoint() {
|
|
3124
3508
|
const wsPath = resolveWsPath();
|
|
3125
3509
|
const reader = new KeepGoingReader(wsPath);
|
|
3510
|
+
const writer = new KeepGoingWriter(wsPath);
|
|
3126
3511
|
const { session: lastSession } = reader.getScopedLastSession();
|
|
3127
3512
|
if (lastSession?.timestamp) {
|
|
3128
3513
|
const ageMs = Date.now() - new Date(lastSession.timestamp).getTime();
|
|
@@ -3132,10 +3517,40 @@ async function handleSaveCheckpoint() {
|
|
|
3132
3517
|
}
|
|
3133
3518
|
const touchedFiles = getTouchedFiles(wsPath);
|
|
3134
3519
|
const commitHashes = getCommitsSince(wsPath, lastSession?.timestamp);
|
|
3135
|
-
|
|
3520
|
+
const gitBranch = getCurrentBranch(wsPath);
|
|
3521
|
+
const sessionId = generateSessionId({ workspaceRoot: wsPath, branch: gitBranch ?? void 0, worktreePath: wsPath });
|
|
3522
|
+
const existingTasks = writer.readCurrentTasks();
|
|
3523
|
+
const existingSession = existingTasks.find((t) => t.sessionId === sessionId);
|
|
3524
|
+
const isPlanning = existingSession?.sessionPhase === "planning";
|
|
3525
|
+
if (touchedFiles.length === 0 && commitHashes.length === 0 && !isPlanning) {
|
|
3526
|
+
process.exit(0);
|
|
3527
|
+
}
|
|
3528
|
+
const projectName = path12.basename(resolveStorageRoot(wsPath));
|
|
3529
|
+
if (touchedFiles.length === 0 && commitHashes.length === 0 && isPlanning) {
|
|
3530
|
+
const summary2 = existingSession?.sessionLabel || existingSession?.taskSummary || "Planning session";
|
|
3531
|
+
const checkpoint2 = createCheckpoint({
|
|
3532
|
+
summary: summary2,
|
|
3533
|
+
nextStep: existingSession?.nextStep || "",
|
|
3534
|
+
gitBranch,
|
|
3535
|
+
touchedFiles: [],
|
|
3536
|
+
commitHashes: [],
|
|
3537
|
+
workspaceRoot: wsPath,
|
|
3538
|
+
source: "auto",
|
|
3539
|
+
sessionId,
|
|
3540
|
+
sessionPhase: "planning",
|
|
3541
|
+
tags: ["plan"]
|
|
3542
|
+
});
|
|
3543
|
+
writer.saveCheckpoint(checkpoint2, projectName);
|
|
3544
|
+
writer.upsertSession({
|
|
3545
|
+
sessionId,
|
|
3546
|
+
sessionActive: false,
|
|
3547
|
+
nextStep: checkpoint2.nextStep || void 0,
|
|
3548
|
+
branch: gitBranch ?? void 0,
|
|
3549
|
+
updatedAt: checkpoint2.timestamp
|
|
3550
|
+
});
|
|
3551
|
+
console.log(`[KeepGoing] Plan checkpoint saved: ${summary2}`);
|
|
3136
3552
|
process.exit(0);
|
|
3137
3553
|
}
|
|
3138
|
-
const gitBranch = getCurrentBranch(wsPath);
|
|
3139
3554
|
const commitMessages = getCommitMessagesSince(wsPath, lastSession?.timestamp);
|
|
3140
3555
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3141
3556
|
const events = buildSessionEvents({
|
|
@@ -3147,10 +3562,9 @@ async function handleSaveCheckpoint() {
|
|
|
3147
3562
|
sessionStartTime: lastSession?.timestamp ?? now,
|
|
3148
3563
|
lastActivityTime: now
|
|
3149
3564
|
});
|
|
3150
|
-
const summary = buildSmartSummary(events) ?? `Worked on ${touchedFiles.slice(0, 5).map((f) =>
|
|
3565
|
+
const summary = buildSmartSummary(events) ?? `Worked on ${touchedFiles.slice(0, 5).map((f) => path12.basename(f)).join(", ")}`;
|
|
3151
3566
|
const nextStep = buildSmartNextStep(events);
|
|
3152
|
-
const
|
|
3153
|
-
const sessionId = generateSessionId({ workspaceRoot: wsPath, branch: gitBranch ?? void 0, worktreePath: wsPath });
|
|
3567
|
+
const sessionPhase = existingSession?.sessionPhase;
|
|
3154
3568
|
const checkpoint = createCheckpoint({
|
|
3155
3569
|
summary,
|
|
3156
3570
|
nextStep,
|
|
@@ -3159,9 +3573,10 @@ async function handleSaveCheckpoint() {
|
|
|
3159
3573
|
commitHashes,
|
|
3160
3574
|
workspaceRoot: wsPath,
|
|
3161
3575
|
source: "auto",
|
|
3162
|
-
sessionId
|
|
3576
|
+
sessionId,
|
|
3577
|
+
...sessionPhase ? { sessionPhase } : {},
|
|
3578
|
+
...sessionPhase === "planning" ? { tags: ["plan"] } : {}
|
|
3163
3579
|
});
|
|
3164
|
-
const writer = new KeepGoingWriter(wsPath);
|
|
3165
3580
|
writer.saveCheckpoint(checkpoint, projectName);
|
|
3166
3581
|
writer.upsertSession({
|
|
3167
3582
|
sessionId,
|
|
@@ -3194,7 +3609,7 @@ async function handleSaveCheckpoint() {
|
|
|
3194
3609
|
}
|
|
3195
3610
|
|
|
3196
3611
|
// src/cli/transcriptUtils.ts
|
|
3197
|
-
import
|
|
3612
|
+
import fs9 from "fs";
|
|
3198
3613
|
var TAIL_READ_BYTES = 32768;
|
|
3199
3614
|
var LATEST_LABEL_READ_BYTES = 65536;
|
|
3200
3615
|
var TOOL_VERB_MAP = {
|
|
@@ -3251,9 +3666,9 @@ function isAssistantEntry(entry) {
|
|
|
3251
3666
|
return entry.message?.role === "assistant";
|
|
3252
3667
|
}
|
|
3253
3668
|
function extractSessionLabel(transcriptPath) {
|
|
3254
|
-
if (!transcriptPath || !
|
|
3669
|
+
if (!transcriptPath || !fs9.existsSync(transcriptPath)) return null;
|
|
3255
3670
|
try {
|
|
3256
|
-
const raw =
|
|
3671
|
+
const raw = fs9.readFileSync(transcriptPath, "utf-8");
|
|
3257
3672
|
for (const line of raw.split("\n")) {
|
|
3258
3673
|
const trimmed = line.trim();
|
|
3259
3674
|
if (!trimmed) continue;
|
|
@@ -3266,7 +3681,9 @@ function extractSessionLabel(transcriptPath) {
|
|
|
3266
3681
|
if (!isUserEntry(entry)) continue;
|
|
3267
3682
|
let text = extractTextFromContent(entry.message?.content);
|
|
3268
3683
|
if (!text) continue;
|
|
3269
|
-
if (text.startsWith("[") || /^<[a-z][\w-]
|
|
3684
|
+
if (text.startsWith("[") || /^<[a-z][\w-]*[\s>]/.test(text)) continue;
|
|
3685
|
+
text = stripAgentTags(text);
|
|
3686
|
+
if (!text) continue;
|
|
3270
3687
|
text = text.replace(/@[\w./\-]+/g, "").trim();
|
|
3271
3688
|
text = text.replace(FILLER_PREFIX_RE, "").trim();
|
|
3272
3689
|
text = text.replace(MARKDOWN_HEADING_RE, "").trim();
|
|
@@ -3282,19 +3699,19 @@ function extractSessionLabel(transcriptPath) {
|
|
|
3282
3699
|
return null;
|
|
3283
3700
|
}
|
|
3284
3701
|
function extractLatestUserLabel(transcriptPath) {
|
|
3285
|
-
if (!transcriptPath || !
|
|
3702
|
+
if (!transcriptPath || !fs9.existsSync(transcriptPath)) return null;
|
|
3286
3703
|
try {
|
|
3287
|
-
const stat =
|
|
3704
|
+
const stat = fs9.statSync(transcriptPath);
|
|
3288
3705
|
const fileSize = stat.size;
|
|
3289
3706
|
if (fileSize === 0) return null;
|
|
3290
3707
|
const readSize = Math.min(fileSize, LATEST_LABEL_READ_BYTES);
|
|
3291
3708
|
const offset = fileSize - readSize;
|
|
3292
3709
|
const buf = Buffer.alloc(readSize);
|
|
3293
|
-
const fd =
|
|
3710
|
+
const fd = fs9.openSync(transcriptPath, "r");
|
|
3294
3711
|
try {
|
|
3295
|
-
|
|
3712
|
+
fs9.readSync(fd, buf, 0, readSize, offset);
|
|
3296
3713
|
} finally {
|
|
3297
|
-
|
|
3714
|
+
fs9.closeSync(fd);
|
|
3298
3715
|
}
|
|
3299
3716
|
const tail = buf.toString("utf-8");
|
|
3300
3717
|
const lines = tail.split("\n").reverse();
|
|
@@ -3310,7 +3727,9 @@ function extractLatestUserLabel(transcriptPath) {
|
|
|
3310
3727
|
if (!isUserEntry(entry)) continue;
|
|
3311
3728
|
let text = extractTextFromContent(entry.message?.content);
|
|
3312
3729
|
if (!text) continue;
|
|
3313
|
-
if (text.startsWith("[") || /^<[a-z][\w-]
|
|
3730
|
+
if (text.startsWith("[") || /^<[a-z][\w-]*[\s>]/.test(text)) continue;
|
|
3731
|
+
text = stripAgentTags(text);
|
|
3732
|
+
if (!text) continue;
|
|
3314
3733
|
text = text.replace(/@[\w./\-]+/g, "").trim();
|
|
3315
3734
|
text = text.replace(FILLER_PREFIX_RE, "").trim();
|
|
3316
3735
|
text = text.replace(MARKDOWN_HEADING_RE, "").trim();
|
|
@@ -3326,19 +3745,19 @@ function extractLatestUserLabel(transcriptPath) {
|
|
|
3326
3745
|
return null;
|
|
3327
3746
|
}
|
|
3328
3747
|
function extractCurrentAction(transcriptPath) {
|
|
3329
|
-
if (!transcriptPath || !
|
|
3748
|
+
if (!transcriptPath || !fs9.existsSync(transcriptPath)) return null;
|
|
3330
3749
|
try {
|
|
3331
|
-
const stat =
|
|
3750
|
+
const stat = fs9.statSync(transcriptPath);
|
|
3332
3751
|
const fileSize = stat.size;
|
|
3333
3752
|
if (fileSize === 0) return null;
|
|
3334
3753
|
const readSize = Math.min(fileSize, TAIL_READ_BYTES);
|
|
3335
3754
|
const offset = fileSize - readSize;
|
|
3336
3755
|
const buf = Buffer.alloc(readSize);
|
|
3337
|
-
const fd =
|
|
3756
|
+
const fd = fs9.openSync(transcriptPath, "r");
|
|
3338
3757
|
try {
|
|
3339
|
-
|
|
3758
|
+
fs9.readSync(fd, buf, 0, readSize, offset);
|
|
3340
3759
|
} finally {
|
|
3341
|
-
|
|
3760
|
+
fs9.closeSync(fd);
|
|
3342
3761
|
}
|
|
3343
3762
|
const tail = buf.toString("utf-8");
|
|
3344
3763
|
const lines = tail.split("\n").reverse();
|
|
@@ -3373,6 +3792,8 @@ async function handleUpdateTask() {
|
|
|
3373
3792
|
if (payloadStr) {
|
|
3374
3793
|
try {
|
|
3375
3794
|
const payload = JSON.parse(payloadStr);
|
|
3795
|
+
if (payload.taskSummary) payload.taskSummary = stripAgentTags(payload.taskSummary);
|
|
3796
|
+
if (payload.sessionLabel) payload.sessionLabel = stripAgentTags(payload.sessionLabel);
|
|
3376
3797
|
const writer = new KeepGoingWriter(wsPath);
|
|
3377
3798
|
const branch = payload.branch ?? getCurrentBranch(wsPath) ?? void 0;
|
|
3378
3799
|
const task = {
|
|
@@ -3423,7 +3844,8 @@ async function handleUpdateTaskFromHook() {
|
|
|
3423
3844
|
branch,
|
|
3424
3845
|
worktreePath: wsPath,
|
|
3425
3846
|
sessionActive: true,
|
|
3426
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3847
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3848
|
+
sessionPhase: "active"
|
|
3427
3849
|
};
|
|
3428
3850
|
const sessionId = hookData.session_id || generateSessionId({ ...task, workspaceRoot: wsPath });
|
|
3429
3851
|
task.sessionId = sessionId;
|
|
@@ -3440,8 +3862,8 @@ async function handleUpdateTaskFromHook() {
|
|
|
3440
3862
|
}
|
|
3441
3863
|
|
|
3442
3864
|
// src/cli/statusline.ts
|
|
3443
|
-
import
|
|
3444
|
-
import
|
|
3865
|
+
import fs10 from "fs";
|
|
3866
|
+
import path13 from "path";
|
|
3445
3867
|
var STDIN_TIMEOUT_MS2 = 3e3;
|
|
3446
3868
|
async function handleStatusline() {
|
|
3447
3869
|
const chunks = [];
|
|
@@ -3471,9 +3893,9 @@ async function handleStatusline() {
|
|
|
3471
3893
|
if (!label) {
|
|
3472
3894
|
try {
|
|
3473
3895
|
const gitRoot = findGitRoot(dir);
|
|
3474
|
-
const tasksFile =
|
|
3475
|
-
if (
|
|
3476
|
-
const data = JSON.parse(
|
|
3896
|
+
const tasksFile = path13.join(gitRoot, ".keepgoing", "current-tasks.json");
|
|
3897
|
+
if (fs10.existsSync(tasksFile)) {
|
|
3898
|
+
const data = JSON.parse(fs10.readFileSync(tasksFile, "utf-8"));
|
|
3477
3899
|
const tasks = pruneStaleTasks(data.tasks ?? []);
|
|
3478
3900
|
const match = sessionId ? tasks.find((t) => t.sessionId === sessionId) : void 0;
|
|
3479
3901
|
if (match?.sessionLabel) {
|
|
@@ -3570,6 +3992,59 @@ async function handleDetectDecisions() {
|
|
|
3570
3992
|
process.exit(0);
|
|
3571
3993
|
}
|
|
3572
3994
|
|
|
3995
|
+
// src/cli/heartbeat.ts
|
|
3996
|
+
var STDIN_TIMEOUT_MS3 = 3e3;
|
|
3997
|
+
var THROTTLE_MS = 3e4;
|
|
3998
|
+
async function handleHeartbeat() {
|
|
3999
|
+
const wsPath = resolveWsPath();
|
|
4000
|
+
const chunks = [];
|
|
4001
|
+
const timeout = setTimeout(() => process.exit(0), STDIN_TIMEOUT_MS3);
|
|
4002
|
+
process.stdin.on("error", () => {
|
|
4003
|
+
clearTimeout(timeout);
|
|
4004
|
+
process.exit(0);
|
|
4005
|
+
});
|
|
4006
|
+
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
4007
|
+
process.stdin.on("end", () => {
|
|
4008
|
+
clearTimeout(timeout);
|
|
4009
|
+
try {
|
|
4010
|
+
const raw = Buffer.concat(chunks).toString("utf-8").trim();
|
|
4011
|
+
if (!raw) {
|
|
4012
|
+
process.exit(0);
|
|
4013
|
+
}
|
|
4014
|
+
const hookData = JSON.parse(raw);
|
|
4015
|
+
const writer = new KeepGoingWriter(wsPath);
|
|
4016
|
+
const existing = writer.readCurrentTasks();
|
|
4017
|
+
const sessionIdFromHook = hookData.session_id;
|
|
4018
|
+
const sessionId = sessionIdFromHook || generateSessionId({ workspaceRoot: wsPath, worktreePath: wsPath, branch: getCurrentBranch(wsPath) ?? void 0 });
|
|
4019
|
+
const existingSession = existing.find((t) => t.sessionId === sessionId);
|
|
4020
|
+
if (existingSession?.updatedAt) {
|
|
4021
|
+
const ageMs = Date.now() - new Date(existingSession.updatedAt).getTime();
|
|
4022
|
+
if (ageMs < THROTTLE_MS) {
|
|
4023
|
+
process.exit(0);
|
|
4024
|
+
}
|
|
4025
|
+
}
|
|
4026
|
+
const sessionPhase = existingSession?.sessionPhase === "active" ? "active" : "planning";
|
|
4027
|
+
const branch = existingSession?.branch ?? getCurrentBranch(wsPath) ?? void 0;
|
|
4028
|
+
const task = {
|
|
4029
|
+
sessionId,
|
|
4030
|
+
sessionActive: true,
|
|
4031
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4032
|
+
sessionPhase,
|
|
4033
|
+
worktreePath: wsPath,
|
|
4034
|
+
branch
|
|
4035
|
+
};
|
|
4036
|
+
if (!existingSession?.sessionLabel && hookData.transcript_path) {
|
|
4037
|
+
const label = extractSessionLabel(hookData.transcript_path);
|
|
4038
|
+
if (label) task.sessionLabel = label;
|
|
4039
|
+
}
|
|
4040
|
+
writer.upsertSession(task);
|
|
4041
|
+
} catch {
|
|
4042
|
+
}
|
|
4043
|
+
process.exit(0);
|
|
4044
|
+
});
|
|
4045
|
+
process.stdin.resume();
|
|
4046
|
+
}
|
|
4047
|
+
|
|
3573
4048
|
// src/index.ts
|
|
3574
4049
|
var CLI_HANDLERS = {
|
|
3575
4050
|
"--print-momentum": handlePrintMomentum,
|
|
@@ -3579,7 +4054,8 @@ var CLI_HANDLERS = {
|
|
|
3579
4054
|
"--print-current": handlePrintCurrent,
|
|
3580
4055
|
"--statusline": handleStatusline,
|
|
3581
4056
|
"--continue-on": handleContinueOn,
|
|
3582
|
-
"--detect-decisions": handleDetectDecisions
|
|
4057
|
+
"--detect-decisions": handleDetectDecisions,
|
|
4058
|
+
"--heartbeat": handleHeartbeat
|
|
3583
4059
|
};
|
|
3584
4060
|
var flag = process.argv.slice(2).find((a) => a in CLI_HANDLERS);
|
|
3585
4061
|
if (flag) {
|
|
@@ -3598,6 +4074,8 @@ if (flag) {
|
|
|
3598
4074
|
registerGetCurrentTask(server, reader);
|
|
3599
4075
|
registerSaveCheckpoint(server, reader, workspacePath);
|
|
3600
4076
|
registerContinueOn(server, reader, workspacePath);
|
|
4077
|
+
registerGetContextSnapshot(server, reader, workspacePath);
|
|
4078
|
+
registerGetWhatsHot(server);
|
|
3601
4079
|
registerSetupProject(server, workspacePath);
|
|
3602
4080
|
registerActivateLicense(server);
|
|
3603
4081
|
registerDeactivateLicense(server);
|