@keepgoingdev/cli 1.2.1 → 1.3.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 +938 -583
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
// ../../packages/shared/src/session.ts
|
|
4
4
|
import { randomUUID } from "crypto";
|
|
5
|
+
|
|
6
|
+
// ../../packages/shared/src/sanitize.ts
|
|
7
|
+
var AGENT_TAG_PATTERN = /<\/?(?:teammate-message|system-reminder|command-name|tool_use)\b[^>]*>/g;
|
|
8
|
+
function stripAgentTags(text) {
|
|
9
|
+
if (!text) return "";
|
|
10
|
+
const stripped = text.replace(AGENT_TAG_PATTERN, "");
|
|
11
|
+
return stripped.trim();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// ../../packages/shared/src/session.ts
|
|
5
15
|
function generateCheckpointId() {
|
|
6
16
|
return randomUUID();
|
|
7
17
|
}
|
|
@@ -9,7 +19,10 @@ function createCheckpoint(fields) {
|
|
|
9
19
|
return {
|
|
10
20
|
id: generateCheckpointId(),
|
|
11
21
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12
|
-
...fields
|
|
22
|
+
...fields,
|
|
23
|
+
summary: stripAgentTags(fields.summary),
|
|
24
|
+
nextStep: stripAgentTags(fields.nextStep),
|
|
25
|
+
blocker: fields.blocker ? stripAgentTags(fields.blocker) : fields.blocker
|
|
13
26
|
};
|
|
14
27
|
}
|
|
15
28
|
|
|
@@ -60,7 +73,8 @@ function findGitRoot(startPath) {
|
|
|
60
73
|
const toplevel = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
61
74
|
cwd: startPath,
|
|
62
75
|
encoding: "utf-8",
|
|
63
|
-
timeout: 5e3
|
|
76
|
+
timeout: 5e3,
|
|
77
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
64
78
|
}).trim();
|
|
65
79
|
return toplevel || startPath;
|
|
66
80
|
} catch {
|
|
@@ -77,12 +91,14 @@ function resolveStorageRoot(startPath) {
|
|
|
77
91
|
const toplevel = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
78
92
|
cwd: startPath,
|
|
79
93
|
encoding: "utf-8",
|
|
80
|
-
timeout: 5e3
|
|
94
|
+
timeout: 5e3,
|
|
95
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
81
96
|
}).trim();
|
|
82
97
|
const commonDir = execFileSync("git", ["rev-parse", "--git-common-dir"], {
|
|
83
98
|
cwd: startPath,
|
|
84
99
|
encoding: "utf-8",
|
|
85
|
-
timeout: 5e3
|
|
100
|
+
timeout: 5e3,
|
|
101
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
86
102
|
}).trim();
|
|
87
103
|
const absoluteCommonDir = path.resolve(toplevel, commonDir);
|
|
88
104
|
const mainRoot = path.dirname(absoluteCommonDir);
|
|
@@ -585,22 +601,160 @@ function formatContinueOnPrompt(context, options) {
|
|
|
585
601
|
return result;
|
|
586
602
|
}
|
|
587
603
|
|
|
588
|
-
// ../../packages/shared/src/
|
|
589
|
-
import
|
|
590
|
-
import
|
|
591
|
-
import { randomUUID as randomUUID2, createHash } from "crypto";
|
|
604
|
+
// ../../packages/shared/src/reader.ts
|
|
605
|
+
import fs4 from "fs";
|
|
606
|
+
import path6 from "path";
|
|
592
607
|
|
|
593
|
-
// ../../packages/shared/src/
|
|
608
|
+
// ../../packages/shared/src/license.ts
|
|
609
|
+
import crypto from "crypto";
|
|
594
610
|
import fs from "fs";
|
|
595
611
|
import os from "os";
|
|
596
612
|
import path3 from "path";
|
|
597
|
-
var
|
|
598
|
-
var
|
|
613
|
+
var LICENSE_FILE = "license.json";
|
|
614
|
+
var DEVICE_ID_FILE = "device-id";
|
|
615
|
+
function getGlobalLicenseDir() {
|
|
616
|
+
return path3.join(os.homedir(), ".keepgoing");
|
|
617
|
+
}
|
|
618
|
+
function getGlobalLicensePath() {
|
|
619
|
+
return path3.join(getGlobalLicenseDir(), LICENSE_FILE);
|
|
620
|
+
}
|
|
621
|
+
function getDeviceId() {
|
|
622
|
+
const dir = getGlobalLicenseDir();
|
|
623
|
+
const filePath = path3.join(dir, DEVICE_ID_FILE);
|
|
624
|
+
try {
|
|
625
|
+
const existing = fs.readFileSync(filePath, "utf-8").trim();
|
|
626
|
+
if (existing) return existing;
|
|
627
|
+
} catch {
|
|
628
|
+
}
|
|
629
|
+
const id = crypto.randomUUID();
|
|
630
|
+
if (!fs.existsSync(dir)) {
|
|
631
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
632
|
+
}
|
|
633
|
+
fs.writeFileSync(filePath, id, "utf-8");
|
|
634
|
+
return id;
|
|
635
|
+
}
|
|
636
|
+
var DECISION_DETECTION_VARIANT_ID = 1361527;
|
|
637
|
+
var SESSION_AWARENESS_VARIANT_ID = 1366510;
|
|
638
|
+
var TEST_DECISION_DETECTION_VARIANT_ID = 1345647;
|
|
639
|
+
var TEST_SESSION_AWARENESS_VARIANT_ID = 1365992;
|
|
640
|
+
var VARIANT_FEATURE_MAP = {
|
|
641
|
+
[DECISION_DETECTION_VARIANT_ID]: ["decisions"],
|
|
642
|
+
[SESSION_AWARENESS_VARIANT_ID]: ["session-awareness"],
|
|
643
|
+
[TEST_DECISION_DETECTION_VARIANT_ID]: ["decisions"],
|
|
644
|
+
[TEST_SESSION_AWARENESS_VARIANT_ID]: ["session-awareness"]
|
|
645
|
+
// Future bundle: [BUNDLE_VARIANT_ID]: ['decisions', 'session-awareness'],
|
|
646
|
+
};
|
|
647
|
+
var KNOWN_VARIANT_IDS = new Set(Object.keys(VARIANT_FEATURE_MAP).map(Number));
|
|
648
|
+
function getVariantLabel(variantId) {
|
|
649
|
+
const features = VARIANT_FEATURE_MAP[variantId];
|
|
650
|
+
if (!features) return "Unknown Add-on";
|
|
651
|
+
if (features.includes("decisions") && features.includes("session-awareness")) return "Pro Bundle";
|
|
652
|
+
if (features.includes("decisions")) return "Decision Detection";
|
|
653
|
+
if (features.includes("session-awareness")) return "Session Awareness";
|
|
654
|
+
return "Pro Add-on";
|
|
655
|
+
}
|
|
656
|
+
var _cachedStore;
|
|
657
|
+
var _cacheTimestamp = 0;
|
|
658
|
+
var LICENSE_CACHE_TTL_MS = 2e3;
|
|
659
|
+
function readLicenseStore() {
|
|
660
|
+
const now = Date.now();
|
|
661
|
+
if (_cachedStore && now - _cacheTimestamp < LICENSE_CACHE_TTL_MS) {
|
|
662
|
+
return _cachedStore;
|
|
663
|
+
}
|
|
664
|
+
const licensePath = getGlobalLicensePath();
|
|
665
|
+
let store;
|
|
666
|
+
try {
|
|
667
|
+
if (!fs.existsSync(licensePath)) {
|
|
668
|
+
store = { version: 2, licenses: [] };
|
|
669
|
+
} else {
|
|
670
|
+
const raw = fs.readFileSync(licensePath, "utf-8");
|
|
671
|
+
const data = JSON.parse(raw);
|
|
672
|
+
if (data?.version === 2 && Array.isArray(data.licenses)) {
|
|
673
|
+
store = data;
|
|
674
|
+
} else {
|
|
675
|
+
store = { version: 2, licenses: [] };
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
} catch {
|
|
679
|
+
store = { version: 2, licenses: [] };
|
|
680
|
+
}
|
|
681
|
+
_cachedStore = store;
|
|
682
|
+
_cacheTimestamp = now;
|
|
683
|
+
return store;
|
|
684
|
+
}
|
|
685
|
+
function writeLicenseStore(store) {
|
|
686
|
+
const dirPath = getGlobalLicenseDir();
|
|
687
|
+
if (!fs.existsSync(dirPath)) {
|
|
688
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
689
|
+
}
|
|
690
|
+
const licensePath = path3.join(dirPath, LICENSE_FILE);
|
|
691
|
+
fs.writeFileSync(licensePath, JSON.stringify(store, null, 2), "utf-8");
|
|
692
|
+
_cachedStore = store;
|
|
693
|
+
_cacheTimestamp = Date.now();
|
|
694
|
+
}
|
|
695
|
+
function addLicenseEntry(entry) {
|
|
696
|
+
const store = readLicenseStore();
|
|
697
|
+
const idx = store.licenses.findIndex((l) => l.licenseKey === entry.licenseKey);
|
|
698
|
+
if (idx >= 0) {
|
|
699
|
+
store.licenses[idx] = entry;
|
|
700
|
+
} else {
|
|
701
|
+
store.licenses.push(entry);
|
|
702
|
+
}
|
|
703
|
+
writeLicenseStore(store);
|
|
704
|
+
}
|
|
705
|
+
function removeLicenseEntry(licenseKey) {
|
|
706
|
+
const store = readLicenseStore();
|
|
707
|
+
store.licenses = store.licenses.filter((l) => l.licenseKey !== licenseKey);
|
|
708
|
+
writeLicenseStore(store);
|
|
709
|
+
}
|
|
710
|
+
function getActiveLicenses() {
|
|
711
|
+
return readLicenseStore().licenses.filter((l) => l.status === "active");
|
|
712
|
+
}
|
|
713
|
+
function getLicenseForFeature(feature) {
|
|
714
|
+
const active = getActiveLicenses();
|
|
715
|
+
return active.find((l) => {
|
|
716
|
+
const features = VARIANT_FEATURE_MAP[l.variantId];
|
|
717
|
+
return features?.includes(feature);
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
function getAllLicensesNeedingRevalidation() {
|
|
721
|
+
return getActiveLicenses().filter((l) => needsRevalidation(l));
|
|
722
|
+
}
|
|
723
|
+
var REVALIDATION_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
|
|
724
|
+
function needsRevalidation(entry) {
|
|
725
|
+
const lastValidated = new Date(entry.lastValidatedAt).getTime();
|
|
726
|
+
return Date.now() - lastValidated > REVALIDATION_THRESHOLD_MS;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// ../../packages/shared/src/storage.ts
|
|
730
|
+
import fs3 from "fs";
|
|
731
|
+
import path5 from "path";
|
|
732
|
+
import { randomUUID as randomUUID2, createHash } from "crypto";
|
|
733
|
+
|
|
734
|
+
// ../../packages/shared/src/registry.ts
|
|
735
|
+
import fs2 from "fs";
|
|
736
|
+
import os2 from "os";
|
|
737
|
+
import path4 from "path";
|
|
738
|
+
var KEEPGOING_DIR = path4.join(os2.homedir(), ".keepgoing");
|
|
739
|
+
var KNOWN_PROJECTS_FILE = path4.join(KEEPGOING_DIR, "known-projects.json");
|
|
740
|
+
var TRAY_CONFIG_FILE = path4.join(KEEPGOING_DIR, "tray-config.json");
|
|
599
741
|
var STALE_PROJECT_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
742
|
+
function readTrayConfigProjects() {
|
|
743
|
+
try {
|
|
744
|
+
if (fs2.existsSync(TRAY_CONFIG_FILE)) {
|
|
745
|
+
const raw = JSON.parse(fs2.readFileSync(TRAY_CONFIG_FILE, "utf-8"));
|
|
746
|
+
if (raw && Array.isArray(raw.projects)) {
|
|
747
|
+
return raw.projects.filter((p) => typeof p === "string");
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
} catch {
|
|
751
|
+
}
|
|
752
|
+
return [];
|
|
753
|
+
}
|
|
600
754
|
function readKnownProjects() {
|
|
601
755
|
try {
|
|
602
|
-
if (
|
|
603
|
-
const raw = JSON.parse(
|
|
756
|
+
if (fs2.existsSync(KNOWN_PROJECTS_FILE)) {
|
|
757
|
+
const raw = JSON.parse(fs2.readFileSync(KNOWN_PROJECTS_FILE, "utf-8"));
|
|
604
758
|
if (raw && Array.isArray(raw.projects)) {
|
|
605
759
|
return raw;
|
|
606
760
|
}
|
|
@@ -610,18 +764,18 @@ function readKnownProjects() {
|
|
|
610
764
|
return { version: 1, projects: [] };
|
|
611
765
|
}
|
|
612
766
|
function writeKnownProjects(data) {
|
|
613
|
-
if (!
|
|
614
|
-
|
|
767
|
+
if (!fs2.existsSync(KEEPGOING_DIR)) {
|
|
768
|
+
fs2.mkdirSync(KEEPGOING_DIR, { recursive: true });
|
|
615
769
|
}
|
|
616
770
|
const tmpFile = KNOWN_PROJECTS_FILE + ".tmp";
|
|
617
|
-
|
|
618
|
-
|
|
771
|
+
fs2.writeFileSync(tmpFile, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
772
|
+
fs2.renameSync(tmpFile, KNOWN_PROJECTS_FILE);
|
|
619
773
|
}
|
|
620
774
|
function registerProject(projectPath, projectName) {
|
|
621
775
|
try {
|
|
622
776
|
const data = readKnownProjects();
|
|
623
777
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
624
|
-
const name = projectName ||
|
|
778
|
+
const name = projectName || path4.basename(projectPath);
|
|
625
779
|
const existingIdx = data.projects.findIndex((p) => p.path === projectPath);
|
|
626
780
|
if (existingIdx >= 0) {
|
|
627
781
|
data.projects[existingIdx].lastSeen = now;
|
|
@@ -660,23 +814,23 @@ var KeepGoingWriter = class {
|
|
|
660
814
|
currentTasksFilePath;
|
|
661
815
|
constructor(workspacePath) {
|
|
662
816
|
const mainRoot = resolveStorageRoot(workspacePath);
|
|
663
|
-
this.storagePath =
|
|
664
|
-
this.sessionsFilePath =
|
|
665
|
-
this.stateFilePath =
|
|
666
|
-
this.metaFilePath =
|
|
667
|
-
this.currentTasksFilePath =
|
|
817
|
+
this.storagePath = path5.join(mainRoot, STORAGE_DIR);
|
|
818
|
+
this.sessionsFilePath = path5.join(this.storagePath, SESSIONS_FILE);
|
|
819
|
+
this.stateFilePath = path5.join(this.storagePath, STATE_FILE);
|
|
820
|
+
this.metaFilePath = path5.join(this.storagePath, META_FILE);
|
|
821
|
+
this.currentTasksFilePath = path5.join(this.storagePath, CURRENT_TASKS_FILE);
|
|
668
822
|
}
|
|
669
823
|
ensureDir() {
|
|
670
|
-
if (!
|
|
671
|
-
|
|
824
|
+
if (!fs3.existsSync(this.storagePath)) {
|
|
825
|
+
fs3.mkdirSync(this.storagePath, { recursive: true });
|
|
672
826
|
}
|
|
673
827
|
}
|
|
674
828
|
saveCheckpoint(checkpoint, projectName) {
|
|
675
829
|
this.ensureDir();
|
|
676
830
|
let sessionsData;
|
|
677
831
|
try {
|
|
678
|
-
if (
|
|
679
|
-
const raw = JSON.parse(
|
|
832
|
+
if (fs3.existsSync(this.sessionsFilePath)) {
|
|
833
|
+
const raw = JSON.parse(fs3.readFileSync(this.sessionsFilePath, "utf-8"));
|
|
680
834
|
if (Array.isArray(raw)) {
|
|
681
835
|
sessionsData = { version: 1, project: projectName, sessions: raw };
|
|
682
836
|
} else {
|
|
@@ -694,13 +848,13 @@ var KeepGoingWriter = class {
|
|
|
694
848
|
if (sessionsData.sessions.length > MAX_SESSIONS) {
|
|
695
849
|
sessionsData.sessions = sessionsData.sessions.slice(-MAX_SESSIONS);
|
|
696
850
|
}
|
|
697
|
-
|
|
851
|
+
fs3.writeFileSync(this.sessionsFilePath, JSON.stringify(sessionsData, null, 2), "utf-8");
|
|
698
852
|
const state = {
|
|
699
853
|
lastSessionId: checkpoint.id,
|
|
700
854
|
lastKnownBranch: checkpoint.gitBranch,
|
|
701
855
|
lastActivityAt: checkpoint.timestamp
|
|
702
856
|
};
|
|
703
|
-
|
|
857
|
+
fs3.writeFileSync(this.stateFilePath, JSON.stringify(state, null, 2), "utf-8");
|
|
704
858
|
this.updateMeta(checkpoint.timestamp);
|
|
705
859
|
const mainRoot = resolveStorageRoot(this.storagePath);
|
|
706
860
|
registerProject(mainRoot, projectName);
|
|
@@ -708,8 +862,8 @@ var KeepGoingWriter = class {
|
|
|
708
862
|
updateMeta(timestamp) {
|
|
709
863
|
let meta;
|
|
710
864
|
try {
|
|
711
|
-
if (
|
|
712
|
-
meta = JSON.parse(
|
|
865
|
+
if (fs3.existsSync(this.metaFilePath)) {
|
|
866
|
+
meta = JSON.parse(fs3.readFileSync(this.metaFilePath, "utf-8"));
|
|
713
867
|
meta.lastUpdated = timestamp;
|
|
714
868
|
} else {
|
|
715
869
|
meta = {
|
|
@@ -725,7 +879,28 @@ var KeepGoingWriter = class {
|
|
|
725
879
|
lastUpdated: timestamp
|
|
726
880
|
};
|
|
727
881
|
}
|
|
728
|
-
|
|
882
|
+
fs3.writeFileSync(this.metaFilePath, JSON.stringify(meta, null, 2), "utf-8");
|
|
883
|
+
}
|
|
884
|
+
// ---------------------------------------------------------------------------
|
|
885
|
+
// Activity signals API
|
|
886
|
+
// ---------------------------------------------------------------------------
|
|
887
|
+
/**
|
|
888
|
+
* Writes activity signals to state.json for momentum computation.
|
|
889
|
+
* Performs a shallow merge: provided fields overwrite existing ones,
|
|
890
|
+
* fields not provided are preserved.
|
|
891
|
+
*/
|
|
892
|
+
writeActivitySignal(signal) {
|
|
893
|
+
this.ensureDir();
|
|
894
|
+
let state = {};
|
|
895
|
+
try {
|
|
896
|
+
if (fs3.existsSync(this.stateFilePath)) {
|
|
897
|
+
state = JSON.parse(fs3.readFileSync(this.stateFilePath, "utf-8"));
|
|
898
|
+
}
|
|
899
|
+
} catch {
|
|
900
|
+
state = {};
|
|
901
|
+
}
|
|
902
|
+
state.activitySignals = { ...state.activitySignals, ...signal };
|
|
903
|
+
fs3.writeFileSync(this.stateFilePath, JSON.stringify(state, null, 2), "utf-8");
|
|
729
904
|
}
|
|
730
905
|
// ---------------------------------------------------------------------------
|
|
731
906
|
// Multi-session API
|
|
@@ -733,8 +908,8 @@ var KeepGoingWriter = class {
|
|
|
733
908
|
/** Read all current tasks from current-tasks.json. Auto-prunes stale sessions. */
|
|
734
909
|
readCurrentTasks() {
|
|
735
910
|
try {
|
|
736
|
-
if (
|
|
737
|
-
const raw = JSON.parse(
|
|
911
|
+
if (fs3.existsSync(this.currentTasksFilePath)) {
|
|
912
|
+
const raw = JSON.parse(fs3.readFileSync(this.currentTasksFilePath, "utf-8"));
|
|
738
913
|
const tasks = Array.isArray(raw) ? raw : raw.tasks ?? [];
|
|
739
914
|
return this.pruneStale(tasks);
|
|
740
915
|
}
|
|
@@ -755,6 +930,8 @@ var KeepGoingWriter = class {
|
|
|
755
930
|
/** Core upsert logic: merges the update into current-tasks.json and returns the pruned task list. */
|
|
756
931
|
upsertSessionCore(update) {
|
|
757
932
|
this.ensureDir();
|
|
933
|
+
if (update.taskSummary) update.taskSummary = stripAgentTags(update.taskSummary);
|
|
934
|
+
if (update.sessionLabel) update.sessionLabel = stripAgentTags(update.sessionLabel);
|
|
758
935
|
const sessionId = update.sessionId || generateSessionId(update);
|
|
759
936
|
const tasks = this.readAllTasksRaw();
|
|
760
937
|
const existingIdx = tasks.findIndex((t) => t.sessionId === sessionId);
|
|
@@ -789,8 +966,8 @@ var KeepGoingWriter = class {
|
|
|
789
966
|
// ---------------------------------------------------------------------------
|
|
790
967
|
readAllTasksRaw() {
|
|
791
968
|
try {
|
|
792
|
-
if (
|
|
793
|
-
const raw = JSON.parse(
|
|
969
|
+
if (fs3.existsSync(this.currentTasksFilePath)) {
|
|
970
|
+
const raw = JSON.parse(fs3.readFileSync(this.currentTasksFilePath, "utf-8"));
|
|
794
971
|
return Array.isArray(raw) ? [...raw] : [...raw.tasks ?? []];
|
|
795
972
|
}
|
|
796
973
|
} catch {
|
|
@@ -802,7 +979,7 @@ var KeepGoingWriter = class {
|
|
|
802
979
|
}
|
|
803
980
|
writeTasksFile(tasks) {
|
|
804
981
|
const data = { version: 1, tasks };
|
|
805
|
-
|
|
982
|
+
fs3.writeFileSync(this.currentTasksFilePath, JSON.stringify(data, null, 2), "utf-8");
|
|
806
983
|
}
|
|
807
984
|
};
|
|
808
985
|
function generateSessionId(context) {
|
|
@@ -818,376 +995,79 @@ function generateSessionId(context) {
|
|
|
818
995
|
return `ses_${hash}`;
|
|
819
996
|
}
|
|
820
997
|
|
|
821
|
-
// ../../packages/shared/src/
|
|
822
|
-
var
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
998
|
+
// ../../packages/shared/src/reader.ts
|
|
999
|
+
var STORAGE_DIR2 = ".keepgoing";
|
|
1000
|
+
var META_FILE2 = "meta.json";
|
|
1001
|
+
var SESSIONS_FILE2 = "sessions.json";
|
|
1002
|
+
var DECISIONS_FILE = "decisions.json";
|
|
1003
|
+
var STATE_FILE2 = "state.json";
|
|
1004
|
+
var CURRENT_TASKS_FILE2 = "current-tasks.json";
|
|
1005
|
+
var KeepGoingReader = class {
|
|
1006
|
+
workspacePath;
|
|
1007
|
+
storagePath;
|
|
1008
|
+
metaFilePath;
|
|
1009
|
+
sessionsFilePath;
|
|
1010
|
+
decisionsFilePath;
|
|
1011
|
+
stateFilePath;
|
|
1012
|
+
currentTasksFilePath;
|
|
1013
|
+
_isWorktree;
|
|
1014
|
+
_cachedBranch = null;
|
|
1015
|
+
// null = not yet resolved
|
|
1016
|
+
constructor(workspacePath) {
|
|
1017
|
+
this.workspacePath = workspacePath;
|
|
1018
|
+
const mainRoot = resolveStorageRoot(workspacePath);
|
|
1019
|
+
this._isWorktree = mainRoot !== workspacePath;
|
|
1020
|
+
this.storagePath = path6.join(mainRoot, STORAGE_DIR2);
|
|
1021
|
+
this.metaFilePath = path6.join(this.storagePath, META_FILE2);
|
|
1022
|
+
this.sessionsFilePath = path6.join(this.storagePath, SESSIONS_FILE2);
|
|
1023
|
+
this.decisionsFilePath = path6.join(this.storagePath, DECISIONS_FILE);
|
|
1024
|
+
this.stateFilePath = path6.join(this.storagePath, STATE_FILE2);
|
|
1025
|
+
this.currentTasksFilePath = path6.join(this.storagePath, CURRENT_TASKS_FILE2);
|
|
1026
|
+
}
|
|
1027
|
+
/** Check if .keepgoing/ directory exists. */
|
|
1028
|
+
exists() {
|
|
1029
|
+
return fs4.existsSync(this.storagePath);
|
|
1030
|
+
}
|
|
1031
|
+
/** Read state.json, returns undefined if missing or corrupt. */
|
|
1032
|
+
getState() {
|
|
1033
|
+
return this.readJsonFile(this.stateFilePath);
|
|
1034
|
+
}
|
|
1035
|
+
/** Read meta.json, returns undefined if missing or corrupt. */
|
|
1036
|
+
getMeta() {
|
|
1037
|
+
return this.readJsonFile(this.metaFilePath);
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Read sessions from sessions.json.
|
|
1041
|
+
* Handles both formats:
|
|
1042
|
+
* - Flat array: SessionCheckpoint[] (from ProjectStorage)
|
|
1043
|
+
* - Wrapper object: ProjectSessions (from SessionStorage)
|
|
1044
|
+
*/
|
|
1045
|
+
getSessions() {
|
|
1046
|
+
return this.parseSessions().sessions;
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Get the most recent session checkpoint.
|
|
1050
|
+
* Uses state.lastSessionId if available, falls back to last in array.
|
|
1051
|
+
*/
|
|
1052
|
+
getLastSession() {
|
|
1053
|
+
const { sessions, wrapperLastSessionId } = this.parseSessions();
|
|
1054
|
+
if (sessions.length === 0) {
|
|
1055
|
+
return void 0;
|
|
1056
|
+
}
|
|
1057
|
+
const state = this.getState();
|
|
1058
|
+
if (state?.lastSessionId) {
|
|
1059
|
+
const found = sessions.find((s) => s.id === state.lastSessionId);
|
|
1060
|
+
if (found) {
|
|
1061
|
+
return found;
|
|
855
1062
|
}
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
1063
|
+
}
|
|
1064
|
+
if (wrapperLastSessionId) {
|
|
1065
|
+
const found = sessions.find((s) => s.id === wrapperLastSessionId);
|
|
1066
|
+
if (found) {
|
|
1067
|
+
return found;
|
|
860
1068
|
}
|
|
861
|
-
groups.get("other").push(msg.trim());
|
|
862
1069
|
}
|
|
863
|
-
|
|
864
|
-
return groups;
|
|
865
|
-
}
|
|
866
|
-
function inferWorkAreas(files) {
|
|
867
|
-
const areas = /* @__PURE__ */ new Map();
|
|
868
|
-
for (const file of files) {
|
|
869
|
-
if (NOISE_PATTERNS.some((p) => file.includes(p))) {
|
|
870
|
-
continue;
|
|
871
|
-
}
|
|
872
|
-
const parts = file.split("/").filter(Boolean);
|
|
873
|
-
let area;
|
|
874
|
-
if (parts.length >= 2 && (parts[0] === "apps" || parts[0] === "packages")) {
|
|
875
|
-
area = parts[1];
|
|
876
|
-
if (parts[0] === "packages" && parts.length >= 4 && parts[2] === "src") {
|
|
877
|
-
const subFile = parts[3].replace(/\.\w+$/, "");
|
|
878
|
-
area = `${parts[1]} ${subFile}`;
|
|
879
|
-
}
|
|
880
|
-
} else if (parts.length >= 2) {
|
|
881
|
-
area = parts[0];
|
|
882
|
-
} else {
|
|
883
|
-
area = "root";
|
|
884
|
-
}
|
|
885
|
-
areas.set(area, (areas.get(area) ?? 0) + 1);
|
|
886
|
-
}
|
|
887
|
-
return [...areas.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([name]) => name);
|
|
888
|
-
}
|
|
889
|
-
function buildSessionEvents(opts) {
|
|
890
|
-
const { wsPath, commitHashes, commitMessages, touchedFiles, currentBranch, sessionStartTime, lastActivityTime } = opts;
|
|
891
|
-
const commits = commitHashes.map((hash, i) => ({
|
|
892
|
-
hash,
|
|
893
|
-
message: commitMessages[i] ?? "",
|
|
894
|
-
filesChanged: getFilesChangedInCommit(wsPath, hash),
|
|
895
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
896
|
-
}));
|
|
897
|
-
const committedFiles = new Set(commits.flatMap((c) => c.filesChanged));
|
|
898
|
-
return {
|
|
899
|
-
commits,
|
|
900
|
-
branchSwitches: [],
|
|
901
|
-
touchedFiles,
|
|
902
|
-
currentBranch,
|
|
903
|
-
sessionStartTime,
|
|
904
|
-
lastActivityTime,
|
|
905
|
-
// Normalize rename arrows ("old -> new") from git status --porcelain
|
|
906
|
-
// so they match the plain filenames from git diff-tree --name-only.
|
|
907
|
-
hasUncommittedChanges: touchedFiles.some((f) => {
|
|
908
|
-
const normalized = f.includes(" -> ") ? f.split(" -> ").pop() : f;
|
|
909
|
-
return !committedFiles.has(normalized);
|
|
910
|
-
})
|
|
911
|
-
};
|
|
912
|
-
}
|
|
913
|
-
function buildSmartSummary(events) {
|
|
914
|
-
const { commits, branchSwitches, touchedFiles, hasUncommittedChanges } = events;
|
|
915
|
-
if (commits.length === 0 && touchedFiles.length === 0 && branchSwitches.length === 0) {
|
|
916
|
-
return void 0;
|
|
917
|
-
}
|
|
918
|
-
const parts = [];
|
|
919
|
-
if (commits.length > 0) {
|
|
920
|
-
const messages = commits.map((c) => c.message);
|
|
921
|
-
const groups = categorizeCommits(messages);
|
|
922
|
-
const phrases = [];
|
|
923
|
-
for (const [prefix, bodies] of groups) {
|
|
924
|
-
const verb = PREFIX_VERBS[prefix] ?? (prefix === "other" ? "" : `${capitalize(prefix)}:`);
|
|
925
|
-
const items = bodies.slice(0, 2).join(" and ");
|
|
926
|
-
const overflow = bodies.length > 2 ? ` (+${bodies.length - 2} more)` : "";
|
|
927
|
-
if (verb) {
|
|
928
|
-
phrases.push(`${verb} ${items}${overflow}`);
|
|
929
|
-
} else {
|
|
930
|
-
phrases.push(`${items}${overflow}`);
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
parts.push(phrases.join(", "));
|
|
934
|
-
} else if (touchedFiles.length > 0) {
|
|
935
|
-
const areas = inferWorkAreas(touchedFiles);
|
|
936
|
-
const areaStr = areas.length > 0 ? areas.join(" and ") : `${touchedFiles.length} files`;
|
|
937
|
-
const suffix = hasUncommittedChanges ? " (uncommitted)" : "";
|
|
938
|
-
parts.push(`Worked on ${areaStr}${suffix}`);
|
|
939
|
-
}
|
|
940
|
-
if (branchSwitches.length > 0) {
|
|
941
|
-
const last = branchSwitches[branchSwitches.length - 1];
|
|
942
|
-
if (branchSwitches.length === 1) {
|
|
943
|
-
parts.push(`switched to ${last.toBranch}`);
|
|
944
|
-
} else {
|
|
945
|
-
parts.push(`switched branches ${branchSwitches.length} times, ended on ${last.toBranch}`);
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
const result = parts.join("; ");
|
|
949
|
-
return result || void 0;
|
|
950
|
-
}
|
|
951
|
-
function buildSmartNextStep(events) {
|
|
952
|
-
const { commits, touchedFiles, currentBranch, hasUncommittedChanges } = events;
|
|
953
|
-
if (hasUncommittedChanges && touchedFiles.length > 0) {
|
|
954
|
-
const areas = inferWorkAreas(touchedFiles);
|
|
955
|
-
const areaStr = areas.length > 0 ? areas.join(" and ") : "working tree";
|
|
956
|
-
return `Review and commit changes in ${areaStr}`;
|
|
957
|
-
}
|
|
958
|
-
if (commits.length > 0) {
|
|
959
|
-
const lastMsg = commits[commits.length - 1].message;
|
|
960
|
-
const wipMatch = lastMsg.match(/^(?:wip|work in progress|start(?:ed)?|begin|draft)[:\s]+(.+)/i);
|
|
961
|
-
if (wipMatch) {
|
|
962
|
-
return `Continue ${wipMatch[1].trim()}`;
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
if (currentBranch && !["main", "master", "develop", "HEAD"].includes(currentBranch)) {
|
|
966
|
-
const branchName = currentBranch.replace(/^(feat|feature|fix|bugfix|hotfix|chore|refactor)[/-]/i, "").replace(/[-_]/g, " ").trim();
|
|
967
|
-
if (branchName) {
|
|
968
|
-
return `Continue ${branchName}`;
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
if (touchedFiles.length > 0) {
|
|
972
|
-
const areas = inferWorkAreas(touchedFiles);
|
|
973
|
-
if (areas.length > 0) {
|
|
974
|
-
return `Review recent changes in ${areas.join(" and ")}`;
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
return "";
|
|
978
|
-
}
|
|
979
|
-
function capitalize(s) {
|
|
980
|
-
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
// ../../packages/shared/src/decisionStorage.ts
|
|
984
|
-
import fs4 from "fs";
|
|
985
|
-
import path6 from "path";
|
|
986
|
-
|
|
987
|
-
// ../../packages/shared/src/license.ts
|
|
988
|
-
import crypto from "crypto";
|
|
989
|
-
import fs3 from "fs";
|
|
990
|
-
import os2 from "os";
|
|
991
|
-
import path5 from "path";
|
|
992
|
-
var LICENSE_FILE = "license.json";
|
|
993
|
-
var DEVICE_ID_FILE = "device-id";
|
|
994
|
-
function getGlobalLicenseDir() {
|
|
995
|
-
return path5.join(os2.homedir(), ".keepgoing");
|
|
996
|
-
}
|
|
997
|
-
function getGlobalLicensePath() {
|
|
998
|
-
return path5.join(getGlobalLicenseDir(), LICENSE_FILE);
|
|
999
|
-
}
|
|
1000
|
-
function getDeviceId() {
|
|
1001
|
-
const dir = getGlobalLicenseDir();
|
|
1002
|
-
const filePath = path5.join(dir, DEVICE_ID_FILE);
|
|
1003
|
-
try {
|
|
1004
|
-
const existing = fs3.readFileSync(filePath, "utf-8").trim();
|
|
1005
|
-
if (existing) return existing;
|
|
1006
|
-
} catch {
|
|
1007
|
-
}
|
|
1008
|
-
const id = crypto.randomUUID();
|
|
1009
|
-
if (!fs3.existsSync(dir)) {
|
|
1010
|
-
fs3.mkdirSync(dir, { recursive: true });
|
|
1011
|
-
}
|
|
1012
|
-
fs3.writeFileSync(filePath, id, "utf-8");
|
|
1013
|
-
return id;
|
|
1014
|
-
}
|
|
1015
|
-
var DECISION_DETECTION_VARIANT_ID = 1361527;
|
|
1016
|
-
var SESSION_AWARENESS_VARIANT_ID = 1366510;
|
|
1017
|
-
var TEST_DECISION_DETECTION_VARIANT_ID = 1345647;
|
|
1018
|
-
var TEST_SESSION_AWARENESS_VARIANT_ID = 1365992;
|
|
1019
|
-
var VARIANT_FEATURE_MAP = {
|
|
1020
|
-
[DECISION_DETECTION_VARIANT_ID]: ["decisions"],
|
|
1021
|
-
[SESSION_AWARENESS_VARIANT_ID]: ["session-awareness"],
|
|
1022
|
-
[TEST_DECISION_DETECTION_VARIANT_ID]: ["decisions"],
|
|
1023
|
-
[TEST_SESSION_AWARENESS_VARIANT_ID]: ["session-awareness"]
|
|
1024
|
-
// Future bundle: [BUNDLE_VARIANT_ID]: ['decisions', 'session-awareness'],
|
|
1025
|
-
};
|
|
1026
|
-
var KNOWN_VARIANT_IDS = new Set(Object.keys(VARIANT_FEATURE_MAP).map(Number));
|
|
1027
|
-
function getVariantLabel(variantId) {
|
|
1028
|
-
const features = VARIANT_FEATURE_MAP[variantId];
|
|
1029
|
-
if (!features) return "Unknown Add-on";
|
|
1030
|
-
if (features.includes("decisions") && features.includes("session-awareness")) return "Pro Bundle";
|
|
1031
|
-
if (features.includes("decisions")) return "Decision Detection";
|
|
1032
|
-
if (features.includes("session-awareness")) return "Session Awareness";
|
|
1033
|
-
return "Pro Add-on";
|
|
1034
|
-
}
|
|
1035
|
-
var _cachedStore;
|
|
1036
|
-
var _cacheTimestamp = 0;
|
|
1037
|
-
var LICENSE_CACHE_TTL_MS = 2e3;
|
|
1038
|
-
function readLicenseStore() {
|
|
1039
|
-
const now = Date.now();
|
|
1040
|
-
if (_cachedStore && now - _cacheTimestamp < LICENSE_CACHE_TTL_MS) {
|
|
1041
|
-
return _cachedStore;
|
|
1042
|
-
}
|
|
1043
|
-
const licensePath = getGlobalLicensePath();
|
|
1044
|
-
let store;
|
|
1045
|
-
try {
|
|
1046
|
-
if (!fs3.existsSync(licensePath)) {
|
|
1047
|
-
store = { version: 2, licenses: [] };
|
|
1048
|
-
} else {
|
|
1049
|
-
const raw = fs3.readFileSync(licensePath, "utf-8");
|
|
1050
|
-
const data = JSON.parse(raw);
|
|
1051
|
-
if (data?.version === 2 && Array.isArray(data.licenses)) {
|
|
1052
|
-
store = data;
|
|
1053
|
-
} else {
|
|
1054
|
-
store = { version: 2, licenses: [] };
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
} catch {
|
|
1058
|
-
store = { version: 2, licenses: [] };
|
|
1059
|
-
}
|
|
1060
|
-
_cachedStore = store;
|
|
1061
|
-
_cacheTimestamp = now;
|
|
1062
|
-
return store;
|
|
1063
|
-
}
|
|
1064
|
-
function writeLicenseStore(store) {
|
|
1065
|
-
const dirPath = getGlobalLicenseDir();
|
|
1066
|
-
if (!fs3.existsSync(dirPath)) {
|
|
1067
|
-
fs3.mkdirSync(dirPath, { recursive: true });
|
|
1068
|
-
}
|
|
1069
|
-
const licensePath = path5.join(dirPath, LICENSE_FILE);
|
|
1070
|
-
fs3.writeFileSync(licensePath, JSON.stringify(store, null, 2), "utf-8");
|
|
1071
|
-
_cachedStore = store;
|
|
1072
|
-
_cacheTimestamp = Date.now();
|
|
1073
|
-
}
|
|
1074
|
-
function addLicenseEntry(entry) {
|
|
1075
|
-
const store = readLicenseStore();
|
|
1076
|
-
const idx = store.licenses.findIndex((l) => l.licenseKey === entry.licenseKey);
|
|
1077
|
-
if (idx >= 0) {
|
|
1078
|
-
store.licenses[idx] = entry;
|
|
1079
|
-
} else {
|
|
1080
|
-
store.licenses.push(entry);
|
|
1081
|
-
}
|
|
1082
|
-
writeLicenseStore(store);
|
|
1083
|
-
}
|
|
1084
|
-
function removeLicenseEntry(licenseKey) {
|
|
1085
|
-
const store = readLicenseStore();
|
|
1086
|
-
store.licenses = store.licenses.filter((l) => l.licenseKey !== licenseKey);
|
|
1087
|
-
writeLicenseStore(store);
|
|
1088
|
-
}
|
|
1089
|
-
function getActiveLicenses() {
|
|
1090
|
-
return readLicenseStore().licenses.filter((l) => l.status === "active");
|
|
1091
|
-
}
|
|
1092
|
-
function getLicenseForFeature(feature) {
|
|
1093
|
-
const active = getActiveLicenses();
|
|
1094
|
-
return active.find((l) => {
|
|
1095
|
-
const features = VARIANT_FEATURE_MAP[l.variantId];
|
|
1096
|
-
return features?.includes(feature);
|
|
1097
|
-
});
|
|
1098
|
-
}
|
|
1099
|
-
function getAllLicensesNeedingRevalidation() {
|
|
1100
|
-
return getActiveLicenses().filter((l) => needsRevalidation(l));
|
|
1101
|
-
}
|
|
1102
|
-
var REVALIDATION_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
|
|
1103
|
-
function needsRevalidation(entry) {
|
|
1104
|
-
const lastValidated = new Date(entry.lastValidatedAt).getTime();
|
|
1105
|
-
return Date.now() - lastValidated > REVALIDATION_THRESHOLD_MS;
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
// ../../packages/shared/src/featureGate.ts
|
|
1109
|
-
var DefaultFeatureGate = class {
|
|
1110
|
-
isEnabled(_feature) {
|
|
1111
|
-
return true;
|
|
1112
|
-
}
|
|
1113
|
-
};
|
|
1114
|
-
var currentGate = new DefaultFeatureGate();
|
|
1115
|
-
|
|
1116
|
-
// ../../packages/shared/src/reader.ts
|
|
1117
|
-
import fs5 from "fs";
|
|
1118
|
-
import path7 from "path";
|
|
1119
|
-
var STORAGE_DIR2 = ".keepgoing";
|
|
1120
|
-
var META_FILE2 = "meta.json";
|
|
1121
|
-
var SESSIONS_FILE2 = "sessions.json";
|
|
1122
|
-
var DECISIONS_FILE = "decisions.json";
|
|
1123
|
-
var STATE_FILE2 = "state.json";
|
|
1124
|
-
var CURRENT_TASKS_FILE2 = "current-tasks.json";
|
|
1125
|
-
var KeepGoingReader = class {
|
|
1126
|
-
workspacePath;
|
|
1127
|
-
storagePath;
|
|
1128
|
-
metaFilePath;
|
|
1129
|
-
sessionsFilePath;
|
|
1130
|
-
decisionsFilePath;
|
|
1131
|
-
stateFilePath;
|
|
1132
|
-
currentTasksFilePath;
|
|
1133
|
-
_isWorktree;
|
|
1134
|
-
_cachedBranch = null;
|
|
1135
|
-
// null = not yet resolved
|
|
1136
|
-
constructor(workspacePath) {
|
|
1137
|
-
this.workspacePath = workspacePath;
|
|
1138
|
-
const mainRoot = resolveStorageRoot(workspacePath);
|
|
1139
|
-
this._isWorktree = mainRoot !== workspacePath;
|
|
1140
|
-
this.storagePath = path7.join(mainRoot, STORAGE_DIR2);
|
|
1141
|
-
this.metaFilePath = path7.join(this.storagePath, META_FILE2);
|
|
1142
|
-
this.sessionsFilePath = path7.join(this.storagePath, SESSIONS_FILE2);
|
|
1143
|
-
this.decisionsFilePath = path7.join(this.storagePath, DECISIONS_FILE);
|
|
1144
|
-
this.stateFilePath = path7.join(this.storagePath, STATE_FILE2);
|
|
1145
|
-
this.currentTasksFilePath = path7.join(this.storagePath, CURRENT_TASKS_FILE2);
|
|
1146
|
-
}
|
|
1147
|
-
/** Check if .keepgoing/ directory exists. */
|
|
1148
|
-
exists() {
|
|
1149
|
-
return fs5.existsSync(this.storagePath);
|
|
1150
|
-
}
|
|
1151
|
-
/** Read state.json, returns undefined if missing or corrupt. */
|
|
1152
|
-
getState() {
|
|
1153
|
-
return this.readJsonFile(this.stateFilePath);
|
|
1154
|
-
}
|
|
1155
|
-
/** Read meta.json, returns undefined if missing or corrupt. */
|
|
1156
|
-
getMeta() {
|
|
1157
|
-
return this.readJsonFile(this.metaFilePath);
|
|
1158
|
-
}
|
|
1159
|
-
/**
|
|
1160
|
-
* Read sessions from sessions.json.
|
|
1161
|
-
* Handles both formats:
|
|
1162
|
-
* - Flat array: SessionCheckpoint[] (from ProjectStorage)
|
|
1163
|
-
* - Wrapper object: ProjectSessions (from SessionStorage)
|
|
1164
|
-
*/
|
|
1165
|
-
getSessions() {
|
|
1166
|
-
return this.parseSessions().sessions;
|
|
1167
|
-
}
|
|
1168
|
-
/**
|
|
1169
|
-
* Get the most recent session checkpoint.
|
|
1170
|
-
* Uses state.lastSessionId if available, falls back to last in array.
|
|
1171
|
-
*/
|
|
1172
|
-
getLastSession() {
|
|
1173
|
-
const { sessions, wrapperLastSessionId } = this.parseSessions();
|
|
1174
|
-
if (sessions.length === 0) {
|
|
1175
|
-
return void 0;
|
|
1176
|
-
}
|
|
1177
|
-
const state = this.getState();
|
|
1178
|
-
if (state?.lastSessionId) {
|
|
1179
|
-
const found = sessions.find((s) => s.id === state.lastSessionId);
|
|
1180
|
-
if (found) {
|
|
1181
|
-
return found;
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
if (wrapperLastSessionId) {
|
|
1185
|
-
const found = sessions.find((s) => s.id === wrapperLastSessionId);
|
|
1186
|
-
if (found) {
|
|
1187
|
-
return found;
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
return sessions[sessions.length - 1];
|
|
1070
|
+
return sessions[sessions.length - 1];
|
|
1191
1071
|
}
|
|
1192
1072
|
/**
|
|
1193
1073
|
* Returns the last N sessions, newest first.
|
|
@@ -1275,130 +1155,516 @@ var KeepGoingReader = class {
|
|
|
1275
1155
|
overlaps.push({ branch, sessions });
|
|
1276
1156
|
}
|
|
1277
1157
|
}
|
|
1278
|
-
return overlaps;
|
|
1279
|
-
}
|
|
1280
|
-
pruneStale(tasks) {
|
|
1281
|
-
return pruneStaleTasks(tasks);
|
|
1282
|
-
}
|
|
1283
|
-
/** Get the last session checkpoint for a specific branch. */
|
|
1284
|
-
getLastSessionForBranch(branch) {
|
|
1285
|
-
const sessions = this.getSessions().filter((s) => s.gitBranch === branch);
|
|
1286
|
-
return sessions.length > 0 ? sessions[sessions.length - 1] : void 0;
|
|
1287
|
-
}
|
|
1288
|
-
/** Returns the last N sessions for a specific branch, newest first. */
|
|
1289
|
-
getRecentSessionsForBranch(branch, count) {
|
|
1290
|
-
const filtered = this.getSessions().filter((s) => s.gitBranch === branch);
|
|
1291
|
-
return filtered.slice(-count).reverse();
|
|
1292
|
-
}
|
|
1293
|
-
/** Returns the last N decisions for a specific branch, newest first. */
|
|
1294
|
-
getRecentDecisionsForBranch(branch, count) {
|
|
1295
|
-
const filtered = this.getDecisions().filter((d) => d.gitBranch === branch);
|
|
1296
|
-
return filtered.slice(-count).reverse();
|
|
1297
|
-
}
|
|
1298
|
-
/** Whether the workspace is inside a git worktree. */
|
|
1299
|
-
get isWorktree() {
|
|
1300
|
-
return this._isWorktree;
|
|
1158
|
+
return overlaps;
|
|
1159
|
+
}
|
|
1160
|
+
pruneStale(tasks) {
|
|
1161
|
+
return pruneStaleTasks(tasks);
|
|
1162
|
+
}
|
|
1163
|
+
/** Get the last session checkpoint for a specific branch. */
|
|
1164
|
+
getLastSessionForBranch(branch) {
|
|
1165
|
+
const sessions = this.getSessions().filter((s) => s.gitBranch === branch);
|
|
1166
|
+
return sessions.length > 0 ? sessions[sessions.length - 1] : void 0;
|
|
1167
|
+
}
|
|
1168
|
+
/** Returns the last N sessions for a specific branch, newest first. */
|
|
1169
|
+
getRecentSessionsForBranch(branch, count) {
|
|
1170
|
+
const filtered = this.getSessions().filter((s) => s.gitBranch === branch);
|
|
1171
|
+
return filtered.slice(-count).reverse();
|
|
1172
|
+
}
|
|
1173
|
+
/** Returns the last N decisions for a specific branch, newest first. */
|
|
1174
|
+
getRecentDecisionsForBranch(branch, count) {
|
|
1175
|
+
const filtered = this.getDecisions().filter((d) => d.gitBranch === branch);
|
|
1176
|
+
return filtered.slice(-count).reverse();
|
|
1177
|
+
}
|
|
1178
|
+
/** Whether the workspace is inside a git worktree. */
|
|
1179
|
+
get isWorktree() {
|
|
1180
|
+
return this._isWorktree;
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* Returns the current git branch for this workspace.
|
|
1184
|
+
* Lazily cached: the branch is resolved once per KeepGoingReader instance.
|
|
1185
|
+
*/
|
|
1186
|
+
getCurrentBranch() {
|
|
1187
|
+
if (this._cachedBranch === null) {
|
|
1188
|
+
this._cachedBranch = getCurrentBranch(this.workspacePath);
|
|
1189
|
+
}
|
|
1190
|
+
return this._cachedBranch;
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Worktree-aware last session lookup.
|
|
1194
|
+
* In a worktree, scopes to the current branch with fallback to global.
|
|
1195
|
+
* Returns the session and whether it fell back to global.
|
|
1196
|
+
*/
|
|
1197
|
+
getScopedLastSession() {
|
|
1198
|
+
const branch = this.getCurrentBranch();
|
|
1199
|
+
if (this._isWorktree && branch) {
|
|
1200
|
+
const scoped = this.getLastSessionForBranch(branch);
|
|
1201
|
+
if (scoped) return { session: scoped, isFallback: false };
|
|
1202
|
+
return { session: this.getLastSession(), isFallback: true };
|
|
1203
|
+
}
|
|
1204
|
+
return { session: this.getLastSession(), isFallback: false };
|
|
1205
|
+
}
|
|
1206
|
+
/** Worktree-aware recent sessions. Scopes to current branch in a worktree. */
|
|
1207
|
+
getScopedRecentSessions(count) {
|
|
1208
|
+
const branch = this.getCurrentBranch();
|
|
1209
|
+
if (this._isWorktree && branch) {
|
|
1210
|
+
return this.getRecentSessionsForBranch(branch, count);
|
|
1211
|
+
}
|
|
1212
|
+
return this.getRecentSessions(count);
|
|
1213
|
+
}
|
|
1214
|
+
/** Worktree-aware recent decisions. Scopes to current branch in a worktree. */
|
|
1215
|
+
getScopedRecentDecisions(count) {
|
|
1216
|
+
const branch = this.getCurrentBranch();
|
|
1217
|
+
if (this._isWorktree && branch) {
|
|
1218
|
+
return this.getRecentDecisionsForBranch(branch, count);
|
|
1219
|
+
}
|
|
1220
|
+
return this.getRecentDecisions(count);
|
|
1221
|
+
}
|
|
1222
|
+
/**
|
|
1223
|
+
* Resolves branch scope from an explicit `branch` parameter.
|
|
1224
|
+
* Used by tools that accept a `branch` argument (e.g. get_session_history, get_decisions).
|
|
1225
|
+
* - `"all"` returns no filter.
|
|
1226
|
+
* - An explicit branch name uses that.
|
|
1227
|
+
* - `undefined` auto-scopes to the current branch in a worktree, or all branches otherwise.
|
|
1228
|
+
*/
|
|
1229
|
+
resolveBranchScope(branch) {
|
|
1230
|
+
if (branch === "all") {
|
|
1231
|
+
return { effectiveBranch: void 0, scopeLabel: "all branches" };
|
|
1232
|
+
}
|
|
1233
|
+
if (branch) {
|
|
1234
|
+
return { effectiveBranch: branch, scopeLabel: `branch \`${branch}\`` };
|
|
1235
|
+
}
|
|
1236
|
+
const currentBranch = this.getCurrentBranch();
|
|
1237
|
+
if (this._isWorktree && currentBranch) {
|
|
1238
|
+
return { effectiveBranch: currentBranch, scopeLabel: `branch \`${currentBranch}\` (worktree)` };
|
|
1239
|
+
}
|
|
1240
|
+
return { effectiveBranch: void 0, scopeLabel: "all branches" };
|
|
1241
|
+
}
|
|
1242
|
+
/**
|
|
1243
|
+
* Parses sessions.json once, returning both the session list
|
|
1244
|
+
* and the optional lastSessionId from a ProjectSessions wrapper.
|
|
1245
|
+
*/
|
|
1246
|
+
parseSessions() {
|
|
1247
|
+
const raw = this.readJsonFile(
|
|
1248
|
+
this.sessionsFilePath
|
|
1249
|
+
);
|
|
1250
|
+
if (!raw) {
|
|
1251
|
+
return { sessions: [] };
|
|
1252
|
+
}
|
|
1253
|
+
if (Array.isArray(raw)) {
|
|
1254
|
+
return { sessions: raw };
|
|
1255
|
+
}
|
|
1256
|
+
return { sessions: raw.sessions ?? [], wrapperLastSessionId: raw.lastSessionId };
|
|
1257
|
+
}
|
|
1258
|
+
parseDecisions() {
|
|
1259
|
+
const raw = this.readJsonFile(this.decisionsFilePath);
|
|
1260
|
+
if (!raw) {
|
|
1261
|
+
return { decisions: [] };
|
|
1262
|
+
}
|
|
1263
|
+
return { decisions: raw.decisions ?? [], lastDecisionId: raw.lastDecisionId };
|
|
1264
|
+
}
|
|
1265
|
+
readJsonFile(filePath) {
|
|
1266
|
+
try {
|
|
1267
|
+
if (!fs4.existsSync(filePath)) {
|
|
1268
|
+
return void 0;
|
|
1269
|
+
}
|
|
1270
|
+
const raw = fs4.readFileSync(filePath, "utf-8");
|
|
1271
|
+
return JSON.parse(raw);
|
|
1272
|
+
} catch {
|
|
1273
|
+
return void 0;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
};
|
|
1277
|
+
|
|
1278
|
+
// ../../packages/shared/src/smartSummary.ts
|
|
1279
|
+
var PREFIX_VERBS = {
|
|
1280
|
+
feat: "Added",
|
|
1281
|
+
fix: "Fixed",
|
|
1282
|
+
refactor: "Refactored",
|
|
1283
|
+
docs: "Updated docs for",
|
|
1284
|
+
test: "Added tests for",
|
|
1285
|
+
chore: "Updated",
|
|
1286
|
+
style: "Styled",
|
|
1287
|
+
perf: "Optimized",
|
|
1288
|
+
ci: "Updated CI for",
|
|
1289
|
+
build: "Updated build for",
|
|
1290
|
+
revert: "Reverted"
|
|
1291
|
+
};
|
|
1292
|
+
var NOISE_PATTERNS = [
|
|
1293
|
+
"node_modules",
|
|
1294
|
+
"package-lock.json",
|
|
1295
|
+
"yarn.lock",
|
|
1296
|
+
"pnpm-lock.yaml",
|
|
1297
|
+
".gitignore",
|
|
1298
|
+
".DS_Store",
|
|
1299
|
+
"dist/",
|
|
1300
|
+
"out/",
|
|
1301
|
+
"build/"
|
|
1302
|
+
];
|
|
1303
|
+
function categorizeCommits(messages) {
|
|
1304
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1305
|
+
for (const msg of messages) {
|
|
1306
|
+
const match = msg.match(/^(\w+)(?:\([^)]*\))?[!]?:\s*(.+)/);
|
|
1307
|
+
if (match) {
|
|
1308
|
+
const prefix = match[1].toLowerCase();
|
|
1309
|
+
const body = match[2].trim();
|
|
1310
|
+
if (!groups.has(prefix)) {
|
|
1311
|
+
groups.set(prefix, []);
|
|
1312
|
+
}
|
|
1313
|
+
groups.get(prefix).push(body);
|
|
1314
|
+
} else {
|
|
1315
|
+
if (!groups.has("other")) {
|
|
1316
|
+
groups.set("other", []);
|
|
1317
|
+
}
|
|
1318
|
+
groups.get("other").push(msg.trim());
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
return groups;
|
|
1322
|
+
}
|
|
1323
|
+
function inferWorkAreas(files) {
|
|
1324
|
+
const areas = /* @__PURE__ */ new Map();
|
|
1325
|
+
for (const file of files) {
|
|
1326
|
+
if (NOISE_PATTERNS.some((p) => file.includes(p))) {
|
|
1327
|
+
continue;
|
|
1328
|
+
}
|
|
1329
|
+
const parts = file.split("/").filter(Boolean);
|
|
1330
|
+
let area;
|
|
1331
|
+
if (parts.length >= 2 && (parts[0] === "apps" || parts[0] === "packages")) {
|
|
1332
|
+
area = parts[1];
|
|
1333
|
+
if (parts[0] === "packages" && parts.length >= 4 && parts[2] === "src") {
|
|
1334
|
+
const subFile = parts[3].replace(/\.\w+$/, "");
|
|
1335
|
+
area = `${parts[1]} ${subFile}`;
|
|
1336
|
+
}
|
|
1337
|
+
} else if (parts.length >= 2) {
|
|
1338
|
+
area = parts[0];
|
|
1339
|
+
} else {
|
|
1340
|
+
area = "root";
|
|
1341
|
+
}
|
|
1342
|
+
areas.set(area, (areas.get(area) ?? 0) + 1);
|
|
1343
|
+
}
|
|
1344
|
+
return [...areas.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([name]) => name);
|
|
1345
|
+
}
|
|
1346
|
+
function buildSessionEvents(opts) {
|
|
1347
|
+
const { wsPath, commitHashes, commitMessages, touchedFiles, currentBranch, sessionStartTime, lastActivityTime } = opts;
|
|
1348
|
+
const commits = commitHashes.map((hash, i) => ({
|
|
1349
|
+
hash,
|
|
1350
|
+
message: commitMessages[i] ?? "",
|
|
1351
|
+
filesChanged: getFilesChangedInCommit(wsPath, hash),
|
|
1352
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1353
|
+
}));
|
|
1354
|
+
const committedFiles = new Set(commits.flatMap((c) => c.filesChanged));
|
|
1355
|
+
return {
|
|
1356
|
+
commits,
|
|
1357
|
+
branchSwitches: [],
|
|
1358
|
+
touchedFiles,
|
|
1359
|
+
currentBranch,
|
|
1360
|
+
sessionStartTime,
|
|
1361
|
+
lastActivityTime,
|
|
1362
|
+
// Normalize rename arrows ("old -> new") from git status --porcelain
|
|
1363
|
+
// so they match the plain filenames from git diff-tree --name-only.
|
|
1364
|
+
hasUncommittedChanges: touchedFiles.some((f) => {
|
|
1365
|
+
const normalized = f.includes(" -> ") ? f.split(" -> ").pop() : f;
|
|
1366
|
+
return !committedFiles.has(normalized);
|
|
1367
|
+
})
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
function buildSmartSummary(events) {
|
|
1371
|
+
const { commits, branchSwitches, touchedFiles, hasUncommittedChanges } = events;
|
|
1372
|
+
if (commits.length === 0 && touchedFiles.length === 0 && branchSwitches.length === 0) {
|
|
1373
|
+
return void 0;
|
|
1374
|
+
}
|
|
1375
|
+
const parts = [];
|
|
1376
|
+
if (commits.length > 0) {
|
|
1377
|
+
const messages = commits.map((c) => c.message);
|
|
1378
|
+
const groups = categorizeCommits(messages);
|
|
1379
|
+
const phrases = [];
|
|
1380
|
+
for (const [prefix, bodies] of groups) {
|
|
1381
|
+
const verb = PREFIX_VERBS[prefix] ?? (prefix === "other" ? "" : `${capitalize(prefix)}:`);
|
|
1382
|
+
const items = bodies.slice(0, 2).join(" and ");
|
|
1383
|
+
const overflow = bodies.length > 2 ? ` (+${bodies.length - 2} more)` : "";
|
|
1384
|
+
if (verb) {
|
|
1385
|
+
phrases.push(`${verb} ${items}${overflow}`);
|
|
1386
|
+
} else {
|
|
1387
|
+
phrases.push(`${items}${overflow}`);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
parts.push(phrases.join(", "));
|
|
1391
|
+
} else if (touchedFiles.length > 0) {
|
|
1392
|
+
const areas = inferWorkAreas(touchedFiles);
|
|
1393
|
+
const areaStr = areas.length > 0 ? areas.join(" and ") : `${touchedFiles.length} files`;
|
|
1394
|
+
const suffix = hasUncommittedChanges ? " (uncommitted)" : "";
|
|
1395
|
+
parts.push(`Worked on ${areaStr}${suffix}`);
|
|
1301
1396
|
}
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
this._cachedBranch = getCurrentBranch(this.workspacePath);
|
|
1397
|
+
if (branchSwitches.length > 0) {
|
|
1398
|
+
const last = branchSwitches[branchSwitches.length - 1];
|
|
1399
|
+
if (branchSwitches.length === 1) {
|
|
1400
|
+
parts.push(`switched to ${last.toBranch}`);
|
|
1401
|
+
} else {
|
|
1402
|
+
parts.push(`switched branches ${branchSwitches.length} times, ended on ${last.toBranch}`);
|
|
1309
1403
|
}
|
|
1310
|
-
return this._cachedBranch;
|
|
1311
1404
|
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
const
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
if (scoped) return { session: scoped, isFallback: false };
|
|
1322
|
-
return { session: this.getLastSession(), isFallback: true };
|
|
1323
|
-
}
|
|
1324
|
-
return { session: this.getLastSession(), isFallback: false };
|
|
1405
|
+
const result = parts.join("; ");
|
|
1406
|
+
return result || void 0;
|
|
1407
|
+
}
|
|
1408
|
+
function buildSmartNextStep(events) {
|
|
1409
|
+
const { commits, touchedFiles, currentBranch, hasUncommittedChanges } = events;
|
|
1410
|
+
if (hasUncommittedChanges && touchedFiles.length > 0) {
|
|
1411
|
+
const areas = inferWorkAreas(touchedFiles);
|
|
1412
|
+
const areaStr = areas.length > 0 ? areas.join(" and ") : "working tree";
|
|
1413
|
+
return `Review and commit changes in ${areaStr}`;
|
|
1325
1414
|
}
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
const
|
|
1329
|
-
if (
|
|
1330
|
-
return
|
|
1415
|
+
if (commits.length > 0) {
|
|
1416
|
+
const lastMsg = commits[commits.length - 1].message;
|
|
1417
|
+
const wipMatch = lastMsg.match(/^(?:wip|work in progress|start(?:ed)?|begin|draft)[:\s]+(.+)/i);
|
|
1418
|
+
if (wipMatch) {
|
|
1419
|
+
return `Continue ${wipMatch[1].trim()}`;
|
|
1331
1420
|
}
|
|
1332
|
-
return this.getRecentSessions(count);
|
|
1333
1421
|
}
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
return this.getRecentDecisionsForBranch(branch, count);
|
|
1422
|
+
if (currentBranch && !["main", "master", "develop", "HEAD"].includes(currentBranch)) {
|
|
1423
|
+
const branchName = currentBranch.replace(/^(feat|feature|fix|bugfix|hotfix|chore|refactor)[/-]/i, "").replace(/[-_]/g, " ").trim();
|
|
1424
|
+
if (branchName) {
|
|
1425
|
+
return `Continue ${branchName}`;
|
|
1339
1426
|
}
|
|
1340
|
-
return this.getRecentDecisions(count);
|
|
1341
1427
|
}
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
* - An explicit branch name uses that.
|
|
1347
|
-
* - `undefined` auto-scopes to the current branch in a worktree, or all branches otherwise.
|
|
1348
|
-
*/
|
|
1349
|
-
resolveBranchScope(branch) {
|
|
1350
|
-
if (branch === "all") {
|
|
1351
|
-
return { effectiveBranch: void 0, scopeLabel: "all branches" };
|
|
1428
|
+
if (touchedFiles.length > 0) {
|
|
1429
|
+
const areas = inferWorkAreas(touchedFiles);
|
|
1430
|
+
if (areas.length > 0) {
|
|
1431
|
+
return `Review recent changes in ${areas.join(" and ")}`;
|
|
1352
1432
|
}
|
|
1353
|
-
|
|
1354
|
-
|
|
1433
|
+
}
|
|
1434
|
+
return "";
|
|
1435
|
+
}
|
|
1436
|
+
function capitalize(s) {
|
|
1437
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
// ../../packages/shared/src/contextSnapshot.ts
|
|
1441
|
+
import fs5 from "fs";
|
|
1442
|
+
import path7 from "path";
|
|
1443
|
+
function formatCompactRelativeTime(date) {
|
|
1444
|
+
const diffMs = Date.now() - date.getTime();
|
|
1445
|
+
if (isNaN(diffMs)) return "?";
|
|
1446
|
+
if (diffMs < 0) return "now";
|
|
1447
|
+
const seconds = Math.floor(diffMs / 1e3);
|
|
1448
|
+
const minutes = Math.floor(seconds / 60);
|
|
1449
|
+
const hours = Math.floor(minutes / 60);
|
|
1450
|
+
const days = Math.floor(hours / 24);
|
|
1451
|
+
const weeks = Math.floor(days / 7);
|
|
1452
|
+
if (seconds < 60) return "now";
|
|
1453
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
1454
|
+
if (hours < 24) return `${hours}h ago`;
|
|
1455
|
+
if (days < 7) return `${days}d ago`;
|
|
1456
|
+
return `${weeks}w ago`;
|
|
1457
|
+
}
|
|
1458
|
+
function computeMomentum(timestamp, signals) {
|
|
1459
|
+
if (signals) {
|
|
1460
|
+
if (signals.lastGitOpAt) {
|
|
1461
|
+
const gitOpDiffMs = Date.now() - new Date(signals.lastGitOpAt).getTime();
|
|
1462
|
+
if (!isNaN(gitOpDiffMs) && gitOpDiffMs >= 0 && gitOpDiffMs < 30 * 60 * 1e3) {
|
|
1463
|
+
return "hot";
|
|
1464
|
+
}
|
|
1355
1465
|
}
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
return { effectiveBranch: currentBranch, scopeLabel: `branch \`${currentBranch}\` (worktree)` };
|
|
1466
|
+
if (signals.recentCommitCount !== void 0 && signals.recentCommitCount >= 2) {
|
|
1467
|
+
return "hot";
|
|
1359
1468
|
}
|
|
1360
|
-
return { effectiveBranch: void 0, scopeLabel: "all branches" };
|
|
1361
1469
|
}
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1470
|
+
const diffMs = Date.now() - new Date(timestamp).getTime();
|
|
1471
|
+
if (isNaN(diffMs) || diffMs < 0) return "hot";
|
|
1472
|
+
const minutes = diffMs / (1e3 * 60);
|
|
1473
|
+
if (minutes < 30) return "hot";
|
|
1474
|
+
if (minutes < 240) return "warm";
|
|
1475
|
+
return "cold";
|
|
1476
|
+
}
|
|
1477
|
+
function inferFocusFromBranch2(branch) {
|
|
1478
|
+
if (!branch || branch === "main" || branch === "master" || branch === "develop" || branch === "HEAD") {
|
|
1479
|
+
return void 0;
|
|
1480
|
+
}
|
|
1481
|
+
const prefixPattern = /^(?:feature|feat|fix|bugfix|hotfix|chore|refactor|docs|test|ci)\//i;
|
|
1482
|
+
const isFix = /^(?:fix|bugfix|hotfix)\//i.test(branch);
|
|
1483
|
+
const stripped = branch.replace(prefixPattern, "");
|
|
1484
|
+
const cleaned = stripped.replace(/[-_/]/g, " ").replace(/^\d+\s*/, "").trim();
|
|
1485
|
+
if (!cleaned) return void 0;
|
|
1486
|
+
return isFix ? `${cleaned} fix` : cleaned;
|
|
1487
|
+
}
|
|
1488
|
+
function cleanCommitMessage(message, maxLen = 60) {
|
|
1489
|
+
if (!message) return "";
|
|
1490
|
+
const match = message.match(/^(?:\w+)(?:\([^)]*\))?[!]?:\s*(.+)/);
|
|
1491
|
+
const body = match ? match[1].trim() : message.trim();
|
|
1492
|
+
if (!body) return "";
|
|
1493
|
+
const capitalized = body.charAt(0).toUpperCase() + body.slice(1);
|
|
1494
|
+
if (capitalized.length <= maxLen) return capitalized;
|
|
1495
|
+
return capitalized.slice(0, maxLen - 3) + "...";
|
|
1496
|
+
}
|
|
1497
|
+
function buildDoing(checkpoint, branch, recentCommitMessages) {
|
|
1498
|
+
if (checkpoint?.summary) return checkpoint.summary;
|
|
1499
|
+
const branchFocus = inferFocusFromBranch2(branch ?? checkpoint?.gitBranch);
|
|
1500
|
+
if (branchFocus) return branchFocus;
|
|
1501
|
+
if (recentCommitMessages && recentCommitMessages.length > 0) {
|
|
1502
|
+
const cleaned = cleanCommitMessage(recentCommitMessages[0]);
|
|
1503
|
+
if (cleaned) return cleaned;
|
|
1504
|
+
}
|
|
1505
|
+
return "Unknown";
|
|
1506
|
+
}
|
|
1507
|
+
function buildWhere(checkpoint, branch) {
|
|
1508
|
+
const effectiveBranch = branch ?? checkpoint?.gitBranch;
|
|
1509
|
+
const parts = [];
|
|
1510
|
+
if (effectiveBranch) {
|
|
1511
|
+
parts.push(effectiveBranch);
|
|
1512
|
+
}
|
|
1513
|
+
if (checkpoint?.touchedFiles && checkpoint.touchedFiles.length > 0) {
|
|
1514
|
+
const fileNames = checkpoint.touchedFiles.slice(0, 2).map((f) => {
|
|
1515
|
+
const segments = f.replace(/\\/g, "/").split("/");
|
|
1516
|
+
return segments[segments.length - 1];
|
|
1517
|
+
});
|
|
1518
|
+
parts.push(fileNames.join(", "));
|
|
1519
|
+
}
|
|
1520
|
+
return parts.join(" \xB7 ") || "unknown";
|
|
1521
|
+
}
|
|
1522
|
+
function detectFocusArea(files) {
|
|
1523
|
+
if (!files || files.length === 0) return void 0;
|
|
1524
|
+
const areas = inferWorkAreas(files);
|
|
1525
|
+
if (areas.length === 0) return void 0;
|
|
1526
|
+
if (areas.length === 1) return areas[0];
|
|
1527
|
+
const topArea = areas[0];
|
|
1528
|
+
const areaCounts = /* @__PURE__ */ new Map();
|
|
1529
|
+
for (const file of files) {
|
|
1530
|
+
const parts = file.split("/").filter(Boolean);
|
|
1531
|
+
let area;
|
|
1532
|
+
if (parts.length <= 1) {
|
|
1533
|
+
area = "root";
|
|
1534
|
+
} else if (parts[0] === "apps" || parts[0] === "packages") {
|
|
1535
|
+
area = parts.length > 1 ? parts[1] : parts[0];
|
|
1536
|
+
} else if (parts[0] === "src") {
|
|
1537
|
+
area = parts.length > 1 ? parts[1] : "src";
|
|
1538
|
+
} else {
|
|
1539
|
+
area = parts[0];
|
|
1375
1540
|
}
|
|
1376
|
-
|
|
1541
|
+
areaCounts.set(area, (areaCounts.get(area) ?? 0) + 1);
|
|
1377
1542
|
}
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
if (
|
|
1381
|
-
|
|
1543
|
+
let topCount = 0;
|
|
1544
|
+
for (const [areaKey, count] of areaCounts) {
|
|
1545
|
+
if (topArea.toLowerCase().includes(areaKey.toLowerCase()) || areaKey.toLowerCase().includes(topArea.toLowerCase().split(" ")[0])) {
|
|
1546
|
+
topCount = Math.max(topCount, count);
|
|
1382
1547
|
}
|
|
1383
|
-
return { decisions: raw.decisions ?? [], lastDecisionId: raw.lastDecisionId };
|
|
1384
1548
|
}
|
|
1385
|
-
|
|
1549
|
+
if (topCount === 0) {
|
|
1550
|
+
topCount = Math.max(...areaCounts.values());
|
|
1551
|
+
}
|
|
1552
|
+
const ratio = topCount / files.length;
|
|
1553
|
+
if (ratio >= 0.6) return topArea;
|
|
1554
|
+
return void 0;
|
|
1555
|
+
}
|
|
1556
|
+
function generateContextSnapshot(projectRoot) {
|
|
1557
|
+
const reader = new KeepGoingReader(projectRoot);
|
|
1558
|
+
if (!reader.exists()) return null;
|
|
1559
|
+
const lastSession = reader.getLastSession();
|
|
1560
|
+
const state = reader.getState();
|
|
1561
|
+
if (!lastSession && !state) return null;
|
|
1562
|
+
const timestamp = state?.lastActivityAt ?? lastSession?.timestamp;
|
|
1563
|
+
if (!timestamp) return null;
|
|
1564
|
+
const branch = state?.lastKnownBranch ?? lastSession?.gitBranch;
|
|
1565
|
+
let activeAgents = 0;
|
|
1566
|
+
try {
|
|
1567
|
+
const activeTasks = reader.getActiveTasks();
|
|
1568
|
+
activeAgents = activeTasks.length;
|
|
1569
|
+
} catch {
|
|
1570
|
+
}
|
|
1571
|
+
const doing = buildDoing(lastSession, branch);
|
|
1572
|
+
const next = lastSession?.nextStep ?? "";
|
|
1573
|
+
const where = buildWhere(lastSession, branch);
|
|
1574
|
+
const when = formatCompactRelativeTime(new Date(timestamp));
|
|
1575
|
+
const momentum = computeMomentum(timestamp, state?.activitySignals);
|
|
1576
|
+
const blocker = lastSession?.blocker;
|
|
1577
|
+
const snapshot = {
|
|
1578
|
+
doing,
|
|
1579
|
+
next,
|
|
1580
|
+
where,
|
|
1581
|
+
when,
|
|
1582
|
+
momentum,
|
|
1583
|
+
lastActivityAt: timestamp
|
|
1584
|
+
};
|
|
1585
|
+
if (blocker) snapshot.blocker = blocker;
|
|
1586
|
+
if (activeAgents > 0) snapshot.activeAgents = activeAgents;
|
|
1587
|
+
const focusArea = detectFocusArea(lastSession?.touchedFiles ?? []);
|
|
1588
|
+
if (focusArea) snapshot.focusArea = focusArea;
|
|
1589
|
+
return snapshot;
|
|
1590
|
+
}
|
|
1591
|
+
function formatCrossProjectLine(entry) {
|
|
1592
|
+
const s = entry.snapshot;
|
|
1593
|
+
const icon = s.momentum === "hot" ? "\u26A1" : s.momentum === "warm" ? "\u{1F536}" : "\u{1F4A4}";
|
|
1594
|
+
const parts = [];
|
|
1595
|
+
parts.push(`${icon} ${entry.name}: ${s.doing}`);
|
|
1596
|
+
if (s.next) {
|
|
1597
|
+
parts.push(`\u2192 ${s.next}`);
|
|
1598
|
+
}
|
|
1599
|
+
let line = parts.join(" ");
|
|
1600
|
+
line += ` (${s.when})`;
|
|
1601
|
+
if (s.blocker) {
|
|
1602
|
+
line += ` \u26D4 ${s.blocker}`;
|
|
1603
|
+
}
|
|
1604
|
+
return line;
|
|
1605
|
+
}
|
|
1606
|
+
var MOMENTUM_RANK = { hot: 0, warm: 1, cold: 2 };
|
|
1607
|
+
function generateCrossProjectSummary() {
|
|
1608
|
+
const registry = readKnownProjects();
|
|
1609
|
+
const trayPaths = readTrayConfigProjects();
|
|
1610
|
+
const seenPaths = new Set(registry.projects.map((p) => p.path));
|
|
1611
|
+
const allEntries = [...registry.projects];
|
|
1612
|
+
for (const tp of trayPaths) {
|
|
1613
|
+
if (!seenPaths.has(tp)) {
|
|
1614
|
+
seenPaths.add(tp);
|
|
1615
|
+
allEntries.push({ path: tp, name: path7.basename(tp) });
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
const projects = [];
|
|
1619
|
+
const seenRoots = /* @__PURE__ */ new Set();
|
|
1620
|
+
for (const entry of allEntries) {
|
|
1621
|
+
if (!fs5.existsSync(entry.path)) continue;
|
|
1622
|
+
const snapshot = generateContextSnapshot(entry.path);
|
|
1623
|
+
if (!snapshot) continue;
|
|
1624
|
+
let resolvedRoot;
|
|
1386
1625
|
try {
|
|
1387
|
-
|
|
1388
|
-
return void 0;
|
|
1389
|
-
}
|
|
1390
|
-
const raw = fs5.readFileSync(filePath, "utf-8");
|
|
1391
|
-
return JSON.parse(raw);
|
|
1626
|
+
resolvedRoot = fs5.realpathSync(entry.path);
|
|
1392
1627
|
} catch {
|
|
1393
|
-
|
|
1394
|
-
}
|
|
1628
|
+
resolvedRoot = entry.path;
|
|
1629
|
+
}
|
|
1630
|
+
if (seenRoots.has(resolvedRoot)) continue;
|
|
1631
|
+
seenRoots.add(resolvedRoot);
|
|
1632
|
+
projects.push({
|
|
1633
|
+
name: entry.name,
|
|
1634
|
+
path: entry.path,
|
|
1635
|
+
snapshot
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
projects.sort((a, b) => {
|
|
1639
|
+
const rankA = MOMENTUM_RANK[a.snapshot.momentum ?? "cold"];
|
|
1640
|
+
const rankB = MOMENTUM_RANK[b.snapshot.momentum ?? "cold"];
|
|
1641
|
+
if (rankA !== rankB) return rankA - rankB;
|
|
1642
|
+
const timeA = a.snapshot.lastActivityAt ? new Date(a.snapshot.lastActivityAt).getTime() : 0;
|
|
1643
|
+
const timeB = b.snapshot.lastActivityAt ? new Date(b.snapshot.lastActivityAt).getTime() : 0;
|
|
1644
|
+
return timeB - timeA;
|
|
1645
|
+
});
|
|
1646
|
+
return {
|
|
1647
|
+
projects,
|
|
1648
|
+
generated: (/* @__PURE__ */ new Date()).toISOString()
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
// ../../packages/shared/src/decisionStorage.ts
|
|
1653
|
+
import fs6 from "fs";
|
|
1654
|
+
import path8 from "path";
|
|
1655
|
+
|
|
1656
|
+
// ../../packages/shared/src/featureGate.ts
|
|
1657
|
+
var DefaultFeatureGate = class {
|
|
1658
|
+
isEnabled(_feature) {
|
|
1659
|
+
return true;
|
|
1395
1660
|
}
|
|
1396
1661
|
};
|
|
1662
|
+
var currentGate = new DefaultFeatureGate();
|
|
1397
1663
|
|
|
1398
1664
|
// ../../packages/shared/src/setup.ts
|
|
1399
|
-
import
|
|
1665
|
+
import fs7 from "fs";
|
|
1400
1666
|
import os3 from "os";
|
|
1401
|
-
import
|
|
1667
|
+
import path9 from "path";
|
|
1402
1668
|
var KEEPGOING_MARKER = "@keepgoingdev/mcp-server";
|
|
1403
1669
|
var SESSION_START_HOOK = {
|
|
1404
1670
|
matcher: "",
|
|
@@ -1436,12 +1702,14 @@ var SESSION_END_HOOK = {
|
|
|
1436
1702
|
}
|
|
1437
1703
|
]
|
|
1438
1704
|
};
|
|
1439
|
-
var KEEPGOING_RULES_VERSION =
|
|
1705
|
+
var KEEPGOING_RULES_VERSION = 2;
|
|
1440
1706
|
var KEEPGOING_RULES_CONTENT = `<!-- @keepgoingdev/mcp-server v${KEEPGOING_RULES_VERSION} -->
|
|
1441
1707
|
## KeepGoing
|
|
1442
1708
|
|
|
1709
|
+
When you see KeepGoing momentum data in your session context (from a SessionStart hook), share a brief welcome with the user that includes: what was last worked on, the suggested next step, and any blockers.
|
|
1710
|
+
|
|
1443
1711
|
After completing a task or meaningful piece of work, call the \`save_checkpoint\` MCP tool with:
|
|
1444
|
-
- \`summary\`: 1-2 sentences. What changed and why
|
|
1712
|
+
- \`summary\`: 1-2 sentences. What changed and why, no file paths, no implementation details (those are captured from git).
|
|
1445
1713
|
- \`nextStep\`: What to do next
|
|
1446
1714
|
- \`blocker\`: Any blocker (if applicable)
|
|
1447
1715
|
`;
|
|
@@ -1451,7 +1719,7 @@ function getRulesFileVersion(content) {
|
|
|
1451
1719
|
}
|
|
1452
1720
|
var STATUSLINE_CMD = "npx -y @keepgoingdev/mcp-server --statusline";
|
|
1453
1721
|
function detectClaudeDir() {
|
|
1454
|
-
return process.env.CLAUDE_CONFIG_DIR ||
|
|
1722
|
+
return process.env.CLAUDE_CONFIG_DIR || path9.join(os3.homedir(), ".claude");
|
|
1455
1723
|
}
|
|
1456
1724
|
function hasKeepGoingHook(hookEntries) {
|
|
1457
1725
|
return hookEntries.some(
|
|
@@ -1463,19 +1731,19 @@ function resolveScopePaths(scope, workspacePath, overrideClaudeDir) {
|
|
|
1463
1731
|
const claudeDir2 = overrideClaudeDir || detectClaudeDir();
|
|
1464
1732
|
return {
|
|
1465
1733
|
claudeDir: claudeDir2,
|
|
1466
|
-
settingsPath:
|
|
1467
|
-
claudeMdPath:
|
|
1468
|
-
rulesPath:
|
|
1734
|
+
settingsPath: path9.join(claudeDir2, "settings.json"),
|
|
1735
|
+
claudeMdPath: path9.join(claudeDir2, "CLAUDE.md"),
|
|
1736
|
+
rulesPath: path9.join(claudeDir2, "rules", "keepgoing.md")
|
|
1469
1737
|
};
|
|
1470
1738
|
}
|
|
1471
|
-
const claudeDir =
|
|
1472
|
-
const dotClaudeMdPath =
|
|
1473
|
-
const rootClaudeMdPath =
|
|
1739
|
+
const claudeDir = path9.join(workspacePath, ".claude");
|
|
1740
|
+
const dotClaudeMdPath = path9.join(workspacePath, ".claude", "CLAUDE.md");
|
|
1741
|
+
const rootClaudeMdPath = path9.join(workspacePath, "CLAUDE.md");
|
|
1474
1742
|
return {
|
|
1475
1743
|
claudeDir,
|
|
1476
|
-
settingsPath:
|
|
1477
|
-
claudeMdPath:
|
|
1478
|
-
rulesPath:
|
|
1744
|
+
settingsPath: path9.join(claudeDir, "settings.json"),
|
|
1745
|
+
claudeMdPath: fs7.existsSync(dotClaudeMdPath) ? dotClaudeMdPath : rootClaudeMdPath,
|
|
1746
|
+
rulesPath: path9.join(workspacePath, ".claude", "rules", "keepgoing.md")
|
|
1479
1747
|
};
|
|
1480
1748
|
}
|
|
1481
1749
|
function writeHooksToSettings(settings) {
|
|
@@ -1515,11 +1783,11 @@ function writeHooksToSettings(settings) {
|
|
|
1515
1783
|
}
|
|
1516
1784
|
function checkHookConflict(scope, workspacePath) {
|
|
1517
1785
|
const otherPaths = resolveScopePaths(scope === "user" ? "project" : "user", workspacePath);
|
|
1518
|
-
if (!
|
|
1786
|
+
if (!fs7.existsSync(otherPaths.settingsPath)) {
|
|
1519
1787
|
return null;
|
|
1520
1788
|
}
|
|
1521
1789
|
try {
|
|
1522
|
-
const otherSettings = JSON.parse(
|
|
1790
|
+
const otherSettings = JSON.parse(fs7.readFileSync(otherPaths.settingsPath, "utf-8"));
|
|
1523
1791
|
const hooks = otherSettings?.hooks;
|
|
1524
1792
|
if (!hooks) return null;
|
|
1525
1793
|
const hasConflict = Array.isArray(hooks.SessionStart) && hasKeepGoingHook(hooks.SessionStart) || Array.isArray(hooks.Stop) && hasKeepGoingHook(hooks.Stop);
|
|
@@ -1548,10 +1816,10 @@ function setupProject(options) {
|
|
|
1548
1816
|
workspacePath,
|
|
1549
1817
|
claudeDirOverride
|
|
1550
1818
|
);
|
|
1551
|
-
const scopeLabel = scope === "user" ?
|
|
1819
|
+
const scopeLabel = scope === "user" ? path9.join("~", path9.relative(os3.homedir(), claudeDir), "settings.json").replace(/\\/g, "/") : ".claude/settings.json";
|
|
1552
1820
|
let settings = {};
|
|
1553
|
-
if (
|
|
1554
|
-
settings = JSON.parse(
|
|
1821
|
+
if (fs7.existsSync(settingsPath)) {
|
|
1822
|
+
settings = JSON.parse(fs7.readFileSync(settingsPath, "utf-8"));
|
|
1555
1823
|
}
|
|
1556
1824
|
let settingsChanged = false;
|
|
1557
1825
|
if (sessionHooks) {
|
|
@@ -1567,7 +1835,7 @@ function setupProject(options) {
|
|
|
1567
1835
|
messages.push(`Warning: ${conflict}`);
|
|
1568
1836
|
}
|
|
1569
1837
|
}
|
|
1570
|
-
|
|
1838
|
+
{
|
|
1571
1839
|
const needsUpdate = settings.statusLine?.command && statusline?.isLegacy?.(settings.statusLine.command);
|
|
1572
1840
|
if (!settings.statusLine || needsUpdate) {
|
|
1573
1841
|
settings.statusLine = {
|
|
@@ -1575,43 +1843,43 @@ function setupProject(options) {
|
|
|
1575
1843
|
command: STATUSLINE_CMD
|
|
1576
1844
|
};
|
|
1577
1845
|
settingsChanged = true;
|
|
1578
|
-
messages.push(needsUpdate ? "Statusline: Migrated to auto-updating npx command" :
|
|
1846
|
+
messages.push(needsUpdate ? "Statusline: Migrated to auto-updating npx command" : `Statusline: Added to ${scopeLabel}`);
|
|
1579
1847
|
} else {
|
|
1580
1848
|
messages.push("Statusline: Already configured in settings, skipped");
|
|
1581
1849
|
}
|
|
1582
1850
|
statusline?.cleanup?.();
|
|
1583
1851
|
}
|
|
1584
1852
|
if (settingsChanged) {
|
|
1585
|
-
if (!
|
|
1586
|
-
|
|
1853
|
+
if (!fs7.existsSync(claudeDir)) {
|
|
1854
|
+
fs7.mkdirSync(claudeDir, { recursive: true });
|
|
1587
1855
|
}
|
|
1588
|
-
|
|
1856
|
+
fs7.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
1589
1857
|
changed = true;
|
|
1590
1858
|
}
|
|
1591
1859
|
if (claudeMd) {
|
|
1592
|
-
const rulesDir =
|
|
1593
|
-
const rulesLabel = scope === "user" ?
|
|
1594
|
-
if (
|
|
1595
|
-
const existing =
|
|
1860
|
+
const rulesDir = path9.dirname(rulesPath);
|
|
1861
|
+
const rulesLabel = scope === "user" ? path9.join(path9.relative(os3.homedir(), path9.dirname(rulesPath)), "keepgoing.md").replace(/\\/g, "/") : ".claude/rules/keepgoing.md";
|
|
1862
|
+
if (fs7.existsSync(rulesPath)) {
|
|
1863
|
+
const existing = fs7.readFileSync(rulesPath, "utf-8");
|
|
1596
1864
|
const existingVersion = getRulesFileVersion(existing);
|
|
1597
1865
|
if (existingVersion === null) {
|
|
1598
1866
|
messages.push(`Rules file: Custom file found at ${rulesLabel}, skipping`);
|
|
1599
1867
|
} else if (existingVersion >= KEEPGOING_RULES_VERSION) {
|
|
1600
1868
|
messages.push(`Rules file: Already up to date (v${existingVersion}), skipped`);
|
|
1601
1869
|
} else {
|
|
1602
|
-
if (!
|
|
1603
|
-
|
|
1870
|
+
if (!fs7.existsSync(rulesDir)) {
|
|
1871
|
+
fs7.mkdirSync(rulesDir, { recursive: true });
|
|
1604
1872
|
}
|
|
1605
|
-
|
|
1873
|
+
fs7.writeFileSync(rulesPath, KEEPGOING_RULES_CONTENT);
|
|
1606
1874
|
changed = true;
|
|
1607
1875
|
messages.push(`Rules file: Updated v${existingVersion} \u2192 v${KEEPGOING_RULES_VERSION} at ${rulesLabel}`);
|
|
1608
1876
|
}
|
|
1609
1877
|
} else {
|
|
1610
|
-
const existingClaudeMd =
|
|
1611
|
-
if (!
|
|
1612
|
-
|
|
1878
|
+
const existingClaudeMd = fs7.existsSync(claudeMdPath) ? fs7.readFileSync(claudeMdPath, "utf-8") : "";
|
|
1879
|
+
if (!fs7.existsSync(rulesDir)) {
|
|
1880
|
+
fs7.mkdirSync(rulesDir, { recursive: true });
|
|
1613
1881
|
}
|
|
1614
|
-
|
|
1882
|
+
fs7.writeFileSync(rulesPath, KEEPGOING_RULES_CONTENT);
|
|
1615
1883
|
changed = true;
|
|
1616
1884
|
if (existingClaudeMd.includes("## KeepGoing")) {
|
|
1617
1885
|
const mdLabel = scope === "user" ? "~/.claude/CLAUDE.md" : "CLAUDE.md";
|
|
@@ -2107,14 +2375,14 @@ function renderEnrichedBriefingQuiet(briefing) {
|
|
|
2107
2375
|
// src/updateCheck.ts
|
|
2108
2376
|
import { spawn } from "child_process";
|
|
2109
2377
|
import { readFileSync, existsSync } from "fs";
|
|
2110
|
-
import
|
|
2378
|
+
import path10 from "path";
|
|
2111
2379
|
import os4 from "os";
|
|
2112
|
-
var CLI_VERSION = "1.
|
|
2380
|
+
var CLI_VERSION = "1.3.0";
|
|
2113
2381
|
var NPM_REGISTRY_URL = "https://registry.npmjs.org/@keepgoingdev/cli/latest";
|
|
2114
2382
|
var FETCH_TIMEOUT_MS = 5e3;
|
|
2115
2383
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
2116
|
-
var CACHE_DIR =
|
|
2117
|
-
var CACHE_PATH =
|
|
2384
|
+
var CACHE_DIR = path10.join(os4.homedir(), ".keepgoing");
|
|
2385
|
+
var CACHE_PATH = path10.join(CACHE_DIR, "update-check.json");
|
|
2118
2386
|
function isNewerVersion(current, latest) {
|
|
2119
2387
|
const cur = current.split(".").map(Number);
|
|
2120
2388
|
const lat = latest.split(".").map(Number);
|
|
@@ -2229,7 +2497,7 @@ async function statusCommand(opts) {
|
|
|
2229
2497
|
}
|
|
2230
2498
|
|
|
2231
2499
|
// src/commands/save.ts
|
|
2232
|
-
import
|
|
2500
|
+
import path11 from "path";
|
|
2233
2501
|
async function saveCommand(opts) {
|
|
2234
2502
|
const { cwd, message, nextStepOverride, json, quiet, force } = opts;
|
|
2235
2503
|
const isManual = !!message;
|
|
@@ -2258,9 +2526,9 @@ async function saveCommand(opts) {
|
|
|
2258
2526
|
sessionStartTime: lastSession?.timestamp ?? now,
|
|
2259
2527
|
lastActivityTime: now
|
|
2260
2528
|
});
|
|
2261
|
-
const summary = message ?? buildSmartSummary(events) ?? `Worked on ${touchedFiles.slice(0, 5).map((f) =>
|
|
2529
|
+
const summary = message ?? buildSmartSummary(events) ?? `Worked on ${touchedFiles.slice(0, 5).map((f) => path11.basename(f)).join(", ")}`;
|
|
2262
2530
|
const nextStep = nextStepOverride ?? buildSmartNextStep(events);
|
|
2263
|
-
const projectName =
|
|
2531
|
+
const projectName = path11.basename(resolveStorageRoot(cwd));
|
|
2264
2532
|
const sessionId = generateSessionId({
|
|
2265
2533
|
workspaceRoot: cwd,
|
|
2266
2534
|
branch: gitBranch ?? void 0,
|
|
@@ -2293,17 +2561,17 @@ async function saveCommand(opts) {
|
|
|
2293
2561
|
}
|
|
2294
2562
|
|
|
2295
2563
|
// src/commands/hook.ts
|
|
2296
|
-
import
|
|
2297
|
-
import
|
|
2564
|
+
import fs8 from "fs";
|
|
2565
|
+
import path12 from "path";
|
|
2298
2566
|
import os5 from "os";
|
|
2299
2567
|
import { execSync } from "child_process";
|
|
2300
2568
|
var HOOK_MARKER_START = "# keepgoing-hook-start";
|
|
2301
2569
|
var HOOK_MARKER_END = "# keepgoing-hook-end";
|
|
2302
2570
|
var POST_COMMIT_MARKER_START = "# keepgoing-post-commit-start";
|
|
2303
2571
|
var POST_COMMIT_MARKER_END = "# keepgoing-post-commit-end";
|
|
2304
|
-
var KEEPGOING_HOOKS_DIR =
|
|
2305
|
-
var POST_COMMIT_HOOK_PATH =
|
|
2306
|
-
var KEEPGOING_MANAGED_MARKER =
|
|
2572
|
+
var KEEPGOING_HOOKS_DIR = path12.join(os5.homedir(), ".keepgoing", "hooks");
|
|
2573
|
+
var POST_COMMIT_HOOK_PATH = path12.join(KEEPGOING_HOOKS_DIR, "post-commit");
|
|
2574
|
+
var KEEPGOING_MANAGED_MARKER = path12.join(KEEPGOING_HOOKS_DIR, ".keepgoing-managed");
|
|
2307
2575
|
var POST_COMMIT_HOOK = `#!/bin/sh
|
|
2308
2576
|
${POST_COMMIT_MARKER_START}
|
|
2309
2577
|
# Runs after every git commit. Detects high-signal decisions.
|
|
@@ -2317,7 +2585,7 @@ var ZSH_HOOK = `${HOOK_MARKER_START}
|
|
|
2317
2585
|
if command -v keepgoing >/dev/null 2>&1; then
|
|
2318
2586
|
function chpwd() {
|
|
2319
2587
|
if [ -d ".keepgoing" ]; then
|
|
2320
|
-
keepgoing
|
|
2588
|
+
keepgoing glance
|
|
2321
2589
|
fi
|
|
2322
2590
|
}
|
|
2323
2591
|
fi
|
|
@@ -2328,7 +2596,7 @@ if command -v keepgoing >/dev/null 2>&1; then
|
|
|
2328
2596
|
function cd() {
|
|
2329
2597
|
builtin cd "$@" || return
|
|
2330
2598
|
if [ -d ".keepgoing" ]; then
|
|
2331
|
-
keepgoing
|
|
2599
|
+
keepgoing glance
|
|
2332
2600
|
fi
|
|
2333
2601
|
}
|
|
2334
2602
|
fi
|
|
@@ -2338,7 +2606,7 @@ var FISH_HOOK = `${HOOK_MARKER_START}
|
|
|
2338
2606
|
if command -v keepgoing >/dev/null 2>&1
|
|
2339
2607
|
function __keepgoing_on_pwd_change --on-variable PWD
|
|
2340
2608
|
if test -d .keepgoing
|
|
2341
|
-
keepgoing
|
|
2609
|
+
keepgoing glance
|
|
2342
2610
|
end
|
|
2343
2611
|
end
|
|
2344
2612
|
end
|
|
@@ -2367,14 +2635,14 @@ function detectShellRcFile(shellOverride) {
|
|
|
2367
2635
|
}
|
|
2368
2636
|
}
|
|
2369
2637
|
if (shell === "zsh") {
|
|
2370
|
-
return { shell: "zsh", rcFile:
|
|
2638
|
+
return { shell: "zsh", rcFile: path12.join(home, ".zshrc") };
|
|
2371
2639
|
}
|
|
2372
2640
|
if (shell === "bash") {
|
|
2373
|
-
return { shell: "bash", rcFile:
|
|
2641
|
+
return { shell: "bash", rcFile: path12.join(home, ".bashrc") };
|
|
2374
2642
|
}
|
|
2375
2643
|
if (shell === "fish") {
|
|
2376
|
-
const xdgConfig = process.env["XDG_CONFIG_HOME"] ||
|
|
2377
|
-
return { shell: "fish", rcFile:
|
|
2644
|
+
const xdgConfig = process.env["XDG_CONFIG_HOME"] || path12.join(home, ".config");
|
|
2645
|
+
return { shell: "fish", rcFile: path12.join(xdgConfig, "fish", "config.fish") };
|
|
2378
2646
|
}
|
|
2379
2647
|
return void 0;
|
|
2380
2648
|
}
|
|
@@ -2385,24 +2653,24 @@ function resolveGlobalGitignorePath() {
|
|
|
2385
2653
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2386
2654
|
}).trim();
|
|
2387
2655
|
if (configured) {
|
|
2388
|
-
return configured.startsWith("~") ?
|
|
2656
|
+
return configured.startsWith("~") ? path12.join(os5.homedir(), configured.slice(1)) : configured;
|
|
2389
2657
|
}
|
|
2390
2658
|
} catch {
|
|
2391
2659
|
}
|
|
2392
|
-
return
|
|
2660
|
+
return path12.join(os5.homedir(), ".gitignore_global");
|
|
2393
2661
|
}
|
|
2394
2662
|
function installGlobalGitignore() {
|
|
2395
2663
|
const ignorePath = resolveGlobalGitignorePath();
|
|
2396
2664
|
let existing = "";
|
|
2397
2665
|
try {
|
|
2398
|
-
existing =
|
|
2666
|
+
existing = fs8.readFileSync(ignorePath, "utf-8");
|
|
2399
2667
|
} catch {
|
|
2400
2668
|
}
|
|
2401
2669
|
if (existing.split("\n").some((line) => line.trim() === ".keepgoing")) {
|
|
2402
2670
|
console.log(`Global gitignore: .keepgoing already present in ${ignorePath}`);
|
|
2403
2671
|
} else {
|
|
2404
2672
|
const suffix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
2405
|
-
|
|
2673
|
+
fs8.appendFileSync(ignorePath, `${suffix}.keepgoing
|
|
2406
2674
|
`, "utf-8");
|
|
2407
2675
|
console.log(`Global gitignore: .keepgoing added to ${ignorePath}`);
|
|
2408
2676
|
}
|
|
@@ -2421,21 +2689,21 @@ function uninstallGlobalGitignore() {
|
|
|
2421
2689
|
const ignorePath = resolveGlobalGitignorePath();
|
|
2422
2690
|
let existing = "";
|
|
2423
2691
|
try {
|
|
2424
|
-
existing =
|
|
2692
|
+
existing = fs8.readFileSync(ignorePath, "utf-8");
|
|
2425
2693
|
} catch {
|
|
2426
2694
|
return;
|
|
2427
2695
|
}
|
|
2428
2696
|
const lines = existing.split("\n");
|
|
2429
2697
|
const filtered = lines.filter((line) => line.trim() !== ".keepgoing");
|
|
2430
2698
|
if (filtered.length !== lines.length) {
|
|
2431
|
-
|
|
2699
|
+
fs8.writeFileSync(ignorePath, filtered.join("\n"), "utf-8");
|
|
2432
2700
|
console.log(`Global gitignore: .keepgoing removed from ${ignorePath}`);
|
|
2433
2701
|
}
|
|
2434
2702
|
}
|
|
2435
2703
|
function installPostCommitHook() {
|
|
2436
|
-
|
|
2437
|
-
if (
|
|
2438
|
-
const existing =
|
|
2704
|
+
fs8.mkdirSync(KEEPGOING_HOOKS_DIR, { recursive: true });
|
|
2705
|
+
if (fs8.existsSync(POST_COMMIT_HOOK_PATH)) {
|
|
2706
|
+
const existing = fs8.readFileSync(POST_COMMIT_HOOK_PATH, "utf-8");
|
|
2439
2707
|
if (existing.includes(POST_COMMIT_MARKER_START)) {
|
|
2440
2708
|
console.log(`Git post-commit hook: already installed at ${POST_COMMIT_HOOK_PATH}`);
|
|
2441
2709
|
} else {
|
|
@@ -2448,14 +2716,14 @@ if command -v keepgoing-mcp-server >/dev/null 2>&1; then
|
|
|
2448
2716
|
fi
|
|
2449
2717
|
${POST_COMMIT_MARKER_END}
|
|
2450
2718
|
`;
|
|
2451
|
-
|
|
2719
|
+
fs8.appendFileSync(POST_COMMIT_HOOK_PATH, block, "utf-8");
|
|
2452
2720
|
console.log(`Git post-commit hook: appended to existing ${POST_COMMIT_HOOK_PATH}`);
|
|
2453
2721
|
}
|
|
2454
2722
|
} else {
|
|
2455
|
-
|
|
2723
|
+
fs8.writeFileSync(POST_COMMIT_HOOK_PATH, POST_COMMIT_HOOK, { mode: 493 });
|
|
2456
2724
|
console.log(`Git post-commit hook: installed at ${POST_COMMIT_HOOK_PATH}`);
|
|
2457
2725
|
}
|
|
2458
|
-
|
|
2726
|
+
fs8.chmodSync(POST_COMMIT_HOOK_PATH, 493);
|
|
2459
2727
|
let currentHooksPath;
|
|
2460
2728
|
try {
|
|
2461
2729
|
currentHooksPath = execSync("git config --global core.hooksPath", {
|
|
@@ -2480,8 +2748,8 @@ ${POST_COMMIT_MARKER_END}
|
|
|
2480
2748
|
}
|
|
2481
2749
|
}
|
|
2482
2750
|
function uninstallPostCommitHook() {
|
|
2483
|
-
if (
|
|
2484
|
-
const existing =
|
|
2751
|
+
if (fs8.existsSync(POST_COMMIT_HOOK_PATH)) {
|
|
2752
|
+
const existing = fs8.readFileSync(POST_COMMIT_HOOK_PATH, "utf-8");
|
|
2485
2753
|
if (existing.includes(POST_COMMIT_MARKER_START)) {
|
|
2486
2754
|
const pattern = new RegExp(
|
|
2487
2755
|
`
|
|
@@ -2491,10 +2759,10 @@ function uninstallPostCommitHook() {
|
|
|
2491
2759
|
);
|
|
2492
2760
|
const updated = existing.replace(pattern, "").trim();
|
|
2493
2761
|
if (!updated || updated === "#!/bin/sh") {
|
|
2494
|
-
|
|
2762
|
+
fs8.unlinkSync(POST_COMMIT_HOOK_PATH);
|
|
2495
2763
|
console.log(`Git post-commit hook: removed ${POST_COMMIT_HOOK_PATH}`);
|
|
2496
2764
|
} else {
|
|
2497
|
-
|
|
2765
|
+
fs8.writeFileSync(POST_COMMIT_HOOK_PATH, updated + "\n", "utf-8");
|
|
2498
2766
|
console.log(`Git post-commit hook: KeepGoing block removed from ${POST_COMMIT_HOOK_PATH}`);
|
|
2499
2767
|
}
|
|
2500
2768
|
}
|
|
@@ -2512,8 +2780,8 @@ function uninstallPostCommitHook() {
|
|
|
2512
2780
|
}
|
|
2513
2781
|
} catch {
|
|
2514
2782
|
}
|
|
2515
|
-
if (
|
|
2516
|
-
|
|
2783
|
+
if (fs8.existsSync(KEEPGOING_MANAGED_MARKER)) {
|
|
2784
|
+
fs8.unlinkSync(KEEPGOING_MANAGED_MARKER);
|
|
2517
2785
|
}
|
|
2518
2786
|
}
|
|
2519
2787
|
function hookInstallCommand(shellOverride) {
|
|
@@ -2528,7 +2796,7 @@ function hookInstallCommand(shellOverride) {
|
|
|
2528
2796
|
const hookBlock = shell === "zsh" ? ZSH_HOOK : shell === "fish" ? FISH_HOOK : BASH_HOOK;
|
|
2529
2797
|
let existing = "";
|
|
2530
2798
|
try {
|
|
2531
|
-
existing =
|
|
2799
|
+
existing = fs8.readFileSync(rcFile, "utf-8");
|
|
2532
2800
|
} catch {
|
|
2533
2801
|
}
|
|
2534
2802
|
if (existing.includes(HOOK_MARKER_START)) {
|
|
@@ -2537,7 +2805,7 @@ function hookInstallCommand(shellOverride) {
|
|
|
2537
2805
|
installPostCommitHook();
|
|
2538
2806
|
return;
|
|
2539
2807
|
}
|
|
2540
|
-
|
|
2808
|
+
fs8.appendFileSync(rcFile, `
|
|
2541
2809
|
${hookBlock}
|
|
2542
2810
|
`, "utf-8");
|
|
2543
2811
|
console.log(`KeepGoing hook installed in ${rcFile}.`);
|
|
@@ -2559,7 +2827,7 @@ function hookUninstallCommand(shellOverride) {
|
|
|
2559
2827
|
const { rcFile } = detected;
|
|
2560
2828
|
let existing = "";
|
|
2561
2829
|
try {
|
|
2562
|
-
existing =
|
|
2830
|
+
existing = fs8.readFileSync(rcFile, "utf-8");
|
|
2563
2831
|
} catch {
|
|
2564
2832
|
console.log(`${rcFile} not found \u2014 nothing to remove.`);
|
|
2565
2833
|
return;
|
|
@@ -2575,7 +2843,7 @@ function hookUninstallCommand(shellOverride) {
|
|
|
2575
2843
|
"g"
|
|
2576
2844
|
);
|
|
2577
2845
|
const updated = existing.replace(pattern, "").replace(/\n{3,}/g, "\n\n");
|
|
2578
|
-
|
|
2846
|
+
fs8.writeFileSync(rcFile, updated, "utf-8");
|
|
2579
2847
|
console.log(`KeepGoing hook removed from ${rcFile}.`);
|
|
2580
2848
|
uninstallGlobalGitignore();
|
|
2581
2849
|
uninstallPostCommitHook();
|
|
@@ -2855,9 +3123,7 @@ async function decisionsCommand(opts) {
|
|
|
2855
3123
|
renderDecisions(decisions, scopeLabel);
|
|
2856
3124
|
}
|
|
2857
3125
|
|
|
2858
|
-
// src/commands/
|
|
2859
|
-
var RESET4 = "\x1B[0m";
|
|
2860
|
-
var DIM4 = "\x1B[2m";
|
|
3126
|
+
// src/commands/logUtils.ts
|
|
2861
3127
|
function parseDate(input) {
|
|
2862
3128
|
const lower = input.toLowerCase().trim();
|
|
2863
3129
|
if (lower === "today") {
|
|
@@ -2975,6 +3241,10 @@ function filterDecisions(decisions, opts) {
|
|
|
2975
3241
|
}
|
|
2976
3242
|
return result;
|
|
2977
3243
|
}
|
|
3244
|
+
|
|
3245
|
+
// src/commands/log.ts
|
|
3246
|
+
var RESET4 = "\x1B[0m";
|
|
3247
|
+
var DIM4 = "\x1B[2m";
|
|
2978
3248
|
function logSessions(reader, opts) {
|
|
2979
3249
|
const { effectiveBranch } = reader.resolveBranchScope(opts.branch || void 0);
|
|
2980
3250
|
let sessions = reader.getSessions();
|
|
@@ -3163,6 +3433,54 @@ async function continueCommand(opts) {
|
|
|
3163
3433
|
}
|
|
3164
3434
|
}
|
|
3165
3435
|
|
|
3436
|
+
// src/commands/glance.ts
|
|
3437
|
+
function glanceCommand(opts) {
|
|
3438
|
+
const snapshot = generateContextSnapshot(opts.cwd);
|
|
3439
|
+
if (!snapshot) {
|
|
3440
|
+
if (opts.json) {
|
|
3441
|
+
console.log("null");
|
|
3442
|
+
}
|
|
3443
|
+
return;
|
|
3444
|
+
}
|
|
3445
|
+
if (opts.json) {
|
|
3446
|
+
console.log(JSON.stringify(snapshot, null, 2));
|
|
3447
|
+
return;
|
|
3448
|
+
}
|
|
3449
|
+
const icon = snapshot.momentum === "hot" ? "\u26A1" : snapshot.momentum === "warm" ? "\u{1F536}" : "\u{1F4A4}";
|
|
3450
|
+
const parts = [];
|
|
3451
|
+
parts.push(`${icon} ${snapshot.doing}`);
|
|
3452
|
+
if (snapshot.next) {
|
|
3453
|
+
parts.push(`\u2192 ${snapshot.next}`);
|
|
3454
|
+
}
|
|
3455
|
+
let line = parts.join(" ");
|
|
3456
|
+
line += ` (${snapshot.when})`;
|
|
3457
|
+
if (snapshot.blocker) {
|
|
3458
|
+
line += ` \u26D4 ${snapshot.blocker}`;
|
|
3459
|
+
}
|
|
3460
|
+
console.log(line);
|
|
3461
|
+
}
|
|
3462
|
+
|
|
3463
|
+
// src/commands/hot.ts
|
|
3464
|
+
function hotCommand(opts) {
|
|
3465
|
+
const summary = generateCrossProjectSummary();
|
|
3466
|
+
if (summary.projects.length === 0) {
|
|
3467
|
+
if (opts.json) {
|
|
3468
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
3469
|
+
} else {
|
|
3470
|
+
console.log("No projects with activity found.");
|
|
3471
|
+
console.log("Projects are registered automatically when you save checkpoints.");
|
|
3472
|
+
}
|
|
3473
|
+
return;
|
|
3474
|
+
}
|
|
3475
|
+
if (opts.json) {
|
|
3476
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
3477
|
+
return;
|
|
3478
|
+
}
|
|
3479
|
+
for (const entry of summary.projects) {
|
|
3480
|
+
console.log(formatCrossProjectLine(entry));
|
|
3481
|
+
}
|
|
3482
|
+
}
|
|
3483
|
+
|
|
3166
3484
|
// src/index.ts
|
|
3167
3485
|
var HELP_TEXT = `
|
|
3168
3486
|
keepgoing: resume side projects without the mental friction
|
|
@@ -3176,6 +3494,8 @@ Commands:
|
|
|
3176
3494
|
briefing Get a re-entry briefing for this project
|
|
3177
3495
|
decisions View decision history (Pro)
|
|
3178
3496
|
log Browse session checkpoints
|
|
3497
|
+
glance Quick context snapshot (single line, <50ms)
|
|
3498
|
+
hot Cross-project activity summary, sorted by momentum
|
|
3179
3499
|
continue Export context for use in another AI tool
|
|
3180
3500
|
save Save a checkpoint (auto-generates from git)
|
|
3181
3501
|
hook Manage the shell hook (zsh, bash, fish)
|
|
@@ -3333,6 +3653,35 @@ Example:
|
|
|
3333
3653
|
keepgoing deactivate: Deactivate the Pro license from this device
|
|
3334
3654
|
|
|
3335
3655
|
Usage: keepgoing deactivate [<key>]
|
|
3656
|
+
`,
|
|
3657
|
+
glance: `
|
|
3658
|
+
keepgoing glance: Quick context snapshot in a single line
|
|
3659
|
+
|
|
3660
|
+
Usage: keepgoing glance [options]
|
|
3661
|
+
|
|
3662
|
+
Outputs a single formatted line showing what you were doing, what's next,
|
|
3663
|
+
and when you last worked. Designed for shell prompts and status bars.
|
|
3664
|
+
Completes in <50ms. Outputs nothing if no data exists.
|
|
3665
|
+
|
|
3666
|
+
Options:
|
|
3667
|
+
--json Output raw JSON
|
|
3668
|
+
--cwd <path> Override the working directory
|
|
3669
|
+
|
|
3670
|
+
Format:
|
|
3671
|
+
Hot: \u26A1 {doing} \u2192 {next} ({when})
|
|
3672
|
+
Warm: \u{1F536} {doing} \u2192 {next} ({when})
|
|
3673
|
+
Cold: \u{1F4A4} {doing} \u2192 {next} ({when})
|
|
3674
|
+
`,
|
|
3675
|
+
hot: `
|
|
3676
|
+
keepgoing hot: Cross-project activity summary, sorted by momentum
|
|
3677
|
+
|
|
3678
|
+
Usage: keepgoing hot [options]
|
|
3679
|
+
|
|
3680
|
+
Shows a one-line summary for each registered project, sorted by momentum
|
|
3681
|
+
(hot first, then warm, then cold). Projects with no data are excluded.
|
|
3682
|
+
|
|
3683
|
+
Options:
|
|
3684
|
+
--json Output raw JSON
|
|
3336
3685
|
`,
|
|
3337
3686
|
continue: `
|
|
3338
3687
|
keepgoing continue: Export context for use in another AI tool
|
|
@@ -3540,6 +3889,12 @@ async function main() {
|
|
|
3540
3889
|
sessions: parsed.sessions
|
|
3541
3890
|
});
|
|
3542
3891
|
break;
|
|
3892
|
+
case "glance":
|
|
3893
|
+
glanceCommand({ cwd, json });
|
|
3894
|
+
break;
|
|
3895
|
+
case "hot":
|
|
3896
|
+
hotCommand({ json });
|
|
3897
|
+
break;
|
|
3543
3898
|
case "continue":
|
|
3544
3899
|
await continueCommand({ cwd, json, quiet, target: parsed.target, open: parsed.open });
|
|
3545
3900
|
break;
|
|
@@ -3563,7 +3918,7 @@ async function main() {
|
|
|
3563
3918
|
}
|
|
3564
3919
|
break;
|
|
3565
3920
|
case "version":
|
|
3566
|
-
console.log(`keepgoing v${"1.
|
|
3921
|
+
console.log(`keepgoing v${"1.3.0"}`);
|
|
3567
3922
|
break;
|
|
3568
3923
|
case "activate":
|
|
3569
3924
|
await activateCommand({ licenseKey: subcommand });
|