use std::path::Path;
use anyhow::{Result, anyhow};
use clap::Args;
use crate::autonomy::AutonomyLlm;
use crate::cli::CliOutput;
use crate::db;
use crate::persona::{
PersonaConfig, PersonaError, PersonaGenerator, get_latest_persona, render_persona_json,
render_persona_md,
};
#[derive(Args, Debug, Clone)]
pub struct PersonaArgs {
#[arg(value_name = "ENTITY_ID")]
pub entity_id: String,
#[arg(long, value_name = "NS", default_value = crate::DEFAULT_NAMESPACE)]
pub namespace: String,
#[arg(long, default_value_t = false)]
pub regenerate: bool,
#[arg(long, default_value_t = false)]
pub json: bool,
}
pub fn run(
db_path: &Path,
args: &PersonaArgs,
llm: Option<&dyn AutonomyLlm>,
active_keypair: Option<&crate::identity::keypair::AgentKeypair>,
out: &mut CliOutput<'_>,
) -> Result<i32> {
let conn = db::open(db_path)?;
if args.regenerate {
let Some(llm) = llm else {
writeln!(
out.stderr,
"persona --regenerate requires an LLM client; \
install Ollama and re-run on the smart tier or higher.",
)?;
return Ok(2);
};
let generator = PersonaGenerator::new(&conn, llm, active_keypair, PersonaConfig::default());
match generator.generate(&args.entity_id, &args.namespace) {
Ok(persona) => {
render_to_stdout(out, &persona, args.json)?;
return Ok(0);
}
Err(PersonaError::NoReflections {
entity_id,
namespace,
}) => {
writeln!(
out.stderr,
"no reflections found for entity '{entity_id}' in namespace '{namespace}'"
)?;
return Ok(2);
}
Err(e) => {
writeln!(out.stderr, "persona generation failed: {e}")?;
return Ok(2);
}
}
}
let persona = get_latest_persona(&conn, &args.entity_id, &args.namespace)?
.ok_or_else(|| {
anyhow!(
"no persona has been minted for '{}' in namespace '{}' — pass --regenerate to create one",
args.entity_id,
args.namespace,
)
})?;
render_to_stdout(out, &persona, args.json)?;
Ok(0)
}
fn render_to_stdout(
out: &mut CliOutput<'_>,
persona: &crate::persona::Persona,
json: bool,
) -> Result<()> {
let text = if json {
render_persona_json(persona)
} else {
render_persona_md(persona)
};
writeln!(out.stdout, "{text}")?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::CliOutput;
use crate::models::{Memory, MemoryKind, Tier};
use crate::storage as db;
use chrono::Utc;
use rusqlite::Connection;
use tempfile::TempDir;
struct StubLlm;
impl AutonomyLlm for StubLlm {
fn auto_tag(&self, _t: &str, _c: &str) -> anyhow::Result<Vec<String>> {
Ok(Vec::new())
}
fn detect_contradiction(&self, _a: &str, _b: &str) -> anyhow::Result<bool> {
Ok(false)
}
fn summarize_memories(&self, mems: &[(String, String)]) -> anyhow::Result<String> {
Ok(format!("CLI persona body ({} sources)", mems.len()))
}
}
fn fresh_db() -> (Connection, TempDir, std::path::PathBuf) {
let dir = TempDir::new().unwrap();
let path = dir.path().join("ai-memory.db");
let conn = db::open(&path).unwrap();
(conn, dir, path)
}
fn seed_reflection(conn: &Connection, namespace: &str, body: &str) -> String {
let now = Utc::now().to_rfc3339();
let mem = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Mid,
namespace: namespace.to_string(),
title: format!("ref about alice {}", &now[..19]),
content: body.to_string(),
tags: vec!["reflection".into()],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: now.clone(),
updated_at: now,
last_accessed_at: None,
expires_at: None,
metadata: serde_json::json!({"agent_id": "ai:test", "entity_id": "alice"}),
reflection_depth: 1,
memory_kind: MemoryKind::Reflection,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
db::insert(conn, &mem).unwrap()
}
#[test]
fn cli_persona_read_with_no_persona_errors() {
let (_conn, _dir, db_path) = fresh_db();
let mut stdout: Vec<u8> = Vec::new();
let mut stderr: Vec<u8> = Vec::new();
let mut out = CliOutput::from_std(&mut stdout, &mut stderr);
let args = PersonaArgs {
entity_id: "alice".into(),
namespace: "team/alpha".into(),
regenerate: false,
json: false,
};
let res = run(&db_path, &args, None, None, &mut out);
assert!(res.is_err());
}
#[test]
fn cli_persona_regenerate_creates_and_prints_md() {
let (conn, _dir, db_path) = fresh_db();
seed_reflection(&conn, "team/alpha", "alice is methodical");
let mut stdout: Vec<u8> = Vec::new();
let mut stderr: Vec<u8> = Vec::new();
let mut out = CliOutput::from_std(&mut stdout, &mut stderr);
let llm = StubLlm;
let args = PersonaArgs {
entity_id: "alice".into(),
namespace: "team/alpha".into(),
regenerate: true,
json: false,
};
let code = run(&db_path, &args, Some(&llm), None, &mut out).unwrap();
assert_eq!(code, 0);
drop(out);
let text = String::from_utf8(stdout).unwrap();
assert!(text.contains("entity_id: alice"));
assert!(text.contains("persona_version: 1"));
}
#[test]
fn cli_persona_regenerate_requires_llm() {
let (_conn, _dir, db_path) = fresh_db();
let mut stdout: Vec<u8> = Vec::new();
let mut stderr: Vec<u8> = Vec::new();
let mut out = CliOutput::from_std(&mut stdout, &mut stderr);
let args = PersonaArgs {
entity_id: "alice".into(),
namespace: "team/alpha".into(),
regenerate: true,
json: false,
};
let code = run(&db_path, &args, None, None, &mut out).unwrap();
assert_eq!(code, 2);
drop(out);
let text = String::from_utf8(stderr).unwrap();
assert!(text.contains("requires an LLM"));
}
#[test]
fn cli_persona_regenerate_json_envelope() {
let (conn, _dir, db_path) = fresh_db();
seed_reflection(&conn, "team/alpha", "alice notes");
let mut stdout: Vec<u8> = Vec::new();
let mut stderr: Vec<u8> = Vec::new();
let mut out = CliOutput::from_std(&mut stdout, &mut stderr);
let llm = StubLlm;
let args = PersonaArgs {
entity_id: "alice".into(),
namespace: "team/alpha".into(),
regenerate: true,
json: true,
};
let code = run(&db_path, &args, Some(&llm), None, &mut out).unwrap();
assert_eq!(code, 0);
drop(out);
let text = String::from_utf8(stdout).unwrap();
let v: serde_json::Value = serde_json::from_str(text.trim()).unwrap();
assert_eq!(v["entity_id"], "alice");
assert_eq!(v["persona_version"], 1);
}
#[test]
fn cli_persona_regenerate_no_reflections_returns_two() {
let (_conn, _dir, db_path) = fresh_db();
let mut stdout: Vec<u8> = Vec::new();
let mut stderr: Vec<u8> = Vec::new();
let mut out = CliOutput::from_std(&mut stdout, &mut stderr);
let llm = StubLlm;
let args = PersonaArgs {
entity_id: "ghost".into(),
namespace: "team/alpha".into(),
regenerate: true,
json: false,
};
let code = run(&db_path, &args, Some(&llm), None, &mut out).unwrap();
assert_eq!(code, 2);
drop(out);
let text = String::from_utf8(stderr).unwrap();
assert!(text.contains("no reflections"), "got: {text}");
}
#[test]
fn cli_persona_read_after_regenerate_succeeds() {
let (conn, _dir, db_path) = fresh_db();
seed_reflection(&conn, "team/alpha", "alice is precise");
let llm = StubLlm;
{
let mut stdout: Vec<u8> = Vec::new();
let mut stderr: Vec<u8> = Vec::new();
let mut out = CliOutput::from_std(&mut stdout, &mut stderr);
let regen = PersonaArgs {
entity_id: "alice".into(),
namespace: "team/alpha".into(),
regenerate: true,
json: false,
};
assert_eq!(
run(&db_path, ®en, Some(&llm), None, &mut out).unwrap(),
0
);
}
let mut stdout: Vec<u8> = Vec::new();
let mut stderr: Vec<u8> = Vec::new();
let mut out = CliOutput::from_std(&mut stdout, &mut stderr);
let read = PersonaArgs {
entity_id: "alice".into(),
namespace: "team/alpha".into(),
regenerate: false,
json: true,
};
let code = run(&db_path, &read, None, None, &mut out).unwrap();
assert_eq!(code, 0);
drop(out);
let v: serde_json::Value =
serde_json::from_str(String::from_utf8(stdout).unwrap().trim()).unwrap();
assert_eq!(v["entity_id"], "alice");
}
}