@keepgoingdev/mcp-server 0.6.1 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -149,6 +149,18 @@ function getCommitsSince(workspacePath, sinceTimestamp) {
149
149
  function getCommitMessagesSince(workspacePath, sinceTimestamp) {
150
150
  return getGitLogSince(workspacePath, "%s", sinceTimestamp);
151
151
  }
152
+ function getCommitMessageByHash(workspacePath, commitHash) {
153
+ try {
154
+ const result = execFileSync("git", ["log", "-1", "--format=%s", commitHash], {
155
+ cwd: workspacePath,
156
+ encoding: "utf-8",
157
+ timeout: 5e3
158
+ });
159
+ return result.trim() || void 0;
160
+ } catch {
161
+ return void 0;
162
+ }
163
+ }
152
164
  function getFilesChangedInCommit(workspacePath, commitHash) {
153
165
  try {
154
166
  const result = execFileSync("git", ["diff-tree", "--no-commit-id", "--name-only", "-r", commitHash], {
@@ -350,7 +362,8 @@ function buildRecentActivity(lastSession, recentSessions, recentCommitMessages)
350
362
  parts.push("1 recent session");
351
363
  }
352
364
  if (lastSession.summary) {
353
- parts.push(`Last: ${lastSession.summary}`);
365
+ const brief = lastSession.summary.length > 120 ? lastSession.summary.slice(0, 117) + "..." : lastSession.summary;
366
+ parts.push(`Last: ${brief}`);
354
367
  }
355
368
  if (lastSession.touchedFiles.length > 0) {
356
369
  parts.push(`${lastSession.touchedFiles.length} files touched`);
@@ -691,9 +704,59 @@ function formatContinueOnPrompt(context, options) {
691
704
  }
692
705
 
693
706
  // ../../packages/shared/src/storage.ts
707
+ import fs2 from "fs";
708
+ import path4 from "path";
709
+ import { randomUUID as randomUUID2, createHash } from "crypto";
710
+
711
+ // ../../packages/shared/src/registry.ts
694
712
  import fs from "fs";
713
+ import os from "os";
695
714
  import path3 from "path";
696
- import { randomUUID as randomUUID2, createHash } from "crypto";
715
+ var KEEPGOING_DIR = path3.join(os.homedir(), ".keepgoing");
716
+ var KNOWN_PROJECTS_FILE = path3.join(KEEPGOING_DIR, "known-projects.json");
717
+ var STALE_PROJECT_MS = 90 * 24 * 60 * 60 * 1e3;
718
+ function readKnownProjects() {
719
+ try {
720
+ if (fs.existsSync(KNOWN_PROJECTS_FILE)) {
721
+ const raw = JSON.parse(fs.readFileSync(KNOWN_PROJECTS_FILE, "utf-8"));
722
+ if (raw && Array.isArray(raw.projects)) {
723
+ return raw;
724
+ }
725
+ }
726
+ } catch {
727
+ }
728
+ return { version: 1, projects: [] };
729
+ }
730
+ function writeKnownProjects(data) {
731
+ if (!fs.existsSync(KEEPGOING_DIR)) {
732
+ fs.mkdirSync(KEEPGOING_DIR, { recursive: true });
733
+ }
734
+ const tmpFile = KNOWN_PROJECTS_FILE + ".tmp";
735
+ fs.writeFileSync(tmpFile, JSON.stringify(data, null, 2) + "\n", "utf-8");
736
+ fs.renameSync(tmpFile, KNOWN_PROJECTS_FILE);
737
+ }
738
+ function registerProject(projectPath, projectName) {
739
+ try {
740
+ const data = readKnownProjects();
741
+ const now = (/* @__PURE__ */ new Date()).toISOString();
742
+ const name = projectName || path3.basename(projectPath);
743
+ const existingIdx = data.projects.findIndex((p) => p.path === projectPath);
744
+ if (existingIdx >= 0) {
745
+ data.projects[existingIdx].lastSeen = now;
746
+ if (projectName) {
747
+ data.projects[existingIdx].name = name;
748
+ }
749
+ } else {
750
+ data.projects.push({ path: projectPath, name, lastSeen: now });
751
+ }
752
+ const cutoff = Date.now() - STALE_PROJECT_MS;
753
+ data.projects = data.projects.filter((p) => new Date(p.lastSeen).getTime() > cutoff);
754
+ writeKnownProjects(data);
755
+ } catch {
756
+ }
757
+ }
758
+
759
+ // ../../packages/shared/src/storage.ts
697
760
  var STORAGE_DIR = ".keepgoing";
698
761
  var META_FILE = "meta.json";
699
762
  var SESSIONS_FILE = "sessions.json";
@@ -715,23 +778,23 @@ var KeepGoingWriter = class {
715
778
  currentTasksFilePath;
716
779
  constructor(workspacePath) {
717
780
  const mainRoot = resolveStorageRoot(workspacePath);
718
- this.storagePath = path3.join(mainRoot, STORAGE_DIR);
719
- this.sessionsFilePath = path3.join(this.storagePath, SESSIONS_FILE);
720
- this.stateFilePath = path3.join(this.storagePath, STATE_FILE);
721
- this.metaFilePath = path3.join(this.storagePath, META_FILE);
722
- this.currentTasksFilePath = path3.join(this.storagePath, CURRENT_TASKS_FILE);
781
+ this.storagePath = path4.join(mainRoot, STORAGE_DIR);
782
+ this.sessionsFilePath = path4.join(this.storagePath, SESSIONS_FILE);
783
+ this.stateFilePath = path4.join(this.storagePath, STATE_FILE);
784
+ this.metaFilePath = path4.join(this.storagePath, META_FILE);
785
+ this.currentTasksFilePath = path4.join(this.storagePath, CURRENT_TASKS_FILE);
723
786
  }
724
787
  ensureDir() {
725
- if (!fs.existsSync(this.storagePath)) {
726
- fs.mkdirSync(this.storagePath, { recursive: true });
788
+ if (!fs2.existsSync(this.storagePath)) {
789
+ fs2.mkdirSync(this.storagePath, { recursive: true });
727
790
  }
728
791
  }
729
792
  saveCheckpoint(checkpoint, projectName) {
730
793
  this.ensureDir();
731
794
  let sessionsData;
732
795
  try {
733
- if (fs.existsSync(this.sessionsFilePath)) {
734
- const raw = JSON.parse(fs.readFileSync(this.sessionsFilePath, "utf-8"));
796
+ if (fs2.existsSync(this.sessionsFilePath)) {
797
+ const raw = JSON.parse(fs2.readFileSync(this.sessionsFilePath, "utf-8"));
735
798
  if (Array.isArray(raw)) {
736
799
  sessionsData = { version: 1, project: projectName, sessions: raw };
737
800
  } else {
@@ -749,20 +812,22 @@ var KeepGoingWriter = class {
749
812
  if (sessionsData.sessions.length > MAX_SESSIONS) {
750
813
  sessionsData.sessions = sessionsData.sessions.slice(-MAX_SESSIONS);
751
814
  }
752
- fs.writeFileSync(this.sessionsFilePath, JSON.stringify(sessionsData, null, 2), "utf-8");
815
+ fs2.writeFileSync(this.sessionsFilePath, JSON.stringify(sessionsData, null, 2), "utf-8");
753
816
  const state = {
754
817
  lastSessionId: checkpoint.id,
755
818
  lastKnownBranch: checkpoint.gitBranch,
756
819
  lastActivityAt: checkpoint.timestamp
757
820
  };
758
- fs.writeFileSync(this.stateFilePath, JSON.stringify(state, null, 2), "utf-8");
821
+ fs2.writeFileSync(this.stateFilePath, JSON.stringify(state, null, 2), "utf-8");
759
822
  this.updateMeta(checkpoint.timestamp);
823
+ const mainRoot = resolveStorageRoot(this.storagePath);
824
+ registerProject(mainRoot, projectName);
760
825
  }
761
826
  updateMeta(timestamp) {
762
827
  let meta;
763
828
  try {
764
- if (fs.existsSync(this.metaFilePath)) {
765
- meta = JSON.parse(fs.readFileSync(this.metaFilePath, "utf-8"));
829
+ if (fs2.existsSync(this.metaFilePath)) {
830
+ meta = JSON.parse(fs2.readFileSync(this.metaFilePath, "utf-8"));
766
831
  meta.lastUpdated = timestamp;
767
832
  } else {
768
833
  meta = {
@@ -778,7 +843,7 @@ var KeepGoingWriter = class {
778
843
  lastUpdated: timestamp
779
844
  };
780
845
  }
781
- fs.writeFileSync(this.metaFilePath, JSON.stringify(meta, null, 2), "utf-8");
846
+ fs2.writeFileSync(this.metaFilePath, JSON.stringify(meta, null, 2), "utf-8");
782
847
  }
783
848
  // ---------------------------------------------------------------------------
784
849
  // Multi-session API
@@ -786,8 +851,8 @@ var KeepGoingWriter = class {
786
851
  /** Read all current tasks from current-tasks.json. Auto-prunes stale sessions. */
787
852
  readCurrentTasks() {
788
853
  try {
789
- if (fs.existsSync(this.currentTasksFilePath)) {
790
- const raw = JSON.parse(fs.readFileSync(this.currentTasksFilePath, "utf-8"));
854
+ if (fs2.existsSync(this.currentTasksFilePath)) {
855
+ const raw = JSON.parse(fs2.readFileSync(this.currentTasksFilePath, "utf-8"));
791
856
  const tasks = Array.isArray(raw) ? raw : raw.tasks ?? [];
792
857
  return this.pruneStale(tasks);
793
858
  }
@@ -802,6 +867,8 @@ var KeepGoingWriter = class {
802
867
  upsertSession(update) {
803
868
  this.ensureDir();
804
869
  this.upsertSessionCore(update);
870
+ const mainRoot = resolveStorageRoot(this.storagePath);
871
+ registerProject(mainRoot);
805
872
  }
806
873
  /** Core upsert logic: merges the update into current-tasks.json and returns the pruned task list. */
807
874
  upsertSessionCore(update) {
@@ -840,8 +907,8 @@ var KeepGoingWriter = class {
840
907
  // ---------------------------------------------------------------------------
841
908
  readAllTasksRaw() {
842
909
  try {
843
- if (fs.existsSync(this.currentTasksFilePath)) {
844
- const raw = JSON.parse(fs.readFileSync(this.currentTasksFilePath, "utf-8"));
910
+ if (fs2.existsSync(this.currentTasksFilePath)) {
911
+ const raw = JSON.parse(fs2.readFileSync(this.currentTasksFilePath, "utf-8"));
845
912
  return Array.isArray(raw) ? [...raw] : [...raw.tasks ?? []];
846
913
  }
847
914
  } catch {
@@ -853,7 +920,7 @@ var KeepGoingWriter = class {
853
920
  }
854
921
  writeTasksFile(tasks) {
855
922
  const data = { version: 1, tasks };
856
- fs.writeFileSync(this.currentTasksFilePath, JSON.stringify(data, null, 2), "utf-8");
923
+ fs2.writeFileSync(this.currentTasksFilePath, JSON.stringify(data, null, 2), "utf-8");
857
924
  }
858
925
  };
859
926
  function generateSessionId(context) {
@@ -1032,35 +1099,35 @@ function capitalize(s) {
1032
1099
  }
1033
1100
 
1034
1101
  // ../../packages/shared/src/decisionStorage.ts
1035
- import fs3 from "fs";
1036
- import path5 from "path";
1102
+ import fs4 from "fs";
1103
+ import path6 from "path";
1037
1104
 
1038
1105
  // ../../packages/shared/src/license.ts
1039
1106
  import crypto from "crypto";
1040
- import fs2 from "fs";
1041
- import os from "os";
1042
- import path4 from "path";
1107
+ import fs3 from "fs";
1108
+ import os2 from "os";
1109
+ import path5 from "path";
1043
1110
  var LICENSE_FILE = "license.json";
1044
1111
  var DEVICE_ID_FILE = "device-id";
1045
1112
  function getGlobalLicenseDir() {
1046
- return path4.join(os.homedir(), ".keepgoing");
1113
+ return path5.join(os2.homedir(), ".keepgoing");
1047
1114
  }
1048
1115
  function getGlobalLicensePath() {
1049
- return path4.join(getGlobalLicenseDir(), LICENSE_FILE);
1116
+ return path5.join(getGlobalLicenseDir(), LICENSE_FILE);
1050
1117
  }
1051
1118
  function getDeviceId() {
1052
1119
  const dir = getGlobalLicenseDir();
1053
- const filePath = path4.join(dir, DEVICE_ID_FILE);
1120
+ const filePath = path5.join(dir, DEVICE_ID_FILE);
1054
1121
  try {
1055
- const existing = fs2.readFileSync(filePath, "utf-8").trim();
1122
+ const existing = fs3.readFileSync(filePath, "utf-8").trim();
1056
1123
  if (existing) return existing;
1057
1124
  } catch {
1058
1125
  }
1059
1126
  const id = crypto.randomUUID();
1060
- if (!fs2.existsSync(dir)) {
1061
- fs2.mkdirSync(dir, { recursive: true });
1127
+ if (!fs3.existsSync(dir)) {
1128
+ fs3.mkdirSync(dir, { recursive: true });
1062
1129
  }
1063
- fs2.writeFileSync(filePath, id, "utf-8");
1130
+ fs3.writeFileSync(filePath, id, "utf-8");
1064
1131
  return id;
1065
1132
  }
1066
1133
  var DECISION_DETECTION_VARIANT_ID = 1361527;
@@ -1094,10 +1161,10 @@ function readLicenseStore() {
1094
1161
  const licensePath = getGlobalLicensePath();
1095
1162
  let store;
1096
1163
  try {
1097
- if (!fs2.existsSync(licensePath)) {
1164
+ if (!fs3.existsSync(licensePath)) {
1098
1165
  store = { version: 2, licenses: [] };
1099
1166
  } else {
1100
- const raw = fs2.readFileSync(licensePath, "utf-8");
1167
+ const raw = fs3.readFileSync(licensePath, "utf-8");
1101
1168
  const data = JSON.parse(raw);
1102
1169
  if (data?.version === 2 && Array.isArray(data.licenses)) {
1103
1170
  store = data;
@@ -1114,11 +1181,11 @@ function readLicenseStore() {
1114
1181
  }
1115
1182
  function writeLicenseStore(store) {
1116
1183
  const dirPath = getGlobalLicenseDir();
1117
- if (!fs2.existsSync(dirPath)) {
1118
- fs2.mkdirSync(dirPath, { recursive: true });
1184
+ if (!fs3.existsSync(dirPath)) {
1185
+ fs3.mkdirSync(dirPath, { recursive: true });
1119
1186
  }
1120
- const licensePath = path4.join(dirPath, LICENSE_FILE);
1121
- fs2.writeFileSync(licensePath, JSON.stringify(store, null, 2), "utf-8");
1187
+ const licensePath = path5.join(dirPath, LICENSE_FILE);
1188
+ fs3.writeFileSync(licensePath, JSON.stringify(store, null, 2), "utf-8");
1122
1189
  _cachedStore = store;
1123
1190
  _cacheTimestamp = Date.now();
1124
1191
  }
@@ -1147,7 +1214,14 @@ function getLicenseForFeature(feature) {
1147
1214
  return features?.includes(feature);
1148
1215
  });
1149
1216
  }
1217
+ function getAllLicensesNeedingRevalidation() {
1218
+ return getActiveLicenses().filter((l) => needsRevalidation(l));
1219
+ }
1150
1220
  var REVALIDATION_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
1221
+ function needsRevalidation(entry) {
1222
+ const lastValidated = new Date(entry.lastValidatedAt).getTime();
1223
+ return Date.now() - lastValidated > REVALIDATION_THRESHOLD_MS;
1224
+ }
1151
1225
 
1152
1226
  // ../../packages/shared/src/featureGate.ts
1153
1227
  var DefaultFeatureGate = class {
@@ -1169,23 +1243,23 @@ var DecisionStorage = class {
1169
1243
  decisionsFilePath;
1170
1244
  constructor(workspacePath) {
1171
1245
  const mainRoot = resolveStorageRoot(workspacePath);
1172
- this.storagePath = path5.join(mainRoot, STORAGE_DIR2);
1173
- this.decisionsFilePath = path5.join(this.storagePath, DECISIONS_FILE);
1246
+ this.storagePath = path6.join(mainRoot, STORAGE_DIR2);
1247
+ this.decisionsFilePath = path6.join(this.storagePath, DECISIONS_FILE);
1174
1248
  }
1175
1249
  ensureStorageDir() {
1176
- if (!fs3.existsSync(this.storagePath)) {
1177
- fs3.mkdirSync(this.storagePath, { recursive: true });
1250
+ if (!fs4.existsSync(this.storagePath)) {
1251
+ fs4.mkdirSync(this.storagePath, { recursive: true });
1178
1252
  }
1179
1253
  }
1180
1254
  getProjectName() {
1181
- return path5.basename(path5.dirname(this.storagePath));
1255
+ return path6.basename(path6.dirname(this.storagePath));
1182
1256
  }
1183
1257
  load() {
1184
1258
  try {
1185
- if (!fs3.existsSync(this.decisionsFilePath)) {
1259
+ if (!fs4.existsSync(this.decisionsFilePath)) {
1186
1260
  return createEmptyProjectDecisions(this.getProjectName());
1187
1261
  }
1188
- const raw = fs3.readFileSync(this.decisionsFilePath, "utf-8");
1262
+ const raw = fs4.readFileSync(this.decisionsFilePath, "utf-8");
1189
1263
  const data = JSON.parse(raw);
1190
1264
  return data;
1191
1265
  } catch {
@@ -1195,7 +1269,7 @@ var DecisionStorage = class {
1195
1269
  save(decisions) {
1196
1270
  this.ensureStorageDir();
1197
1271
  const content = JSON.stringify(decisions, null, 2);
1198
- fs3.writeFileSync(this.decisionsFilePath, content, "utf-8");
1272
+ fs4.writeFileSync(this.decisionsFilePath, content, "utf-8");
1199
1273
  }
1200
1274
  /**
1201
1275
  * Save a decision record as a draft. Always persists regardless of Pro
@@ -1446,8 +1520,8 @@ function tryDetectDecision(opts) {
1446
1520
  }
1447
1521
 
1448
1522
  // ../../packages/shared/src/reader.ts
1449
- import fs4 from "fs";
1450
- import path6 from "path";
1523
+ import fs5 from "fs";
1524
+ import path7 from "path";
1451
1525
  var STORAGE_DIR3 = ".keepgoing";
1452
1526
  var META_FILE2 = "meta.json";
1453
1527
  var SESSIONS_FILE2 = "sessions.json";
@@ -1469,16 +1543,16 @@ var KeepGoingReader = class {
1469
1543
  this.workspacePath = workspacePath;
1470
1544
  const mainRoot = resolveStorageRoot(workspacePath);
1471
1545
  this._isWorktree = mainRoot !== workspacePath;
1472
- this.storagePath = path6.join(mainRoot, STORAGE_DIR3);
1473
- this.metaFilePath = path6.join(this.storagePath, META_FILE2);
1474
- this.sessionsFilePath = path6.join(this.storagePath, SESSIONS_FILE2);
1475
- this.decisionsFilePath = path6.join(this.storagePath, DECISIONS_FILE2);
1476
- this.stateFilePath = path6.join(this.storagePath, STATE_FILE2);
1477
- this.currentTasksFilePath = path6.join(this.storagePath, CURRENT_TASKS_FILE2);
1546
+ this.storagePath = path7.join(mainRoot, STORAGE_DIR3);
1547
+ this.metaFilePath = path7.join(this.storagePath, META_FILE2);
1548
+ this.sessionsFilePath = path7.join(this.storagePath, SESSIONS_FILE2);
1549
+ this.decisionsFilePath = path7.join(this.storagePath, DECISIONS_FILE2);
1550
+ this.stateFilePath = path7.join(this.storagePath, STATE_FILE2);
1551
+ this.currentTasksFilePath = path7.join(this.storagePath, CURRENT_TASKS_FILE2);
1478
1552
  }
1479
1553
  /** Check if .keepgoing/ directory exists. */
1480
1554
  exists() {
1481
- return fs4.existsSync(this.storagePath);
1555
+ return fs5.existsSync(this.storagePath);
1482
1556
  }
1483
1557
  /** Read state.json, returns undefined if missing or corrupt. */
1484
1558
  getState() {
@@ -1716,10 +1790,10 @@ var KeepGoingReader = class {
1716
1790
  }
1717
1791
  readJsonFile(filePath) {
1718
1792
  try {
1719
- if (!fs4.existsSync(filePath)) {
1793
+ if (!fs5.existsSync(filePath)) {
1720
1794
  return void 0;
1721
1795
  }
1722
- const raw = fs4.readFileSync(filePath, "utf-8");
1796
+ const raw = fs5.readFileSync(filePath, "utf-8");
1723
1797
  return JSON.parse(raw);
1724
1798
  } catch {
1725
1799
  return void 0;
@@ -1728,9 +1802,9 @@ var KeepGoingReader = class {
1728
1802
  };
1729
1803
 
1730
1804
  // ../../packages/shared/src/setup.ts
1731
- import fs5 from "fs";
1732
- import os2 from "os";
1733
- import path7 from "path";
1805
+ import fs6 from "fs";
1806
+ import os3 from "os";
1807
+ import path8 from "path";
1734
1808
  var KEEPGOING_MARKER = "@keepgoingdev/mcp-server";
1735
1809
  var SESSION_START_HOOK = {
1736
1810
  matcher: "",
@@ -1759,36 +1833,55 @@ var POST_TOOL_USE_HOOK = {
1759
1833
  }
1760
1834
  ]
1761
1835
  };
1762
- var CLAUDE_MD_SECTION = `
1836
+ var SESSION_END_HOOK = {
1837
+ matcher: "",
1838
+ hooks: [
1839
+ {
1840
+ type: "command",
1841
+ command: "npx -y @keepgoingdev/mcp-server --save-checkpoint"
1842
+ }
1843
+ ]
1844
+ };
1845
+ var KEEPGOING_RULES_VERSION = 1;
1846
+ var KEEPGOING_RULES_CONTENT = `<!-- @keepgoingdev/mcp-server v${KEEPGOING_RULES_VERSION} -->
1763
1847
  ## KeepGoing
1764
1848
 
1765
1849
  After completing a task or meaningful piece of work, call the \`save_checkpoint\` MCP tool with:
1766
- - \`summary\`: What you accomplished
1767
- - \`nextStep\`: What should be done next
1850
+ - \`summary\`: 1-2 sentences. What changed and why \u2014 no file paths, no implementation details (those are captured from git).
1851
+ - \`nextStep\`: What to do next
1768
1852
  - \`blocker\`: Any blocker (if applicable)
1769
1853
  `;
1854
+ function getRulesFileVersion(content) {
1855
+ const match = content.match(/<!-- @keepgoingdev\/mcp-server v(\d+) -->/);
1856
+ return match ? parseInt(match[1], 10) : null;
1857
+ }
1770
1858
  var STATUSLINE_CMD = "npx -y @keepgoingdev/mcp-server --statusline";
1859
+ function detectClaudeDir() {
1860
+ return process.env.CLAUDE_CONFIG_DIR || path8.join(os3.homedir(), ".claude");
1861
+ }
1771
1862
  function hasKeepGoingHook(hookEntries) {
1772
1863
  return hookEntries.some(
1773
1864
  (entry) => entry?.hooks?.some((h) => typeof h?.command === "string" && h.command.includes(KEEPGOING_MARKER))
1774
1865
  );
1775
1866
  }
1776
- function resolveScopePaths(scope, workspacePath) {
1867
+ function resolveScopePaths(scope, workspacePath, overrideClaudeDir) {
1777
1868
  if (scope === "user") {
1778
- const claudeDir2 = path7.join(os2.homedir(), ".claude");
1869
+ const claudeDir2 = overrideClaudeDir || detectClaudeDir();
1779
1870
  return {
1780
1871
  claudeDir: claudeDir2,
1781
- settingsPath: path7.join(claudeDir2, "settings.json"),
1782
- claudeMdPath: path7.join(claudeDir2, "CLAUDE.md")
1872
+ settingsPath: path8.join(claudeDir2, "settings.json"),
1873
+ claudeMdPath: path8.join(claudeDir2, "CLAUDE.md"),
1874
+ rulesPath: path8.join(claudeDir2, "rules", "keepgoing.md")
1783
1875
  };
1784
1876
  }
1785
- const claudeDir = path7.join(workspacePath, ".claude");
1786
- const dotClaudeMdPath = path7.join(workspacePath, ".claude", "CLAUDE.md");
1787
- const rootClaudeMdPath = path7.join(workspacePath, "CLAUDE.md");
1877
+ const claudeDir = path8.join(workspacePath, ".claude");
1878
+ const dotClaudeMdPath = path8.join(workspacePath, ".claude", "CLAUDE.md");
1879
+ const rootClaudeMdPath = path8.join(workspacePath, "CLAUDE.md");
1788
1880
  return {
1789
1881
  claudeDir,
1790
- settingsPath: path7.join(claudeDir, "settings.json"),
1791
- claudeMdPath: fs5.existsSync(dotClaudeMdPath) ? dotClaudeMdPath : rootClaudeMdPath
1882
+ settingsPath: path8.join(claudeDir, "settings.json"),
1883
+ claudeMdPath: fs6.existsSync(dotClaudeMdPath) ? dotClaudeMdPath : rootClaudeMdPath,
1884
+ rulesPath: path8.join(workspacePath, ".claude", "rules", "keepgoing.md")
1792
1885
  };
1793
1886
  }
1794
1887
  function writeHooksToSettings(settings) {
@@ -1817,15 +1910,22 @@ function writeHooksToSettings(settings) {
1817
1910
  settings.hooks.PostToolUse.push(POST_TOOL_USE_HOOK);
1818
1911
  changed = true;
1819
1912
  }
1913
+ if (!Array.isArray(settings.hooks.SessionEnd)) {
1914
+ settings.hooks.SessionEnd = [];
1915
+ }
1916
+ if (!hasKeepGoingHook(settings.hooks.SessionEnd)) {
1917
+ settings.hooks.SessionEnd.push(SESSION_END_HOOK);
1918
+ changed = true;
1919
+ }
1820
1920
  return changed;
1821
1921
  }
1822
1922
  function checkHookConflict(scope, workspacePath) {
1823
1923
  const otherPaths = resolveScopePaths(scope === "user" ? "project" : "user", workspacePath);
1824
- if (!fs5.existsSync(otherPaths.settingsPath)) {
1924
+ if (!fs6.existsSync(otherPaths.settingsPath)) {
1825
1925
  return null;
1826
1926
  }
1827
1927
  try {
1828
- const otherSettings = JSON.parse(fs5.readFileSync(otherPaths.settingsPath, "utf-8"));
1928
+ const otherSettings = JSON.parse(fs6.readFileSync(otherPaths.settingsPath, "utf-8"));
1829
1929
  const hooks = otherSettings?.hooks;
1830
1930
  if (!hooks) return null;
1831
1931
  const hasConflict = Array.isArray(hooks.SessionStart) && hasKeepGoingHook(hooks.SessionStart) || Array.isArray(hooks.Stop) && hasKeepGoingHook(hooks.Stop);
@@ -1844,15 +1944,20 @@ function setupProject(options) {
1844
1944
  scope = "project",
1845
1945
  sessionHooks = true,
1846
1946
  claudeMd = true,
1947
+ claudeDir: claudeDirOverride,
1847
1948
  statusline
1848
1949
  } = options;
1849
1950
  const messages = [];
1850
1951
  let changed = false;
1851
- const { claudeDir, settingsPath, claudeMdPath } = resolveScopePaths(scope, workspacePath);
1852
- const scopeLabel = scope === "user" ? "~/.claude/settings.json" : ".claude/settings.json";
1952
+ const { claudeDir, settingsPath, claudeMdPath, rulesPath } = resolveScopePaths(
1953
+ scope,
1954
+ workspacePath,
1955
+ claudeDirOverride
1956
+ );
1957
+ const scopeLabel = scope === "user" ? path8.join("~", path8.relative(os3.homedir(), claudeDir), "settings.json").replace(/\\/g, "/") : ".claude/settings.json";
1853
1958
  let settings = {};
1854
- if (fs5.existsSync(settingsPath)) {
1855
- settings = JSON.parse(fs5.readFileSync(settingsPath, "utf-8"));
1959
+ if (fs6.existsSync(settingsPath)) {
1960
+ settings = JSON.parse(fs6.readFileSync(settingsPath, "utf-8"));
1856
1961
  }
1857
1962
  let settingsChanged = false;
1858
1963
  if (sessionHooks) {
@@ -1883,29 +1988,45 @@ function setupProject(options) {
1883
1988
  statusline?.cleanup?.();
1884
1989
  }
1885
1990
  if (settingsChanged) {
1886
- if (!fs5.existsSync(claudeDir)) {
1887
- fs5.mkdirSync(claudeDir, { recursive: true });
1991
+ if (!fs6.existsSync(claudeDir)) {
1992
+ fs6.mkdirSync(claudeDir, { recursive: true });
1888
1993
  }
1889
- fs5.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
1994
+ fs6.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
1890
1995
  changed = true;
1891
1996
  }
1892
1997
  if (claudeMd) {
1893
- let existing = "";
1894
- if (fs5.existsSync(claudeMdPath)) {
1895
- existing = fs5.readFileSync(claudeMdPath, "utf-8");
1896
- }
1897
- const mdLabel = scope === "user" ? "~/.claude/CLAUDE.md" : "CLAUDE.md";
1898
- if (existing.includes("## KeepGoing")) {
1899
- messages.push(`CLAUDE.md: KeepGoing section already present in ${mdLabel}, skipped`);
1998
+ const rulesDir = path8.dirname(rulesPath);
1999
+ const rulesLabel = scope === "user" ? path8.join(path8.relative(os3.homedir(), path8.dirname(rulesPath)), "keepgoing.md").replace(/\\/g, "/") : ".claude/rules/keepgoing.md";
2000
+ if (fs6.existsSync(rulesPath)) {
2001
+ const existing = fs6.readFileSync(rulesPath, "utf-8");
2002
+ const existingVersion = getRulesFileVersion(existing);
2003
+ if (existingVersion === null) {
2004
+ messages.push(`Rules file: Custom file found at ${rulesLabel}, skipping`);
2005
+ } else if (existingVersion >= KEEPGOING_RULES_VERSION) {
2006
+ messages.push(`Rules file: Already up to date (v${existingVersion}), skipped`);
2007
+ } else {
2008
+ if (!fs6.existsSync(rulesDir)) {
2009
+ fs6.mkdirSync(rulesDir, { recursive: true });
2010
+ }
2011
+ fs6.writeFileSync(rulesPath, KEEPGOING_RULES_CONTENT);
2012
+ changed = true;
2013
+ messages.push(`Rules file: Updated v${existingVersion} \u2192 v${KEEPGOING_RULES_VERSION} at ${rulesLabel}`);
2014
+ }
1900
2015
  } else {
1901
- const updated = existing + CLAUDE_MD_SECTION;
1902
- const mdDir = path7.dirname(claudeMdPath);
1903
- if (!fs5.existsSync(mdDir)) {
1904
- fs5.mkdirSync(mdDir, { recursive: true });
2016
+ const existingClaudeMd = fs6.existsSync(claudeMdPath) ? fs6.readFileSync(claudeMdPath, "utf-8") : "";
2017
+ if (!fs6.existsSync(rulesDir)) {
2018
+ fs6.mkdirSync(rulesDir, { recursive: true });
1905
2019
  }
1906
- fs5.writeFileSync(claudeMdPath, updated);
2020
+ fs6.writeFileSync(rulesPath, KEEPGOING_RULES_CONTENT);
1907
2021
  changed = true;
1908
- messages.push(`CLAUDE.md: Added KeepGoing section to ${mdLabel}`);
2022
+ if (existingClaudeMd.includes("## KeepGoing")) {
2023
+ const mdLabel = scope === "user" ? "~/.claude/CLAUDE.md" : "CLAUDE.md";
2024
+ messages.push(
2025
+ `Rules file: Created ${rulesLabel} (you can now remove the ## KeepGoing section from ${mdLabel})`
2026
+ );
2027
+ } else {
2028
+ messages.push(`Rules file: Created ${rulesLabel}`);
2029
+ }
1909
2030
  }
1910
2031
  }
1911
2032
  return { messages, changed };
@@ -1982,6 +2103,39 @@ async function activateLicense(licenseKey, instanceName, options) {
1982
2103
  return { valid: false, error: message };
1983
2104
  }
1984
2105
  }
2106
+ async function validateLicense(licenseKey, instanceId, options) {
2107
+ try {
2108
+ const res = await fetchWithTimeout(`${BASE_URL}/validate`, {
2109
+ method: "POST",
2110
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
2111
+ body: new URLSearchParams({ license_key: licenseKey, instance_id: instanceId })
2112
+ });
2113
+ const data = await safeJson(res);
2114
+ if (!res.ok || !data?.valid) {
2115
+ return { valid: false, isNetworkError: false, error: data?.error || `Validation failed (${res.status})` };
2116
+ }
2117
+ if (!options?.allowTestMode && data.license_key?.test_mode) {
2118
+ return { valid: false, isNetworkError: false, error: "This is a test license key. Please use a production license key from your purchase confirmation." };
2119
+ }
2120
+ if (!options?.allowTestMode) {
2121
+ const productError = validateProductIdentity(data.meta);
2122
+ if (productError) {
2123
+ return { valid: false, isNetworkError: false, error: productError };
2124
+ }
2125
+ }
2126
+ return {
2127
+ valid: true,
2128
+ licenseKey: data.license_key?.key,
2129
+ customerName: data.meta?.customer_name,
2130
+ productName: data.meta?.product_name,
2131
+ variantId: data.meta?.variant_id,
2132
+ variantName: data.meta?.variant_name
2133
+ };
2134
+ } catch (err) {
2135
+ const message = err instanceof Error && err.name === "AbortError" ? "Request timed out. Please check your network connection and try again." : err instanceof Error ? err.message : "Network error";
2136
+ return { valid: false, isNetworkError: true, error: message };
2137
+ }
2138
+ }
1985
2139
  async function deactivateLicense(licenseKey, instanceId) {
1986
2140
  try {
1987
2141
  const res = await fetchWithTimeout(`${BASE_URL}/deactivate`, {
@@ -2000,6 +2154,52 @@ async function deactivateLicense(licenseKey, instanceId) {
2000
2154
  }
2001
2155
  }
2002
2156
 
2157
+ // ../../packages/shared/src/licenseRevalidation.ts
2158
+ async function revalidateStaleLicenses(options) {
2159
+ const stale = getAllLicensesNeedingRevalidation();
2160
+ const result = { checked: stale.length, refreshed: 0, revoked: 0, skippedNetworkError: 0 };
2161
+ if (stale.length === 0) return result;
2162
+ const updates = /* @__PURE__ */ new Map();
2163
+ for (const entry of stale) {
2164
+ const vResult = await validateLicense(entry.licenseKey, entry.instanceId, {
2165
+ allowTestMode: options?.allowTestMode
2166
+ });
2167
+ if (vResult.valid) {
2168
+ const patch = {
2169
+ lastValidatedAt: (/* @__PURE__ */ new Date()).toISOString()
2170
+ };
2171
+ if (vResult.customerName) patch.customerName = vResult.customerName;
2172
+ updates.set(entry.licenseKey, patch);
2173
+ result.refreshed++;
2174
+ } else if (vResult.isNetworkError) {
2175
+ result.skippedNetworkError++;
2176
+ } else {
2177
+ updates.set(entry.licenseKey, { status: "inactive" });
2178
+ result.revoked++;
2179
+ }
2180
+ }
2181
+ if (updates.size > 0) {
2182
+ const store = readLicenseStore();
2183
+ for (const [key, patch] of updates) {
2184
+ const idx = store.licenses.findIndex((l) => l.licenseKey === key);
2185
+ if (idx >= 0) {
2186
+ Object.assign(store.licenses[idx], patch);
2187
+ }
2188
+ }
2189
+ writeLicenseStore(store);
2190
+ }
2191
+ return result;
2192
+ }
2193
+ async function getLicenseForFeatureWithRevalidation(feature, options) {
2194
+ const license = getLicenseForFeature(feature);
2195
+ if (!license) return void 0;
2196
+ if (needsRevalidation(license)) {
2197
+ await revalidateStaleLicenses(options);
2198
+ return getLicenseForFeature(feature);
2199
+ }
2200
+ return license;
2201
+ }
2202
+
2003
2203
  // src/tools/getMomentum.ts
2004
2204
  import { z } from "zod";
2005
2205
  function registerGetMomentum(server, reader, workspacePath) {
@@ -2253,7 +2453,7 @@ function registerGetReentryBriefing(server, reader, workspacePath) {
2253
2453
  }
2254
2454
 
2255
2455
  // src/tools/saveCheckpoint.ts
2256
- import path8 from "path";
2456
+ import path9 from "path";
2257
2457
  import { z as z4 } from "zod";
2258
2458
  function registerSaveCheckpoint(server, reader, workspacePath) {
2259
2459
  server.tool(
@@ -2269,7 +2469,7 @@ function registerSaveCheckpoint(server, reader, workspacePath) {
2269
2469
  const gitBranch = getCurrentBranch(workspacePath);
2270
2470
  const touchedFiles = getTouchedFiles(workspacePath);
2271
2471
  const commitHashes = getCommitsSince(workspacePath, lastSession?.timestamp);
2272
- const projectName = path8.basename(resolveStorageRoot(workspacePath));
2472
+ const projectName = path9.basename(resolveStorageRoot(workspacePath));
2273
2473
  const sessionId = generateSessionId({ workspaceRoot: workspacePath, branch: gitBranch ?? void 0, worktreePath: workspacePath });
2274
2474
  const checkpoint = createCheckpoint({
2275
2475
  summary,
@@ -2336,7 +2536,7 @@ function registerGetDecisions(server, reader) {
2336
2536
  ]
2337
2537
  };
2338
2538
  }
2339
- if (process.env.KEEPGOING_PRO_BYPASS !== "1" && !getLicenseForFeature("decisions")) {
2539
+ if (process.env.KEEPGOING_PRO_BYPASS !== "1" && !await getLicenseForFeatureWithRevalidation("decisions")) {
2340
2540
  return {
2341
2541
  content: [
2342
2542
  {
@@ -2389,7 +2589,7 @@ function registerGetDecisions(server, reader) {
2389
2589
  function registerGetCurrentTask(server, reader) {
2390
2590
  server.tool(
2391
2591
  "get_current_task",
2392
- "Get current live session tasks. Shows all active AI agent sessions, what each is doing, last files edited, and next steps. Supports multiple concurrent sessions.",
2592
+ "Get a bird's eye view of all active Claude sessions. See what each session is working on, which branch it is on, and when it last did something. Useful when running multiple parallel sessions across worktrees.",
2393
2593
  {},
2394
2594
  async () => {
2395
2595
  if (!reader.exists()) {
@@ -2402,7 +2602,7 @@ function registerGetCurrentTask(server, reader) {
2402
2602
  ]
2403
2603
  };
2404
2604
  }
2405
- if (process.env.KEEPGOING_PRO_BYPASS !== "1" && !getLicenseForFeature("session-awareness")) {
2605
+ if (process.env.KEEPGOING_PRO_BYPASS !== "1" && !await getLicenseForFeatureWithRevalidation("session-awareness")) {
2406
2606
  return {
2407
2607
  content: [
2408
2608
  {
@@ -2438,12 +2638,15 @@ function registerGetCurrentTask(server, reader) {
2438
2638
  for (const task of [...activeTasks, ...finishedTasks]) {
2439
2639
  const statusIcon = task.sessionActive ? "\u{1F7E2}" : "\u2705";
2440
2640
  const statusLabel = task.sessionActive ? "Active" : "Finished";
2441
- const sessionLabel = task.agentLabel || task.sessionId || "Session";
2641
+ const sessionLabel = task.sessionLabel || task.agentLabel || task.sessionId || "Session";
2442
2642
  lines.push(`### ${statusIcon} ${sessionLabel} (${statusLabel})`);
2443
2643
  lines.push(`- **Updated:** ${formatRelativeTime(task.updatedAt)}`);
2444
2644
  if (task.branch) {
2445
2645
  lines.push(`- **Branch:** ${task.branch}`);
2446
2646
  }
2647
+ if (task.agentLabel && task.sessionLabel) {
2648
+ lines.push(`- **Agent:** ${task.agentLabel}`);
2649
+ }
2447
2650
  if (task.taskSummary) {
2448
2651
  lines.push(`- **Doing:** ${task.taskSummary}`);
2449
2652
  }
@@ -2484,25 +2687,25 @@ function registerGetCurrentTask(server, reader) {
2484
2687
  import { z as z6 } from "zod";
2485
2688
 
2486
2689
  // src/cli/migrate.ts
2487
- import fs6 from "fs";
2488
- import os3 from "os";
2489
- import path9 from "path";
2690
+ import fs7 from "fs";
2691
+ import os4 from "os";
2692
+ import path10 from "path";
2490
2693
  var STATUSLINE_CMD2 = "npx -y @keepgoingdev/mcp-server --statusline";
2491
2694
  function isLegacyStatusline(command) {
2492
2695
  return !command.includes("--statusline") && command.includes("keepgoing-statusline");
2493
2696
  }
2494
2697
  function migrateStatusline(wsPath) {
2495
- const settingsPath = path9.join(wsPath, ".claude", "settings.json");
2496
- if (!fs6.existsSync(settingsPath)) return void 0;
2698
+ const settingsPath = path10.join(wsPath, ".claude", "settings.json");
2699
+ if (!fs7.existsSync(settingsPath)) return void 0;
2497
2700
  try {
2498
- const settings = JSON.parse(fs6.readFileSync(settingsPath, "utf-8"));
2701
+ const settings = JSON.parse(fs7.readFileSync(settingsPath, "utf-8"));
2499
2702
  const cmd = settings.statusLine?.command;
2500
2703
  if (!cmd || !isLegacyStatusline(cmd)) return void 0;
2501
2704
  settings.statusLine = {
2502
2705
  type: "command",
2503
2706
  command: STATUSLINE_CMD2
2504
2707
  };
2505
- fs6.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
2708
+ fs7.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
2506
2709
  cleanupLegacyScript();
2507
2710
  return "[KeepGoing] Migrated statusline to auto-updating command (restart Claude Code to apply)";
2508
2711
  } catch {
@@ -2510,10 +2713,10 @@ function migrateStatusline(wsPath) {
2510
2713
  }
2511
2714
  }
2512
2715
  function cleanupLegacyScript() {
2513
- const legacyScript = path9.join(os3.homedir(), ".claude", "keepgoing-statusline.sh");
2514
- if (fs6.existsSync(legacyScript)) {
2716
+ const legacyScript = path10.join(os4.homedir(), ".claude", "keepgoing-statusline.sh");
2717
+ if (fs7.existsSync(legacyScript)) {
2515
2718
  try {
2516
- fs6.unlinkSync(legacyScript);
2719
+ fs7.unlinkSync(legacyScript);
2517
2720
  } catch {
2518
2721
  }
2519
2722
  }
@@ -2526,15 +2729,17 @@ function registerSetupProject(server, workspacePath) {
2526
2729
  'Set up KeepGoing hooks and instructions. Use scope "user" for global setup (all projects) or "project" for per-project setup.',
2527
2730
  {
2528
2731
  sessionHooks: z6.boolean().optional().default(true).describe("Add session hooks to settings.json"),
2529
- claudeMd: z6.boolean().optional().default(true).describe("Add KeepGoing instructions to CLAUDE.md"),
2530
- scope: z6.enum(["project", "user"]).optional().default("project").describe('Where to write config: "user" for global (~/.claude/), "project" for per-project (.claude/)')
2732
+ claudeMd: z6.boolean().optional().default(true).describe("Add KeepGoing instructions to .claude/rules/keepgoing.md"),
2733
+ scope: z6.enum(["project", "user"]).optional().default("project").describe('Where to write config: "user" for global (~/.claude/), "project" for per-project (.claude/)'),
2734
+ claudeDir: z6.string().optional().describe("Override the Claude config directory for user scope (defaults to CLAUDE_CONFIG_DIR env var or ~/.claude)")
2531
2735
  },
2532
- async ({ sessionHooks, claudeMd, scope }) => {
2736
+ async ({ sessionHooks, claudeMd, scope, claudeDir }) => {
2533
2737
  const result = setupProject({
2534
2738
  workspacePath,
2535
2739
  scope,
2536
2740
  sessionHooks,
2537
2741
  claudeMd,
2742
+ claudeDir,
2538
2743
  statusline: {
2539
2744
  isLegacy: isLegacyStatusline,
2540
2745
  cleanup: cleanupLegacyScript
@@ -2872,7 +3077,7 @@ async function handlePrintMomentum() {
2872
3077
  process.exit(0);
2873
3078
  }
2874
3079
  async function handlePrintCurrent() {
2875
- if (process.env.KEEPGOING_PRO_BYPASS !== "1" && !getLicenseForFeature("session-awareness")) {
3080
+ if (process.env.KEEPGOING_PRO_BYPASS !== "1" && !await getLicenseForFeatureWithRevalidation("session-awareness")) {
2876
3081
  process.exit(0);
2877
3082
  }
2878
3083
  const wsPath = resolveWsPath();
@@ -2909,7 +3114,7 @@ async function handlePrintCurrent() {
2909
3114
  }
2910
3115
 
2911
3116
  // src/cli/saveCheckpoint.ts
2912
- import path10 from "path";
3117
+ import path11 from "path";
2913
3118
  async function handleSaveCheckpoint() {
2914
3119
  const wsPath = resolveWsPath();
2915
3120
  const reader = new KeepGoingReader(wsPath);
@@ -2937,9 +3142,9 @@ async function handleSaveCheckpoint() {
2937
3142
  sessionStartTime: lastSession?.timestamp ?? now,
2938
3143
  lastActivityTime: now
2939
3144
  });
2940
- const summary = buildSmartSummary(events) ?? `Worked on ${touchedFiles.slice(0, 5).map((f) => path10.basename(f)).join(", ")}`;
3145
+ const summary = buildSmartSummary(events) ?? `Worked on ${touchedFiles.slice(0, 5).map((f) => path11.basename(f)).join(", ")}`;
2941
3146
  const nextStep = buildSmartNextStep(events);
2942
- const projectName = path10.basename(resolveStorageRoot(wsPath));
3147
+ const projectName = path11.basename(resolveStorageRoot(wsPath));
2943
3148
  const sessionId = generateSessionId({ workspaceRoot: wsPath, branch: gitBranch ?? void 0, worktreePath: wsPath });
2944
3149
  const checkpoint = createCheckpoint({
2945
3150
  summary,
@@ -2960,16 +3165,19 @@ async function handleSaveCheckpoint() {
2960
3165
  branch: gitBranch ?? void 0,
2961
3166
  updatedAt: checkpoint.timestamp
2962
3167
  });
2963
- if ((process.env.KEEPGOING_PRO_BYPASS === "1" || getLicenseForFeature("decisions")) && commitMessages.length > 0) {
2964
- const headHash = getHeadCommitHash(wsPath) || commitHashes[0];
2965
- if (headHash) {
3168
+ if (process.env.KEEPGOING_PRO_BYPASS === "1" || await getLicenseForFeatureWithRevalidation("decisions")) {
3169
+ for (let i = 0; i < commitHashes.length; i++) {
3170
+ const hash = commitHashes[i];
3171
+ const message = commitMessages[i];
3172
+ if (!hash || !message) continue;
3173
+ const files = getFilesChangedInCommit(wsPath, hash);
2966
3174
  const detected = tryDetectDecision({
2967
3175
  workspacePath: wsPath,
2968
3176
  checkpointId: checkpoint.id,
2969
3177
  gitBranch,
2970
- commitHash: headHash,
2971
- commitMessage: commitMessages[0],
2972
- filesChanged: touchedFiles
3178
+ commitHash: hash,
3179
+ commitMessage: message,
3180
+ filesChanged: files
2973
3181
  });
2974
3182
  if (detected) {
2975
3183
  console.log(`[KeepGoing] Decision detected: ${detected.category} (${(detected.confidence * 100).toFixed(0)}% confidence)`);
@@ -2981,7 +3189,7 @@ async function handleSaveCheckpoint() {
2981
3189
  }
2982
3190
 
2983
3191
  // src/cli/transcriptUtils.ts
2984
- import fs7 from "fs";
3192
+ import fs8 from "fs";
2985
3193
  var TAIL_READ_BYTES = 8192;
2986
3194
  var TOOL_VERB_MAP = {
2987
3195
  Edit: "editing",
@@ -3032,9 +3240,9 @@ function isAssistantEntry(entry) {
3032
3240
  return entry.message?.role === "assistant";
3033
3241
  }
3034
3242
  function extractSessionLabel(transcriptPath) {
3035
- if (!transcriptPath || !fs7.existsSync(transcriptPath)) return null;
3243
+ if (!transcriptPath || !fs8.existsSync(transcriptPath)) return null;
3036
3244
  try {
3037
- const raw = fs7.readFileSync(transcriptPath, "utf-8");
3245
+ const raw = fs8.readFileSync(transcriptPath, "utf-8");
3038
3246
  for (const line of raw.split("\n")) {
3039
3247
  const trimmed = line.trim();
3040
3248
  if (!trimmed) continue;
@@ -3063,19 +3271,19 @@ function extractSessionLabel(transcriptPath) {
3063
3271
  return null;
3064
3272
  }
3065
3273
  function extractCurrentAction(transcriptPath) {
3066
- if (!transcriptPath || !fs7.existsSync(transcriptPath)) return null;
3274
+ if (!transcriptPath || !fs8.existsSync(transcriptPath)) return null;
3067
3275
  try {
3068
- const stat = fs7.statSync(transcriptPath);
3276
+ const stat = fs8.statSync(transcriptPath);
3069
3277
  const fileSize = stat.size;
3070
3278
  if (fileSize === 0) return null;
3071
3279
  const readSize = Math.min(fileSize, TAIL_READ_BYTES);
3072
3280
  const offset = fileSize - readSize;
3073
3281
  const buf = Buffer.alloc(readSize);
3074
- const fd = fs7.openSync(transcriptPath, "r");
3282
+ const fd = fs8.openSync(transcriptPath, "r");
3075
3283
  try {
3076
- fs7.readSync(fd, buf, 0, readSize, offset);
3284
+ fs8.readSync(fd, buf, 0, readSize, offset);
3077
3285
  } finally {
3078
- fs7.closeSync(fd);
3286
+ fs8.closeSync(fd);
3079
3287
  }
3080
3288
  const tail = buf.toString("utf-8");
3081
3289
  const lines = tail.split("\n").reverse();
@@ -3177,8 +3385,8 @@ async function handleUpdateTaskFromHook() {
3177
3385
  }
3178
3386
 
3179
3387
  // src/cli/statusline.ts
3180
- import fs8 from "fs";
3181
- import path11 from "path";
3388
+ import fs9 from "fs";
3389
+ import path12 from "path";
3182
3390
  var STDIN_TIMEOUT_MS2 = 3e3;
3183
3391
  async function handleStatusline() {
3184
3392
  const chunks = [];
@@ -3205,9 +3413,9 @@ async function handleStatusline() {
3205
3413
  if (!label) {
3206
3414
  try {
3207
3415
  const gitRoot = findGitRoot(dir);
3208
- const tasksFile = path11.join(gitRoot, ".keepgoing", "current-tasks.json");
3209
- if (fs8.existsSync(tasksFile)) {
3210
- const data = JSON.parse(fs8.readFileSync(tasksFile, "utf-8"));
3416
+ const tasksFile = path12.join(gitRoot, ".keepgoing", "current-tasks.json");
3417
+ if (fs9.existsSync(tasksFile)) {
3418
+ const data = JSON.parse(fs9.readFileSync(tasksFile, "utf-8"));
3211
3419
  const tasks = pruneStaleTasks(data.tasks ?? []);
3212
3420
  const match = sessionId ? tasks.find((t) => t.sessionId === sessionId) : void 0;
3213
3421
  if (match?.sessionLabel) {
@@ -3254,6 +3462,35 @@ async function handleContinueOn() {
3254
3462
  process.exit(0);
3255
3463
  }
3256
3464
 
3465
+ // src/cli/detectDecisions.ts
3466
+ async function handleDetectDecisions() {
3467
+ const wsPath = resolveWsPath();
3468
+ if (!(process.env.KEEPGOING_PRO_BYPASS === "1" || await getLicenseForFeatureWithRevalidation("decisions"))) {
3469
+ process.exit(0);
3470
+ }
3471
+ const reader = new KeepGoingReader(wsPath);
3472
+ if (!reader.exists()) {
3473
+ process.exit(0);
3474
+ }
3475
+ const gitBranch = getCurrentBranch(wsPath);
3476
+ const headHash = getHeadCommitHash(wsPath);
3477
+ if (!headHash) process.exit(0);
3478
+ const commitMessage = getCommitMessageByHash(wsPath, headHash);
3479
+ if (!commitMessage) process.exit(0);
3480
+ const files = getFilesChangedInCommit(wsPath, headHash);
3481
+ const detected = tryDetectDecision({
3482
+ workspacePath: wsPath,
3483
+ gitBranch,
3484
+ commitHash: headHash,
3485
+ commitMessage,
3486
+ filesChanged: files
3487
+ });
3488
+ if (detected) {
3489
+ console.log(`[KeepGoing] Decision detected: ${detected.category} (${(detected.confidence * 100).toFixed(0)}% confidence)`);
3490
+ }
3491
+ process.exit(0);
3492
+ }
3493
+
3257
3494
  // src/index.ts
3258
3495
  var CLI_HANDLERS = {
3259
3496
  "--print-momentum": handlePrintMomentum,
@@ -3262,7 +3499,8 @@ var CLI_HANDLERS = {
3262
3499
  "--update-task-from-hook": handleUpdateTaskFromHook,
3263
3500
  "--print-current": handlePrintCurrent,
3264
3501
  "--statusline": handleStatusline,
3265
- "--continue-on": handleContinueOn
3502
+ "--continue-on": handleContinueOn,
3503
+ "--detect-decisions": handleDetectDecisions
3266
3504
  };
3267
3505
  var flag = process.argv.slice(2).find((a) => a in CLI_HANDLERS);
3268
3506
  if (flag) {