cf-memory-mcp 3.49.0 → 3.51.0

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.
@@ -3919,7 +3919,7 @@ if (process.argv.includes('--version') || process.argv.includes('-v')) {
3919
3919
 
3920
3920
  // Global --help only when no subcommand is present. With a subcommand, fall
3921
3921
  // through to the per-command help dispatch below.
3922
- const SUBCOMMANDS = new Set(['resume', 'list', 'checkpoint', 'status', 'clean', 'export', 'import', 'doctor', 'completion', 'delete', 'env', 'history', 'link']);
3922
+ const SUBCOMMANDS = new Set(['resume', 'list', 'checkpoint', 'status', 'clean', 'export', 'import', 'doctor', 'completion', 'delete', 'env', 'history', 'link', 'explain']);
3923
3923
  const hasSubcommand = process.argv[2] && SUBCOMMANDS.has(process.argv[2]);
3924
3924
 
3925
3925
  if (!hasSubcommand && (process.argv.includes('--help') || process.argv.includes('-h'))) {
@@ -3935,6 +3935,7 @@ Usage:
3935
3935
  npx cf-memory-mcp history List all handoffs for cwd, chronological
3936
3936
  npx cf-memory-mcp checkpoint ["<goal>"] Snapshot current state (keep_open)
3937
3937
  npx cf-memory-mcp link <child> --parent <id> Retroactively link a child session to a parent
3938
+ npx cf-memory-mcp explain <id> Break down the handoff's quality_score
3938
3939
  npx cf-memory-mcp status Show bridge state + server resume availability
3939
3940
  npx cf-memory-mcp clean Delete local disk cache for current cwd
3940
3941
  npx cf-memory-mcp clean --all Delete ALL local disk caches
@@ -4399,21 +4400,34 @@ async function runResumeCli() {
4399
4400
  // --diff <other-id>: compare two handoffs. Shows added/removed
4400
4401
  // next_steps, branch shift, status change, files diff. Useful
4401
4402
  // for "what changed between yesterday's session and today's?".
4403
+ // Special value "--parent" diffs against the chain parent.
4402
4404
  if (flags.diff) {
4403
4405
  if (!found) {
4404
4406
  process.stderr.write('No source handoff available to diff against.\n');
4405
4407
  process.exit(3);
4406
4408
  }
4409
+ let diffTarget = flags.diff;
4410
+ // --diff --parent: resolve to the parent_session_id from the
4411
+ // current handoff so users can do "what's new since the
4412
+ // last checkpoint" without typing the parent's id.
4413
+ if (diffTarget === '--parent' || diffTarget === 'parent') {
4414
+ const parentId = payload.resume_handoff.handoff?.parent_session_id;
4415
+ if (!parentId) {
4416
+ process.stderr.write('This handoff has no parent_session_id to diff against.\n');
4417
+ process.exit(3);
4418
+ }
4419
+ diffTarget = parentId;
4420
+ }
4407
4421
  const otherRes = await server.makeRequest({
4408
4422
  jsonrpc: '2.0', id: `cli-diff-${Date.now()}`,
4409
4423
  method: 'tools/call',
4410
- params: { name: 'get_context_bootstrap', arguments: { resume: true, session_id_hint: flags.diff } },
4424
+ params: { name: 'get_context_bootstrap', arguments: { resume: true, session_id_hint: diffTarget } },
4411
4425
  });
4412
4426
  const otherText = otherRes?.result?.content?.[0]?.text;
4413
4427
  const otherPayload = JSON.parse(otherText || '{}');
4414
4428
  const otherHandoff = otherPayload.resume_handoff?.handoff;
4415
4429
  if (!otherHandoff) {
4416
- process.stderr.write(`Could not fetch handoff "${flags.diff}" to diff against.\n`);
4430
+ process.stderr.write(`Could not fetch handoff "${diffTarget}" to diff against.\n`);
4417
4431
  process.exit(3);
4418
4432
  }
4419
4433
  const h = payload.resume_handoff.handoff;
@@ -4563,6 +4577,102 @@ async function runResumeCli() {
4563
4577
  }
4564
4578
  }
4565
4579
 
4580
+ async function runExplainCli() {
4581
+ if (!API_KEY) {
4582
+ console.error('Error: CF_MEMORY_API_KEY environment variable is required');
4583
+ process.exit(1);
4584
+ }
4585
+ const { positional, flags } = parseCliArgs(process.argv.slice(3));
4586
+ const idArg = positional[0];
4587
+ if (!idArg) {
4588
+ console.error('Usage: cf-memory-mcp explain <session-id-or-prefix>');
4589
+ process.exit(1);
4590
+ }
4591
+ const server = new CFMemoryMCP();
4592
+ server.logDebug = () => {};
4593
+ try {
4594
+ const response = await server.makeRequest({
4595
+ jsonrpc: '2.0', id: `cli-explain-${Date.now()}`,
4596
+ method: 'tools/call',
4597
+ params: { name: 'get_context_bootstrap', arguments: { resume: true, session_id_hint: idArg } },
4598
+ });
4599
+ const text = response?.result?.content?.[0]?.text;
4600
+ const payload = JSON.parse(text || '{}');
4601
+ const envelope = payload.resume_handoff;
4602
+ if (!envelope?.handoff) {
4603
+ process.stderr.write((payload.empty_hint || `No handoff found for "${idArg}".`) + '\n');
4604
+ process.exit(3);
4605
+ }
4606
+ const h = envelope.handoff;
4607
+ const ageMin = envelope.handoff_age_minutes;
4608
+ const sizeBytes = envelope.handoff_size_bytes ?? 0;
4609
+
4610
+ // Mirror the server-side rubric (see computeQualityScore).
4611
+ const components = [];
4612
+ const add = (label, points, present, detail) =>
4613
+ components.push({ label, points, present, detail });
4614
+
4615
+ add('goal present (>=5 chars)', 0.20, !!(h.goal && h.goal.length >= 5), h.goal ? `"${h.goal.slice(0,40)}"` : 'missing');
4616
+ add('next_steps present', 0.20,
4617
+ Array.isArray(h.next_steps) && h.next_steps.length > 0,
4618
+ `${h.next_steps?.length ?? 0} step${(h.next_steps?.length ?? 0) === 1 ? '' : 's'}`);
4619
+ add('code_anchors present', 0.15,
4620
+ Array.isArray(h.code_anchors) && h.code_anchors.length > 0,
4621
+ `${h.code_anchors?.length ?? 0} anchor${(h.code_anchors?.length ?? 0) === 1 ? '' : 's'}`);
4622
+ add('files_touched present', 0.10,
4623
+ Array.isArray(h.files_touched) && h.files_touched.length > 0,
4624
+ `${h.files_touched?.length ?? 0} file${(h.files_touched?.length ?? 0) === 1 ? '' : 's'}`);
4625
+ add('decisions present', 0.10,
4626
+ Array.isArray(h.decisions) && h.decisions.length > 0,
4627
+ `${h.decisions?.length ?? 0} decision${(h.decisions?.length ?? 0) === 1 ? '' : 's'}`);
4628
+
4629
+ // Recency
4630
+ let recency = 0;
4631
+ let recencyDetail = '?';
4632
+ if (ageMin !== undefined) {
4633
+ if (ageMin <= 1440) recency = 0.15;
4634
+ else if (ageMin <= 10080) recency = 0.15 * (1 - (ageMin - 1440) / (10080 - 1440));
4635
+ recencyDetail = `${ageMin}m old`;
4636
+ }
4637
+ components.push({ label: 'recency (decays past 24h)', points: 0.15, present: recency > 0, detail: `${recencyDetail} → +${recency.toFixed(3)}` });
4638
+
4639
+ // Size in target band
4640
+ let sizeBonus = 0;
4641
+ let sizeDetail;
4642
+ if (sizeBytes >= 500 && sizeBytes <= 8000) { sizeBonus = 0.10; sizeDetail = 'in target 500-8000 byte range'; }
4643
+ else if (sizeBytes >= 200 && sizeBytes <= 16000) { sizeBonus = 0.05; sizeDetail = 'in extended 200-16000 byte range'; }
4644
+ else { sizeDetail = sizeBytes < 200 ? 'too small (<200 bytes)' : 'too large (>16000 bytes)'; }
4645
+ components.push({ label: 'size sweet spot', points: 0.10, present: sizeBonus > 0, detail: `${sizeBytes} bytes — ${sizeDetail}` });
4646
+
4647
+ if (flags.json) {
4648
+ process.stdout.write(JSON.stringify({
4649
+ session_id: envelope.session_id,
4650
+ quality_score: envelope.quality_score,
4651
+ components,
4652
+ }, null, 2) + '\n');
4653
+ process.exit(0);
4654
+ }
4655
+ const shortId = (envelope.session_id || '').slice(0, 8);
4656
+ process.stdout.write(`Quality breakdown for ${shortId} (final score: ${envelope.quality_score}):\n\n`);
4657
+ let earned = 0;
4658
+ for (const c of components) {
4659
+ const mark = c.present ? '✓' : '✗';
4660
+ const earned_pts = c.present ? c.points : 0;
4661
+ earned += earned_pts;
4662
+ process.stdout.write(` ${mark} ${c.label.padEnd(32)} +${c.points.toFixed(2)} ${c.detail || ''}\n`);
4663
+ }
4664
+ process.stdout.write(`\n Total earned: ${earned.toFixed(2)} / 1.00\n`);
4665
+ const missing = components.filter(c => !c.present);
4666
+ if (missing.length > 0) {
4667
+ process.stdout.write(`\n To improve: add ${missing.map(m => m.label).join(', ')}\n`);
4668
+ }
4669
+ process.exit(0);
4670
+ } catch (err) {
4671
+ console.error('explain command failed:', err.message);
4672
+ process.exit(1);
4673
+ }
4674
+ }
4675
+
4566
4676
  async function runListCli() {
4567
4677
  if (!API_KEY) {
4568
4678
  console.error('Error: CF_MEMORY_API_KEY environment variable is required');
@@ -5006,7 +5116,8 @@ function runEnvCli() {
5006
5116
  function runCompletionCli() {
5007
5117
  const shell = process.argv[3] || 'bash';
5008
5118
  const install = process.argv.includes('--install');
5009
- const commands = ['resume', 'list', 'history', 'checkpoint', 'link', 'status', 'clean', 'export', 'import', 'delete', 'doctor', 'env', 'completion'];
5119
+ const uninstall = process.argv.includes('--uninstall');
5120
+ const commands = ['resume', 'list', 'history', 'checkpoint', 'link', 'explain', 'status', 'clean', 'export', 'import', 'delete', 'doctor', 'env', 'completion'];
5010
5121
  const flags = ['--json', '-j', '--limit', '-n', '--md', '--all', '--force', '-f', '--version', '-v', '--help', '-h', '--diagnose'];
5011
5122
 
5012
5123
  // Determine the install target for each shell. Use user-local paths
@@ -5034,6 +5145,27 @@ function runCompletionCli() {
5034
5145
  }
5035
5146
  return null;
5036
5147
  })();
5148
+ // --uninstall: delete the previously-installed completion file. No
5149
+ // need to generate the script. Idempotent.
5150
+ if (uninstall) {
5151
+ if (!installTarget) {
5152
+ process.stderr.write(`No uninstall target for shell ${shell}.\n`);
5153
+ process.exit(1);
5154
+ }
5155
+ try {
5156
+ if (fs.existsSync(installTarget)) {
5157
+ fs.unlinkSync(installTarget);
5158
+ process.stderr.write(`Uninstalled ${shell} completion from ${installTarget}\n`);
5159
+ } else {
5160
+ process.stderr.write(`Nothing to uninstall (no file at ${installTarget}).\n`);
5161
+ }
5162
+ process.exit(0);
5163
+ } catch (err) {
5164
+ process.stderr.write(`Failed to uninstall: ${err.message}\n`);
5165
+ process.exit(1);
5166
+ }
5167
+ }
5168
+
5037
5169
  // Buffer the output so we can either print or write-to-disk.
5038
5170
  let output = '';
5039
5171
  const emit = (chunk) => { output += chunk; };
@@ -5247,6 +5379,11 @@ const PER_COMMAND_HELP = {
5247
5379
  a parent in the chain. Useful when you forgot --force on checkpoint
5248
5380
  or want to manually chain sessions across repos.
5249
5381
  --json, -j Emit a JSON status object.`,
5382
+ explain: `cf-memory-mcp explain <session-id-or-prefix> [--json]
5383
+ Break down the handoff's quality_score into its rubric components,
5384
+ showing what's earned + what's missing. Lets users see why a handoff
5385
+ scored 0.5 vs 0.9 — and how to improve it.
5386
+ --json, -j Emit the breakdown as JSON.`,
5250
5387
  };
5251
5388
 
5252
5389
  function printPerCommandHelp(cmd) {
@@ -5359,8 +5496,25 @@ async function runDoctorCli() {
5359
5496
  }
5360
5497
  }
5361
5498
 
5499
+ // Optional: handoff stats summary at the bottom (visibility into
5500
+ // how much resume context is captured for the current user).
5501
+ let handoffStats = null;
5502
+ if (API_KEY) {
5503
+ try {
5504
+ const statsRes = await server.makeRequestOnce({
5505
+ jsonrpc: '2.0', id: `doctor-stats-${Date.now()}`,
5506
+ method: 'tools/call', params: { name: 'get_stats', arguments: {} },
5507
+ });
5508
+ const statsText = statsRes?.result?.content?.[0]?.text;
5509
+ if (statsText) {
5510
+ const statsPayload = JSON.parse(statsText);
5511
+ if (statsPayload?.handoffs) handoffStats = statsPayload.handoffs;
5512
+ }
5513
+ } catch (_) { /* skip on failure */ }
5514
+ }
5515
+
5362
5516
  if (flags.json) {
5363
- process.stdout.write(JSON.stringify({ checks }, null, 2) + '\n');
5517
+ process.stdout.write(JSON.stringify({ checks, ...(handoffStats ? { handoffs: handoffStats } : {}) }, null, 2) + '\n');
5364
5518
  process.exit(checks.every(c => c.ok) ? 0 : 1);
5365
5519
  }
5366
5520
  process.stdout.write(`cf-memory-mcp v${PACKAGE_VERSION} — doctor\n\n`);
@@ -5373,6 +5527,23 @@ async function runDoctorCli() {
5373
5527
  process.stdout.write(` → ${c.fix}\n`);
5374
5528
  }
5375
5529
  }
5530
+ if (handoffStats) {
5531
+ process.stdout.write('\nHandoff stats:\n');
5532
+ process.stdout.write(` total: ${handoffStats.total}\n`);
5533
+ if (handoffStats.by_status) {
5534
+ const byStatus = Object.entries(handoffStats.by_status)
5535
+ .map(([s, n]) => `${s}=${n}`).join(', ');
5536
+ process.stdout.write(` by_status: ${byStatus}\n`);
5537
+ }
5538
+ if (handoffStats.oldest_in_progress) {
5539
+ const o = handoffStats.oldest_in_progress;
5540
+ const shortId = (o.session_id || '').slice(0, 8);
5541
+ process.stdout.write(` oldest in_progress: ${shortId} (${o.age_minutes}m ago)\n`);
5542
+ }
5543
+ if (handoffStats.by_repo && handoffStats.by_repo.length > 0) {
5544
+ process.stdout.write(` top repos: ${handoffStats.by_repo.slice(0, 3).map(r => `${r.repo_path.split('/').pop()}=${r.count}`).join(', ')}\n`);
5545
+ }
5546
+ }
5376
5547
  process.stdout.write('\n');
5377
5548
  process.stdout.write(anyFailed ? 'Some checks failed. See suggestions above.\n' : 'All checks passed.\n');
5378
5549
  process.exit(anyFailed ? 1 : 0);
@@ -5998,6 +6169,11 @@ if (process.argv[2] === 'link') {
5998
6169
  return;
5999
6170
  }
6000
6171
 
6172
+ if (process.argv[2] === 'explain') {
6173
+ runExplainCli();
6174
+ return;
6175
+ }
6176
+
6001
6177
  if (process.argv.includes('--diagnose')) {
6002
6178
  (async () => {
6003
6179
  console.log(`CF Memory MCP v${PACKAGE_VERSION} - Diagnostics`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cf-memory-mcp",
3
- "version": "3.49.0",
3
+ "version": "3.51.0",
4
4
  "description": "Cloudflare-hosted MCP server for code indexing, retrieval, and assistant memory with a direct remote MCP endpoint and local stdio bridge.",
5
5
  "main": "bin/cf-memory-mcp.js",
6
6
  "bin": {