cf-memory-mcp 3.43.0 → 3.45.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.
@@ -3967,6 +3967,12 @@ function parseCliArgs(rest) {
3967
3967
  flags.copy = true;
3968
3968
  } else if (a === '--card') {
3969
3969
  flags.card = true;
3970
+ } else if (a === '--short') {
3971
+ flags.short = true;
3972
+ } else if (a === '--age') {
3973
+ flags.age = true;
3974
+ } else if (a === '--quality') {
3975
+ flags.quality = true;
3970
3976
  } else if (a === '--older-than') {
3971
3977
  // Accept "7d" / "30d" / "12h" / raw number (days).
3972
3978
  const raw = rest[++i] || '';
@@ -4129,6 +4135,35 @@ async function runResumeCli() {
4129
4135
  process.exit(0);
4130
4136
  }
4131
4137
 
4138
+ // --age: print just handoff_age_minutes (single integer).
4139
+ if (flags.age) {
4140
+ const ageMin = payload.resume_handoff?.handoff_age_minutes;
4141
+ if (typeof ageMin !== 'number') process.exit(3);
4142
+ process.stdout.write(String(ageMin) + '\n');
4143
+ process.exit(0);
4144
+ }
4145
+
4146
+ // --quality: print just quality_score (single float).
4147
+ if (flags.quality) {
4148
+ const q = payload.resume_handoff?.quality_score;
4149
+ if (typeof q !== 'number') process.exit(3);
4150
+ process.stdout.write(String(q) + '\n');
4151
+ process.exit(0);
4152
+ }
4153
+
4154
+ // --short: single line for tmux/status bars. "goal · status"
4155
+ // truncated to terminal-friendly length.
4156
+ if (flags.short) {
4157
+ const envelope = payload.resume_handoff;
4158
+ if (!envelope) process.exit(3);
4159
+ const h = envelope.handoff || {};
4160
+ const goal = (h.goal || '(no goal)').slice(0, 60);
4161
+ const status = h.status || '?';
4162
+ const stale = envelope.stale ? ' [STALE]' : '';
4163
+ process.stdout.write(`${goal} · ${status}${stale}\n`);
4164
+ process.exit(0);
4165
+ }
4166
+
4132
4167
  // --card: compact 2-3 line summary card for status widgets.
4133
4168
  // Format: [status · age · q=score] goal / → next_step
4134
4169
  if (flags.card) {
@@ -4750,10 +4785,41 @@ function runEnvCli() {
4750
4785
 
4751
4786
  function runCompletionCli() {
4752
4787
  const shell = process.argv[3] || 'bash';
4788
+ const install = process.argv.includes('--install');
4753
4789
  const commands = ['resume', 'list', 'history', 'checkpoint', 'link', 'status', 'clean', 'export', 'import', 'delete', 'doctor', 'env', 'completion'];
4754
4790
  const flags = ['--json', '-j', '--limit', '-n', '--md', '--all', '--force', '-f', '--version', '-v', '--help', '-h', '--diagnose'];
4791
+
4792
+ // Determine the install target for each shell. Use user-local paths
4793
+ // only (no sudo). Caller can override via stdout-redirect when
4794
+ // --install isn't used.
4795
+ const installTarget = (() => {
4796
+ const home = os.homedir();
4797
+ if (shell === 'bash') {
4798
+ // Prefer ~/.bash_completion.d/ if it exists; fall back to ~/.bash_completion.
4799
+ const dir = path.join(home, '.bash_completion.d');
4800
+ return fs.existsSync(dir) ? path.join(dir, 'cf-memory-mcp') : path.join(home, '.bash_completion');
4801
+ }
4802
+ if (shell === 'zsh') {
4803
+ // Honor $ZSH_CUSTOM/plugins or ~/.zsh/completions/
4804
+ const zdir = process.env.ZSH_CUSTOM
4805
+ ? path.join(process.env.ZSH_CUSTOM, 'plugins', 'cf-memory-mcp')
4806
+ : path.join(home, '.zsh', 'completions');
4807
+ return path.join(zdir, '_cf-memory-mcp');
4808
+ }
4809
+ if (shell === 'fish') {
4810
+ return path.join(home, '.config', 'fish', 'completions', 'cf-memory-mcp.fish');
4811
+ }
4812
+ if (shell === 'powershell' || shell === 'pwsh') {
4813
+ return path.join(home, '.config', 'powershell', 'cf-memory-mcp.ps1');
4814
+ }
4815
+ return null;
4816
+ })();
4817
+ // Buffer the output so we can either print or write-to-disk.
4818
+ let output = '';
4819
+ const emit = (chunk) => { output += chunk; };
4820
+
4755
4821
  if (shell === 'bash') {
4756
- process.stdout.write(`# cf-memory-mcp bash completion
4822
+ emit(`# cf-memory-mcp bash completion
4757
4823
  # Install: cf-memory-mcp completion bash > /usr/local/etc/bash_completion.d/cf-memory-mcp
4758
4824
  # Or for the current shell: source <(cf-memory-mcp completion bash)
4759
4825
  _cf_memory_mcp_complete() {
@@ -4775,7 +4841,7 @@ complete -F _cf_memory_mcp_complete cf-memory-mcp
4775
4841
  complete -F _cf_memory_mcp_complete cfm
4776
4842
  `);
4777
4843
  } else if (shell === 'zsh') {
4778
- process.stdout.write(`# cf-memory-mcp zsh completion
4844
+ emit(`# cf-memory-mcp zsh completion
4779
4845
  # Install: cf-memory-mcp completion zsh > "\${fpath[1]}/_cf-memory-mcp" && compinit
4780
4846
  # Or for the current shell: source <(cf-memory-mcp completion zsh)
4781
4847
  #compdef cf-memory-mcp cfm
@@ -4796,7 +4862,7 @@ _cf_memory_mcp() {
4796
4862
  _cf_memory_mcp "\$@"
4797
4863
  `);
4798
4864
  } else if (shell === 'powershell' || shell === 'pwsh') {
4799
- process.stdout.write(`# cf-memory-mcp PowerShell completion
4865
+ emit(`# cf-memory-mcp PowerShell completion
4800
4866
  # Install: cf-memory-mcp completion powershell | Out-String | Invoke-Expression
4801
4867
  # To persist: add the above to your $PROFILE
4802
4868
  Register-ArgumentCompleter -Native -CommandName cf-memory-mcp,cfm -ScriptBlock {
@@ -4816,7 +4882,7 @@ Register-ArgumentCompleter -Native -CommandName cf-memory-mcp,cfm -ScriptBlock {
4816
4882
  }
4817
4883
  `);
4818
4884
  } else if (shell === 'fish') {
4819
- process.stdout.write(`# cf-memory-mcp fish completion
4885
+ emit(`# cf-memory-mcp fish completion
4820
4886
  # Install: cf-memory-mcp completion fish > ~/.config/fish/completions/cf-memory-mcp.fish
4821
4887
  ${commands.map(c => `complete -c cf-memory-mcp -n '__fish_use_subcommand' -a '${c}' -d '${c} command'`).join('\n')}
4822
4888
  ${commands.map(c => `complete -c cfm -n '__fish_use_subcommand' -a '${c}' -d '${c} command'`).join('\n')}
@@ -4831,6 +4897,31 @@ complete -c cf-memory-mcp -l version -s v -d 'Show version'
4831
4897
  process.stderr.write(`Unknown shell "${shell}". Supported: bash, zsh, fish, powershell.\n`);
4832
4898
  process.exit(1);
4833
4899
  }
4900
+
4901
+ // Either install to a user-local path or print to stdout.
4902
+ if (install) {
4903
+ if (!installTarget) {
4904
+ process.stderr.write(`No install target for shell ${shell}.\n`);
4905
+ process.exit(1);
4906
+ }
4907
+ try {
4908
+ const dir = path.dirname(installTarget);
4909
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
4910
+ fs.writeFileSync(installTarget, output);
4911
+ process.stderr.write(`Installed ${shell} completion to ${installTarget}\n`);
4912
+ if (shell === 'bash') {
4913
+ process.stderr.write(`Add to ~/.bashrc to autoload: source ${installTarget}\n`);
4914
+ } else if (shell === 'zsh') {
4915
+ const dirToFpath = path.dirname(installTarget);
4916
+ process.stderr.write(`Add to ~/.zshrc:\n fpath=(${dirToFpath} $fpath)\n autoload -Uz compinit && compinit\n`);
4917
+ }
4918
+ } catch (err) {
4919
+ process.stderr.write(`Failed to install: ${err.message}\n`);
4920
+ process.exit(1);
4921
+ }
4922
+ } else {
4923
+ process.stdout.write(output);
4924
+ }
4834
4925
  process.exit(0);
4835
4926
  }
4836
4927
 
@@ -4852,6 +4943,8 @@ const PER_COMMAND_HELP = {
4852
4943
  --chain Walk parent_session_id back; show the thread history.
4853
4944
  --validate Check that files_touched + code_anchors still exist locally.
4854
4945
  --raw Print just the raw handoff JSON (no envelope metadata).
4946
+ --age Print handoff_age_minutes (single integer).
4947
+ --quality Print quality_score (single float, 0-1).
4855
4948
  --json, -j Full bootstrap payload as JSON.
4856
4949
  Exit codes: 0 = found, 3 = no handoff / no data, 4 = --validate found missing files.`,
4857
4950
  list: `cf-memory-mcp list [--status S] [--since ISO] [--repo PATH] [--limit N] [--json]
@@ -4879,10 +4972,13 @@ const PER_COMMAND_HELP = {
4879
4972
  --all Delete ALL cf-memory disk caches.
4880
4973
  --json, -j Emit a JSON list of removed paths.`,
4881
4974
  export: `cf-memory-mcp export <session-id-or-prefix> [--md path]
4975
+ cf-memory-mcp export --all
4882
4976
  Print a handoff as a JSON bundle to stdout (or write to file). For
4883
- backup or cross-machine sync.
4977
+ backup or cross-machine sync. With --all, streams ALL handoffs as
4978
+ NDJSON (one bundle per line) suitable for piping to a file.
4884
4979
  <session-id> Full UUID or short prefix (>=8 chars).
4885
- --md <path> Write the JSON to a file.`,
4980
+ --md <path> Write the JSON to a file (single-handoff mode).
4981
+ --all Stream all handoffs as NDJSON.`,
4886
4982
  import: `cf-memory-mcp import [<file>|-] [--json]
4887
4983
  Restore a handoff bundle into a NEW session (keep_open). Cross-machine
4888
4984
  sync. Use "-" to read from stdin.
@@ -5060,12 +5156,91 @@ async function runExportCli() {
5060
5156
  }
5061
5157
  const { positional, flags } = parseCliArgs(process.argv.slice(3));
5062
5158
  const sessionArg = positional[0];
5063
- if (!sessionArg) {
5064
- console.error('Usage: cf-memory-mcp export <session-id-or-prefix> [--md path]');
5159
+ const exportAll = process.argv.includes('--all');
5160
+ if (!sessionArg && !exportAll) {
5161
+ console.error('Usage:');
5162
+ console.error(' cf-memory-mcp export <session-id-or-prefix> # single handoff bundle');
5163
+ console.error(' cf-memory-mcp export --all # all handoffs as NDJSON');
5065
5164
  process.exit(1);
5066
5165
  }
5067
5166
  const server = new CFMemoryMCP();
5068
5167
  server.logDebug = () => {};
5168
+
5169
+ // --all: stream every handoff as NDJSON (one bundle per line).
5170
+ if (exportAll) {
5171
+ // EPIPE handler: when piping to `head` etc., downstream closes
5172
+ // stdout while we're still writing. Exit cleanly instead of
5173
+ // dumping a Node stack trace.
5174
+ process.stdout.on('error', (err) => {
5175
+ if (err.code === 'EPIPE') process.exit(0);
5176
+ throw err;
5177
+ });
5178
+ try {
5179
+ // Page through all the user's handoffs via get_context_bootstrap
5180
+ // with recent_handoff_limit=50, sorted by recency. Use `since`
5181
+ // to walk back in time without dupes — query for entries before
5182
+ // the oldest one in the previous batch.
5183
+ const seen = new Set();
5184
+ let cursor = null; // ISO timestamp; entries with ended_at|started_at >= cursor are returned
5185
+ let totalEmitted = 0;
5186
+ for (let page = 0; page < 100; page++) {
5187
+ const args = { resume: true, recent_handoff_limit: 50 };
5188
+ const meta = server.getRepoMetadata();
5189
+ if (meta.repo_path) args.repo_path = meta.repo_path;
5190
+ if (cursor) args.since = cursor;
5191
+ const res = await server.makeRequest({
5192
+ jsonrpc: '2.0', id: `cli-export-all-${page}-${Date.now()}`,
5193
+ method: 'tools/call',
5194
+ params: { name: 'get_context_bootstrap', arguments: args },
5195
+ });
5196
+ const text = res?.result?.content?.[0]?.text;
5197
+ const payload = JSON.parse(text || '{}');
5198
+ const handoffs = payload.recent_handoffs || [];
5199
+ if (handoffs.length === 0) break;
5200
+ let newCount = 0;
5201
+ let oldestSeen = null;
5202
+ for (const h of handoffs) {
5203
+ if (seen.has(h.session_id)) continue;
5204
+ seen.add(h.session_id);
5205
+ // Fetch the full handoff (not just the summary).
5206
+ const fullRes = await server.makeRequest({
5207
+ jsonrpc: '2.0', id: `cli-export-all-detail-${Date.now()}`,
5208
+ method: 'tools/call',
5209
+ params: { name: 'get_context_bootstrap', arguments: { resume: true, session_id_hint: h.session_id } },
5210
+ });
5211
+ const fullText = fullRes?.result?.content?.[0]?.text;
5212
+ const fullPayload = JSON.parse(fullText || '{}');
5213
+ const envelope = fullPayload.resume_handoff;
5214
+ if (!envelope) continue;
5215
+ const bundle = {
5216
+ kind: 'cf-memory-handoff',
5217
+ version: 1,
5218
+ exported_at: new Date().toISOString(),
5219
+ session_id: envelope.session_id,
5220
+ started_at: envelope.started_at,
5221
+ ended_at: envelope.ended_at,
5222
+ handoff: envelope.handoff,
5223
+ };
5224
+ process.stdout.write(JSON.stringify(bundle) + '\n');
5225
+ totalEmitted++;
5226
+ newCount++;
5227
+ const t = envelope.ended_at || envelope.started_at;
5228
+ if (!oldestSeen || t > oldestSeen) oldestSeen = t;
5229
+ }
5230
+ // No new entries → we've exhausted the timeline.
5231
+ if (newCount === 0) break;
5232
+ // Step the cursor forward (next page returns entries from
5233
+ // after the oldest we saw).
5234
+ cursor = oldestSeen;
5235
+ }
5236
+ process.stderr.write(`Exported ${totalEmitted} handoffs.\n`);
5237
+ process.exit(totalEmitted === 0 ? 3 : 0);
5238
+ } catch (err) {
5239
+ console.error('export --all failed:', err.message);
5240
+ process.exit(1);
5241
+ }
5242
+ return;
5243
+ }
5069
5244
  try {
5070
5245
  const args = { resume: true, session_id_hint: sessionArg };
5071
5246
  const response = await server.makeRequest({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cf-memory-mcp",
3
- "version": "3.43.0",
3
+ "version": "3.45.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": {