cf-memory-mcp 3.61.0 → 3.63.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 +204 -8
- package/package.json +1 -1
package/bin/cf-memory-mcp.js
CHANGED
|
@@ -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
|
|
@@ -3919,7 +3955,7 @@ if (process.argv.includes('--version') || process.argv.includes('-v')) {
|
|
|
3919
3955
|
|
|
3920
3956
|
// Global --help only when no subcommand is present. With a subcommand, fall
|
|
3921
3957
|
// through to the per-command help dispatch below.
|
|
3922
|
-
const SUBCOMMANDS = new Set(['resume', 'list', 'checkpoint', 'status', 'clean', 'export', 'import', 'doctor', 'completion', 'delete', 'env', 'history', 'link', 'explain', 'gha']);
|
|
3958
|
+
const SUBCOMMANDS = new Set(['resume', 'list', 'search', 'checkpoint', 'status', 'clean', 'export', 'import', 'doctor', 'completion', 'delete', 'env', 'history', 'link', 'explain', 'gha']);
|
|
3923
3959
|
const hasSubcommand = process.argv[2] && SUBCOMMANDS.has(process.argv[2]);
|
|
3924
3960
|
|
|
3925
3961
|
if (!hasSubcommand && (process.argv.includes('--help') || process.argv.includes('-h'))) {
|
|
@@ -3932,6 +3968,7 @@ Usage:
|
|
|
3932
3968
|
npx cf-memory-mcp Start the MCP server
|
|
3933
3969
|
npx cf-memory-mcp resume [id] Print the prior resume handoff (markdown)
|
|
3934
3970
|
npx cf-memory-mcp list List recent handoffs for cwd (status-ranked)
|
|
3971
|
+
npx cf-memory-mcp search <query> Find handoffs by goal/notes/decisions text (cross-repo default)
|
|
3935
3972
|
npx cf-memory-mcp history List all handoffs for cwd, chronological
|
|
3936
3973
|
npx cf-memory-mcp checkpoint ["<goal>"] Snapshot current state (keep_open)
|
|
3937
3974
|
npx cf-memory-mcp link <child> --parent <id> Retroactively link a child session to a parent
|
|
@@ -4122,6 +4159,10 @@ function parseCliArgs(rest) {
|
|
|
4122
4159
|
flags.since = a.slice('--since='.length);
|
|
4123
4160
|
} else if (a === '--yes' || a === '-y') {
|
|
4124
4161
|
flags.yes = true;
|
|
4162
|
+
} else if (a === '--ids-only') {
|
|
4163
|
+
flags.ids_only = true;
|
|
4164
|
+
} else if (a === '--here') {
|
|
4165
|
+
flags.here = true;
|
|
4125
4166
|
} else positional.push(a);
|
|
4126
4167
|
}
|
|
4127
4168
|
return { positional, flags };
|
|
@@ -4959,6 +5000,116 @@ async function runListCli() {
|
|
|
4959
5000
|
}
|
|
4960
5001
|
}
|
|
4961
5002
|
|
|
5003
|
+
async function runSearchCli() {
|
|
5004
|
+
if (!API_KEY) {
|
|
5005
|
+
console.error('Error: CF_MEMORY_API_KEY environment variable is required');
|
|
5006
|
+
process.exit(1);
|
|
5007
|
+
}
|
|
5008
|
+
const { positional, flags } = parseCliArgs(process.argv.slice(3));
|
|
5009
|
+
const query = positional[0];
|
|
5010
|
+
if (!query) {
|
|
5011
|
+
console.error('Usage: cf-memory-mcp search <query> [--repo <path>] [--project-id <id>] [--status <s>] [--since <iso>] [--limit N] [--json] [--ids-only]');
|
|
5012
|
+
console.error('Searches across goal/next_steps/notes/decisions/blockers (case-insensitive substring).');
|
|
5013
|
+
console.error('Defaults to ALL your handoffs (cross-repo). Use --repo or --project-id to scope.');
|
|
5014
|
+
process.exit(1);
|
|
5015
|
+
}
|
|
5016
|
+
const server = new CFMemoryMCP();
|
|
5017
|
+
server.logDebug = () => {};
|
|
5018
|
+
server.logError = (...a) => process.stderr.write(a.join(' ') + '\n');
|
|
5019
|
+
try {
|
|
5020
|
+
// Server-side substring filter via goal_contains (which actually
|
|
5021
|
+
// matches the whole serialized handoff JSON — goal, next_steps,
|
|
5022
|
+
// notes, decisions, blockers, code_anchors, files_touched).
|
|
5023
|
+
const args = { resume: true, goal_contains: query };
|
|
5024
|
+
// Default: NO repo/project scoping — search is meant to be global.
|
|
5025
|
+
// Opt-in via --repo or --project-id (or --here for cwd's repo).
|
|
5026
|
+
if (flags.repo_path) args.repo_path = flags.repo_path;
|
|
5027
|
+
if (flags.project_id) args.project_id = flags.project_id;
|
|
5028
|
+
if (flags.here) {
|
|
5029
|
+
const meta = server.getRepoMetadata();
|
|
5030
|
+
if (meta.repo_path) args.repo_path = meta.repo_path;
|
|
5031
|
+
if (meta.branch) args.branch = meta.branch;
|
|
5032
|
+
}
|
|
5033
|
+
if (flags.status) args.status_filter = flags.status;
|
|
5034
|
+
if (flags.since) args.since = flags.since;
|
|
5035
|
+
|
|
5036
|
+
const response = await server.makeRequest({
|
|
5037
|
+
jsonrpc: '2.0',
|
|
5038
|
+
id: `cli-search-${Date.now()}`,
|
|
5039
|
+
method: 'tools/call',
|
|
5040
|
+
params: { name: 'get_context_bootstrap', arguments: args },
|
|
5041
|
+
});
|
|
5042
|
+
const text = response?.result?.content?.[0]?.text;
|
|
5043
|
+
const payload = JSON.parse(text || '{}');
|
|
5044
|
+
const allList = Array.isArray(payload.recent_handoffs) ? payload.recent_handoffs : [];
|
|
5045
|
+
const list = flags.limit ? allList.slice(0, flags.limit) : allList;
|
|
5046
|
+
|
|
5047
|
+
if (flags.ids_only) {
|
|
5048
|
+
for (const h of list) process.stdout.write(`${h.session_id || ''}\n`);
|
|
5049
|
+
process.exit(list.length === 0 ? 3 : 0);
|
|
5050
|
+
}
|
|
5051
|
+
if (flags.json) {
|
|
5052
|
+
process.stdout.write(JSON.stringify({
|
|
5053
|
+
query,
|
|
5054
|
+
scope: args.repo_path ? 'repo' : args.project_id ? 'project' : 'global',
|
|
5055
|
+
repo_path: args.repo_path,
|
|
5056
|
+
project_id: args.project_id,
|
|
5057
|
+
status_filter: flags.status,
|
|
5058
|
+
since: flags.since,
|
|
5059
|
+
count: list.length,
|
|
5060
|
+
handoffs: list,
|
|
5061
|
+
}, null, 2) + '\n');
|
|
5062
|
+
process.exit(list.length === 0 ? 3 : 0);
|
|
5063
|
+
}
|
|
5064
|
+
if (list.length === 0) {
|
|
5065
|
+
process.stderr.write(`No handoffs match "${query}".\n`);
|
|
5066
|
+
process.exit(3);
|
|
5067
|
+
}
|
|
5068
|
+
// Render with terminal highlight (ANSI bold) when stdout is a TTY.
|
|
5069
|
+
const tty = process.stdout.isTTY;
|
|
5070
|
+
const lcQuery = query.toLowerCase();
|
|
5071
|
+
const highlight = (str) => {
|
|
5072
|
+
if (!str) return str;
|
|
5073
|
+
if (!tty) return str;
|
|
5074
|
+
const idx = str.toLowerCase().indexOf(lcQuery);
|
|
5075
|
+
if (idx < 0) return str;
|
|
5076
|
+
const before = str.slice(0, idx);
|
|
5077
|
+
const match = str.slice(idx, idx + query.length);
|
|
5078
|
+
const after = str.slice(idx + query.length);
|
|
5079
|
+
return `${before}\x1b[1;33m${match}\x1b[0m${after}`;
|
|
5080
|
+
};
|
|
5081
|
+
const fmtAge = (iso) => {
|
|
5082
|
+
if (!iso) return '?';
|
|
5083
|
+
const min = Math.round((Date.now() - new Date(iso).getTime()) / 60000);
|
|
5084
|
+
if (min < 60) return `${min}m`;
|
|
5085
|
+
if (min < 1440) return `${Math.round(min/60)}h`;
|
|
5086
|
+
return `${Math.round(min/1440)}d`;
|
|
5087
|
+
};
|
|
5088
|
+
const scopeLabel = args.repo_path ? `in repo ${args.repo_path}` : args.project_id ? `in project ${args.project_id}` : 'across all your handoffs';
|
|
5089
|
+
process.stdout.write(`Found ${list.length} handoff${list.length === 1 ? '' : 's'} matching "${query}" ${scopeLabel}:\n\n`);
|
|
5090
|
+
for (const h of list) {
|
|
5091
|
+
const shortId = (h.session_id || '').slice(0, 8);
|
|
5092
|
+
const age = fmtAge(h.ended_at || h.started_at);
|
|
5093
|
+
const status = (h.status || 'unknown').padEnd(11);
|
|
5094
|
+
const branch = h.branch ? `[${h.branch}] ` : '';
|
|
5095
|
+
const goal = highlight((h.goal || '(no goal)').slice(0, 100));
|
|
5096
|
+
const q = typeof h.quality_score === 'number' ? ` q=${h.quality_score.toFixed(2)}` : '';
|
|
5097
|
+
process.stdout.write(` ${shortId} ${status} ${age.padStart(4)} ago${q} ${branch}${goal}\n`);
|
|
5098
|
+
if (h.next_step) process.stdout.write(` next: ${highlight(h.next_step.slice(0, 100))}\n`);
|
|
5099
|
+
// If the repo differs from the user's cwd, surface it so they
|
|
5100
|
+
// know which project the handoff belongs to.
|
|
5101
|
+
if (h.repo_path && !args.repo_path) {
|
|
5102
|
+
process.stdout.write(` repo: ${h.repo_path}\n`);
|
|
5103
|
+
}
|
|
5104
|
+
}
|
|
5105
|
+
process.stdout.write(`\n(Run "npx cf-memory-mcp resume <id>" to load one, or pipe to "xargs -I{} npx cf-memory-mcp resume {}" with --ids-only)\n`);
|
|
5106
|
+
process.exit(0);
|
|
5107
|
+
} catch (err) {
|
|
5108
|
+
console.error('search command failed:', err.message);
|
|
5109
|
+
process.exit(1);
|
|
5110
|
+
}
|
|
5111
|
+
}
|
|
5112
|
+
|
|
4962
5113
|
async function runLinkCli() {
|
|
4963
5114
|
if (!API_KEY) {
|
|
4964
5115
|
console.error('Error: CF_MEMORY_API_KEY environment variable is required');
|
|
@@ -5215,6 +5366,12 @@ async function runDeleteCli() {
|
|
|
5215
5366
|
});
|
|
5216
5367
|
const deleteText = deleteRes?.result?.content?.[0]?.text;
|
|
5217
5368
|
const payload = JSON.parse(deleteText || '{}');
|
|
5369
|
+
// Implicit-cache invalidation: if any deleted id matches the
|
|
5370
|
+
// current cwd's implicit session, drop the cache so the next
|
|
5371
|
+
// end_session creates a fresh one.
|
|
5372
|
+
if (payload.deleted) {
|
|
5373
|
+
server.invalidateImplicitSessionIfMatches(payload.deleted_session_ids || []);
|
|
5374
|
+
}
|
|
5218
5375
|
if (flags.json) {
|
|
5219
5376
|
process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
|
|
5220
5377
|
process.exit(payload.deleted ? 0 : 3);
|
|
@@ -5254,6 +5411,12 @@ async function runDeleteCli() {
|
|
|
5254
5411
|
});
|
|
5255
5412
|
const deleteText = deleteRes?.result?.content?.[0]?.text;
|
|
5256
5413
|
const payload = JSON.parse(deleteText || '{}');
|
|
5414
|
+
// Drop the implicit-session disk cache if the deleted id matches
|
|
5415
|
+
// it. Otherwise a subsequent end_session would try to write to a
|
|
5416
|
+
// session that no longer exists.
|
|
5417
|
+
if (payload.deleted) {
|
|
5418
|
+
server.invalidateImplicitSessionIfMatches([fullId]);
|
|
5419
|
+
}
|
|
5257
5420
|
if (flags.json) {
|
|
5258
5421
|
process.stdout.write(JSON.stringify({ ...payload, session_id: fullId }, null, 2) + '\n');
|
|
5259
5422
|
process.exit(payload.deleted ? 0 : 3);
|
|
@@ -5341,7 +5504,7 @@ function runCompletionCli() {
|
|
|
5341
5504
|
process.stderr.write(`\nDone. Installed completion for ${installed} shell${installed === 1 ? '' : 's'}.\n`);
|
|
5342
5505
|
process.exit(installed > 0 ? 0 : 1);
|
|
5343
5506
|
}
|
|
5344
|
-
const commands = ['resume', 'list', 'history', 'checkpoint', 'link', 'explain', 'gha', 'status', 'clean', 'export', 'import', 'delete', 'doctor', 'env', 'completion'];
|
|
5507
|
+
const commands = ['resume', 'list', 'search', 'history', 'checkpoint', 'link', 'explain', 'gha', 'status', 'clean', 'export', 'import', 'delete', 'doctor', 'env', 'completion'];
|
|
5345
5508
|
const flags = ['--json', '-j', '--limit', '-n', '--md', '--all', '--force', '-f', '--version', '-v', '--help', '-h', '--diagnose'];
|
|
5346
5509
|
|
|
5347
5510
|
// Determine the install target for each shell. Use user-local paths
|
|
@@ -5545,6 +5708,23 @@ const PER_COMMAND_HELP = {
|
|
|
5545
5708
|
--repo PATH Override the cwd's repo (peek at another repo).
|
|
5546
5709
|
--limit N, -n N Max number of entries (default 5, max 50).
|
|
5547
5710
|
--json, -j Emit a JSON array for scripts.`,
|
|
5711
|
+
search: `cf-memory-mcp search <query> [--here | --repo PATH | --project-id ID] [--status S] [--since ISO] [--limit N] [--json|--ids-only]
|
|
5712
|
+
Find handoffs matching <query> across goal, next_steps, notes,
|
|
5713
|
+
decisions, blockers, code_anchors, and files_touched (case-insensitive
|
|
5714
|
+
substring). Default scope is ALL your handoffs (cross-repo). Highlights
|
|
5715
|
+
the matched substring in terminal output.
|
|
5716
|
+
<query> Required substring to search for.
|
|
5717
|
+
--here Scope to cwd's repo (otherwise: global).
|
|
5718
|
+
--repo PATH Scope to a specific repo path.
|
|
5719
|
+
--project-id ID Scope to a specific project.
|
|
5720
|
+
--status S Filter by status (in_progress, completed, ...).
|
|
5721
|
+
--since ISO Lower bound on handoff timestamp (ISO 8601).
|
|
5722
|
+
--limit N Max entries to render (server may cap at 50).
|
|
5723
|
+
--ids-only Print only session_ids (xargs-friendly).
|
|
5724
|
+
--json, -j Emit JSON for scripts.
|
|
5725
|
+
Exit codes: 0 = matches, 3 = no matches.
|
|
5726
|
+
Example: cf-memory-mcp search "OAuth" --status in_progress
|
|
5727
|
+
cf-memory-mcp search "migration" --ids-only | xargs -I{} cfm resume {}`,
|
|
5548
5728
|
checkpoint: `cf-memory-mcp checkpoint ["<goal>"] [--force] [--json]
|
|
5549
5729
|
Snapshot current state to a fresh handoff with keep_open:true. The
|
|
5550
5730
|
session stays active for future checkpoints. The final end_session (or
|
|
@@ -6097,11 +6277,15 @@ async function runCleanCli() {
|
|
|
6097
6277
|
const removed = [];
|
|
6098
6278
|
try {
|
|
6099
6279
|
if (all) {
|
|
6100
|
-
// Clear ALL disk caches in ~/.cf-memory
|
|
6280
|
+
// Clear ALL disk caches in ~/.cf-memory/: handoff-*.json
|
|
6281
|
+
// (offline resume cache) AND implicit-*.json (persistent
|
|
6282
|
+
// implicit session id per cwd).
|
|
6101
6283
|
const dir = path.join(os.homedir(), '.cf-memory');
|
|
6102
6284
|
if (fs.existsSync(dir)) {
|
|
6103
6285
|
for (const entry of fs.readdirSync(dir)) {
|
|
6104
|
-
|
|
6286
|
+
const isHandoff = entry.startsWith('handoff-') && entry.endsWith('.json');
|
|
6287
|
+
const isImplicit = entry.startsWith('implicit-') && entry.endsWith('.json');
|
|
6288
|
+
if (isHandoff || isImplicit) {
|
|
6105
6289
|
const full = path.join(dir, entry);
|
|
6106
6290
|
fs.unlinkSync(full);
|
|
6107
6291
|
removed.push(full);
|
|
@@ -6109,10 +6293,17 @@ async function runCleanCli() {
|
|
|
6109
6293
|
}
|
|
6110
6294
|
}
|
|
6111
6295
|
} else {
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6296
|
+
// cwd-only mode: drop both the handoff cache AND the
|
|
6297
|
+
// implicit session cache for this cwd.
|
|
6298
|
+
const handoffPath = server.getDiskCachePath();
|
|
6299
|
+
if (handoffPath && fs.existsSync(handoffPath)) {
|
|
6300
|
+
fs.unlinkSync(handoffPath);
|
|
6301
|
+
removed.push(handoffPath);
|
|
6302
|
+
}
|
|
6303
|
+
const implicitPath = server.getImplicitSessionDiskPath();
|
|
6304
|
+
if (implicitPath && fs.existsSync(implicitPath)) {
|
|
6305
|
+
fs.unlinkSync(implicitPath);
|
|
6306
|
+
removed.push(implicitPath);
|
|
6116
6307
|
}
|
|
6117
6308
|
}
|
|
6118
6309
|
if (flags.json) {
|
|
@@ -6369,6 +6560,11 @@ if (process.argv[2] === 'list') {
|
|
|
6369
6560
|
return;
|
|
6370
6561
|
}
|
|
6371
6562
|
|
|
6563
|
+
if (process.argv[2] === 'search') {
|
|
6564
|
+
runSearchCli();
|
|
6565
|
+
return;
|
|
6566
|
+
}
|
|
6567
|
+
|
|
6372
6568
|
if (process.argv[2] === 'checkpoint') {
|
|
6373
6569
|
runCheckpointCli();
|
|
6374
6570
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cf-memory-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.63.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": {
|