cellstate/entity/
scope.rs

1//! `cellstate scope` — Manage scopes (bounded contexts within a trajectory).
2//!
3//! Subcommands: create, get, update, checkpoint, close, turns, artifacts.
4
5use anyhow::Result;
6use clap::{Arg, ArgMatches, Command};
7
8use cellstate_core::api_types::{
9    CreateCheckpointRequest, CreateScopeRequest, ScopeResponse, UpdateScopeRequest,
10};
11
12use super::client::ApiClient;
13use super::output::OutputConfig;
14use super::require_arg;
15
16const BASE: &str = "/api/v1/scopes";
17
18pub fn build_command() -> Command {
19    Command::new("scope")
20        .about("Manage scopes (bounded contexts within a trajectory)")
21        .subcommand_required(true)
22        .subcommand(
23            Command::new("create")
24                .about("Create a new scope")
25                .arg(
26                    Arg::new("trajectory-id").help(
27                        "Trajectory ID this scope belongs to (defaults to session trajectory)",
28                    ),
29                )
30                .arg(Arg::new("name").required(true).help("Scope name"))
31                .arg(
32                    Arg::new("purpose")
33                        .long("purpose")
34                        .short('p')
35                        .help("Purpose/description"),
36                )
37                .arg(
38                    Arg::new("token-budget")
39                        .long("token-budget")
40                        .short('t')
41                        .default_value("8000")
42                        .help("Token budget for this scope"),
43                )
44                .arg(
45                    Arg::new("parent-scope-id")
46                        .long("parent-scope-id")
47                        .help("Parent scope ID (for nested scopes)"),
48                ),
49        )
50        .subcommand(
51            Command::new("get")
52                .about("Get scope details")
53                .arg(Arg::new("id").required(true).help("Scope ID")),
54        )
55        .subcommand(
56            Command::new("update")
57                .about("Update a scope")
58                .arg(Arg::new("id").required(true).help("Scope ID"))
59                .arg(Arg::new("name").long("name").help("New name"))
60                .arg(
61                    Arg::new("purpose")
62                        .long("purpose")
63                        .short('p')
64                        .help("New purpose"),
65                )
66                .arg(
67                    Arg::new("token-budget")
68                        .long("token-budget")
69                        .short('t')
70                        .help("New token budget"),
71                ),
72        )
73        .subcommand(
74            Command::new("checkpoint")
75                .about("Create a checkpoint for a scope")
76                .arg(Arg::new("id").required(true).help("Scope ID"))
77                .arg(
78                    Arg::new("context-state")
79                        .required(true)
80                        .help("Serialized context state (base64-encoded string)"),
81                )
82                .arg(
83                    Arg::new("recoverable")
84                        .long("recoverable")
85                        .default_value("true")
86                        .help("Whether this checkpoint is recoverable"),
87                ),
88        )
89        .subcommand(
90            Command::new("close")
91                .about("Close a scope")
92                .arg(Arg::new("id").required(true).help("Scope ID")),
93        )
94        .subcommand(
95            Command::new("turns")
96                .about("List turns in a scope")
97                .arg(Arg::new("id").required(true).help("Scope ID")),
98        )
99        .subcommand(
100            Command::new("artifacts")
101                .about("List artifacts in a scope")
102                .arg(Arg::new("id").required(true).help("Scope ID")),
103        )
104}
105
106pub async fn dispatch(
107    matches: &ArgMatches,
108    client: &ApiClient,
109    output: &OutputConfig,
110    session: &crate::session::CliSession,
111) -> Result<()> {
112    match matches.subcommand() {
113        Some(("create", sub)) => {
114            let trajectory_id = require_arg(
115                sub,
116                "trajectory-id",
117                session.trajectory_id.as_deref(),
118                "trajectory",
119            )?
120            .parse()?;
121            let name = sub.get_one::<String>("name").unwrap().clone();
122            let token_budget: i32 = sub.get_one::<String>("token-budget").unwrap().parse()?;
123            let req = CreateScopeRequest {
124                trajectory_id,
125                parent_scope_id: sub
126                    .get_one::<String>("parent-scope-id")
127                    .map(|s| s.parse())
128                    .transpose()?,
129                name,
130                purpose: sub.get_one::<String>("purpose").cloned(),
131                token_budget,
132                metadata: None,
133            };
134            let resp: ScopeResponse = client.post(BASE, &req).await?;
135            output.print(&resp);
136        }
137        Some(("get", sub)) => {
138            let id = sub.get_one::<String>("id").unwrap();
139            let resp: ScopeResponse = client.get(&format!("{BASE}/{id}")).await?;
140            output.print(&resp);
141        }
142        Some(("update", sub)) => {
143            let id = sub.get_one::<String>("id").unwrap();
144            let req = UpdateScopeRequest {
145                name: sub.get_one::<String>("name").cloned(),
146                purpose: sub.get_one::<String>("purpose").cloned(),
147                token_budget: sub
148                    .get_one::<String>("token-budget")
149                    .map(|s| s.parse())
150                    .transpose()?,
151                metadata: None,
152            };
153            let resp: ScopeResponse = client.patch(&format!("{BASE}/{id}"), &req).await?;
154            output.print(&resp);
155        }
156        Some(("checkpoint", sub)) => {
157            let id = sub.get_one::<String>("id").unwrap();
158            let context_state_str = sub.get_one::<String>("context-state").unwrap();
159            let context_state = context_state_str.as_bytes().to_vec();
160            let recoverable: bool = sub.get_one::<String>("recoverable").unwrap().parse()?;
161            let req = CreateCheckpointRequest {
162                context_state,
163                recoverable,
164            };
165            let resp: serde_json::Value = client
166                .post_raw(&format!("{BASE}/{id}/checkpoint"), &req)
167                .await?;
168            output.print_value(&resp);
169        }
170        Some(("close", sub)) => {
171            let id = sub.get_one::<String>("id").unwrap();
172            let resp: serde_json::Value = client
173                .post_no_body_raw(&format!("{BASE}/{id}/close"))
174                .await?;
175            output.print_value(&resp);
176        }
177        Some(("turns", sub)) => {
178            let id = sub.get_one::<String>("id").unwrap();
179            let resp: serde_json::Value = client.get_raw(&format!("{BASE}/{id}/turns")).await?;
180            output.print_value(&resp);
181        }
182        Some(("artifacts", sub)) => {
183            let id = sub.get_one::<String>("id").unwrap();
184            let resp: serde_json::Value = client.get_raw(&format!("{BASE}/{id}/artifacts")).await?;
185            output.print_value(&resp);
186        }
187        _ => unreachable!("subcommand_required(true) prevents this"),
188    }
189    Ok(())
190}