use crate::models::field_names;
use anyhow::Result;
use clap::Args;
use serde_json::{Value, json};
use crate::cli::CliOutput;
use crate::storage as db;
#[derive(Args, Debug, Clone)]
pub struct ShareArgs {
#[arg(long = "memory-id", value_name = "ID")]
pub memory_id: String,
#[arg(long = "target-agent", value_name = "AGENT_ID")]
pub target_agent: String,
#[arg(long)]
pub json: bool,
}
pub fn cmd_share(
db_path: &std::path::Path,
args: &ShareArgs,
out: &mut CliOutput<'_>,
) -> Result<()> {
let conn = db::open(db_path)?;
let params: Value = json!({
(field_names::SOURCE_MEMORY_ID): args.memory_id,
(field_names::TARGET_AGENT_ID): args.target_agent,
});
let envelope = crate::mcp::share::handle_share(&conn, ¶ms)
.map_err(|e| anyhow::anyhow!("share: {e}"))?;
if args.json {
writeln!(out.stdout, "{}", serde_json::to_string(&envelope)?)?;
return Ok(());
}
let shared_id = envelope
.get("shared_memory_id")
.and_then(Value::as_str)
.unwrap_or("?");
let target_ns = envelope
.get(field_names::TARGET_NAMESPACE)
.and_then(Value::as_str)
.unwrap_or("?");
let from_agent = envelope
.get(field_names::FROM_AGENT_ID)
.and_then(Value::as_str)
.unwrap_or("?");
writeln!(
out.stdout,
"shared {} ({} → {}) into {}",
shared_id, from_agent, args.target_agent, target_ns,
)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::test_utils::{TestEnv, seed_memory};
#[test]
fn share_cli_copies_memory_into_shared_namespace_1095() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
let src_id = seed_memory(&db, "alice/notes", "shared-src-cli", "share me via CLI");
{
let conn = db::open(&db).expect("open db for metadata patch");
conn.execute(
"UPDATE memories SET metadata = json_set(metadata, '$.agent_id', 'ai:alice') WHERE id = ?1",
rusqlite::params![src_id],
)
.expect("patch metadata.agent_id");
}
let args = ShareArgs {
memory_id: src_id.clone(),
target_agent: "ai:bob".to_string(),
json: true,
};
{
let mut out = env.output();
cmd_share(&db, &args, &mut out).expect("share ok");
}
let stdout = env.stdout_str();
let envelope: Value = serde_json::from_str(stdout.trim()).expect("parse envelope");
assert!(
envelope["shared_memory_id"].is_string(),
"#1095: shared_memory_id present"
);
assert_eq!(
envelope["source_memory_id"], src_id,
"#1095: source_memory_id echoes input"
);
assert_eq!(
envelope["target_agent_id"], "ai:bob",
"#1095: target_agent_id echoes input"
);
assert_eq!(
envelope["from_agent_id"], "ai:alice",
"#1095: from_agent_id derived from source row metadata"
);
assert!(
envelope["target_namespace"]
.as_str()
.unwrap_or("")
.starts_with("_shared/"),
"#1095: target_namespace begins with _shared/"
);
}
#[test]
fn share_cli_text_mode_emits_one_line_summary_1095() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
let src_id = seed_memory(&db, "alice/notes", "shared-src-text", "share me");
{
let conn = db::open(&db).expect("open db for metadata patch");
conn.execute(
"UPDATE memories SET metadata = json_set(metadata, '$.agent_id', 'ai:alice') WHERE id = ?1",
rusqlite::params![src_id],
)
.expect("patch metadata.agent_id");
}
let args = ShareArgs {
memory_id: src_id,
target_agent: "ai:bob".to_string(),
json: false,
};
{
let mut out = env.output();
cmd_share(&db, &args, &mut out).expect("share ok");
}
let stdout = env.stdout_str();
assert!(stdout.starts_with("shared "), "got: {stdout}");
assert!(stdout.contains("ai:alice"), "got: {stdout}");
assert!(stdout.contains("ai:bob"), "got: {stdout}");
assert!(stdout.contains("_shared/"), "got: {stdout}");
}
#[test]
fn share_cli_missing_source_returns_err_1095() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
let args = ShareArgs {
memory_id: uuid::Uuid::new_v4().to_string(),
target_agent: "ai:bob".to_string(),
json: true,
};
let mut out = env.output();
let err = cmd_share(&db, &args, &mut out).expect_err("must fail");
assert!(err.to_string().contains("not found"), "got: {err}");
}
}