@keepgoingdev/cli 0.3.3 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +1833 -167
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -105,6 +105,44 @@ function getCurrentBranch(workspacePath) {
105
105
  return void 0;
106
106
  }
107
107
  }
108
+ function getGitLogSince(workspacePath, format, sinceTimestamp) {
109
+ try {
110
+ const since = sinceTimestamp || new Date(Date.now() - 24 * 60 * 60 * 1e3).toISOString();
111
+ const result = execFileSync(
112
+ "git",
113
+ ["log", `--since=${since}`, `--format=${format}`],
114
+ {
115
+ cwd: workspacePath,
116
+ encoding: "utf-8",
117
+ timeout: 5e3
118
+ }
119
+ );
120
+ if (!result.trim()) {
121
+ return [];
122
+ }
123
+ return result.trim().split("\n").filter((line) => line.length > 0);
124
+ } catch {
125
+ return [];
126
+ }
127
+ }
128
+ function getCommitsSince(workspacePath, sinceTimestamp) {
129
+ return getGitLogSince(workspacePath, "%H", sinceTimestamp);
130
+ }
131
+ function getCommitMessagesSince(workspacePath, sinceTimestamp) {
132
+ return getGitLogSince(workspacePath, "%s", sinceTimestamp);
133
+ }
134
+ function getFilesChangedInCommit(workspacePath, commitHash) {
135
+ try {
136
+ const result = execFileSync("git", ["diff-tree", "--no-commit-id", "--name-only", "-r", commitHash], {
137
+ cwd: workspacePath,
138
+ encoding: "utf-8",
139
+ timeout: 5e3
140
+ });
141
+ return result.trim().split("\n").filter(Boolean);
142
+ } catch {
143
+ return [];
144
+ }
145
+ }
108
146
  function getTouchedFiles(workspacePath) {
109
147
  try {
110
148
  const result = execFileSync("git", ["status", "--porcelain"], {
@@ -123,9 +161,203 @@ function getTouchedFiles(workspacePath) {
123
161
 
124
162
  // ../../packages/shared/src/reentry.ts
125
163
  var RECENT_SESSION_COUNT = 5;
164
+ function generateBriefing(lastSession, recentSessions, projectState, gitBranch, recentCommitMessages) {
165
+ if (!lastSession) {
166
+ return void 0;
167
+ }
168
+ return {
169
+ lastWorked: formatRelativeTime(lastSession.timestamp),
170
+ currentFocus: buildCurrentFocus(lastSession, projectState, gitBranch),
171
+ recentActivity: buildRecentActivity(
172
+ lastSession,
173
+ recentSessions,
174
+ recentCommitMessages
175
+ ),
176
+ suggestedNext: buildSuggestedNext(lastSession, gitBranch),
177
+ smallNextStep: buildSmallNextStep(
178
+ lastSession,
179
+ gitBranch,
180
+ recentCommitMessages
181
+ )
182
+ };
183
+ }
126
184
  function getRecentSessions(allSessions, count = RECENT_SESSION_COUNT) {
127
185
  return allSessions.slice(-count).reverse();
128
186
  }
187
+ function buildCurrentFocus(lastSession, projectState, gitBranch) {
188
+ if (projectState.derivedCurrentFocus) {
189
+ return projectState.derivedCurrentFocus;
190
+ }
191
+ const branchFocus = inferFocusFromBranch(gitBranch);
192
+ if (branchFocus) {
193
+ return branchFocus;
194
+ }
195
+ if (lastSession.summary) {
196
+ return lastSession.summary;
197
+ }
198
+ if (lastSession.touchedFiles.length > 0) {
199
+ return inferFocusFromFiles(lastSession.touchedFiles);
200
+ }
201
+ return "Unknown, save a checkpoint to set context";
202
+ }
203
+ function buildRecentActivity(lastSession, recentSessions, recentCommitMessages) {
204
+ const parts = [];
205
+ const sessionCount = recentSessions.length;
206
+ if (sessionCount > 1) {
207
+ parts.push(`${sessionCount} recent sessions`);
208
+ } else if (sessionCount === 1) {
209
+ parts.push("1 recent session");
210
+ }
211
+ if (lastSession.summary) {
212
+ parts.push(`Last: ${lastSession.summary}`);
213
+ }
214
+ if (lastSession.touchedFiles.length > 0) {
215
+ parts.push(`${lastSession.touchedFiles.length} files touched`);
216
+ }
217
+ if (recentCommitMessages && recentCommitMessages.length > 0) {
218
+ parts.push(`${recentCommitMessages.length} recent commits`);
219
+ }
220
+ return parts.length > 0 ? parts.join(". ") : "No recent activity recorded";
221
+ }
222
+ function buildSuggestedNext(lastSession, gitBranch) {
223
+ if (lastSession.nextStep) {
224
+ return lastSession.nextStep;
225
+ }
226
+ const branchFocus = inferFocusFromBranch(gitBranch);
227
+ if (branchFocus) {
228
+ return `Continue working on ${branchFocus}`;
229
+ }
230
+ if (lastSession.touchedFiles.length > 0) {
231
+ return `Continue working on ${inferFocusFromFiles(lastSession.touchedFiles)}`;
232
+ }
233
+ return "Save a checkpoint to track your next step";
234
+ }
235
+ function buildSmallNextStep(lastSession, gitBranch, recentCommitMessages) {
236
+ const fallback = "Review last changed files to resume flow";
237
+ if (lastSession.nextStep) {
238
+ const distilled = distillToSmallStep(
239
+ lastSession.nextStep,
240
+ lastSession.touchedFiles
241
+ );
242
+ if (distilled) {
243
+ return distilled;
244
+ }
245
+ }
246
+ if (recentCommitMessages && recentCommitMessages.length > 0) {
247
+ const commitStep = deriveStepFromCommits(recentCommitMessages);
248
+ if (commitStep) {
249
+ return commitStep;
250
+ }
251
+ }
252
+ if (lastSession.touchedFiles.length > 0) {
253
+ const fileStep = deriveStepFromFiles(lastSession.touchedFiles);
254
+ if (fileStep) {
255
+ return fileStep;
256
+ }
257
+ }
258
+ const branchFocus = inferFocusFromBranch(gitBranch);
259
+ if (branchFocus) {
260
+ return `Check git status for ${branchFocus}`;
261
+ }
262
+ return fallback;
263
+ }
264
+ function distillToSmallStep(nextStep, touchedFiles) {
265
+ if (!nextStep.trim()) {
266
+ return void 0;
267
+ }
268
+ const words = nextStep.trim().split(/\s+/);
269
+ if (words.length <= 12) {
270
+ if (touchedFiles.length > 0 && !mentionsFile(nextStep)) {
271
+ const primaryFile = getPrimaryFileName(touchedFiles);
272
+ const enhanced = `${nextStep.trim()} in ${primaryFile}`;
273
+ if (enhanced.split(/\s+/).length <= 12) {
274
+ return enhanced;
275
+ }
276
+ }
277
+ return nextStep.trim();
278
+ }
279
+ return words.slice(0, 12).join(" ");
280
+ }
281
+ function deriveStepFromCommits(commitMessages) {
282
+ const lastCommit = commitMessages[0];
283
+ if (!lastCommit || !lastCommit.trim()) {
284
+ return void 0;
285
+ }
286
+ const wipPattern = /^(?:wip|work in progress|started?|begin|draft)[:\s]/i;
287
+ if (wipPattern.test(lastCommit)) {
288
+ const topic = lastCommit.replace(wipPattern, "").trim();
289
+ if (topic) {
290
+ const words = topic.split(/\s+/).slice(0, 8).join(" ");
291
+ return `Continue ${words}`;
292
+ }
293
+ }
294
+ return void 0;
295
+ }
296
+ function deriveStepFromFiles(files) {
297
+ const primaryFile = getPrimaryFileName(files);
298
+ if (files.length > 1) {
299
+ return `Open ${primaryFile} and review ${files.length} changed files`;
300
+ }
301
+ return `Open ${primaryFile} and pick up where you left off`;
302
+ }
303
+ function getPrimaryFileName(files) {
304
+ const sourceFiles = files.filter((f) => {
305
+ const lower = f.toLowerCase();
306
+ return !lower.includes("test") && !lower.includes("spec") && !lower.includes(".config") && !lower.includes("package.json") && !lower.includes("tsconfig");
307
+ });
308
+ const target = sourceFiles.length > 0 ? sourceFiles[0] : files[0];
309
+ const parts = target.replace(/\\/g, "/").split("/");
310
+ return parts[parts.length - 1];
311
+ }
312
+ function mentionsFile(text) {
313
+ return /\w+\.(?:ts|tsx|js|jsx|py|go|rs|java|rb|css|scss|html|json|yaml|yml|md|sql|sh)\b/i.test(
314
+ text
315
+ );
316
+ }
317
+ function inferFocusFromBranch(branch) {
318
+ if (!branch || branch === "main" || branch === "master" || branch === "develop" || branch === "HEAD") {
319
+ return void 0;
320
+ }
321
+ const prefixPattern = /^(?:feature|feat|fix|bugfix|hotfix|chore|refactor|docs|test|ci)\//i;
322
+ const isFix = /^(?:fix|bugfix|hotfix)\//i.test(branch);
323
+ const stripped = branch.replace(prefixPattern, "");
324
+ const cleaned = stripped.replace(/[-_/]/g, " ").replace(/^\d+\s*/, "").trim();
325
+ if (!cleaned) {
326
+ return void 0;
327
+ }
328
+ return isFix ? `${cleaned} fix` : cleaned;
329
+ }
330
+ function inferFocusFromFiles(files) {
331
+ if (files.length === 0) {
332
+ return "unknown files";
333
+ }
334
+ const dirs = files.map((f) => {
335
+ const parts = f.replace(/\\/g, "/").split("/");
336
+ return parts.length > 1 ? parts.slice(0, -1).join("/") : "";
337
+ }).filter((d) => d.length > 0);
338
+ if (dirs.length > 0) {
339
+ const counts = /* @__PURE__ */ new Map();
340
+ for (const dir of dirs) {
341
+ counts.set(dir, (counts.get(dir) ?? 0) + 1);
342
+ }
343
+ let topDir = "";
344
+ let topCount = 0;
345
+ for (const [dir, count] of counts) {
346
+ if (count > topCount) {
347
+ topDir = dir;
348
+ topCount = count;
349
+ }
350
+ }
351
+ if (topDir) {
352
+ return `files in ${topDir}`;
353
+ }
354
+ }
355
+ const names = files.slice(0, 3).map((f) => {
356
+ const parts = f.replace(/\\/g, "/").split("/");
357
+ return parts[parts.length - 1];
358
+ });
359
+ return names.join(", ");
360
+ }
129
361
 
130
362
  // ../../packages/shared/src/storage.ts
131
363
  import fs from "fs";
@@ -306,6 +538,168 @@ function generateSessionId(context) {
306
538
  return `ses_${hash}`;
307
539
  }
308
540
 
541
+ // ../../packages/shared/src/smartSummary.ts
542
+ var PREFIX_VERBS = {
543
+ feat: "Added",
544
+ fix: "Fixed",
545
+ refactor: "Refactored",
546
+ docs: "Updated docs for",
547
+ test: "Added tests for",
548
+ chore: "Updated",
549
+ style: "Styled",
550
+ perf: "Optimized",
551
+ ci: "Updated CI for",
552
+ build: "Updated build for",
553
+ revert: "Reverted"
554
+ };
555
+ var NOISE_PATTERNS = [
556
+ "node_modules",
557
+ "package-lock.json",
558
+ "yarn.lock",
559
+ "pnpm-lock.yaml",
560
+ ".gitignore",
561
+ ".DS_Store",
562
+ "dist/",
563
+ "out/",
564
+ "build/"
565
+ ];
566
+ function categorizeCommits(messages) {
567
+ const groups = /* @__PURE__ */ new Map();
568
+ for (const msg of messages) {
569
+ const match = msg.match(/^(\w+)(?:\([^)]*\))?[!]?:\s*(.+)/);
570
+ if (match) {
571
+ const prefix = match[1].toLowerCase();
572
+ const body = match[2].trim();
573
+ if (!groups.has(prefix)) {
574
+ groups.set(prefix, []);
575
+ }
576
+ groups.get(prefix).push(body);
577
+ } else {
578
+ if (!groups.has("other")) {
579
+ groups.set("other", []);
580
+ }
581
+ groups.get("other").push(msg.trim());
582
+ }
583
+ }
584
+ return groups;
585
+ }
586
+ function inferWorkAreas(files) {
587
+ const areas = /* @__PURE__ */ new Map();
588
+ for (const file of files) {
589
+ if (NOISE_PATTERNS.some((p) => file.includes(p))) {
590
+ continue;
591
+ }
592
+ const parts = file.split("/").filter(Boolean);
593
+ let area;
594
+ if (parts.length >= 2 && (parts[0] === "apps" || parts[0] === "packages")) {
595
+ area = parts[1];
596
+ if (parts[0] === "packages" && parts.length >= 4 && parts[2] === "src") {
597
+ const subFile = parts[3].replace(/\.\w+$/, "");
598
+ area = `${parts[1]} ${subFile}`;
599
+ }
600
+ } else if (parts.length >= 2) {
601
+ area = parts[0];
602
+ } else {
603
+ area = "root";
604
+ }
605
+ areas.set(area, (areas.get(area) ?? 0) + 1);
606
+ }
607
+ return [...areas.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([name]) => name);
608
+ }
609
+ function buildSessionEvents(opts) {
610
+ const { wsPath, commitHashes, commitMessages, touchedFiles, currentBranch, sessionStartTime, lastActivityTime } = opts;
611
+ const commits = commitHashes.map((hash, i) => ({
612
+ hash,
613
+ message: commitMessages[i] ?? "",
614
+ filesChanged: getFilesChangedInCommit(wsPath, hash),
615
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
616
+ }));
617
+ const committedFiles = new Set(commits.flatMap((c) => c.filesChanged));
618
+ return {
619
+ commits,
620
+ branchSwitches: [],
621
+ touchedFiles,
622
+ currentBranch,
623
+ sessionStartTime,
624
+ lastActivityTime,
625
+ // Normalize rename arrows ("old -> new") from git status --porcelain
626
+ // so they match the plain filenames from git diff-tree --name-only.
627
+ hasUncommittedChanges: touchedFiles.some((f) => {
628
+ const normalized = f.includes(" -> ") ? f.split(" -> ").pop() : f;
629
+ return !committedFiles.has(normalized);
630
+ })
631
+ };
632
+ }
633
+ function buildSmartSummary(events) {
634
+ const { commits, branchSwitches, touchedFiles, hasUncommittedChanges } = events;
635
+ if (commits.length === 0 && touchedFiles.length === 0 && branchSwitches.length === 0) {
636
+ return void 0;
637
+ }
638
+ const parts = [];
639
+ if (commits.length > 0) {
640
+ const messages = commits.map((c) => c.message);
641
+ const groups = categorizeCommits(messages);
642
+ const phrases = [];
643
+ for (const [prefix, bodies] of groups) {
644
+ const verb = PREFIX_VERBS[prefix] ?? (prefix === "other" ? "" : `${capitalize(prefix)}:`);
645
+ const items = bodies.slice(0, 2).join(" and ");
646
+ const overflow = bodies.length > 2 ? ` (+${bodies.length - 2} more)` : "";
647
+ if (verb) {
648
+ phrases.push(`${verb} ${items}${overflow}`);
649
+ } else {
650
+ phrases.push(`${items}${overflow}`);
651
+ }
652
+ }
653
+ parts.push(phrases.join(", "));
654
+ } else if (touchedFiles.length > 0) {
655
+ const areas = inferWorkAreas(touchedFiles);
656
+ const areaStr = areas.length > 0 ? areas.join(" and ") : `${touchedFiles.length} files`;
657
+ const suffix = hasUncommittedChanges ? " (uncommitted)" : "";
658
+ parts.push(`Worked on ${areaStr}${suffix}`);
659
+ }
660
+ if (branchSwitches.length > 0) {
661
+ const last = branchSwitches[branchSwitches.length - 1];
662
+ if (branchSwitches.length === 1) {
663
+ parts.push(`switched to ${last.toBranch}`);
664
+ } else {
665
+ parts.push(`switched branches ${branchSwitches.length} times, ended on ${last.toBranch}`);
666
+ }
667
+ }
668
+ const result = parts.join("; ");
669
+ return result || void 0;
670
+ }
671
+ function buildSmartNextStep(events) {
672
+ const { commits, touchedFiles, currentBranch, hasUncommittedChanges } = events;
673
+ if (hasUncommittedChanges && touchedFiles.length > 0) {
674
+ const areas = inferWorkAreas(touchedFiles);
675
+ const areaStr = areas.length > 0 ? areas.join(" and ") : "working tree";
676
+ return `Review and commit changes in ${areaStr}`;
677
+ }
678
+ if (commits.length > 0) {
679
+ const lastMsg = commits[commits.length - 1].message;
680
+ const wipMatch = lastMsg.match(/^(?:wip|work in progress|start(?:ed)?|begin|draft)[:\s]+(.+)/i);
681
+ if (wipMatch) {
682
+ return `Continue ${wipMatch[1].trim()}`;
683
+ }
684
+ }
685
+ if (currentBranch && !["main", "master", "develop", "HEAD"].includes(currentBranch)) {
686
+ const branchName = currentBranch.replace(/^(feat|feature|fix|bugfix|hotfix|chore|refactor)[/-]/i, "").replace(/[-_]/g, " ").trim();
687
+ if (branchName) {
688
+ return `Continue ${branchName}`;
689
+ }
690
+ }
691
+ if (touchedFiles.length > 0) {
692
+ const areas = inferWorkAreas(touchedFiles);
693
+ if (areas.length > 0) {
694
+ return `Review recent changes in ${areas.join(" and ")}`;
695
+ }
696
+ }
697
+ return "";
698
+ }
699
+ function capitalize(s) {
700
+ return s.charAt(0).toUpperCase() + s.slice(1);
701
+ }
702
+
309
703
  // ../../packages/shared/src/decisionStorage.ts
310
704
  import fs3 from "fs";
311
705
  import path4 from "path";
@@ -415,6 +809,13 @@ function removeLicenseEntry(licenseKey) {
415
809
  function getActiveLicenses() {
416
810
  return readLicenseStore().licenses.filter((l) => l.status === "active");
417
811
  }
812
+ function getLicenseForFeature(feature) {
813
+ const active = getActiveLicenses();
814
+ return active.find((l) => {
815
+ const features = VARIANT_FEATURE_MAP[l.variantId];
816
+ return features?.includes(feature);
817
+ });
818
+ }
418
819
  var REVALIDATION_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
419
820
 
420
821
  // ../../packages/shared/src/featureGate.ts
@@ -425,6 +826,473 @@ var DefaultFeatureGate = class {
425
826
  };
426
827
  var currentGate = new DefaultFeatureGate();
427
828
 
829
+ // ../../packages/shared/src/reader.ts
830
+ import fs4 from "fs";
831
+ import path5 from "path";
832
+ var STORAGE_DIR2 = ".keepgoing";
833
+ var META_FILE2 = "meta.json";
834
+ var SESSIONS_FILE2 = "sessions.json";
835
+ var DECISIONS_FILE = "decisions.json";
836
+ var STATE_FILE2 = "state.json";
837
+ var CURRENT_TASKS_FILE2 = "current-tasks.json";
838
+ var KeepGoingReader = class {
839
+ workspacePath;
840
+ storagePath;
841
+ metaFilePath;
842
+ sessionsFilePath;
843
+ decisionsFilePath;
844
+ stateFilePath;
845
+ currentTasksFilePath;
846
+ _isWorktree;
847
+ _cachedBranch = null;
848
+ // null = not yet resolved
849
+ constructor(workspacePath) {
850
+ this.workspacePath = workspacePath;
851
+ const mainRoot = resolveStorageRoot(workspacePath);
852
+ this._isWorktree = mainRoot !== workspacePath;
853
+ this.storagePath = path5.join(mainRoot, STORAGE_DIR2);
854
+ this.metaFilePath = path5.join(this.storagePath, META_FILE2);
855
+ this.sessionsFilePath = path5.join(this.storagePath, SESSIONS_FILE2);
856
+ this.decisionsFilePath = path5.join(this.storagePath, DECISIONS_FILE);
857
+ this.stateFilePath = path5.join(this.storagePath, STATE_FILE2);
858
+ this.currentTasksFilePath = path5.join(this.storagePath, CURRENT_TASKS_FILE2);
859
+ }
860
+ /** Check if .keepgoing/ directory exists. */
861
+ exists() {
862
+ return fs4.existsSync(this.storagePath);
863
+ }
864
+ /** Read state.json, returns undefined if missing or corrupt. */
865
+ getState() {
866
+ return this.readJsonFile(this.stateFilePath);
867
+ }
868
+ /** Read meta.json, returns undefined if missing or corrupt. */
869
+ getMeta() {
870
+ return this.readJsonFile(this.metaFilePath);
871
+ }
872
+ /**
873
+ * Read sessions from sessions.json.
874
+ * Handles both formats:
875
+ * - Flat array: SessionCheckpoint[] (from ProjectStorage)
876
+ * - Wrapper object: ProjectSessions (from SessionStorage)
877
+ */
878
+ getSessions() {
879
+ return this.parseSessions().sessions;
880
+ }
881
+ /**
882
+ * Get the most recent session checkpoint.
883
+ * Uses state.lastSessionId if available, falls back to last in array.
884
+ */
885
+ getLastSession() {
886
+ const { sessions, wrapperLastSessionId } = this.parseSessions();
887
+ if (sessions.length === 0) {
888
+ return void 0;
889
+ }
890
+ const state = this.getState();
891
+ if (state?.lastSessionId) {
892
+ const found = sessions.find((s) => s.id === state.lastSessionId);
893
+ if (found) {
894
+ return found;
895
+ }
896
+ }
897
+ if (wrapperLastSessionId) {
898
+ const found = sessions.find((s) => s.id === wrapperLastSessionId);
899
+ if (found) {
900
+ return found;
901
+ }
902
+ }
903
+ return sessions[sessions.length - 1];
904
+ }
905
+ /**
906
+ * Returns the last N sessions, newest first.
907
+ */
908
+ getRecentSessions(count) {
909
+ return getRecentSessions(this.getSessions(), count);
910
+ }
911
+ /** Read all decisions from decisions.json. */
912
+ getDecisions() {
913
+ return this.parseDecisions().decisions;
914
+ }
915
+ /** Returns the last N decisions, newest first. */
916
+ getRecentDecisions(count) {
917
+ const all = this.getDecisions();
918
+ return all.slice(-count).reverse();
919
+ }
920
+ /** Read the multi-license store from `~/.keepgoing/license.json`. */
921
+ getLicenseStore() {
922
+ return readLicenseStore();
923
+ }
924
+ /**
925
+ * Read all current tasks from current-tasks.json.
926
+ * Automatically filters out stale finished sessions (> 2 hours).
927
+ */
928
+ getCurrentTasks() {
929
+ const multiRaw = this.readJsonFile(this.currentTasksFilePath);
930
+ if (multiRaw) {
931
+ const tasks = Array.isArray(multiRaw) ? multiRaw : multiRaw.tasks ?? [];
932
+ return this.pruneStale(tasks);
933
+ }
934
+ return [];
935
+ }
936
+ /** Get only active sessions (sessionActive=true and within stale threshold). */
937
+ getActiveTasks() {
938
+ return this.getCurrentTasks().filter((t) => t.sessionActive);
939
+ }
940
+ /** Get a specific session by ID. */
941
+ getTaskBySessionId(sessionId) {
942
+ return this.getCurrentTasks().find((t) => t.sessionId === sessionId);
943
+ }
944
+ /**
945
+ * Detect files being edited by multiple sessions simultaneously.
946
+ * Returns pairs of session IDs and the conflicting file paths.
947
+ */
948
+ detectFileConflicts() {
949
+ const activeTasks = this.getActiveTasks();
950
+ if (activeTasks.length < 2) return [];
951
+ const fileToSessions = /* @__PURE__ */ new Map();
952
+ for (const task of activeTasks) {
953
+ if (task.lastFileEdited && task.sessionId) {
954
+ const existing = fileToSessions.get(task.lastFileEdited) ?? [];
955
+ existing.push({
956
+ sessionId: task.sessionId,
957
+ agentLabel: task.agentLabel,
958
+ branch: task.branch
959
+ });
960
+ fileToSessions.set(task.lastFileEdited, existing);
961
+ }
962
+ }
963
+ const conflicts = [];
964
+ for (const [file, sessions] of fileToSessions) {
965
+ if (sessions.length > 1) {
966
+ conflicts.push({ file, sessions });
967
+ }
968
+ }
969
+ return conflicts;
970
+ }
971
+ /**
972
+ * Detect sessions on the same branch (possible duplicate work).
973
+ */
974
+ detectBranchOverlap() {
975
+ const activeTasks = this.getActiveTasks();
976
+ if (activeTasks.length < 2) return [];
977
+ const branchToSessions = /* @__PURE__ */ new Map();
978
+ for (const task of activeTasks) {
979
+ if (task.branch && task.sessionId) {
980
+ const existing = branchToSessions.get(task.branch) ?? [];
981
+ existing.push({ sessionId: task.sessionId, agentLabel: task.agentLabel });
982
+ branchToSessions.set(task.branch, existing);
983
+ }
984
+ }
985
+ const overlaps = [];
986
+ for (const [branch, sessions] of branchToSessions) {
987
+ if (sessions.length > 1) {
988
+ overlaps.push({ branch, sessions });
989
+ }
990
+ }
991
+ return overlaps;
992
+ }
993
+ pruneStale(tasks) {
994
+ return pruneStaleTasks(tasks);
995
+ }
996
+ /** Get the last session checkpoint for a specific branch. */
997
+ getLastSessionForBranch(branch) {
998
+ const sessions = this.getSessions().filter((s) => s.gitBranch === branch);
999
+ return sessions.length > 0 ? sessions[sessions.length - 1] : void 0;
1000
+ }
1001
+ /** Returns the last N sessions for a specific branch, newest first. */
1002
+ getRecentSessionsForBranch(branch, count) {
1003
+ const filtered = this.getSessions().filter((s) => s.gitBranch === branch);
1004
+ return filtered.slice(-count).reverse();
1005
+ }
1006
+ /** Returns the last N decisions for a specific branch, newest first. */
1007
+ getRecentDecisionsForBranch(branch, count) {
1008
+ const filtered = this.getDecisions().filter((d) => d.gitBranch === branch);
1009
+ return filtered.slice(-count).reverse();
1010
+ }
1011
+ /** Whether the workspace is inside a git worktree. */
1012
+ get isWorktree() {
1013
+ return this._isWorktree;
1014
+ }
1015
+ /**
1016
+ * Returns the current git branch for this workspace.
1017
+ * Lazily cached: the branch is resolved once per KeepGoingReader instance.
1018
+ */
1019
+ getCurrentBranch() {
1020
+ if (this._cachedBranch === null) {
1021
+ this._cachedBranch = getCurrentBranch(this.workspacePath);
1022
+ }
1023
+ return this._cachedBranch;
1024
+ }
1025
+ /**
1026
+ * Worktree-aware last session lookup.
1027
+ * In a worktree, scopes to the current branch with fallback to global.
1028
+ * Returns the session and whether it fell back to global.
1029
+ */
1030
+ getScopedLastSession() {
1031
+ const branch = this.getCurrentBranch();
1032
+ if (this._isWorktree && branch) {
1033
+ const scoped = this.getLastSessionForBranch(branch);
1034
+ if (scoped) return { session: scoped, isFallback: false };
1035
+ return { session: this.getLastSession(), isFallback: true };
1036
+ }
1037
+ return { session: this.getLastSession(), isFallback: false };
1038
+ }
1039
+ /** Worktree-aware recent sessions. Scopes to current branch in a worktree. */
1040
+ getScopedRecentSessions(count) {
1041
+ const branch = this.getCurrentBranch();
1042
+ if (this._isWorktree && branch) {
1043
+ return this.getRecentSessionsForBranch(branch, count);
1044
+ }
1045
+ return this.getRecentSessions(count);
1046
+ }
1047
+ /** Worktree-aware recent decisions. Scopes to current branch in a worktree. */
1048
+ getScopedRecentDecisions(count) {
1049
+ const branch = this.getCurrentBranch();
1050
+ if (this._isWorktree && branch) {
1051
+ return this.getRecentDecisionsForBranch(branch, count);
1052
+ }
1053
+ return this.getRecentDecisions(count);
1054
+ }
1055
+ /**
1056
+ * Resolves branch scope from an explicit `branch` parameter.
1057
+ * Used by tools that accept a `branch` argument (e.g. get_session_history, get_decisions).
1058
+ * - `"all"` returns no filter.
1059
+ * - An explicit branch name uses that.
1060
+ * - `undefined` auto-scopes to the current branch in a worktree, or all branches otherwise.
1061
+ */
1062
+ resolveBranchScope(branch) {
1063
+ if (branch === "all") {
1064
+ return { effectiveBranch: void 0, scopeLabel: "all branches" };
1065
+ }
1066
+ if (branch) {
1067
+ return { effectiveBranch: branch, scopeLabel: `branch \`${branch}\`` };
1068
+ }
1069
+ const currentBranch = this.getCurrentBranch();
1070
+ if (this._isWorktree && currentBranch) {
1071
+ return { effectiveBranch: currentBranch, scopeLabel: `branch \`${currentBranch}\` (worktree)` };
1072
+ }
1073
+ return { effectiveBranch: void 0, scopeLabel: "all branches" };
1074
+ }
1075
+ /**
1076
+ * Parses sessions.json once, returning both the session list
1077
+ * and the optional lastSessionId from a ProjectSessions wrapper.
1078
+ */
1079
+ parseSessions() {
1080
+ const raw = this.readJsonFile(
1081
+ this.sessionsFilePath
1082
+ );
1083
+ if (!raw) {
1084
+ return { sessions: [] };
1085
+ }
1086
+ if (Array.isArray(raw)) {
1087
+ return { sessions: raw };
1088
+ }
1089
+ return { sessions: raw.sessions ?? [], wrapperLastSessionId: raw.lastSessionId };
1090
+ }
1091
+ parseDecisions() {
1092
+ const raw = this.readJsonFile(this.decisionsFilePath);
1093
+ if (!raw) {
1094
+ return { decisions: [] };
1095
+ }
1096
+ return { decisions: raw.decisions ?? [], lastDecisionId: raw.lastDecisionId };
1097
+ }
1098
+ readJsonFile(filePath) {
1099
+ try {
1100
+ if (!fs4.existsSync(filePath)) {
1101
+ return void 0;
1102
+ }
1103
+ const raw = fs4.readFileSync(filePath, "utf-8");
1104
+ return JSON.parse(raw);
1105
+ } catch {
1106
+ return void 0;
1107
+ }
1108
+ }
1109
+ };
1110
+
1111
+ // ../../packages/shared/src/setup.ts
1112
+ import fs5 from "fs";
1113
+ import os2 from "os";
1114
+ import path6 from "path";
1115
+ var KEEPGOING_MARKER = "@keepgoingdev/mcp-server";
1116
+ var SESSION_START_HOOK = {
1117
+ matcher: "",
1118
+ hooks: [
1119
+ {
1120
+ type: "command",
1121
+ command: "npx -y @keepgoingdev/mcp-server --print-momentum"
1122
+ }
1123
+ ]
1124
+ };
1125
+ var STOP_HOOK = {
1126
+ matcher: "",
1127
+ hooks: [
1128
+ {
1129
+ type: "command",
1130
+ command: "npx -y @keepgoingdev/mcp-server --save-checkpoint"
1131
+ }
1132
+ ]
1133
+ };
1134
+ var POST_TOOL_USE_HOOK = {
1135
+ matcher: "Edit|Write|MultiEdit",
1136
+ hooks: [
1137
+ {
1138
+ type: "command",
1139
+ command: "npx -y @keepgoingdev/mcp-server --update-task-from-hook"
1140
+ }
1141
+ ]
1142
+ };
1143
+ var CLAUDE_MD_SECTION = `
1144
+ ## KeepGoing
1145
+
1146
+ After completing a task or meaningful piece of work, call the \`save_checkpoint\` MCP tool with:
1147
+ - \`summary\`: What you accomplished
1148
+ - \`nextStep\`: What should be done next
1149
+ - \`blocker\`: Any blocker (if applicable)
1150
+ `;
1151
+ var STATUSLINE_CMD = "npx -y @keepgoingdev/mcp-server --statusline";
1152
+ function hasKeepGoingHook(hookEntries) {
1153
+ return hookEntries.some(
1154
+ (entry) => entry?.hooks?.some((h) => typeof h?.command === "string" && h.command.includes(KEEPGOING_MARKER))
1155
+ );
1156
+ }
1157
+ function resolveScopePaths(scope, workspacePath) {
1158
+ if (scope === "user") {
1159
+ const claudeDir2 = path6.join(os2.homedir(), ".claude");
1160
+ return {
1161
+ claudeDir: claudeDir2,
1162
+ settingsPath: path6.join(claudeDir2, "settings.json"),
1163
+ claudeMdPath: path6.join(claudeDir2, "CLAUDE.md")
1164
+ };
1165
+ }
1166
+ const claudeDir = path6.join(workspacePath, ".claude");
1167
+ const dotClaudeMdPath = path6.join(workspacePath, ".claude", "CLAUDE.md");
1168
+ const rootClaudeMdPath = path6.join(workspacePath, "CLAUDE.md");
1169
+ return {
1170
+ claudeDir,
1171
+ settingsPath: path6.join(claudeDir, "settings.json"),
1172
+ claudeMdPath: fs5.existsSync(dotClaudeMdPath) ? dotClaudeMdPath : rootClaudeMdPath
1173
+ };
1174
+ }
1175
+ function writeHooksToSettings(settings) {
1176
+ let changed = false;
1177
+ if (!settings.hooks) {
1178
+ settings.hooks = {};
1179
+ }
1180
+ if (!Array.isArray(settings.hooks.SessionStart)) {
1181
+ settings.hooks.SessionStart = [];
1182
+ }
1183
+ if (!hasKeepGoingHook(settings.hooks.SessionStart)) {
1184
+ settings.hooks.SessionStart.push(SESSION_START_HOOK);
1185
+ changed = true;
1186
+ }
1187
+ if (!Array.isArray(settings.hooks.Stop)) {
1188
+ settings.hooks.Stop = [];
1189
+ }
1190
+ if (!hasKeepGoingHook(settings.hooks.Stop)) {
1191
+ settings.hooks.Stop.push(STOP_HOOK);
1192
+ changed = true;
1193
+ }
1194
+ if (!Array.isArray(settings.hooks.PostToolUse)) {
1195
+ settings.hooks.PostToolUse = [];
1196
+ }
1197
+ if (!hasKeepGoingHook(settings.hooks.PostToolUse)) {
1198
+ settings.hooks.PostToolUse.push(POST_TOOL_USE_HOOK);
1199
+ changed = true;
1200
+ }
1201
+ return changed;
1202
+ }
1203
+ function checkHookConflict(scope, workspacePath) {
1204
+ const otherPaths = resolveScopePaths(scope === "user" ? "project" : "user", workspacePath);
1205
+ if (!fs5.existsSync(otherPaths.settingsPath)) {
1206
+ return null;
1207
+ }
1208
+ try {
1209
+ const otherSettings = JSON.parse(fs5.readFileSync(otherPaths.settingsPath, "utf-8"));
1210
+ const hooks = otherSettings?.hooks;
1211
+ if (!hooks) return null;
1212
+ const hasConflict = Array.isArray(hooks.SessionStart) && hasKeepGoingHook(hooks.SessionStart) || Array.isArray(hooks.Stop) && hasKeepGoingHook(hooks.Stop);
1213
+ if (hasConflict) {
1214
+ const otherScope = scope === "user" ? "project" : "user";
1215
+ const otherFile = otherPaths.settingsPath;
1216
+ return `KeepGoing hooks are also configured at ${otherScope} scope (${otherFile}). Having hooks at both scopes may cause them to fire twice. Consider removing the ${otherScope}-level hooks if you want to use ${scope}-level only.`;
1217
+ }
1218
+ } catch {
1219
+ }
1220
+ return null;
1221
+ }
1222
+ function setupProject(options) {
1223
+ const {
1224
+ workspacePath,
1225
+ scope = "project",
1226
+ sessionHooks = true,
1227
+ claudeMd = true,
1228
+ hasProLicense = false,
1229
+ statusline
1230
+ } = options;
1231
+ const messages = [];
1232
+ let changed = false;
1233
+ const { claudeDir, settingsPath, claudeMdPath } = resolveScopePaths(scope, workspacePath);
1234
+ const scopeLabel = scope === "user" ? "~/.claude/settings.json" : ".claude/settings.json";
1235
+ let settings = {};
1236
+ if (fs5.existsSync(settingsPath)) {
1237
+ settings = JSON.parse(fs5.readFileSync(settingsPath, "utf-8"));
1238
+ }
1239
+ let settingsChanged = false;
1240
+ if (sessionHooks) {
1241
+ const hooksChanged = writeHooksToSettings(settings);
1242
+ settingsChanged = hooksChanged;
1243
+ if (hooksChanged) {
1244
+ messages.push(`Session hooks: Added to ${scopeLabel}`);
1245
+ } else {
1246
+ messages.push("Session hooks: Already present, skipped");
1247
+ }
1248
+ const conflict = checkHookConflict(scope, workspacePath);
1249
+ if (conflict) {
1250
+ messages.push(`Warning: ${conflict}`);
1251
+ }
1252
+ }
1253
+ if (scope === "project" && hasProLicense) {
1254
+ const needsUpdate = settings.statusLine?.command && statusline?.isLegacy?.(settings.statusLine.command);
1255
+ if (!settings.statusLine || needsUpdate) {
1256
+ settings.statusLine = {
1257
+ type: "command",
1258
+ command: STATUSLINE_CMD
1259
+ };
1260
+ settingsChanged = true;
1261
+ messages.push(needsUpdate ? "Statusline: Migrated to auto-updating npx command" : "Statusline: Added to .claude/settings.json");
1262
+ } else {
1263
+ messages.push("Statusline: Already configured in settings, skipped");
1264
+ }
1265
+ statusline?.cleanup?.();
1266
+ }
1267
+ if (settingsChanged) {
1268
+ if (!fs5.existsSync(claudeDir)) {
1269
+ fs5.mkdirSync(claudeDir, { recursive: true });
1270
+ }
1271
+ fs5.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
1272
+ changed = true;
1273
+ }
1274
+ if (claudeMd) {
1275
+ let existing = "";
1276
+ if (fs5.existsSync(claudeMdPath)) {
1277
+ existing = fs5.readFileSync(claudeMdPath, "utf-8");
1278
+ }
1279
+ const mdLabel = scope === "user" ? "~/.claude/CLAUDE.md" : "CLAUDE.md";
1280
+ if (existing.includes("## KeepGoing")) {
1281
+ messages.push(`CLAUDE.md: KeepGoing section already present in ${mdLabel}, skipped`);
1282
+ } else {
1283
+ const updated = existing + CLAUDE_MD_SECTION;
1284
+ const mdDir = path6.dirname(claudeMdPath);
1285
+ if (!fs5.existsSync(mdDir)) {
1286
+ fs5.mkdirSync(mdDir, { recursive: true });
1287
+ }
1288
+ fs5.writeFileSync(claudeMdPath, updated);
1289
+ changed = true;
1290
+ messages.push(`CLAUDE.md: Added KeepGoing section to ${mdLabel}`);
1291
+ }
1292
+ }
1293
+ return { messages, changed };
1294
+ }
1295
+
428
1296
  // ../../packages/shared/src/licenseClient.ts
429
1297
  var BASE_URL = "https://api.lemonsqueezy.com/v1/licenses";
430
1298
  var REQUEST_TIMEOUT_MS = 15e3;
@@ -514,78 +1382,14 @@ async function deactivateLicense(licenseKey, instanceId) {
514
1382
  }
515
1383
  }
516
1384
 
517
- // src/storage.ts
518
- import fs4 from "fs";
519
- import path5 from "path";
520
- var STORAGE_DIR2 = ".keepgoing";
521
- var META_FILE2 = "meta.json";
522
- var SESSIONS_FILE2 = "sessions.json";
523
- var STATE_FILE2 = "state.json";
524
- var KeepGoingReader = class {
525
- storagePath;
526
- metaFilePath;
527
- sessionsFilePath;
528
- stateFilePath;
529
- constructor(workspacePath) {
530
- this.storagePath = path5.join(workspacePath, STORAGE_DIR2);
531
- this.metaFilePath = path5.join(this.storagePath, META_FILE2);
532
- this.sessionsFilePath = path5.join(this.storagePath, SESSIONS_FILE2);
533
- this.stateFilePath = path5.join(this.storagePath, STATE_FILE2);
534
- }
535
- exists() {
536
- return fs4.existsSync(this.storagePath);
537
- }
538
- getState() {
539
- return this.readJsonFile(this.stateFilePath);
540
- }
541
- getMeta() {
542
- return this.readJsonFile(this.metaFilePath);
543
- }
544
- getSessions() {
545
- return this.parseSessions().sessions;
546
- }
547
- getLastSession() {
548
- const { sessions, wrapperLastSessionId } = this.parseSessions();
549
- if (sessions.length === 0) {
550
- return void 0;
551
- }
552
- const state = this.getState();
553
- if (state?.lastSessionId) {
554
- const found = sessions.find((s) => s.id === state.lastSessionId);
555
- if (found) return found;
556
- }
557
- if (wrapperLastSessionId) {
558
- const found = sessions.find((s) => s.id === wrapperLastSessionId);
559
- if (found) return found;
560
- }
561
- return sessions[sessions.length - 1];
562
- }
563
- getRecentSessions(count) {
564
- return getRecentSessions(this.getSessions(), count);
565
- }
566
- parseSessions() {
567
- const raw = this.readJsonFile(this.sessionsFilePath);
568
- if (!raw) return { sessions: [] };
569
- if (Array.isArray(raw)) return { sessions: raw };
570
- return { sessions: raw.sessions ?? [], wrapperLastSessionId: raw.lastSessionId };
571
- }
572
- readJsonFile(filePath) {
573
- try {
574
- if (!fs4.existsSync(filePath)) return void 0;
575
- const raw = fs4.readFileSync(filePath, "utf-8");
576
- return JSON.parse(raw);
577
- } catch {
578
- return void 0;
579
- }
580
- }
581
- };
582
-
583
1385
  // src/render.ts
584
1386
  var RESET = "\x1B[0m";
585
1387
  var BOLD = "\x1B[1m";
586
1388
  var DIM = "\x1B[2m";
1389
+ var GREEN = "\x1B[32m";
587
1390
  var YELLOW = "\x1B[33m";
588
1391
  var CYAN = "\x1B[36m";
1392
+ var MAGENTA = "\x1B[35m";
589
1393
  function renderCheckpoint(checkpoint, daysSince) {
590
1394
  const relTime = formatRelativeTime(checkpoint.timestamp);
591
1395
  if (daysSince !== void 0 && daysSince >= 7) {
@@ -598,46 +1402,218 @@ ${BOLD}KeepGoing${RESET} \xB7 ${DIM}${relTime}${RESET}
598
1402
  if (checkpoint.summary) {
599
1403
  console.log(` ${label("Summary:")} ${checkpoint.summary}`);
600
1404
  }
601
- if (checkpoint.nextStep) {
602
- console.log(` ${label("Next step:")} ${checkpoint.nextStep}`);
1405
+ if (checkpoint.nextStep) {
1406
+ console.log(` ${label("Next step:")} ${checkpoint.nextStep}`);
1407
+ }
1408
+ if (checkpoint.blocker) {
1409
+ console.log(` ${label("Blocker:")} ${checkpoint.blocker}`);
1410
+ }
1411
+ if (checkpoint.gitBranch) {
1412
+ console.log(` ${label("Branch:")} ${checkpoint.gitBranch}`);
1413
+ }
1414
+ if (checkpoint.touchedFiles && checkpoint.touchedFiles.length > 0) {
1415
+ const MAX_FILES = 3;
1416
+ const shown = checkpoint.touchedFiles.slice(0, MAX_FILES).join(", ");
1417
+ const extra = checkpoint.touchedFiles.length - MAX_FILES;
1418
+ const filesStr = extra > 0 ? `${shown} (+${extra} more)` : shown;
1419
+ console.log(` ${label("Files:")} ${filesStr}`);
1420
+ }
1421
+ console.log("");
1422
+ }
1423
+ function renderQuiet(checkpoint) {
1424
+ const relTime = formatRelativeTime(checkpoint.timestamp);
1425
+ const summary = checkpoint.summary || checkpoint.nextStep || "checkpoint saved";
1426
+ console.log(`KeepGoing \xB7 ${relTime} \xB7 ${summary}`);
1427
+ }
1428
+ function renderNoData() {
1429
+ console.log(
1430
+ `No KeepGoing data found. Run ${BOLD}keepgoing save${RESET} to save your first checkpoint.`
1431
+ );
1432
+ }
1433
+ function renderBriefing(briefing, decisions) {
1434
+ const label = (s) => `${CYAN}${s}${RESET}`;
1435
+ console.log(`
1436
+ ${BOLD}KeepGoing Re-entry Briefing${RESET}
1437
+ `);
1438
+ console.log(` ${label("Last worked:")} ${briefing.lastWorked}`);
1439
+ console.log(` ${label("Focus:")} ${briefing.currentFocus}`);
1440
+ console.log(` ${label("Activity:")} ${briefing.recentActivity}`);
1441
+ console.log(` ${label("Next:")} ${briefing.suggestedNext}`);
1442
+ console.log(` ${label("Quick start:")} ${briefing.smallNextStep}`);
1443
+ if (decisions && decisions.length > 0) {
1444
+ console.log(`
1445
+ ${label("Recent decisions:")}`);
1446
+ for (const d of decisions) {
1447
+ const relTime = formatRelativeTime(d.timestamp);
1448
+ console.log(` ${d.classification.category}: ${d.commitMessage} ${DIM}(${relTime})${RESET}`);
1449
+ }
1450
+ }
1451
+ console.log("");
1452
+ }
1453
+ function renderBriefingQuiet(briefing) {
1454
+ console.log(`KeepGoing \xB7 ${briefing.lastWorked} \xB7 Focus: ${briefing.currentFocus} \xB7 Next: ${briefing.suggestedNext}`);
1455
+ }
1456
+ function renderMomentum(checkpoint, ctx) {
1457
+ const relTime = formatRelativeTime(checkpoint.timestamp);
1458
+ const label = (s) => `${CYAN}${s}${RESET}`;
1459
+ console.log(`
1460
+ ${BOLD}KeepGoing Momentum${RESET} \xB7 ${DIM}${relTime}${RESET}
1461
+ `);
1462
+ if (ctx.isWorktree && ctx.currentBranch) {
1463
+ console.log(` ${DIM}Worktree: scoped to ${ctx.currentBranch}${RESET}`);
1464
+ if (ctx.isFallback) {
1465
+ console.log(` ${YELLOW}No checkpoints for this branch, showing last global checkpoint${RESET}`);
1466
+ }
1467
+ console.log("");
1468
+ }
1469
+ if (checkpoint.summary) {
1470
+ console.log(` ${label("Summary:")} ${checkpoint.summary}`);
1471
+ }
1472
+ if (checkpoint.nextStep) {
1473
+ console.log(` ${label("Next step:")} ${checkpoint.nextStep}`);
1474
+ }
1475
+ if (checkpoint.blocker) {
1476
+ console.log(` ${label("Blocker:")} ${YELLOW}${checkpoint.blocker}${RESET}`);
1477
+ }
1478
+ if (checkpoint.projectIntent) {
1479
+ console.log(` ${label("Intent:")} ${checkpoint.projectIntent}`);
1480
+ }
1481
+ if (ctx.currentBranch) {
1482
+ console.log(` ${label("Branch:")} ${ctx.currentBranch}`);
1483
+ }
1484
+ if (ctx.branchChanged && !ctx.isWorktree) {
1485
+ console.log(` ${YELLOW}\u26A0 Branch changed since last checkpoint (was ${checkpoint.gitBranch})${RESET}`);
1486
+ }
1487
+ if (checkpoint.touchedFiles && checkpoint.touchedFiles.length > 0) {
1488
+ const MAX_FILES = 5;
1489
+ const shown = checkpoint.touchedFiles.slice(0, MAX_FILES).join(", ");
1490
+ const extra = checkpoint.touchedFiles.length - MAX_FILES;
1491
+ const filesStr = extra > 0 ? `${shown} (+${extra} more)` : shown;
1492
+ console.log(` ${label("Files:")} ${filesStr}`);
1493
+ }
1494
+ if (ctx.derivedFocus) {
1495
+ console.log(` ${label("Focus:")} ${ctx.derivedFocus}`);
1496
+ }
1497
+ console.log("");
1498
+ }
1499
+ function renderMomentumQuiet(checkpoint) {
1500
+ const relTime = formatRelativeTime(checkpoint.timestamp);
1501
+ const summary = checkpoint.summary || checkpoint.nextStep || "no momentum data";
1502
+ console.log(`KeepGoing \xB7 ${relTime} \xB7 ${summary}`);
1503
+ }
1504
+ function renderSaveConfirmation(summary, fileCount, branch) {
1505
+ const parts = [];
1506
+ if (fileCount > 0) parts.push(`${fileCount} file${fileCount === 1 ? "" : "s"}`);
1507
+ if (branch) parts.push(branch);
1508
+ const meta = parts.length > 0 ? ` ${DIM}(${parts.join(", ")})${RESET}` : "";
1509
+ console.log(`${GREEN}\u2714 Saved:${RESET} ${summary}${meta}`);
1510
+ }
1511
+ function renderDecisions(decisions, scopeLabel) {
1512
+ const label = (s) => `${CYAN}${s}${RESET}`;
1513
+ console.log(`
1514
+ ${BOLD}KeepGoing Decisions${RESET} ${DIM}(last ${decisions.length}, ${scopeLabel})${RESET}
1515
+ `);
1516
+ for (const d of decisions) {
1517
+ const relTime = formatRelativeTime(d.timestamp);
1518
+ const confidence = `${(d.classification.confidence * 100).toFixed(0)}%`;
1519
+ console.log(` ${label(d.classification.category + ":")} ${d.commitMessage} ${DIM}${confidence} \xB7 ${relTime}${RESET}`);
1520
+ if (d.classification.reasons.length > 0) {
1521
+ console.log(` ${DIM}Signals: ${d.classification.reasons.join("; ")}${RESET}`);
1522
+ }
1523
+ }
1524
+ console.log("");
1525
+ }
1526
+ function renderDecisionsQuiet(decisions) {
1527
+ const latest = decisions[0];
1528
+ if (!latest) return;
1529
+ const relTime = formatRelativeTime(latest.timestamp);
1530
+ console.log(`KeepGoing \xB7 ${decisions.length} decision${decisions.length === 1 ? "" : "s"} \xB7 latest: ${latest.classification.category}: ${latest.commitMessage} (${relTime})`);
1531
+ }
1532
+ function formatDuration(minutes) {
1533
+ if (minutes < 60) return `${minutes}m`;
1534
+ const h = Math.floor(minutes / 60);
1535
+ const m = minutes % 60;
1536
+ return m > 0 ? `${h}h ${m}m` : `${h}h`;
1537
+ }
1538
+ function renderLogSession(session, showStat) {
1539
+ const relTime = formatRelativeTime(session.timestamp);
1540
+ const branch = session.gitBranch ? ` ${GREEN}(${session.gitBranch})${RESET}` : "";
1541
+ const source = session.source ? ` ${DIM}[${session.source}]${RESET}` : "";
1542
+ console.log(`${BOLD}\u25CF${RESET} ${DIM}${relTime}${RESET}${branch}${source}`);
1543
+ if (session.summary) {
1544
+ console.log(` ${session.summary}`);
1545
+ }
1546
+ if (session.nextStep) {
1547
+ console.log(` ${CYAN}\u2192 Next:${RESET} ${session.nextStep}`);
603
1548
  }
604
- if (checkpoint.blocker) {
605
- console.log(` ${label("Blocker:")} ${checkpoint.blocker}`);
1549
+ const parts = [];
1550
+ if (session.sessionDuration) {
1551
+ parts.push(`\u23F1 ${formatDuration(session.sessionDuration)}`);
606
1552
  }
607
- if (checkpoint.gitBranch) {
608
- console.log(` ${label("Branch:")} ${checkpoint.gitBranch}`);
1553
+ if (session.touchedFiles && session.touchedFiles.length > 0) {
1554
+ parts.push(`${session.touchedFiles.length} file${session.touchedFiles.length !== 1 ? "s" : ""}`);
609
1555
  }
610
- if (checkpoint.touchedFiles && checkpoint.touchedFiles.length > 0) {
611
- const MAX_FILES = 3;
612
- const shown = checkpoint.touchedFiles.slice(0, MAX_FILES).join(", ");
613
- const extra = checkpoint.touchedFiles.length - MAX_FILES;
614
- const filesStr = extra > 0 ? `${shown} (+${extra} more)` : shown;
615
- console.log(` ${label("Files:")} ${filesStr}`);
1556
+ if (parts.length > 0) {
1557
+ console.log(` ${DIM}${parts.join(" \xB7 ")}${RESET}`);
1558
+ }
1559
+ if (session.blocker) {
1560
+ console.log(` ${YELLOW}\u26A0 Blocker: ${session.blocker}${RESET}`);
1561
+ }
1562
+ if (showStat && session.touchedFiles && session.touchedFiles.length > 0) {
1563
+ for (const f of session.touchedFiles) {
1564
+ console.log(` ${DIM}${f}${RESET}`);
1565
+ }
616
1566
  }
617
- console.log("");
618
1567
  }
619
- function renderQuiet(checkpoint) {
620
- const relTime = formatRelativeTime(checkpoint.timestamp);
621
- const summary = checkpoint.summary || checkpoint.nextStep || "checkpoint saved";
622
- console.log(`KeepGoing \xB7 ${relTime} \xB7 ${summary}`);
1568
+ function renderLogSessionOneline(session) {
1569
+ const id = session.id.slice(0, 7);
1570
+ const relTime = formatRelativeTime(session.timestamp);
1571
+ const branch = session.gitBranch ? ` ${GREEN}(${session.gitBranch})${RESET}` : "";
1572
+ const summary = session.summary || session.nextStep || "checkpoint";
1573
+ console.log(`${DIM}${id}${RESET} ${relTime}${branch} ${summary}`);
623
1574
  }
624
- function renderNoData() {
625
- console.log(
626
- `No KeepGoing data found. Run ${BOLD}keepgoing save${RESET} to save your first checkpoint.`
627
- );
1575
+ function renderLogDecision(decision, showStat) {
1576
+ const relTime = formatRelativeTime(decision.timestamp);
1577
+ const branch = decision.gitBranch ? ` ${GREEN}(${decision.gitBranch})${RESET}` : "";
1578
+ const cat = decision.classification.category;
1579
+ const conf = Math.round(decision.classification.confidence * 100);
1580
+ const tag = `${MAGENTA}[${cat} \xB7 ${conf}%]${RESET}`;
1581
+ console.log(`${BOLD}\u25C6${RESET} ${DIM}${relTime}${RESET}${branch} ${tag}`);
1582
+ console.log(` ${decision.commitMessage}`);
1583
+ if (decision.classification.reasons.length > 0) {
1584
+ console.log(` ${DIM}Signals: ${decision.classification.reasons.join("; ")}${RESET}`);
1585
+ }
1586
+ if (decision.filesChanged.length > 0) {
1587
+ console.log(` ${DIM}${decision.filesChanged.length} file${decision.filesChanged.length !== 1 ? "s" : ""} changed${RESET}`);
1588
+ }
1589
+ if (showStat && decision.filesChanged.length > 0) {
1590
+ for (const f of decision.filesChanged) {
1591
+ console.log(` ${DIM}${f}${RESET}`);
1592
+ }
1593
+ }
1594
+ }
1595
+ function renderLogDecisionOneline(decision) {
1596
+ const id = decision.id.slice(0, 7);
1597
+ const relTime = formatRelativeTime(decision.timestamp);
1598
+ const cat = decision.classification.category;
1599
+ console.log(`${DIM}${id}${RESET} ${relTime} ${MAGENTA}[${cat}]${RESET} ${decision.commitMessage}`);
1600
+ }
1601
+ function renderSessionGroupHeader(sessionId, count) {
1602
+ const shortId = sessionId.slice(0, 8);
1603
+ console.log(`${BOLD}Session ${shortId}${RESET} ${DIM}(${count} checkpoint${count !== 1 ? "s" : ""})${RESET}`);
628
1604
  }
629
1605
 
630
1606
  // src/updateCheck.ts
631
1607
  import { spawn } from "child_process";
632
1608
  import { readFileSync, existsSync } from "fs";
633
- import path6 from "path";
634
- import os2 from "os";
635
- var CLI_VERSION = "0.3.3";
1609
+ import path7 from "path";
1610
+ import os3 from "os";
1611
+ var CLI_VERSION = "1.0.0";
636
1612
  var NPM_REGISTRY_URL = "https://registry.npmjs.org/@keepgoingdev/cli/latest";
637
1613
  var FETCH_TIMEOUT_MS = 5e3;
638
1614
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
639
- var CACHE_DIR = path6.join(os2.homedir(), ".keepgoing");
640
- var CACHE_PATH = path6.join(CACHE_DIR, "update-check.json");
1615
+ var CACHE_DIR = path7.join(os3.homedir(), ".keepgoing");
1616
+ var CACHE_PATH = path7.join(CACHE_DIR, "update-check.json");
641
1617
  function isNewerVersion(current, latest) {
642
1618
  const cur = current.split(".").map(Number);
643
1619
  const lat = latest.split(".").map(Number);
@@ -752,61 +1728,73 @@ async function statusCommand(opts) {
752
1728
  }
753
1729
 
754
1730
  // src/commands/save.ts
755
- import readline from "readline";
756
- import path7 from "path";
757
- function prompt(rl, question) {
758
- return new Promise((resolve) => {
759
- rl.question(question, (answer) => {
760
- resolve(answer.trim());
761
- });
762
- });
763
- }
1731
+ import path8 from "path";
764
1732
  async function saveCommand(opts) {
765
- const rl = readline.createInterface({
766
- input: process.stdin,
767
- output: process.stdout
768
- });
769
- let summary = "";
770
- let nextStep = "";
771
- let blocker = "";
772
- try {
773
- while (!summary) {
774
- summary = await prompt(rl, "What did you work on? ");
775
- if (!summary) {
776
- console.log(" (This field is required)");
777
- }
778
- }
779
- while (!nextStep) {
780
- nextStep = await prompt(rl, "What's your next step? ");
781
- if (!nextStep) {
782
- console.log(" (This field is required)");
783
- }
1733
+ const { cwd, message, nextStepOverride, json, quiet, force } = opts;
1734
+ const isManual = !!message;
1735
+ const reader = new KeepGoingReader(cwd);
1736
+ const { session: lastSession } = reader.getScopedLastSession();
1737
+ if (!force && !isManual && lastSession?.timestamp) {
1738
+ const ageMs = Date.now() - new Date(lastSession.timestamp).getTime();
1739
+ if (ageMs < 2 * 60 * 1e3) {
1740
+ return;
784
1741
  }
785
- blocker = await prompt(rl, "Any blockers? (leave empty to skip) ");
786
- } finally {
787
- rl.close();
788
1742
  }
789
- const gitBranch = getCurrentBranch(opts.cwd);
790
- const touchedFiles = getTouchedFiles(opts.cwd);
1743
+ const touchedFiles = getTouchedFiles(cwd);
1744
+ const commitHashes = getCommitsSince(cwd, lastSession?.timestamp);
1745
+ if (!force && !isManual && touchedFiles.length === 0 && commitHashes.length === 0) {
1746
+ return;
1747
+ }
1748
+ const gitBranch = getCurrentBranch(cwd);
1749
+ const commitMessages = getCommitMessagesSince(cwd, lastSession?.timestamp);
1750
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1751
+ const events = buildSessionEvents({
1752
+ wsPath: cwd,
1753
+ commitHashes,
1754
+ commitMessages,
1755
+ touchedFiles,
1756
+ currentBranch: gitBranch ?? void 0,
1757
+ sessionStartTime: lastSession?.timestamp ?? now,
1758
+ lastActivityTime: now
1759
+ });
1760
+ const summary = message ?? buildSmartSummary(events) ?? `Worked on ${touchedFiles.slice(0, 5).map((f) => path8.basename(f)).join(", ")}`;
1761
+ const nextStep = nextStepOverride ?? buildSmartNextStep(events);
1762
+ const projectName = path8.basename(resolveStorageRoot(cwd));
1763
+ const sessionId = generateSessionId({
1764
+ workspaceRoot: cwd,
1765
+ branch: gitBranch ?? void 0,
1766
+ worktreePath: cwd
1767
+ });
791
1768
  const checkpoint = createCheckpoint({
792
1769
  summary,
793
1770
  nextStep,
794
- blocker: blocker || void 0,
795
1771
  gitBranch,
796
1772
  touchedFiles,
797
- workspaceRoot: opts.cwd,
798
- source: "manual"
1773
+ commitHashes,
1774
+ workspaceRoot: cwd,
1775
+ source: isManual ? "manual" : "auto",
1776
+ sessionId
799
1777
  });
800
- const projectName = path7.basename(opts.cwd);
801
- const writer = new KeepGoingWriter(opts.cwd);
1778
+ const writer = new KeepGoingWriter(cwd);
802
1779
  writer.saveCheckpoint(checkpoint, projectName);
803
- console.log("Checkpoint saved.");
1780
+ writer.upsertSession({
1781
+ sessionId,
1782
+ sessionActive: false,
1783
+ nextStep: checkpoint.nextStep || void 0,
1784
+ branch: gitBranch ?? void 0,
1785
+ updatedAt: checkpoint.timestamp
1786
+ });
1787
+ if (json) {
1788
+ console.log(JSON.stringify(checkpoint, null, 2));
1789
+ } else if (!quiet) {
1790
+ renderSaveConfirmation(summary, touchedFiles.length, gitBranch ?? void 0);
1791
+ }
804
1792
  }
805
1793
 
806
1794
  // src/commands/hook.ts
807
- import fs5 from "fs";
808
- import path8 from "path";
809
- import os3 from "os";
1795
+ import fs6 from "fs";
1796
+ import path9 from "path";
1797
+ import os4 from "os";
810
1798
  import { execSync } from "child_process";
811
1799
  var HOOK_MARKER_START = "# keepgoing-hook-start";
812
1800
  var HOOK_MARKER_END = "# keepgoing-hook-end";
@@ -842,7 +1830,7 @@ if command -v keepgoing >/dev/null 2>&1
842
1830
  end
843
1831
  ${HOOK_MARKER_END}`;
844
1832
  function detectShellRcFile(shellOverride) {
845
- const home = os3.homedir();
1833
+ const home = os4.homedir();
846
1834
  let shell;
847
1835
  if (shellOverride) {
848
1836
  shell = shellOverride.toLowerCase();
@@ -865,14 +1853,14 @@ function detectShellRcFile(shellOverride) {
865
1853
  }
866
1854
  }
867
1855
  if (shell === "zsh") {
868
- return { shell: "zsh", rcFile: path8.join(home, ".zshrc") };
1856
+ return { shell: "zsh", rcFile: path9.join(home, ".zshrc") };
869
1857
  }
870
1858
  if (shell === "bash") {
871
- return { shell: "bash", rcFile: path8.join(home, ".bashrc") };
1859
+ return { shell: "bash", rcFile: path9.join(home, ".bashrc") };
872
1860
  }
873
1861
  if (shell === "fish") {
874
- const xdgConfig = process.env["XDG_CONFIG_HOME"] || path8.join(home, ".config");
875
- return { shell: "fish", rcFile: path8.join(xdgConfig, "fish", "config.fish") };
1862
+ const xdgConfig = process.env["XDG_CONFIG_HOME"] || path9.join(home, ".config");
1863
+ return { shell: "fish", rcFile: path9.join(xdgConfig, "fish", "config.fish") };
876
1864
  }
877
1865
  return void 0;
878
1866
  }
@@ -888,14 +1876,14 @@ function hookInstallCommand(shellOverride) {
888
1876
  const hookBlock = shell === "zsh" ? ZSH_HOOK : shell === "fish" ? FISH_HOOK : BASH_HOOK;
889
1877
  let existing = "";
890
1878
  try {
891
- existing = fs5.readFileSync(rcFile, "utf-8");
1879
+ existing = fs6.readFileSync(rcFile, "utf-8");
892
1880
  } catch {
893
1881
  }
894
1882
  if (existing.includes(HOOK_MARKER_START)) {
895
1883
  console.log(`KeepGoing hook is already installed in ${rcFile}.`);
896
1884
  return;
897
1885
  }
898
- fs5.appendFileSync(rcFile, `
1886
+ fs6.appendFileSync(rcFile, `
899
1887
  ${hookBlock}
900
1888
  `, "utf-8");
901
1889
  console.log(`KeepGoing hook installed in ${rcFile}.`);
@@ -914,7 +1902,7 @@ function hookUninstallCommand(shellOverride) {
914
1902
  const { rcFile } = detected;
915
1903
  let existing = "";
916
1904
  try {
917
- existing = fs5.readFileSync(rcFile, "utf-8");
1905
+ existing = fs6.readFileSync(rcFile, "utf-8");
918
1906
  } catch {
919
1907
  console.log(`${rcFile} not found \u2014 nothing to remove.`);
920
1908
  return;
@@ -930,7 +1918,7 @@ function hookUninstallCommand(shellOverride) {
930
1918
  "g"
931
1919
  );
932
1920
  const updated = existing.replace(pattern, "").replace(/\n{3,}/g, "\n\n");
933
- fs5.writeFileSync(rcFile, updated, "utf-8");
1921
+ fs6.writeFileSync(rcFile, updated, "utf-8");
934
1922
  console.log(`KeepGoing hook removed from ${rcFile}.`);
935
1923
  console.log(`Reload your shell config to deactivate it:
936
1924
  `);
@@ -1027,51 +2015,656 @@ async function deactivateCommand(opts) {
1027
2015
  }
1028
2016
  }
1029
2017
 
2018
+ // src/commands/briefing.ts
2019
+ async function briefingCommand(opts) {
2020
+ const reader = new KeepGoingReader(opts.cwd);
2021
+ if (!reader.exists()) {
2022
+ if (!opts.quiet) {
2023
+ renderNoData();
2024
+ }
2025
+ return;
2026
+ }
2027
+ const gitBranch = reader.getCurrentBranch();
2028
+ const { session: lastSession } = reader.getScopedLastSession();
2029
+ const recentSessions = reader.getScopedRecentSessions(5);
2030
+ const state = reader.getState() ?? {};
2031
+ const sinceTimestamp = lastSession?.timestamp;
2032
+ const recentCommits = sinceTimestamp ? getCommitMessagesSince(opts.cwd, sinceTimestamp) : [];
2033
+ const briefing = generateBriefing(
2034
+ lastSession,
2035
+ recentSessions,
2036
+ state,
2037
+ gitBranch,
2038
+ recentCommits
2039
+ );
2040
+ if (!briefing) {
2041
+ if (!opts.quiet) {
2042
+ console.log("No session data available to generate a briefing.");
2043
+ }
2044
+ return;
2045
+ }
2046
+ const decisions = reader.getScopedRecentDecisions(3);
2047
+ if (opts.json) {
2048
+ const output = decisions.length > 0 ? { ...briefing, decisions } : briefing;
2049
+ console.log(JSON.stringify(output, null, 2));
2050
+ return;
2051
+ }
2052
+ if (opts.quiet) {
2053
+ renderBriefingQuiet(briefing);
2054
+ return;
2055
+ }
2056
+ renderBriefing(briefing, decisions);
2057
+ }
2058
+
2059
+ // src/commands/init.ts
2060
+ var RESET3 = "\x1B[0m";
2061
+ var BOLD3 = "\x1B[1m";
2062
+ var GREEN2 = "\x1B[32m";
2063
+ var YELLOW2 = "\x1B[33m";
2064
+ var CYAN2 = "\x1B[36m";
2065
+ var DIM3 = "\x1B[2m";
2066
+ function initCommand(options) {
2067
+ const scope = options.scope === "user" ? "user" : "project";
2068
+ const hasProLicense = process.env.KEEPGOING_PRO_BYPASS === "1" || !!getLicenseForFeature("session-awareness");
2069
+ const result = setupProject({
2070
+ workspacePath: options.cwd,
2071
+ scope,
2072
+ hasProLicense
2073
+ });
2074
+ console.log(`
2075
+ ${BOLD3}KeepGoing Init${RESET3} ${DIM3}(${scope} scope)${RESET3}
2076
+ `);
2077
+ for (const msg of result.messages) {
2078
+ const colonIdx = msg.indexOf(":");
2079
+ if (colonIdx === -1) {
2080
+ console.log(` ${msg}`);
2081
+ continue;
2082
+ }
2083
+ const label = msg.slice(0, colonIdx + 1);
2084
+ const body = msg.slice(colonIdx + 1);
2085
+ if (label.startsWith("Warning")) {
2086
+ console.log(` ${YELLOW2}${label}${RESET3}${body}`);
2087
+ } else if (body.includes("Added")) {
2088
+ console.log(` ${GREEN2}${label}${RESET3}${body}`);
2089
+ } else {
2090
+ console.log(` ${CYAN2}${label}${RESET3}${body}`);
2091
+ }
2092
+ }
2093
+ if (result.changed) {
2094
+ console.log(`
2095
+ ${GREEN2}Done!${RESET3} KeepGoing is set up for this project.
2096
+ `);
2097
+ } else {
2098
+ console.log(`
2099
+ Everything was already configured. No changes made.
2100
+ `);
2101
+ }
2102
+ }
2103
+
2104
+ // src/commands/momentum.ts
2105
+ async function momentumCommand(opts) {
2106
+ const reader = new KeepGoingReader(opts.cwd);
2107
+ if (!reader.exists()) {
2108
+ if (!opts.quiet) {
2109
+ renderNoData();
2110
+ }
2111
+ return;
2112
+ }
2113
+ const { session: lastSession, isFallback } = reader.getScopedLastSession();
2114
+ const currentBranch = reader.getCurrentBranch();
2115
+ if (!lastSession) {
2116
+ if (!opts.quiet) {
2117
+ console.log("KeepGoing is set up but no session checkpoints exist yet.");
2118
+ }
2119
+ return;
2120
+ }
2121
+ const state = reader.getState();
2122
+ const branchChanged = lastSession.gitBranch && currentBranch && lastSession.gitBranch !== currentBranch;
2123
+ if (opts.json) {
2124
+ console.log(JSON.stringify({
2125
+ lastCheckpoint: lastSession.timestamp,
2126
+ summary: lastSession.summary,
2127
+ nextStep: lastSession.nextStep,
2128
+ blocker: lastSession.blocker || null,
2129
+ projectIntent: lastSession.projectIntent || null,
2130
+ branch: currentBranch || null,
2131
+ branchChanged: branchChanged ? lastSession.gitBranch : null,
2132
+ touchedFiles: lastSession.touchedFiles,
2133
+ derivedFocus: state?.derivedCurrentFocus || null,
2134
+ isWorktree: reader.isWorktree,
2135
+ isFallback
2136
+ }, null, 2));
2137
+ return;
2138
+ }
2139
+ if (opts.quiet) {
2140
+ renderMomentumQuiet(lastSession);
2141
+ return;
2142
+ }
2143
+ renderMomentum(lastSession, {
2144
+ currentBranch,
2145
+ branchChanged: !!branchChanged,
2146
+ isWorktree: reader.isWorktree,
2147
+ isFallback,
2148
+ derivedFocus: state?.derivedCurrentFocus
2149
+ });
2150
+ }
2151
+
2152
+ // src/commands/decisions.ts
2153
+ async function decisionsCommand(opts) {
2154
+ const reader = new KeepGoingReader(opts.cwd);
2155
+ if (!reader.exists()) {
2156
+ if (!opts.quiet) {
2157
+ renderNoData();
2158
+ }
2159
+ return;
2160
+ }
2161
+ if (process.env.KEEPGOING_PRO_BYPASS !== "1" && !getLicenseForFeature("decisions")) {
2162
+ console.error(
2163
+ 'Decision Detection requires a Pro license.\nRun "keepgoing activate <key>" or visit https://keepgoing.dev/add-ons to purchase.'
2164
+ );
2165
+ process.exit(1);
2166
+ }
2167
+ const { effectiveBranch, scopeLabel } = reader.resolveBranchScope(opts.branch || void 0);
2168
+ const decisions = effectiveBranch ? reader.getRecentDecisionsForBranch(effectiveBranch, opts.limit) : reader.getRecentDecisions(opts.limit);
2169
+ if (decisions.length === 0) {
2170
+ if (!opts.quiet) {
2171
+ const msg = effectiveBranch ? `No decisions found for branch \`${effectiveBranch}\`. Use --branch all to see all branches.` : "No decisions found.";
2172
+ console.log(msg);
2173
+ }
2174
+ return;
2175
+ }
2176
+ if (opts.json) {
2177
+ console.log(JSON.stringify(decisions, null, 2));
2178
+ return;
2179
+ }
2180
+ if (opts.quiet) {
2181
+ renderDecisionsQuiet(decisions);
2182
+ return;
2183
+ }
2184
+ renderDecisions(decisions, scopeLabel);
2185
+ }
2186
+
2187
+ // src/commands/log.ts
2188
+ var RESET4 = "\x1B[0m";
2189
+ var DIM4 = "\x1B[2m";
2190
+ function parseDate(input) {
2191
+ const lower = input.toLowerCase().trim();
2192
+ if (lower === "today") {
2193
+ const d2 = /* @__PURE__ */ new Date();
2194
+ d2.setHours(0, 0, 0, 0);
2195
+ return d2;
2196
+ }
2197
+ if (lower === "yesterday") {
2198
+ const d2 = /* @__PURE__ */ new Date();
2199
+ d2.setDate(d2.getDate() - 1);
2200
+ d2.setHours(0, 0, 0, 0);
2201
+ return d2;
2202
+ }
2203
+ if (lower === "last week") {
2204
+ const d2 = /* @__PURE__ */ new Date();
2205
+ d2.setDate(d2.getDate() - 7);
2206
+ d2.setHours(0, 0, 0, 0);
2207
+ return d2;
2208
+ }
2209
+ const agoMatch = lower.match(/^(\d+)\s+(second|minute|hour|day|week|month)s?\s+ago$/);
2210
+ if (agoMatch) {
2211
+ const n = parseInt(agoMatch[1], 10);
2212
+ const unit = agoMatch[2];
2213
+ const now = /* @__PURE__ */ new Date();
2214
+ const msPerUnit = {
2215
+ second: 1e3,
2216
+ minute: 60 * 1e3,
2217
+ hour: 60 * 60 * 1e3,
2218
+ day: 24 * 60 * 60 * 1e3,
2219
+ week: 7 * 24 * 60 * 60 * 1e3,
2220
+ month: 30 * 24 * 60 * 60 * 1e3
2221
+ };
2222
+ return new Date(now.getTime() - n * (msPerUnit[unit] ?? 0));
2223
+ }
2224
+ const d = new Date(input);
2225
+ if (!isNaN(d.getTime())) return d;
2226
+ return void 0;
2227
+ }
2228
+ function filterSessions(sessions, opts) {
2229
+ let result = sessions;
2230
+ let sinceDate;
2231
+ if (opts.today) {
2232
+ sinceDate = parseDate("today");
2233
+ } else if (opts.week) {
2234
+ sinceDate = parseDate("last week");
2235
+ } else if (opts.since) {
2236
+ sinceDate = parseDate(opts.since);
2237
+ }
2238
+ if (sinceDate) {
2239
+ const ts = sinceDate.getTime();
2240
+ result = result.filter((s) => new Date(s.timestamp).getTime() >= ts);
2241
+ }
2242
+ if (opts.until) {
2243
+ const untilDate = parseDate(opts.until);
2244
+ if (untilDate) {
2245
+ const ts = untilDate.getTime();
2246
+ result = result.filter((s) => new Date(s.timestamp).getTime() <= ts);
2247
+ }
2248
+ }
2249
+ if (opts.source) {
2250
+ const src = opts.source.toLowerCase();
2251
+ result = result.filter((s) => s.source?.toLowerCase() === src);
2252
+ }
2253
+ if (opts.blockerOnly) {
2254
+ result = result.filter((s) => s.blocker && s.blocker.trim().length > 0);
2255
+ }
2256
+ if (opts.follow) {
2257
+ const file = opts.follow.toLowerCase();
2258
+ result = result.filter(
2259
+ (s) => s.touchedFiles?.some((f) => f.toLowerCase().includes(file))
2260
+ );
2261
+ }
2262
+ if (opts.search) {
2263
+ const term = opts.search.toLowerCase();
2264
+ result = result.filter((s) => {
2265
+ const haystack = [s.summary, s.nextStep, s.blocker].filter(Boolean).join(" ").toLowerCase();
2266
+ return haystack.includes(term);
2267
+ });
2268
+ }
2269
+ return result;
2270
+ }
2271
+ function filterDecisions(decisions, opts) {
2272
+ let result = decisions;
2273
+ let sinceDate;
2274
+ if (opts.today) {
2275
+ sinceDate = parseDate("today");
2276
+ } else if (opts.week) {
2277
+ sinceDate = parseDate("last week");
2278
+ } else if (opts.since) {
2279
+ sinceDate = parseDate(opts.since);
2280
+ }
2281
+ if (sinceDate) {
2282
+ const ts = sinceDate.getTime();
2283
+ result = result.filter((d) => new Date(d.timestamp).getTime() >= ts);
2284
+ }
2285
+ if (opts.until) {
2286
+ const untilDate = parseDate(opts.until);
2287
+ if (untilDate) {
2288
+ const ts = untilDate.getTime();
2289
+ result = result.filter((d) => new Date(d.timestamp).getTime() <= ts);
2290
+ }
2291
+ }
2292
+ if (opts.follow) {
2293
+ const file = opts.follow.toLowerCase();
2294
+ result = result.filter(
2295
+ (d) => d.filesChanged?.some((f) => f.toLowerCase().includes(file))
2296
+ );
2297
+ }
2298
+ if (opts.search) {
2299
+ const term = opts.search.toLowerCase();
2300
+ result = result.filter((d) => {
2301
+ const haystack = [d.commitMessage, d.rationale].filter(Boolean).join(" ").toLowerCase();
2302
+ return haystack.includes(term);
2303
+ });
2304
+ }
2305
+ return result;
2306
+ }
2307
+ function logSessions(reader, opts) {
2308
+ const { effectiveBranch } = reader.resolveBranchScope(opts.branch || void 0);
2309
+ let sessions = reader.getSessions();
2310
+ if (effectiveBranch) {
2311
+ sessions = sessions.filter((s) => s.gitBranch === effectiveBranch);
2312
+ }
2313
+ sessions.reverse();
2314
+ sessions = filterSessions(sessions, opts);
2315
+ const totalFiltered = sessions.length;
2316
+ if (totalFiltered === 0) {
2317
+ console.log(`${DIM4}No checkpoints match the given filters.${RESET4}`);
2318
+ return;
2319
+ }
2320
+ const displayed = sessions.slice(0, opts.count);
2321
+ if (opts.json) {
2322
+ console.log(JSON.stringify(displayed, null, 2));
2323
+ return;
2324
+ }
2325
+ if (opts.quiet) {
2326
+ console.log(`${totalFiltered} checkpoint${totalFiltered !== 1 ? "s" : ""} found`);
2327
+ return;
2328
+ }
2329
+ if (opts.sessions) {
2330
+ renderGrouped(displayed, opts.stat);
2331
+ } else if (opts.oneline) {
2332
+ for (const s of displayed) {
2333
+ renderLogSessionOneline(s);
2334
+ }
2335
+ } else {
2336
+ for (const s of displayed) {
2337
+ renderLogSession(s, opts.stat);
2338
+ }
2339
+ }
2340
+ if (totalFiltered > opts.count) {
2341
+ console.log(`${DIM4}(showing ${displayed.length} of ${totalFiltered} checkpoints)${RESET4}`);
2342
+ }
2343
+ }
2344
+ function renderGrouped(sessions, showStat) {
2345
+ const groups = /* @__PURE__ */ new Map();
2346
+ for (const s of sessions) {
2347
+ const key = s.sessionId || s.id;
2348
+ const group = groups.get(key);
2349
+ if (group) {
2350
+ group.push(s);
2351
+ } else {
2352
+ groups.set(key, [s]);
2353
+ }
2354
+ }
2355
+ let first = true;
2356
+ for (const [sessionId, items] of groups) {
2357
+ if (!first) console.log("");
2358
+ first = false;
2359
+ renderSessionGroupHeader(sessionId, items.length);
2360
+ for (const s of items) {
2361
+ renderLogSession(s, showStat);
2362
+ }
2363
+ }
2364
+ }
2365
+ function logDecisions(reader, opts) {
2366
+ const license = getLicenseForFeature("decisions");
2367
+ if (!license) {
2368
+ console.log("Decision tracking requires a Pro license. Run: keepgoing activate <key>");
2369
+ return;
2370
+ }
2371
+ const { effectiveBranch } = reader.resolveBranchScope(opts.branch || void 0);
2372
+ let decisions = reader.getDecisions();
2373
+ if (effectiveBranch) {
2374
+ decisions = decisions.filter((d) => d.gitBranch === effectiveBranch);
2375
+ }
2376
+ decisions.reverse();
2377
+ decisions = filterDecisions(decisions, opts);
2378
+ const totalFiltered = decisions.length;
2379
+ if (totalFiltered === 0) {
2380
+ console.log(`${DIM4}No decisions match the given filters.${RESET4}`);
2381
+ return;
2382
+ }
2383
+ const displayed = decisions.slice(0, opts.count);
2384
+ if (opts.json) {
2385
+ console.log(JSON.stringify(displayed, null, 2));
2386
+ return;
2387
+ }
2388
+ if (opts.quiet) {
2389
+ console.log(`${totalFiltered} decision${totalFiltered !== 1 ? "s" : ""} found`);
2390
+ return;
2391
+ }
2392
+ if (opts.oneline) {
2393
+ for (const d of displayed) {
2394
+ renderLogDecisionOneline(d);
2395
+ }
2396
+ } else {
2397
+ for (const d of displayed) {
2398
+ renderLogDecision(d, opts.stat);
2399
+ }
2400
+ }
2401
+ if (totalFiltered > opts.count) {
2402
+ console.log(`${DIM4}(showing ${displayed.length} of ${totalFiltered} decisions)${RESET4}`);
2403
+ }
2404
+ }
2405
+ async function logCommand(opts) {
2406
+ const reader = new KeepGoingReader(opts.cwd);
2407
+ if (!reader.exists()) {
2408
+ renderNoData();
2409
+ return;
2410
+ }
2411
+ if (opts.subcommand === "decisions") {
2412
+ logDecisions(reader, opts);
2413
+ } else {
2414
+ logSessions(reader, opts);
2415
+ }
2416
+ }
2417
+
1030
2418
  // src/index.ts
1031
2419
  var HELP_TEXT = `
1032
2420
  keepgoing: resume side projects without the mental friction
1033
2421
 
1034
- Usage:
1035
- keepgoing status Show the last checkpoint for this project
1036
- keepgoing save Save a new checkpoint interactively
1037
- keepgoing hook Manage the shell hook
1038
- keepgoing activate <key> Activate a Pro license on this device
1039
- keepgoing deactivate Deactivate the Pro license from this device
2422
+ Usage: keepgoing <command> [options]
1040
2423
 
1041
- Options:
2424
+ Commands:
2425
+ init Set up KeepGoing hooks and CLAUDE.md in this project
2426
+ status Show the last checkpoint for this project
2427
+ momentum Show your current developer momentum
2428
+ briefing Get a re-entry briefing for this project
2429
+ decisions View decision history (Pro)
2430
+ log Browse session checkpoints
2431
+ save Save a checkpoint (auto-generates from git)
2432
+ hook Manage the shell hook (zsh, bash, fish)
2433
+ activate <key> Activate a Pro license on this device
2434
+ deactivate Deactivate the Pro license from this device
2435
+
2436
+ Global options:
1042
2437
  --cwd <path> Override the working directory (default: current directory)
1043
- --json Output raw JSON (status only)
1044
- --quiet Output a single summary line (status only)
1045
- --shell <name> Override shell detection (zsh, bash, fish) for hook commands
2438
+ --json Output raw JSON
2439
+ --quiet Suppress output
1046
2440
  -v, --version Show the CLI version
1047
- -h, --help Show this help text
2441
+ -h, --help Show help (use with a command for detailed options)
1048
2442
 
1049
- Hook subcommands:
1050
- keepgoing hook install Install the shell hook (zsh, bash, fish)
1051
- keepgoing hook uninstall Remove the shell hook
2443
+ Run "keepgoing <command> --help" for detailed options on any command.
1052
2444
  `;
2445
+ var COMMAND_HELP = {
2446
+ init: `
2447
+ keepgoing init: Set up KeepGoing hooks and CLAUDE.md in this project
2448
+
2449
+ Usage: keepgoing init [options]
2450
+
2451
+ Options:
2452
+ --scope <s> Scope: "project" (default) or "user" (global)
2453
+ --cwd <path> Override the working directory
2454
+ `,
2455
+ setup: `
2456
+ keepgoing init: Set up KeepGoing hooks and CLAUDE.md in this project
2457
+
2458
+ Usage: keepgoing init [options]
2459
+
2460
+ Options:
2461
+ --scope <s> Scope: "project" (default) or "user" (global)
2462
+ --cwd <path> Override the working directory
2463
+ `,
2464
+ status: `
2465
+ keepgoing status: Show the last checkpoint for this project
2466
+
2467
+ Usage: keepgoing status [options]
2468
+
2469
+ Options:
2470
+ --json Output raw JSON
2471
+ --quiet Suppress output
2472
+ --cwd <path> Override the working directory
2473
+ `,
2474
+ momentum: `
2475
+ keepgoing momentum: Show your current developer momentum
2476
+
2477
+ Usage: keepgoing momentum [options]
2478
+
2479
+ Options:
2480
+ --json Output raw JSON
2481
+ --quiet Suppress output
2482
+ --cwd <path> Override the working directory
2483
+ `,
2484
+ briefing: `
2485
+ keepgoing briefing: Get a re-entry briefing for this project
2486
+
2487
+ Usage: keepgoing briefing [options]
2488
+
2489
+ Options:
2490
+ --json Output raw JSON
2491
+ --quiet Suppress output
2492
+ --cwd <path> Override the working directory
2493
+ `,
2494
+ decisions: `
2495
+ keepgoing decisions: View decision history (Pro)
2496
+
2497
+ Usage: keepgoing decisions [options]
2498
+
2499
+ Options:
2500
+ --branch <name> Filter by branch, or "all" for all branches
2501
+ --limit <n> Number of decisions to show (default: 10)
2502
+ --json Output raw JSON
2503
+ --quiet Suppress output
2504
+ --cwd <path> Override the working directory
2505
+ `,
2506
+ log: `
2507
+ keepgoing log: Browse session checkpoints
2508
+
2509
+ Usage:
2510
+ keepgoing log [options] Browse session checkpoints
2511
+ keepgoing log decisions [options] Browse decision records (Pro)
2512
+
2513
+ Options:
2514
+ -n <count> Number of entries to show (default: 10)
2515
+ --branch <name> Filter by branch ("all" for all branches)
2516
+ --since <date> Show entries after date (ISO, "today", "yesterday", "N days ago")
2517
+ --until <date> Show entries before date
2518
+ --source <type> Filter by source (manual, auto)
2519
+ --follow <file> Filter by touched file path
2520
+ --search <term> Search in summary, next step, blocker
2521
+ --oneline Compact one-line format
2522
+ --stat Show touched file paths
2523
+ --blocker Only show entries with blockers
2524
+ --today Shorthand for --since today
2525
+ --week Shorthand for --since "last week"
2526
+ --sessions Group checkpoints by session
2527
+ --json Output raw JSON
2528
+ --quiet Suppress output
2529
+ --cwd <path> Override the working directory
2530
+
2531
+ Examples:
2532
+ keepgoing log --today Show today's checkpoints
2533
+ keepgoing log --week --oneline This week's checkpoints, compact
2534
+ keepgoing log --follow src/app.ts Checkpoints that touched a file
2535
+ keepgoing log --search "auth" Search checkpoint summaries
2536
+ keepgoing log --sessions Group by session
2537
+ keepgoing log decisions Browse decision records (Pro)
2538
+ `,
2539
+ save: `
2540
+ keepgoing save: Save a checkpoint (auto-generates from git)
2541
+
2542
+ Usage: keepgoing save [options]
2543
+
2544
+ Options:
2545
+ -m, --message <text> Use a custom summary instead of auto-generating
2546
+ -n, --next <text> Use a custom next step instead of auto-generating
2547
+ --force Save even if recent checkpoint exists or no changes
2548
+ --json Output raw JSON
2549
+ --quiet Suppress output
2550
+ --cwd <path> Override the working directory
2551
+
2552
+ Examples:
2553
+ keepgoing save Auto-generate from git
2554
+ keepgoing save -m "Finished auth flow" Custom summary
2555
+ keepgoing save --force Save even if no changes
2556
+ `,
2557
+ hook: `
2558
+ keepgoing hook: Manage the shell hook
2559
+
2560
+ The shell hook shows a quick status line when you cd into a KeepGoing project.
2561
+
2562
+ Usage:
2563
+ keepgoing hook install Install the shell hook
2564
+ keepgoing hook uninstall Remove the shell hook
2565
+
2566
+ Supported shells: zsh, bash, fish
2567
+
2568
+ Options:
2569
+ --shell <name> Override shell detection (zsh, bash, fish)
2570
+
2571
+ The hook auto-detects your current shell. Use --shell to override.
2572
+ `,
2573
+ activate: `
2574
+ keepgoing activate: Activate a Pro license on this device
2575
+
2576
+ Usage: keepgoing activate <key>
2577
+
2578
+ Example:
2579
+ keepgoing activate XXXX-XXXX-XXXX-XXXX
2580
+ `,
2581
+ deactivate: `
2582
+ keepgoing deactivate: Deactivate the Pro license from this device
2583
+
2584
+ Usage: keepgoing deactivate [<key>]
2585
+ `
2586
+ };
1053
2587
  function parseArgs(argv) {
1054
2588
  const args = argv.slice(2);
1055
2589
  let command = "";
1056
2590
  let subcommand = "";
2591
+ let help = false;
1057
2592
  let cwd = process.cwd();
1058
2593
  let json = false;
1059
2594
  let quiet = false;
1060
2595
  let shell = "";
2596
+ let scope = "project";
2597
+ let message = "";
2598
+ let nextStepOverride = "";
2599
+ let force = false;
2600
+ let branch = "";
2601
+ let limit = 10;
2602
+ let count = 10;
2603
+ let since = "";
2604
+ let until = "";
2605
+ let source = "";
2606
+ let follow = "";
2607
+ let search = "";
2608
+ let oneline = false;
2609
+ let stat = false;
2610
+ let blockerOnly = false;
2611
+ let today = false;
2612
+ let week = false;
2613
+ let sessions = false;
1061
2614
  for (let i = 0; i < args.length; i++) {
1062
2615
  const arg = args[i];
1063
2616
  if (arg === "--cwd" && i + 1 < args.length) {
1064
2617
  cwd = args[++i];
1065
2618
  } else if (arg === "--shell" && i + 1 < args.length) {
1066
2619
  shell = args[++i];
2620
+ } else if (arg === "--scope" && i + 1 < args.length) {
2621
+ scope = args[++i];
2622
+ } else if ((arg === "-m" || arg === "--message") && i + 1 < args.length) {
2623
+ message = args[++i];
2624
+ } else if (arg === "--next" && i + 1 < args.length) {
2625
+ nextStepOverride = args[++i];
2626
+ } else if (arg === "-n" && i + 1 < args.length) {
2627
+ if (command === "log") {
2628
+ count = parseInt(args[++i], 10) || 10;
2629
+ } else {
2630
+ nextStepOverride = args[++i];
2631
+ }
2632
+ } else if (arg === "--branch" && i + 1 < args.length) {
2633
+ branch = args[++i];
2634
+ } else if (arg === "--limit" && i + 1 < args.length) {
2635
+ limit = parseInt(args[++i], 10) || 10;
1067
2636
  } else if (arg === "--json") {
1068
2637
  json = true;
1069
2638
  } else if (arg === "--quiet") {
1070
2639
  quiet = true;
2640
+ } else if (arg === "--force") {
2641
+ force = true;
2642
+ } else if (arg === "--since" && i + 1 < args.length) {
2643
+ since = args[++i];
2644
+ } else if (arg === "--until" && i + 1 < args.length) {
2645
+ until = args[++i];
2646
+ } else if (arg === "--source" && i + 1 < args.length) {
2647
+ source = args[++i];
2648
+ } else if (arg === "--follow" && i + 1 < args.length) {
2649
+ follow = args[++i];
2650
+ } else if (arg === "--search" && i + 1 < args.length) {
2651
+ search = args[++i];
2652
+ } else if (arg === "--oneline") {
2653
+ oneline = true;
2654
+ } else if (arg === "--stat") {
2655
+ stat = true;
2656
+ } else if (arg === "--blocker") {
2657
+ blockerOnly = true;
2658
+ } else if (arg === "--today") {
2659
+ today = true;
2660
+ } else if (arg === "--week") {
2661
+ week = true;
2662
+ } else if (arg === "--sessions") {
2663
+ sessions = true;
1071
2664
  } else if (arg === "-v" || arg === "--version") {
1072
2665
  command = "version";
1073
2666
  } else if (arg === "-h" || arg === "--help") {
1074
- command = "help";
2667
+ help = true;
1075
2668
  } else if (!command) {
1076
2669
  command = arg;
1077
2670
  } else if (!subcommand) {
@@ -1079,16 +2672,93 @@ function parseArgs(argv) {
1079
2672
  }
1080
2673
  }
1081
2674
  cwd = findGitRoot(cwd);
1082
- return { command, subcommand, cwd, json, quiet, shell };
2675
+ return {
2676
+ command,
2677
+ subcommand,
2678
+ help,
2679
+ cwd,
2680
+ json,
2681
+ quiet,
2682
+ shell,
2683
+ scope,
2684
+ message,
2685
+ nextStepOverride,
2686
+ force,
2687
+ branch,
2688
+ limit,
2689
+ count,
2690
+ since,
2691
+ until,
2692
+ source,
2693
+ follow,
2694
+ search,
2695
+ oneline,
2696
+ stat,
2697
+ blockerOnly,
2698
+ today,
2699
+ week,
2700
+ sessions
2701
+ };
1083
2702
  }
1084
2703
  async function main() {
1085
- const { command, subcommand, cwd, json, quiet, shell } = parseArgs(process.argv);
2704
+ const parsed = parseArgs(process.argv);
2705
+ const { command, subcommand, cwd, json, quiet, shell, scope, message, nextStepOverride, force, branch, limit } = parsed;
2706
+ if (parsed.help || command === "help") {
2707
+ const helpCmd = parsed.help ? command : subcommand;
2708
+ if (helpCmd && COMMAND_HELP[helpCmd]) {
2709
+ console.log(COMMAND_HELP[helpCmd]);
2710
+ } else {
2711
+ console.log(HELP_TEXT);
2712
+ }
2713
+ return;
2714
+ }
1086
2715
  switch (command) {
2716
+ case "init":
2717
+ case "setup":
2718
+ initCommand({ cwd, scope });
2719
+ break;
1087
2720
  case "status":
1088
2721
  await statusCommand({ cwd, json, quiet });
1089
2722
  break;
2723
+ case "momentum":
2724
+ await momentumCommand({ cwd, json, quiet });
2725
+ break;
2726
+ case "briefing":
2727
+ await briefingCommand({ cwd, json, quiet });
2728
+ break;
2729
+ case "decisions":
2730
+ await decisionsCommand({ cwd, json, quiet, branch, limit });
2731
+ break;
2732
+ case "log":
2733
+ await logCommand({
2734
+ cwd,
2735
+ json,
2736
+ quiet,
2737
+ subcommand,
2738
+ count: parsed.count,
2739
+ branch: parsed.branch,
2740
+ since: parsed.since,
2741
+ until: parsed.until,
2742
+ source: parsed.source,
2743
+ follow: parsed.follow,
2744
+ search: parsed.search,
2745
+ oneline: parsed.oneline,
2746
+ stat: parsed.stat,
2747
+ blockerOnly: parsed.blockerOnly,
2748
+ today: parsed.today,
2749
+ week: parsed.week,
2750
+ sessions: parsed.sessions
2751
+ });
2752
+ break;
1090
2753
  case "save":
1091
- await saveCommand({ cwd });
2754
+ await saveCommand({
2755
+ cwd,
2756
+ message: message || void 0,
2757
+ nextStepOverride: nextStepOverride || void 0,
2758
+ json,
2759
+ quiet,
2760
+ force
2761
+ });
1092
2762
  break;
1093
2763
  case "hook":
1094
2764
  if (subcommand === "install") {
@@ -1096,14 +2766,11 @@ async function main() {
1096
2766
  } else if (subcommand === "uninstall") {
1097
2767
  hookUninstallCommand(shell || void 0);
1098
2768
  } else {
1099
- console.error(
1100
- `Unknown hook subcommand: "${subcommand}". Use "install" or "uninstall".`
1101
- );
1102
- process.exit(1);
2769
+ console.log(COMMAND_HELP.hook);
1103
2770
  }
1104
2771
  break;
1105
2772
  case "version":
1106
- console.log(`keepgoing v${"0.3.3"}`);
2773
+ console.log(`keepgoing v${"1.0.0"}`);
1107
2774
  break;
1108
2775
  case "activate":
1109
2776
  await activateCommand({ licenseKey: subcommand });
@@ -1111,7 +2778,6 @@ async function main() {
1111
2778
  case "deactivate":
1112
2779
  await deactivateCommand({ licenseKey: subcommand || void 0 });
1113
2780
  break;
1114
- case "help":
1115
2781
  case "":
1116
2782
  console.log(HELP_TEXT);
1117
2783
  break;