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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user