@keepgoingdev/mcp-server 0.7.0 → 0.7.2

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 CHANGED
@@ -78,7 +78,8 @@ function findGitRoot(startPath) {
78
78
  const toplevel = execFileSync("git", ["rev-parse", "--show-toplevel"], {
79
79
  cwd: startPath,
80
80
  encoding: "utf-8",
81
- timeout: 5e3
81
+ timeout: 5e3,
82
+ stdio: ["pipe", "pipe", "pipe"]
82
83
  }).trim();
83
84
  return toplevel || startPath;
84
85
  } catch {
@@ -95,12 +96,14 @@ function resolveStorageRoot(startPath) {
95
96
  const toplevel = execFileSync("git", ["rev-parse", "--show-toplevel"], {
96
97
  cwd: startPath,
97
98
  encoding: "utf-8",
98
- timeout: 5e3
99
+ timeout: 5e3,
100
+ stdio: ["pipe", "pipe", "pipe"]
99
101
  }).trim();
100
102
  const commonDir = execFileSync("git", ["rev-parse", "--git-common-dir"], {
101
103
  cwd: startPath,
102
104
  encoding: "utf-8",
103
- timeout: 5e3
105
+ timeout: 5e3,
106
+ stdio: ["pipe", "pipe", "pipe"]
104
107
  }).trim();
105
108
  const absoluteCommonDir = path.resolve(toplevel, commonDir);
106
109
  const mainRoot = path.dirname(absoluteCommonDir);
@@ -1214,7 +1217,14 @@ function getLicenseForFeature(feature) {
1214
1217
  return features?.includes(feature);
1215
1218
  });
1216
1219
  }
1220
+ function getAllLicensesNeedingRevalidation() {
1221
+ return getActiveLicenses().filter((l) => needsRevalidation(l));
1222
+ }
1217
1223
  var REVALIDATION_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
1224
+ function needsRevalidation(entry) {
1225
+ const lastValidated = new Date(entry.lastValidatedAt).getTime();
1226
+ return Date.now() - lastValidated > REVALIDATION_THRESHOLD_MS;
1227
+ }
1218
1228
 
1219
1229
  // ../../packages/shared/src/featureGate.ts
1220
1230
  var DefaultFeatureGate = class {
@@ -1835,12 +1845,14 @@ var SESSION_END_HOOK = {
1835
1845
  }
1836
1846
  ]
1837
1847
  };
1838
- var KEEPGOING_RULES_VERSION = 1;
1848
+ var KEEPGOING_RULES_VERSION = 2;
1839
1849
  var KEEPGOING_RULES_CONTENT = `<!-- @keepgoingdev/mcp-server v${KEEPGOING_RULES_VERSION} -->
1840
1850
  ## KeepGoing
1841
1851
 
1852
+ When you see KeepGoing momentum data in your session context (from a SessionStart hook), share a brief welcome with the user that includes: what was last worked on, the suggested next step, and any blockers.
1853
+
1842
1854
  After completing a task or meaningful piece of work, call the \`save_checkpoint\` MCP tool with:
1843
- - \`summary\`: 1-2 sentences. What changed and why \u2014 no file paths, no implementation details (those are captured from git).
1855
+ - \`summary\`: 1-2 sentences. What changed and why, no file paths, no implementation details (those are captured from git).
1844
1856
  - \`nextStep\`: What to do next
1845
1857
  - \`blocker\`: Any blocker (if applicable)
1846
1858
  `;
@@ -1966,7 +1978,7 @@ function setupProject(options) {
1966
1978
  messages.push(`Warning: ${conflict}`);
1967
1979
  }
1968
1980
  }
