@keepgoingdev/cli 1.2.2 → 1.3.0

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