diff --git a/cli/node/src/commands/agent-rush.ts b/cli/node/src/commands/agent-rush.ts index b01f79226..95b1e6d73 100644 --- a/cli/node/src/commands/agent-rush.ts +++ b/cli/node/src/commands/agent-rush.ts @@ -3,10 +3,18 @@ * Project routing is implicit (server-side); zero flags needed. */ +import readline from "node:readline"; import { colors, printError, printSuccess } from "../branding.js"; -import { loadConfig } from "../config.js"; +import { loadConfig, saveConfig } from "../config.js"; import { CLI_VERSION } from "../version.js"; +const PII_WARNING = [ + "", + "⚠️ AGENTRUSH memories are PUBLIC — visible to any other player.", + " Do not include real names, emails, secrets, work content, or PII.", + "", +].join("\n"); + const ERROR_HINTS: Record = { agentrush_search_first: "Run 3 'mem0 agent-rush search' commands before adding.", @@ -66,7 +74,52 @@ async function callEndpoint( return json; } +function promptLine(question: string): Promise { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + return new Promise((resolve) => { + rl.question(question, (answer) => { + rl.close(); + resolve(answer.trim()); + }); + }); +} + +/** + * Ensure the human has acknowledged that AGENTRUSH memories are PUBLIC. + * + * Interactive (TTY): show the prompt; on "y" persist `agentRush.acknowledgedAt` + * so we never ask the same machine twice. On anything else, abort. + * + * Non-interactive (agent invocation, no TTY): print the warning to stderr + * for the human reading the agent's transcript and proceed — agents can't + * answer y/N prompts. + */ +async function ensureWarningAcknowledged(): Promise { + const config = loadConfig(); + if (config.agentRush?.acknowledgedAt) return; + + if (!process.stdin.isTTY || !process.stdout.isTTY) { + // Agent context: surface the warning to stderr, don't block. + console.error(PII_WARNING); + return; + } + + console.log(PII_WARNING); + const answer = (await promptLine(" Continue? [y/N]: ")).toLowerCase(); + if (answer !== "y" && answer !== "yes") { + printError("Aborted."); + process.exit(1); + } + + config.agentRush.acknowledgedAt = new Date().toISOString(); + saveConfig(config); +} + export async function cmdAgentRushAdd(content: string): Promise { + await ensureWarningAcknowledged(); const result = await callEndpoint("/v1/agent-rush/memories/", { content }); printSuccess( `Memory submitted (event_id: ${(result as { event_id?: string }).event_id ?? "?"})`, diff --git a/cli/node/src/config.ts b/cli/node/src/config.ts index 13b8ba227..5774fdcb9 100644 --- a/cli/node/src/config.ts +++ b/cli/node/src/config.ts @@ -40,11 +40,18 @@ export interface TelemetryConfig { anonymousId: string; } +export interface AgentRushConfig { + // ISO timestamp the human acknowledged the "memories are public" warning. + // Empty until first interactive `mem0 agent-rush add`. + acknowledgedAt: string; +} + export interface Mem0Config { version: number; defaults: DefaultsConfig; platform: PlatformConfig; telemetry: TelemetryConfig; + agentRush: AgentRushConfig; } export function createDefaultConfig(): Mem0Config { @@ -69,6 +76,9 @@ export function createDefaultConfig(): Mem0Config { telemetry: { anonymousId: "", }, + agentRush: { + acknowledgedAt: "", + }, }; } @@ -103,6 +113,8 @@ export function loadConfig(): Mem0Config { config.defaults.runId = defaults.run_id ?? ""; const telemetry = data.telemetry ?? {}; config.telemetry.anonymousId = telemetry.anonymous_id ?? ""; + const agentRush = data.agent_rush ?? {}; + config.agentRush.acknowledgedAt = agentRush.acknowledged_at ?? ""; } // Environment variable overrides @@ -143,6 +155,9 @@ export function saveConfig(config: Mem0Config): void { telemetry: { anonymous_id: config.telemetry.anonymousId, }, + agent_rush: { + acknowledged_at: config.agentRush.acknowledgedAt, + }, }; fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2)); diff --git a/cli/python/src/mem0_cli/commands/agent_rush_cmd.py b/cli/python/src/mem0_cli/commands/agent_rush_cmd.py index cd958b40d..b40a3a3a2 100644 --- a/cli/python/src/mem0_cli/commands/agent_rush_cmd.py +++ b/cli/python/src/mem0_cli/commands/agent_rush_cmd.py @@ -6,16 +6,26 @@ Hardcoded routing; no flags needed. from __future__ import annotations +import sys +from datetime import datetime, timezone + import httpx import typer from rich.console import Console from mem0_cli.branding import BRAND_COLOR, print_error, print_success -from mem0_cli.config import load_config +from mem0_cli.config import load_config, save_config console = Console() err_console = Console(stderr=True) +_PII_WARNING_LINES = ( + "", + "[yellow]⚠️ AGENTRUSH memories are PUBLIC — visible to any other player.[/yellow]", + "[yellow] Do not include real names, emails, secrets, work content, or PII.[/yellow]", + "", +) + _SOURCE_HEADERS = { "X-Mem0-Source": "cli", "X-Mem0-Client-Language": "python", @@ -69,7 +79,39 @@ def _call(path: str, body: dict) -> dict: return data +def _ensure_warning_acknowledged() -> None: + """Block the first interactive add on the PII warning; pass-through for agents. + + Interactive (TTY): show prompt, require explicit 'y', persist + `agent_rush.acknowledged_at` so we never ask the same machine twice. + + Non-interactive (no TTY — typical when an agent runs the CLI): surface + the warning to stderr for the human reading the agent transcript and + proceed without prompting (agents can't answer y/N). + """ + config = load_config() + if config.agent_rush.acknowledged_at: + return + + is_tty = sys.stdin.isatty() and sys.stdout.isatty() + if not is_tty: + for line in _PII_WARNING_LINES: + err_console.print(line) + return + + for line in _PII_WARNING_LINES: + console.print(line) + answer = typer.prompt(" Continue? [y/N]", default="N", show_default=False).strip().lower() + if answer not in ("y", "yes"): + print_error(err_console, "Aborted.") + raise typer.Exit(1) + + config.agent_rush.acknowledged_at = datetime.now(timezone.utc).isoformat() + save_config(config) + + def run_agent_rush_add(content: str) -> None: + _ensure_warning_acknowledged() result = _call("/v1/agent-rush/memories/", {"content": content}) event_id = result.get("event_id", "?") print_success(console, f"Memory submitted (event_id: {event_id})") diff --git a/cli/python/src/mem0_cli/config.py b/cli/python/src/mem0_cli/config.py index 6cd11c6ce..0d8de27b9 100644 --- a/cli/python/src/mem0_cli/config.py +++ b/cli/python/src/mem0_cli/config.py @@ -51,12 +51,20 @@ class TelemetryConfig: anonymous_id: str = "" +@dataclass +class AgentRushConfig: + # ISO timestamp the human acknowledged the "memories are public" warning. + # Empty until first interactive `mem0 agent-rush add`. + acknowledged_at: str = "" + + @dataclass class Mem0Config: version: int = CONFIG_VERSION defaults: DefaultsConfig = field(default_factory=DefaultsConfig) platform: PlatformConfig = field(default_factory=PlatformConfig) telemetry: TelemetryConfig = field(default_factory=TelemetryConfig) + agent_rush: AgentRushConfig = field(default_factory=AgentRushConfig) SHORT_KEY_ALIASES: dict[str, str] = { @@ -105,6 +113,9 @@ def load_config() -> Mem0Config: telemetry = data.get("telemetry", {}) config.telemetry.anonymous_id = telemetry.get("anonymous_id", "") + agent_rush = data.get("agent_rush", {}) + config.agent_rush.acknowledged_at = agent_rush.get("acknowledged_at", "") + # Environment variable overrides env_key = os.environ.get("MEM0_API_KEY") if env_key: @@ -158,6 +169,9 @@ def save_config(config: Mem0Config) -> None: "telemetry": { "anonymous_id": config.telemetry.anonymous_id, }, + "agent_rush": { + "acknowledged_at": config.agent_rush.acknowledged_at, + }, } with open(CONFIG_FILE, "w") as f: