@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.
Files changed (2) hide show
  1. package/dist/index.js +928 -556
  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
 
@@ -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
- parts.push(`${sessionCount} recent sessions`);
336
+ const planSuffix = planCount > 0 ? ` (${planCount} plan-only)` : "";
337
+ parts.push(`${sessionCount} recent sessions${planSuffix}`);
322
338
  } else if (sessionCount === 1) {
323
- parts.push("1 recent session");
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/storage.ts
592
- import fs2 from "fs";
593
- import path4 from "path";
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/registry.ts
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 KEEPGOING_DIR = path3.join(os.homedir(), ".keepgoing");
601
- var KNOWN_PROJECTS_FILE = path3.join(KEEPGOING_DIR, "known-projects.json");
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 (fs.existsSync(KNOWN_PROJECTS_FILE)) {
606
- const raw = JSON.parse(fs.readFileSync(KNOWN_PROJECTS_FILE, "utf-8"));
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 (!fs.existsSync(KEEPGOING_DIR)) {
617
- fs.mkdirSync(KEEPGOING_DIR, { recursive: true });
771
+ if (!fs2.existsSync(KEEPGOING_DIR)) {
772
+ fs2.mkdirSync(KEEPGOING_DIR, { recursive: true });
618
773
  }
619
774
  const tmpFile = KNOWN_PROJECTS_FILE + ".tmp";
620
- fs.writeFileSync(tmpFile, JSON.stringify(data, null, 2) + "\n", "utf-8");
621
- fs.renameSync(tmpFile, KNOWN_PROJECTS_FILE);
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 || path3.basename(projectPath);
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 = path4.join(mainRoot, STORAGE_DIR);
667
- this.sessionsFilePath = path4.join(this.storagePath, SESSIONS_FILE);
668
- this.stateFilePath = path4.join(this.storagePath, STATE_FILE);
669
- this.metaFilePath = path4.join(this.storagePath, META_FILE);
670
- this.currentTasksFilePath = path4.join(this.storagePath, CURRENT_TASKS_FILE);
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 (!fs2.existsSync(this.storagePath)) {
674
- fs2.mkdirSync(this.storagePath, { recursive: true });
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 (fs2.existsSync(this.sessionsFilePath)) {
682
- const raw = JSON.parse(fs2.readFileSync(this.sessionsFilePath, "utf-8"));
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
- fs2.writeFileSync(this.sessionsFilePath, JSON.stringify(sessionsData, null, 2), "utf-8");
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
- fs2.writeFileSync(this.stateFilePath, JSON.stringify(state, null, 2), "utf-8");
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 (fs2.existsSync(this.metaFilePath)) {
715
- meta = JSON.parse(fs2.readFileSync(this.metaFilePath, "utf-8"));
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
- fs2.writeFileSync(this.metaFilePath, JSON.stringify(meta, null, 2), "utf-8");
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 (fs2.existsSync(this.currentTasksFilePath)) {
740
- const raw = JSON.parse(fs2.readFileSync(this.currentTasksFilePath, "utf-8"));
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 (fs2.existsSync(this.currentTasksFilePath)) {
796
- const raw = JSON.parse(fs2.readFileSync(this.currentTasksFilePath, "utf-8"));
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
- fs2.writeFileSync(this.currentTasksFilePath, JSON.stringify(data, null, 2), "utf-8");
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/smartSummary.ts
825
- var PREFIX_VERBS = {
826
- feat: "Added",
827
- fix: "Fixed",
828
- refactor: "Refactored",
829
- docs: "Updated docs for",
830
- test: "Added tests for",
831
- chore: "Updated",
832
- style: "Styled",
833
- perf: "Optimized",
834
- ci: "Updated CI for",
835
- build: "Updated build for",
836
- revert: "Reverted"
837
- };
838
- var NOISE_PATTERNS = [
839
- "node_modules",
840
- "package-lock.json",
841
- "yarn.lock",
842
- "pnpm-lock.yaml",
843
- ".gitignore",
844
- ".DS_Store",
845
- "dist/",
846
- "out/",
847
- "build/"
848
- ];
849
- function categorizeCommits(messages) {
850
- const groups = /* @__PURE__ */ new Map();
851
- for (const msg of messages) {
852
- const match = msg.match(/^(\w+)(?:\([^)]*\))?[!]?:\s*(.+)/);
853
- if (match) {
854
- const prefix = match[1].toLowerCase();
855
- const body = match[2].trim();
856
- if (!groups.has(prefix)) {
857
- groups.set(prefix, []);
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
- groups.get(prefix).push(body);
860
- } else {
861
- if (!groups.has("other")) {
862
- groups.set("other", []);
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
- return this._cachedBranch;
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
- * Worktree-aware last session lookup.
1317
- * In a worktree, scopes to the current branch with fallback to global.
1318
- * Returns the session and whether it fell back to global.
1319
- */
1320
- getScopedLastSession() {
1321
- const branch = this.getCurrentBranch();
1322
- if (this._isWorktree && branch) {
1323
- const scoped = this.getLastSessionForBranch(branch);
1324
- if (scoped) return { session: scoped, isFallback: false };
1325
- return { session: this.getLastSession(), isFallback: true };
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
- /** Worktree-aware recent sessions. Scopes to current branch in a worktree. */
1330
- getScopedRecentSessions(count) {
1331
- const branch = this.getCurrentBranch();
1332
- if (this._isWorktree && branch) {
1333
- return this.getRecentSessionsForBranch(branch, count);
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
- /** Worktree-aware recent decisions. Scopes to current branch in a worktree. */
1338
- getScopedRecentDecisions(count) {
1339
- const branch = this.getCurrentBranch();
1340
- if (this._isWorktree && branch) {
1341
- return this.getRecentDecisionsForBranch(branch, count);
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
- * Resolves branch scope from an explicit `branch` parameter.
1347
- * Used by tools that accept a `branch` argument (e.g. get_session_history, get_decisions).
1348
- * - `"all"` returns no filter.
1349
- * - An explicit branch name uses that.
1350
- * - `undefined` auto-scopes to the current branch in a worktree, or all branches otherwise.
1351
- */
1352
- resolveBranchScope(branch) {
1353
- if (branch === "all") {
1354
- return { effectiveBranch: void 0, scopeLabel: "all branches" };
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
- if (branch) {
1357
- return { effectiveBranch: branch, scopeLabel: `branch \`${branch}\`` };
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
- const currentBranch = this.getCurrentBranch();
1360
- if (this._isWorktree && currentBranch) {
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
- * Parses sessions.json once, returning both the session list
1367
- * and the optional lastSessionId from a ProjectSessions wrapper.
1368
- */
1369
- parseSessions() {
1370
- const raw = this.readJsonFile(
1371
- this.sessionsFilePath
1372
- );
1373
- if (!raw) {
1374
- return { sessions: [] };
1375
- }
1376
- if (Array.isArray(raw)) {
1377
- return { sessions: raw };
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
- return { sessions: raw.sessions ?? [], wrapperLastSessionId: raw.lastSessionId };
1545
+ areaCounts.set(area, (areaCounts.get(area) ?? 0) + 1);
1380
1546
  }
1381
- parseDecisions() {
1382
- const raw = this.readJsonFile(this.decisionsFilePath);
1383
- if (!raw) {
1384
- return { decisions: [] };
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
- readJsonFile(filePath) {
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
- if (!fs5.existsSync(filePath)) {
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
- return void 0;
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 fs6 from "fs";
1669
+ import fs7 from "fs";
1403
1670
  import os3 from "os";
1404
- import path8 from "path";
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 = 2;
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 || path8.join(os3.homedir(), ".claude");
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: path8.join(claudeDir2, "settings.json"),
1472
- claudeMdPath: path8.join(claudeDir2, "CLAUDE.md"),
1473
- rulesPath: path8.join(claudeDir2, "rules", "keepgoing.md")
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 = path8.join(workspacePath, ".claude");
1477
- const dotClaudeMdPath = path8.join(workspacePath, ".claude", "CLAUDE.md");
1478
- const rootClaudeMdPath = path8.join(workspacePath, "CLAUDE.md");
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: path8.join(claudeDir, "settings.json"),
1482
- claudeMdPath: fs6.existsSync(dotClaudeMdPath) ? dotClaudeMdPath : rootClaudeMdPath,
1483
- rulesPath: path8.join(workspacePath, ".claude", "rules", "keepgoing.md")
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 (!fs6.existsSync(otherPaths.settingsPath)) {
1808
+ if (!fs7.existsSync(otherPaths.settingsPath)) {
1524
1809
  return null;
1525
1810
  }
1526
1811
  try {
1527
- const otherSettings = JSON.parse(fs6.readFileSync(otherPaths.settingsPath, "utf-8"));
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" ? path8.join("~", path8.relative(os3.homedir(), claudeDir), "settings.json").replace(/\\/g, "/") : ".claude/settings.json";
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 (fs6.existsSync(settingsPath)) {
1559
- settings = JSON.parse(fs6.readFileSync(settingsPath, "utf-8"));
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 (!fs6.existsSync(claudeDir)) {
1591
- fs6.mkdirSync(claudeDir, { recursive: true });
1875
+ if (!fs7.existsSync(claudeDir)) {
1876
+ fs7.mkdirSync(claudeDir, { recursive: true });
1592
1877
  }
1593
- fs6.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
1878
+ fs7.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
1594
1879
  changed = true;
1595
1880
  }
1596
1881
  if (claudeMd) {
1597
- const rulesDir = path8.dirname(rulesPath);
1598
- const rulesLabel = scope === "user" ? path8.join(path8.relative(os3.homedir(), path8.dirname(rulesPath)), "keepgoing.md").replace(/\\/g, "/") : ".claude/rules/keepgoing.md";
1599
- if (fs6.existsSync(rulesPath)) {
1600
- const existing = fs6.readFileSync(rulesPath, "utf-8");
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 (!fs6.existsSync(rulesDir)) {
1608
- fs6.mkdirSync(rulesDir, { recursive: true });
1892
+ if (!fs7.existsSync(rulesDir)) {
1893
+ fs7.mkdirSync(rulesDir, { recursive: true });
1609
1894
  }
1610
- fs6.writeFileSync(rulesPath, KEEPGOING_RULES_CONTENT);
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 = fs6.existsSync(claudeMdPath) ? fs6.readFileSync(claudeMdPath, "utf-8") : "";
1616
- if (!fs6.existsSync(rulesDir)) {
1617
- fs6.mkdirSync(rulesDir, { recursive: true });
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
- fs6.writeFileSync(rulesPath, KEEPGOING_RULES_CONTENT);
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 path9 from "path";
2400
+ import path10 from "path";
2116
2401
  import os4 from "os";
2117
- var CLI_VERSION = "1.2.2";
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 = path9.join(os4.homedir(), ".keepgoing");
2122
- var CACHE_PATH = path9.join(CACHE_DIR, "update-check.json");
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 path10 from "path";
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) => path10.basename(f)).join(", ")}`;
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 = path10.basename(resolveStorageRoot(cwd));
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 fs7 from "fs";
2302
- import path11 from "path";
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 = path11.join(os5.homedir(), ".keepgoing", "hooks");
2310
- var POST_COMMIT_HOOK_PATH = path11.join(KEEPGOING_HOOKS_DIR, "post-commit");
2311
- var KEEPGOING_MANAGED_MARKER = path11.join(KEEPGOING_HOOKS_DIR, ".keepgoing-managed");
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 status --quiet
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 status --quiet
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 status --quiet
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: path11.join(home, ".zshrc") };
2660
+ return { shell: "zsh", rcFile: path12.join(home, ".zshrc") };
2376
2661
  }
2377
2662
  if (shell === "bash") {
2378
- return { shell: "bash", rcFile: path11.join(home, ".bashrc") };
2663
+ return { shell: "bash", rcFile: path12.join(home, ".bashrc") };
2379
2664
  }
2380
2665
  if (shell === "fish") {
2381
- const xdgConfig = process.env["XDG_CONFIG_HOME"] || path11.join(home, ".config");
2382
- return { shell: "fish", rcFile: path11.join(xdgConfig, "fish", "config.fish") };
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("~") ? path11.join(os5.homedir(), configured.slice(1)) : configured;
2678
+ return configured.startsWith("~") ? path12.join(os5.homedir(), configured.slice(1)) : configured;
2394
2679
  }
2395
2680
  } catch {
2396
2681
  }
2397
- return path11.join(os5.homedir(), ".gitignore_global");
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 = fs7.readFileSync(ignorePath, "utf-8");
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
- fs7.appendFileSync(ignorePath, `${suffix}.keepgoing
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 = fs7.readFileSync(ignorePath, "utf-8");
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
- fs7.writeFileSync(ignorePath, filtered.join("\n"), "utf-8");
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
- fs7.mkdirSync(KEEPGOING_HOOKS_DIR, { recursive: true });
2442
- if (fs7.existsSync(POST_COMMIT_HOOK_PATH)) {
2443
- const existing = fs7.readFileSync(POST_COMMIT_HOOK_PATH, "utf-8");
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
- fs7.appendFileSync(POST_COMMIT_HOOK_PATH, block, "utf-8");
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
- fs7.writeFileSync(POST_COMMIT_HOOK_PATH, POST_COMMIT_HOOK, { mode: 493 });
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
- fs7.chmodSync(POST_COMMIT_HOOK_PATH, 493);
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 (fs7.existsSync(POST_COMMIT_HOOK_PATH)) {
2489
- const existing = fs7.readFileSync(POST_COMMIT_HOOK_PATH, "utf-8");
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
- fs7.unlinkSync(POST_COMMIT_HOOK_PATH);
2784
+ fs8.unlinkSync(POST_COMMIT_HOOK_PATH);
2500
2785
  console.log(`Git post-commit hook: removed ${POST_COMMIT_HOOK_PATH}`);
2501
2786
  } else {
2502
- fs7.writeFileSync(POST_COMMIT_HOOK_PATH, updated + "\n", "utf-8");
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 (fs7.existsSync(KEEPGOING_MANAGED_MARKER)) {
2521
- fs7.unlinkSync(KEEPGOING_MANAGED_MARKER);
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 = fs7.readFileSync(rcFile, "utf-8");
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
- fs7.appendFileSync(rcFile, `
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 = fs7.readFileSync(rcFile, "utf-8");
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
- fs7.writeFileSync(rcFile, updated, "utf-8");
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/log.ts
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.2.2"}`);
3943
+ console.log(`keepgoing v${"1.3.1"}`);
3572
3944
  break;
3573
3945
  case "activate":
3574
3946
  await activateCommand({ licenseKey: subcommand });