cf-memory-mcp 3.62.0 → 3.64.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.
@@ -3955,7 +3955,7 @@ if (process.argv.includes('--version') || process.argv.includes('-v')) {
3955
3955
 
3956
3956
  // Global --help only when no subcommand is present. With a subcommand, fall
3957
3957
  // through to the per-command help dispatch below.
3958
- const SUBCOMMANDS = new Set(['resume', 'list', 'checkpoint', 'status', 'clean', 'export', 'import', 'doctor', 'completion', 'delete', 'env', 'history', 'link', 'explain', 'gha']);
3958
+ const SUBCOMMANDS = new Set(['resume', 'list', 'search', 'checkpoint', 'status', 'clean', 'export', 'import', 'doctor', 'completion', 'delete', 'env', 'history', 'link', 'explain', 'gha']);
3959
3959
  const hasSubcommand = process.argv[2] && SUBCOMMANDS.has(process.argv[2]);
3960
3960
 
3961
3961
  if (!hasSubcommand && (process.argv.includes('--help') || process.argv.includes('-h'))) {
@@ -3968,6 +3968,7 @@ Usage:
3968
3968
  npx cf-memory-mcp Start the MCP server
3969
3969
  npx cf-memory-mcp resume [id] Print the prior resume handoff (markdown)
3970
3970
  npx cf-memory-mcp list List recent handoffs for cwd (status-ranked)
3971
+ npx cf-memory-mcp search <query> Find handoffs by goal/notes/decisions text (cross-repo default)
3971
3972
  npx cf-memory-mcp history List all handoffs for cwd, chronological
3972
3973
  npx cf-memory-mcp checkpoint ["<goal>"] Snapshot current state (keep_open)
3973
3974
  npx cf-memory-mcp link <child> --parent <id> Retroactively link a child session to a parent
@@ -4144,6 +4145,10 @@ function parseCliArgs(rest) {
4144
4145
  flags.repo_path = rest[++i];
4145
4146
  } else if (a.startsWith('--repo=')) {
4146
4147
  flags.repo_path = a.slice('--repo='.length);
4148
+ } else if (a === '--repo-prefix') {
4149
+ flags.repo_path_prefix = rest[++i];
4150
+ } else if (a.startsWith('--repo-prefix=')) {
4151
+ flags.repo_path_prefix = a.slice('--repo-prefix='.length);
4147
4152
  } else if (a === '--project-id') {
4148
4153
  flags.project_id = rest[++i];
4149
4154
  } else if (a.startsWith('--project-id=')) {
@@ -4158,6 +4163,10 @@ function parseCliArgs(rest) {
4158
4163
  flags.since = a.slice('--since='.length);
4159
4164
  } else if (a === '--yes' || a === '-y') {
4160
4165
  flags.yes = true;
4166
+ } else if (a === '--ids-only') {
4167
+ flags.ids_only = true;
4168
+ } else if (a === '--here') {
4169
+ flags.here = true;
4161
4170
  } else positional.push(a);
4162
4171
  }
4163
4172
  return { positional, flags };
@@ -4995,6 +5004,116 @@ async function runListCli() {
4995
5004
  }
4996
5005
  }
4997
5006
 
5007
+ async function runSearchCli() {
5008
+ if (!API_KEY) {
5009
+ console.error('Error: CF_MEMORY_API_KEY environment variable is required');
5010
+ process.exit(1);
5011
+ }
5012
+ const { positional, flags } = parseCliArgs(process.argv.slice(3));
5013
+ const query = positional[0];
5014
+ if (!query) {
5015
+ console.error('Usage: cf-memory-mcp search <query> [--repo <path>] [--project-id <id>] [--status <s>] [--since <iso>] [--limit N] [--json] [--ids-only]');
5016
+ console.error('Searches across goal/next_steps/notes/decisions/blockers (case-insensitive substring).');
5017
+ console.error('Defaults to ALL your handoffs (cross-repo). Use --repo or --project-id to scope.');
5018
+ process.exit(1);
5019
+ }
5020
+ const server = new CFMemoryMCP();
5021
+ server.logDebug = () => {};
5022
+ server.logError = (...a) => process.stderr.write(a.join(' ') + '\n');
5023
+ try {
5024
+ // Server-side substring filter via goal_contains (which actually
5025
+ // matches the whole serialized handoff JSON — goal, next_steps,
5026
+ // notes, decisions, blockers, code_anchors, files_touched).
5027
+ const args = { resume: true, goal_contains: query };
5028
+ // Default: NO repo/project scoping — search is meant to be global.
5029
+ // Opt-in via --repo or --project-id (or --here for cwd's repo).
5030
+ if (flags.repo_path) args.repo_path = flags.repo_path;
5031
+ if (flags.project_id) args.project_id = flags.project_id;
5032
+ if (flags.here) {
5033
+ const meta = server.getRepoMetadata();
5034
+ if (meta.repo_path) args.repo_path = meta.repo_path;
5035
+ if (meta.branch) args.branch = meta.branch;
5036
+ }
5037
+ if (flags.status) args.status_filter = flags.status;
5038
+ if (flags.since) args.since = flags.since;
5039
+
5040
+ const response = await server.makeRequest({
5041
+ jsonrpc: '2.0',
5042
+ id: `cli-search-${Date.now()}`,
5043
+ method: 'tools/call',
5044
+ params: { name: 'get_context_bootstrap', arguments: args },
5045
+ });
5046
+ const text = response?.result?.content?.[0]?.text;
5047
+ const payload = JSON.parse(text || '{}');
5048
+ const allList = Array.isArray(payload.recent_handoffs) ? payload.recent_handoffs : [];
5049
+ const list = flags.limit ? allList.slice(0, flags.limit) : allList;
5050
+
5051
+ if (flags.ids_only) {
5052
+ for (const h of list) process.stdout.write(`${h.session_id || ''}\n`);
5053
+ process.exit(list.length === 0 ? 3 : 0);
5054
+ }
5055
+ if (flags.json) {
5056
+ process.stdout.write(JSON.stringify({
5057
+ query,
5058
+ scope: args.repo_path ? 'repo' : args.project_id ? 'project' : 'global',
5059
+ repo_path: args.repo_path,
5060
+ project_id: args.project_id,
5061
+ status_filter: flags.status,
5062
+ since: flags.since,
5063
+ count: list.length,
5064
+ handoffs: list,
5065
+ }, null, 2) + '\n');
5066
+ process.exit(list.length === 0 ? 3 : 0);
5067
+ }
5068
+ if (list.length === 0) {
5069
+ process.stderr.write(`No handoffs match "${query}".\n`);
5070
+ process.exit(3);
5071
+ }
5072
+ // Render with terminal highlight (ANSI bold) when stdout is a TTY.
5073
+ const tty = process.stdout.isTTY;
5074
+ const lcQuery = query.toLowerCase();
5075
+ const highlight = (str) => {
5076
+ if (!str) return str;
5077
+ if (!tty) return str;
5078
+ const idx = str.toLowerCase().indexOf(lcQuery);
5079
+ if (idx < 0) return str;
5080
+ const before = str.slice(0, idx);
5081
+ const match = str.slice(idx, idx + query.length);
5082
+ const after = str.slice(idx + query.length);
5083
+ return `${before}\x1b[1;33m${match}\x1b[0m${after}`;
5084
+ };
5085
+ const fmtAge = (iso) => {
5086
+ if (!iso) return '?';
5087
+ const min = Math.round((Date.now() - new Date(iso).getTime()) / 60000);
5088
+ if (min < 60) return `${min}m`;
5089
+ if (min < 1440) return `${Math.round(min/60)}h`;
5090
+ return `${Math.round(min/1440)}d`;
5091
+ };
5092
+ const scopeLabel = args.repo_path ? `in repo ${args.repo_path}` : args.project_id ? `in project ${args.project_id}` : 'across all your handoffs';
5093
+ process.stdout.write(`Found ${list.length} handoff${list.length === 1 ? '' : 's'} matching "${query}" ${scopeLabel}:\n\n`);
5094
+ for (const h of list) {
5095
+ const shortId = (h.session_id || '').slice(0, 8);
5096
+ const age = fmtAge(h.ended_at || h.started_at);
5097
+ const status = (h.status || 'unknown').padEnd(11);
5098
+ const branch = h.branch ? `[${h.branch}] ` : '';
5099
+ const goal = highlight((h.goal || '(no goal)').slice(0, 100));
5100
+ const q = typeof h.quality_score === 'number' ? ` q=${h.quality_score.toFixed(2)}` : '';
5101
+ process.stdout.write(` ${shortId} ${status} ${age.padStart(4)} ago${q} ${branch}${goal}\n`);
5102
+ if (h.next_step) process.stdout.write(` next: ${highlight(h.next_step.slice(0, 100))}\n`);
5103
+ // If the repo differs from the user's cwd, surface it so they
5104
+ // know which project the handoff belongs to.
5105
+ if (h.repo_path && !args.repo_path) {
5106
+ process.stdout.write(` repo: ${h.repo_path}\n`);
5107
+ }
5108
+ }
5109
+ process.stdout.write(`\n(Run "npx cf-memory-mcp resume <id>" to load one, or pipe to "xargs -I{} npx cf-memory-mcp resume {}" with --ids-only)\n`);
5110
+ process.exit(0);
5111
+ } catch (err) {
5112
+ console.error('search command failed:', err.message);
5113
+ process.exit(1);
5114
+ }
5115
+ }
5116
+
4998
5117
  async function runLinkCli() {
4999
5118
  if (!API_KEY) {
5000
5119
  console.error('Error: CF_MEMORY_API_KEY environment variable is required');
@@ -5165,14 +5284,15 @@ async function runDeleteCli() {
5165
5284
  }
5166
5285
  const { positional, flags } = parseCliArgs(process.argv.slice(3));
5167
5286
  const idArg = positional[0];
5168
- const bulkMode = !idArg && (flags.older_than_days !== undefined || flags.status || flags.repo_path);
5287
+ const bulkMode = !idArg && (flags.older_than_days !== undefined || flags.status || flags.repo_path || flags.repo_path_prefix);
5169
5288
  if (!idArg && !bulkMode) {
5170
5289
  console.error('Usage:');
5171
5290
  console.error(' cf-memory-mcp delete <session-id-or-prefix> # single delete');
5172
5291
  console.error(' cf-memory-mcp delete --older-than 30d # bulk delete by age');
5173
5292
  console.error(' cf-memory-mcp delete --status abandoned # bulk by status');
5174
- console.error(' cf-memory-mcp delete --repo /path/to/repo # bulk by repo');
5175
- console.error(' Combine filters; use --yes to confirm >5 deletions.');
5293
+ console.error(' cf-memory-mcp delete --repo /path/to/repo # bulk by exact repo');
5294
+ console.error(' cf-memory-mcp delete --repo-prefix /tmp/cfm- # bulk by repo prefix');
5295
+ console.error(' Combine filters; without --yes you get a dry-run preview.');
5176
5296
  process.exit(1);
5177
5297
  }
5178
5298
  const server = new CFMemoryMCP();
@@ -5183,6 +5303,7 @@ async function runDeleteCli() {
5183
5303
  if (flags.status) args.status = flags.status;
5184
5304
  if (flags.older_than_days !== undefined) args.older_than_days = flags.older_than_days;
5185
5305
  if (flags.repo_path) args.repo_path = flags.repo_path;
5306
+ if (flags.repo_path_prefix) args.repo_path_prefix = flags.repo_path_prefix;
5186
5307
 
5187
5308
  // Dry-run preview when --yes is missing. The server returns
5188
5309
  // {deleted:false, deleted_count, deleted_session_ids[]} so the
@@ -5193,6 +5314,16 @@ async function runDeleteCli() {
5193
5314
  method: 'tools/call',
5194
5315
  params: { name: 'delete_session', arguments: { ...args, dry_run: true } },
5195
5316
  });
5317
+ // Surface JSON-RPC errors (e.g. "repo_path_prefix too short")
5318
+ // instead of swallowing them into a "no matches" message.
5319
+ if (previewRes?.error) {
5320
+ if (flags.json) {
5321
+ process.stdout.write(JSON.stringify({ error: previewRes.error }, null, 2) + '\n');
5322
+ } else {
5323
+ process.stderr.write(`Error: ${previewRes.error.message || JSON.stringify(previewRes.error)}\n`);
5324
+ }
5325
+ process.exit(1);
5326
+ }
5196
5327
  const previewText = previewRes?.result?.content?.[0]?.text;
5197
5328
  const preview = JSON.parse(previewText || '{}');
5198
5329
  if (flags.json) {
@@ -5249,6 +5380,14 @@ async function runDeleteCli() {
5249
5380
  method: 'tools/call',
5250
5381
  params: { name: 'delete_session', arguments: args },
5251
5382
  });
5383
+ if (deleteRes?.error) {
5384
+ if (flags.json) {
5385
+ process.stdout.write(JSON.stringify({ error: deleteRes.error }, null, 2) + '\n');
5386
+ } else {
5387
+ process.stderr.write(`Error: ${deleteRes.error.message || JSON.stringify(deleteRes.error)}\n`);
5388
+ }
5389
+ process.exit(1);
5390
+ }
5252
5391
  const deleteText = deleteRes?.result?.content?.[0]?.text;
5253
5392
  const payload = JSON.parse(deleteText || '{}');
5254
5393
  // Implicit-cache invalidation: if any deleted id matches the
@@ -5294,6 +5433,14 @@ async function runDeleteCli() {
5294
5433
  method: 'tools/call',
5295
5434
  params: { name: 'delete_session', arguments: { session_id: fullId } },
5296
5435
  });
5436
+ if (deleteRes?.error) {
5437
+ if (flags.json) {
5438
+ process.stdout.write(JSON.stringify({ error: deleteRes.error, session_id: fullId }, null, 2) + '\n');
5439
+ } else {
5440
+ process.stderr.write(`Error: ${deleteRes.error.message || JSON.stringify(deleteRes.error)}\n`);
5441
+ }
5442
+ process.exit(1);
5443
+ }
5297
5444
  const deleteText = deleteRes?.result?.content?.[0]?.text;
5298
5445
  const payload = JSON.parse(deleteText || '{}');
5299
5446
  // Drop the implicit-session disk cache if the deleted id matches
@@ -5389,7 +5536,7 @@ function runCompletionCli() {
5389
5536
  process.stderr.write(`\nDone. Installed completion for ${installed} shell${installed === 1 ? '' : 's'}.\n`);
5390
5537
  process.exit(installed > 0 ? 0 : 1);
5391
5538
  }
5392
- const commands = ['resume', 'list', 'history', 'checkpoint', 'link', 'explain', 'gha', 'status', 'clean', 'export', 'import', 'delete', 'doctor', 'env', 'completion'];
5539
+ const commands = ['resume', 'list', 'search', 'history', 'checkpoint', 'link', 'explain', 'gha', 'status', 'clean', 'export', 'import', 'delete', 'doctor', 'env', 'completion'];
5393
5540
  const flags = ['--json', '-j', '--limit', '-n', '--md', '--all', '--force', '-f', '--version', '-v', '--help', '-h', '--diagnose'];
5394
5541
 
5395
5542
  // Determine the install target for each shell. Use user-local paths
@@ -5593,6 +5740,23 @@ const PER_COMMAND_HELP = {
5593
5740
  --repo PATH Override the cwd's repo (peek at another repo).
5594
5741
  --limit N, -n N Max number of entries (default 5, max 50).
5595
5742
  --json, -j Emit a JSON array for scripts.`,
5743
+ search: `cf-memory-mcp search <query> [--here | --repo PATH | --project-id ID] [--status S] [--since ISO] [--limit N] [--json|--ids-only]
5744
+ Find handoffs matching <query> across goal, next_steps, notes,
5745
+ decisions, blockers, code_anchors, and files_touched (case-insensitive
5746
+ substring). Default scope is ALL your handoffs (cross-repo). Highlights
5747
+ the matched substring in terminal output.
5748
+ <query> Required substring to search for.
5749
+ --here Scope to cwd's repo (otherwise: global).
5750
+ --repo PATH Scope to a specific repo path.
5751
+ --project-id ID Scope to a specific project.
5752
+ --status S Filter by status (in_progress, completed, ...).
5753
+ --since ISO Lower bound on handoff timestamp (ISO 8601).
5754
+ --limit N Max entries to render (server may cap at 50).
5755
+ --ids-only Print only session_ids (xargs-friendly).
5756
+ --json, -j Emit JSON for scripts.
5757
+ Exit codes: 0 = matches, 3 = no matches.
5758
+ Example: cf-memory-mcp search "OAuth" --status in_progress
5759
+ cf-memory-mcp search "migration" --ids-only | xargs -I{} cfm resume {}`,
5596
5760
  checkpoint: `cf-memory-mcp checkpoint ["<goal>"] [--force] [--json]
5597
5761
  Snapshot current state to a fresh handoff with keep_open:true. The
5598
5762
  session stays active for future checkpoints. The final end_session (or
@@ -5634,12 +5798,15 @@ const PER_COMMAND_HELP = {
5634
5798
  --uninstall Remove a previously-installed script.
5635
5799
  --all-shells With --install: install for every shell at once.`,
5636
5800
  delete: `cf-memory-mcp delete <session-id-or-prefix> [--json]
5637
- cf-memory-mcp delete [--status STATUS] [--older-than 30d] [--repo PATH] --yes [--json]
5801
+ cf-memory-mcp delete [--status STATUS] [--older-than 30d] [--repo PATH] [--repo-prefix PFX] --yes [--json]
5638
5802
  Delete session row(s) and any session_summary memories tied to them.
5639
5803
  <session-id> Single delete: full UUID or short prefix (>=8 chars).
5640
5804
  --status STATUS Bulk filter by status (abandoned, completed, ...).
5641
5805
  --older-than 30d Bulk filter by age (suffixes: d, h, m).
5642
- --repo PATH Bulk filter by repo_path.
5806
+ --repo PATH Bulk filter by repo_path (exact match).
5807
+ --repo-prefix PFX Bulk filter by repo_path PREFIX (min 4 chars).
5808
+ E.g. --repo-prefix /tmp/cfm-lifecycle- nukes
5809
+ accumulated benchmark artifacts in one call.
5643
5810
  --yes, -y Required for bulk delete to confirm intent.
5644
5811
  --json, -j Emit JSON.
5645
5812
  Exit codes: 0 = deleted, 2 = bulk missing --yes, 3 = nothing matched.`,
@@ -6428,6 +6595,11 @@ if (process.argv[2] === 'list') {
6428
6595
  return;
6429
6596
  }
6430
6597
 
6598
+ if (process.argv[2] === 'search') {
6599
+ runSearchCli();
6600
+ return;
6601
+ }
6602
+
6431
6603
  if (process.argv[2] === 'checkpoint') {
6432
6604
  runCheckpointCli();
6433
6605
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cf-memory-mcp",
3
- "version": "3.62.0",
3
+ "version": "3.64.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": {