cf-memory-mcp 3.35.0 → 3.37.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 +183 -17
- package/package.json +1 -1
package/bin/cf-memory-mcp.js
CHANGED
|
@@ -217,7 +217,8 @@ const TOOLS_LIST = [
|
|
|
217
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
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.' },
|
|
219
219
|
since: { type: 'string', description: 'Lower bound on handoff timestamp (ISO 8601). Scope resume to a time window.' },
|
|
220
|
-
status_filter: { description: 'Restrict matches to handoffs with this status. Single string or array of strings (e.g., ["in_progress","blocked"]).' }
|
|
220
|
+
status_filter: { description: 'Restrict matches to handoffs with this status. Single string or array of strings (e.g., ["in_progress","blocked"]).' },
|
|
221
|
+
recent_handoff_limit: { type: 'number', description: 'Override the default recent_handoffs cap (5). Range 1-50.' }
|
|
221
222
|
}
|
|
222
223
|
}
|
|
223
224
|
},
|
|
@@ -3806,7 +3807,7 @@ if (process.argv.includes('--version') || process.argv.includes('-v')) {
|
|
|
3806
3807
|
|
|
3807
3808
|
// Global --help only when no subcommand is present. With a subcommand, fall
|
|
3808
3809
|
// through to the per-command help dispatch below.
|
|
3809
|
-
const SUBCOMMANDS = new Set(['resume', 'list', 'checkpoint', 'status', 'clean', 'export', 'import', 'doctor', 'completion', 'delete', 'env']);
|
|
3810
|
+
const SUBCOMMANDS = new Set(['resume', 'list', 'checkpoint', 'status', 'clean', 'export', 'import', 'doctor', 'completion', 'delete', 'env', 'history']);
|
|
3810
3811
|
const hasSubcommand = process.argv[2] && SUBCOMMANDS.has(process.argv[2]);
|
|
3811
3812
|
|
|
3812
3813
|
if (!hasSubcommand && (process.argv.includes('--help') || process.argv.includes('-h'))) {
|
|
@@ -3818,7 +3819,8 @@ A portable MCP (Model Context Protocol) server for AI memory storage using Cloud
|
|
|
3818
3819
|
Usage:
|
|
3819
3820
|
npx cf-memory-mcp Start the MCP server
|
|
3820
3821
|
npx cf-memory-mcp resume [id] Print the prior resume handoff (markdown)
|
|
3821
|
-
npx cf-memory-mcp list List recent handoffs for cwd
|
|
3822
|
+
npx cf-memory-mcp list List recent handoffs for cwd (status-ranked)
|
|
3823
|
+
npx cf-memory-mcp history List all handoffs for cwd, chronological
|
|
3822
3824
|
npx cf-memory-mcp checkpoint ["<goal>"] Snapshot current state (keep_open)
|
|
3823
3825
|
npx cf-memory-mcp status Show bridge state + server resume availability
|
|
3824
3826
|
npx cf-memory-mcp clean Delete local disk cache for current cwd
|
|
@@ -3878,6 +3880,12 @@ function parseCliArgs(rest) {
|
|
|
3878
3880
|
flags.files_only = true;
|
|
3879
3881
|
} else if (a === '--anchors-only') {
|
|
3880
3882
|
flags.anchors_only = true;
|
|
3883
|
+
} else if (a === '--decisions-only') {
|
|
3884
|
+
flags.decisions_only = true;
|
|
3885
|
+
} else if (a === '--blockers-only') {
|
|
3886
|
+
flags.blockers_only = true;
|
|
3887
|
+
} else if (a === '--chain') {
|
|
3888
|
+
flags.chain = true;
|
|
3881
3889
|
} else if (a === '--older-than') {
|
|
3882
3890
|
// Accept "7d" / "30d" / "12h" / raw number (days).
|
|
3883
3891
|
const raw = rest[++i] || '';
|
|
@@ -4014,6 +4022,76 @@ async function runResumeCli() {
|
|
|
4014
4022
|
process.exit(0);
|
|
4015
4023
|
}
|
|
4016
4024
|
|
|
4025
|
+
// --decisions-only: list decisions from the handoff, one per line.
|
|
4026
|
+
if (flags.decisions_only) {
|
|
4027
|
+
const decisions = payload.resume_handoff?.handoff?.decisions || [];
|
|
4028
|
+
if (decisions.length === 0) process.exit(3);
|
|
4029
|
+
for (const d of decisions) process.stdout.write(`- ${d}\n`);
|
|
4030
|
+
process.exit(0);
|
|
4031
|
+
}
|
|
4032
|
+
|
|
4033
|
+
// --blockers-only: list open blockers, one per line. Empty list
|
|
4034
|
+
// exits 3 so scripts can branch on "no blockers".
|
|
4035
|
+
if (flags.blockers_only) {
|
|
4036
|
+
const blockers = payload.resume_handoff?.handoff?.blockers || [];
|
|
4037
|
+
if (blockers.length === 0) process.exit(3);
|
|
4038
|
+
for (const b of blockers) process.stdout.write(`- ${b}\n`);
|
|
4039
|
+
process.exit(0);
|
|
4040
|
+
}
|
|
4041
|
+
|
|
4042
|
+
// --chain: walk parent_session_id back through the thread history.
|
|
4043
|
+
// Bounded at 20 iterations to avoid an infinite loop on malformed
|
|
4044
|
+
// data. Prints each handoff as "<id> [age] status — goal".
|
|
4045
|
+
if (flags.chain) {
|
|
4046
|
+
if (!found) {
|
|
4047
|
+
process.stderr.write('No resume handoff available to walk back from.\n');
|
|
4048
|
+
process.exit(3);
|
|
4049
|
+
}
|
|
4050
|
+
const fmtAge = (iso) => {
|
|
4051
|
+
if (!iso) return '?';
|
|
4052
|
+
const min = Math.round((Date.now() - new Date(iso).getTime()) / 60000);
|
|
4053
|
+
if (min < 60) return `${min}m`;
|
|
4054
|
+
if (min < 1440) return `${Math.round(min/60)}h`;
|
|
4055
|
+
return `${Math.round(min/1440)}d`;
|
|
4056
|
+
};
|
|
4057
|
+
const chain = [];
|
|
4058
|
+
let cur = payload.resume_handoff;
|
|
4059
|
+
chain.push(cur);
|
|
4060
|
+
for (let i = 0; i < 20; i++) {
|
|
4061
|
+
const parentId = cur?.handoff?.parent_session_id;
|
|
4062
|
+
if (!parentId) break;
|
|
4063
|
+
// Fetch parent
|
|
4064
|
+
const parentRes = await server.makeRequest({
|
|
4065
|
+
jsonrpc: '2.0', id: `cli-chain-${i}-${Date.now()}`,
|
|
4066
|
+
method: 'tools/call',
|
|
4067
|
+
params: { name: 'get_context_bootstrap', arguments: { resume: true, session_id_hint: parentId } },
|
|
4068
|
+
});
|
|
4069
|
+
const parentText = parentRes?.result?.content?.[0]?.text;
|
|
4070
|
+
const parentPayload = JSON.parse(parentText || '{}');
|
|
4071
|
+
if (!parentPayload.resume_handoff) break;
|
|
4072
|
+
chain.push(parentPayload.resume_handoff);
|
|
4073
|
+
cur = parentPayload.resume_handoff;
|
|
4074
|
+
}
|
|
4075
|
+
if (flags.json) {
|
|
4076
|
+
process.stdout.write(JSON.stringify({ chain }, null, 2) + '\n');
|
|
4077
|
+
process.exit(0);
|
|
4078
|
+
}
|
|
4079
|
+
process.stdout.write(`Thread chain (newest first, ${chain.length} session${chain.length === 1 ? '' : 's'}):\n\n`);
|
|
4080
|
+
for (let i = 0; i < chain.length; i++) {
|
|
4081
|
+
const e = chain[i];
|
|
4082
|
+
const shortId = (e.session_id || '').slice(0, 8);
|
|
4083
|
+
const age = fmtAge(e.ended_at || e.started_at);
|
|
4084
|
+
const status = e.handoff?.status || '?';
|
|
4085
|
+
const goal = e.handoff?.goal || '(no goal)';
|
|
4086
|
+
const prefix = i === 0 ? '→' : ' ';
|
|
4087
|
+
process.stdout.write(`${prefix} ${shortId} ${status.padEnd(11)} ${age.padStart(4)} ago ${goal}\n`);
|
|
4088
|
+
}
|
|
4089
|
+
if (chain[chain.length-1]?.handoff?.parent_session_id) {
|
|
4090
|
+
process.stdout.write(`\n(chain truncated at 20 — increase with multiple --chain calls)\n`);
|
|
4091
|
+
}
|
|
4092
|
+
process.exit(0);
|
|
4093
|
+
}
|
|
4094
|
+
|
|
4017
4095
|
// --md <path>: write the rendered markdown to a file instead of
|
|
4018
4096
|
// (or in addition to) stdout. Useful for piping to a markdown
|
|
4019
4097
|
// viewer or saving to the project.
|
|
@@ -4154,6 +4232,71 @@ async function runListCli() {
|
|
|
4154
4232
|
}
|
|
4155
4233
|
}
|
|
4156
4234
|
|
|
4235
|
+
async function runHistoryCli() {
|
|
4236
|
+
if (!API_KEY) {
|
|
4237
|
+
console.error('Error: CF_MEMORY_API_KEY environment variable is required');
|
|
4238
|
+
process.exit(1);
|
|
4239
|
+
}
|
|
4240
|
+
const { flags } = parseCliArgs(process.argv.slice(3));
|
|
4241
|
+
const limit = flags.limit || 25;
|
|
4242
|
+
const server = new CFMemoryMCP();
|
|
4243
|
+
server.logDebug = () => {};
|
|
4244
|
+
try {
|
|
4245
|
+
const meta = server.getRepoMetadata();
|
|
4246
|
+
const args = { resume: true, recent_handoff_limit: limit };
|
|
4247
|
+
if (meta.repo_path) args.repo_path = meta.repo_path;
|
|
4248
|
+
if (meta.branch) args.branch = meta.branch;
|
|
4249
|
+
if (flags.repo_path) args.repo_path = flags.repo_path;
|
|
4250
|
+
if (flags.since) args.since = flags.since;
|
|
4251
|
+
if (!flags.repo_path) {
|
|
4252
|
+
const fake = { params: { name: 'retrieve_context', arguments: {} } };
|
|
4253
|
+
await server.maybeFillProjectId(fake);
|
|
4254
|
+
if (fake.params.arguments.project_id) args.project_id = fake.params.arguments.project_id;
|
|
4255
|
+
}
|
|
4256
|
+
|
|
4257
|
+
const response = await server.makeRequest({
|
|
4258
|
+
jsonrpc: '2.0',
|
|
4259
|
+
id: `cli-history-${Date.now()}`,
|
|
4260
|
+
method: 'tools/call',
|
|
4261
|
+
params: { name: 'get_context_bootstrap', arguments: args },
|
|
4262
|
+
});
|
|
4263
|
+
const text = response?.result?.content?.[0]?.text;
|
|
4264
|
+
const payload = JSON.parse(text || '{}');
|
|
4265
|
+
const all = (payload.recent_handoffs || []).slice();
|
|
4266
|
+
// Chronological order (oldest first).
|
|
4267
|
+
all.sort((a, b) => {
|
|
4268
|
+
const ta = new Date(a.ended_at || a.started_at).getTime();
|
|
4269
|
+
const tb = new Date(b.ended_at || b.started_at).getTime();
|
|
4270
|
+
return ta - tb;
|
|
4271
|
+
});
|
|
4272
|
+
if (flags.json) {
|
|
4273
|
+
process.stdout.write(JSON.stringify({
|
|
4274
|
+
repo_path: args.repo_path,
|
|
4275
|
+
count: all.length,
|
|
4276
|
+
handoffs: all,
|
|
4277
|
+
}, null, 2) + '\n');
|
|
4278
|
+
process.exit(all.length === 0 ? 3 : 0);
|
|
4279
|
+
}
|
|
4280
|
+
if (all.length === 0) {
|
|
4281
|
+
process.stderr.write((payload.empty_hint || 'No handoffs for this context.') + '\n');
|
|
4282
|
+
process.exit(3);
|
|
4283
|
+
}
|
|
4284
|
+
const fmtDate = (iso) => iso ? new Date(iso).toISOString().slice(0, 16).replace('T', ' ') : '?';
|
|
4285
|
+
process.stdout.write(`History for ${args.repo_path || 'this cwd'} (${all.length} handoff${all.length === 1 ? '' : 's'}, oldest first):\n\n`);
|
|
4286
|
+
for (const h of all) {
|
|
4287
|
+
const shortId = (h.session_id || '').slice(0, 8);
|
|
4288
|
+
const when = fmtDate(h.ended_at || h.started_at);
|
|
4289
|
+
const status = (h.status || 'unknown').padEnd(11);
|
|
4290
|
+
const goal = (h.goal || '(no goal)').slice(0, 70);
|
|
4291
|
+
process.stdout.write(` ${when} ${shortId} ${status} ${goal}\n`);
|
|
4292
|
+
}
|
|
4293
|
+
process.exit(0);
|
|
4294
|
+
} catch (err) {
|
|
4295
|
+
console.error('history command failed:', err.message);
|
|
4296
|
+
process.exit(1);
|
|
4297
|
+
}
|
|
4298
|
+
}
|
|
4299
|
+
|
|
4157
4300
|
async function runDeleteCli() {
|
|
4158
4301
|
if (!API_KEY) {
|
|
4159
4302
|
console.error('Error: CF_MEMORY_API_KEY environment variable is required');
|
|
@@ -4346,7 +4489,7 @@ function runEnvCli() {
|
|
|
4346
4489
|
|
|
4347
4490
|
function runCompletionCli() {
|
|
4348
4491
|
const shell = process.argv[3] || 'bash';
|
|
4349
|
-
const commands = ['resume', 'list', 'checkpoint', 'status', 'clean', 'export', 'import', 'delete', 'doctor', 'env', 'completion'];
|
|
4492
|
+
const commands = ['resume', 'list', 'history', 'checkpoint', 'status', 'clean', 'export', 'import', 'delete', 'doctor', 'env', 'completion'];
|
|
4350
4493
|
const flags = ['--json', '-j', '--limit', '-n', '--md', '--all', '--force', '-f', '--version', '-v', '--help', '-h', '--diagnose'];
|
|
4351
4494
|
if (shell === 'bash') {
|
|
4352
4495
|
process.stdout.write(`# cf-memory-mcp bash completion
|
|
@@ -4412,16 +4555,19 @@ complete -c cf-memory-mcp -l version -s v -d 'Show version'
|
|
|
4412
4555
|
|
|
4413
4556
|
// Per-command help texts. Used when "cf-memory-mcp <cmd> --help" is invoked.
|
|
4414
4557
|
const PER_COMMAND_HELP = {
|
|
4415
|
-
resume: `cf-memory-mcp resume [<session-id-prefix>] [--md path] [
|
|
4558
|
+
resume: `cf-memory-mcp resume [<session-id-prefix>] [--md path] [<extract-flag>] [--json]
|
|
4416
4559
|
Print the prior resume handoff (markdown by default).
|
|
4417
4560
|
<session-id-prefix> Optional: pick a specific session (>=8 char prefix or full UUID).
|
|
4418
4561
|
--md <path> Write the markdown to a file instead of stdout.
|
|
4419
|
-
|
|
4420
|
-
--next-
|
|
4421
|
-
--
|
|
4422
|
-
--
|
|
4423
|
-
--
|
|
4424
|
-
|
|
4562
|
+
Extract flags (pick one; each exits 3 if the section is empty):
|
|
4563
|
+
--next-only First next_step only (for shell prompts).
|
|
4564
|
+
--next-steps All next_steps, numbered.
|
|
4565
|
+
--files-only files_touched paths, one per line (xargs-friendly).
|
|
4566
|
+
--anchors-only code_anchors as "file:lines symbol".
|
|
4567
|
+
--decisions-only decisions, one per line.
|
|
4568
|
+
--blockers-only open blockers, one per line.
|
|
4569
|
+
--json, -j Full bootstrap payload as JSON.
|
|
4570
|
+
Exit codes: 0 = found, 3 = no handoff / no data.`,
|
|
4425
4571
|
list: `cf-memory-mcp list [--status S] [--since ISO] [--repo PATH] [--limit N] [--json]
|
|
4426
4572
|
List recent handoffs for the current cwd, status-ranked. Header shows
|
|
4427
4573
|
a status-count summary.
|
|
@@ -4477,6 +4623,14 @@ const PER_COMMAND_HELP = {
|
|
|
4477
4623
|
Print all CF_MEMORY_* env vars the bridge reads, with their current
|
|
4478
4624
|
values + descriptions. Useful for discovering knobs.
|
|
4479
4625
|
--json, -j Emit JSON for scripts.`,
|
|
4626
|
+
history: `cf-memory-mcp history [--limit N] [--repo PATH] [--since ISO] [--json]
|
|
4627
|
+
Print all handoffs for the current cwd in chronological order (oldest
|
|
4628
|
+
first). Unlike 'list' (status-ranked, default 5), this is a flat
|
|
4629
|
+
timeline view.
|
|
4630
|
+
--limit N Max number of handoffs (default 25, max 50).
|
|
4631
|
+
--repo PATH Override the cwd's repo.
|
|
4632
|
+
--since ISO Lower bound on handoff timestamp.
|
|
4633
|
+
--json, -j Emit JSON.`,
|
|
4480
4634
|
};
|
|
4481
4635
|
|
|
4482
4636
|
function printPerCommandHelp(cmd) {
|
|
@@ -4890,11 +5044,14 @@ async function runCheckpointCli() {
|
|
|
4890
5044
|
// Optional positional goal argument: `cf-memory-mcp checkpoint "<goal text>"`
|
|
4891
5045
|
const goalArg = positional.filter(p => p !== '--force' && p !== '-f').join(' ').trim();
|
|
4892
5046
|
|
|
4893
|
-
// Duplicate detection: before creating a new
|
|
4894
|
-
// check if there's a recent in_progress handoff
|
|
4895
|
-
// repo/branch. If so, suggest resuming it instead
|
|
4896
|
-
// new one.
|
|
4897
|
-
|
|
5047
|
+
// Duplicate detection + parent linking: before creating a new
|
|
5048
|
+
// implicit session, check if there's a recent in_progress handoff
|
|
5049
|
+
// for the same repo/branch. If so, suggest resuming it instead
|
|
5050
|
+
// of churning a new one. With --force, proceed but record the
|
|
5051
|
+
// existing session id as the new handoff's parent_session_id so
|
|
5052
|
+
// the chain can be traced later.
|
|
5053
|
+
let parentSessionId = null;
|
|
5054
|
+
if (true) { // always probe to capture parent; --force only bypasses the abort
|
|
4898
5055
|
const meta = server.getRepoMetadata();
|
|
4899
5056
|
if (meta.repo_path) {
|
|
4900
5057
|
const probeArgs = { resume: true, repo_path: meta.repo_path, status_filter: 'in_progress', max_age_minutes: 60 };
|
|
@@ -4912,7 +5069,7 @@ async function runCheckpointCli() {
|
|
|
4912
5069
|
// Same-cwd implicit session won't be in the worker yet
|
|
4913
5070
|
// unless someone called start_session — we look for
|
|
4914
5071
|
// OTHER active threads on the same repo.
|
|
4915
|
-
if (recent) {
|
|
5072
|
+
if (recent && !force) {
|
|
4916
5073
|
const shortId = (recent.session_id || '').slice(0, 8);
|
|
4917
5074
|
const ageMin = recent.handoff_age_minutes ?? '?';
|
|
4918
5075
|
const status = recent.handoff?.status || '?';
|
|
@@ -4933,6 +5090,9 @@ async function runCheckpointCli() {
|
|
|
4933
5090
|
process.stderr.write(`Resume it: cf-memory-mcp resume ${shortId}\n`);
|
|
4934
5091
|
process.stderr.write(`Or re-run with --force to create a new session anyway.\n`);
|
|
4935
5092
|
process.exit(4);
|
|
5093
|
+
} else if (recent && force) {
|
|
5094
|
+
// --force: proceed, but record the parent link.
|
|
5095
|
+
parentSessionId = recent.session_id;
|
|
4936
5096
|
}
|
|
4937
5097
|
} catch (_) { /* probe failure is non-fatal */ }
|
|
4938
5098
|
}
|
|
@@ -4950,6 +5110,7 @@ async function runCheckpointCli() {
|
|
|
4950
5110
|
// this is a fresh process).
|
|
4951
5111
|
const handoff = server.synthesizeMinimalHandoff();
|
|
4952
5112
|
if (goalArg) handoff.goal = goalArg;
|
|
5113
|
+
if (parentSessionId) handoff.parent_session_id = parentSessionId;
|
|
4953
5114
|
if (meta.repo_path) handoff.repo_path = meta.repo_path;
|
|
4954
5115
|
if (meta.branch) handoff.branch = meta.branch;
|
|
4955
5116
|
const fake = { params: { name: 'retrieve_context', arguments: {} } };
|
|
@@ -5059,6 +5220,11 @@ if (process.argv[2] === 'env') {
|
|
|
5059
5220
|
return;
|
|
5060
5221
|
}
|
|
5061
5222
|
|
|
5223
|
+
if (process.argv[2] === 'history') {
|
|
5224
|
+
runHistoryCli();
|
|
5225
|
+
return;
|
|
5226
|
+
}
|
|
5227
|
+
|
|
5062
5228
|
if (process.argv.includes('--diagnose')) {
|
|
5063
5229
|
(async () => {
|
|
5064
5230
|
console.log(`CF Memory MCP v${PACKAGE_VERSION} - Diagnostics`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cf-memory-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.37.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": {
|