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