@keepgoingdev/cli 1.2.2 → 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 +926 -576
- 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
|
|
|
@@ -588,22 +601,160 @@ function formatContinueOnPrompt(context, options) {
|
|
|
588
601
|
return result;
|
|
589
602
|
}
|
|
590
603
|
|
|
591
|
-
// ../../packages/shared/src/
|
|
592
|
-
import
|
|
593
|
-
import
|
|
594
|
-
import { randomUUID as randomUUID2, createHash } from "crypto";
|
|
604
|
+
// ../../packages/shared/src/reader.ts
|
|
605
|
+
import fs4 from "fs";
|
|
606
|
+
import path6 from "path";
|
|
595
607
|
|
|
596
|
-
// ../../packages/shared/src/
|
|
608
|
+
// ../../packages/shared/src/license.ts
|
|
609
|
+
import crypto from "crypto";
|
|
597
610
|
import fs from "fs";
|
|
598
611
|
import os from "os";
|
|
599
612
|
import path3 from "path";
|
|
600
|
-
var
|
|
601
|
-
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");
|
|
602
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
|
+
}
|
|
603
754
|
function readKnownProjects() {
|
|
604
755
|
try {
|
|
605
|
-
if (
|
|
606
|
-
const raw = JSON.parse(
|
|
756
|
+
if (fs2.existsSync(KNOWN_PROJECTS_FILE)) {
|
|
757
|
+
const raw = JSON.parse(fs2.readFileSync(KNOWN_PROJECTS_FILE, "utf-8"));
|
|
607
758
|
if (raw && Array.isArray(raw.projects)) {
|
|
608
759
|
return raw;
|
|
609
760
|
}
|
|
@@ -613,18 +764,18 @@ function readKnownProjects() {
|
|
|
613
764
|
return { version: 1, projects: [] };
|
|
614
765
|
}
|
|
615
766
|
function writeKnownProjects(data) {
|
|
616
|
-
if (!
|
|
617
|
-
|
|
767
|
+
if (!fs2.existsSync(KEEPGOING_DIR)) {
|
|
768
|
+
fs2.mkdirSync(KEEPGOING_DIR, { recursive: true });
|
|
618
769
|
}
|
|
619
770
|
const tmpFile = KNOWN_PROJECTS_FILE + ".tmp";
|
|
620
|
-
|
|
621
|
-
|
|
771
|
+
fs2.writeFileSync(tmpFile, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
772
|
+
fs2.renameSync(tmpFile, KNOWN_PROJECTS_FILE);
|
|
622
773
|
}
|
|
623
774
|
function registerProject(projectPath, projectName) {
|
|
624
775
|
try {
|
|
625
776
|
const data = readKnownProjects();
|
|
626
777
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
627
|
-
const name = projectName ||
|
|
778
|
+
const name = projectName || path4.basename(projectPath);
|
|
628
779
|
const existingIdx = data.projects.findIndex((p) => p.path === projectPath);
|
|
629
780
|
if (existingIdx >= 0) {
|
|
630
781
|
data.projects[existingIdx].lastSeen = now;
|
|
@@ -663,23 +814,23 @@ var KeepGoingWriter = class {
|
|
|
663
814
|
currentTasksFilePath;
|
|
664
815
|
constructor(workspacePath) {
|
|
665
816
|
const mainRoot = resolveStorageRoot(workspacePath);
|
|
666
|
-
this.storagePath =
|
|
667
|
-
this.sessionsFilePath =
|
|
668
|
-
this.stateFilePath =
|
|
669
|
-
this.metaFilePath =
|
|
670
|
-
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);
|
|
671
822
|
}
|
|
672
823
|
ensureDir() {
|
|
673
|
-
if (!
|
|
674
|
-
|
|
824
|
+
if (!fs3.existsSync(this.storagePath)) {
|
|
825
|
+
fs3.mkdirSync(this.storagePath, { recursive: true });
|
|
675
826
|
}
|
|
676
827
|
}
|
|
677
828
|
saveCheckpoint(checkpoint, projectName) {
|
|
678
829
|
this.ensureDir();
|
|
679
830
|
let sessionsData;
|
|
680
831
|
try {
|
|
681
|
-
if (
|
|
682
|
-
const raw = JSON.parse(
|
|
832
|
+
if (fs3.existsSync(this.sessionsFilePath)) {
|
|
833
|
+
const raw = JSON.parse(fs3.readFileSync(this.sessionsFilePath, "utf-8"));
|
|
683
834
|
if (Array.isArray(raw)) {
|
|
684
835
|
sessionsData = { version: 1, project: projectName, sessions: raw };
|
|
685
836
|
} else {
|
|
@@ -697,13 +848,13 @@ var KeepGoingWriter = class {
|
|
|
697
848
|
if (sessionsData.sessions.length > MAX_SESSIONS) {
|
|
698
849
|
sessionsData.sessions = sessionsData.sessions.slice(-MAX_SESSIONS);
|
|
699
850
|
}
|
|
700
|
-
|
|
851
|
+
fs3.writeFileSync(this.sessionsFilePath, JSON.stringify(sessionsData, null, 2), "utf-8");
|
|
701
852
|
const state = {
|
|
702
853
|
lastSessionId: checkpoint.id,
|
|
703
854
|
lastKnownBranch: checkpoint.gitBranch,
|
|
704
855
|
lastActivityAt: checkpoint.timestamp
|
|
705
856
|
};
|
|
706
|
-
|
|
857
|
+
fs3.writeFileSync(this.stateFilePath, JSON.stringify(state, null, 2), "utf-8");
|
|
707
858
|
this.updateMeta(checkpoint.timestamp);
|
|
708
859
|
const mainRoot = resolveStorageRoot(this.storagePath);
|
|
709
860
|
registerProject(mainRoot, projectName);
|
|
@@ -711,8 +862,8 @@ var KeepGoingWriter = class {
|
|
|
711
862
|
updateMeta(timestamp) {
|
|
712
863
|
let meta;
|
|
713
864
|
try {
|
|
714
|
-
if (
|
|
715
|
-
meta = JSON.parse(
|
|
865
|
+
if (fs3.existsSync(this.metaFilePath)) {
|
|
866
|
+
meta = JSON.parse(fs3.readFileSync(this.metaFilePath, "utf-8"));
|
|
716
867
|
meta.lastUpdated = timestamp;
|
|
717
868
|
} else {
|
|
718
869
|
meta = {
|
|
@@ -728,7 +879,28 @@ var KeepGoingWriter = class {
|
|
|
728
879
|
lastUpdated: timestamp
|
|
729
880
|
};
|
|
730
881
|
}
|
|
731
|
-
|
|
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");
|
|
732
904
|
}
|
|
733
905
|
// ---------------------------------------------------------------------------
|
|
734
906
|
// Multi-session API
|
|
@@ -736,8 +908,8 @@ var KeepGoingWriter = class {
|
|
|
736
908
|
/** Read all current tasks from current-tasks.json. Auto-prunes stale sessions. */
|
|
737
909
|
readCurrentTasks() {
|
|
738
910
|
try {
|
|
739
|
-
if (
|
|
740
|
-
const raw = JSON.parse(
|
|
911
|
+
if (fs3.existsSync(this.currentTasksFilePath)) {
|
|
912
|
+
const raw = JSON.parse(fs3.readFileSync(this.currentTasksFilePath, "utf-8"));
|
|
741
913
|
const tasks = Array.isArray(raw) ? raw : raw.tasks ?? [];
|
|
742
914
|
return this.pruneStale(tasks);
|
|
743
915
|
}
|
|
@@ -758,6 +930,8 @@ var KeepGoingWriter = class {
|
|
|
758
930
|
/** Core upsert logic: merges the update into current-tasks.json and returns the pruned task list. */
|
|
759
931
|
upsertSessionCore(update) {
|
|
760
932
|
this.ensureDir();
|
|
933
|
+
if (update.taskSummary) update.taskSummary = stripAgentTags(update.taskSummary);
|
|
934
|
+
if (update.sessionLabel) update.sessionLabel = stripAgentTags(update.sessionLabel);
|
|
761
935
|
const sessionId = update.sessionId || generateSessionId(update);
|
|
762
936
|
const tasks = this.readAllTasksRaw();
|
|
763
937
|
const existingIdx = tasks.findIndex((t) => t.sessionId === sessionId);
|
|
@@ -792,8 +966,8 @@ var KeepGoingWriter = class {
|
|
|
792
966
|
// ---------------------------------------------------------------------------
|
|
793
967
|
readAllTasksRaw() {
|
|
794
968
|
try {
|
|
795
|
-
if (
|
|
796
|
-
const raw = JSON.parse(
|
|
969
|
+
if (fs3.existsSync(this.currentTasksFilePath)) {
|
|
970
|
+
const raw = JSON.parse(fs3.readFileSync(this.currentTasksFilePath, "utf-8"));
|
|
797
971
|
return Array.isArray(raw) ? [...raw] : [...raw.tasks ?? []];
|
|
798
972
|
}
|
|
799
973
|
} catch {
|
|
@@ -805,7 +979,7 @@ var KeepGoingWriter = class {
|
|
|
805
979
|
}
|
|
806
980
|
writeTasksFile(tasks) {
|
|
807
981
|
const data = { version: 1, tasks };
|
|
808
|
-
|
|
982
|
+
fs3.writeFileSync(this.currentTasksFilePath, JSON.stringify(data, null, 2), "utf-8");
|
|
809
983
|
}
|
|
810
984
|
};
|
|
811
985
|
function generateSessionId(context) {
|
|
@@ -821,376 +995,79 @@ function generateSessionId(context) {
|
|
|
821
995
|
return `ses_${hash}`;
|
|
822
996
|
}
|
|
823
997
|
|
|
824
|
-
// ../../packages/shared/src/
|
|
825
|
-
var
|
|
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
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
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;
|
|
858
1062
|
}
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
1063
|
+
}
|
|
1064
|
+
if (wrapperLastSessionId) {
|
|
1065
|
+
const found = sessions.find((s) => s.id === wrapperLastSessionId);
|
|
1066
|
+
if (found) {
|
|
1067
|
+
return found;
|
|
863
1068
|
}
|
|
864
|
-
groups.get("other").push(msg.trim());
|
|
865
1069
|
}
|
|
866
|
-
|
|
867
|
-
return groups;
|
|
868
|
-
}
|
|
869
|
-
function inferWorkAreas(files) {
|
|
870
|
-
const areas = /* @__PURE__ */ new Map();
|
|
871
|
-
for (const file of files) {
|
|
872
|
-
if (NOISE_PATTERNS.some((p) => file.includes(p))) {
|
|
873
|
-
continue;
|
|
874
|
-
}
|
|
875
|
-
const parts = file.split("/").filter(Boolean);
|
|
876
|
-
let area;
|
|
877
|
-
if (parts.length >= 2 && (parts[0] === "apps" || parts[0] === "packages")) {
|
|
878
|
-
area = parts[1];
|
|
879
|
-
if (parts[0] === "packages" && parts.length >= 4 && parts[2] === "src") {
|
|
880
|
-
const subFile = parts[3].replace(/\.\w+$/, "");
|
|
881
|
-
area = `${parts[1]} ${subFile}`;
|
|
882
|
-
}
|
|
883
|
-
} else if (parts.length >= 2) {
|
|
884
|
-
area = parts[0];
|
|
885
|
-
} else {
|
|
886
|
-
area = "root";
|
|
887
|
-
}
|
|
888
|
-
areas.set(area, (areas.get(area) ?? 0) + 1);
|
|
889
|
-
}
|
|
890
|
-
return [...areas.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([name]) => name);
|
|
891
|
-
}
|
|
892
|
-
function buildSessionEvents(opts) {
|
|
893
|
-
const { wsPath, commitHashes, commitMessages, touchedFiles, currentBranch, sessionStartTime, lastActivityTime } = opts;
|
|
894
|
-
const commits = commitHashes.map((hash, i) => ({
|
|
895
|
-
hash,
|
|
896
|
-
message: commitMessages[i] ?? "",
|
|
897
|
-
filesChanged: getFilesChangedInCommit(wsPath, hash),
|
|
898
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
899
|
-
}));
|
|
900
|
-
const committedFiles = new Set(commits.flatMap((c) => c.filesChanged));
|
|
901
|
-
return {
|
|
902
|
-
commits,
|
|
903
|
-
branchSwitches: [],
|
|
904
|
-
touchedFiles,
|
|
905
|
-
currentBranch,
|
|
906
|
-
sessionStartTime,
|
|
907
|
-
lastActivityTime,
|
|
908
|
-
// Normalize rename arrows ("old -> new") from git status --porcelain
|
|
909
|
-
// so they match the plain filenames from git diff-tree --name-only.
|
|
910
|
-
hasUncommittedChanges: touchedFiles.some((f) => {
|
|
911
|
-
const normalized = f.includes(" -> ") ? f.split(" -> ").pop() : f;
|
|
912
|
-
return !committedFiles.has(normalized);
|
|
913
|
-
})
|
|
914
|
-
};
|
|
915
|
-
}
|
|
916
|
-
function buildSmartSummary(events) {
|
|
917
|
-
const { commits, branchSwitches, touchedFiles, hasUncommittedChanges } = events;
|
|
918
|
-
if (commits.length === 0 && touchedFiles.length === 0 && branchSwitches.length === 0) {
|
|
919
|
-
return void 0;
|
|
920
|
-
}
|
|
921
|
-
const parts = [];
|
|
922
|
-
if (commits.length > 0) {
|
|
923
|
-
const messages = commits.map((c) => c.message);
|
|
924
|
-
const groups = categorizeCommits(messages);
|
|
925
|
-
const phrases = [];
|
|
926
|
-
for (const [prefix, bodies] of groups) {
|
|
927
|
-
const verb = PREFIX_VERBS[prefix] ?? (prefix === "other" ? "" : `${capitalize(prefix)}:`);
|
|
928
|
-
const items = bodies.slice(0, 2).join(" and ");
|
|
929
|
-
const overflow = bodies.length > 2 ? ` (+${bodies.length - 2} more)` : "";
|
|
930
|
-
if (verb) {
|
|
931
|
-
phrases.push(`${verb} ${items}${overflow}`);
|
|
932
|
-
} else {
|
|
933
|
-
phrases.push(`${items}${overflow}`);
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
parts.push(phrases.join(", "));
|
|
937
|
-
} else if (touchedFiles.length > 0) {
|
|
938
|
-
const areas = inferWorkAreas(touchedFiles);
|
|
939
|
-
const areaStr = areas.length > 0 ? areas.join(" and ") : `${touchedFiles.length} files`;
|
|
940
|
-
const suffix = hasUncommittedChanges ? " (uncommitted)" : "";
|
|
941
|
-
parts.push(`Worked on ${areaStr}${suffix}`);
|
|
942
|
-
}
|
|
943
|
-
if (branchSwitches.length > 0) {
|
|
944
|
-
const last = branchSwitches[branchSwitches.length - 1];
|
|
945
|
-
if (branchSwitches.length === 1) {
|
|
946
|
-
parts.push(`switched to ${last.toBranch}`);
|
|
947
|
-
} else {
|
|
948
|
-
parts.push(`switched branches ${branchSwitches.length} times, ended on ${last.toBranch}`);
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
const result = parts.join("; ");
|
|
952
|
-
return result || void 0;
|
|
953
|
-
}
|
|
954
|
-
function buildSmartNextStep(events) {
|
|
955
|
-
const { commits, touchedFiles, currentBranch, hasUncommittedChanges } = events;
|
|
956
|
-
if (hasUncommittedChanges && touchedFiles.length > 0) {
|
|
957
|
-
const areas = inferWorkAreas(touchedFiles);
|
|
958
|
-
const areaStr = areas.length > 0 ? areas.join(" and ") : "working tree";
|
|
959
|
-
return `Review and commit changes in ${areaStr}`;
|
|
960
|
-
}
|
|
961
|
-
if (commits.length > 0) {
|
|
962
|
-
const lastMsg = commits[commits.length - 1].message;
|
|
963
|
-
const wipMatch = lastMsg.match(/^(?:wip|work in progress|start(?:ed)?|begin|draft)[:\s]+(.+)/i);
|
|
964
|
-
if (wipMatch) {
|
|
965
|
-
return `Continue ${wipMatch[1].trim()}`;
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
if (currentBranch && !["main", "master", "develop", "HEAD"].includes(currentBranch)) {
|
|
969
|
-
const branchName = currentBranch.replace(/^(feat|feature|fix|bugfix|hotfix|chore|refactor)[/-]/i, "").replace(/[-_]/g, " ").trim();
|
|
970
|
-
if (branchName) {
|
|
971
|
-
return `Continue ${branchName}`;
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
if (touchedFiles.length > 0) {
|
|
975
|
-
const areas = inferWorkAreas(touchedFiles);
|
|
976
|
-
if (areas.length > 0) {
|
|
977
|
-
return `Review recent changes in ${areas.join(" and ")}`;
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
return "";
|
|
981
|
-
}
|
|
982
|
-
function capitalize(s) {
|
|
983
|
-
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
// ../../packages/shared/src/decisionStorage.ts
|
|
987
|
-
import fs4 from "fs";
|
|
988
|
-
import path6 from "path";
|
|
989
|
-
|
|
990
|
-
// ../../packages/shared/src/license.ts
|
|
991
|
-
import crypto from "crypto";
|
|
992
|
-
import fs3 from "fs";
|
|
993
|
-
import os2 from "os";
|
|
994
|
-
import path5 from "path";
|
|
995
|
-
var LICENSE_FILE = "license.json";
|
|
996
|
-
var DEVICE_ID_FILE = "device-id";
|
|
997
|
-
function getGlobalLicenseDir() {
|
|
998
|
-
return path5.join(os2.homedir(), ".keepgoing");
|
|
999
|
-
}
|
|
1000
|
-
function getGlobalLicensePath() {
|
|
1001
|
-
return path5.join(getGlobalLicenseDir(), LICENSE_FILE);
|
|
1002
|
-
}
|
|
1003
|
-
function getDeviceId() {
|
|
1004
|
-
const dir = getGlobalLicenseDir();
|
|
1005
|
-
const filePath = path5.join(dir, DEVICE_ID_FILE);
|
|
1006
|
-
try {
|
|
1007
|
-
const existing = fs3.readFileSync(filePath, "utf-8").trim();
|
|
1008
|
-
if (existing) return existing;
|
|
1009
|
-
} catch {
|
|
1010
|
-
}
|
|
1011
|
-
const id = crypto.randomUUID();
|
|
1012
|
-
if (!fs3.existsSync(dir)) {
|
|
1013
|
-
fs3.mkdirSync(dir, { recursive: true });
|
|
1014
|
-
}
|
|
1015
|
-
fs3.writeFileSync(filePath, id, "utf-8");
|
|
1016
|
-
return id;
|
|
1017
|
-
}
|
|
1018
|
-
var DECISION_DETECTION_VARIANT_ID = 1361527;
|
|
1019
|
-
var SESSION_AWARENESS_VARIANT_ID = 1366510;
|
|
1020
|
-
var TEST_DECISION_DETECTION_VARIANT_ID = 1345647;
|
|
1021
|
-
var TEST_SESSION_AWARENESS_VARIANT_ID = 1365992;
|
|
1022
|
-
var VARIANT_FEATURE_MAP = {
|
|
1023
|
-
[DECISION_DETECTION_VARIANT_ID]: ["decisions"],
|
|
1024
|
-
[SESSION_AWARENESS_VARIANT_ID]: ["session-awareness"],
|
|
1025
|
-
[TEST_DECISION_DETECTION_VARIANT_ID]: ["decisions"],
|
|
1026
|
-
[TEST_SESSION_AWARENESS_VARIANT_ID]: ["session-awareness"]
|
|
1027
|
-
// Future bundle: [BUNDLE_VARIANT_ID]: ['decisions', 'session-awareness'],
|
|
1028
|
-
};
|
|
1029
|
-
var KNOWN_VARIANT_IDS = new Set(Object.keys(VARIANT_FEATURE_MAP).map(Number));
|
|
1030
|
-
function getVariantLabel(variantId) {
|
|
1031
|
-
const features = VARIANT_FEATURE_MAP[variantId];
|
|
1032
|
-
if (!features) return "Unknown Add-on";
|
|
1033
|
-
if (features.includes("decisions") && features.includes("session-awareness")) return "Pro Bundle";
|
|
1034
|
-
if (features.includes("decisions")) return "Decision Detection";
|
|
1035
|
-
if (features.includes("session-awareness")) return "Session Awareness";
|
|
1036
|
-
return "Pro Add-on";
|
|
1037
|
-
}
|
|
1038
|
-
var _cachedStore;
|
|
1039
|
-
var _cacheTimestamp = 0;
|
|
1040
|
-
var LICENSE_CACHE_TTL_MS = 2e3;
|
|
1041
|
-
function readLicenseStore() {
|
|
1042
|
-
const now = Date.now();
|
|
1043
|
-
if (_cachedStore && now - _cacheTimestamp < LICENSE_CACHE_TTL_MS) {
|
|
1044
|
-
return _cachedStore;
|
|
1045
|
-
}
|
|
1046
|
-
const licensePath = getGlobalLicensePath();
|
|
1047
|
-
let store;
|
|
1048
|
-
try {
|
|
1049
|
-
if (!fs3.existsSync(licensePath)) {
|
|
1050
|
-
store = { version: 2, licenses: [] };
|
|
1051
|
-
} else {
|
|
1052
|
-
const raw = fs3.readFileSync(licensePath, "utf-8");
|
|
1053
|
-
const data = JSON.parse(raw);
|
|
1054
|
-
if (data?.version === 2 && Array.isArray(data.licenses)) {
|
|
1055
|
-
store = data;
|
|
1056
|
-
} else {
|
|
1057
|
-
store = { version: 2, licenses: [] };
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
} catch {
|
|
1061
|
-
store = { version: 2, licenses: [] };
|
|
1062
|
-
}
|
|
1063
|
-
_cachedStore = store;
|
|
1064
|
-
_cacheTimestamp = now;
|
|
1065
|
-
return store;
|
|
1066
|
-
}
|
|
1067
|
-
function writeLicenseStore(store) {
|
|
1068
|
-
const dirPath = getGlobalLicenseDir();
|
|
1069
|
-
if (!fs3.existsSync(dirPath)) {
|
|
1070
|
-
fs3.mkdirSync(dirPath, { recursive: true });
|
|
1071
|
-
}
|
|
1072
|
-
const licensePath = path5.join(dirPath, LICENSE_FILE);
|
|
1073
|
-
fs3.writeFileSync(licensePath, JSON.stringify(store, null, 2), "utf-8");
|
|
1074
|
-
_cachedStore = store;
|
|
1075
|
-
_cacheTimestamp = Date.now();
|
|
1076
|
-
}
|
|
1077
|
-
function addLicenseEntry(entry) {
|
|
1078
|
-
const store = readLicenseStore();
|
|
1079
|
-
const idx = store.licenses.findIndex((l) => l.licenseKey === entry.licenseKey);
|
|
1080
|
-
if (idx >= 0) {
|
|
1081
|
-
store.licenses[idx] = entry;
|
|
1082
|
-
} else {
|
|
1083
|
-
store.licenses.push(entry);
|
|
1084
|
-
}
|
|
1085
|
-
writeLicenseStore(store);
|
|
1086
|
-
}
|
|
1087
|
-
function removeLicenseEntry(licenseKey) {
|
|
1088
|
-
const store = readLicenseStore();
|
|
1089
|
-
store.licenses = store.licenses.filter((l) => l.licenseKey !== licenseKey);
|
|
1090
|
-
writeLicenseStore(store);
|
|
1091
|
-
}
|
|
1092
|
-
function getActiveLicenses() {
|
|
1093
|
-
return readLicenseStore().licenses.filter((l) => l.status === "active");
|
|
1094
|
-
}
|
|
1095
|
-
function getLicenseForFeature(feature) {
|
|
1096
|
-
const active = getActiveLicenses();
|
|
1097
|
-
return active.find((l) => {
|
|
1098
|
-
const features = VARIANT_FEATURE_MAP[l.variantId];
|
|
1099
|
-
return features?.includes(feature);
|
|
1100
|
-
});
|
|
1101
|
-
}
|
|
1102
|
-
function getAllLicensesNeedingRevalidation() {
|
|
1103
|
-
return getActiveLicenses().filter((l) => needsRevalidation(l));
|
|
1104
|
-
}
|
|
1105
|
-
var REVALIDATION_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
|
|
1106
|
-
function needsRevalidation(entry) {
|
|
1107
|
-
const lastValidated = new Date(entry.lastValidatedAt).getTime();
|
|
1108
|
-
return Date.now() - lastValidated > REVALIDATION_THRESHOLD_MS;
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
// ../../packages/shared/src/featureGate.ts
|
|
1112
|
-
var DefaultFeatureGate = class {
|
|
1113
|
-
isEnabled(_feature) {
|
|
1114
|
-
return true;
|
|
1115
|
-
}
|
|
1116
|
-
};
|
|
1117
|
-
var currentGate = new DefaultFeatureGate();
|
|
1118
|
-
|
|
1119
|
-
// ../../packages/shared/src/reader.ts
|
|
1120
|
-
import fs5 from "fs";
|
|
1121
|
-
import path7 from "path";
|
|
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 = path7.join(mainRoot, STORAGE_DIR2);
|
|
1144
|
-
this.metaFilePath = path7.join(this.storagePath, META_FILE2);
|
|
1145
|
-
this.sessionsFilePath = path7.join(this.storagePath, SESSIONS_FILE2);
|
|
1146
|
-
this.decisionsFilePath = path7.join(this.storagePath, DECISIONS_FILE);
|
|
1147
|
-
this.stateFilePath = path7.join(this.storagePath, STATE_FILE2);
|
|
1148
|
-
this.currentTasksFilePath = path7.join(this.storagePath, CURRENT_TASKS_FILE2);
|
|
1149
|
-
}
|
|
1150
|
-
/** Check if .keepgoing/ directory exists. */
|
|
1151
|
-
exists() {
|
|
1152
|
-
return fs5.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;
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
if (wrapperLastSessionId) {
|
|
1188
|
-
const found = sessions.find((s) => s.id === wrapperLastSessionId);
|
|
1189
|
-
if (found) {
|
|
1190
|
-
return found;
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
return sessions[sessions.length - 1];
|
|
1070
|
+
return sessions[sessions.length - 1];
|
|
1194
1071
|
}
|
|
1195
1072
|
/**
|
|
1196
1073
|
* Returns the last N sessions, newest first.
|
|
@@ -1278,130 +1155,516 @@ var KeepGoingReader = class {
|
|
|
1278
1155
|
overlaps.push({ branch, sessions });
|
|
1279
1156
|
}
|
|
1280
1157
|
}
|
|
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;
|
|
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}`);
|
|
1304
1396
|
}
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
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}`);
|
|
1312
1403
|
}
|
|
1313
|
-
return this._cachedBranch;
|
|
1314
1404
|
}
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
const
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
if (scoped) return { session: scoped, isFallback: false };
|
|
1325
|
-
return { session: this.getLastSession(), isFallback: true };
|
|
1326
|
-
}
|
|
1327
|
-
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}`;
|
|
1328
1414
|
}
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
const
|
|
1332
|
-
if (
|
|
1333
|
-
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()}`;
|
|
1334
1420
|
}
|
|
1335
|
-
return this.getRecentSessions(count);
|
|
1336
1421
|
}
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
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}`;
|
|
1342
1426
|
}
|
|
1343
|
-
return this.getRecentDecisions(count);
|
|
1344
1427
|
}
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
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" };
|
|
1428
|
+
if (touchedFiles.length > 0) {
|
|
1429
|
+
const areas = inferWorkAreas(touchedFiles);
|
|
1430
|
+
if (areas.length > 0) {
|
|
1431
|
+
return `Review recent changes in ${areas.join(" and ")}`;
|
|
1355
1432
|
}
|
|
1356
|
-
|
|
1357
|
-
|
|
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
|
+
}
|
|
1358
1465
|
}
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
return { effectiveBranch: currentBranch, scopeLabel: `branch \`${currentBranch}\` (worktree)` };
|
|
1466
|
+
if (signals.recentCommitCount !== void 0 && signals.recentCommitCount >= 2) {
|
|
1467
|
+
return "hot";
|
|
1362
1468
|
}
|
|
1363
|
-
return { effectiveBranch: void 0, scopeLabel: "all branches" };
|
|
1364
1469
|
}
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
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];
|
|
1378
1540
|
}
|
|
1379
|
-
|
|
1541
|
+
areaCounts.set(area, (areaCounts.get(area) ?? 0) + 1);
|
|
1380
1542
|
}
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
if (
|
|
1384
|
-
|
|
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);
|
|
1385
1547
|
}
|
|
1386
|
-
return { decisions: raw.decisions ?? [], lastDecisionId: raw.lastDecisionId };
|
|
1387
1548
|
}
|
|
1388
|
-
|
|
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;
|
|
1389
1625
|
try {
|
|
1390
|
-
|
|
1391
|
-
return void 0;
|
|
1392
|
-
}
|
|
1393
|
-
const raw = fs5.readFileSync(filePath, "utf-8");
|
|
1394
|
-
return JSON.parse(raw);
|
|
1626
|
+
resolvedRoot = fs5.realpathSync(entry.path);
|
|
1395
1627
|
} catch {
|
|
1396
|
-
|
|
1397
|
-
}
|
|
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;
|
|
1398
1660
|
}
|
|
1399
1661
|
};
|
|
1662
|
+
var currentGate = new DefaultFeatureGate();
|
|
1400
1663
|
|
|
1401
1664
|
// ../../packages/shared/src/setup.ts
|
|
1402
|
-
import
|
|
1665
|
+
import fs7 from "fs";
|
|
1403
1666
|
import os3 from "os";
|
|
1404
|
-
import
|
|
1667
|
+
import path9 from "path";
|
|
1405
1668
|
var KEEPGOING_MARKER = "@keepgoingdev/mcp-server";
|
|
1406
1669
|
var SESSION_START_HOOK = {
|
|
1407
1670
|
matcher: "",
|
|
@@ -1456,7 +1719,7 @@ function getRulesFileVersion(content) {
|
|
|
1456
1719
|
}
|
|
1457
1720
|
var STATUSLINE_CMD = "npx -y @keepgoingdev/mcp-server --statusline";
|
|
1458
1721
|
function detectClaudeDir() {
|
|
1459
|
-
return process.env.CLAUDE_CONFIG_DIR ||
|
|
1722
|
+
return process.env.CLAUDE_CONFIG_DIR || path9.join(os3.homedir(), ".claude");
|
|
1460
1723
|
}
|
|
1461
1724
|
function hasKeepGoingHook(hookEntries) {
|
|
1462
1725
|
return hookEntries.some(
|
|
@@ -1468,19 +1731,19 @@ function resolveScopePaths(scope, workspacePath, overrideClaudeDir) {
|
|
|
1468
1731
|
const claudeDir2 = overrideClaudeDir || detectClaudeDir();
|
|
1469
1732
|
return {
|
|
1470
1733
|
claudeDir: claudeDir2,
|
|
1471
|
-
settingsPath:
|
|
1472
|
-
claudeMdPath:
|
|
1473
|
-
rulesPath:
|
|
1734
|
+
settingsPath: path9.join(claudeDir2, "settings.json"),
|
|
1735
|
+
claudeMdPath: path9.join(claudeDir2, "CLAUDE.md"),
|
|
1736
|
+
rulesPath: path9.join(claudeDir2, "rules", "keepgoing.md")
|
|
1474
1737
|
};
|
|
1475
1738
|
}
|
|
1476
|
-
const claudeDir =
|
|
1477
|
-
const dotClaudeMdPath =
|
|
1478
|
-
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");
|
|
1479
1742
|
return {
|
|
1480
1743
|
claudeDir,
|
|
1481
|
-
settingsPath:
|
|
1482
|
-
claudeMdPath:
|
|
1483
|
-
rulesPath:
|
|
1744
|
+
settingsPath: path9.join(claudeDir, "settings.json"),
|
|
1745
|
+
claudeMdPath: fs7.existsSync(dotClaudeMdPath) ? dotClaudeMdPath : rootClaudeMdPath,
|
|
1746
|
+
rulesPath: path9.join(workspacePath, ".claude", "rules", "keepgoing.md")
|
|
1484
1747
|
};
|
|
1485
1748
|
}
|
|
1486
1749
|
function writeHooksToSettings(settings) {
|
|
@@ -1520,11 +1783,11 @@ function writeHooksToSettings(settings) {
|
|
|
1520
1783
|
}
|
|
1521
1784
|
function checkHookConflict(scope, workspacePath) {
|
|
1522
1785
|
const otherPaths = resolveScopePaths(scope === "user" ? "project" : "user", workspacePath);
|
|
1523
|
-
if (!
|
|
1786
|
+
if (!fs7.existsSync(otherPaths.settingsPath)) {
|
|
1524
1787
|
return null;
|
|
1525
1788
|
}
|
|
1526
1789
|
try {
|
|
1527
|
-
const otherSettings = JSON.parse(
|
|
1790
|
+
const otherSettings = JSON.parse(fs7.readFileSync(otherPaths.settingsPath, "utf-8"));
|
|
1528
1791
|
const hooks = otherSettings?.hooks;
|
|
1529
1792
|
if (!hooks) return null;
|
|
1530
1793
|
const hasConflict = Array.isArray(hooks.SessionStart) && hasKeepGoingHook(hooks.SessionStart) || Array.isArray(hooks.Stop) && hasKeepGoingHook(hooks.Stop);
|
|
@@ -1553,10 +1816,10 @@ function setupProject(options) {
|
|
|
1553
1816
|
workspacePath,
|
|
1554
1817
|
claudeDirOverride
|
|
1555
1818
|
);
|
|
1556
|
-
const scopeLabel = scope === "user" ?
|
|
1819
|
+
const scopeLabel = scope === "user" ? path9.join("~", path9.relative(os3.homedir(), claudeDir), "settings.json").replace(/\\/g, "/") : ".claude/settings.json";
|
|
1557
1820
|
let settings = {};
|
|
1558
|
-
if (
|
|
1559
|
-
settings = JSON.parse(
|
|
1821
|
+
if (fs7.existsSync(settingsPath)) {
|
|
1822
|
+
settings = JSON.parse(fs7.readFileSync(settingsPath, "utf-8"));
|
|
1560
1823
|
}
|
|
1561
1824
|
let settingsChanged = false;
|
|
1562
1825
|
if (sessionHooks) {
|
|
@@ -1587,36 +1850,36 @@ function setupProject(options) {
|
|
|
1587
1850
|
statusline?.cleanup?.();
|
|
1588
1851
|
}
|
|
1589
1852
|
if (settingsChanged) {
|
|
1590
|
-
if (!
|
|
1591
|
-
|
|
1853
|
+
if (!fs7.existsSync(claudeDir)) {
|
|
1854
|
+
fs7.mkdirSync(claudeDir, { recursive: true });
|
|
1592
1855
|
}
|
|
1593
|
-
|
|
1856
|
+
fs7.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
1594
1857
|
changed = true;
|
|
1595
1858
|
}
|
|
1596
1859
|
if (claudeMd) {
|
|
1597
|
-
const rulesDir =
|
|
1598
|
-
const rulesLabel = scope === "user" ?
|
|
1599
|
-
if (
|
|
1600
|
-
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");
|
|
1601
1864
|
const existingVersion = getRulesFileVersion(existing);
|
|
1602
1865
|
if (existingVersion === null) {
|
|
1603
1866
|
messages.push(`Rules file: Custom file found at ${rulesLabel}, skipping`);
|
|
1604
1867
|
} else if (existingVersion >= KEEPGOING_RULES_VERSION) {
|
|
1605
1868
|
messages.push(`Rules file: Already up to date (v${existingVersion}), skipped`);
|
|
1606
1869
|
} else {
|
|
1607
|
-
if (!
|
|
1608
|
-
|
|
1870
|
+
if (!fs7.existsSync(rulesDir)) {
|
|
1871
|
+
fs7.mkdirSync(rulesDir, { recursive: true });
|
|
1609
1872
|
}
|
|
1610
|
-
|
|
1873
|
+
fs7.writeFileSync(rulesPath, KEEPGOING_RULES_CONTENT);
|
|
1611
1874
|
changed = true;
|
|
1612
1875
|
messages.push(`Rules file: Updated v${existingVersion} \u2192 v${KEEPGOING_RULES_VERSION} at ${rulesLabel}`);
|
|
1613
1876
|
}
|
|
1614
1877
|
} else {
|
|
1615
|
-
const existingClaudeMd =
|
|
1616
|
-
if (!
|
|
1617
|
-
|
|
1878
|
+
const existingClaudeMd = fs7.existsSync(claudeMdPath) ? fs7.readFileSync(claudeMdPath, "utf-8") : "";
|
|
1879
|
+
if (!fs7.existsSync(rulesDir)) {
|
|
1880
|
+
fs7.mkdirSync(rulesDir, { recursive: true });
|
|
1618
1881
|
}
|
|
1619
|
-
|
|
1882
|
+
fs7.writeFileSync(rulesPath, KEEPGOING_RULES_CONTENT);
|
|
1620
1883
|
changed = true;
|
|
1621
1884
|
if (existingClaudeMd.includes("## KeepGoing")) {
|
|
1622
1885
|
const mdLabel = scope === "user" ? "~/.claude/CLAUDE.md" : "CLAUDE.md";
|
|
@@ -2112,14 +2375,14 @@ function renderEnrichedBriefingQuiet(briefing) {
|
|
|
2112
2375
|
// src/updateCheck.ts
|
|
2113
2376
|
import { spawn } from "child_process";
|
|
2114
2377
|
import { readFileSync, existsSync } from "fs";
|
|
2115
|
-
import
|
|
2378
|
+
import path10 from "path";
|
|
2116
2379
|
import os4 from "os";
|
|
2117
|
-
var CLI_VERSION = "1.
|
|
2380
|
+
var CLI_VERSION = "1.3.0";
|
|
2118
2381
|
var NPM_REGISTRY_URL = "https://registry.npmjs.org/@keepgoingdev/cli/latest";
|
|
2119
2382
|
var FETCH_TIMEOUT_MS = 5e3;
|
|
2120
2383
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
2121
|
-
var CACHE_DIR =
|
|
2122
|
-
var CACHE_PATH =
|
|
2384
|
+
var CACHE_DIR = path10.join(os4.homedir(), ".keepgoing");
|
|
2385
|
+
var CACHE_PATH = path10.join(CACHE_DIR, "update-check.json");
|
|
2123
2386
|
function isNewerVersion(current, latest) {
|
|
2124
2387
|
const cur = current.split(".").map(Number);
|
|
2125
2388
|
const lat = latest.split(".").map(Number);
|
|
@@ -2234,7 +2497,7 @@ async function statusCommand(opts) {
|
|
|
2234
2497
|
}
|
|
2235
2498
|
|
|
2236
2499
|
// src/commands/save.ts
|
|
2237
|
-
import
|
|
2500
|
+
import path11 from "path";
|
|
2238
2501
|
async function saveCommand(opts) {
|
|
2239
2502
|
const { cwd, message, nextStepOverride, json, quiet, force } = opts;
|
|
2240
2503
|
const isManual = !!message;
|
|
@@ -2263,9 +2526,9 @@ async function saveCommand(opts) {
|
|
|
2263
2526
|
sessionStartTime: lastSession?.timestamp ?? now,
|
|
2264
2527
|
lastActivityTime: now
|
|
2265
2528
|
});
|
|
2266
|
-
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(", ")}`;
|
|
2267
2530
|
const nextStep = nextStepOverride ?? buildSmartNextStep(events);
|
|
2268
|
-
const projectName =
|
|
2531
|
+
const projectName = path11.basename(resolveStorageRoot(cwd));
|
|
2269
2532
|
const sessionId = generateSessionId({
|
|
2270
2533
|
workspaceRoot: cwd,
|
|
2271
2534
|
branch: gitBranch ?? void 0,
|
|
@@ -2298,17 +2561,17 @@ async function saveCommand(opts) {
|
|
|
2298
2561
|
}
|
|
2299
2562
|
|
|
2300
2563
|
// src/commands/hook.ts
|
|
2301
|
-
import
|
|
2302
|
-
import
|
|
2564
|
+
import fs8 from "fs";
|
|
2565
|
+
import path12 from "path";
|
|
2303
2566
|
import os5 from "os";
|
|
2304
2567
|
import { execSync } from "child_process";
|
|
2305
2568
|
var HOOK_MARKER_START = "# keepgoing-hook-start";
|
|
2306
2569
|
var HOOK_MARKER_END = "# keepgoing-hook-end";
|
|
2307
2570
|
var POST_COMMIT_MARKER_START = "# keepgoing-post-commit-start";
|
|
2308
2571
|
var POST_COMMIT_MARKER_END = "# keepgoing-post-commit-end";
|
|
2309
|
-
var KEEPGOING_HOOKS_DIR =
|
|
2310
|
-
var POST_COMMIT_HOOK_PATH =
|
|
2311
|
-
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");
|
|
2312
2575
|
var POST_COMMIT_HOOK = `#!/bin/sh
|
|
2313
2576
|
${POST_COMMIT_MARKER_START}
|
|
2314
2577
|
# Runs after every git commit. Detects high-signal decisions.
|
|
@@ -2322,7 +2585,7 @@ var ZSH_HOOK = `${HOOK_MARKER_START}
|
|
|
2322
2585
|
if command -v keepgoing >/dev/null 2>&1; then
|
|
2323
2586
|
function chpwd() {
|
|
2324
2587
|
if [ -d ".keepgoing" ]; then
|
|
2325
|
-
keepgoing
|
|
2588
|
+
keepgoing glance
|
|
2326
2589
|
fi
|
|
2327
2590
|
}
|
|
2328
2591
|
fi
|
|
@@ -2333,7 +2596,7 @@ if command -v keepgoing >/dev/null 2>&1; then
|
|
|
2333
2596
|
function cd() {
|
|
2334
2597
|
builtin cd "$@" || return
|
|
2335
2598
|
if [ -d ".keepgoing" ]; then
|
|
2336
|
-
keepgoing
|
|
2599
|
+
keepgoing glance
|
|
2337
2600
|
fi
|
|
2338
2601
|
}
|
|
2339
2602
|
fi
|
|
@@ -2343,7 +2606,7 @@ var FISH_HOOK = `${HOOK_MARKER_START}
|
|
|
2343
2606
|
if command -v keepgoing >/dev/null 2>&1
|
|
2344
2607
|
function __keepgoing_on_pwd_change --on-variable PWD
|
|
2345
2608
|
if test -d .keepgoing
|
|
2346
|
-
keepgoing
|
|
2609
|
+
keepgoing glance
|
|
2347
2610
|
end
|
|
2348
2611
|
end
|
|
2349
2612
|
end
|
|
@@ -2372,14 +2635,14 @@ function detectShellRcFile(shellOverride) {
|
|
|
2372
2635
|
}
|
|
2373
2636
|
}
|
|
2374
2637
|
if (shell === "zsh") {
|
|
2375
|
-
return { shell: "zsh", rcFile:
|
|
2638
|
+
return { shell: "zsh", rcFile: path12.join(home, ".zshrc") };
|
|
2376
2639
|
}
|
|
2377
2640
|
if (shell === "bash") {
|
|
2378
|
-
return { shell: "bash", rcFile:
|
|
2641
|
+
return { shell: "bash", rcFile: path12.join(home, ".bashrc") };
|
|
2379
2642
|
}
|
|
2380
2643
|
if (shell === "fish") {
|
|
2381
|
-
const xdgConfig = process.env["XDG_CONFIG_HOME"] ||
|
|
2382
|
-
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") };
|
|
2383
2646
|
}
|
|
2384
2647
|
return void 0;
|
|
2385
2648
|
}
|
|
@@ -2390,24 +2653,24 @@ function resolveGlobalGitignorePath() {
|
|
|
2390
2653
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2391
2654
|
}).trim();
|
|
2392
2655
|
if (configured) {
|
|
2393
|
-
return configured.startsWith("~") ?
|
|
2656
|
+
return configured.startsWith("~") ? path12.join(os5.homedir(), configured.slice(1)) : configured;
|
|
2394
2657
|
}
|
|
2395
2658
|
} catch {
|
|
2396
2659
|
}
|
|
2397
|
-
return
|
|
2660
|
+
return path12.join(os5.homedir(), ".gitignore_global");
|
|
2398
2661
|
}
|
|
2399
2662
|
function installGlobalGitignore() {
|
|
2400
2663
|
const ignorePath = resolveGlobalGitignorePath();
|
|
2401
2664
|
let existing = "";
|
|
2402
2665
|
try {
|
|
2403
|
-
existing =
|
|
2666
|
+
existing = fs8.readFileSync(ignorePath, "utf-8");
|
|
2404
2667
|
} catch {
|
|
2405
2668
|
}
|
|
2406
2669
|
if (existing.split("\n").some((line) => line.trim() === ".keepgoing")) {
|
|
2407
2670
|
console.log(`Global gitignore: .keepgoing already present in ${ignorePath}`);
|
|
2408
2671
|
} else {
|
|
2409
2672
|
const suffix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
2410
|
-
|
|
2673
|
+
fs8.appendFileSync(ignorePath, `${suffix}.keepgoing
|
|
2411
2674
|
`, "utf-8");
|
|
2412
2675
|
console.log(`Global gitignore: .keepgoing added to ${ignorePath}`);
|
|
2413
2676
|
}
|
|
@@ -2426,21 +2689,21 @@ function uninstallGlobalGitignore() {
|
|
|
2426
2689
|
const ignorePath = resolveGlobalGitignorePath();
|
|
2427
2690
|
let existing = "";
|
|
2428
2691
|
try {
|
|
2429
|
-
existing =
|
|
2692
|
+
existing = fs8.readFileSync(ignorePath, "utf-8");
|
|
2430
2693
|
} catch {
|
|
2431
2694
|
return;
|
|
2432
2695
|
}
|
|
2433
2696
|
const lines = existing.split("\n");
|
|
2434
2697
|
const filtered = lines.filter((line) => line.trim() !== ".keepgoing");
|
|
2435
2698
|
if (filtered.length !== lines.length) {
|
|
2436
|
-
|
|
2699
|
+
fs8.writeFileSync(ignorePath, filtered.join("\n"), "utf-8");
|
|
2437
2700
|
console.log(`Global gitignore: .keepgoing removed from ${ignorePath}`);
|
|
2438
2701
|
}
|
|
2439
2702
|
}
|
|
2440
2703
|
function installPostCommitHook() {
|
|
2441
|
-
|
|
2442
|
-
if (
|
|
2443
|
-
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");
|
|
2444
2707
|
if (existing.includes(POST_COMMIT_MARKER_START)) {
|
|
2445
2708
|
console.log(`Git post-commit hook: already installed at ${POST_COMMIT_HOOK_PATH}`);
|
|
2446
2709
|
} else {
|
|
@@ -2453,14 +2716,14 @@ if command -v keepgoing-mcp-server >/dev/null 2>&1; then
|
|
|
2453
2716
|
fi
|
|
2454
2717
|
${POST_COMMIT_MARKER_END}
|
|
2455
2718
|
`;
|
|
2456
|
-
|
|
2719
|
+
fs8.appendFileSync(POST_COMMIT_HOOK_PATH, block, "utf-8");
|
|
2457
2720
|
console.log(`Git post-commit hook: appended to existing ${POST_COMMIT_HOOK_PATH}`);
|
|
2458
2721
|
}
|
|
2459
2722
|
} else {
|
|
2460
|
-
|
|
2723
|
+
fs8.writeFileSync(POST_COMMIT_HOOK_PATH, POST_COMMIT_HOOK, { mode: 493 });
|
|
2461
2724
|
console.log(`Git post-commit hook: installed at ${POST_COMMIT_HOOK_PATH}`);
|
|
2462
2725
|
}
|
|
2463
|
-
|
|
2726
|
+
fs8.chmodSync(POST_COMMIT_HOOK_PATH, 493);
|
|
2464
2727
|
let currentHooksPath;
|
|
2465
2728
|
try {
|
|
2466
2729
|
currentHooksPath = execSync("git config --global core.hooksPath", {
|
|
@@ -2485,8 +2748,8 @@ ${POST_COMMIT_MARKER_END}
|
|
|
2485
2748
|
}
|
|
2486
2749
|
}
|
|
2487
2750
|
function uninstallPostCommitHook() {
|
|
2488
|
-
if (
|
|
2489
|
-
const existing =
|
|
2751
|
+
if (fs8.existsSync(POST_COMMIT_HOOK_PATH)) {
|
|
2752
|
+
const existing = fs8.readFileSync(POST_COMMIT_HOOK_PATH, "utf-8");
|
|
2490
2753
|
if (existing.includes(POST_COMMIT_MARKER_START)) {
|
|
2491
2754
|
const pattern = new RegExp(
|
|
2492
2755
|
`
|
|
@@ -2496,10 +2759,10 @@ function uninstallPostCommitHook() {
|
|
|
2496
2759
|
);
|
|
2497
2760
|
const updated = existing.replace(pattern, "").trim();
|
|
2498
2761
|
if (!updated || updated === "#!/bin/sh") {
|
|
2499
|
-
|
|
2762
|
+
fs8.unlinkSync(POST_COMMIT_HOOK_PATH);
|
|
2500
2763
|
console.log(`Git post-commit hook: removed ${POST_COMMIT_HOOK_PATH}`);
|
|
2501
2764
|
} else {
|
|
2502
|
-
|
|
2765
|
+
fs8.writeFileSync(POST_COMMIT_HOOK_PATH, updated + "\n", "utf-8");
|
|
2503
2766
|
console.log(`Git post-commit hook: KeepGoing block removed from ${POST_COMMIT_HOOK_PATH}`);
|
|
2504
2767
|
}
|
|
2505
2768
|
}
|
|
@@ -2517,8 +2780,8 @@ function uninstallPostCommitHook() {
|
|
|
2517
2780
|
}
|
|
2518
2781
|
} catch {
|
|
2519
2782
|
}
|
|
2520
|
-
if (
|
|
2521
|
-
|
|
2783
|
+
if (fs8.existsSync(KEEPGOING_MANAGED_MARKER)) {
|
|
2784
|
+
fs8.unlinkSync(KEEPGOING_MANAGED_MARKER);
|
|
2522
2785
|
}
|
|
2523
2786
|
}
|
|
2524
2787
|
function hookInstallCommand(shellOverride) {
|
|
@@ -2533,7 +2796,7 @@ function hookInstallCommand(shellOverride) {
|
|
|
2533
2796
|
const hookBlock = shell === "zsh" ? ZSH_HOOK : shell === "fish" ? FISH_HOOK : BASH_HOOK;
|
|
2534
2797
|
let existing = "";
|
|
2535
2798
|
try {
|
|
2536
|
-
existing =
|
|
2799
|
+
existing = fs8.readFileSync(rcFile, "utf-8");
|
|
2537
2800
|
} catch {
|
|
2538
2801
|
}
|
|
2539
2802
|
if (existing.includes(HOOK_MARKER_START)) {
|
|
@@ -2542,7 +2805,7 @@ function hookInstallCommand(shellOverride) {
|
|
|
2542
2805
|
installPostCommitHook();
|
|
2543
2806
|
return;
|
|
2544
2807
|
}
|
|
2545
|
-
|
|
2808
|
+
fs8.appendFileSync(rcFile, `
|
|
2546
2809
|
${hookBlock}
|
|
2547
2810
|
`, "utf-8");
|
|
2548
2811
|
console.log(`KeepGoing hook installed in ${rcFile}.`);
|
|
@@ -2564,7 +2827,7 @@ function hookUninstallCommand(shellOverride) {
|
|
|
2564
2827
|
const { rcFile } = detected;
|
|
2565
2828
|
let existing = "";
|
|
2566
2829
|
try {
|
|
2567
|
-
existing =
|
|
2830
|
+
existing = fs8.readFileSync(rcFile, "utf-8");
|
|
2568
2831
|
} catch {
|
|
2569
2832
|
console.log(`${rcFile} not found \u2014 nothing to remove.`);
|
|
2570
2833
|
return;
|
|
@@ -2580,7 +2843,7 @@ function hookUninstallCommand(shellOverride) {
|
|
|
2580
2843
|
"g"
|
|
2581
2844
|
);
|
|
2582
2845
|
const updated = existing.replace(pattern, "").replace(/\n{3,}/g, "\n\n");
|
|
2583
|
-
|
|
2846
|
+
fs8.writeFileSync(rcFile, updated, "utf-8");
|
|
2584
2847
|
console.log(`KeepGoing hook removed from ${rcFile}.`);
|
|
2585
2848
|
uninstallGlobalGitignore();
|
|
2586
2849
|
uninstallPostCommitHook();
|
|
@@ -2860,9 +3123,7 @@ async function decisionsCommand(opts) {
|
|
|
2860
3123
|
renderDecisions(decisions, scopeLabel);
|
|
2861
3124
|
}
|
|
2862
3125
|
|
|
2863
|
-
// src/commands/
|
|
2864
|
-
var RESET4 = "\x1B[0m";
|
|
2865
|
-
var DIM4 = "\x1B[2m";
|
|
3126
|
+
// src/commands/logUtils.ts
|
|
2866
3127
|
function parseDate(input) {
|
|
2867
3128
|
const lower = input.toLowerCase().trim();
|
|
2868
3129
|
if (lower === "today") {
|
|
@@ -2980,6 +3241,10 @@ function filterDecisions(decisions, opts) {
|
|
|
2980
3241
|
}
|
|
2981
3242
|
return result;
|
|
2982
3243
|
}
|
|
3244
|
+
|
|
3245
|
+
// src/commands/log.ts
|
|
3246
|
+
var RESET4 = "\x1B[0m";
|
|
3247
|
+
var DIM4 = "\x1B[2m";
|
|
2983
3248
|
function logSessions(reader, opts) {
|
|
2984
3249
|
const { effectiveBranch } = reader.resolveBranchScope(opts.branch || void 0);
|
|
2985
3250
|
let sessions = reader.getSessions();
|
|
@@ -3168,6 +3433,54 @@ async function continueCommand(opts) {
|
|
|
3168
3433
|
}
|
|
3169
3434
|
}
|
|
3170
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
|
+
|
|
3171
3484
|
// src/index.ts
|
|
3172
3485
|
var HELP_TEXT = `
|
|
3173
3486
|
keepgoing: resume side projects without the mental friction
|
|
@@ -3181,6 +3494,8 @@ Commands:
|
|
|
3181
3494
|
briefing Get a re-entry briefing for this project
|
|
3182
3495
|
decisions View decision history (Pro)
|
|
3183
3496
|
log Browse session checkpoints
|
|
3497
|
+
glance Quick context snapshot (single line, <50ms)
|
|
3498
|
+
hot Cross-project activity summary, sorted by momentum
|
|
3184
3499
|
continue Export context for use in another AI tool
|
|
3185
3500
|
save Save a checkpoint (auto-generates from git)
|
|
3186
3501
|
hook Manage the shell hook (zsh, bash, fish)
|
|
@@ -3338,6 +3653,35 @@ Example:
|
|
|
3338
3653
|
keepgoing deactivate: Deactivate the Pro license from this device
|
|
3339
3654
|
|
|
3340
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
|
|
3341
3685
|
`,
|
|
3342
3686
|
continue: `
|
|
3343
3687
|
keepgoing continue: Export context for use in another AI tool
|
|
@@ -3545,6 +3889,12 @@ async function main() {
|
|
|
3545
3889
|
sessions: parsed.sessions
|
|
3546
3890
|
});
|
|
3547
3891
|
break;
|
|
3892
|
+
case "glance":
|
|
3893
|
+
glanceCommand({ cwd, json });
|
|
3894
|
+
break;
|
|
3895
|
+
case "hot":
|
|
3896
|
+
hotCommand({ json });
|
|
3897
|
+
break;
|
|
3548
3898
|
case "continue":
|
|
3549
3899
|
await continueCommand({ cwd, json, quiet, target: parsed.target, open: parsed.open });
|
|
3550
3900
|
break;
|
|
@@ -3568,7 +3918,7 @@ async function main() {
|
|
|
3568
3918
|
}
|
|
3569
3919
|
break;
|
|
3570
3920
|
case "version":
|
|
3571
|
-
console.log(`keepgoing v${"1.
|
|
3921
|
+
console.log(`keepgoing v${"1.3.0"}`);
|
|
3572
3922
|
break;
|
|
3573
3923
|
case "activate":
|
|
3574
3924
|
await activateCommand({ licenseKey: subcommand });
|