memstead_cli/commands/
entity.rs1use clap::Parser;
2
3use memstead_base::Entity;
4use memstead_base::EntityId;
5use memstead_base::Store;
6use memstead_base::chunking::apply_chunking;
7use memstead_base::render;
8
9use crate::CliError;
10use crate::output::{ExitKind, print_markdown};
11use crate::setup::{CliContext, CliEngine};
12
13#[derive(Parser, Debug)]
15pub struct Args {
16 pub id: String,
18
19 #[arg(long = "section", value_name = "KEY")]
21 pub sections: Vec<String>,
22
23 #[arg(long)]
25 pub include_relations: bool,
26
27 #[arg(long)]
29 pub token_budget: Option<usize>,
30
31 #[arg(long)]
33 pub chunk: Option<usize>,
34}
35
36pub fn run(ctx: &CliContext, args: Args) -> anyhow::Result<()> {
37 let id = EntityId::canonical(&args.id);
38 let not_found = || {
43 CliError::new(
44 ExitKind::NotFound,
45 "ENTITY_NOT_FOUND",
46 format!("Entity not found: {}", args.id),
47 )
48 .with_details(serde_json::json!({ "id": args.id }))
49 };
50 let (entity, output, outgoing_snapshot) = match ctx.cli_engine()? {
57 #[cfg(feature = "mem-repo")]
58 CliEngine::MemRepo(engine) => {
59 let entity = engine.get_entity(&id).cloned().ok_or_else(not_found)?;
60 let md = render_with_optional_relations(&entity, &id, engine.store(), &args);
61 let outgoing = engine.store().outgoing(&id).to_vec();
62 (entity, md, outgoing)
63 }
64 CliEngine::Filesystem(engine) => {
65 let entity = engine.get_entity(&id).cloned().ok_or_else(not_found)?;
66 let md = render_with_optional_relations(&entity, &id, engine.store(), &args);
67 let outgoing = engine.store().outgoing(&id).to_vec();
68 (entity, md, outgoing)
69 }
70 };
71
72 let chunked = match args.token_budget {
73 Some(budget) => apply_chunking(
74 &output,
75 budget,
76 args.chunk,
77 &[("_hash", &entity.content_hash)],
78 )
79 .map_err(|e| CliError::new(ExitKind::Generic, "CHUNK_OUT_OF_RANGE", e))?,
80 None => output.to_string(),
81 };
82
83 if ctx.json {
84 let sections_filter = if args.sections.is_empty() {
92 None
93 } else {
94 Some(args.sections.as_slice())
95 };
96 let rendered_body_tokens = memstead_base::chunking::estimate_tokens(&output);
97 let full_tokens = if sections_filter.is_some() {
98 let full_body = render::render_entity_markdown(&entity, None);
99 Some(memstead_base::chunking::estimate_tokens(&full_body))
100 } else {
101 None
102 };
103 let envelope = render::build_entity_envelope(
104 &entity,
105 rendered_body_tokens,
106 full_tokens,
107 sections_filter,
108 None,
109 &outgoing_snapshot,
110 );
111 crate::output::print_json(&envelope)?;
112 } else {
113 print_markdown(&chunked);
114 }
115 Ok(())
116}
117
118fn render_with_optional_relations(
122 entity: &Entity,
123 id: &EntityId,
124 store: &Store,
125 args: &Args,
126) -> String {
127 let sections_filter = if args.sections.is_empty() {
128 None
129 } else {
130 Some(args.sections.as_slice())
131 };
132 let mut md = render::render_entity_markdown(entity, sections_filter);
133 if args.include_relations {
134 let outgoing = store.outgoing(id).to_vec();
135 let incoming = store.incoming(id).to_vec();
136 let rel_json = render::render_relations_json(id.as_ref(), &outgoing, &incoming);
137 md.push_str("\n## Relations (JSON)\n\n```json\n");
138 md.push_str(&serde_json::to_string_pretty(&rel_json).unwrap_or_else(|_| "{}".to_string()));
139 md.push_str("\n```\n");
140 }
141 md
142}