@keepgoingdev/mcp-server 0.4.0 → 0.5.1

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