cf-memory-mcp 3.48.0 → 3.50.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.
@@ -4061,6 +4061,10 @@ function parseCliArgs(rest) {
4061
4061
  flags.fields = rest[++i];
4062
4062
  } else if (a.startsWith('--fields=')) {
4063
4063
  flags.fields = a.slice('--fields='.length);
4064
+ } else if (a === '--diff') {
4065
+ flags.diff = rest[++i];
4066
+ } else if (a.startsWith('--diff=')) {
4067
+ flags.diff = a.slice('--diff='.length);
4064
4068
  } else if (a === '--older-than') {
4065
4069
  // Accept "7d" / "30d" / "12h" / raw number (days).
4066
4070
  const raw = rest[++i] || '';
@@ -4392,6 +4396,78 @@ async function runResumeCli() {
4392
4396
  process.exit(0);
4393
4397
  }
4394
4398
 
4399
+ // --diff <other-id>: compare two handoffs. Shows added/removed
4400
+ // next_steps, branch shift, status change, files diff. Useful
4401
+ // for "what changed between yesterday's session and today's?".
4402
+ // Special value "--parent" diffs against the chain parent.
4403
+ if (flags.diff) {
4404
+ if (!found) {
4405
+ process.stderr.write('No source handoff available to diff against.\n');
4406
+ process.exit(3);
4407
+ }
4408
+ let diffTarget = flags.diff;
4409
+ // --diff --parent: resolve to the parent_session_id from the
4410
+ // current handoff so users can do "what's new since the
4411
+ // last checkpoint" without typing the parent's id.
4412
+ if (diffTarget === '--parent' || diffTarget === 'parent') {
4413
+ const parentId = payload.resume_handoff.handoff?.parent_session_id;
4414
+ if (!parentId) {
4415
+ process.stderr.write('This handoff has no parent_session_id to diff against.\n');
4416
+ process.exit(3);
4417
+ }
4418
+ diffTarget = parentId;
4419
+ }
4420
+ const otherRes = await server.makeRequest({
4421
+ jsonrpc: '2.0', id: `cli-diff-${Date.now()}`,
4422
+ method: 'tools/call',
4423
+ params: { name: 'get_context_bootstrap', arguments: { resume: true, session_id_hint: diffTarget } },
4424
+ });
4425
+ const otherText = otherRes?.result?.content?.[0]?.text;
4426
+ const otherPayload = JSON.parse(otherText || '{}');
4427
+ const otherHandoff = otherPayload.resume_handoff?.handoff;
4428
+ if (!otherHandoff) {
4429
+ process.stderr.write(`Could not fetch handoff "${diffTarget}" to diff against.\n`);
4430
+ process.exit(3);
4431
+ }
4432
+ const h = payload.resume_handoff.handoff;
4433
+ const setOf = (arr, key) =>
4434
+ new Set((arr || []).map(x => typeof x === 'string' ? x : x[key]).filter(Boolean));
4435
+ const diffArr = (label, a, b, key) => {
4436
+ const A = setOf(a, key); const B = setOf(b, key);
4437
+ const added = [...A].filter(x => !B.has(x));
4438
+ const removed = [...B].filter(x => !A.has(x));
4439
+ const lines = [];
4440
+ if (added.length) lines.push(` + ${label}: ${added.join(', ')}`);
4441
+ if (removed.length) lines.push(` - ${label}: ${removed.join(', ')}`);
4442
+ return lines.join('\n');
4443
+ };
4444
+
4445
+ const aShort = (payload.resume_handoff.session_id || '').slice(0, 8);
4446
+ const bShort = (otherPayload.resume_handoff.session_id || '').slice(0, 8);
4447
+ const lines = [`Diff: ${aShort} (this) vs ${bShort} (other)`, ''];
4448
+ // Scalar fields
4449
+ const scalars = ['goal', 'status', 'branch', 'repo_path'];
4450
+ for (const f of scalars) {
4451
+ if (h[f] !== otherHandoff[f]) {
4452
+ lines.push(` ~ ${f}: "${otherHandoff[f] || ''}" → "${h[f] || ''}"`);
4453
+ }
4454
+ }
4455
+ // Array fields
4456
+ const arrDiff = diffArr('next_steps', h.next_steps, otherHandoff.next_steps);
4457
+ if (arrDiff) lines.push(arrDiff);
4458
+ const blockerDiff = diffArr('blockers', h.blockers, otherHandoff.blockers);
4459
+ if (blockerDiff) lines.push(blockerDiff);
4460
+ const decisionDiff = diffArr('decisions', h.decisions, otherHandoff.decisions);
4461
+ if (decisionDiff) lines.push(decisionDiff);
4462
+ const fileDiff = diffArr('files_touched', h.files_touched, otherHandoff.files_touched, 'path');
4463
+ if (fileDiff) lines.push(fileDiff);
4464
+ const anchorDiff = diffArr('code_anchors', h.code_anchors, otherHandoff.code_anchors, 'file_path');
4465
+ if (anchorDiff) lines.push(anchorDiff);
4466
+ if (lines.length === 2) lines.push(' (no differences)');
4467
+ process.stdout.write(lines.join('\n') + '\n');
4468
+ process.exit(0);
4469
+ }
4470
+
4395
4471
  // --validate: verify the handoff's file references still exist
4396
4472
  // on disk. Resolves each path relative to the handoff's repo_path
4397
4473
  // (or cwd). Reports per-path status. Useful before resuming to
@@ -5098,6 +5174,7 @@ const PER_COMMAND_HELP = {
5098
5174
  --decisions-only decisions, one per line.
5099
5175
  --blockers-only open blockers, one per line.
5100
5176
  --chain Walk parent_session_id back; show the thread history.
5177
+ --diff <other-id> Compare this handoff against another (added/removed/changed fields).
5101
5178
  --validate Check that files_touched + code_anchors still exist locally.
5102
5179
  --raw Print just the raw handoff JSON (no envelope metadata).
5103
5180
  --age Print handoff_age_minutes (single integer).
@@ -5295,8 +5372,25 @@ async function runDoctorCli() {
5295
5372
  }
5296
5373
  }
5297
5374
 
5375
+ // Optional: handoff stats summary at the bottom (visibility into
5376
+ // how much resume context is captured for the current user).
5377
+ let handoffStats = null;
5378
+ if (API_KEY) {
5379
+ try {
5380
+ const statsRes = await server.makeRequestOnce({
5381
+ jsonrpc: '2.0', id: `doctor-stats-${Date.now()}`,
5382
+ method: 'tools/call', params: { name: 'get_stats', arguments: {} },
5383
+ });
5384
+ const statsText = statsRes?.result?.content?.[0]?.text;
5385
+ if (statsText) {
5386
+ const statsPayload = JSON.parse(statsText);
5387
+ if (statsPayload?.handoffs) handoffStats = statsPayload.handoffs;
5388
+ }
5389
+ } catch (_) { /* skip on failure */ }
5390
+ }
5391
+
5298
5392
  if (flags.json) {
5299
- process.stdout.write(JSON.stringify({ checks }, null, 2) + '\n');
5393
+ process.stdout.write(JSON.stringify({ checks, ...(handoffStats ? { handoffs: handoffStats } : {}) }, null, 2) + '\n');
5300
5394
  process.exit(checks.every(c => c.ok) ? 0 : 1);
5301
5395
  }
5302
5396
  process.stdout.write(`cf-memory-mcp v${PACKAGE_VERSION} — doctor\n\n`);
@@ -5309,6 +5403,23 @@ async function runDoctorCli() {
5309
5403
  process.stdout.write(` → ${c.fix}\n`);
5310
5404
  }
5311
5405
  }
