cf-memory-mcp 3.60.0 → 3.62.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.
@@ -2672,6 +2672,42 @@ class CFMemoryMCP {
2672
2672
  }
2673
2673
  }
2674
2674
 
2675
+ /**
2676
+ * Drop the implicit-session disk cache iff the cached session id is
2677
+ * in `deletedSessionIds`. Called from the delete CLI so a subsequent
2678
+ * end_session against the implicit cache won't write to a session
2679
+ * that no longer exists on the server. Also clears the in-memory map
2680
+ * for the matching cwd. Returns true when something was dropped.
2681
+ */
2682
+ invalidateImplicitSessionIfMatches(deletedSessionIds) {
2683
+ if (!deletedSessionIds || !deletedSessionIds.length) return false;
2684
+ const matchSet = new Set(deletedSessionIds);
2685
+ let dropped = false;
2686
+ try {
2687
+ const implicitPath = this.getImplicitSessionDiskPath();
2688
+ if (implicitPath && fs.existsSync(implicitPath)) {
2689
+ const entry = JSON.parse(fs.readFileSync(implicitPath, 'utf8'));
2690
+ if (matchSet.has(entry.session_id)) {
2691
+ fs.unlinkSync(implicitPath);
2692
+ dropped = true;
2693
+ }
2694
+ }
2695
+ } catch (_) { /* non-fatal */ }
2696
+ // Also evict in-memory entries so a long-lived bridge stops
2697
+ // auto-filling the deleted id.
2698
+ try {
2699
+ if (this._implicitSessionByCwd && this._implicitSessionByCwd.size) {
2700
+ for (const [cwd, sid] of this._implicitSessionByCwd.entries()) {
2701
+ if (matchSet.has(sid)) {
2702
+ this._implicitSessionByCwd.delete(cwd);
2703
+ dropped = true;
2704
+ }
2705
+ }
2706
+ }
2707
+ } catch (_) { /* non-fatal */ }
2708
+ return dropped;
2709
+ }
2710
+
2675
2711
  /**
2676
2712
  * Return the implicit session_id for the current cwd, creating one via
2677
2713
  * start_session when no session is active. Lets agents skip explicit
@@ -4051,6 +4087,14 @@ function parseCliArgs(rest) {
4051
4087
  flags.raw = true;
4052
4088
  } else if (a === '--copy') {
4053
4089
  flags.copy = true;
4090
+ } else if (a === '--watch') {
4091
+ flags.watch = true;
4092
+ } else if (a === '--interval') {
4093
+ const n = parseInt(rest[++i], 10);
4094
+ flags.interval = Number.isFinite(n) && n >= 5 ? Math.min(n, 600) : 30;
4095
+ } else if (a.startsWith('--interval=')) {
4096
+ const n = parseInt(a.slice('--interval='.length), 10);
4097
+ flags.interval = Number.isFinite(n) && n >= 5 ? Math.min(n, 600) : 30;
4054
4098
  } else if (a === '--card') {
4055
4099
  flags.card = true;
4056
4100
  } else if (a === '--short') {
@@ -4127,6 +4171,42 @@ async function runResumeCli() {
4127
4171
  process.exit(1);
4128
4172
  }
4129
4173
  const { positional, flags } = parseCliArgs(process.argv.slice(3));
4174
+
4175
+ // --watch: re-spawn this command without --watch on an interval,
4176
+ // clearing the screen between renders. Bounded by --interval (5-600s,
4177
+ // default 30s). Ctrl+C exits cleanly.
4178
+ if (flags.watch) {
4179
+ const interval = (flags.interval || 30) * 1000;
4180
+ const { spawnSync } = require('child_process');
4181
+ // Strip --watch from argv for the inner call. Pass everything else.
4182
+ const innerArgs = process.argv.slice(3).filter(a => a !== '--watch' && a !== '--interval' && !a.startsWith('--interval='))
4183
+ .filter((_, i, arr) => {
4184
+ // Also drop the value after --interval if it was separate.
4185
+ const prev = arr[i - 1];
4186
+ return prev !== '--interval';
4187
+ });
4188
+ let stopped = false;
4189
+ const onSig = () => { stopped = true; process.stderr.write('\n[watch] stopped\n'); process.exit(0); };
4190
+ process.on('SIGINT', onSig);
4191
+ process.on('SIGTERM', onSig);
4192
+ while (!stopped) {
4193
+ // Clear screen (ANSI). Skip when not a TTY (just append).
4194
+ if (process.stdout.isTTY) process.stdout.write('\x1b[2J\x1b[H');
4195
+ process.stdout.write(`[watch] ${new Date().toISOString()} (every ${interval/1000}s, Ctrl+C to exit)\n\n`);
4196
+ try {
4197
+ const res = spawnSync(process.execPath, [__filename, 'resume', ...innerArgs], {
4198
+ stdio: ['ignore', 'inherit', 'inherit'],
4199
+ });
4200
+ if (res.status !== 0 && res.status !== 3) {
4201
+ process.stderr.write(`\n[watch] resume exited ${res.status}\n`);
4202
+ }
4203
+ } catch (err) {
4204
+ process.stderr.write(`\n[watch] error: ${err.message}\n`);
4205
+ }
4206
+ await new Promise(r => setTimeout(r, interval));
4207
+ }
4208
+ process.exit(0);
4209
+ }
4130
4210
  const server = new CFMemoryMCP();
4131
4211
  server.logDebug = () => {};
4132
4212
  server.logError = (...a) => process.stderr.write(a.join(' ') + '\n');
@@ -5171,6 +5251,12 @@ async function runDeleteCli() {
5171
5251
  });
5172
5252
  const deleteText = deleteRes?.result?.content?.[0]?.text;
5173
5253
  const payload = JSON.parse(deleteText || '{}');
5254
+ // Implicit-cache invalidation: if any deleted id matches the
5255
+ // current cwd's implicit session, drop the cache so the next
5256
+ // end_session creates a fresh one.
5257
+ if (payload.deleted) {
5258
+ server.invalidateImplicitSessionIfMatches(payload.deleted_session_ids || []);
5259
+ }
5174
5260
  if (flags.json) {
5175
5261
  process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
5176
5262
  process.exit(payload.deleted ? 0 : 3);
@@ -5210,6 +5296,12 @@ async function runDeleteCli() {
5210
5296
  });
5211
5297
  const deleteText = deleteRes?.result?.content?.[0]?.text;
5212
5298
  const payload = JSON.parse(deleteText || '{}');
5299
+ // Drop the implicit-session disk cache if the deleted id matches
5300
+ // it. Otherwise a subsequent end_session would try to write to a
5301
+ // session that no longer exists.
5302
+ if (payload.deleted) {
5303
+ server.invalidateImplicitSessionIfMatches([fullId]);
5304
+ }
5213
5305
  if (flags.json) {
5214
5306
  process.stdout.write(JSON.stringify({ ...payload, session_id: fullId }, null, 2) + '\n');
5215
5307
  process.exit(payload.deleted ? 0 : 3);
@@ -5467,6 +5559,7 @@ const PER_COMMAND_HELP = {
5467
5559
  --md <path> Write the markdown to a file instead of stdout.
5468
5560
  --copy Pipe the markdown to the platform clipboard (pbcopy/xclip/wl-copy/clip).
5469
5561
  --card Compact 2-3 line status card (for terminal status widgets).
5562
+ --watch [--interval N] Re-render every N seconds (default 30, range 5-600). Ctrl+C to exit.
5470
5563
  Extract flags (pick one; each exits 3 if the section is empty):
5471
5564
  --next-only First next_step only (for shell prompts).
5472
5565
  --next-steps All next_steps, numbered.
@@ -6052,11 +6145,15 @@ async function runCleanCli() {
6052
6145
  const removed = [];
6053
6146
  try {
6054
6147
  if (all) {
6055
- // Clear ALL disk caches in ~/.cf-memory/.
6148
+ // Clear ALL disk caches in ~/.cf-memory/: handoff-*.json
6149
+ // (offline resume cache) AND implicit-*.json (persistent
6150
+ // implicit session id per cwd).
6056
6151
  const dir = path.join(os.homedir(), '.cf-memory');
6057
6152
  if (fs.existsSync(dir)) {
6058
6153
  for (const entry of fs.readdirSync(dir)) {
6059
- if (entry.startsWith('handoff-') && entry.endsWith('.json')) {
6154
+ const isHandoff = entry.startsWith('handoff-') && entry.endsWith('.json');
6155
+ const isImplicit = entry.startsWith('implicit-') && entry.endsWith('.json');
6156
+ if (isHandoff || isImplicit) {
6060
6157
  const full = path.join(dir, entry);
6061
6158
  fs.unlinkSync(full);
6062
6159
  removed.push(full);
@@ -6064,10 +6161,17 @@ async function runCleanCli() {
6064
6161
  }
6065
6162
  }
6066
6163
  } else {
6067
- const p = server.getDiskCachePath();
6068
- if (p && fs.existsSync(p)) {
6069
- fs.unlinkSync(p);
6070
- removed.push(p);
6164
+ // cwd-only mode: drop both the handoff cache AND the
6165
+ // implicit session cache for this cwd.
6166
+ const handoffPath = server.getDiskCachePath();
6167
+ if (handoffPath && fs.existsSync(handoffPath)) {
6168
+ fs.unlinkSync(handoffPath);
6169
+ removed.push(handoffPath);
6170
+ }
6171
+ const implicitPath = server.getImplicitSessionDiskPath();
6172
+ if (implicitPath && fs.existsSync(implicitPath)) {
6173
+ fs.unlinkSync(implicitPath);
6174
+ removed.push(implicitPath);
6071
6175
  }
6072
6176
  }
6073
6177
  if (flags.json) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cf-memory-mcp",
3
- "version": "3.60.0",
3
+ "version": "3.62.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": {