1969
- if (scope === "project") {
1981
+ {
1970
1982
  const needsUpdate = settings.statusLine?.command && statusline?.isLegacy?.(settings.statusLine.command);
1971
1983
  if (!settings.statusLine || needsUpdate) {
1972
1984
  settings.statusLine = {
@@ -1974,7 +1986,7 @@ function setupProject(options) {
1974
1986
  command: STATUSLINE_CMD
1975
1987
  };
1976
1988
  settingsChanged = true;
1977
- messages.push(needsUpdate ? "Statusline: Migrated to auto-updating npx command" : "Statusline: Added to .claude/settings.json");
1989
+ messages.push(needsUpdate ? "Statusline: Migrated to auto-updating npx command" : `Statusline: Added to ${scopeLabel}`);
1978
1990
  } else {
1979
1991
  messages.push("Statusline: Already configured in settings, skipped");
1980
1992
  }
@@ -2096,6 +2108,39 @@ async function activateLicense(licenseKey, instanceName, options) {
2096
2108
  return { valid: false, error: message };
2097
2109
  }
2098
2110
  }
2111
+ async function validateLicense(licenseKey, instanceId, options) {
2112
+ try {
2113
+ const res = await fetchWithTimeout(`${BASE_URL}/validate`, {
2114
+ method: "POST",
2115
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
2116
+ body: new URLSearchParams({ license_key: licenseKey, instance_id: instanceId })
2117
+ });
2118
+ const data = await safeJson(res);
2119
+ if (!res.ok || !data?.valid) {
2120
+ return { valid: false, isNetworkError: false, error: data?.error || `Validation failed (${res.status})` };
2121
+ }
2122
+ if (!options?.allowTestMode && data.license_key?.test_mode) {
2123
+ return { valid: false, isNetworkError: false, error: "This is a test license key. Please use a production license key from your purchase confirmation." };
2124
+ }
2125
+ if (!options?.allowTestMode) {
2126
+ const productError = validateProductIdentity(data.meta);
2127
+ if (productError) {
2128
+ return { valid: false, isNetworkError: false, error: productError };
2129
+ }
2130
+ }
2131
+ return {
2132
+ valid: true,
2133
+ licenseKey: data.license_key?.key,
2134
+ customerName: data.meta?.customer_name,
2135
+ productName: data.meta?.product_name,
2136
+ variantId: data.meta?.variant_id,
2137
+ variantName: data.meta?.variant_name
2138
+ };
2139
+ } catch (err) {
2140
+ 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";
2141
+ return { valid: false, isNetworkError: true, error: message };
2142
+ }
2143
+ }
2099
2144
  async function deactivateLicense(licenseKey, instanceId) {
2100
2145
  try {
2101
2146
  const res = await fetchWithTimeout(`${BASE_URL}/deactivate`, {
@@ -2114,6 +2159,52 @@ async function deactivateLicense(licenseKey, instanceId) {
2114
2159
  }
2115
2160
  }
2116
2161
 
2162
+ // ../../packages/shared/src/licenseRevalidation.ts
2163
+ async function revalidateStaleLicenses(options) {
2164
+ const stale = getAllLicensesNeedingRevalidation();
2165
+ const result = { checked: stale.length, refreshed: 0, revoked: 0, skippedNetworkError: 0 };
2166
+ if (stale.length === 0) return result;
2167
+ const updates = /* @__PURE__ */ new Map();
2168
+ for (const entry of stale) {
2169
+ const vResult = await validateLicense(entry.licenseKey, entry.instanceId, {
2170
+ allowTestMode: options?.allowTestMode
2171
+ });
2172
+ if (vResult.valid) {
2173
+ const patch = {
2174
+ lastValidatedAt: (/* @__PURE__ */ new Date()).toISOString()
2175
+ };
2176
+ if (vResult.customerName) patch.customerName = vResult.customerName;
2177
+ updates.set(entry.licenseKey, patch);
2178
+ result.refreshed++;
2179
+ } else if (vResult.isNetworkError) {
2180
+ result.skippedNetworkError++;
2181
+ } else {
2182
+ updates.set(entry.licenseKey, { status: "inactive" });
2183
+ result.revoked++;
2184
+ }
2185
+ }
2186
+ if (updates.size > 0) {
2187
+ const store = readLicenseStore();
2188
+ for (const [key, patch] of updates) {
2189
+ const idx = store.licenses.findIndex((l) => l.licenseKey === key);
2190
+ if (idx >= 0) {
2191
+ Object.assign(store.licenses[idx], patch);
2192
+ }
2193
+ }
2194
+ writeLicenseStore(store);
2195
+ }
2196
+ return result;
2197
+ }
2198
+ async function getLicenseForFeatureWithRevalidation(feature, options) {
2199
+ const license = getLicenseForFeature(feature);
2200
+ if (!license) return void 0;
2201
+ if (needsRevalidation(license)) {
2202
+ await revalidateStaleLicenses(options);
2203
+ return getLicenseForFeature(feature);
2204
+ }
2205
+ return license;
2206
+ }
2207
+
2117
2208
  // src/tools/getMomentum.ts
2118
2209
  import { z } from "zod";
2119
2210
  function registerGetMomentum(server, reader, workspacePath) {
@@ -2450,7 +2541,7 @@ function registerGetDecisions(server, reader) {
2450
2541
  ]
2451
2542
  };
2452
2543
  }
2453
- if (process.env.KEEPGOING_PRO_BYPASS !== "1" && !getLicenseForFeature("decisions")) {
2544
+ if (process.env.KEEPGOING_PRO_BYPASS !== "1" && !await getLicenseForFeatureWithRevalidation("decisions")) {
2454
2545
  return {
2455
2546
  content: [
2456
2547
  {
@@ -2516,7 +2607,7 @@ function registerGetCurrentTask(server, reader) {
2516
2607
  ]
2517
2608
  };
2518
2609
  }
2519
- if (process.env.KEEPGOING_PRO_BYPASS !== "1" && !getLicenseForFeature("session-awareness")) {
2610
+ if (process.env.KEEPGOING_PRO_BYPASS !== "1" && !await getLicenseForFeatureWithRevalidation("session-awareness")) {
2520
2611
  return {
2521
2612
  content: [
2522
2613
  {
@@ -2991,7 +3082,7 @@ async function handlePrintMomentum() {
2991
3082
  process.exit(0);
2992
3083
  }
2993
3084
  async function handlePrintCurrent() {
2994
- if (process.env.KEEPGOING_PRO_BYPASS !== "1" && !getLicenseForFeature("session-awareness")) {
3085
+ if (process.env.KEEPGOING_PRO_BYPASS !== "1" && !await getLicenseForFeatureWithRevalidation("session-awareness")) {
2995
3086
  process.exit(0);
2996
3087
  }
2997
3088
  const wsPath = resolveWsPath();
@@ -3079,7 +3170,7 @@ async function handleSaveCheckpoint() {
3079
3170
  branch: gitBranch ?? void 0,
3080
3171
  updatedAt: checkpoint.timestamp
3081
3172
  });
3082
- if (process.env.KEEPGOING_PRO_BYPASS === "1" || getLicenseForFeature("decisions")) {
3173
+ if (process.env.KEEPGOING_PRO_BYPASS === "1" || await getLicenseForFeatureWithRevalidation("decisions")) {
3083
3174
  for (let i = 0; i < commitHashes.length; i++) {
3084
3175
  const hash = commitHashes[i];
3085
3176
  const message = commitMessages[i];
@@ -3104,7 +3195,8 @@ async function handleSaveCheckpoint() {
3104
3195
 
3105
3196
  // src/cli/transcriptUtils.ts
3106
3197
  import fs8 from "fs";
3107
- var TAIL_READ_BYTES = 8192;
3198
+ var TAIL_READ_BYTES = 32768;
3199
+ var LATEST_LABEL_READ_BYTES = 65536;
3108
3200
  var TOOL_VERB_MAP = {
3109
3201
  Edit: "editing",
3110
3202
  MultiEdit: "editing",
@@ -3116,7 +3208,12 @@ var TOOL_VERB_MAP = {
3116
3208
  Agent: "delegating",
3117
3209
  WebFetch: "browsing",
3118
3210
  WebSearch: "browsing",
3119
- TodoWrite: "planning"
3211
+ TodoWrite: "planning",
3212
+ AskUserQuestion: "discussing",
3213
+ EnterPlanMode: "planning",
3214
+ ExitPlanMode: "planning",
3215
+ TaskCreate: "planning",
3216
+ TaskUpdate: "planning"
3120
3217
  };
3121
3218
  function truncateAtWord(text, max) {
3122
3219
  if (text.length <= max) return text;
@@ -3184,6 +3281,50 @@ function extractSessionLabel(transcriptPath) {
3184
3281
  }
3185
3282
  return null;
3186
3283
  }
3284
+ function extractLatestUserLabel(transcriptPath) {
3285
+ if (!transcriptPath || !fs8.existsSync(transcriptPath)) return null;
3286
+ try {
3287
+ const stat = fs8.statSync(transcriptPath);
3288
+ const fileSize = stat.size;
3289
+ if (fileSize === 0) return null;
3290
+ const readSize = Math.min(fileSize, LATEST_LABEL_READ_BYTES);
3291
+ const offset = fileSize - readSize;
3292
+ const buf = Buffer.alloc(readSize);
3293
+ const fd = fs8.openSync(transcriptPath, "r");
3294
+ try {
3295
+ fs8.readSync(fd, buf, 0, readSize, offset);
3296
+ } finally {
3297
+ fs8.closeSync(fd);
3298
+ }
3299
+ const tail = buf.toString("utf-8");
3300
+ const lines = tail.split("\n").reverse();
3301
+ for (const line of lines) {
3302
+ const trimmed = line.trim();
3303
+ if (!trimmed) continue;
3304
+ let entry;
3305
+ try {
3306
+ entry = JSON.parse(trimmed);
3307
+ } catch {
3308
+ continue;
3309
+ }
3310
+ if (!isUserEntry(entry)) continue;
3311
+ let text = extractTextFromContent(entry.message?.content);
3312
+ if (!text) continue;
3313
+ if (text.startsWith("[") || /^<[a-z][\w-]*>/.test(text)) continue;
3314
+ text = text.replace(/@[\w./\-]+/g, "").trim();
3315
+ text = text.replace(FILLER_PREFIX_RE, "").trim();
3316
+ text = text.replace(MARKDOWN_HEADING_RE, "").trim();
3317
+ text = text.replace(/\s+/g, " ").trim();
3318
+ if (text.length < 20) continue;
3319
+ if (text.length > 80) {
3320
+ text = text.slice(0, 80);
3321
+ }
3322
+ return text;
3323
+ }
3324
+ } catch {
3325
+ }
3326
+ return null;
3327
+ }
3187
3328
  function extractCurrentAction(transcriptPath) {
3188
3329
  if (!transcriptPath || !fs8.existsSync(transcriptPath)) return null;
3189
3330
  try {
@@ -3324,6 +3465,9 @@ async function handleStatusline() {
3324
3465
  if (input.agent?.name) {
3325
3466
  label = input.agent.name;
3326
3467
  }
3468
+ if (!label && transcriptPath) {
3469
+ label = extractLatestUserLabel(transcriptPath);
3470
+ }
3327
3471
  if (!label) {
3328
3472
  try {
3329
3473
  const gitRoot = findGitRoot(dir);
@@ -3342,7 +3486,28 @@ async function handleStatusline() {
3342
3486
  if (!label && transcriptPath) {
3343
3487
  label = extractSessionLabel(transcriptPath);
3344
3488
  }
3345
- if (!label) process.exit(0);
3489
+ if (!label) {
3490
+ try {
3491
+ const gitRoot = findGitRoot(dir);
3492
+ const reader = new KeepGoingReader(gitRoot);
3493
+ if (reader.exists()) {
3494
+ const recent = reader.getScopedRecentSessions(10);
3495
+ const last = recent.find((s) => s.source !== "auto") ?? recent[0];
3496
+ if (last) {
3497
+ const ago = formatRelativeTime(last.timestamp);
3498
+ const summary = last.summary ? truncateAtWord(last.summary, 40) : null;
3499
+ const next = last.nextStep ? truncateAtWord(last.nextStep, 30) : null;
3500
+ const parts = [`[KG] ${ago}`];
3501
+ if (summary) parts.push(summary);
3502
+ if (next) parts.push(`\u2192 ${next}`);
3503
+ process.stdout.write(`${parts.join(" \xB7 ")}
3504
+ `);
3505
+ }
3506
+ }
3507
+ } catch {
3508
+ }
3509
+ process.exit(0);
3510
+ }
3346
3511
  const action = transcriptPath ? extractCurrentAction(transcriptPath) : null;
3347
3512
  const budget = action ? 40 : 55;
3348
3513
  const displayLabel = truncateAtWord(label, budget);
@@ -3379,7 +3544,7 @@ async function handleContinueOn() {
3379
3544
  // src/cli/detectDecisions.ts
3380
3545
  async function handleDetectDecisions() {
3381
3546
  const wsPath = resolveWsPath();
3382
- if (!(process.env.KEEPGOING_PRO_BYPASS === "1" || getLicenseForFeature("decisions"))) {
3547
+ if (!(process.env.KEEPGOING_PRO_BYPASS === "1" || await getLicenseForFeatureWithRevalidation("decisions"))) {
3383
3548
  process.exit(0);
3384
3549
  }
3385
3550
  const reader = new KeepGoingReader(wsPath);