@keepgoingdev/cli 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +373 -115
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -320,7 +320,8 @@ function buildRecentActivity(lastSession, recentSessions, recentCommitMessages)
320
320
  parts.push("1 recent session");
321
321
  }
322
322
  if (lastSession.summary) {
323
- parts.push(`Last: ${lastSession.summary}`);
323
+ const brief = lastSession.summary.length > 120 ? lastSession.summary.slice(0, 117) + "..." : lastSession.summary;
324
+ parts.push(`Last: ${brief}`);
324
325
  }
325
326
  if (lastSession.touchedFiles.length > 0) {
326
327
  parts.push(`${lastSession.touchedFiles.length} files touched`);
@@ -585,9 +586,59 @@ function formatContinueOnPrompt(context, options) {
585
586
  }
586
587
 
587
588
  // ../../packages/shared/src/storage.ts
589
+ import fs2 from "fs";
590
+ import path4 from "path";
591
+ import { randomUUID as randomUUID2, createHash } from "crypto";
592
+
593
+ // ../../packages/shared/src/registry.ts
588
594
  import fs from "fs";
595
+ import os from "os";
589
596
  import path3 from "path";
590
- import { randomUUID as randomUUID2, createHash } from "crypto";
597
+ var KEEPGOING_DIR = path3.join(os.homedir(), ".keepgoing");
598
+ var KNOWN_PROJECTS_FILE = path3.join(KEEPGOING_DIR, "known-projects.json");
599
+ var STALE_PROJECT_MS = 90 * 24 * 60 * 60 * 1e3;
600
+ function readKnownProjects() {
601
+ try {
602
+ if (fs.existsSync(KNOWN_PROJECTS_FILE)) {
603
+ const raw = JSON.parse(fs.readFileSync(KNOWN_PROJECTS_FILE, "utf-8"));
604
+ if (raw && Array.isArray(raw.projects)) {
605
+ return raw;
606
+ }
607
+ }
608
+ } catch {
609
+ }
610
+ return { version: 1, projects: [] };
611
+ }
612
+ function writeKnownProjects(data) {
613
+ if (!fs.existsSync(KEEPGOING_DIR)) {
614
+ fs.mkdirSync(KEEPGOING_DIR, { recursive: true });
615
+ }
616
+ const tmpFile = KNOWN_PROJECTS_FILE + ".tmp";
617
+ fs.writeFileSync(tmpFile, JSON.stringify(data, null, 2) + "\n", "utf-8");
618
+ fs.renameSync(tmpFile, KNOWN_PROJECTS_FILE);
619
+ }
620
+ function registerProject(projectPath, projectName) {
621
+ try {
622
+ const data = readKnownProjects();
623
+ const now = (/* @__PURE__ */ new Date()).toISOString();
624
+ const name = projectName || path3.basename(projectPath);
625
+ const existingIdx = data.projects.findIndex((p) => p.path === projectPath);
626
+ if (existingIdx >= 0) {
627
+ data.projects[existingIdx].lastSeen = now;
628
+ if (projectName) {
629
+ data.projects[existingIdx].name = name;
630
+ }
631
+ } else {
632
+ data.projects.push({ path: projectPath, name, lastSeen: now });
633
+ }
634
+ const cutoff = Date.now() - STALE_PROJECT_MS;
635
+ data.projects = data.projects.filter((p) => new Date(p.lastSeen).getTime() > cutoff);
636
+ writeKnownProjects(data);
637
+ } catch {
638
+ }
639
+ }
640
+
641
+ // ../../packages/shared/src/storage.ts
591
642
  var STORAGE_DIR = ".keepgoing";
592
643
  var META_FILE = "meta.json";
593
644
  var SESSIONS_FILE = "sessions.json";
@@ -609,23 +660,23 @@ var KeepGoingWriter = class {
609
660
  currentTasksFilePath;
610
661
  constructor(workspacePath) {
611
662
  const mainRoot = resolveStorageRoot(workspacePath);
612
- this.storagePath = path3.join(mainRoot, STORAGE_DIR);
613
- this.sessionsFilePath = path3.join(this.storagePath, SESSIONS_FILE);
614
- this.stateFilePath = path3.join(this.storagePath, STATE_FILE);
615
- this.metaFilePath = path3.join(this.storagePath, META_FILE);
616
- this.currentTasksFilePath = path3.join(this.storagePath, CURRENT_TASKS_FILE);
663
+ this.storagePath = path4.join(mainRoot, STORAGE_DIR);
664
+ this.sessionsFilePath = path4.join(this.storagePath, SESSIONS_FILE);
665
+ this.stateFilePath = path4.join(this.storagePath, STATE_FILE);
666
+ this.metaFilePath = path4.join(this.storagePath, META_FILE);
667
+ this.currentTasksFilePath = path4.join(this.storagePath, CURRENT_TASKS_FILE);
617
668
  }
618
669
  ensureDir() {
619
- if (!fs.existsSync(this.storagePath)) {
620
- fs.mkdirSync(this.storagePath, { recursive: true });
670
+ if (!fs2.existsSync(this.storagePath)) {
671
+ fs2.mkdirSync(this.storagePath, { recursive: true });
621
672
  }
622
673
  }
623
674
  saveCheckpoint(checkpoint, projectName) {
624
675
  this.ensureDir();
625
676
  let sessionsData;
626
677
  try {
627
- if (fs.existsSync(this.sessionsFilePath)) {
628
- const raw = JSON.parse(fs.readFileSync(this.sessionsFilePath, "utf-8"));
678
+ if (fs2.existsSync(this.sessionsFilePath)) {
679
+ const raw = JSON.parse(fs2.readFileSync(this.sessionsFilePath, "utf-8"));
629
680
  if (Array.isArray(raw)) {
630
681
  sessionsData = { version: 1, project: projectName, sessions: raw };
631
682
  } else {
@@ -643,20 +694,22 @@ var KeepGoingWriter = class {
643
694
  if (sessionsData.sessions.length > MAX_SESSIONS) {
644
695
  sessionsData.sessions = sessionsData.sessions.slice(-MAX_SESSIONS);
645
696
  }
646
- fs.writeFileSync(this.sessionsFilePath, JSON.stringify(sessionsData, null, 2), "utf-8");
697
+ fs2.writeFileSync(this.sessionsFilePath, JSON.stringify(sessionsData, null, 2), "utf-8");
647
698
  const state = {
648
699
  lastSessionId: checkpoint.id,
649
700
  lastKnownBranch: checkpoint.gitBranch,
650
701
  lastActivityAt: checkpoint.timestamp
651
702
  };
652
- fs.writeFileSync(this.stateFilePath, JSON.stringify(state, null, 2), "utf-8");
703
+ fs2.writeFileSync(this.stateFilePath, JSON.stringify(state, null, 2), "utf-8");
653
704
  this.updateMeta(checkpoint.timestamp);
705
+ const mainRoot = resolveStorageRoot(this.storagePath);
706
+ registerProject(mainRoot, projectName);
654
707
  }
655
708
  updateMeta(timestamp) {
656
709
  let meta;
657
710
  try {
658
- if (fs.existsSync(this.metaFilePath)) {
659
- meta = JSON.parse(fs.readFileSync(this.metaFilePath, "utf-8"));
711
+ if (fs2.existsSync(this.metaFilePath)) {
712
+ meta = JSON.parse(fs2.readFileSync(this.metaFilePath, "utf-8"));
660
713
  meta.lastUpdated = timestamp;
661
714
  } else {
662
715
  meta = {
@@ -672,7 +725,7 @@ var KeepGoingWriter = class {
672
725
  lastUpdated: timestamp
673
726
  };
674
727
  }
675
- fs.writeFileSync(this.metaFilePath, JSON.stringify(meta, null, 2), "utf-8");
728
+ fs2.writeFileSync(this.metaFilePath, JSON.stringify(meta, null, 2), "utf-8");
676
729
  }
677
730
  // ---------------------------------------------------------------------------
678
731
  // Multi-session API
@@ -680,8 +733,8 @@ var KeepGoingWriter = class {
680
733
  /** Read all current tasks from current-tasks.json. Auto-prunes stale sessions. */
681
734
  readCurrentTasks() {
682
735
  try {
683
- if (fs.existsSync(this.currentTasksFilePath)) {
684
- const raw = JSON.parse(fs.readFileSync(this.currentTasksFilePath, "utf-8"));
736
+ if (fs2.existsSync(this.currentTasksFilePath)) {
737
+ const raw = JSON.parse(fs2.readFileSync(this.currentTasksFilePath, "utf-8"));
685
738
  const tasks = Array.isArray(raw) ? raw : raw.tasks ?? [];
686
739
  return this.pruneStale(tasks);
687
740
  }
@@ -696,6 +749,8 @@ var KeepGoingWriter = class {
696
749
  upsertSession(update) {
697
750
  this.ensureDir();
698
751
  this.upsertSessionCore(update);
752
+ const mainRoot = resolveStorageRoot(this.storagePath);
753
+ registerProject(mainRoot);
699
754
  }
700
755
  /** Core upsert logic: merges the update into current-tasks.json and returns the pruned task list. */
701
756
  upsertSessionCore(update) {
@@ -734,8 +789,8 @@ var KeepGoingWriter = class {
734
789
  // ---------------------------------------------------------------------------
735
790
  readAllTasksRaw() {
736
791
  try {
737
- if (fs.existsSync(this.currentTasksFilePath)) {
738
- const raw = JSON.parse(fs.readFileSync(this.currentTasksFilePath, "utf-8"));
792
+ if (fs2.existsSync(this.currentTasksFilePath)) {
793
+ const raw = JSON.parse(fs2.readFileSync(this.currentTasksFilePath, "utf-8"));
739
794
  return Array.isArray(raw) ? [...raw] : [...raw.tasks ?? []];
740
795
  }
741
796
  } catch {
@@ -747,7 +802,7 @@ var KeepGoingWriter = class {
747
802
  }
748
803
  writeTasksFile(tasks) {
749
804
  const data = { version: 1, tasks };
750
- fs.writeFileSync(this.currentTasksFilePath, JSON.stringify(data, null, 2), "utf-8");
805
+ fs2.writeFileSync(this.currentTasksFilePath, JSON.stringify(data, null, 2), "utf-8");
751
806
  }
752
807
  };
753
808
  function generateSessionId(context) {
@@ -926,35 +981,35 @@ function capitalize(s) {
926
981
  }
927
982
 
928
983
  // ../../packages/shared/src/decisionStorage.ts
929
- import fs3 from "fs";
930
- import path5 from "path";
984
+ import fs4 from "fs";
985
+ import path6 from "path";
931
986
 
932
987
  // ../../packages/shared/src/license.ts
933
988
  import crypto from "crypto";
934
- import fs2 from "fs";
935
- import os from "os";
936
- import path4 from "path";
989
+ import fs3 from "fs";
990
+ import os2 from "os";
991
+ import path5 from "path";
937
992
  var LICENSE_FILE = "license.json";
938
993
  var DEVICE_ID_FILE = "device-id";
939
994
  function getGlobalLicenseDir() {
940
- return path4.join(os.homedir(), ".keepgoing");
995
+ return path5.join(os2.homedir(), ".keepgoing");
941
996
  }
942
997
  function getGlobalLicensePath() {
943
- return path4.join(getGlobalLicenseDir(), LICENSE_FILE);
998
+ return path5.join(getGlobalLicenseDir(), LICENSE_FILE);
944
999
  }
945
1000
  function getDeviceId() {
946
1001
  const dir = getGlobalLicenseDir();
947
- const filePath = path4.join(dir, DEVICE_ID_FILE);
1002
+ const filePath = path5.join(dir, DEVICE_ID_FILE);
948
1003
  try {
949
- const existing = fs2.readFileSync(filePath, "utf-8").trim();
1004
+ const existing = fs3.readFileSync(filePath, "utf-8").trim();
950
1005
  if (existing) return existing;
951
1006
  } catch {
952
1007
  }
953
1008
  const id = crypto.randomUUID();
954
- if (!fs2.existsSync(dir)) {
955
- fs2.mkdirSync(dir, { recursive: true });
1009
+ if (!fs3.existsSync(dir)) {
1010
+ fs3.mkdirSync(dir, { recursive: true });
956
1011
  }
957
- fs2.writeFileSync(filePath, id, "utf-8");
1012
+ fs3.writeFileSync(filePath, id, "utf-8");
958
1013
  return id;
959
1014
  }
960
1015
  var DECISION_DETECTION_VARIANT_ID = 1361527;
@@ -988,10 +1043,10 @@ function readLicenseStore() {
988
1043
  const licensePath = getGlobalLicensePath();
989
1044
  let store;
990
1045
  try {
991
- if (!fs2.existsSync(licensePath)) {
1046
+ if (!fs3.existsSync(licensePath)) {
992
1047
  store = { version: 2, licenses: [] };
993
1048
  } else {
994
- const raw = fs2.readFileSync(licensePath, "utf-8");
1049
+ const raw = fs3.readFileSync(licensePath, "utf-8");
995
1050
  const data = JSON.parse(raw);
996
1051
  if (data?.version === 2 && Array.isArray(data.licenses)) {
997
1052
  store = data;
@@ -1008,11 +1063,11 @@ function readLicenseStore() {
1008
1063
  }
1009
1064
  function writeLicenseStore(store) {
1010
1065
  const dirPath = getGlobalLicenseDir();
1011
- if (!fs2.existsSync(dirPath)) {
1012
- fs2.mkdirSync(dirPath, { recursive: true });
1066
+ if (!fs3.existsSync(dirPath)) {
1067
+ fs3.mkdirSync(dirPath, { recursive: true });
1013
1068
  }
1014
- const licensePath = path4.join(dirPath, LICENSE_FILE);
1015
- fs2.writeFileSync(licensePath, JSON.stringify(store, null, 2), "utf-8");
1069
+ const licensePath = path5.join(dirPath, LICENSE_FILE);
1070
+ fs3.writeFileSync(licensePath, JSON.stringify(store, null, 2), "utf-8");
1016
1071
  _cachedStore = store;
1017
1072
  _cacheTimestamp = Date.now();
1018
1073
  }
@@ -1052,8 +1107,8 @@ var DefaultFeatureGate = class {
1052
1107
  var currentGate = new DefaultFeatureGate();
1053
1108
 
1054
1109
  // ../../packages/shared/src/reader.ts
1055
- import fs4 from "fs";
1056
- import path6 from "path";
1110
+ import fs5 from "fs";
1111
+ import path7 from "path";
1057
1112
  var STORAGE_DIR2 = ".keepgoing";
1058
1113
  var META_FILE2 = "meta.json";
1059
1114
  var SESSIONS_FILE2 = "sessions.json";
@@ -1075,16 +1130,16 @@ var KeepGoingReader = class {
1075
1130
  this.workspacePath = workspacePath;
1076
1131
  const mainRoot = resolveStorageRoot(workspacePath);
1077
1132
  this._isWorktree = mainRoot !== workspacePath;
1078
- this.storagePath = path6.join(mainRoot, STORAGE_DIR2);
1079
- this.metaFilePath = path6.join(this.storagePath, META_FILE2);
1080
- this.sessionsFilePath = path6.join(this.storagePath, SESSIONS_FILE2);
1081
- this.decisionsFilePath = path6.join(this.storagePath, DECISIONS_FILE);
1082
- this.stateFilePath = path6.join(this.storagePath, STATE_FILE2);
1083
- this.currentTasksFilePath = path6.join(this.storagePath, CURRENT_TASKS_FILE2);
1133
+ this.storagePath = path7.join(mainRoot, STORAGE_DIR2);
1134
+ this.metaFilePath = path7.join(this.storagePath, META_FILE2);
1135
+ this.sessionsFilePath = path7.join(this.storagePath, SESSIONS_FILE2);
1136
+ this.decisionsFilePath = path7.join(this.storagePath, DECISIONS_FILE);
1137
+ this.stateFilePath = path7.join(this.storagePath, STATE_FILE2);
1138
+ this.currentTasksFilePath = path7.join(this.storagePath, CURRENT_TASKS_FILE2);
1084
1139
  }
1085
1140
  /** Check if .keepgoing/ directory exists. */
1086
1141
  exists() {
1087
- return fs4.existsSync(this.storagePath);
1142
+ return fs5.existsSync(this.storagePath);
1088
1143
  }
1089
1144
  /** Read state.json, returns undefined if missing or corrupt. */
1090
1145
  getState() {
@@ -1322,10 +1377,10 @@ var KeepGoingReader = class {
1322
1377
  }
1323
1378
  readJsonFile(filePath) {
1324
1379
  try {
1325
- if (!fs4.existsSync(filePath)) {
1380
+ if (!fs5.existsSync(filePath)) {
1326
1381
  return void 0;
1327
1382
  }
1328
- const raw = fs4.readFileSync(filePath, "utf-8");
1383
+ const raw = fs5.readFileSync(filePath, "utf-8");
1329
1384
  return JSON.parse(raw);
1330
1385
  } catch {
1331
1386
  return void 0;
@@ -1334,9 +1389,9 @@ var KeepGoingReader = class {
1334
1389
  };
1335
1390
 
1336
1391
  // ../../packages/shared/src/setup.ts
1337
- import fs5 from "fs";
1338
- import os2 from "os";
1339
- import path7 from "path";
1392
+ import fs6 from "fs";
1393
+ import os3 from "os";
1394
+ import path8 from "path";
1340
1395
  var KEEPGOING_MARKER = "@keepgoingdev/mcp-server";
1341
1396
  var SESSION_START_HOOK = {
1342
1397
  matcher: "",
@@ -1365,36 +1420,55 @@ var POST_TOOL_USE_HOOK = {
1365
1420
  }
1366
1421
  ]
1367
1422
  };
1368
- var CLAUDE_MD_SECTION = `
1423
+ var SESSION_END_HOOK = {
1424
+ matcher: "",
1425
+ hooks: [
1426
+ {
1427
+ type: "command",
1428
+ command: "npx -y @keepgoingdev/mcp-server --save-checkpoint"
1429
+ }
1430
+ ]
1431
+ };
1432
+ var KEEPGOING_RULES_VERSION = 1;
1433
+ var KEEPGOING_RULES_CONTENT = `<!-- @keepgoingdev/mcp-server v${KEEPGOING_RULES_VERSION} -->
1369
1434
  ## KeepGoing
1370
1435
 
1371
1436
  After completing a task or meaningful piece of work, call the \`save_checkpoint\` MCP tool with:
1372
- - \`summary\`: What you accomplished
1373
- - \`nextStep\`: What should be done next
1437
+ - \`summary\`: 1-2 sentences. What changed and why \u2014 no file paths, no implementation details (those are captured from git).
1438
+ - \`nextStep\`: What to do next
1374
1439
  - \`blocker\`: Any blocker (if applicable)
1375
1440
  `;
1441
+ function getRulesFileVersion(content) {
1442
+ const match = content.match(/<!-- @keepgoingdev\/mcp-server v(\d+) -->/);
1443
+ return match ? parseInt(match[1], 10) : null;
1444
+ }
1376
1445
  var STATUSLINE_CMD = "npx -y @keepgoingdev/mcp-server --statusline";
1446
+ function detectClaudeDir() {
1447
+ return process.env.CLAUDE_CONFIG_DIR || path8.join(os3.homedir(), ".claude");
1448
+ }
1377
1449
  function hasKeepGoingHook(hookEntries) {
1378
1450
  return hookEntries.some(
1379
1451
  (entry) => entry?.hooks?.some((h) => typeof h?.command === "string" && h.command.includes(KEEPGOING_MARKER))
1380
1452
  );
1381
1453
  }
1382
- function resolveScopePaths(scope, workspacePath) {
1454
+ function resolveScopePaths(scope, workspacePath, overrideClaudeDir) {
1383
1455
  if (scope === "user") {
1384
- const claudeDir2 = path7.join(os2.homedir(), ".claude");
1456
+ const claudeDir2 = overrideClaudeDir || detectClaudeDir();
1385
1457
  return {
1386
1458
  claudeDir: claudeDir2,
1387
- settingsPath: path7.join(claudeDir2, "settings.json"),
1388
- claudeMdPath: path7.join(claudeDir2, "CLAUDE.md")
1459
+ settingsPath: path8.join(claudeDir2, "settings.json"),
1460
+ claudeMdPath: path8.join(claudeDir2, "CLAUDE.md"),
1461
+ rulesPath: path8.join(claudeDir2, "rules", "keepgoing.md")
1389
1462
  };
1390
1463
  }
1391
- const claudeDir = path7.join(workspacePath, ".claude");
1392
- const dotClaudeMdPath = path7.join(workspacePath, ".claude", "CLAUDE.md");
1393
- const rootClaudeMdPath = path7.join(workspacePath, "CLAUDE.md");
1464
+ const claudeDir = path8.join(workspacePath, ".claude");
1465
+ const dotClaudeMdPath = path8.join(workspacePath, ".claude", "CLAUDE.md");
1466
+ const rootClaudeMdPath = path8.join(workspacePath, "CLAUDE.md");
1394
1467
  return {
1395
1468
  claudeDir,
1396
- settingsPath: path7.join(claudeDir, "settings.json"),
1397
- claudeMdPath: fs5.existsSync(dotClaudeMdPath) ? dotClaudeMdPath : rootClaudeMdPath
1469
+ settingsPath: path8.join(claudeDir, "settings.json"),
1470
+ claudeMdPath: fs6.existsSync(dotClaudeMdPath) ? dotClaudeMdPath : rootClaudeMdPath,
1471
+ rulesPath: path8.join(workspacePath, ".claude", "rules", "keepgoing.md")
1398
1472
  };
1399
1473
  }
1400
1474
  function writeHooksToSettings(settings) {
@@ -1423,15 +1497,22 @@ function writeHooksToSettings(settings) {
1423
1497
  settings.hooks.PostToolUse.push(POST_TOOL_USE_HOOK);
1424
1498
  changed = true;
1425
1499
  }
1500
+ if (!Array.isArray(settings.hooks.SessionEnd)) {
1501
+ settings.hooks.SessionEnd = [];
1502
+ }
1503
+ if (!hasKeepGoingHook(settings.hooks.SessionEnd)) {
1504
+ settings.hooks.SessionEnd.push(SESSION_END_HOOK);
1505
+ changed = true;
1506
+ }
1426
1507
  return changed;
1427
1508
  }
1428
1509
  function checkHookConflict(scope, workspacePath) {
1429
1510
  const otherPaths = resolveScopePaths(scope === "user" ? "project" : "user", workspacePath);
1430
- if (!fs5.existsSync(otherPaths.settingsPath)) {
1511
+ if (!fs6.existsSync(otherPaths.settingsPath)) {
1431
1512
  return null;
1432
1513
  }
1433
1514
  try {
1434
- const otherSettings = JSON.parse(fs5.readFileSync(otherPaths.settingsPath, "utf-8"));
1515
+ const otherSettings = JSON.parse(fs6.readFileSync(otherPaths.settingsPath, "utf-8"));
1435
1516
  const hooks = otherSettings?.hooks;
1436
1517
  if (!hooks) return null;
1437
1518
  const hasConflict = Array.isArray(hooks.SessionStart) && hasKeepGoingHook(hooks.SessionStart) || Array.isArray(hooks.Stop) && hasKeepGoingHook(hooks.Stop);
@@ -1450,16 +1531,20 @@ function setupProject(options) {
1450
1531
  scope = "project",
1451
1532
  sessionHooks = true,
1452
1533
  claudeMd = true,
1453
- hasProLicense = false,
1534
+ claudeDir: claudeDirOverride,
1454
1535
  statusline
1455
1536
  } = options;
1456
1537
  const messages = [];
1457
1538
  let changed = false;
1458
- const { claudeDir, settingsPath, claudeMdPath } = resolveScopePaths(scope, workspacePath);
1459
- const scopeLabel = scope === "user" ? "~/.claude/settings.json" : ".claude/settings.json";
1539
+ const { claudeDir, settingsPath, claudeMdPath, rulesPath } = resolveScopePaths(
1540
+ scope,
1541
+ workspacePath,
1542
+ claudeDirOverride
1543
+ );
1544
+ const scopeLabel = scope === "user" ? path8.join("~", path8.relative(os3.homedir(), claudeDir), "settings.json").replace(/\\/g, "/") : ".claude/settings.json";
1460
1545
  let settings = {};
1461
- if (fs5.existsSync(settingsPath)) {
1462
- settings = JSON.parse(fs5.readFileSync(settingsPath, "utf-8"));
1546
+ if (fs6.existsSync(settingsPath)) {
1547
+ settings = JSON.parse(fs6.readFileSync(settingsPath, "utf-8"));
1463
1548
  }
1464
1549
  let settingsChanged = false;
1465
1550
  if (sessionHooks) {
@@ -1475,7 +1560,7 @@ function setupProject(options) {
1475
1560
  messages.push(`Warning: ${conflict}`);
1476
1561
  }
1477
1562
  }
1478
- if (scope === "project" && hasProLicense) {
1563
+ if (scope === "project") {
1479
1564
  const needsUpdate = settings.statusLine?.command && statusline?.isLegacy?.(settings.statusLine.command);
1480
1565
  if (!settings.statusLine || needsUpdate) {
1481
1566
  settings.statusLine = {
@@ -1490,29 +1575,45 @@ function setupProject(options) {
1490
1575
  statusline?.cleanup?.();
1491
1576
  }
1492
1577
  if (settingsChanged) {
1493
- if (!fs5.existsSync(claudeDir)) {
1494
- fs5.mkdirSync(claudeDir, { recursive: true });
1578
+ if (!fs6.existsSync(claudeDir)) {
1579
+ fs6.mkdirSync(claudeDir, { recursive: true });
1495
1580
  }
1496
- fs5.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
1581
+ fs6.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
1497
1582
  changed = true;
1498
1583
  }
1499
1584
  if (claudeMd) {
1500
- let existing = "";
1501
- if (fs5.existsSync(claudeMdPath)) {
1502
- existing = fs5.readFileSync(claudeMdPath, "utf-8");
1503
- }
1504
- const mdLabel = scope === "user" ? "~/.claude/CLAUDE.md" : "CLAUDE.md";
1505
- if (existing.includes("## KeepGoing")) {
1506
- messages.push(`CLAUDE.md: KeepGoing section already present in ${mdLabel}, skipped`);
1585
+ const rulesDir = path8.dirname(rulesPath);
1586
+ const rulesLabel = scope === "user" ? path8.join(path8.relative(os3.homedir(), path8.dirname(rulesPath)), "keepgoing.md").replace(/\\/g, "/") : ".claude/rules/keepgoing.md";
1587
+ if (fs6.existsSync(rulesPath)) {
1588
+ const existing = fs6.readFileSync(rulesPath, "utf-8");
1589
+ const existingVersion = getRulesFileVersion(existing);
1590
+ if (existingVersion === null) {
1591
+ messages.push(`Rules file: Custom file found at ${rulesLabel}, skipping`);
1592
+ } else if (existingVersion >= KEEPGOING_RULES_VERSION) {
1593
+ messages.push(`Rules file: Already up to date (v${existingVersion}), skipped`);
1594
+ } else {
1595
+ if (!fs6.existsSync(rulesDir)) {
1596
+ fs6.mkdirSync(rulesDir, { recursive: true });
1597
+ }
1598
+ fs6.writeFileSync(rulesPath, KEEPGOING_RULES_CONTENT);
1599
+ changed = true;
1600
+ messages.push(`Rules file: Updated v${existingVersion} \u2192 v${KEEPGOING_RULES_VERSION} at ${rulesLabel}`);
1601
+ }
1507
1602
  } else {
1508
- const updated = existing + CLAUDE_MD_SECTION;
1509
- const mdDir = path7.dirname(claudeMdPath);
1510
- if (!fs5.existsSync(mdDir)) {
1511
- fs5.mkdirSync(mdDir, { recursive: true });
1603
+ const existingClaudeMd = fs6.existsSync(claudeMdPath) ? fs6.readFileSync(claudeMdPath, "utf-8") : "";
1604
+ if (!fs6.existsSync(rulesDir)) {
1605
+ fs6.mkdirSync(rulesDir, { recursive: true });
1512
1606
  }
1513
- fs5.writeFileSync(claudeMdPath, updated);
1607
+ fs6.writeFileSync(rulesPath, KEEPGOING_RULES_CONTENT);
1514
1608
  changed = true;
1515
- messages.push(`CLAUDE.md: Added KeepGoing section to ${mdLabel}`);
1609
+ if (existingClaudeMd.includes("## KeepGoing")) {
1610
+ const mdLabel = scope === "user" ? "~/.claude/CLAUDE.md" : "CLAUDE.md";
1611
+ messages.push(
1612
+ `Rules file: Created ${rulesLabel} (you can now remove the ## KeepGoing section from ${mdLabel})`
1613
+ );
1614
+ } else {
1615
+ messages.push(`Rules file: Created ${rulesLabel}`);
1616
+ }
1516
1617
  }
1517
1618
  }
1518
1619
  return { messages, changed };
@@ -1920,14 +2021,14 @@ function renderEnrichedBriefingQuiet(briefing) {
1920
2021
  // src/updateCheck.ts
1921
2022
  import { spawn } from "child_process";
1922
2023
  import { readFileSync, existsSync } from "fs";
1923
- import path8 from "path";
1924
- import os3 from "os";
1925
- var CLI_VERSION = "1.1.0";
2024
+ import path9 from "path";
2025
+ import os4 from "os";
2026
+ var CLI_VERSION = "1.2.0";
1926
2027
  var NPM_REGISTRY_URL = "https://registry.npmjs.org/@keepgoingdev/cli/latest";
1927
2028
  var FETCH_TIMEOUT_MS = 5e3;
1928
2029
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
1929
- var CACHE_DIR = path8.join(os3.homedir(), ".keepgoing");
1930
- var CACHE_PATH = path8.join(CACHE_DIR, "update-check.json");
2030
+ var CACHE_DIR = path9.join(os4.homedir(), ".keepgoing");
2031
+ var CACHE_PATH = path9.join(CACHE_DIR, "update-check.json");
1931
2032
  function isNewerVersion(current, latest) {
1932
2033
  const cur = current.split(".").map(Number);
1933
2034
  const lat = latest.split(".").map(Number);
@@ -2042,7 +2143,7 @@ async function statusCommand(opts) {
2042
2143
  }
2043
2144
 
2044
2145
  // src/commands/save.ts
2045
- import path9 from "path";
2146
+ import path10 from "path";
2046
2147
  async function saveCommand(opts) {
2047
2148
  const { cwd, message, nextStepOverride, json, quiet, force } = opts;
2048
2149
  const isManual = !!message;
@@ -2071,9 +2172,9 @@ async function saveCommand(opts) {
2071
2172
  sessionStartTime: lastSession?.timestamp ?? now,
2072
2173
  lastActivityTime: now
2073
2174
  });
2074
- const summary = message ?? buildSmartSummary(events) ?? `Worked on ${touchedFiles.slice(0, 5).map((f) => path9.basename(f)).join(", ")}`;
2175
+ const summary = message ?? buildSmartSummary(events) ?? `Worked on ${touchedFiles.slice(0, 5).map((f) => path10.basename(f)).join(", ")}`;
2075
2176
  const nextStep = nextStepOverride ?? buildSmartNextStep(events);
2076
- const projectName = path9.basename(resolveStorageRoot(cwd));
2177
+ const projectName = path10.basename(resolveStorageRoot(cwd));
2077
2178
  const sessionId = generateSessionId({
2078
2179
  workspaceRoot: cwd,
2079
2180
  branch: gitBranch ?? void 0,
@@ -2106,12 +2207,25 @@ async function saveCommand(opts) {
2106
2207
  }
2107
2208
 
2108
2209
  // src/commands/hook.ts
2109
- import fs6 from "fs";
2110
- import path10 from "path";
2111
- import os4 from "os";
2210
+ import fs7 from "fs";
2211
+ import path11 from "path";
2212
+ import os5 from "os";
2112
2213
  import { execSync } from "child_process";
2113
2214
  var HOOK_MARKER_START = "# keepgoing-hook-start";
2114
2215
  var HOOK_MARKER_END = "# keepgoing-hook-end";
2216
+ var POST_COMMIT_MARKER_START = "# keepgoing-post-commit-start";
2217
+ var POST_COMMIT_MARKER_END = "# keepgoing-post-commit-end";
2218
+ var KEEPGOING_HOOKS_DIR = path11.join(os5.homedir(), ".keepgoing", "hooks");
2219
+ var POST_COMMIT_HOOK_PATH = path11.join(KEEPGOING_HOOKS_DIR, "post-commit");
2220
+ var KEEPGOING_MANAGED_MARKER = path11.join(KEEPGOING_HOOKS_DIR, ".keepgoing-managed");
2221
+ var POST_COMMIT_HOOK = `#!/bin/sh
2222
+ ${POST_COMMIT_MARKER_START}
2223
+ # Runs after every git commit. Detects high-signal decisions.
2224
+ if command -v keepgoing-mcp-server >/dev/null 2>&1; then
2225
+ keepgoing-mcp-server --detect-decisions 2>/dev/null &
2226
+ fi
2227
+ ${POST_COMMIT_MARKER_END}
2228
+ `;
2115
2229
  var ZSH_HOOK = `${HOOK_MARKER_START}
2116
2230
  # KeepGoing shell hook \u2014 auto-injected by 'keepgoing hook install'
2117
2231
  if command -v keepgoing >/dev/null 2>&1; then
@@ -2144,7 +2258,7 @@ if command -v keepgoing >/dev/null 2>&1
2144
2258
  end
2145
2259
  ${HOOK_MARKER_END}`;
2146
2260
  function detectShellRcFile(shellOverride) {
2147
- const home = os4.homedir();
2261
+ const home = os5.homedir();
2148
2262
  let shell;
2149
2263
  if (shellOverride) {
2150
2264
  shell = shellOverride.toLowerCase();
@@ -2167,17 +2281,155 @@ function detectShellRcFile(shellOverride) {
2167
2281
  }
2168
2282
  }
2169
2283
  if (shell === "zsh") {
2170
- return { shell: "zsh", rcFile: path10.join(home, ".zshrc") };
2284
+ return { shell: "zsh", rcFile: path11.join(home, ".zshrc") };
2171
2285
  }
2172
2286
  if (shell === "bash") {
2173
- return { shell: "bash", rcFile: path10.join(home, ".bashrc") };
2287
+ return { shell: "bash", rcFile: path11.join(home, ".bashrc") };
2174
2288
  }
2175
2289
  if (shell === "fish") {
2176
- const xdgConfig = process.env["XDG_CONFIG_HOME"] || path10.join(home, ".config");
2177
- return { shell: "fish", rcFile: path10.join(xdgConfig, "fish", "config.fish") };
2290
+ const xdgConfig = process.env["XDG_CONFIG_HOME"] || path11.join(home, ".config");
2291
+ return { shell: "fish", rcFile: path11.join(xdgConfig, "fish", "config.fish") };
2178
2292
  }
2179
2293
  return void 0;
2180
2294
  }
2295
+ function resolveGlobalGitignorePath() {
2296
+ try {
2297
+ const configured = execSync("git config --global core.excludesfile", {
2298
+ encoding: "utf-8",
2299
+ stdio: ["pipe", "pipe", "pipe"]
2300
+ }).trim();
2301
+ if (configured) {
2302
+ return configured.startsWith("~") ? path11.join(os5.homedir(), configured.slice(1)) : configured;
2303
+ }
2304
+ } catch {
2305
+ }
2306
+ return path11.join(os5.homedir(), ".gitignore_global");
2307
+ }
2308
+ function installGlobalGitignore() {
2309
+ const ignorePath = resolveGlobalGitignorePath();
2310
+ let existing = "";
2311
+ try {
2312
+ existing = fs7.readFileSync(ignorePath, "utf-8");
2313
+ } catch {
2314
+ }
2315
+ if (existing.split("\n").some((line) => line.trim() === ".keepgoing")) {
2316
+ console.log(`Global gitignore: .keepgoing already present in ${ignorePath}`);
2317
+ } else {
2318
+ const suffix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
2319
+ fs7.appendFileSync(ignorePath, `${suffix}.keepgoing
2320
+ `, "utf-8");
2321
+ console.log(`Global gitignore: .keepgoing added to ${ignorePath}`);
2322
+ }
2323
+ try {
2324
+ execSync("git config --global core.excludesfile", {
2325
+ encoding: "utf-8",
2326
+ stdio: ["pipe", "pipe", "pipe"]
2327
+ }).trim();
2328
+ } catch {
2329
+ execSync(`git config --global core.excludesfile "${ignorePath}"`, {
2330
+ stdio: ["pipe", "pipe", "pipe"]
2331
+ });
2332
+ }
2333
+ }
2334
+ function uninstallGlobalGitignore() {
2335
+ const ignorePath = resolveGlobalGitignorePath();
2336
+ let existing = "";
2337
+ try {
2338
+ existing = fs7.readFileSync(ignorePath, "utf-8");
2339
+ } catch {
2340
+ return;
2341
+ }
2342
+ const lines = existing.split("\n");
2343
+ const filtered = lines.filter((line) => line.trim() !== ".keepgoing");
2344
+ if (filtered.length !== lines.length) {
2345
+ fs7.writeFileSync(ignorePath, filtered.join("\n"), "utf-8");
2346
+ console.log(`Global gitignore: .keepgoing removed from ${ignorePath}`);
2347
+ }
2348
+ }
2349
+ function installPostCommitHook() {
2350
+ fs7.mkdirSync(KEEPGOING_HOOKS_DIR, { recursive: true });
2351
+ if (fs7.existsSync(POST_COMMIT_HOOK_PATH)) {
2352
+ const existing = fs7.readFileSync(POST_COMMIT_HOOK_PATH, "utf-8");
2353
+ if (existing.includes(POST_COMMIT_MARKER_START)) {
2354
+ console.log(`Git post-commit hook: already installed at ${POST_COMMIT_HOOK_PATH}`);
2355
+ } else {
2356
+ const suffix = existing.endsWith("\n") ? "" : "\n";
2357
+ const block = `${suffix}
2358
+ ${POST_COMMIT_MARKER_START}
2359
+ # Runs after every git commit. Detects high-signal decisions.
2360
+ if command -v keepgoing-mcp-server >/dev/null 2>&1; then
2361
+ keepgoing-mcp-server --detect-decisions 2>/dev/null &
2362
+ fi
2363
+ ${POST_COMMIT_MARKER_END}
2364
+ `;
2365
+ fs7.appendFileSync(POST_COMMIT_HOOK_PATH, block, "utf-8");
2366
+ console.log(`Git post-commit hook: appended to existing ${POST_COMMIT_HOOK_PATH}`);
2367
+ }
2368
+ } else {
2369
+ fs7.writeFileSync(POST_COMMIT_HOOK_PATH, POST_COMMIT_HOOK, { mode: 493 });
2370
+ console.log(`Git post-commit hook: installed at ${POST_COMMIT_HOOK_PATH}`);
2371
+ }
2372
+ fs7.chmodSync(POST_COMMIT_HOOK_PATH, 493);
2373
+ let currentHooksPath;
2374
+ try {
2375
+ currentHooksPath = execSync("git config --global core.hooksPath", {
2376
+ encoding: "utf-8",
2377
+ stdio: ["pipe", "pipe", "pipe"]
2378
+ }).trim();
2379
+ } catch {
2380
+ }
2381
+ if (currentHooksPath === KEEPGOING_HOOKS_DIR) {
2382
+ console.log(`Git hooks path: already set to ${KEEPGOING_HOOKS_DIR}`);
2383
+ } else if (currentHooksPath) {
2384
+ console.log(`Git hooks path: set to ${currentHooksPath}`);
2385
+ console.log(` To use KeepGoing's post-commit hook, add the following to ${currentHooksPath}/post-commit:`);
2386
+ console.log(` if command -v keepgoing-mcp-server >/dev/null 2>&1; then`);
2387
+ console.log(` keepgoing-mcp-server --detect-decisions 2>/dev/null &`);
2388
+ console.log(` fi`);
2389
+ } else {
2390
+ console.log(`Git post-commit hook: saved to ${POST_COMMIT_HOOK_PATH}`);
2391
+ console.log(` To activate it globally, run:`);
2392
+ console.log(` git config --global core.hooksPath "${KEEPGOING_HOOKS_DIR}"`);
2393
+ console.log(` Note: this overrides per-repo .git/hooks/ for all repositories.`);
2394
+ }
2395
+ }
2396
+ function uninstallPostCommitHook() {
2397
+ if (fs7.existsSync(POST_COMMIT_HOOK_PATH)) {
2398
+ const existing = fs7.readFileSync(POST_COMMIT_HOOK_PATH, "utf-8");
2399
+ if (existing.includes(POST_COMMIT_MARKER_START)) {
2400
+ const pattern = new RegExp(
2401
+ `
2402
+ ?${POST_COMMIT_MARKER_START}[\\s\\S]*?${POST_COMMIT_MARKER_END}
2403
+ ?`,
2404
+ "g"
2405
+ );
2406
+ const updated = existing.replace(pattern, "").trim();
2407
+ if (!updated || updated === "#!/bin/sh") {
2408
+ fs7.unlinkSync(POST_COMMIT_HOOK_PATH);
2409
+ console.log(`Git post-commit hook: removed ${POST_COMMIT_HOOK_PATH}`);
2410
+ } else {
2411
+ fs7.writeFileSync(POST_COMMIT_HOOK_PATH, updated + "\n", "utf-8");
2412
+ console.log(`Git post-commit hook: KeepGoing block removed from ${POST_COMMIT_HOOK_PATH}`);
2413
+ }
2414
+ }
2415
+ }
2416
+ try {
2417
+ const currentHooksPath = execSync("git config --global core.hooksPath", {
2418
+ encoding: "utf-8",
2419
+ stdio: ["pipe", "pipe", "pipe"]
2420
+ }).trim();
2421
+ if (currentHooksPath === KEEPGOING_HOOKS_DIR) {
2422
+ execSync("git config --global --unset core.hooksPath", {
2423
+ stdio: ["pipe", "pipe", "pipe"]
2424
+ });
2425
+ console.log("Git hooks path: global core.hooksPath unset");
2426
+ }
2427
+ } catch {
2428
+ }
2429
+ if (fs7.existsSync(KEEPGOING_MANAGED_MARKER)) {
2430
+ fs7.unlinkSync(KEEPGOING_MANAGED_MARKER);
2431
+ }
2432
+ }
2181
2433
  function hookInstallCommand(shellOverride) {
2182
2434
  const detected = detectShellRcFile(shellOverride);
2183
2435
  if (!detected) {
@@ -2190,18 +2442,23 @@ function hookInstallCommand(shellOverride) {
2190
2442
  const hookBlock = shell === "zsh" ? ZSH_HOOK : shell === "fish" ? FISH_HOOK : BASH_HOOK;
2191
2443
  let existing = "";
2192
2444
  try {
2193
- existing = fs6.readFileSync(rcFile, "utf-8");
2445
+ existing = fs7.readFileSync(rcFile, "utf-8");
2194
2446
  } catch {
2195
2447
  }
2196
2448
  if (existing.includes(HOOK_MARKER_START)) {
2197
2449
  console.log(`KeepGoing hook is already installed in ${rcFile}.`);
2450
+ installGlobalGitignore();
2451
+ installPostCommitHook();
2198
2452
  return;
2199
2453
  }
2200
- fs6.appendFileSync(rcFile, `
2454
+ fs7.appendFileSync(rcFile, `
2201
2455
  ${hookBlock}
2202
2456
  `, "utf-8");
2203
2457
  console.log(`KeepGoing hook installed in ${rcFile}.`);
2204
- console.log(`Reload your shell config to activate it:
2458
+ installGlobalGitignore();
2459
+ installPostCommitHook();
2460
+ console.log(`
2461
+ Reload your shell config to activate it:
2205
2462
  `);
2206
2463
  console.log(` source ${rcFile}`);
2207
2464
  }
@@ -2216,7 +2473,7 @@ function hookUninstallCommand(shellOverride) {
2216
2473
  const { rcFile } = detected;
2217
2474
  let existing = "";
2218
2475
  try {
2219
- existing = fs6.readFileSync(rcFile, "utf-8");
2476
+ existing = fs7.readFileSync(rcFile, "utf-8");
2220
2477
  } catch {
2221
2478
  console.log(`${rcFile} not found \u2014 nothing to remove.`);
2222
2479
  return;
@@ -2232,9 +2489,12 @@ function hookUninstallCommand(shellOverride) {
2232
2489
  "g"
2233
2490
  );
2234
2491
  const updated = existing.replace(pattern, "").replace(/\n{3,}/g, "\n\n");
2235
- fs6.writeFileSync(rcFile, updated, "utf-8");
2492
+ fs7.writeFileSync(rcFile, updated, "utf-8");
2236
2493
  console.log(`KeepGoing hook removed from ${rcFile}.`);
2237
- console.log(`Reload your shell config to deactivate it:
2494
+ uninstallGlobalGitignore();
2495
+ uninstallPostCommitHook();
2496
+ console.log(`
2497
+ Reload your shell config to deactivate it:
2238
2498
  `);
2239
2499
  console.log(` source ${rcFile}`);
2240
2500
  }
@@ -2392,11 +2652,9 @@ var CYAN2 = "\x1B[36m";
2392
2652
  var DIM3 = "\x1B[2m";
2393
2653
  function initCommand(options) {
2394
2654
  const scope = options.scope === "user" ? "user" : "project";
2395
- const hasProLicense = process.env.KEEPGOING_PRO_BYPASS === "1" || !!getLicenseForFeature("session-awareness");
2396
2655
  const result = setupProject({
2397
2656
  workspacePath: options.cwd,
2398
- scope,
2399
- hasProLicense
2657
+ scope
2400
2658
  });
2401
2659
  console.log(`
2402
2660
  ${BOLD3}KeepGoing Init${RESET3} ${DIM3}(${scope} scope)${RESET3}
@@ -3219,7 +3477,7 @@ async function main() {
3219
3477
  }
3220
3478
  break;
3221
3479
  case "version":
3222
- console.log(`keepgoing v${"1.1.0"}`);
3480
+ console.log(`keepgoing v${"1.2.0"}`);
3223
3481
  break;
3224
3482
  case "activate":
3225
3483
  await activateCommand({ licenseKey: subcommand });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keepgoingdev/cli",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Terminal CLI for KeepGoing. Resume side projects without the mental friction.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",