cf-memory-mcp 3.47.0 → 3.48.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/bin/cf-memory-mcp.js +120 -0
- package/package.json +1 -1
package/bin/cf-memory-mcp.js
CHANGED
|
@@ -2612,6 +2612,66 @@ class CFMemoryMCP {
|
|
|
2612
2612
|
}
|
|
2613
2613
|
}
|
|
2614
2614
|
|
|
2615
|
+
/**
|
|
2616
|
+
* Path for the on-disk implicit-session cache (keyed by cwd hash).
|
|
2617
|
+
* Lets the bridge survive restarts without losing the implicit
|
|
2618
|
+
* session — otherwise a fresh process would create a duplicate
|
|
2619
|
+
* session for the same cwd.
|
|
2620
|
+
*/
|
|
2621
|
+
getImplicitSessionDiskPath() {
|
|
2622
|
+
try {
|
|
2623
|
+
const cwd = process.env.CF_MEMORY_WATCH_PATH || process.cwd();
|
|
2624
|
+
const crypto = require('crypto');
|
|
2625
|
+
const hash = crypto.createHash('sha256').update(cwd).digest('hex').slice(0, 16);
|
|
2626
|
+
return path.join(os.homedir(), '.cf-memory', `implicit-${hash}.json`);
|
|
2627
|
+
} catch (_) { return null; }
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
/**
|
|
2631
|
+
* Load the disk-persisted implicit session for the current cwd, if any.
|
|
2632
|
+
* Stale entries (>1 day old) are ignored — sessions that haven't been
|
|
2633
|
+
* touched in a day are probably not "implicitly active" anymore.
|
|
2634
|
+
*/
|
|
2635
|
+
loadImplicitSessionFromDisk() {
|
|
2636
|
+
try {
|
|
2637
|
+
const p = this.getImplicitSessionDiskPath();
|
|
2638
|
+
if (!p || !fs.existsSync(p)) return null;
|
|
2639
|
+
const entry = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
2640
|
+
const ageMs = Date.now() - new Date(entry.cached_at).getTime();
|
|
2641
|
+
if (ageMs > 24 * 60 * 60 * 1000) return null; // stale
|
|
2642
|
+
return entry.session_id || null;
|
|
2643
|
+
} catch (_) { return null; }
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
/**
|
|
2647
|
+
* Persist the current implicit session id to disk. Called from
|
|
2648
|
+
* getOrCreateImplicitSession and cacheImplicitSessionFromResponse.
|
|
2649
|
+
* Atomic via .tmp + rename, same as the resume cache.
|
|
2650
|
+
*/
|
|
2651
|
+
saveImplicitSessionToDisk(sessionId) {
|
|
2652
|
+
try {
|
|
2653
|
+
const p = this.getImplicitSessionDiskPath();
|
|
2654
|
+
if (!p) return;
|
|
2655
|
+
const dir = path.dirname(p);
|
|
2656
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
2657
|
+
const entry = {
|
|
2658
|
+
cached_at: new Date().toISOString(),
|
|
2659
|
+
cwd: process.env.CF_MEMORY_WATCH_PATH || process.cwd(),
|
|
2660
|
+
session_id: sessionId,
|
|
2661
|
+
};
|
|
2662
|
+
const tmp = `${p}.tmp.${process.pid}`;
|
|
2663
|
+
try {
|
|
2664
|
+
fs.writeFileSync(tmp, JSON.stringify(entry, null, 2));
|
|
2665
|
+
fs.renameSync(tmp, p);
|
|
2666
|
+
} catch (renameErr) {
|
|
2667
|
+
try { fs.unlinkSync(tmp); } catch (_) { /* ignore */ }
|
|
2668
|
+
fs.writeFileSync(p, JSON.stringify(entry, null, 2));
|
|
2669
|
+
}
|
|
2670
|
+
} catch (err) {
|
|
2671
|
+
this.logDebug(`saveImplicitSessionToDisk failed: ${err && err.message}`);
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2615
2675
|
/**
|
|
2616
2676
|
* Return the implicit session_id for the current cwd, creating one via
|
|
2617
2677
|
* start_session when no session is active. Lets agents skip explicit
|
|
@@ -2621,6 +2681,7 @@ class CFMemoryMCP {
|
|
|
2621
2681
|
* The cache is keyed by cwd so multiple bridge processes for different
|
|
2622
2682
|
* repos don't share state. Sessions finalized via end_session WITHOUT
|
|
2623
2683
|
* keep_open clear the implicit cache so the next call starts fresh.
|
|
2684
|
+
* Persists to disk so bridge restarts don't churn new sessions.
|
|
2624
2685
|
*/
|
|
2625
2686
|
async getOrCreateImplicitSession() {
|
|
2626
2687
|
try {
|
|
@@ -2628,6 +2689,13 @@ class CFMemoryMCP {
|
|
|
2628
2689
|
if (!this._implicitSessionByCwd) this._implicitSessionByCwd = new Map();
|
|
2629
2690
|
const cached = this._implicitSessionByCwd.get(cwd);
|
|
2630
2691
|
if (cached) return cached;
|
|
2692
|
+
// Try disk cache (survives bridge restarts).
|
|
2693
|
+
const fromDisk = this.loadImplicitSessionFromDisk();
|
|
2694
|
+
if (fromDisk) {
|
|
2695
|
+
this._implicitSessionByCwd.set(cwd, fromDisk);
|
|
2696
|
+
_mcpTrace('IMPLICIT_SESSION', `restored implicit session=${fromDisk} from disk for cwd=${cwd}`);
|
|
2697
|
+
return fromDisk;
|
|
2698
|
+
}
|
|
2631
2699
|
|
|
2632
2700
|
// Lazily start a session, tagged with the same repo metadata as
|
|
2633
2701
|
// start_session would auto-attach if called explicitly.
|
|
@@ -2652,6 +2720,7 @@ class CFMemoryMCP {
|
|
|
2652
2720
|
} catch (_) { /* malformed response */ }
|
|
2653
2721
|
if (sessionId) {
|
|
2654
2722
|
this._implicitSessionByCwd.set(cwd, sessionId);
|
|
2723
|
+
this.saveImplicitSessionToDisk(sessionId);
|
|
2655
2724
|
_mcpTrace('IMPLICIT_SESSION', `created implicit session=${sessionId} for cwd=${cwd}`);
|
|
2656
2725
|
}
|
|
2657
2726
|
return sessionId;
|
|
@@ -2678,6 +2747,7 @@ class CFMemoryMCP {
|
|
|
2678
2747
|
const cwd = process.env.CF_MEMORY_WATCH_PATH || process.cwd();
|
|
2679
2748
|
if (!this._implicitSessionByCwd) this._implicitSessionByCwd = new Map();
|
|
2680
2749
|
this._implicitSessionByCwd.set(cwd, sessionId);
|
|
2750
|
+
this.saveImplicitSessionToDisk(sessionId);
|
|
2681
2751
|
_mcpTrace('IMPLICIT_SESSION', `cached implicit session=${sessionId} for cwd=${cwd}`);
|
|
2682
2752
|
} catch (err) {
|
|
2683
2753
|
this.logDebug(`cacheImplicitSessionFromResponse failed: ${err && err.message}`);
|
|
@@ -2708,6 +2778,12 @@ class CFMemoryMCP {
|
|
|
2708
2778
|
this._implicitSessionByCwd.delete(cwd);
|
|
2709
2779
|
_mcpTrace('IMPLICIT_SESSION', `cleared implicit session for cwd=${cwd}`);
|
|
2710
2780
|
}
|
|
2781
|
+
// Also drop the on-disk persistent cache so a future restart
|
|
2782
|
+
// doesn't pick the just-finalized session back up.
|
|
2783
|
+
try {
|
|
2784
|
+
const p = this.getImplicitSessionDiskPath();
|
|
2785
|
+
if (p && fs.existsSync(p)) fs.unlinkSync(p);
|
|
2786
|
+
} catch (_) { /* ignore */ }
|
|
2711
2787
|
} catch (err) {
|
|
2712
2788
|
this.logDebug(`clearImplicitSessionIfFinalized failed: ${err && err.message}`);
|
|
2713
2789
|
}
|
|
@@ -3981,6 +4057,10 @@ function parseCliArgs(rest) {
|
|
|
3981
4057
|
flags.branch_only = true;
|
|
3982
4058
|
} else if (a === '--repo-only') {
|
|
3983
4059
|
flags.repo_only = true;
|
|
4060
|
+
} else if (a === '--fields') {
|
|
4061
|
+
flags.fields = rest[++i];
|
|
4062
|
+
} else if (a.startsWith('--fields=')) {
|
|
4063
|
+
flags.fields = a.slice('--fields='.length);
|
|
3984
4064
|
} else if (a === '--older-than') {
|
|
3985
4065
|
// Accept "7d" / "30d" / "12h" / raw number (days).
|
|
3986
4066
|
const raw = rest[++i] || '';
|
|
@@ -4175,6 +4255,43 @@ async function runResumeCli() {
|
|
|
4175
4255
|
process.exit(0);
|
|
4176
4256
|
}
|
|
4177
4257
|
|
|
4258
|
+
// --fields a,b,c: multi-field extract. Print each field on its
|
|
4259
|
+
// own line (or as `name=value` for shell scripting). Supports
|
|
4260
|
+
// any top-level handoff field plus envelope metadata.
|
|
4261
|
+
if (flags.fields) {
|
|
4262
|
+
const envelope = payload.resume_handoff;
|
|
4263
|
+
if (!envelope) process.exit(3);
|
|
4264
|
+
const handoff = envelope.handoff || {};
|
|
4265
|
+
const fieldLookup = (name) => {
|
|
4266
|
+
switch (name) {
|
|
4267
|
+
case 'session_id': return envelope.session_id;
|
|
4268
|
+
case 'age_minutes': return envelope.handoff_age_minutes;
|
|
4269
|
+
case 'quality_score': return envelope.quality_score;
|
|
4270
|
+
case 'match_confidence': return envelope.match_confidence;
|
|
4271
|
+
case 'match_reason': return envelope.match_reason;
|
|
4272
|
+
case 'stale': return envelope.stale ? 'true' : 'false';
|
|
4273
|
+
default: return handoff[name];
|
|
4274
|
+
}
|
|
4275
|
+
};
|
|
4276
|
+
const wantNames = flags.fields.split(',').map(s => s.trim()).filter(Boolean);
|
|
4277
|
+
if (wantNames.length === 0) process.exit(3);
|
|
4278
|
+
const lines = [];
|
|
4279
|
+
for (const name of wantNames) {
|
|
4280
|
+
const v = fieldLookup(name);
|
|
4281
|
+
if (v === undefined || v === null) continue;
|
|
4282
|
+
if (Array.isArray(v)) {
|
|
4283
|
+
lines.push(`${name}=${v.length} item${v.length === 1 ? '' : 's'}`);
|
|
4284
|
+
} else if (typeof v === 'object') {
|
|
4285
|
+
lines.push(`${name}=${JSON.stringify(v)}`);
|
|
4286
|
+
} else {
|
|
4287
|
+
lines.push(`${name}=${v}`);
|
|
4288
|
+
}
|
|
4289
|
+
}
|
|
4290
|
+
if (lines.length === 0) process.exit(3);
|
|
4291
|
+
process.stdout.write(lines.join('\n') + '\n');
|
|
4292
|
+
process.exit(0);
|
|
4293
|
+
}
|
|
4294
|
+
|
|
4178
4295
|
// --age: print just handoff_age_minutes (single integer).
|
|
4179
4296
|
if (flags.age) {
|
|
4180
4297
|
const ageMin = payload.resume_handoff?.handoff_age_minutes;
|
|
@@ -4989,6 +5106,9 @@ const PER_COMMAND_HELP = {
|
|
|
4989
5106
|
--status-only Print just the status string.
|
|
4990
5107
|
--branch-only Print just the branch.
|
|
4991
5108
|
--repo-only Print just the repo_path.
|
|
5109
|
+
--fields <list> Print specified fields as "name=value", comma-sep.
|
|
5110
|
+
Supports any handoff field plus session_id, age_minutes,
|
|
5111
|
+
quality_score, match_confidence, match_reason, stale.
|
|
4992
5112
|
--json, -j Full bootstrap payload as JSON.
|
|
4993
5113
|
Exit codes: 0 = found, 3 = no handoff / no data, 4 = --validate found missing files.`,
|
|
4994
5114
|
list: `cf-memory-mcp list [--status S] [--since ISO] [--repo PATH] [--limit N] [--json]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cf-memory-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.48.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": {
|