@keepgoingdev/cli 0.3.3 → 1.1.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 +2326 -211
  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"], {
@@ -121,15 +159,434 @@ function getTouchedFiles(workspacePath) {
121
159
  }
122
160
  }
123
161
 
162
+ // ../../packages/shared/src/briefingTier.ts
163
+ var MODEL_TIERS = {
164
+ // Standard: small/fast models (compact is only reachable via explicit tier or context window)
165
+ "claude-3-haiku": "standard",
166
+ "claude-3.5-haiku": "standard",
167
+ "claude-haiku-4-5": "standard",
168
+ "gpt-4o-mini": "standard",
169
+ "gemini-flash": "standard",
170
+ // Detailed: mid-tier models
171
+ "claude-3.5-sonnet": "detailed",
172
+ "claude-sonnet-4": "detailed",
173
+ "claude-sonnet-4-5": "detailed",
174
+ "claude-sonnet-4-6": "detailed",
175
+ "gpt-4o": "detailed",
176
+ "gemini-pro": "detailed",
177
+ // Full: large context models
178
+ "claude-opus-4": "full",
179
+ "claude-opus-4-5": "full",
180
+ "claude-opus-4-6": "full",
181
+ "o1": "full",
182
+ "o3": "full",
183
+ "gemini-ultra": "full"
184
+ };
185
+ function resolveTier(opts) {
186
+ if (opts?.tier) {
187
+ return opts.tier;
188
+ }
189
+ if (opts?.model) {
190
+ const fromModel = tierFromModelName(opts.model);
191
+ if (fromModel) return fromModel;
192
+ }
193
+ if (opts?.contextWindow !== void 0) {
194
+ return tierFromContextWindow(opts.contextWindow);
195
+ }
196
+ return "standard";
197
+ }
198
+ function tierFromModelName(model) {
199
+ const normalized = model.toLowerCase().trim();
200
+ if (MODEL_TIERS[normalized]) {
201
+ return MODEL_TIERS[normalized];
202
+ }
203
+ const entries = Object.entries(MODEL_TIERS).sort((a, b) => b[0].length - a[0].length);
204
+ for (const [key, tier] of entries) {
205
+ if (normalized.startsWith(key)) {
206
+ return tier;
207
+ }
208
+ }
209
+ return void 0;
210
+ }
211
+ function tierFromContextWindow(tokens) {
212
+ if (tokens < 16e3) return "compact";
213
+ if (tokens < 64e3) return "standard";
214
+ if (tokens < 2e5) return "detailed";
215
+ return "full";
216
+ }
217
+
124
218
  // ../../packages/shared/src/reentry.ts
125
219
  var RECENT_SESSION_COUNT = 5;
220
+ function generateBriefing(lastSession, recentSessions, projectState, gitBranch, recentCommitMessages) {
221
+ if (!lastSession) {
222
+ return void 0;
223
+ }
224
+ return {
225
+ lastWorked: formatRelativeTime(lastSession.timestamp),
226
+ currentFocus: buildCurrentFocus(lastSession, projectState, gitBranch),
227
+ recentActivity: buildRecentActivity(
228
+ lastSession,
229
+ recentSessions,
230
+ recentCommitMessages
231
+ ),
232
+ suggestedNext: buildSuggestedNext(lastSession, gitBranch),
233
+ smallNextStep: buildSmallNextStep(
234
+ lastSession,
235
+ gitBranch,
236
+ recentCommitMessages
237
+ )
238
+ };
239
+ }
126
240
  function getRecentSessions(allSessions, count = RECENT_SESSION_COUNT) {
127
241
  return allSessions.slice(-count).reverse();
128
242
  }
243
+ function generateEnrichedBriefing(opts) {
244
+ const tier = resolveTier({
245
+ tier: opts.tier,
246
+ model: opts.model,
247
+ contextWindow: opts.contextWindow
248
+ });
249
+ const core = generateBriefing(
250
+ opts.lastSession,
251
+ opts.recentSessions,
252
+ opts.projectState,
253
+ opts.gitBranch,
254
+ opts.recentCommits
255
+ );
256
+ if (!core) return void 0;
257
+ const enriched = { tier, core };
258
+ if (tier === "compact") {
259
+ return enriched;
260
+ }
261
+ enriched.blocker = opts.lastSession?.blocker;
262
+ enriched.gitBranch = opts.gitBranch;
263
+ enriched.isWorktree = opts.isWorktree;
264
+ if (tier === "standard") {
265
+ return enriched;
266
+ }
267
+ if (opts.decisions && opts.decisions.length > 0) {
268
+ enriched.decisions = opts.decisions.slice(0, tier === "detailed" ? 3 : 10);
269
+ }
270
+ if (opts.allTouchedFiles && opts.allTouchedFiles.length > 0) {
271
+ enriched.touchedFiles = tier === "detailed" ? opts.allTouchedFiles.slice(0, 10) : opts.allTouchedFiles;
272
+ } else if (opts.lastSession?.touchedFiles && opts.lastSession.touchedFiles.length > 0) {
273
+ enriched.touchedFiles = tier === "detailed" ? opts.lastSession.touchedFiles.slice(0, 10) : opts.lastSession.touchedFiles;
274
+ }
275
+ if (tier === "detailed") {
276
+ return enriched;
277
+ }
278
+ if (opts.allSessions && opts.allSessions.length > 0) {
279
+ const recent = opts.allSessions.slice(-5).reverse();
280
+ enriched.sessionHistory = recent.map((s) => ({
281
+ timestamp: s.timestamp,
282
+ summary: s.summary || "",
283
+ nextStep: s.nextStep || "",
284
+ branch: s.gitBranch
285
+ }));
286
+ }
287
+ if (opts.recentCommits && opts.recentCommits.length > 0) {
288
+ enriched.recentCommits = opts.recentCommits;
289
+ }
290
+ if (opts.fileConflicts && opts.fileConflicts.length > 0) {
291
+ enriched.fileConflicts = opts.fileConflicts;
292
+ }
293
+ if (opts.branchOverlaps && opts.branchOverlaps.length > 0) {
294
+ enriched.branchOverlaps = opts.branchOverlaps;
295
+ }
296
+ return enriched;
297
+ }
298
+ function buildCurrentFocus(lastSession, projectState, gitBranch) {
299
+ if (projectState.derivedCurrentFocus) {
300
+ return projectState.derivedCurrentFocus;
301
+ }
302
+ const branchFocus = inferFocusFromBranch(gitBranch);
303
+ if (branchFocus) {
304
+ return branchFocus;
305
+ }
306
+ if (lastSession.summary) {
307
+ return lastSession.summary;
308
+ }
309
+ if (lastSession.touchedFiles.length > 0) {
310
+ return inferFocusFromFiles(lastSession.touchedFiles);
311
+ }
312
+ return "Unknown, save a checkpoint to set context";
313
+ }
314
+ function buildRecentActivity(lastSession, recentSessions, recentCommitMessages) {
315
+ const parts = [];
316
+ const sessionCount = recentSessions.length;
317
+ if (sessionCount > 1) {
318
+ parts.push(`${sessionCount} recent sessions`);
319
+ } else if (sessionCount === 1) {
320
+ parts.push("1 recent session");
321
+ }
322
+ if (lastSession.summary) {
323
+ parts.push(`Last: ${lastSession.summary}`);
324
+ }
325
+ if (lastSession.touchedFiles.length > 0) {
326
+ parts.push(`${lastSession.touchedFiles.length} files touched`);
327
+ }
328
+ if (recentCommitMessages && recentCommitMessages.length > 0) {
329
+ parts.push(`${recentCommitMessages.length} recent commits`);
330
+ }
331
+ return parts.length > 0 ? parts.join(". ") : "No recent activity recorded";
332
+ }
333
+ function buildSuggestedNext(lastSession, gitBranch) {
334
+ if (lastSession.nextStep) {
335
+ return lastSession.nextStep;
336
+ }
337
+ const branchFocus = inferFocusFromBranch(gitBranch);
338
+ if (branchFocus) {
339
+ return `Continue working on ${branchFocus}`;
340
+ }
341
+ if (lastSession.touchedFiles.length > 0) {
342
+ return `Continue working on ${inferFocusFromFiles(lastSession.touchedFiles)}`;
343
+ }
344
+ return "Save a checkpoint to track your next step";
345
+ }
346
+ function buildSmallNextStep(lastSession, gitBranch, recentCommitMessages) {
347
+ const fallback = "Review last changed files to resume flow";
348
+ if (lastSession.nextStep) {
349
+ const distilled = distillToSmallStep(
350
+ lastSession.nextStep,
351
+ lastSession.touchedFiles
352
+ );
353
+ if (distilled) {
354
+ return distilled;
355
+ }
356
+ }
357
+ if (recentCommitMessages && recentCommitMessages.length > 0) {
358
+ const commitStep = deriveStepFromCommits(recentCommitMessages);
359
+ if (commitStep) {
360
+ return commitStep;
361
+ }
362
+ }
363
+ if (lastSession.touchedFiles.length > 0) {
364
+ const fileStep = deriveStepFromFiles(lastSession.touchedFiles);
365
+ if (fileStep) {
366
+ return fileStep;
367
+ }
368
+ }
369
+ const branchFocus = inferFocusFromBranch(gitBranch);
370
+ if (branchFocus) {
371
+ return `Check git status for ${branchFocus}`;
372
+ }
373
+ return fallback;
374
+ }
375
+ function distillToSmallStep(nextStep, touchedFiles) {
376
+ if (!nextStep.trim()) {
377
+ return void 0;
378
+ }
379
+ const words = nextStep.trim().split(/\s+/);
380
+ if (words.length <= 12) {
381
+ if (touchedFiles.length > 0 && !mentionsFile(nextStep)) {
382
+ const primaryFile = getPrimaryFileName(touchedFiles);
383
+ const enhanced = `${nextStep.trim()} in ${primaryFile}`;
384
+ if (enhanced.split(/\s+/).length <= 12) {
385
+ return enhanced;
386
+ }
387
+ }
388
+ return nextStep.trim();
389
+ }
390
+ return words.slice(0, 12).join(" ");
391
+ }
392
+ function deriveStepFromCommits(commitMessages) {
393
+ const lastCommit = commitMessages[0];
394
+ if (!lastCommit || !lastCommit.trim()) {
395
+ return void 0;
396
+ }
397
+ const wipPattern = /^(?:wip|work in progress|started?|begin|draft)[:\s]/i;
398
+ if (wipPattern.test(lastCommit)) {
399
+ const topic = lastCommit.replace(wipPattern, "").trim();
400
+ if (topic) {
401
+ const words = topic.split(/\s+/).slice(0, 8).join(" ");
402
+ return `Continue ${words}`;
403
+ }
404
+ }
405
+ return void 0;
406
+ }
407
+ function deriveStepFromFiles(files) {
408
+ const primaryFile = getPrimaryFileName(files);
409
+ if (files.length > 1) {
410
+ return `Open ${primaryFile} and review ${files.length} changed files`;
411
+ }
412
+ return `Open ${primaryFile} and pick up where you left off`;
413
+ }
414
+ function getPrimaryFileName(files) {
415
+ const sourceFiles = files.filter((f) => {
416
+ const lower = f.toLowerCase();
417
+ return !lower.includes("test") && !lower.includes("spec") && !lower.includes(".config") && !lower.includes("package.json") && !lower.includes("tsconfig");
418
+ });
419
+ const target = sourceFiles.length > 0 ? sourceFiles[0] : files[0];
420
+ const parts = target.replace(/\\/g, "/").split("/");
421
+ return parts[parts.length - 1];
422
+ }
423
+ function mentionsFile(text) {
424
+ return /\w+\.(?:ts|tsx|js|jsx|py|go|rs|java|rb|css|scss|html|json|yaml|yml|md|sql|sh)\b/i.test(
425
+ text
426
+ );
427
+ }
428
+ function inferFocusFromBranch(branch) {
429
+ if (!branch || branch === "main" || branch === "master" || branch === "develop" || branch === "HEAD") {
430
+ return void 0;
431
+ }
432
+ const prefixPattern = /^(?:feature|feat|fix|bugfix|hotfix|chore|refactor|docs|test|ci)\//i;
433
+ const isFix = /^(?:fix|bugfix|hotfix)\//i.test(branch);
434
+ const stripped = branch.replace(prefixPattern, "");
435
+ const cleaned = stripped.replace(/[-_/]/g, " ").replace(/^\d+\s*/, "").trim();
436
+ if (!cleaned) {
437
+ return void 0;
438
+ }
439
+ return isFix ? `${cleaned} fix` : cleaned;
440
+ }
441
+ function inferFocusFromFiles(files) {
442
+ if (files.length === 0) {
443
+ return "unknown files";
444
+ }
445
+ const dirs = files.map((f) => {
446
+ const parts = f.replace(/\\/g, "/").split("/");
447
+ return parts.length > 1 ? parts.slice(0, -1).join("/") : "";
448
+ }).filter((d) => d.length > 0);
449
+ if (dirs.length > 0) {
450
+ const counts = /* @__PURE__ */ new Map();
451
+ for (const dir of dirs) {
452
+ counts.set(dir, (counts.get(dir) ?? 0) + 1);
453
+ }
454
+ let topDir = "";
455
+ let topCount = 0;
456
+ for (const [dir, count] of counts) {
457
+ if (count > topCount) {
458
+ topDir = dir;
459
+ topCount = count;
460
+ }
461
+ }
462
+ if (topDir) {
463
+ return `files in ${topDir}`;
464
+ }
465
+ }
466
+ const names = files.slice(0, 3).map((f) => {
467
+ const parts = f.replace(/\\/g, "/").split("/");
468
+ return parts[parts.length - 1];
469
+ });
470
+ return names.join(", ");
471
+ }
472
+
473
+ // ../../packages/shared/src/continueOn.ts
474
+ import path2 from "path";
475
+ function gatherContinueOnContext(reader, workspacePath) {
476
+ const projectName = path2.basename(workspacePath);
477
+ const gitBranch = reader.getCurrentBranch();
478
+ if (!reader.exists()) {
479
+ return {
480
+ projectName,
481
+ gitBranch,
482
+ recentCheckpoints: [],
483
+ recentDecisions: [],
484
+ currentTasks: [],
485
+ recentCommitMessages: []
486
+ };
487
+ }
488
+ const { session: lastCheckpoint } = reader.getScopedLastSession();
489
+ const recentCheckpoints = reader.getScopedRecentSessions(5);
490
+ const recentDecisions = reader.getScopedRecentDecisions(3);
491
+ const currentTasks = reader.getCurrentTasks();
492
+ const state = reader.getState() ?? {};
493
+ const sinceTimestamp = lastCheckpoint?.timestamp;
494
+ const recentCommitMessages = sinceTimestamp ? getCommitMessagesSince(workspacePath, sinceTimestamp) : [];
495
+ const briefing = generateBriefing(
496
+ lastCheckpoint,
497
+ recentCheckpoints,
498
+ state,
499
+ gitBranch,
500
+ recentCommitMessages
501
+ );
502
+ return {
503
+ projectName,
504
+ gitBranch,
505
+ briefing,
506
+ lastCheckpoint,
507
+ recentCheckpoints,
508
+ recentDecisions,
509
+ currentTasks,
510
+ recentCommitMessages
511
+ };
512
+ }
513
+ function formatContinueOnPrompt(context, options) {
514
+ const opts = { includeCommits: true, includeFiles: true, ...options };
515
+ const lines = [];
516
+ lines.push(`# Project Context: ${context.projectName}`);
517
+ lines.push("");
518
+ lines.push("I'm continuing work on a project. Here is my development context from KeepGoing.");
519
+ lines.push("");
520
+ lines.push("## Current Status");
521
+ if (context.gitBranch) {
522
+ lines.push(`- **Branch:** ${context.gitBranch}`);
523
+ }
524
+ if (context.briefing) {
525
+ lines.push(`- **Last worked:** ${context.briefing.lastWorked}`);
526
+ lines.push(`- **Focus:** ${context.briefing.currentFocus}`);
527
+ } else if (context.lastCheckpoint) {
528
+ lines.push(`- **Last worked:** ${formatRelativeTime(context.lastCheckpoint.timestamp)}`);
529
+ if (context.lastCheckpoint.summary) {
530
+ lines.push(`- **Focus:** ${context.lastCheckpoint.summary}`);
531
+ }
532
+ }
533
+ if (context.lastCheckpoint) {
534
+ lines.push("");
535
+ lines.push("## Last Session");
536
+ if (context.lastCheckpoint.summary) {
537
+ lines.push(`- **Summary:** ${context.lastCheckpoint.summary}`);
538
+ }
539
+ if (context.lastCheckpoint.nextStep) {
540
+ lines.push(`- **Next step:** ${context.lastCheckpoint.nextStep}`);
541
+ }
542
+ if (context.lastCheckpoint.blocker) {
543
+ lines.push(`- **Blocker:** ${context.lastCheckpoint.blocker}`);
544
+ }
545
+ if (opts.includeFiles && context.lastCheckpoint.touchedFiles.length > 0) {
546
+ const files = context.lastCheckpoint.touchedFiles.slice(0, 10).join(", ");
547
+ const extra = context.lastCheckpoint.touchedFiles.length > 10 ? ` (+${context.lastCheckpoint.touchedFiles.length - 10} more)` : "";
548
+ lines.push(`- **Files touched:** ${files}${extra}`);
549
+ }
550
+ }
551
+ const activeTasks = context.currentTasks.filter((t) => t.sessionActive);
552
+ if (activeTasks.length > 0) {
553
+ lines.push("");
554
+ lines.push("## Active Sessions");
555
+ for (const task of activeTasks) {
556
+ const label = task.agentLabel || "Unknown agent";
557
+ const branch = task.branch ? ` on \`${task.branch}\`` : "";
558
+ lines.push(`- **${label}**${branch}: ${task.taskSummary || "Working"}`);
559
+ }
560
+ }
561
+ if (context.recentDecisions.length > 0) {
562
+ lines.push("");
563
+ lines.push("## Recent Decisions");
564
+ for (const d of context.recentDecisions) {
565
+ lines.push(`- ${d.classification.category}: ${d.commitMessage}`);
566
+ }
567
+ }
568
+ if (opts.includeCommits && context.recentCommitMessages.length > 0) {
569
+ lines.push("");
570
+ lines.push("## Recent Commits");
571
+ const commits = context.recentCommitMessages.slice(0, 10);
572
+ for (const msg of commits) {
573
+ lines.push(`- ${msg}`);
574
+ }
575
+ }
576
+ lines.push("");
577
+ lines.push("---");
578
+ const nextStep = context.lastCheckpoint?.nextStep || context.briefing?.suggestedNext || "continue where I left off";
579
+ lines.push(`Please help me continue this work. My next step is: ${nextStep}.`);
580
+ let result = lines.join("\n");
581
+ if (opts.maxLength && result.length > opts.maxLength) {
582
+ result = result.slice(0, opts.maxLength - 3) + "...";
583
+ }
584
+ return result;
585
+ }
129
586
 
