cf-memory-mcp 3.31.0 → 3.33.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.
package/README.md CHANGED
@@ -35,6 +35,32 @@ The active runtime is at [src-simplified/index.ts](src-simplified/index.ts). It
35
35
  - Coordination and progress with Durable Objects
36
36
  - Diagnostics: `health_check` tool + `npx cf-memory-mcp --diagnose` + `CF_MEMORY_TRACE=1`
37
37
 
38
+ ## Resume Context (cross-chat handoff)
39
+
40
+ Beyond code retrieval, cf-memory-mcp persists structured "handoff" packets so one chat can hand work off to the next. The next agent calls `get_context_bootstrap({resume:true})` (or the CLI `cf-memory-mcp resume`) and gets back the goal, status, next steps, code anchors with staleness markers, files touched, and a pre-rendered markdown prompt — without re-exploring the repo.
41
+
42
+ **Measured impact** (`scripts/benchmark-cold-vs-warm.ts`): warm resume is **82-85% faster** with **3-4 fewer tool calls** than cold-start exploration, and the warm path returns the exact next_action the cold agent would have eventually figured out.
43
+
44
+ ```bash
45
+ $ npx cf-memory-mcp resume # print prior handoff for cwd
46
+ $ npx cf-memory-mcp list # all recent handoffs (with status counts)
47
+ $ npx cf-memory-mcp checkpoint # snapshot current state
48
+ $ npx cf-memory-mcp status # bridge + server health for cwd
49
+ $ npx cf-memory-mcp doctor # diagnose common setup issues
50
+ $ npx cf-memory-mcp export <id> # backup a handoff
51
+ $ npx cf-memory-mcp import <file> # cross-machine sync
52
+ $ npx cf-memory-mcp delete <id> # remove a session
53
+ $ cfm resume # short alias
54
+ ```
55
+
56
+ Resume context also surfaces via:
57
+
58
+ - **MCP tools**: `start_session`, `end_session` (with structured `handoff`), `get_context_bootstrap` (with `resume`, `session_id_hint`, `goal_contains`, `since`, `status_filter`, `max_age_minutes`)
59
+ - **MCP resources**: `cfm://resume/current`, `cfm://resume/recent`, `cfm://resume/session/<id>`
60
+ - **MCP prompts**: `/resume`, `/list_threads` (for clients with slash-command UI)
61
+
62
+ See [docs/RESUME_CONTEXT_PLAN.md](docs/RESUME_CONTEXT_PLAN.md) for design rationale and [agents.md](agents.md) for the full agent-facing reference.
63
+
38
64
  ## Active Infra
39
65
 
40
66
  - Workers
@@ -411,13 +411,16 @@ const TOOLS_LIST = [
411
411
  },
412
412
  {
413
413
  name: 'delete_session',
414
- description: 'Delete a session row and any session_summary memories tied to it. Use to remove test sessions or accidental handoffs. Different from end_session — this physically removes the row. Pass the full UUID (not a prefix).',
414
+ description: 'Delete session row(s) and any session_summary memories tied to them. Accepts single-id or bulk filters. Different from end_session — this physically removes rows.',
415
415
  inputSchema: {
416
416
  type: 'object',
417
417
  properties: {
418
- session_id: { type: 'string', description: 'Full session UUID to delete.' }
419
- },
420
- required: ['session_id']
418
+ session_id: { type: 'string', description: 'Full session UUID to delete (not a prefix).' },
419
+ status: { description: 'Status filter: single string or array of statuses (in_progress, blocked, completed, abandoned).' },
420
+ older_than_days: { type: 'number', description: 'Delete sessions older than N days.' },
421
+ repo_path: { type: 'string', description: 'Restrict to a specific repo_path.' },
422
+ dry_run: { type: 'boolean', description: 'When true, preview what would be deleted without actually deleting.' }
423
+ }
421
424
  }
422
425
  }
423
426
  ];
@@ -3850,7 +3853,7 @@ For more information, visit: https://github.com/johnlam90/cf-memory-mcp
3850
3853
  }
3851
3854
 
3852
3855
  // Parse positional + flag args for CLI subcommands. Returns
