cf-memory-mcp 3.30.0 → 3.32.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
@@ -408,6 +408,19 @@ const TOOLS_LIST = [
408
408
  older_than_days: { type: 'number', description: 'Delete memories created more than N days ago' }
409
409
  }
410
410
  }
411
+ },
412
+ {
413
+ name: 'delete_session',
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
+ inputSchema: {
416
+ type: 'object',
417
+ properties: {
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
+ }
423
+ }
411
424
  }
412
425
  ];
413
426
 
@@ -3792,7 +3805,7 @@ if (process.argv.includes('--version') || process.argv.includes('-v')) {
3792
3805
 
3793
3806
  // Global --help only when no subcommand is present. With a subcommand, fall
3794
3807
  // through to the per-command help dispatch below.
3795
- const SUBCOMMANDS = new Set(['resume', 'list', 'checkpoint', 'status', 'clean', 'export', 'import', 'doctor', 'completion']);
3808
+ const SUBCOMMANDS = new Set(['resume', 'list', 'checkpoint', 'status', 'clean', 'export', 'import', 'doctor', 'completion', 'delete', 'env']);
3796
3809
  const hasSubcommand = process.argv[2] && SUBCOMMANDS.has(process.argv[2]);
3797
3810
 
3798
3811
  if (!hasSubcommand && (process.argv.includes('--help') || process.argv.includes('-h'))) {
@@ -3811,7 +3824,9 @@ Usage:
3811
3824
  npx cf-memory-mcp clean --all Delete ALL local disk caches
3812
3825
  npx cf-memory-mcp export <id> Print a session's handoff as JSON bundle (backup)
3813
3826
  npx cf-memory-mcp import <file> Restore a handoff bundle (cross-machine sync); use "-" for stdin
3827
+ npx cf-memory-mcp delete <id> Delete a session and its associated memories
3814
3828
  npx cf-memory-mcp doctor Diagnose common setup issues
3829
+ npx cf-memory-mcp env Print all CF_MEMORY_* env vars + descriptions
3815
3830
  npx cf-memory-mcp completion bash Output shell completion script (bash|zsh|fish)
3816
3831
  npx cf-memory-mcp <cmd> --help Show flags for a specific command
3817
3832
  npx cf-memory-mcp --version Show version
@@ -3837,7 +3852,7 @@ For more information, visit: https://github.com/johnlam90/cf-memory-mcp
3837
3852
  }
3838
3853
 
3839
3854
  // Parse positional + flag args for CLI subcommands. Returns
3840
- // { positional: string[], flags: { json, limit, md_path } }.
3855
+ // { positional: string[], flags: { json, limit, md_path, older_than_days, status, repo_path, yes } }.
3841
3856
  function parseCliArgs(rest) {
3842
3857
  const positional = [];
3843
3858
  const flags = { json: false };
@@ -3851,10 +3866,36 @@ function parseCliArgs(rest) {
3851
3866
  const n = parseInt(a.slice('--limit='.length), 10);
3852
3867
  if (Number.isFinite(n) && n > 0) flags.limit = Math.min(n, 50);
3853
3868
  } else if (a === '--md') {
3854
- // Next arg is path; supports `--md=path` too.
3855
3869
  flags.md_path = rest[++i];
3856
3870
  } else if (a.startsWith('--md=')) {
3857
3871
  flags.md_path = a.slice('--md='.length);
3872
+ } else if (a === '--older-than') {
3873
+ // Accept "7d" / "30d" / "12h" / raw number (days).
3874
+ const raw = rest[++i] || '';
3875
+ const m = raw.match(/^(\d+)\s*(d|h|m)?$/);
3876
+ if (m) {
3877
+ const n = parseInt(m[1], 10);
3878
+ const unit = m[2] || 'd';
3879
+ flags.older_than_days = unit === 'd' ? n : unit === 'h' ? n / 24 : n / 1440;
3880
+ }
3881
+ } else if (a.startsWith('--older-than=')) {
3882
+ const raw = a.slice('--older-than='.length);
3883
+ const m = raw.match(/^(\d+)\s*(d|h|m)?$/);
3884
+ if (m) {
3885
+ const n = parseInt(m[1], 10);
3886
+ const unit = m[2] || 'd';
3887
+ flags.older_than_days = unit === 'd' ? n : unit === 'h' ? n / 24 : n / 1440;
3888
+ }
3889
+ } else if (a === '--status') {
3890
+ flags.status = rest[++i];
3891
+ } else if (a.startsWith('--status=')) {
3892
+ flags.status = a.slice('--status='.length);
3893
+ } else if (a === '--repo') {
3894
+ flags.repo_path = rest[++i];
3895
+ } else if (a.startsWith('--repo=')) {
3896
+ flags.repo_path = a.slice('--repo='.length);
3897
+ } else if (a === '--yes' || a === '-y') {
3898
+ flags.yes = true;
3858
3899
  } else positional.push(a);
3859
3900
  }
3860
3901
  return { positional, flags };
@@ -3996,8 +4037,18 @@ async function runListCli() {
3996
4037
  process.stderr.write((payload.empty_hint || 'No handoffs for this context.') + '\n');
3997
4038
  process.exit(3);
3998
4039
  }
4040
+ // Status counts header: scan the visible list to give an at-a-glance
4041
+ // summary before the per-row details.
4042
+ const counts = {};
4043
+ for (const h of allList) {
4044
+ const s = h.status || 'unknown';
4045
+ counts[s] = (counts[s] || 0) + 1;
4046
+ }
4047
+ const countSummary = Object.entries(counts)
4048
+ .map(([s, n]) => `${n} ${s}`)
4049
+ .join(', ');
3999
4050
  // Render a compact table to terminal.
4000
- process.stdout.write(`Recent handoffs for ${meta.repo_path || 'this cwd'}:\n\n`);
4051
+ process.stdout.write(`Recent handoffs for ${meta.repo_path || 'this cwd'} (${countSummary}):\n\n`);
4001
4052
  const fmtAge = (iso) => {
4002
4053
  if (!iso) return '?';
4003
4054
  const min = Math.round((Date.now() - new Date(iso).getTime()) / 60000);
@@ -4022,9 +4073,152 @@ async function runListCli() {
4022
4073
  }
4023
4074
  }
4024
4075
 
4076
+ async function runDeleteCli() {
4077
+ if (!API_KEY) {
4078
+ console.error('Error: CF_MEMORY_API_KEY environment variable is required');
4079
+ process.exit(1);
4080
+ }
4081
+ const { positional, flags } = parseCliArgs(process.argv.slice(3));
4082
+ const idArg = positional[0];
4083
+ const bulkMode = !idArg && (flags.older_than_days !== undefined || flags.status || flags.repo_path);
4084
+ if (!idArg && !bulkMode) {
4085
+ console.error('Usage:');
4086
+ console.error(' cf-memory-mcp delete <session-id-or-prefix> # single delete');
4087
+ console.error(' cf-memory-mcp delete --older-than 30d # bulk delete by age');
4088
+ console.error(' cf-memory-mcp delete --status abandoned # bulk by status');
4089
+ console.error(' cf-memory-mcp delete --repo /path/to/repo # bulk by repo');
4090
+ console.error(' Combine filters; use --yes to confirm >5 deletions.');
4091
+ process.exit(1);
4092
+ }
4093
+ const server = new CFMemoryMCP();
4094
+ server.logDebug = () => {};
4095
+ try {
4096
+ if (bulkMode) {
4097
+ // Bulk delete path. First dry-run: query matching sessions
4098
+ // count via list, then confirm with --yes for >5.
4099
+ const args = {};
4100
+ if (flags.status) args.status = flags.status;
4101
+ if (flags.older_than_days !== undefined) args.older_than_days = flags.older_than_days;
4102
+ if (flags.repo_path) args.repo_path = flags.repo_path;
4103
+
4104
+ // Preview: do an initial delete_session with the same filters
4105
+ // to count matches via the response's deleted_session_ids
4106
+ // (capped at 50). For larger counts we accept the cap as an
4107
+ // approximation; the server handles all of them anyway.
4108
+ // Use --yes to skip the preview/confirm prompt.
4109
+ if (!flags.yes) {
4110
+ process.stderr.write(`Dry-run: previewing matches without confirm prompt is not supported yet.\n`);
4111
+ process.stderr.write(`Run again with --yes to confirm: cf-memory-mcp delete ${process.argv.slice(3).join(' ')} --yes\n`);
4112
+ process.exit(2);
4113
+ }
4114
+
4115
+ const deleteRes = await server.makeRequest({
4116
+ jsonrpc: '2.0', id: `cli-delete-bulk-${Date.now()}`,
4117
+ method: 'tools/call',
4118
+ params: { name: 'delete_session', arguments: args },
4119
+ });
4120
+ const deleteText = deleteRes?.result?.content?.[0]?.text;
4121
+ const payload = JSON.parse(deleteText || '{}');
4122
+ if (flags.json) {
4123
+ process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
4124
+ process.exit(payload.deleted ? 0 : 3);
4125
+ }
4126
+ if (payload.deleted) {
4127
+ process.stdout.write(`Deleted ${payload.deleted_count} session${payload.deleted_count === 1 ? '' : 's'} (and ${payload.cleaned_memories || 0} associated memories).\n`);
4128
+ process.exit(0);
4129
+ } else {
4130
+ process.stderr.write(`No sessions matched the filters.\n`);
4131
+ process.exit(3);
4132
+ }
4133
+ return;
4134
+ }
4135
+
4136
+ // Single-delete path (existing).
4137
+ let fullId = idArg;
4138
+ if (idArg.length < 36) {
4139
+ const resolveRes = await server.makeRequest({
4140
+ jsonrpc: '2.0', id: `cli-delete-resolve-${Date.now()}`,
4141
+ method: 'tools/call',
4142
+ params: { name: 'get_context_bootstrap', arguments: { resume: true, session_id_hint: idArg } },
4143
+ });
4144
+ const resolveText = resolveRes?.result?.content?.[0]?.text;
4145
+ const resolvePayload = JSON.parse(resolveText || '{}');
4146
+ if (resolvePayload.resume_handoff?.session_id) {
4147
+ fullId = resolvePayload.resume_handoff.session_id;
4148
+ } else {
4149
+ process.stderr.write((resolvePayload.empty_hint || `Could not resolve "${idArg}" to a full session id.`) + '\n');
4150
+ process.exit(3);
4151
+ }
4152
+ }
4153
+
4154
+ const deleteRes = await server.makeRequest({
4155
+ jsonrpc: '2.0', id: `cli-delete-${Date.now()}`,
4156
+ method: 'tools/call',
4157
+ params: { name: 'delete_session', arguments: { session_id: fullId } },
4158
+ });
4159
+ const deleteText = deleteRes?.result?.content?.[0]?.text;
4160
+ const payload = JSON.parse(deleteText || '{}');
4161
+ if (flags.json) {
4162
+ process.stdout.write(JSON.stringify({ ...payload, session_id: fullId }, null, 2) + '\n');
4163
+ process.exit(payload.deleted ? 0 : 3);
4164
+ }
4165
+ if (payload.deleted) {
4166
+ process.stdout.write(`Deleted session ${fullId.slice(0,8)} (and ${payload.cleaned_memories || 0} associated memories).\n`);
4167
+ process.exit(0);
4168
+ } else {
4169
+ process.stderr.write(`Session ${fullId} not found (or not yours).\n`);
4170
+ process.exit(3);
4171
+ }
4172
+ } catch (err) {
4173
+ console.error('delete command failed:', err.message);
4174
+ process.exit(1);
4175
+ }
4176
+ }
4177
+
4178
+ function runEnvCli() {
4179
+ const { flags } = parseCliArgs(process.argv.slice(3));
4180
+ // Every env var the bridge reads, with default + description.
4181
+ const vars = [
4182
+ ['CF_MEMORY_API_KEY', 'required: your CF Memory API key'],
4183
+ ['CF_MEMORY_BASE_URL', `default: ${BASE_URL}; override to point at a different worker`],
4184
+ ['CF_MEMORY_WATCH_PATH', `default: cwd; use to override the repo path the bridge auto-detects`],
4185
+ ['CF_MEMORY_AUTO_REFRESH', 'default: off; "1"/"true" auto-refreshes stale chunks before re-querying retrieve_context'],
4186
+ ['CF_MEMORY_AUTO_CHECKPOINT', 'default: on; "off" disables the every-25-tool-calls auto-checkpoint'],
4187
+ ['CF_MEMORY_CHECKPOINT_EVERY', 'default: 25; tool-call interval between auto-checkpoints'],
4188
+ ['CF_MEMORY_AUTO_HANDOFF', 'default: on; "off" disables synthesis of a minimal handoff when end_session is called without one'],
4189
+ ['CF_MEMORY_SHUTDOWN_HANDOFF', 'default: on; "off" disables the SIGINT/SIGTERM handoff flush'],
4190
+ ['CF_MEMORY_PREWARM_RESUME', 'default: on; "off" disables the background resume prewarm at bridge startup'],
4191
+ ['CF_MEMORY_RESUME_DIFF', 'default: on; "off" disables git-diff injection into resume responses'],
4192
+ ['CF_MEMORY_DISK_CACHE', 'default: on; "off" disables ~/.cf-memory/handoff-*.json disk caching'],
4193
+ ['CF_MEMORY_NO_RETRY', 'default: off; "1" disables the single retry-on-transient-failure'],
4194
+ ['CF_MEMORY_LOG', 'default: off; "1" writes DEBUG/ERROR lines to ~/.cf-memory/bridge.log'],
4195
+ ['CF_MEMORY_LOG_FILE', 'default: ~/.cf-memory/bridge.log; override the log file path'],
4196
+ ['CF_MEMORY_TRACE', 'default: off; "1" emits structured MCP traces to stderr'],
4197
+ ['CF_MEMORY_PROGRESS', 'default: off; "true" streams indexing progress events to stderr'],
4198
+ ['CF_MEMORY_AUTO_WATCH', 'default: off; "1" starts a filesystem watcher that auto-reindexes on change'],
4199
+ ['DEBUG', 'default: off; "1" enables debug logging on stderr'],
4200
+ ['MCP_DEBUG', 'default: off; "1" enables MCP-protocol debug logging on stderr'],
4201
+ ];
4202
+ if (flags.json) {
4203
+ const out = {};
4204
+ for (const [name, desc] of vars) {
4205
+ out[name] = { value: process.env[name] || null, description: desc };
4206
+ }
4207
+ process.stdout.write(JSON.stringify(out, null, 2) + '\n');
4208
+ process.exit(0);
4209
+ }
4210
+ process.stdout.write(`cf-memory-mcp env vars (set = bold value, unset = "(unset)"):\n\n`);
4211
+ for (const [name, desc] of vars) {
4212
+ const val = process.env[name];
4213
+ const display = val ? `"${name === 'CF_MEMORY_API_KEY' && val ? '(redacted)' : val}"` : '(unset)';
4214
+ process.stdout.write(` ${name.padEnd(30)} ${display}\n ${desc}\n\n`);
4215
+ }
4216
+ process.exit(0);
4217
+ }
4218
+
4025
4219
  function runCompletionCli() {
4026
4220
  const shell = process.argv[3] || 'bash';
4027
- const commands = ['resume', 'list', 'checkpoint', 'status', 'clean', 'export', 'import', 'doctor', 'completion'];
4221
+ const commands = ['resume', 'list', 'checkpoint', 'status', 'clean', 'export', 'import', 'delete', 'doctor', 'env', 'completion'];
4028
4222
  const flags = ['--json', '-j', '--limit', '-n', '--md', '--all', '--force', '-f', '--version', '-v', '--help', '-h', '--diagnose'];
4029
4223
  if (shell === 'bash') {
4030
4224
  process.stdout.write(`# cf-memory-mcp bash completion
@@ -4133,6 +4327,20 @@ const PER_COMMAND_HELP = {
4133
4327
  --json, -j Emit a JSON list of checks.`,
4134
4328
  completion: `cf-memory-mcp completion [bash|zsh|fish]
4135
4329
  Output shell completion script. Pipe to your shell's completion dir.`,
4330
+ delete: `cf-memory-mcp delete <session-id-or-prefix> [--json]
4331
+ cf-memory-mcp delete [--status STATUS] [--older-than 30d] [--repo PATH] --yes [--json]
4332
+ Delete session row(s) and any session_summary memories tied to them.
4333
+ <session-id> Single delete: full UUID or short prefix (>=8 chars).
4334
+ --status STATUS Bulk filter by status (abandoned, completed, ...).
4335
+ --older-than 30d Bulk filter by age (suffixes: d, h, m).
4336
+ --repo PATH Bulk filter by repo_path.
4337
+ --yes, -y Required for bulk delete to confirm intent.
4338
+ --json, -j Emit JSON.
4339
+ Exit codes: 0 = deleted, 2 = bulk missing --yes, 3 = nothing matched.`,
4340
+ env: `cf-memory-mcp env [--json]
4341
+ Print all CF_MEMORY_* env vars the bridge reads, with their current
4342
+ values + descriptions. Useful for discovering knobs.
4343
+ --json, -j Emit JSON for scripts.`,
4136
4344
  };
4137
4345
 
4138
4346
  function printPerCommandHelp(cmd) {
@@ -4705,6 +4913,16 @@ if (process.argv[2] === 'completion') {
4705
4913
  return;
4706
4914
  }
4707
4915
 
4916
+ if (process.argv[2] === 'delete') {
4917
+ runDeleteCli();
4918
+ return;
4919
+ }
4920
+
4921
+ if (process.argv[2] === 'env') {
4922
+ runEnvCli();
4923
+ return;
4924
+ }
4925
+
4708
4926
  if (process.argv.includes('--diagnose')) {
4709
4927
  (async () => {
4710
4928
  console.log(`CF Memory MCP v${PACKAGE_VERSION} - Diagnostics`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cf-memory-mcp",
3
- "version": "3.30.0",
3
+ "version": "3.32.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": {