feat(cli): Agent Mode bootstrap + claim flow (Node)

Mirrors the Python implementation in TypeScript:
  - New PlatformConfig fields: agentMode, createdVia, claimedAt, defaultUserId.
  - agent-detect.ts: detectAgentCaller() — env-var detection covering
    CLAUDECODE / CURSOR_AGENT / CODEX_CLI / CLINE / CONTINUE / AIDER /
    GOOSE / WINDSURF.
  - commands/agent-mode.ts: bootstrapViaBackend() + claimViaDeviceFlow().
  - commands/init.ts: decision tree dispatches to bootstrap (positive agent
    signal + no email/api-key) or claim (--email with existing agent-mode
    config). Raw API key never leaves the device through the claim.
  - index.ts: --agent and --source flags added to `mem0 init`. Skip the
    preAction auto-fire for init so it can fire its own M1-M6 cli.init.
  - telemetry.ts: all cli.* events now carry agent_mode based on
    config.platform.agentMode (per growth-doc M4).

End-to-end verified against the sandbox:
  bootstrap (CLAUDECODE=1) → config.agent_mode=true → claim via --email →
  config.agent_mode=false, claimed_at set, api_key unchanged.
This commit is contained in:
Mgeeeek
2026-05-13 23:08:20 +05:30
parent 84ffb36190
commit bcba0560c4
6 changed files with 296 additions and 2 deletions
+42 -1
View File
@@ -249,14 +249,31 @@ export async function runInit(
email?: string;
code?: string;
force?: boolean;
agent?: boolean;
source?: string;
} = {},
): Promise<void> {
const { detectAgentCaller } = await import("../agent-detect.js");
const { bootstrapViaBackend, claimViaDeviceFlow } = await import("./agent-mode.js");
const { isAgentMode } = await import("../state.js");
const { captureEvent } = await import("../telemetry.js");
const fireInit = (mode: "agent" | "email" | "api_key" | "existing_key", claimed = false) => {
const props: Record<string, unknown> = { command: "init", mode };
const caller = detectAgentCaller();
if (caller) props.agent_caller = caller;
if (opts.source) props.signup_source = opts.source;
if (claimed) props.claimed_agent_mode = true;
captureEvent("cli.init", props);
};
const config = createDefaultConfig();
const savedConfig = loadConfig();
const baseUrl =
process.env.MEM0_BASE_URL ||
savedConfig.platform.baseUrl ||
DEFAULT_BASE_URL;
config.platform.baseUrl = baseUrl;
// Guards
if (opts.code && !opts.email) {
@@ -268,6 +285,16 @@ export async function runInit(
process.exit(1);
}
// ── Claim flow: --email against an existing agent-mode config ───────────
if (opts.email && fs.existsSync(CONFIG_FILE) && savedConfig.platform.agentMode && savedConfig.platform.apiKey) {
const email = opts.email.trim().toLowerCase();
validateEmail(email);
printInfo(`Claiming Agent Mode account to ${email}...`);
await claimViaDeviceFlow(savedConfig, { email });
fireInit("email", true);
return;
}
// Warn if an existing config with an API key would be overwritten
if (
!opts.force &&
@@ -338,6 +365,20 @@ 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 ──────────────────────────────────────────────────────────
// Non-TTY: resolve defaults so partial flags work in pipelines / CI
@@ -345,7 +386,7 @@ export async function runInit(
if (!opts.apiKey) {
printError(
"Non-interactive terminal detected and --api-key is required.",
"Usage: mem0 init --api-key <key> [--user-id <id>]",
"Usage: mem0 init --api-key <key>, --email <addr>, or --agent for unattended Agent Mode bootstrap.",
);
process.exit(1);
}