Skip to main content

ai_memory/cli/commands/
entity_register.rs

1// Copyright 2026 AlphaOne LLC
2// SPDX-License-Identifier: Apache-2.0
3
4//! v0.7.0 ARCH-3 / FX-C3 (batch2) — `ai-memory entity-register` CLI
5//! subcommand.
6//!
7//! Closes the three-surface-parity gap on `memory_entity_register`.
8//! The MCP tool ([`crate::mcp::handle_entity_register`]) and the HTTP
9//! route landed previously; this module wires the CLI surface so
10//! operators can register a canonical entity (with aliases) from a
11//! terminal.
12
13use crate::models::field_names;
14use anyhow::Result;
15use clap::Args;
16use serde_json::{Value, json};
17
18use crate::cli::CliOutput;
19use crate::storage as db;
20
21/// CLI args for `ai-memory entity-register`.
22#[derive(Args, Debug, Clone)]
23pub struct EntityRegisterArgs {
24    /// Display name (entity memory title).
25    #[arg(long = "canonical-name", value_name = "NAME")]
26    pub canonical_name: String,
27
28    /// Entity namespace.
29    #[arg(long, value_name = "NS")]
30    pub namespace: String,
31
32    /// Optional aliases (comma-separated). Blanks skipped, deduped.
33    #[arg(long, value_name = "CSV", value_delimiter = ',')]
34    pub aliases: Vec<String>,
35
36    /// Optional caller agent_id override.
37    #[arg(long = "agent-id", value_name = "AGENT_ID")]
38    pub agent_id: Option<String>,
39
40    /// Emit the raw JSON envelope.
41    #[arg(long)]
42    pub json: bool,
43}
44
45/// `ai-memory entity-register` dispatch entry.
46///
47/// # Errors
48///
49/// - The DB at `db_path` cannot be opened.
50/// - The substrate refuses the call (validation, name collision with
51///   non-entity row).
52/// - `serde_json::to_string` cannot serialise the envelope.
53pub fn cmd_entity_register(
54    db_path: &std::path::Path,
55    args: &EntityRegisterArgs,
56    out: &mut CliOutput<'_>,
57) -> Result<()> {
58    let conn = db::open(db_path)?;
59
60    let mut params = json!({
61        (field_names::CANONICAL_NAME): args.canonical_name,
62        "namespace": args.namespace,
63    });
64    if !args.aliases.is_empty() {
65        params["aliases"] = json!(args.aliases);
66    }
67    if let Some(a) = &args.agent_id {
68        params["agent_id"] = json!(a);
69    }
70
71    let envelope = crate::mcp::handle_entity_register(&conn, &params, None)
72        .map_err(|e| anyhow::anyhow!("entity-register: {e}"))?;
73
74    if args.json {
75        writeln!(out.stdout, "{}", serde_json::to_string(&envelope)?)?;
76        return Ok(());
77    }
78
79    let id = envelope
80        .get("entity_id")
81        .and_then(Value::as_str)
82        .unwrap_or("?");
83    let created = envelope
84        .get("created")
85        .and_then(Value::as_bool)
86        .unwrap_or(false);
87    writeln!(
88        out.stdout,
89        "entity-register: entity_id={id}  created={created}"
90    )?;
91    Ok(())
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use crate::cli::test_utils::TestEnv;
98
99    #[test]
100    fn entity_register_cli_happy_path_writes_envelope() {
101        let mut env = TestEnv::fresh();
102        let db = env.db_path.clone();
103        let args = EntityRegisterArgs {
104            canonical_name: "Alice".into(),
105            namespace: "characters".into(),
106            aliases: vec!["al".into(), "ali".into()],
107            agent_id: Some("ai:tester".into()),
108            json: true,
109        };
110        {
111            let mut out = env.output();
112            cmd_entity_register(&db, &args, &mut out).expect("ok");
113        }
114        let stdout = env.stdout_str();
115        let envelope: Value = serde_json::from_str(stdout.trim()).expect("parse envelope");
116        assert_eq!(envelope["canonical_name"].as_str(), Some("Alice"));
117        assert_eq!(envelope["created"].as_bool(), Some(true));
118    }
119
120    #[test]
121    fn entity_register_cli_empty_name_returns_err() {
122        let mut env = TestEnv::fresh();
123        let db = env.db_path.clone();
124        let args = EntityRegisterArgs {
125            canonical_name: String::new(),
126            namespace: "characters".into(),
127            aliases: vec![],
128            agent_id: None,
129            json: true,
130        };
131        let mut out = env.output();
132        let err = cmd_entity_register(&db, &args, &mut out).expect_err("must fail");
133        assert!(err.to_string().contains("entity-register"), "got: {err}");
134    }
135
136    #[test]
137    fn entity_register_cli_text_output_no_aliases_no_agent() {
138        let mut env = TestEnv::fresh();
139        let db = env.db_path.clone();
140        let args = EntityRegisterArgs {
141            canonical_name: "Dave".into(),
142            namespace: "characters".into(),
143            aliases: vec![],
144            agent_id: None,
145            json: false,
146        };
147        {
148            let mut out = env.output();
149            cmd_entity_register(&db, &args, &mut out).expect("ok");
150        }
151        let stdout = env.stdout_str();
152        assert!(
153            stdout.contains("entity-register: entity_id="),
154            "got: {stdout}"
155        );
156        assert!(stdout.contains("created=true"), "got: {stdout}");
157    }
158}