3853
- // { positional: string[], flags: { json, limit, md_path } }.
3856
+ // { positional: string[], flags: { json, limit, md_path, older_than_days, status, repo_path, yes } }.
3854
3857
  function parseCliArgs(rest) {
3855
3858
  const positional = [];
3856
3859
  const flags = { json: false };
@@ -3864,10 +3867,40 @@ function parseCliArgs(rest) {
3864
3867
  const n = parseInt(a.slice('--limit='.length), 10);
3865
3868
  if (Number.isFinite(n) && n > 0) flags.limit = Math.min(n, 50);
3866
3869
  } else if (a === '--md') {
3867
- // Next arg is path; supports `--md=path` too.
3868
3870
  flags.md_path = rest[++i];
3869
3871
  } else if (a.startsWith('--md=')) {
3870
3872
  flags.md_path = a.slice('--md='.length);
3873
+ } else if (a === '--older-than') {
3874
+ // Accept "7d" / "30d" / "12h" / raw number (days).
3875
+ const raw = rest[++i] || '';
3876
+ const m = raw.match(/^(\d+)\s*(d|h|m)?$/);
3877
+ if (m) {
3878
+ const n = parseInt(m[1], 10);
3879
+ const unit = m[2] || 'd';
3880
+ flags.older_than_days = unit === 'd' ? n : unit === 'h' ? n / 24 : n / 1440;
3881
+ }
3882
+ } else if (a.startsWith('--older-than=')) {
3883
+ const raw = a.slice('--older-than='.length);
3884
+ const m = raw.match(/^(\d+)\s*(d|h|m)?$/);
3885
+ if (m) {
3886
+ const n = parseInt(m[1], 10);
3887
+ const unit = m[2] || 'd';
3888
+ flags.older_than_days = unit === 'd' ? n : unit === 'h' ? n / 24 : n / 1440;
3889
+ }
3890
+ } else if (a === '--status') {
3891
+ flags.status = rest[++i];
3892
+ } else if (a.startsWith('--status=')) {
3893
+ flags.status = a.slice('--status='.length);
3894
+ } else if (a === '--repo') {
3895
+ flags.repo_path = rest[++i];
3896
+ } else if (a.startsWith('--repo=')) {
3897
+ flags.repo_path = a.slice('--repo='.length);
3898
+ } else if (a === '--since') {
3899
+ flags.since = rest[++i];
3900
+ } else if (a.startsWith('--since=')) {
3901
+ flags.since = a.slice('--since='.length);
3902
+ } else if (a === '--yes' || a === '-y') {
3903
+ flags.yes = true;
3871
3904
  } else positional.push(a);
3872
3905
  }
3873
3906
  return { positional, flags };
