cellstate/commands/
inspect.rs1use crate::http;
9use crate::ui;
10use console::style;
11use reqwest::blocking::Client;
12use serde::Deserialize;
13use std::env;
14use std::time::Duration;
15
16#[derive(Debug, Deserialize)]
21struct NoteListResponse {
22 notes: Vec<NoteItem>,
23 #[serde(default)]
24 total: i32,
25}
26
27#[derive(Debug, Deserialize)]
28struct NoteItem {
29 note_id: String,
30 title: String,
31 content: String,
32 #[serde(default)]
33 abstraction_level: Option<String>,
34 created_at: String,
35 #[serde(default)]
36 receipt_hash: Option<String>,
37}
38
39#[derive(Debug, Deserialize)]
40struct HealthResponse {
41 status: String,
42 #[serde(default)]
43 version: Option<String>,
44}
45
46#[derive(Debug, clap::Args)]
51pub struct InspectArgs {
52 #[arg(
54 long,
55 env = "CELLSTATE_BASE_URL",
56 default_value = http::DEFAULT_BASE_URL,
57 )]
58 pub base_url: String,
59
60 #[arg(long, env = "CELLSTATE_API_KEY")]
62 pub api_key: Option<String>,
63
64 #[arg(long, short = 'i', default_value = "0")]
66 pub interval: u64,
67
68 #[arg(long, default_value = "20")]
70 pub limit: u32,
71}
72
73pub fn run(args: &InspectArgs) -> anyhow::Result<()> {
74 let api_key = args
75 .api_key
76 .clone()
77 .or_else(|| env::var("CELLSTATE_API_KEY").ok());
78
79 let client = http::default_http_client(10)?;
80
81 if args.interval == 0 {
82 print_snapshot(&client, args, api_key.as_deref())?;
84 } else {
85 loop {
87 print_snapshot(&client, args, api_key.as_deref())?;
88 println!();
89 println!(
90 "{}",
91 style(format!(
92 "Refreshing every {}s — Ctrl+C to stop",
93 args.interval
94 ))
95 .dim()
96 );
97 std::thread::sleep(Duration::from_secs(args.interval));
98 print!("\x1B[2J\x1B[H");
100 }
101 }
102
103 Ok(())
104}
105
106fn print_snapshot(
107 client: &Client,
108 args: &InspectArgs,
109 api_key: Option<&str>,
110) -> anyhow::Result<()> {
111 ui::print_banner();
112
113 let health_url = format!("{}/health/live", args.base_url);
115 match client.get(&health_url).send() {
116 Ok(r) if r.status().is_success() => {
117 let h: HealthResponse = r.json().unwrap_or_else(|e| {
118 eprintln!("Failed to parse health response: {e}");
119 HealthResponse {
120 status: "unknown".into(),
121 version: None,
122 }
123 });
124 let version_str = h.version.as_deref().unwrap_or("unknown");
125 let status_badge = match h.status.as_str() {
126 "ok" | "live" => style(&h.status).green(),
127 "degraded" => style(&h.status).yellow(),
128 _ => style(&h.status).red(),
129 };
130 ui::kv_ok(
131 "Server",
132 &format!("{} (v{}) — {}", args.base_url, version_str, status_badge),
133 );
134 }
135 _ => {
136 ui::fail(&format!("Cannot reach CELLSTATE at {}", args.base_url));
137 ui::info("Is CELLSTATE running? Try: cellstate start");
138 return Ok(());
139 }
140 }
141
142 ui::section("Recent Memories");
144
145 let notes_url = format!("{}/api/v1/notes?limit={}", args.base_url, args.limit);
146 let mut req = client.get(¬es_url);
147 if let Some(key) = api_key {
148 req = req.header("X-Api-Key", key);
149 }
150
151 match req.send() {
152 Ok(r) if r.status().is_success() => {
153 let resp: NoteListResponse = r.json()?;
154 let total = if resp.total > 0 {
155 resp.total as usize
156 } else {
157 resp.notes.len()
158 };
159
160 println!(
161 " Showing {} of {} memories",
162 style(resp.notes.len()).cyan(),
163 style(total).cyan()
164 );
165 println!();
166
167 for note in &resp.notes {
168 let level = note.abstraction_level.as_deref().unwrap_or("Raw");
169 let level_badge = match level {
170 "Summary" | "L1" => style("L1").yellow(),
171 "Principle" | "L2" => style("L2").magenta(),
172 _ => style("L0").cyan(),
173 };
174
175 print!(" {} ", level_badge);
177 println!("{}", style(¬e.title).bold());
178
179 let content_preview = if note.content.len() > 100 {
180 format!("{}…", ¬e.content[..100])
181 } else {
182 note.content.clone()
183 };
184 println!(" {}", style(&content_preview).dim());
185
186 let short_id = ¬e.note_id[..8.min(note.note_id.len())];
187 print!(" {} {}", style("id:").dim(), style(short_id).dim());
188
189 if let Some(receipt) = ¬e.receipt_hash {
190 let short_receipt = &receipt[..12.min(receipt.len())];
191 print!(
192 " {} {}",
193 style("receipt:").dim(),
194 style(short_receipt).green()
195 );
196 }
197
198 let ts = note
199 .created_at
200 .split('T')
201 .next()
202 .unwrap_or(¬e.created_at);
203 println!(" {} {}", style("created:").dim(), style(ts).dim());
204 println!();
205 }
206
207 if resp.notes.is_empty() {
208 println!(" {}", style("No memories yet.").dim().italic());
209 println!(
210 " {}",
211 style("Try: memory.add('User prefers dark mode')").dim()
212 );
213 println!();
214 }
215 }
216 Ok(r) if r.status() == 401 => {
217 ui::warn("Unauthorized. Set CELLSTATE_API_KEY or pass --api-key.");
218 }
219 Ok(r) => {
220 ui::warn(&format!("API returned {}", r.status()));
221 }
222 Err(e) => {
223 ui::fail(&format!("Request failed: {e}"));
224 }
225 }
226
227 println!(
229 "{} {} {}",
230 style("Tip:").dim(),
231 style("cellstate inspect --interval 5").cyan(),
232 style("for live updates").dim()
233 );
234 println!(
235 "{} {} {}",
236 style("Tip:").dim(),
237 style("cellstate watch").cyan(),
238 style("for full streaming TUI (coming soon)").dim()
239 );
240 println!();
241
242 Ok(())
243}