@keepgoingdev/mcp-server 0.5.4 → 0.5.6

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 KeepGoing.dev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
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
@@ -1960,10 +1960,48 @@ function registerGetCurrentTask(server, reader) {
1960
1960
  }
1961
1961
 
1962
1962
  // src/tools/setupProject.ts
1963
+ import fs6 from "fs";
1964
+ import os3 from "os";
1965
+ import path8 from "path";
1966
+ import { z as z4 } from "zod";
1967
+
1968
+ // src/cli/migrate.ts
1963
1969
  import fs5 from "fs";
1964
1970
  import os2 from "os";
1965
1971
  import path7 from "path";
1966
- import { z as z4 } from "zod";
1972
+ var STATUSLINE_CMD = "npx -y @keepgoingdev/mcp-server --statusline";
1973
+ function isLegacyStatusline(command) {
1974
+ return !command.includes("--statusline") && command.includes("keepgoing-statusline");
1975
+ }
1976
+ function migrateStatusline(wsPath) {
1977
+ const settingsPath = path7.join(wsPath, ".claude", "settings.json");
1978
+ if (!fs5.existsSync(settingsPath)) return void 0;
1979
+ try {
1980
+ const settings = JSON.parse(fs5.readFileSync(settingsPath, "utf-8"));
1981
+ const cmd = settings.statusLine?.command;
1982
+ if (!cmd || !isLegacyStatusline(cmd)) return void 0;
1983
+ settings.statusLine = {
1984
+ type: "command",
1985
+ command: STATUSLINE_CMD
1986
+ };
1987
+ fs5.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
1988
+ cleanupLegacyScript();
1989
+ return "[KeepGoing] Migrated statusline to auto-updating command (restart Claude Code to apply)";
1990
+ } catch {
1991
+ return void 0;
1992
+ }
1993
+ }
1994
+ function cleanupLegacyScript() {
1995
+ const legacyScript = path7.join(os2.homedir(), ".claude", "keepgoing-statusline.sh");
1996
+ if (fs5.existsSync(legacyScript)) {
1997
+ try {
1998
+ fs5.unlinkSync(legacyScript);
1999
+ } catch {
2000
+ }
2001
+ }
2002
+ }
2003
+
2004
+ // src/tools/setupProject.ts
1967
2005
  var KEEPGOING_MARKER = "@keepgoingdev/mcp-server";
