cf-memory-mcp 3.47.0 → 3.49.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 +184 -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,14 @@ 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);
|
|
4064
|
+
} else if (a === '--diff') {
|
|
4065
|
+
flags.diff = rest[++i];
|
|
4066
|
+
} else if (a.startsWith('--diff=')) {
|
|
4067
|
+
flags.diff = a.slice('--diff='.length);
|
|
3984
4068
|
} else if (a === '--older-than') {
|
|
3985
4069
|
// Accept "7d" / "30d" / "12h" / raw number (days).
|
|
3986
4070
|
const raw = rest[++i] || '';
|
|
@@ -4175,6 +4259,43 @@ async function runResumeCli() {
|
|
|
4175
4259
|
process.exit(0);
|
|
4176
4260
|
}
|
|
4177
4261
|
|
|
4262
|
+
// --fields a,b,c: multi-field extract. Print each field on its
|
|
4263
|
+
// own line (or as `name=value` for shell scripting). Supports
|
|
4264
|
+
// any top-level handoff field plus envelope metadata.
|
|
4265
|
+
if (flags.fields) {
|
|
4266
|
+
const envelope = payload.resume_handoff;
|
|
4267
|
+
if (!envelope) process.exit(3);
|
|
4268
|
+
const handoff = envelope.handoff || {};
|
|
4269
|
+
const fieldLookup = (name) => {
|
|
4270
|
+
switch (name) {
|
|
4271
|
+
case 'session_id': return envelope.session_id;
|
|
4272
|
+
case 'age_minutes': return envelope.handoff_age_minutes;
|
|
4273
|
+
case 'quality_score': return envelope.quality_score;
|
|
4274
|
+
case 'match_confidence': return envelope.match_confidence;
|
|
4275
|
+
case 'match_reason': return envelope.match_reason;
|
|
4276
|
+
case 'stale': return envelope.stale ? 'true' : 'false';
|
|
4277
|
+
default: return handoff[name];
|
|
4278
|
+
}
|
|
4279
|
+
};
|
|
4280
|
+
const wantNames = flags.fields.split(',').map(s => s.trim()).filter(Boolean);
|
|
4281
|
+
if (wantNames.length === 0) process.exit(3);
|
|
4282
|
+
const lines = [];
|
|
4283
|
+
for (const name of wantNames) {
|
|
4284
|
+
const v = fieldLookup(name);
|
|
4285
|
+
if (v === undefined || v === null) continue;
|
|
4286
|
+
if (Array.isArray(v)) {
|
|
4287
|
+
lines.push(`${name}=${v.length} item${v.length === 1 ? '' : 's'}`);
|
|
4288
|
+
} else if (typeof v === 'object') {
|
|
4289
|
+
lines.push(`${name}=${JSON.stringify(v)}`);
|
|
4290
|
+
} else {
|
|
4291
|
+
lines.push(`${name}=${v}`);
|
|
4292
|
+
}
|
|
4293
|
+
}
|
|
4294
|
+
if (lines.length === 0) process.exit(3);
|
|
4295
|
+
process.stdout.write(lines.join('\n') + '\n');
|
|
4296
|
+
process.exit(0);
|
|
4297
|
+
}
|
|
4298
|
+
|
|
4178
4299
|
// --age: print just handoff_age_minutes (single integer).
|
|
4179
4300
|
if (flags.age) {
|
|
4180
4301
|
const ageMin = payload.resume_handoff?.handoff_age_minutes;
|
|
@@ -4275,6 +4396,65 @@ async function runResumeCli() {
|
|
|
4275
4396
|
process.exit(0);
|
|
4276
4397
|
}
|
|
4277
4398
|
|
|
4399
|
+
// --diff <other-id>: compare two handoffs. Shows added/removed
|
|
4400
|
+
// next_steps, branch shift, status change, files diff. Useful
|
|
4401
|
+
// for "what changed between yesterday's session and today's?".
|
|
4402
|
+
if (flags.diff) {
|
|
4403
|
+
if (!found) {
|
|
4404
|
+
process.stderr.write('No source handoff available to diff against.\n');
|
|
4405
|
+
process.exit(3);
|
|
4406
|
+
}
|
|
4407
|
+
const otherRes = await server.makeRequest({
|
|
4408
|
+
jsonrpc: '2.0', id: `cli-diff-${Date.now()}`,
|
|
4409
|
+
method: 'tools/call',
|
|
4410
|
+
params: { name: 'get_context_bootstrap', arguments: { resume: true, session_id_hint: flags.diff } },
|
|
4411
|
+
});
|
|
4412
|
+
const otherText = otherRes?.result?.content?.[0]?.text;
|
|
4413
|
+
const otherPayload = JSON.parse(otherText || '{}');
|
|
4414
|
+
const otherHandoff = otherPayload.resume_handoff?.handoff;
|
|
4415
|
+
if (!otherHandoff) {
|
|
4416
|
+
process.stderr.write(`Could not fetch handoff "${flags.diff}" to diff against.\n`);
|
|
4417
|
+
process.exit(3);
|
|
4418
|
+
}
|
|
4419
|
+
const h = payload.resume_handoff.handoff;
|
|
4420
|
+
const setOf = (arr, key) =>
|
|
4421
|
+
new Set((arr || []).map(x => typeof x === 'string' ? x : x[key]).filter(Boolean));
|
|
4422
|
+
const diffArr = (label, a, b, key) => {
|
|
4423
|
+
const A = setOf(a, key); const B = setOf(b, key);
|
|
4424
|
+
const added = [...A].filter(x => !B.has(x));
|
|
4425
|
+
const removed = [...B].filter(x => !A.has(x));
|
|
4426
|
+
const lines = [];
|
|
4427
|
+
if (added.length) lines.push(` + ${label}: ${added.join(', ')}`);
|
|
4428
|
+
if (removed.length) lines.push(` - ${label}: ${removed.join(', ')}`);
|
|
4429
|
+
return lines.join('\n');
|
|
4430
|
+
};
|
|
4431
|
+
|
|
4432
|
+
const aShort = (payload.resume_handoff.session_id || '').slice(0, 8);
|
|
4433
|
+
const bShort = (otherPayload.resume_handoff.session_id || '').slice(0, 8);
|
|
4434
|
+
const lines = [`Diff: ${aShort} (this) vs ${bShort} (other)`, ''];
|
|
4435
|
+
// Scalar fields
|
|
4436
|
+
const scalars = ['goal', 'status', 'branch', 'repo_path'];
|
|
4437
|
+
for (const f of scalars) {
|
|
4438
|
+
if (h[f] !== otherHandoff[f]) {
|
|
4439
|
+
lines.push(` ~ ${f}: "${otherHandoff[f] || ''}" → "${h[f] || ''}"`);
|
|
4440
|
+
}
|
|
4441
|
+
}
|
|
4442
|
+
// Array fields
|
|
4443
|
+
const arrDiff = diffArr('next_steps', h.next_steps, otherHandoff.next_steps);
|
|
4444
|
+
if (arrDiff) lines.push(arrDiff);
|
|
4445
|
+
const blockerDiff = diffArr('blockers', h.blockers, otherHandoff.blockers);
|
|
4446
|
+
if (blockerDiff) lines.push(blockerDiff);
|
|
4447
|
+
const decisionDiff = diffArr('decisions', h.decisions, otherHandoff.decisions);
|
|
4448
|
+
if (decisionDiff) lines.push(decisionDiff);
|
|
4449
|
+
const fileDiff = diffArr('files_touched', h.files_touched, otherHandoff.files_touched, 'path');
|
|
4450
|
+
if (fileDiff) lines.push(fileDiff);
|
|
4451
|
+
const anchorDiff = diffArr('code_anchors', h.code_anchors, otherHandoff.code_anchors, 'file_path');
|
|
4452
|
+
if (anchorDiff) lines.push(anchorDiff);
|
|
4453
|
+
if (lines.length === 2) lines.push(' (no differences)');
|
|
4454
|
+
process.stdout.write(lines.join('\n') + '\n');
|
|
4455
|
+
process.exit(0);
|
|
4456
|
+
}
|
|
4457
|
+
|
|
4278
4458
|
// --validate: verify the handoff's file references still exist
|
|
4279
4459
|
// on disk. Resolves each path relative to the handoff's repo_path
|
|
4280
4460
|
// (or cwd). Reports per-path status. Useful before resuming to
|
|
@@ -4981,6 +5161,7 @@ const PER_COMMAND_HELP = {
|
|
|
4981
5161
|
--decisions-only decisions, one per line.
|
|
4982
5162
|
--blockers-only open blockers, one per line.
|
|
4983
5163
|
--chain Walk parent_session_id back; show the thread history.
|
|
5164
|
+
--diff <other-id> Compare this handoff against another (added/removed/changed fields).
|
|
4984
5165
|
--validate Check that files_touched + code_anchors still exist locally.
|
|
4985
5166
|
--raw Print just the raw handoff JSON (no envelope metadata).
|
|
4986
5167
|
--age Print handoff_age_minutes (single integer).
|
|
@@ -4989,6 +5170,9 @@ const PER_COMMAND_HELP = {
|
|
|
4989
5170
|
--status-only Print just the status string.
|
|
4990
5171
|
--branch-only Print just the branch.
|
|
4991
5172
|
--repo-only Print just the repo_path.
|
|
5173
|
+
--fields <list> Print specified fields as "name=value", comma-sep.
|
|
5174
|
+
Supports any handoff field plus session_id, age_minutes,
|
|
5175
|
+
quality_score, match_confidence, match_reason, stale.
|
|
4992
5176
|
--json, -j Full bootstrap payload as JSON.
|
|
4993
5177
|
Exit codes: 0 = found, 3 = no handoff / no data, 4 = --validate found missing files.`,
|
|
4994
5178
|
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.49.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": {
|