Skip to main content

systemprompt_cli/commands/core/artifacts/
show.rs

1//! `core artifacts show` command rendering artifact parts.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use anyhow::{Context, Result};
7use clap::Args;
8use serde_json::Value as JsonValue;
9use systemprompt_agent::models::a2a::Artifact;
10use systemprompt_agent::repository::content::artifact::ArtifactRepository;
11use systemprompt_database::DbPool;
12use systemprompt_identifiers::ArtifactId;
13use systemprompt_logging::CliService;
14use systemprompt_models::a2a::Part;
15
16use super::types::{ArtifactPartOutput, ArtifactSummary};
17use crate::cli_settings::CliConfig;
18use crate::context::CommandContext;
19use crate::session::get_or_create_session;
20use crate::shared::CommandOutput;
21
22#[derive(Debug, Args)]
23pub struct ShowArgs {
24    #[arg(
25        value_name = "ARTIFACT_ID",
26        help = "Artifact ID (full or partial prefix)"
27    )]
28    pub artifact: String,
29
30    #[arg(long, help = "Show full content without truncation")]
31    pub full: bool,
32}
33
34pub(super) async fn execute(args: ShowArgs, ctx: &CommandContext) -> Result<CommandOutput> {
35    let _session_ctx = get_or_create_session(ctx).await?;
36    execute_with_pool(args, &ctx.db_pool().await?, &ctx.cli).await
37}
38
39pub async fn execute_with_pool(
40    args: ShowArgs,
41    pool: &DbPool,
42    config: &CliConfig,
43) -> Result<CommandOutput> {
44    let repo = ArtifactRepository::new(pool)?;
45
46    let artifact_id = resolve_artifact_id(&args.artifact, &repo).await?;
47
48    let artifact = repo
49        .get_artifact_by_id(&artifact_id)
50        .await
51        .context("Failed to fetch artifact")?
52        .ok_or_else(|| anyhow::anyhow!("Artifact not found: {}", args.artifact))?;
53
54    let parts: Vec<ArtifactPartOutput> = artifact
55        .parts
56        .iter()
57        .map(|p| match p {
58            Part::Text(text_part) => ArtifactPartOutput {
59                kind: "text".to_owned(),
60                text: Some(text_part.text.clone()),
61                data: None,
62            },
63            Part::Data(data_part) => ArtifactPartOutput {
64                kind: "data".to_owned(),
65                text: None,
66                data: Some(JsonValue::Object(data_part.data.clone())),
67            },
68            Part::File(file_part) => ArtifactPartOutput {
69                kind: "file".to_owned(),
70                text: file_part.file.name.clone(),
71                data: Some(serde_json::json!({
72                    "mimeType": file_part.file.mime_type,
73                    "bytes": format!("[{} bytes]", file_part.file.bytes.as_deref().unwrap_or("").len()),
74                })),
75            },
76        })
77        .collect();
78
79    let output = ArtifactSummary {
80        artifact_id: artifact.id.clone(),
81        name: artifact.title.clone(),
82        artifact_type: artifact.metadata.artifact_type.clone(),
83        tool_name: artifact.metadata.tool_name.clone(),
84        task_id: artifact.metadata.task_id.clone(),
85        created_at: chrono::DateTime::parse_from_rfc3339(&artifact.metadata.created_at)
86            .map_or_else(|_| chrono::Utc::now(), |dt| dt.with_timezone(&chrono::Utc)),
87    };
88
89    if !config.is_json_output() {
90        render_artifact(&artifact, &parts, args.full);
91    }
92
93    Ok(CommandOutput::card_value("Artifact Details", &output))
94}
95
96fn render_artifact(artifact: &Artifact, parts: &[ArtifactPartOutput], full: bool) {
97    CliService::section("Artifact Details");
98    CliService::key_value("ID", artifact.id.as_str());
99
100    if let Some(ref name) = artifact.title {
101        CliService::key_value("Name", name);
102    }
103
104    if let Some(ref desc) = artifact.description {
105        CliService::key_value("Description", desc);
106    }
107
108    CliService::key_value("Type", &artifact.metadata.artifact_type);
109
110    if let Some(ref tool) = artifact.metadata.tool_name {
111        CliService::key_value("Tool", tool);
112    }
113
114    if let Some(ref source) = artifact.metadata.source {
115        CliService::key_value("Source", source);
116    }
117
118    CliService::key_value("Task ID", artifact.metadata.task_id.as_str());
119    CliService::key_value("Context ID", artifact.metadata.context_id.as_str());
120
121    if let Some(ref skill_name) = artifact.metadata.skill_name {
122        CliService::key_value("Skill", skill_name);
123    }
124
125    if let Some(ref mcp_id) = artifact.metadata.mcp_execution_id {
126        CliService::key_value("MCP Execution", mcp_id);
127    }
128
129    if let Some(ref fingerprint) = artifact.metadata.fingerprint {
130        CliService::key_value("Fingerprint", fingerprint);
131    }
132
133    CliService::key_value("Created", &artifact.metadata.created_at);
134
135    CliService::info("");
136    CliService::section("Parts");
137
138    for (i, part) in parts.iter().enumerate() {
139        render_part(i, part, full);
140    }
141}
142
143fn render_part(index: usize, part: &ArtifactPartOutput, full: bool) {
144    CliService::info(&format!("Part {} [{}]:", index + 1, part.kind));
145
146    if let Some(ref text) = part.text {
147        let display_text = if full || text.len() <= 500 {
148            text.clone()
149        } else {
150            format!(
151                "{}...\n[Truncated - use --full for complete content]",
152                &text[..500]
153            )
154        };
155
156        for line in display_text.lines() {
157            CliService::info(&format!("  {}", line));
158        }
159    }
160
161    if let Some(ref data) = part.data {
162        let formatted = serde_json::to_string_pretty(data).unwrap_or_else(|_| data.to_string());
163
164        let display_data = if full || formatted.len() <= 1000 {
165            formatted
166        } else {
167            format!(
168                "{}...\n[Truncated - use --full for complete content]",
169                &formatted[..1000]
170            )
171        };
172
173        for line in display_data.lines() {
174            CliService::info(&format!("  {}", line));
175        }
176    }
177}
178
179async fn resolve_artifact_id(input: &str, repo: &ArtifactRepository) -> Result<ArtifactId> {
180    let artifact_id = ArtifactId::new(input);
181    if repo.get_artifact_by_id(&artifact_id).await?.is_some() {
182        return Ok(artifact_id);
183    }
184
185    let all_artifacts = repo.get_all_artifacts(Some(100)).await?;
186    let matches: Vec<_> = all_artifacts
187        .iter()
188        .filter(|a| a.id.as_str().starts_with(input))
189        .collect();
190
191    match matches.len() {
192        0 => Err(anyhow::anyhow!("No artifact found matching: {}", input)),
193        1 => Ok(matches[0].id.clone()),
194        _ => {
195            let ids: Vec<&str> = matches.iter().map(|a| a.id.as_str()).collect();
196            Err(anyhow::anyhow!(
197                "Multiple artifacts match prefix '{}': {:?}. Please be more specific.",
198                input,
199                ids
200            ))
201        },
202    }
203}