cf-memory-mcp 3.27.0 → 3.29.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.
@@ -3778,7 +3778,12 @@ if (process.argv.includes('--version') || process.argv.includes('-v')) {
3778
3778
  process.exit(0);
3779
3779
  }
3780
3780
 
3781
- if (process.argv.includes('--help') || process.argv.includes('-h')) {
3781
+ // Global --help only when no subcommand is present. With a subcommand, fall
3782
+ // through to the per-command help dispatch below.
3783
+ const SUBCOMMANDS = new Set(['resume', 'list', 'checkpoint', 'status', 'clean', 'export', 'import', 'doctor', 'completion']);
3784
+ const hasSubcommand = process.argv[2] && SUBCOMMANDS.has(process.argv[2]);
3785
+
3786
+ if (!hasSubcommand && (process.argv.includes('--help') || process.argv.includes('-h'))) {
3782
3787
  console.log(`
3783
3788
  CF Memory MCP v${PACKAGE_VERSION}
3784
3789
 
@@ -3794,7 +3799,12 @@ Usage:
3794
3799
  npx cf-memory-mcp clean --all Delete ALL local disk caches
3795
3800
  npx cf-memory-mcp export <id> Print a session's handoff as JSON bundle (backup)
3796
3801
  npx cf-memory-mcp import <file> Restore a handoff bundle (cross-machine sync); use "-" for stdin
3802
+ npx cf-memory-mcp doctor Diagnose common setup issues
3803
+ npx cf-memory-mcp completion bash Output shell completion script (bash|zsh|fish)
3804
+ npx cf-memory-mcp <cmd> --help Show flags for a specific command
3797
3805
  npx cf-memory-mcp --version Show version
3806
+
3807
+ Short alias: \`cfm\` works as a drop-in for \`cf-memory-mcp\` (e.g., \`cfm resume\`).
3798
3808
  npx cf-memory-mcp --help Show this help
3799
3809
  npx cf-memory-mcp --diagnose Test connectivity and report issues
3800
3810
 
@@ -4000,6 +4010,234 @@ async function runListCli() {
4000
4010
  }
4001
4011
  }
4002
4012
 
4013
+ function runCompletionCli() {
4014
+ const shell = process.argv[3] || 'bash';
4015
+ const commands = ['resume', 'list', 'checkpoint', 'status', 'clean', 'export', 'import', 'doctor', 'completion'];
4016
+ const flags = ['--json', '-j', '--limit', '-n', '--md', '--all', '--force', '-f', '--version', '-v', '--help', '-h', '--diagnose'];
4017
+ if (shell === 'bash') {
4018
+ process.stdout.write(`# cf-memory-mcp bash completion
4019
+ # Install: cf-memory-mcp completion bash > /usr/local/etc/bash_completion.d/cf-memory-mcp
4020
+ # Or for the current shell: source <(cf-memory-mcp completion bash)
4021
+ _cf_memory_mcp_complete() {
4022
+ local cur prev words
4023
+ COMPREPLY=()
4024
+ cur="\${COMP_WORDS[COMP_CWORD]}"
4025
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
4026
+ if [ "\$COMP_CWORD" -eq 1 ]; then
4027
+ COMPREPLY=( $(compgen -W "${commands.join(' ')}" -- "\$cur") )
4028
+ return 0
4029
+ fi
4030
+ case "\$cur" in
4031
+ --*)
4032
+ COMPREPLY=( $(compgen -W "${flags.filter(f => f.startsWith('--')).join(' ')}" -- "\$cur") )
4033
+ ;;
4034
+ esac
4035
+ }
4036
+ complete -F _cf_memory_mcp_complete cf-memory-mcp
4037
+ complete -F _cf_memory_mcp_complete cfm
4038
+ `);
4039
+ } else if (shell === 'zsh') {
4040
+ process.stdout.write(`# cf-memory-mcp zsh completion
4041
+ # Install: cf-memory-mcp completion zsh > "\${fpath[1]}/_cf-memory-mcp" && compinit
4042
+ # Or for the current shell: source <(cf-memory-mcp completion zsh)
4043
+ #compdef cf-memory-mcp cfm
4044
+ _cf_memory_mcp() {
4045
+ local -a commands
4046
+ commands=(
4047
+ ${commands.map(c => `'${c}:${c} command'`).join('\n ')}
4048
+ )
4049
+ if (( CURRENT == 2 )); then
4050
+ _describe 'command' commands
4051
+ else
4052
+ case "\$words[2]" in
4053
+ resume|export) _files ;;
4054
+ import) _files -g '*.json' ;;
4055
+ esac
4056
+ fi
4057
+ }
4058
+ _cf_memory_mcp "\$@"
4059
+ `);
4060
+ } else if (shell === 'fish') {
4061
+ process.stdout.write(`# cf-memory-mcp fish completion
4062
+ # Install: cf-memory-mcp completion fish > ~/.config/fish/completions/cf-memory-mcp.fish
4063
+ ${commands.map(c => `complete -c cf-memory-mcp -n '__fish_use_subcommand' -a '${c}' -d '${c} command'`).join('\n')}
4064
+ ${commands.map(c => `complete -c cfm -n '__fish_use_subcommand' -a '${c}' -d '${c} command'`).join('\n')}
4065
+ complete -c cf-memory-mcp -l json -s j -d 'Emit JSON for scripts'
4066
+ complete -c cf-memory-mcp -l limit -s n -d 'Limit number of results'
4067
+ complete -c cf-memory-mcp -l md -d 'Write markdown to file'
4068
+ complete -c cf-memory-mcp -l force -s f -d 'Bypass duplicate detection'
4069
+ complete -c cf-memory-mcp -l help -s h -d 'Show help'
4070
+ complete -c cf-memory-mcp -l version -s v -d 'Show version'
4071
+ `);
4072
+ } else {
4073
+ process.stderr.write(`Unknown shell "${shell}". Supported: bash, zsh, fish.\n`);
4074
+ process.exit(1);
4075
+ }
4076
+ process.exit(0);
4077
+ }
4078
+
4079
+ // Per-command help texts. Used when "cf-memory-mcp <cmd> --help" is invoked.
4080
+ const PER_COMMAND_HELP = {
4081
+ resume: `cf-memory-mcp resume [<session-id-prefix>] [--md path] [--json]
4082
+ Print the prior resume handoff (markdown by default).
4083
+ <session-id-prefix> Optional: pick a specific session (>=8 char prefix or full UUID).
4084
+ --md <path> Write the markdown to a file instead of stdout.
4085
+ --json, -j Emit the full bootstrap payload as JSON for scripts.
4086
+ Exit codes: 0 = handoff found, 3 = no handoff found.`,
4087
+ list: `cf-memory-mcp list [--limit N] [--json]
4088
+ List recent handoffs for the current cwd, status-ranked.
4089
+ --limit N, -n N Max number of entries (default 5, max 50).
4090
+ --json, -j Emit a JSON array for scripts.`,
4091
+ checkpoint: `cf-memory-mcp checkpoint ["<goal>"] [--force] [--json]
4092
+ Snapshot current state to a fresh handoff with keep_open:true. The
4093
+ session stays active for future checkpoints. The final end_session (or
4094
+ bridge shutdown) finalizes.
4095
+ <goal> Explicit goal string. Defaults to synthesized.
4096
+ --force, -f Bypass duplicate-session detection.
4097
+ --json, -j Emit a JSON status object.
4098
+ Exit codes: 0 = saved, 2 = not stored, 4 = duplicate detected.`,
4099
+ status: `cf-memory-mcp status [--json]
4100
+ Show bridge + server state for the current cwd: version, repo/branch,
4101
+ disk-cache age, server resume availability, and recent-handoff count.
4102
+ --json, -j Emit a JSON status object.`,
4103
+ clean: `cf-memory-mcp clean [--all] [--json]
4104
+ Delete the local disk cache for the current cwd.
4105
+ --all Delete ALL cf-memory disk caches.
4106
+ --json, -j Emit a JSON list of removed paths.`,
4107
+ export: `cf-memory-mcp export <session-id-or-prefix> [--md path]
4108
+ Print a handoff as a JSON bundle to stdout (or write to file). For
4109
+ backup or cross-machine sync.
4110
+ <session-id> Full UUID or short prefix (>=8 chars).
4111
+ --md <path> Write the JSON to a file.`,
4112
+ import: `cf-memory-mcp import [<file>|-] [--json]
4113
+ Restore a handoff bundle into a NEW session (keep_open). Cross-machine
4114
+ sync. Use "-" to read from stdin.
4115
+ <file> Bundle file path; "-" for stdin.
4116
+ --json, -j Emit a JSON status object.`,
4117
+ doctor: `cf-memory-mcp doctor [--json]
4118
+ Diagnose common setup issues. Checks API key, git repo/branch, disk-
4119
+ cache writability, worker reachability, project indexing, resume
4120
+ availability. Exit nonzero if any check fails.
4121
+ --json, -j Emit a JSON list of checks.`,
4122
+ completion: `cf-memory-mcp completion [bash|zsh|fish]
4123
+ Output shell completion script. Pipe to your shell's completion dir.`,
4124
+ };
4125
+
4126
+ function printPerCommandHelp(cmd) {
4127
+ const help = PER_COMMAND_HELP[cmd];
4128
+ if (help) {
4129
+ process.stdout.write(help + '\n');
4130
+ process.exit(0);
4131
+ }
4132
+ }
4133
+
4134
+ async function runDoctorCli() {
4135
+ const { flags } = parseCliArgs(process.argv.slice(3));
4136
+ const server = new CFMemoryMCP();
4137
+ server.logDebug = () => {};
4138
+ const checks = [];
4139
+ const add = (label, ok, detail, fix) => checks.push({ label, ok, detail, ...(fix && !ok ? { fix } : {}) });
4140
+
4141
+ // 1. API key set?
4142
+ add('CF_MEMORY_API_KEY set', !!API_KEY,
4143
+ API_KEY ? '(redacted)' : 'unset — most commands will fail',
4144
+ 'export CF_MEMORY_API_KEY=<your-key> # get one at https://memcp.ai');
4145
+
4146
+ // 2. Cwd is in a git repo?
4147
+ const meta = server.getRepoMetadata();
4148
+ add('git repo detected', !!meta.repo_path,
4149
+ meta.repo_path || 'no .git in cwd; resume metadata will be empty',
4150
+ 'cd into a repo, or run: git init');
4151
+ add('git branch detected', !!meta.branch,
4152
+ meta.branch || 'detached HEAD or no commits',
4153
+ meta.repo_path ? 'git checkout -b main # or commit something first' : null);
4154
+
4155
+ // 3. Disk cache writable?
4156
+ const cachePath = server.getDiskCachePath();
4157
+ let diskWritable = false;
4158
+ if (cachePath) {
4159
+ try {
4160
+ const dir = path.dirname(cachePath);
4161
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
4162
+ const probe = path.join(dir, '.doctor-probe');
4163
+ fs.writeFileSync(probe, 'ok');
4164
+ fs.unlinkSync(probe);
4165
+ diskWritable = true;
4166
+ } catch (err) {
4167
+ add('disk cache writable', false, `failed: ${err.message}`,
4168
+ `chmod -R u+w ${path.dirname(cachePath)} # or set CF_MEMORY_DISK_CACHE=off to disable`);
4169
+ }
4170
+ }
4171
+ if (diskWritable) add('disk cache writable', true, path.dirname(cachePath));
4172
+
4173
+ // 4. Worker reachable?
4174
+ if (API_KEY) {
4175
+ const t0 = Date.now();
4176
+ let workerReachable = false;
4177
+ let workerMs = null;
4178
+ try {
4179
+ const res = await server.makeRequestOnce({
4180
+ jsonrpc: '2.0', id: `doctor-${Date.now()}`,
4181
+ method: 'tools/call', params: { name: 'health_check', arguments: {} },
4182
+ });
4183
+ workerReachable = !res?.error;
4184
+ workerMs = Date.now() - t0;
4185
+ } catch (_) { /* unreachable */ }
4186
+ add('worker reachable', workerReachable,
4187
+ workerReachable ? `${workerMs}ms` : `failed`,
4188
+ `curl -I ${BASE_URL}/health # check the worker URL and your network`);
4189
+
4190
+ // 5. Project indexed?
4191
+ if (workerReachable) {
4192
+ const fake = { params: { name: 'retrieve_context', arguments: {} } };
4193
+ await server.maybeFillProjectId(fake);
4194
+ const pid = fake.params.arguments.project_id;
4195
+ add('project indexed', !!pid,
4196
+ pid || 'no project matched cwd — run index_project to enable retrieve_context',
4197
+ 'In your MCP client: call index_project({project_path:"' + (meta.repo_path || process.cwd()) + '"})');
4198
+
4199
+ // 6. Resume handoff available?
4200
+ if (meta.repo_path) {
4201
+ const probeArgs = { resume: true, repo_path: meta.repo_path };
4202
+ if (meta.branch) probeArgs.branch = meta.branch;
4203
+ if (pid) probeArgs.project_id = pid;
4204
+ try {
4205
+ const probeRes = await server.makeRequestOnce({
4206
+ jsonrpc: '2.0', id: `doctor-resume-${Date.now()}`,
4207
+ method: 'tools/call', params: { name: 'get_context_bootstrap', arguments: probeArgs },
4208
+ });
4209
+ const probeText = probeRes?.result?.content?.[0]?.text;
4210
+ const probePayload = JSON.parse(probeText || '{}');
4211
+ const hasHandoff = !!probePayload.resume_handoff;
4212
+ add('resume handoff available', hasHandoff,
4213
+ hasHandoff
4214
+ ? `${(probePayload.resume_handoff.session_id||'').slice(0,8)} (${probePayload.resume_handoff.handoff_age_minutes}m old)`
4215
+ : 'none for this context',
4216
+ 'cf-memory-mcp checkpoint "what you are working on" # or end_session({handoff:...}) in your MCP client');
4217
+ } catch (_) { /* skip */ }
4218
+ }
4219
+ }
4220
+ }
4221
+
4222
+ if (flags.json) {
4223
+ process.stdout.write(JSON.stringify({ checks }, null, 2) + '\n');
4224
+ process.exit(checks.every(c => c.ok) ? 0 : 1);
4225
+ }
4226
+ process.stdout.write(`cf-memory-mcp v${PACKAGE_VERSION} — doctor\n\n`);
4227
+ let anyFailed = false;
4228
+ for (const c of checks) {
4229
+ const mark = c.ok ? '✓' : '✗';
4230
+ if (!c.ok) anyFailed = true;
4231
+ process.stdout.write(` ${mark} ${c.label.padEnd(28)} ${c.detail || ''}\n`);
4232
+ if (!c.ok && c.fix) {
4233
+ process.stdout.write(` → ${c.fix}\n`);
4234
+ }
4235
+ }
4236
+ process.stdout.write('\n');
4237
+ process.stdout.write(anyFailed ? 'Some checks failed. See suggestions above.\n' : 'All checks passed.\n');
4238
+ process.exit(anyFailed ? 1 : 0);
4239
+ }
4240
+
4003
4241
  async function runExportCli() {
4004
4242
  if (!API_KEY) {
4005
4243
  console.error('Error: CF_MEMORY_API_KEY environment variable is required');
@@ -4288,12 +4526,61 @@ async function runCheckpointCli() {
4288
4526
  process.exit(1);
4289
4527
  }
4290
4528
  const { positional, flags } = parseCliArgs(process.argv.slice(3));
4529
+ const force = process.argv.includes('--force') || process.argv.includes('-f');
4291
4530
  const server = new CFMemoryMCP();
4292
4531
  server.logDebug = () => {};
4293
4532
  server.logError = (...a) => process.stderr.write(a.join(' ') + '\n');
4294
4533
  try {
4295
4534
  // Optional positional goal argument: `cf-memory-mcp checkpoint "<goal text>"`
4296
- const goalArg = positional.join(' ').trim();
4535
+ const goalArg = positional.filter(p => p !== '--force' && p !== '-f').join(' ').trim();
4536
+
4537
+ // Duplicate detection: before creating a new implicit session,
4538
+ // check if there's a recent in_progress handoff for the same
4539
+ // repo/branch. If so, suggest resuming it instead of churning a
4540
+ // new one. Skipped with --force.
4541
+ if (!force) {
4542
+ const meta = server.getRepoMetadata();
4543
+ if (meta.repo_path) {
4544
+ const probeArgs = { resume: true, repo_path: meta.repo_path, status_filter: 'in_progress', max_age_minutes: 60 };
4545
+ if (meta.branch) probeArgs.branch = meta.branch;
4546
+ try {
4547
+ const probeRes = await server.makeRequest({
4548
+ jsonrpc: '2.0',
4549
+ id: `cli-checkpoint-probe-${Date.now()}`,
4550
+ method: 'tools/call',
4551
+ params: { name: 'get_context_bootstrap', arguments: probeArgs },
4552
+ });
4553
+ const probeText = probeRes?.result?.content?.[0]?.text;
4554
+ const probePayload = JSON.parse(probeText || '{}');
4555
+ const recent = probePayload.resume_handoff;
4556
+ // Same-cwd implicit session won't be in the worker yet
4557
+ // unless someone called start_session — we look for
4558
+ // OTHER active threads on the same repo.
4559
+ if (recent) {
4560
+ const shortId = (recent.session_id || '').slice(0, 8);
4561
+ const ageMin = recent.handoff_age_minutes ?? '?';
4562
+ const status = recent.handoff?.status || '?';
4563
+ if (flags.json) {
4564
+ process.stdout.write(JSON.stringify({
4565
+ duplicate_detected: true,
4566
+ existing_session_id: recent.session_id,
4567
+ existing_short_id: shortId,
4568
+ existing_age_minutes: ageMin,
4569
+ existing_status: status,
4570
+ existing_goal: recent.handoff?.goal,
4571
+ hint: `Resume the existing in_progress session instead: cf-memory-mcp resume ${shortId}. Re-run with --force to create a new session anyway.`,
4572
+ }, null, 2) + '\n');
4573
+ process.exit(4);
4574
+ }
4575
+ process.stderr.write(`There's already an in_progress handoff for this repo:\n`);
4576
+ process.stderr.write(` ${shortId} ${status} ${ageMin}m ago — "${recent.handoff?.goal || '(no goal)'}"\n`);
4577
+ process.stderr.write(`Resume it: cf-memory-mcp resume ${shortId}\n`);
4578
+ process.stderr.write(`Or re-run with --force to create a new session anyway.\n`);
4579
+ process.exit(4);
4580
+ }
4581
+ } catch (_) { /* probe failure is non-fatal */ }
4582
+ }
4583
+ }
4297
4584
  const meta = server.getRepoMetadata();
4298
4585
  // Need a session to attach the handoff to. Use or create the
4299
4586
  // implicit session for this cwd (creates a new one if no implicit).
@@ -4354,6 +4641,13 @@ async function runCheckpointCli() {
4354
4641
  }
4355
4642
  }
4356
4643
 
4644
+ // Per-command --help: intercept BEFORE any subcommand dispatch so the
4645
+ // dispatched function doesn't see --help as a positional arg.
4646
+ if (process.argv[2] && process.argv.slice(3).some(a => a === '--help' || a === '-h')) {
4647
+ printPerCommandHelp(process.argv[2]);
4648
+ // If not a known command, fall through to general help.
4649
+ }
4650
+
4357
4651
  if (process.argv[2] === 'resume') {
4358
4652
  runResumeCli();
4359
4653
  return;
@@ -4389,6 +4683,16 @@ if (process.argv[2] === 'import') {
4389
4683
  return;
4390
4684
  }
4391
4685
 
4686
+ if (process.argv[2] === 'doctor') {
4687
+ runDoctorCli();
4688
+ return;
4689
+ }
4690
+
4691
+ if (process.argv[2] === 'completion') {
4692
+ runCompletionCli();
4693
+ return;
4694
+ }
4695
+
4392
4696
  if (process.argv.includes('--diagnose')) {
4393
4697
  (async () => {
4394
4698
  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.29.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
  },