cf-memory-mcp 3.50.0 → 3.52.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
@@ -4090,6 +4091,10 @@ function parseCliArgs(rest) {
4090
4091
  flags.repo_path = rest[++i];
4091
4092
  } else if (a.startsWith('--repo=')) {
4092
4093
  flags.repo_path = a.slice('--repo='.length);
4094
+ } else if (a === '--project-id') {
4095
+ flags.project_id = rest[++i];
4096
+ } else if (a.startsWith('--project-id=')) {
4097
+ flags.project_id = a.slice('--project-id='.length);
4093
4098
  } else if (a === '--since') {
4094
4099
  flags.since = rest[++i];
4095
4100
  } else if (a.startsWith('--since=')) {
@@ -4120,9 +4125,20 @@ async function runResumeCli() {
4120
4125
  // Optional positional session_id argument.
4121
4126
  const sessionArg = positional[0];
4122
4127
  if (sessionArg) args.session_id_hint = sessionArg;
4123
- const fake = { params: { name: 'retrieve_context', arguments: {} } };
4124
- await server.maybeFillProjectId(fake);
4125
- if (fake.params.arguments.project_id) args.project_id = fake.params.arguments.project_id;
4128
+ // Cross-laptop overrides: --repo and --project-id let users bypass
4129
+ // cwd-based matching when paths differ across machines. --project-id
4130
+ // is the most reliable cross-machine anchor since it survives
4131
+ // path differences.
4132
+ if (flags.repo_path) {
4133
+ args.repo_path = flags.repo_path;
4134
+ // Don't auto-fill project_id from the local cwd when --repo
4135
+ // overrides (would otherwise short-circuit to a wrong session).
4136
+ } else {
4137
+ const fake = { params: { name: 'retrieve_context', arguments: {} } };
4138
+ await server.maybeFillProjectId(fake);
4139
+ if (fake.params.arguments.project_id) args.project_id = fake.params.arguments.project_id;
4140
+ }
4141
+ if (flags.project_id) args.project_id = flags.project_id;
4126
4142
 
4127
4143
  const response = await server.makeRequest({
4128
4144
  jsonrpc: '2.0',
@@ -4576,6 +4592,102 @@ async function runResumeCli() {
4576
4592
  }
4577
4593
  }
4578
4594
 
4595
+ async function runExplainCli() {
4596
+ if (!API_KEY) {
4597
+ console.error('Error: CF_MEMORY_API_KEY environment variable is required');
4598
+ process.exit(1);
4599
+ }
4600
+ const { positional, flags } = parseCliArgs(process.argv.slice(3));
4601
+ const idArg = positional[0];
4602
+ if (!idArg) {
4603
+ console.error('Usage: cf-memory-mcp explain <session-id-or-prefix>');
4604
+ process.exit(1);
4605
+ }
4606
+ const server = new CFMemoryMCP();
4607
+ server.logDebug = () => {};
4608
+ try {
4609
+ const response = await server.makeRequest({
4610
+ jsonrpc: '2.0', id: `cli-explain-${Date.now()}`,
4611
+ method: 'tools/call',
4612
+ params: { name: 'get_context_bootstrap', arguments: { resume: true, session_id_hint: idArg } },
4613
+ });
4614
+ const text = response?.result?.content?.[0]?.text;
4615
+ const payload = JSON.parse(text || '{}');
4616
+ const envelope = payload.resume_handoff;
4617
+ if (!envelope?.handoff) {
4618
+ process.stderr.write((payload.empty_hint || `No handoff found for "${idArg}".`) + '\n');
4619
+ process.exit(3);
4620
+ }
4621
+ const h = envelope.handoff;
4622
+ const ageMin = envelope.handoff_age_minutes;
4623
+ const sizeBytes = envelope.handoff_size_bytes ?? 0;
4624
+
4625
+ // Mirror the server-side rubric (see computeQualityScore).
4626
+ const components = [];
4627
+ const add = (label, points, present, detail) =>
4628
+ components.push({ label, points, present, detail });
4629
+
4630
+ add('goal present (>=5 chars)', 0.20, !!(h.goal && h.goal.length >= 5), h.goal ? `"${h.goal.slice(0,40)}"` : 'missing');
4631
+ add('next_steps present', 0.20,
4632
+ Array.isArray(h.next_steps) && h.next_steps.length > 0,
4633
+ `${h.next_steps?.length ?? 0} step${(h.next_steps?.length ?? 0) === 1 ? '' : 's'}`);
4634
+ add('code_anchors present', 0.15,
4635
+ Array.isArray(h.code_anchors) && h.code_anchors.length > 0,
4636
+ `${h.code_anchors?.length ?? 0} anchor${(h.code_anchors?.length ?? 0) === 1 ? '' : 's'}`);
4637
+ add('files_touched present', 0.10,
4638
+ Array.isArray(h.files_touched) && h.files_touched.length > 0,
4639
+ `${h.files_touched?.length ?? 0} file${(h.files_touched?.length ?? 0) === 1 ? '' : 's'}`);
4640
+ add('decisions present', 0.10,
4641
+ Array.isArray(h.decisions) && h.decisions.length > 0,
4642
+ `${h.decisions?.length ?? 0} decision${(h.decisions?.length ?? 0) === 1 ? '' : 's'}`);
4643
+
4644
+ // Recency
4645
+ let recency = 0;
4646
+ let recencyDetail = '?';
4647
+ if (ageMin !== undefined) {
4648
+ if (ageMin <= 1440) recency = 0.15;
4649
+ else if (ageMin <= 10080) recency = 0.15 * (1 - (ageMin - 1440) / (10080 - 1440));
4650
+ recencyDetail = `${ageMin}m old`;
4651
+ }
4652
+ components.push({ label: 'recency (decays past 24h)', points: 0.15, present: recency > 0, detail: `${recencyDetail} → +${recency.toFixed(3)}` });
4653
+
4654
+ // Size in target band
4655
+ let sizeBonus = 0;
4656
+ let sizeDetail;
4657
+ if (sizeBytes >= 500 && sizeBytes <= 8000) { sizeBonus = 0.10; sizeDetail = 'in target 500-8000 byte range'; }
4658
+ else if (sizeBytes >= 200 && sizeBytes <= 16000) { sizeBonus = 0.05; sizeDetail = 'in extended 200-16000 byte range'; }
4659
+ else { sizeDetail = sizeBytes < 200 ? 'too small (<200 bytes)' : 'too large (>16000 bytes)'; }
4660
+ components.push({ label: 'size sweet spot', points: 0.10, present: sizeBonus > 0, detail: `${sizeBytes} bytes — ${sizeDetail}` });
4661
+
4662
+ if (flags.json) {
4663
+ process.stdout.write(JSON.stringify({
4664
+ session_id: envelope.session_id,
4665
+ quality_score: envelope.quality_score,
4666
+ components,
4667
+ }, null, 2) + '\n');
4668
+ process.exit(0);
4669
+ }
4670
+ const shortId = (envelope.session_id || '').slice(0, 8);
4671
+ process.stdout.write(`Quality breakdown for ${shortId} (final score: ${envelope.quality_score}):\n\n`);
4672
+ let earned = 0;
4673
+ for (const c of components) {
4674
+ const mark = c.present ? '✓' : '✗';
4675
+ const earned_pts = c.present ? c.points : 0;
4676
+ earned += earned_pts;
4677
+ process.stdout.write(` ${mark} ${c.label.padEnd(32)} +${c.points.toFixed(2)} ${c.detail || ''}\n`);
4678
+ }
4679
+ process.stdout.write(`\n Total earned: ${earned.toFixed(2)} / 1.00\n`);
4680
+ const missing = components.filter(c => !c.present);
4681
+ if (missing.length > 0) {
4682
+ process.stdout.write(`\n To improve: add ${missing.map(m => m.label).join(', ')}\n`);
4683
+ }
4684
+ process.exit(0);
4685
+ } catch (err) {
4686
+ console.error('explain command failed:', err.message);
4687
+ process.exit(1);
4688
+ }
4689
+ }
4690
+
4579
4691
  async function runListCli() {
4580
4692
  if (!API_KEY) {
4581
4693
  console.error('Error: CF_MEMORY_API_KEY environment variable is required');
@@ -4603,6 +4715,8 @@ async function runListCli() {
4603
4715
  await server.maybeFillProjectId(fake);
4604
4716
  if (fake.params.arguments.project_id) args.project_id = fake.params.arguments.project_id;
4605
4717
  }
4718
+ // --project-id explicit override (overrides any auto-fill above).
4719
+ if (flags.project_id) args.project_id = flags.project_id;
4606
4720
 
4607
4721
  const response = await server.makeRequest({
4608
4722
  jsonrpc: '2.0',
@@ -4782,6 +4896,9 @@ async function runHistoryCli() {
4782
4896
  await server.maybeFillProjectId(fake);
4783
4897
  if (fake.params.arguments.project_id) args.project_id = fake.params.arguments.project_id;
4784
4898
  }
4899
+ // --project-id overrides cwd-derived project_id. Useful when
4900
+ // resuming a project you haven't indexed on this machine.
4901
+ if (flags.project_id) args.project_id = flags.project_id;
4785
4902
 
4786
4903
  const response = await server.makeRequest({
4787
4904
  jsonrpc: '2.0',
@@ -5019,7 +5136,8 @@ function runEnvCli() {
5019
5136
  function runCompletionCli() {
5020
5137
  const shell = process.argv[3] || 'bash';
5021
5138
  const install = process.argv.includes('--install');
5022
- const commands = ['resume', 'list', 'history', 'checkpoint', 'link', 'status', 'clean', 'export', 'import', 'delete', 'doctor', 'env', 'completion'];
5139
+ const uninstall = process.argv.includes('--uninstall');
5140
+ const commands = ['resume', 'list', 'history', 'checkpoint', 'link', 'explain', 'status', 'clean', 'export', 'import', 'delete', 'doctor', 'env', 'completion'];
5023
5141
  const flags = ['--json', '-j', '--limit', '-n', '--md', '--all', '--force', '-f', '--version', '-v', '--help', '-h', '--diagnose'];
5024
5142
 
5025
5143
  // Determine the install target for each shell. Use user-local paths
@@ -5047,6 +5165,27 @@ function runCompletionCli() {
5047
5165
  }
5048
5166
  return null;
5049
5167
  })();
5168
+ // --uninstall: delete the previously-installed completion file. No
5169
+ // need to generate the script. Idempotent.
5170
+ if (uninstall) {
5171
+ if (!installTarget) {
5172
+ process.stderr.write(`No uninstall target for shell ${shell}.\n`);
5173
+ process.exit(1);
5174
+ }
5175
+ try {
5176
+ if (fs.existsSync(installTarget)) {
5177
+ fs.unlinkSync(installTarget);
5178
+ process.stderr.write(`Uninstalled ${shell} completion from ${installTarget}\n`);
5179
+ } else {
5180
+ process.stderr.write(`Nothing to uninstall (no file at ${installTarget}).\n`);
5181
+ }
5182
+ process.exit(0);
5183
+ } catch (err) {
5184
+ process.stderr.write(`Failed to uninstall: ${err.message}\n`);
5185
+ process.exit(1);
5186
+ }
5187
+ }
5188
+
5050
5189
  // Buffer the output so we can either print or write-to-disk.
5051
5190
  let output = '';
5052
5191
  const emit = (chunk) => { output += chunk; };
@@ -5163,6 +5302,8 @@ const PER_COMMAND_HELP = {
5163
5302
  resume: `cf-memory-mcp resume [<session-id-prefix>] [--md path] [<extract-flag>] [--json]
5164
5303
  Print the prior resume handoff (markdown by default).
5165
5304
  <session-id-prefix> Optional: pick a specific session (>=8 char prefix or full UUID).
5305
+ --repo <path> Override cwd's repo (for cross-laptop or workspace-switch).
5306
+ --project-id <id> Override cwd's project_id (best for cross-laptop continuity).
5166
5307
  --md <path> Write the markdown to a file instead of stdout.
5167
5308
  --copy Pipe the markdown to the platform clipboard (pbcopy/xclip/wl-copy/clip).
5168
5309
  --card Compact 2-3 line status card (for terminal status widgets).
@@ -5260,6 +5401,11 @@ const PER_COMMAND_HELP = {
5260
5401
  a parent in the chain. Useful when you forgot --force on checkpoint
5261
5402
  or want to manually chain sessions across repos.
5262
5403
  --json, -j Emit a JSON status object.`,
5404
+ explain: `cf-memory-mcp explain <session-id-or-prefix> [--json]
5405
+ Break down the handoff's quality_score into its rubric components,
5406
+ showing what's earned + what's missing. Lets users see why a handoff
5407
+ scored 0.5 vs 0.9 — and how to improve it.
5408
+ --json, -j Emit the breakdown as JSON.`,
5263
5409
  };
5264
5410
 
5265
5411
  function printPerCommandHelp(cmd) {
@@ -6045,6 +6191,11 @@ if (process.argv[2] === 'link') {
6045
6191
  return;
6046
6192
  }
6047
6193
 
6194
+ if (process.argv[2] === 'explain') {
6195
+ runExplainCli();
6196
+ return;
6197
+ }
6198
+
6048
6199
  if (process.argv.includes('--diagnose')) {
6049
6200
  (async () => {
6050
6201
  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.50.0",
3
+ "version": "3.52.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": {