@keepgoingdev/mcp-server 0.6.1 → 0.7.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.
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
  }
@@ -1169,23 +1236,23 @@ var DecisionStorage = class {
1169
1236
  decisionsFilePath;
1170
1237
  constructor(workspacePath) {
1171
1238
  const mainRoot = resolveStorageRoot(workspacePath);
1172
- this.storagePath = path5.join(mainRoot, STORAGE_DIR2);
1173
- this.decisionsFilePath = path5.join(this.storagePath, DECISIONS_FILE);
1239
+ this.storagePath = path6.join(mainRoot, STORAGE_DIR2);
1240
+ this.decisionsFilePath = path6.join(this.storagePath, DECISIONS_FILE);
1174
1241
  }
1175
1242
  ensureStorageDir() {
1176
- if (!fs3.existsSync(this.storagePath)) {
1177
- fs3.mkdirSync(this.storagePath, { recursive: true });
1243
+ if (!fs4.existsSync(this.storagePath)) {
1244
+ fs4.mkdirSync(this.storagePath, { recursive: true });
1178
1245
  }
1179
1246
  }
1180
1247
  getProjectName() {
1181
- return path5.basename(path5.dirname(this.storagePath));
1248
+ return path6.basename(path6.dirname(this.storagePath));
1182
1249
  }
1183
1250
  load() {
1184
1251
  try {
1185
- if (!fs3.existsSync(this.decisionsFilePath)) {
1252
+ if (!fs4.existsSync(this.decisionsFilePath)) {
1186
1253
  return createEmptyProjectDecisions(this.getProjectName());
1187
1254
  }
1188
- const raw = fs3.readFileSync(this.decisionsFilePath, "utf-8");
1255
+ const raw = fs4.readFileSync(this.decisionsFilePath, "utf-8");
1189
1256
  const data = JSON.parse(raw);
1190
1257
  return data;
1191
1258
  } catch {
@@ -1195,7 +1262,7 @@ var DecisionStorage = class {
1195
1262
  save(decisions) {
1196
1263
  this.ensureStorageDir();
1197
1264
  const content = JSON.stringify(decisions, null, 2);
1198
- fs3.writeFileSync(this.decisionsFilePath, content, "utf-8");
1265
+ fs4.writeFileSync(this.decisionsFilePath, content, "utf-8");
1199
1266
  }
1200
1267
  /**
1201
1268
  * Save a decision record as a draft. Always persists regardless of Pro
@@ -1446,8 +1513,8 @@ function tryDetectDecision(opts) {
1446
1513
  }
1447
1514
 
1448
1515
  // ../../packages/shared/src/reader.ts
1449
- import fs4 from "fs";
1450
- import path6 from "path";
1516
+ import fs5 from "fs";
1517
+ import path7 from "path";
1451
1518
  var STORAGE_DIR3 = ".keepgoing";
1452
1519
  var META_FILE2 = "meta.json";
1453
1520
  var SESSIONS_FILE2 = "sessions.json";
@@ -1469,16 +1536,16 @@ var KeepGoingReader = class {
1469
1536
  this.workspacePath = workspacePath;
1470
1537
  const mainRoot = resolveStorageRoot(workspacePath);
1471
1538
  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);
1539
+ this.storagePath = path7.join(mainRoot, STORAGE_DIR3);
1540
+ this.metaFilePath = path7.join(this.storagePath, META_FILE2);
1541
+ this.sessionsFilePath = path7.join(this.storagePath, SESSIONS_FILE2);
1542
+ this.decisionsFilePath = path7.join(this.storagePath, DECISIONS_FILE2);
1543
+ this.stateFilePath = path7.join(this.storagePath, STATE_FILE2);
1544
+ this.currentTasksFilePath = path7.join(this.storagePath, CURRENT_TASKS_FILE2);
1478
1545
  }
1479
1546
  /** Check if .keepgoing/ directory exists. */
1480
1547
  exists() {
1481
- return fs4.existsSync(this.storagePath);
1548
+ return fs5.existsSync(this.storagePath);
1482
1549
  }
1483
1550
  /** Read state.json, returns undefined if missing or corrupt. */
1484
1551
  getState() {
@@ -1716,10 +1783,10 @@ var KeepGoingReader = class {
1716
1783
  }
1717
1784
  readJsonFile(filePath) {
1718
1785
  try {
1719
- if (!fs4.existsSync(filePath)) {
1786
+ if (!fs5.existsSync(filePath)) {
1720
1787
  return void 0;
1721
1788
  }
1722
- const raw = fs4.readFileSync(filePath, "utf-8");
1789
+ const raw = fs5.readFileSync(filePath, "utf-8");
1723
1790
  return JSON.parse(raw);
1724
1791
  } catch {
1725
1792
  return void 0;
@@ -1728,9 +1795,9 @@ var KeepGoingReader = class {
1728
1795
  };
1729
1796
 
1730
1797
  // ../../packages/shared/src/setup.ts
1731
- import fs5 from "fs";
1732
- import os2 from "os";
1733
- import path7 from "path";
1798
+ import fs6 from "fs";
1799
+ import os3 from "os";
1800
+ import path8 from "path";
1734
1801
  var KEEPGOING_MARKER = "@keepgoingdev/mcp-server";
1735
1802
  var SESSION_START_HOOK = {
1736
1803
  matcher: "",
@@ -1759,36 +1826,55 @@ var POST_TOOL_USE_HOOK = {
1759
1826
  }
1760
1827
  ]
1761
1828
  };
1762
- var CLAUDE_MD_SECTION = `
1829
+ var SESSION_END_HOOK = {
1830
+ matcher: "",
1831
+ hooks: [
1832
+ {
1833
+ type: "command",
1834
+ command: "npx -y @keepgoingdev/mcp-server --save-checkpoint"
1835
+ }
1836
+ ]
1837
+ };
1838
+ var KEEPGOING_RULES_VERSION = 1;
1839
+ var KEEPGOING_RULES_CONTENT = `<!-- @keepgoingdev/mcp-server v${KEEPGOING_RULES_VERSION} -->
1763
1840
  ## KeepGoing
1764
1841
 
1765
1842
  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
1843
+ - \`summary\`: 1-2 sentences. What changed and why \u2014 no file paths, no implementation details (those are captured from git).
1844
+ - \`nextStep\`: What to do next
1768
1845
  - \`blocker\`: Any blocker (if applicable)
1769
1846
  `;
1847
+ function getRulesFileVersion(content) {
1848
+ const match = content.match(/<!-- @keepgoingdev\/mcp-server v(\d+) -->/);
1849
+ return match ? parseInt(match[1], 10) : null;
1850
+ }
1770
1851
  var STATUSLINE_CMD = "npx -y @keepgoingdev/mcp-server --statusline";
1852
+ function detectClaudeDir() {
1853
+ return process.env.CLAUDE_CONFIG_DIR || path8.join(os3.homedir(), ".claude");
1854
+ }
1771
1855
  function hasKeepGoingHook(hookEntries) {
1772
1856
  return hookEntries.some(
1773
1857
  (entry) => entry?.hooks?.some((h) => typeof h?.command === "string" && h.command.includes(KEEPGOING_MARKER))
1774
1858
  );
1775
1859
  }
1776
- function resolveScopePaths(scope, workspacePath) {
1860
+ function resolveScopePaths(scope, workspacePath, overrideClaudeDir) {
1777
1861
  if (scope === "user") {
1778
- const claudeDir2 = path7.join(os2.homedir(), ".claude");
1862
+ const claudeDir2 = overrideClaudeDir || detectClaudeDir();
1779
1863
  return {
1780
1864
  claudeDir: claudeDir2,
1781
- settingsPath: path7.join(claudeDir2, "settings.json"),
1782
- claudeMdPath: path7.join(claudeDir2, "CLAUDE.md")
1865
+ settingsPath: path8.join(claudeDir2, "settings.json"),
1866
+ claudeMdPath: path8.join(claudeDir2, "CLAUDE.md"),
1867
+ rulesPath: path8.join(claudeDir2, "rules", "keepgoing.md")
1783
1868
  };
1784
1869
  }
1785
- const claudeDir = path7.join(workspacePath, ".claude");
1786
- const dotClaudeMdPath = path7.join(workspacePath, ".claude", "CLAUDE.md");
1787
- const rootClaudeMdPath = path7.join(workspacePath, "CLAUDE.md");
1870
+ const claudeDir = path8.join(workspacePath, ".claude");
1871
+ const dotClaudeMdPath = path8.join(workspacePath, ".claude", "CLAUDE.md");
1872
+ const rootClaudeMdPath = path8.join(workspacePath, "CLAUDE.md");
1788
1873
  return {
1789
1874
  claudeDir,
1790
- settingsPath: path7.join(claudeDir, "settings.json"),
1791
- claudeMdPath: fs5.existsSync(dotClaudeMdPath) ? dotClaudeMdPath : rootClaudeMdPath
1875
+ settingsPath: path8.join(claudeDir, "settings.json"),
1876
+ claudeMdPath: fs6.existsSync(dotClaudeMdPath) ? dotClaudeMdPath : rootClaudeMdPath,
1877
+ rulesPath: path8.join(workspacePath, ".claude", "rules", "keepgoing.md")
1792
1878
  };
1793
1879
  }
1794
1880
  function writeHooksToSettings(settings) {
@@ -1817,15 +1903,22 @@ function writeHooksToSettings(settings) {
1817
1903
  settings.hooks.PostToolUse.push(POST_TOOL_USE_HOOK);
1818
1904
  changed = true;
1819
1905
  }
1906
+ if (!Array.isArray(settings.hooks.SessionEnd)) {
1907
+ settings.hooks.SessionEnd = [];
1908
+ }
1909
+ if (!hasKeepGoingHook(settings.hooks.SessionEnd)) {
1910
+ settings.hooks.SessionEnd.push(SESSION_END_HOOK);
1911
+ changed = true;
1912
+ }
1820
1913
  return changed;
1821
1914
  }
1822
1915
  function checkHookConflict(scope, workspacePath) {
1823
1916
  const otherPaths = resolveScopePaths(scope === "user" ? "project" : "user", workspacePath);
1824
- if (!fs5.existsSync(otherPaths.settingsPath)) {
1917
+ if (!fs6.existsSync(otherPaths.settingsPath)) {
1825
1918
  return null;
1826
1919
  }
1827
1920
  try {
1828
- const otherSettings = JSON.parse(fs5.readFileSync(otherPaths.settingsPath, "utf-8"));
1921
+ const otherSettings = JSON.parse(fs6.readFileSync(otherPaths.settingsPath, "utf-8"));
1829
1922
  const hooks = otherSettings?.hooks;
1830
1923
  if (!hooks) return null;
1831
1924
  const hasConflict = Array.isArray(hooks.SessionStart) && hasKeepGoingHook(hooks.SessionStart) || Array.isArray(hooks.Stop) && hasKeepGoingHook(hooks.Stop);
@@ -1844,15 +1937,20 @@ function setupProject(options) {
1844
1937
  scope = "project",
1845
1938
  sessionHooks = true,
1846
1939
  claudeMd = true,
1940
+ claudeDir: claudeDirOverride,
1847
1941
  statusline
1848
1942
  } = options;
1849
1943
  const messages = [];
1850
1944
  let changed = false;
1851
- const { claudeDir, settingsPath, claudeMdPath } = resolveScopePaths(scope, workspacePath);
1852
- const scopeLabel = scope === "user" ? "~/.claude/settings.json" : ".claude/settings.json";
1945
+ const { claudeDir, settingsPath, claudeMdPath, rulesPath } = resolveScopePaths(
1946
+ scope,
1947
+ workspacePath,
1948
+ claudeDirOverride
1949
+ );
1950
+ const scopeLabel = scope === "user" ? path8.join("~", path8.relative(os3.homedir(), claudeDir), "settings.json").replace(/\\/g, "/") : ".claude/settings.json";
1853
1951
  let settings = {};
1854
- if (fs5.existsSync(settingsPath)) {
1855
- settings = JSON.parse(fs5.readFileSync(settingsPath, "utf-8"));
1952
+ if (fs6.existsSync(settingsPath)) {
1953
+ settings = JSON.parse(fs6.readFileSync(settingsPath, "utf-8"));
1856
1954
  }
1857
1955
  let settingsChanged = false;
1858
1956
  if (sessionHooks) {
@@ -1883,29 +1981,45 @@ function setupProject(options) {
1883
1981
  statusline?.cleanup?.();
1884
1982
  }
1885
1983
  if (settingsChanged) {
1886
- if (!fs5.existsSync(claudeDir)) {
1887
- fs5.mkdirSync(claudeDir, { recursive: true });
1984
+ if (!fs6.existsSync(claudeDir)) {
1985
+ fs6.mkdirSync(claudeDir, { recursive: true });
1888
1986
  }
1889
- fs5.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
1987
+ fs6.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
1890
1988
  changed = true;
1891
1989
  }
1892
1990
  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`);
1991
+ const rulesDir = path8.dirname(rulesPath);
1992
+ const rulesLabel = scope === "user" ? path8.join(path8.relative(os3.homedir(), path8.dirname(rulesPath)), "keepgoing.md").replace(/\\/g, "/") : ".claude/rules/keepgoing.md";
1993
+ if (fs6.existsSync(rulesPath)) {
1994
+ const existing = fs6.readFileSync(rulesPath, "utf-8");
1995
+ const existingVersion = getRulesFileVersion(existing);
1996
+ if (existingVersion === null) {
1997
+ messages.push(`Rules file: Custom file found at ${rulesLabel}, skipping`);
1998
+ } else if (existingVersion >= KEEPGOING_RULES_VERSION) {
1999
+ messages.push(`Rules file: Already up to date (v${existingVersion}), skipped`);
2000
+ } else {
2001
+ if (!fs6.existsSync(rulesDir)) {
2002
+ fs6.mkdirSync(rulesDir, { recursive: true });
2003
+ }
2004
+ fs6.writeFileSync(rulesPath, KEEPGOING_RULES_CONTENT);
2005
+ changed = true;
2006
+ messages.push(`Rules file: Updated v${existingVersion} \u2192 v${KEEPGOING_RULES_VERSION} at ${rulesLabel}`);
2007
+ }
1900
2008
  } 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 });
2009
+ const existingClaudeMd = fs6.existsSync(claudeMdPath) ? fs6.readFileSync(claudeMdPath, "utf-8") : "";
2010
+ if (!fs6.existsSync(rulesDir)) {
2011
+ fs6.mkdirSync(rulesDir, { recursive: true });
1905
2012
  }
1906
- fs5.writeFileSync(claudeMdPath, updated);
2013
+ fs6.writeFileSync(rulesPath, KEEPGOING_RULES_CONTENT);
1907
2014
  changed = true;
1908
- messages.push(`CLAUDE.md: Added KeepGoing section to ${mdLabel}`);
2015
+ if (existingClaudeMd.includes("## KeepGoing")) {
2016
+ const mdLabel = scope === "user" ? "~/.claude/CLAUDE.md" : "CLAUDE.md";
2017
+ messages.push(
2018
+ `Rules file: Created ${rulesLabel} (you can now remove the ## KeepGoing section from ${mdLabel})`
2019
+ );
2020
+ } else {
2021
+ messages.push(`Rules file: Created ${rulesLabel}`);
2022
+ }
1909
2023
  }
1910
2024
  }
1911
2025
  return { messages, changed };
@@ -2253,7 +2367,7 @@ function registerGetReentryBriefing(server, reader, workspacePath) {
2253
2367
  }
2254
2368
 
2255
2369
  // src/tools/saveCheckpoint.ts
2256
- import path8 from "path";
2370
+ import path9 from "path";
2257
2371
  import { z as z4 } from "zod";
2258
2372
  function registerSaveCheckpoint(server, reader, workspacePath) {
2259
2373
  server.tool(
@@ -2269,7 +2383,7 @@ function registerSaveCheckpoint(server, reader, workspacePath) {
2269
2383
  const gitBranch = getCurrentBranch(workspacePath);
2270
2384
  const touchedFiles = getTouchedFiles(workspacePath);
2271
2385
  const commitHashes = getCommitsSince(workspacePath, lastSession?.timestamp);
2272
- const projectName = path8.basename(resolveStorageRoot(workspacePath));
2386
+ const projectName = path9.basename(resolveStorageRoot(workspacePath));
2273
2387
  const sessionId = generateSessionId({ workspaceRoot: workspacePath, branch: gitBranch ?? void 0, worktreePath: workspacePath });
2274
2388
  const checkpoint = createCheckpoint({
2275
2389
  summary,
@@ -2389,7 +2503,7 @@ function registerGetDecisions(server, reader) {
2389
2503
  function registerGetCurrentTask(server, reader) {
2390
2504
  server.tool(
2391
2505
  "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.",
2506
+ "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
2507
  {},
2394
2508
  async () => {
2395
2509
  if (!reader.exists()) {
@@ -2438,12 +2552,15 @@ function registerGetCurrentTask(server, reader) {
2438
2552
  for (const task of [...activeTasks, ...finishedTasks]) {
2439
2553
  const statusIcon = task.sessionActive ? "\u{1F7E2}" : "\u2705";
2440
2554
  const statusLabel = task.sessionActive ? "Active" : "Finished";
2441
- const sessionLabel = task.agentLabel || task.sessionId || "Session";
2555
+ const sessionLabel = task.sessionLabel || task.agentLabel || task.sessionId || "Session";
2442
2556
  lines.push(`### ${statusIcon} ${sessionLabel} (${statusLabel})`);
2443
2557
  lines.push(`- **Updated:** ${formatRelativeTime(task.updatedAt)}`);
2444
2558
  if (task.branch) {
2445
2559
  lines.push(`- **Branch:** ${task.branch}`);
2446
2560
  }
2561
+ if (task.agentLabel && task.sessionLabel) {
2562
+ lines.push(`- **Agent:** ${task.agentLabel}`);
2563
+ }
2447
2564
  if (task.taskSummary) {
2448
2565
  lines.push(`- **Doing:** ${task.taskSummary}`);
2449
2566
  }
@@ -2484,25 +2601,25 @@ function registerGetCurrentTask(server, reader) {
2484
2601
  import { z as z6 } from "zod";
2485
2602
 
2486
2603
  // src/cli/migrate.ts
2487
- import fs6 from "fs";
2488
- import os3 from "os";
2489
- import path9 from "path";
2604
+ import fs7 from "fs";
2605
+ import os4 from "os";
2606
+ import path10 from "path";
2490
2607
  var STATUSLINE_CMD2 = "npx -y @keepgoingdev/mcp-server --statusline";
2491
2608
  function isLegacyStatusline(command) {
2492
2609
  return !command.includes("--statusline") && command.includes("keepgoing-statusline");
2493
2610
  }
2494
2611
  function migrateStatusline(wsPath) {
2495
- const settingsPath = path9.join(wsPath, ".claude", "settings.json");
2496
- if (!fs6.existsSync(settingsPath)) return void 0;
2612
+ const settingsPath = path10.join(wsPath, ".claude", "settings.json");
2613
+ if (!fs7.existsSync(settingsPath)) return void 0;
2497
2614
  try {
2498
- const settings = JSON.parse(fs6.readFileSync(settingsPath, "utf-8"));
2615
+ const settings = JSON.parse(fs7.readFileSync(settingsPath, "utf-8"));
2499
2616
  const cmd = settings.statusLine?.command;
2500
2617
  if (!cmd || !isLegacyStatusline(cmd)) return void 0;
2501
2618
  settings.statusLine = {
2502
2619
  type: "command",
2503
2620
  command: STATUSLINE_CMD2
2504
2621
  };
2505
- fs6.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
2622
+ fs7.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
2506
2623
  cleanupLegacyScript();
2507
2624
  return "[KeepGoing] Migrated statusline to auto-updating command (restart Claude Code to apply)";
2508
2625
  } catch {
@@ -2510,10 +2627,10 @@ function migrateStatusline(wsPath) {
2510
2627
  }
2511
2628
  }
2512
2629
  function cleanupLegacyScript() {
2513
- const legacyScript = path9.join(os3.homedir(), ".claude", "keepgoing-statusline.sh");
2514
- if (fs6.existsSync(legacyScript)) {
2630
+ const legacyScript = path10.join(os4.homedir(), ".claude", "keepgoing-statusline.sh");
2631
+ if (fs7.existsSync(legacyScript)) {
2515
2632
  try {
2516
- fs6.unlinkSync(legacyScript);
2633
+ fs7.unlinkSync(legacyScript);
2517
2634
  } catch {
2518
2635
  }
2519
2636
  }
@@ -2526,15 +2643,17 @@ function registerSetupProject(server, workspacePath) {
2526
2643
  'Set up KeepGoing hooks and instructions. Use scope "user" for global setup (all projects) or "project" for per-project setup.',
2527
2644
  {
2528
2645
  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/)')
2646
+ claudeMd: z6.boolean().optional().default(true).describe("Add KeepGoing instructions to .claude/rules/keepgoing.md"),
2647
+ scope: z6.enum(["project", "user"]).optional().default("project").describe('Where to write config: "user" for global (~/.claude/), "project" for per-project (.claude/)'),
2648
+ claudeDir: z6.string().optional().describe("Override the Claude config directory for user scope (defaults to CLAUDE_CONFIG_DIR env var or ~/.claude)")
2531
2649
  },
2532
- async ({ sessionHooks, claudeMd, scope }) => {
2650
+ async ({ sessionHooks, claudeMd, scope, claudeDir }) => {
2533
2651
  const result = setupProject({
2534
2652
  workspacePath,
2535
2653
  scope,
2536
2654
  sessionHooks,
2537
2655
  claudeMd,
2656
+ claudeDir,
2538
2657
  statusline: {
2539
2658
  isLegacy: isLegacyStatusline,
2540
2659
  cleanup: cleanupLegacyScript
@@ -2909,7 +3028,7 @@ async function handlePrintCurrent() {
2909
3028
  }
2910
3029
 
2911
3030
  // src/cli/saveCheckpoint.ts
2912
- import path10 from "path";
3031
+ import path11 from "path";
2913
3032
  async function handleSaveCheckpoint() {
2914
3033
  const wsPath = resolveWsPath();
2915
3034
  const reader = new KeepGoingReader(wsPath);
@@ -2937,9 +3056,9 @@ async function handleSaveCheckpoint() {
2937
3056
  sessionStartTime: lastSession?.timestamp ?? now,
2938
3057
  lastActivityTime: now
2939
3058
  });
2940
- const summary = buildSmartSummary(events) ?? `Worked on ${touchedFiles.slice(0, 5).map((f) => path10.basename(f)).join(", ")}`;
3059
+ const summary = buildSmartSummary(events) ?? `Worked on ${touchedFiles.slice(0, 5).map((f) => path11.basename(f)).join(", ")}`;
2941
3060
  const nextStep = buildSmartNextStep(events);
2942
- const projectName = path10.basename(resolveStorageRoot(wsPath));
3061
+ const projectName = path11.basename(resolveStorageRoot(wsPath));
2943
3062
  const sessionId = generateSessionId({ workspaceRoot: wsPath, branch: gitBranch ?? void 0, worktreePath: wsPath });
2944
3063
  const checkpoint = createCheckpoint({
2945
3064
  summary,
@@ -2960,16 +3079,19 @@ async function handleSaveCheckpoint() {
2960
3079
  branch: gitBranch ?? void 0,
2961
3080
  updatedAt: checkpoint.timestamp
2962
3081
  });
2963
- if ((process.env.KEEPGOING_PRO_BYPASS === "1" || getLicenseForFeature("decisions")) && commitMessages.length > 0) {
2964
- const headHash = getHeadCommitHash(wsPath) || commitHashes[0];
2965
- if (headHash) {
3082
+ if (process.env.KEEPGOING_PRO_BYPASS === "1" || getLicenseForFeature("decisions")) {
3083
+ for (let i = 0; i < commitHashes.length; i++) {
3084
+ const hash = commitHashes[i];
3085
+ const message = commitMessages[i];
3086
+ if (!hash || !message) continue;
3087
+ const files = getFilesChangedInCommit(wsPath, hash);
2966
3088
  const detected = tryDetectDecision({
2967
3089
  workspacePath: wsPath,
2968
3090
  checkpointId: checkpoint.id,
2969
3091
  gitBranch,
2970
- commitHash: headHash,
2971
- commitMessage: commitMessages[0],
2972
- filesChanged: touchedFiles
3092
+ commitHash: hash,
3093
+ commitMessage: message,
3094
+ filesChanged: files
2973
3095
  });
2974
3096
  if (detected) {
2975
3097
  console.log(`[KeepGoing] Decision detected: ${detected.category} (${(detected.confidence * 100).toFixed(0)}% confidence)`);
@@ -2981,7 +3103,7 @@ async function handleSaveCheckpoint() {
2981
3103
  }
2982
3104
 
2983
3105
  // src/cli/transcriptUtils.ts
2984
- import fs7 from "fs";
3106
+ import fs8 from "fs";
2985
3107
  var TAIL_READ_BYTES = 8192;
2986
3108
  var TOOL_VERB_MAP = {
2987
3109
  Edit: "editing",
@@ -3032,9 +3154,9 @@ function isAssistantEntry(entry) {
3032
3154
  return entry.message?.role === "assistant";
3033
3155
  }
3034
3156
  function extractSessionLabel(transcriptPath) {
3035
- if (!transcriptPath || !fs7.existsSync(transcriptPath)) return null;
3157
+ if (!transcriptPath || !fs8.existsSync(transcriptPath)) return null;
3036
3158
  try {
3037
- const raw = fs7.readFileSync(transcriptPath, "utf-8");
3159
+ const raw = fs8.readFileSync(transcriptPath, "utf-8");
3038
3160
  for (const line of raw.split("\n")) {
3039
3161
  const trimmed = line.trim();
3040
3162
  if (!trimmed) continue;
@@ -3063,19 +3185,19 @@ function extractSessionLabel(transcriptPath) {
3063
3185
  return null;
3064
3186
  }
3065
3187
  function extractCurrentAction(transcriptPath) {
3066
- if (!transcriptPath || !fs7.existsSync(transcriptPath)) return null;
3188
+ if (!transcriptPath || !fs8.existsSync(transcriptPath)) return null;
3067
3189
  try {
3068
- const stat = fs7.statSync(transcriptPath);
3190
+ const stat = fs8.statSync(transcriptPath);
3069
3191
  const fileSize = stat.size;
3070
3192
  if (fileSize === 0) return null;
3071
3193
  const readSize = Math.min(fileSize, TAIL_READ_BYTES);
3072
3194
  const offset = fileSize - readSize;
3073
3195
  const buf = Buffer.alloc(readSize);
3074
- const fd = fs7.openSync(transcriptPath, "r");
3196
+ const fd = fs8.openSync(transcriptPath, "r");
3075
3197
  try {
3076
- fs7.readSync(fd, buf, 0, readSize, offset);
3198
+ fs8.readSync(fd, buf, 0, readSize, offset);
3077
3199
  } finally {
3078
- fs7.closeSync(fd);
3200
+ fs8.closeSync(fd);
3079
3201
  }
3080
3202
  const tail = buf.toString("utf-8");
3081
3203
  const lines = tail.split("\n").reverse();
@@ -3177,8 +3299,8 @@ async function handleUpdateTaskFromHook() {
3177
3299
  }
3178
3300
 
3179
3301
  // src/cli/statusline.ts
3180
- import fs8 from "fs";
3181
- import path11 from "path";
3302
+ import fs9 from "fs";
3303
+ import path12 from "path";
3182
3304
  var STDIN_TIMEOUT_MS2 = 3e3;
3183
3305
  async function handleStatusline() {
3184
3306
  const chunks = [];
@@ -3205,9 +3327,9 @@ async function handleStatusline() {
3205
3327
  if (!label) {
3206
3328
  try {
3207
3329
  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"));
3330
+ const tasksFile = path12.join(gitRoot, ".keepgoing", "current-tasks.json");
3331
+ if (fs9.existsSync(tasksFile)) {
3332
+ const data = JSON.parse(fs9.readFileSync(tasksFile, "utf-8"));
3211
3333
  const tasks = pruneStaleTasks(data.tasks ?? []);
3212
3334
  const match = sessionId ? tasks.find((t) => t.sessionId === sessionId) : void 0;
3213
3335
  if (match?.sessionLabel) {
@@ -3254,6 +3376,35 @@ async function handleContinueOn() {
3254
3376
  process.exit(0);
3255
3377
  }
3256
3378
 
3379
+ // src/cli/detectDecisions.ts
3380
+ async function handleDetectDecisions() {
3381
+ const wsPath = resolveWsPath();
3382
+ if (!(process.env.KEEPGOING_PRO_BYPASS === "1" || getLicenseForFeature("decisions"))) {
3383
+ process.exit(0);
3384
+ }
3385
+ const reader = new KeepGoingReader(wsPath);
3386
+ if (!reader.exists()) {
3387
+ process.exit(0);
3388
+ }
3389
+ const gitBranch = getCurrentBranch(wsPath);
3390
+ const headHash = getHeadCommitHash(wsPath);
3391
+ if (!headHash) process.exit(0);
3392
+ const commitMessage = getCommitMessageByHash(wsPath, headHash);
3393
+ if (!commitMessage) process.exit(0);
3394
+ const files = getFilesChangedInCommit(wsPath, headHash);
3395
+ const detected = tryDetectDecision({
3396
+ workspacePath: wsPath,
3397
+ gitBranch,
3398
+ commitHash: headHash,
3399
+ commitMessage,
3400
+ filesChanged: files
3401
+ });
3402
+ if (detected) {
3403
+ console.log(`[KeepGoing] Decision detected: ${detected.category} (${(detected.confidence * 100).toFixed(0)}% confidence)`);
3404
+ }
3405
+ process.exit(0);
3406
+ }
3407
+
3257
3408
  // src/index.ts
3258
3409
  var CLI_HANDLERS = {
3259
3410
  "--print-momentum": handlePrintMomentum,
@@ -3262,7 +3413,8 @@ var CLI_HANDLERS = {
3262
3413
  "--update-task-from-hook": handleUpdateTaskFromHook,
3263
3414
  "--print-current": handlePrintCurrent,
3264
3415
  "--statusline": handleStatusline,
3265
- "--continue-on": handleContinueOn
3416
+ "--continue-on": handleContinueOn,
3417
+ "--detect-decisions": handleDetectDecisions
3266
3418
  };
3267
3419
  var flag = process.argv.slice(2).find((a) => a in CLI_HANDLERS);
3268
3420
  if (flag) {