@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 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/claude_code_config.json`):
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/licenseClient.ts
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 path6 from "path";
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 = path6.basename(resolveStorageRoot(workspacePath));
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 fs5 from "fs";
1969
- import os2 from "os";
1970
- import path7 from "path";
1971
- var STATUSLINE_CMD = "npx -y @keepgoingdev/mcp-server --statusline";
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 = path7.join(wsPath, ".claude", "settings.json");
1977
- if (!fs5.existsSync(settingsPath)) return void 0;
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(fs5.readFileSync(settingsPath, "utf-8"));
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: STATUSLINE_CMD
2167
+ command: STATUSLINE_CMD2
1985
2168
  };
1986
- fs5.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
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 = path7.join(os2.homedir(), ".claude", "keepgoing-statusline.sh");
1995
- if (fs5.existsSync(legacyScript)) {
2177
+ const legacyScript = path8.join(os3.homedir(), ".claude", "keepgoing-statusline.sh");
2178
+ if (fs6.existsSync(legacyScript)) {
1996
2179
  try {
1997
- fs5.unlinkSync(legacyScript);
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
- "Set up KeepGoing in the current project. Adds session hooks to .claude/settings.json and CLAUDE.md instructions so checkpoints are saved automatically.",
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 .claude/settings.json"),
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 results = [];
2055
- const claudeDir = path8.join(workspacePath, ".claude");
2056
- const settingsPath = path8.join(claudeDir, "settings.json");
2057
- let settings = {};
2058
- if (fs6.existsSync(settingsPath)) {
2059
- settings = JSON.parse(fs6.readFileSync(settingsPath, "utf-8"));
2060
- }
2061
- let settingsChanged = false;
2062
- if (sessionHooks) {
2063
- if (!settings.hooks) {
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
- if (!Array.isArray(settings.hooks.SessionStart)) {
2067
- settings.hooks.SessionStart = [];
2068
- }
2069
- if (!hasKeepGoingHook(settings.hooks.SessionStart)) {
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: results.join("\n") }]
2213
+ content: [{ type: "text", text: formatted.join("\n") }]
2131
2214
  };
2132
2215
  }
2133
2216
  );