@keepgoingdev/mcp-server 0.4.0 → 0.5.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
@@ -1,14 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import path8 from "path";
5
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
6
 
8
- // src/storage.ts
9
- import fs4 from "fs";
10
- import path5 from "path";
11
-
12
7
  // ../../packages/shared/src/session.ts
13
8
  import { randomUUID } from "crypto";
14
9
  function generateCheckpointId() {
@@ -116,10 +111,10 @@ function resolveStorageRoot(startPath) {
116
111
  return startPath;
117
112
  }
118
113
  }
119
- function getCurrentBranch(workspacePath2) {
114
+ function getCurrentBranch(workspacePath) {
120
115
  try {
121
116
  const result = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
122
- cwd: workspacePath2,
117
+ cwd: workspacePath,
123
118
  encoding: "utf-8",
124
119
  timeout: 5e3
125
120
  });
@@ -128,14 +123,14 @@ function getCurrentBranch(workspacePath2) {
128
123
  return void 0;
129
124
  }
130
125
  }
131
- function getGitLogSince(workspacePath2, format, sinceTimestamp) {
126
+ function getGitLogSince(workspacePath, format, sinceTimestamp) {
132
127
  try {
133
128
  const since = sinceTimestamp || new Date(Date.now() - 24 * 60 * 60 * 1e3).toISOString();
134
129
  const result = execFileSync(
135
130
  "git",
136
131
  ["log", `--since=${since}`, `--format=${format}`],
137
132
  {
138
- cwd: workspacePath2,
133
+ cwd: workspacePath,
139
134
  encoding: "utf-8",
140
135
  timeout: 5e3
141
136
  }
@@ -148,16 +143,16 @@ function getGitLogSince(workspacePath2, format, sinceTimestamp) {
148
143
  return [];
149
144
  }
150
145
  }
151
- function getCommitsSince(workspacePath2, sinceTimestamp) {
152
- return getGitLogSince(workspacePath2, "%H", sinceTimestamp);
146
+ function getCommitsSince(workspacePath, sinceTimestamp) {
147
+ return getGitLogSince(workspacePath, "%H", sinceTimestamp);
153
148
  }
154
- function getCommitMessagesSince(workspacePath2, sinceTimestamp) {
155
- return getGitLogSince(workspacePath2, "%s", sinceTimestamp);
149
+ function getCommitMessagesSince(workspacePath, sinceTimestamp) {
150
+ return getGitLogSince(workspacePath, "%s", sinceTimestamp);
156
151
  }
157
- function getHeadCommitHash(workspacePath2) {
152
+ function getHeadCommitHash(workspacePath) {
158
153
  try {
159
154
  const result = execFileSync("git", ["rev-parse", "HEAD"], {
160
- cwd: workspacePath2,
155
+ cwd: workspacePath,
161
156
  encoding: "utf-8",
162
157
  timeout: 5e3
163
158
  });
@@ -166,10 +161,10 @@ function getHeadCommitHash(workspacePath2) {
166
161
  return void 0;
167
162
  }
168
163
  }
169
- function getTouchedFiles(workspacePath2) {
164
+ function getTouchedFiles(workspacePath) {
170
165
  try {
171
166
  const result = execFileSync("git", ["status", "--porcelain"], {
172
- cwd: workspacePath2,
167
+ cwd: workspacePath,
173
168
  encoding: "utf-8",
174
169
  timeout: 5e3
175
170
  });
@@ -385,22 +380,34 @@ function inferFocusFromFiles(files) {
385
380
  // ../../packages/shared/src/storage.ts
386
381
  import fs from "fs";
387
382
  import path2 from "path";
388
- import { randomUUID as randomUUID2 } from "crypto";
383
+ import { randomUUID as randomUUID2, createHash } from "crypto";
389
384
  var STORAGE_DIR = ".keepgoing";
390
385
  var META_FILE = "meta.json";
391
386
  var SESSIONS_FILE = "sessions.json";
392
387
  var STATE_FILE = "state.json";
388
+ var CURRENT_TASKS_FILE = "current-tasks.json";
389
+ var STALE_SESSION_MS = 2 * 60 * 60 * 1e3;
390
+ function pruneStaleTasks(tasks) {
391
+ const now = Date.now();
392
+ return tasks.filter((t) => {
393
+ if (t.sessionActive) return true;
394
+ const updatedAt = new Date(t.updatedAt).getTime();
395
+ return !isNaN(updatedAt) && now - updatedAt < STALE_SESSION_MS;
396
+ });
397
+ }
393
398
  var KeepGoingWriter = class {
394
399
  storagePath;
395
400
  sessionsFilePath;
396
401
  stateFilePath;
397
402
  metaFilePath;
398
- constructor(workspacePath2) {
399
- const mainRoot = resolveStorageRoot(workspacePath2);
403
+ currentTasksFilePath;
404
+ constructor(workspacePath) {
405
+ const mainRoot = resolveStorageRoot(workspacePath);
400
406
  this.storagePath = path2.join(mainRoot, STORAGE_DIR);
401
407
  this.sessionsFilePath = path2.join(this.storagePath, SESSIONS_FILE);
402
408
  this.stateFilePath = path2.join(this.storagePath, STATE_FILE);
403
409
  this.metaFilePath = path2.join(this.storagePath, META_FILE);
410
+ this.currentTasksFilePath = path2.join(this.storagePath, CURRENT_TASKS_FILE);
404
411
  }
405
412
  ensureDir() {
406
413
  if (!fs.existsSync(this.storagePath)) {
@@ -461,11 +468,212 @@ var KeepGoingWriter = class {
461
468
  }
462
469
  fs.writeFileSync(this.metaFilePath, JSON.stringify(meta, null, 2), "utf-8");
463
470
  }
471
+ // ---------------------------------------------------------------------------
472
+ // Multi-session API
473
+ // ---------------------------------------------------------------------------
474
+ /** Read all current tasks from current-tasks.json. Auto-prunes stale sessions. */
475
+ readCurrentTasks() {
476
+ try {
477
+ if (fs.existsSync(this.currentTasksFilePath)) {
478
+ const raw = JSON.parse(fs.readFileSync(this.currentTasksFilePath, "utf-8"));
479
+ const tasks = Array.isArray(raw) ? raw : raw.tasks ?? [];
480
+ return this.pruneStale(tasks);
481
+ }
482
+ } catch {
483
+ }
484
+ return [];
485
+ }
486
+ /**
487
+ * Upsert a session task by sessionId into current-tasks.json.
488
+ * If no sessionId is present on the task, generates one.
489
+ */
490
+ upsertSession(update) {
491
+ this.ensureDir();
492
+ this.upsertSessionCore(update);
493
+ }
494
+ /** Core upsert logic: merges the update into current-tasks.json and returns the pruned task list. */
495
+ upsertSessionCore(update) {
496
+ this.ensureDir();
497
+ const sessionId = update.sessionId || generateSessionId(update);
498
+ const tasks = this.readAllTasksRaw();
499
+ const existingIdx = tasks.findIndex((t) => t.sessionId === sessionId);
500
+ let merged;
501
+ if (existingIdx >= 0) {
502
+ const existing = tasks[existingIdx];
503
+ merged = { ...existing, ...update, sessionId };
504
+ tasks[existingIdx] = merged;
505
+ } else {
506
+ merged = { ...update, sessionId };
507
+ tasks.push(merged);
508
+ }
509
+ const pruned = this.pruneStale(tasks);
510
+ this.writeTasksFile(pruned);
511
+ return pruned;
512
+ }
513
+ /** Remove a specific session by ID. */
514
+ removeSession(sessionId) {
515
+ const tasks = this.readAllTasksRaw().filter((t) => t.sessionId !== sessionId);
516
+ this.writeTasksFile(tasks);
517
+ }
518
+ /** Get all active sessions (sessionActive=true and within stale threshold). */
519
+ getActiveSessions() {
520
+ return this.readCurrentTasks().filter((t) => t.sessionActive);
521
+ }
522
+ /** Get a specific session by ID. */
523
+ getSession(sessionId) {
524
+ return this.readCurrentTasks().find((t) => t.sessionId === sessionId);
525
+ }
526
+ // ---------------------------------------------------------------------------
527
+ // Private helpers
528
+ // ---------------------------------------------------------------------------
529
+ readAllTasksRaw() {
530
+ try {
531
+ if (fs.existsSync(this.currentTasksFilePath)) {
532
+ const raw = JSON.parse(fs.readFileSync(this.currentTasksFilePath, "utf-8"));
533
+ return Array.isArray(raw) ? [...raw] : [...raw.tasks ?? []];
534
+ }
535
+ } catch {
536
+ }
537
+ return [];
538
+ }
539
+ pruneStale(tasks) {
540
+ return pruneStaleTasks(tasks);
541
+ }
542
+ writeTasksFile(tasks) {
543
+ const data = { version: 1, tasks };
544
+ fs.writeFileSync(this.currentTasksFilePath, JSON.stringify(data, null, 2), "utf-8");
545
+ }
464
546
  };
547
+ function generateSessionId(context) {
548
+ const parts = [
549
+ context.worktreePath || context.workspaceRoot || "",
550
+ context.agentLabel || "",
551
+ context.branch || ""
552
+ ].filter(Boolean);
553
+ if (parts.length === 0) {
554
+ return randomUUID2();
555
+ }
556
+ const hash = createHash("sha256").update(parts.join("|")).digest("hex").slice(0, 12);
557
+ return `ses_${hash}`;
558
+ }
465
559
 
466
560
  // ../../packages/shared/src/decisionStorage.ts
561
+ import fs3 from "fs";
562
+ import path4 from "path";
563
+
564
+ // ../../packages/shared/src/license.ts
565
+ import crypto from "crypto";
467
566
  import fs2 from "fs";
567
+ import os from "os";
468
568
  import path3 from "path";
569
+ var LICENSE_FILE = "license.json";
570
+ var DEVICE_ID_FILE = "device-id";
571
+ function getGlobalLicenseDir() {
572
+ return path3.join(os.homedir(), ".keepgoing");
573
+ }
574
+ function getGlobalLicensePath() {
575
+ return path3.join(getGlobalLicenseDir(), LICENSE_FILE);
576
+ }
577
+ function getDeviceId() {
578
+ const dir = getGlobalLicenseDir();
579
+ const filePath = path3.join(dir, DEVICE_ID_FILE);
580
+ try {
581
+ const existing = fs2.readFileSync(filePath, "utf-8").trim();
582
+ if (existing) return existing;
583
+ } catch {
584
+ }
585
+ const id = crypto.randomUUID();
586
+ if (!fs2.existsSync(dir)) {
587
+ fs2.mkdirSync(dir, { recursive: true });
588
+ }
589
+ fs2.writeFileSync(filePath, id, "utf-8");
590
+ return id;
591
+ }
592
+ var DECISION_DETECTION_VARIANT_ID = 1361527;
593
+ var SESSION_AWARENESS_VARIANT_ID = 1366510;
594
+ var TEST_DECISION_DETECTION_VARIANT_ID = 1345647;
595
+ var TEST_SESSION_AWARENESS_VARIANT_ID = 1365992;
596
+ var VARIANT_FEATURE_MAP = {
597
+ [DECISION_DETECTION_VARIANT_ID]: ["decisions"],
598
+ [SESSION_AWARENESS_VARIANT_ID]: ["session-awareness"],
599
+ [TEST_DECISION_DETECTION_VARIANT_ID]: ["decisions"],
600
+ [TEST_SESSION_AWARENESS_VARIANT_ID]: ["session-awareness"]
601
+ // Future bundle: [BUNDLE_VARIANT_ID]: ['decisions', 'session-awareness'],
602
+ };
603
+ var KNOWN_VARIANT_IDS = new Set(Object.keys(VARIANT_FEATURE_MAP).map(Number));
604
+ function getVariantLabel(variantId) {
605
+ const features = VARIANT_FEATURE_MAP[variantId];
606
+ if (!features) return "Unknown Add-on";
607
+ if (features.includes("decisions") && features.includes("session-awareness")) return "Pro Bundle";
608
+ if (features.includes("decisions")) return "Decision Detection";
609
+ if (features.includes("session-awareness")) return "Session Awareness";
610
+ return "Pro Add-on";
611
+ }
612
+ var _cachedStore;
613
+ var _cacheTimestamp = 0;
614
+ var LICENSE_CACHE_TTL_MS = 2e3;
615
+ function readLicenseStore() {
616
+ const now = Date.now();
617
+ if (_cachedStore && now - _cacheTimestamp < LICENSE_CACHE_TTL_MS) {
618
+ return _cachedStore;
619
+ }
620
+ const licensePath = getGlobalLicensePath();
621
+ let store;
622
+ try {
623
+ if (!fs2.existsSync(licensePath)) {
624
+ store = { version: 2, licenses: [] };
625
+ } else {
626
+ const raw = fs2.readFileSync(licensePath, "utf-8");
627
+ const data = JSON.parse(raw);
628
+ if (data?.version === 2 && Array.isArray(data.licenses)) {
629
+ store = data;
630
+ } else {
631
+ store = { version: 2, licenses: [] };
632
+ }
633
+ }
634
+ } catch {
635
+ store = { version: 2, licenses: [] };
636
+ }
637
+ _cachedStore = store;
638
+ _cacheTimestamp = now;
639
+ return store;
640
+ }
641
+ function writeLicenseStore(store) {
642
+ const dirPath = getGlobalLicenseDir();
643
+ if (!fs2.existsSync(dirPath)) {
644
+ fs2.mkdirSync(dirPath, { recursive: true });
645
+ }
646
+ const licensePath = path3.join(dirPath, LICENSE_FILE);
647
+ fs2.writeFileSync(licensePath, JSON.stringify(store, null, 2), "utf-8");
648
+ _cachedStore = store;
649
+ _cacheTimestamp = Date.now();
650
+ }
651
+ function addLicenseEntry(entry) {
652
+ const store = readLicenseStore();
653
+ const idx = store.licenses.findIndex((l) => l.licenseKey === entry.licenseKey);
654
+ if (idx >= 0) {
655
+ store.licenses[idx] = entry;
656
+ } else {
657
+ store.licenses.push(entry);
658
+ }
659
+ writeLicenseStore(store);
660
+ }
661
+ function removeLicenseEntry(licenseKey) {
662
+ const store = readLicenseStore();
663
+ store.licenses = store.licenses.filter((l) => l.licenseKey !== licenseKey);
664
+ writeLicenseStore(store);
665
+ }
666
+ function getActiveLicenses() {
667
+ return readLicenseStore().licenses.filter((l) => l.status === "active");
668
+ }
669
+ function getLicenseForFeature(feature) {
670
+ const active = getActiveLicenses();
671
+ return active.find((l) => {
672
+ const features = VARIANT_FEATURE_MAP[l.variantId];
673
+ return features?.includes(feature);
674
+ });
675
+ }
676
+ var REVALIDATION_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
469
677
 
470
678
  // ../../packages/shared/src/featureGate.ts
471
679
  var DefaultFeatureGate = class {
@@ -485,25 +693,25 @@ var MAX_DECISIONS = 100;
485
693
  var DecisionStorage = class {
486
694
  storagePath;
487
695
  decisionsFilePath;
488
- constructor(workspacePath2) {
489
- const mainRoot = resolveStorageRoot(workspacePath2);
490
- this.storagePath = path3.join(mainRoot, STORAGE_DIR2);
491
- this.decisionsFilePath = path3.join(this.storagePath, DECISIONS_FILE);
696
+ constructor(workspacePath) {
697
+ const mainRoot = resolveStorageRoot(workspacePath);
698
+ this.storagePath = path4.join(mainRoot, STORAGE_DIR2);
699
+ this.decisionsFilePath = path4.join(this.storagePath, DECISIONS_FILE);
492
700
  }
493
701
  ensureStorageDir() {
494
- if (!fs2.existsSync(this.storagePath)) {
495
- fs2.mkdirSync(this.storagePath, { recursive: true });
702
+ if (!fs3.existsSync(this.storagePath)) {
703
+ fs3.mkdirSync(this.storagePath, { recursive: true });
496
704
  }
497
705
  }
498
706
  getProjectName() {
499
- return path3.basename(path3.dirname(this.storagePath));
707
+ return path4.basename(path4.dirname(this.storagePath));
500
708
  }
501
709
  load() {
502
710
  try {
503
- if (!fs2.existsSync(this.decisionsFilePath)) {
711
+ if (!fs3.existsSync(this.decisionsFilePath)) {
504
712
  return createEmptyProjectDecisions(this.getProjectName());
505
713
  }
506
- const raw = fs2.readFileSync(this.decisionsFilePath, "utf-8");
714
+ const raw = fs3.readFileSync(this.decisionsFilePath, "utf-8");
507
715
  const data = JSON.parse(raw);
508
716
  return data;
509
717
  } catch {
@@ -513,7 +721,7 @@ var DecisionStorage = class {
513
721
  save(decisions) {
514
722
  this.ensureStorageDir();
515
723
  const content = JSON.stringify(decisions, null, 2);
516
- fs2.writeFileSync(this.decisionsFilePath, content, "utf-8");
724
+ fs3.writeFileSync(this.decisionsFilePath, content, "utf-8");
517
725
  }
518
726
  /**
519
727
  * Save a decision record as a draft. Always persists regardless of Pro
@@ -763,68 +971,6 @@ function tryDetectDecision(opts) {
763
971
  };
764
972
  }
765
973
 
766
- // ../../packages/shared/src/license.ts
767
- import crypto from "crypto";
768
- import fs3 from "fs";
769
- import os from "os";
770
- import path4 from "path";
771
- var LICENSE_FILE = "license.json";
772
- var DEVICE_ID_FILE = "device-id";
773
- function getGlobalLicenseDir() {
774
- return path4.join(os.homedir(), ".keepgoing");
775
- }
776
- function getGlobalLicensePath() {
777
- return path4.join(getGlobalLicenseDir(), LICENSE_FILE);
778
- }
779
- function getDeviceId() {
780
- const dir = getGlobalLicenseDir();
781
- const filePath = path4.join(dir, DEVICE_ID_FILE);
782
- try {
783
- const existing = fs3.readFileSync(filePath, "utf-8").trim();
784
- if (existing) return existing;
785
- } catch {
786
- }
787
- const id = crypto.randomUUID();
788
- if (!fs3.existsSync(dir)) {
789
- fs3.mkdirSync(dir, { recursive: true });
790
- }
791
- fs3.writeFileSync(filePath, id, "utf-8");
792
- return id;
793
- }
794
- function readLicenseCache() {
795
- const licensePath = getGlobalLicensePath();
796
- try {
797
- if (!fs3.existsSync(licensePath)) {
798
- return void 0;
799
- }
800
- const raw = fs3.readFileSync(licensePath, "utf-8");
801
- return JSON.parse(raw);
802
- } catch {
803
- return void 0;
804
- }
805
- }
806
- function writeLicenseCache(cache) {
807
- const dirPath = getGlobalLicenseDir();
808
- if (!fs3.existsSync(dirPath)) {
809
- fs3.mkdirSync(dirPath, { recursive: true });
810
- }
811
- const licensePath = path4.join(dirPath, LICENSE_FILE);
812
- fs3.writeFileSync(licensePath, JSON.stringify(cache, null, 2), "utf-8");
813
- }
814
- function deleteLicenseCache() {
815
- const licensePath = getGlobalLicensePath();
816
- try {
817
- if (fs3.existsSync(licensePath)) {
818
- fs3.unlinkSync(licensePath);
819
- }
820
- } catch {
821
- }
822
- }
823
- function isCachedLicenseValid(cache) {
824
- return cache?.status === "active";
825
- }
826
- var REVALIDATION_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
827
-
828
974
  // ../../packages/shared/src/licenseClient.ts
829
975
  var BASE_URL = "https://api.lemonsqueezy.com/v1/licenses";
830
976
  var REQUEST_TIMEOUT_MS = 15e3;
@@ -875,13 +1021,21 @@ async function activateLicense(licenseKey, instanceName, options) {
875
1021
  }
876
1022
  return { valid: false, error: productError };
877
1023
  }
1024
+ if (data.meta?.variant_id && !KNOWN_VARIANT_IDS.has(data.meta.variant_id)) {
1025
+ if (data.license_key?.key && data.instance?.id) {
1026
+ await deactivateLicense(data.license_key.key, data.instance.id);
1027
+ }
1028
+ return { valid: false, error: "This license key is for an unrecognized add-on variant. Please update KeepGoing or contact support." };
1029
+ }
878
1030
  }
879
1031
  return {
880
1032
  valid: true,
881
1033
  licenseKey: data.license_key?.key,
882
1034
  instanceId: data.instance?.id,
883
1035
  customerName: data.meta?.customer_name,
884
- productName: data.meta?.product_name
1036
+ productName: data.meta?.product_name,
1037
+ variantId: data.meta?.variant_id,
1038
+ variantName: data.meta?.variant_name
885
1039
  };
886
1040
  } catch (err) {
887
1041
  const message = err instanceof Error && err.name === "AbortError" ? "Request timed out. Please check your network connection and try again." : err instanceof Error ? err.message : "Network error";
@@ -907,11 +1061,14 @@ async function deactivateLicense(licenseKey, instanceId) {
907
1061
  }
908
1062
 
909
1063
  // src/storage.ts
1064
+ import fs4 from "fs";
1065
+ import path5 from "path";
910
1066
  var STORAGE_DIR3 = ".keepgoing";
911
1067
  var META_FILE2 = "meta.json";
912
1068
  var SESSIONS_FILE2 = "sessions.json";
913
1069
  var DECISIONS_FILE2 = "decisions.json";
914
1070
  var STATE_FILE2 = "state.json";
1071
+ var CURRENT_TASKS_FILE2 = "current-tasks.json";
915
1072
  var KeepGoingReader = class {
916
1073
  workspacePath;
917
1074
  storagePath;
@@ -919,18 +1076,20 @@ var KeepGoingReader = class {
919
1076
  sessionsFilePath;
920
1077
  decisionsFilePath;
921
1078
  stateFilePath;
1079
+ currentTasksFilePath;
922
1080
  _isWorktree;
923
1081
  _cachedBranch = null;
924
1082
  // null = not yet resolved
925
- constructor(workspacePath2) {
926
- this.workspacePath = workspacePath2;
927
- const mainRoot = resolveStorageRoot(workspacePath2);
928
- this._isWorktree = mainRoot !== workspacePath2;
1083
+ constructor(workspacePath) {
1084
+ this.workspacePath = workspacePath;
1085
+ const mainRoot = resolveStorageRoot(workspacePath);
1086
+ this._isWorktree = mainRoot !== workspacePath;
929
1087
  this.storagePath = path5.join(mainRoot, STORAGE_DIR3);
930
1088
  this.metaFilePath = path5.join(this.storagePath, META_FILE2);
931
1089
  this.sessionsFilePath = path5.join(this.storagePath, SESSIONS_FILE2);
932
1090
  this.decisionsFilePath = path5.join(this.storagePath, DECISIONS_FILE2);
933
1091
  this.stateFilePath = path5.join(this.storagePath, STATE_FILE2);
1092
+ this.currentTasksFilePath = path5.join(this.storagePath, CURRENT_TASKS_FILE2);
934
1093
  }
935
1094
  /** Check if .keepgoing/ directory exists. */
936
1095
  exists() {
@@ -992,9 +1151,81 @@ var KeepGoingReader = class {
992
1151
  const all = this.getDecisions();
993
1152
  return all.slice(-count).reverse();
994
1153
  }
995
- /** Read cached license data from the global `~/.keepgoing/license.json`. */
996
- getLicenseCache() {
997
- return readLicenseCache();
1154
+ /** Read the multi-license store from `~/.keepgoing/license.json`. */
1155
+ getLicenseStore() {
1156
+ return readLicenseStore();
1157
+ }
1158
+ /**
1159
+ * Read all current tasks from current-tasks.json.
1160
+ * Automatically filters out stale finished sessions (> 2 hours).
1161
+ */
1162
+ getCurrentTasks() {
1163
+ const multiRaw = this.readJsonFile(this.currentTasksFilePath);
1164
+ if (multiRaw) {
1165
+ const tasks = Array.isArray(multiRaw) ? multiRaw : multiRaw.tasks ?? [];
1166
+ return this.pruneStale(tasks);
1167
+ }
1168
+ return [];
1169
+ }
1170
+ /** Get only active sessions (sessionActive=true and within stale threshold). */
1171
+ getActiveTasks() {
1172
+ return this.getCurrentTasks().filter((t) => t.sessionActive);
1173
+ }
1174
+ /** Get a specific session by ID. */
1175
+ getTaskBySessionId(sessionId) {
1176
+ return this.getCurrentTasks().find((t) => t.sessionId === sessionId);
1177
+ }
1178
+ /**
1179
+ * Detect files being edited by multiple sessions simultaneously.
1180
+ * Returns pairs of session IDs and the conflicting file paths.
1181
+ */
1182
+ detectFileConflicts() {
1183
+ const activeTasks = this.getActiveTasks();
1184
+ if (activeTasks.length < 2) return [];
1185
+ const fileToSessions = /* @__PURE__ */ new Map();
1186
+ for (const task of activeTasks) {
1187
+ if (task.lastFileEdited && task.sessionId) {
1188
+ const existing = fileToSessions.get(task.lastFileEdited) ?? [];
1189
+ existing.push({
1190
+ sessionId: task.sessionId,
1191
+ agentLabel: task.agentLabel,
1192
+ branch: task.branch
1193
+ });
1194
+ fileToSessions.set(task.lastFileEdited, existing);
1195
+ }
1196
+ }
1197
+ const conflicts = [];
1198
+ for (const [file, sessions] of fileToSessions) {
1199
+ if (sessions.length > 1) {
1200
+ conflicts.push({ file, sessions });
1201
+ }
1202
+ }
1203
+ return conflicts;
1204
+ }
1205
+ /**
1206
+ * Detect sessions on the same branch (possible duplicate work).
1207
+ */
1208
+ detectBranchOverlap() {
1209
+ const activeTasks = this.getActiveTasks();
1210
+ if (activeTasks.length < 2) return [];
1211
+ const branchToSessions = /* @__PURE__ */ new Map();
1212
+ for (const task of activeTasks) {
1213
+ if (task.branch && task.sessionId) {
1214
+ const existing = branchToSessions.get(task.branch) ?? [];
1215
+ existing.push({ sessionId: task.sessionId, agentLabel: task.agentLabel });
1216
+ branchToSessions.set(task.branch, existing);
1217
+ }
1218
+ }
1219
+ const overlaps = [];
1220
+ for (const [branch, sessions] of branchToSessions) {
1221
+ if (sessions.length > 1) {
1222
+ overlaps.push({ branch, sessions });
1223
+ }
1224
+ }
1225
+ return overlaps;
1226
+ }
1227
+ pruneStale(tasks) {
1228
+ return pruneStaleTasks(tasks);
998
1229
  }
999
1230
  /** Get the last session checkpoint for a specific branch. */
1000
1231
  getLastSessionForBranch(branch) {
@@ -1112,13 +1343,13 @@ var KeepGoingReader = class {
1112
1343
  };
1113
1344
 
1114
1345
  // src/tools/getMomentum.ts
1115
- function registerGetMomentum(server2, reader2, workspacePath2) {
1116
- server2.tool(
1346
+ function registerGetMomentum(server, reader, workspacePath) {
1347
+ server.tool(
1117
1348
  "get_momentum",
1118
1349
  "Get current developer momentum: last checkpoint, next step, blockers, and branch context. Use this to understand where the developer left off.",
1119
1350
  {},
1120
1351
  async () => {
1121
- if (!reader2.exists()) {
1352
+ if (!reader.exists()) {
1122
1353
  return {
1123
1354
  content: [
1124
1355
  {
@@ -1128,8 +1359,8 @@ function registerGetMomentum(server2, reader2, workspacePath2) {
1128
1359
  ]
1129
1360
  };
1130
1361
  }
1131
- const { session: lastSession, isFallback } = reader2.getScopedLastSession();
1132
- const currentBranch = reader2.getCurrentBranch();
1362
+ const { session: lastSession, isFallback } = reader.getScopedLastSession();
1363
+ const currentBranch = reader.getCurrentBranch();
1133
1364
  if (!lastSession) {
1134
1365
  return {
1135
1366
  content: [
@@ -1140,13 +1371,13 @@ function registerGetMomentum(server2, reader2, workspacePath2) {
1140
1371
  ]
1141
1372
  };
1142
1373
  }
1143
- const state = reader2.getState();
1374
+ const state = reader.getState();
1144
1375
  const branchChanged = lastSession.gitBranch && currentBranch && lastSession.gitBranch !== currentBranch;
1145
1376
  const lines = [
1146
1377
  `## Developer Momentum`,
1147
1378
  ""
1148
1379
  ];
1149
- if (reader2.isWorktree && currentBranch) {
1380
+ if (reader.isWorktree && currentBranch) {
1150
1381
  lines.push(`**Worktree context:** Scoped to branch \`${currentBranch}\``);
1151
1382
  if (isFallback) {
1152
1383
  lines.push(`**Note:** No checkpoints found for branch \`${currentBranch}\`. Showing last global checkpoint.`);
@@ -1168,7 +1399,7 @@ function registerGetMomentum(server2, reader2, workspacePath2) {
1168
1399
  if (currentBranch) {
1169
1400
  lines.push(`**Current branch:** ${currentBranch}`);
1170
1401
  }
1171
- if (branchChanged && !reader2.isWorktree) {
1402
+ if (branchChanged && !reader.isWorktree) {
1172
1403
  lines.push(
1173
1404
  `**Note:** Branch changed since last checkpoint (was \`${lastSession.gitBranch}\`, now \`${currentBranch}\`)`
1174
1405
  );
@@ -1197,8 +1428,8 @@ function registerGetMomentum(server2, reader2, workspacePath2) {
1197
1428
 
1198
1429
  // src/tools/getSessionHistory.ts
1199
1430
  import { z } from "zod";
1200
- function registerGetSessionHistory(server2, reader2) {
1201
- server2.tool(
1431
+ function registerGetSessionHistory(server, reader) {
1432
+ server.tool(
1202
1433
  "get_session_history",
1203
1434
  "Get recent session checkpoints. Returns a chronological list of what the developer worked on.",
1204
1435
  {
@@ -1206,7 +1437,7 @@ function registerGetSessionHistory(server2, reader2) {
1206
1437
  branch: z.string().optional().describe('Filter to a specific branch name, or "all" to show all branches. Auto-detected from worktree context by default.')
1207
1438
  },
1208
1439
  async ({ limit, branch }) => {
1209
- if (!reader2.exists()) {
1440
+ if (!reader.exists()) {
1210
1441
  return {
1211
1442
  content: [
1212
1443
  {
@@ -1216,8 +1447,8 @@ function registerGetSessionHistory(server2, reader2) {
1216
1447
  ]
1217
1448
  };
1218
1449
  }
1219
- const { effectiveBranch, scopeLabel } = reader2.resolveBranchScope(branch);
1220
- const sessions = effectiveBranch ? reader2.getRecentSessionsForBranch(effectiveBranch, limit) : reader2.getRecentSessions(limit);
1450
+ const { effectiveBranch, scopeLabel } = reader.resolveBranchScope(branch);
1451
+ const sessions = effectiveBranch ? reader.getRecentSessionsForBranch(effectiveBranch, limit) : reader.getRecentSessions(limit);
1221
1452
  if (sessions.length === 0) {
1222
1453
  return {
1223
1454
  content: [
@@ -1257,13 +1488,13 @@ function registerGetSessionHistory(server2, reader2) {
1257
1488
  }
1258
1489
 
1259
1490
  // src/tools/getReentryBriefing.ts
1260
- function registerGetReentryBriefing(server2, reader2, workspacePath2) {
1261
- server2.tool(
1491
+ function registerGetReentryBriefing(server, reader, workspacePath) {
1492
+ server.tool(
1262
1493
  "get_reentry_briefing",
1263
1494
  "Get a synthesized re-entry briefing that helps a developer understand where they left off. Includes focus, recent activity, and suggested next steps.",
1264
1495
  {},
1265
1496
  async () => {
1266
- if (!reader2.exists()) {
1497
+ if (!reader.exists()) {
1267
1498
  return {
1268
1499
  content: [
1269
1500
  {
@@ -1273,12 +1504,12 @@ function registerGetReentryBriefing(server2, reader2, workspacePath2) {
1273
1504
  ]
1274
1505
  };
1275
1506
  }
1276
- const gitBranch = reader2.getCurrentBranch();
1277
- const { session: lastSession } = reader2.getScopedLastSession();
1278
- const recentSessions = reader2.getScopedRecentSessions(5);
1279
- const state = reader2.getState() ?? {};
1507
+ const gitBranch = reader.getCurrentBranch();
1508
+ const { session: lastSession } = reader.getScopedLastSession();
1509
+ const recentSessions = reader.getScopedRecentSessions(5);
1510
+ const state = reader.getState() ?? {};
1280
1511
  const sinceTimestamp = lastSession?.timestamp;
1281
- const recentCommits = sinceTimestamp ? getCommitMessagesSince(workspacePath2, sinceTimestamp) : [];
1512
+ const recentCommits = sinceTimestamp ? getCommitMessagesSince(workspacePath, sinceTimestamp) : [];
1282
1513
  const briefing = generateBriefing(
1283
1514
  lastSession,
1284
1515
  recentSessions,
@@ -1300,7 +1531,7 @@ function registerGetReentryBriefing(server2, reader2, workspacePath2) {
1300
1531
  `## Re-entry Briefing`,
1301
1532
  ""
1302
1533
  ];
1303
- if (reader2.isWorktree && gitBranch) {
1534
+ if (reader.isWorktree && gitBranch) {
1304
1535
  lines.push(`**Worktree context:** Scoped to branch \`${gitBranch}\``);
1305
1536
  lines.push("");
1306
1537
  }
@@ -1311,7 +1542,7 @@ function registerGetReentryBriefing(server2, reader2, workspacePath2) {
1311
1542
  `**Suggested next:** ${briefing.suggestedNext}`,
1312
1543
  `**Quick start:** ${briefing.smallNextStep}`
1313
1544
  );
1314
- const recentDecisions = reader2.getScopedRecentDecisions(3);
1545
+ const recentDecisions = reader.getScopedRecentDecisions(3);
1315
1546
  if (recentDecisions.length > 0) {
1316
1547
  lines.push("");
1317
1548
  lines.push("### Recent decisions");
@@ -1330,8 +1561,8 @@ function registerGetReentryBriefing(server2, reader2, workspacePath2) {
1330
1561
  // src/tools/saveCheckpoint.ts
1331
1562
  import path6 from "path";
1332
1563
  import { z as z2 } from "zod";
1333
- function registerSaveCheckpoint(server2, reader2, workspacePath2) {
1334
- server2.tool(
1564
+ function registerSaveCheckpoint(server, reader, workspacePath) {
1565
+ server.tool(
1335
1566
  "save_checkpoint",
1336
1567
  "Save a development checkpoint. Call this after completing a task or meaningful piece of work, not just at end of session. Each checkpoint helps the next session (or developer) pick up exactly where you left off.",
1337
1568
  {
@@ -1340,11 +1571,12 @@ function registerSaveCheckpoint(server2, reader2, workspacePath2) {
1340
1571
  blocker: z2.string().optional().describe("Any blocker preventing progress")
1341
1572
  },
1342
1573
  async ({ summary, nextStep, blocker }) => {
1343
- const lastSession = reader2.getLastSession();
1344
- const gitBranch = getCurrentBranch(workspacePath2);
1345
- const touchedFiles = getTouchedFiles(workspacePath2);
1346
- const commitHashes = getCommitsSince(workspacePath2, lastSession?.timestamp);
1347
- const projectName = path6.basename(resolveStorageRoot(workspacePath2));
1574
+ const lastSession = reader.getLastSession();
1575
+ const gitBranch = getCurrentBranch(workspacePath);
1576
+ const touchedFiles = getTouchedFiles(workspacePath);
1577
+ const commitHashes = getCommitsSince(workspacePath, lastSession?.timestamp);
1578
+ const projectName = path6.basename(resolveStorageRoot(workspacePath));
1579
+ const sessionId = generateSessionId({ workspaceRoot: workspacePath, branch: gitBranch ?? void 0, worktreePath: workspacePath });
1348
1580
  const checkpoint = createCheckpoint({
1349
1581
  summary,
1350
1582
  nextStep: nextStep || "",
@@ -1352,10 +1584,11 @@ function registerSaveCheckpoint(server2, reader2, workspacePath2) {
1352
1584
  gitBranch,
1353
1585
  touchedFiles,
1354
1586
  commitHashes,
1355
- workspaceRoot: workspacePath2,
1356
- source: "manual"
1587
+ workspaceRoot: workspacePath,
1588
+ source: "manual",
1589
+ sessionId
1357
1590
  });
1358
- const writer = new KeepGoingWriter(workspacePath2);
1591
+ const writer = new KeepGoingWriter(workspacePath);
1359
1592
  writer.saveCheckpoint(checkpoint, projectName);
1360
1593
  const lines = [
1361
1594
  `Checkpoint saved.`,
@@ -1365,11 +1598,11 @@ function registerSaveCheckpoint(server2, reader2, workspacePath2) {
1365
1598
  `- **Commits captured:** ${commitHashes.length}`
1366
1599
  ];
1367
1600
  if (commitHashes.length > 0) {
1368
- const commitMessages = getCommitMessagesSince(workspacePath2, lastSession?.timestamp);
1369
- const headHash = getHeadCommitHash(workspacePath2);
1601
+ const commitMessages = getCommitMessagesSince(workspacePath, lastSession?.timestamp);
1602
+ const headHash = getHeadCommitHash(workspacePath);
1370
1603
  if (commitMessages.length > 0 && headHash) {
1371
1604
  const detected = tryDetectDecision({
1372
- workspacePath: workspacePath2,
1605
+ workspacePath,
1373
1606
  checkpointId: checkpoint.id,
1374
1607
  gitBranch,
1375
1608
  commitHash: headHash,
@@ -1390,8 +1623,8 @@ function registerSaveCheckpoint(server2, reader2, workspacePath2) {
1390
1623
 
1391
1624
  // src/tools/getDecisions.ts
1392
1625
  import { z as z3 } from "zod";
1393
- function registerGetDecisions(server2, reader2) {
1394
- server2.tool(
1626
+ function registerGetDecisions(server, reader) {
1627
+ server.tool(
1395
1628
  "get_decisions",
1396
1629
  "Get recent decision records. Returns detected high-signal commits with their category, confidence, and rationale.",
1397
1630
  {
@@ -1399,7 +1632,7 @@ function registerGetDecisions(server2, reader2) {
1399
1632
  branch: z3.string().optional().describe('Filter to a specific branch name, or "all" to show all branches. Auto-detected from worktree context by default.')
1400
1633
  },
1401
1634
  async ({ limit, branch }) => {
1402
- if (!reader2.exists()) {
1635
+ if (!reader.exists()) {
1403
1636
  return {
1404
1637
  content: [
1405
1638
  {
@@ -1409,8 +1642,7 @@ function registerGetDecisions(server2, reader2) {
1409
1642
  ]
1410
1643
  };
1411
1644
  }
1412
- const licenseCache = reader2.getLicenseCache();
1413
- if (!isCachedLicenseValid(licenseCache)) {
1645
+ if (process.env.KEEPGOING_PRO_BYPASS !== "1" && !getLicenseForFeature("decisions")) {
1414
1646
  return {
1415
1647
  content: [
1416
1648
  {
@@ -1420,8 +1652,8 @@ function registerGetDecisions(server2, reader2) {
1420
1652
  ]
1421
1653
  };
1422
1654
  }
1423
- const { effectiveBranch, scopeLabel } = reader2.resolveBranchScope(branch);
1424
- const decisions = effectiveBranch ? reader2.getRecentDecisionsForBranch(effectiveBranch, limit) : reader2.getRecentDecisions(limit);
1655
+ const { effectiveBranch, scopeLabel } = reader.resolveBranchScope(branch);
1656
+ const decisions = effectiveBranch ? reader.getRecentDecisionsForBranch(effectiveBranch, limit) : reader.getRecentDecisions(limit);
1425
1657
  if (decisions.length === 0) {
1426
1658
  return {
1427
1659
  content: [
@@ -1459,8 +1691,104 @@ function registerGetDecisions(server2, reader2) {
1459
1691
  );
1460
1692
  }
1461
1693
 
1694
+ // src/tools/getCurrentTask.ts
1695
+ function registerGetCurrentTask(server, reader) {
1696
+ server.tool(
1697
+ "get_current_task",
1698
+ "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.",
1699
+ {},
1700
+ async () => {
1701
+ if (!reader.exists()) {
1702
+ return {
1703
+ content: [
1704
+ {
1705
+ type: "text",
1706
+ text: "No KeepGoing data found."
1707
+ }
1708
+ ]
1709
+ };
1710
+ }
1711
+ if (process.env.KEEPGOING_PRO_BYPASS !== "1" && !getLicenseForFeature("session-awareness")) {
1712
+ return {
1713
+ content: [
1714
+ {
1715
+ type: "text",
1716
+ text: "Session Awareness requires a license. Use the activate_license tool, run `keepgoing activate <key>` in your terminal, or visit https://keepgoing.dev/add-ons to purchase."
1717
+ }
1718
+ ]
1719
+ };
1720
+ }
1721
+ const tasks = reader.getCurrentTasks();
1722
+ if (tasks.length === 0) {
1723
+ return {
1724
+ content: [
1725
+ {
1726
+ type: "text",
1727
+ text: "No current task data found. The agent has not started writing session data yet."
1728
+ }
1729
+ ]
1730
+ };
1731
+ }
1732
+ const activeTasks = tasks.filter((t) => t.sessionActive);
1733
+ const finishedTasks = tasks.filter((t) => !t.sessionActive);
1734
+ const lines = [];
1735
+ const totalActive = activeTasks.length;
1736
+ const totalFinished = finishedTasks.length;
1737
+ if (totalActive > 0 || totalFinished > 0) {
1738
+ const parts = [];
1739
+ if (totalActive > 0) parts.push(`${totalActive} active`);
1740
+ if (totalFinished > 0) parts.push(`${totalFinished} finished`);
1741
+ lines.push(`## Live Sessions (${parts.join(", ")})`);
1742
+ lines.push("");
1743
+ }
1744
+ for (const task of [...activeTasks, ...finishedTasks]) {
1745
+ const statusIcon = task.sessionActive ? "\u{1F7E2}" : "\u2705";
1746
+ const statusLabel = task.sessionActive ? "Active" : "Finished";
1747
+ const sessionLabel = task.agentLabel || task.sessionId || "Session";
1748
+ lines.push(`### ${statusIcon} ${sessionLabel} (${statusLabel})`);
1749
+ lines.push(`- **Updated:** ${formatRelativeTime(task.updatedAt)}`);
1750
+ if (task.branch) {
1751
+ lines.push(`- **Branch:** ${task.branch}`);
1752
+ }
1753
+ if (task.taskSummary) {
1754
+ lines.push(`- **Doing:** ${task.taskSummary}`);
1755
+ }
1756
+ if (task.lastFileEdited) {
1757
+ lines.push(`- **Last file:** ${task.lastFileEdited}`);
1758
+ }
1759
+ if (task.nextStep) {
1760
+ lines.push(`- **Next step:** ${task.nextStep}`);
1761
+ }
1762
+ lines.push("");
1763
+ }
1764
+ const conflicts = reader.detectFileConflicts();
1765
+ if (conflicts.length > 0) {
1766
+ lines.push("### \u26A0\uFE0F Potential Conflicts");
1767
+ for (const conflict of conflicts) {
1768
+ const sessionLabels = conflict.sessions.map((s) => s.agentLabel || s.sessionId || "unknown").join(", ");
1769
+ lines.push(`- **${conflict.file}** is being edited by: ${sessionLabels}`);
1770
+ }
1771
+ lines.push("");
1772
+ }
1773
+ const overlaps = reader.detectBranchOverlap();
1774
+ if (overlaps.length > 0) {
1775
+ lines.push("### \u2139\uFE0F Branch Overlap");
1776
+ for (const overlap of overlaps) {
1777
+ const sessionLabels = overlap.sessions.map((s) => s.agentLabel || s.sessionId || "unknown").join(", ");
1778
+ lines.push(`- **${overlap.branch}**: ${sessionLabels} (possible duplicate work)`);
1779
+ }
1780
+ lines.push("");
1781
+ }
1782
+ return {
1783
+ content: [{ type: "text", text: lines.join("\n") }]
1784
+ };
1785
+ }
1786
+ );
1787
+ }
1788
+
1462
1789
  // src/tools/setupProject.ts
1463
1790
  import fs5 from "fs";
1791
+ import os2 from "os";
1464
1792
  import path7 from "path";
1465
1793
  import { z as z4 } from "zod";
1466
1794
  var KEEPGOING_MARKER = "@keepgoingdev/mcp-server";
@@ -1482,6 +1810,15 @@ var STOP_HOOK = {
1482
1810
  }
1483
1811
  ]
1484
1812
  };
1813
+ var POST_TOOL_USE_HOOK = {
1814
+ matcher: "Edit|Write|MultiEdit",
1815
+ hooks: [
1816
+ {
1817
+ type: "command",
1818
+ command: "npx -y @keepgoingdev/mcp-server --update-task-from-hook"
1819
+ }
1820
+ ]
1821
+ };
1485
1822
  var CLAUDE_MD_SECTION = `
1486
1823
  ## KeepGoing
1487
1824
 
@@ -1495,8 +1832,8 @@ function hasKeepGoingHook(hookEntries) {
1495
1832
  (entry) => entry?.hooks?.some((h) => typeof h?.command === "string" && h.command.includes(KEEPGOING_MARKER))
1496
1833
  );
1497
1834
  }
1498
- function registerSetupProject(server2, workspacePath2) {
1499
- server2.tool(
1835
+ function registerSetupProject(server, workspacePath) {
1836
+ server.tool(
1500
1837
  "setup_project",
1501
1838
  "Set up KeepGoing in the current project. Adds session hooks to .claude/settings.json and CLAUDE.md instructions so checkpoints are saved automatically.",
1502
1839
  {
@@ -1505,44 +1842,80 @@ function registerSetupProject(server2, workspacePath2) {
1505
1842
  },
1506
1843
  async ({ sessionHooks, claudeMd }) => {
1507
1844
  const results = [];
1845
+ const claudeDir = path7.join(workspacePath, ".claude");
1846
+ const settingsPath = path7.join(claudeDir, "settings.json");
1847
+ let settings = {};
1848
+ if (fs5.existsSync(settingsPath)) {
1849
+ settings = JSON.parse(fs5.readFileSync(settingsPath, "utf-8"));
1850
+ }
1851
+ let settingsChanged = false;
1508
1852
  if (sessionHooks) {
1509
- const claudeDir = path7.join(workspacePath2, ".claude");
1510
- const settingsPath = path7.join(claudeDir, "settings.json");
1511
- let settings = {};
1512
- if (fs5.existsSync(settingsPath)) {
1513
- settings = JSON.parse(fs5.readFileSync(settingsPath, "utf-8"));
1514
- }
1515
1853
  if (!settings.hooks) {
1516
1854
  settings.hooks = {};
1517
1855
  }
1518
- let hooksChanged = false;
1519
1856
  if (!Array.isArray(settings.hooks.SessionStart)) {
1520
1857
  settings.hooks.SessionStart = [];
1521
1858
  }
1522
1859
  if (!hasKeepGoingHook(settings.hooks.SessionStart)) {
1523
1860
  settings.hooks.SessionStart.push(SESSION_START_HOOK);
1524
- hooksChanged = true;
1861
+ settingsChanged = true;
1525
1862
  }
1526
1863
  if (!Array.isArray(settings.hooks.Stop)) {
1527
1864
  settings.hooks.Stop = [];
1528
1865
  }
1529
1866
  if (!hasKeepGoingHook(settings.hooks.Stop)) {
1530
1867
  settings.hooks.Stop.push(STOP_HOOK);
1531
- hooksChanged = true;
1868
+ settingsChanged = true;
1532
1869
  }
1533
- if (hooksChanged) {
1534
- if (!fs5.existsSync(claudeDir)) {
1535
- fs5.mkdirSync(claudeDir, { recursive: true });
1536
- }
1537
- fs5.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
1870
+ if (!Array.isArray(settings.hooks.PostToolUse)) {
1871
+ settings.hooks.PostToolUse = [];
1872
+ }
1873
+ if (!hasKeepGoingHook(settings.hooks.PostToolUse)) {
1874
+ settings.hooks.PostToolUse.push(POST_TOOL_USE_HOOK);
1875
+ settingsChanged = true;
1876
+ }
1877
+ if (settingsChanged) {
1538
1878
  results.push("**Session hooks:** Added to `.claude/settings.json`");
1539
1879
  } else {
1540
1880
  results.push("**Session hooks:** Already present, skipped");
1541
1881
  }
1542
1882
  }
1883
+ if (process.env.KEEPGOING_PRO_BYPASS === "1" || getLicenseForFeature("session-awareness")) {
1884
+ const statuslineSrc = path7.resolve(
1885
+ new URL(".", import.meta.url).pathname,
1886
+ "statusline.sh"
1887
+ );
1888
+ const claudeHome = path7.join(os2.homedir(), ".claude");
1889
+ const statuslineDest = path7.join(claudeHome, "keepgoing-statusline.sh");
1890
+ if (fs5.existsSync(statuslineSrc)) {
1891
+ if (!fs5.existsSync(claudeHome)) {
1892
+ fs5.mkdirSync(claudeHome, { recursive: true });
1893
+ }
1894
+ fs5.copyFileSync(statuslineSrc, statuslineDest);
1895
+ fs5.chmodSync(statuslineDest, 493);
1896
+ if (!settings.statusLine) {
1897
+ settings.statusLine = {
1898
+ type: "command",
1899
+ command: statuslineDest
1900
+ };
1901
+ settingsChanged = true;
1902
+ results.push("**Statusline:** Installed `keepgoing-statusline.sh` and added to `.claude/settings.json`");
1903
+ } else {
1904
+ results.push("**Statusline:** `statusLine` already configured in settings, skipped");
1905
+ }
1906
+ } else {
1907
+ results.push("**Statusline:** Script not found in package, skipped");
1908
+ }
1909
+ }
1910
+ if (settingsChanged) {
1911
+ if (!fs5.existsSync(claudeDir)) {
1912
+ fs5.mkdirSync(claudeDir, { recursive: true });
1913
+ }
1914
+ fs5.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
1915
+ }
1543
1916
  if (claudeMd) {
1544
- const dotClaudeMdPath = path7.join(workspacePath2, ".claude", "CLAUDE.md");
1545
- const rootClaudeMdPath = path7.join(workspacePath2, "CLAUDE.md");
1917
+ const dotClaudeMdPath = path7.join(workspacePath, ".claude", "CLAUDE.md");
1918
+ const rootClaudeMdPath = path7.join(workspacePath, "CLAUDE.md");
1546
1919
  const claudeMdPath = fs5.existsSync(dotClaudeMdPath) ? dotClaudeMdPath : rootClaudeMdPath;
1547
1920
  let existing = "";
1548
1921
  if (fs5.existsSync(claudeMdPath)) {
@@ -1565,20 +1938,24 @@ function registerSetupProject(server2, workspacePath2) {
1565
1938
 
1566
1939
  // src/tools/activateLicense.ts
1567
1940
  import { z as z5 } from "zod";
1568
- function registerActivateLicense(server2) {
1569
- server2.tool(
1941
+ function registerActivateLicense(server) {
1942
+ server.tool(
1570
1943
  "activate_license",
1571
- "Activate a KeepGoing Pro license on this device. Unlocks Decision Detection and future Pro features.",
1944
+ "Activate a KeepGoing Pro license on this device. Unlocks add-ons like Decision Detection and Session Awareness.",
1572
1945
  { license_key: z5.string().describe("Your KeepGoing Pro license key") },
1573
1946
  async ({ license_key }) => {
1574
- const existing = readLicenseCache();
1575
- if (isCachedLicenseValid(existing)) {
1576
- const who2 = existing.customerName ? ` (${existing.customerName})` : "";
1947
+ const store = readLicenseStore();
1948
+ const existingForKey = store.licenses.find(
1949
+ (l) => l.status === "active" && l.licenseKey === license_key
1950
+ );
1951
+ if (existingForKey) {
1952
+ const label2 = getVariantLabel(existingForKey.variantId);
1953
+ const who2 = existingForKey.customerName ? ` (${existingForKey.customerName})` : "";
1577
1954
  return {
1578
1955
  content: [
1579
1956
  {
1580
1957
  type: "text",
1581
- text: `Pro license is already active${who2}. No action needed.`
1958
+ text: `${label2} is already active${who2}. No action needed.`
1582
1959
  }
1583
1960
  ]
1584
1961
  };
@@ -1594,22 +1971,43 @@ function registerActivateLicense(server2) {
1594
1971
  ]
1595
1972
  };
1596
1973
  }
1974
+ const variantId = result.variantId;
1975
+ const existingForVariant = store.licenses.find(
1976
+ (l) => l.status === "active" && l.variantId === variantId
1977
+ );
1978
+ if (existingForVariant) {
1979
+ const label2 = getVariantLabel(variantId);
1980
+ const who2 = existingForVariant.customerName ? ` (${existingForVariant.customerName})` : "";
1981
+ return {
1982
+ content: [
1983
+ {
1984
+ type: "text",
1985
+ text: `${label2} is already active${who2}. No action needed.`
1986
+ }
1987
+ ]
1988
+ };
1989
+ }
1597
1990
  const now = (/* @__PURE__ */ new Date()).toISOString();
1598
- writeLicenseCache({
1991
+ addLicenseEntry({
1599
1992
  licenseKey: result.licenseKey || license_key,
1600
1993
  instanceId: result.instanceId || getDeviceId(),
1601
1994
  status: "active",
1602
1995
  lastValidatedAt: now,
1603
1996
  activatedAt: now,
1997
+ variantId,
1604
1998
  customerName: result.customerName,
1605
- productName: result.productName
1999
+ productName: result.productName,
2000
+ variantName: result.variantName
1606
2001
  });
2002
+ const label = getVariantLabel(variantId);
2003
+ const features = VARIANT_FEATURE_MAP[variantId];
2004
+ const featureList = features ? features.join(", ") : "Pro features";
1607
2005
  const who = result.customerName ? ` Welcome, ${result.customerName}!` : "";
1608
2006
  return {
1609
2007
  content: [
1610
2008
  {
1611
2009
  type: "text",
1612
- text: `Pro license activated successfully.${who} Decision Detection is now enabled.`
2010
+ text: `${label} activated successfully.${who} Enabled: ${featureList}.`
1613
2011
  }
1614
2012
  ]
1615
2013
  };
@@ -1618,14 +2016,18 @@ function registerActivateLicense(server2) {
1618
2016
  }
1619
2017
 
1620
2018
  // src/tools/deactivateLicense.ts
1621
- function registerDeactivateLicense(server2) {
1622
- server2.tool(
2019
+ import { z as z6 } from "zod";
2020
+ function registerDeactivateLicense(server) {
2021
+ server.tool(
1623
2022
  "deactivate_license",
1624
2023
  "Deactivate the KeepGoing Pro license on this device.",
1625
- {},
1626
- async () => {
1627
- const cache = readLicenseCache();
1628
- if (!cache) {
2024
+ {
2025
+ license_key: z6.string().optional().describe("Specific license key to deactivate. If omitted and only one license is active, deactivates it. If multiple are active, lists them.")
2026
+ },
2027
+ async ({ license_key }) => {
2028
+ const store = readLicenseStore();
2029
+ const activeLicenses = store.licenses.filter((l) => l.status === "active");
2030
+ if (activeLicenses.length === 0) {
1629
2031
  return {
1630
2032
  content: [
1631
2033
  {
@@ -1635,14 +2037,41 @@ function registerDeactivateLicense(server2) {
1635
2037
  ]
1636
2038
  };
1637
2039
  }
1638
- const result = await deactivateLicense(cache.licenseKey, cache.instanceId);
1639
- deleteLicenseCache();
2040
+ let target;
2041
+ if (license_key) {
2042
+ target = activeLicenses.find((l) => l.licenseKey === license_key);
2043
+ if (!target) {
2044
+ return {
2045
+ content: [
2046
+ {
2047
+ type: "text",
2048
+ text: `No active license found with key "${license_key}".`
2049
+ }
2050
+ ]
2051
+ };
2052
+ }
2053
+ } else if (activeLicenses.length === 1) {
2054
+ target = activeLicenses[0];
2055
+ } else {
2056
+ const lines = ["Multiple active licenses found. Please specify which to deactivate using the license_key parameter:", ""];
2057
+ for (const l of activeLicenses) {
2058
+ const label2 = getVariantLabel(l.variantId);
2059
+ const who = l.customerName ? ` (${l.customerName})` : "";
2060
+ lines.push(`- ${label2}${who}: ${l.licenseKey}`);
2061
+ }
2062
+ return {
2063
+ content: [{ type: "text", text: lines.join("\n") }]
2064
+ };
2065
+ }
2066
+ const result = await deactivateLicense(target.licenseKey, target.instanceId);
2067
+ removeLicenseEntry(target.licenseKey);
2068
+ const label = getVariantLabel(target.variantId);
1640
2069
  if (!result.deactivated) {
1641
2070
  return {
1642
2071
  content: [
1643
2072
  {
1644
2073
  type: "text",
1645
- text: `License cleared locally, but remote deactivation failed: ${result.error ?? "unknown error"}`
2074
+ text: `${label} license cleared locally, but remote deactivation failed: ${result.error ?? "unknown error"}`
1646
2075
  }
1647
2076
  ]
1648
2077
  };
@@ -1651,7 +2080,7 @@ function registerDeactivateLicense(server2) {
1651
2080
  content: [
1652
2081
  {
1653
2082
  type: "text",
1654
- text: "Pro license deactivated successfully. The activation slot has been freed."
2083
+ text: `${label} license deactivated successfully. The activation slot has been freed.`
1655
2084
  }
1656
2085
  ]
1657
2086
  };
@@ -1660,8 +2089,8 @@ function registerDeactivateLicense(server2) {
1660
2089
  }
1661
2090
 
1662
2091
  // src/prompts/resume.ts
1663
- function registerResumePrompt(server2) {
1664
- server2.prompt(
2092
+ function registerResumePrompt(server) {
2093
+ server.prompt(
1665
2094
  "resume",
1666
2095
  "Check developer momentum and suggest what to work on next",
1667
2096
  async () => ({
@@ -1688,8 +2117,8 @@ function registerResumePrompt(server2) {
1688
2117
  }
1689
2118
 
1690
2119
  // src/prompts/decisions.ts
1691
- function registerDecisionsPrompt(server2) {
1692
- server2.prompt(
2120
+ function registerDecisionsPrompt(server) {
2121
+ server.prompt(
1693
2122
  "decisions",
1694
2123
  "Review recent architectural decisions and their rationale",
1695
2124
  async () => ({
@@ -1716,8 +2145,8 @@ function registerDecisionsPrompt(server2) {
1716
2145
  }
1717
2146
 
1718
2147
  // src/prompts/progress.ts
1719
- function registerProgressPrompt(server2) {
1720
- server2.prompt(
2148
+ function registerProgressPrompt(server) {
2149
+ server.prompt(
1721
2150
  "progress",
1722
2151
  "Summarize recent development progress across sessions",
1723
2152
  async () => ({
@@ -1743,14 +2172,20 @@ function registerProgressPrompt(server2) {
1743
2172
  );
1744
2173
  }
1745
2174
 
1746
- // src/index.ts
1747
- if (process.argv.includes("--print-momentum")) {
1748
- const wsPath = findGitRoot(process.argv.slice(2).find((a) => a !== "--print-momentum") || process.cwd());
1749
- const reader2 = new KeepGoingReader(wsPath);
1750
- if (!reader2.exists()) {
2175
+ // src/cli/util.ts
2176
+ function resolveWsPath(args = process.argv.slice(2)) {
2177
+ const explicit = args.find((a) => !a.startsWith("--"));
2178
+ return findGitRoot(explicit || process.cwd());
2179
+ }
2180
+
2181
+ // src/cli/print.ts
2182
+ async function handlePrintMomentum() {
2183
+ const wsPath = resolveWsPath();
2184
+ const reader = new KeepGoingReader(wsPath);
2185
+ if (!reader.exists()) {
1751
2186
  process.exit(0);
1752
2187
  }
1753
- const { session: lastSession } = reader2.getScopedLastSession();
2188
+ const { session: lastSession } = reader.getScopedLastSession();
1754
2189
  if (!lastSession) {
1755
2190
  process.exit(0);
1756
2191
  }
@@ -1776,10 +2211,49 @@ if (process.argv.includes("--print-momentum")) {
1776
2211
  console.log(lines.join("\n"));
1777
2212
  process.exit(0);
1778
2213
  }
1779
- if (process.argv.includes("--save-checkpoint")) {
1780
- const wsPath = findGitRoot(process.argv.slice(2).find((a) => !a.startsWith("--")) || process.cwd());
1781
- const reader2 = new KeepGoingReader(wsPath);
1782
- const { session: lastSession } = reader2.getScopedLastSession();
2214
+ async function handlePrintCurrent() {
2215
+ if (process.env.KEEPGOING_PRO_BYPASS !== "1" && !getLicenseForFeature("session-awareness")) {
2216
+ process.exit(0);
2217
+ }
2218
+ const wsPath = resolveWsPath();
2219
+ const reader = new KeepGoingReader(wsPath);
2220
+ const tasks = reader.getCurrentTasks();
2221
+ if (tasks.length === 0) {
2222
+ process.exit(0);
2223
+ }
2224
+ const activeTasks = tasks.filter((t) => t.sessionActive);
2225
+ const finishedTasks = tasks.filter((t) => !t.sessionActive);
2226
+ if (tasks.length > 1) {
2227
+ const parts = [];
2228
+ if (activeTasks.length > 0) parts.push(`${activeTasks.length} active`);
2229
+ if (finishedTasks.length > 0) parts.push(`${finishedTasks.length} finished`);
2230
+ console.log(`[KeepGoing] Sessions: ${parts.join(", ")}`);
2231
+ }
2232
+ for (const task of [...activeTasks, ...finishedTasks]) {
2233
+ const prefix = task.sessionActive ? "[KeepGoing] Current task:" : "[KeepGoing] \u2705 Last task:";
2234
+ const sessionLabel = task.agentLabel || task.sessionId || "";
2235
+ const labelSuffix = sessionLabel ? ` (${sessionLabel})` : "";
2236
+ const lines = [`${prefix} ${formatRelativeTime(task.updatedAt)}${labelSuffix}`];
2237
+ if (task.branch) {
2238
+ lines.push(` Branch: ${task.branch}`);
2239
+ }
2240
+ if (task.taskSummary) {
2241
+ lines.push(` Doing: ${task.taskSummary}`);
2242
+ }
2243
+ if (task.nextStep) {
2244
+ lines.push(` Next: ${task.nextStep}`);
2245
+ }
2246
+ console.log(lines.join("\n"));
2247
+ }
2248
+ process.exit(0);
2249
+ }
2250
+
2251
+ // src/cli/saveCheckpoint.ts
2252
+ import path8 from "path";
2253
+ async function handleSaveCheckpoint() {
2254
+ const wsPath = resolveWsPath();
2255
+ const reader = new KeepGoingReader(wsPath);
2256
+ const { session: lastSession } = reader.getScopedLastSession();
1783
2257
  if (lastSession?.timestamp) {
1784
2258
  const ageMs = Date.now() - new Date(lastSession.timestamp).getTime();
1785
2259
  if (ageMs < 2 * 60 * 1e3) {
@@ -1804,6 +2278,7 @@ if (process.argv.includes("--save-checkpoint")) {
1804
2278
  }
1805
2279
  }
1806
2280
  const projectName = path8.basename(resolveStorageRoot(wsPath));
2281
+ const sessionId = generateSessionId({ workspaceRoot: wsPath, branch: gitBranch ?? void 0, worktreePath: wsPath });
1807
2282
  const checkpoint = createCheckpoint({
1808
2283
  summary,
1809
2284
  nextStep: "",
@@ -1811,12 +2286,19 @@ if (process.argv.includes("--save-checkpoint")) {
1811
2286
  touchedFiles,
1812
2287
  commitHashes,
1813
2288
  workspaceRoot: wsPath,
1814
- source: "auto"
2289
+ source: "auto",
2290
+ sessionId
1815
2291
  });
1816
2292
  const writer = new KeepGoingWriter(wsPath);
1817
2293
  writer.saveCheckpoint(checkpoint, projectName);
1818
- const licenseCache = readLicenseCache();
1819
- if (isCachedLicenseValid(licenseCache) && commitMessages.length > 0) {
2294
+ writer.upsertSession({
2295
+ sessionId,
2296
+ sessionActive: false,
2297
+ nextStep: checkpoint.nextStep || void 0,
2298
+ branch: gitBranch ?? void 0,
2299
+ updatedAt: checkpoint.timestamp
2300
+ });
2301
+ if ((process.env.KEEPGOING_PRO_BYPASS === "1" || getLicenseForFeature("decisions")) && commitMessages.length > 0) {
1820
2302
  const headHash = getHeadCommitHash(wsPath) || commitHashes[0];
1821
2303
  if (headHash) {
1822
2304
  const detected = tryDetectDecision({
@@ -1835,24 +2317,109 @@ if (process.argv.includes("--save-checkpoint")) {
1835
2317
  console.log(`[KeepGoing] Auto-checkpoint saved: ${summary}`);
1836
2318
  process.exit(0);
1837
2319
  }
1838
- var workspacePath = findGitRoot(process.argv[2] || process.cwd());
1839
- var reader = new KeepGoingReader(workspacePath);
1840
- var server = new McpServer({
1841
- name: "keepgoing",
1842
- version: "0.1.0"
1843
- });
1844
- registerGetMomentum(server, reader, workspacePath);
1845
- registerGetSessionHistory(server, reader);
1846
- registerGetReentryBriefing(server, reader, workspacePath);
1847
- registerGetDecisions(server, reader);
1848
- registerSaveCheckpoint(server, reader, workspacePath);
1849
- registerSetupProject(server, workspacePath);
1850
- registerActivateLicense(server);
1851
- registerDeactivateLicense(server);
1852
- registerResumePrompt(server);
1853
- registerDecisionsPrompt(server);
1854
- registerProgressPrompt(server);
1855
- var transport = new StdioServerTransport();
1856
- await server.connect(transport);
1857
- console.error("KeepGoing MCP server started");
2320
+
2321
+ // src/cli/updateTask.ts
2322
+ async function handleUpdateTask() {
2323
+ const args = process.argv.slice(2);
2324
+ const flagIndex = args.indexOf("--update-task");
2325
+ const payloadStr = args[flagIndex + 1];
2326
+ const wsArgs = args.filter((a, i) => !a.startsWith("--") && i !== flagIndex + 1);
2327
+ const wsPath = resolveWsPath(wsArgs.length > 0 ? wsArgs : void 0);
2328
+ if (payloadStr) {
2329
+ try {
2330
+ const payload = JSON.parse(payloadStr);
2331
+ const writer = new KeepGoingWriter(wsPath);
2332
+ const branch = payload.branch ?? getCurrentBranch(wsPath) ?? void 0;
2333
+ const task = {
2334
+ ...payload,
2335
+ branch,
2336
+ worktreePath: wsPath,
2337
+ sessionActive: payload.sessionActive !== false,
2338
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2339
+ };
2340
+ const sessionId = payload.sessionId || generateSessionId({ ...task, workspaceRoot: wsPath });
2341
+ task.sessionId = sessionId;
2342
+ writer.upsertSession(task);
2343
+ } catch {
2344
+ }
2345
+ }
2346
+ process.exit(0);
2347
+ }
2348
+ var STDIN_TIMEOUT_MS = 5e3;
2349
+ async function handleUpdateTaskFromHook() {
2350
+ const wsPath = resolveWsPath();
2351
+ const chunks = [];
2352
+ const timeout = setTimeout(() => process.exit(0), STDIN_TIMEOUT_MS);
2353
+ process.stdin.on("error", () => {
2354
+ clearTimeout(timeout);
2355
+ process.exit(0);
2356
+ });
2357
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
2358
+ process.stdin.on("end", () => {
2359
+ clearTimeout(timeout);
2360
+ try {
2361
+ const raw = Buffer.concat(chunks).toString("utf-8").trim();
2362
+ if (!raw) {
2363
+ process.exit(0);
2364
+ }
2365
+ const hookData = JSON.parse(raw);
2366
+ const toolName = hookData.tool_name ?? "Edit";
2367
+ const filePath = hookData.tool_input?.file_path ?? hookData.tool_input?.path ?? "";
2368
+ const fileName = filePath ? filePath.split("/").pop() ?? filePath : "";
2369
+ const writer = new KeepGoingWriter(wsPath);
2370
+ const existing = writer.readCurrentTasks();
2371
+ const cachedBranch = existing.find((t) => t.sessionActive && t.worktreePath === wsPath)?.branch;
2372
+ const branch = cachedBranch ?? getCurrentBranch(wsPath) ?? void 0;
2373
+ const task = {
2374
+ taskSummary: fileName ? `${toolName} ${fileName}` : `Used ${toolName}`,
2375
+ lastFileEdited: filePath || void 0,
2376
+ branch,
2377
+ worktreePath: wsPath,
2378
+ sessionActive: true,
2379
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2380
+ };
2381
+ const sessionId = hookData.session_id || generateSessionId({ ...task, workspaceRoot: wsPath });
2382
+ task.sessionId = sessionId;
2383
+ writer.upsertSession(task);
2384
+ } catch {
2385
+ }
2386
+ process.exit(0);
2387
+ });
2388
+ process.stdin.resume();
2389
+ }
2390
+
2391
+ // src/index.ts
2392
+ var CLI_HANDLERS = {
2393
+ "--print-momentum": handlePrintMomentum,
2394
+ "--save-checkpoint": handleSaveCheckpoint,
2395
+ "--update-task": handleUpdateTask,
2396
+ "--update-task-from-hook": handleUpdateTaskFromHook,
2397
+ "--print-current": handlePrintCurrent
2398
+ };
2399
+ var flag = process.argv.slice(2).find((a) => a in CLI_HANDLERS);
2400
+ if (flag) {
2401
+ await CLI_HANDLERS[flag]();
2402
+ } else {
2403
+ const workspacePath = findGitRoot(process.argv[2] || process.cwd());
2404
+ const reader = new KeepGoingReader(workspacePath);
2405
+ const server = new McpServer({
2406
+ name: "keepgoing",
2407
+ version: "0.1.0"
2408
+ });
2409
+ registerGetMomentum(server, reader, workspacePath);
2410
+ registerGetSessionHistory(server, reader);
2411
+ registerGetReentryBriefing(server, reader, workspacePath);
2412
+ registerGetDecisions(server, reader);
2413
+ registerGetCurrentTask(server, reader);
2414
+ registerSaveCheckpoint(server, reader, workspacePath);
2415
+ registerSetupProject(server, workspacePath);
2416
+ registerActivateLicense(server);
2417
+ registerDeactivateLicense(server);
2418
+ registerResumePrompt(server);
2419
+ registerDecisionsPrompt(server);
2420
+ registerProgressPrompt(server);
2421
+ const transport = new StdioServerTransport();
2422
+ await server.connect(transport);
2423
+ console.error("KeepGoing MCP server started");
2424
+ }
1858
2425
  //# sourceMappingURL=index.js.map