@keepgoingdev/cli 1.2.1 → 1.3.0

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