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