@keepgoingdev/mcp-server 0.5.6 → 0.5.7
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 +304 -274
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1144,96 +1144,7 @@ function tryDetectDecision(opts) {
|
|
|
1144
1144
|
};
|
|
1145
1145
|
}
|
|
1146
1146
|
|
|
1147
|
-
// ../../packages/shared/src/
|
|
1148
|
-
var BASE_URL = "https://api.lemonsqueezy.com/v1/licenses";
|
|
1149
|
-
var REQUEST_TIMEOUT_MS = 15e3;
|
|
1150
|
-
var EXPECTED_STORE_ID = 301555;
|
|
1151
|
-
var EXPECTED_PRODUCT_ID = 864311;
|
|
1152
|
-
function fetchWithTimeout(url, init) {
|
|
1153
|
-
const controller = new AbortController();
|
|
1154
|
-
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
1155
|
-
return fetch(url, { ...init, signal: controller.signal }).finally(() => clearTimeout(timer));
|
|
1156
|
-
}
|
|
1157
|
-
function validateProductIdentity(meta) {
|
|
1158
|
-
if (!meta) return "License response missing product metadata.";
|
|
1159
|
-
if (meta.store_id !== EXPECTED_STORE_ID || meta.product_id !== EXPECTED_PRODUCT_ID) {
|
|
1160
|
-
return "This license key does not belong to KeepGoing.";
|
|
1161
|
-
}
|
|
1162
|
-
return void 0;
|
|
1163
|
-
}
|
|
1164
|
-
async function safeJson(res) {
|
|
1165
|
-
try {
|
|
1166
|
-
const text = await res.text();
|
|
1167
|
-
return JSON.parse(text);
|
|
1168
|
-
} catch {
|
|
1169
|
-
return null;
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1172
|
-
async function activateLicense(licenseKey, instanceName, options) {
|
|
1173
|
-
try {
|
|
1174
|
-
const res = await fetchWithTimeout(`${BASE_URL}/activate`, {
|
|
1175
|
-
method: "POST",
|
|
1176
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1177
|
-
body: new URLSearchParams({ license_key: licenseKey, instance_name: instanceName })
|
|
1178
|
-
});
|
|
1179
|
-
const data = await safeJson(res);
|
|
1180
|
-
if (!res.ok || !data?.activated) {
|
|
1181
|
-
return { valid: false, error: data?.error || `Activation failed (${res.status})` };
|
|
1182
|
-
}
|
|
1183
|
-
if (!options?.allowTestMode && data.license_key?.test_mode) {
|
|
1184
|
-
if (data.license_key?.key && data.instance?.id) {
|
|
1185
|
-
await deactivateLicense(data.license_key.key, data.instance.id);
|
|
1186
|
-
}
|
|
1187
|
-
return { valid: false, error: "This is a test license key. Please use a production license key from your purchase confirmation." };
|
|
1188
|
-
}
|
|
1189
|
-
if (!options?.allowTestMode) {
|
|
1190
|
-
const productError = validateProductIdentity(data.meta);
|
|
1191
|
-
if (productError) {
|
|
1192
|
-
if (data.license_key?.key && data.instance?.id) {
|
|
1193
|
-
await deactivateLicense(data.license_key.key, data.instance.id);
|
|
1194
|
-
}
|
|
1195
|
-
return { valid: false, error: productError };
|
|
1196
|
-
}
|
|
1197
|
-
if (data.meta?.variant_id && !KNOWN_VARIANT_IDS.has(data.meta.variant_id)) {
|
|
1198
|
-
if (data.license_key?.key && data.instance?.id) {
|
|
1199
|
-
await deactivateLicense(data.license_key.key, data.instance.id);
|
|
1200
|
-
}
|
|
1201
|
-
return { valid: false, error: "This license key is for an unrecognized add-on variant. Please update KeepGoing or contact support." };
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
return {
|
|
1205
|
-
valid: true,
|
|
1206
|
-
licenseKey: data.license_key?.key,
|
|
1207
|
-
instanceId: data.instance?.id,
|
|
1208
|
-
customerName: data.meta?.customer_name,
|
|
1209
|
-
productName: data.meta?.product_name,
|
|
1210
|
-
variantId: data.meta?.variant_id,
|
|
1211
|
-
variantName: data.meta?.variant_name
|
|
1212
|
-
};
|
|
1213
|
-
} catch (err) {
|
|
1214
|
-
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";
|
|
1215
|
-
return { valid: false, error: message };
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
|
-
async function deactivateLicense(licenseKey, instanceId) {
|
|
1219
|
-
try {
|
|
1220
|
-
const res = await fetchWithTimeout(`${BASE_URL}/deactivate`, {
|
|
1221
|
-
method: "POST",
|
|
1222
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1223
|
-
body: new URLSearchParams({ license_key: licenseKey, instance_id: instanceId })
|
|
1224
|
-
});
|
|
1225
|
-
const data = await safeJson(res);
|
|
1226
|
-
if (!res.ok || !data?.deactivated) {
|
|
1227
|
-
return { deactivated: false, error: data?.error || `Deactivation failed (${res.status})` };
|
|
1228
|
-
}
|
|
1229
|
-
return { deactivated: true };
|
|
1230
|
-
} catch (err) {
|
|
1231
|
-
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";
|
|
1232
|
-
return { deactivated: false, error: message };
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
// src/storage.ts
|
|
1147
|
+
// ../../packages/shared/src/reader.ts
|
|
1237
1148
|
import fs4 from "fs";
|
|
1238
1149
|
import path5 from "path";
|
|
1239
1150
|
var STORAGE_DIR3 = ".keepgoing";
|
|
@@ -1515,6 +1426,280 @@ var KeepGoingReader = class {
|
|
|
1515
1426
|
}
|
|
1516
1427
|
};
|
|
1517
1428
|
|
|
1429
|
+
// ../../packages/shared/src/setup.ts
|
|
1430
|
+
import fs5 from "fs";
|
|
1431
|
+
import os2 from "os";
|
|
1432
|
+
import path6 from "path";
|
|
1433
|
+
var KEEPGOING_MARKER = "@keepgoingdev/mcp-server";
|
|
1434
|
+
var SESSION_START_HOOK = {
|
|
1435
|
+
matcher: "",
|
|
1436
|
+
hooks: [
|
|
1437
|
+
{
|
|
1438
|
+
type: "command",
|
|
1439
|
+
command: "npx -y @keepgoingdev/mcp-server --print-momentum"
|
|
1440
|
+
}
|
|
1441
|
+
]
|
|
1442
|
+
};
|
|
1443
|
+
var STOP_HOOK = {
|
|
1444
|
+
matcher: "",
|
|
1445
|
+
hooks: [
|
|
1446
|
+
{
|
|
1447
|
+
type: "command",
|
|
1448
|
+
command: "npx -y @keepgoingdev/mcp-server --save-checkpoint"
|
|
1449
|
+
}
|
|
1450
|
+
]
|
|
1451
|
+
};
|
|
1452
|
+
var POST_TOOL_USE_HOOK = {
|
|
1453
|
+
matcher: "Edit|Write|MultiEdit",
|
|
1454
|
+
hooks: [
|
|
1455
|
+
{
|
|
1456
|
+
type: "command",
|
|
1457
|
+
command: "npx -y @keepgoingdev/mcp-server --update-task-from-hook"
|
|
1458
|
+
}
|
|
1459
|
+
]
|
|
1460
|
+
};
|
|
1461
|
+
var CLAUDE_MD_SECTION = `
|
|
1462
|
+
## KeepGoing
|
|
1463
|
+
|
|
1464
|
+
After completing a task or meaningful piece of work, call the \`save_checkpoint\` MCP tool with:
|
|
1465
|
+
- \`summary\`: What you accomplished
|
|
1466
|
+
- \`nextStep\`: What should be done next
|
|
1467
|
+
- \`blocker\`: Any blocker (if applicable)
|
|
1468
|
+
`;
|
|
1469
|
+
var STATUSLINE_CMD = "npx -y @keepgoingdev/mcp-server --statusline";
|
|
1470
|
+
function hasKeepGoingHook(hookEntries) {
|
|
1471
|
+
return hookEntries.some(
|
|
1472
|
+
(entry) => entry?.hooks?.some((h) => typeof h?.command === "string" && h.command.includes(KEEPGOING_MARKER))
|
|
1473
|
+
);
|
|
1474
|
+
}
|
|
1475
|
+
function resolveScopePaths(scope, workspacePath) {
|
|
1476
|
+
if (scope === "user") {
|
|
1477
|
+
const claudeDir2 = path6.join(os2.homedir(), ".claude");
|
|
1478
|
+
return {
|
|
1479
|
+
claudeDir: claudeDir2,
|
|
1480
|
+
settingsPath: path6.join(claudeDir2, "settings.json"),
|
|
1481
|
+
claudeMdPath: path6.join(claudeDir2, "CLAUDE.md")
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
const claudeDir = path6.join(workspacePath, ".claude");
|
|
1485
|
+
const dotClaudeMdPath = path6.join(workspacePath, ".claude", "CLAUDE.md");
|
|
1486
|
+
const rootClaudeMdPath = path6.join(workspacePath, "CLAUDE.md");
|
|
1487
|
+
return {
|
|
1488
|
+
claudeDir,
|
|
1489
|
+
settingsPath: path6.join(claudeDir, "settings.json"),
|
|
1490
|
+
claudeMdPath: fs5.existsSync(dotClaudeMdPath) ? dotClaudeMdPath : rootClaudeMdPath
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1493
|
+
function writeHooksToSettings(settings) {
|
|
1494
|
+
let changed = false;
|
|
1495
|
+
if (!settings.hooks) {
|
|
1496
|
+
settings.hooks = {};
|
|
1497
|
+
}
|
|
1498
|
+
if (!Array.isArray(settings.hooks.SessionStart)) {
|
|
1499
|
+
settings.hooks.SessionStart = [];
|
|
1500
|
+
}
|
|
1501
|
+
if (!hasKeepGoingHook(settings.hooks.SessionStart)) {
|
|
1502
|
+
settings.hooks.SessionStart.push(SESSION_START_HOOK);
|
|
1503
|
+
changed = true;
|
|
1504
|
+
}
|
|
1505
|
+
if (!Array.isArray(settings.hooks.Stop)) {
|
|
1506
|
+
settings.hooks.Stop = [];
|
|
1507
|
+
}
|
|
1508
|
+
if (!hasKeepGoingHook(settings.hooks.Stop)) {
|
|
1509
|
+
settings.hooks.Stop.push(STOP_HOOK);
|
|
1510
|
+
changed = true;
|
|
1511
|
+
}
|
|
1512
|
+
if (!Array.isArray(settings.hooks.PostToolUse)) {
|
|
1513
|
+
settings.hooks.PostToolUse = [];
|
|
1514
|
+
}
|
|
1515
|
+
if (!hasKeepGoingHook(settings.hooks.PostToolUse)) {
|
|
1516
|
+
settings.hooks.PostToolUse.push(POST_TOOL_USE_HOOK);
|
|
1517
|
+
changed = true;
|
|
1518
|
+
}
|
|
1519
|
+
return changed;
|
|
1520
|
+
}
|
|
1521
|
+
function checkHookConflict(scope, workspacePath) {
|
|
1522
|
+
const otherPaths = resolveScopePaths(scope === "user" ? "project" : "user", workspacePath);
|
|
1523
|
+
if (!fs5.existsSync(otherPaths.settingsPath)) {
|
|
1524
|
+
return null;
|
|
1525
|
+
}
|
|
1526
|
+
try {
|
|
1527
|
+
const otherSettings = JSON.parse(fs5.readFileSync(otherPaths.settingsPath, "utf-8"));
|
|
1528
|
+
const hooks = otherSettings?.hooks;
|
|
1529
|
+
if (!hooks) return null;
|
|
1530
|
+
const hasConflict = Array.isArray(hooks.SessionStart) && hasKeepGoingHook(hooks.SessionStart) || Array.isArray(hooks.Stop) && hasKeepGoingHook(hooks.Stop);
|
|
1531
|
+
if (hasConflict) {
|
|
1532
|
+
const otherScope = scope === "user" ? "project" : "user";
|
|
1533
|
+
const otherFile = otherPaths.settingsPath;
|
|
1534
|
+
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.`;
|
|
1535
|
+
}
|
|
1536
|
+
} catch {
|
|
1537
|
+
}
|
|
1538
|
+
return null;
|
|
1539
|
+
}
|
|
1540
|
+
function setupProject(options) {
|
|
1541
|
+
const {
|
|
1542
|
+
workspacePath,
|
|
1543
|
+
scope = "project",
|
|
1544
|
+
sessionHooks = true,
|
|
1545
|
+
claudeMd = true,
|
|
1546
|
+
hasProLicense = false,
|
|
1547
|
+
statusline
|
|
1548
|
+
} = options;
|
|
1549
|
+
const messages = [];
|
|
1550
|
+
let changed = false;
|
|
1551
|
+
const { claudeDir, settingsPath, claudeMdPath } = resolveScopePaths(scope, workspacePath);
|
|
1552
|
+
const scopeLabel = scope === "user" ? "~/.claude/settings.json" : ".claude/settings.json";
|
|
1553
|
+
let settings = {};
|
|
1554
|
+
if (fs5.existsSync(settingsPath)) {
|
|
1555
|
+
settings = JSON.parse(fs5.readFileSync(settingsPath, "utf-8"));
|
|
1556
|
+
}
|
|
1557
|
+
let settingsChanged = false;
|
|
1558
|
+
if (sessionHooks) {
|
|
1559
|
+
const hooksChanged = writeHooksToSettings(settings);
|
|
1560
|
+
settingsChanged = hooksChanged;
|
|
1561
|
+
if (hooksChanged) {
|
|
1562
|
+
messages.push(`Session hooks: Added to ${scopeLabel}`);
|
|
1563
|
+
} else {
|
|
1564
|
+
messages.push("Session hooks: Already present, skipped");
|
|
1565
|
+
}
|
|
1566
|
+
const conflict = checkHookConflict(scope, workspacePath);
|
|
1567
|
+
if (conflict) {
|
|
1568
|
+
messages.push(`Warning: ${conflict}`);
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
if (scope === "project" && hasProLicense) {
|
|
1572
|
+
const needsUpdate = settings.statusLine?.command && statusline?.isLegacy?.(settings.statusLine.command);
|
|
1573
|
+
if (!settings.statusLine || needsUpdate) {
|
|
1574
|
+
settings.statusLine = {
|
|
1575
|
+
type: "command",
|
|
1576
|
+
command: STATUSLINE_CMD
|
|
1577
|
+
};
|
|
1578
|
+
settingsChanged = true;
|
|
1579
|
+
messages.push(needsUpdate ? "Statusline: Migrated to auto-updating npx command" : "Statusline: Added to .claude/settings.json");
|
|
1580
|
+
} else {
|
|
1581
|
+
messages.push("Statusline: Already configured in settings, skipped");
|
|
1582
|
+
}
|
|
1583
|
+
statusline?.cleanup?.();
|
|
1584
|
+
}
|
|
1585
|
+
if (settingsChanged) {
|
|
1586
|
+
if (!fs5.existsSync(claudeDir)) {
|
|
1587
|
+
fs5.mkdirSync(claudeDir, { recursive: true });
|
|
1588
|
+
}
|
|
1589
|
+
fs5.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
1590
|
+
changed = true;
|
|
1591
|
+
}
|
|
1592
|
+
if (claudeMd) {
|
|
1593
|
+
let existing = "";
|
|
1594
|
+
if (fs5.existsSync(claudeMdPath)) {
|
|
1595
|
+
existing = fs5.readFileSync(claudeMdPath, "utf-8");
|
|
1596
|
+
}
|
|
1597
|
+
const mdLabel = scope === "user" ? "~/.claude/CLAUDE.md" : "CLAUDE.md";
|
|
1598
|
+
if (existing.includes("## KeepGoing")) {
|
|
1599
|
+
messages.push(`CLAUDE.md: KeepGoing section already present in ${mdLabel}, skipped`);
|
|
1600
|
+
} else {
|
|
1601
|
+
const updated = existing + CLAUDE_MD_SECTION;
|
|
1602
|
+
const mdDir = path6.dirname(claudeMdPath);
|
|
1603
|
+
if (!fs5.existsSync(mdDir)) {
|
|
1604
|
+
fs5.mkdirSync(mdDir, { recursive: true });
|
|
1605
|
+
}
|
|
1606
|
+
fs5.writeFileSync(claudeMdPath, updated);
|
|
1607
|
+
changed = true;
|
|
1608
|
+
messages.push(`CLAUDE.md: Added KeepGoing section to ${mdLabel}`);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
return { messages, changed };
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
// ../../packages/shared/src/licenseClient.ts
|
|
1615
|
+
var BASE_URL = "https://api.lemonsqueezy.com/v1/licenses";
|
|
1616
|
+
var REQUEST_TIMEOUT_MS = 15e3;
|
|
1617
|
+
var EXPECTED_STORE_ID = 301555;
|
|
1618
|
+
var EXPECTED_PRODUCT_ID = 864311;
|
|
1619
|
+
function fetchWithTimeout(url, init) {
|
|
1620
|
+
const controller = new AbortController();
|
|
1621
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
1622
|
+
return fetch(url, { ...init, signal: controller.signal }).finally(() => clearTimeout(timer));
|
|
1623
|
+
}
|
|
1624
|
+
function validateProductIdentity(meta) {
|
|
1625
|
+
if (!meta) return "License response missing product metadata.";
|
|
1626
|
+
if (meta.store_id !== EXPECTED_STORE_ID || meta.product_id !== EXPECTED_PRODUCT_ID) {
|
|
1627
|
+
return "This license key does not belong to KeepGoing.";
|
|
1628
|
+
}
|
|
1629
|
+
return void 0;
|
|
1630
|
+
}
|
|
1631
|
+
async function safeJson(res) {
|
|
1632
|
+
try {
|
|
1633
|
+
const text = await res.text();
|
|
1634
|
+
return JSON.parse(text);
|
|
1635
|
+
} catch {
|
|
1636
|
+
return null;
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
async function activateLicense(licenseKey, instanceName, options) {
|
|
1640
|
+
try {
|
|
1641
|
+
const res = await fetchWithTimeout(`${BASE_URL}/activate`, {
|
|
1642
|
+
method: "POST",
|
|
1643
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1644
|
+
body: new URLSearchParams({ license_key: licenseKey, instance_name: instanceName })
|
|
1645
|
+
});
|
|
1646
|
+
const data = await safeJson(res);
|
|
1647
|
+
if (!res.ok || !data?.activated) {
|
|
1648
|
+
return { valid: false, error: data?.error || `Activation failed (${res.status})` };
|
|
1649
|
+
}
|
|
1650
|
+
if (!options?.allowTestMode && data.license_key?.test_mode) {
|
|
1651
|
+
if (data.license_key?.key && data.instance?.id) {
|
|
1652
|
+
await deactivateLicense(data.license_key.key, data.instance.id);
|
|
1653
|
+
}
|
|
1654
|
+
return { valid: false, error: "This is a test license key. Please use a production license key from your purchase confirmation." };
|
|
1655
|
+
}
|
|
1656
|
+
if (!options?.allowTestMode) {
|
|
1657
|
+
const productError = validateProductIdentity(data.meta);
|
|
1658
|
+
if (productError) {
|
|
1659
|
+
if (data.license_key?.key && data.instance?.id) {
|
|
1660
|
+
await deactivateLicense(data.license_key.key, data.instance.id);
|
|
1661
|
+
}
|
|
1662
|
+
return { valid: false, error: productError };
|
|
1663
|
+
}
|
|
1664
|
+
if (data.meta?.variant_id && !KNOWN_VARIANT_IDS.has(data.meta.variant_id)) {
|
|
1665
|
+
if (data.license_key?.key && data.instance?.id) {
|
|
1666
|
+
await deactivateLicense(data.license_key.key, data.instance.id);
|
|
1667
|
+
}
|
|
1668
|
+
return { valid: false, error: "This license key is for an unrecognized add-on variant. Please update KeepGoing or contact support." };
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
return {
|
|
1672
|
+
valid: true,
|
|
1673
|
+
licenseKey: data.license_key?.key,
|
|
1674
|
+
instanceId: data.instance?.id,
|
|
1675
|
+
customerName: data.meta?.customer_name,
|
|
1676
|
+
productName: data.meta?.product_name,
|
|
1677
|
+
variantId: data.meta?.variant_id,
|
|
1678
|
+
variantName: data.meta?.variant_name
|
|
1679
|
+
};
|
|
1680
|
+
} catch (err) {
|
|
1681
|
+
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";
|
|
1682
|
+
return { valid: false, error: message };
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
async function deactivateLicense(licenseKey, instanceId) {
|
|
1686
|
+
try {
|
|
1687
|
+
const res = await fetchWithTimeout(`${BASE_URL}/deactivate`, {
|
|
1688
|
+
method: "POST",
|
|
1689
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1690
|
+
body: new URLSearchParams({ license_key: licenseKey, instance_id: instanceId })
|
|
1691
|
+
});
|
|
1692
|
+
const data = await safeJson(res);
|
|
1693
|
+
if (!res.ok || !data?.deactivated) {
|
|
1694
|
+
return { deactivated: false, error: data?.error || `Deactivation failed (${res.status})` };
|
|
1695
|
+
}
|
|
1696
|
+
return { deactivated: true };
|
|
1697
|
+
} catch (err) {
|
|
1698
|
+
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";
|
|
1699
|
+
return { deactivated: false, error: message };
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1518
1703
|
// src/tools/getMomentum.ts
|
|
1519
1704
|
function registerGetMomentum(server, reader, workspacePath) {
|
|
1520
1705
|
server.tool(
|
|
@@ -1732,7 +1917,7 @@ function registerGetReentryBriefing(server, reader, workspacePath) {
|
|
|
1732
1917
|
}
|
|
1733
1918
|
|
|
1734
1919
|
// src/tools/saveCheckpoint.ts
|
|
1735
|
-
import
|
|
1920
|
+
import path7 from "path";
|
|
1736
1921
|
import { z as z2 } from "zod";
|
|
1737
1922
|
function registerSaveCheckpoint(server, reader, workspacePath) {
|
|
1738
1923
|
server.tool(
|
|
@@ -1748,7 +1933,7 @@ function registerSaveCheckpoint(server, reader, workspacePath) {
|
|
|
1748
1933
|
const gitBranch = getCurrentBranch(workspacePath);
|
|
1749
1934
|
const touchedFiles = getTouchedFiles(workspacePath);
|
|
1750
1935
|
const commitHashes = getCommitsSince(workspacePath, lastSession?.timestamp);
|
|
1751
|
-
const projectName =
|
|
1936
|
+
const projectName = path7.basename(resolveStorageRoot(workspacePath));
|
|
1752
1937
|
const sessionId = generateSessionId({ workspaceRoot: workspacePath, branch: gitBranch ?? void 0, worktreePath: workspacePath });
|
|
1753
1938
|
const checkpoint = createCheckpoint({
|
|
1754
1939
|
summary,
|
|
@@ -1960,31 +2145,28 @@ function registerGetCurrentTask(server, reader) {
|
|
|
1960
2145
|
}
|
|
1961
2146
|
|
|
1962
2147
|
// src/tools/setupProject.ts
|
|
1963
|
-
import fs6 from "fs";
|
|
1964
|
-
import os3 from "os";
|
|
1965
|
-
import path8 from "path";
|
|
1966
2148
|
import { z as z4 } from "zod";
|
|
1967
2149
|
|
|
1968
2150
|
// src/cli/migrate.ts
|
|
1969
|
-
import
|
|
1970
|
-
import
|
|
1971
|
-
import
|
|
1972
|
-
var
|
|
2151
|
+
import fs6 from "fs";
|
|
2152
|
+
import os3 from "os";
|
|
2153
|
+
import path8 from "path";
|
|
2154
|
+
var STATUSLINE_CMD2 = "npx -y @keepgoingdev/mcp-server --statusline";
|
|
1973
2155
|
function isLegacyStatusline(command) {
|
|
1974
2156
|
return !command.includes("--statusline") && command.includes("keepgoing-statusline");
|
|
1975
2157
|
}
|
|
1976
2158
|
function migrateStatusline(wsPath) {
|
|
1977
|
-
const settingsPath =
|
|
1978
|
-
if (!
|
|
2159
|
+
const settingsPath = path8.join(wsPath, ".claude", "settings.json");
|
|
2160
|
+
if (!fs6.existsSync(settingsPath)) return void 0;
|
|
1979
2161
|
try {
|
|
1980
|
-
const settings = JSON.parse(
|
|
2162
|
+
const settings = JSON.parse(fs6.readFileSync(settingsPath, "utf-8"));
|
|
1981
2163
|
const cmd = settings.statusLine?.command;
|
|
1982
2164
|
if (!cmd || !isLegacyStatusline(cmd)) return void 0;
|
|
1983
2165
|
settings.statusLine = {
|
|
1984
2166
|
type: "command",
|
|
1985
|
-
command:
|
|
2167
|
+
command: STATUSLINE_CMD2
|
|
1986
2168
|
};
|
|
1987
|
-
|
|
2169
|
+
fs6.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
1988
2170
|
cleanupLegacyScript();
|
|
1989
2171
|
return "[KeepGoing] Migrated statusline to auto-updating command (restart Claude Code to apply)";
|
|
1990
2172
|
} catch {
|
|
@@ -1992,122 +2174,16 @@ function migrateStatusline(wsPath) {
|
|
|
1992
2174
|
}
|
|
1993
2175
|
}
|
|
1994
2176
|
function cleanupLegacyScript() {
|
|
1995
|
-
const legacyScript =
|
|
1996
|
-
if (
|
|
2177
|
+
const legacyScript = path8.join(os3.homedir(), ".claude", "keepgoing-statusline.sh");
|
|
2178
|
+
if (fs6.existsSync(legacyScript)) {
|
|
1997
2179
|
try {
|
|
1998
|
-
|
|
2180
|
+
fs6.unlinkSync(legacyScript);
|
|
1999
2181
|
} catch {
|
|
2000
2182
|
}
|
|
2001
2183
|
}
|
|
2002
2184
|
}
|
|
2003
2185
|
|
|
2004
2186
|
// src/tools/setupProject.ts
|
|
2005
|
-
var KEEPGOING_MARKER = "@keepgoingdev/mcp-server";
|
|
2006
|
-
var SESSION_START_HOOK = {
|
|
2007
|
-
matcher: "",
|
|
2008
|
-
hooks: [
|
|
2009
|
-
{
|
|
2010
|
-
type: "command",
|
|
2011
|
-
command: "npx -y @keepgoingdev/mcp-server --print-momentum"
|
|
2012
|
-
}
|
|
2013
|
-
]
|
|
2014
|
-
};
|
|
2015
|
-
var STOP_HOOK = {
|
|
2016
|
-
matcher: "",
|
|
2017
|
-
hooks: [
|
|
2018
|
-
{
|
|
2019
|
-
type: "command",
|
|
2020
|
-
command: "npx -y @keepgoingdev/mcp-server --save-checkpoint"
|
|
2021
|
-
}
|
|
2022
|
-
]
|
|
2023
|
-
};
|
|
2024
|
-
var POST_TOOL_USE_HOOK = {
|
|
2025
|
-
matcher: "Edit|Write|MultiEdit",
|
|
2026
|
-
hooks: [
|
|
2027
|
-
{
|
|
2028
|
-
type: "command",
|
|
2029
|
-
command: "npx -y @keepgoingdev/mcp-server --update-task-from-hook"
|
|
2030
|
-
}
|
|
2031
|
-
]
|
|
2032
|
-
};
|
|
2033
|
-
var CLAUDE_MD_SECTION = `
|
|
2034
|
-
## KeepGoing
|
|
2035
|
-
|
|
2036
|
-
After completing a task or meaningful piece of work, call the \`save_checkpoint\` MCP tool with:
|
|
2037
|
-
- \`summary\`: What you accomplished
|
|
2038
|
-
- \`nextStep\`: What should be done next
|
|
2039
|
-
- \`blocker\`: Any blocker (if applicable)
|
|
2040
|
-
`;
|
|
2041
|
-
function hasKeepGoingHook(hookEntries) {
|
|
2042
|
-
return hookEntries.some(
|
|
2043
|
-
(entry) => entry?.hooks?.some((h) => typeof h?.command === "string" && h.command.includes(KEEPGOING_MARKER))
|
|
2044
|
-
);
|
|
2045
|
-
}
|
|
2046
|
-
function resolveScopePaths(scope, workspacePath) {
|
|
2047
|
-
if (scope === "user") {
|
|
2048
|
-
const claudeDir2 = path8.join(os3.homedir(), ".claude");
|
|
2049
|
-
return {
|
|
2050
|
-
claudeDir: claudeDir2,
|
|
2051
|
-
settingsPath: path8.join(claudeDir2, "settings.json"),
|
|
2052
|
-
claudeMdPath: path8.join(claudeDir2, "CLAUDE.md")
|
|
2053
|
-
};
|
|
2054
|
-
}
|
|
2055
|
-
const claudeDir = path8.join(workspacePath, ".claude");
|
|
2056
|
-
const dotClaudeMdPath = path8.join(workspacePath, ".claude", "CLAUDE.md");
|
|
2057
|
-
const rootClaudeMdPath = path8.join(workspacePath, "CLAUDE.md");
|
|
2058
|
-
return {
|
|
2059
|
-
claudeDir,
|
|
2060
|
-
settingsPath: path8.join(claudeDir, "settings.json"),
|
|
2061
|
-
claudeMdPath: fs6.existsSync(dotClaudeMdPath) ? dotClaudeMdPath : rootClaudeMdPath
|
|
2062
|
-
};
|
|
2063
|
-
}
|
|
2064
|
-
function writeHooksToSettings(settings) {
|
|
2065
|
-
let changed = false;
|
|
2066
|
-
if (!settings.hooks) {
|
|
2067
|
-
settings.hooks = {};
|
|
2068
|
-
}
|
|
2069
|
-
if (!Array.isArray(settings.hooks.SessionStart)) {
|
|
2070
|
-
settings.hooks.SessionStart = [];
|
|
2071
|
-
}
|
|
2072
|
-
if (!hasKeepGoingHook(settings.hooks.SessionStart)) {
|
|
2073
|
-
settings.hooks.SessionStart.push(SESSION_START_HOOK);
|
|
2074
|
-
changed = true;
|
|
2075
|
-
}
|
|
2076
|
-
if (!Array.isArray(settings.hooks.Stop)) {
|
|
2077
|
-
settings.hooks.Stop = [];
|
|
2078
|
-
}
|
|
2079
|
-
if (!hasKeepGoingHook(settings.hooks.Stop)) {
|
|
2080
|
-
settings.hooks.Stop.push(STOP_HOOK);
|
|
2081
|
-
changed = true;
|
|
2082
|
-
}
|
|
2083
|
-
if (!Array.isArray(settings.hooks.PostToolUse)) {
|
|
2084
|
-
settings.hooks.PostToolUse = [];
|
|
2085
|
-
}
|
|
2086
|
-
if (!hasKeepGoingHook(settings.hooks.PostToolUse)) {
|
|
2087
|
-
settings.hooks.PostToolUse.push(POST_TOOL_USE_HOOK);
|
|
2088
|
-
changed = true;
|
|
2089
|
-
}
|
|
2090
|
-
return changed;
|
|
2091
|
-
}
|
|
2092
|
-
function checkHookConflict(scope, workspacePath) {
|
|
2093
|
-
const otherPaths = resolveScopePaths(scope === "user" ? "project" : "user", workspacePath);
|
|
2094
|
-
if (!fs6.existsSync(otherPaths.settingsPath)) {
|
|
2095
|
-
return null;
|
|
2096
|
-
}
|
|
2097
|
-
try {
|
|
2098
|
-
const otherSettings = JSON.parse(fs6.readFileSync(otherPaths.settingsPath, "utf-8"));
|
|
2099
|
-
const hooks = otherSettings?.hooks;
|
|
2100
|
-
if (!hooks) return null;
|
|
2101
|
-
const hasConflict = Array.isArray(hooks.SessionStart) && hasKeepGoingHook(hooks.SessionStart) || Array.isArray(hooks.Stop) && hasKeepGoingHook(hooks.Stop);
|
|
2102
|
-
if (hasConflict) {
|
|
2103
|
-
const otherScope = scope === "user" ? "project" : "user";
|
|
2104
|
-
const otherFile = otherPaths.settingsPath;
|
|
2105
|
-
return `**Warning:** 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.`;
|
|
2106
|
-
}
|
|
2107
|
-
} catch {
|
|
2108
|
-
}
|
|
2109
|
-
return null;
|
|
2110
|
-
}
|
|
2111
2187
|
function registerSetupProject(server, workspacePath) {
|
|
2112
2188
|
server.tool(
|
|
2113
2189
|
"setup_project",
|
|
@@ -2118,69 +2194,23 @@ function registerSetupProject(server, workspacePath) {
|
|
|
2118
2194
|
scope: z4.enum(["project", "user"]).optional().default("project").describe('Where to write config: "user" for global (~/.claude/), "project" for per-project (.claude/)')
|
|
2119
2195
|
},
|
|
2120
2196
|
async ({ sessionHooks, claudeMd, scope }) => {
|
|
2121
|
-
const
|
|
2122
|
-
const
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
settingsChanged = hooksChanged;
|
|
2132
|
-
if (hooksChanged) {
|
|
2133
|
-
results.push(`**Session hooks:** Added to ${scopeLabel}`);
|
|
2134
|
-
} else {
|
|
2135
|
-
results.push("**Session hooks:** Already present, skipped");
|
|
2136
|
-
}
|
|
2137
|
-
const conflict = checkHookConflict(scope, workspacePath);
|
|
2138
|
-
if (conflict) {
|
|
2139
|
-
results.push(conflict);
|
|
2197
|
+
const hasProLicense = process.env.KEEPGOING_PRO_BYPASS === "1" || !!getLicenseForFeature("session-awareness");
|
|
2198
|
+
const result = setupProject({
|
|
2199
|
+
workspacePath,
|
|
2200
|
+
scope,
|
|
2201
|
+
sessionHooks,
|
|
2202
|
+
claudeMd,
|
|
2203
|
+
hasProLicense,
|
|
2204
|
+
statusline: {
|
|
2205
|
+
isLegacy: isLegacyStatusline,
|
|
2206
|
+
cleanup: cleanupLegacyScript
|
|
2140
2207
|
}
|
|
2141
|
-
}
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
if (!settings.statusLine || needsUpdate) {
|
|
2146
|
-
settings.statusLine = {
|
|
2147
|
-
type: "command",
|
|
2148
|
-
command: STATUSLINE_CMD
|
|
2149
|
-
};
|
|
2150
|
-
settingsChanged = true;
|
|
2151
|
-
results.push(needsUpdate ? "**Statusline:** Migrated to auto-updating `npx` command" : "**Statusline:** Added to `.claude/settings.json`");
|
|
2152
|
-
} else {
|
|
2153
|
-
results.push("**Statusline:** `statusLine` already configured in settings, skipped");
|
|
2154
|
-
}
|
|
2155
|
-
cleanupLegacyScript();
|
|
2156
|
-
}
|
|
2157
|
-
}
|
|
2158
|
-
if (settingsChanged) {
|
|
2159
|
-
if (!fs6.existsSync(claudeDir)) {
|
|
2160
|
-
fs6.mkdirSync(claudeDir, { recursive: true });
|
|
2161
|
-
}
|
|
2162
|
-
fs6.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
2163
|
-
}
|
|
2164
|
-
if (claudeMd) {
|
|
2165
|
-
let existing = "";
|
|
2166
|
-
if (fs6.existsSync(claudeMdPath)) {
|
|
2167
|
-
existing = fs6.readFileSync(claudeMdPath, "utf-8");
|
|
2168
|
-
}
|
|
2169
|
-
const mdLabel = scope === "user" ? "`~/.claude/CLAUDE.md`" : "`CLAUDE.md`";
|
|
2170
|
-
if (existing.includes("## KeepGoing")) {
|
|
2171
|
-
results.push(`**CLAUDE.md:** KeepGoing section already present in ${mdLabel}, skipped`);
|
|
2172
|
-
} else {
|
|
2173
|
-
const updated = existing + CLAUDE_MD_SECTION;
|
|
2174
|
-
const mdDir = path8.dirname(claudeMdPath);
|
|
2175
|
-
if (!fs6.existsSync(mdDir)) {
|
|
2176
|
-
fs6.mkdirSync(mdDir, { recursive: true });
|
|
2177
|
-
}
|
|
2178
|
-
fs6.writeFileSync(claudeMdPath, updated);
|
|
2179
|
-
results.push(`**CLAUDE.md:** Added KeepGoing section to ${mdLabel}`);
|
|
2180
|
-
}
|
|
2181
|
-
}
|
|
2208
|
+
});
|
|
2209
|
+
const formatted = result.messages.map((msg) => {
|
|
2210
|
+
return msg.replace(/^([^:]+:)/, "**$1**");
|
|
2211
|
+
});
|
|
2182
2212
|
return {
|
|
2183
|
-
content: [{ type: "text", text:
|
|
2213
|
+
content: [{ type: "text", text: formatted.join("\n") }]
|
|
2184
2214
|
};
|
|
2185
2215
|
}
|
|
2186
2216
|
);
|