cf-memory-mcp 3.24.0 → 3.26.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.
@@ -3548,7 +3548,31 @@ class CFMemoryMCP {
3548
3548
  }
3549
3549
  }
3550
3550
 
3551
+ /**
3552
+ * Retry-aware wrapper. Tries once, then once more after a short
3553
+ * backoff if the first attempt got a network error, 5xx, or timeout.
3554
+ * Opt out with CF_MEMORY_NO_RETRY=1.
3555
+ */
3551
3556
  async makeRequest(message, extraHeaders = null) {
3557
+ const noRetry = process.env.CF_MEMORY_NO_RETRY === '1' || process.env.CF_MEMORY_NO_RETRY === 'true';
3558
+ const first = await this.makeRequestOnce(message, extraHeaders);
3559
+ if (noRetry) return first;
3560
+ // Retry only on transient failures: connection error, timeout, or 5xx.
3561
+ // We never retry application-level errors (4xx / -32602 / -32603 from
3562
+ // server logic) since those are likely to repeat.
3563
+ const isTransient = first?.error && (
3564
+ /Network error|Request timeout/.test(first.error.message || '') ||
3565
+ /HTTP 5\d\d/.test(first.error.message || '')
3566
+ );
3567
+ if (!isTransient) return first;
3568
+ // Brief backoff to let an edge blip clear.
3569
+ await new Promise(r => setTimeout(r, 250));
3570
+ _mcpTrace('RETRY', `id=${message.id} after transient error: ${first.error.message}`);
3571
+ const second = await this.makeRequestOnce(message, extraHeaders);
3572
+ return second;
3573
+ }
3574
+
3575
+ async makeRequestOnce(message, extraHeaders = null) {
3552
3576
  return new Promise((resolve) => {
3553
3577
  const serverUrl = this.useStreamableHttp ? this.streamableHttpUrl : this.legacyServerUrl;
3554
3578
  const url = new URL(serverUrl);
@@ -3588,6 +3612,18 @@ class CFMemoryMCP {
3588
3612
  res.on('end', () => {
3589
3613
  try {
3590
3614
  const response = JSON.parse(body);
3615
+ // Promote 5xx HTTP status to a transient error
3616
+ // envelope so the retry path can catch it. The
3617
+ // server may have returned a valid JSON-RPC error
3618
+ // wrapped in 200, OR a body-less 5xx with no
3619
+ // error field — handle both.
3620
+ if (res.statusCode && res.statusCode >= 500 && !response.error) {
3621
+ response.error = {
3622
+ code: -32603,
3623
+ message: `HTTP ${res.statusCode}`,
3624
+ data: `Server returned ${res.statusCode}`,
3625
+ };
3626
+ }
3591
3627
  resolve(response);
3592
3628
  } catch (error) {
3593
3629
  // Include HTTP status + body snippet so callers can
@@ -3596,12 +3632,16 @@ class CFMemoryMCP {
3596
3632
  // "Invalid JSON" alone hides whether the worker is
3597
3633
  // even reachable.
3598
3634
  const bodyPreview = body.slice(0, 200).replace(/\s+/g, ' ');
3635
+ // 5xx with non-JSON body is transient too (Cloudflare
3636
+ // edge HTML pages). Tag the error message so retry catches it.
3637
+ const transientTag = (res.statusCode && res.statusCode >= 500)
3638
+ ? `HTTP ${res.statusCode}` : `HTTP ${res.statusCode || '?'}`;
3599
3639
  resolve({
3600
3640
  jsonrpc: '2.0',
3601
3641
  id: message.id || null,
3602
3642
  error: {
3603
3643
  code: -32603,
3604
- message: `Invalid JSON response from server (HTTP ${res.statusCode})`,
3644
+ message: `Invalid JSON response from server (${transientTag})`,
3605
3645
  data: `${error.message}; body[0..200]=${bodyPreview}`
3606
3646
  }
3607
3647
  });
@@ -3750,6 +3790,8 @@ Usage:
3750
3790
  npx cf-memory-mcp list List recent handoffs for cwd
3751
3791
  npx cf-memory-mcp checkpoint ["<goal>"] Snapshot current state (keep_open)
3752
3792
  npx cf-memory-mcp status Show bridge state + server resume availability
3793
+ npx cf-memory-mcp clean Delete local disk cache for current cwd
3794
+ npx cf-memory-mcp clean --all Delete ALL local disk caches
3753
3795
  npx cf-memory-mcp --version Show version
3754
3796
  npx cf-memory-mcp --help Show this help
3755
3797
  npx cf-memory-mcp --diagnose Test connectivity and report issues
@@ -3771,7 +3813,7 @@ For more information, visit: https://github.com/johnlam90/cf-memory-mcp
3771
3813
  }
3772
3814
 
3773
3815
  // Parse positional + flag args for CLI subcommands. Returns
3774
- // { positional: string[], flags: { json: boolean, limit?: number } }.
3816
+ // { positional: string[], flags: { json, limit, md_path } }.
3775
3817
  function parseCliArgs(rest) {
3776
3818
  const positional = [];
3777
3819
  const flags = { json: false };
@@ -3784,6 +3826,11 @@ function parseCliArgs(rest) {
3784
3826
  } else if (a.startsWith('--limit=')) {
3785
3827
  const n = parseInt(a.slice('--limit='.length), 10);
3786
3828
  if (Number.isFinite(n) && n > 0) flags.limit = Math.min(n, 50);
3829
+ } else if (a === '--md') {
3830
+ // Next arg is path; supports `--md=path` too.
3831
+ flags.md_path = rest[++i];
3832
+ } else if (a.startsWith('--md=')) {
3833
+ flags.md_path = a.slice('--md='.length);
3787
3834
  } else positional.push(a);
3788
3835
  }
3789
3836
  return { positional, flags };
@@ -3828,6 +3875,27 @@ async function runResumeCli() {
3828
3875
  // 0 — handoff found and printed
3829
3876
  // 3 — no handoff found (lets scripts branch: `if ! cf-memory-mcp resume; then ...`)
3830
3877
  const found = !!payload.resume_handoff?.handoff;
3878
+
3879
+ // Warm the disk cache. The CLI bypasses the stdio dispatch hook
3880
+ // that normally writes the cache on a successful resume, so do
3881
+ // it here too. Means `cf-memory-mcp resume` ALSO populates the
3882
+ // offline-fallback cache, not just MCP-stdio usage.
3883
+ if (found) server.saveResumeToDisk(response);
3884
+
3885
+ // --md <path>: write the rendered markdown to a file instead of
3886
+ // (or in addition to) stdout. Useful for piping to a markdown
3887
+ // viewer or saving to the project.
3888
+ if (found && flags.md_path && payload.resume_prompt) {
3889
+ try {
3890
+ fs.writeFileSync(flags.md_path, payload.resume_prompt + '\n');
3891
+ process.stdout.write(`Wrote ${payload.resume_prompt.length} chars to ${flags.md_path}\n`);
3892
+ process.exit(0);
3893
+ } catch (err) {
3894
+ console.error(`Failed to write ${flags.md_path}: ${err.message}`);
3895
+ process.exit(1);
3896
+ }
3897
+ }
3898
+
3831
3899
  if (flags.json) {
3832
3900
  process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
3833
3901
  process.exit(found ? 0 : 3);
@@ -3887,6 +3955,8 @@ async function runListCli() {
3887
3955
  const payload = JSON.parse(text || '{}');
3888
3956
  const allList = Array.isArray(payload.recent_handoffs) ? payload.recent_handoffs : [];
3889
3957
  const list = flags.limit ? allList.slice(0, flags.limit) : allList;
3958
+ // Warm the disk cache when there's a primary handoff in the payload.
3959
+ if (payload.resume_handoff) server.saveResumeToDisk(response);
3890
3960
 
3891
3961
  if (flags.json) {
3892
3962
  process.stdout.write(JSON.stringify({
@@ -3928,6 +3998,49 @@ async function runListCli() {
3928
3998
  }
3929
3999
  }
3930
4000
 
4001
+ async function runCleanCli() {
4002
+ const { flags, positional } = parseCliArgs(process.argv.slice(3));
4003
+ const all = positional.includes('--all') || process.argv.includes('--all');
4004
+ const server = new CFMemoryMCP();
4005
+ server.logDebug = () => {};
4006
+ const removed = [];
4007
+ try {
4008
+ if (all) {
4009
+ // Clear ALL disk caches in ~/.cf-memory/.
4010
+ const dir = path.join(os.homedir(), '.cf-memory');
4011
+ if (fs.existsSync(dir)) {
4012
+ for (const entry of fs.readdirSync(dir)) {
4013
+ if (entry.startsWith('handoff-') && entry.endsWith('.json')) {
4014
+ const full = path.join(dir, entry);
4015
+ fs.unlinkSync(full);
4016
+ removed.push(full);
4017
+ }
4018
+ }
4019
+ }
4020
+ } else {
4021
+ const p = server.getDiskCachePath();
4022
+ if (p && fs.existsSync(p)) {
4023
+ fs.unlinkSync(p);
4024
+ removed.push(p);
4025
+ }
4026
+ }
4027
+ if (flags.json) {
4028
+ process.stdout.write(JSON.stringify({ removed }, null, 2) + '\n');
4029
+ process.exit(0);
4030
+ }
4031
+ if (removed.length === 0) {
4032
+ process.stdout.write('(nothing to clean)\n');
4033
+ } else {
4034
+ process.stdout.write(`Removed ${removed.length} cache file${removed.length === 1 ? '' : 's'}:\n`);
4035
+ for (const r of removed) process.stdout.write(` ${r}\n`);
4036
+ }
4037
+ process.exit(0);
4038
+ } catch (err) {
4039
+ console.error('clean command failed:', err.message);
4040
+ process.exit(1);
4041
+ }
4042
+ }
4043
+
3931
4044
  async function runStatusCli() {
3932
4045
  const { flags } = parseCliArgs(process.argv.slice(3));
3933
4046
  const server = new CFMemoryMCP();
@@ -4112,6 +4225,11 @@ if (process.argv[2] === 'status') {
4112
4225
  return;
4113
4226
  }
4114
4227
 
4228
+ if (process.argv[2] === 'clean') {
4229
+ runCleanCli();
4230
+ return;
4231
+ }
4232
+
4115
4233
  if (process.argv.includes('--diagnose')) {
4116
4234
  (async () => {
4117
4235
  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.24.0",
3
+ "version": "3.26.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": {