1968
2006
  var SESSION_START_HOOK = {
1969
2007
  matcher: "",
@@ -2005,101 +2043,140 @@ function hasKeepGoingHook(hookEntries) {
2005
2043
  (entry) => entry?.hooks?.some((h) => typeof h?.command === "string" && h.command.includes(KEEPGOING_MARKER))
2006
2044
  );
2007
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
+ }
2008
2111
  function registerSetupProject(server, workspacePath) {
2009
2112
  server.tool(
2010
2113
  "setup_project",
2011
- "Set up KeepGoing in the current project. Adds session hooks to .claude/settings.json and CLAUDE.md instructions so checkpoints are saved automatically.",
2114
+ 'Set up KeepGoing hooks and instructions. Use scope "user" for global setup (all projects) or "project" for per-project setup.',
2012
2115
  {
2013
- sessionHooks: z4.boolean().optional().default(true).describe("Add session hooks to .claude/settings.json"),
2014
- claudeMd: z4.boolean().optional().default(true).describe("Add KeepGoing instructions to CLAUDE.md")
2116
+ sessionHooks: z4.boolean().optional().default(true).describe("Add session hooks to settings.json"),
2117
+ claudeMd: z4.boolean().optional().default(true).describe("Add KeepGoing instructions to CLAUDE.md"),
2118
+ scope: z4.enum(["project", "user"]).optional().default("project").describe('Where to write config: "user" for global (~/.claude/), "project" for per-project (.claude/)')
2015
2119
  },
2016
- async ({ sessionHooks, claudeMd }) => {
2120
+ async ({ sessionHooks, claudeMd, scope }) => {
2017
2121
  const results = [];
2018
- const claudeDir = path7.join(workspacePath, ".claude");
2019
- const settingsPath = path7.join(claudeDir, "settings.json");
2122
+ const { claudeDir, settingsPath, claudeMdPath } = resolveScopePaths(scope, workspacePath);
2123
+ const scopeLabel = scope === "user" ? "`~/.claude/settings.json`" : "`.claude/settings.json`";
2020
2124
  let settings = {};
2021
- if (fs5.existsSync(settingsPath)) {
2022
- settings = JSON.parse(fs5.readFileSync(settingsPath, "utf-8"));
2125
+ if (fs6.existsSync(settingsPath)) {
2126
+ settings = JSON.parse(fs6.readFileSync(settingsPath, "utf-8"));
2023
2127
  }
2024
2128
  let settingsChanged = false;
2025
2129
  if (sessionHooks) {
2026
- if (!settings.hooks) {
2027
- settings.hooks = {};
2028
- }
2029
- if (!Array.isArray(settings.hooks.SessionStart)) {
2030
- settings.hooks.SessionStart = [];
2031
- }
2032
- if (!hasKeepGoingHook(settings.hooks.SessionStart)) {
2033
- settings.hooks.SessionStart.push(SESSION_START_HOOK);
2034
- settingsChanged = true;
2035
- }
2036
- if (!Array.isArray(settings.hooks.Stop)) {
2037
- settings.hooks.Stop = [];
2038
- }
2039
- if (!hasKeepGoingHook(settings.hooks.Stop)) {
2040
- settings.hooks.Stop.push(STOP_HOOK);
2041
- settingsChanged = true;
2042
- }
2043
- if (!Array.isArray(settings.hooks.PostToolUse)) {
2044
- settings.hooks.PostToolUse = [];
2045
- }
2046
- if (!hasKeepGoingHook(settings.hooks.PostToolUse)) {
2047
- settings.hooks.PostToolUse.push(POST_TOOL_USE_HOOK);
2048
- settingsChanged = true;
2049
- }
2050
- if (settingsChanged) {
2051
- results.push("**Session hooks:** Added to `.claude/settings.json`");
2130
+ const hooksChanged = writeHooksToSettings(settings);
2131
+ settingsChanged = hooksChanged;
2132
+ if (hooksChanged) {
2133
+ results.push(`**Session hooks:** Added to ${scopeLabel}`);
2052
2134
  } else {
2053
2135
  results.push("**Session hooks:** Already present, skipped");
2054
2136
  }
2137
+ const conflict = checkHookConflict(scope, workspacePath);
2138
+ if (conflict) {
2139
+ results.push(conflict);
2140
+ }
2055
2141
  }
2056
- if (process.env.KEEPGOING_PRO_BYPASS === "1" || getLicenseForFeature("session-awareness")) {
2057
- const statuslineSrc = path7.resolve(
2058
- new URL(".", import.meta.url).pathname,
2059
- "statusline.sh"
2060
- );
2061
- const claudeHome = path7.join(os2.homedir(), ".claude");
2062
- const statuslineDest = path7.join(claudeHome, "keepgoing-statusline.sh");
2063
- if (fs5.existsSync(statuslineSrc)) {
2064
- if (!fs5.existsSync(claudeHome)) {
2065
- fs5.mkdirSync(claudeHome, { recursive: true });
2066
- }
2067
- fs5.copyFileSync(statuslineSrc, statuslineDest);
2068
- fs5.chmodSync(statuslineDest, 493);
2069
- if (!settings.statusLine) {
2142
+ if (scope === "project") {
2143
+ if (process.env.KEEPGOING_PRO_BYPASS === "1" || getLicenseForFeature("session-awareness")) {
2144
+ const needsUpdate = settings.statusLine?.command && isLegacyStatusline(settings.statusLine.command);
2145
+ if (!settings.statusLine || needsUpdate) {
2070
2146
  settings.statusLine = {
2071
2147
  type: "command",
2072
- command: statuslineDest
2148
+ command: STATUSLINE_CMD
2073
2149
  };
2074
2150
  settingsChanged = true;
2075
- results.push("**Statusline:** Installed `keepgoing-statusline.sh` and added to `.claude/settings.json`");
2151
+ results.push(needsUpdate ? "**Statusline:** Migrated to auto-updating `npx` command" : "**Statusline:** Added to `.claude/settings.json`");
2076
2152
  } else {
2077
2153
  results.push("**Statusline:** `statusLine` already configured in settings, skipped");
2078
2154
  }
2079
- } else {
2080
- results.push("**Statusline:** Script not found in package, skipped");
2155
+ cleanupLegacyScript();
2081
2156
  }
2082
2157
  }
2083
2158
  if (settingsChanged) {
2084
- if (!fs5.existsSync(claudeDir)) {
2085
- fs5.mkdirSync(claudeDir, { recursive: true });
2159
+ if (!fs6.existsSync(claudeDir)) {
2160
+ fs6.mkdirSync(claudeDir, { recursive: true });
2086
2161
  }
2087
- fs5.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
2162
+ fs6.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
2088
2163
  }
2089
2164
  if (claudeMd) {
2090
- const dotClaudeMdPath = path7.join(workspacePath, ".claude", "CLAUDE.md");
2091
- const rootClaudeMdPath = path7.join(workspacePath, "CLAUDE.md");
2092
- const claudeMdPath = fs5.existsSync(dotClaudeMdPath) ? dotClaudeMdPath : rootClaudeMdPath;
2093
2165
  let existing = "";
2094
- if (fs5.existsSync(claudeMdPath)) {
2095
- existing = fs5.readFileSync(claudeMdPath, "utf-8");
2166
+ if (fs6.existsSync(claudeMdPath)) {
2167
+ existing = fs6.readFileSync(claudeMdPath, "utf-8");
2096
2168
  }
2169
+ const mdLabel = scope === "user" ? "`~/.claude/CLAUDE.md`" : "`CLAUDE.md`";
2097
2170
  if (existing.includes("## KeepGoing")) {
2098
- results.push("**CLAUDE.md:** KeepGoing section already present, skipped");
2171
+ results.push(`**CLAUDE.md:** KeepGoing section already present in ${mdLabel}, skipped`);
2099
2172
  } else {
2100
2173
  const updated = existing + CLAUDE_MD_SECTION;
2101
- fs5.writeFileSync(claudeMdPath, updated);
2102
- results.push("**CLAUDE.md:** Added KeepGoing 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}`);
2103
2180
  }
2104
2181
  }
2105
2182
  return {
@@ -2381,6 +2458,10 @@ async function handlePrintMomentum() {
2381
2458
  lines.push(` Worked on ${touchedCount} files on ${lastSession.gitBranch ?? "unknown branch"}`);
2382
2459
  }
2383
2460
  lines.push(" Tip: Use the get_reentry_briefing tool for a full briefing");
2461
+ const migrationMsg = migrateStatusline(wsPath);
2462
+ if (migrationMsg) {
2463
+ lines.push(migrationMsg);
2464
+ }
2384
2465
  console.log(lines.join("\n"));
2385
2466
  process.exit(0);
2386
2467
  }
@@ -2422,7 +2503,7 @@ async function handlePrintCurrent() {
2422
2503
  }
2423
2504
 
2424
2505
  // src/cli/saveCheckpoint.ts
2425
- import path8 from "path";
2506
+ import path9 from "path";
2426
2507
  async function handleSaveCheckpoint() {
2427
2508
  const wsPath = resolveWsPath();
2428
2509
  const reader = new KeepGoingReader(wsPath);
@@ -2450,9 +2531,9 @@ async function handleSaveCheckpoint() {
2450
2531
  sessionStartTime: lastSession?.timestamp ?? now,
2451
2532
  lastActivityTime: now
2452
2533
  });
2453
- const summary = buildSmartSummary(events) ?? `Worked on ${touchedFiles.slice(0, 5).map((f) => path8.basename(f)).join(", ")}`;
2534
+ const summary = buildSmartSummary(events) ?? `Worked on ${touchedFiles.slice(0, 5).map((f) => path9.basename(f)).join(", ")}`;
2454
2535
  const nextStep = buildSmartNextStep(events);
2455
- const projectName = path8.basename(resolveStorageRoot(wsPath));
2536
+ const projectName = path9.basename(resolveStorageRoot(wsPath));
2456
2537
  const sessionId = generateSessionId({ workspaceRoot: wsPath, branch: gitBranch ?? void 0, worktreePath: wsPath });
2457
2538
  const checkpoint = createCheckpoint({
2458
2539
  summary,
@@ -2543,7 +2624,8 @@ async function handleUpdateTaskFromHook() {
2543
2624
  const fileName = filePath ? filePath.split("/").pop() ?? filePath : "";
2544
2625
  const writer = new KeepGoingWriter(wsPath);
2545
2626
  const existing = writer.readCurrentTasks();
2546
- const cachedBranch = existing.find((t) => t.sessionActive && t.worktreePath === wsPath)?.branch;
2627
+ const sessionIdFromHook = hookData.session_id;
2628
+ const cachedBranch = sessionIdFromHook ? existing.find((t) => t.sessionId === sessionIdFromHook)?.branch : void 0;
2547
2629
  const branch = cachedBranch ?? getCurrentBranch(wsPath) ?? void 0;
2548
2630
  const task = {
2549
2631
  taskSummary: fileName ? `${toolName} ${fileName}` : `Used ${toolName}`,
@@ -2563,13 +2645,69 @@ async function handleUpdateTaskFromHook() {
2563
2645
  process.stdin.resume();
2564
2646
  }
2565
2647
 
2648
+ // src/cli/statusline.ts
2649
+ import fs7 from "fs";
2650
+ import path10 from "path";
2651
+ var STDIN_TIMEOUT_MS2 = 3e3;
2652
+ async function handleStatusline() {
2653
+ const chunks = [];
2654
+ const timeout = setTimeout(() => process.exit(0), STDIN_TIMEOUT_MS2);
2655
+ process.stdin.on("error", () => {
2656
+ clearTimeout(timeout);
2657
+ process.exit(0);
2658
+ });
2659
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
2660
+ process.stdin.on("end", () => {
2661
+ clearTimeout(timeout);
2662
+ try {
2663
+ const raw = Buffer.concat(chunks).toString("utf-8").trim();
2664
+ if (!raw) {
2665
+ process.exit(0);
2666
+ }
2667
+ const input = JSON.parse(raw);
2668
+ const dir = input.workspace?.current_dir ?? input.cwd;
2669
+ if (!dir) {
2670
+ process.exit(0);
2671
+ }
2672
+ const gitRoot = findGitRoot(dir);
2673
+ const tasksFile = path10.join(gitRoot, ".keepgoing", "current-tasks.json");
2674
+ if (!fs7.existsSync(tasksFile)) {
2675
+ process.exit(0);
2676
+ }
2677
+ const data = JSON.parse(fs7.readFileSync(tasksFile, "utf-8"));
2678
+ const branch = getCurrentBranch(gitRoot) ?? "";
2679
+ const active = pruneStaleTasks(data.tasks ?? []).filter((t) => t.sessionActive && t.branch === branch);
2680
+ if (active.length === 0) {
2681
+ process.exit(0);
2682
+ }
2683
+ active.sort((a, b) => a.updatedAt.localeCompare(b.updatedAt));
2684
+ const task = active[active.length - 1];
2685
+ const summary = task.taskSummary;
2686
+ if (!summary) {
2687
+ process.exit(0);
2688
+ }
2689
+ if (branch) {
2690
+ process.stdout.write(`[KG] ${branch}: ${summary}
2691
+ `);
2692
+ } else {
2693
+ process.stdout.write(`[KG] ${summary}
2694
+ `);
2695
+ }
2696
+ } catch {
2697
+ }
2698
+ process.exit(0);
2699
+ });
2700
+ process.stdin.resume();
2701
+ }
2702
+
2566
2703
  // src/index.ts
2567
2704
  var CLI_HANDLERS = {
2568
2705
  "--print-momentum": handlePrintMomentum,
2569
2706
  "--save-checkpoint": handleSaveCheckpoint,
2570
2707
  "--update-task": handleUpdateTask,
2571
2708
  "--update-task-from-hook": handleUpdateTaskFromHook,
2572
- "--print-current": handlePrintCurrent
2709
+ "--print-current": handlePrintCurrent,
2710
+ "--statusline": handleStatusline
2573
2711
  };
2574
2712
  var flag = process.argv.slice(2).find((a) => a in CLI_HANDLERS);
2575
2713
  if (flag) {