feat(cli): implement Dev Spec C4 rules 1-2 — reuse valid env/config key

Before this change, `mem0 init --agent` always minted a fresh shadow,
even when a valid MEM0_API_KEY env var (set by the Claude plugin or
shell rc) was already in place. The env var would then silently shadow
the freshly-minted shadow key on the next CLI call — leading to the
surprising "Agent Mode active but my personal account does the work"
behaviour.

Dev Spec §C4 already specifies the right precedence:
  Rule 1: env MEM0_API_KEY valid → reuse, emit existing_key, no new key
  Rule 2: config api_key valid → reuse, emit existing_key
  Rule 3: mint a fresh shadow

This commit implements rules 1-3 in both runtimes (Python + Node), with
a 5-second ping to validate candidate keys against /v1/ping/.

Also reorders so the Agent Mode branch runs BEFORE the existing-config
overwrite guard. The guard's intent is "warn before overwriting a valid
key" — but rules 1/2 REUSE (not overwrite) a valid key, so the guard
must not fire on the agent path.

Net effect: the plugin's MEM0_API_KEY env var and the CLI's
config.json::platform.api_key now naturally co-exist:

  - User installs plugin → MEM0_API_KEY set, persisted via shell rc
  - User runs `mem0 init --agent` → rule 1 fires → existing key kept,
    no shadow created, no confusion
  - Plugin's MCP server (which reads ${MEM0_API_KEY}) and the CLI use
    the same key

No new shadow accounts on prod from CI / repeat invocations.
This commit is contained in:
Mgeeeek
2026-05-14 16:10:31 +05:30
parent 37d5658171
commit 477279daeb
2 changed files with 97 additions and 22 deletions
+52 -12
View File
@@ -33,6 +33,22 @@ function validateEmail(email: string): void {
}
}
async function pingKey(
apiKey: string,
baseUrl: string,
timeoutMs = 5000,
): Promise<boolean> {
try {
const resp = await fetch(`${baseUrl.replace(/\/+$/, "")}/v1/ping/`, {
headers: { Authorization: `Token ${apiKey}` },
signal: AbortSignal.timeout(timeoutMs),
});
return resp.status === 200;
} catch {
return false;
}
}
async function emailLogin(
email: string,
code: string | undefined,
@@ -304,6 +320,40 @@ export async function runInit(
return;
}
// ── Agent Mode path runs BEFORE the existing-config guard ──────────────
// Rule 1/2 will REUSE a valid existing key (not overwrite), so we must
// short-circuit before the guard prompts the user about overwriting.
// Rule 3 only mints when there's no valid key to reuse — in that case
// overwriting is what the user wants.
const agentCtx =
opts.agent === true || isAgentMode() || detectAgentCaller() !== null;
if (!opts.apiKey && !opts.email && agentCtx) {
// Rule 1: env MEM0_API_KEY valid → reuse, no new key.
const envKey = (process.env.MEM0_API_KEY || "").trim();
if (envKey && (await pingKey(envKey, baseUrl))) {
printSuccess(
"Existing MEM0_API_KEY is valid; reusing it. No new Agent Mode key was minted.",
);
fireInit("existing_key");
return;
}
// Rule 2: existing config api_key valid → reuse.
if (
savedConfig.platform.apiKey &&
(await pingKey(savedConfig.platform.apiKey, baseUrl))
) {
printSuccess(
"Existing API key in config is valid; reusing it. No new Agent Mode key was minted.",
);
fireInit("existing_key");
return;
}
// Rule 3: mint a fresh shadow (no valid key to reuse).
await bootstrapViaBackend(config, { source: opts.source ?? null });
fireInit("agent");
return;
}
// Warn if an existing config with an API key would be overwritten
if (
!opts.force &&
@@ -375,19 +425,9 @@ export async function runInit(
return;
}
// ── Agent Mode auto-bootstrap (no email, no api_key flag) ───────────
// Positive agent signal required: --agent flag (local or global) OR a
// recognized agent env var. Pure "no TTY" alone is NOT enough — pipe
// users would get surprised by a silent shadow signup.
const agentCtx =
opts.agent === true || isAgentMode() || detectAgentCaller() !== null;
if (!opts.apiKey && !opts.email && agentCtx) {
await bootstrapViaBackend(config, { source: opts.source ?? null });
fireInit("agent");
return;
}
// ── API key flow ──────────────────────────────────────────────────────────
// (Agent Mode branch runs earlier — see above, before the existing-config
// guard, so Rules 1/2 can REUSE a valid key without prompting overwrite.)
// Non-TTY: resolve defaults so partial flags work in pipelines / CI
if (!process.stdin.isTTY) {