@keepgoingdev/cli 1.2.2 → 1.3.1
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 +928 -556
- 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
|
|
|
@@ -284,7 +297,8 @@ function generateEnrichedBriefing(opts) {
|
|
|
284
297
|
timestamp: s.timestamp,
|
|
285
298
|
summary: s.summary || "",
|
|
286
299
|
nextStep: s.nextStep || "",
|
|
287
|
-
branch: s.gitBranch
|
|
300
|
+
branch: s.gitBranch,
|
|
301
|
+
sessionPhase: s.sessionPhase
|
|
288
302
|
}));
|
|
289
303
|
}
|
|
290
304
|
if (opts.recentCommits && opts.recentCommits.length > 0) {
|
|
@@ -317,10 +331,13 @@ function buildCurrentFocus(lastSession, projectState, gitBranch) {
|
|
|
317
331
|
function buildRecentActivity(lastSession, recentSessions, recentCommitMessages) {
|
|
318
332
|
const parts = [];
|
|
319
333
|
const sessionCount = recentSessions.length;
|
|
334
|
+
const planCount = recentSessions.filter((s) => s.sessionPhase === "planning").length;
|
|
320
335
|
if (sessionCount > 1) {
|
|
321
|
-
|
|
336
|
+
const planSuffix = planCount > 0 ? ` (${planCount} plan-only)` : "";
|
|
337
|
+
parts.push(`${sessionCount} recent sessions${planSuffix}`);
|
|
322
338
|
} else if (sessionCount === 1) {
|
|
323
|
-
|
|
339
|
+
const planSuffix = planCount === 1 ? " (plan-only)" : "";
|
|
340
|
+
parts.push(`1 recent session${planSuffix}`);
|
|
324
341
|
}
|
|
325
342
|
if (lastSession.summary) {
|
|
326
343
|
const brief = lastSession.summary.length > 120 ? lastSession.summary.slice(0, 117) + "..." : lastSession.summary;
|
|
@@ -588,22 +605,160 @@ function formatContinueOnPrompt(context, options) {
|
|
|
588
605
|
return result;
|
|
589
606
|
}
|
|
590
607
|
|
|
591
|
-
// ../../packages/shared/src/
|
|
592
|
-
import
|
|
593
|
-
import
|
|
594
|
-
import { randomUUID as randomUUID2, createHash } from "crypto";
|
|
608
|
+
// ../../packages/shared/src/reader.ts
|
|
609
|
+
import fs4 from "fs";
|
|
610
|
+
import path6 from "path";
|
|
595
611
|
|
|
596
|
-
// ../../packages/shared/src/
|
|
612
|
+
// ../../packages/shared/src/license.ts
|
|
613
|
+
import crypto from "crypto";
|
|
597
614
|
import fs from "fs";
|
|
598
615
|
import os from "os";
|
|
599
616
|
import path3 from "path";
|
|
600
|
-
var
|
|
601
|
-
var
|
|
617
|
+
var LICENSE_FILE = "license.json";
|
|
618
|
+
var DEVICE_ID_FILE = "device-id";
|
|
619
|
+
function getGlobalLicenseDir() {
|
|
620
|
+
return path3.join(os.homedir(), ".keepgoing");
|
|
621
|
+
}
|
|
622
|
+
function getGlobalLicensePath() {
|
|
623
|
+
return path3.join(getGlobalLicenseDir(), LICENSE_FILE);
|
|
624
|
+
}
|
|
625
|
+
function getDeviceId() {
|
|
626
|
+
const dir = getGlobalLicenseDir();
|
|
627
|
+
const filePath = path3.join(dir, DEVICE_ID_FILE);
|
|
628
|
+
try {
|
|
629
|
+
const existing = fs.readFileSync(filePath, "utf-8").trim();
|
|
630
|
+
if (existing) return existing;
|
|
631
|
+
} catch {
|
|
632
|
+
}
|
|
633
|
+
const id = crypto.randomUUID();
|
|
634
|
+
if (!fs.existsSync(dir)) {
|
|
635
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
636
|
+
}
|
|
637
|
+
fs.writeFileSync(filePath, id, "utf-8");
|
|
638
|
+
return id;
|
|
639
|
+
}
|
|
640
|
+
var DECISION_DETECTION_VARIANT_ID = 1361527;
|
|
641
|
+
var SESSION_AWARENESS_VARIANT_ID = 1366510;
|
|
642
|
+
var TEST_DECISION_DETECTION_VARIANT_ID = 1345647;
|
|
643
|
+
var TEST_SESSION_AWARENESS_VARIANT_ID = 1365992;
|
|
644
|
+
var VARIANT_FEATURE_MAP = {
|
|
645
|
+
[DECISION_DETECTION_VARIANT_ID]: ["decisions"],
|
|
646
|
+
[SESSION_AWARENESS_VARIANT_ID]: ["session-awareness"],
|
|
647
|
+
[TEST_DECISION_DETECTION_VARIANT_ID]: ["decisions"],
|
|
648
|
+
[TEST_SESSION_AWARENESS_VARIANT_ID]: ["session-awareness"]
|
|
649
|
+
// Future bundle: [BUNDLE_VARIANT_ID]: ['decisions', 'session-awareness'],
|
|
650
|
+
};
|
|
651
|
+
var KNOWN_VARIANT_IDS = new Set(Object.keys(VARIANT_FEATURE_MAP).map(Number));
|
|
652
|
+
function getVariantLabel(variantId) {
|
|
653
|
+
const features = VARIANT_FEATURE_MAP[variantId];
|
|
654
|
+
if (!features) return "Unknown Add-on";
|
|
655
|
+
if (features.includes("decisions") && features.includes("session-awareness")) return "Pro Bundle";
|
|
656
|
+
if (features.includes("decisions")) return "Decision Detection";
|
|
657
|
+
if (features.includes("session-awareness")) return "Session Awareness";
|
|
658
|
+
return "Pro Add-on";
|
|
659
|
+
}
|
|
660
|
+
var _cachedStore;
|
|
661
|
+
var _cacheTimestamp = 0;
|
|
662
|
+
var LICENSE_CACHE_TTL_MS = 2e3;
|
|
663
|
+
function readLicenseStore() {
|
|
664
|
+
const now = Date.now();
|
|
665
|
+
if (_cachedStore && now - _cacheTimestamp < LICENSE_CACHE_TTL_MS) {
|
|
666
|
+
return _cachedStore;
|
|
667
|
+
}
|
|
668
|
+
const licensePath = getGlobalLicensePath();
|
|
669
|
+
let store;
|
|
670
|
+
try {
|
|
671
|
+
if (!fs.existsSync(licensePath)) {
|
|
672
|
+
store = { version: 2, licenses: [] };
|
|
673
|
+
} else {
|
|
674
|
+
const raw = fs.readFileSync(licensePath, "utf-8");
|
|
675
|
+
const data = JSON.parse(raw);
|
|
676
|
+
if (data?.version === 2 && Array.isArray(data.licenses)) {
|
|
677
|
+
store = data;
|
|
678
|
+
} else {
|
|
679
|
+
store = { version: 2, licenses: [] };
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
} catch {
|
|
683
|
+
store = { version: 2, licenses: [] };
|
|
684
|
+
}
|
|
685
|
+
_cachedStore = store;
|
|
686
|
+
_cacheTimestamp = now;
|
|
687
|
+
return store;
|
|
688
|
+
}
|
|
689
|
+
function writeLicenseStore(store) {
|
|
690
|
+
const dirPath = getGlobalLicenseDir();
|
|
691
|
+
if (!fs.existsSync(dirPath)) {
|
|
692
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
693
|
+
}
|
|
694
|
+
const licensePath = path3.join(dirPath, LICENSE_FILE);
|
|
695
|
+
fs.writeFileSync(licensePath, JSON.stringify(store, null, 2), "utf-8");
|
|
696
|
+
_cachedStore = store;
|
|
697
|
+
_cacheTimestamp = Date.now();
|
|
698
|
+
}
|
|
699
|
+
function addLicenseEntry(entry) {
|
|
700
|
+
const store = readLicenseStore();
|
|
701
|
+
const idx = store.licenses.findIndex((l) => l.licenseKey === entry.licenseKey);
|
|
702
|
+
if (idx >= 0) {
|
|
703
|
+
store.licenses[idx] = entry;
|
|
704
|
+
} else {
|
|
705
|
+
store.licenses.push(entry);
|
|
706
|
+
}
|
|
707
|
+
writeLicenseStore(store);
|
|
708
|
+
}
|
|
709
|
+
function removeLicenseEntry(licenseKey) {
|
|
710
|
+
const store = readLicenseStore();
|
|
711
|
+
store.licenses = store.licenses.filter((l) => l.licenseKey !== licenseKey);
|
|
712
|
+
writeLicenseStore(store);
|
|
713
|
+
}
|
|
714
|
+
function getActiveLicenses() {
|
|
715
|
+
return readLicenseStore().licenses.filter((l) => l.status === "active");
|
|
716
|
+
}
|
|
717
|
+
function getLicenseForFeature(feature) {
|
|
718
|
+
const active = getActiveLicenses();
|
|
719
|
+
return active.find((l) => {
|
|
720
|
+
const features = VARIANT_FEATURE_MAP[l.variantId];
|
|
721
|
+
return features?.includes(feature);
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
function getAllLicensesNeedingRevalidation() {
|
|
725
|
+
return getActiveLicenses().filter((l) => needsRevalidation(l));
|
|
726
|
+
}
|
|
727
|
+
var REVALIDATION_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
|
|
728
|
+
function needsRevalidation(entry) {
|
|
729
|
+
const lastValidated = new Date(entry.lastValidatedAt).getTime();
|
|
730
|
+
return Date.now() - lastValidated > REVALIDATION_THRESHOLD_MS;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// ../../packages/shared/src/storage.ts
|
|
734
|
+
import fs3 from "fs";
|
|
735
|
+
import path5 from "path";
|
|
736
|
+
import { randomUUID as randomUUID2, createHash } from "crypto";
|
|
737
|
+
|
|
738
|
+
// ../../packages/shared/src/registry.ts
|
|
739
|
+
import fs2 from "fs";
|
|
740
|
+
import os2 from "os";
|
|
741
|
+
import path4 from "path";
|
|
742
|
+
var KEEPGOING_DIR = path4.join(os2.homedir(), ".keepgoing");
|
|
743
|
+
var KNOWN_PROJECTS_FILE = path4.join(KEEPGOING_DIR, "known-projects.json");
|
|
744
|
+
var TRAY_CONFIG_FILE = path4.join(KEEPGOING_DIR, "tray-config.json");
|
|
602
745
|
var STALE_PROJECT_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
746
|
+
function readTrayConfigProjects() {
|
|
747
|
+
try {
|
|
748
|
+
if (fs2.existsSync(TRAY_CONFIG_FILE)) {
|
|
749
|
+
const raw = JSON.parse(fs2.readFileSync(TRAY_CONFIG_FILE, "utf-8"));
|
|
750
|
+
if (raw && Array.isArray(raw.projects)) {
|
|
751
|
+
return raw.projects.filter((p) => typeof p === "string");
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
} catch {
|
|
755
|
+
}
|
|
756
|
+
return [];
|
|
757
|
+
}
|
|
603
758
|
function readKnownProjects() {
|
|
604
759
|
try {
|
|
605
|
-
if (
|
|
606
|
-
const raw = JSON.parse(
|
|
760
|
+
if (fs2.existsSync(KNOWN_PROJECTS_FILE)) {
|
|
761
|
+
const raw = JSON.parse(fs2.readFileSync(KNOWN_PROJECTS_FILE, "utf-8"));
|
|
607
762
|
if (raw && Array.isArray(raw.projects)) {
|
|
608
763
|
return raw;
|
|
609
764
|
}
|
|
@@ -613,18 +768,18 @@ function readKnownProjects() {
|
|
|
613
768
|
return { version: 1, projects: [] };
|
|
614
769
|
}
|
|
615
770
|
function writeKnownProjects(data) {
|
|
616
|
-
if (!
|
|
617
|
-
|
|
771
|
+
if (!fs2.existsSync(KEEPGOING_DIR)) {
|
|
772
|
+
fs2.mkdirSync(KEEPGOING_DIR, { recursive: true });
|
|
618
773
|
}
|
|
619
774
|
const tmpFile = KNOWN_PROJECTS_FILE + ".tmp";
|
|
620
|
-
|
|
621
|
-
|
|
775
|
+
fs2.writeFileSync(tmpFile, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
776
|
+
fs2.renameSync(tmpFile, KNOWN_PROJECTS_FILE);
|
|
622
777
|
}
|
|
623
778
|
function registerProject(projectPath, projectName) {
|
|
624
779
|
try {
|
|
625
780
|
const data = readKnownProjects();
|
|
626
781
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
627
|
-
const name = projectName ||
|
|
782
|
+
const name = projectName || path4.basename(projectPath);
|
|
628
783
|
const existingIdx = data.projects.findIndex((p) => p.path === projectPath);
|
|
629
784
|
if (existingIdx >= 0) {
|
|
630
785
|
data.projects[existingIdx].lastSeen = now;
|
|
@@ -663,23 +818,23 @@ var KeepGoingWriter = class {
|
|
|
663
818
|
currentTasksFilePath;
|
|
664
819
|
constructor(workspacePath) {
|
|
665
820
|
const mainRoot = resolveStorageRoot(workspacePath);
|
|
666
|
-
this.storagePath =
|
|
667
|
-
this.sessionsFilePath =
|
|
668
|
-
this.stateFilePath =
|
|
669
|
-
this.metaFilePath =
|
|
670
|
-
this.currentTasksFilePath =
|
|
821
|
+
this.storagePath = path5.join(mainRoot, STORAGE_DIR);
|
|
822
|
+
this.sessionsFilePath = path5.join(this.storagePath, SESSIONS_FILE);
|
|
823
|
+
this.stateFilePath = path5.join(this.storagePath, STATE_FILE);
|
|
824
|
+
this.metaFilePath = path5.join(this.storagePath, META_FILE);
|
|
825
|
+
this.currentTasksFilePath = path5.join(this.storagePath, CURRENT_TASKS_FILE);
|
|
671
826
|
}
|
|
672
827
|
ensureDir() {
|
|
673
|
-
if (!
|
|
674
|
-
|
|
828
|
+
if (!fs3.existsSync(this.storagePath)) {
|
|
829
|
+
fs3.mkdirSync(this.storagePath, { recursive: true });
|
|
675
830
|
}
|
|
676
831
|
}
|
|
677
832
|
saveCheckpoint(checkpoint, projectName) {
|
|
678
833
|
this.ensureDir();
|
|
679
834
|
let sessionsData;
|
|
680
835
|
try {
|
|
681
|
-
if (
|
|
682
|
-
const raw = JSON.parse(
|
|
836
|
+
if (fs3.existsSync(this.sessionsFilePath)) {
|
|
837
|
+
const raw = JSON.parse(fs3.readFileSync(this.sessionsFilePath, "utf-8"));
|
|
683
838
|
if (Array.isArray(raw)) {
|
|
684
839
|
sessionsData = { version: 1, project: projectName, sessions: raw };
|
|
685
840
|
} else {
|
|
@@ -697,13 +852,13 @@ var KeepGoingWriter = class {
|
|
|
697
852
|
if (sessionsData.sessions.length > MAX_SESSIONS) {
|
|
698
853
|
sessionsData.sessions = sessionsData.sessions.slice(-MAX_SESSIONS);
|
|
699
854
|
}
|
|
700
|
-
|
|
855
|
+
fs3.writeFileSync(this.sessionsFilePath, JSON.stringify(sessionsData, null, 2), "utf-8");
|
|
701
856
|
const state = {
|
|
702
857
|
lastSessionId: checkpoint.id,
|
|
703
858
|
lastKnownBranch: checkpoint.gitBranch,
|
|
704
859
|
lastActivityAt: checkpoint.timestamp
|
|
705
860
|
};
|
|
706
|
-
|
|
861
|
+
fs3.writeFileSync(this.stateFilePath, JSON.stringify(state, null, 2), "utf-8");
|
|
707
862
|
this.updateMeta(checkpoint.timestamp);
|
|
708
863
|
const mainRoot = resolveStorageRoot(this.storagePath);
|
|
709
864
|
registerProject(mainRoot, projectName);
|
|
@@ -711,8 +866,8 @@ var KeepGoingWriter = class {
|
|
|
711
866
|
updateMeta(timestamp) {
|
|
712
867
|
let meta;
|
|
713
868
|
try {
|
|
714
|
-
if (
|
|
715
|
-
meta = JSON.parse(
|
|
869
|
+
if (fs3.existsSync(this.metaFilePath)) {
|
|
870
|
+
meta = JSON.parse(fs3.readFileSync(this.metaFilePath, "utf-8"));
|
|
716
871
|
meta.lastUpdated = timestamp;
|
|
717
872
|
} else {
|
|
718
873
|
meta = {
|
|
@@ -728,7 +883,28 @@ var KeepGoingWriter = class {
|
|
|
728
883
|
lastUpdated: timestamp
|
|
729
884
|
};
|
|
730
885
|
}
|
|
731
|
-
|
|
886
|
+
fs3.writeFileSync(this.metaFilePath, JSON.stringify(meta, null, 2), "utf-8");
|
|
887
|
+
}
|
|
888
|
+
// ---------------------------------------------------------------------------
|
|
889
|
+
// Activity signals API
|
|
890
|
+
// ---------------------------------------------------------------------------
|
|
891
|
+
/**
|
|
892
|
+
* Writes activity signals to state.json for momentum computation.
|
|
893
|
+
* Performs a shallow merge: provided fields overwrite existing ones,
|
|
894
|
+
* fields not provided are preserved.
|
|
895
|
+
*/
|
|
896
|
+
writeActivitySignal(signal) {
|
|
897
|
+
this.ensureDir();
|
|
898
|
+
let state = {};
|
|
899
|
+
try {
|
|
900
|
+
if (fs3.existsSync(this.stateFilePath)) {
|
|
901
|
+
state = JSON.parse(fs3.readFileSync(this.stateFilePath, "utf-8"));
|
|
902
|
+
}
|
|
903
|
+
} catch {
|
|
904
|
+
state = {};
|
|
905
|
+
}
|
|
906
|
+
state.activitySignals = { ...state.activitySignals, ...signal };
|
|
907
|
+
fs3.writeFileSync(this.stateFilePath, JSON.stringify(state, null, 2), "utf-8");
|
|
732
908
|
}
|
|
733
909
|
// ---------------------------------------------------------------------------
|
|
734
910
|
// Multi-session API
|
|
@@ -736,8 +912,8 @@ var KeepGoingWriter = class {
|
|
|
736
912
|
/** Read all current tasks from current-tasks.json. Auto-prunes stale sessions. */
|
|
737
913
|
readCurrentTasks() {
|
|
738
914
|
try {
|
|
739
|
-
if (
|
|
740
|
-
const raw = JSON.parse(
|
|
915
|
+
if (fs3.existsSync(this.currentTasksFilePath)) {
|
|
916
|
+
const raw = JSON.parse(fs3.readFileSync(this.currentTasksFilePath, "utf-8"));
|
|
741
917
|
const tasks = Array.isArray(raw) ? raw : raw.tasks ?? [];
|
|
742
918
|
return this.pruneStale(tasks);
|
|
743
919
|
}
|
|
@@ -758,6 +934,8 @@ var KeepGoingWriter = class {
|
|
|
758
934
|
/** Core upsert logic: merges the update into current-tasks.json and returns the pruned task list. */
|
|
759
935
|
upsertSessionCore(update) {
|
|
760
936
|
this.ensureDir();
|
|
937
|
+
if (update.taskSummary) update.taskSummary = stripAgentTags(update.taskSummary);
|
|
938
|
+
if (update.sessionLabel) update.sessionLabel = stripAgentTags(update.sessionLabel);
|
|
761
939
|
const sessionId = update.sessionId || generateSessionId(update);
|
|
762
940
|
const tasks = this.readAllTasksRaw();
|
|
763
941
|
const existingIdx = tasks.findIndex((t) => t.sessionId === sessionId);
|
|
@@ -792,8 +970,8 @@ var KeepGoingWriter = class {
|
|
|
792
970
|
// ---------------------------------------------------------------------------
|
|
793
971
|
readAllTasksRaw() {
|
|
794
972
|
try {
|
|
795
|
-
if (
|
|
796
|
-
const raw = JSON.parse(
|
|
973
|
+
if (fs3.existsSync(this.currentTasksFilePath)) {
|
|
974
|
+
const raw = JSON.parse(fs3.readFileSync(this.currentTasksFilePath, "utf-8"));
|
|
797
975
|
return Array.isArray(raw) ? [...raw] : [...raw.tasks ?? []];
|
|
798
976
|
}
|
|
799
977
|
} catch {
|
|
@@ -805,7 +983,7 @@ var KeepGoingWriter = class {
|
|
|
805
983
|
}
|
|
806
984
|
writeTasksFile(tasks) {
|
|
807
985
|
const data = { version: 1, tasks };
|
|
808
|
-
|
|
986
|
+
fs3.writeFileSync(this.currentTasksFilePath, JSON.stringify(data, null, 2), "utf-8");
|
|
809
987
|
}
|
|
810
988
|
};
|
|
811
989
|
function generateSessionId(context) {
|
|
@@ -821,376 +999,79 @@ function generateSessionId(context) {
|
|
|
821
999
|
return `ses_${hash}`;
|
|
822
1000
|
}
|
|
823
1001
|
|
|
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
|
-
|
|
1002
|
+
// ../../packages/shared/src/reader.ts
|
|
1003
|
+
var STORAGE_DIR2 = ".keepgoing";
|
|
1004
|
+
var META_FILE2 = "meta.json";
|
|
1005
|
+
var SESSIONS_FILE2 = "sessions.json";
|
|
1006
|
+
var DECISIONS_FILE = "decisions.json";
|
|
1007
|
+
var STATE_FILE2 = "state.json";
|
|
1008
|
+
var CURRENT_TASKS_FILE2 = "current-tasks.json";
|
|
1009
|
+
var KeepGoingReader = class {
|
|
1010
|
+
workspacePath;
|
|
1011
|
+
storagePath;
|
|
1012
|
+
metaFilePath;
|
|
1013
|
+
sessionsFilePath;
|
|
1014
|
+
decisionsFilePath;
|
|
1015
|
+
stateFilePath;
|
|
1016
|
+
currentTasksFilePath;
|
|
1017
|
+
_isWorktree;
|
|
1018
|
+
_cachedBranch = null;
|
|
1019
|
+
// null = not yet resolved
|
|
1020
|
+
constructor(workspacePath) {
|
|
1021
|
+
this.workspacePath = workspacePath;
|
|
1022
|
+
const mainRoot = resolveStorageRoot(workspacePath);
|
|
1023
|
+
this._isWorktree = mainRoot !== workspacePath;
|
|
1024
|
+
this.storagePath = path6.join(mainRoot, STORAGE_DIR2);
|
|
1025
|
+
this.metaFilePath = path6.join(this.storagePath, META_FILE2);
|
|
1026
|
+
this.sessionsFilePath = path6.join(this.storagePath, SESSIONS_FILE2);
|
|
1027
|
+
this.decisionsFilePath = path6.join(this.storagePath, DECISIONS_FILE);
|
|
1028
|
+
this.stateFilePath = path6.join(this.storagePath, STATE_FILE2);
|
|
1029
|
+
this.currentTasksFilePath = path6.join(this.storagePath, CURRENT_TASKS_FILE2);
|
|
1030
|
+
}
|
|
1031
|
+
/** Check if .keepgoing/ directory exists. */
|
|
1032
|
+
exists() {
|
|
1033
|
+
return fs4.existsSync(this.storagePath);
|
|
1034
|
+
}
|
|
1035
|
+
/** Read state.json, returns undefined if missing or corrupt. */
|
|
1036
|
+
getState() {
|
|
1037
|
+
return this.readJsonFile(this.stateFilePath);
|
|
1038
|
+
}
|
|
1039
|
+
/** Read meta.json, returns undefined if missing or corrupt. */
|
|
1040
|
+
getMeta() {
|
|
1041
|
+
return this.readJsonFile(this.metaFilePath);
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Read sessions from sessions.json.
|
|
1045
|
+
* Handles both formats:
|
|
1046
|
+
* - Flat array: SessionCheckpoint[] (from ProjectStorage)
|
|
1047
|
+
* - Wrapper object: ProjectSessions (from SessionStorage)
|
|
1048
|
+
*/
|
|
1049
|
+
getSessions() {
|
|
1050
|
+
return this.parseSessions().sessions;
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Get the most recent session checkpoint.
|
|
1054
|
+
* Uses state.lastSessionId if available, falls back to last in array.
|
|
1055
|
+
*/
|
|
1056
|
+
getLastSession() {
|
|
1057
|
+
const { sessions, wrapperLastSessionId } = this.parseSessions();
|
|
1058
|
+
if (sessions.length === 0) {
|
|
1059
|
+
return void 0;
|
|
1060
|
+
}
|
|
1061
|
+
const state = this.getState();
|
|
1062
|
+
if (state?.lastSessionId) {
|
|
1063
|
+
const found = sessions.find((s) => s.id === state.lastSessionId);
|
|
1064
|
+
if (found) {
|
|
1065
|
+
return found;
|
|
858
1066
|
}
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
1067
|
+
}
|
|
1068
|
+
if (wrapperLastSessionId) {
|
|
1069
|
+
const found = sessions.find((s) => s.id === wrapperLastSessionId);
|
|
1070
|
+
if (found) {
|
|
1071
|
+
return found;
|
|
863
1072
|
}
|
|
864
|
-
groups.get("other").push(msg.trim());
|
|
865
1073
|
}
|
|
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];
|
|
1074
|
+
return sessions[sessions.length - 1];
|
|
1194
1075
|
}
|
|
1195
1076
|
/**
|
|
1196
1077
|
* Returns the last N sessions, newest first.
|
|
@@ -1302,106 +1183,492 @@ var KeepGoingReader = class {
|
|
|
1302
1183
|
get isWorktree() {
|
|
1303
1184
|
return this._isWorktree;
|
|
1304
1185
|
}
|
|
1305
|
-
/**
|
|
1306
|
-
* Returns the current git branch for this workspace.
|
|
1307
|
-
* Lazily cached: the branch is resolved once per KeepGoingReader instance.
|
|
1308
|
-
*/
|
|
1309
|
-
getCurrentBranch() {
|
|
1310
|
-
if (this._cachedBranch === null) {
|
|
1311
|
-
this._cachedBranch = getCurrentBranch(this.workspacePath);
|
|
1186
|
+
/**
|
|
1187
|
+
* Returns the current git branch for this workspace.
|
|
1188
|
+
* Lazily cached: the branch is resolved once per KeepGoingReader instance.
|
|
1189
|
+
*/
|
|
1190
|
+
getCurrentBranch() {
|
|
1191
|
+
if (this._cachedBranch === null) {
|
|
1192
|
+
this._cachedBranch = getCurrentBranch(this.workspacePath);
|
|
1193
|
+
}
|
|
1194
|
+
return this._cachedBranch;
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Worktree-aware last session lookup.
|
|
1198
|
+
* In a worktree, scopes to the current branch with fallback to global.
|
|
1199
|
+
* Returns the session and whether it fell back to global.
|
|
1200
|
+
*/
|
|
1201
|
+
getScopedLastSession() {
|
|
1202
|
+
const branch = this.getCurrentBranch();
|
|
1203
|
+
if (this._isWorktree && branch) {
|
|
1204
|
+
const scoped = this.getLastSessionForBranch(branch);
|
|
1205
|
+
if (scoped) return { session: scoped, isFallback: false };
|
|
1206
|
+
return { session: this.getLastSession(), isFallback: true };
|
|
1207
|
+
}
|
|
1208
|
+
return { session: this.getLastSession(), isFallback: false };
|
|
1209
|
+
}
|
|
1210
|
+
/** Worktree-aware recent sessions. Scopes to current branch in a worktree. */
|
|
1211
|
+
getScopedRecentSessions(count) {
|
|
1212
|
+
const branch = this.getCurrentBranch();
|
|
1213
|
+
if (this._isWorktree && branch) {
|
|
1214
|
+
return this.getRecentSessionsForBranch(branch, count);
|
|
1215
|
+
}
|
|
1216
|
+
return this.getRecentSessions(count);
|
|
1217
|
+
}
|
|
1218
|
+
/** Worktree-aware recent decisions. Scopes to current branch in a worktree. */
|
|
1219
|
+
getScopedRecentDecisions(count) {
|
|
1220
|
+
const branch = this.getCurrentBranch();
|
|
1221
|
+
if (this._isWorktree && branch) {
|
|
1222
|
+
return this.getRecentDecisionsForBranch(branch, count);
|
|
1223
|
+
}
|
|
1224
|
+
return this.getRecentDecisions(count);
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Resolves branch scope from an explicit `branch` parameter.
|
|
1228
|
+
* Used by tools that accept a `branch` argument (e.g. get_session_history, get_decisions).
|
|
1229
|
+
* - `"all"` returns no filter.
|
|
1230
|
+
* - An explicit branch name uses that.
|
|
1231
|
+
* - `undefined` auto-scopes to the current branch in a worktree, or all branches otherwise.
|
|
1232
|
+
*/
|
|
1233
|
+
resolveBranchScope(branch) {
|
|
1234
|
+
if (branch === "all") {
|
|
1235
|
+
return { effectiveBranch: void 0, scopeLabel: "all branches" };
|
|
1236
|
+
}
|
|
1237
|
+
if (branch) {
|
|
1238
|
+
return { effectiveBranch: branch, scopeLabel: `branch \`${branch}\`` };
|
|
1239
|
+
}
|
|
1240
|
+
const currentBranch = this.getCurrentBranch();
|
|
1241
|
+
if (this._isWorktree && currentBranch) {
|
|
1242
|
+
return { effectiveBranch: currentBranch, scopeLabel: `branch \`${currentBranch}\` (worktree)` };
|
|
1243
|
+
}
|
|
1244
|
+
return { effectiveBranch: void 0, scopeLabel: "all branches" };
|
|
1245
|
+
}
|
|
1246
|
+
/**
|
|
1247
|
+
* Parses sessions.json once, returning both the session list
|
|
1248
|
+
* and the optional lastSessionId from a ProjectSessions wrapper.
|
|
1249
|
+
*/
|
|
1250
|
+
parseSessions() {
|
|
1251
|
+
const raw = this.readJsonFile(
|
|
1252
|
+
this.sessionsFilePath
|
|
1253
|
+
);
|
|
1254
|
+
if (!raw) {
|
|
1255
|
+
return { sessions: [] };
|
|
1256
|
+
}
|
|
1257
|
+
if (Array.isArray(raw)) {
|
|
1258
|
+
return { sessions: raw };
|
|
1259
|
+
}
|
|
1260
|
+
return { sessions: raw.sessions ?? [], wrapperLastSessionId: raw.lastSessionId };
|
|
1261
|
+
}
|
|
1262
|
+
parseDecisions() {
|
|
1263
|
+
const raw = this.readJsonFile(this.decisionsFilePath);
|
|
1264
|
+
if (!raw) {
|
|
1265
|
+
return { decisions: [] };
|
|
1266
|
+
}
|
|
1267
|
+
return { decisions: raw.decisions ?? [], lastDecisionId: raw.lastDecisionId };
|
|
1268
|
+
}
|
|
1269
|
+
readJsonFile(filePath) {
|
|
1270
|
+
try {
|
|
1271
|
+
if (!fs4.existsSync(filePath)) {
|
|
1272
|
+
return void 0;
|
|
1273
|
+
}
|
|
1274
|
+
const raw = fs4.readFileSync(filePath, "utf-8");
|
|
1275
|
+
return JSON.parse(raw);
|
|
1276
|
+
} catch {
|
|
1277
|
+
return void 0;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
};
|
|
1281
|
+
|
|
1282
|
+
// ../../packages/shared/src/smartSummary.ts
|
|
1283
|
+
var PREFIX_VERBS = {
|
|
1284
|
+
feat: "Added",
|
|
1285
|
+
fix: "Fixed",
|
|
1286
|
+
refactor: "Refactored",
|
|
1287
|
+
docs: "Updated docs for",
|
|
1288
|
+
test: "Added tests for",
|
|
1289
|
+
chore: "Updated",
|
|
1290
|
+
style: "Styled",
|
|
1291
|
+
perf: "Optimized",
|
|
1292
|
+
ci: "Updated CI for",
|
|
1293
|
+
build: "Updated build for",
|
|
1294
|
+
revert: "Reverted"
|
|
1295
|
+
};
|
|
1296
|
+
var NOISE_PATTERNS = [
|
|
1297
|
+
"node_modules",
|
|
1298
|
+
"package-lock.json",
|
|
1299
|
+
"yarn.lock",
|
|
1300
|
+
"pnpm-lock.yaml",
|
|
1301
|
+
".gitignore",
|
|
1302
|
+
".DS_Store",
|
|
1303
|
+
"dist/",
|
|
1304
|
+
"out/",
|
|
1305
|
+
"build/"
|
|
1306
|
+
];
|
|
1307
|
+
function categorizeCommits(messages) {
|
|
1308
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1309
|
+
for (const msg of messages) {
|
|
1310
|
+
const match = msg.match(/^(\w+)(?:\([^)]*\))?[!]?:\s*(.+)/);
|
|
1311
|
+
if (match) {
|
|
1312
|
+
const prefix = match[1].toLowerCase();
|
|
1313
|
+
const body = match[2].trim();
|
|
1314
|
+
if (!groups.has(prefix)) {
|
|
1315
|
+
groups.set(prefix, []);
|
|
1316
|
+
}
|
|
1317
|
+
groups.get(prefix).push(body);
|
|
1318
|
+
} else {
|
|
1319
|
+
if (!groups.has("other")) {
|
|
1320
|
+
groups.set("other", []);
|
|
1321
|
+
}
|
|
1322
|
+
groups.get("other").push(msg.trim());
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
return groups;
|
|
1326
|
+
}
|
|
1327
|
+
function inferWorkAreas(files) {
|
|
1328
|
+
const areas = /* @__PURE__ */ new Map();
|
|
1329
|
+
for (const file of files) {
|
|
1330
|
+
if (NOISE_PATTERNS.some((p) => file.includes(p))) {
|
|
1331
|
+
continue;
|
|
1332
|
+
}
|
|
1333
|
+
const parts = file.split("/").filter(Boolean);
|
|
1334
|
+
let area;
|
|
1335
|
+
if (parts.length >= 2 && (parts[0] === "apps" || parts[0] === "packages")) {
|
|
1336
|
+
area = parts[1];
|
|
1337
|
+
if (parts[0] === "packages" && parts.length >= 4 && parts[2] === "src") {
|
|
1338
|
+
const subFile = parts[3].replace(/\.\w+$/, "");
|
|
1339
|
+
area = `${parts[1]} ${subFile}`;
|
|
1340
|
+
}
|
|
1341
|
+
} else if (parts.length >= 2) {
|
|
1342
|
+
area = parts[0];
|
|
1343
|
+
} else {
|
|
1344
|
+
area = "root";
|
|
1345
|
+
}
|
|
1346
|
+
areas.set(area, (areas.get(area) ?? 0) + 1);
|
|
1347
|
+
}
|
|
1348
|
+
return [...areas.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([name]) => name);
|
|
1349
|
+
}
|
|
1350
|
+
function buildSessionEvents(opts) {
|
|
1351
|
+
const { wsPath, commitHashes, commitMessages, touchedFiles, currentBranch, sessionStartTime, lastActivityTime } = opts;
|
|
1352
|
+
const commits = commitHashes.map((hash, i) => ({
|
|
1353
|
+
hash,
|
|
1354
|
+
message: commitMessages[i] ?? "",
|
|
1355
|
+
filesChanged: getFilesChangedInCommit(wsPath, hash),
|
|
1356
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1357
|
+
}));
|
|
1358
|
+
const committedFiles = new Set(commits.flatMap((c) => c.filesChanged));
|
|
1359
|
+
return {
|
|
1360
|
+
commits,
|
|
1361
|
+
branchSwitches: [],
|
|
1362
|
+
touchedFiles,
|
|
1363
|
+
currentBranch,
|
|
1364
|
+
sessionStartTime,
|
|
1365
|
+
lastActivityTime,
|
|
1366
|
+
// Normalize rename arrows ("old -> new") from git status --porcelain
|
|
1367
|
+
// so they match the plain filenames from git diff-tree --name-only.
|
|
1368
|
+
hasUncommittedChanges: touchedFiles.some((f) => {
|
|
1369
|
+
const normalized = f.includes(" -> ") ? f.split(" -> ").pop() : f;
|
|
1370
|
+
return !committedFiles.has(normalized);
|
|
1371
|
+
})
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
function buildSmartSummary(events) {
|
|
1375
|
+
const { commits, branchSwitches, touchedFiles, hasUncommittedChanges } = events;
|
|
1376
|
+
if (commits.length === 0 && touchedFiles.length === 0 && branchSwitches.length === 0) {
|
|
1377
|
+
return void 0;
|
|
1378
|
+
}
|
|
1379
|
+
const parts = [];
|
|
1380
|
+
if (commits.length > 0) {
|
|
1381
|
+
const messages = commits.map((c) => c.message);
|
|
1382
|
+
const groups = categorizeCommits(messages);
|
|
1383
|
+
const phrases = [];
|
|
1384
|
+
for (const [prefix, bodies] of groups) {
|
|
1385
|
+
const verb = PREFIX_VERBS[prefix] ?? (prefix === "other" ? "" : `${capitalize(prefix)}:`);
|
|
1386
|
+
const items = bodies.slice(0, 2).join(" and ");
|
|
1387
|
+
const overflow = bodies.length > 2 ? ` (+${bodies.length - 2} more)` : "";
|
|
1388
|
+
if (verb) {
|
|
1389
|
+
phrases.push(`${verb} ${items}${overflow}`);
|
|
1390
|
+
} else {
|
|
1391
|
+
phrases.push(`${items}${overflow}`);
|
|
1392
|
+
}
|
|
1312
1393
|
}
|
|
1313
|
-
|
|
1394
|
+
parts.push(phrases.join(", "));
|
|
1395
|
+
} else if (touchedFiles.length > 0) {
|
|
1396
|
+
const areas = inferWorkAreas(touchedFiles);
|
|
1397
|
+
const areaStr = areas.length > 0 ? areas.join(" and ") : `${touchedFiles.length} files`;
|
|
1398
|
+
const suffix = hasUncommittedChanges ? " (uncommitted)" : "";
|
|
1399
|
+
parts.push(`Worked on ${areaStr}${suffix}`);
|
|
1314
1400
|
}
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
const branch = this.getCurrentBranch();
|
|
1322
|
-
if (this._isWorktree && branch) {
|
|
1323
|
-
const scoped = this.getLastSessionForBranch(branch);
|
|
1324
|
-
if (scoped) return { session: scoped, isFallback: false };
|
|
1325
|
-
return { session: this.getLastSession(), isFallback: true };
|
|
1401
|
+
if (branchSwitches.length > 0) {
|
|
1402
|
+
const last = branchSwitches[branchSwitches.length - 1];
|
|
1403
|
+
if (branchSwitches.length === 1) {
|
|
1404
|
+
parts.push(`switched to ${last.toBranch}`);
|
|
1405
|
+
} else {
|
|
1406
|
+
parts.push(`switched branches ${branchSwitches.length} times, ended on ${last.toBranch}`);
|
|
1326
1407
|
}
|
|
1327
|
-
return { session: this.getLastSession(), isFallback: false };
|
|
1328
1408
|
}
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1409
|
+
const result = parts.join("; ");
|
|
1410
|
+
return result || void 0;
|
|
1411
|
+
}
|
|
1412
|
+
function buildSmartNextStep(events) {
|
|
1413
|
+
const { commits, touchedFiles, currentBranch, hasUncommittedChanges } = events;
|
|
1414
|
+
if (hasUncommittedChanges && touchedFiles.length > 0) {
|
|
1415
|
+
const areas = inferWorkAreas(touchedFiles);
|
|
1416
|
+
const areaStr = areas.length > 0 ? areas.join(" and ") : "working tree";
|
|
1417
|
+
return `Review and commit changes in ${areaStr}`;
|
|
1418
|
+
}
|
|
1419
|
+
if (commits.length > 0) {
|
|
1420
|
+
const lastMsg = commits[commits.length - 1].message;
|
|
1421
|
+
const wipMatch = lastMsg.match(/^(?:wip|work in progress|start(?:ed)?|begin|draft)[:\s]+(.+)/i);
|
|
1422
|
+
if (wipMatch) {
|
|
1423
|
+
return `Continue ${wipMatch[1].trim()}`;
|
|
1334
1424
|
}
|
|
1335
|
-
return this.getRecentSessions(count);
|
|
1336
1425
|
}
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
return this.getRecentDecisionsForBranch(branch, count);
|
|
1426
|
+
if (currentBranch && !["main", "master", "develop", "HEAD"].includes(currentBranch)) {
|
|
1427
|
+
const branchName = currentBranch.replace(/^(feat|feature|fix|bugfix|hotfix|chore|refactor)[/-]/i, "").replace(/[-_]/g, " ").trim();
|
|
1428
|
+
if (branchName) {
|
|
1429
|
+
return `Continue ${branchName}`;
|
|
1342
1430
|
}
|
|
1343
|
-
return this.getRecentDecisions(count);
|
|
1344
1431
|
}
|
|
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" };
|
|
1432
|
+
if (touchedFiles.length > 0) {
|
|
1433
|
+
const areas = inferWorkAreas(touchedFiles);
|
|
1434
|
+
if (areas.length > 0) {
|
|
1435
|
+
return `Review recent changes in ${areas.join(" and ")}`;
|
|
1355
1436
|
}
|
|
1356
|
-
|
|
1357
|
-
|
|
1437
|
+
}
|
|
1438
|
+
return "";
|
|
1439
|
+
}
|
|
1440
|
+
function capitalize(s) {
|
|
1441
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
// ../../packages/shared/src/contextSnapshot.ts
|
|
1445
|
+
import fs5 from "fs";
|
|
1446
|
+
import path7 from "path";
|
|
1447
|
+
function formatCompactRelativeTime(date) {
|
|
1448
|
+
const diffMs = Date.now() - date.getTime();
|
|
1449
|
+
if (isNaN(diffMs)) return "?";
|
|
1450
|
+
if (diffMs < 0) return "now";
|
|
1451
|
+
const seconds = Math.floor(diffMs / 1e3);
|
|
1452
|
+
const minutes = Math.floor(seconds / 60);
|
|
1453
|
+
const hours = Math.floor(minutes / 60);
|
|
1454
|
+
const days = Math.floor(hours / 24);
|
|
1455
|
+
const weeks = Math.floor(days / 7);
|
|
1456
|
+
if (seconds < 60) return "now";
|
|
1457
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
1458
|
+
if (hours < 24) return `${hours}h ago`;
|
|
1459
|
+
if (days < 7) return `${days}d ago`;
|
|
1460
|
+
return `${weeks}w ago`;
|
|
1461
|
+
}
|
|
1462
|
+
function computeMomentum(timestamp, signals) {
|
|
1463
|
+
if (signals) {
|
|
1464
|
+
if (signals.lastGitOpAt) {
|
|
1465
|
+
const gitOpDiffMs = Date.now() - new Date(signals.lastGitOpAt).getTime();
|
|
1466
|
+
if (!isNaN(gitOpDiffMs) && gitOpDiffMs >= 0 && gitOpDiffMs < 30 * 60 * 1e3) {
|
|
1467
|
+
return "hot";
|
|
1468
|
+
}
|
|
1358
1469
|
}
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
return { effectiveBranch: currentBranch, scopeLabel: `branch \`${currentBranch}\` (worktree)` };
|
|
1470
|
+
if (signals.recentCommitCount !== void 0 && signals.recentCommitCount >= 2) {
|
|
1471
|
+
return "hot";
|
|
1362
1472
|
}
|
|
1363
|
-
return { effectiveBranch: void 0, scopeLabel: "all branches" };
|
|
1364
1473
|
}
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1474
|
+
const diffMs = Date.now() - new Date(timestamp).getTime();
|
|
1475
|
+
if (isNaN(diffMs) || diffMs < 0) return "hot";
|
|
1476
|
+
const minutes = diffMs / (1e3 * 60);
|
|
1477
|
+
if (minutes < 30) return "hot";
|
|
1478
|
+
if (minutes < 240) return "warm";
|
|
1479
|
+
return "cold";
|
|
1480
|
+
}
|
|
1481
|
+
function inferFocusFromBranch2(branch) {
|
|
1482
|
+
if (!branch || branch === "main" || branch === "master" || branch === "develop" || branch === "HEAD") {
|
|
1483
|
+
return void 0;
|
|
1484
|
+
}
|
|
1485
|
+
const prefixPattern = /^(?:feature|feat|fix|bugfix|hotfix|chore|refactor|docs|test|ci)\//i;
|
|
1486
|
+
const isFix = /^(?:fix|bugfix|hotfix)\//i.test(branch);
|
|
1487
|
+
const stripped = branch.replace(prefixPattern, "");
|
|
1488
|
+
const cleaned = stripped.replace(/[-_/]/g, " ").replace(/^\d+\s*/, "").trim();
|
|
1489
|
+
if (!cleaned) return void 0;
|
|
1490
|
+
return isFix ? `${cleaned} fix` : cleaned;
|
|
1491
|
+
}
|
|
1492
|
+
function cleanCommitMessage(message, maxLen = 60) {
|
|
1493
|
+
if (!message) return "";
|
|
1494
|
+
const match = message.match(/^(?:\w+)(?:\([^)]*\))?[!]?:\s*(.+)/);
|
|
1495
|
+
const body = match ? match[1].trim() : message.trim();
|
|
1496
|
+
if (!body) return "";
|
|
1497
|
+
const capitalized = body.charAt(0).toUpperCase() + body.slice(1);
|
|
1498
|
+
if (capitalized.length <= maxLen) return capitalized;
|
|
1499
|
+
return capitalized.slice(0, maxLen - 3) + "...";
|
|
1500
|
+
}
|
|
1501
|
+
function buildDoing(checkpoint, branch, recentCommitMessages) {
|
|
1502
|
+
if (checkpoint?.summary) return checkpoint.summary;
|
|
1503
|
+
const branchFocus = inferFocusFromBranch2(branch ?? checkpoint?.gitBranch);
|
|
1504
|
+
if (branchFocus) return branchFocus;
|
|
1505
|
+
if (recentCommitMessages && recentCommitMessages.length > 0) {
|
|
1506
|
+
const cleaned = cleanCommitMessage(recentCommitMessages[0]);
|
|
1507
|
+
if (cleaned) return cleaned;
|
|
1508
|
+
}
|
|
1509
|
+
return "Unknown";
|
|
1510
|
+
}
|
|
1511
|
+
function buildWhere(checkpoint, branch) {
|
|
1512
|
+
const effectiveBranch = branch ?? checkpoint?.gitBranch;
|
|
1513
|
+
const parts = [];
|
|
1514
|
+
if (effectiveBranch) {
|
|
1515
|
+
parts.push(effectiveBranch);
|
|
1516
|
+
}
|
|
1517
|
+
if (checkpoint?.touchedFiles && checkpoint.touchedFiles.length > 0) {
|
|
1518
|
+
const fileNames = checkpoint.touchedFiles.slice(0, 2).map((f) => {
|
|
1519
|
+
const segments = f.replace(/\\/g, "/").split("/");
|
|
1520
|
+
return segments[segments.length - 1];
|
|
1521
|
+
});
|
|
1522
|
+
parts.push(fileNames.join(", "));
|
|
1523
|
+
}
|
|
1524
|
+
return parts.join(" \xB7 ") || "unknown";
|
|
1525
|
+
}
|
|
1526
|
+
function detectFocusArea(files) {
|
|
1527
|
+
if (!files || files.length === 0) return void 0;
|
|
1528
|
+
const areas = inferWorkAreas(files);
|
|
1529
|
+
if (areas.length === 0) return void 0;
|
|
1530
|
+
if (areas.length === 1) return areas[0];
|
|
1531
|
+
const topArea = areas[0];
|
|
1532
|
+
const areaCounts = /* @__PURE__ */ new Map();
|
|
1533
|
+
for (const file of files) {
|
|
1534
|
+
const parts = file.split("/").filter(Boolean);
|
|
1535
|
+
let area;
|
|
1536
|
+
if (parts.length <= 1) {
|
|
1537
|
+
area = "root";
|
|
1538
|
+
} else if (parts[0] === "apps" || parts[0] === "packages") {
|
|
1539
|
+
area = parts.length > 1 ? parts[1] : parts[0];
|
|
1540
|
+
} else if (parts[0] === "src") {
|
|
1541
|
+
area = parts.length > 1 ? parts[1] : "src";
|
|
1542
|
+
} else {
|
|
1543
|
+
area = parts[0];
|
|
1378
1544
|
}
|
|
1379
|
-
|
|
1545
|
+
areaCounts.set(area, (areaCounts.get(area) ?? 0) + 1);
|
|
1380
1546
|
}
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
if (
|
|
1384
|
-
|
|
1547
|
+
let topCount = 0;
|
|
1548
|
+
for (const [areaKey, count] of areaCounts) {
|
|
1549
|
+
if (topArea.toLowerCase().includes(areaKey.toLowerCase()) || areaKey.toLowerCase().includes(topArea.toLowerCase().split(" ")[0])) {
|
|
1550
|
+
topCount = Math.max(topCount, count);
|
|
1385
1551
|
}
|
|
1386
|
-
return { decisions: raw.decisions ?? [], lastDecisionId: raw.lastDecisionId };
|
|
1387
1552
|
}
|
|
1388
|
-
|
|
1553
|
+
if (topCount === 0) {
|
|
1554
|
+
topCount = Math.max(...areaCounts.values());
|
|
1555
|
+
}
|
|
1556
|
+
const ratio = topCount / files.length;
|
|
1557
|
+
if (ratio >= 0.6) return topArea;
|
|
1558
|
+
return void 0;
|
|
1559
|
+
}
|
|
1560
|
+
function generateContextSnapshot(projectRoot) {
|
|
1561
|
+
const reader = new KeepGoingReader(projectRoot);
|
|
1562
|
+
if (!reader.exists()) return null;
|
|
1563
|
+
const lastSession = reader.getLastSession();
|
|
1564
|
+
const state = reader.getState();
|
|
1565
|
+
if (!lastSession && !state) return null;
|
|
1566
|
+
const timestamp = state?.lastActivityAt ?? lastSession?.timestamp;
|
|
1567
|
+
if (!timestamp) return null;
|
|
1568
|
+
const branch = state?.lastKnownBranch ?? lastSession?.gitBranch;
|
|
1569
|
+
let activeAgents = 0;
|
|
1570
|
+
try {
|
|
1571
|
+
const activeTasks = reader.getActiveTasks();
|
|
1572
|
+
activeAgents = activeTasks.length;
|
|
1573
|
+
} catch {
|
|
1574
|
+
}
|
|
1575
|
+
const doing = buildDoing(lastSession, branch);
|
|
1576
|
+
const next = lastSession?.nextStep ?? "";
|
|
1577
|
+
const where = buildWhere(lastSession, branch);
|
|
1578
|
+
const when = formatCompactRelativeTime(new Date(timestamp));
|
|
1579
|
+
const momentum = computeMomentum(timestamp, state?.activitySignals);
|
|
1580
|
+
const blocker = lastSession?.blocker;
|
|
1581
|
+
const snapshot = {
|
|
1582
|
+
doing,
|
|
1583
|
+
next,
|
|
1584
|
+
where,
|
|
1585
|
+
when,
|
|
1586
|
+
momentum,
|
|
1587
|
+
lastActivityAt: timestamp
|
|
1588
|
+
};
|
|
1589
|
+
if (blocker) snapshot.blocker = blocker;
|
|
1590
|
+
if (activeAgents > 0) snapshot.activeAgents = activeAgents;
|
|
1591
|
+
const focusArea = detectFocusArea(lastSession?.touchedFiles ?? []);
|
|
1592
|
+
if (focusArea) snapshot.focusArea = focusArea;
|
|
1593
|
+
return snapshot;
|
|
1594
|
+
}
|
|
1595
|
+
function formatCrossProjectLine(entry) {
|
|
1596
|
+
const s = entry.snapshot;
|
|
1597
|
+
const icon = s.momentum === "hot" ? "\u26A1" : s.momentum === "warm" ? "\u{1F536}" : "\u{1F4A4}";
|
|
1598
|
+
const parts = [];
|
|
1599
|
+
parts.push(`${icon} ${entry.name}: ${s.doing}`);
|
|
1600
|
+
if (s.next) {
|
|
1601
|
+
parts.push(`\u2192 ${s.next}`);
|
|
1602
|
+
}
|
|
1603
|
+
let line = parts.join(" ");
|
|
1604
|
+
line += ` (${s.when})`;
|
|
1605
|
+
if (s.blocker) {
|
|
1606
|
+
line += ` \u26D4 ${s.blocker}`;
|
|
1607
|
+
}
|
|
1608
|
+
return line;
|
|
1609
|
+
}
|
|
1610
|
+
var MOMENTUM_RANK = { hot: 0, warm: 1, cold: 2 };
|
|
1611
|
+
function generateCrossProjectSummary() {
|
|
1612
|
+
const registry = readKnownProjects();
|
|
1613
|
+
const trayPaths = readTrayConfigProjects();
|
|
1614
|
+
const seenPaths = new Set(registry.projects.map((p) => p.path));
|
|
1615
|
+
const allEntries = [...registry.projects];
|
|
1616
|
+
for (const tp of trayPaths) {
|
|
1617
|
+
if (!seenPaths.has(tp)) {
|
|
1618
|
+
seenPaths.add(tp);
|
|
1619
|
+
allEntries.push({ path: tp, name: path7.basename(tp) });
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
const projects = [];
|
|
1623
|
+
const seenRoots = /* @__PURE__ */ new Set();
|
|
1624
|
+
for (const entry of allEntries) {
|
|
1625
|
+
if (!fs5.existsSync(entry.path)) continue;
|
|
1626
|
+
const snapshot = generateContextSnapshot(entry.path);
|
|
1627
|
+
if (!snapshot) continue;
|
|
1628
|
+
let resolvedRoot;
|
|
1389
1629
|
try {
|
|
1390
|
-
|
|
1391
|
-
return void 0;
|
|
1392
|
-
}
|
|
1393
|
-
const raw = fs5.readFileSync(filePath, "utf-8");
|
|
1394
|
-
return JSON.parse(raw);
|
|
1630
|
+
resolvedRoot = fs5.realpathSync(entry.path);
|
|
1395
1631
|
} catch {
|
|
1396
|
-
|
|
1397
|
-
}
|
|
1632
|
+
resolvedRoot = entry.path;
|
|
1633
|
+
}
|
|
1634
|
+
if (seenRoots.has(resolvedRoot)) continue;
|
|
1635
|
+
seenRoots.add(resolvedRoot);
|
|
1636
|
+
projects.push({
|
|
1637
|
+
name: entry.name,
|
|
1638
|
+
path: entry.path,
|
|
1639
|
+
snapshot
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
projects.sort((a, b) => {
|
|
1643
|
+
const rankA = MOMENTUM_RANK[a.snapshot.momentum ?? "cold"];
|
|
1644
|
+
const rankB = MOMENTUM_RANK[b.snapshot.momentum ?? "cold"];
|
|
1645
|
+
if (rankA !== rankB) return rankA - rankB;
|
|
1646
|
+
const timeA = a.snapshot.lastActivityAt ? new Date(a.snapshot.lastActivityAt).getTime() : 0;
|
|
1647
|
+
const timeB = b.snapshot.lastActivityAt ? new Date(b.snapshot.lastActivityAt).getTime() : 0;
|
|
1648
|
+
return timeB - timeA;
|
|
1649
|
+
});
|
|
1650
|
+
return {
|
|
1651
|
+
projects,
|
|
1652
|
+
generated: (/* @__PURE__ */ new Date()).toISOString()
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
// ../../packages/shared/src/decisionStorage.ts
|
|
1657
|
+
import fs6 from "fs";
|
|
1658
|
+
import path8 from "path";
|
|
1659
|
+
|
|
1660
|
+
// ../../packages/shared/src/featureGate.ts
|
|
1661
|
+
var DefaultFeatureGate = class {
|
|
1662
|
+
isEnabled(_feature) {
|
|
1663
|
+
return true;
|
|
1398
1664
|
}
|
|
1399
1665
|
};
|
|
1666
|
+
var currentGate = new DefaultFeatureGate();
|
|
1400
1667
|
|
|
1401
1668
|
// ../../packages/shared/src/setup.ts
|
|
1402
|
-
import
|
|
1669
|
+
import fs7 from "fs";
|
|
1403
1670
|
import os3 from "os";
|
|
1404
|
-
import
|
|
1671
|
+
import path9 from "path";
|
|
1405
1672
|
var KEEPGOING_MARKER = "@keepgoingdev/mcp-server";
|
|
1406
1673
|
var SESSION_START_HOOK = {
|
|
1407
1674
|
matcher: "",
|
|
@@ -1430,6 +1697,15 @@ var POST_TOOL_USE_HOOK = {
|
|
|
1430
1697
|
}
|
|
1431
1698
|
]
|
|
1432
1699
|
};
|
|
1700
|
+
var PLAN_MODE_HOOK = {
|
|
1701
|
+
matcher: "Read|Grep|Glob|Bash|WebSearch",
|
|
1702
|
+
hooks: [
|
|
1703
|
+
{
|
|
1704
|
+
type: "command",
|
|
1705
|
+
command: "npx -y @keepgoingdev/mcp-server --heartbeat"
|
|
1706
|
+
}
|
|
1707
|
+
]
|
|
1708
|
+
};
|
|
1433
1709
|
var SESSION_END_HOOK = {
|
|
1434
1710
|
matcher: "",
|
|
1435
1711
|
hooks: [
|
|
@@ -1439,7 +1715,7 @@ var SESSION_END_HOOK = {
|
|
|
1439
1715
|
}
|
|
1440
1716
|
]
|
|
1441
1717
|
};
|
|
1442
|
-
var KEEPGOING_RULES_VERSION =
|
|
1718
|
+
var KEEPGOING_RULES_VERSION = 3;
|
|
1443
1719
|
var KEEPGOING_RULES_CONTENT = `<!-- @keepgoingdev/mcp-server v${KEEPGOING_RULES_VERSION} -->
|
|
1444
1720
|
## KeepGoing
|
|
1445
1721
|
|
|
@@ -1449,6 +1725,8 @@ After completing a task or meaningful piece of work, call the \`save_checkpoint\
|
|
|
1449
1725
|
- \`summary\`: 1-2 sentences. What changed and why, no file paths, no implementation details (those are captured from git).
|
|
1450
1726
|
- \`nextStep\`: What to do next
|
|
1451
1727
|
- \`blocker\`: Any blocker (if applicable)
|
|
1728
|
+
|
|
1729
|
+
When working in plan mode (investigating, designing, iterating on an approach before any edits), call \`save_checkpoint\` when you reach a significant milestone or conclusion. Use the summary to capture what was investigated and decided. This preserves planning context for future sessions.
|
|
1452
1730
|
`;
|
|
1453
1731
|
function getRulesFileVersion(content) {
|
|
1454
1732
|
const match = content.match(/<!-- @keepgoingdev\/mcp-server v(\d+) -->/);
|
|
@@ -1456,7 +1734,7 @@ function getRulesFileVersion(content) {
|
|
|
1456
1734
|
}
|
|
1457
1735
|
var STATUSLINE_CMD = "npx -y @keepgoingdev/mcp-server --statusline";
|
|
1458
1736
|
function detectClaudeDir() {
|
|
1459
|
-
return process.env.CLAUDE_CONFIG_DIR ||
|
|
1737
|
+
return process.env.CLAUDE_CONFIG_DIR || path9.join(os3.homedir(), ".claude");
|
|
1460
1738
|
}
|
|
1461
1739
|
function hasKeepGoingHook(hookEntries) {
|
|
1462
1740
|
return hookEntries.some(
|
|
@@ -1468,19 +1746,19 @@ function resolveScopePaths(scope, workspacePath, overrideClaudeDir) {
|
|
|
1468
1746
|
const claudeDir2 = overrideClaudeDir || detectClaudeDir();
|
|
1469
1747
|
return {
|
|
1470
1748
|
claudeDir: claudeDir2,
|
|
1471
|
-
settingsPath:
|
|
1472
|
-
claudeMdPath:
|
|
1473
|
-
rulesPath:
|
|
1749
|
+
settingsPath: path9.join(claudeDir2, "settings.json"),
|
|
1750
|
+
claudeMdPath: path9.join(claudeDir2, "CLAUDE.md"),
|
|
1751
|
+
rulesPath: path9.join(claudeDir2, "rules", "keepgoing.md")
|
|
1474
1752
|
};
|
|
1475
1753
|
}
|
|
1476
|
-
const claudeDir =
|
|
1477
|
-
const dotClaudeMdPath =
|
|
1478
|
-
const rootClaudeMdPath =
|
|
1754
|
+
const claudeDir = path9.join(workspacePath, ".claude");
|
|
1755
|
+
const dotClaudeMdPath = path9.join(workspacePath, ".claude", "CLAUDE.md");
|
|
1756
|
+
const rootClaudeMdPath = path9.join(workspacePath, "CLAUDE.md");
|
|
1479
1757
|
return {
|
|
1480
1758
|
claudeDir,
|
|
1481
|
-
settingsPath:
|
|
1482
|
-
claudeMdPath:
|
|
1483
|
-
rulesPath:
|
|
1759
|
+
settingsPath: path9.join(claudeDir, "settings.json"),
|
|
1760
|
+
claudeMdPath: fs7.existsSync(dotClaudeMdPath) ? dotClaudeMdPath : rootClaudeMdPath,
|
|
1761
|
+
rulesPath: path9.join(workspacePath, ".claude", "rules", "keepgoing.md")
|
|
1484
1762
|
};
|
|
1485
1763
|
}
|
|
1486
1764
|
function writeHooksToSettings(settings) {
|
|
@@ -1509,6 +1787,13 @@ function writeHooksToSettings(settings) {
|
|
|
1509
1787
|
settings.hooks.PostToolUse.push(POST_TOOL_USE_HOOK);
|
|
1510
1788
|
changed = true;
|
|
1511
1789
|
}
|
|
1790
|
+
const hasHeartbeat = settings.hooks.PostToolUse.some(
|
|
1791
|
+
(entry) => entry?.hooks?.some((h) => typeof h?.command === "string" && h.command.includes("--heartbeat"))
|
|
1792
|
+
);
|
|
1793
|
+
if (!hasHeartbeat) {
|
|
1794
|
+
settings.hooks.PostToolUse.push(PLAN_MODE_HOOK);
|
|
1795
|
+
changed = true;
|
|
1796
|
+
}
|
|
1512
1797
|
if (!Array.isArray(settings.hooks.SessionEnd)) {
|
|
1513
1798
|
settings.hooks.SessionEnd = [];
|
|
1514
1799
|
}
|
|
@@ -1520,11 +1805,11 @@ function writeHooksToSettings(settings) {
|
|
|
1520
1805
|
}
|
|
1521
1806
|
function checkHookConflict(scope, workspacePath) {
|
|
1522
1807
|
const otherPaths = resolveScopePaths(scope === "user" ? "project" : "user", workspacePath);
|
|
1523
|
-
if (!
|
|
1808
|
+
if (!fs7.existsSync(otherPaths.settingsPath)) {
|
|
1524
1809
|
return null;
|
|
1525
1810
|
}
|
|
1526
1811
|
try {
|
|
1527
|
-
const otherSettings = JSON.parse(
|
|
1812
|
+
const otherSettings = JSON.parse(fs7.readFileSync(otherPaths.settingsPath, "utf-8"));
|
|
1528
1813
|
const hooks = otherSettings?.hooks;
|
|
1529
1814
|
if (!hooks) return null;
|
|
1530
1815
|
const hasConflict = Array.isArray(hooks.SessionStart) && hasKeepGoingHook(hooks.SessionStart) || Array.isArray(hooks.Stop) && hasKeepGoingHook(hooks.Stop);
|
|
@@ -1553,10 +1838,10 @@ function setupProject(options) {
|
|
|
1553
1838
|
workspacePath,
|
|
1554
1839
|
claudeDirOverride
|
|
1555
1840
|
);
|
|
1556
|
-
const scopeLabel = scope === "user" ?
|
|
1841
|
+
const scopeLabel = scope === "user" ? path9.join("~", path9.relative(os3.homedir(), claudeDir), "settings.json").replace(/\\/g, "/") : ".claude/settings.json";
|
|
1557
1842
|
let settings = {};
|
|
1558
|
-
if (
|
|
1559
|
-
settings = JSON.parse(
|
|
1843
|
+
if (fs7.existsSync(settingsPath)) {
|
|
1844
|
+
settings = JSON.parse(fs7.readFileSync(settingsPath, "utf-8"));
|
|
1560
1845
|
}
|
|
1561
1846
|
let settingsChanged = false;
|
|
1562
1847
|
if (sessionHooks) {
|
|
@@ -1587,36 +1872,36 @@ function setupProject(options) {
|
|
|
1587
1872
|
statusline?.cleanup?.();
|
|
1588
1873
|
}
|
|
1589
1874
|
if (settingsChanged) {
|
|
1590
|
-
if (!
|
|
1591
|
-
|
|
1875
|
+
if (!fs7.existsSync(claudeDir)) {
|
|
1876
|
+
fs7.mkdirSync(claudeDir, { recursive: true });
|
|
1592
1877
|
}
|
|
1593
|
-
|
|
1878
|
+
fs7.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
1594
1879
|
changed = true;
|
|
1595
1880
|
}
|
|
1596
1881
|
if (claudeMd) {
|
|
1597
|
-
const rulesDir =
|
|
1598
|
-
const rulesLabel = scope === "user" ?
|
|
1599
|
-
if (
|
|
1600
|
-
const existing =
|
|
1882
|
+
const rulesDir = path9.dirname(rulesPath);
|
|
1883
|
+
const rulesLabel = scope === "user" ? path9.join(path9.relative(os3.homedir(), path9.dirname(rulesPath)), "keepgoing.md").replace(/\\/g, "/") : ".claude/rules/keepgoing.md";
|
|
1884
|
+
if (fs7.existsSync(rulesPath)) {
|
|
1885
|
+
const existing = fs7.readFileSync(rulesPath, "utf-8");
|
|
1601
1886
|
const existingVersion = getRulesFileVersion(existing);
|
|
1602
1887
|
if (existingVersion === null) {
|
|
1603
1888
|
messages.push(`Rules file: Custom file found at ${rulesLabel}, skipping`);
|
|
1604
1889
|
} else if (existingVersion >= KEEPGOING_RULES_VERSION) {
|
|
1605
1890
|
messages.push(`Rules file: Already up to date (v${existingVersion}), skipped`);
|
|
1606
1891
|
} else {
|
|
1607
|
-
if (!
|
|
1608
|
-
|
|
1892
|
+
if (!fs7.existsSync(rulesDir)) {
|
|
1893
|
+
fs7.mkdirSync(rulesDir, { recursive: true });
|
|
1609
1894
|
}
|
|
1610
|
-
|
|
1895
|
+
fs7.writeFileSync(rulesPath, KEEPGOING_RULES_CONTENT);
|
|
1611
1896
|
changed = true;
|
|
1612
1897
|
messages.push(`Rules file: Updated v${existingVersion} \u2192 v${KEEPGOING_RULES_VERSION} at ${rulesLabel}`);
|
|
1613
1898
|
}
|
|
1614
1899
|
} else {
|
|
1615
|
-
const existingClaudeMd =
|
|
1616
|
-
if (!
|
|
1617
|
-
|
|
1900
|
+
const existingClaudeMd = fs7.existsSync(claudeMdPath) ? fs7.readFileSync(claudeMdPath, "utf-8") : "";
|
|
1901
|
+
if (!fs7.existsSync(rulesDir)) {
|
|
1902
|
+
fs7.mkdirSync(rulesDir, { recursive: true });
|
|
1618
1903
|
}
|
|
1619
|
-
|
|
1904
|
+
fs7.writeFileSync(rulesPath, KEEPGOING_RULES_CONTENT);
|
|
1620
1905
|
changed = true;
|
|
1621
1906
|
if (existingClaudeMd.includes("## KeepGoing")) {
|
|
1622
1907
|
const mdLabel = scope === "user" ? "~/.claude/CLAUDE.md" : "CLAUDE.md";
|
|
@@ -2112,14 +2397,14 @@ function renderEnrichedBriefingQuiet(briefing) {
|
|
|
2112
2397
|
// src/updateCheck.ts
|
|
2113
2398
|
import { spawn } from "child_process";
|
|
2114
2399
|
import { readFileSync, existsSync } from "fs";
|
|
2115
|
-
import
|
|
2400
|
+
import path10 from "path";
|
|
2116
2401
|
import os4 from "os";
|
|
2117
|
-
var CLI_VERSION = "1.
|
|
2402
|
+
var CLI_VERSION = "1.3.1";
|
|
2118
2403
|
var NPM_REGISTRY_URL = "https://registry.npmjs.org/@keepgoingdev/cli/latest";
|
|
2119
2404
|
var FETCH_TIMEOUT_MS = 5e3;
|
|
2120
2405
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
2121
|
-
var CACHE_DIR =
|
|
2122
|
-
var CACHE_PATH =
|
|
2406
|
+
var CACHE_DIR = path10.join(os4.homedir(), ".keepgoing");
|
|
2407
|
+
var CACHE_PATH = path10.join(CACHE_DIR, "update-check.json");
|
|
2123
2408
|
function isNewerVersion(current, latest) {
|
|
2124
2409
|
const cur = current.split(".").map(Number);
|
|
2125
2410
|
const lat = latest.split(".").map(Number);
|
|
@@ -2234,7 +2519,7 @@ async function statusCommand(opts) {
|
|
|
2234
2519
|
}
|
|
2235
2520
|
|
|
2236
2521
|
// src/commands/save.ts
|
|
2237
|
-
import
|
|
2522
|
+
import path11 from "path";
|
|
2238
2523
|
async function saveCommand(opts) {
|
|
2239
2524
|
const { cwd, message, nextStepOverride, json, quiet, force } = opts;
|
|
2240
2525
|
const isManual = !!message;
|
|
@@ -2263,9 +2548,9 @@ async function saveCommand(opts) {
|
|
|
2263
2548
|
sessionStartTime: lastSession?.timestamp ?? now,
|
|
2264
2549
|
lastActivityTime: now
|
|
2265
2550
|
});
|
|
2266
|
-
const summary = message ?? buildSmartSummary(events) ?? `Worked on ${touchedFiles.slice(0, 5).map((f) =>
|
|
2551
|
+
const summary = message ?? buildSmartSummary(events) ?? `Worked on ${touchedFiles.slice(0, 5).map((f) => path11.basename(f)).join(", ")}`;
|
|
2267
2552
|
const nextStep = nextStepOverride ?? buildSmartNextStep(events);
|
|
2268
|
-
const projectName =
|
|
2553
|
+
const projectName = path11.basename(resolveStorageRoot(cwd));
|
|
2269
2554
|
const sessionId = generateSessionId({
|
|
2270
2555
|
workspaceRoot: cwd,
|
|
2271
2556
|
branch: gitBranch ?? void 0,
|
|
@@ -2298,17 +2583,17 @@ async function saveCommand(opts) {
|
|
|
2298
2583
|
}
|
|
2299
2584
|
|
|
2300
2585
|
// src/commands/hook.ts
|
|
2301
|
-
import
|
|
2302
|
-
import
|
|
2586
|
+
import fs8 from "fs";
|
|
2587
|
+
import path12 from "path";
|
|
2303
2588
|
import os5 from "os";
|
|
2304
2589
|
import { execSync } from "child_process";
|
|
2305
2590
|
var HOOK_MARKER_START = "# keepgoing-hook-start";
|
|
2306
2591
|
var HOOK_MARKER_END = "# keepgoing-hook-end";
|
|
2307
2592
|
var POST_COMMIT_MARKER_START = "# keepgoing-post-commit-start";
|
|
2308
2593
|
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 =
|
|
2594
|
+
var KEEPGOING_HOOKS_DIR = path12.join(os5.homedir(), ".keepgoing", "hooks");
|
|
2595
|
+
var POST_COMMIT_HOOK_PATH = path12.join(KEEPGOING_HOOKS_DIR, "post-commit");
|
|
2596
|
+
var KEEPGOING_MANAGED_MARKER = path12.join(KEEPGOING_HOOKS_DIR, ".keepgoing-managed");
|
|
2312
2597
|
var POST_COMMIT_HOOK = `#!/bin/sh
|
|
2313
2598
|
${POST_COMMIT_MARKER_START}
|
|
2314
2599
|
# Runs after every git commit. Detects high-signal decisions.
|
|
@@ -2322,7 +2607,7 @@ var ZSH_HOOK = `${HOOK_MARKER_START}
|
|
|
2322
2607
|
if command -v keepgoing >/dev/null 2>&1; then
|
|
2323
2608
|
function chpwd() {
|
|
2324
2609
|
if [ -d ".keepgoing" ]; then
|
|
2325
|
-
keepgoing
|
|
2610
|
+
keepgoing glance
|
|
2326
2611
|
fi
|
|
2327
2612
|
}
|
|
2328
2613
|
fi
|
|
@@ -2333,7 +2618,7 @@ if command -v keepgoing >/dev/null 2>&1; then
|
|
|
2333
2618
|
function cd() {
|
|
2334
2619
|
builtin cd "$@" || return
|
|
2335
2620
|
if [ -d ".keepgoing" ]; then
|
|
2336
|
-
keepgoing
|
|
2621
|
+
keepgoing glance
|
|
2337
2622
|
fi
|
|
2338
2623
|
}
|
|
2339
2624
|
fi
|
|
@@ -2343,7 +2628,7 @@ var FISH_HOOK = `${HOOK_MARKER_START}
|
|
|
2343
2628
|
if command -v keepgoing >/dev/null 2>&1
|
|
2344
2629
|
function __keepgoing_on_pwd_change --on-variable PWD
|
|
2345
2630
|
if test -d .keepgoing
|
|
2346
|
-
keepgoing
|
|
2631
|
+
keepgoing glance
|
|
2347
2632
|
end
|
|
2348
2633
|
end
|
|
2349
2634
|
end
|
|
@@ -2372,14 +2657,14 @@ function detectShellRcFile(shellOverride) {
|
|
|
2372
2657
|
}
|
|
2373
2658
|
}
|
|
2374
2659
|
if (shell === "zsh") {
|
|
2375
|
-
return { shell: "zsh", rcFile:
|
|
2660
|
+
return { shell: "zsh", rcFile: path12.join(home, ".zshrc") };
|
|
2376
2661
|
}
|
|
2377
2662
|
if (shell === "bash") {
|
|
2378
|
-
return { shell: "bash", rcFile:
|
|
2663
|
+
return { shell: "bash", rcFile: path12.join(home, ".bashrc") };
|
|
2379
2664
|
}
|
|
2380
2665
|
if (shell === "fish") {
|
|
2381
|
-
const xdgConfig = process.env["XDG_CONFIG_HOME"] ||
|
|
2382
|
-
return { shell: "fish", rcFile:
|
|
2666
|
+
const xdgConfig = process.env["XDG_CONFIG_HOME"] || path12.join(home, ".config");
|
|
2667
|
+
return { shell: "fish", rcFile: path12.join(xdgConfig, "fish", "config.fish") };
|
|
2383
2668
|
}
|
|
2384
2669
|
return void 0;
|
|
2385
2670
|
}
|
|
@@ -2390,24 +2675,24 @@ function resolveGlobalGitignorePath() {
|
|
|
2390
2675
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2391
2676
|
}).trim();
|
|
2392
2677
|
if (configured) {
|
|
2393
|
-
return configured.startsWith("~") ?
|
|
2678
|
+
return configured.startsWith("~") ? path12.join(os5.homedir(), configured.slice(1)) : configured;
|
|
2394
2679
|
}
|
|
2395
2680
|
} catch {
|
|
2396
2681
|
}
|
|
2397
|
-
return
|
|
2682
|
+
return path12.join(os5.homedir(), ".gitignore_global");
|
|
2398
2683
|
}
|
|
2399
2684
|
function installGlobalGitignore() {
|
|
2400
2685
|
const ignorePath = resolveGlobalGitignorePath();
|
|
2401
2686
|
let existing = "";
|
|
2402
2687
|
try {
|
|
2403
|
-
existing =
|
|
2688
|
+
existing = fs8.readFileSync(ignorePath, "utf-8");
|
|
2404
2689
|
} catch {
|
|
2405
2690
|
}
|
|
2406
2691
|
if (existing.split("\n").some((line) => line.trim() === ".keepgoing")) {
|
|
2407
2692
|
console.log(`Global gitignore: .keepgoing already present in ${ignorePath}`);
|
|
2408
2693
|
} else {
|
|
2409
2694
|
const suffix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
2410
|
-
|
|
2695
|
+
fs8.appendFileSync(ignorePath, `${suffix}.keepgoing
|
|
2411
2696
|
`, "utf-8");
|
|
2412
2697
|
console.log(`Global gitignore: .keepgoing added to ${ignorePath}`);
|
|
2413
2698
|
}
|
|
@@ -2426,21 +2711,21 @@ function uninstallGlobalGitignore() {
|
|
|
2426
2711
|
const ignorePath = resolveGlobalGitignorePath();
|
|
2427
2712
|
let existing = "";
|
|
2428
2713
|
try {
|
|
2429
|
-
existing =
|
|
2714
|
+
existing = fs8.readFileSync(ignorePath, "utf-8");
|
|
2430
2715
|
} catch {
|
|
2431
2716
|
return;
|
|
2432
2717
|
}
|
|
2433
2718
|
const lines = existing.split("\n");
|
|
2434
2719
|
const filtered = lines.filter((line) => line.trim() !== ".keepgoing");
|
|
2435
2720
|
if (filtered.length !== lines.length) {
|
|
2436
|
-
|
|
2721
|
+
fs8.writeFileSync(ignorePath, filtered.join("\n"), "utf-8");
|
|
2437
2722
|
console.log(`Global gitignore: .keepgoing removed from ${ignorePath}`);
|
|
2438
2723
|
}
|
|
2439
2724
|
}
|
|
2440
2725
|
function installPostCommitHook() {
|
|
2441
|
-
|
|
2442
|
-
if (
|
|
2443
|
-
const existing =
|
|
2726
|
+
fs8.mkdirSync(KEEPGOING_HOOKS_DIR, { recursive: true });
|
|
2727
|
+
if (fs8.existsSync(POST_COMMIT_HOOK_PATH)) {
|
|
2728
|
+
const existing = fs8.readFileSync(POST_COMMIT_HOOK_PATH, "utf-8");
|
|
2444
2729
|
if (existing.includes(POST_COMMIT_MARKER_START)) {
|
|
2445
2730
|
console.log(`Git post-commit hook: already installed at ${POST_COMMIT_HOOK_PATH}`);
|
|
2446
2731
|
} else {
|
|
@@ -2453,14 +2738,14 @@ if command -v keepgoing-mcp-server >/dev/null 2>&1; then
|
|
|
2453
2738
|
fi
|
|
2454
2739
|
${POST_COMMIT_MARKER_END}
|
|
2455
2740
|
`;
|
|
2456
|
-
|
|
2741
|
+
fs8.appendFileSync(POST_COMMIT_HOOK_PATH, block, "utf-8");
|
|
2457
2742
|
console.log(`Git post-commit hook: appended to existing ${POST_COMMIT_HOOK_PATH}`);
|
|
2458
2743
|
}
|
|
2459
2744
|
} else {
|
|
2460
|
-
|
|
2745
|
+
fs8.writeFileSync(POST_COMMIT_HOOK_PATH, POST_COMMIT_HOOK, { mode: 493 });
|
|
2461
2746
|
console.log(`Git post-commit hook: installed at ${POST_COMMIT_HOOK_PATH}`);
|
|
2462
2747
|
}
|
|
2463
|
-
|
|
2748
|
+
fs8.chmodSync(POST_COMMIT_HOOK_PATH, 493);
|
|
2464
2749
|
let currentHooksPath;
|
|
2465
2750
|
try {
|
|
2466
2751
|
currentHooksPath = execSync("git config --global core.hooksPath", {
|
|
@@ -2485,8 +2770,8 @@ ${POST_COMMIT_MARKER_END}
|
|
|
2485
2770
|
}
|
|
2486
2771
|
}
|
|
2487
2772
|
function uninstallPostCommitHook() {
|
|
2488
|
-
if (
|
|
2489
|
-
const existing =
|
|
2773
|
+
if (fs8.existsSync(POST_COMMIT_HOOK_PATH)) {
|
|
2774
|
+
const existing = fs8.readFileSync(POST_COMMIT_HOOK_PATH, "utf-8");
|
|
2490
2775
|
if (existing.includes(POST_COMMIT_MARKER_START)) {
|
|
2491
2776
|
const pattern = new RegExp(
|
|
2492
2777
|
`
|
|
@@ -2496,10 +2781,10 @@ function uninstallPostCommitHook() {
|
|
|
2496
2781
|
);
|
|
2497
2782
|
const updated = existing.replace(pattern, "").trim();
|
|
2498
2783
|
if (!updated || updated === "#!/bin/sh") {
|
|
2499
|
-
|
|
2784
|
+
fs8.unlinkSync(POST_COMMIT_HOOK_PATH);
|
|
2500
2785
|
console.log(`Git post-commit hook: removed ${POST_COMMIT_HOOK_PATH}`);
|
|
2501
2786
|
} else {
|
|
2502
|
-
|
|
2787
|
+
fs8.writeFileSync(POST_COMMIT_HOOK_PATH, updated + "\n", "utf-8");
|
|
2503
2788
|
console.log(`Git post-commit hook: KeepGoing block removed from ${POST_COMMIT_HOOK_PATH}`);
|
|
2504
2789
|
}
|
|
2505
2790
|
}
|
|
@@ -2517,8 +2802,8 @@ function uninstallPostCommitHook() {
|
|
|
2517
2802
|
}
|
|
2518
2803
|
} catch {
|
|
2519
2804
|
}
|
|
2520
|
-
if (
|
|
2521
|
-
|
|
2805
|
+
if (fs8.existsSync(KEEPGOING_MANAGED_MARKER)) {
|
|
2806
|
+
fs8.unlinkSync(KEEPGOING_MANAGED_MARKER);
|
|
2522
2807
|
}
|
|
2523
2808
|
}
|
|
2524
2809
|
function hookInstallCommand(shellOverride) {
|
|
@@ -2533,7 +2818,7 @@ function hookInstallCommand(shellOverride) {
|
|
|
2533
2818
|
const hookBlock = shell === "zsh" ? ZSH_HOOK : shell === "fish" ? FISH_HOOK : BASH_HOOK;
|
|
2534
2819
|
let existing = "";
|
|
2535
2820
|
try {
|
|
2536
|
-
existing =
|
|
2821
|
+
existing = fs8.readFileSync(rcFile, "utf-8");
|
|
2537
2822
|
} catch {
|
|
2538
2823
|
}
|
|
2539
2824
|
if (existing.includes(HOOK_MARKER_START)) {
|
|
@@ -2542,7 +2827,7 @@ function hookInstallCommand(shellOverride) {
|
|
|
2542
2827
|
installPostCommitHook();
|
|
2543
2828
|
return;
|
|
2544
2829
|
}
|
|
2545
|
-
|
|
2830
|
+
fs8.appendFileSync(rcFile, `
|
|
2546
2831
|
${hookBlock}
|
|
2547
2832
|
`, "utf-8");
|
|
2548
2833
|
console.log(`KeepGoing hook installed in ${rcFile}.`);
|
|
@@ -2564,7 +2849,7 @@ function hookUninstallCommand(shellOverride) {
|
|
|
2564
2849
|
const { rcFile } = detected;
|
|
2565
2850
|
let existing = "";
|
|
2566
2851
|
try {
|
|
2567
|
-
existing =
|
|
2852
|
+
existing = fs8.readFileSync(rcFile, "utf-8");
|
|
2568
2853
|
} catch {
|
|
2569
2854
|
console.log(`${rcFile} not found \u2014 nothing to remove.`);
|
|
2570
2855
|
return;
|
|
@@ -2580,7 +2865,7 @@ function hookUninstallCommand(shellOverride) {
|
|
|
2580
2865
|
"g"
|
|
2581
2866
|
);
|
|
2582
2867
|
const updated = existing.replace(pattern, "").replace(/\n{3,}/g, "\n\n");
|
|
2583
|
-
|
|
2868
|
+
fs8.writeFileSync(rcFile, updated, "utf-8");
|
|
2584
2869
|
console.log(`KeepGoing hook removed from ${rcFile}.`);
|
|
2585
2870
|
uninstallGlobalGitignore();
|
|
2586
2871
|
uninstallPostCommitHook();
|
|
@@ -2860,9 +3145,7 @@ async function decisionsCommand(opts) {
|
|
|
2860
3145
|
renderDecisions(decisions, scopeLabel);
|
|
2861
3146
|
}
|
|
2862
3147
|
|
|
2863
|
-
// src/commands/
|
|
2864
|
-
var RESET4 = "\x1B[0m";
|
|
2865
|
-
var DIM4 = "\x1B[2m";
|
|
3148
|
+
// src/commands/logUtils.ts
|
|
2866
3149
|
function parseDate(input) {
|
|
2867
3150
|
const lower = input.toLowerCase().trim();
|
|
2868
3151
|
if (lower === "today") {
|
|
@@ -2980,6 +3263,10 @@ function filterDecisions(decisions, opts) {
|
|
|
2980
3263
|
}
|
|
2981
3264
|
return result;
|
|
2982
3265
|
}
|
|
3266
|
+
|
|
3267
|
+
// src/commands/log.ts
|
|
3268
|
+
var RESET4 = "\x1B[0m";
|
|
3269
|
+
var DIM4 = "\x1B[2m";
|
|
2983
3270
|
function logSessions(reader, opts) {
|
|
2984
3271
|
const { effectiveBranch } = reader.resolveBranchScope(opts.branch || void 0);
|
|
2985
3272
|
let sessions = reader.getSessions();
|
|
@@ -3168,6 +3455,54 @@ async function continueCommand(opts) {
|
|
|
3168
3455
|
}
|
|
3169
3456
|
}
|
|
3170
3457
|
|
|
3458
|
+
// src/commands/glance.ts
|
|
3459
|
+
function glanceCommand(opts) {
|
|
3460
|
+
const snapshot = generateContextSnapshot(opts.cwd);
|
|
3461
|
+
if (!snapshot) {
|
|
3462
|
+
if (opts.json) {
|
|
3463
|
+
console.log("null");
|
|
3464
|
+
}
|
|
3465
|
+
return;
|
|
3466
|
+
}
|
|
3467
|
+
if (opts.json) {
|
|
3468
|
+
console.log(JSON.stringify(snapshot, null, 2));
|
|
3469
|
+
return;
|
|
3470
|
+
}
|
|
3471
|
+
const icon = snapshot.momentum === "hot" ? "\u26A1" : snapshot.momentum === "warm" ? "\u{1F536}" : "\u{1F4A4}";
|
|
3472
|
+
const parts = [];
|
|
3473
|
+
parts.push(`${icon} ${snapshot.doing}`);
|
|
3474
|
+
if (snapshot.next) {
|
|
3475
|
+
parts.push(`\u2192 ${snapshot.next}`);
|
|
3476
|
+
}
|
|
3477
|
+
let line = parts.join(" ");
|
|
3478
|
+
line += ` (${snapshot.when})`;
|
|
3479
|
+
if (snapshot.blocker) {
|
|
3480
|
+
line += ` \u26D4 ${snapshot.blocker}`;
|
|
3481
|
+
}
|
|
3482
|
+
console.log(line);
|
|
3483
|
+
}
|
|
3484
|
+
|
|
3485
|
+
// src/commands/hot.ts
|
|
3486
|
+
function hotCommand(opts) {
|
|
3487
|
+
const summary = generateCrossProjectSummary();
|
|
3488
|
+
if (summary.projects.length === 0) {
|
|
3489
|
+
if (opts.json) {
|
|
3490
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
3491
|
+
} else {
|
|
3492
|
+
console.log("No projects with activity found.");
|
|
3493
|
+
console.log("Projects are registered automatically when you save checkpoints.");
|
|
3494
|
+
}
|
|
3495
|
+
return;
|
|
3496
|
+
}
|
|
3497
|
+
if (opts.json) {
|
|
3498
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
3499
|
+
return;
|
|
3500
|
+
}
|
|
3501
|
+
for (const entry of summary.projects) {
|
|
3502
|
+
console.log(formatCrossProjectLine(entry));
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
|
|
3171
3506
|
// src/index.ts
|
|
3172
3507
|
var HELP_TEXT = `
|
|
3173
3508
|
keepgoing: resume side projects without the mental friction
|
|
@@ -3181,6 +3516,8 @@ Commands:
|
|
|
3181
3516
|
briefing Get a re-entry briefing for this project
|
|
3182
3517
|
decisions View decision history (Pro)
|
|
3183
3518
|
log Browse session checkpoints
|
|
3519
|
+
glance Quick context snapshot (single line, <50ms)
|
|
3520
|
+
hot Cross-project activity summary, sorted by momentum
|
|
3184
3521
|
continue Export context for use in another AI tool
|
|
3185
3522
|
save Save a checkpoint (auto-generates from git)
|
|
3186
3523
|
hook Manage the shell hook (zsh, bash, fish)
|
|
@@ -3338,6 +3675,35 @@ Example:
|
|
|
3338
3675
|
keepgoing deactivate: Deactivate the Pro license from this device
|
|
3339
3676
|
|
|
3340
3677
|
Usage: keepgoing deactivate [<key>]
|
|
3678
|
+
`,
|
|
3679
|
+
glance: `
|
|
3680
|
+
keepgoing glance: Quick context snapshot in a single line
|
|
3681
|
+
|
|
3682
|
+
Usage: keepgoing glance [options]
|
|
3683
|
+
|
|
3684
|
+
Outputs a single formatted line showing what you were doing, what's next,
|
|
3685
|
+
and when you last worked. Designed for shell prompts and status bars.
|
|
3686
|
+
Completes in <50ms. Outputs nothing if no data exists.
|
|
3687
|
+
|
|
3688
|
+
Options:
|
|
3689
|
+
--json Output raw JSON
|
|
3690
|
+
--cwd <path> Override the working directory
|
|
3691
|
+
|
|
3692
|
+
Format:
|
|
3693
|
+
Hot: \u26A1 {doing} \u2192 {next} ({when})
|
|
3694
|
+
Warm: \u{1F536} {doing} \u2192 {next} ({when})
|
|
3695
|
+
Cold: \u{1F4A4} {doing} \u2192 {next} ({when})
|
|
3696
|
+
`,
|
|
3697
|
+
hot: `
|
|
3698
|
+
keepgoing hot: Cross-project activity summary, sorted by momentum
|
|
3699
|
+
|
|
3700
|
+
Usage: keepgoing hot [options]
|
|
3701
|
+
|
|
3702
|
+
Shows a one-line summary for each registered project, sorted by momentum
|
|
3703
|
+
(hot first, then warm, then cold). Projects with no data are excluded.
|
|
3704
|
+
|
|
3705
|
+
Options:
|
|
3706
|
+
--json Output raw JSON
|
|
3341
3707
|
`,
|
|
3342
3708
|
continue: `
|
|
3343
3709
|
keepgoing continue: Export context for use in another AI tool
|
|
@@ -3545,6 +3911,12 @@ async function main() {
|
|
|
3545
3911
|
sessions: parsed.sessions
|
|
3546
3912
|
});
|
|
3547
3913
|
break;
|
|
3914
|
+
case "glance":
|
|
3915
|
+
glanceCommand({ cwd, json });
|
|
3916
|
+
break;
|
|
3917
|
+
case "hot":
|
|
3918
|
+
hotCommand({ json });
|
|
3919
|
+
break;
|
|
3548
3920
|
case "continue":
|
|
3549
3921
|
await continueCommand({ cwd, json, quiet, target: parsed.target, open: parsed.open });
|
|
3550
3922
|
break;
|
|
@@ -3568,7 +3940,7 @@ async function main() {
|
|
|
3568
3940
|
}
|
|
3569
3941
|
break;
|
|
3570
3942
|
case "version":
|
|
3571
|
-
console.log(`keepgoing v${"1.
|
|
3943
|
+
console.log(`keepgoing v${"1.3.1"}`);
|
|
3572
3944
|
break;
|
|
3573
3945
|
case "activate":
|
|
3574
3946
|
await activateCommand({ licenseKey: subcommand });
|