5406
+ if (handoffStats) {
5407
+ process.stdout.write('\nHandoff stats:\n');
5408
+ process.stdout.write(` total: ${handoffStats.total}\n`);
5409
+ if (handoffStats.by_status) {
5410
+ const byStatus = Object.entries(handoffStats.by_status)
5411
+ .map(([s, n]) => `${s}=${n}`).join(', ');
5412
+ process.stdout.write(` by_status: ${byStatus}\n`);
5413
+ }
5414
+ if (handoffStats.oldest_in_progress) {
5415
+ const o = handoffStats.oldest_in_progress;
5416
+ const shortId = (o.session_id || '').slice(0, 8);
5417
+ process.stdout.write(` oldest in_progress: ${shortId} (${o.age_minutes}m ago)\n`);
5418
+ }
5419
+ if (handoffStats.by_repo && handoffStats.by_repo.length > 0) {
5420
+ process.stdout.write(` top repos: ${handoffStats.by_repo.slice(0, 3).map(r => `${r.repo_path.split('/').pop()}=${r.count}`).join(', ')}\n`);
5421
+ }
5422
+ }
5312
5423
  process.stdout.write('\n');
5313
5424
  process.stdout.write(anyFailed ? 'Some checks failed. See suggestions above.\n' : 'All checks passed.\n');
5314
5425
  process.exit(anyFailed ? 1 : 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cf-memory-mcp",
3
- "version": "3.48.0",
3
+ "version": "3.50.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": {