@keepgoingdev/mcp-server 0.3.1 → 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() {
@@ -80,14 +75,12 @@ import { promisify } from "util";
80
75
  var execFileAsync = promisify(execFile);
81
76
  function findGitRoot(startPath) {
82
77
  try {
83
- const gitCommonDir = execFileSync("git", ["rev-parse", "--git-common-dir"], {
78
+ const toplevel = execFileSync("git", ["rev-parse", "--show-toplevel"], {
84
79
  cwd: startPath,
85
80
  encoding: "utf-8",
86
81
  timeout: 5e3
87
82
  }).trim();
88
- if (!gitCommonDir) return startPath;
89
- const absoluteGitDir = path.isAbsolute(gitCommonDir) ? gitCommonDir : path.resolve(startPath, gitCommonDir);
90
- return path.dirname(absoluteGitDir);
83
+ return toplevel || startPath;
91
84
  } catch {
92
85
  return startPath;
93
86
  }
@@ -118,10 +111,10 @@ function resolveStorageRoot(startPath) {
118
111
  return startPath;
119
112
  }
120
113
  }
121
- function getCurrentBranch(workspacePath2) {
114
+ function getCurrentBranch(workspacePath) {
122
115
  try {
123
116
  const result = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
124
- cwd: workspacePath2,
117
+ cwd: workspacePath,
125
118
  encoding: "utf-8",
126
119
  timeout: 5e3
127
120
  });
@@ -130,14 +123,14 @@ function getCurrentBranch(workspacePath2) {
130
123
  return void 0;
131
124
  }
132
125
  }
133
- function getGitLogSince(workspacePath2, format, sinceTimestamp) {
126
+ function getGitLogSince(workspacePath, format, sinceTimestamp) {
134
127
  try {
135
128
  const since = sinceTimestamp || new Date(Date.now() - 24 * 60 * 60 * 1e3).toISOString();
136
129
  const result = execFileSync(
137
130
  "git",
138
131
  ["log", `--since=${since}`, `--format=${format}`],
139
132
  {
140
- cwd: workspacePath2,
133
+ cwd: workspacePath,
141
134
  encoding: "utf-8",
142
135
  timeout: 5e3
143
136
  }
@@ -150,16 +143,16 @@ function getGitLogSince(workspacePath2, format, sinceTimestamp) {
150
143
  return [];
151
144
  }
152
145
  }
153
- function getCommitsSince(workspacePath2, sinceTimestamp) {
154
- return getGitLogSince(workspacePath2, "%H", sinceTimestamp);
146
+ function getCommitsSince(workspacePath, sinceTimestamp) {
147
+ return getGitLogSince(workspacePath, "%H", sinceTimestamp);
155
148
  }
156
- function getCommitMessagesSince(workspacePath2, sinceTimestamp) {
157
- return getGitLogSince(workspacePath2, "%s", sinceTimestamp);
149
+ function getCommitMessagesSince(workspacePath, sinceTimestamp) {
150
+ return getGitLogSince(workspacePath, "%s", sinceTimestamp);
158
151
  }
159
- function getHeadCommitHash(workspacePath2) {
152
+ function getHeadCommitHash(workspacePath) {
160
153
  try {
161
154
  const result = execFileSync("git", ["rev-parse", "HEAD"], {
162
- cwd: workspacePath2,
155
+ cwd: workspacePath,
163
156
  encoding: "utf-8",
164
157
  timeout: 5e3
165
158
  });
@@ -168,10 +161,10 @@ function getHeadCommitHash(workspacePath2) {
168
161
  return void 0;
169
162
  }
170
163
  }
171
- function getTouchedFiles(workspacePath2) {
164
+ function getTouchedFiles(workspacePath) {
172
165
  try {
173
166
  const result = execFileSync("git", ["status", "--porcelain"], {
174
- cwd: workspacePath2,
167
+ cwd: workspacePath,
175
168
  encoding: "utf-8",
176
169
  timeout: 5e3
177
170
  });
@@ -387,22 +380,34 @@ function inferFocusFromFiles(files) {
387
380
  // ../../packages/shared/src/storage.ts
388
381
  import fs from "fs";
389
382
  import path2 from "path";
390
- import { randomUUID as randomUUID2 } from "crypto";
383
+ import { randomUUID as randomUUID2, createHash } from "crypto";
391
384
  var STORAGE_DIR = ".keepgoing";
392
385
  var META_FILE = "meta.json";
393
386
  var SESSIONS_FILE = "sessions.json";
394
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
+ }
395
398
  var KeepGoingWriter = class {
396
399
  storagePath;
397
400
  sessionsFilePath;
398
401
  stateFilePath;
399
402
  metaFilePath;
400
- constructor(workspacePath2) {
401
- const mainRoot = resolveStorageRoot(workspacePath2);
403
+ currentTasksFilePath;
404
+ constructor(workspacePath) {
405
+ const mainRoot = resolveStorageRoot(workspacePath);
402
406
  this.storagePath = path2.join(mainRoot, STORAGE_DIR);
403
407
  this.sessionsFilePath = path2.join(this.storagePath, SESSIONS_FILE);
404
408
  this.stateFilePath = path2.join(this.storagePath, STATE_FILE);
405
409
  this.metaFilePath = path2.join(this.storagePath, META_FILE);
410
+ this.currentTasksFilePath = path2.join(this.storagePath, CURRENT_TASKS_FILE);
406
411
  }
407
412
  ensureDir() {
408
413
  if (!fs.existsSync(this.storagePath)) {
@@ -463,11 +468,212 @@ var KeepGoingWriter = class {
463
468
  }
464
469
  fs.writeFileSync(this.metaFilePath, JSON.stringify(meta, null, 2), "utf-8");
465
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
+ }
466
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
+ }
467
559
 
468
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";
469
566
  import fs2 from "fs";
567
+ import os from "os";
470
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;
471
677
 
472
678
  // ../../packages/shared/src/featureGate.ts
473
679
  var DefaultFeatureGate = class {
@@ -487,25 +693,25 @@ var MAX_DECISIONS = 100;
487
693
  var DecisionStorage = class {
488
694
  storagePath;
489
695
  decisionsFilePath;
490
- constructor(workspacePath2) {
491
- const mainRoot = resolveStorageRoot(workspacePath2);
492
- this.storagePath = path3.join(mainRoot, STORAGE_DIR2);
493
- 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);
494
700
  }
495
701
  ensureStorageDir() {
496
- if (!fs2.existsSync(this.storagePath)) {
497
- fs2.mkdirSync(this.storagePath, { recursive: true });
702
+ if (!fs3.existsSync(this.storagePath)) {
703
+ fs3.mkdirSync(this.storagePath, { recursive: true });
498
704
  }
499
705
  }
500
706
  getProjectName() {
501
- return path3.basename(path3.dirname(this.storagePath));
707
+ return path4.basename(path4.dirname(this.storagePath));
502
708
  }
503
709
  load() {
504
710
  try {
505
- if (!fs2.existsSync(this.decisionsFilePath)) {
711
+ if (!fs3.existsSync(this.decisionsFilePath)) {
506
712
  return createEmptyProjectDecisions(this.getProjectName());
507
713
  }
508
- const raw = fs2.readFileSync(this.decisionsFilePath, "utf-8");
714
+ const raw = fs3.readFileSync(this.decisionsFilePath, "utf-8");
509
715
  const data = JSON.parse(raw);
510
716
  return data;
511
717
  } catch {
@@ -515,15 +721,14 @@ var DecisionStorage = class {
515
721
  save(decisions) {
516
722
  this.ensureStorageDir();
517
723
  const content = JSON.stringify(decisions, null, 2);
518
- fs2.writeFileSync(this.decisionsFilePath, content, "utf-8");
724
+ fs3.writeFileSync(this.decisionsFilePath, content, "utf-8");
519
725
  }
520
726
  /**
521
- * Save a decision record. Returns true if saved, false if gated or error.
727
+ * Save a decision record as a draft. Always persists regardless of Pro
728
+ * status so decisions are captured at the correct time. Returns true if
729
+ * saved, false on I/O error.
522
730
  */
523
731
  saveDecision(decision) {
524
- if (!isDecisionsEnabled()) {
525
- return false;
526
- }
527
732
  const data = this.load();
528
733
  data.decisions.push(decision);
529
734
  data.lastDecisionId = decision.id;
@@ -766,128 +971,125 @@ function tryDetectDecision(opts) {
766
971
  };
767
972
  }
768
973
 
769
- // ../../packages/shared/src/license.ts
770
- import crypto from "crypto";
771
- import fs3 from "fs";
772
- import os from "os";
773
- import path4 from "path";
774
- var LICENSE_FILE = "license.json";
775
- var DEVICE_ID_FILE = "device-id";
776
- function getGlobalLicenseDir() {
777
- return path4.join(os.homedir(), ".keepgoing");
778
- }
779
- function getGlobalLicensePath() {
780
- return path4.join(getGlobalLicenseDir(), LICENSE_FILE);
781
- }
782
- function getDeviceId() {
783
- const dir = getGlobalLicenseDir();
784
- const filePath = path4.join(dir, DEVICE_ID_FILE);
785
- try {
786
- const existing = fs3.readFileSync(filePath, "utf-8").trim();
787
- if (existing) return existing;
788
- } catch {
789
- }
790
- const id = crypto.randomUUID();
791
- if (!fs3.existsSync(dir)) {
792
- fs3.mkdirSync(dir, { recursive: true });
793
- }
794
- fs3.writeFileSync(filePath, id, "utf-8");
795
- return id;
796
- }
797
- function readLicenseCache() {
798
- const licensePath = getGlobalLicensePath();
799
- try {
800
- if (!fs3.existsSync(licensePath)) {
801
- return void 0;
802
- }
803
- const raw = fs3.readFileSync(licensePath, "utf-8");
804
- return JSON.parse(raw);
805
- } catch {
806
- return void 0;
807
- }
974
+ // ../../packages/shared/src/licenseClient.ts
975
+ var BASE_URL = "https://api.lemonsqueezy.com/v1/licenses";
976
+ var REQUEST_TIMEOUT_MS = 15e3;
977
+ var EXPECTED_STORE_ID = 301555;
978
+ var EXPECTED_PRODUCT_ID = 864311;
979
+ function fetchWithTimeout(url, init) {
980
+ const controller = new AbortController();
981
+ const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
982
+ return fetch(url, { ...init, signal: controller.signal }).finally(() => clearTimeout(timer));
808
983
  }
809
- function writeLicenseCache(cache) {
810
- const dirPath = getGlobalLicenseDir();
811
- if (!fs3.existsSync(dirPath)) {
812
- fs3.mkdirSync(dirPath, { recursive: true });
984
+ function validateProductIdentity(meta) {
985
+ if (!meta) return "License response missing product metadata.";
986
+ if (meta.store_id !== EXPECTED_STORE_ID || meta.product_id !== EXPECTED_PRODUCT_ID) {
987
+ return "This license key does not belong to KeepGoing.";
813
988
  }
814
- const licensePath = path4.join(dirPath, LICENSE_FILE);
815
- fs3.writeFileSync(licensePath, JSON.stringify(cache, null, 2), "utf-8");
989
+ return void 0;
816
990
  }
817
- function deleteLicenseCache() {
818
- const licensePath = getGlobalLicensePath();
991
+ async function safeJson(res) {
819
992
  try {
820
- if (fs3.existsSync(licensePath)) {
821
- fs3.unlinkSync(licensePath);
822
- }
993
+ const text = await res.text();
994
+ return JSON.parse(text);
823
995
  } catch {
996
+ return null;
824
997
  }
825
998
  }
826
- function isCachedLicenseValid(cache) {
827
- return cache?.status === "active";
828
- }
829
- var REVALIDATION_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
830
-
831
- // ../../packages/shared/src/licenseClient.ts
832
- var BASE_URL = "https://api.lemonsqueezy.com/v1/licenses";
833
- async function activateLicense(licenseKey, instanceName) {
999
+ async function activateLicense(licenseKey, instanceName, options) {
834
1000
  try {
835
- const res = await fetch(`${BASE_URL}/activate`, {
1001
+ const res = await fetchWithTimeout(`${BASE_URL}/activate`, {
836
1002
  method: "POST",
837
1003
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
838
1004
  body: new URLSearchParams({ license_key: licenseKey, instance_name: instanceName })
839
1005
  });
840
- const data = await res.json();
841
- if (!res.ok || !data.activated) {
842
- return { valid: false, error: data.error || `Activation failed (${res.status})` };
1006
+ const data = await safeJson(res);
1007
+ if (!res.ok || !data?.activated) {
1008
+ return { valid: false, error: data?.error || `Activation failed (${res.status})` };
1009
+ }
1010
+ if (!options?.allowTestMode && data.license_key?.test_mode) {
1011
+ if (data.license_key?.key && data.instance?.id) {
1012
+ await deactivateLicense(data.license_key.key, data.instance.id);
1013
+ }
1014
+ return { valid: false, error: "This is a test license key. Please use a production license key from your purchase confirmation." };
1015
+ }
1016
+ if (!options?.allowTestMode) {
1017
+ const productError = validateProductIdentity(data.meta);
1018
+ if (productError) {
1019
+ if (data.license_key?.key && data.instance?.id) {
1020
+ await deactivateLicense(data.license_key.key, data.instance.id);
1021
+ }
1022
+ return { valid: false, error: productError };
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
+ }
843
1030
  }
844
1031
  return {
845
1032
  valid: true,
846
1033
  licenseKey: data.license_key?.key,
847
1034
  instanceId: data.instance?.id,
848
1035
  customerName: data.meta?.customer_name,
849
- productName: data.meta?.product_name
1036
+ productName: data.meta?.product_name,
1037
+ variantId: data.meta?.variant_id,
1038
+ variantName: data.meta?.variant_name
850
1039
  };
851
1040
  } catch (err) {
852
- return { valid: false, error: err instanceof Error ? err.message : "Network error" };
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";
1042
+ return { valid: false, error: message };
853
1043
  }
854
1044
  }
855
1045
  async function deactivateLicense(licenseKey, instanceId) {
856
1046
  try {
857
- const res = await fetch(`${BASE_URL}/deactivate`, {
1047
+ const res = await fetchWithTimeout(`${BASE_URL}/deactivate`, {
858
1048
  method: "POST",
859
1049
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
860
1050
  body: new URLSearchParams({ license_key: licenseKey, instance_id: instanceId })
861
1051
  });
862
- const data = await res.json();
863
- if (!res.ok || !data.deactivated) {
864
- return { deactivated: false, error: data.error || `Deactivation failed (${res.status})` };
1052
+ const data = await safeJson(res);
1053
+ if (!res.ok || !data?.deactivated) {
1054
+ return { deactivated: false, error: data?.error || `Deactivation failed (${res.status})` };
865
1055
  }
866
1056
  return { deactivated: true };
867
1057
  } catch (err) {
868
- return { deactivated: false, error: err instanceof Error ? err.message : "Network error" };
1058
+ 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";
1059
+ return { deactivated: false, error: message };
869
1060
  }
870
1061
  }
871
1062
 
872
1063
  // src/storage.ts
1064
+ import fs4 from "fs";
1065
+ import path5 from "path";
873
1066
  var STORAGE_DIR3 = ".keepgoing";
874
1067
  var META_FILE2 = "meta.json";
875
1068
  var SESSIONS_FILE2 = "sessions.json";
876
1069
  var DECISIONS_FILE2 = "decisions.json";
877
1070
  var STATE_FILE2 = "state.json";
1071
+ var CURRENT_TASKS_FILE2 = "current-tasks.json";
878
1072
  var KeepGoingReader = class {
1073
+ workspacePath;
879
1074
  storagePath;
880
1075
  metaFilePath;
881
1076
  sessionsFilePath;
882
1077
  decisionsFilePath;
883
1078
  stateFilePath;
884
- constructor(workspacePath2) {
885
- const mainRoot = resolveStorageRoot(workspacePath2);
1079
+ currentTasksFilePath;
1080
+ _isWorktree;
1081
+ _cachedBranch = null;
1082
+ // null = not yet resolved
1083
+ constructor(workspacePath) {
1084
+ this.workspacePath = workspacePath;
1085
+ const mainRoot = resolveStorageRoot(workspacePath);
1086
+ this._isWorktree = mainRoot !== workspacePath;
886
1087
  this.storagePath = path5.join(mainRoot, STORAGE_DIR3);
887
1088
  this.metaFilePath = path5.join(this.storagePath, META_FILE2);
888
1089
  this.sessionsFilePath = path5.join(this.storagePath, SESSIONS_FILE2);
889
1090
  this.decisionsFilePath = path5.join(this.storagePath, DECISIONS_FILE2);
890
1091
  this.stateFilePath = path5.join(this.storagePath, STATE_FILE2);
1092
+ this.currentTasksFilePath = path5.join(this.storagePath, CURRENT_TASKS_FILE2);
891
1093
  }
892
1094
  /** Check if .keepgoing/ directory exists. */
893
1095
  exists() {
@@ -949,9 +1151,160 @@ var KeepGoingReader = class {
949
1151
  const all = this.getDecisions();
950
1152
  return all.slice(-count).reverse();
951
1153
  }
952
- /** Read cached license data from the global `~/.keepgoing/license.json`. */
953
- getLicenseCache() {
954
- 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);
1229
+ }
1230
+ /** Get the last session checkpoint for a specific branch. */
1231
+ getLastSessionForBranch(branch) {
1232
+ const sessions = this.getSessions().filter((s) => s.gitBranch === branch);
1233
+ return sessions.length > 0 ? sessions[sessions.length - 1] : void 0;
1234
+ }
1235
+ /** Returns the last N sessions for a specific branch, newest first. */
1236
+ getRecentSessionsForBranch(branch, count) {
1237
+ const filtered = this.getSessions().filter((s) => s.gitBranch === branch);
1238
+ return filtered.slice(-count).reverse();
1239
+ }
1240
+ /** Returns the last N decisions for a specific branch, newest first. */
1241
+ getRecentDecisionsForBranch(branch, count) {
1242
+ const filtered = this.getDecisions().filter((d) => d.gitBranch === branch);
1243
+ return filtered.slice(-count).reverse();
1244
+ }
1245
+ /** Whether the workspace is inside a git worktree. */
1246
+ get isWorktree() {
1247
+ return this._isWorktree;
1248
+ }
1249
+ /**
1250
+ * Returns the current git branch for this workspace.
1251
+ * Lazily cached: the branch is resolved once per KeepGoingReader instance.
1252
+ */
1253
+ getCurrentBranch() {
1254
+ if (this._cachedBranch === null) {
1255
+ this._cachedBranch = getCurrentBranch(this.workspacePath);
1256
+ }
1257
+ return this._cachedBranch;
1258
+ }
1259
+ /**
1260
+ * Worktree-aware last session lookup.
1261
+ * In a worktree, scopes to the current branch with fallback to global.
1262
+ * Returns the session and whether it fell back to global.
1263
+ */
1264
+ getScopedLastSession() {
1265
+ const branch = this.getCurrentBranch();
1266
+ if (this._isWorktree && branch) {
1267
+ const scoped = this.getLastSessionForBranch(branch);
1268
+ if (scoped) return { session: scoped, isFallback: false };
1269
+ return { session: this.getLastSession(), isFallback: true };
1270
+ }
1271
+ return { session: this.getLastSession(), isFallback: false };
1272
+ }
1273
+ /** Worktree-aware recent sessions. Scopes to current branch in a worktree. */
1274
+ getScopedRecentSessions(count) {
1275
+ const branch = this.getCurrentBranch();
1276
+ if (this._isWorktree && branch) {
1277
+ return this.getRecentSessionsForBranch(branch, count);
1278
+ }
1279
+ return this.getRecentSessions(count);
1280
+ }
1281
+ /** Worktree-aware recent decisions. Scopes to current branch in a worktree. */
1282
+ getScopedRecentDecisions(count) {
1283
+ const branch = this.getCurrentBranch();
1284
+ if (this._isWorktree && branch) {
1285
+ return this.getRecentDecisionsForBranch(branch, count);
1286
+ }
1287
+ return this.getRecentDecisions(count);
1288
+ }
1289
+ /**
1290
+ * Resolves branch scope from an explicit `branch` parameter.
1291
+ * Used by tools that accept a `branch` argument (e.g. get_session_history, get_decisions).
1292
+ * - `"all"` returns no filter.
1293
+ * - An explicit branch name uses that.
1294
+ * - `undefined` auto-scopes to the current branch in a worktree, or all branches otherwise.
1295
+ */
1296
+ resolveBranchScope(branch) {
1297
+ if (branch === "all") {
1298
+ return { effectiveBranch: void 0, scopeLabel: "all branches" };
1299
+ }
1300
+ if (branch) {
1301
+ return { effectiveBranch: branch, scopeLabel: `branch \`${branch}\`` };
1302
+ }
1303
+ const currentBranch = this.getCurrentBranch();
1304
+ if (this._isWorktree && currentBranch) {
1305
+ return { effectiveBranch: currentBranch, scopeLabel: `branch \`${currentBranch}\` (worktree)` };
1306
+ }
1307
+ return { effectiveBranch: void 0, scopeLabel: "all branches" };
955
1308
  }
956
1309
  /**
957
1310
  * Parses sessions.json once, returning both the session list
@@ -990,13 +1343,13 @@ var KeepGoingReader = class {
990
1343
  };
991
1344
 
992
1345
  // src/tools/getMomentum.ts
993
- function registerGetMomentum(server2, reader2, workspacePath2) {
994
- server2.tool(
1346
+ function registerGetMomentum(server, reader, workspacePath) {
1347
+ server.tool(
995
1348
  "get_momentum",
996
1349
  "Get current developer momentum: last checkpoint, next step, blockers, and branch context. Use this to understand where the developer left off.",
997
1350
  {},
998
1351
  async () => {
999
- if (!reader2.exists()) {
1352
+ if (!reader.exists()) {
1000
1353
  return {
1001
1354
  content: [
1002
1355
  {
@@ -1006,7 +1359,8 @@ function registerGetMomentum(server2, reader2, workspacePath2) {
1006
1359
  ]
1007
1360
  };
1008
1361
  }
1009
- const lastSession = reader2.getLastSession();
1362
+ const { session: lastSession, isFallback } = reader.getScopedLastSession();
1363
+ const currentBranch = reader.getCurrentBranch();
1010
1364
  if (!lastSession) {
1011
1365
  return {
1012
1366
  content: [
@@ -1017,16 +1371,24 @@ function registerGetMomentum(server2, reader2, workspacePath2) {
1017
1371
  ]
1018
1372
  };
1019
1373
  }
1020
- const state = reader2.getState();
1021
- const currentBranch = getCurrentBranch(workspacePath2);
1374
+ const state = reader.getState();
1022
1375
  const branchChanged = lastSession.gitBranch && currentBranch && lastSession.gitBranch !== currentBranch;
1023
1376
  const lines = [
1024
1377
  `## Developer Momentum`,
1025
- "",
1378
+ ""
1379
+ ];
1380
+ if (reader.isWorktree && currentBranch) {
1381
+ lines.push(`**Worktree context:** Scoped to branch \`${currentBranch}\``);
1382
+ if (isFallback) {
1383
+ lines.push(`**Note:** No checkpoints found for branch \`${currentBranch}\`. Showing last global checkpoint.`);
1384
+ }
1385
+ lines.push("");
1386
+ }
1387
+ lines.push(
1026
1388
  `**Last checkpoint:** ${formatRelativeTime(lastSession.timestamp)}`,
1027
1389
  `**Summary:** ${lastSession.summary || "No summary"}`,
1028
1390
  `**Next step:** ${lastSession.nextStep || "Not specified"}`
1029
- ];
1391
+ );
1030
1392
  if (lastSession.blocker) {
1031
1393
  lines.push(`**Blocker:** ${lastSession.blocker}`);
1032
1394
  }
@@ -1037,7 +1399,7 @@ function registerGetMomentum(server2, reader2, workspacePath2) {
1037
1399
  if (currentBranch) {
1038
1400
  lines.push(`**Current branch:** ${currentBranch}`);
1039
1401
  }
1040
- if (branchChanged) {
1402
+ if (branchChanged && !reader.isWorktree) {
1041
1403
  lines.push(
1042
1404
  `**Note:** Branch changed since last checkpoint (was \`${lastSession.gitBranch}\`, now \`${currentBranch}\`)`
1043
1405
  );
@@ -1066,13 +1428,16 @@ function registerGetMomentum(server2, reader2, workspacePath2) {
1066
1428
 
1067
1429
  // src/tools/getSessionHistory.ts
1068
1430
  import { z } from "zod";
1069
- function registerGetSessionHistory(server2, reader2) {
1070
- server2.tool(
1431
+ function registerGetSessionHistory(server, reader) {
1432
+ server.tool(
1071
1433
  "get_session_history",
1072
1434
  "Get recent session checkpoints. Returns a chronological list of what the developer worked on.",
1073
- { limit: z.number().min(1).max(50).default(5).describe("Number of recent sessions to return (1-50, default 5)") },
1074
- async ({ limit }) => {
1075
- if (!reader2.exists()) {
1435
+ {
1436
+ limit: z.number().min(1).max(50).default(5).describe("Number of recent sessions to return (1-50, default 5)"),
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.')
1438
+ },
1439
+ async ({ limit, branch }) => {
1440
+ if (!reader.exists()) {
1076
1441
  return {
1077
1442
  content: [
1078
1443
  {
@@ -1082,19 +1447,20 @@ function registerGetSessionHistory(server2, reader2) {
1082
1447
  ]
1083
1448
  };
1084
1449
  }
1085
- const sessions = reader2.getRecentSessions(limit);
1450
+ const { effectiveBranch, scopeLabel } = reader.resolveBranchScope(branch);
1451
+ const sessions = effectiveBranch ? reader.getRecentSessionsForBranch(effectiveBranch, limit) : reader.getRecentSessions(limit);
1086
1452
  if (sessions.length === 0) {
1087
1453
  return {
1088
1454
  content: [
1089
1455
  {
1090
1456
  type: "text",
1091
- text: "No session checkpoints found."
1457
+ text: effectiveBranch ? `No session checkpoints found for branch \`${effectiveBranch}\`. Use branch: "all" to see all branches.` : "No session checkpoints found."
1092
1458
  }
1093
1459
  ]
1094
1460
  };
1095
1461
  }
1096
1462
  const lines = [
1097
- `## Session History (last ${sessions.length})`,
1463
+ `## Session History (last ${sessions.length}, ${scopeLabel})`,
1098
1464
  ""
1099
1465
  ];
1100
1466
  for (const session of sessions) {
@@ -1122,13 +1488,13 @@ function registerGetSessionHistory(server2, reader2) {
1122
1488
  }
1123
1489
 
1124
1490
  // src/tools/getReentryBriefing.ts
1125
- function registerGetReentryBriefing(server2, reader2, workspacePath2) {
1126
- server2.tool(
1491
+ function registerGetReentryBriefing(server, reader, workspacePath) {
1492
+ server.tool(
1127
1493
  "get_reentry_briefing",
1128
1494
  "Get a synthesized re-entry briefing that helps a developer understand where they left off. Includes focus, recent activity, and suggested next steps.",
1129
1495
  {},
1130
1496
  async () => {
1131
- if (!reader2.exists()) {
1497
+ if (!reader.exists()) {
1132
1498
  return {
1133
1499
  content: [
1134
1500
  {
@@ -1138,12 +1504,12 @@ function registerGetReentryBriefing(server2, reader2, workspacePath2) {
1138
1504
  ]
1139
1505
  };
1140
1506
  }
1141
- const lastSession = reader2.getLastSession();
1142
- const recentSessions = reader2.getRecentSessions(5);
1143
- const state = reader2.getState() ?? {};
1144
- const gitBranch = getCurrentBranch(workspacePath2);
1507
+ const gitBranch = reader.getCurrentBranch();
1508
+ const { session: lastSession } = reader.getScopedLastSession();
1509
+ const recentSessions = reader.getScopedRecentSessions(5);
1510
+ const state = reader.getState() ?? {};
1145
1511
  const sinceTimestamp = lastSession?.timestamp;
1146
- const recentCommits = sinceTimestamp ? getCommitMessagesSince(workspacePath2, sinceTimestamp) : [];
1512
+ const recentCommits = sinceTimestamp ? getCommitMessagesSince(workspacePath, sinceTimestamp) : [];
1147
1513
  const briefing = generateBriefing(
1148
1514
  lastSession,
1149
1515
  recentSessions,
@@ -1163,14 +1529,20 @@ function registerGetReentryBriefing(server2, reader2, workspacePath2) {
1163
1529
  }
1164
1530
  const lines = [
1165
1531
  `## Re-entry Briefing`,
1166
- "",
1532
+ ""
1533
+ ];
1534
+ if (reader.isWorktree && gitBranch) {
1535
+ lines.push(`**Worktree context:** Scoped to branch \`${gitBranch}\``);
1536
+ lines.push("");
1537
+ }
1538
+ lines.push(
1167
1539
  `**Last worked:** ${briefing.lastWorked}`,
1168
1540
  `**Current focus:** ${briefing.currentFocus}`,
1169
1541
  `**Recent activity:** ${briefing.recentActivity}`,
1170
1542
  `**Suggested next:** ${briefing.suggestedNext}`,
1171
1543
  `**Quick start:** ${briefing.smallNextStep}`
1172
- ];
1173
- const recentDecisions = reader2.getRecentDecisions(3);
1544
+ );
1545
+ const recentDecisions = reader.getScopedRecentDecisions(3);
1174
1546
  if (recentDecisions.length > 0) {
1175
1547
  lines.push("");
1176
1548
  lines.push("### Recent decisions");
@@ -1189,8 +1561,8 @@ function registerGetReentryBriefing(server2, reader2, workspacePath2) {
1189
1561
  // src/tools/saveCheckpoint.ts
1190
1562
  import path6 from "path";
1191
1563
  import { z as z2 } from "zod";
1192
- function registerSaveCheckpoint(server2, reader2, workspacePath2) {
1193
- server2.tool(
1564
+ function registerSaveCheckpoint(server, reader, workspacePath) {
1565
+ server.tool(
1194
1566
  "save_checkpoint",
1195
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.",
1196
1568
  {
@@ -1199,11 +1571,12 @@ function registerSaveCheckpoint(server2, reader2, workspacePath2) {
1199
1571
  blocker: z2.string().optional().describe("Any blocker preventing progress")
1200
1572
  },
1201
1573
  async ({ summary, nextStep, blocker }) => {
1202
- const lastSession = reader2.getLastSession();
1203
- const gitBranch = getCurrentBranch(workspacePath2);
1204
- const touchedFiles = getTouchedFiles(workspacePath2);
1205
- const commitHashes = getCommitsSince(workspacePath2, lastSession?.timestamp);
1206
- 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 });
1207
1580
  const checkpoint = createCheckpoint({
1208
1581
  summary,
1209
1582
  nextStep: nextStep || "",
@@ -1211,10 +1584,11 @@ function registerSaveCheckpoint(server2, reader2, workspacePath2) {
1211
1584
  gitBranch,
1212
1585
  touchedFiles,
1213
1586
  commitHashes,
1214
- workspaceRoot: workspacePath2,
1215
- source: "manual"
1587
+ workspaceRoot: workspacePath,
1588
+ source: "manual",
1589
+ sessionId
1216
1590
  });
1217
- const writer = new KeepGoingWriter(workspacePath2);
1591
+ const writer = new KeepGoingWriter(workspacePath);
1218
1592
  writer.saveCheckpoint(checkpoint, projectName);
1219
1593
  const lines = [
1220
1594
  `Checkpoint saved.`,
@@ -1224,11 +1598,11 @@ function registerSaveCheckpoint(server2, reader2, workspacePath2) {
1224
1598
  `- **Commits captured:** ${commitHashes.length}`
1225
1599
  ];
1226
1600
  if (commitHashes.length > 0) {
1227
- const commitMessages = getCommitMessagesSince(workspacePath2, lastSession?.timestamp);
1228
- const headHash = getHeadCommitHash(workspacePath2);
1601
+ const commitMessages = getCommitMessagesSince(workspacePath, lastSession?.timestamp);
1602
+ const headHash = getHeadCommitHash(workspacePath);
1229
1603
  if (commitMessages.length > 0 && headHash) {
1230
1604
  const detected = tryDetectDecision({
1231
- workspacePath: workspacePath2,
1605
+ workspacePath,
1232
1606
  checkpointId: checkpoint.id,
1233
1607
  gitBranch,
1234
1608
  commitHash: headHash,
@@ -1249,13 +1623,16 @@ function registerSaveCheckpoint(server2, reader2, workspacePath2) {
1249
1623
 
1250
1624
  // src/tools/getDecisions.ts
1251
1625
  import { z as z3 } from "zod";
1252
- function registerGetDecisions(server2, reader2) {
1253
- server2.tool(
1626
+ function registerGetDecisions(server, reader) {
1627
+ server.tool(
1254
1628
  "get_decisions",
1255
1629
  "Get recent decision records. Returns detected high-signal commits with their category, confidence, and rationale.",
1256
- { limit: z3.number().min(1).max(50).default(10).describe("Number of recent decisions to return (1-50, default 10)") },
1257
- async ({ limit }) => {
1258
- if (!reader2.exists()) {
1630
+ {
1631
+ limit: z3.number().min(1).max(50).default(10).describe("Number of recent decisions to return (1-50, default 10)"),
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.')
1633
+ },
1634
+ async ({ limit, branch }) => {
1635
+ if (!reader.exists()) {
1259
1636
  return {
1260
1637
  content: [
1261
1638
  {
@@ -1265,8 +1642,7 @@ function registerGetDecisions(server2, reader2) {
1265
1642
  ]
1266
1643
  };
1267
1644
  }
1268
- const licenseCache = reader2.getLicenseCache();
1269
- if (!isCachedLicenseValid(licenseCache)) {
1645
+ if (process.env.KEEPGOING_PRO_BYPASS !== "1" && !getLicenseForFeature("decisions")) {
1270
1646
  return {
1271
1647
  content: [
1272
1648
  {
@@ -1276,19 +1652,20 @@ function registerGetDecisions(server2, reader2) {
1276
1652
  ]
1277
1653
  };
1278
1654
  }
1279
- const decisions = reader2.getRecentDecisions(limit);
1655
+ const { effectiveBranch, scopeLabel } = reader.resolveBranchScope(branch);
1656
+ const decisions = effectiveBranch ? reader.getRecentDecisionsForBranch(effectiveBranch, limit) : reader.getRecentDecisions(limit);
1280
1657
  if (decisions.length === 0) {
1281
1658
  return {
1282
1659
  content: [
1283
1660
  {
1284
1661
  type: "text",
1285
- text: "No decision records found."
1662
+ text: effectiveBranch ? `No decision records found for branch \`${effectiveBranch}\`. Use branch: "all" to see all branches.` : "No decision records found."
1286
1663
  }
1287
1664
  ]
1288
1665
  };
1289
1666
  }
1290
1667
  const lines = [
1291
- `## Decisions (last ${decisions.length})`,
1668
+ `## Decisions (last ${decisions.length}, ${scopeLabel})`,
1292
1669
  ""
1293
1670
  ];
1294
1671
  for (const decision of decisions) {
@@ -1314,8 +1691,104 @@ function registerGetDecisions(server2, reader2) {
1314
1691
  );
1315
1692
  }
1316
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
+
1317
1789
  // src/tools/setupProject.ts
1318
1790
  import fs5 from "fs";
1791
+ import os2 from "os";
1319
1792
  import path7 from "path";
1320
1793
  import { z as z4 } from "zod";
1321
1794
  var KEEPGOING_MARKER = "@keepgoingdev/mcp-server";
@@ -1337,6 +1810,15 @@ var STOP_HOOK = {
1337
1810
  }
1338
1811
  ]
1339
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
+ };
1340
1822
  var CLAUDE_MD_SECTION = `
1341
1823
  ## KeepGoing
1342
1824
 
@@ -1350,8 +1832,8 @@ function hasKeepGoingHook(hookEntries) {
1350
1832
  (entry) => entry?.hooks?.some((h) => typeof h?.command === "string" && h.command.includes(KEEPGOING_MARKER))
1351
1833
  );
1352
1834
  }
1353
- function registerSetupProject(server2, workspacePath2) {
1354
- server2.tool(
1835
+ function registerSetupProject(server, workspacePath) {
1836
+ server.tool(
1355
1837
  "setup_project",
1356
1838
  "Set up KeepGoing in the current project. Adds session hooks to .claude/settings.json and CLAUDE.md instructions so checkpoints are saved automatically.",
1357
1839
  {
@@ -1360,44 +1842,80 @@ function registerSetupProject(server2, workspacePath2) {
1360
1842
  },
1361
1843
  async ({ sessionHooks, claudeMd }) => {
1362
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;
1363
1852
  if (sessionHooks) {
1364
- const claudeDir = path7.join(workspacePath2, ".claude");
1365
- const settingsPath = path7.join(claudeDir, "settings.json");
1366
- let settings = {};
1367
- if (fs5.existsSync(settingsPath)) {
1368
- settings = JSON.parse(fs5.readFileSync(settingsPath, "utf-8"));
1369
- }
1370
1853
  if (!settings.hooks) {
1371
1854
  settings.hooks = {};
1372
1855
  }
1373
- let hooksChanged = false;
1374
1856
  if (!Array.isArray(settings.hooks.SessionStart)) {
1375
1857
  settings.hooks.SessionStart = [];
1376
1858
  }
1377
1859
  if (!hasKeepGoingHook(settings.hooks.SessionStart)) {
1378
1860
  settings.hooks.SessionStart.push(SESSION_START_HOOK);
1379
- hooksChanged = true;
1861
+ settingsChanged = true;
1380
1862
  }
1381
1863
  if (!Array.isArray(settings.hooks.Stop)) {
1382
1864
  settings.hooks.Stop = [];
1383
1865
  }
1384
1866
  if (!hasKeepGoingHook(settings.hooks.Stop)) {
1385
1867
  settings.hooks.Stop.push(STOP_HOOK);
1386
- hooksChanged = true;
1868
+ settingsChanged = true;
1387
1869
  }
1388
- if (hooksChanged) {
1389
- if (!fs5.existsSync(claudeDir)) {
1390
- fs5.mkdirSync(claudeDir, { recursive: true });
1391
- }
1392
- 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) {
1393
1878
  results.push("**Session hooks:** Added to `.claude/settings.json`");
1394
1879
  } else {
1395
1880
  results.push("**Session hooks:** Already present, skipped");
1396
1881
  }
1397
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
+ }
1398
1916
  if (claudeMd) {
1399
- const dotClaudeMdPath = path7.join(workspacePath2, ".claude", "CLAUDE.md");
1400
- const rootClaudeMdPath = path7.join(workspacePath2, "CLAUDE.md");
1917
+ const dotClaudeMdPath = path7.join(workspacePath, ".claude", "CLAUDE.md");
1918
+ const rootClaudeMdPath = path7.join(workspacePath, "CLAUDE.md");
1401
1919
  const claudeMdPath = fs5.existsSync(dotClaudeMdPath) ? dotClaudeMdPath : rootClaudeMdPath;
1402
1920
  let existing = "";
1403
1921
  if (fs5.existsSync(claudeMdPath)) {
@@ -1420,20 +1938,24 @@ function registerSetupProject(server2, workspacePath2) {
1420
1938
 
1421
1939
  // src/tools/activateLicense.ts
1422
1940
  import { z as z5 } from "zod";
1423
- function registerActivateLicense(server2) {
1424
- server2.tool(
1941
+ function registerActivateLicense(server) {
1942
+ server.tool(
1425
1943
  "activate_license",
1426
- "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.",
1427
1945
  { license_key: z5.string().describe("Your KeepGoing Pro license key") },
1428
1946
  async ({ license_key }) => {
1429
- const existing = readLicenseCache();
1430
- if (isCachedLicenseValid(existing)) {
1431
- 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})` : "";
1432
1954
  return {
1433
1955
  content: [
1434
1956
  {
1435
1957
  type: "text",
1436
- text: `Pro license is already active${who2}. No action needed.`
1958
+ text: `${label2} is already active${who2}. No action needed.`
1437
1959
  }
1438
1960
  ]
1439
1961
  };
@@ -1449,22 +1971,43 @@ function registerActivateLicense(server2) {
1449
1971
  ]
1450
1972
  };
1451
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
+ }
1452
1990
  const now = (/* @__PURE__ */ new Date()).toISOString();
1453
- writeLicenseCache({
1991
+ addLicenseEntry({
1454
1992
  licenseKey: result.licenseKey || license_key,
1455
1993
  instanceId: result.instanceId || getDeviceId(),
1456
1994
  status: "active",
1457
1995
  lastValidatedAt: now,
1458
1996
  activatedAt: now,
1997
+ variantId,
1459
1998
  customerName: result.customerName,
1460
- productName: result.productName
1999
+ productName: result.productName,
2000
+ variantName: result.variantName
1461
2001
  });
2002
+ const label = getVariantLabel(variantId);
2003
+ const features = VARIANT_FEATURE_MAP[variantId];
2004
+ const featureList = features ? features.join(", ") : "Pro features";
1462
2005
  const who = result.customerName ? ` Welcome, ${result.customerName}!` : "";
1463
2006
  return {
1464
2007
  content: [
1465
2008
  {
1466
2009
  type: "text",
1467
- text: `Pro license activated successfully.${who} Decision Detection is now enabled.`
2010
+ text: `${label} activated successfully.${who} Enabled: ${featureList}.`
1468
2011
  }
1469
2012
  ]
1470
2013
  };
@@ -1473,14 +2016,18 @@ function registerActivateLicense(server2) {
1473
2016
  }
1474
2017
 
1475
2018
  // src/tools/deactivateLicense.ts
1476
- function registerDeactivateLicense(server2) {
1477
- server2.tool(
2019
+ import { z as z6 } from "zod";
2020
+ function registerDeactivateLicense(server) {
2021
+ server.tool(
1478
2022
  "deactivate_license",
1479
2023
  "Deactivate the KeepGoing Pro license on this device.",
1480
- {},
1481
- async () => {
1482
- const cache = readLicenseCache();
1483
- 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) {
1484
2031
  return {
1485
2032
  content: [
1486
2033
  {
@@ -1490,14 +2037,41 @@ function registerDeactivateLicense(server2) {
1490
2037
  ]
1491
2038
  };
1492
2039
  }
1493
- const result = await deactivateLicense(cache.licenseKey, cache.instanceId);
1494
- 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);
1495
2069
  if (!result.deactivated) {
1496
2070
  return {
1497
2071
  content: [
1498
2072
  {
1499
2073
  type: "text",
1500
- 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"}`
1501
2075
  }
1502
2076
  ]
1503
2077
  };
@@ -1506,7 +2080,7 @@ function registerDeactivateLicense(server2) {
1506
2080
  content: [
1507
2081
  {
1508
2082
  type: "text",
1509
- text: "Pro license deactivated successfully. The activation slot has been freed."
2083
+ text: `${label} license deactivated successfully. The activation slot has been freed.`
1510
2084
  }
1511
2085
  ]
1512
2086
  };
@@ -1515,8 +2089,8 @@ function registerDeactivateLicense(server2) {
1515
2089
  }
1516
2090
 
1517
2091
  // src/prompts/resume.ts
1518
- function registerResumePrompt(server2) {
1519
- server2.prompt(
2092
+ function registerResumePrompt(server) {
2093
+ server.prompt(
1520
2094
  "resume",
1521
2095
  "Check developer momentum and suggest what to work on next",
1522
2096
  async () => ({
@@ -1542,14 +2116,76 @@ function registerResumePrompt(server2) {
1542
2116
  );
1543
2117
  }
1544
2118
 
1545
- // src/index.ts
1546
- if (process.argv.includes("--print-momentum")) {
1547
- const wsPath = findGitRoot(process.argv.slice(2).find((a) => a !== "--print-momentum") || process.cwd());
1548
- const reader2 = new KeepGoingReader(wsPath);
1549
- if (!reader2.exists()) {
2119
+ // src/prompts/decisions.ts
2120
+ function registerDecisionsPrompt(server) {
2121
+ server.prompt(
2122
+ "decisions",
2123
+ "Review recent architectural decisions and their rationale",
2124
+ async () => ({
2125
+ messages: [
2126
+ {
2127
+ role: "user",
2128
+ content: {
2129
+ type: "text",
2130
+ text: [
2131
+ "I want to review recent architectural decisions in this project.",
2132
+ "",
2133
+ "Please use the KeepGoing tools to:",
2134
+ "1. Fetch recent decision records (get_decisions)",
2135
+ "2. Get my current branch context (get_momentum)",
2136
+ "3. Summarize the decisions, highlighting any that were made on the current branch",
2137
+ "",
2138
+ "Keep your response brief and organized."
2139
+ ].join("\n")
2140
+ }
2141
+ }
2142
+ ]
2143
+ })
2144
+ );
2145
+ }
2146
+
2147
+ // src/prompts/progress.ts
2148
+ function registerProgressPrompt(server) {
2149
+ server.prompt(
2150
+ "progress",
2151
+ "Summarize recent development progress across sessions",
2152
+ async () => ({
2153
+ messages: [
2154
+ {
2155
+ role: "user",
2156
+ content: {
2157
+ type: "text",
2158
+ text: [
2159
+ "I need a summary of recent development progress for this project.",
2160
+ "",
2161
+ "Please use the KeepGoing tools to:",
2162
+ "1. Fetch session history with a higher limit for broader coverage (get_session_history, limit: 20)",
2163
+ "2. Get my current branch context (get_momentum)",
2164
+ "3. Synthesize a progress summary grouped by branch or feature, highlighting the current branch",
2165
+ "",
2166
+ "Format the summary so it can be used in a standup or sprint review."
2167
+ ].join("\n")
2168
+ }
2169
+ }
2170
+ ]
2171
+ })
2172
+ );
2173
+ }
2174
+
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()) {
1550
2186
  process.exit(0);
1551
2187
  }
1552
- const lastSession = reader2.getLastSession();
2188
+ const { session: lastSession } = reader.getScopedLastSession();
1553
2189
  if (!lastSession) {
1554
2190
  process.exit(0);
1555
2191
  }
@@ -1575,10 +2211,49 @@ if (process.argv.includes("--print-momentum")) {
1575
2211
  console.log(lines.join("\n"));
1576
2212
  process.exit(0);
1577
2213
  }
1578
- if (process.argv.includes("--save-checkpoint")) {
1579
- const wsPath = findGitRoot(process.argv.slice(2).find((a) => !a.startsWith("--")) || process.cwd());
1580
- const reader2 = new KeepGoingReader(wsPath);
1581
- const lastSession = reader2.getLastSession();
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();
1582
2257
  if (lastSession?.timestamp) {
1583
2258
  const ageMs = Date.now() - new Date(lastSession.timestamp).getTime();
1584
2259
  if (ageMs < 2 * 60 * 1e3) {
@@ -1603,6 +2278,7 @@ if (process.argv.includes("--save-checkpoint")) {
1603
2278
  }
1604
2279
  }
1605
2280
  const projectName = path8.basename(resolveStorageRoot(wsPath));
2281
+ const sessionId = generateSessionId({ workspaceRoot: wsPath, branch: gitBranch ?? void 0, worktreePath: wsPath });
1606
2282
  const checkpoint = createCheckpoint({
1607
2283
  summary,
1608
2284
  nextStep: "",
@@ -1610,12 +2286,19 @@ if (process.argv.includes("--save-checkpoint")) {
1610
2286
  touchedFiles,
1611
2287
  commitHashes,
1612
2288
  workspaceRoot: wsPath,
1613
- source: "auto"
2289
+ source: "auto",
2290
+ sessionId
1614
2291
  });
1615
2292
  const writer = new KeepGoingWriter(wsPath);
1616
2293
  writer.saveCheckpoint(checkpoint, projectName);
1617
- const licenseCache = readLicenseCache();
1618
- 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) {
1619
2302
  const headHash = getHeadCommitHash(wsPath) || commitHashes[0];
1620
2303
  if (headHash) {
1621
2304
  const detected = tryDetectDecision({
@@ -1634,22 +2317,109 @@ if (process.argv.includes("--save-checkpoint")) {
1634
2317
  console.log(`[KeepGoing] Auto-checkpoint saved: ${summary}`);
1635
2318
  process.exit(0);
1636
2319
  }
1637
- var workspacePath = findGitRoot(process.argv[2] || process.cwd());
1638
- var reader = new KeepGoingReader(workspacePath);
1639
- var server = new McpServer({
1640
- name: "keepgoing",
1641
- version: "0.1.0"
1642
- });
1643
- registerGetMomentum(server, reader, workspacePath);
1644
- registerGetSessionHistory(server, reader);
1645
- registerGetReentryBriefing(server, reader, workspacePath);
1646
- registerGetDecisions(server, reader);
1647
- registerSaveCheckpoint(server, reader, workspacePath);
1648
- registerSetupProject(server, workspacePath);
1649
- registerActivateLicense(server);
1650
- registerDeactivateLicense(server);
1651
- registerResumePrompt(server);
1652
- var transport = new StdioServerTransport();
1653
- await server.connect(transport);
1654
- 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
+ }
1655
2425
  //# sourceMappingURL=index.js.map