cellstate/entity/
scope.rs1use 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}