cf-memory-mcp 3.26.0 → 3.28.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 +309 -1
- package/package.json +2 -1
package/bin/cf-memory-mcp.js
CHANGED
|
@@ -3792,7 +3792,12 @@ Usage:
|
|
|
3792
3792
|
npx cf-memory-mcp status Show bridge state + server resume availability
|
|
3793
3793
|
npx cf-memory-mcp clean Delete local disk cache for current cwd
|
|
3794
3794
|
npx cf-memory-mcp clean --all Delete ALL local disk caches
|
|
3795
|
+
npx cf-memory-mcp export <id> Print a session's handoff as JSON bundle (backup)
|
|
3796
|
+
npx cf-memory-mcp import <file> Restore a handoff bundle (cross-machine sync); use "-" for stdin
|
|
3797
|
+
npx cf-memory-mcp doctor Diagnose common setup issues
|
|
3795
3798
|
npx cf-memory-mcp --version Show version
|
|
3799
|
+
|
|
3800
|
+
Short alias: \`cfm\` works as a drop-in for \`cf-memory-mcp\` (e.g., \`cfm resume\`).
|
|
3796
3801
|
npx cf-memory-mcp --help Show this help
|
|
3797
3802
|
npx cf-memory-mcp --diagnose Test connectivity and report issues
|
|
3798
3803
|
|
|
@@ -3998,6 +4003,245 @@ async function runListCli() {
|
|
|
3998
4003
|
}
|
|
3999
4004
|
}
|
|
4000
4005
|
|
|
4006
|
+
async function runDoctorCli() {
|
|
4007
|
+
const { flags } = parseCliArgs(process.argv.slice(3));
|
|
4008
|
+
const server = new CFMemoryMCP();
|
|
4009
|
+
server.logDebug = () => {};
|
|
4010
|
+
const checks = [];
|
|
4011
|
+
const add = (label, ok, detail) => checks.push({ label, ok, detail });
|
|
4012
|
+
|
|
4013
|
+
// 1. API key set?
|
|
4014
|
+
add('CF_MEMORY_API_KEY set', !!API_KEY, API_KEY ? '(redacted)' : 'unset — most commands will fail');
|
|
4015
|
+
|
|
4016
|
+
// 2. Cwd is in a git repo?
|
|
4017
|
+
const meta = server.getRepoMetadata();
|
|
4018
|
+
add('git repo detected', !!meta.repo_path, meta.repo_path || 'no .git in cwd; resume metadata will be empty');
|
|
4019
|
+
add('git branch detected', !!meta.branch, meta.branch || 'detached HEAD or no commits');
|
|
4020
|
+
|
|
4021
|
+
// 3. Disk cache writable?
|
|
4022
|
+
const cachePath = server.getDiskCachePath();
|
|
4023
|
+
let diskWritable = false;
|
|
4024
|
+
if (cachePath) {
|
|
4025
|
+
try {
|
|
4026
|
+
const dir = path.dirname(cachePath);
|
|
4027
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
4028
|
+
const probe = path.join(dir, '.doctor-probe');
|
|
4029
|
+
fs.writeFileSync(probe, 'ok');
|
|
4030
|
+
fs.unlinkSync(probe);
|
|
4031
|
+
diskWritable = true;
|
|
4032
|
+
} catch (err) {
|
|
4033
|
+
add('disk cache writable', false, `failed: ${err.message}`);
|
|
4034
|
+
}
|
|
4035
|
+
}
|
|
4036
|
+
if (diskWritable) add('disk cache writable', true, path.dirname(cachePath));
|
|
4037
|
+
|
|
4038
|
+
// 4. Worker reachable?
|
|
4039
|
+
if (API_KEY) {
|
|
4040
|
+
const t0 = Date.now();
|
|
4041
|
+
let workerReachable = false;
|
|
4042
|
+
let workerMs = null;
|
|
4043
|
+
try {
|
|
4044
|
+
const res = await server.makeRequestOnce({
|
|
4045
|
+
jsonrpc: '2.0', id: `doctor-${Date.now()}`,
|
|
4046
|
+
method: 'tools/call', params: { name: 'health_check', arguments: {} },
|
|
4047
|
+
});
|
|
4048
|
+
workerReachable = !res?.error;
|
|
4049
|
+
workerMs = Date.now() - t0;
|
|
4050
|
+
} catch (_) { /* unreachable */ }
|
|
4051
|
+
add('worker reachable', workerReachable, workerReachable ? `${workerMs}ms` : `failed`);
|
|
4052
|
+
|
|
4053
|
+
// 5. Project indexed?
|
|
4054
|
+
if (workerReachable) {
|
|
4055
|
+
const fake = { params: { name: 'retrieve_context', arguments: {} } };
|
|
4056
|
+
await server.maybeFillProjectId(fake);
|
|
4057
|
+
const pid = fake.params.arguments.project_id;
|
|
4058
|
+
add('project indexed', !!pid, pid || 'no project matched cwd — run index_project to enable retrieve_context');
|
|
4059
|
+
|
|
4060
|
+
// 6. Resume handoff available?
|
|
4061
|
+
if (meta.repo_path) {
|
|
4062
|
+
const probeArgs = { resume: true, repo_path: meta.repo_path };
|
|
4063
|
+
if (meta.branch) probeArgs.branch = meta.branch;
|
|
4064
|
+
if (pid) probeArgs.project_id = pid;
|
|
4065
|
+
try {
|
|
4066
|
+
const probeRes = await server.makeRequestOnce({
|
|
4067
|
+
jsonrpc: '2.0', id: `doctor-resume-${Date.now()}`,
|
|
4068
|
+
method: 'tools/call', params: { name: 'get_context_bootstrap', arguments: probeArgs },
|
|
4069
|
+
});
|
|
4070
|
+
const probeText = probeRes?.result?.content?.[0]?.text;
|
|
4071
|
+
const probePayload = JSON.parse(probeText || '{}');
|
|
4072
|
+
const hasHandoff = !!probePayload.resume_handoff;
|
|
4073
|
+
add('resume handoff available', hasHandoff,
|
|
4074
|
+
hasHandoff
|
|
4075
|
+
? `${(probePayload.resume_handoff.session_id||'').slice(0,8)} (${probePayload.resume_handoff.handoff_age_minutes}m old)`
|
|
4076
|
+
: 'none for this context');
|
|
4077
|
+
} catch (_) { /* skip */ }
|
|
4078
|
+
}
|
|
4079
|
+
}
|
|
4080
|
+
}
|
|
4081
|
+
|
|
4082
|
+
if (flags.json) {
|
|
4083
|
+
process.stdout.write(JSON.stringify({ checks }, null, 2) + '\n');
|
|
4084
|
+
process.exit(checks.every(c => c.ok) ? 0 : 1);
|
|
4085
|
+
}
|
|
4086
|
+
process.stdout.write(`cf-memory-mcp v${PACKAGE_VERSION} — doctor\n\n`);
|
|
4087
|
+
let anyFailed = false;
|
|
4088
|
+
for (const c of checks) {
|
|
4089
|
+
const mark = c.ok ? '✓' : '✗';
|
|
4090
|
+
if (!c.ok) anyFailed = true;
|
|
4091
|
+
process.stdout.write(` ${mark} ${c.label.padEnd(28)} ${c.detail || ''}\n`);
|
|
4092
|
+
}
|
|
4093
|
+
process.stdout.write('\n');
|
|
4094
|
+
process.stdout.write(anyFailed ? 'Some checks failed. See details above.\n' : 'All checks passed.\n');
|
|
4095
|
+
process.exit(anyFailed ? 1 : 0);
|
|
4096
|
+
}
|
|
4097
|
+
|
|
4098
|
+
async function runExportCli() {
|
|
4099
|
+
if (!API_KEY) {
|
|
4100
|
+
console.error('Error: CF_MEMORY_API_KEY environment variable is required');
|
|
4101
|
+
process.exit(1);
|
|
4102
|
+
}
|
|
4103
|
+
const { positional, flags } = parseCliArgs(process.argv.slice(3));
|
|
4104
|
+
const sessionArg = positional[0];
|
|
4105
|
+
if (!sessionArg) {
|
|
4106
|
+
console.error('Usage: cf-memory-mcp export <session-id-or-prefix> [--md path]');
|
|
4107
|
+
process.exit(1);
|
|
4108
|
+
}
|
|
4109
|
+
const server = new CFMemoryMCP();
|
|
4110
|
+
server.logDebug = () => {};
|
|
4111
|
+
try {
|
|
4112
|
+
const args = { resume: true, session_id_hint: sessionArg };
|
|
4113
|
+
const response = await server.makeRequest({
|
|
4114
|
+
jsonrpc: '2.0',
|
|
4115
|
+
id: `cli-export-${Date.now()}`,
|
|
4116
|
+
method: 'tools/call',
|
|
4117
|
+
params: { name: 'get_context_bootstrap', arguments: args },
|
|
4118
|
+
});
|
|
4119
|
+
const text = response?.result?.content?.[0]?.text;
|
|
4120
|
+
const payload = JSON.parse(text || '{}');
|
|
4121
|
+
const envelope = payload.resume_handoff;
|
|
4122
|
+
if (!envelope?.handoff) {
|
|
4123
|
+
process.stderr.write((payload.empty_hint || `No handoff found for "${sessionArg}".`) + '\n');
|
|
4124
|
+
process.exit(3);
|
|
4125
|
+
}
|
|
4126
|
+
// Build an exportable bundle: enough to fully reconstruct via import.
|
|
4127
|
+
const bundle = {
|
|
4128
|
+
kind: 'cf-memory-handoff',
|
|
4129
|
+
version: 1,
|
|
4130
|
+
exported_at: new Date().toISOString(),
|
|
4131
|
+
session_id: envelope.session_id,
|
|
4132
|
+
started_at: envelope.started_at,
|
|
4133
|
+
ended_at: envelope.ended_at,
|
|
4134
|
+
handoff: envelope.handoff,
|
|
4135
|
+
};
|
|
4136
|
+
const output = JSON.stringify(bundle, null, 2);
|
|
4137
|
+
if (flags.md_path) {
|
|
4138
|
+
fs.writeFileSync(flags.md_path, output);
|
|
4139
|
+
process.stdout.write(`Wrote ${output.length} bytes to ${flags.md_path}\n`);
|
|
4140
|
+
} else {
|
|
4141
|
+
process.stdout.write(output + '\n');
|
|
4142
|
+
}
|
|
4143
|
+
process.exit(0);
|
|
4144
|
+
} catch (err) {
|
|
4145
|
+
console.error('export command failed:', err.message);
|
|
4146
|
+
process.exit(1);
|
|
4147
|
+
}
|
|
4148
|
+
}
|
|
4149
|
+
|
|
4150
|
+
async function runImportCli() {
|
|
4151
|
+
if (!API_KEY) {
|
|
4152
|
+
console.error('Error: CF_MEMORY_API_KEY environment variable is required');
|
|
4153
|
+
process.exit(1);
|
|
4154
|
+
}
|
|
4155
|
+
const { positional, flags } = parseCliArgs(process.argv.slice(3));
|
|
4156
|
+
const sourcePath = positional[0];
|
|
4157
|
+
let raw;
|
|
4158
|
+
try {
|
|
4159
|
+
if (sourcePath === '-' || sourcePath === undefined) {
|
|
4160
|
+
// Read from stdin so `cat handoff.json | cf-memory-mcp import` works.
|
|
4161
|
+
raw = fs.readFileSync(0, 'utf8'); // fd 0 = stdin
|
|
4162
|
+
} else {
|
|
4163
|
+
raw = fs.readFileSync(sourcePath, 'utf8');
|
|
4164
|
+
}
|
|
4165
|
+
} catch (err) {
|
|
4166
|
+
console.error('import: cannot read input:', err.message);
|
|
4167
|
+
process.exit(1);
|
|
4168
|
+
}
|
|
4169
|
+
let bundle;
|
|
4170
|
+
try { bundle = JSON.parse(raw); } catch (err) {
|
|
4171
|
+
console.error('import: input is not valid JSON:', err.message);
|
|
4172
|
+
process.exit(1);
|
|
4173
|
+
}
|
|
4174
|
+
if (bundle.kind !== 'cf-memory-handoff' || !bundle.handoff) {
|
|
4175
|
+
console.error('import: input is not a cf-memory-handoff bundle (expected kind="cf-memory-handoff" + handoff field).');
|
|
4176
|
+
process.exit(1);
|
|
4177
|
+
}
|
|
4178
|
+
const server = new CFMemoryMCP();
|
|
4179
|
+
server.logDebug = () => {};
|
|
4180
|
+
try {
|
|
4181
|
+
// Imports always create a fresh session and write the imported
|
|
4182
|
+
// handoff to it. The original session_id is preserved in
|
|
4183
|
+
// handoff.notes so the link to the source isn't lost.
|
|
4184
|
+
const meta = server.getRepoMetadata();
|
|
4185
|
+
const startArgs = { context: 'main' };
|
|
4186
|
+
if (meta.repo_path) startArgs.repo_path = meta.repo_path;
|
|
4187
|
+
if (meta.branch) startArgs.branch = meta.branch;
|
|
4188
|
+
const fake = { params: { name: 'retrieve_context', arguments: {} } };
|
|
4189
|
+
await server.maybeFillProjectId(fake);
|
|
4190
|
+
if (fake.params.arguments.project_id) startArgs.project_id = fake.params.arguments.project_id;
|
|
4191
|
+
|
|
4192
|
+
const startRes = await server.makeRequest({
|
|
4193
|
+
jsonrpc: '2.0', id: `cli-import-start-${Date.now()}`,
|
|
4194
|
+
method: 'tools/call', params: { name: 'start_session', arguments: startArgs },
|
|
4195
|
+
});
|
|
4196
|
+
const startText = startRes?.result?.content?.[0]?.text;
|
|
4197
|
+
const startPayload = JSON.parse(startText || '{}');
|
|
4198
|
+
const newSessionId = startPayload.session_id;
|
|
4199
|
+
if (!newSessionId) {
|
|
4200
|
+
console.error('import: failed to create new session:', startText);
|
|
4201
|
+
process.exit(1);
|
|
4202
|
+
}
|
|
4203
|
+
|
|
4204
|
+
// Build the handoff: take the imported one, append import note.
|
|
4205
|
+
const importNote = `[imported from session ${bundle.session_id} on ${new Date().toISOString()}; originally started ${bundle.started_at}]`;
|
|
4206
|
+
const handoff = { ...bundle.handoff };
|
|
4207
|
+
handoff.notes = handoff.notes ? `${handoff.notes}\n\n${importNote}` : importNote;
|
|
4208
|
+
// Fill repo metadata from current context if the imported handoff
|
|
4209
|
+
// didn't specify (cross-machine sync use case).
|
|
4210
|
+
if (!handoff.repo_path && meta.repo_path) handoff.repo_path = meta.repo_path;
|
|
4211
|
+
if (!handoff.branch && meta.branch) handoff.branch = meta.branch;
|
|
4212
|
+
if (!handoff.project_id && fake.params.arguments.project_id) handoff.project_id = fake.params.arguments.project_id;
|
|
4213
|
+
|
|
4214
|
+
const endRes = await server.makeRequest({
|
|
4215
|
+
jsonrpc: '2.0', id: `cli-import-end-${Date.now()}`,
|
|
4216
|
+
method: 'tools/call', params: { name: 'end_session', arguments: {
|
|
4217
|
+
session_id: newSessionId,
|
|
4218
|
+
keep_open: true,
|
|
4219
|
+
handoff,
|
|
4220
|
+
} },
|
|
4221
|
+
});
|
|
4222
|
+
const endText = endRes?.result?.content?.[0]?.text;
|
|
4223
|
+
const endPayload = JSON.parse(endText || '{}');
|
|
4224
|
+
if (flags.json) {
|
|
4225
|
+
process.stdout.write(JSON.stringify({
|
|
4226
|
+
imported_from: bundle.session_id,
|
|
4227
|
+
new_session_id: newSessionId,
|
|
4228
|
+
short_id: newSessionId.slice(0, 8),
|
|
4229
|
+
handoff_stored: !!endPayload.handoff_stored,
|
|
4230
|
+
resume_command: `npx cf-memory-mcp resume ${newSessionId.slice(0, 8)}`,
|
|
4231
|
+
}, null, 2) + '\n');
|
|
4232
|
+
} else {
|
|
4233
|
+
process.stdout.write(`Imported handoff into session ${newSessionId.slice(0, 8)} (kept_open).\n`);
|
|
4234
|
+
process.stdout.write(`Original: ${bundle.session_id}\n`);
|
|
4235
|
+
process.stdout.write(`Goal: ${handoff.goal}\n`);
|
|
4236
|
+
process.stdout.write(`To resume: npx cf-memory-mcp resume ${newSessionId.slice(0, 8)}\n`);
|
|
4237
|
+
}
|
|
4238
|
+
process.exit(endPayload.handoff_stored ? 0 : 2);
|
|
4239
|
+
} catch (err) {
|
|
4240
|
+
console.error('import command failed:', err.message);
|
|
4241
|
+
process.exit(1);
|
|
4242
|
+
}
|
|
4243
|
+
}
|
|
4244
|
+
|
|
4001
4245
|
async function runCleanCli() {
|
|
4002
4246
|
const { flags, positional } = parseCliArgs(process.argv.slice(3));
|
|
4003
4247
|
const all = positional.includes('--all') || process.argv.includes('--all');
|
|
@@ -4139,12 +4383,61 @@ async function runCheckpointCli() {
|
|
|
4139
4383
|
process.exit(1);
|
|
4140
4384
|
}
|
|
4141
4385
|
const { positional, flags } = parseCliArgs(process.argv.slice(3));
|
|
4386
|
+
const force = process.argv.includes('--force') || process.argv.includes('-f');
|
|
4142
4387
|
const server = new CFMemoryMCP();
|
|
4143
4388
|
server.logDebug = () => {};
|
|
4144
4389
|
server.logError = (...a) => process.stderr.write(a.join(' ') + '\n');
|
|
4145
4390
|
try {
|
|
4146
4391
|
// Optional positional goal argument: `cf-memory-mcp checkpoint "<goal text>"`
|
|
4147
|
-
const goalArg = positional.join(' ').trim();
|
|
4392
|
+
const goalArg = positional.filter(p => p !== '--force' && p !== '-f').join(' ').trim();
|
|
4393
|
+
|
|
4394
|
+
// Duplicate detection: before creating a new implicit session,
|
|
4395
|
+
// check if there's a recent in_progress handoff for the same
|
|
4396
|
+
// repo/branch. If so, suggest resuming it instead of churning a
|
|
4397
|
+
// new one. Skipped with --force.
|
|
4398
|
+
if (!force) {
|
|
4399
|
+
const meta = server.getRepoMetadata();
|
|
4400
|
+
if (meta.repo_path) {
|
|
4401
|
+
const probeArgs = { resume: true, repo_path: meta.repo_path, status_filter: 'in_progress', max_age_minutes: 60 };
|
|
4402
|
+
if (meta.branch) probeArgs.branch = meta.branch;
|
|
4403
|
+
try {
|
|
4404
|
+
const probeRes = await server.makeRequest({
|
|
4405
|
+
jsonrpc: '2.0',
|
|
4406
|
+
id: `cli-checkpoint-probe-${Date.now()}`,
|
|
4407
|
+
method: 'tools/call',
|
|
4408
|
+
params: { name: 'get_context_bootstrap', arguments: probeArgs },
|
|
4409
|
+
});
|
|
4410
|
+
const probeText = probeRes?.result?.content?.[0]?.text;
|
|
4411
|
+
const probePayload = JSON.parse(probeText || '{}');
|
|
4412
|
+
const recent = probePayload.resume_handoff;
|
|
4413
|
+
// Same-cwd implicit session won't be in the worker yet
|
|
4414
|
+
// unless someone called start_session — we look for
|
|
4415
|
+
// OTHER active threads on the same repo.
|
|
4416
|
+
if (recent) {
|
|
4417
|
+
const shortId = (recent.session_id || '').slice(0, 8);
|
|
4418
|
+
const ageMin = recent.handoff_age_minutes ?? '?';
|
|
4419
|
+
const status = recent.handoff?.status || '?';
|
|
4420
|
+
if (flags.json) {
|
|
4421
|
+
process.stdout.write(JSON.stringify({
|
|
4422
|
+
duplicate_detected: true,
|
|
4423
|
+
existing_session_id: recent.session_id,
|
|
4424
|
+
existing_short_id: shortId,
|
|
4425
|
+
existing_age_minutes: ageMin,
|
|
4426
|
+
existing_status: status,
|
|
4427
|
+
existing_goal: recent.handoff?.goal,
|
|
4428
|
+
hint: `Resume the existing in_progress session instead: cf-memory-mcp resume ${shortId}. Re-run with --force to create a new session anyway.`,
|
|
4429
|
+
}, null, 2) + '\n');
|
|
4430
|
+
process.exit(4);
|
|
4431
|
+
}
|
|
4432
|
+
process.stderr.write(`There's already an in_progress handoff for this repo:\n`);
|
|
4433
|
+
process.stderr.write(` ${shortId} ${status} ${ageMin}m ago — "${recent.handoff?.goal || '(no goal)'}"\n`);
|
|
4434
|
+
process.stderr.write(`Resume it: cf-memory-mcp resume ${shortId}\n`);
|
|
4435
|
+
process.stderr.write(`Or re-run with --force to create a new session anyway.\n`);
|
|
4436
|
+
process.exit(4);
|
|
4437
|
+
}
|
|
4438
|
+
} catch (_) { /* probe failure is non-fatal */ }
|
|
4439
|
+
}
|
|
4440
|
+
}
|
|
4148
4441
|
const meta = server.getRepoMetadata();
|
|
4149
4442
|
// Need a session to attach the handoff to. Use or create the
|
|
4150
4443
|
// implicit session for this cwd (creates a new one if no implicit).
|
|
@@ -4230,6 +4523,21 @@ if (process.argv[2] === 'clean') {
|
|
|
4230
4523
|
return;
|
|
4231
4524
|
}
|
|
4232
4525
|
|
|
4526
|
+
if (process.argv[2] === 'export') {
|
|
4527
|
+
runExportCli();
|
|
4528
|
+
return;
|
|
4529
|
+
}
|
|
4530
|
+
|
|
4531
|
+
if (process.argv[2] === 'import') {
|
|
4532
|
+
runImportCli();
|
|
4533
|
+
return;
|
|
4534
|
+
}
|
|
4535
|
+
|
|
4536
|
+
if (process.argv[2] === 'doctor') {
|
|
4537
|
+
runDoctorCli();
|
|
4538
|
+
return;
|
|
4539
|
+
}
|
|
4540
|
+
|
|
4233
4541
|
if (process.argv.includes('--diagnose')) {
|
|
4234
4542
|
(async () => {
|
|
4235
4543
|
console.log(`CF Memory MCP v${PACKAGE_VERSION} - Diagnostics`);
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cf-memory-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.28.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": {
|
|
7
7
|
"cf-memory-mcp": "bin/cf-memory-mcp.js",
|
|
8
|
+
"cfm": "bin/cf-memory-mcp.js",
|
|
8
9
|
"cf-memory-index": "bin/cf-memory-mcp-indexer.js",
|
|
9
10
|
"cf-memory-watch": "bin/cf-memory-mcp-indexer.js"
|
|
10
11
|
},
|