@keepgoingdev/cli 1.1.1 → 1.2.1
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 +461 -114
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -320,7 +320,8 @@ function buildRecentActivity(lastSession, recentSessions, recentCommitMessages)
|
|
|
320
320
|
parts.push("1 recent session");
|
|
321
321
|
}
|
|
322
322
|
if (lastSession.summary) {
|
|
323
|
-
|
|
323
|
+
const brief = lastSession.summary.length > 120 ? lastSession.summary.slice(0, 117) + "..." : lastSession.summary;
|
|
324
|
+
parts.push(`Last: ${brief}`);
|
|
324
325
|
}
|
|
325
326
|
if (lastSession.touchedFiles.length > 0) {
|
|
326
327
|
parts.push(`${lastSession.touchedFiles.length} files touched`);
|
|
@@ -585,9 +586,59 @@ function formatContinueOnPrompt(context, options) {
|
|
|
585
586
|
}
|
|
586
587
|
|
|
587
588
|
// ../../packages/shared/src/storage.ts
|
|
589
|
+
import fs2 from "fs";
|
|
590
|
+
import path4 from "path";
|
|
591
|
+
import { randomUUID as randomUUID2, createHash } from "crypto";
|
|
592
|
+
|
|
593
|
+
// ../../packages/shared/src/registry.ts
|
|
588
594
|
import fs from "fs";
|
|
595
|
+
import os from "os";
|
|
589
596
|
import path3 from "path";
|
|
590
|
-
|
|
597
|
+
var KEEPGOING_DIR = path3.join(os.homedir(), ".keepgoing");
|
|
598
|
+
var KNOWN_PROJECTS_FILE = path3.join(KEEPGOING_DIR, "known-projects.json");
|
|
599
|
+
var STALE_PROJECT_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
600
|
+
function readKnownProjects() {
|
|
601
|
+
try {
|
|
602
|
+
if (fs.existsSync(KNOWN_PROJECTS_FILE)) {
|
|
603
|
+
const raw = JSON.parse(fs.readFileSync(KNOWN_PROJECTS_FILE, "utf-8"));
|
|
604
|
+
if (raw && Array.isArray(raw.projects)) {
|
|
605
|
+
return raw;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
} catch {
|
|
609
|
+
}
|
|
610
|
+
return { version: 1, projects: [] };
|
|
611
|
+
}
|
|
612
|
+
function writeKnownProjects(data) {
|
|
613
|
+
if (!fs.existsSync(KEEPGOING_DIR)) {
|
|
614
|
+
fs.mkdirSync(KEEPGOING_DIR, { recursive: true });
|
|
615
|
+
}
|
|
616
|
+
const tmpFile = KNOWN_PROJECTS_FILE + ".tmp";
|
|
617
|
+
fs.writeFileSync(tmpFile, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
618
|
+
fs.renameSync(tmpFile, KNOWN_PROJECTS_FILE);
|
|
619
|
+
}
|
|
620
|
+
function registerProject(projectPath, projectName) {
|
|
621
|
+
try {
|
|
622
|
+
const data = readKnownProjects();
|
|
623
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
624
|
+
const name = projectName || path3.basename(projectPath);
|
|
625
|
+
const existingIdx = data.projects.findIndex((p) => p.path === projectPath);
|
|
626
|
+
if (existingIdx >= 0) {
|
|
627
|
+
data.projects[existingIdx].lastSeen = now;
|
|
628
|
+
if (projectName) {
|
|
629
|
+
data.projects[existingIdx].name = name;
|
|
630
|
+
}
|
|
631
|
+
} else {
|
|
632
|
+
data.projects.push({ path: projectPath, name, lastSeen: now });
|
|
633
|
+
}
|
|
634
|
+
const cutoff = Date.now() - STALE_PROJECT_MS;
|
|
635
|
+
data.projects = data.projects.filter((p) => new Date(p.lastSeen).getTime() > cutoff);
|
|
636
|
+
writeKnownProjects(data);
|
|
637
|
+
} catch {
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// ../../packages/shared/src/storage.ts
|
|
591
642
|
var STORAGE_DIR = ".keepgoing";
|
|
592
643
|
var META_FILE = "meta.json";
|
|
593
644
|
var SESSIONS_FILE = "sessions.json";
|
|
@@ -609,23 +660,23 @@ var KeepGoingWriter = class {
|
|
|
609
660
|
currentTasksFilePath;
|
|
610
661
|
constructor(workspacePath) {
|
|
611
662
|
const mainRoot = resolveStorageRoot(workspacePath);
|
|
612
|
-
this.storagePath =
|
|
613
|
-
this.sessionsFilePath =
|
|
614
|
-
this.stateFilePath =
|
|
615
|
-
this.metaFilePath =
|
|
616
|
-
this.currentTasksFilePath =
|
|
663
|
+
this.storagePath = path4.join(mainRoot, STORAGE_DIR);
|
|
664
|
+
this.sessionsFilePath = path4.join(this.storagePath, SESSIONS_FILE);
|
|
665
|
+
this.stateFilePath = path4.join(this.storagePath, STATE_FILE);
|
|
666
|
+
this.metaFilePath = path4.join(this.storagePath, META_FILE);
|
|
667
|
+
this.currentTasksFilePath = path4.join(this.storagePath, CURRENT_TASKS_FILE);
|
|
617
668
|
}
|
|
618
669
|
ensureDir() {
|
|
619
|
-
if (!
|
|
620
|
-
|
|
670
|
+
if (!fs2.existsSync(this.storagePath)) {
|
|
671
|
+
fs2.mkdirSync(this.storagePath, { recursive: true });
|
|
621
672
|
}
|
|
622
673
|
}
|
|
623
674
|
saveCheckpoint(checkpoint, projectName) {
|
|
624
675
|
this.ensureDir();
|
|
625
676
|
let sessionsData;
|
|
626
677
|
try {
|
|
627
|
-
if (
|
|
628
|
-
const raw = JSON.parse(
|
|
678
|
+
if (fs2.existsSync(this.sessionsFilePath)) {
|
|
679
|
+
const raw = JSON.parse(fs2.readFileSync(this.sessionsFilePath, "utf-8"));
|
|
629
680
|
if (Array.isArray(raw)) {
|
|
630
681
|
sessionsData = { version: 1, project: projectName, sessions: raw };
|
|
631
682
|
} else {
|
|
@@ -643,20 +694,22 @@ var KeepGoingWriter = class {
|
|
|
643
694
|
if (sessionsData.sessions.length > MAX_SESSIONS) {
|
|
644
695
|
sessionsData.sessions = sessionsData.sessions.slice(-MAX_SESSIONS);
|
|
645
696
|
}
|
|
646
|
-
|
|
697
|
+
fs2.writeFileSync(this.sessionsFilePath, JSON.stringify(sessionsData, null, 2), "utf-8");
|
|
647
698
|
const state = {
|
|
648
699
|
lastSessionId: checkpoint.id,
|
|
649
700
|
lastKnownBranch: checkpoint.gitBranch,
|
|
650
701
|
lastActivityAt: checkpoint.timestamp
|
|
651
702
|
};
|
|
652
|
-
|
|
703
|
+
fs2.writeFileSync(this.stateFilePath, JSON.stringify(state, null, 2), "utf-8");
|
|
653
704
|
this.updateMeta(checkpoint.timestamp);
|
|
705
|
+
const mainRoot = resolveStorageRoot(this.storagePath);
|
|
706
|
+
registerProject(mainRoot, projectName);
|
|
654
707
|
}
|
|
655
708
|
updateMeta(timestamp) {
|
|
656
709
|
let meta;
|
|
657
710
|
try {
|
|
658
|
-
if (
|
|
659
|
-
meta = JSON.parse(
|
|
711
|
+
if (fs2.existsSync(this.metaFilePath)) {
|
|
712
|
+
meta = JSON.parse(fs2.readFileSync(this.metaFilePath, "utf-8"));
|
|
660
713
|
meta.lastUpdated = timestamp;
|
|
661
714
|
} else {
|
|
662
715
|
meta = {
|
|
@@ -672,7 +725,7 @@ var KeepGoingWriter = class {
|
|
|
672
725
|
lastUpdated: timestamp
|
|
673
726
|
};
|
|
674
727
|
}
|
|
675
|
-
|
|
728
|
+
fs2.writeFileSync(this.metaFilePath, JSON.stringify(meta, null, 2), "utf-8");
|
|
676
729
|
}
|
|
677
730
|
// ---------------------------------------------------------------------------
|
|
678
731
|
// Multi-session API
|
|
@@ -680,8 +733,8 @@ var KeepGoingWriter = class {
|
|
|
680
733
|
/** Read all current tasks from current-tasks.json. Auto-prunes stale sessions. */
|
|
681
734
|
readCurrentTasks() {
|
|
682
735
|
try {
|
|
683
|
-
if (
|
|
684
|
-
const raw = JSON.parse(
|
|
736
|
+
if (fs2.existsSync(this.currentTasksFilePath)) {
|
|
737
|
+
const raw = JSON.parse(fs2.readFileSync(this.currentTasksFilePath, "utf-8"));
|
|
685
738
|
const tasks = Array.isArray(raw) ? raw : raw.tasks ?? [];
|
|
686
739
|
return this.pruneStale(tasks);
|
|
687
740
|
}
|
|
@@ -696,6 +749,8 @@ var KeepGoingWriter = class {
|
|
|
696
749
|
upsertSession(update) {
|
|
697
750
|
this.ensureDir();
|
|
698
751
|
this.upsertSessionCore(update);
|
|
752
|
+
const mainRoot = resolveStorageRoot(this.storagePath);
|
|
753
|
+
registerProject(mainRoot);
|
|
699
754
|
}
|
|
700
755
|
/** Core upsert logic: merges the update into current-tasks.json and returns the pruned task list. */
|
|
701
756
|
upsertSessionCore(update) {
|
|
@@ -734,8 +789,8 @@ var KeepGoingWriter = class {
|
|
|
734
789
|
// ---------------------------------------------------------------------------
|
|
735
790
|
readAllTasksRaw() {
|
|
736
791
|
try {
|
|
737
|
-
if (
|
|
738
|
-
const raw = JSON.parse(
|
|
792
|
+
if (fs2.existsSync(this.currentTasksFilePath)) {
|
|
793
|
+
const raw = JSON.parse(fs2.readFileSync(this.currentTasksFilePath, "utf-8"));
|
|
739
794
|
return Array.isArray(raw) ? [...raw] : [...raw.tasks ?? []];
|
|
740
795
|
}
|
|
741
796
|
} catch {
|
|
@@ -747,7 +802,7 @@ var KeepGoingWriter = class {
|
|
|
747
802
|
}
|
|
748
803
|
writeTasksFile(tasks) {
|
|
749
804
|
const data = { version: 1, tasks };
|
|
750
|
-
|
|
805
|
+
fs2.writeFileSync(this.currentTasksFilePath, JSON.stringify(data, null, 2), "utf-8");
|
|
751
806
|
}
|
|
752
807
|
};
|
|
753
808
|
function generateSessionId(context) {
|
|
@@ -926,35 +981,35 @@ function capitalize(s) {
|
|
|
926
981
|
}
|
|
927
982
|
|
|
928
983
|
// ../../packages/shared/src/decisionStorage.ts
|
|
929
|
-
import
|
|
930
|
-
import
|
|
984
|
+
import fs4 from "fs";
|
|
985
|
+
import path6 from "path";
|
|
931
986
|
|
|
932
987
|
// ../../packages/shared/src/license.ts
|
|
933
988
|
import crypto from "crypto";
|
|
934
|
-
import
|
|
935
|
-
import
|
|
936
|
-
import
|
|
989
|
+
import fs3 from "fs";
|
|
990
|
+
import os2 from "os";
|
|
991
|
+
import path5 from "path";
|
|
937
992
|
var LICENSE_FILE = "license.json";
|
|
938
993
|
var DEVICE_ID_FILE = "device-id";
|
|
939
994
|
function getGlobalLicenseDir() {
|
|
940
|
-
return
|
|
995
|
+
return path5.join(os2.homedir(), ".keepgoing");
|
|
941
996
|
}
|
|
942
997
|
function getGlobalLicensePath() {
|
|
943
|
-
return
|
|
998
|
+
return path5.join(getGlobalLicenseDir(), LICENSE_FILE);
|
|
944
999
|
}
|
|
945
1000
|
function getDeviceId() {
|
|
946
1001
|
const dir = getGlobalLicenseDir();
|
|
947
|
-
const filePath =
|
|
1002
|
+
const filePath = path5.join(dir, DEVICE_ID_FILE);
|
|
948
1003
|
try {
|
|
949
|
-
const existing =
|
|
1004
|
+
const existing = fs3.readFileSync(filePath, "utf-8").trim();
|
|
950
1005
|
if (existing) return existing;
|
|
951
1006
|
} catch {
|
|
952
1007
|
}
|
|
953
1008
|
const id = crypto.randomUUID();
|
|
954
|
-
if (!
|
|
955
|
-
|
|
1009
|
+
if (!fs3.existsSync(dir)) {
|
|
1010
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
956
1011
|
}
|
|
957
|
-
|
|
1012
|
+
fs3.writeFileSync(filePath, id, "utf-8");
|
|
958
1013
|
return id;
|
|
959
1014
|
}
|
|
960
1015
|
var DECISION_DETECTION_VARIANT_ID = 1361527;
|
|
@@ -988,10 +1043,10 @@ function readLicenseStore() {
|
|
|
988
1043
|
const licensePath = getGlobalLicensePath();
|
|
989
1044
|
let store;
|
|
990
1045
|
try {
|
|
991
|
-
if (!
|
|
1046
|
+
if (!fs3.existsSync(licensePath)) {
|
|
992
1047
|
store = { version: 2, licenses: [] };
|
|
993
1048
|
} else {
|
|
994
|
-
const raw =
|
|
1049
|
+
const raw = fs3.readFileSync(licensePath, "utf-8");
|
|
995
1050
|
const data = JSON.parse(raw);
|
|
996
1051
|
if (data?.version === 2 && Array.isArray(data.licenses)) {
|
|
997
1052
|
store = data;
|
|
@@ -1008,11 +1063,11 @@ function readLicenseStore() {
|
|
|
1008
1063
|
}
|
|
1009
1064
|
function writeLicenseStore(store) {
|
|
1010
1065
|
const dirPath = getGlobalLicenseDir();
|
|
1011
|
-
if (!
|
|
1012
|
-
|
|
1066
|
+
if (!fs3.existsSync(dirPath)) {
|
|
1067
|
+
fs3.mkdirSync(dirPath, { recursive: true });
|
|
1013
1068
|
}
|
|
1014
|
-
const licensePath =
|
|
1015
|
-
|
|
1069
|
+
const licensePath = path5.join(dirPath, LICENSE_FILE);
|
|
1070
|
+
fs3.writeFileSync(licensePath, JSON.stringify(store, null, 2), "utf-8");
|
|
1016
1071
|
_cachedStore = store;
|
|
1017
1072
|
_cacheTimestamp = Date.now();
|
|
1018
1073
|
}
|
|
@@ -1041,7 +1096,14 @@ function getLicenseForFeature(feature) {
|
|
|
1041
1096
|
return features?.includes(feature);
|
|
1042
1097
|
});
|
|
1043
1098
|
}
|
|
1099
|
+
function getAllLicensesNeedingRevalidation() {
|
|
1100
|
+
return getActiveLicenses().filter((l) => needsRevalidation(l));
|
|
1101
|
+
}
|
|
1044
1102
|
var REVALIDATION_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
|
|
1103
|
+
function needsRevalidation(entry) {
|
|
1104
|
+
const lastValidated = new Date(entry.lastValidatedAt).getTime();
|
|
1105
|
+
return Date.now() - lastValidated > REVALIDATION_THRESHOLD_MS;
|
|
1106
|
+
}
|
|
1045
1107
|
|
|
1046
1108
|
// ../../packages/shared/src/featureGate.ts
|
|
1047
1109
|
var DefaultFeatureGate = class {
|
|
@@ -1052,8 +1114,8 @@ var DefaultFeatureGate = class {
|
|
|
1052
1114
|
var currentGate = new DefaultFeatureGate();
|
|
1053
1115
|
|
|
1054
1116
|
// ../../packages/shared/src/reader.ts
|
|
1055
|
-
import
|
|
1056
|
-
import
|
|
1117
|
+
import fs5 from "fs";
|
|
1118
|
+
import path7 from "path";
|
|
1057
1119
|
var STORAGE_DIR2 = ".keepgoing";
|
|
1058
1120
|
var META_FILE2 = "meta.json";
|
|
1059
1121
|
var SESSIONS_FILE2 = "sessions.json";
|
|
@@ -1075,16 +1137,16 @@ var KeepGoingReader = class {
|
|
|
1075
1137
|
this.workspacePath = workspacePath;
|
|
1076
1138
|
const mainRoot = resolveStorageRoot(workspacePath);
|
|
1077
1139
|
this._isWorktree = mainRoot !== workspacePath;
|
|
1078
|
-
this.storagePath =
|
|
1079
|
-
this.metaFilePath =
|
|
1080
|
-
this.sessionsFilePath =
|
|
1081
|
-
this.decisionsFilePath =
|
|
1082
|
-
this.stateFilePath =
|
|
1083
|
-
this.currentTasksFilePath =
|
|
1140
|
+
this.storagePath = path7.join(mainRoot, STORAGE_DIR2);
|
|
1141
|
+
this.metaFilePath = path7.join(this.storagePath, META_FILE2);
|
|
1142
|
+
this.sessionsFilePath = path7.join(this.storagePath, SESSIONS_FILE2);
|
|
1143
|
+
this.decisionsFilePath = path7.join(this.storagePath, DECISIONS_FILE);
|
|
1144
|
+
this.stateFilePath = path7.join(this.storagePath, STATE_FILE2);
|
|
1145
|
+
this.currentTasksFilePath = path7.join(this.storagePath, CURRENT_TASKS_FILE2);
|
|
1084
1146
|
}
|
|
1085
1147
|
/** Check if .keepgoing/ directory exists. */
|
|
1086
1148
|
exists() {
|
|
1087
|
-
return
|
|
1149
|
+
return fs5.existsSync(this.storagePath);
|
|
1088
1150
|
}
|
|
1089
1151
|
/** Read state.json, returns undefined if missing or corrupt. */
|
|
1090
1152
|
getState() {
|
|
@@ -1322,10 +1384,10 @@ var KeepGoingReader = class {
|
|
|
1322
1384
|
}
|
|
1323
1385
|
readJsonFile(filePath) {
|
|
1324
1386
|
try {
|
|
1325
|
-
if (!
|
|
1387
|
+
if (!fs5.existsSync(filePath)) {
|
|
1326
1388
|
return void 0;
|
|
1327
1389
|
}
|
|
1328
|
-
const raw =
|
|
1390
|
+
const raw = fs5.readFileSync(filePath, "utf-8");
|
|
1329
1391
|
return JSON.parse(raw);
|
|
1330
1392
|
} catch {
|
|
1331
1393
|
return void 0;
|
|
@@ -1334,9 +1396,9 @@ var KeepGoingReader = class {
|
|
|
1334
1396
|
};
|
|
1335
1397
|
|
|
1336
1398
|
// ../../packages/shared/src/setup.ts
|
|
1337
|
-
import
|
|
1338
|
-
import
|
|
1339
|
-
import
|
|
1399
|
+
import fs6 from "fs";
|
|
1400
|
+
import os3 from "os";
|
|
1401
|
+
import path8 from "path";
|
|
1340
1402
|
var KEEPGOING_MARKER = "@keepgoingdev/mcp-server";
|
|
1341
1403
|
var SESSION_START_HOOK = {
|
|
1342
1404
|
matcher: "",
|
|
@@ -1365,36 +1427,55 @@ var POST_TOOL_USE_HOOK = {
|
|
|
1365
1427
|
}
|
|
1366
1428
|
]
|
|
1367
1429
|
};
|
|
1368
|
-
var
|
|
1430
|
+
var SESSION_END_HOOK = {
|
|
1431
|
+
matcher: "",
|
|
1432
|
+
hooks: [
|
|
1433
|
+
{
|
|
1434
|
+
type: "command",
|
|
1435
|
+
command: "npx -y @keepgoingdev/mcp-server --save-checkpoint"
|
|
1436
|
+
}
|
|
1437
|
+
]
|
|
1438
|
+
};
|
|
1439
|
+
var KEEPGOING_RULES_VERSION = 1;
|
|
1440
|
+
var KEEPGOING_RULES_CONTENT = `<!-- @keepgoingdev/mcp-server v${KEEPGOING_RULES_VERSION} -->
|
|
1369
1441
|
## KeepGoing
|
|
1370
1442
|
|
|
1371
1443
|
After completing a task or meaningful piece of work, call the \`save_checkpoint\` MCP tool with:
|
|
1372
|
-
- \`summary\`: What
|
|
1373
|
-
- \`nextStep\`: What
|
|
1444
|
+
- \`summary\`: 1-2 sentences. What changed and why \u2014 no file paths, no implementation details (those are captured from git).
|
|
1445
|
+
- \`nextStep\`: What to do next
|
|
1374
1446
|
- \`blocker\`: Any blocker (if applicable)
|
|
1375
1447
|
`;
|
|
1448
|
+
function getRulesFileVersion(content) {
|
|
1449
|
+
const match = content.match(/<!-- @keepgoingdev\/mcp-server v(\d+) -->/);
|
|
1450
|
+
return match ? parseInt(match[1], 10) : null;
|
|
1451
|
+
}
|
|
1376
1452
|
var STATUSLINE_CMD = "npx -y @keepgoingdev/mcp-server --statusline";
|
|
1453
|
+
function detectClaudeDir() {
|
|
1454
|
+
return process.env.CLAUDE_CONFIG_DIR || path8.join(os3.homedir(), ".claude");
|
|
1455
|
+
}
|
|
1377
1456
|
function hasKeepGoingHook(hookEntries) {
|
|
1378
1457
|
return hookEntries.some(
|
|
1379
1458
|
(entry) => entry?.hooks?.some((h) => typeof h?.command === "string" && h.command.includes(KEEPGOING_MARKER))
|
|
1380
1459
|
);
|
|
1381
1460
|
}
|
|
1382
|
-
function resolveScopePaths(scope, workspacePath) {
|
|
1461
|
+
function resolveScopePaths(scope, workspacePath, overrideClaudeDir) {
|
|
1383
1462
|
if (scope === "user") {
|
|
1384
|
-
const claudeDir2 =
|
|
1463
|
+
const claudeDir2 = overrideClaudeDir || detectClaudeDir();
|
|
1385
1464
|
return {
|
|
1386
1465
|
claudeDir: claudeDir2,
|
|
1387
|
-
settingsPath:
|
|
1388
|
-
claudeMdPath:
|
|
1466
|
+
settingsPath: path8.join(claudeDir2, "settings.json"),
|
|
1467
|
+
claudeMdPath: path8.join(claudeDir2, "CLAUDE.md"),
|
|
1468
|
+
rulesPath: path8.join(claudeDir2, "rules", "keepgoing.md")
|
|
1389
1469
|
};
|
|
1390
1470
|
}
|
|
1391
|
-
const claudeDir =
|
|
1392
|
-
const dotClaudeMdPath =
|
|
1393
|
-
const rootClaudeMdPath =
|
|
1471
|
+
const claudeDir = path8.join(workspacePath, ".claude");
|
|
1472
|
+
const dotClaudeMdPath = path8.join(workspacePath, ".claude", "CLAUDE.md");
|
|
1473
|
+
const rootClaudeMdPath = path8.join(workspacePath, "CLAUDE.md");
|
|
1394
1474
|
return {
|
|
1395
1475
|
claudeDir,
|
|
1396
|
-
settingsPath:
|
|
1397
|
-
claudeMdPath:
|
|
1476
|
+
settingsPath: path8.join(claudeDir, "settings.json"),
|
|
1477
|
+
claudeMdPath: fs6.existsSync(dotClaudeMdPath) ? dotClaudeMdPath : rootClaudeMdPath,
|
|
1478
|
+
rulesPath: path8.join(workspacePath, ".claude", "rules", "keepgoing.md")
|
|
1398
1479
|
};
|
|
1399
1480
|
}
|
|
1400
1481
|
function writeHooksToSettings(settings) {
|
|
@@ -1423,15 +1504,22 @@ function writeHooksToSettings(settings) {
|
|
|
1423
1504
|
settings.hooks.PostToolUse.push(POST_TOOL_USE_HOOK);
|
|
1424
1505
|
changed = true;
|
|
1425
1506
|
}
|
|
1507
|
+
if (!Array.isArray(settings.hooks.SessionEnd)) {
|
|
1508
|
+
settings.hooks.SessionEnd = [];
|
|
1509
|
+
}
|
|
1510
|
+
if (!hasKeepGoingHook(settings.hooks.SessionEnd)) {
|
|
1511
|
+
settings.hooks.SessionEnd.push(SESSION_END_HOOK);
|
|
1512
|
+
changed = true;
|
|
1513
|
+
}
|
|
1426
1514
|
return changed;
|
|
1427
1515
|
}
|
|
1428
1516
|
function checkHookConflict(scope, workspacePath) {
|
|
1429
1517
|
const otherPaths = resolveScopePaths(scope === "user" ? "project" : "user", workspacePath);
|
|
1430
|
-
if (!
|
|
1518
|
+
if (!fs6.existsSync(otherPaths.settingsPath)) {
|
|
1431
1519
|
return null;
|
|
1432
1520
|
}
|
|
1433
1521
|
try {
|
|
1434
|
-
const otherSettings = JSON.parse(
|
|
1522
|
+
const otherSettings = JSON.parse(fs6.readFileSync(otherPaths.settingsPath, "utf-8"));
|
|
1435
1523
|
const hooks = otherSettings?.hooks;
|
|
1436
1524
|
if (!hooks) return null;
|
|
1437
1525
|
const hasConflict = Array.isArray(hooks.SessionStart) && hasKeepGoingHook(hooks.SessionStart) || Array.isArray(hooks.Stop) && hasKeepGoingHook(hooks.Stop);
|
|
@@ -1450,15 +1538,20 @@ function setupProject(options) {
|
|
|
1450
1538
|
scope = "project",
|
|
1451
1539
|
sessionHooks = true,
|
|
1452
1540
|
claudeMd = true,
|
|
1541
|
+
claudeDir: claudeDirOverride,
|
|
1453
1542
|
statusline
|
|
1454
1543
|
} = options;
|
|
1455
1544
|
const messages = [];
|
|
1456
1545
|
let changed = false;
|
|
1457
|
-
const { claudeDir, settingsPath, claudeMdPath } = resolveScopePaths(
|
|
1458
|
-
|
|
1546
|
+
const { claudeDir, settingsPath, claudeMdPath, rulesPath } = resolveScopePaths(
|
|
1547
|
+
scope,
|
|
1548
|
+
workspacePath,
|
|
1549
|
+
claudeDirOverride
|
|
1550
|
+
);
|
|
1551
|
+
const scopeLabel = scope === "user" ? path8.join("~", path8.relative(os3.homedir(), claudeDir), "settings.json").replace(/\\/g, "/") : ".claude/settings.json";
|
|
1459
1552
|
let settings = {};
|
|
1460
|
-
if (
|
|
1461
|
-
settings = JSON.parse(
|
|
1553
|
+
if (fs6.existsSync(settingsPath)) {
|
|
1554
|
+
settings = JSON.parse(fs6.readFileSync(settingsPath, "utf-8"));
|
|
1462
1555
|
}
|
|
1463
1556
|
let settingsChanged = false;
|
|
1464
1557
|
if (sessionHooks) {
|
|
@@ -1489,29 +1582,45 @@ function setupProject(options) {
|
|
|
1489
1582
|
statusline?.cleanup?.();
|
|
1490
1583
|
}
|
|
1491
1584
|
if (settingsChanged) {
|
|
1492
|
-
if (!
|
|
1493
|
-
|
|
1585
|
+
if (!fs6.existsSync(claudeDir)) {
|
|
1586
|
+
fs6.mkdirSync(claudeDir, { recursive: true });
|
|
1494
1587
|
}
|
|
1495
|
-
|
|
1588
|
+
fs6.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
1496
1589
|
changed = true;
|
|
1497
1590
|
}
|
|
1498
1591
|
if (claudeMd) {
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1592
|
+
const rulesDir = path8.dirname(rulesPath);
|
|
1593
|
+
const rulesLabel = scope === "user" ? path8.join(path8.relative(os3.homedir(), path8.dirname(rulesPath)), "keepgoing.md").replace(/\\/g, "/") : ".claude/rules/keepgoing.md";
|
|
1594
|
+
if (fs6.existsSync(rulesPath)) {
|
|
1595
|
+
const existing = fs6.readFileSync(rulesPath, "utf-8");
|
|
1596
|
+
const existingVersion = getRulesFileVersion(existing);
|
|
1597
|
+
if (existingVersion === null) {
|
|
1598
|
+
messages.push(`Rules file: Custom file found at ${rulesLabel}, skipping`);
|
|
1599
|
+
} else if (existingVersion >= KEEPGOING_RULES_VERSION) {
|
|
1600
|
+
messages.push(`Rules file: Already up to date (v${existingVersion}), skipped`);
|
|
1601
|
+
} else {
|
|
1602
|
+
if (!fs6.existsSync(rulesDir)) {
|
|
1603
|
+
fs6.mkdirSync(rulesDir, { recursive: true });
|
|
1604
|
+
}
|
|
1605
|
+
fs6.writeFileSync(rulesPath, KEEPGOING_RULES_CONTENT);
|
|
1606
|
+
changed = true;
|
|
1607
|
+
messages.push(`Rules file: Updated v${existingVersion} \u2192 v${KEEPGOING_RULES_VERSION} at ${rulesLabel}`);
|
|
1608
|
+
}
|
|
1506
1609
|
} else {
|
|
1507
|
-
const
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
fs5.mkdirSync(mdDir, { recursive: true });
|
|
1610
|
+
const existingClaudeMd = fs6.existsSync(claudeMdPath) ? fs6.readFileSync(claudeMdPath, "utf-8") : "";
|
|
1611
|
+
if (!fs6.existsSync(rulesDir)) {
|
|
1612
|
+
fs6.mkdirSync(rulesDir, { recursive: true });
|
|
1511
1613
|
}
|
|
1512
|
-
|
|
1614
|
+
fs6.writeFileSync(rulesPath, KEEPGOING_RULES_CONTENT);
|
|
1513
1615
|
changed = true;
|
|
1514
|
-
|
|
1616
|
+
if (existingClaudeMd.includes("## KeepGoing")) {
|
|
1617
|
+
const mdLabel = scope === "user" ? "~/.claude/CLAUDE.md" : "CLAUDE.md";
|
|
1618
|
+
messages.push(
|
|
1619
|
+
`Rules file: Created ${rulesLabel} (you can now remove the ## KeepGoing section from ${mdLabel})`
|
|
1620
|
+
);
|
|
1621
|
+
} else {
|
|
1622
|
+
messages.push(`Rules file: Created ${rulesLabel}`);
|
|
1623
|
+
}
|
|
1515
1624
|
}
|
|
1516
1625
|
}
|
|
1517
1626
|
return { messages, changed };
|
|
@@ -1588,6 +1697,39 @@ async function activateLicense(licenseKey, instanceName, options) {
|
|
|
1588
1697
|
return { valid: false, error: message };
|
|
1589
1698
|
}
|
|
1590
1699
|
}
|
|
1700
|
+
async function validateLicense(licenseKey, instanceId, options) {
|
|
1701
|
+
try {
|
|
1702
|
+
const res = await fetchWithTimeout(`${BASE_URL}/validate`, {
|
|
1703
|
+
method: "POST",
|
|
1704
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1705
|
+
body: new URLSearchParams({ license_key: licenseKey, instance_id: instanceId })
|
|
1706
|
+
});
|
|
1707
|
+
const data = await safeJson(res);
|
|
1708
|
+
if (!res.ok || !data?.valid) {
|
|
1709
|
+
return { valid: false, isNetworkError: false, error: data?.error || `Validation failed (${res.status})` };
|
|
1710
|
+
}
|
|
1711
|
+
if (!options?.allowTestMode && data.license_key?.test_mode) {
|
|
1712
|
+
return { valid: false, isNetworkError: false, error: "This is a test license key. Please use a production license key from your purchase confirmation." };
|
|
1713
|
+
}
|
|
1714
|
+
if (!options?.allowTestMode) {
|
|
1715
|
+
const productError = validateProductIdentity(data.meta);
|
|
1716
|
+
if (productError) {
|
|
1717
|
+
return { valid: false, isNetworkError: false, error: productError };
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
return {
|
|
1721
|
+
valid: true,
|
|
1722
|
+
licenseKey: data.license_key?.key,
|
|
1723
|
+
customerName: data.meta?.customer_name,
|
|
1724
|
+
productName: data.meta?.product_name,
|
|
1725
|
+
variantId: data.meta?.variant_id,
|
|
1726
|
+
variantName: data.meta?.variant_name
|
|
1727
|
+
};
|
|
1728
|
+
} catch (err) {
|
|
1729
|
+
const message = err instanceof Error && err.name === "AbortError" ? "Request timed out. Please check your network connection and try again." : err instanceof Error ? err.message : "Network error";
|
|
1730
|
+
return { valid: false, isNetworkError: true, error: message };
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1591
1733
|
async function deactivateLicense(licenseKey, instanceId) {
|
|
1592
1734
|
try {
|
|
1593
1735
|
const res = await fetchWithTimeout(`${BASE_URL}/deactivate`, {
|
|
@@ -1606,6 +1748,52 @@ async function deactivateLicense(licenseKey, instanceId) {
|
|
|
1606
1748
|
}
|
|
1607
1749
|
}
|
|
1608
1750
|
|
|
1751
|
+
// ../../packages/shared/src/licenseRevalidation.ts
|
|
1752
|
+
async function revalidateStaleLicenses(options) {
|
|
1753
|
+
const stale = getAllLicensesNeedingRevalidation();
|
|
1754
|
+
const result = { checked: stale.length, refreshed: 0, revoked: 0, skippedNetworkError: 0 };
|
|
1755
|
+
if (stale.length === 0) return result;
|
|
1756
|
+
const updates = /* @__PURE__ */ new Map();
|
|
1757
|
+
for (const entry of stale) {
|
|
1758
|
+
const vResult = await validateLicense(entry.licenseKey, entry.instanceId, {
|
|
1759
|
+
allowTestMode: options?.allowTestMode
|
|
1760
|
+
});
|
|
1761
|
+
if (vResult.valid) {
|
|
1762
|
+
const patch = {
|
|
1763
|
+
lastValidatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1764
|
+
};
|
|
1765
|
+
if (vResult.customerName) patch.customerName = vResult.customerName;
|
|
1766
|
+
updates.set(entry.licenseKey, patch);
|
|
1767
|
+
result.refreshed++;
|
|
1768
|
+
} else if (vResult.isNetworkError) {
|
|
1769
|
+
result.skippedNetworkError++;
|
|
1770
|
+
} else {
|
|
1771
|
+
updates.set(entry.licenseKey, { status: "inactive" });
|
|
1772
|
+
result.revoked++;
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
if (updates.size > 0) {
|
|
1776
|
+
const store = readLicenseStore();
|
|
1777
|
+
for (const [key, patch] of updates) {
|
|
1778
|
+
const idx = store.licenses.findIndex((l) => l.licenseKey === key);
|
|
1779
|
+
if (idx >= 0) {
|
|
1780
|
+
Object.assign(store.licenses[idx], patch);
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
writeLicenseStore(store);
|
|
1784
|
+
}
|
|
1785
|
+
return result;
|
|
1786
|
+
}
|
|
1787
|
+
async function getLicenseForFeatureWithRevalidation(feature, options) {
|
|
1788
|
+
const license = getLicenseForFeature(feature);
|
|
1789
|
+
if (!license) return void 0;
|
|
1790
|
+
if (needsRevalidation(license)) {
|
|
1791
|
+
await revalidateStaleLicenses(options);
|
|
1792
|
+
return getLicenseForFeature(feature);
|
|
1793
|
+
}
|
|
1794
|
+
return license;
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1609
1797
|
// src/render.ts
|
|
1610
1798
|
var RESET = "\x1B[0m";
|
|
1611
1799
|
var BOLD = "\x1B[1m";
|
|
@@ -1919,14 +2107,14 @@ function renderEnrichedBriefingQuiet(briefing) {
|
|
|
1919
2107
|
// src/updateCheck.ts
|
|
1920
2108
|
import { spawn } from "child_process";
|
|
1921
2109
|
import { readFileSync, existsSync } from "fs";
|
|
1922
|
-
import
|
|
1923
|
-
import
|
|
1924
|
-
var CLI_VERSION = "1.
|
|
2110
|
+
import path9 from "path";
|
|
2111
|
+
import os4 from "os";
|
|
2112
|
+
var CLI_VERSION = "1.2.1";
|
|
1925
2113
|
var NPM_REGISTRY_URL = "https://registry.npmjs.org/@keepgoingdev/cli/latest";
|
|
1926
2114
|
var FETCH_TIMEOUT_MS = 5e3;
|
|
1927
2115
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
1928
|
-
var CACHE_DIR =
|
|
1929
|
-
var CACHE_PATH =
|
|
2116
|
+
var CACHE_DIR = path9.join(os4.homedir(), ".keepgoing");
|
|
2117
|
+
var CACHE_PATH = path9.join(CACHE_DIR, "update-check.json");
|
|
1930
2118
|
function isNewerVersion(current, latest) {
|
|
1931
2119
|
const cur = current.split(".").map(Number);
|
|
1932
2120
|
const lat = latest.split(".").map(Number);
|
|
@@ -2041,7 +2229,7 @@ async function statusCommand(opts) {
|
|
|
2041
2229
|
}
|
|
2042
2230
|
|
|
2043
2231
|
// src/commands/save.ts
|
|
2044
|
-
import
|
|
2232
|
+
import path10 from "path";
|
|
2045
2233
|
async function saveCommand(opts) {
|
|
2046
2234
|
const { cwd, message, nextStepOverride, json, quiet, force } = opts;
|
|
2047
2235
|
const isManual = !!message;
|
|
@@ -2070,9 +2258,9 @@ async function saveCommand(opts) {
|
|
|
2070
2258
|
sessionStartTime: lastSession?.timestamp ?? now,
|
|
2071
2259
|
lastActivityTime: now
|
|
2072
2260
|
});
|
|
2073
|
-
const summary = message ?? buildSmartSummary(events) ?? `Worked on ${touchedFiles.slice(0, 5).map((f) =>
|
|
2261
|
+
const summary = message ?? buildSmartSummary(events) ?? `Worked on ${touchedFiles.slice(0, 5).map((f) => path10.basename(f)).join(", ")}`;
|
|
2074
2262
|
const nextStep = nextStepOverride ?? buildSmartNextStep(events);
|
|
2075
|
-
const projectName =
|
|
2263
|
+
const projectName = path10.basename(resolveStorageRoot(cwd));
|
|
2076
2264
|
const sessionId = generateSessionId({
|
|
2077
2265
|
workspaceRoot: cwd,
|
|
2078
2266
|
branch: gitBranch ?? void 0,
|
|
@@ -2105,12 +2293,25 @@ async function saveCommand(opts) {
|
|
|
2105
2293
|
}
|
|
2106
2294
|
|
|
2107
2295
|
// src/commands/hook.ts
|
|
2108
|
-
import
|
|
2109
|
-
import
|
|
2110
|
-
import
|
|
2296
|
+
import fs7 from "fs";
|
|
2297
|
+
import path11 from "path";
|
|
2298
|
+
import os5 from "os";
|
|
2111
2299
|
import { execSync } from "child_process";
|
|
2112
2300
|
var HOOK_MARKER_START = "# keepgoing-hook-start";
|
|
2113
2301
|
var HOOK_MARKER_END = "# keepgoing-hook-end";
|
|
2302
|
+
var POST_COMMIT_MARKER_START = "# keepgoing-post-commit-start";
|
|
2303
|
+
var POST_COMMIT_MARKER_END = "# keepgoing-post-commit-end";
|
|
2304
|
+
var KEEPGOING_HOOKS_DIR = path11.join(os5.homedir(), ".keepgoing", "hooks");
|
|
2305
|
+
var POST_COMMIT_HOOK_PATH = path11.join(KEEPGOING_HOOKS_DIR, "post-commit");
|
|
2306
|
+
var KEEPGOING_MANAGED_MARKER = path11.join(KEEPGOING_HOOKS_DIR, ".keepgoing-managed");
|
|
2307
|
+
var POST_COMMIT_HOOK = `#!/bin/sh
|
|
2308
|
+
${POST_COMMIT_MARKER_START}
|
|
2309
|
+
# Runs after every git commit. Detects high-signal decisions.
|
|
2310
|
+
if command -v keepgoing-mcp-server >/dev/null 2>&1; then
|
|
2311
|
+
keepgoing-mcp-server --detect-decisions 2>/dev/null &
|
|
2312
|
+
fi
|
|
2313
|
+
${POST_COMMIT_MARKER_END}
|
|
2314
|
+
`;
|
|
2114
2315
|
var ZSH_HOOK = `${HOOK_MARKER_START}
|
|
2115
2316
|
# KeepGoing shell hook \u2014 auto-injected by 'keepgoing hook install'
|
|
2116
2317
|
if command -v keepgoing >/dev/null 2>&1; then
|
|
@@ -2143,7 +2344,7 @@ if command -v keepgoing >/dev/null 2>&1
|
|
|
2143
2344
|
end
|
|
2144
2345
|
${HOOK_MARKER_END}`;
|
|
2145
2346
|
function detectShellRcFile(shellOverride) {
|
|
2146
|
-
const home =
|
|
2347
|
+
const home = os5.homedir();
|
|
2147
2348
|
let shell;
|
|
2148
2349
|
if (shellOverride) {
|
|
2149
2350
|
shell = shellOverride.toLowerCase();
|
|
@@ -2166,17 +2367,155 @@ function detectShellRcFile(shellOverride) {
|
|
|
2166
2367
|
}
|
|
2167
2368
|
}
|
|
2168
2369
|
if (shell === "zsh") {
|
|
2169
|
-
return { shell: "zsh", rcFile:
|
|
2370
|
+
return { shell: "zsh", rcFile: path11.join(home, ".zshrc") };
|
|
2170
2371
|
}
|
|
2171
2372
|
if (shell === "bash") {
|
|
2172
|
-
return { shell: "bash", rcFile:
|
|
2373
|
+
return { shell: "bash", rcFile: path11.join(home, ".bashrc") };
|
|
2173
2374
|
}
|
|
2174
2375
|
if (shell === "fish") {
|
|
2175
|
-
const xdgConfig = process.env["XDG_CONFIG_HOME"] ||
|
|
2176
|
-
return { shell: "fish", rcFile:
|
|
2376
|
+
const xdgConfig = process.env["XDG_CONFIG_HOME"] || path11.join(home, ".config");
|
|
2377
|
+
return { shell: "fish", rcFile: path11.join(xdgConfig, "fish", "config.fish") };
|
|
2177
2378
|
}
|
|
2178
2379
|
return void 0;
|
|
2179
2380
|
}
|
|
2381
|
+
function resolveGlobalGitignorePath() {
|
|
2382
|
+
try {
|
|
2383
|
+
const configured = execSync("git config --global core.excludesfile", {
|
|
2384
|
+
encoding: "utf-8",
|
|
2385
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2386
|
+
}).trim();
|
|
2387
|
+
if (configured) {
|
|
2388
|
+
return configured.startsWith("~") ? path11.join(os5.homedir(), configured.slice(1)) : configured;
|
|
2389
|
+
}
|
|
2390
|
+
} catch {
|
|
2391
|
+
}
|
|
2392
|
+
return path11.join(os5.homedir(), ".gitignore_global");
|
|
2393
|
+
}
|
|
2394
|
+
function installGlobalGitignore() {
|
|
2395
|
+
const ignorePath = resolveGlobalGitignorePath();
|
|
2396
|
+
let existing = "";
|
|
2397
|
+
try {
|
|
2398
|
+
existing = fs7.readFileSync(ignorePath, "utf-8");
|
|
2399
|
+
} catch {
|
|
2400
|
+
}
|
|
2401
|
+
if (existing.split("\n").some((line) => line.trim() === ".keepgoing")) {
|
|
2402
|
+
console.log(`Global gitignore: .keepgoing already present in ${ignorePath}`);
|
|
2403
|
+
} else {
|
|
2404
|
+
const suffix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
2405
|
+
fs7.appendFileSync(ignorePath, `${suffix}.keepgoing
|
|
2406
|
+
`, "utf-8");
|
|
2407
|
+
console.log(`Global gitignore: .keepgoing added to ${ignorePath}`);
|
|
2408
|
+
}
|
|
2409
|
+
try {
|
|
2410
|
+
execSync("git config --global core.excludesfile", {
|
|
2411
|
+
encoding: "utf-8",
|
|
2412
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2413
|
+
}).trim();
|
|
2414
|
+
} catch {
|
|
2415
|
+
execSync(`git config --global core.excludesfile "${ignorePath}"`, {
|
|
2416
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2417
|
+
});
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
function uninstallGlobalGitignore() {
|
|
2421
|
+
const ignorePath = resolveGlobalGitignorePath();
|
|
2422
|
+
let existing = "";
|
|
2423
|
+
try {
|
|
2424
|
+
existing = fs7.readFileSync(ignorePath, "utf-8");
|
|
2425
|
+
} catch {
|
|
2426
|
+
return;
|
|
2427
|
+
}
|
|
2428
|
+
const lines = existing.split("\n");
|
|
2429
|
+
const filtered = lines.filter((line) => line.trim() !== ".keepgoing");
|
|
2430
|
+
if (filtered.length !== lines.length) {
|
|
2431
|
+
fs7.writeFileSync(ignorePath, filtered.join("\n"), "utf-8");
|
|
2432
|
+
console.log(`Global gitignore: .keepgoing removed from ${ignorePath}`);
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
function installPostCommitHook() {
|
|
2436
|
+
fs7.mkdirSync(KEEPGOING_HOOKS_DIR, { recursive: true });
|
|
2437
|
+
if (fs7.existsSync(POST_COMMIT_HOOK_PATH)) {
|
|
2438
|
+
const existing = fs7.readFileSync(POST_COMMIT_HOOK_PATH, "utf-8");
|
|
2439
|
+
if (existing.includes(POST_COMMIT_MARKER_START)) {
|
|
2440
|
+
console.log(`Git post-commit hook: already installed at ${POST_COMMIT_HOOK_PATH}`);
|
|
2441
|
+
} else {
|
|
2442
|
+
const suffix = existing.endsWith("\n") ? "" : "\n";
|
|
2443
|
+
const block = `${suffix}
|
|
2444
|
+
${POST_COMMIT_MARKER_START}
|
|
2445
|
+
# Runs after every git commit. Detects high-signal decisions.
|
|
2446
|
+
if command -v keepgoing-mcp-server >/dev/null 2>&1; then
|
|
2447
|
+
keepgoing-mcp-server --detect-decisions 2>/dev/null &
|
|
2448
|
+
fi
|
|
2449
|
+
${POST_COMMIT_MARKER_END}
|
|
2450
|
+
`;
|
|
2451
|
+
fs7.appendFileSync(POST_COMMIT_HOOK_PATH, block, "utf-8");
|
|
2452
|
+
console.log(`Git post-commit hook: appended to existing ${POST_COMMIT_HOOK_PATH}`);
|
|
2453
|
+
}
|
|
2454
|
+
} else {
|
|
2455
|
+
fs7.writeFileSync(POST_COMMIT_HOOK_PATH, POST_COMMIT_HOOK, { mode: 493 });
|
|
2456
|
+
console.log(`Git post-commit hook: installed at ${POST_COMMIT_HOOK_PATH}`);
|
|
2457
|
+
}
|
|
2458
|
+
fs7.chmodSync(POST_COMMIT_HOOK_PATH, 493);
|
|
2459
|
+
let currentHooksPath;
|
|
2460
|
+
try {
|
|
2461
|
+
currentHooksPath = execSync("git config --global core.hooksPath", {
|
|
2462
|
+
encoding: "utf-8",
|
|
2463
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2464
|
+
}).trim();
|
|
2465
|
+
} catch {
|
|
2466
|
+
}
|
|
2467
|
+
if (currentHooksPath === KEEPGOING_HOOKS_DIR) {
|
|
2468
|
+
console.log(`Git hooks path: already set to ${KEEPGOING_HOOKS_DIR}`);
|
|
2469
|
+
} else if (currentHooksPath) {
|
|
2470
|
+
console.log(`Git hooks path: set to ${currentHooksPath}`);
|
|
2471
|
+
console.log(` To use KeepGoing's post-commit hook, add the following to ${currentHooksPath}/post-commit:`);
|
|
2472
|
+
console.log(` if command -v keepgoing-mcp-server >/dev/null 2>&1; then`);
|
|
2473
|
+
console.log(` keepgoing-mcp-server --detect-decisions 2>/dev/null &`);
|
|
2474
|
+
console.log(` fi`);
|
|
2475
|
+
} else {
|
|
2476
|
+
console.log(`Git post-commit hook: saved to ${POST_COMMIT_HOOK_PATH}`);
|
|
2477
|
+
console.log(` To activate it globally, run:`);
|
|
2478
|
+
console.log(` git config --global core.hooksPath "${KEEPGOING_HOOKS_DIR}"`);
|
|
2479
|
+
console.log(` Note: this overrides per-repo .git/hooks/ for all repositories.`);
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
function uninstallPostCommitHook() {
|
|
2483
|
+
if (fs7.existsSync(POST_COMMIT_HOOK_PATH)) {
|
|
2484
|
+
const existing = fs7.readFileSync(POST_COMMIT_HOOK_PATH, "utf-8");
|
|
2485
|
+
if (existing.includes(POST_COMMIT_MARKER_START)) {
|
|
2486
|
+
const pattern = new RegExp(
|
|
2487
|
+
`
|
|
2488
|
+
?${POST_COMMIT_MARKER_START}[\\s\\S]*?${POST_COMMIT_MARKER_END}
|
|
2489
|
+
?`,
|
|
2490
|
+
"g"
|
|
2491
|
+
);
|
|
2492
|
+
const updated = existing.replace(pattern, "").trim();
|
|
2493
|
+
if (!updated || updated === "#!/bin/sh") {
|
|
2494
|
+
fs7.unlinkSync(POST_COMMIT_HOOK_PATH);
|
|
2495
|
+
console.log(`Git post-commit hook: removed ${POST_COMMIT_HOOK_PATH}`);
|
|
2496
|
+
} else {
|
|
2497
|
+
fs7.writeFileSync(POST_COMMIT_HOOK_PATH, updated + "\n", "utf-8");
|
|
2498
|
+
console.log(`Git post-commit hook: KeepGoing block removed from ${POST_COMMIT_HOOK_PATH}`);
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
try {
|
|
2503
|
+
const currentHooksPath = execSync("git config --global core.hooksPath", {
|
|
2504
|
+
encoding: "utf-8",
|
|
2505
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2506
|
+
}).trim();
|
|
2507
|
+
if (currentHooksPath === KEEPGOING_HOOKS_DIR) {
|
|
2508
|
+
execSync("git config --global --unset core.hooksPath", {
|
|
2509
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2510
|
+
});
|
|
2511
|
+
console.log("Git hooks path: global core.hooksPath unset");
|
|
2512
|
+
}
|
|
2513
|
+
} catch {
|
|
2514
|
+
}
|
|
2515
|
+
if (fs7.existsSync(KEEPGOING_MANAGED_MARKER)) {
|
|
2516
|
+
fs7.unlinkSync(KEEPGOING_MANAGED_MARKER);
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2180
2519
|
function hookInstallCommand(shellOverride) {
|
|
2181
2520
|
const detected = detectShellRcFile(shellOverride);
|
|
2182
2521
|
if (!detected) {
|
|
@@ -2189,18 +2528,23 @@ function hookInstallCommand(shellOverride) {
|
|
|
2189
2528
|
const hookBlock = shell === "zsh" ? ZSH_HOOK : shell === "fish" ? FISH_HOOK : BASH_HOOK;
|
|
2190
2529
|
let existing = "";
|
|
2191
2530
|
try {
|
|
2192
|
-
existing =
|
|
2531
|
+
existing = fs7.readFileSync(rcFile, "utf-8");
|
|
2193
2532
|
} catch {
|
|
2194
2533
|
}
|
|
2195
2534
|
if (existing.includes(HOOK_MARKER_START)) {
|
|
2196
2535
|
console.log(`KeepGoing hook is already installed in ${rcFile}.`);
|
|
2536
|
+
installGlobalGitignore();
|
|
2537
|
+
installPostCommitHook();
|
|
2197
2538
|
return;
|
|
2198
2539
|
}
|
|
2199
|
-
|
|
2540
|
+
fs7.appendFileSync(rcFile, `
|
|
2200
2541
|
${hookBlock}
|
|
2201
2542
|
`, "utf-8");
|
|
2202
2543
|
console.log(`KeepGoing hook installed in ${rcFile}.`);
|
|
2203
|
-
|
|
2544
|
+
installGlobalGitignore();
|
|
2545
|
+
installPostCommitHook();
|
|
2546
|
+
console.log(`
|
|
2547
|
+
Reload your shell config to activate it:
|
|
2204
2548
|
`);
|
|
2205
2549
|
console.log(` source ${rcFile}`);
|
|
2206
2550
|
}
|
|
@@ -2215,7 +2559,7 @@ function hookUninstallCommand(shellOverride) {
|
|
|
2215
2559
|
const { rcFile } = detected;
|
|
2216
2560
|
let existing = "";
|
|
2217
2561
|
try {
|
|
2218
|
-
existing =
|
|
2562
|
+
existing = fs7.readFileSync(rcFile, "utf-8");
|
|
2219
2563
|
} catch {
|
|
2220
2564
|
console.log(`${rcFile} not found \u2014 nothing to remove.`);
|
|
2221
2565
|
return;
|
|
@@ -2231,9 +2575,12 @@ function hookUninstallCommand(shellOverride) {
|
|
|
2231
2575
|
"g"
|
|
2232
2576
|
);
|
|
2233
2577
|
const updated = existing.replace(pattern, "").replace(/\n{3,}/g, "\n\n");
|
|
2234
|
-
|
|
2578
|
+
fs7.writeFileSync(rcFile, updated, "utf-8");
|
|
2235
2579
|
console.log(`KeepGoing hook removed from ${rcFile}.`);
|
|
2236
|
-
|
|
2580
|
+
uninstallGlobalGitignore();
|
|
2581
|
+
uninstallPostCommitHook();
|
|
2582
|
+
console.log(`
|
|
2583
|
+
Reload your shell config to deactivate it:
|
|
2237
2584
|
`);
|
|
2238
2585
|
console.log(` source ${rcFile}`);
|
|
2239
2586
|
}
|
|
@@ -2482,7 +2829,7 @@ async function decisionsCommand(opts) {
|
|
|
2482
2829
|
}
|
|
2483
2830
|
return;
|
|
2484
2831
|
}
|
|
2485
|
-
if (process.env.KEEPGOING_PRO_BYPASS !== "1" && !
|
|
2832
|
+
if (process.env.KEEPGOING_PRO_BYPASS !== "1" && !await getLicenseForFeatureWithRevalidation("decisions")) {
|
|
2486
2833
|
console.error(
|
|
2487
2834
|
'Decision Detection requires a Pro license.\nRun "keepgoing activate <key>" or visit https://keepgoing.dev/add-ons to purchase.'
|
|
2488
2835
|
);
|
|
@@ -2686,8 +3033,8 @@ function renderGrouped(sessions, showStat) {
|
|
|
2686
3033
|
}
|
|
2687
3034
|
}
|
|
2688
3035
|
}
|
|
2689
|
-
function logDecisions(reader, opts) {
|
|
2690
|
-
const license =
|
|
3036
|
+
async function logDecisions(reader, opts) {
|
|
3037
|
+
const license = await getLicenseForFeatureWithRevalidation("decisions");
|
|
2691
3038
|
if (!license) {
|
|
2692
3039
|
console.log("Decision tracking requires a Pro license. Run: keepgoing activate <key>");
|
|
2693
3040
|
return;
|
|
@@ -2733,7 +3080,7 @@ async function logCommand(opts) {
|
|
|
2733
3080
|
return;
|
|
2734
3081
|
}
|
|
2735
3082
|
if (opts.subcommand === "decisions") {
|
|
2736
|
-
logDecisions(reader, opts);
|
|
3083
|
+
await logDecisions(reader, opts);
|
|
2737
3084
|
} else {
|
|
2738
3085
|
logSessions(reader, opts);
|
|
2739
3086
|
}
|
|
@@ -3216,7 +3563,7 @@ async function main() {
|
|
|
3216
3563
|
}
|
|
3217
3564
|
break;
|
|
3218
3565
|
case "version":
|
|
3219
|
-
console.log(`keepgoing v${"1.
|
|
3566
|
+
console.log(`keepgoing v${"1.2.1"}`);
|
|
3220
3567
|
break;
|
|
3221
3568
|
case "activate":
|
|
3222
3569
|
await activateCommand({ licenseKey: subcommand });
|