130
587
  // ../../packages/shared/src/storage.ts
131
588
  import fs from "fs";
132
- import path2 from "path";
589
+ import path3 from "path";
133
590
  import { randomUUID as randomUUID2, createHash } from "crypto";
134
591
  var STORAGE_DIR = ".keepgoing";
135
592
  var META_FILE = "meta.json";
@@ -152,11 +609,11 @@ var KeepGoingWriter = class {
152
609
  currentTasksFilePath;
153
610
  constructor(workspacePath) {
154
611
  const mainRoot = resolveStorageRoot(workspacePath);
155
- this.storagePath = path2.join(mainRoot, STORAGE_DIR);
156
- this.sessionsFilePath = path2.join(this.storagePath, SESSIONS_FILE);
157
- this.stateFilePath = path2.join(this.storagePath, STATE_FILE);
158
- this.metaFilePath = path2.join(this.storagePath, META_FILE);
159
- this.currentTasksFilePath = path2.join(this.storagePath, CURRENT_TASKS_FILE);
612
+ this.storagePath = path3.join(mainRoot, STORAGE_DIR);
613
+ this.sessionsFilePath = path3.join(this.storagePath, SESSIONS_FILE);
614
+ this.stateFilePath = path3.join(this.storagePath, STATE_FILE);
615
+ this.metaFilePath = path3.join(this.storagePath, META_FILE);
616
+ this.currentTasksFilePath = path3.join(this.storagePath, CURRENT_TASKS_FILE);
160
617
  }
161
618
  ensureDir() {
162
619
  if (!fs.existsSync(this.storagePath)) {
@@ -306,26 +763,188 @@ function generateSessionId(context) {
306
763
  return `ses_${hash}`;
307
764
  }
308
765
 
766
+ // ../../packages/shared/src/smartSummary.ts
767
+ var PREFIX_VERBS = {
768
+ feat: "Added",
769
+ fix: "Fixed",
770
+ refactor: "Refactored",
771
+ docs: "Updated docs for",
772
+ test: "Added tests for",
773
+ chore: "Updated",
774
+ style: "Styled",
775
+ perf: "Optimized",
776
+ ci: "Updated CI for",
777
+ build: "Updated build for",
778
+ revert: "Reverted"
779
+ };
780
+ var NOISE_PATTERNS = [
781
+ "node_modules",
782
+ "package-lock.json",
783
+ "yarn.lock",
784
+ "pnpm-lock.yaml",
785
+ ".gitignore",
786
+ ".DS_Store",
787
+ "dist/",
788
+ "out/",
789
+ "build/"
790
+ ];
791
+ function categorizeCommits(messages) {
792
+ const groups = /* @__PURE__ */ new Map();
793
+ for (const msg of messages) {
794
+ const match = msg.match(/^(\w+)(?:\([^)]*\))?[!]?:\s*(.+)/);
795
+ if (match) {
796
+ const prefix = match[1].toLowerCase();
797
+ const body = match[2].trim();
798
+ if (!groups.has(prefix)) {
799
+ groups.set(prefix, []);
800
+ }
801
+ groups.get(prefix).push(body);
802
+ } else {
803
+ if (!groups.has("other")) {
804
+ groups.set("other", []);
805
+ }
806
+ groups.get("other").push(msg.trim());
807
+ }
808
+ }
809
+ return groups;
810
+ }
811
+ function inferWorkAreas(files) {
812
+ const areas = /* @__PURE__ */ new Map();
813
+ for (const file of files) {
814
+ if (NOISE_PATTERNS.some((p) => file.includes(p))) {
815
+ continue;
816
+ }
817
+ const parts = file.split("/").filter(Boolean);
818
+ let area;
819
+ if (parts.length >= 2 && (parts[0] === "apps" || parts[0] === "packages")) {
820
+ area = parts[1];
821
+ if (parts[0] === "packages" && parts.length >= 4 && parts[2] === "src") {
822
+ const subFile = parts[3].replace(/\.\w+$/, "");
823
+ area = `${parts[1]} ${subFile}`;
824
+ }
825
+ } else if (parts.length >= 2) {
826
+ area = parts[0];
827
+ } else {
828
+ area = "root";
829
+ }
830
+ areas.set(area, (areas.get(area) ?? 0) + 1);
831
+ }
832
+ return [...areas.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([name]) => name);
833
+ }
834
+ function buildSessionEvents(opts) {
835
+ const { wsPath, commitHashes, commitMessages, touchedFiles, currentBranch, sessionStartTime, lastActivityTime } = opts;
836
+ const commits = commitHashes.map((hash, i) => ({
837
+ hash,
838
+ message: commitMessages[i] ?? "",
839
+ filesChanged: getFilesChangedInCommit(wsPath, hash),
840
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
841
+ }));
842
+ const committedFiles = new Set(commits.flatMap((c) => c.filesChanged));
843
+ return {
844
+ commits,
845
+ branchSwitches: [],
846
+ touchedFiles,
847
+ currentBranch,
848
+ sessionStartTime,
849
+ lastActivityTime,
850
+ // Normalize rename arrows ("old -> new") from git status --porcelain
851
+ // so they match the plain filenames from git diff-tree --name-only.
852
+ hasUncommittedChanges: touchedFiles.some((f) => {
853
+ const normalized = f.includes(" -> ") ? f.split(" -> ").pop() : f;
854
+ return !committedFiles.has(normalized);
855
+ })
856
+ };
857
+ }
858
+ function buildSmartSummary(events) {
859
+ const { commits, branchSwitches, touchedFiles, hasUncommittedChanges } = events;
860
+ if (commits.length === 0 && touchedFiles.length === 0 && branchSwitches.length === 0) {
861
+ return void 0;
862
+ }
863
+ const parts = [];
864
+ if (commits.length > 0) {
865
+ const messages = commits.map((c) => c.message);
866
+ const groups = categorizeCommits(messages);
867
+ const phrases = [];
868
+ for (const [prefix, bodies] of groups) {
869
+ const verb = PREFIX_VERBS[prefix] ?? (prefix === "other" ? "" : `${capitalize(prefix)}:`);
870
+ const items = bodies.slice(0, 2).join(" and ");
871
+ const overflow = bodies.length > 2 ? ` (+${bodies.length - 2} more)` : "";
872
+ if (verb) {
873
+ phrases.push(`${verb} ${items}${overflow}`);
874
+ } else {
875
+ phrases.push(`${items}${overflow}`);
876
+ }
877
+ }
878
+ parts.push(phrases.join(", "));
879
+ } else if (touchedFiles.length > 0) {
880
+ const areas = inferWorkAreas(touchedFiles);
881
+ const areaStr = areas.length > 0 ? areas.join(" and ") : `${touchedFiles.length} files`;
882
+ const suffix = hasUncommittedChanges ? " (uncommitted)" : "";
883
+ parts.push(`Worked on ${areaStr}${suffix}`);
884
+ }
885
+ if (branchSwitches.length > 0) {
886
+ const last = branchSwitches[branchSwitches.length - 1];
887
+ if (branchSwitches.length === 1) {
888
+ parts.push(`switched to ${last.toBranch}`);
889
+ } else {
890
+ parts.push(`switched branches ${branchSwitches.length} times, ended on ${last.toBranch}`);
891
+ }
892
+ }
893
+ const result = parts.join("; ");
894
+ return result || void 0;
895
+ }
896
+ function buildSmartNextStep(events) {
897
+ const { commits, touchedFiles, currentBranch, hasUncommittedChanges } = events;
898
+ if (hasUncommittedChanges && touchedFiles.length > 0) {
899
+ const areas = inferWorkAreas(touchedFiles);
900
+ const areaStr = areas.length > 0 ? areas.join(" and ") : "working tree";
901
+ return `Review and commit changes in ${areaStr}`;
902
+ }
903
+ if (commits.length > 0) {
904
+ const lastMsg = commits[commits.length - 1].message;
905
+ const wipMatch = lastMsg.match(/^(?:wip|work in progress|start(?:ed)?|begin|draft)[:\s]+(.+)/i);
906
+ if (wipMatch) {
907
+ return `Continue ${wipMatch[1].trim()}`;
908
+ }
909
+ }
910
+ if (currentBranch && !["main", "master", "develop", "HEAD"].includes(currentBranch)) {
911
+ const branchName = currentBranch.replace(/^(feat|feature|fix|bugfix|hotfix|chore|refactor)[/-]/i, "").replace(/[-_]/g, " ").trim();
912
+ if (branchName) {
913
+ return `Continue ${branchName}`;
914
+ }
915
+ }
916
+ if (touchedFiles.length > 0) {
917
+ const areas = inferWorkAreas(touchedFiles);
918
+ if (areas.length > 0) {
919
+ return `Review recent changes in ${areas.join(" and ")}`;
920
+ }
921
+ }
922
+ return "";
923
+ }
924
+ function capitalize(s) {
925
+ return s.charAt(0).toUpperCase() + s.slice(1);
926
+ }
927
+
309
928
  // ../../packages/shared/src/decisionStorage.ts
310
929
  import fs3 from "fs";
311
- import path4 from "path";
930
+ import path5 from "path";
312
931
 
313
932
  // ../../packages/shared/src/license.ts
314
933
  import crypto from "crypto";
315
934
  import fs2 from "fs";
316
935
  import os from "os";
317
- import path3 from "path";
936
+ import path4 from "path";
318
937
  var LICENSE_FILE = "license.json";
319
938
  var DEVICE_ID_FILE = "device-id";
320
939
  function getGlobalLicenseDir() {
321
- return path3.join(os.homedir(), ".keepgoing");
940
+ return path4.join(os.homedir(), ".keepgoing");
322
941
  }
323
942
  function getGlobalLicensePath() {
324
- return path3.join(getGlobalLicenseDir(), LICENSE_FILE);
943
+ return path4.join(getGlobalLicenseDir(), LICENSE_FILE);
325
944
  }
326
945
  function getDeviceId() {
327
946
  const dir = getGlobalLicenseDir();
328
- const filePath = path3.join(dir, DEVICE_ID_FILE);
947
+ const filePath = path4.join(dir, DEVICE_ID_FILE);
329
948
  try {
330
949
  const existing = fs2.readFileSync(filePath, "utf-8").trim();
331
950
  if (existing) return existing;
@@ -392,7 +1011,7 @@ function writeLicenseStore(store) {
392
1011
  if (!fs2.existsSync(dirPath)) {
393
1012
  fs2.mkdirSync(dirPath, { recursive: true });
394
1013
  }
395
- const licensePath = path3.join(dirPath, LICENSE_FILE);
1014
+ const licensePath = path4.join(dirPath, LICENSE_FILE);
396
1015
  fs2.writeFileSync(licensePath, JSON.stringify(store, null, 2), "utf-8");
397
1016
  _cachedStore = store;
398
1017
  _cacheTimestamp = Date.now();
@@ -415,6 +1034,13 @@ function removeLicenseEntry(licenseKey) {
415
1034
  function getActiveLicenses() {
416
1035
  return readLicenseStore().licenses.filter((l) => l.status === "active");
417
1036
  }
1037
+ function getLicenseForFeature(feature) {
1038
+ const active = getActiveLicenses();
1039
+ return active.find((l) => {
1040
+ const features = VARIANT_FEATURE_MAP[l.variantId];
1041
+ return features?.includes(feature);
1042
+ });
1043
+ }
418
1044
  var REVALIDATION_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
419
1045
 
420
1046
  // ../../packages/shared/src/featureGate.ts
@@ -425,66 +1051,533 @@ var DefaultFeatureGate = class {
425
1051
  };
426
1052
  var currentGate = new DefaultFeatureGate();
427
1053
 
428
- // ../../packages/shared/src/licenseClient.ts
429
- var BASE_URL = "https://api.lemonsqueezy.com/v1/licenses";
430
- var REQUEST_TIMEOUT_MS = 15e3;
431
- var EXPECTED_STORE_ID = 301555;
432
- var EXPECTED_PRODUCT_ID = 864311;
433
- function fetchWithTimeout(url, init) {
434
- const controller = new AbortController();
435
- const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
436
- return fetch(url, { ...init, signal: controller.signal }).finally(() => clearTimeout(timer));
437
- }
438
- function validateProductIdentity(meta) {
439
- if (!meta) return "License response missing product metadata.";
440
- if (meta.store_id !== EXPECTED_STORE_ID || meta.product_id !== EXPECTED_PRODUCT_ID) {
441
- return "This license key does not belong to KeepGoing.";
1054
+ // ../../packages/shared/src/reader.ts
1055
+ import fs4 from "fs";
1056
+ import path6 from "path";
1057
+ var STORAGE_DIR2 = ".keepgoing";
1058
+ var META_FILE2 = "meta.json";
1059
+ var SESSIONS_FILE2 = "sessions.json";
1060
+ var DECISIONS_FILE = "decisions.json";
1061
+ var STATE_FILE2 = "state.json";
1062
+ var CURRENT_TASKS_FILE2 = "current-tasks.json";
1063
+ var KeepGoingReader = class {
1064
+ workspacePath;
1065
+ storagePath;
1066
+ metaFilePath;
1067
+ sessionsFilePath;
1068
+ decisionsFilePath;
1069
+ stateFilePath;
1070
+ currentTasksFilePath;
1071
+ _isWorktree;
1072
+ _cachedBranch = null;
1073
+ // null = not yet resolved
1074
+ constructor(workspacePath) {
1075
+ this.workspacePath = workspacePath;
1076
+ const mainRoot = resolveStorageRoot(workspacePath);
1077
+ this._isWorktree = mainRoot !== workspacePath;
1078
+ this.storagePath = path6.join(mainRoot, STORAGE_DIR2);
1079
+ this.metaFilePath = path6.join(this.storagePath, META_FILE2);
1080
+ this.sessionsFilePath = path6.join(this.storagePath, SESSIONS_FILE2);
1081
+ this.decisionsFilePath = path6.join(this.storagePath, DECISIONS_FILE);
1082
+ this.stateFilePath = path6.join(this.storagePath, STATE_FILE2);
1083
+ this.currentTasksFilePath = path6.join(this.storagePath, CURRENT_TASKS_FILE2);
1084
+ }
1085
+ /** Check if .keepgoing/ directory exists. */
1086
+ exists() {
1087
+ return fs4.existsSync(this.storagePath);
442
1088
  }
443
- return void 0;
444
- }
445
- async function safeJson(res) {
446
- try {
447
- const text = await res.text();
448
- return JSON.parse(text);
449
- } catch {
450
- return null;
1089
+ /** Read state.json, returns undefined if missing or corrupt. */
1090
+ getState() {
1091
+ return this.readJsonFile(this.stateFilePath);
451
1092
  }
452
- }
453
- async function activateLicense(licenseKey, instanceName, options) {
454
- try {
455
- const res = await fetchWithTimeout(`${BASE_URL}/activate`, {
456
- method: "POST",
457
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
458
- body: new URLSearchParams({ license_key: licenseKey, instance_name: instanceName })
459
- });
460
- const data = await safeJson(res);
461
- if (!res.ok || !data?.activated) {
462
- return { valid: false, error: data?.error || `Activation failed (${res.status})` };
1093
+ /** Read meta.json, returns undefined if missing or corrupt. */
1094
+ getMeta() {
1095
+ return this.readJsonFile(this.metaFilePath);
1096
+ }
1097
+ /**
1098
+ * Read sessions from sessions.json.
1099
+ * Handles both formats:
1100
+ * - Flat array: SessionCheckpoint[] (from ProjectStorage)
1101
+ * - Wrapper object: ProjectSessions (from SessionStorage)
1102
+ */
1103
+ getSessions() {
1104
+ return this.parseSessions().sessions;
1105
+ }
1106
+ /**
1107
+ * Get the most recent session checkpoint.
1108
+ * Uses state.lastSessionId if available, falls back to last in array.
1109
+ */
1110
+ getLastSession() {
1111
+ const { sessions, wrapperLastSessionId } = this.parseSessions();
1112
+ if (sessions.length === 0) {
1113
+ return void 0;
463
1114
  }
464
- if (!options?.allowTestMode && data.license_key?.test_mode) {
465
- if (data.license_key?.key && data.instance?.id) {
466
- await deactivateLicense(data.license_key.key, data.instance.id);
1115
+ const state = this.getState();
1116
+ if (state?.lastSessionId) {
1117
+ const found = sessions.find((s) => s.id === state.lastSessionId);
1118
+ if (found) {
1119
+ return found;
467
1120
  }
468
- return { valid: false, error: "This is a test license key. Please use a production license key from your purchase confirmation." };
469
1121
  }
470
- if (!options?.allowTestMode) {
471
- const productError = validateProductIdentity(data.meta);
472
- if (productError) {
473
- if (data.license_key?.key && data.instance?.id) {
474
- await deactivateLicense(data.license_key.key, data.instance.id);
475
- }
476
- return { valid: false, error: productError };
477
- }
478
- if (data.meta?.variant_id && !KNOWN_VARIANT_IDS.has(data.meta.variant_id)) {
479
- if (data.license_key?.key && data.instance?.id) {
480
- await deactivateLicense(data.license_key.key, data.instance.id);
481
- }
482
- return { valid: false, error: "This license key is for an unrecognized add-on variant. Please update KeepGoing or contact support." };
1122
+ if (wrapperLastSessionId) {
1123
+ const found = sessions.find((s) => s.id === wrapperLastSessionId);
1124
+ if (found) {
1125
+ return found;
483
1126
  }
484
1127
  }
485
- return {
486
- valid: true,
487
- licenseKey: data.license_key?.key,
1128
+ return sessions[sessions.length - 1];
1129
+ }
1130
+ /**
1131
+ * Returns the last N sessions, newest first.
1132
+ */
1133
+ getRecentSessions(count) {
1134
+ return getRecentSessions(this.getSessions(), count);
1135
+ }
1136
+ /** Read all decisions from decisions.json. */
1137
+ getDecisions() {
1138
+ return this.parseDecisions().decisions;
1139
+ }
1140
+ /** Returns the last N decisions, newest first. */
1141
+ getRecentDecisions(count) {
1142
+ const all = this.getDecisions();
1143
+ return all.slice(-count).reverse();
1144
+ }
1145
+ /** Read the multi-license store from `~/.keepgoing/license.json`. */
1146
+ getLicenseStore() {
1147
+ return readLicenseStore();
1148
+ }
1149
+ /**
1150
+ * Read all current tasks from current-tasks.json.
1151
+ * Automatically filters out stale finished sessions (> 2 hours).
1152
+ */
1153
+ getCurrentTasks() {
1154
+ const multiRaw = this.readJsonFile(this.currentTasksFilePath);
1155
+ if (multiRaw) {
1156
+ const tasks = Array.isArray(multiRaw) ? multiRaw : multiRaw.tasks ?? [];
1157
+ return this.pruneStale(tasks);
1158
+ }
1159
+ return [];
1160
+ }
1161
+ /** Get only active sessions (sessionActive=true and within stale threshold). */
1162
+ getActiveTasks() {
1163
+ return this.getCurrentTasks().filter((t) => t.sessionActive);
1164
+ }
1165
+ /** Get a specific session by ID. */
1166
+ getTaskBySessionId(sessionId) {
1167
+ return this.getCurrentTasks().find((t) => t.sessionId === sessionId);
1168
+ }
1169
+ /**
1170
+ * Detect files being edited by multiple sessions simultaneously.
1171
+ * Returns pairs of session IDs and the conflicting file paths.
1172
+ */
1173
+ detectFileConflicts() {
1174
+ const activeTasks = this.getActiveTasks();
1175
+ if (activeTasks.length < 2) return [];
1176
+ const fileToSessions = /* @__PURE__ */ new Map();
1177
+ for (const task of activeTasks) {
1178
+ if (task.lastFileEdited && task.sessionId) {
1179
+ const existing = fileToSessions.get(task.lastFileEdited) ?? [];
1180
+ existing.push({
1181
+ sessionId: task.sessionId,
1182
+ agentLabel: task.agentLabel,
1183
+ branch: task.branch
1184
+ });
1185
+ fileToSessions.set(task.lastFileEdited, existing);
1186
+ }
1187
+ }
1188
+ const conflicts = [];
1189
+ for (const [file, sessions] of fileToSessions) {
1190
+ if (sessions.length > 1) {
1191
+ conflicts.push({ file, sessions });
1192
+ }
1193
+ }
1194
+ return conflicts;
1195
+ }
1196
+ /**
1197
+ * Detect sessions on the same branch (possible duplicate work).
1198
+ */
1199
+ detectBranchOverlap() {
1200
+ const activeTasks = this.getActiveTasks();
1201
+ if (activeTasks.length < 2) return [];
1202
+ const branchToSessions = /* @__PURE__ */ new Map();
1203
+ for (const task of activeTasks) {
1204
+ if (task.branch && task.sessionId) {
1205
+ const existing = branchToSessions.get(task.branch) ?? [];
1206
+ existing.push({ sessionId: task.sessionId, agentLabel: task.agentLabel });
1207
+ branchToSessions.set(task.branch, existing);
1208
+ }
1209
+ }
1210
+ const overlaps = [];
1211
+ for (const [branch, sessions] of branchToSessions) {
1212
+ if (sessions.length > 1) {
1213
+ overlaps.push({ branch, sessions });
1214
+ }
1215
+ }
1216
+ return overlaps;
1217
+ }
1218
+ pruneStale(tasks) {
1219
+ return pruneStaleTasks(tasks);
1220
+ }
1221
+ /** Get the last session checkpoint for a specific branch. */
1222
+ getLastSessionForBranch(branch) {
1223
+ const sessions = this.getSessions().filter((s) => s.gitBranch === branch);
1224
+ return sessions.length > 0 ? sessions[sessions.length - 1] : void 0;
1225
+ }
1226
+ /** Returns the last N sessions for a specific branch, newest first. */
1227
+ getRecentSessionsForBranch(branch, count) {
1228
+ const filtered = this.getSessions().filter((s) => s.gitBranch === branch);
1229
+ return filtered.slice(-count).reverse();
1230
+ }
1231
+ /** Returns the last N decisions for a specific branch, newest first. */
1232
+ getRecentDecisionsForBranch(branch, count) {
1233
+ const filtered = this.getDecisions().filter((d) => d.gitBranch === branch);
1234
+ return filtered.slice(-count).reverse();
1235
+ }
1236
+ /** Whether the workspace is inside a git worktree. */
1237
+ get isWorktree() {
1238
+ return this._isWorktree;
1239
+ }
1240
+ /**
1241
+ * Returns the current git branch for this workspace.
1242
+ * Lazily cached: the branch is resolved once per KeepGoingReader instance.
1243
+ */
1244
+ getCurrentBranch() {
1245
+ if (this._cachedBranch === null) {
1246
+ this._cachedBranch = getCurrentBranch(this.workspacePath);
1247
+ }
1248
+ return this._cachedBranch;
1249
+ }
1250
+ /**
1251
+ * Worktree-aware last session lookup.
1252
+ * In a worktree, scopes to the current branch with fallback to global.
1253
+ * Returns the session and whether it fell back to global.
1254
+ */
1255
+ getScopedLastSession() {
1256
+ const branch = this.getCurrentBranch();
1257
+ if (this._isWorktree && branch) {
1258
+ const scoped = this.getLastSessionForBranch(branch);
1259
+ if (scoped) return { session: scoped, isFallback: false };
1260
+ return { session: this.getLastSession(), isFallback: true };
1261
+ }
1262
+ return { session: this.getLastSession(), isFallback: false };
1263
+ }
1264
+ /** Worktree-aware recent sessions. Scopes to current branch in a worktree. */
1265
+ getScopedRecentSessions(count) {
1266
+ const branch = this.getCurrentBranch();
1267
+ if (this._isWorktree && branch) {
1268
+ return this.getRecentSessionsForBranch(branch, count);
1269
+ }
1270
+ return this.getRecentSessions(count);
1271
+ }
1272
+ /** Worktree-aware recent decisions. Scopes to current branch in a worktree. */
1273
+ getScopedRecentDecisions(count) {
1274
+ const branch = this.getCurrentBranch();
1275
+ if (this._isWorktree && branch) {
1276
+ return this.getRecentDecisionsForBranch(branch, count);
1277
+ }
1278
+ return this.getRecentDecisions(count);
1279
+ }
1280
+ /**
1281
+ * Resolves branch scope from an explicit `branch` parameter.
1282
+ * Used by tools that accept a `branch` argument (e.g. get_session_history, get_decisions).
1283
+ * - `"all"` returns no filter.
1284
+ * - An explicit branch name uses that.
1285
+ * - `undefined` auto-scopes to the current branch in a worktree, or all branches otherwise.
1286
+ */
1287
+ resolveBranchScope(branch) {
1288
+ if (branch === "all") {
1289
+ return { effectiveBranch: void 0, scopeLabel: "all branches" };
1290
+ }
1291
+ if (branch) {
1292
+ return { effectiveBranch: branch, scopeLabel: `branch \`${branch}\`` };
1293
+ }
1294
+ const currentBranch = this.getCurrentBranch();
1295
+ if (this._isWorktree && currentBranch) {
1296
+ return { effectiveBranch: currentBranch, scopeLabel: `branch \`${currentBranch}\` (worktree)` };
1297
+ }
1298
+ return { effectiveBranch: void 0, scopeLabel: "all branches" };
1299
+ }
1300
+ /**
1301
+ * Parses sessions.json once, returning both the session list
1302
+ * and the optional lastSessionId from a ProjectSessions wrapper.
1303
+ */
1304
+ parseSessions() {
1305
+ const raw = this.readJsonFile(
1306
+ this.sessionsFilePath
1307
+ );
1308
+ if (!raw) {
1309
+ return { sessions: [] };
1310
+ }
1311
+ if (Array.isArray(raw)) {
1312
+ return { sessions: raw };
1313
+ }
1314
+ return { sessions: raw.sessions ?? [], wrapperLastSessionId: raw.lastSessionId };
1315
+ }
1316
+ parseDecisions() {
1317
+ const raw = this.readJsonFile(this.decisionsFilePath);
1318
+ if (!raw) {
1319
+ return { decisions: [] };
1320
+ }
1321
+ return { decisions: raw.decisions ?? [], lastDecisionId: raw.lastDecisionId };
1322
+ }
1323
+ readJsonFile(filePath) {
1324
+ try {
1325
+ if (!fs4.existsSync(filePath)) {
1326
+ return void 0;
1327
+ }
1328
+ const raw = fs4.readFileSync(filePath, "utf-8");
1329
+ return JSON.parse(raw);
1330
+ } catch {
1331
+ return void 0;
1332
+ }
1333
+ }
1334
+ };
1335
+
1336
+ // ../../packages/shared/src/setup.ts
1337
+ import fs5 from "fs";
1338
+ import os2 from "os";
1339
+ import path7 from "path";
1340
+ var KEEPGOING_MARKER = "@keepgoingdev/mcp-server";
1341
+ var SESSION_START_HOOK = {
1342
+ matcher: "",
1343
+ hooks: [
1344
+ {
1345
+ type: "command",
1346
+ command: "npx -y @keepgoingdev/mcp-server --print-momentum"
1347
+ }
1348
+ ]
1349
+ };
1350
+ var STOP_HOOK = {
1351
+ matcher: "",
1352
+ hooks: [
1353
+ {
1354
+ type: "command",
1355
+ command: "npx -y @keepgoingdev/mcp-server --save-checkpoint"
1356
+ }
1357
+ ]
1358
+ };
1359
+ var POST_TOOL_USE_HOOK = {
1360
+ matcher: "Edit|Write|MultiEdit",
1361
+ hooks: [
1362
+ {
1363
+ type: "command",
1364
+ command: "npx -y @keepgoingdev/mcp-server --update-task-from-hook"
1365
+ }
1366
+ ]
1367
+ };
1368
+ var CLAUDE_MD_SECTION = `
1369
+ ## KeepGoing
1370
+
1371
+ After completing a task or meaningful piece of work, call the \`save_checkpoint\` MCP tool with:
1372
+ - \`summary\`: What you accomplished
1373
+ - \`nextStep\`: What should be done next
1374
+ - \`blocker\`: Any blocker (if applicable)
1375
+ `;
1376
+ var STATUSLINE_CMD = "npx -y @keepgoingdev/mcp-server --statusline";
1377
+ function hasKeepGoingHook(hookEntries) {
1378
+ return hookEntries.some(
1379
+ (entry) => entry?.hooks?.some((h) => typeof h?.command === "string" && h.command.includes(KEEPGOING_MARKER))
1380
+ );
1381
+ }
1382
+ function resolveScopePaths(scope, workspacePath) {
1383
+ if (scope === "user") {
1384
+ const claudeDir2 = path7.join(os2.homedir(), ".claude");
1385
+ return {
1386
+ claudeDir: claudeDir2,
1387
+ settingsPath: path7.join(claudeDir2, "settings.json"),
1388
+ claudeMdPath: path7.join(claudeDir2, "CLAUDE.md")
1389
+ };
1390
+ }
1391
+ const claudeDir = path7.join(workspacePath, ".claude");
1392
+ const dotClaudeMdPath = path7.join(workspacePath, ".claude", "CLAUDE.md");
1393
+ const rootClaudeMdPath = path7.join(workspacePath, "CLAUDE.md");
1394
+ return {
1395
+ claudeDir,
1396
+ settingsPath: path7.join(claudeDir, "settings.json"),
1397
+ claudeMdPath: fs5.existsSync(dotClaudeMdPath) ? dotClaudeMdPath : rootClaudeMdPath
1398
+ };
1399
+ }
1400
+ function writeHooksToSettings(settings) {
1401
+ let changed = false;
1402
+ if (!settings.hooks) {
1403
+ settings.hooks = {};
1404
+ }
1405
+ if (!Array.isArray(settings.hooks.SessionStart)) {
1406
+ settings.hooks.SessionStart = [];
1407
+ }
1408
+ if (!hasKeepGoingHook(settings.hooks.SessionStart)) {
1409
+ settings.hooks.SessionStart.push(SESSION_START_HOOK);
1410
+ changed = true;
1411
+ }
1412
+ if (!Array.isArray(settings.hooks.Stop)) {
1413
+ settings.hooks.Stop = [];
1414
+ }
1415
+ if (!hasKeepGoingHook(settings.hooks.Stop)) {
1416
+ settings.hooks.Stop.push(STOP_HOOK);
1417
+ changed = true;
1418
+ }
1419
+ if (!Array.isArray(settings.hooks.PostToolUse)) {
1420
+ settings.hooks.PostToolUse = [];
1421
+ }
1422
+ if (!hasKeepGoingHook(settings.hooks.PostToolUse)) {
1423
+ settings.hooks.PostToolUse.push(POST_TOOL_USE_HOOK);
1424
+ changed = true;
1425
+ }
1426
+ return changed;
1427
+ }
1428
+ function checkHookConflict(scope, workspacePath) {
1429
+ const otherPaths = resolveScopePaths(scope === "user" ? "project" : "user", workspacePath);
1430
+ if (!fs5.existsSync(otherPaths.settingsPath)) {
1431
+ return null;
1432
+ }
1433
+ try {
1434
+ const otherSettings = JSON.parse(fs5.readFileSync(otherPaths.settingsPath, "utf-8"));
1435
+ const hooks = otherSettings?.hooks;
1436
+ if (!hooks) return null;
1437
+ const hasConflict = Array.isArray(hooks.SessionStart) && hasKeepGoingHook(hooks.SessionStart) || Array.isArray(hooks.Stop) && hasKeepGoingHook(hooks.Stop);
1438
+ if (hasConflict) {
1439
+ const otherScope = scope === "user" ? "project" : "user";
1440
+ const otherFile = otherPaths.settingsPath;
1441
+ 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.`;
1442
+ }
1443
+ } catch {
1444
+ }
1445
+ return null;
1446
+ }
1447
+ function setupProject(options) {
1448
+ const {
1449
+ workspacePath,
1450
+ scope = "project",
1451
+ sessionHooks = true,
1452
+ claudeMd = true,
1453
+ hasProLicense = false,
1454
+ statusline
1455
+ } = options;
1456
+ const messages = [];
1457
+ let changed = false;
1458
+ const { claudeDir, settingsPath, claudeMdPath } = resolveScopePaths(scope, workspacePath);
1459
+ const scopeLabel = scope === "user" ? "~/.claude/settings.json" : ".claude/settings.json";
1460
+ let settings = {};
1461
+ if (fs5.existsSync(settingsPath)) {
1462
+ settings = JSON.parse(fs5.readFileSync(settingsPath, "utf-8"));
1463
+ }
1464
+ let settingsChanged = false;
1465
+ if (sessionHooks) {
1466
+ const hooksChanged = writeHooksToSettings(settings);
1467
+ settingsChanged = hooksChanged;
1468
+ if (hooksChanged) {
1469
+ messages.push(`Session hooks: Added to ${scopeLabel}`);
1470
+ } else {
1471
+ messages.push("Session hooks: Already present, skipped");
1472
+ }
1473
+ const conflict = checkHookConflict(scope, workspacePath);
1474
+ if (conflict) {
1475
+ messages.push(`Warning: ${conflict}`);
1476
+ }
1477
+ }
1478
+ if (scope === "project" && hasProLicense) {
1479
+ const needsUpdate = settings.statusLine?.command && statusline?.isLegacy?.(settings.statusLine.command);
1480
+ if (!settings.statusLine || needsUpdate) {
1481
+ settings.statusLine = {
1482
+ type: "command",
1483
+ command: STATUSLINE_CMD
1484
+ };
1485
+ settingsChanged = true;
1486
+ messages.push(needsUpdate ? "Statusline: Migrated to auto-updating npx command" : "Statusline: Added to .claude/settings.json");
1487
+ } else {
1488
+ messages.push("Statusline: Already configured in settings, skipped");
1489
+ }
1490
+ statusline?.cleanup?.();
1491
+ }
1492
+ if (settingsChanged) {
1493
+ if (!fs5.existsSync(claudeDir)) {
1494
+ fs5.mkdirSync(claudeDir, { recursive: true });
1495
+ }
1496
+ fs5.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
1497
+ changed = true;
1498
+ }
1499
+ if (claudeMd) {
1500
+ let existing = "";
1501
+ if (fs5.existsSync(claudeMdPath)) {
1502
+ existing = fs5.readFileSync(claudeMdPath, "utf-8");
1503
+ }
1504
+ const mdLabel = scope === "user" ? "~/.claude/CLAUDE.md" : "CLAUDE.md";
1505
+ if (existing.includes("## KeepGoing")) {
1506
+ messages.push(`CLAUDE.md: KeepGoing section already present in ${mdLabel}, skipped`);
1507
+ } else {
1508
+ const updated = existing + CLAUDE_MD_SECTION;
1509
+ const mdDir = path7.dirname(claudeMdPath);
1510
+ if (!fs5.existsSync(mdDir)) {
1511
+ fs5.mkdirSync(mdDir, { recursive: true });
1512
+ }
1513
+ fs5.writeFileSync(claudeMdPath, updated);
1514
+ changed = true;
1515
+ messages.push(`CLAUDE.md: Added KeepGoing section to ${mdLabel}`);
1516
+ }
1517
+ }
1518
+ return { messages, changed };
1519
+ }
1520
+
1521
+ // ../../packages/shared/src/licenseClient.ts
1522
+ var BASE_URL = "https://api.lemonsqueezy.com/v1/licenses";
1523
+ var REQUEST_TIMEOUT_MS = 15e3;
1524
+ var EXPECTED_STORE_ID = 301555;
1525
+ var EXPECTED_PRODUCT_ID = 864311;
1526
+ function fetchWithTimeout(url, init) {
1527
+ const controller = new AbortController();
1528
+ const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
1529
+ return fetch(url, { ...init, signal: controller.signal }).finally(() => clearTimeout(timer));
1530
+ }
1531
+ function validateProductIdentity(meta) {
1532
+ if (!meta) return "License response missing product metadata.";
1533
+ if (meta.store_id !== EXPECTED_STORE_ID || meta.product_id !== EXPECTED_PRODUCT_ID) {
1534
+ return "This license key does not belong to KeepGoing.";
1535
+ }
1536
+ return void 0;
1537
+ }
1538
+ async function safeJson(res) {
1539
+ try {
1540
+ const text = await res.text();
1541
+ return JSON.parse(text);
1542
+ } catch {
1543
+ return null;
1544
+ }
1545
+ }
1546
+ async function activateLicense(licenseKey, instanceName, options) {
1547
+ try {
1548
+ const res = await fetchWithTimeout(`${BASE_URL}/activate`, {
1549
+ method: "POST",
1550
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1551
+ body: new URLSearchParams({ license_key: licenseKey, instance_name: instanceName })
1552
+ });
1553
+ const data = await safeJson(res);
1554
+ if (!res.ok || !data?.activated) {
1555
+ return { valid: false, error: data?.error || `Activation failed (${res.status})` };
1556
+ }
1557
+ if (!options?.allowTestMode && data.license_key?.test_mode) {
1558
+ if (data.license_key?.key && data.instance?.id) {
1559
+ await deactivateLicense(data.license_key.key, data.instance.id);
1560
+ }
1561
+ return { valid: false, error: "This is a test license key. Please use a production license key from your purchase confirmation." };
1562
+ }
1563
+ if (!options?.allowTestMode) {
1564
+ const productError = validateProductIdentity(data.meta);
1565
+ if (productError) {
1566
+ if (data.license_key?.key && data.instance?.id) {
1567
+ await deactivateLicense(data.license_key.key, data.instance.id);
1568
+ }
1569
+ return { valid: false, error: productError };
1570
+ }
1571
+ if (data.meta?.variant_id && !KNOWN_VARIANT_IDS.has(data.meta.variant_id)) {
1572
+ if (data.license_key?.key && data.instance?.id) {
1573
+ await deactivateLicense(data.license_key.key, data.instance.id);
1574
+ }
1575
+ return { valid: false, error: "This license key is for an unrecognized add-on variant. Please update KeepGoing or contact support." };
1576
+ }
1577
+ }
1578
+ return {
1579
+ valid: true,
1580
+ licenseKey: data.license_key?.key,
488
1581
  instanceId: data.instance?.id,
489
1582
  customerName: data.meta?.customer_name,
490
1583
  productName: data.meta?.product_name,
@@ -514,78 +1607,14 @@ async function deactivateLicense(licenseKey, instanceId) {
514
1607
  }
515
1608
  }
516
1609
 
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
1610
  // src/render.ts
584
1611
  var RESET = "\x1B[0m";
585
1612
  var BOLD = "\x1B[1m";
586
1613
  var DIM = "\x1B[2m";
1614
+ var GREEN = "\x1B[32m";
587
1615
  var YELLOW = "\x1B[33m";
588
1616
  var CYAN = "\x1B[36m";
1617
+ var MAGENTA = "\x1B[35m";
589
1618
  function renderCheckpoint(checkpoint, daysSince) {
590
1619
  const relTime = formatRelativeTime(checkpoint.timestamp);
591
1620
  if (daysSince !== void 0 && daysSince >= 7) {
@@ -626,18 +1655,279 @@ function renderNoData() {
626
1655
  `No KeepGoing data found. Run ${BOLD}keepgoing save${RESET} to save your first checkpoint.`
627
1656
  );
628
1657
  }
1658
+ function renderMomentum(checkpoint, ctx) {
1659
+ const relTime = formatRelativeTime(checkpoint.timestamp);
1660
+ const label = (s) => `${CYAN}${s}${RESET}`;
1661
+ console.log(`
1662
+ ${BOLD}KeepGoing Momentum${RESET} \xB7 ${DIM}${relTime}${RESET}
1663
+ `);
1664
+ if (ctx.isWorktree && ctx.currentBranch) {
1665
+ console.log(` ${DIM}Worktree: scoped to ${ctx.currentBranch}${RESET}`);
1666
+ if (ctx.isFallback) {
1667
+ console.log(` ${YELLOW}No checkpoints for this branch, showing last global checkpoint${RESET}`);
1668
+ }
1669
+ console.log("");
1670
+ }
1671
+ if (checkpoint.summary) {
1672
+ console.log(` ${label("Summary:")} ${checkpoint.summary}`);
1673
+ }
1674
+ if (checkpoint.nextStep) {
1675
+ console.log(` ${label("Next step:")} ${checkpoint.nextStep}`);
1676
+ }
1677
+ if (checkpoint.blocker) {
1678
+ console.log(` ${label("Blocker:")} ${YELLOW}${checkpoint.blocker}${RESET}`);
1679
+ }
1680
+ if (checkpoint.projectIntent) {
1681
+ console.log(` ${label("Intent:")} ${checkpoint.projectIntent}`);
1682
+ }
1683
+ if (ctx.currentBranch) {
1684
+ console.log(` ${label("Branch:")} ${ctx.currentBranch}`);
1685
+ }
1686
+ if (ctx.branchChanged && !ctx.isWorktree) {
1687
+ console.log(` ${YELLOW}\u26A0 Branch changed since last checkpoint (was ${checkpoint.gitBranch})${RESET}`);
1688
+ }
1689
+ if (checkpoint.touchedFiles && checkpoint.touchedFiles.length > 0) {
1690
+ const MAX_FILES = 5;
1691
+ const shown = checkpoint.touchedFiles.slice(0, MAX_FILES).join(", ");
1692
+ const extra = checkpoint.touchedFiles.length - MAX_FILES;
1693
+ const filesStr = extra > 0 ? `${shown} (+${extra} more)` : shown;
1694
+ console.log(` ${label("Files:")} ${filesStr}`);
1695
+ }
1696
+ if (ctx.derivedFocus) {
1697
+ console.log(` ${label("Focus:")} ${ctx.derivedFocus}`);
1698
+ }
1699
+ console.log("");
1700
+ }
1701
+ function renderMomentumQuiet(checkpoint) {
1702
+ const relTime = formatRelativeTime(checkpoint.timestamp);
1703
+ const summary = checkpoint.summary || checkpoint.nextStep || "no momentum data";
1704
+ console.log(`KeepGoing \xB7 ${relTime} \xB7 ${summary}`);
1705
+ }
1706
+ function renderSaveConfirmation(summary, fileCount, branch) {
1707
+ const parts = [];
1708
+ if (fileCount > 0) parts.push(`${fileCount} file${fileCount === 1 ? "" : "s"}`);
1709
+ if (branch) parts.push(branch);
1710
+ const meta = parts.length > 0 ? ` ${DIM}(${parts.join(", ")})${RESET}` : "";
1711
+ console.log(`${GREEN}\u2714 Saved:${RESET} ${summary}${meta}`);
1712
+ }
1713
+ function renderDecisions(decisions, scopeLabel) {
1714
+ const label = (s) => `${CYAN}${s}${RESET}`;
1715
+ console.log(`
1716
+ ${BOLD}KeepGoing Decisions${RESET} ${DIM}(last ${decisions.length}, ${scopeLabel})${RESET}
1717
+ `);
1718
+ for (const d of decisions) {
1719
+ const relTime = formatRelativeTime(d.timestamp);
1720
+ const confidence = `${(d.classification.confidence * 100).toFixed(0)}%`;
1721
+ console.log(` ${label(d.classification.category + ":")} ${d.commitMessage} ${DIM}${confidence} \xB7 ${relTime}${RESET}`);
1722
+ if (d.classification.reasons.length > 0) {
1723
+ console.log(` ${DIM}Signals: ${d.classification.reasons.join("; ")}${RESET}`);
1724
+ }
1725
+ }
1726
+ console.log("");
1727
+ }
1728
+ function renderDecisionsQuiet(decisions) {
1729
+ const latest = decisions[0];
1730
+ if (!latest) return;
1731
+ const relTime = formatRelativeTime(latest.timestamp);
1732
+ console.log(`KeepGoing \xB7 ${decisions.length} decision${decisions.length === 1 ? "" : "s"} \xB7 latest: ${latest.classification.category}: ${latest.commitMessage} (${relTime})`);
1733
+ }
1734
+ function formatDuration(minutes) {
1735
+ if (minutes < 60) return `${minutes}m`;
1736
+ const h = Math.floor(minutes / 60);
1737
+ const m = minutes % 60;
1738
+ return m > 0 ? `${h}h ${m}m` : `${h}h`;
1739
+ }
1740
+ function renderLogSession(session, showStat) {
1741
+ const relTime = formatRelativeTime(session.timestamp);
1742
+ const branch = session.gitBranch ? ` ${GREEN}(${session.gitBranch})${RESET}` : "";
1743
+ const source = session.source ? ` ${DIM}[${session.source}]${RESET}` : "";
1744
+ console.log(`${BOLD}\u25CF${RESET} ${DIM}${relTime}${RESET}${branch}${source}`);
1745
+ if (session.summary) {
1746
+ console.log(` ${session.summary}`);
1747
+ }
1748
+ if (session.nextStep) {
1749
+ console.log(` ${CYAN}\u2192 Next:${RESET} ${session.nextStep}`);
1750
+ }
1751
+ const parts = [];
1752
+ if (session.sessionDuration) {
1753
+ parts.push(`\u23F1 ${formatDuration(session.sessionDuration)}`);
1754
+ }
1755
+ if (session.touchedFiles && session.touchedFiles.length > 0) {
1756
+ parts.push(`${session.touchedFiles.length} file${session.touchedFiles.length !== 1 ? "s" : ""}`);
1757
+ }
1758
+ if (parts.length > 0) {
1759
+ console.log(` ${DIM}${parts.join(" \xB7 ")}${RESET}`);
1760
+ }
1761
+ if (session.blocker) {
1762
+ console.log(` ${YELLOW}\u26A0 Blocker: ${session.blocker}${RESET}`);
1763
+ }
1764
+ if (showStat && session.touchedFiles && session.touchedFiles.length > 0) {
1765
+ for (const f of session.touchedFiles) {
1766
+ console.log(` ${DIM}${f}${RESET}`);
1767
+ }
1768
+ }
1769
+ }
1770
+ function renderLogSessionOneline(session) {
1771
+ const id = session.id.slice(0, 7);
1772
+ const relTime = formatRelativeTime(session.timestamp);
1773
+ const branch = session.gitBranch ? ` ${GREEN}(${session.gitBranch})${RESET}` : "";
1774
+ const summary = session.summary || session.nextStep || "checkpoint";
1775
+ console.log(`${DIM}${id}${RESET} ${relTime}${branch} ${summary}`);
1776
+ }
1777
+ function renderLogDecision(decision, showStat) {
1778
+ const relTime = formatRelativeTime(decision.timestamp);
1779
+ const branch = decision.gitBranch ? ` ${GREEN}(${decision.gitBranch})${RESET}` : "";
1780
+ const cat = decision.classification.category;
1781
+ const conf = Math.round(decision.classification.confidence * 100);
1782
+ const tag = `${MAGENTA}[${cat} \xB7 ${conf}%]${RESET}`;
1783
+ console.log(`${BOLD}\u25C6${RESET} ${DIM}${relTime}${RESET}${branch} ${tag}`);
1784
+ console.log(` ${decision.commitMessage}`);
1785
+ if (decision.classification.reasons.length > 0) {
1786
+ console.log(` ${DIM}Signals: ${decision.classification.reasons.join("; ")}${RESET}`);
1787
+ }
1788
+ if (decision.filesChanged.length > 0) {
1789
+ console.log(` ${DIM}${decision.filesChanged.length} file${decision.filesChanged.length !== 1 ? "s" : ""} changed${RESET}`);
1790
+ }
1791
+ if (showStat && decision.filesChanged.length > 0) {
1792
+ for (const f of decision.filesChanged) {
1793
+ console.log(` ${DIM}${f}${RESET}`);
1794
+ }
1795
+ }
1796
+ }
1797
+ function renderLogDecisionOneline(decision) {
1798
+ const id = decision.id.slice(0, 7);
1799
+ const relTime = formatRelativeTime(decision.timestamp);
1800
+ const cat = decision.classification.category;
1801
+ console.log(`${DIM}${id}${RESET} ${relTime} ${MAGENTA}[${cat}]${RESET} ${decision.commitMessage}`);
1802
+ }
1803
+ function renderSessionGroupHeader(sessionId, count) {
1804
+ const shortId = sessionId.slice(0, 8);
1805
+ console.log(`${BOLD}Session ${shortId}${RESET} ${DIM}(${count} checkpoint${count !== 1 ? "s" : ""})${RESET}`);
1806
+ }
1807
+ function renderContinueOn(prompt, copied, target) {
1808
+ console.log(`
1809
+ ${BOLD}KeepGoing Continue On${RESET}
1810
+ `);
1811
+ if (copied) {
1812
+ console.log(`${GREEN}\u2714 Context copied to clipboard${RESET}`);
1813
+ } else {
1814
+ console.log(`${YELLOW}\u26A0 Could not copy to clipboard. Prompt printed below.${RESET}`);
1815
+ }
1816
+ if (target) {
1817
+ console.log(`${DIM}Target: ${target}${RESET}`);
1818
+ }
1819
+ console.log("");
1820
+ console.log(`${DIM}--- prompt start ---${RESET}`);
1821
+ console.log(prompt);
1822
+ console.log(`${DIM}--- prompt end ---${RESET}`);
1823
+ console.log("");
1824
+ }
1825
+ function renderContinueOnQuiet(copied, target) {
1826
+ const targetStr = target ? ` (${target})` : "";
1827
+ if (copied) {
1828
+ console.log(`KeepGoing \xB7 Context copied to clipboard${targetStr}`);
1829
+ } else {
1830
+ console.log(`KeepGoing \xB7 Failed to copy context to clipboard${targetStr}`);
1831
+ }
1832
+ }
1833
+ function renderEnrichedBriefing(briefing) {
1834
+ const { tier, core } = briefing;
1835
+ const label = (s) => `${CYAN}${s}${RESET}`;
1836
+ console.log(`
1837
+ ${BOLD}KeepGoing Re-entry Briefing${RESET} ${DIM}(${tier})${RESET}
1838
+ `);
1839
+ if (briefing.isWorktree && briefing.gitBranch) {
1840
+ console.log(` ${DIM}Worktree: scoped to ${briefing.gitBranch}${RESET}
1841
+ `);
1842
+ }
1843
+ if (tier === "compact") {
1844
+ console.log(` ${label("Last worked:")} ${core.lastWorked}`);
1845
+ console.log(` ${label("Focus:")} ${core.currentFocus}`);
1846
+ console.log(` ${label("Quick start:")} ${core.smallNextStep}`);
1847
+ console.log("");
1848
+ return;
1849
+ }
1850
+ console.log(` ${label("Last worked:")} ${core.lastWorked}`);
1851
+ console.log(` ${label("Focus:")} ${core.currentFocus}`);
1852
+ console.log(` ${label("Activity:")} ${core.recentActivity}`);
1853
+ console.log(` ${label("Next:")} ${core.suggestedNext}`);
1854
+ console.log(` ${label("Quick start:")} ${core.smallNextStep}`);
1855
+ if (briefing.blocker) {
1856
+ console.log(` ${label("Blocker:")} ${YELLOW}${briefing.blocker}${RESET}`);
1857
+ }
1858
+ if (briefing.decisions && briefing.decisions.length > 0) {
1859
+ console.log(`
1860
+ ${label("Recent decisions:")}`);
1861
+ for (const d of briefing.decisions) {
1862
+ const relTime = formatRelativeTime(d.timestamp);
1863
+ console.log(` ${d.classification.category}: ${d.commitMessage} ${DIM}(${relTime})${RESET}`);
1864
+ }
1865
+ }
1866
+ if (briefing.touchedFiles && briefing.touchedFiles.length > 0) {
1867
+ console.log(`
1868
+ ${label(`Files touched (${briefing.touchedFiles.length}):`)}`);
1869
+ const MAX_FILES = tier === "full" ? 20 : 10;
1870
+ const shown = briefing.touchedFiles.slice(0, MAX_FILES);
1871
+ for (const f of shown) {
1872
+ console.log(` ${DIM}${f}${RESET}`);
1873
+ }
1874
+ if (briefing.touchedFiles.length > MAX_FILES) {
1875
+ console.log(` ${DIM}...and ${briefing.touchedFiles.length - MAX_FILES} more${RESET}`);
1876
+ }
1877
+ }
1878
+ if (briefing.sessionHistory && briefing.sessionHistory.length > 0) {
1879
+ console.log(`
1880
+ ${label("Session history:")}`);
1881
+ for (const s of briefing.sessionHistory) {
1882
+ const relTime = formatRelativeTime(s.timestamp);
1883
+ const branch = s.branch ? ` ${GREEN}(${s.branch})${RESET}` : "";
1884
+ console.log(` ${DIM}${relTime}${branch}${RESET} ${s.summary || "No summary"}`);
1885
+ if (s.nextStep) {
1886
+ console.log(` ${CYAN}\u2192${RESET} ${s.nextStep}`);
1887
+ }
1888
+ }
1889
+ }
1890
+ if (briefing.recentCommits && briefing.recentCommits.length > 0) {
1891
+ console.log(`
1892
+ ${label("Recent commits:")}`);
1893
+ for (const msg of briefing.recentCommits.slice(0, 10)) {
1894
+ console.log(` ${DIM}${msg}${RESET}`);
1895
+ }
1896
+ }
1897
+ if (briefing.fileConflicts && briefing.fileConflicts.length > 0) {
1898
+ console.log(`
1899
+ ${YELLOW}File conflicts:${RESET}`);
1900
+ for (const c of briefing.fileConflicts) {
1901
+ const labels = c.sessions.map((s) => s.agentLabel || s.sessionId).join(", ");
1902
+ console.log(` ${c.file}: ${labels}`);
1903
+ }
1904
+ }
1905
+ if (briefing.branchOverlaps && briefing.branchOverlaps.length > 0) {
1906
+ console.log(`
1907
+ ${YELLOW}Branch overlaps:${RESET}`);
1908
+ for (const o of briefing.branchOverlaps) {
1909
+ const labels = o.sessions.map((s) => s.agentLabel || s.sessionId).join(", ");
1910
+ console.log(` ${o.branch}: ${labels}`);
1911
+ }
1912
+ }
1913
+ console.log("");
1914
+ }
1915
+ function renderEnrichedBriefingQuiet(briefing) {
1916
+ const { core } = briefing;
1917
+ console.log(`KeepGoing \xB7 ${core.lastWorked} \xB7 Focus: ${core.currentFocus} \xB7 Next: ${core.smallNextStep}`);
1918
+ }
629
1919
 
630
1920
  // src/updateCheck.ts
631
1921
  import { spawn } from "child_process";
632
1922
  import { readFileSync, existsSync } from "fs";
633
- import path6 from "path";
634
- import os2 from "os";
635
- var CLI_VERSION = "0.3.3";
1923
+ import path8 from "path";
1924
+ import os3 from "os";
1925
+ var CLI_VERSION = "1.1.0";
636
1926
  var NPM_REGISTRY_URL = "https://registry.npmjs.org/@keepgoingdev/cli/latest";
637
1927
  var FETCH_TIMEOUT_MS = 5e3;
638
1928
  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");
1929
+ var CACHE_DIR = path8.join(os3.homedir(), ".keepgoing");
1930
+ var CACHE_PATH = path8.join(CACHE_DIR, "update-check.json");
641
1931
  function isNewerVersion(current, latest) {
642
1932
  const cur = current.split(".").map(Number);
643
1933
  const lat = latest.split(".").map(Number);
@@ -752,61 +2042,73 @@ async function statusCommand(opts) {
752
2042
  }
753
2043
 
754
2044
  // 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
- }
2045
+ import path9 from "path";
764
2046
  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
- }
2047
+ const { cwd, message, nextStepOverride, json, quiet, force } = opts;
2048
+ const isManual = !!message;
2049
+ const reader = new KeepGoingReader(cwd);
2050
+ const { session: lastSession } = reader.getScopedLastSession();
2051
+ if (!force && !isManual && lastSession?.timestamp) {
2052
+ const ageMs = Date.now() - new Date(lastSession.timestamp).getTime();
2053
+ if (ageMs < 2 * 60 * 1e3) {
2054
+ return;
784
2055
  }
785
- blocker = await prompt(rl, "Any blockers? (leave empty to skip) ");
786
- } finally {
787
- rl.close();
788
2056
  }
789
- const gitBranch = getCurrentBranch(opts.cwd);
790
- const touchedFiles = getTouchedFiles(opts.cwd);
2057
+ const touchedFiles = getTouchedFiles(cwd);
2058
+ const commitHashes = getCommitsSince(cwd, lastSession?.timestamp);
2059
+ if (!force && !isManual && touchedFiles.length === 0 && commitHashes.length === 0) {
2060
+ return;
2061
+ }
2062
+ const gitBranch = getCurrentBranch(cwd);
2063
+ const commitMessages = getCommitMessagesSince(cwd, lastSession?.timestamp);
2064
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2065
+ const events = buildSessionEvents({
2066
+ wsPath: cwd,
2067
+ commitHashes,
2068
+ commitMessages,
2069
+ touchedFiles,
2070
+ currentBranch: gitBranch ?? void 0,
2071
+ sessionStartTime: lastSession?.timestamp ?? now,
2072
+ lastActivityTime: now
2073
+ });
2074
+ const summary = message ?? buildSmartSummary(events) ?? `Worked on ${touchedFiles.slice(0, 5).map((f) => path9.basename(f)).join(", ")}`;
2075
+ const nextStep = nextStepOverride ?? buildSmartNextStep(events);
2076
+ const projectName = path9.basename(resolveStorageRoot(cwd));
2077
+ const sessionId = generateSessionId({
2078
+ workspaceRoot: cwd,
2079
+ branch: gitBranch ?? void 0,
2080
+ worktreePath: cwd
2081
+ });
791
2082
  const checkpoint = createCheckpoint({
792
2083
  summary,
793
2084
  nextStep,
794
- blocker: blocker || void 0,
795
2085
  gitBranch,
796
2086
  touchedFiles,
797
- workspaceRoot: opts.cwd,
798
- source: "manual"
2087
+ commitHashes,
2088
+ workspaceRoot: cwd,
2089
+ source: isManual ? "manual" : "auto",
2090
+ sessionId
799
2091
  });
800
- const projectName = path7.basename(opts.cwd);
801
- const writer = new KeepGoingWriter(opts.cwd);
2092
+ const writer = new KeepGoingWriter(cwd);
802
2093
  writer.saveCheckpoint(checkpoint, projectName);
803
- console.log("Checkpoint saved.");
2094
+ writer.upsertSession({
2095
+ sessionId,
2096
+ sessionActive: false,
2097
+ nextStep: checkpoint.nextStep || void 0,
2098
+ branch: gitBranch ?? void 0,
2099
+ updatedAt: checkpoint.timestamp
2100
+ });
2101
+ if (json) {
2102
+ console.log(JSON.stringify(checkpoint, null, 2));
2103
+ } else if (!quiet) {
2104
+ renderSaveConfirmation(summary, touchedFiles.length, gitBranch ?? void 0);
2105
+ }
804
2106
  }
805
2107
 
806
2108
  // src/commands/hook.ts
807
- import fs5 from "fs";
808
- import path8 from "path";
809
- import os3 from "os";
2109
+ import fs6 from "fs";
2110
+ import path10 from "path";
2111
+ import os4 from "os";
810
2112
  import { execSync } from "child_process";
811
2113
  var HOOK_MARKER_START = "# keepgoing-hook-start";
812
2114
  var HOOK_MARKER_END = "# keepgoing-hook-end";
@@ -842,7 +2144,7 @@ if command -v keepgoing >/dev/null 2>&1
842
2144
  end
843
2145
  ${HOOK_MARKER_END}`;
844
2146
  function detectShellRcFile(shellOverride) {
845
- const home = os3.homedir();
2147
+ const home = os4.homedir();
846
2148
  let shell;
847
2149
  if (shellOverride) {
848
2150
  shell = shellOverride.toLowerCase();
@@ -865,14 +2167,14 @@ function detectShellRcFile(shellOverride) {
865
2167
  }
866
2168
  }
867
2169
  if (shell === "zsh") {
868
- return { shell: "zsh", rcFile: path8.join(home, ".zshrc") };
2170
+ return { shell: "zsh", rcFile: path10.join(home, ".zshrc") };
869
2171
  }
870
2172
  if (shell === "bash") {
871
- return { shell: "bash", rcFile: path8.join(home, ".bashrc") };
2173
+ return { shell: "bash", rcFile: path10.join(home, ".bashrc") };
872
2174
  }
873
2175
  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") };
2176
+ const xdgConfig = process.env["XDG_CONFIG_HOME"] || path10.join(home, ".config");
2177
+ return { shell: "fish", rcFile: path10.join(xdgConfig, "fish", "config.fish") };
876
2178
  }
877
2179
  return void 0;
878
2180
  }
@@ -888,14 +2190,14 @@ function hookInstallCommand(shellOverride) {
888
2190
  const hookBlock = shell === "zsh" ? ZSH_HOOK : shell === "fish" ? FISH_HOOK : BASH_HOOK;
889
2191
  let existing = "";
890
2192
  try {
891
- existing = fs5.readFileSync(rcFile, "utf-8");
2193
+ existing = fs6.readFileSync(rcFile, "utf-8");
892
2194
  } catch {
893
2195
  }
894
2196
  if (existing.includes(HOOK_MARKER_START)) {
895
2197
  console.log(`KeepGoing hook is already installed in ${rcFile}.`);
896
2198
  return;
897
2199
  }
898
- fs5.appendFileSync(rcFile, `
2200
+ fs6.appendFileSync(rcFile, `
899
2201
  ${hookBlock}
900
2202
  `, "utf-8");
901
2203
  console.log(`KeepGoing hook installed in ${rcFile}.`);
@@ -914,7 +2216,7 @@ function hookUninstallCommand(shellOverride) {
914
2216
  const { rcFile } = detected;
915
2217
  let existing = "";
916
2218
  try {
917
- existing = fs5.readFileSync(rcFile, "utf-8");
2219
+ existing = fs6.readFileSync(rcFile, "utf-8");
918
2220
  } catch {
919
2221
  console.log(`${rcFile} not found \u2014 nothing to remove.`);
920
2222
  return;
@@ -930,7 +2232,7 @@ function hookUninstallCommand(shellOverride) {
930
2232
  "g"
931
2233
  );
932
2234
  const updated = existing.replace(pattern, "").replace(/\n{3,}/g, "\n\n");
933
- fs5.writeFileSync(rcFile, updated, "utf-8");
2235
+ fs6.writeFileSync(rcFile, updated, "utf-8");
934
2236
  console.log(`KeepGoing hook removed from ${rcFile}.`);
935
2237
  console.log(`Reload your shell config to deactivate it:
936
2238
  `);
@@ -1027,51 +2329,778 @@ async function deactivateCommand(opts) {
1027
2329
  }
1028
2330
  }
1029
2331
 
2332
+ // src/commands/briefing.ts
2333
+ async function briefingCommand(opts) {
2334
+ const reader = new KeepGoingReader(opts.cwd);
2335
+ if (!reader.exists()) {
2336
+ if (!opts.quiet) {
2337
+ renderNoData();
2338
+ }
2339
+ return;
2340
+ }
2341
+ const gitBranch = reader.getCurrentBranch();
2342
+ const { session: lastSession } = reader.getScopedLastSession();
2343
+ const recentSessions = reader.getScopedRecentSessions(5);
2344
+ const state = reader.getState() ?? {};
2345
+ const sinceTimestamp = lastSession?.timestamp;
2346
+ const recentCommits = sinceTimestamp ? getCommitMessagesSince(opts.cwd, sinceTimestamp) : [];
2347
+ const decisions = reader.getScopedRecentDecisions(10);
2348
+ const allSessions = reader.getSessions();
2349
+ const fileConflicts = reader.detectFileConflicts();
2350
+ const branchOverlaps = reader.detectBranchOverlap();
2351
+ const effectiveTier = opts.quiet ? "compact" : opts.tier;
2352
+ const validTiers = ["compact", "standard", "detailed", "full"];
2353
+ const tier = effectiveTier && validTiers.includes(effectiveTier) ? effectiveTier : void 0;
2354
+ const briefing = generateEnrichedBriefing({
2355
+ tier,
2356
+ model: opts.model,
2357
+ lastSession,
2358
+ recentSessions,
2359
+ projectState: state,
2360
+ gitBranch,
2361
+ recentCommits,
2362
+ decisions,
2363
+ allTouchedFiles: lastSession?.touchedFiles,
2364
+ allSessions,
2365
+ fileConflicts,
2366
+ branchOverlaps,
2367
+ isWorktree: reader.isWorktree
2368
+ });
2369
+ if (!briefing) {
2370
+ if (!opts.quiet) {
2371
+ console.log("No session data available to generate a briefing.");
2372
+ }
2373
+ return;
2374
+ }
2375
+ if (opts.json) {
2376
+ console.log(JSON.stringify(briefing, null, 2));
2377
+ return;
2378
+ }
2379
+ if (opts.quiet) {
2380
+ renderEnrichedBriefingQuiet(briefing);
2381
+ return;
2382
+ }
2383
+ renderEnrichedBriefing(briefing);
2384
+ }
2385
+
2386
+ // src/commands/init.ts
2387
+ var RESET3 = "\x1B[0m";
2388
+ var BOLD3 = "\x1B[1m";
2389
+ var GREEN2 = "\x1B[32m";
2390
+ var YELLOW2 = "\x1B[33m";
2391
+ var CYAN2 = "\x1B[36m";
2392
+ var DIM3 = "\x1B[2m";
2393
+ function initCommand(options) {
2394
+ const scope = options.scope === "user" ? "user" : "project";
2395
+ const hasProLicense = process.env.KEEPGOING_PRO_BYPASS === "1" || !!getLicenseForFeature("session-awareness");
2396
+ const result = setupProject({
2397
+ workspacePath: options.cwd,
2398
+ scope,
2399
+ hasProLicense
2400
+ });
2401
+ console.log(`
2402
+ ${BOLD3}KeepGoing Init${RESET3} ${DIM3}(${scope} scope)${RESET3}
2403
+ `);
2404
+ for (const msg of result.messages) {
2405
+ const colonIdx = msg.indexOf(":");
2406
+ if (colonIdx === -1) {
2407
+ console.log(` ${msg}`);
2408
+ continue;
2409
+ }
2410
+ const label = msg.slice(0, colonIdx + 1);
2411
+ const body = msg.slice(colonIdx + 1);
2412
+ if (label.startsWith("Warning")) {
2413
+ console.log(` ${YELLOW2}${label}${RESET3}${body}`);
2414
+ } else if (body.includes("Added")) {
2415
+ console.log(` ${GREEN2}${label}${RESET3}${body}`);
2416
+ } else {
2417
+ console.log(` ${CYAN2}${label}${RESET3}${body}`);
2418
+ }
2419
+ }
2420
+ if (result.changed) {
2421
+ console.log(`
2422
+ ${GREEN2}Done!${RESET3} KeepGoing is set up for this project.
2423
+ `);
2424
+ } else {
2425
+ console.log(`
2426
+ Everything was already configured. No changes made.
2427
+ `);
2428
+ }
2429
+ }
2430
+
2431
+ // src/commands/momentum.ts
2432
+ async function momentumCommand(opts) {
2433
+ const reader = new KeepGoingReader(opts.cwd);
2434
+ if (!reader.exists()) {
2435
+ if (!opts.quiet) {
2436
+ renderNoData();
2437
+ }
2438
+ return;
2439
+ }
2440
+ const { session: lastSession, isFallback } = reader.getScopedLastSession();
2441
+ const currentBranch = reader.getCurrentBranch();
2442
+ if (!lastSession) {
2443
+ if (!opts.quiet) {
2444
+ console.log("KeepGoing is set up but no session checkpoints exist yet.");
2445
+ }
2446
+ return;
2447
+ }
2448
+ const state = reader.getState();
2449
+ const branchChanged = lastSession.gitBranch && currentBranch && lastSession.gitBranch !== currentBranch;
2450
+ if (opts.json) {
2451
+ console.log(JSON.stringify({
2452
+ lastCheckpoint: lastSession.timestamp,
2453
+ summary: lastSession.summary,
2454
+ nextStep: lastSession.nextStep,
2455
+ blocker: lastSession.blocker || null,
2456
+ projectIntent: lastSession.projectIntent || null,
2457
+ branch: currentBranch || null,
2458
+ branchChanged: branchChanged ? lastSession.gitBranch : null,
2459
+ touchedFiles: lastSession.touchedFiles,
2460
+ derivedFocus: state?.derivedCurrentFocus || null,
2461
+ isWorktree: reader.isWorktree,
2462
+ isFallback
2463
+ }, null, 2));
2464
+ return;
2465
+ }
2466
+ if (opts.quiet) {
2467
+ renderMomentumQuiet(lastSession);
2468
+ return;
2469
+ }
2470
+ renderMomentum(lastSession, {
2471
+ currentBranch,
2472
+ branchChanged: !!branchChanged,
2473
+ isWorktree: reader.isWorktree,
2474
+ isFallback,
2475
+ derivedFocus: state?.derivedCurrentFocus
2476
+ });
2477
+ }
2478
+
2479
+ // src/commands/decisions.ts
2480
+ async function decisionsCommand(opts) {
2481
+ const reader = new KeepGoingReader(opts.cwd);
2482
+ if (!reader.exists()) {
2483
+ if (!opts.quiet) {
2484
+ renderNoData();
2485
+ }
2486
+ return;
2487
+ }
2488
+ if (process.env.KEEPGOING_PRO_BYPASS !== "1" && !getLicenseForFeature("decisions")) {
2489
+ console.error(
2490
+ 'Decision Detection requires a Pro license.\nRun "keepgoing activate <key>" or visit https://keepgoing.dev/add-ons to purchase.'
2491
+ );
2492
+ process.exit(1);
2493
+ }
2494
+ const { effectiveBranch, scopeLabel } = reader.resolveBranchScope(opts.branch || void 0);
2495
+ const decisions = effectiveBranch ? reader.getRecentDecisionsForBranch(effectiveBranch, opts.limit) : reader.getRecentDecisions(opts.limit);
2496
+ if (decisions.length === 0) {
2497
+ if (!opts.quiet) {
2498
+ const msg = effectiveBranch ? `No decisions found for branch \`${effectiveBranch}\`. Use --branch all to see all branches.` : "No decisions found.";
2499
+ console.log(msg);
2500
+ }
2501
+ return;
2502
+ }
2503
+ if (opts.json) {
2504
+ console.log(JSON.stringify(decisions, null, 2));
2505
+ return;
2506
+ }
2507
+ if (opts.quiet) {
2508
+ renderDecisionsQuiet(decisions);
2509
+ return;
2510
+ }
2511
+ renderDecisions(decisions, scopeLabel);
2512
+ }
2513
+
2514
+ // src/commands/log.ts
2515
+ var RESET4 = "\x1B[0m";
2516
+ var DIM4 = "\x1B[2m";
2517
+ function parseDate(input) {
2518
+ const lower = input.toLowerCase().trim();
2519
+ if (lower === "today") {
2520
+ const d2 = /* @__PURE__ */ new Date();
2521
+ d2.setHours(0, 0, 0, 0);
2522
+ return d2;
2523
+ }
2524
+ if (lower === "yesterday") {
2525
+ const d2 = /* @__PURE__ */ new Date();
2526
+ d2.setDate(d2.getDate() - 1);
2527
+ d2.setHours(0, 0, 0, 0);
2528
+ return d2;
2529
+ }
2530
+ if (lower === "last week") {
2531
+ const d2 = /* @__PURE__ */ new Date();
2532
+ d2.setDate(d2.getDate() - 7);
2533
+ d2.setHours(0, 0, 0, 0);
2534
+ return d2;
2535
+ }
2536
+ const agoMatch = lower.match(/^(\d+)\s+(second|minute|hour|day|week|month)s?\s+ago$/);
2537
+ if (agoMatch) {
2538
+ const n = parseInt(agoMatch[1], 10);
2539
+ const unit = agoMatch[2];
2540
+ const now = /* @__PURE__ */ new Date();
2541
+ const msPerUnit = {
2542
+ second: 1e3,
2543
+ minute: 60 * 1e3,
2544
+ hour: 60 * 60 * 1e3,
2545
+ day: 24 * 60 * 60 * 1e3,
2546
+ week: 7 * 24 * 60 * 60 * 1e3,
2547
+ month: 30 * 24 * 60 * 60 * 1e3
2548
+ };
2549
+ return new Date(now.getTime() - n * (msPerUnit[unit] ?? 0));
2550
+ }
2551
+ const d = new Date(input);
2552
+ if (!isNaN(d.getTime())) return d;
2553
+ return void 0;
2554
+ }
2555
+ function filterSessions(sessions, opts) {
2556
+ let result = sessions;
2557
+ let sinceDate;
2558
+ if (opts.today) {
2559
+ sinceDate = parseDate("today");
2560
+ } else if (opts.week) {
2561
+ sinceDate = parseDate("last week");
2562
+ } else if (opts.since) {
2563
+ sinceDate = parseDate(opts.since);
2564
+ }
2565
+ if (sinceDate) {
2566
+ const ts = sinceDate.getTime();
2567
+ result = result.filter((s) => new Date(s.timestamp).getTime() >= ts);
2568
+ }
2569
+ if (opts.until) {
2570
+ const untilDate = parseDate(opts.until);
2571
+ if (untilDate) {
2572
+ const ts = untilDate.getTime();
2573
+ result = result.filter((s) => new Date(s.timestamp).getTime() <= ts);
2574
+ }
2575
+ }
2576
+ if (opts.source) {
2577
+ const src = opts.source.toLowerCase();
2578
+ result = result.filter((s) => s.source?.toLowerCase() === src);
2579
+ }
2580
+ if (opts.blockerOnly) {
2581
+ result = result.filter((s) => s.blocker && s.blocker.trim().length > 0);
2582
+ }
2583
+ if (opts.follow) {
2584
+ const file = opts.follow.toLowerCase();
2585
+ result = result.filter(
2586
+ (s) => s.touchedFiles?.some((f) => f.toLowerCase().includes(file))
2587
+ );
2588
+ }
2589
+ if (opts.search) {
2590
+ const term = opts.search.toLowerCase();
2591
+ result = result.filter((s) => {
2592
+ const haystack = [s.summary, s.nextStep, s.blocker].filter(Boolean).join(" ").toLowerCase();
2593
+ return haystack.includes(term);
2594
+ });
2595
+ }
2596
+ return result;
2597
+ }
2598
+ function filterDecisions(decisions, opts) {
2599
+ let result = decisions;
2600
+ let sinceDate;
2601
+ if (opts.today) {
2602
+ sinceDate = parseDate("today");
2603
+ } else if (opts.week) {
2604
+ sinceDate = parseDate("last week");
2605
+ } else if (opts.since) {
2606
+ sinceDate = parseDate(opts.since);
2607
+ }
2608
+ if (sinceDate) {
2609
+ const ts = sinceDate.getTime();
2610
+ result = result.filter((d) => new Date(d.timestamp).getTime() >= ts);
2611
+ }
2612
+ if (opts.until) {
2613
+ const untilDate = parseDate(opts.until);
2614
+ if (untilDate) {
2615
+ const ts = untilDate.getTime();
2616
+ result = result.filter((d) => new Date(d.timestamp).getTime() <= ts);
2617
+ }
2618
+ }
2619
+ if (opts.follow) {
2620
+ const file = opts.follow.toLowerCase();
2621
+ result = result.filter(
2622
+ (d) => d.filesChanged?.some((f) => f.toLowerCase().includes(file))
2623
+ );
2624
+ }
2625
+ if (opts.search) {
2626
+ const term = opts.search.toLowerCase();
2627
+ result = result.filter((d) => {
2628
+ const haystack = [d.commitMessage, d.rationale].filter(Boolean).join(" ").toLowerCase();
2629
+ return haystack.includes(term);
2630
+ });
2631
+ }
2632
+ return result;
2633
+ }
2634
+ function logSessions(reader, opts) {
2635
+ const { effectiveBranch } = reader.resolveBranchScope(opts.branch || void 0);
2636
+ let sessions = reader.getSessions();
2637
+ if (effectiveBranch) {
2638
+ sessions = sessions.filter((s) => s.gitBranch === effectiveBranch);
2639
+ }
2640
+ sessions.reverse();
2641
+ sessions = filterSessions(sessions, opts);
2642
+ const totalFiltered = sessions.length;
2643
+ if (totalFiltered === 0) {
2644
+ console.log(`${DIM4}No checkpoints match the given filters.${RESET4}`);
2645
+ return;
2646
+ }
2647
+ const displayed = sessions.slice(0, opts.count);
2648
+ if (opts.json) {
2649
+ console.log(JSON.stringify(displayed, null, 2));
2650
+ return;
2651
+ }
2652
+ if (opts.quiet) {
2653
+ console.log(`${totalFiltered} checkpoint${totalFiltered !== 1 ? "s" : ""} found`);
2654
+ return;
2655
+ }
2656
+ if (opts.sessions) {
2657
+ renderGrouped(displayed, opts.stat);
2658
+ } else if (opts.oneline) {
2659
+ for (const s of displayed) {
2660
+ renderLogSessionOneline(s);
2661
+ }
2662
+ } else {
2663
+ for (const s of displayed) {
2664
+ renderLogSession(s, opts.stat);
2665
+ }
2666
+ }
2667
+ if (totalFiltered > opts.count) {
2668
+ console.log(`${DIM4}(showing ${displayed.length} of ${totalFiltered} checkpoints)${RESET4}`);
2669
+ }
2670
+ }
2671
+ function renderGrouped(sessions, showStat) {
2672
+ const groups = /* @__PURE__ */ new Map();
2673
+ for (const s of sessions) {
2674
+ const key = s.sessionId || s.id;
2675
+ const group = groups.get(key);
2676
+ if (group) {
2677
+ group.push(s);
2678
+ } else {
2679
+ groups.set(key, [s]);
2680
+ }
2681
+ }
2682
+ let first = true;
2683
+ for (const [sessionId, items] of groups) {
2684
+ if (!first) console.log("");
2685
+ first = false;
2686
+ renderSessionGroupHeader(sessionId, items.length);
2687
+ for (const s of items) {
2688
+ renderLogSession(s, showStat);
2689
+ }
2690
+ }
2691
+ }
2692
+ function logDecisions(reader, opts) {
2693
+ const license = getLicenseForFeature("decisions");
2694
+ if (!license) {
2695
+ console.log("Decision tracking requires a Pro license. Run: keepgoing activate <key>");
2696
+ return;
2697
+ }
2698
+ const { effectiveBranch } = reader.resolveBranchScope(opts.branch || void 0);
2699
+ let decisions = reader.getDecisions();
2700
+ if (effectiveBranch) {
2701
+ decisions = decisions.filter((d) => d.gitBranch === effectiveBranch);
2702
+ }
2703
+ decisions.reverse();
2704
+ decisions = filterDecisions(decisions, opts);
2705
+ const totalFiltered = decisions.length;
2706
+ if (totalFiltered === 0) {
2707
+ console.log(`${DIM4}No decisions match the given filters.${RESET4}`);
2708
+ return;
2709
+ }
2710
+ const displayed = decisions.slice(0, opts.count);
2711
+ if (opts.json) {
2712
+ console.log(JSON.stringify(displayed, null, 2));
2713
+ return;
2714
+ }
2715
+ if (opts.quiet) {
2716
+ console.log(`${totalFiltered} decision${totalFiltered !== 1 ? "s" : ""} found`);
2717
+ return;
2718
+ }
2719
+ if (opts.oneline) {
2720
+ for (const d of displayed) {
2721
+ renderLogDecisionOneline(d);
2722
+ }
2723
+ } else {
2724
+ for (const d of displayed) {
2725
+ renderLogDecision(d, opts.stat);
2726
+ }
2727
+ }
2728
+ if (totalFiltered > opts.count) {
2729
+ console.log(`${DIM4}(showing ${displayed.length} of ${totalFiltered} decisions)${RESET4}`);
2730
+ }
2731
+ }
2732
+ async function logCommand(opts) {
2733
+ const reader = new KeepGoingReader(opts.cwd);
2734
+ if (!reader.exists()) {
2735
+ renderNoData();
2736
+ return;
2737
+ }
2738
+ if (opts.subcommand === "decisions") {
2739
+ logDecisions(reader, opts);
2740
+ } else {
2741
+ logSessions(reader, opts);
2742
+ }
2743
+ }
2744
+
2745
+ // src/platform.ts
2746
+ import { execSync as execSync2, spawnSync } from "child_process";
2747
+ function copyToClipboard(text) {
2748
+ try {
2749
+ const platform = process.platform;
2750
+ if (platform === "darwin") {
2751
+ execSync2("pbcopy", { input: text, stdio: ["pipe", "ignore", "ignore"] });
2752
+ } else if (platform === "win32") {
2753
+ execSync2("clip.exe", { input: text, stdio: ["pipe", "ignore", "ignore"] });
2754
+ } else {
2755
+ try {
2756
+ execSync2("xclip -selection clipboard", { input: text, stdio: ["pipe", "ignore", "ignore"] });
2757
+ } catch {
2758
+ execSync2("xsel --clipboard --input", { input: text, stdio: ["pipe", "ignore", "ignore"] });
2759
+ }
2760
+ }
2761
+ return true;
2762
+ } catch {
2763
+ return false;
2764
+ }
2765
+ }
2766
+ function openUrl(url) {
2767
+ try {
2768
+ const platform = process.platform;
2769
+ if (platform === "darwin") {
2770
+ spawnSync("open", [url], { stdio: "ignore" });
2771
+ } else if (platform === "win32") {
2772
+ spawnSync("cmd", ["/c", "start", "", url], { stdio: "ignore" });
2773
+ } else {
2774
+ spawnSync("xdg-open", [url], { stdio: "ignore" });
2775
+ }
2776
+ } catch {
2777
+ }
2778
+ }
2779
+
2780
+ // src/commands/continue.ts
2781
+ var TARGET_URLS = {
2782
+ chatgpt: "https://chat.openai.com",
2783
+ gemini: "https://gemini.google.com/app",
2784
+ copilot: "https://github.com/copilot",
2785
+ claude: "https://claude.ai/new"
2786
+ };
2787
+ async function continueCommand(opts) {
2788
+ const reader = new KeepGoingReader(opts.cwd);
2789
+ if (!reader.exists()) {
2790
+ if (!opts.quiet) {
2791
+ renderNoData();
2792
+ }
2793
+ return;
2794
+ }
2795
+ const context = gatherContinueOnContext(reader, opts.cwd);
2796
+ if (!context.lastCheckpoint && !context.briefing) {
2797
+ if (!opts.quiet) {
2798
+ console.log("No session data available. Save a checkpoint first.");
2799
+ }
2800
+ return;
2801
+ }
2802
+ const formatOpts = {};
2803
+ if (opts.target && opts.target in TARGET_URLS) {
2804
+ formatOpts.target = opts.target;
2805
+ }
2806
+ const prompt = formatContinueOnPrompt(context, formatOpts);
2807
+ if (opts.json) {
2808
+ console.log(JSON.stringify({ prompt, context }, null, 2));
2809
+ return;
2810
+ }
2811
+ const copied = copyToClipboard(prompt);
2812
+ if (opts.quiet) {
2813
+ renderContinueOnQuiet(copied, opts.target);
2814
+ return;
2815
+ }
2816
+ renderContinueOn(prompt, copied, opts.target);
2817
+ if (opts.open && opts.target && TARGET_URLS[opts.target]) {
2818
+ openUrl(TARGET_URLS[opts.target]);
2819
+ }
2820
+ }
2821
+
1030
2822
  // src/index.ts
1031
2823
  var HELP_TEXT = `
1032
2824
  keepgoing: resume side projects without the mental friction
1033
2825
 
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
2826
+ Usage: keepgoing <command> [options]
1040
2827
 
1041
- Options:
2828
+ Commands:
2829
+ init Set up KeepGoing hooks and CLAUDE.md in this project
2830
+ status Show the last checkpoint for this project
2831
+ momentum Show your current developer momentum
2832
+ briefing Get a re-entry briefing for this project
2833
+ decisions View decision history (Pro)
2834
+ log Browse session checkpoints
2835
+ continue Export context for use in another AI tool
2836
+ save Save a checkpoint (auto-generates from git)
2837
+ hook Manage the shell hook (zsh, bash, fish)
2838
+ activate <key> Activate a Pro license on this device
2839
+ deactivate Deactivate the Pro license from this device
2840
+
2841
+ Global options:
1042
2842
  --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
2843
+ --json Output raw JSON
2844
+ --quiet Suppress output
1046
2845
  -v, --version Show the CLI version
1047
- -h, --help Show this help text
2846
+ -h, --help Show help (use with a command for detailed options)
1048
2847
 
1049
- Hook subcommands:
1050
- keepgoing hook install Install the shell hook (zsh, bash, fish)
1051
- keepgoing hook uninstall Remove the shell hook
2848
+ Run "keepgoing <command> --help" for detailed options on any command.
1052
2849
  `;
2850
+ var COMMAND_HELP = {
2851
+ init: `
2852
+ keepgoing init: Set up KeepGoing hooks and CLAUDE.md in this project
2853
+
2854
+ Usage: keepgoing init [options]
2855
+
2856
+ Options:
2857
+ --scope <s> Scope: "project" (default) or "user" (global)
2858
+ --cwd <path> Override the working directory
2859
+ `,
2860
+ setup: `
2861
+ keepgoing init: Set up KeepGoing hooks and CLAUDE.md in this project
2862
+
2863
+ Usage: keepgoing init [options]
2864
+
2865
+ Options:
2866
+ --scope <s> Scope: "project" (default) or "user" (global)
2867
+ --cwd <path> Override the working directory
2868
+ `,
2869
+ status: `
2870
+ keepgoing status: Show the last checkpoint for this project
2871
+
2872
+ Usage: keepgoing status [options]
2873
+
2874
+ Options:
2875
+ --json Output raw JSON
2876
+ --quiet Suppress output
2877
+ --cwd <path> Override the working directory
2878
+ `,
2879
+ momentum: `
2880
+ keepgoing momentum: Show your current developer momentum
2881
+
2882
+ Usage: keepgoing momentum [options]
2883
+
2884
+ Options:
2885
+ --json Output raw JSON
2886
+ --quiet Suppress output
2887
+ --cwd <path> Override the working directory
2888
+ `,
2889
+ briefing: `
2890
+ keepgoing briefing: Get a re-entry briefing for this project
2891
+
2892
+ Usage: keepgoing briefing [options]
2893
+
2894
+ Options:
2895
+ --tier <tier> Detail level: compact, standard (default), detailed, full
2896
+ --model <name> Auto-resolve tier from model name (e.g. "claude-opus-4")
2897
+ --json Output raw JSON
2898
+ --quiet Suppress output
2899
+ --cwd <path> Override the working directory
2900
+ `,
2901
+ decisions: `
2902
+ keepgoing decisions: View decision history (Pro)
2903
+
2904
+ Usage: keepgoing decisions [options]
2905
+
2906
+ Options:
2907
+ --branch <name> Filter by branch, or "all" for all branches
2908
+ --limit <n> Number of decisions to show (default: 10)
2909
+ --json Output raw JSON
2910
+ --quiet Suppress output
2911
+ --cwd <path> Override the working directory
2912
+ `,
2913
+ log: `
2914
+ keepgoing log: Browse session checkpoints
2915
+
2916
+ Usage:
2917
+ keepgoing log [options] Browse session checkpoints
2918
+ keepgoing log decisions [options] Browse decision records (Pro)
2919
+
2920
+ Options:
2921
+ -n <count> Number of entries to show (default: 10)
2922
+ --branch <name> Filter by branch ("all" for all branches)
2923
+ --since <date> Show entries after date (ISO, "today", "yesterday", "N days ago")
2924
+ --until <date> Show entries before date
2925
+ --source <type> Filter by source (manual, auto)
2926
+ --follow <file> Filter by touched file path
2927
+ --search <term> Search in summary, next step, blocker
2928
+ --oneline Compact one-line format
2929
+ --stat Show touched file paths
2930
+ --blocker Only show entries with blockers
2931
+ --today Shorthand for --since today
2932
+ --week Shorthand for --since "last week"
2933
+ --sessions Group checkpoints by session
2934
+ --json Output raw JSON
2935
+ --quiet Suppress output
2936
+ --cwd <path> Override the working directory
2937
+
2938
+ Examples:
2939
+ keepgoing log --today Show today's checkpoints
2940
+ keepgoing log --week --oneline This week's checkpoints, compact
2941
+ keepgoing log --follow src/app.ts Checkpoints that touched a file
2942
+ keepgoing log --search "auth" Search checkpoint summaries
2943
+ keepgoing log --sessions Group by session
2944
+ keepgoing log decisions Browse decision records (Pro)
2945
+ `,
2946
+ save: `
2947
+ keepgoing save: Save a checkpoint (auto-generates from git)
2948
+
2949
+ Usage: keepgoing save [options]
2950
+
2951
+ Options:
2952
+ -m, --message <text> Use a custom summary instead of auto-generating
2953
+ -n, --next <text> Use a custom next step instead of auto-generating
2954
+ --force Save even if recent checkpoint exists or no changes
2955
+ --json Output raw JSON
2956
+ --quiet Suppress output
2957
+ --cwd <path> Override the working directory
2958
+
2959
+ Examples:
2960
+ keepgoing save Auto-generate from git
2961
+ keepgoing save -m "Finished auth flow" Custom summary
2962
+ keepgoing save --force Save even if no changes
2963
+ `,
2964
+ hook: `
2965
+ keepgoing hook: Manage the shell hook
2966
+
2967
+ The shell hook shows a quick status line when you cd into a KeepGoing project.
2968
+
2969
+ Usage:
2970
+ keepgoing hook install Install the shell hook
2971
+ keepgoing hook uninstall Remove the shell hook
2972
+
2973
+ Supported shells: zsh, bash, fish
2974
+
2975
+ Options:
2976
+ --shell <name> Override shell detection (zsh, bash, fish)
2977
+
2978
+ The hook auto-detects your current shell. Use --shell to override.
2979
+ `,
2980
+ activate: `
2981
+ keepgoing activate: Activate a Pro license on this device
2982
+
2983
+ Usage: keepgoing activate <key>
2984
+
2985
+ Example:
2986
+ keepgoing activate XXXX-XXXX-XXXX-XXXX
2987
+ `,
2988
+ deactivate: `
2989
+ keepgoing deactivate: Deactivate the Pro license from this device
2990
+
2991
+ Usage: keepgoing deactivate [<key>]
2992
+ `,
2993
+ continue: `
2994
+ keepgoing continue: Export context for use in another AI tool
2995
+
2996
+ Usage: keepgoing continue [options]
2997
+
2998
+ Options:
2999
+ --target <tool> Target AI tool (chatgpt, gemini, copilot, claude, general)
3000
+ --open Auto-open the target tool's URL in your browser
3001
+ --json Output raw JSON (prompt + context)
3002
+ --quiet Suppress output (just copy to clipboard)
3003
+ --cwd <path> Override the working directory
3004
+
3005
+ Examples:
3006
+ keepgoing continue Copy context to clipboard
3007
+ keepgoing continue --target chatgpt --open Copy and open ChatGPT
3008
+ keepgoing continue --json Output as JSON
3009
+ `
3010
+ };
1053
3011
  function parseArgs(argv) {
1054
3012
  const args = argv.slice(2);
1055
3013
  let command = "";
1056
3014
  let subcommand = "";
3015
+ let help = false;
1057
3016
  let cwd = process.cwd();
1058
3017
  let json = false;
1059
3018
  let quiet = false;
1060
3019
  let shell = "";
3020
+ let scope = "project";
3021
+ let message = "";
3022
+ let nextStepOverride = "";
3023
+ let force = false;
3024
+ let branch = "";
3025
+ let limit = 10;
3026
+ let count = 10;
3027
+ let since = "";
3028
+ let until = "";
3029
+ let source = "";
3030
+ let follow = "";
3031
+ let search = "";
3032
+ let oneline = false;
3033
+ let stat = false;
3034
+ let blockerOnly = false;
3035
+ let today = false;
3036
+ let week = false;
3037
+ let sessions = false;
3038
+ let target = "";
3039
+ let open = false;
3040
+ let tier = "";
3041
+ let model = "";
1061
3042
  for (let i = 0; i < args.length; i++) {
1062
3043
  const arg = args[i];
1063
3044
  if (arg === "--cwd" && i + 1 < args.length) {
1064
3045
  cwd = args[++i];
1065
3046
  } else if (arg === "--shell" && i + 1 < args.length) {
1066
3047
  shell = args[++i];
3048
+ } else if (arg === "--scope" && i + 1 < args.length) {
3049
+ scope = args[++i];
3050
+ } else if ((arg === "-m" || arg === "--message") && i + 1 < args.length) {
3051
+ message = args[++i];
3052
+ } else if (arg === "--next" && i + 1 < args.length) {
3053
+ nextStepOverride = args[++i];
3054
+ } else if (arg === "-n" && i + 1 < args.length) {
3055
+ if (command === "log") {
3056
+ count = parseInt(args[++i], 10) || 10;
3057
+ } else {
3058
+ nextStepOverride = args[++i];
3059
+ }
3060
+ } else if (arg === "--branch" && i + 1 < args.length) {
3061
+ branch = args[++i];
3062
+ } else if (arg === "--limit" && i + 1 < args.length) {
3063
+ limit = parseInt(args[++i], 10) || 10;
3064
+ } else if (arg === "--target" && i + 1 < args.length) {
3065
+ target = args[++i];
3066
+ } else if (arg === "--open") {
3067
+ open = true;
1067
3068
  } else if (arg === "--json") {
1068
3069
  json = true;
1069
3070
  } else if (arg === "--quiet") {
1070
3071
  quiet = true;
3072
+ } else if (arg === "--force") {
3073
+ force = true;
3074
+ } else if (arg === "--since" && i + 1 < args.length) {
3075
+ since = args[++i];
3076
+ } else if (arg === "--until" && i + 1 < args.length) {
3077
+ until = args[++i];
3078
+ } else if (arg === "--source" && i + 1 < args.length) {
3079
+ source = args[++i];
3080
+ } else if (arg === "--follow" && i + 1 < args.length) {
3081
+ follow = args[++i];
3082
+ } else if (arg === "--search" && i + 1 < args.length) {
3083
+ search = args[++i];
3084
+ } else if (arg === "--oneline") {
3085
+ oneline = true;
3086
+ } else if (arg === "--stat") {
3087
+ stat = true;
3088
+ } else if (arg === "--blocker") {
3089
+ blockerOnly = true;
3090
+ } else if (arg === "--today") {
3091
+ today = true;
3092
+ } else if (arg === "--week") {
3093
+ week = true;
3094
+ } else if (arg === "--sessions") {
3095
+ sessions = true;
3096
+ } else if (arg === "--tier" && i + 1 < args.length) {
3097
+ tier = args[++i];
3098
+ } else if (arg === "--model" && i + 1 < args.length) {
3099
+ model = args[++i];
1071
3100
  } else if (arg === "-v" || arg === "--version") {
1072
3101
  command = "version";
1073
3102
  } else if (arg === "-h" || arg === "--help") {
1074
- command = "help";
3103
+ help = true;
1075
3104
  } else if (!command) {
1076
3105
  command = arg;
1077
3106
  } else if (!subcommand) {
@@ -1079,16 +3108,106 @@ function parseArgs(argv) {
1079
3108
  }
1080
3109
  }
1081
3110
  cwd = findGitRoot(cwd);
1082
- return { command, subcommand, cwd, json, quiet, shell };
3111
+ return {
3112
+ command,
3113
+ subcommand,
3114
+ help,
3115
+ cwd,
3116
+ json,
3117
+ quiet,
3118
+ shell,
3119
+ scope,
3120
+ message,
3121
+ nextStepOverride,
3122
+ force,
3123
+ branch,
3124
+ limit,
3125
+ count,
3126
+ since,
3127
+ until,
3128
+ source,
3129
+ follow,
3130
+ search,
3131
+ oneline,
3132
+ stat,
3133
+ blockerOnly,
3134
+ today,
3135
+ week,
3136
+ sessions,
3137
+ target,
3138
+ open,
3139
+ tier,
3140
+ model
3141
+ };
1083
3142
  }
1084
3143
  async function main() {
1085
- const { command, subcommand, cwd, json, quiet, shell } = parseArgs(process.argv);
3144
+ const parsed = parseArgs(process.argv);
3145
+ const { command, subcommand, cwd, json, quiet, shell, scope, message, nextStepOverride, force, branch, limit } = parsed;
3146
+ if (parsed.help || command === "help") {
3147
+ const helpCmd = parsed.help ? command : subcommand;
3148
+ if (helpCmd && COMMAND_HELP[helpCmd]) {
3149
+ console.log(COMMAND_HELP[helpCmd]);
3150
+ } else {
3151
+ console.log(HELP_TEXT);
3152
+ }
3153
+ return;
3154
+ }
1086
3155
  switch (command) {
3156
+ case "init":
3157
+ case "setup":
3158
+ initCommand({ cwd, scope });
3159
+ break;
1087
3160
  case "status":
1088
3161
  await statusCommand({ cwd, json, quiet });
1089
3162
  break;
3163
+ case "momentum":
3164
+ await momentumCommand({ cwd, json, quiet });
3165
+ break;
3166
+ case "briefing":
3167
+ await briefingCommand({
3168
+ cwd,
3169
+ json,
3170
+ quiet,
3171
+ tier: parsed.tier || void 0,
3172
+ model: parsed.model || void 0
3173
+ });
3174
+ break;
3175
+ case "decisions":
3176
+ await decisionsCommand({ cwd, json, quiet, branch, limit });
3177
+ break;
3178
+ case "log":
3179
+ await logCommand({
3180
+ cwd,
3181
+ json,
3182
+ quiet,
3183
+ subcommand,
3184
+ count: parsed.count,
3185
+ branch: parsed.branch,
3186
+ since: parsed.since,
3187
+ until: parsed.until,
3188
+ source: parsed.source,
3189
+ follow: parsed.follow,
3190
+ search: parsed.search,
3191
+ oneline: parsed.oneline,
3192
+ stat: parsed.stat,
3193
+ blockerOnly: parsed.blockerOnly,
3194
+ today: parsed.today,
3195
+ week: parsed.week,
3196
+ sessions: parsed.sessions
3197
+ });
3198
+ break;
3199
+ case "continue":
3200
+ await continueCommand({ cwd, json, quiet, target: parsed.target, open: parsed.open });
3201
+ break;
1090
3202
  case "save":
1091
- await saveCommand({ cwd });
3203
+ await saveCommand({
3204
+ cwd,
3205
+ message: message || void 0,
3206
+ nextStepOverride: nextStepOverride || void 0,
3207
+ json,
3208
+ quiet,
3209
+ force
3210
+ });
1092
3211
  break;
1093
3212
  case "hook":
1094
3213
  if (subcommand === "install") {
@@ -1096,14 +3215,11 @@ async function main() {
1096
3215
  } else if (subcommand === "uninstall") {
1097
3216
  hookUninstallCommand(shell || void 0);
1098
3217
  } else {
1099
- console.error(
1100
- `Unknown hook subcommand: "${subcommand}". Use "install" or "uninstall".`
1101
- );
1102
- process.exit(1);
3218
+ console.log(COMMAND_HELP.hook);
1103
3219
  }
1104
3220
  break;
1105
3221
  case "version":
1106
- console.log(`keepgoing v${"0.3.3"}`);
3222
+ console.log(`keepgoing v${"1.1.0"}`);
1107
3223
  break;
1108
3224
  case "activate":
1109
3225
  await activateCommand({ licenseKey: subcommand });
@@ -1111,7 +3227,6 @@ async function main() {
1111
3227
  case "deactivate":
1112
3228
  await deactivateCommand({ licenseKey: subcommand || void 0 });
1113
3229
  break;
1114
- case "help":
1115
3230
  case "":
1116
3231
  console.log(HELP_TEXT);
1117
3232
  break;