cf-memory-mcp 3.44.0 → 3.46.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.
@@ -3969,6 +3969,14 @@ function parseCliArgs(rest) {
3969
3969
  flags.card = true;
3970
3970
  } else if (a === '--short') {
3971
3971
  flags.short = true;
3972
+ } else if (a === '--age') {
3973
+ flags.age = true;
3974
+ } else if (a === '--quality') {
3975
+ flags.quality = true;
3976
+ } else if (a === '--goal-only') {
3977
+ flags.goal_only = true;
3978
+ } else if (a === '--status-only') {
3979
+ flags.status_only = true;
3972
3980
  } else if (a === '--older-than') {
3973
3981
  // Accept "7d" / "30d" / "12h" / raw number (days).
3974
3982
  const raw = rest[++i] || '';
@@ -4131,6 +4139,38 @@ async function runResumeCli() {
4131
4139
  process.exit(0);
4132
4140
  }
4133
4141
 
4142
+ // --goal-only: print just the goal (single line).
4143
+ if (flags.goal_only) {
4144
+ const goal = payload.resume_handoff?.handoff?.goal;
4145
+ if (!goal) process.exit(3);
4146
+ process.stdout.write(goal + '\n');
4147
+ process.exit(0);
4148
+ }
4149
+
4150
+ // --status-only: print just the status string.
4151
+ if (flags.status_only) {
4152
+ const status = payload.resume_handoff?.handoff?.status;
4153
+ if (!status) process.exit(3);
4154
+ process.stdout.write(status + '\n');
4155
+ process.exit(0);
4156
+ }
4157
+
4158
+ // --age: print just handoff_age_minutes (single integer).
4159
+ if (flags.age) {
4160
+ const ageMin = payload.resume_handoff?.handoff_age_minutes;
4161
+ if (typeof ageMin !== 'number') process.exit(3);
4162
+ process.stdout.write(String(ageMin) + '\n');
4163
+ process.exit(0);
4164
+ }
4165
+
4166
+ // --quality: print just quality_score (single float).
4167
+ if (flags.quality) {
4168
+ const q = payload.resume_handoff?.quality_score;
4169
+ if (typeof q !== 'number') process.exit(3);
4170
+ process.stdout.write(String(q) + '\n');
4171
+ process.exit(0);
4172
+ }
4173
+
4134
4174
  // --short: single line for tmux/status bars. "goal · status"
4135
4175
  // truncated to terminal-friendly length.
4136
4176
  if (flags.short) {
@@ -4923,6 +4963,10 @@ const PER_COMMAND_HELP = {
4923
4963
  --chain Walk parent_session_id back; show the thread history.
4924
4964
  --validate Check that files_touched + code_anchors still exist locally.
4925
4965
  --raw Print just the raw handoff JSON (no envelope metadata).
4966
+ --age Print handoff_age_minutes (single integer).
4967
+ --quality Print quality_score (single float, 0-1).
4968
+ --goal-only Print just the goal text (single line).
4969
+ --status-only Print just the status string.
4926
4970
  --json, -j Full bootstrap payload as JSON.
4927
4971
  Exit codes: 0 = found, 3 = no handoff / no data, 4 = --validate found missing files.`,
4928
4972
  list: `cf-memory-mcp list [--status S] [--since ISO] [--repo PATH] [--limit N] [--json]
@@ -4950,14 +4994,18 @@ const PER_COMMAND_HELP = {
4950
4994
  --all Delete ALL cf-memory disk caches.
4951
4995
  --json, -j Emit a JSON list of removed paths.`,
4952
4996
  export: `cf-memory-mcp export <session-id-or-prefix> [--md path]
4997
+ cf-memory-mcp export --all
4953
4998
  Print a handoff as a JSON bundle to stdout (or write to file). For
4954
- backup or cross-machine sync.
4999
+ backup or cross-machine sync. With --all, streams ALL handoffs as
5000
+ NDJSON (one bundle per line) suitable for piping to a file.
4955
5001
  <session-id> Full UUID or short prefix (>=8 chars).
4956
- --md <path> Write the JSON to a file.`,
4957
- import: `cf-memory-mcp import [<file>|-] [--json]
5002
+ --md <path> Write the JSON to a file (single-handoff mode).
5003
+ --all Stream all handoffs as NDJSON.`,
5004
+ import: `cf-memory-mcp import [<file>|-] [--all] [--json]
4958
5005
  Restore a handoff bundle into a NEW session (keep_open). Cross-machine
4959
5006
  sync. Use "-" to read from stdin.
4960
5007
  <file> Bundle file path; "-" for stdin.
5008
+ --all Input is NDJSON (one bundle per line). Imports each.
4961
5009
  --json, -j Emit a JSON status object.`,
4962
5010
  doctor: `cf-memory-mcp doctor [--json]
4963
5011
  Diagnose common setup issues. Checks API key, git repo/branch, disk-
@@ -5131,12 +5179,91 @@ async function runExportCli() {
5131
5179
  }
5132
5180
  const { positional, flags } = parseCliArgs(process.argv.slice(3));
5133
5181
  const sessionArg = positional[0];
5134
- if (!sessionArg) {
5135
- console.error('Usage: cf-memory-mcp export <session-id-or-prefix> [--md path]');
5182
+ const exportAll = process.argv.includes('--all');
5183
+ if (!sessionArg && !exportAll) {
5184
+ console.error('Usage:');
5185
+ console.error(' cf-memory-mcp export <session-id-or-prefix> # single handoff bundle');
5186
+ console.error(' cf-memory-mcp export --all # all handoffs as NDJSON');
5136
5187
  process.exit(1);
5137
5188
  }
5138
5189
  const server = new CFMemoryMCP();
5139
5190
  server.logDebug = () => {};
5191
+
5192
+ // --all: stream every handoff as NDJSON (one bundle per line).
5193
+ if (exportAll) {
5194
+ // EPIPE handler: when piping to `head` etc., downstream closes
5195
+ // stdout while we're still writing. Exit cleanly instead of
5196
+ // dumping a Node stack trace.
5197
+ process.stdout.on('error', (err) => {
5198
+ if (err.code === 'EPIPE') process.exit(0);
5199
+ throw err;
5200
+ });
5201
+ try {
5202
+ // Page through all the user's handoffs via get_context_bootstrap
5203
+ // with recent_handoff_limit=50, sorted by recency. Use `since`
5204
+ // to walk back in time without dupes — query for entries before
5205
+ // the oldest one in the previous batch.
5206
+ const seen = new Set();
5207
+ let cursor = null; // ISO timestamp; entries with ended_at|started_at >= cursor are returned
5208
+ let totalEmitted = 0;
5209
+ for (let page = 0; page < 100; page++) {
5210
+ const args = { resume: true, recent_handoff_limit: 50 };
5211
+ const meta = server.getRepoMetadata();
5212
+ if (meta.repo_path) args.repo_path = meta.repo_path;
5213
+ if (cursor) args.since = cursor;
5214
+ const res = await server.makeRequest({
5215
+ jsonrpc: '2.0', id: `cli-export-all-${page}-${Date.now()}`,
5216
+ method: 'tools/call',
5217
+ params: { name: 'get_context_bootstrap', arguments: args },
5218
+ });
5219
+ const text = res?.result?.content?.[0]?.text;
5220
+ const payload = JSON.parse(text || '{}');
5221
+ const handoffs = payload.recent_handoffs || [];
5222
+ if (handoffs.length === 0) break;
5223
+ let newCount = 0;
5224
+ let oldestSeen = null;
5225
+ for (const h of handoffs) {
5226
+ if (seen.has(h.session_id)) continue;
5227
+ seen.add(h.session_id);
5228
+ // Fetch the full handoff (not just the summary).
5229
+ const fullRes = await server.makeRequest({
5230
+ jsonrpc: '2.0', id: `cli-export-all-detail-${Date.now()}`,
5231
+ method: 'tools/call',
5232
+ params: { name: 'get_context_bootstrap', arguments: { resume: true, session_id_hint: h.session_id } },
5233
+ });
5234
+ const fullText = fullRes?.result?.content?.[0]?.text;
5235
+ const fullPayload = JSON.parse(fullText || '{}');
5236
+ const envelope = fullPayload.resume_handoff;
5237
+ if (!envelope) continue;
5238
+ const bundle = {
5239
+ kind: 'cf-memory-handoff',
5240
+ version: 1,
5241
+ exported_at: new Date().toISOString(),
5242
+ session_id: envelope.session_id,
5243
+ started_at: envelope.started_at,
5244
+ ended_at: envelope.ended_at,
5245
+ handoff: envelope.handoff,
5246
+ };
5247
+ process.stdout.write(JSON.stringify(bundle) + '\n');
5248
+ totalEmitted++;
5249
+ newCount++;
5250
+ const t = envelope.ended_at || envelope.started_at;
5251
+ if (!oldestSeen || t > oldestSeen) oldestSeen = t;
5252
+ }
5253
+ // No new entries → we've exhausted the timeline.
5254
+ if (newCount === 0) break;
5255
+ // Step the cursor forward (next page returns entries from
5256
+ // after the oldest we saw).
5257
+ cursor = oldestSeen;
5258
+ }
5259
+ process.stderr.write(`Exported ${totalEmitted} handoffs.\n`);
5260
+ process.exit(totalEmitted === 0 ? 3 : 0);
5261
+ } catch (err) {
5262
+ console.error('export --all failed:', err.message);
5263
+ process.exit(1);
5264
+ }
5265
+ return;
5266
+ }
5140
5267
  try {
5141
5268
  const args = { resume: true, session_id_hint: sessionArg };
5142
5269
  const response = await server.makeRequest({
@@ -5182,6 +5309,7 @@ async function runImportCli() {
5182
5309
  process.exit(1);
5183
5310
  }
5184
5311
  const { positional, flags } = parseCliArgs(process.argv.slice(3));
5312
+ const bulk = process.argv.includes('--all');
5185
5313
  const sourcePath = positional[0];
5186
5314
  let raw;
5187
5315
  try {
@@ -5195,6 +5323,64 @@ async function runImportCli() {
5195
5323
  console.error('import: cannot read input:', err.message);
5196
5324
  process.exit(1);
5197
5325
  }
5326
+
5327
+ // Bulk: parse NDJSON (one bundle per line) and import each.
5328
+ if (bulk) {
5329
+ const lines = raw.split('\n').filter(l => l.trim());
5330
+ const server = new CFMemoryMCP();
5331
+ server.logDebug = () => {};
5332
+ const results = { imported: 0, failed: 0, errors: [] };
5333
+ const meta = server.getRepoMetadata();
5334
+ for (const line of lines) {
5335
+ let bundle;
5336
+ try { bundle = JSON.parse(line); } catch (err) {
5337
+ results.failed++;
5338
+ results.errors.push(`line skip: not valid JSON`);
5339
+ continue;
5340
+ }
5341
+ if (bundle.kind !== 'cf-memory-handoff' || !bundle.handoff) {
5342
+ results.failed++;
5343
+ results.errors.push(`line skip: not a cf-memory-handoff bundle`);
5344
+ continue;
5345
+ }
5346
+ try {
5347
+ const startArgs = { context: 'main' };
5348
+ if (meta.repo_path) startArgs.repo_path = meta.repo_path;
5349
+ if (meta.branch) startArgs.branch = meta.branch;
5350
+ const startRes = await server.makeRequest({
5351
+ jsonrpc: '2.0', id: `cli-import-all-start-${Date.now()}`,
5352
+ method: 'tools/call', params: { name: 'start_session', arguments: startArgs },
5353
+ });
5354
+ const sp = JSON.parse(startRes?.result?.content?.[0]?.text || '{}');
5355
+ const newId = sp.session_id;
5356
+ if (!newId) { results.failed++; continue; }
5357
+ const handoff = {
5358
+ ...bundle.handoff,
5359
+ parent_session_id: bundle.session_id,
5360
+ notes: (bundle.handoff.notes || '') + `\n\n[bulk-imported from ${bundle.session_id}]`,
5361
+ };
5362
+ await server.makeRequest({
5363
+ jsonrpc: '2.0', id: `cli-import-all-end-${Date.now()}`,
5364
+ method: 'tools/call', params: { name: 'end_session', arguments: {
5365
+ session_id: newId, keep_open: true, handoff,
5366
+ } },
5367
+ });
5368
+ results.imported++;
5369
+ } catch (err) {
5370
+ results.failed++;
5371
+ results.errors.push(`${bundle.session_id}: ${err.message}`);
5372
+ }
5373
+ }
5374
+ if (flags.json) {
5375
+ process.stdout.write(JSON.stringify(results, null, 2) + '\n');
5376
+ } else {
5377
+ process.stderr.write(`Imported ${results.imported} handoffs.`);
5378
+ if (results.failed > 0) process.stderr.write(` Failed: ${results.failed}.`);
5379
+ process.stderr.write('\n');
5380
+ }
5381
+ process.exit(results.imported > 0 ? 0 : 3);
5382
+ }
5383
+
5198
5384
  let bundle;
5199
5385
  try { bundle = JSON.parse(raw); } catch (err) {
5200
5386
  console.error('import: input is not valid JSON:', err.message);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cf-memory-mcp",
3
- "version": "3.44.0",
3
+ "version": "3.46.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": {