systemprompt_cli/commands/core/artifacts/
show.rs1use 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}