cf-memory-mcp 3.16.0 → 3.18.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 +159 -3
- package/package.json +1 -1
package/bin/cf-memory-mcp.js
CHANGED
|
@@ -213,8 +213,9 @@ const TOOLS_LIST = [
|
|
|
213
213
|
repo_path: { type: 'string', description: 'Absolute project root path. Bridge auto-fills from cwd if omitted.' },
|
|
214
214
|
project_id: { type: 'string', description: 'Canonical project id (proj_...). Used before repo_path when set.' },
|
|
215
215
|
branch: { type: 'string', description: 'Current git branch. Mismatch downgrades match_confidence but does not hide. Bridge auto-fills from git.' },
|
|
216
|
-
session_id_hint: { type: 'string', description: 'Resume a specific session by id (overrides repo/branch matching). Useful after seeing recent_handoffs.' },
|
|
217
|
-
max_age_minutes: { type: 'number', description: 'Hard cutoff: hide handoffs older than this many minutes. Without it, older handoffs surface with reduced confidence.' }
|
|
216
|
+
session_id_hint: { type: 'string', description: 'Resume a specific session by id (overrides repo/branch matching). Full UUID or short prefix (>=8 chars). Useful after seeing recent_handoffs.' },
|
|
217
|
+
max_age_minutes: { type: 'number', description: 'Hard cutoff: hide handoffs older than this many minutes. Without it, older handoffs surface with reduced confidence.' },
|
|
218
|
+
goal_contains: { type: 'string', description: 'Substring filter on handoff content (goal/notes/decisions). Case-insensitive. Lets agents find a specific thread by what it was about.' }
|
|
218
219
|
}
|
|
219
220
|
}
|
|
220
221
|
},
|
|
@@ -932,6 +933,7 @@ class CFMemoryMCP {
|
|
|
932
933
|
message.params.arguments?.resume) {
|
|
933
934
|
const cached = this.maybeServePrewarmedResume(message);
|
|
934
935
|
if (cached) {
|
|
936
|
+
this.injectRepoDiffIntoResume(cached);
|
|
935
937
|
_mcpTrace('RESUME_PREWARM', `served get_context_bootstrap id=${message.id} from cache`);
|
|
936
938
|
process.stdout.write(JSON.stringify(cached) + '\n');
|
|
937
939
|
return;
|
|
@@ -986,6 +988,16 @@ class CFMemoryMCP {
|
|
|
986
988
|
this.cacheImplicitSessionFromResponse(response);
|
|
987
989
|
}
|
|
988
990
|
|
|
991
|
+
// Inject bridge-side git diff into resume responses. Lets the
|
|
992
|
+
// agent see what's changed in the repo since the handoff was
|
|
993
|
+
// written — files modified, commits added, branch moves —
|
|
994
|
+
// without having to call git themselves.
|
|
995
|
+
if (message.method === 'tools/call' &&
|
|
996
|
+
message.params?.name === 'get_context_bootstrap' &&
|
|
997
|
+
message.params.arguments?.resume) {
|
|
998
|
+
this.injectRepoDiffIntoResume(response);
|
|
999
|
+
}
|
|
1000
|
+
|
|
989
1001
|
// Send response to stdout
|
|
990
1002
|
process.stdout.write(JSON.stringify(response) + '\n');
|
|
991
1003
|
|
|
@@ -2502,6 +2514,97 @@ class CFMemoryMCP {
|
|
|
2502
2514
|
}
|
|
2503
2515
|
}
|
|
2504
2516
|
|
|
2517
|
+
/**
|
|
2518
|
+
* Mutates a get_context_bootstrap response to attach a `repo_diff`
|
|
2519
|
+
* field describing what's changed in the local repo since the
|
|
2520
|
+
* handoff. Bridge-only — server has no git access. Best-effort, silent
|
|
2521
|
+
* on failure.
|
|
2522
|
+
*
|
|
2523
|
+
* Opt out via CF_MEMORY_RESUME_DIFF=off.
|
|
2524
|
+
*/
|
|
2525
|
+
injectRepoDiffIntoResume(response) {
|
|
2526
|
+
try {
|
|
2527
|
+
const opt = process.env.CF_MEMORY_RESUME_DIFF;
|
|
2528
|
+
if (opt === '0' || opt === 'false' || opt === 'off') return;
|
|
2529
|
+
const text = response?.result?.content?.[0]?.text;
|
|
2530
|
+
if (!text) return;
|
|
2531
|
+
let parsed;
|
|
2532
|
+
try { parsed = JSON.parse(text); } catch (_) { return; }
|
|
2533
|
+
const envelope = parsed?.resume_handoff;
|
|
2534
|
+
if (!envelope) return;
|
|
2535
|
+
|
|
2536
|
+
const meta = this.getRepoMetadata();
|
|
2537
|
+
const repoRoot = meta.repo_path;
|
|
2538
|
+
if (!repoRoot) return;
|
|
2539
|
+
// Reference point: prefer ended_at, fall back to started_at.
|
|
2540
|
+
const since = envelope.ended_at || envelope.started_at;
|
|
2541
|
+
if (!since) return;
|
|
2542
|
+
|
|
2543
|
+
const diff = this.computeRepoDiff(repoRoot, since, envelope.handoff?.branch || meta.branch);
|
|
2544
|
+
if (diff) {
|
|
2545
|
+
envelope.repo_diff = diff;
|
|
2546
|
+
// Re-serialize the modified envelope back into the response.
|
|
2547
|
+
response.result.content[0].text = JSON.stringify(parsed);
|
|
2548
|
+
}
|
|
2549
|
+
} catch (err) {
|
|
2550
|
+
this.logDebug(`injectRepoDiffIntoResume failed: ${err && err.message}`);
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
/**
|
|
2555
|
+
* Run a bounded git query in the repo root to summarize changes since
|
|
2556
|
+
* the handoff was written. Returns { commits, files_changed,
|
|
2557
|
+
* branch_now, branch_at_handoff } or null if git isn't available.
|
|
2558
|
+
*/
|
|
2559
|
+
computeRepoDiff(repoRoot, sinceIso, handoffBranch) {
|
|
2560
|
+
try {
|
|
2561
|
+
const { execSync } = require('child_process');
|
|
2562
|
+
const run = (cmd) => execSync(cmd, {
|
|
2563
|
+
cwd: repoRoot,
|
|
2564
|
+
encoding: 'utf8',
|
|
2565
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
2566
|
+
timeout: 1000,
|
|
2567
|
+
});
|
|
2568
|
+
// Git accepts ISO timestamps via --since
|
|
2569
|
+
const commitsOut = run(`git log --since='${sinceIso}' --pretty=format:'%h %s' -n 25`);
|
|
2570
|
+
const commits = commitsOut.split('\n').filter(Boolean).map(line => {
|
|
2571
|
+
const [hash, ...rest] = line.split(' ');
|
|
2572
|
+
return { hash, subject: rest.join(' ').slice(0, 120) };
|
|
2573
|
+
});
|
|
2574
|
+
|
|
2575
|
+
// Files changed since the handoff. Compare HEAD against the
|
|
2576
|
+
// commit closest to the handoff time. If no commits since,
|
|
2577
|
+
// diff vs. HEAD~0 returns nothing.
|
|
2578
|
+
let filesChanged = [];
|
|
2579
|
+
try {
|
|
2580
|
+
const namesOut = run(`git log --since='${sinceIso}' --name-only --pretty=format:''`);
|
|
2581
|
+
const names = new Set(namesOut.split('\n').map(s => s.trim()).filter(Boolean));
|
|
2582
|
+
filesChanged = Array.from(names).slice(0, 50);
|
|
2583
|
+
} catch (_) { /* empty diff is fine */ }
|
|
2584
|
+
|
|
2585
|
+
// Branch information.
|
|
2586
|
+
let branchNow = null;
|
|
2587
|
+
try {
|
|
2588
|
+
branchNow = run('git symbolic-ref --short HEAD').trim();
|
|
2589
|
+
} catch (_) { /* detached HEAD */ }
|
|
2590
|
+
|
|
2591
|
+
const diff = {
|
|
2592
|
+
commits_since_handoff: commits,
|
|
2593
|
+
files_changed_count: filesChanged.length,
|
|
2594
|
+
files_changed: filesChanged,
|
|
2595
|
+
};
|
|
2596
|
+
if (branchNow) diff.branch_now = branchNow;
|
|
2597
|
+
if (handoffBranch) diff.branch_at_handoff = handoffBranch;
|
|
2598
|
+
if (handoffBranch && branchNow && handoffBranch !== branchNow) {
|
|
2599
|
+
diff.branch_changed = true;
|
|
2600
|
+
}
|
|
2601
|
+
return diff;
|
|
2602
|
+
} catch (err) {
|
|
2603
|
+
this.logDebug(`computeRepoDiff failed: ${err && err.message}`);
|
|
2604
|
+
return null;
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2505
2608
|
/**
|
|
2506
2609
|
* Build a minimal handoff packet from observed bridge activity. Used by
|
|
2507
2610
|
* end_session auto-synthesis, the periodic auto-checkpoint, and the
|
|
@@ -2535,10 +2638,63 @@ class CFMemoryMCP {
|
|
|
2535
2638
|
// sees the recent trajectory ("what was I doing").
|
|
2536
2639
|
const timelineNote = this.formatActivityTimeline();
|
|
2537
2640
|
const baseNote = `Auto-synthesized by cf-memory-mcp bridge. Agent did not provide a handoff. Tool calls observed: ${this._toolCallCount || 0}. Set CF_MEMORY_AUTO_HANDOFF=off to disable.`;
|
|
2538
|
-
|
|
2641
|
+
|
|
2642
|
+
// Workspace state: uncommitted changes + recent commits give the
|
|
2643
|
+
// next chat real signal about what's been happening in the repo,
|
|
2644
|
+
// not just what tools the agent called.
|
|
2645
|
+
const workspaceState = this.getGitWorkspaceState();
|
|
2646
|
+
const sections = [baseNote];
|
|
2647
|
+
if (timelineNote) sections.push(`Recent activity:\n${timelineNote}`);
|
|
2648
|
+
if (workspaceState) sections.push(`Workspace state:\n${workspaceState}`);
|
|
2649
|
+
handoff.notes = sections.join('\n\n');
|
|
2539
2650
|
return handoff;
|
|
2540
2651
|
}
|
|
2541
2652
|
|
|
2653
|
+
/**
|
|
2654
|
+
* Snapshot the local git workspace: uncommitted file count, top
|
|
2655
|
+
* changed files, last 3 commit messages. Used by auto-synthesized
|
|
2656
|
+
* handoffs so the next chat sees real workspace state, not just
|
|
2657
|
+
* tool-call activity. Best-effort: returns "" when git unavailable.
|
|
2658
|
+
*/
|
|
2659
|
+
getGitWorkspaceState() {
|
|
2660
|
+
try {
|
|
2661
|
+
const meta = this.getRepoMetadata();
|
|
2662
|
+
if (!meta.repo_path) return '';
|
|
2663
|
+
const { execSync } = require('child_process');
|
|
2664
|
+
const run = (cmd) => execSync(cmd, {
|
|
2665
|
+
cwd: meta.repo_path,
|
|
2666
|
+
encoding: 'utf8',
|
|
2667
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
2668
|
+
timeout: 500,
|
|
2669
|
+
});
|
|
2670
|
+
const lines = [];
|
|
2671
|
+
// Uncommitted: `git status --porcelain` is one line per changed file.
|
|
2672
|
+
try {
|
|
2673
|
+
const status = run('git status --porcelain').trim();
|
|
2674
|
+
if (status) {
|
|
2675
|
+
const files = status.split('\n').slice(0, 8);
|
|
2676
|
+
const more = status.split('\n').length > 8 ? ` (+ ${status.split('\n').length - 8} more)` : '';
|
|
2677
|
+
lines.push(` Uncommitted changes:${more}`);
|
|
2678
|
+
files.forEach(f => lines.push(` ${f}`));
|
|
2679
|
+
} else {
|
|
2680
|
+
lines.push(' Working tree clean.');
|
|
2681
|
+
}
|
|
2682
|
+
} catch (_) { /* git status failed */ }
|
|
2683
|
+
// Last 3 commits — short hash + subject only.
|
|
2684
|
+
try {
|
|
2685
|
+
const log = run('git log -3 --pretty=format:%h\\ %s').trim();
|
|
2686
|
+
if (log) {
|
|
2687
|
+
lines.push(' Recent commits:');
|
|
2688
|
+
log.split('\n').forEach(c => lines.push(` ${c}`));
|
|
2689
|
+
}
|
|
2690
|
+
} catch (_) { /* git log failed */ }
|
|
2691
|
+
return lines.join('\n');
|
|
2692
|
+
} catch (err) {
|
|
2693
|
+
this.logDebug(`getGitWorkspaceState failed: ${err && err.message}`);
|
|
2694
|
+
return '';
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2542
2698
|
/**
|
|
2543
2699
|
* Condense an MCP tool call into a single-line summary. Used for the
|
|
2544
2700
|
* activity timeline so the synthesized handoff includes "what was I
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cf-memory-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.18.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": {
|