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