cellstate/main.rs
1//! CELLSTATE CLI — Universal installer and local development tool.
2//!
3//! Install:
4//! curl -fsSL https://cellstate.batterypack.dev/install | sh
5//!
6//! Commands:
7//! cellstate setup <- Interactive first-run installer (detect env, install SDKs)
8//! cellstate init <- Scaffold .cellstate/ config in current project
9//! cellstate start <- Start local CELLSTATE server (Docker)
10//! cellstate stop <- Stop local CELLSTATE server
11//! cellstate inspect <- View live memory state
12//! cellstate use <- Set or display session scope
13//! cellstate status <- Show session state and server connectivity
14//! cellstate trajectory <- Manage trajectories
15//! cellstate agent <- Manage agents
16//! cellstate note <- Manage notes
17//! cellstate context <- Context commit and recall
18
19mod commands;
20pub(crate) mod entity;
21pub(crate) mod http;
22pub(crate) mod session;
23mod ui;
24
25use clap::{CommandFactory, FromArgMatches, Parser, Subcommand};
26
27// ============================================================================
28// CLI definition (derive-based infra commands)
29// ============================================================================
30
31#[derive(Parser)]
32#[command(
33 name = "cellstate",
34 about = "Agent memory that doesn't forget, lie, or black-box you.",
35 long_about = None,
36 version,
37 propagate_version = true,
38)]
39struct Cli {
40 /// Output as JSON (for agent/script consumption)
41 #[arg(long, global = true)]
42 json: bool,
43
44 #[command(subcommand)]
45 command: Option<Commands>,
46}
47
48#[derive(Subcommand)]
49enum Commands {
50 /// Interactive first-run setup: detect environment, install SDKs, start server.
51 ///
52 /// Run this first. It detects Python, Node, Rust, and Docker,
53 /// asks what you're building with, installs only what you need,
54 /// and optionally starts a local CELLSTATE server.
55 Setup,
56
57 /// Scaffold a CELLSTATE project in the current directory.
58 ///
59 /// Creates .cellstate/config.toml, .env.example, and cellstate.lock.
60 Init,
61
62 /// Start the local CELLSTATE server (PostgreSQL + API) via Docker.
63 ///
64 /// Equivalent to docker compose up with a minimal production-like stack.
65 /// Runs in the background. Use `cellstate stop` to shut down.
66 Start,
67
68 /// Stop the local CELLSTATE server.
69 Stop,
70
71 /// View live memory state from a running CELLSTATE instance.
72 ///
73 /// Polls the API and displays active memories with Blake3 receipts.
74 /// For streaming TUI: see `cellstate watch` (coming soon).
75 Inspect(commands::inspect::InspectArgs),
76
77 /// Pack compilation and AAIF convergence output commands.
78 ///
79 /// Compile a CELLSTATE pack and emit convergence outputs: AGENTS.md,
80 /// llms.txt, A2A Agent Card, SKILL.md, A2UI schema.
81 Pack(commands::pack::PackArgs),
82
83 /// Set or display session scope (tenant, agent, trajectory, scope).
84 ///
85 /// Persists scope selections so they don't need to be passed as flags
86 /// on every command. Use `cellstate use tenant <uuid>` to set scope,
87 /// `cellstate use --clear` to reset, or `cellstate use` to display.
88 #[command(name = "use")]
89 Use(commands::use_cmd::UseArgs),
90
91 /// Show session state and server connectivity.
92 ///
93 /// Displays current scope, server health, version, and auth status.
94 Status,
95}
96
97// ============================================================================
98// Known entity command names (Phase 3 pilot)
99// ============================================================================
100
101/// Entity command names that are handled by the builder-API entity module.
102const ENTITY_COMMANDS: &[&str] = &[
103 "trajectory",
104 "agent",
105 "scope",
106 "artifact",
107 "note",
108 "turn",
109 "context",
110 "lock",
111 "message",
112 "delegation",
113 "handoff",
114 "edge",
115 "team",
116 "tenant",
117 "api-key",
118 "event-dag",
119 "search",
120 "batch",
121 "summarization-policy",
122 "summarization-request",
123 "deployment",
124 "working-set",
125 "config",
126 "gates",
127 "models",
128];
129
130fn is_entity_command(name: &str) -> bool {
131 ENTITY_COMMANDS.contains(&name)
132}
133
134// ============================================================================
135// Entry point
136// ============================================================================
137
138#[tokio::main]
139async fn main() {
140 // Build the full command tree: derive commands + builder entity commands.
141 // This lets `cellstate --help` show both infra and entity commands.
142 let mut app = Cli::command();
143 for cmd in entity::build_entity_commands() {
144 app = app.subcommand(cmd);
145 }
146 let mut matches = app.get_matches();
147
148 // Extract global --json flag before dispatching
149 let json = matches.get_flag("json");
150
151 // Try derive-based infra commands first.
152 // from_arg_matches may fail if the subcommand is a builder entity command,
153 // which is expected — we fall through to entity dispatch.
154 if let Ok(cli) = Cli::from_arg_matches_mut(&mut matches) {
155 // Infra commands are sync and some use reqwest::blocking::Client which
156 // creates its own tokio runtime. We must run them on a blocking thread
157 // to avoid "cannot drop a runtime in async context" panics.
158 let result = tokio::task::spawn_blocking(move || match cli.command {
159 Some(Commands::Setup) | None => commands::setup::run(),
160 Some(Commands::Init) => commands::init::run(),
161 Some(Commands::Start) => commands::start::run(),
162 Some(Commands::Stop) => commands::start::stop(),
163 Some(Commands::Inspect(args)) => commands::inspect::run(&args),
164 Some(Commands::Pack(args)) => commands::pack::run(&args),
165 Some(Commands::Use(args)) => commands::use_cmd::run_with_json(&args, cli.json),
166 Some(Commands::Status) => commands::status::run_with_json(cli.json),
167 })
168 .await;
169 match result {
170 Ok(Ok(())) => {}
171 Ok(Err(e)) => {
172 eprintln!("{} {}", console::style("error:").red().bold(), e);
173 std::process::exit(1);
174 }
175 Err(e) => {
176 eprintln!("{} {}", console::style("error:").red().bold(), e);
177 std::process::exit(1);
178 }
179 }
180 return;
181 }
182
183 // Try entity commands (builder-API, async)
184 if let Some((sub_name, sub_matches)) = matches.subcommand() {
185 if is_entity_command(sub_name) {
186 let session = match session::CliSession::load() {
187 Ok(s) => s,
188 Err(e) => {
189 eprintln!("{} {}", console::style("error:").red().bold(), e);
190 std::process::exit(1);
191 }
192 };
193 let client = entity::client::ApiClient::new(
194 session.effective_base_url(None),
195 session.effective_api_key(None),
196 session.tenant_id.clone(),
197 );
198 let output = entity::output::OutputConfig { json };
199 if let Err(e) =
200 entity::dispatch(sub_name, sub_matches, &client, &output, &session).await
201 {
202 eprintln!("{} {}", console::style("error:").red().bold(), e);
203 std::process::exit(1);
204 }
205 return;
206 }
207 }
208
209 // No command matched — show help
210 let _ = Cli::command().print_help();
211}