@keepgoingdev/mcp-server 0.6.0 → 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,16 +1937,20 @@ function setupProject(options) {
1844
1937
  scope = "project",
1845
1938
  sessionHooks = true,
1846
1939
  claudeMd = true,
1847
- hasProLicense = false,
1940
+ claudeDir: claudeDirOverride,
1848
1941
  statusline
1849
1942
  } = options;
1850
1943
  const messages = [];
1851
1944
  let changed = false;
1852
- const { claudeDir, settingsPath, claudeMdPath } = resolveScopePaths(scope, workspacePath);
1853
- 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";
1854
1951
  let settings = {};
1855
- if (fs5.existsSync(settingsPath)) {
1856
- settings = JSON.parse(fs5.readFileSync(settingsPath, "utf-8"));
1952
+ if (fs6.existsSync(settingsPath)) {
1953
+ settings = JSON.parse(fs6.readFileSync(settingsPath, "utf-8"));
1857
1954
  }
1858
1955
  let settingsChanged = false;
1859
1956
  if (sessionHooks) {
@@ -1869,7 +1966,7 @@ function setupProject(options) {
1869
1966
  messages.push(`Warning: ${conflict}`);
1870
1967
  }
1871
1968
  }
1872
- if (scope === "project" && hasProLicense) {
1969
+ if (scope === "project") {
1873
1970
  const needsUpdate = settings.statusLine?.command && statusline?.isLegacy?.(settings.statusLine.command);
1874
1971
  if (!settings.statusLine || needsUpdate) {
1875
1972
  settings.statusLine = {
@@ -1884,29 +1981,45 @@ function setupProject(options) {
1884
1981
  statusline?.cleanup?.();
1885
1982
  }
1886
1983
  if (settingsChanged) {
1887
- if (!fs5.existsSync(claudeDir)) {
1888
- fs5.mkdirSync(claudeDir, { recursive: true });
1984
+ if (!fs6.existsSync(claudeDir)) {
1985
+ fs6.mkdirSync(claudeDir, { recursive: true });
1889
1986
  }
1890
- fs5.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
1987
+ fs6.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
1891
1988
  changed = true;
1892
1989
  }
1893
1990
  if (claudeMd) {
1894
- let existing = "";
1895
- if (fs5.existsSync(claudeMdPath)) {
1896
- existing = fs5.readFileSync(claudeMdPath, "utf-8");
1897
- }
1898
- const mdLabel = scope === "user" ? "~/.claude/CLAUDE.md" : "CLAUDE.md";
1899
- if (existing.includes("## KeepGoing")) {
1900
- 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
+ }
1901
2008
  } else {
1902
- const updated = existing + CLAUDE_MD_SECTION;
1903
- const mdDir = path7.dirname(claudeMdPath);
1904
- if (!fs5.existsSync(mdDir)) {
1905
- 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 });
1906
2012
  }
1907
- fs5.writeFileSync(claudeMdPath, updated);
2013
+ fs6.writeFileSync(rulesPath, KEEPGOING_RULES_CONTENT);
1908
2014
  changed = true;
1909
- 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
+ }
1910
2023
  }
1911
2024
  }
1912
2025
  return { messages, changed };
@@ -2254,7 +2367,7 @@ function registerGetReentryBriefing(server, reader, workspacePath) {
2254
2367
  }
2255
2368
 
2256
2369
  // src/tools/saveCheckpoint.ts
2257
- import path8 from "path";
2370
+ import path9 from "path";
2258
2371
  import { z as z4 } from "zod";
2259
2372
  function registerSaveCheckpoint(server, reader, workspacePath) {
2260
2373
  server.tool(
@@ -2270,7 +2383,7 @@ function registerSaveCheckpoint(server, reader, workspacePath) {
2270
2383
  const gitBranch = getCurrentBranch(workspacePath);
2271
2384
  const touchedFiles = getTouchedFiles(workspacePath);
2272
2385
  const commitHashes = getCommitsSince(workspacePath, lastSession?.timestamp);
2273
- const projectName = path8.basename(resolveStorageRoot(workspacePath));
2386
+ const projectName = path9.basename(resolveStorageRoot(workspacePath));
2274
2387
  const sessionId = generateSessionId({ workspaceRoot: workspacePath, branch: gitBranch ?? void 0, worktreePath: workspacePath });
2275
2388
  const checkpoint = createCheckpoint({
2276
2389
  summary,
@@ -2390,7 +2503,7 @@ function registerGetDecisions(server, reader) {
2390
2503
  function registerGetCurrentTask(server, reader) {
2391
2504
  server.tool(
2392
2505
  "get_current_task",
2393
- "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.",
2394
2507
  {},
2395
2508
  async () => {
2396
2509
  if (!reader.exists()) {
@@ -2439,12 +2552,15 @@ function registerGetCurrentTask(server, reader) {
2439
2552
  for (const task of [...activeTasks, ...finishedTasks]) {
2440
2553
  const statusIcon = task.sessionActive ? "\u{1F7E2}" : "\u2705";
2441
2554
  const statusLabel = task.sessionActive ? "Active" : "Finished";
2442
- const sessionLabel = task.agentLabel || task.sessionId || "Session";
2555
+ const sessionLabel = task.sessionLabel || task.agentLabel || task.sessionId || "Session";
2443
2556
  lines.push(`### ${statusIcon} ${sessionLabel} (${statusLabel})`);
2444
2557
  lines.push(`- **Updated:** ${formatRelativeTime(task.updatedAt)}`);
2445
2558
  if (task.branch) {
2446
2559
  lines.push(`- **Branch:** ${task.branch}`);
2447
2560
  }
2561
+ if (task.agentLabel && task.sessionLabel) {
2562
+ lines.push(`- **Agent:** ${task.agentLabel}`);
2563
+ }
2448
2564
  if (task.taskSummary) {
2449
2565
  lines.push(`- **Doing:** ${task.taskSummary}`);
2450
2566
  }
@@ -2485,25 +2601,25 @@ function registerGetCurrentTask(server, reader) {
2485
2601
  import { z as z6 } from "zod";
2486
2602
 
2487
2603
  // src/cli/migrate.ts
2488
- import fs6 from "fs";
2489
- import os3 from "os";
2490
- import path9 from "path";
2604
+ import fs7 from "fs";
2605
+ import os4 from "os";
2606
+ import path10 from "path";
2491
2607
  var STATUSLINE_CMD2 = "npx -y @keepgoingdev/mcp-server --statusline";
2492
2608
  function isLegacyStatusline(command) {
2493
2609
  return !command.includes("--statusline") && command.includes("keepgoing-statusline");
2494
2610
  }
2495
2611
  function migrateStatusline(wsPath) {
2496
- const settingsPath = path9.join(wsPath, ".claude", "settings.json");
2497
- if (!fs6.existsSync(settingsPath)) return void 0;
2612
+ const settingsPath = path10.join(wsPath, ".claude", "settings.json");
2613
+ if (!fs7.existsSync(settingsPath)) return void 0;
2498
2614
  try {
2499
- const settings = JSON.parse(fs6.readFileSync(settingsPath, "utf-8"));
2615
+ const settings = JSON.parse(fs7.readFileSync(settingsPath, "utf-8"));
2500
2616
  const cmd = settings.statusLine?.command;
2501
2617
  if (!cmd || !isLegacyStatusline(cmd)) return void 0;
2502
2618
  settings.statusLine = {
2503
2619
  type: "command",
2504
2620
  command: STATUSLINE_CMD2
2505
2621
  };
2506
- fs6.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
2622
+ fs7.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
2507
2623
  cleanupLegacyScript();
2508
2624
  return "[KeepGoing] Migrated statusline to auto-updating command (restart Claude Code to apply)";
2509
2625
  } catch {
@@ -2511,10 +2627,10 @@ function migrateStatusline(wsPath) {
2511
2627
  }
2512
2628
  }
2513
2629
  function cleanupLegacyScript() {
2514
- const legacyScript = path9.join(os3.homedir(), ".claude", "keepgoing-statusline.sh");
2515
- if (fs6.existsSync(legacyScript)) {
2630
+ const legacyScript = path10.join(os4.homedir(), ".claude", "keepgoing-statusline.sh");
2631
+ if (fs7.existsSync(legacyScript)) {
2516
2632
  try {
2517
- fs6.unlinkSync(legacyScript);
2633
+ fs7.unlinkSync(legacyScript);
2518
2634
  } catch {
2519
2635
  }
2520
2636
  }
@@ -2527,17 +2643,17 @@ function registerSetupProject(server, workspacePath) {
2527
2643
  'Set up KeepGoing hooks and instructions. Use scope "user" for global setup (all projects) or "project" for per-project setup.',
2528
2644
  {
2529
2645
  sessionHooks: z6.boolean().optional().default(true).describe("Add session hooks to settings.json"),
2530
- claudeMd: z6.boolean().optional().default(true).describe("Add KeepGoing instructions to CLAUDE.md"),
2531
- 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)")
2532
2649
  },
2533
- async ({ sessionHooks, claudeMd, scope }) => {
2534
- const hasProLicense = process.env.KEEPGOING_PRO_BYPASS === "1" || !!getLicenseForFeature("session-awareness");
2650
+ async ({ sessionHooks, claudeMd, scope, claudeDir }) => {
2535
2651
  const result = setupProject({
2536
2652
  workspacePath,
2537
2653
  scope,
2538
2654
  sessionHooks,
2539
2655
  claudeMd,
2540
- hasProLicense,
2656
+ claudeDir,
2541
2657
  statusline: {
2542
2658
  isLegacy: isLegacyStatusline,
2543
2659
  cleanup: cleanupLegacyScript
@@ -2912,7 +3028,7 @@ async function handlePrintCurrent() {
2912
3028
  }
2913
3029
 
2914
3030
  // src/cli/saveCheckpoint.ts
2915
- import path10 from "path";
3031
+ import path11 from "path";
2916
3032
  async function handleSaveCheckpoint() {
2917
3033
  const wsPath = resolveWsPath();
2918
3034
  const reader = new KeepGoingReader(wsPath);
@@ -2940,9 +3056,9 @@ async function handleSaveCheckpoint() {
2940
3056
  sessionStartTime: lastSession?.timestamp ?? now,
2941
3057
  lastActivityTime: now
2942
3058
  });
2943
- 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(", ")}`;
2944
3060
  const nextStep = buildSmartNextStep(events);
2945
- const projectName = path10.basename(resolveStorageRoot(wsPath));
3061
+ const projectName = path11.basename(resolveStorageRoot(wsPath));
2946
3062
  const sessionId = generateSessionId({ workspaceRoot: wsPath, branch: gitBranch ?? void 0, worktreePath: wsPath });
2947
3063
  const checkpoint = createCheckpoint({
2948
3064
  summary,
@@ -2963,16 +3079,19 @@ async function handleSaveCheckpoint() {
2963
3079
  branch: gitBranch ?? void 0,
2964
3080
  updatedAt: checkpoint.timestamp
2965
3081
  });
2966
- if ((process.env.KEEPGOING_PRO_BYPASS === "1" || getLicenseForFeature("decisions")) && commitMessages.length > 0) {
2967
- const headHash = getHeadCommitHash(wsPath) || commitHashes[0];
2968
- 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);
2969
3088
  const detected = tryDetectDecision({
2970
3089
  workspacePath: wsPath,
2971
3090
  checkpointId: checkpoint.id,
2972
3091
  gitBranch,
2973
- commitHash: headHash,
2974
- commitMessage: commitMessages[0],
2975
- filesChanged: touchedFiles
3092
+ commitHash: hash,
3093
+ commitMessage: message,
3094
+ filesChanged: files
2976
3095
  });
2977
3096
  if (detected) {
2978
3097
  console.log(`[KeepGoing] Decision detected: ${detected.category} (${(detected.confidence * 100).toFixed(0)}% confidence)`);
@@ -2983,6 +3102,126 @@ async function handleSaveCheckpoint() {
2983
3102
  process.exit(0);
2984
3103
  }
2985
3104
 
3105
+ // src/cli/transcriptUtils.ts
3106
+ import fs8 from "fs";
3107
+ var TAIL_READ_BYTES = 8192;
3108
+ var TOOL_VERB_MAP = {
3109
+ Edit: "editing",
3110
+ MultiEdit: "editing",
3111
+ Write: "editing",
3112
+ Read: "researching",
3113
+ Glob: "researching",
3114
+ Grep: "researching",
3115
+ Bash: "running",
3116
+ Agent: "delegating",
3117
+ WebFetch: "browsing",
3118
+ WebSearch: "browsing",
3119
+ TodoWrite: "planning"
3120
+ };
3121
+ function truncateAtWord(text, max) {
3122
+ if (text.length <= max) return text;
3123
+ const cut = text.slice(0, max);
3124
+ const lastSpace = cut.lastIndexOf(" ");
3125
+ return (lastSpace > max / 2 ? cut.slice(0, lastSpace) : cut.slice(0, max - 1)) + "\u2026";
3126
+ }
3127
+ var FILLER_PREFIX_RE = /^(i want to|can you|please|let['']?s|could you|help me|i need to|i['']d like to|implement the following plan[:\s]*|implement this plan[:\s]*)\s*/i;
3128
+ var MARKDOWN_HEADING_RE = /^#+\s+/;
3129
+ function extractTextFromContent(content) {
3130
+ if (typeof content === "string") return content;
3131
+ if (!Array.isArray(content)) return "";
3132
+ let text = "";
3133
+ for (const part of content) {
3134
+ if (part.type === "text" && typeof part.text === "string") {
3135
+ text += part.text + " ";
3136
+ }
3137
+ }
3138
+ return text.trim();
3139
+ }
3140
+ function isUserEntry(entry) {
3141
+ return entry.type === "user" && entry.message?.role === "user";
3142
+ }
3143
+ function getToolUseFromEntry(entry) {
3144
+ const content = entry.message?.content;
3145
+ if (!Array.isArray(content)) return null;
3146
+ for (const part of content.slice().reverse()) {
3147
+ if (part.type === "tool_use" && typeof part.name === "string") {
3148
+ return part.name;
3149
+ }
3150
+ }
3151
+ return null;
3152
+ }
3153
+ function isAssistantEntry(entry) {
3154
+ return entry.message?.role === "assistant";
3155
+ }
3156
+ function extractSessionLabel(transcriptPath) {
3157
+ if (!transcriptPath || !fs8.existsSync(transcriptPath)) return null;
3158
+ try {
3159
+ const raw = fs8.readFileSync(transcriptPath, "utf-8");
3160
+ for (const line of raw.split("\n")) {
3161
+ const trimmed = line.trim();
3162
+ if (!trimmed) continue;
3163
+ let entry;
3164
+ try {
3165
+ entry = JSON.parse(trimmed);
3166
+ } catch {
3167
+ continue;
3168
+ }
3169
+ if (!isUserEntry(entry)) continue;
3170
+ let text = extractTextFromContent(entry.message?.content);
3171
+ if (!text) continue;
3172
+ if (text.startsWith("[") || /^<[a-z][\w-]*>/.test(text)) continue;
3173
+ text = text.replace(/@[\w./\-]+/g, "").trim();
3174
+ text = text.replace(FILLER_PREFIX_RE, "").trim();
3175
+ text = text.replace(MARKDOWN_HEADING_RE, "").trim();
3176
+ text = text.replace(/\s+/g, " ").trim();
3177
+ if (text.length < 20) continue;
3178
+ if (text.length > 80) {
3179
+ text = text.slice(0, 80);
3180
+ }
3181
+ return text;
3182
+ }
3183
+ } catch {
3184
+ }
3185
+ return null;
3186
+ }
3187
+ function extractCurrentAction(transcriptPath) {
3188
+ if (!transcriptPath || !fs8.existsSync(transcriptPath)) return null;
3189
+ try {
3190
+ const stat = fs8.statSync(transcriptPath);
3191
+ const fileSize = stat.size;
3192
+ if (fileSize === 0) return null;
3193
+ const readSize = Math.min(fileSize, TAIL_READ_BYTES);
3194
+ const offset = fileSize - readSize;
3195
+ const buf = Buffer.alloc(readSize);
3196
+ const fd = fs8.openSync(transcriptPath, "r");
3197
+ try {
3198
+ fs8.readSync(fd, buf, 0, readSize, offset);
3199
+ } finally {
3200
+ fs8.closeSync(fd);
3201
+ }
3202
+ const tail = buf.toString("utf-8");
3203
+ const lines = tail.split("\n").reverse();
3204
+ for (const line of lines) {
3205
+ const trimmed = line.trim();
3206
+ if (!trimmed) continue;
3207
+ let entry;
3208
+ try {
3209
+ entry = JSON.parse(trimmed);
3210
+ } catch {
3211
+ continue;
3212
+ }
3213
+ if (!isAssistantEntry(entry)) continue;
3214
+ const toolName = getToolUseFromEntry(entry);
3215
+ if (toolName) {
3216
+ return TOOL_VERB_MAP[toolName] ?? "working";
3217
+ }
3218
+ return "done";
3219
+ }
3220
+ } catch {
3221
+ }
3222
+ return null;
3223
+ }
3224
+
2986
3225
  // src/cli/updateTask.ts
2987
3226
  async function handleUpdateTask() {
2988
3227
  const args = process.argv.slice(2);
@@ -3034,7 +3273,8 @@ async function handleUpdateTaskFromHook() {
3034
3273
  const writer = new KeepGoingWriter(wsPath);
3035
3274
  const existing = writer.readCurrentTasks();
3036
3275
  const sessionIdFromHook = hookData.session_id;
3037
- const cachedBranch = sessionIdFromHook ? existing.find((t) => t.sessionId === sessionIdFromHook)?.branch : void 0;
3276
+ const existingSession = sessionIdFromHook ? existing.find((t) => t.sessionId === sessionIdFromHook) : void 0;
3277
+ const cachedBranch = existingSession?.branch;
3038
3278
  const branch = cachedBranch ?? getCurrentBranch(wsPath) ?? void 0;
3039
3279
  const task = {
3040
3280
  taskSummary: fileName ? `${toolName} ${fileName}` : `Used ${toolName}`,
@@ -3046,6 +3286,10 @@ async function handleUpdateTaskFromHook() {
3046
3286
  };
3047
3287
  const sessionId = hookData.session_id || generateSessionId({ ...task, workspaceRoot: wsPath });
3048
3288
  task.sessionId = sessionId;
3289
+ if (!existingSession?.sessionLabel && hookData.transcript_path) {
3290
+ const label = extractSessionLabel(hookData.transcript_path);
3291
+ if (label) task.sessionLabel = label;
3292
+ }
3049
3293
  writer.upsertSession(task);
3050
3294
  } catch {
3051
3295
  }
@@ -3055,8 +3299,8 @@ async function handleUpdateTaskFromHook() {
3055
3299
  }
3056
3300
 
3057
3301
  // src/cli/statusline.ts
3058
- import fs7 from "fs";
3059
- import path11 from "path";
3302
+ import fs9 from "fs";
3303
+ import path12 from "path";
3060
3304
  var STDIN_TIMEOUT_MS2 = 3e3;
3061
3305
  async function handleStatusline() {
3062
3306
  const chunks = [];
@@ -3070,36 +3314,43 @@ async function handleStatusline() {
3070
3314
  clearTimeout(timeout);
3071
3315
  try {
3072
3316
  const raw = Buffer.concat(chunks).toString("utf-8").trim();
3073
- if (!raw) {
3074
- process.exit(0);
3075
- }
3317
+ if (!raw) process.exit(0);
3076
3318
  const input = JSON.parse(raw);
3077
3319
  const dir = input.workspace?.current_dir ?? input.cwd;
3078
- if (!dir) {
3079
- process.exit(0);
3080
- }
3081
- const gitRoot = findGitRoot(dir);
3082
- const tasksFile = path11.join(gitRoot, ".keepgoing", "current-tasks.json");
3083
- if (!fs7.existsSync(tasksFile)) {
3084
- process.exit(0);
3085
- }
3086
- const data = JSON.parse(fs7.readFileSync(tasksFile, "utf-8"));
3087
- const branch = getCurrentBranch(gitRoot) ?? "";
3088
- const active = pruneStaleTasks(data.tasks ?? []).filter((t) => t.sessionActive && t.branch === branch);
3089
- if (active.length === 0) {
3090
- process.exit(0);
3320
+ if (!dir) process.exit(0);
3321
+ const transcriptPath = input.transcript_path;
3322
+ const sessionId = input.session_id;
3323
+ let label = null;
3324
+ if (input.agent?.name) {
3325
+ label = input.agent.name;
3326
+ }
3327
+ if (!label) {
3328
+ try {
3329
+ const gitRoot = findGitRoot(dir);
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"));
3333
+ const tasks = pruneStaleTasks(data.tasks ?? []);
3334
+ const match = sessionId ? tasks.find((t) => t.sessionId === sessionId) : void 0;
3335
+ if (match?.sessionLabel) {
3336
+ label = match.sessionLabel;
3337
+ }
3338
+ }
3339
+ } catch {
3340
+ }
3091
3341
  }
3092
- active.sort((a, b) => a.updatedAt.localeCompare(b.updatedAt));
3093
- const task = active[active.length - 1];
3094
- const summary = task.taskSummary;
3095
- if (!summary) {
3096
- process.exit(0);
3342
+ if (!label && transcriptPath) {
3343
+ label = extractSessionLabel(transcriptPath);
3097
3344
  }
3098
- if (branch) {
3099
- process.stdout.write(`[KG] ${branch}: ${summary}
3345
+ if (!label) process.exit(0);
3346
+ const action = transcriptPath ? extractCurrentAction(transcriptPath) : null;
3347
+ const budget = action ? 40 : 55;
3348
+ const displayLabel = truncateAtWord(label, budget);
3349
+ if (action) {
3350
+ process.stdout.write(`[KG] ${displayLabel} \xB7 ${action}
3100
3351
  `);
3101
3352
  } else {
3102
- process.stdout.write(`[KG] ${summary}
3353
+ process.stdout.write(`[KG] ${displayLabel}
3103
3354
  `);
3104
3355
  }
3105
3356
  } catch {
@@ -3125,6 +3376,35 @@ async function handleContinueOn() {
3125
3376
  process.exit(0);
3126
3377
  }
3127
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
+
3128
3408
  // src/index.ts
3129
3409
  var CLI_HANDLERS = {
3130
3410
  "--print-momentum": handlePrintMomentum,
@@ -3133,7 +3413,8 @@ var CLI_HANDLERS = {
3133
3413
  "--update-task-from-hook": handleUpdateTaskFromHook,
3134
3414
  "--print-current": handlePrintCurrent,
3135
3415
  "--statusline": handleStatusline,
3136
- "--continue-on": handleContinueOn
3416
+ "--continue-on": handleContinueOn,
3417
+ "--detect-decisions": handleDetectDecisions
3137
3418
  };
3138
3419
  var flag = process.argv.slice(2).find((a) => a in CLI_HANDLERS);
3139
3420
  if (flag) {