scripts/paperclip-upload-artifact.sh
scripts/paperclip-upload-artifact.shBrowse 9 files
2,992 tokens
10,377 bytes
Token encoding: o200k_base
Snapshot 5b913e7
← Back to SKILL.md
1#!/usr/bin/env bash2 3set -euo pipefail4 5usage() {6 cat <<'EOF'7Usage:8 paperclip-upload-artifact.sh FILE [options]9 10Uploads a generated file from the current workspace to the current Paperclip11issue, then creates an attachment-backed artifact work product by default.12 13Required environment for live uploads:14 PAPERCLIP_API_URL, PAPERCLIP_API_KEY, PAPERCLIP_COMPANY_ID, PAPERCLIP_TASK_ID, PAPERCLIP_RUN_ID15 16Options:17 --issue-id ID Issue id to attach to (default: PAPERCLIP_TASK_ID)18 --company-id ID Company id (default: PAPERCLIP_COMPANY_ID)19 --title TEXT Work product title (default: file basename)20 --summary TEXT Work product summary21 --content-type TYPE Override detected upload content type22 --status STATUS Work product status (default: ready_for_review)23 --no-work-product Only upload the issue attachment24 --no-primary Do not mark the artifact work product primary for its type25 --output FORMAT markdown or json (default: markdown)26 --dry-run Print resolved upload settings without calling the API27 --help, -h Show this help28 29Examples:30 scripts/paperclip-upload-artifact.sh dist/demo.mp4 \31 --title "Demo video render" \32 --summary "MP4 render for board review"33 34 scripts/paperclip-upload-artifact.sh out/walkthrough.webm \35 --title "Walkthrough video" \36 --content-type video/webm37EOF38}39 40require_command() {41 if ! command -v "$1" >/dev/null 2>&1; then42 printf 'Missing required command: %s\n' "$1" >&243 exit 144 fi45}46 47json_bool() {48 if [[ "${1:-0}" == "1" ]]; then49 printf 'true'50 else51 printf 'false'52 fi53}54 55detect_content_type() {56 local path="$1"57 local lower58 lower="$(printf '%s' "$path" | tr '[:upper:]' '[:lower:]')"59 60 case "$lower" in61 *.mp4|*.m4v) printf 'video/mp4' ;;62 *.webm) printf 'video/webm' ;;63 *.mov|*.qt) printf 'video/quicktime' ;;64 *.png) printf 'image/png' ;;65 *.jpg|*.jpeg) printf 'image/jpeg' ;;66 *.gif) printf 'image/gif' ;;67 *.webp) printf 'image/webp' ;;68 *.svg) printf 'image/svg+xml' ;;69 *.pdf) printf 'application/pdf' ;;70 *.txt|*.log) printf 'text/plain' ;;71 *.md|*.markdown) printf 'text/markdown' ;;72 *.json) printf 'application/json' ;;73 *.csv) printf 'text/csv' ;;74 *.html|*.htm) printf 'text/html' ;;75 *.zip) printf 'application/zip' ;;76 *)77 if command -v file >/dev/null 2>&1; then78 file --brief --mime-type "$path"79 else80 printf 'application/octet-stream'81 fi82 ;;83 esac84}85 86request_json() {87 local method="$1"88 local url="$2"89 local body="${3:-}"90 local response_file91 local status_code92 93 response_file="$(mktemp)"94 if [[ -n "$body" ]]; then95 status_code="$(96 curl -sS -X "$method" -w '%{http_code}' -o "$response_file" \97 "$url" \98 -H "Authorization: Bearer $PAPERCLIP_API_KEY" \99 -H "X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID" \100 -H 'Content-Type: application/json' \101 --data-binary "$body"102 )"103 else104 status_code="$(105 curl -sS -X "$method" -w '%{http_code}' -o "$response_file" \106 "$url" \107 -H "Authorization: Bearer $PAPERCLIP_API_KEY" \108 -H "X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID"109 )"110 fi111 112 if [[ "$status_code" -lt 200 || "$status_code" -ge 300 ]]; then113 printf 'Request failed (%s): %s\n' "$status_code" "$url" >&2114 cat "$response_file" >&2115 printf '\n' >&2116 rm -f "$response_file"117 exit 1118 fi119 120 cat "$response_file"121 rm -f "$response_file"122}123 124upload_file() {125 local url="$1"126 local path="$2"127 local content_type="$3"128 local escaped_path129 local response_file130 local status_code131 132 escaped_path="${path//\\/\\\\}"133 escaped_path="${escaped_path//\"/\\\"}"134 response_file="$(mktemp)"135 status_code="$(136 curl -sS -X POST -w '%{http_code}' -o "$response_file" \137 "$url" \138 -H "Authorization: Bearer $PAPERCLIP_API_KEY" \139 -H "X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID" \140 -F "file=@\"${escaped_path}\";type=${content_type}"141 )"142 143 if [[ "$status_code" -lt 200 || "$status_code" -ge 300 ]]; then144 printf 'Upload failed (%s): %s\n' "$status_code" "$url" >&2145 cat "$response_file" >&2146 printf '\n' >&2147 rm -f "$response_file"148 exit 1149 fi150 151 cat "$response_file"152 rm -f "$response_file"153}154 155file_path=""156issue_id="${PAPERCLIP_TASK_ID:-}"157company_id="${PAPERCLIP_COMPANY_ID:-}"158title=""159summary=""160content_type=""161status="ready_for_review"162create_work_product=1163is_primary=1164output_format="markdown"165dry_run=0166 167while [[ $# -gt 0 ]]; do168 case "$1" in169 --issue-id)170 issue_id="${2:-}"171 shift 2172 ;;173 --company-id)174 company_id="${2:-}"175 shift 2176 ;;177 --title)178 title="${2:-}"179 shift 2180 ;;181 --summary)182 summary="${2:-}"183 shift 2184 ;;185 --content-type)186 content_type="${2:-}"187 shift 2188 ;;189 --status)190 status="${2:-}"191 shift 2192 ;;193 --no-work-product)194 create_work_product=0195 shift196 ;;197 --no-primary)198 is_primary=0199 shift200 ;;201 --output)202 output_format="${2:-}"203 shift 2204 ;;205 --dry-run)206 dry_run=1207 shift208 ;;209 --help|-h)210 usage211 exit 0212 ;;213 --*)214 printf 'Unknown argument: %s\n' "$1" >&2215 usage >&2216 exit 1217 ;;218 *)219 if [[ -n "$file_path" ]]; then220 printf 'Unexpected positional argument: %s\n' "$1" >&2221 usage >&2222 exit 1223 fi224 file_path="$1"225 shift226 ;;227 esac228done229 230if [[ -z "$file_path" ]]; then231 printf 'Missing file path.\n' >&2232 usage >&2233 exit 1234fi235 236if [[ ! -f "$file_path" ]]; then237 printf 'Artifact file does not exist: %s\n' "$file_path" >&2238 exit 1239fi240 241if [[ "$output_format" != "markdown" && "$output_format" != "json" ]]; then242 printf 'Unsupported output format: %s\n' "$output_format" >&2243 exit 1244fi245 246require_command curl247require_command jq248 249if [[ -z "$title" ]]; then250 title="$(basename "$file_path")"251fi252 253if [[ -z "$content_type" ]]; then254 content_type="$(detect_content_type "$file_path")"255fi256 257if [[ "$dry_run" == "1" ]]; then258 create_work_product_json="$(json_bool "$create_work_product")"259 is_primary_json="$(json_bool "$is_primary")"260 jq -n \261 --arg file "$file_path" \262 --arg issueId "$issue_id" \263 --arg companyId "$company_id" \264 --arg title "$title" \265 --arg summary "$summary" \266 --arg contentType "$content_type" \267 --arg status "$status" \268 --argjson createWorkProduct "$create_work_product_json" \269 --argjson isPrimary "$is_primary_json" \270 '{file: $file, issueId: $issueId, companyId: $companyId, title: $title, summary: $summary, contentType: $contentType, status: $status, createWorkProduct: $createWorkProduct, isPrimary: $isPrimary}'271 exit 0272fi273 274if [[ -z "${PAPERCLIP_API_URL:-}" || -z "${PAPERCLIP_API_KEY:-}" || -z "${PAPERCLIP_RUN_ID:-}" ]]; then275 printf 'Missing PAPERCLIP_API_URL, PAPERCLIP_API_KEY, or PAPERCLIP_RUN_ID.\n' >&2276 exit 1277fi278 279if [[ -z "$issue_id" || -z "$company_id" ]]; then280 printf 'Missing issue or company id. Pass --issue-id/--company-id or set PAPERCLIP_TASK_ID/PAPERCLIP_COMPANY_ID.\n' >&2281 exit 1282fi283 284api_base="${PAPERCLIP_API_URL%/}/api"285attachment="$(286 upload_file \287 "$api_base/companies/$company_id/issues/$issue_id/attachments" \288 "$file_path" \289 "$content_type"290)"291 292work_product="null"293if [[ "$create_work_product" == "1" ]]; then294 is_primary_json="$(json_bool "$is_primary")"295 attachment_id="$(jq -r '.id // empty' <<<"$attachment")"296 byte_size="$(jq -r '.byteSize // 0' <<<"$attachment")"297 content_path="$(jq -r '.contentPath // empty' <<<"$attachment")"298 open_path="$(jq -r '.openPath // .contentPath // empty' <<<"$attachment")"299 download_path="$(jq -r '.downloadPath // (if .contentPath then (.contentPath + "?download=1") else "" end)' <<<"$attachment")"300 original_filename="$(jq -r '.originalFilename // empty' <<<"$attachment")"301 302 if [[ -z "$attachment_id" || -z "$content_path" || -z "$download_path" ]]; then303 printf 'Upload response did not include attachment path metadata.\n' >&2304 printf '%s\n' "$attachment" >&2305 exit 1306 fi307 308 work_product_payload="$(309 jq -nc \310 --arg title "$title" \311 --arg summary "$summary" \312 --arg status "$status" \313 --arg runId "$PAPERCLIP_RUN_ID" \314 --arg attachmentId "$attachment_id" \315 --arg contentType "$content_type" \316 --argjson byteSize "$byte_size" \317 --arg contentPath "$content_path" \318 --arg openPath "$open_path" \319 --arg downloadPath "$download_path" \320 --arg originalFilename "$original_filename" \321 --argjson isPrimary "$is_primary_json" \322 '{323 type: "artifact",324 provider: "paperclip",325 title: $title,326 status: $status,327 reviewState: "none",328 isPrimary: $isPrimary,329 healthStatus: "unknown",330 summary: (if $summary == "" then null else $summary end),331 createdByRunId: $runId,332 metadata: {333 attachmentId: $attachmentId,334 contentType: $contentType,335 byteSize: $byteSize,336 contentPath: $contentPath,337 openPath: $openPath,338 downloadPath: $downloadPath,339 originalFilename: (if $originalFilename == "" then null else $originalFilename end)340 }341 }'342 )"343 344 work_product="$(345 request_json \346 POST \347 "$api_base/issues/$issue_id/work-products" \348 "$work_product_payload"349 )"350fi351 352if [[ "$output_format" == "json" ]]; then353 jq -n --argjson attachment "$attachment" --argjson workProduct "$work_product" \354 '{attachment: $attachment, workProduct: $workProduct}'355 exit 0356fi357 358content_path="$(jq -r '.contentPath // empty' <<<"$attachment")"359download_path="$(jq -r '.downloadPath // (if .contentPath then (.contentPath + "?download=1") else "" end)' <<<"$attachment")"360attachment_id="$(jq -r '.id // empty' <<<"$attachment")"361work_product_id="$(jq -r '.id // empty' <<<"$work_product")"362 363printf 'Uploaded artifact\n\n'364printf -- '- Attachment: [%s](%s)\n' "$title" "$content_path"365printf -- '- Download: [%s](%s)\n' "$title" "$download_path"366printf -- '- Attachment ID: `%s`\n' "$attachment_id"367if [[ -n "$work_product_id" ]]; then368 printf -- '- Work product ID: `%s`\n' "$work_product_id"369fi370printf '\nFinal comment snippet:\n\n'371printf -- '- Artifact: [%s](%s)\n' "$title" "$content_path"372