@keepgoingdev/mcp-server 0.5.5 → 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/README.md +12 -2
- package/dist/index.js +309 -226
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,13 +8,23 @@ KeepGoing auto-captures checkpoints (what you were doing, what's next, which fil
|
|
|
8
8
|
|
|
9
9
|
### Claude Code
|
|
10
10
|
|
|
11
|
+
**Global (recommended)** — works across all your projects:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
claude mcp add keepgoing --scope user -- npx -y @keepgoingdev/mcp-server
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**Per-project** — scoped to a single project:
|
|
18
|
+
|
|
11
19
|
```bash
|
|
12
|
-
claude mcp add keepgoing -- npx -y @keepgoingdev/mcp-server
|
|
20
|
+
claude mcp add keepgoing --scope project -- npx -y @keepgoingdev/mcp-server
|
|
13
21
|
```
|
|
14
22
|
|
|
23
|
+
Then ask Claude Code to run `setup_project` (with `scope: "user"` for global, or default for per-project) to add session hooks and CLAUDE.md instructions.
|
|
24
|
+
|
|
15
25
|
### Manual config
|
|
16
26
|
|
|
17
|
-
Add to your MCP config (e.g., `~/.claude
|
|
27
|
+
Add to your MCP config (e.g., `~/.claude.json` for global, or `.mcp.json` for per-project):
|
|
18
28
|
|
|
19
29
|
```json
|
|
20
30
|
{
|
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,30 +2145,28 @@ function registerGetCurrentTask(server, reader) {
|
|
|
1960
2145
|
}
|
|
1961
2146
|
|
|
1962
2147
|
// src/tools/setupProject.ts
|
|
1963
|
-
import fs6 from "fs";
|
|
1964
|
-
import path8 from "path";
|
|
1965
2148
|
import { z as z4 } from "zod";
|
|
1966
2149
|
|
|
1967
2150
|
// src/cli/migrate.ts
|
|
1968
|
-
import
|
|
1969
|
-
import
|
|
1970
|
-
import
|
|
1971
|
-
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";
|
|
1972
2155
|
function isLegacyStatusline(command) {
|
|
1973
2156
|
return !command.includes("--statusline") && command.includes("keepgoing-statusline");
|
|
1974
2157
|
}
|
|
1975
2158
|
function migrateStatusline(wsPath) {
|
|
1976
|
-
const settingsPath =
|
|
1977
|
-
if (!
|
|
2159
|
+
const settingsPath = path8.join(wsPath, ".claude", "settings.json");
|
|
2160
|
+
if (!fs6.existsSync(settingsPath)) return void 0;
|
|
1978
2161
|
try {
|
|
1979
|
-
const settings = JSON.parse(
|
|
2162
|
+
const settings = JSON.parse(fs6.readFileSync(settingsPath, "utf-8"));
|
|
1980
2163
|
const cmd = settings.statusLine?.command;
|
|
1981
2164
|
if (!cmd || !isLegacyStatusline(cmd)) return void 0;
|
|
1982
2165
|
settings.statusLine = {
|
|
1983
2166
|
type: "command",
|
|
1984
|
-
command:
|
|
2167
|
+
command: STATUSLINE_CMD2
|
|
1985
2168
|
};
|
|
1986
|
-
|
|
2169
|
+
fs6.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
1987
2170
|
cleanupLegacyScript();
|
|
1988
2171
|
return "[KeepGoing] Migrated statusline to auto-updating command (restart Claude Code to apply)";
|
|
1989
2172
|
} catch {
|
|
@@ -1991,143 +2174,43 @@ function migrateStatusline(wsPath) {
|
|
|
1991
2174
|
}
|
|
1992
2175
|
}
|
|
1993
2176
|
function cleanupLegacyScript() {
|
|
1994
|
-
const legacyScript =
|
|
1995
|
-
if (
|
|
2177
|
+
const legacyScript = path8.join(os3.homedir(), ".claude", "keepgoing-statusline.sh");
|
|
2178
|
+
if (fs6.existsSync(legacyScript)) {
|
|
1996
2179
|
try {
|
|
1997
|
-
|
|
2180
|
+
fs6.unlinkSync(legacyScript);
|
|
1998
2181
|
} catch {
|
|
1999
2182
|
}
|
|
2000
2183
|
}
|
|
2001
2184
|
}
|
|
2002
2185
|
|
|
2003
2186
|
// src/tools/setupProject.ts
|
|
2004
|
-
var KEEPGOING_MARKER = "@keepgoingdev/mcp-server";
|
|
2005
|
-
var SESSION_START_HOOK = {
|
|
2006
|
-
matcher: "",
|
|
2007
|
-
hooks: [
|
|
2008
|
-
{
|
|
2009
|
-
type: "command",
|
|
2010
|
-
command: "npx -y @keepgoingdev/mcp-server --print-momentum"
|
|
2011
|
-
}
|
|
2012
|
-
]
|
|
2013
|
-
};
|
|
2014
|
-
var STOP_HOOK = {
|
|
2015
|
-
matcher: "",
|
|
2016
|
-
hooks: [
|
|
2017
|
-
{
|
|
2018
|
-
type: "command",
|
|
2019
|
-
command: "npx -y @keepgoingdev/mcp-server --save-checkpoint"
|
|
2020
|
-
}
|
|
2021
|
-
]
|
|
2022
|
-
};
|
|
2023
|
-
var POST_TOOL_USE_HOOK = {
|
|
2024
|
-
matcher: "Edit|Write|MultiEdit",
|
|
2025
|
-
hooks: [
|
|
2026
|
-
{
|
|
2027
|
-
type: "command",
|
|
2028
|
-
command: "npx -y @keepgoingdev/mcp-server --update-task-from-hook"
|
|
2029
|
-
}
|
|
2030
|
-
]
|
|
2031
|
-
};
|
|
2032
|
-
var CLAUDE_MD_SECTION = `
|
|
2033
|
-
## KeepGoing
|
|
2034
|
-
|
|
2035
|
-
After completing a task or meaningful piece of work, call the \`save_checkpoint\` MCP tool with:
|
|
2036
|
-
- \`summary\`: What you accomplished
|
|
2037
|
-
- \`nextStep\`: What should be done next
|
|
2038
|
-
- \`blocker\`: Any blocker (if applicable)
|
|
2039
|
-
`;
|
|
2040
|
-
function hasKeepGoingHook(hookEntries) {
|
|
2041
|
-
return hookEntries.some(
|
|
2042
|
-
(entry) => entry?.hooks?.some((h) => typeof h?.command === "string" && h.command.includes(KEEPGOING_MARKER))
|
|
2043
|
-
);
|
|
2044
|
-
}
|
|
2045
2187
|
function registerSetupProject(server, workspacePath) {
|
|
2046
2188
|
server.tool(
|
|
2047
2189
|
"setup_project",
|
|
2048
|
-
|
|
2190
|
+
'Set up KeepGoing hooks and instructions. Use scope "user" for global setup (all projects) or "project" for per-project setup.',
|
|
2049
2191
|
{
|
|
2050
|
-
sessionHooks: z4.boolean().optional().default(true).describe("Add session hooks to
|
|
2051
|
-
claudeMd: z4.boolean().optional().default(true).describe("Add KeepGoing instructions to CLAUDE.md")
|
|
2192
|
+
sessionHooks: z4.boolean().optional().default(true).describe("Add session hooks to settings.json"),
|
|
2193
|
+
claudeMd: z4.boolean().optional().default(true).describe("Add KeepGoing instructions to CLAUDE.md"),
|
|
2194
|
+
scope: z4.enum(["project", "user"]).optional().default("project").describe('Where to write config: "user" for global (~/.claude/), "project" for per-project (.claude/)')
|
|
2052
2195
|
},
|
|
2053
|
-
async ({ sessionHooks, claudeMd }) => {
|
|
2054
|
-
const
|
|
2055
|
-
const
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
settings.hooks = {};
|
|
2196
|
+
async ({ sessionHooks, claudeMd, scope }) => {
|
|
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
|
|
2065
2207
|
}
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
settings.hooks.SessionStart.push(SESSION_START_HOOK);
|
|
2071
|
-
settingsChanged = true;
|
|
2072
|
-
}
|
|
2073
|
-
if (!Array.isArray(settings.hooks.Stop)) {
|
|
2074
|
-
settings.hooks.Stop = [];
|
|
2075
|
-
}
|
|
2076
|
-
if (!hasKeepGoingHook(settings.hooks.Stop)) {
|
|
2077
|
-
settings.hooks.Stop.push(STOP_HOOK);
|
|
2078
|
-
settingsChanged = true;
|
|
2079
|
-
}
|
|
2080
|
-
if (!Array.isArray(settings.hooks.PostToolUse)) {
|
|
2081
|
-
settings.hooks.PostToolUse = [];
|
|
2082
|
-
}
|
|
2083
|
-
if (!hasKeepGoingHook(settings.hooks.PostToolUse)) {
|
|
2084
|
-
settings.hooks.PostToolUse.push(POST_TOOL_USE_HOOK);
|
|
2085
|
-
settingsChanged = true;
|
|
2086
|
-
}
|
|
2087
|
-
if (settingsChanged) {
|
|
2088
|
-
results.push("**Session hooks:** Added to `.claude/settings.json`");
|
|
2089
|
-
} else {
|
|
2090
|
-
results.push("**Session hooks:** Already present, skipped");
|
|
2091
|
-
}
|
|
2092
|
-
}
|
|
2093
|
-
if (process.env.KEEPGOING_PRO_BYPASS === "1" || getLicenseForFeature("session-awareness")) {
|
|
2094
|
-
const needsUpdate = settings.statusLine?.command && isLegacyStatusline(settings.statusLine.command);
|
|
2095
|
-
if (!settings.statusLine || needsUpdate) {
|
|
2096
|
-
settings.statusLine = {
|
|
2097
|
-
type: "command",
|
|
2098
|
-
command: STATUSLINE_CMD
|
|
2099
|
-
};
|
|
2100
|
-
settingsChanged = true;
|
|
2101
|
-
results.push(needsUpdate ? "**Statusline:** Migrated to auto-updating `npx` command" : "**Statusline:** Added to `.claude/settings.json`");
|
|
2102
|
-
} else {
|
|
2103
|
-
results.push("**Statusline:** `statusLine` already configured in settings, skipped");
|
|
2104
|
-
}
|
|
2105
|
-
cleanupLegacyScript();
|
|
2106
|
-
}
|
|
2107
|
-
if (settingsChanged) {
|
|
2108
|
-
if (!fs6.existsSync(claudeDir)) {
|
|
2109
|
-
fs6.mkdirSync(claudeDir, { recursive: true });
|
|
2110
|
-
}
|
|
2111
|
-
fs6.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
2112
|
-
}
|
|
2113
|
-
if (claudeMd) {
|
|
2114
|
-
const dotClaudeMdPath = path8.join(workspacePath, ".claude", "CLAUDE.md");
|
|
2115
|
-
const rootClaudeMdPath = path8.join(workspacePath, "CLAUDE.md");
|
|
2116
|
-
const claudeMdPath = fs6.existsSync(dotClaudeMdPath) ? dotClaudeMdPath : rootClaudeMdPath;
|
|
2117
|
-
let existing = "";
|
|
2118
|
-
if (fs6.existsSync(claudeMdPath)) {
|
|
2119
|
-
existing = fs6.readFileSync(claudeMdPath, "utf-8");
|
|
2120
|
-
}
|
|
2121
|
-
if (existing.includes("## KeepGoing")) {
|
|
2122
|
-
results.push("**CLAUDE.md:** KeepGoing section already present, skipped");
|
|
2123
|
-
} else {
|
|
2124
|
-
const updated = existing + CLAUDE_MD_SECTION;
|
|
2125
|
-
fs6.writeFileSync(claudeMdPath, updated);
|
|
2126
|
-
results.push("**CLAUDE.md:** Added KeepGoing section");
|
|
2127
|
-
}
|
|
2128
|
-
}
|
|
2208
|
+
});
|
|
2209
|
+
const formatted = result.messages.map((msg) => {
|
|
2210
|
+
return msg.replace(/^([^:]+:)/, "**$1**");
|
|
2211
|
+
});
|
|
2129
2212
|
return {
|
|
2130
|
-
content: [{ type: "text", text:
|
|
2213
|
+
content: [{ type: "text", text: formatted.join("\n") }]
|
|
2131
2214
|
};
|
|
2132
2215
|
}
|
|
2133
2216
|
);
|