@@ -3978,9 +4011,19 @@ async function runListCli() {
3978
4011
  const args = { resume: true };
3979
4012
  if (meta.repo_path) args.repo_path = meta.repo_path;
3980
4013
  if (meta.branch) args.branch = meta.branch;
3981
- const fake = { params: { name: 'retrieve_context', arguments: {} } };
3982
- await server.maybeFillProjectId(fake);
3983
- if (fake.params.arguments.project_id) args.project_id = fake.params.arguments.project_id;
4014
+ // Pass-through server-side filters when set on the CLI.
4015
+ if (flags.status) args.status_filter = flags.status;
4016
+ if (flags.since) args.since = flags.since;
4017
+ if (flags.repo_path) {
4018
+ // --repo overrides cwd → skip cwd's project_id too. Otherwise
4019
+ // tier-1 project_id_exact would short-circuit the lookup and
4020
+ // return cwd's handoff regardless of repo_path.
4021
+ args.repo_path = flags.repo_path;
4022
+ } else {
4023
+ const fake = { params: { name: 'retrieve_context', arguments: {} } };
4024
+ await server.maybeFillProjectId(fake);
4025
+ if (fake.params.arguments.project_id) args.project_id = fake.params.arguments.project_id;
4026
+ }
3984
4027
 
3985
4028
  const response = await server.makeRequest({
3986
4029
  jsonrpc: '2.0',
@@ -3997,8 +4040,10 @@ async function runListCli() {
3997
4040
 
3998
4041
  if (flags.json) {
3999
4042
  process.stdout.write(JSON.stringify({
4000
- repo_path: meta.repo_path,
4001
- branch: meta.branch,
4043
+ repo_path: args.repo_path || meta.repo_path,
4044
+ branch: args.branch || meta.branch,
4045
+ status_filter: flags.status,
4046
+ since: flags.since,
4002
4047
  count: list.length,
4003
4048
  handoffs: list,
4004
4049
  empty_hint: list.length === 0 ? payload.empty_hint : undefined,
@@ -4009,8 +4054,20 @@ async function runListCli() {
4009
4054
  process.stderr.write((payload.empty_hint || 'No handoffs for this context.') + '\n');
4010
4055
  process.exit(3);
4011
4056
  }
4012
- // Render a compact table to terminal.
4013
- process.stdout.write(`Recent handoffs for ${meta.repo_path || 'this cwd'}:\n\n`);
4057
+ // Status counts header: scan the visible list to give an at-a-glance
4058
+ // summary before the per-row details.
4059
+ const counts = {};
4060
+ for (const h of allList) {
4061
+ const s = h.status || 'unknown';
4062
+ counts[s] = (counts[s] || 0) + 1;
4063
+ }
4064
+ const countSummary = Object.entries(counts)
4065
+ .map(([s, n]) => `${n} ${s}`)
4066
+ .join(', ');
4067
+ // Render a compact table to terminal. Show the actual filter
4068
+ // applied (--repo override, or cwd default).
4069
+ const displayRepo = args.repo_path || 'this cwd';
4070
+ process.stdout.write(`Recent handoffs for ${displayRepo} (${countSummary}):\n\n`);
4014
4071
  const fmtAge = (iso) => {
4015
4072
  if (!iso) return '?';
4016
4073
  const min = Math.round((Date.now() - new Date(iso).getTime()) / 60000);
@@ -4042,15 +4099,79 @@ async function runDeleteCli() {
4042
4099
  }
4043
4100
  const { positional, flags } = parseCliArgs(process.argv.slice(3));
4044
4101
  const idArg = positional[0];
4045
- if (!idArg) {
4046
- console.error('Usage: cf-memory-mcp delete <session-id-or-prefix>');
4102
+ const bulkMode = !idArg && (flags.older_than_days !== undefined || flags.status || flags.repo_path);
4103
+ if (!idArg && !bulkMode) {
4104
+ console.error('Usage:');
4105
+ console.error(' cf-memory-mcp delete <session-id-or-prefix> # single delete');
4106
+ console.error(' cf-memory-mcp delete --older-than 30d # bulk delete by age');
4107
+ console.error(' cf-memory-mcp delete --status abandoned # bulk by status');
4108
+ console.error(' cf-memory-mcp delete --repo /path/to/repo # bulk by repo');
4109
+ console.error(' Combine filters; use --yes to confirm >5 deletions.');
4047
4110
  process.exit(1);
4048
4111
  }
4049
4112
  const server = new CFMemoryMCP();
4050
4113
  server.logDebug = () => {};
4051
4114
  try {
4052
- // Resolve prefix to full id via get_context_bootstrap. The server's
4053
- // delete_session requires a full UUID (no implicit prefix).
4115
+ if (bulkMode) {
4116
+ const args = {};
4117
+ if (flags.status) args.status = flags.status;
4118
+ if (flags.older_than_days !== undefined) args.older_than_days = flags.older_than_days;
4119
+ if (flags.repo_path) args.repo_path = flags.repo_path;
4120
+
4121
+ // Dry-run preview when --yes is missing. The server returns
4122
+ // {deleted:false, deleted_count, deleted_session_ids[]} so the
4123
+ // user can see what WOULD be deleted before committing.
4124
+ if (!flags.yes) {
4125
+ const previewRes = await server.makeRequest({
4126
+ jsonrpc: '2.0', id: `cli-delete-preview-${Date.now()}`,
4127
+ method: 'tools/call',
4128
+ params: { name: 'delete_session', arguments: { ...args, dry_run: true } },
4129
+ });
4130
+ const previewText = previewRes?.result?.content?.[0]?.text;
4131
+ const preview = JSON.parse(previewText || '{}');
4132
+ if (flags.json) {
4133
+ process.stdout.write(JSON.stringify({ ...preview, would_delete: true }, null, 2) + '\n');
4134
+ process.exit(2);
4135
+ }
4136
+ if (preview.deleted_count === 0) {
4137
+ process.stderr.write(`No sessions match the filters. Nothing to delete.\n`);
4138
+ process.exit(3);
4139
+ }
4140
+ process.stderr.write(`Would delete ${preview.deleted_count} session${preview.deleted_count === 1 ? '' : 's'}:\n`);
4141
+ const previewIds = preview.deleted_session_ids || [];
4142
+ for (const id of previewIds.slice(0, 10)) {
4143
+ process.stderr.write(` ${id.slice(0, 8)}\n`);
4144
+ }
4145
+ if (previewIds.length > 10) {
4146
+ process.stderr.write(` ... and ${preview.deleted_count - 10} more\n`);
4147
+ }
4148
+ process.stderr.write(`\nRe-run with --yes to confirm:\n`);
4149
+ process.stderr.write(` cf-memory-mcp delete ${process.argv.slice(3).join(' ')} --yes\n`);
4150
+ process.exit(2);
4151
+ }
4152
+
4153
+ const deleteRes = await server.makeRequest({
4154
+ jsonrpc: '2.0', id: `cli-delete-bulk-${Date.now()}`,
4155
+ method: 'tools/call',
4156
+ params: { name: 'delete_session', arguments: args },
4157
+ });
4158
+ const deleteText = deleteRes?.result?.content?.[0]?.text;
4159
+ const payload = JSON.parse(deleteText || '{}');
4160
+ if (flags.json) {
4161
+ process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
4162
+ process.exit(payload.deleted ? 0 : 3);
4163
+ }
4164
+ if (payload.deleted) {
4165
+ process.stdout.write(`Deleted ${payload.deleted_count} session${payload.deleted_count === 1 ? '' : 's'} (and ${payload.cleaned_memories || 0} associated memories).\n`);
4166
+ process.exit(0);
4167
+ } else {
4168
+ process.stderr.write(`No sessions matched the filters.\n`);
4169
+ process.exit(3);
4170
+ }
4171
+ return;
4172
+ }
4173
+
4174
+ // Single-delete path (existing).
4054
4175
  let fullId = idArg;
4055
4176
  if (idArg.length < 36) {
4056
4177
  const resolveRes = await server.makeRequest({
@@ -4207,8 +4328,12 @@ const PER_COMMAND_HELP = {
4207
4328
  --md <path> Write the markdown to a file instead of stdout.
4208
4329
  --json, -j Emit the full bootstrap payload as JSON for scripts.
4209
4330
  Exit codes: 0 = handoff found, 3 = no handoff found.`,
4210
- list: `cf-memory-mcp list [--limit N] [--json]
4211
- List recent handoffs for the current cwd, status-ranked.
4331
+ list: `cf-memory-mcp list [--status S] [--since ISO] [--repo PATH] [--limit N] [--json]
4332
+ List recent handoffs for the current cwd, status-ranked. Header shows
4333
+ a status-count summary.
4334
+ --status S Restrict to a given status (in_progress, completed, ...).
4335
+ --since ISO Lower bound on handoff timestamp (ISO 8601).
4336
+ --repo PATH Override the cwd's repo (peek at another repo).
4212
4337
  --limit N, -n N Max number of entries (default 5, max 50).
4213
4338
  --json, -j Emit a JSON array for scripts.`,
4214
4339
  checkpoint: `cf-memory-mcp checkpoint ["<goal>"] [--force] [--json]
@@ -4245,11 +4370,15 @@ const PER_COMMAND_HELP = {
4245
4370
  completion: `cf-memory-mcp completion [bash|zsh|fish]
4246
4371
  Output shell completion script. Pipe to your shell's completion dir.`,
4247
4372
  delete: `cf-memory-mcp delete <session-id-or-prefix> [--json]
4248
- Delete a session row and its associated session_summary memories.
4249
- <session-id> Full UUID or short prefix (>=8 chars). The CLI
4250
- resolves prefix to a full id via the server.
4251
- --json, -j Emit a JSON status object.
4252
- Exit codes: 0 = deleted, 3 = not found / could not resolve.`,
4373
+ cf-memory-mcp delete [--status STATUS] [--older-than 30d] [--repo PATH] --yes [--json]
4374
+ Delete session row(s) and any session_summary memories tied to them.
4375
+ <session-id> Single delete: full UUID or short prefix (>=8 chars).
4376
+ --status STATUS Bulk filter by status (abandoned, completed, ...).
4377
+ --older-than 30d Bulk filter by age (suffixes: d, h, m).
4378
+ --repo PATH Bulk filter by repo_path.
4379
+ --yes, -y Required for bulk delete to confirm intent.
4380
+ --json, -j Emit JSON.
4381
+ Exit codes: 0 = deleted, 2 = bulk missing --yes, 3 = nothing matched.`,
4253
4382
  env: `cf-memory-mcp env [--json]
4254
4383
  Print all CF_MEMORY_* env vars the bridge reads, with their current
4255
4384
  values + descriptions. Useful for discovering knobs.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cf-memory-mcp",
3
- "version": "3.31.0",
3
+ "version": "3.33.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": {