mirror of
https://github.com/block/buzz.git
synced 2026-10-31 06:21:34 +00:00
Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Mari <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Quinn <96f056ad5f2305c8ddf637dc65d048aa4c12d7daeb8867690e34fca46b0ef64c@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Sami <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
244 lines
9.2 KiB
Rust
244 lines
9.2 KiB
Rust
use sha2::{Digest, Sha256};
|
|
|
|
use crate::client::{
|
|
extract_d_tag, extract_relay_response_field, normalize_write_response, print_create_response,
|
|
BuzzClient,
|
|
};
|
|
use crate::error::CliError;
|
|
use crate::validate::{parse_uuid, read_or_stdin, sdk_err, validate_uuid};
|
|
|
|
// TODO(phase-4): Replace raw nostr::EventBuilder usage with buzz-sdk builder functions
|
|
|
|
/// List workflows in a channel — query kind:30620 workflow definition events.
|
|
pub async fn cmd_list_workflows(client: &BuzzClient, channel_id: &str) -> Result<(), CliError> {
|
|
validate_uuid(channel_id)?;
|
|
let filter = serde_json::json!({
|
|
"kinds": [30620],
|
|
"#h": [channel_id]
|
|
});
|
|
let resp = client.query(&filter).await?;
|
|
let events: Vec<serde_json::Value> = serde_json::from_str(&resp).unwrap_or_default();
|
|
let workflows: Vec<serde_json::Value> = events
|
|
.iter()
|
|
.map(|e| {
|
|
serde_json::json!({
|
|
"workflow_id": extract_d_tag(e),
|
|
"content": e.get("content").and_then(|v| v.as_str()).unwrap_or(""),
|
|
"created_at": e.get("created_at").and_then(|v| v.as_u64()).unwrap_or(0),
|
|
"pubkey": e.get("pubkey").and_then(|v| v.as_str()).unwrap_or(""),
|
|
})
|
|
})
|
|
.collect();
|
|
let output = serde_json::to_string(&workflows).unwrap_or_default();
|
|
println!("{output}");
|
|
Ok(())
|
|
}
|
|
|
|
/// Get a single workflow definition.
|
|
pub async fn cmd_get_workflow(client: &BuzzClient, workflow_id: &str) -> Result<(), CliError> {
|
|
validate_uuid(workflow_id)?;
|
|
let filter = serde_json::json!({
|
|
"kinds": [30620],
|
|
"#d": [workflow_id]
|
|
});
|
|
let resp = client.query(&filter).await?;
|
|
let events: Vec<serde_json::Value> = serde_json::from_str(&resp).unwrap_or_default();
|
|
if let Some(e) = events.first() {
|
|
let normalized = serde_json::json!({
|
|
"workflow_id": extract_d_tag(e),
|
|
"content": e.get("content").and_then(|v| v.as_str()).unwrap_or(""),
|
|
"created_at": e.get("created_at").and_then(|v| v.as_u64()).unwrap_or(0),
|
|
"pubkey": e.get("pubkey").and_then(|v| v.as_str()).unwrap_or(""),
|
|
});
|
|
println!("{normalized}");
|
|
} else {
|
|
println!("null");
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Get workflow run history — query kinds [46001, 46002, 46003].
|
|
///
|
|
/// NOTE: The relay does not currently emit workflow execution events (46001-46003).
|
|
/// Run history is stored in the workflow_runs DB table, not as Nostr events.
|
|
/// This command will return an empty array until the relay adds event emission
|
|
/// or a dedicated REST endpoint for run history.
|
|
pub async fn cmd_get_workflow_runs(
|
|
client: &BuzzClient,
|
|
workflow_id: &str,
|
|
limit: Option<u32>,
|
|
) -> Result<(), CliError> {
|
|
validate_uuid(workflow_id)?;
|
|
let limit = limit.unwrap_or(20).min(100);
|
|
let filter = serde_json::json!({
|
|
"kinds": [46001, 46002, 46003],
|
|
"#d": [workflow_id],
|
|
"limit": limit
|
|
});
|
|
let resp = client.query(&filter).await?;
|
|
let events: Vec<serde_json::Value> = serde_json::from_str(&resp).unwrap_or_default();
|
|
let normalized: Vec<serde_json::Value> = events
|
|
.iter()
|
|
.map(|e| {
|
|
serde_json::json!({
|
|
"event_id": e.get("id").and_then(|v| v.as_str()).unwrap_or(""),
|
|
"kind": e.get("kind").and_then(|v| v.as_u64()).unwrap_or(0),
|
|
"content": e.get("content").and_then(|v| v.as_str()).unwrap_or(""),
|
|
"created_at": e.get("created_at").and_then(|v| v.as_u64()).unwrap_or(0),
|
|
"tags": e.get("tags").cloned().unwrap_or(serde_json::json!([])),
|
|
})
|
|
})
|
|
.collect();
|
|
let output = serde_json::to_string(&normalized).unwrap_or_default();
|
|
println!("{output}");
|
|
Ok(())
|
|
}
|
|
|
|
/// Create a workflow — sign and submit a kind:30620 event.
|
|
pub async fn cmd_create_workflow(
|
|
client: &BuzzClient,
|
|
channel_id: &str,
|
|
yaml: &str,
|
|
) -> Result<(), CliError> {
|
|
let channel_uuid = parse_uuid(channel_id)?;
|
|
let yaml_definition = read_or_stdin(yaml)?;
|
|
|
|
let workflow_id = uuid::Uuid::new_v4();
|
|
let builder = buzz_sdk::build_workflow_def(channel_uuid, workflow_id, &yaml_definition)
|
|
.map_err(sdk_err)?;
|
|
let event = client.sign_event(builder)?;
|
|
|
|
let resp = client.submit_event(event).await?;
|
|
let final_workflow_id = extract_relay_response_field(&resp, "workflow_id")
|
|
.unwrap_or_else(|| workflow_id.to_string());
|
|
print_create_response(&resp, "workflow_id", &final_workflow_id);
|
|
Ok(())
|
|
}
|
|
|
|
/// Update a workflow — sign and submit an updated kind:30620 event with same d-tag.
|
|
pub async fn cmd_update_workflow(
|
|
client: &BuzzClient,
|
|
channel_id: &str,
|
|
workflow_id: &str,
|
|
yaml: &str,
|
|
) -> Result<(), CliError> {
|
|
let channel_uuid = parse_uuid(channel_id)?;
|
|
let wf_uuid = parse_uuid(workflow_id)?;
|
|
let yaml_definition = read_or_stdin(yaml)?;
|
|
|
|
let builder = buzz_sdk::build_workflow_update(channel_uuid, wf_uuid, &yaml_definition)
|
|
.map_err(sdk_err)?;
|
|
let event = client.sign_event(builder)?;
|
|
|
|
let resp = client.submit_event(event).await?;
|
|
println!("{}", normalize_write_response(&resp));
|
|
Ok(())
|
|
}
|
|
|
|
/// Delete a workflow — sign and submit a kind:5 deletion event.
|
|
pub async fn cmd_delete_workflow(client: &BuzzClient, workflow_id: &str) -> Result<(), CliError> {
|
|
let wf_uuid = parse_uuid(workflow_id)?;
|
|
let keys = client.keys();
|
|
|
|
let builder =
|
|
buzz_sdk::build_workflow_delete(&keys.public_key().to_hex(), wf_uuid).map_err(sdk_err)?;
|
|
let event = client.sign_event(builder)?;
|
|
|
|
let resp = client.submit_event(event).await?;
|
|
println!("{}", normalize_write_response(&resp));
|
|
Ok(())
|
|
}
|
|
|
|
/// Trigger a workflow — sign and submit a kind:46020 event.
|
|
///
|
|
/// When `inputs` is provided, it is parsed as a JSON object and used as the
|
|
/// event content (MCP parity). When omitted, the event content is `{}`.
|
|
pub async fn cmd_trigger_workflow(
|
|
client: &BuzzClient,
|
|
workflow_id: &str,
|
|
inputs: Option<&str>,
|
|
) -> Result<(), CliError> {
|
|
let wf_uuid = parse_uuid(workflow_id)?;
|
|
|
|
if let Some(raw) = inputs {
|
|
// Parse and validate it is a JSON object, then build the event manually
|
|
// so we can embed the inputs as the event content.
|
|
let parsed: serde_json::Value = serde_json::from_str(raw)
|
|
.map_err(|e| CliError::Usage(format!("--inputs is not valid JSON: {e}")))?;
|
|
if !parsed.is_object() {
|
|
return Err(CliError::Usage("--inputs must be a JSON object".into()));
|
|
}
|
|
let content = serde_json::to_string(&parsed).unwrap_or_default();
|
|
use nostr::{EventBuilder, Kind, Tag};
|
|
let tags = vec![Tag::parse(["d", &wf_uuid.to_string()])
|
|
.map_err(|e| CliError::Other(format!("tag error: {e}")))?];
|
|
let builder = EventBuilder::new(
|
|
Kind::Custom(buzz_sdk::kind::KIND_WORKFLOW_TRIGGER as u16),
|
|
&content,
|
|
)
|
|
.tags(tags);
|
|
let event = client.sign_event(builder)?;
|
|
let resp = client.submit_event(event).await?;
|
|
println!("{}", normalize_write_response(&resp));
|
|
} else {
|
|
let builder = buzz_sdk::build_workflow_trigger(wf_uuid).map_err(sdk_err)?;
|
|
let event = client.sign_event(builder)?;
|
|
let resp = client.submit_event(event).await?;
|
|
println!("{}", normalize_write_response(&resp));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Approve or deny a workflow step — sign and submit a kind:46030 (grant) or 46031 (deny) event.
|
|
pub async fn cmd_approve_step(
|
|
client: &BuzzClient,
|
|
approval_token: &str,
|
|
approved: bool,
|
|
note: Option<&str>,
|
|
) -> Result<(), CliError> {
|
|
validate_uuid(approval_token)?;
|
|
|
|
let content = note.unwrap_or("");
|
|
|
|
// The relay expects d-tag = hex(SHA256(token)), not the raw token UUID.
|
|
let token_hash = hex::encode(Sha256::digest(approval_token.as_bytes()));
|
|
let builder =
|
|
buzz_sdk::build_workflow_approval(&token_hash, approved, content).map_err(sdk_err)?;
|
|
let event = client.sign_event(builder)?;
|
|
|
|
let resp = client.submit_event(event).await?;
|
|
println!("{}", normalize_write_response(&resp));
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn dispatch(cmd: crate::WorkflowsCmd, client: &BuzzClient) -> Result<(), CliError> {
|
|
use crate::WorkflowsCmd;
|
|
match cmd {
|
|
WorkflowsCmd::List { channel } => cmd_list_workflows(client, &channel).await,
|
|
WorkflowsCmd::Get { workflow } => cmd_get_workflow(client, &workflow).await,
|
|
WorkflowsCmd::Create { channel, yaml } => {
|
|
cmd_create_workflow(client, &channel, &yaml).await
|
|
}
|
|
WorkflowsCmd::Update {
|
|
channel,
|
|
workflow,
|
|
yaml,
|
|
} => cmd_update_workflow(client, &channel, &workflow, &yaml).await,
|
|
WorkflowsCmd::Delete { workflow } => cmd_delete_workflow(client, &workflow).await,
|
|
WorkflowsCmd::Trigger { workflow, inputs } => {
|
|
cmd_trigger_workflow(client, &workflow, inputs.as_deref()).await
|
|
}
|
|
WorkflowsCmd::Runs { workflow, limit } => {
|
|
cmd_get_workflow_runs(client, &workflow, limit).await
|
|
}
|
|
WorkflowsCmd::Approve {
|
|
token,
|
|
approved,
|
|
note,
|
|
} => {
|
|
// approved is already a bool — no parse_bool_flag needed
|
|
cmd_approve_step(client, &token, approved, note.as_deref()).await
|
|
}
|
|
}
|
|
}
|