cf-memory-mcp 3.27.0 → 3.28.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.
@@ -3794,7 +3794,10 @@ Usage:
3794
3794
  npx cf-memory-mcp clean --all Delete ALL local disk caches
3795
3795
  npx cf-memory-mcp export <id> Print a session's handoff as JSON bundle (backup)
3796
3796
  npx cf-memory-mcp import <file> Restore a handoff bundle (cross-machine sync); use "-" for stdin
3797
+ npx cf-memory-mcp doctor Diagnose common setup issues
3797
3798
  npx cf-memory-mcp --version Show version
3799
+
3800
+ Short alias: \`cfm\` works as a drop-in for \`cf-memory-mcp\` (e.g., \`cfm resume\`).
3798
3801
  npx cf-memory-mcp --help Show this help
3799
3802
  npx cf-memory-mcp --diagnose Test connectivity and report issues
3800
3803
 
@@ -4000,6 +4003,98 @@ async function runListCli() {
4000
4003
  }
4001
4004
  }
4002
4005
 
4006
+ async function runDoctorCli() {
4007
+ const { flags } = parseCliArgs(process.argv.slice(3));
4008
+ const server = new CFMemoryMCP();
4009
+ server.logDebug = () => {};
4010
+ const checks = [];
4011
+ const add = (label, ok, detail) => checks.push({ label, ok, detail });
4012
+
4013
+ // 1. API key set?
4014
+ add('CF_MEMORY_API_KEY set', !!API_KEY, API_KEY ? '(redacted)' : 'unset — most commands will fail');
4015
+
4016
+ // 2. Cwd is in a git repo?
4017
+ const meta = server.getRepoMetadata();
4018
+ add('git repo detected', !!meta.repo_path, meta.repo_path || 'no .git in cwd; resume metadata will be empty');
4019
+ add('git branch detected', !!meta.branch, meta.branch || 'detached HEAD or no commits');
4020
+
4021
+ // 3. Disk cache writable?
4022
+ const cachePath = server.getDiskCachePath();
4023
+ let diskWritable = false;
4024
+ if (cachePath) {
4025
+ try {
4026
+ const dir = path.dirname(cachePath);
4027
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
4028
+ const probe = path.join(dir, '.doctor-probe');
4029
+ fs.writeFileSync(probe, 'ok');
4030
+ fs.unlinkSync(probe);
4031
+ diskWritable = true;
4032
+ } catch (err) {
4033
+ add('disk cache writable', false, `failed: ${err.message}`);
4034
+ }
4035
+ }
4036
+ if (diskWritable) add('disk cache writable', true, path.dirname(cachePath));
4037
+
4038
+ // 4. Worker reachable?
4039
+ if (API_KEY) {
4040
+ const t0 = Date.now();
4041
+ let workerReachable = false;
4042
+ let workerMs = null;
4043
+ try {
4044
+ const res = await server.makeRequestOnce({
4045
+ jsonrpc: '2.0', id: `doctor-${Date.now()}`,
4046
+ method: 'tools/call', params: { name: 'health_check', arguments: {} },
4047
+ });
4048
+ workerReachable = !res?.error;
4049
+ workerMs = Date.now() - t0;
4050
+ } catch (_) { /* unreachable */ }
4051
+ add('worker reachable', workerReachable, workerReachable ? `${workerMs}ms` : `failed`);
4052
+
4053
+ // 5. Project indexed?
4054
+ if (workerReachable) {
4055
+ const fake = { params: { name: 'retrieve_context', arguments: {} } };
4056
+ await server.maybeFillProjectId(fake);
4057
+ const pid = fake.params.arguments.project_id;
4058
+ add('project indexed', !!pid, pid || 'no project matched cwd — run index_project to enable retrieve_context');
4059
+
4060
+ // 6. Resume handoff available?
4061
+ if (meta.repo_path) {
4062
+ const probeArgs = { resume: true, repo_path: meta.repo_path };
4063
+ if (meta.branch) probeArgs.branch = meta.branch;
4064
+ if (pid) probeArgs.project_id = pid;
4065
+ try {
4066
+ const probeRes = await server.makeRequestOnce({
4067
+ jsonrpc: '2.0', id: `doctor-resume-${Date.now()}`,
4068
+ method: 'tools/call', params: { name: 'get_context_bootstrap', arguments: probeArgs },
4069
+ });
4070
+ const probeText = probeRes?.result?.content?.[0]?.text;
4071
+ const probePayload = JSON.parse(probeText || '{}');
4072
+ const hasHandoff = !!probePayload.resume_handoff;
4073
+ add('resume handoff available', hasHandoff,
4074
+ hasHandoff
4075
+ ? `${(probePayload.resume_handoff.session_id||'').slice(0,8)} (${probePayload.resume_handoff.handoff_age_minutes}m old)`
4076
+ : 'none for this context');
4077
+ } catch (_) { /* skip */ }
4078
+ }
4079
+ }
4080
+ }
4081
+
4082
+ if (flags.json) {
4083
+ process.stdout.write(JSON.stringify({ checks }, null, 2) + '\n');
4084
+ process.exit(checks.every(c => c.ok) ? 0 : 1);
4085
+ }
4086
+ process.stdout.write(`cf-memory-mcp v${PACKAGE_VERSION} — doctor\n\n`);
4087
+ let anyFailed = false;
4088
+ for (const c of checks) {
4089
+ const mark = c.ok ? '✓' : '✗';
4090
+ if (!c.ok) anyFailed = true;
4091
+ process.stdout.write(` ${mark} ${c.label.padEnd(28)} ${c.detail || ''}\n`);
4092
+ }
4093
+ process.stdout.write('\n');
4094
+ process.stdout.write(anyFailed ? 'Some checks failed. See details above.\n' : 'All checks passed.\n');
4095
+ process.exit(anyFailed ? 1 : 0);
4096
+ }
4097
+
4003
4098
  async function runExportCli() {
4004
4099
  if (!API_KEY) {
4005
4100
  console.error('Error: CF_MEMORY_API_KEY environment variable is required');
@@ -4288,12 +4383,61 @@ async function runCheckpointCli() {
4288
4383
  process.exit(1);
4289
4384
  }
4290
4385
  const { positional, flags } = parseCliArgs(process.argv.slice(3));
4386
+ const force = process.argv.includes('--force') || process.argv.includes('-f');
4291
4387
  const server = new CFMemoryMCP();
4292
4388
  server.logDebug = () => {};
4293
4389
  server.logError = (...a) => process.stderr.write(a.join(' ') + '\n');
4294
4390
  try {
4295
4391
  // Optional positional goal argument: `cf-memory-mcp checkpoint "<goal text>"`
4296
- const goalArg = positional.join(' ').trim();
4392
+ const goalArg = positional.filter(p => p !== '--force' && p !== '-f').join(' ').trim();
4393
+
4394
+ // Duplicate detection: before creating a new implicit session,
4395
+ // check if there's a recent in_progress handoff for the same
4396
+ // repo/branch. If so, suggest resuming it instead of churning a
4397
+ // new one. Skipped with --force.
4398
+ if (!force) {
4399
+ const meta = server.getRepoMetadata();
4400
+ if (meta.repo_path) {
4401
+ const probeArgs = { resume: true, repo_path: meta.repo_path, status_filter: 'in_progress', max_age_minutes: 60 };
4402
+ if (meta.branch) probeArgs.branch = meta.branch;
4403
+ try {
4404
+ const probeRes = await server.makeRequest({
4405
+ jsonrpc: '2.0',
4406
+ id: `cli-checkpoint-probe-${Date.now()}`,
4407
+ method: 'tools/call',
4408
+ params: { name: 'get_context_bootstrap', arguments: probeArgs },
4409
+ });
4410
+ const probeText = probeRes?.result?.content?.[0]?.text;
4411
+ const probePayload = JSON.parse(probeText || '{}');
4412
+ const recent = probePayload.resume_handoff;
4413
+ // Same-cwd implicit session won't be in the worker yet
4414
+ // unless someone called start_session — we look for
4415
+ // OTHER active threads on the same repo.
4416
+ if (recent) {
4417
+ const shortId = (recent.session_id || '').slice(0, 8);
4418
+ const ageMin = recent.handoff_age_minutes ?? '?';
4419
+ const status = recent.handoff?.status || '?';
4420
+ if (flags.json) {
4421
+ process.stdout.write(JSON.stringify({
4422
+ duplicate_detected: true,
4423
+ existing_session_id: recent.session_id,
4424
+ existing_short_id: shortId,
4425
+ existing_age_minutes: ageMin,
4426
+ existing_status: status,
4427
+ existing_goal: recent.handoff?.goal,
4428
+ hint: `Resume the existing in_progress session instead: cf-memory-mcp resume ${shortId}. Re-run with --force to create a new session anyway.`,
4429
+ }, null, 2) + '\n');
4430
+ process.exit(4);
4431
+ }
4432
+ process.stderr.write(`There's already an in_progress handoff for this repo:\n`);
4433
+ process.stderr.write(` ${shortId} ${status} ${ageMin}m ago — "${recent.handoff?.goal || '(no goal)'}"\n`);
4434
+ process.stderr.write(`Resume it: cf-memory-mcp resume ${shortId}\n`);
4435
+ process.stderr.write(`Or re-run with --force to create a new session anyway.\n`);
4436
+ process.exit(4);
4437
+ }
4438
+ } catch (_) { /* probe failure is non-fatal */ }
4439
+ }
4440
+ }
4297
4441
  const meta = server.getRepoMetadata();
4298
4442
  // Need a session to attach the handoff to. Use or create the
4299
4443
  // implicit session for this cwd (creates a new one if no implicit).
@@ -4389,6 +4533,11 @@ if (process.argv[2] === 'import') {
4389
4533
  return;
4390
4534
  }
4391
4535
 
4536
+ if (process.argv[2] === 'doctor') {
4537
+ runDoctorCli();
4538
+ return;
4539
+ }
4540
+
4392
4541
  if (process.argv.includes('--diagnose')) {
4393
4542
  (async () => {
4394
4543
  console.log(`CF Memory MCP v${PACKAGE_VERSION} - Diagnostics`);
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "cf-memory-mcp",
3
- "version": "3.27.0",
3
+ "version": "3.28.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": {
7
7
  "cf-memory-mcp": "bin/cf-memory-mcp.js",
8
+ "cfm": "bin/cf-memory-mcp.js",
8
9
  "cf-memory-index": "bin/cf-memory-mcp-indexer.js",
9
10
  "cf-memory-watch": "bin/cf-memory-mcp-indexer.js"
10
11
  },