
Text To Video
- 46 installs
- 762 repo stars
- Updated July 21, 2026
- pexoai/pexo-skills
Helps with ai & agent building tasks.
About
text-to-video is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- text-to-video
- AI & Agent Building
- AI-coding skill
Text To Video by the numbers
- 46 all-time installs (skills.sh)
- +4 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #7,629 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/pexoai/pexo-skills --skill text-to-videoAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 46 |
|---|---|
| repo stars | ★ 762 |
| Last updated | July 21, 2026 |
| Repository | pexoai/pexo-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Text to Video — Pexo
Pexo: https://pexo.ai — get an API key, watch your project render, and buy credits there.
Turn a written prompt or script into a finished, publish-ready video. You do not generate anything yourself: you hand the request to the hosted Pexo agent (same backend as pexo-agent, scoped to text-to-video) and deliver the result. Pexo writes the script, picks the best model per shot, generates every shot, and adds music, subtitles, and transitions.
Your role: relay, don't create
Create a project, send the user's request verbatim, poll, deliver. Pexo's backend handles all creative work — scriptwriting, model choice, prompts, music. Adding your own direction (duration, style, models the user didn't ask for) overrides its judgment and produces worse videos.
Config
~/.pexo/config:
PEXO_BASE_URL="https://pexo.ai"
PEXO_API_KEY="sk-<your-api-key>"No account / first run → read references/SETUP-CHECKLIST.md and walk the user through it — it carries the signup flow with the invite code that grants new users bonus credits, plus how to create the config above. Config error → run scripts/pexo-doctor.sh and follow its output.
Workflow
Scripts live in this skill's scripts/. Reply to the user in their language.
1. Create a project: pexo-project-create.sh "<short brief>" → save the project_id. 2. Upload any files the user gave: pexo-upload.sh <project_id> <path> → save asset_id, reference it inline as <original-image>asset_id</original-image> (or <original-video> / <original-audio>). Tags are required — a bare asset_id is ignored. Pexo can't crawl URLs — download, then upload. 3. Send the request: pexo-chat.sh <project_id> "<user's exact words> <asset tags>". Copy the user's words exactly; only add asset tags. 4. Tell the user (their language): submitted ✓ · ~15–20 min · https://pexo.ai/project/<project_id>. 5. Poll: every ≥60s run pexo-project-get.sh <project_id> and act on nextAction:
- WAIT → keep polling; every ~5 polls send a one-line update with the project link.
- RESPOND → handle each event in
recentMessages: relay Pexo's text (wait for the
user's answer if it asked, then pexo-chat.sh their reply); for preview_video, run pexo-asset-get.sh <project_id> <assetId> per option, show the URLs (A/B/C), let the user pick, then pexo-chat.sh <project_id> "<choice>" --choice <assetId>; for a document event, mention it to the user.
- DELIVER →
pexo-asset-get.sh <project_id> <final assetId>, then send the user the
full asset URL as plain text — all ?… query params, never truncated or wrapped in markdown — plus the project link.
- FAILED → explain
nextActionHintin plain terms and offer to retry. - RECONNECT →
pexo-chat.sh <project_id> "continue", tell the user the connection
dropped and you're resuming, then keep polling.
- Never call
pexo-chat.shduring WAIT — it triggers duplicate production. - Taking too long → if it's been >30 min and still WAIT, tell the user (with the project
link + https://pexo.ai/connect/openclaw) it's running long; ask whether to keep waiting or stop. Don't poll forever.
Revisions
After delivery, the user's tweaks ("make it shorter", "new music", "different shot") reuse the same project: pexo-chat.sh <project_id> "<their feedback>", then poll again (step 5). Never create a new project for a revision — it throws away Pexo's server-side context.
Credits
If a script fails with "Credits balance" / "Insufficient credits": if the error carries a purchase link, pass it to the user; otherwise tell them to add credits at https://pexo.ai/home → Credits → Buy Credits. Retry after they confirm.
Example
User: "Make a 30-second video about our new coffee subscription."
pid=$(pexo-project-create.sh "coffee subscription promo")
pexo-chat.sh "$pid" "Make a 30-second video about our new coffee subscription."
# Tell the user: submitted, ~15–20 min, https://pexo.ai/project/$pid
# Poll pexo-project-get.sh "$pid" until nextAction is DELIVER, then deliver the asset URL.Scripts
| Script | Usage | Returns |
|---|---|---|
pexo-project-create.sh | "<brief>" | project_id |
pexo-upload.sh | <project_id> <file> | asset_id |
pexo-chat.sh | <project_id> "<message>" [--choice <id>] | ack (async) |
pexo-project-get.sh | <project_id> | JSON: nextAction, recentMessages |
pexo-asset-get.sh | <project_id> <asset_id> | JSON with url |
pexo-doctor.sh | — | setup diagnostic |
Error codes and edge cases → references/TROUBLESHOOTING.md.
Text to Video — Pexo
Turn a text prompt or script into a finished, multi-shot video. Pexo auto-selects the best model across 10+ engines (Seedance, Kling, Veo, Sora, and more), writes the prompts, generates every shot, and assembles a publish-ready video with music, subtitles, and transitions — no prompt engineering, no model picking, no editing.
Install
npx skills add https://github.com/pexoai/pexo-skills --skill text-to-videoConfigure
Create ~/.pexo/config:
PEXO_BASE_URL="https://pexo.ai"
PEXO_API_KEY="sk-<your-key>"Get an API key at <https://pexo.ai>.
Use
Ask your agent, for example:
Make a 30-second video about our new coffee subscription.
Pexo writes the script, picks the right model per shot, generates everything, and returns a finished video with music and subtitles. See SKILL.md for the full workflow; run scripts/pexo-doctor.sh if setup fails.
What this is
A thin wrapper that delegates to the hosted Pexo video agent — the same backend as the pexo-agent skill, scoped to the text-to-video scenario. All creative work (scriptwriting, model selection, prompts, music, subtitles) happens server-side.
Setup Checklist
This guide covers first-time setup and environment diagnostics for the pexo-video skill.
Quick Start
1. Create config file
mkdir -p ~/.pexo
cat > ~/.pexo/config << 'EOF'
PEXO_BASE_URL="https://pexo.ai"
PEXO_API_KEY="sk-<your-api-key>"
EOFGet your API key at: https://pexo.ai
- If you do not have an account:
Go to https://pexo.ai and sign up. During registration, you will be asked for an invite code. Use invite code: 37A5EQ New users receive bonus credits upon registration — enough to try out video generation right away.
- If you are already logged in:
click the top-right avatar → API Keys → Create Key, then copy the new key.
2. Run diagnostics
pexo-doctor.shThis checks:
- Config file exists and is readable
PEXO_BASE_URLandPEXO_API_KEYare setcurl,jq, andfileare installed- Network connectivity to Pexo servers
- API key is valid (attempts to list projects)
Fix any issues reported before using other scripts.
3. Verify
pexo-project-list.shIf this returns a JSON list (even if empty), setup is complete.
Troubleshooting Setup Issues
"Set PEXO_BASE_URL in ~/.pexo/config or env"
Config file is missing or doesn't contain the required variables. Create it per step 1 above.
"Set PEXO_API_KEY in ~/.pexo/config or env"
Same as above — the API key line is missing from the config file.
API key invalid (401 Unauthenticated)
Your API key may be expired or incorrect. Log in at https://pexo.ai to generate a new one. Replace the value in ~/.pexo/config.
curl, jq, or file not found
Install the missing dependency:
# macOS (file is usually preinstalled)
brew install curl jq
# Ubuntu/Debian
apt-get install -y curl jq file
# CentOS/RHEL
yum install -y curl jq fileNetwork connectivity failure
If pexo-doctor.sh reports a connectivity issue:
- Check if your server can reach
pexo.ai(e.g.curl -I https://pexo.ai) - Check firewall rules for outbound HTTPS (port 443)
- If behind a proxy, configure
http_proxy/https_proxyenvironment variables
Environment Variables
All scripts read ~/.pexo/config automatically. You can also override via environment variables:
| Variable | Description | Required |
|---|---|---|
PEXO_BASE_URL | Pexo API base URL | Yes |
PEXO_API_KEY | Your Pexo API key (starts with sk-) | Yes |
PEXO_CONFIG | Custom path to config file (default: ~/.pexo/config) | No |
Troubleshooting
Script Exit Behavior
- Exit
0: success - Exit
1: request/transport/backend failure - Exit
2: local usage error (missing args, invalid flags, invalid local input)
On request failure, scripts print compact JSON to stderr, for example:
{"ok":false,"httpCode":429,"message":"Daily creation limit reached. Contact support email for more access."}Fields you may see:
httpCode: the real HTTP status code returned to the scripterror: auth/proxy error code such asINVALID_API_KEYorINTERNAL_ERRORmessage: the most useful user-facing message extracted from the responsedetails: extra backend detail when available
When the error is credit-related (httpCode 429 or 412 from pexo-project-create.sh or pexo-chat.sh), the script automatically fetches the user's credit balance and appends two extra lines to stderr:
Credits balance: 0 — your account has no available credits.
To purchase credits: visit https://pexo.ai/home → click Credits (top-right) → Buy Credits → Extra CreditsAuth And Proxy Errors
These can happen on every script that makes API calls:
| HTTP | error | Meaning | What to do |
|---|---|---|---|
| 401 | INVALID_API_KEY | API key is invalid or revoked | Update PEXO_API_KEY in ~/.pexo/config. Get a new key at pexo.ai. |
| 401 | MISSING_TOKEN | The request was sent without an API key | Run pexo-doctor.sh to verify config. Make sure ~/.pexo/config is sourced correctly. |
| 401 | INTERNAL_ERROR | The service failed to process the request before authentication completed | This is a temporary service issue, not a problem with the API key. Wait a moment and retry; if it persists, contact support. |
| 409 | SESSION_REPLACED | This API key's session was invalidated by a new login elsewhere | Unusual for API-key usage. Retry the command. If it keeps happening, regenerate the API key at pexo.ai. |
If the message says Invalid API key, it is an auth problem. If the body says error=INTERNAL_ERROR, do not tell the user to rotate the key first; the service may simply be temporarily down.
Script-Specific Errors
pexo-project-create.sh
Real statuses:
400: project name is too long. Ask the user to use a shorter name and retry.401: auth failure — see Auth and Proxy Errors above.429: creation limit reached — could be any of:- User already has an active project running (must wait for it to finish)
- Insufficient credits to start a new project
The script automatically fetches and prints the credit balance + top-up URL after any 429.
500: an unexpected server error occurred. Retry in a moment; if the problem persists, contact support at pexo.ai.
Notes:
- If no project name is provided, the script defaults to
"Untitled".
pexo-project-list.sh
Real statuses:
401: auth failure — see Auth and Proxy Errors above.500: an unexpected server error occurred. Retry in a moment; if the problem persists, contact support at pexo.ai.
Notes:
- Invalid
page/page_sizevalues are handled locally by the script before request time. - Backend page size is effectively capped at
100.
pexo-project-get.sh
Real statuses from the first project fetch:
401: auth failure — see Auth and Proxy Errors above.404: the project does not exist or has been deleted. Verify the project_id; if correct, start a new project.500: an unexpected server error occurred. Retry in a moment; if the problem persists, contact support at pexo.ai.
Subsequent status fetches can also fail with:
401: auth failure — see Auth and Proxy Errors above.404: project not found. Same action as above.500: an unexpected server error occurred. Retry in a moment; if the problem persists, contact support at pexo.ai.
pexo-upload.sh
This script has three phases, and the failure source matters.
Phase 1: upload credential
Real statuses:
400: the file name or file size is invalid. Check that the file exists and is not empty; rename it if it contains special characters.401: auth failure — see Auth and Proxy Errors above.500: an unexpected server error occurred. Retry in a moment; if the problem persists, contact support at pexo.ai.
Notes:
- The script rejects unsupported extensions locally. Supported formats:
- Images:
jpg,jpeg,png,webp,bmp,tiff,heic,heif - Videos:
mp4,mov,avi - Audio:
mp3,wav,aac,m4a,ogg,flac
Phase 2: file transfer
Possible failures:
4xx/5xx: the file storage service rejected the upload. Check network connectivity and retry. If the problem persists, contact support at pexo.ai.
The script surfaces this directly as:
Error: upload failed with HTTP <code>Phase 3: finalize
Real statuses:
400: the file was rejected — possible reasons: file exceeds the size limit, file format is not supported, or the file content does not match its extension. Convert or compress the file and re-upload from scratch usingpexo-upload.sh.401: auth failure — see Auth and Proxy Errors above.404: the file record was not found. The upload session may have been cleaned up. Re-upload from scratch usingpexo-upload.sh.412: the upload session has already expired or been completed. Re-upload from scratch usingpexo-upload.sh.500: an unexpected server error occurred. Retry in a moment; if the problem persists, contact support at pexo.ai.
pexo-chat.sh
Real statuses:
400: the message could not be sent due to invalid content. Check the message text; if the issue persists, start a new project.401: auth failure — see Auth and Proxy Errors above.404: the project does not exist or has been deleted. Start a new project.412: two possible causes:- Project no longer supported: this project was created with an older version of Pexo's production system and cannot be continued. Start a new project.
- Account billing issue: the account's credits are frozen or suspended. The script automatically fetches and prints the credit balance + top-up URL. Direct the user to top up or contact support at pexo.ai.
429: limit reached — could be insufficient credits or the project's video output limit. The script automatically fetches and prints the credit balance + top-up URL after a429.500: an unexpected server error occurred. Retry in a moment; if the problem persists, contact support at pexo.ai.
Business errors (credit-related):
error=”credits.insufficient_credits_err”: account has no available credits.pexo-chat.shexits non-zero and prints compact JSON tostderr, for example:
{“ok”:false,”httpCode”:200,”message”:”Insufficient credits”,”error”:”credits.insufficient_credits_err”}Notes:
pexo-chat.shis asynchronous. Success means the request was accepted, not that the video is done.- For non-auth failures, use the HTTP status code as the primary signal. The automatically-appended credit balance lines are the most actionable hint.
- A successful
pexo-chat.shcall should be followed bypexo-project-get.shpolling, typically every60seconds.
pexo-entitlements.sh
Real statuses:
401: auth failure — see Auth and Proxy Errors above.500: an unexpected server error occurred. Retry in a moment; if the problem persists, contact support at pexo.ai.
Notes:
- Returns JSON with
credits.availableCredits,credits.subscriptionCredits,credits.bonusCredits,credits.purchaseCredits, and plan info. - When
availableCreditsis0, the top-up URL is also printed to stderr. - You generally do not need to call this script manually —
pexo-project-create.shandpexo-chat.shcall it automatically on429/412failures and include the balance in their error output.
pexo-asset-get.sh
Real statuses:
401: auth failure — see Auth and Proxy Errors above.404: the file does not exist, or it belongs to a different project. Verify the asset_id and project_id.500: an unexpected server error occurred. Retry in a moment; if the problem persists, contact support at pexo.ai.
Secondary download failures after metadata fetch:
403: the download link has expired. Re-runpexo-asset-get.shto get a fresh link.000: network request failed before receiving a response. Check network connectivity and retry.- local filesystem write failure: the temp directory (
~/.pexo/tmp/) is not writable or the disk is full. Free up space or setPEXO_TMP_DIRto a writable path.
Notes:
- The script downloads the file into
~/.pexo/tmp/(or$PEXO_TMP_DIR) and returns bothurlandlocalPath. - If the asset metadata exists but
downloadUrlis absent, the script returnslocalPath: null.
pexo-doctor.sh
200: config and API key look healthy401+INVALID_API_KEY: API key is invalid or revoked. UpdatePEXO_API_KEYin~/.pexo/config.401+INTERNAL_ERROR: the service failed temporarily — not a key problem. Wait and retry.409: session conflict, unusual for API-key usage. Retry the command.000: no response received — network is unreachable or DNS failed. Check connectivity.
Common Scenarios
Insufficient credits — 429 or 412 with credit balance printed
When pexo-project-create.sh or pexo-chat.sh fails with 429 or 412, the script automatically fetches the credit balance and appends:
Credits balance: 0 — your account has no available credits.
To purchase credits: visit https://pexo.ai/home → click Credits (top-right) → Buy Credits → Extra CreditsIf availableCredits is 0:
- Explain to the user that they have run out of credits.
- Guide them to purchase credits: visit https://pexo.ai/home, click Credits in the top-right corner → Buy Credits, then find Extra Credits.
- Do NOT retry the failed operation — it will fail again until credits are added.
If availableCredits is non-zero but the error still appears:
- The
429is likely the concurrent-project limit: the user already has an active project running. - Re-read the
messagefield from the error JSON to confirm, then tell the user to wait for the current project to finish before creating a new one.
pexo-chat.sh returns success immediately
This is expected.
The script only confirms that the request was accepted by the server, then exits. It does not stream progress or final results to the terminal.
Next step:
1. Wait 60 seconds. 2. Run pexo-project-get.sh <project_id>. 3. Follow nextAction.
pexo-chat.sh prints credits.insufficient_credits_err
Meaning:
- The account has no available credits.
Action:
1. Tell the user the account has no available credits for this chat request. 2. Direct them to top up credits at https://pexo.ai/home. 3. Do not retry pexo-chat.sh until credits are added; it will fail again with the same error.
WAIT lasts a long time
This is normal for video generation.
Practical guideline:
1. Keep polling every 60 seconds. 2. Do not send another pexo-chat.sh message while nextAction=WAIT. 3. If the project later becomes RECONNECT, send a short continuation message and resume polling.
RECONNECT keeps appearing
Meaning:
- The connection to the video generation service was interrupted.
Action:
1. Send a short message with pexo-chat.sh, for example continue. 2. Resume polling with pexo-project-get.sh. 3. If this repeats multiple times, start a new project instead of looping forever.
Download URL expired or returns 403
Signed URLs are temporary.
Action:
1. Re-run pexo-asset-get.sh <project_id> <asset_id>. 2. The script will fetch a fresh downloadUrl and re-download the file into ~/.pexo/tmp/. 3. Deliver the fresh downloadUrl.
Upload fails locally with “unsupported file type”
This is a local pre-check, not a backend outage.
Action:
1. Convert the file into one of the supported formats listed above. 2. Retry pexo-upload.sh.
A script says 401, but the API key may still be fine
Inspect the error payload:
error=INVALID_API_KEY: fix the keyerror=INTERNAL_ERROR: treat it as a temporary service issue, not a key problem
#!/usr/bin/env bash
# Shared configuration for Pexo scripts.
# Sources ~/.pexo/config automatically; env vars override.
# Agent scripts source this file -- no need to handle auth manually.
set -euo pipefail
_PEXO_CONFIG="${PEXO_CONFIG:-$HOME/.pexo/config}"
[[ -f "$_PEXO_CONFIG" ]] && source "$_PEXO_CONFIG"
PEXO_LAST_HTTP_CODE=0
_PEXO_CONNECT_TIMEOUT="${PEXO_CONNECT_TIMEOUT:-10}"
_PEXO_REQUEST_TIMEOUT="${PEXO_REQUEST_TIMEOUT:-60}"
pexo_require_config() {
local missing=()
if [[ -z "${PEXO_BASE_URL:-}" ]]; then
missing+=("PEXO_BASE_URL")
fi
if [[ -z "${PEXO_API_KEY:-}" ]]; then
missing+=("PEXO_API_KEY")
fi
if [[ ${#missing[@]} -gt 0 ]]; then
printf 'Missing required config: %s\n' "${missing[*]}" >&2
printf 'Set them in %s or in the environment.\n' "$_PEXO_CONFIG" >&2
return 1
fi
}
_pexo_auth_header() {
printf 'Authorization: Bearer %s' "$PEXO_API_KEY"
}
pexo_tmp_dir() {
local tmp_dir="${PEXO_TMP_DIR:-$HOME/.pexo/tmp}"
mkdir -p "$tmp_dir"
printf '%s\n' "$tmp_dir"
}
_pexo_is_json() {
local payload="${1:-}"
[[ -n "$payload" ]] && jq -e . >/dev/null 2>&1 <<<"$payload"
}
_pexo_extract_http_code() {
local header_file="$1"
awk '/^HTTP\// { code = $2 } END { print code + 0 }' "$header_file"
}
_pexo_extract_content_type() {
local header_file="$1"
awk '
tolower($1) == "content-type:" {
value = $0
}
END {
sub(/\r$/, "", value)
sub(/^[^:]*:[[:space:]]*/, "", value)
print tolower(value)
}
' "$header_file"
}
_pexo_emit_success() {
local body="${1:-}"
if [[ -z "$body" ]]; then
return 0
fi
if _pexo_is_json "$body"; then
if jq -e 'type == "object" and has("code") and has("data")' >/dev/null 2>&1 <<<"$body"; then
jq '.data' <<<"$body"
return 0
fi
jq '.' <<<"$body"
return 0
fi
printf '%s\n' "$body"
}
_pexo_emit_error() {
local http_code="${1:-0}"
local body="${2:-}"
local transport_error="${3:-}"
export PEXO_LAST_HTTP_CODE="$http_code"
if [[ "$http_code" == "0" && -n "$transport_error" ]]; then
jq -nc \
--argjson httpCode 0 \
--arg message "Network request failed" \
--arg details "$transport_error" \
'{ok:false, httpCode:$httpCode, message:$message, details:$details}' >&2
return 1
fi
if _pexo_is_json "$body"; then
jq -c --argjson httpCode "${http_code:-0}" '
def maybe(field; value):
if value == null or value == "" then {} else { (field): value } end;
{
ok: false,
httpCode: $httpCode,
message: (
if (.data | type) == "object" and (.data.message? // "") != "" then .data.message
elif (.message? // "") != "" then .message
elif (.error? // "") != "" then .error
else "request failed"
end
)
}
+ (
if (.data | type) == "object" and (.data.code? != null) then
{businessCode: .data.code}
else
{}
end
)
+ (
if (.data | type) == "object" and (.data.error? // "") != "" then
{error: .data.error}
elif (.error? // "") != "" then
{error: .error}
else
{}
end
)
+ (
if (.data | type) == "object" and (.data.details? // "") != "" then
{details: .data.details}
elif (.details? // "") != "" then
{details: .details}
else
{}
end
)
' <<<"$body" >&2
return 1
fi
jq -nc \
--argjson httpCode "${http_code:-0}" \
--arg message "request failed" \
--arg details "${transport_error:-$body}" \
'{ok:false, httpCode:$httpCode, message:$message} + (if $details != "" then {details:$details} else {} end)' >&2
return 1
}
_pexo_extract_sse_event_data() {
local response="${1:-}"
local target_event="${2:-error}"
awk -v target_event="$target_event" '
BEGIN {
in_event = 0
data = ""
}
/^event:[[:space:]]*/ {
current = $0
sub(/^event:[[:space:]]*/, "", current)
in_event = (current == target_event)
next
}
in_event && /^data:[[:space:]]*/ {
line = $0
sub(/^data:[[:space:]]*/, "", line)
if (data == "") {
data = line
} else {
data = data "\n" line
}
next
}
in_event && /^$/ {
print data
exit
}
END {
if (in_event && data != "") {
print data
}
}
' <<<"$response"
}
_pexo_emit_sse_error() {
local payload="${1:-}"
if _pexo_is_json "$payload"; then
jq -c '
{
ok: false,
httpCode: 200,
message: (.error_message // .message // "request failed")
}
+ (if (.error_code // "") != "" then {error: .error_code} else {} end)
+ (if (.details // "") != "" then {details: .details} else {} end)
+ (if (.hint // "") != "" then {hint: .hint} else {} end)
' <<<"$payload" >&2
return 1
fi
_pexo_emit_error 200 "" "${payload:-SSE returned an error event}"
}
_pexo_request_json() {
local method="$1"
local path="$2"
local body="${3:-}"
shift 3 || true
pexo_require_config
local body_file header_file err_file
local response http_code curl_status=0
body_file=$(mktemp)
header_file=$(mktemp)
err_file=$(mktemp)
if [[ -n "$body" ]]; then
http_code=$(curl -sS \
--connect-timeout "$_PEXO_CONNECT_TIMEOUT" \
--max-time "$_PEXO_REQUEST_TIMEOUT" \
-X "$method" \
-H "$(_pexo_auth_header)" \
-H "Content-Type: application/json" \
-D "$header_file" \
-o "$body_file" \
-w '%{http_code}' \
-d "$body" \
"$@" \
"${PEXO_BASE_URL}${path}" 2>"$err_file") || curl_status=$?
else
http_code=$(curl -sS \
--connect-timeout "$_PEXO_CONNECT_TIMEOUT" \
--max-time "$_PEXO_REQUEST_TIMEOUT" \
-X "$method" \
-H "$(_pexo_auth_header)" \
-H "Content-Type: application/json" \
-D "$header_file" \
-o "$body_file" \
-w '%{http_code}' \
"$@" \
"${PEXO_BASE_URL}${path}" 2>"$err_file") || curl_status=$?
fi
response=$(cat "$body_file")
export PEXO_LAST_HTTP_CODE="${http_code:-0}"
if [[ $curl_status -ne 0 && "${http_code:-0}" == "000" ]]; then
_pexo_emit_error 0 "" "$(cat "$err_file")"
rm -f "$body_file" "$header_file" "$err_file"
return 1
fi
if [[ "${http_code:-0}" -ge 400 ]] 2>/dev/null; then
_pexo_emit_error "$http_code" "$response" "$(cat "$err_file")"
rm -f "$body_file" "$header_file" "$err_file"
return 1
fi
_pexo_emit_success "$response"
rm -f "$body_file" "$header_file" "$err_file"
}
# _pexo_credit_hint: silently fetch the user's credit balance and emit a
# diagnostic line + top-up URL to stderr. Always returns 0 — never disrupts
# the caller's exit path or overwrites PEXO_LAST_HTTP_CODE.
_pexo_credit_hint() {
local _saved_code="${PEXO_LAST_HTTP_CODE:-0}"
local entitlements available
local topup_url="${PEXO_BASE_URL:-https://pexo.ai}/home"
# Run in a subshell so set -e / PEXO_LAST_HTTP_CODE side-effects stay isolated.
entitlements=$(
set +e
pexo_require_config 2>/dev/null || exit 0
_pexo_request_json GET "/api/biz/auth/entitlements" "" 2>/dev/null
) || true
export PEXO_LAST_HTTP_CODE="$_saved_code"
[[ -n "$entitlements" ]] || return 0
available=$(printf '%s' "$entitlements" \
| jq -r '.credits.availableCredits // empty' 2>/dev/null) || true
[[ -n "$available" ]] || return 0
if [[ "$available" == "0" ]] || \
{ [[ "$available" =~ ^[0-9]+$ ]] && [[ "$available" -le 0 ]]; }; then
printf 'Credits balance: 0 — your account has no available credits.\n' >&2
else
printf 'Credits balance: %s available.\n' "$available" >&2
fi
printf 'To purchase credits: visit %s → click Credits (top-right) → Buy Credits → Extra Credits\n' "$topup_url" >&2
}
# GET -> unwraps response envelope when present
pexo_get() {
local path="$1"
shift || true
_pexo_request_json GET "$path" "" "$@"
}
# POST with optional JSON body -> extracts .data
pexo_post() {
local path="$1"
local body="${2:-}"
shift 2 || true
_pexo_request_json POST "$path" "$body" "$@"
}
pexo_post_sse_ack() {
local path="$1"
local body="${2:-}"
local timeout="${3:-20}"
pexo_require_config
local body_file header_file err_file
local response http_code content_type
body_file=$(mktemp)
header_file=$(mktemp)
err_file=$(mktemp)
set +o pipefail
if [[ -n "$body" ]]; then
curl -sS -N \
--connect-timeout "$_PEXO_CONNECT_TIMEOUT" \
--max-time "$timeout" \
-X POST \
-H "$(_pexo_auth_header)" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-D "$header_file" \
-d "$body" \
"${PEXO_BASE_URL}${path}" >"$body_file" 2>"$err_file"
else
curl -sS -N \
--connect-timeout "$_PEXO_CONNECT_TIMEOUT" \
--max-time "$timeout" \
-X POST \
-H "$(_pexo_auth_header)" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-D "$header_file" \
"${PEXO_BASE_URL}${path}" >"$body_file" 2>"$err_file"
fi
set -o pipefail
response=$(cat "$body_file")
http_code=$(_pexo_extract_http_code "$header_file")
content_type=$(_pexo_extract_content_type "$header_file")
export PEXO_LAST_HTTP_CODE="${http_code:-0}"
if [[ "${http_code:-0}" -ge 400 ]] 2>/dev/null; then
_pexo_emit_error "$http_code" "$response" "$(cat "$err_file")"
rm -f "$body_file" "$header_file" "$err_file"
return 1
fi
local sse_error_payload
sse_error_payload=$(_pexo_extract_sse_event_data "$response" "error")
if [[ -n "$sse_error_payload" ]]; then
_pexo_emit_sse_error "$sse_error_payload"
rm -f "$body_file" "$header_file" "$err_file"
return 1
fi
if [[ "$http_code" == "200" && "$content_type" == text/event-stream* && "$response" == *": stream opened"* ]]; then
rm -f "$body_file" "$header_file" "$err_file"
return 0
fi
if [[ "$http_code" == "0" ]]; then
_pexo_emit_error 0 "" "$(cat "$err_file")"
rm -f "$body_file" "$header_file" "$err_file"
return 1
fi
_pexo_emit_error 0 "" "Timed out waiting for SSE acknowledgement from ${path}"
rm -f "$body_file" "$header_file" "$err_file"
return 1
}
# Detect asset type from file extension
detect_asset_type() {
local ext="${1##*.}"
ext=$(echo "$ext" | tr '[:upper:]' '[:lower:]')
case "$ext" in
jpg|jpeg|png|webp|bmp|tiff|heic|heif) echo "IMAGE" ;;
mp4|mov|avi) echo "VIDEO" ;;
mp3|wav|aac|m4a|ogg|flac) echo "AUDIO" ;;
*) echo "UNKNOWN" ;;
esac
}
# Detect MIME type
detect_mime() {
file --brief --mime-type "$1" 2>/dev/null || echo "application/octet-stream"
}
mime_supported_for_asset_type() {
local mime_type
local asset_type="$2"
mime_type=$(echo "$1" | tr '[:upper:]' '[:lower:]')
case "${asset_type}:${mime_type}" in
IMAGE:image/jpeg|IMAGE:image/jpg|IMAGE:image/png|IMAGE:image/webp|IMAGE:image/tiff|IMAGE:image/bmp|IMAGE:image/heic|IMAGE:image/heif)
return 0
;;
VIDEO:video/mp4|VIDEO:video/x-msvideo|VIDEO:video/avi|VIDEO:video/quicktime)
return 0
;;
AUDIO:audio/mpeg|AUDIO:audio/wav|AUDIO:audio/wave|AUDIO:audio/aac|AUDIO:audio/mp4|AUDIO:audio/x-m4a|AUDIO:audio/ogg|AUDIO:audio/flac)
return 0
;;
*)
return 1
;;
esac
}
#!/usr/bin/env bash
[ -n "${BASH_VERSION:-}" ] || exec bash "$0" "$@"
usage() {
cat <<'EOF'
Usage:
pexo-asset-get.sh <project_id> <asset_id>
pexo-asset-get.sh -h | --help
Description:
Fetch asset details for a project.
If the asset has a downloadUrl, this script also downloads the file into
~/.pexo/tmp/ (or $PEXO_TMP_DIR when set) and returns both the signed URL and
the local file path.
Returns:
Asset JSON plus:
- url: signed download URL
- localPath: downloaded local cache path, or null when downloadUrl is absent
Common errors:
401 Invalid API key or auth failure
404 Asset not found, or asset does not belong to the project/user
403 Signed asset URL expired or object storage denied download
500 Backend/internal failure
EOF
}
source "$(dirname "$0")/_common.sh"
case "${1:-}" in
-h|--help)
usage
exit 0
;;
esac
if [[ $# -ne 2 ]]; then
usage >&2
exit 2
fi
pid="$1"
aid="$2"
asset=$(pexo_get "/api/biz/projects/${pid}/assets/${aid}")
download_url=$(echo "$asset" | jq -r '.downloadUrl // empty')
if [[ -z "$download_url" ]]; then
echo "$asset" | jq '. + {url:(.downloadUrl // null), localPath:null}'
exit 0
fi
tmp_dir=$(pexo_tmp_dir)
file_name=$(echo "$asset" | jq -r '.fileName // .assetName // empty')
[[ -n "$file_name" && "$file_name" != "null" ]] || file_name="${aid}.bin"
safe_name=$(printf '%s' "$file_name" | sed 's#[/[:space:]]#_#g')
local_path="${tmp_dir}/${aid}-${safe_name}"
part_path="${local_path}.part.$$"
err_file=$(mktemp)
http_code=""
curl_status=0
http_code=$(curl -sS -L \
--connect-timeout "$_PEXO_CONNECT_TIMEOUT" \
--max-time "$_PEXO_REQUEST_TIMEOUT" \
-o "$part_path" \
-w '%{http_code}' \
"$download_url" 2>"$err_file") || curl_status=$?
if [[ $curl_status -ne 0 && "${http_code:-0}" == "000" ]]; then
err_text=$(cat "$err_file")
rm -f "$part_path" "$err_file"
_pexo_emit_error 0 "" "${err_text:-Failed to download asset from signed URL}"
fi
if [[ ! "${http_code:-}" =~ ^2 ]]; then
err_text=$(cat "$err_file")
rm -f "$part_path"
rm -f "$err_file"
_pexo_emit_error "${http_code:-0}" "" "${err_text:-Failed to download asset from signed URL}"
fi
mv -f "$part_path" "$local_path"
rm -f "$err_file"
echo "$asset" | jq --arg url "$download_url" --arg localPath "$local_path" '. + {url:$url, localPath:$localPath}'
#!/usr/bin/env bash
[ -n "${BASH_VERSION:-}" ] || exec bash "$0" "$@"
usage() {
cat <<'EOF'
Usage:
pexo-chat.sh <project_id> <message> [--choice <preview_asset_id>] [--timeout <seconds>]
pexo-chat.sh -h | --help
Description:
Submit a message to an existing Pexo project.
This script submits the message asynchronously. It waits until the server
acknowledges the request, then exits.
If the message references uploaded assets, wrap each asset ID with one of:
<original-image>asset_id</original-image>
<original-video>asset_id</original-video>
<original-audio>asset_id</original-audio>
Bare asset IDs inside the message are ignored by Pexo and rejected locally.
Options:
--choice <id> Send the selected preview asset ID as choices.preview_id
--timeout <sec> Wait time for SSE acknowledgement (default: 20)
Returns:
JSON acknowledgement:
{
"projectId": "...",
"status": "submitted",
"submissionMode": "async",
"submittedAt": "...",
"pollAfterSeconds": 60,
"nextActionHint": "Use pexo-project-get.sh to poll for progress."
}
Common errors:
Local validation error: asset IDs in <message> are not wrapped in valid tags
400 Invalid request body
401 Invalid API key or auth failure
404 Project not found
412 Project agent version incompatible, or account credits frozen / billing issue
Credit balance and a top-up link are printed to stderr automatically.
429 Project video limit reached, or insufficient credits to start production
Credit balance and a top-up link are printed to stderr automatically.
500 Backend/internal failure
EOF
}
source "$(dirname "$0")/_common.sh"
strip_valid_asset_tags() {
local text="$1"
printf '%s' "$text" \
| sed -E 's#<original-image>((a_[1-9A-HJ-NP-Za-km-z]{7,24})|([0-9A-Z]{26}))</original-image># #g' \
| sed -E 's#<original-video>((a_[1-9A-HJ-NP-Za-km-z]{7,24})|([0-9A-Z]{26}))</original-video># #g' \
| sed -E 's#<original-audio>((a_[1-9A-HJ-NP-Za-km-z]{7,24})|([0-9A-Z]{26}))</original-audio># #g'
}
find_unwrapped_asset_ids() {
local text="$1"
printf '%s' "$text" \
| tr -cs 'A-Za-z0-9_' '\n' \
| awk '/^([0-9A-Z]{26}|a_[1-9A-HJ-NP-Za-km-z]{7,24})$/ && !seen[$0]++'
}
validate_message_asset_references() {
local text="$1"
local stripped invalid_refs joined
stripped=$(strip_valid_asset_tags "$text")
invalid_refs=$(find_unwrapped_asset_ids "$stripped")
if [[ -z "$invalid_refs" ]]; then
return 0
fi
joined=$(printf '%s\n' "$invalid_refs" | awk 'BEGIN { first = 1 } { printf("%s%s", first ? "" : ", ", $0); first = 0 }')
echo 'Error: asset IDs in <message> must be wrapped with <original-image>...</original-image>, <original-video>...</original-video>, or <original-audio>...</original-audio>.' >&2
printf 'Invalid asset reference(s): %s\n' "$joined" >&2
echo 'Example: pexo-chat.sh <project_id> "Use <original-image>a_xxx</original-image> as the reference image."' >&2
return 1
}
case "${1:-}" in
-h|--help)
usage
exit 0
;;
esac
if [[ $# -lt 2 ]]; then
usage >&2
exit 2
fi
pid="$1"
msg="$2"
shift 2
choice=""
timeout="${PEXO_CHAT_ACK_TIMEOUT:-20}"
while [[ $# -gt 0 ]]; do
case "$1" in
--choice)
[[ $# -ge 2 ]] || { echo 'Error: --choice requires a value' >&2; exit 2; }
choice="$2"
shift 2
;;
--timeout)
[[ $# -ge 2 ]] || { echo 'Error: --timeout requires a value' >&2; exit 2; }
timeout="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Error: unknown option: $1" >&2
usage >&2
exit 2
;;
esac
done
validate_message_asset_references "$msg" || exit 2
ts=$(date +%s000)
if [[ -n "$choice" ]]; then
body=$(jq -nc --arg pid "$pid" --arg msg "$msg" --arg ts "$ts" --arg ch "$choice" \
'{project_id:$pid, timestamp:$ts, user_visible:true, native_inputs:{text:$msg}, choices:{preview_id:$ch}}')
else
body=$(jq -nc --arg pid "$pid" --arg msg "$msg" --arg ts "$ts" \
'{project_id:$pid, timestamp:$ts, user_visible:true, native_inputs:{text:$msg}}')
fi
pexo_post_sse_ack "/api/chat" "$body" "$timeout" || {
if [[ "$PEXO_LAST_HTTP_CODE" == "429" || "$PEXO_LAST_HTTP_CODE" == "412" ]]; then
_pexo_credit_hint
fi
exit 1
}
jq -nc \
--arg pid "$pid" \
--arg submitted_at "$ts" \
'{
projectId: $pid,
status: "submitted",
submissionMode: "async",
submittedAt: $submitted_at,
pollAfterSeconds: 60,
nextActionHint: "Use pexo-project-get.sh to poll for progress."
}'
#!/usr/bin/env bash
[ -n "${BASH_VERSION:-}" ] || exec bash "$0" "$@"
# Pexo environment diagnostic tool.
# Checks config, dependencies, connectivity, and API key validity.
# Run this when first setting up or when scripts fail unexpectedly.
#
# Usage: pexo-doctor.sh
set -uo pipefail
usage() {
cat <<'EOF'
Usage:
pexo-doctor.sh
pexo-doctor.sh -h | --help
Description:
Run environment checks for the Pexo shell scripts:
- config file presence
- required variables
- local dependencies
- network reachability
- API key/auth validity check
Notes:
API keys are expected to use the sk- prefix.
EOF
}
extract_message() {
local payload="${1:-}"
echo "$payload" | jq -r '.data.message // .message // .error // "unknown"' 2>/dev/null || echo "unknown"
}
extract_error_code() {
local payload="${1:-}"
echo "$payload" | jq -r '.data.error // .error // empty' 2>/dev/null || true
}
mask_secret() {
local value="${1:-}"
if [[ -z "$value" ]]; then
printf '%s\n' ""
return 0
fi
if [[ ${#value} -le 12 ]]; then
printf '%s\n' "$value"
return 0
fi
printf '%s...%s\n' "${value:0:8}" "${value: -4}"
}
case "${1:-}" in
-h|--help)
usage
exit 0
;;
esac
PASS="✓"
FAIL="✗"
WARN="!"
errors=0
echo "=== Pexo Environment Diagnostic ==="
echo ""
config_path="${PEXO_CONFIG:-$HOME/.pexo/config}"
# 1. Config file
if [[ -f "$config_path" ]]; then
echo "$PASS Config file found: $config_path"
source "$config_path"
else
echo "$FAIL Config file not found: $config_path"
echo " Create it with:"
echo ' mkdir -p ~/.pexo && cat > ~/.pexo/config << EOF'
echo ' PEXO_BASE_URL="https://pexo.ai"'
echo ' PEXO_API_KEY="sk-<your-api-key>"'
echo ' EOF'
errors=$((errors + 1))
fi
# 2. Required variables
if [[ -n "${PEXO_BASE_URL:-}" ]]; then
echo "$PASS PEXO_BASE_URL is set: $PEXO_BASE_URL"
else
echo "$FAIL PEXO_BASE_URL is not set"
errors=$((errors + 1))
fi
if [[ -n "${PEXO_API_KEY:-}" ]]; then
masked=$(mask_secret "$PEXO_API_KEY")
echo "$PASS PEXO_API_KEY is set: $masked"
if [[ "$PEXO_API_KEY" != sk-* ]]; then
echo "$WARN PEXO_API_KEY does not start with sk-"
echo " API keys are expected to start with sk-."
fi
else
echo "$FAIL PEXO_API_KEY is not set"
echo " Get your API key at: https://pexo.ai"
errors=$((errors + 1))
fi
# 3. Dependencies
echo ""
for cmd in curl jq file; do
if command -v "$cmd" &>/dev/null; then
ver=$("$cmd" --version 2>&1 | head -1)
echo "$PASS $cmd is installed: $ver"
else
echo "$FAIL $cmd is not installed"
if [[ "$cmd" == "file" ]]; then
echo " Install the package that provides file(1) for your OS. It is usually preinstalled on macOS."
else
echo " Install: brew install $cmd (macOS) or apt-get install $cmd (Linux)"
fi
errors=$((errors + 1))
fi
done
# 4. Network connectivity
echo ""
if [[ -n "${PEXO_BASE_URL:-}" ]]; then
http_code=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 10 "${PEXO_BASE_URL}" 2>/dev/null || echo "000")
if [[ "$http_code" != "000" ]]; then
echo "$PASS Network: can reach $PEXO_BASE_URL (HTTP $http_code)"
else
echo "$FAIL Network: cannot reach $PEXO_BASE_URL"
echo " Check your network connection, firewall, and DNS settings."
errors=$((errors + 1))
fi
else
echo "$WARN Network: skipped (PEXO_BASE_URL not set)"
fi
# 5. API key validation
echo ""
if [[ -n "${PEXO_BASE_URL:-}" && -n "${PEXO_API_KEY:-}" ]]; then
tmp_body=$(mktemp)
tmp_err=$(mktemp)
http_code=$(curl -sS \
--connect-timeout 10 \
-H "Authorization: Bearer $PEXO_API_KEY" \
-H "Content-Type: application/json" \
-o "$tmp_body" \
-w '%{http_code}' \
"${PEXO_BASE_URL}/api/biz/projects?page_size=1" 2>"$tmp_err" || echo "000")
resp=$(cat "$tmp_body")
curl_err=$(cat "$tmp_err")
rm -f "$tmp_body" "$tmp_err"
if [[ "$http_code" == "200" ]]; then
echo "$PASS API key is valid (projects endpoint responded OK)"
elif [[ "$http_code" == "401" ]]; then
auth_error=$(extract_error_code "$resp")
message=$(extract_message "$resp")
if [[ "$auth_error" == "INVALID_API_KEY" ]]; then
echo "$FAIL API key is invalid or expired (HTTP 401)"
echo " Message: $message"
echo " Get a new key at: https://pexo.ai"
errors=$((errors + 1))
elif [[ "$auth_error" == "INTERNAL_ERROR" ]]; then
echo "$WARN API check returned HTTP 401 with INTERNAL_ERROR"
echo " This is a temporary service issue, not a problem with the API key. Wait and retry."
echo " Message: $message"
else
echo "$FAIL API check returned HTTP 401"
echo " Message: $message"
errors=$((errors + 1))
fi
elif [[ "$http_code" == "409" ]]; then
echo "$WARN API check returned HTTP 409"
echo " Message: $(extract_message "$resp")"
echo " This is normal for JWT session replacement, but unusual for API-key auth."
elif [[ "$http_code" == "000" ]]; then
echo "$FAIL API validation request failed before receiving a response"
echo " Curl error: ${curl_err:-unknown}"
errors=$((errors + 1))
else
echo "$WARN API check returned HTTP $http_code"
echo " Message: $(extract_message "$resp")"
fi
else
echo "$WARN API key validation: skipped (missing config)"
fi
# Summary
echo ""
echo "=== Summary ==="
if [[ $errors -eq 0 ]]; then
echo "$PASS All checks passed. Pexo is ready to use."
else
echo "$FAIL $errors issue(s) found. Fix the items marked with $FAIL above."
fi
exit $errors
#!/usr/bin/env bash
[ -n "${BASH_VERSION:-}" ] || exec bash "$0" "$@"
usage() {
cat <<'EOF'
Usage:
pexo-entitlements.sh
pexo-entitlements.sh -h | --help
Description:
Fetch the current user's credit balance and plan entitlements.
Useful for proactively checking available credits before starting
production, or diagnosing why a previous request failed with 429.
Returns (stdout):
JSON object with shape:
{
"userId": "...",
"credits": {
"availableCredits": 120,
"subscriptionCredits": 100,
"bonusCredits": 20,
"purchaseCredits": 0,
"frozenCredits": 0,
"lifetimeGranted": 1000,
"lifetimeConsumed": 880,
"lifetimeExpired": 0
},
"plan": { ... }
}
When availableCredits is 0, a top-up URL is also printed to stderr.
Common errors:
401 Invalid API key or auth failure
500 Backend/internal failure
EOF
}
case "${1:-}" in
-h|--help)
usage
exit 0
;;
esac
source "$(dirname "$0")/_common.sh"
topup_url="${PEXO_BASE_URL:-https://pexo.ai}/home"
result=$(pexo_get "/api/biz/auth/entitlements")
available=$(printf '%s' "$result" \
| jq -r '.credits.availableCredits // empty' 2>/dev/null) || true
printf '%s\n' "$result"
if [[ -n "$available" ]] && \
{ [[ "$available" == "0" ]] || \
{ [[ "$available" =~ ^[0-9]+$ ]] && [[ "$available" -le 0 ]]; }; }; then
printf '\nCredits balance: 0 — your account has no available credits.\n' >&2
printf 'To purchase credits: visit %s → click Credits (top-right) → Buy Credits → Extra Credits\n' "$topup_url" >&2
fi
#!/usr/bin/env bash
[ -n "${BASH_VERSION:-}" ] || exec bash "$0" "$@"
usage() {
cat <<'EOF'
Usage:
pexo-project-create.sh [project_name]
pexo-project-create.sh --name <project_name>
pexo-project-create.sh -h | --help
Description:
Create a new Pexo project.
If no project name is provided, the script uses "Untitled".
Returns:
project_id string on stdout
Common errors:
400 Invalid project name
401 Invalid API key or auth failure
429 Creation limit reached (concurrent-project limit or insufficient credits)
Credit balance and a top-up link are printed to stderr automatically.
500 Backend/internal failure
EOF
}
source "$(dirname "$0")/_common.sh"
project_name=""
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
usage
exit 0
;;
--name)
[[ $# -ge 2 ]] || { echo 'Error: --name requires a value' >&2; exit 2; }
project_name="$2"
shift 2
;;
--)
shift
break
;;
-*)
echo "Error: unknown option: $1" >&2
usage >&2
exit 2
;;
*)
if [[ -n "$project_name" ]]; then
echo "Error: unexpected argument: $1" >&2
usage >&2
exit 2
fi
project_name="$1"
shift
;;
esac
done
if [[ $# -gt 0 ]]; then
echo "Error: unexpected argument: $1" >&2
usage >&2
exit 2
fi
[[ -n "$project_name" ]] || project_name="Untitled"
body=$(jq -nc --arg n "$project_name" '{project_name: $n}')
result=$(pexo_post "/api/biz/projects" "$body") || {
[[ "$PEXO_LAST_HTTP_CODE" == "429" ]] && _pexo_credit_hint
exit 1
}
project_id=$(echo "$result" | jq -r '.projectId // empty')
if [[ -z "$project_id" ]]; then
echo 'Error: create project response missing projectId' >&2
echo "$result" >&2
exit 1
fi
printf '%s\n' "$project_id"
#!/usr/bin/env bash
# If invoked with sh, re-exec with bash (this script uses bash-only syntax).
[ -n "${BASH_VERSION:-}" ] || exec bash "$0" "$@"
usage() {
cat <<'EOF'
Usage:
pexo-project-get.sh <project_id> [--full-history]
pexo-project-get.sh -h | --help
Description:
Fetch project state and derive nextAction for agent-side orchestration.
Options:
--full-history Return simplified full message history instead of nextAction view
Returns:
Default mode:
Project JSON with nextAction, nextActionHint, and recentMessages when action is needed
--full-history:
Project JSON with recentMessages for the full simplified history
Common errors:
401 Invalid API key or auth failure
404 Project not found
500 Backend/internal failure
EOF
}
# Get project details with next-action recommendation.
# Returns a clean project JSON with:
#
# nextAction — WAIT | RESPOND | DELIVER | FAILED | RECONNECT
# nextActionHint — plain-language instruction for what to do next
# recentMessages — simplified last conversation round (when nextAction is RESPOND / DELIVER / FAILED / RECONNECT)
#
# Internal status fields and progress values are stripped from output.
# Callers should only use nextAction to decide what to do.
#
# recentMessages format (simplified, actionable-only):
# USER → {role, text}
# message → {role, event:"message", text}
# final_video → {role, event:"final_video", assetId}
# preview_video → {role, event:"preview_video", assetIds:[...]}
# document → {role, event:"document", documentType, documentName}
# (planning / progress / thinking / meta / voice etc. are omitted)
#
# Usage: pexo-project-get.sh <project_id> [--full-history]
source "$(dirname "$0")/_common.sh"
case "${1:-}" in
-h|--help)
usage
exit 0
;;
esac
if [[ $# -lt 1 ]]; then
usage >&2
exit 2
fi
pid="$1"
shift
full_history=false
while [[ $# -gt 0 ]]; do
case "$1" in
--full-history) full_history=true; shift ;;
-h|--help)
usage
exit 0
;;
*)
echo "Error: unknown option: $1" >&2
usage >&2
exit 2
;;
esac
done
# jq filter: simplify a raw messages array into actionable-only entries.
_SIMPLIFY_MSGS='[.[] |
if (.role | ascii_downcase) == "user" then
{role: "USER", text: (.content.native_inputs.text // null)}
else
(.content.event // "") as $evt |
(.content.data // {}) as $d |
if $evt == "message" then {role: "ASSISTANT", event: "message", text: ($d.message // null)}
elif $evt == "final_video" then {role: "ASSISTANT", event: "final_video", assetId: ($d.final_video_id // null)}
elif $evt == "preview_video" then {role: "ASSISTANT", event: "preview_video", assetIds: ($d.preview_video_ids // [])}
elif $evt == "document" then {role: "ASSISTANT", event: "document", documentType: ($d.type // null), documentName: ($d.name // null)}
elif $evt == "attachment" then {role: "ASSISTANT", event: "attachment", assetIds: ($d.attachment_ids // [])}
else empty
end
end
]'
_raw=$(pexo_get "/api/biz/projects/${pid}")
# Read status fields needed for nextAction logic before stripping them
exec_status=$(echo "$_raw" | jq -r '.executionStatus // ""')
svc_status=$(echo "$_raw" | jq -r '.serviceStatus // ""')
# Strip internal status fields from the output project object
project=$(echo "$_raw" | jq 'del(.status, .executionStatus, .serviceStatus, .executionProgress, .stepProgress)')
# ── Full history mode (bypass nextAction logic) ───────────────────────────────
if [[ "$full_history" == "true" ]]; then
history=$(pexo_get "/api/biz/projects/${pid}/history?page=1&page_size=200&sort_order=ASC")
raw_msgs=$(echo "$history" | jq 'if type == "array" then . else (.messages // []) end' 2>/dev/null || echo '[]')
messages=$(echo "$raw_msgs" | jq "$_SIMPLIFY_MSGS" 2>/dev/null || echo '[]')
echo "$project" | jq --argjson msgs "$messages" '. + {recentMessages: $msgs}'
exit 0
fi
# ── Determine nextAction from status fields ──────────────────────────────────
if [[ "$exec_status" == "FAILED" ]]; then
next_action="FAILED"
hint="Production failed. Read recentMessages for error details. Send a new message via pexo-chat.sh to retry with a modified brief."
elif [[ "$exec_status" == "COMPLETED" && "$svc_status" != "PROCESSING" ]]; then
next_action="DELIVER"
hint="Production complete. Find assetId in recentMessages[event=final_video], fetch it with pexo-asset-get.sh."
elif [[ "$exec_status" == "INTERRUPTED" ]]; then
next_action="RESPOND"
hint="Pexo is waiting for your input. Read recentMessages to understand what is needed, then call pexo-chat.sh to respond."
elif [[ "$exec_status" == "RUNNING" && "$svc_status" == "IDLE" ]]; then
next_action="RECONNECT"
hint="Connection may have been lost. Re-initiate the conversation by sending a new message via pexo-chat.sh."
else
next_action="WAIT"
hint="Production is in progress. Poll again in 60 seconds."
fi
# ── Fetch and simplify recentMessages when caller must act ───────────────────
if [[ "$next_action" == "RESPOND" || "$next_action" == "DELIVER" || "$next_action" == "FAILED" || "$next_action" == "RECONNECT" ]]; then
# Paginate DESC (newest first) until we find a page with a user message,
# then take from that user message to the top and reverse to chronological order.
page=1
page_size=50
accumulated='[]'
recent_raw='[]'
while true; do
resp=$(pexo_get "/api/biz/projects/${pid}/history?page=${page}&page_size=${page_size}&sort_order=DESC")
new_msgs=$(echo "$resp" | jq 'if type == "array" then . else (.messages // []) end' 2>/dev/null || echo '[]')
has_more=$(echo "$resp" | jq '.hasMore // false' 2>/dev/null)
accumulated=$(jq -n --argjson a "$accumulated" --argjson b "$new_msgs" '$a + $b' 2>/dev/null || echo '[]')
user_count=$(echo "$accumulated" | jq '[.[] | select((.role | ascii_downcase) == "user")] | length' 2>/dev/null || echo 0)
if [[ "${user_count:-0}" -gt 0 ]]; then
recent_raw=$(echo "$accumulated" | jq '
. as $all |
[range(length)] | map(select(($all[.].role | ascii_downcase) == "user")) |
if length > 0 then (first as $idx | $all[0:($idx+1)] | reverse)
else []
end
' 2>/dev/null || echo '[]')
break
fi
if [[ "$has_more" != "true" ]]; then
break
fi
page=$((page + 1))
done
recent=$(echo "$recent_raw" | jq "$_SIMPLIFY_MSGS" 2>/dev/null || echo '[]')
echo "$project" | jq \
--arg na "$next_action" \
--arg hint "$hint" \
--argjson msgs "$recent" \
'. + {nextAction: $na, nextActionHint: $hint, recentMessages: $msgs}'
else
echo "$project" | jq \
--arg na "$next_action" \
--arg hint "$hint" \
'. + {nextAction: $na, nextActionHint: $hint}'
fi
#!/usr/bin/env bash
[ -n "${BASH_VERSION:-}" ] || exec bash "$0" "$@"
usage() {
cat <<'EOF'
Usage:
pexo-project-list.sh [page_size]
pexo-project-list.sh [--page <n>] [--page-size <n>]
pexo-project-list.sh -h | --help
Description:
List projects for the authenticated user.
Options:
--page <n> Page number (default: 1)
--page-size <n> Page size (default: 20, effective max: 100)
Returns:
Projects JSON
Common errors:
401 Invalid API key or auth failure
500 Backend/internal failure
EOF
}
source "$(dirname "$0")/_common.sh"
page=1
page_size=20
legacy_page_size=""
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
usage
exit 0
;;
--page)
[[ $# -ge 2 ]] || { echo 'Error: --page requires a value' >&2; exit 2; }
page="$2"
shift 2
;;
--page-size)
[[ $# -ge 2 ]] || { echo 'Error: --page-size requires a value' >&2; exit 2; }
page_size="$2"
shift 2
;;
-*)
echo "Error: unknown option: $1" >&2
usage >&2
exit 2
;;
*)
if [[ -n "$legacy_page_size" ]]; then
echo "Error: unexpected argument: $1" >&2
usage >&2
exit 2
fi
legacy_page_size="$1"
shift
;;
esac
done
if [[ -n "$legacy_page_size" ]]; then
page_size="$legacy_page_size"
fi
if [[ ! "$page" =~ ^[0-9]+$ || "$page" == "0" ]]; then
echo "Error: page must be a positive integer: $page" >&2
exit 2
fi
if [[ ! "$page_size" =~ ^[0-9]+$ || "$page_size" == "0" ]]; then
echo "Error: page_size must be a positive integer: $page_size" >&2
exit 2
fi
pexo_get "/api/biz/projects?page=${page}&page_size=${page_size}"
#!/usr/bin/env bash
[ -n "${BASH_VERSION:-}" ] || exec bash "$0" "$@"
usage() {
cat <<'EOF'
Usage:
pexo-upload.sh <project_id> <file_path>
pexo-upload.sh -h | --help
Description:
Upload a local media file to a project in three steps:
1. Request upload credential
2. Upload the file
3. Finalize the asset
Supported file types:
Images: jpg, jpeg, png, webp, bmp, tiff, heic, heif
Videos: mp4, mov, avi
Audio: mp3, wav, aac, m4a, ogg, flac
Returns:
asset_id string on stdout
Common errors:
400 Invalid file metadata or unsupported media type
401 Invalid API key or auth failure
404 Asset not found during finalize
412 Asset is no longer in UPLOADING state during finalize
500 Upload credential/finalize backend failure
EOF
}
source "$(dirname "$0")/_common.sh"
case "${1:-}" in
-h|--help)
usage
exit 0
;;
esac
if [[ $# -ne 2 ]]; then
usage >&2
exit 2
fi
pid="$1"
filepath="$2"
[[ -f "$filepath" ]] || { echo "Error: file not found: $filepath" >&2; exit 1; }
filename=$(basename "$filepath")
filesize=$(stat -f%z "$filepath" 2>/dev/null || stat -c%s "$filepath" 2>/dev/null)
asset_type=$(detect_asset_type "$filename")
mime_type=$(detect_mime "$filepath")
finalize_mime_type="$mime_type"
[[ -n "${filesize:-}" ]] || { echo "Error: failed to determine file size: $filepath" >&2; exit 1; }
[[ "$asset_type" != "UNKNOWN" ]] || {
echo "Error: unsupported file type: $filename" >&2
echo "Allowed: jpg jpeg png webp bmp tiff heic heif mp4 mov avi mp3 wav aac m4a ogg flac" >&2
exit 1
}
if ! mime_supported_for_asset_type "$mime_type" "$asset_type"; then
finalize_mime_type=""
fi
# Phase 1: get upload credential
cred=$(pexo_post "/api/biz/projects/${pid}/assets/upload-credential" \
"{\"file_name\":\"$filename\",\"file_size\":$filesize}")
upload_url=$(echo "$cred" | jq -r '.uploadUrl')
asset_id=$(echo "$cred" | jq -r '.assetId')
storage_path=$(echo "$cred" | jq -r '.storagePath')
[[ -n "$upload_url" && "$upload_url" != "null" ]] || { echo "Error: failed to get upload credential" >&2; echo "$cred" >&2; exit 1; }
[[ -n "$asset_id" && "$asset_id" != "null" ]] || { echo "Error: upload credential missing assetId" >&2; echo "$cred" >&2; exit 1; }
[[ -n "$storage_path" && "$storage_path" != "null" ]] || { echo "Error: upload credential missing storagePath" >&2; echo "$cred" >&2; exit 1; }
# Phase 2: upload file
http_code=$(curl -sS -X PUT -H "Content-Type: $mime_type" \
--data-binary "@$filepath" -o /dev/null -w '%{http_code}' "$upload_url" 2>/dev/null || echo "000")
[[ "$http_code" =~ ^2 ]] || { echo "Error: upload failed with HTTP $http_code" >&2; exit 1; }
# Phase 3: finalize
finalize_body=$(jq -nc \
--arg name "$filename" \
--arg type "$asset_type" \
--arg fname "$filename" \
--argjson size "$filesize" \
--arg mime "$finalize_mime_type" \
--arg spath "$storage_path" \
'{
asset_name:$name,
asset_type:$type,
file_name:$fname,
file_size:$size,
storage_path:$spath
} + (if $mime != "" then {mime_type:$mime} else {} end)')
pexo_post "/api/biz/projects/${pid}/assets/${asset_id}/finalize" "$finalize_body" > /dev/null
printf '%s\n' "$asset_id"