cellstate/entity/
working_set.rs

1//! `cellstate working-set` — Manage agent working-set entries.
2//!
3//! Subcommands: list, upsert, get, update, delete.
4
5use anyhow::Result;
6use clap::{Arg, ArgMatches, Command};
7
8use cellstate_core::api_types::{
9    ListWorkingSetResponse, PatchWorkingSetEntryRequest, UpsertWorkingSetEntryRequest,
10    WorkingSetEntryResponse,
11};
12
13use super::client::ApiClient;
14use super::output::OutputConfig;
15
16const BASE: &str = "/api/v1/working-set";
17
18pub fn build_command() -> Command {
19    Command::new("working-set")
20        .about("Manage agent working-set entries")
21        .subcommand_required(true)
22        .subcommand(
23            Command::new("list")
24                .about("List working-set entries")
25                .arg(
26                    Arg::new("agent-id")
27                        .long("agent-id")
28                        .help("Filter by agent ID"),
29                )
30                .arg(
31                    Arg::new("scope-id")
32                        .long("scope-id")
33                        .help("Filter by scope ID"),
34                )
35                .arg(
36                    Arg::new("key-prefix")
37                        .long("key-prefix")
38                        .help("Filter by key prefix"),
39                )
40                .arg(
41                    Arg::new("active-only")
42                        .long("active-only")
43                        .action(clap::ArgAction::SetTrue)
44                        .help("Exclude expired entries"),
45                )
46                .arg(
47                    Arg::new("limit")
48                        .long("limit")
49                        .default_value("20")
50                        .help("Max results"),
51                )
52                .arg(
53                    Arg::new("offset")
54                        .long("offset")
55                        .default_value("0")
56                        .help("Offset"),
57                ),
58        )
59        .subcommand(
60            Command::new("upsert")
61                .about("Upsert a working-set entry")
62                .arg(
63                    Arg::new("agent-id")
64                        .long("agent-id")
65                        .required(true)
66                        .help("Agent ID"),
67                )
68                .arg(Arg::new("key").required(true).help("Entry key"))
69                .arg(
70                    Arg::new("value")
71                        .required(true)
72                        .help("Entry value (JSON string)"),
73                )
74                .arg(
75                    Arg::new("scope-id")
76                        .long("scope-id")
77                        .help("Optional scope ID"),
78                ),
79        )
80        .subcommand(
81            Command::new("get")
82                .about("Get a working-set entry")
83                .arg(Arg::new("agent-id").required(true).help("Agent ID"))
84                .arg(Arg::new("key").required(true).help("Entry key")),
85        )
86        .subcommand(
87            Command::new("update")
88                .about("Update a working-set entry")
89                .arg(Arg::new("agent-id").required(true).help("Agent ID"))
90                .arg(Arg::new("key").required(true).help("Entry key"))
91                .arg(
92                    Arg::new("value")
93                        .long("value")
94                        .help("New value (JSON string)"),
95                )
96                .arg(Arg::new("scope-id").long("scope-id").help("New scope ID")),
97        )
98        .subcommand(
99            Command::new("delete")
100                .about("Delete a working-set entry")
101                .arg(Arg::new("agent-id").required(true).help("Agent ID"))
102                .arg(Arg::new("key").required(true).help("Entry key")),
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(("list", sub)) => {
114            let limit: i64 = sub.get_one::<String>("limit").unwrap().parse()?;
115            let offset: i64 = sub.get_one::<String>("offset").unwrap().parse()?;
116            let mut path = format!("{BASE}?limit={limit}&offset={offset}");
117            if let Some(agent_id) = sub.get_one::<String>("agent-id") {
118                path.push_str(&format!("&agent_id={agent_id}"));
119            }
120            if let Some(scope_id) = sub.get_one::<String>("scope-id") {
121                path.push_str(&format!("&scope_id={scope_id}"));
122            }
123            if let Some(key_prefix) = sub.get_one::<String>("key-prefix") {
124                path.push_str(&format!("&key_prefix={key_prefix}"));
125            }
126            if sub.get_flag("active-only") {
127                path.push_str("&active_only=true");
128            }
129            let resp: ListWorkingSetResponse = client.get(&path).await?;
130            output.print_list(&resp.entries, resp.total);
131        }
132        Some(("upsert", sub)) => {
133            let agent_id = sub.get_one::<String>("agent-id").unwrap().parse()?;
134            let key = sub.get_one::<String>("key").unwrap().clone();
135            let value: serde_json::Value =
136                serde_json::from_str(sub.get_one::<String>("value").unwrap())?;
137            let req = UpsertWorkingSetEntryRequest {
138                agent_id,
139                scope_id: sub
140                    .get_one::<String>("scope-id")
141                    .map(|s| s.parse())
142                    .transpose()?,
143                key,
144                value,
145                expires_at: None,
146                metadata: None,
147            };
148            // NOTE: This should be PUT, but the client doesn't have a put method.
149            // Using post_raw as a workaround.
150            let resp: serde_json::Value = client.post_raw(BASE, &req).await?;
151            output.print_value(&resp);
152        }
153        Some(("get", sub)) => {
154            let agent_id = sub.get_one::<String>("agent-id").unwrap();
155            let key = sub.get_one::<String>("key").unwrap();
156            let resp: WorkingSetEntryResponse =
157                client.get(&format!("{BASE}/{agent_id}/{key}")).await?;
158            output.print(&resp);
159        }
160        Some(("update", sub)) => {
161            let agent_id = sub.get_one::<String>("agent-id").unwrap();
162            let key = sub.get_one::<String>("key").unwrap();
163            let req = PatchWorkingSetEntryRequest {
164                value: sub
165                    .get_one::<String>("value")
166                    .map(|s| serde_json::from_str(s))
167                    .transpose()?,
168                scope_id: sub
169                    .get_one::<String>("scope-id")
170                    .map(|s| s.parse())
171                    .transpose()?,
172                expires_at: None,
173                metadata: None,
174            };
175            let resp: serde_json::Value = client
176                .patch(&format!("{BASE}/{agent_id}/{key}"), &req)
177                .await?;
178            output.print_value(&resp);
179        }
180        Some(("delete", sub)) => {
181            let agent_id = sub.get_one::<String>("agent-id").unwrap();
182            let key = sub.get_one::<String>("key").unwrap();
183            client.delete(&format!("{BASE}/{agent_id}/{key}")).await?;
184            if output.json {
185                println!(r#"{{"deleted": "{agent_id}/{key}"}}"#);
186            } else {
187                println!("Deleted working-set entry {agent_id}/{key}");
188            }
189        }
190        _ => unreachable!("subcommand_required(true) prevents this"),
191    }
192    Ok(())
193}