Skip to main content

systemprompt_cli/commands/core/artifacts/
show.rs

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