From ec14db26c92e3688f5b99bdc77d717989aad1a8a Mon Sep 17 00:00:00 2001 From: finns0309 <2449473420@qq.com> Date: Mon, 8 Jun 2026 20:35:53 +0800 Subject: [PATCH] feat(skills): add scenario video skills Adds thin-client skills for common video tasks, each delegating to the hosted Pexo video agent (same backend as pexo-agent) through the shared scripts/ API client: image-to-video, ai-video-generation, make-a-video, video-ad, product-video, tiktok-video-ad, youtube-short-maker, explainer-video. Each documents setup, troubleshooting, and the full async workflow (create -> upload -> chat -> poll -> deliver, with reconnect/timeout/revisions). All creative work happens server-side in Pexo. Co-Authored-By: Claude Opus 4.8 --- skills/ai-video-generation/README.md | 38 ++ skills/ai-video-generation/SKILL.md | 107 +++++ .../references/SETUP-CHECKLIST.md | 93 ++++ .../references/TROUBLESHOOTING.md | 289 ++++++++++++ skills/ai-video-generation/scripts/_common.sh | 430 ++++++++++++++++++ .../scripts/pexo-asset-get.sh | 88 ++++ .../ai-video-generation/scripts/pexo-chat.sh | 154 +++++++ .../scripts/pexo-doctor.sh | 200 ++++++++ .../scripts/pexo-entitlements.sh | 63 +++ .../scripts/pexo-project-create.sh | 84 ++++ .../scripts/pexo-project-get.sh | 177 +++++++ .../scripts/pexo-project-list.sh | 80 ++++ .../scripts/pexo-upload.sh | 106 +++++ skills/explainer-video/README.md | 38 ++ skills/explainer-video/SKILL.md | 107 +++++ .../references/SETUP-CHECKLIST.md | 93 ++++ .../references/TROUBLESHOOTING.md | 289 ++++++++++++ skills/explainer-video/scripts/_common.sh | 430 ++++++++++++++++++ .../explainer-video/scripts/pexo-asset-get.sh | 88 ++++ skills/explainer-video/scripts/pexo-chat.sh | 154 +++++++ skills/explainer-video/scripts/pexo-doctor.sh | 200 ++++++++ .../scripts/pexo-entitlements.sh | 63 +++ .../scripts/pexo-project-create.sh | 84 ++++ .../scripts/pexo-project-get.sh | 177 +++++++ .../scripts/pexo-project-list.sh | 80 ++++ skills/explainer-video/scripts/pexo-upload.sh | 106 +++++ skills/image-to-video/README.md | 38 ++ skills/image-to-video/SKILL.md | 108 +++++ .../references/SETUP-CHECKLIST.md | 93 ++++ .../references/TROUBLESHOOTING.md | 289 ++++++++++++ skills/image-to-video/scripts/_common.sh | 430 ++++++++++++++++++ .../image-to-video/scripts/pexo-asset-get.sh | 88 ++++ skills/image-to-video/scripts/pexo-chat.sh | 154 +++++++ skills/image-to-video/scripts/pexo-doctor.sh | 200 ++++++++ .../scripts/pexo-entitlements.sh | 63 +++ .../scripts/pexo-project-create.sh | 84 ++++ .../scripts/pexo-project-get.sh | 177 +++++++ .../scripts/pexo-project-list.sh | 80 ++++ skills/image-to-video/scripts/pexo-upload.sh | 106 +++++ skills/make-a-video/README.md | 38 ++ skills/make-a-video/SKILL.md | 107 +++++ .../references/SETUP-CHECKLIST.md | 93 ++++ .../references/TROUBLESHOOTING.md | 289 ++++++++++++ skills/make-a-video/scripts/_common.sh | 430 ++++++++++++++++++ skills/make-a-video/scripts/pexo-asset-get.sh | 88 ++++ skills/make-a-video/scripts/pexo-chat.sh | 154 +++++++ skills/make-a-video/scripts/pexo-doctor.sh | 200 ++++++++ .../make-a-video/scripts/pexo-entitlements.sh | 63 +++ .../scripts/pexo-project-create.sh | 84 ++++ .../make-a-video/scripts/pexo-project-get.sh | 177 +++++++ .../make-a-video/scripts/pexo-project-list.sh | 80 ++++ skills/make-a-video/scripts/pexo-upload.sh | 106 +++++ skills/product-video/README.md | 38 ++ skills/product-video/SKILL.md | 107 +++++ .../references/SETUP-CHECKLIST.md | 93 ++++ .../references/TROUBLESHOOTING.md | 289 ++++++++++++ skills/product-video/scripts/_common.sh | 430 ++++++++++++++++++ .../product-video/scripts/pexo-asset-get.sh | 88 ++++ skills/product-video/scripts/pexo-chat.sh | 154 +++++++ skills/product-video/scripts/pexo-doctor.sh | 200 ++++++++ .../scripts/pexo-entitlements.sh | 63 +++ .../scripts/pexo-project-create.sh | 84 ++++ .../product-video/scripts/pexo-project-get.sh | 177 +++++++ .../scripts/pexo-project-list.sh | 80 ++++ skills/product-video/scripts/pexo-upload.sh | 106 +++++ skills/tiktok-video-ad/README.md | 38 ++ skills/tiktok-video-ad/SKILL.md | 107 +++++ .../references/SETUP-CHECKLIST.md | 93 ++++ .../references/TROUBLESHOOTING.md | 289 ++++++++++++ skills/tiktok-video-ad/scripts/_common.sh | 430 ++++++++++++++++++ .../tiktok-video-ad/scripts/pexo-asset-get.sh | 88 ++++ skills/tiktok-video-ad/scripts/pexo-chat.sh | 154 +++++++ skills/tiktok-video-ad/scripts/pexo-doctor.sh | 200 ++++++++ .../scripts/pexo-entitlements.sh | 63 +++ .../scripts/pexo-project-create.sh | 84 ++++ .../scripts/pexo-project-get.sh | 177 +++++++ .../scripts/pexo-project-list.sh | 80 ++++ skills/tiktok-video-ad/scripts/pexo-upload.sh | 106 +++++ skills/video-ad/README.md | 38 ++ skills/video-ad/SKILL.md | 107 +++++ skills/video-ad/references/SETUP-CHECKLIST.md | 93 ++++ skills/video-ad/references/TROUBLESHOOTING.md | 289 ++++++++++++ skills/video-ad/scripts/_common.sh | 430 ++++++++++++++++++ skills/video-ad/scripts/pexo-asset-get.sh | 88 ++++ skills/video-ad/scripts/pexo-chat.sh | 154 +++++++ skills/video-ad/scripts/pexo-doctor.sh | 200 ++++++++ skills/video-ad/scripts/pexo-entitlements.sh | 63 +++ .../video-ad/scripts/pexo-project-create.sh | 84 ++++ skills/video-ad/scripts/pexo-project-get.sh | 177 +++++++ skills/video-ad/scripts/pexo-project-list.sh | 80 ++++ skills/video-ad/scripts/pexo-upload.sh | 106 +++++ skills/youtube-short-maker/README.md | 38 ++ skills/youtube-short-maker/SKILL.md | 107 +++++ .../references/SETUP-CHECKLIST.md | 93 ++++ .../references/TROUBLESHOOTING.md | 289 ++++++++++++ skills/youtube-short-maker/scripts/_common.sh | 430 ++++++++++++++++++ .../scripts/pexo-asset-get.sh | 88 ++++ .../youtube-short-maker/scripts/pexo-chat.sh | 154 +++++++ .../scripts/pexo-doctor.sh | 200 ++++++++ .../scripts/pexo-entitlements.sh | 63 +++ .../scripts/pexo-project-create.sh | 84 ++++ .../scripts/pexo-project-get.sh | 177 +++++++ .../scripts/pexo-project-list.sh | 80 ++++ .../scripts/pexo-upload.sh | 106 +++++ 104 files changed, 15273 insertions(+) create mode 100644 skills/ai-video-generation/README.md create mode 100644 skills/ai-video-generation/SKILL.md create mode 100644 skills/ai-video-generation/references/SETUP-CHECKLIST.md create mode 100644 skills/ai-video-generation/references/TROUBLESHOOTING.md create mode 100644 skills/ai-video-generation/scripts/_common.sh create mode 100644 skills/ai-video-generation/scripts/pexo-asset-get.sh create mode 100644 skills/ai-video-generation/scripts/pexo-chat.sh create mode 100644 skills/ai-video-generation/scripts/pexo-doctor.sh create mode 100644 skills/ai-video-generation/scripts/pexo-entitlements.sh create mode 100644 skills/ai-video-generation/scripts/pexo-project-create.sh create mode 100644 skills/ai-video-generation/scripts/pexo-project-get.sh create mode 100644 skills/ai-video-generation/scripts/pexo-project-list.sh create mode 100644 skills/ai-video-generation/scripts/pexo-upload.sh create mode 100644 skills/explainer-video/README.md create mode 100644 skills/explainer-video/SKILL.md create mode 100644 skills/explainer-video/references/SETUP-CHECKLIST.md create mode 100644 skills/explainer-video/references/TROUBLESHOOTING.md create mode 100644 skills/explainer-video/scripts/_common.sh create mode 100644 skills/explainer-video/scripts/pexo-asset-get.sh create mode 100644 skills/explainer-video/scripts/pexo-chat.sh create mode 100644 skills/explainer-video/scripts/pexo-doctor.sh create mode 100644 skills/explainer-video/scripts/pexo-entitlements.sh create mode 100644 skills/explainer-video/scripts/pexo-project-create.sh create mode 100644 skills/explainer-video/scripts/pexo-project-get.sh create mode 100644 skills/explainer-video/scripts/pexo-project-list.sh create mode 100644 skills/explainer-video/scripts/pexo-upload.sh create mode 100644 skills/image-to-video/README.md create mode 100644 skills/image-to-video/SKILL.md create mode 100644 skills/image-to-video/references/SETUP-CHECKLIST.md create mode 100644 skills/image-to-video/references/TROUBLESHOOTING.md create mode 100644 skills/image-to-video/scripts/_common.sh create mode 100644 skills/image-to-video/scripts/pexo-asset-get.sh create mode 100644 skills/image-to-video/scripts/pexo-chat.sh create mode 100644 skills/image-to-video/scripts/pexo-doctor.sh create mode 100644 skills/image-to-video/scripts/pexo-entitlements.sh create mode 100644 skills/image-to-video/scripts/pexo-project-create.sh create mode 100644 skills/image-to-video/scripts/pexo-project-get.sh create mode 100644 skills/image-to-video/scripts/pexo-project-list.sh create mode 100644 skills/image-to-video/scripts/pexo-upload.sh create mode 100644 skills/make-a-video/README.md create mode 100644 skills/make-a-video/SKILL.md create mode 100644 skills/make-a-video/references/SETUP-CHECKLIST.md create mode 100644 skills/make-a-video/references/TROUBLESHOOTING.md create mode 100644 skills/make-a-video/scripts/_common.sh create mode 100644 skills/make-a-video/scripts/pexo-asset-get.sh create mode 100644 skills/make-a-video/scripts/pexo-chat.sh create mode 100644 skills/make-a-video/scripts/pexo-doctor.sh create mode 100644 skills/make-a-video/scripts/pexo-entitlements.sh create mode 100644 skills/make-a-video/scripts/pexo-project-create.sh create mode 100644 skills/make-a-video/scripts/pexo-project-get.sh create mode 100644 skills/make-a-video/scripts/pexo-project-list.sh create mode 100644 skills/make-a-video/scripts/pexo-upload.sh create mode 100644 skills/product-video/README.md create mode 100644 skills/product-video/SKILL.md create mode 100644 skills/product-video/references/SETUP-CHECKLIST.md create mode 100644 skills/product-video/references/TROUBLESHOOTING.md create mode 100644 skills/product-video/scripts/_common.sh create mode 100644 skills/product-video/scripts/pexo-asset-get.sh create mode 100644 skills/product-video/scripts/pexo-chat.sh create mode 100644 skills/product-video/scripts/pexo-doctor.sh create mode 100644 skills/product-video/scripts/pexo-entitlements.sh create mode 100644 skills/product-video/scripts/pexo-project-create.sh create mode 100644 skills/product-video/scripts/pexo-project-get.sh create mode 100644 skills/product-video/scripts/pexo-project-list.sh create mode 100644 skills/product-video/scripts/pexo-upload.sh create mode 100644 skills/tiktok-video-ad/README.md create mode 100644 skills/tiktok-video-ad/SKILL.md create mode 100644 skills/tiktok-video-ad/references/SETUP-CHECKLIST.md create mode 100644 skills/tiktok-video-ad/references/TROUBLESHOOTING.md create mode 100644 skills/tiktok-video-ad/scripts/_common.sh create mode 100644 skills/tiktok-video-ad/scripts/pexo-asset-get.sh create mode 100644 skills/tiktok-video-ad/scripts/pexo-chat.sh create mode 100644 skills/tiktok-video-ad/scripts/pexo-doctor.sh create mode 100644 skills/tiktok-video-ad/scripts/pexo-entitlements.sh create mode 100644 skills/tiktok-video-ad/scripts/pexo-project-create.sh create mode 100644 skills/tiktok-video-ad/scripts/pexo-project-get.sh create mode 100644 skills/tiktok-video-ad/scripts/pexo-project-list.sh create mode 100644 skills/tiktok-video-ad/scripts/pexo-upload.sh create mode 100644 skills/video-ad/README.md create mode 100644 skills/video-ad/SKILL.md create mode 100644 skills/video-ad/references/SETUP-CHECKLIST.md create mode 100644 skills/video-ad/references/TROUBLESHOOTING.md create mode 100644 skills/video-ad/scripts/_common.sh create mode 100644 skills/video-ad/scripts/pexo-asset-get.sh create mode 100644 skills/video-ad/scripts/pexo-chat.sh create mode 100644 skills/video-ad/scripts/pexo-doctor.sh create mode 100644 skills/video-ad/scripts/pexo-entitlements.sh create mode 100644 skills/video-ad/scripts/pexo-project-create.sh create mode 100644 skills/video-ad/scripts/pexo-project-get.sh create mode 100644 skills/video-ad/scripts/pexo-project-list.sh create mode 100644 skills/video-ad/scripts/pexo-upload.sh create mode 100644 skills/youtube-short-maker/README.md create mode 100644 skills/youtube-short-maker/SKILL.md create mode 100644 skills/youtube-short-maker/references/SETUP-CHECKLIST.md create mode 100644 skills/youtube-short-maker/references/TROUBLESHOOTING.md create mode 100644 skills/youtube-short-maker/scripts/_common.sh create mode 100644 skills/youtube-short-maker/scripts/pexo-asset-get.sh create mode 100644 skills/youtube-short-maker/scripts/pexo-chat.sh create mode 100644 skills/youtube-short-maker/scripts/pexo-doctor.sh create mode 100644 skills/youtube-short-maker/scripts/pexo-entitlements.sh create mode 100644 skills/youtube-short-maker/scripts/pexo-project-create.sh create mode 100644 skills/youtube-short-maker/scripts/pexo-project-get.sh create mode 100644 skills/youtube-short-maker/scripts/pexo-project-list.sh create mode 100644 skills/youtube-short-maker/scripts/pexo-upload.sh diff --git a/skills/ai-video-generation/README.md b/skills/ai-video-generation/README.md new file mode 100644 index 0000000..f6dc7be --- /dev/null +++ b/skills/ai-video-generation/README.md @@ -0,0 +1,38 @@ +# AI Video Generation — Pexo + +Generate AI video from text, images, or scripts. Pexo auto-routes each shot to the best of 10+ +models, handles the prompts and generation, and returns a finished video with music and +subtitles — one skill, no model picking. + +## Install + +```bash +npx skills add https://github.com/pexoai/pexo-skills --skill ai-video-generation +``` + +## Configure + +Create `~/.pexo/config`: + +``` +PEXO_BASE_URL="https://pexo.ai" +PEXO_API_KEY="sk-" +``` + +Get an API key at . + +## Use + +Ask your agent, for example: + +> Generate a 20-second AI video introducing our productivity app. + +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 ai-video-generation scenario. All creative work (scriptwriting, +model selection, prompts, music, subtitles) happens server-side. diff --git a/skills/ai-video-generation/SKILL.md b/skills/ai-video-generation/SKILL.md new file mode 100644 index 0000000..273eb35 --- /dev/null +++ b/skills/ai-video-generation/SKILL.md @@ -0,0 +1,107 @@ +--- +name: ai-video-generation +description: > + Generate AI video from any input — text, image, or script — with Pexo. Auto-routes each shot to + the best of 10+ models (Seedance, Kling, Veo, Sora, and more), writes the prompts, generates + every shot, and returns a finished video with music and subtitles. Use for any AI video + generation request: "ai video generation", "generate a video with ai", "ai video generator", + "make an ai video". NOT for editing an existing video. +homepage: https://pexo.ai +repository: https://github.com/pexoai/pexo-skills +version: "0.1.0" +requires: + env: [PEXO_API_KEY, PEXO_BASE_URL] + runtime: [curl, jq, file] +metadata: + author: pexoai +--- + +# AI Video Generation — Pexo + +**Pexo:** https://pexo.ai — get an API key, watch your project render, and buy credits there. + +Generate a finished video with AI from whatever the user gives you — text, an image, or a +script. You relay the request to the hosted Pexo agent and deliver the result; Pexo picks the +models, writes the prompts, and assembles the final video. + +## 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-" +``` +**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 ""` → save the `project_id`. +2. **Upload any files** the user gave: `pexo-upload.sh ` → save `asset_id`, + reference it inline as `asset_id` (or `` / + ``). Tags are required — a bare `asset_id` is ignored. Pexo can't crawl URLs — + download, then upload. +3. **Send the request:** `pexo-chat.sh " "`. + Copy the user's words exactly; only add asset tags. +4. **Tell the user** (their language): submitted ✓ · ~15–20 min · `https://pexo.ai/project/`. +5. **Poll:** every ≥60s run `pexo-project-get.sh ` 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 ` per option, show the URLs (A/B/C), let the user + pick, then `pexo-chat.sh "" --choice `; for a `document` + event, mention it to the user. + - **DELIVER** → `pexo-asset-get.sh `, 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 `nextActionHint` in plain terms and offer to retry. + - **RECONNECT** → `pexo-chat.sh "continue"`, tell the user the connection + dropped and you're resuming, then keep polling. + - Never call `pexo-chat.sh` during 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 ""`, 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: "Generate a 20-second AI video introducing our productivity app." + +```bash +pid=$(pexo-project-create.sh "AI video app intro") +pexo-chat.sh "$pid" "Generate a 20-second AI video introducing our productivity app." +# 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` | `""` | `project_id` | +| `pexo-upload.sh` | ` ` | `asset_id` | +| `pexo-chat.sh` | ` "" [--choice ]` | ack (async) | +| `pexo-project-get.sh` | `` | JSON: `nextAction`, `recentMessages` | +| `pexo-asset-get.sh` | ` ` | JSON with `url` | +| `pexo-doctor.sh` | — | setup diagnostic | + +Error codes and edge cases → `references/TROUBLESHOOTING.md`. diff --git a/skills/ai-video-generation/references/SETUP-CHECKLIST.md b/skills/ai-video-generation/references/SETUP-CHECKLIST.md new file mode 100644 index 0000000..182547f --- /dev/null +++ b/skills/ai-video-generation/references/SETUP-CHECKLIST.md @@ -0,0 +1,93 @@ +# Setup Checklist + +This guide covers first-time setup and environment diagnostics for the pexo-video skill. + +## Quick Start + +### 1. Create config file + +```bash +mkdir -p ~/.pexo +cat > ~/.pexo/config << 'EOF' +PEXO_BASE_URL="https://pexo.ai" +PEXO_API_KEY="sk-" +EOF +``` + +Get 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: **3MU4LH** + 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 + +```bash +pexo-doctor.sh +``` + +This checks: +- Config file exists and is readable +- `PEXO_BASE_URL` and `PEXO_API_KEY` are set +- `curl`, `jq`, and `file` are installed +- Network connectivity to Pexo servers +- API key is valid (attempts to list projects) + +Fix any issues reported before using other scripts. + +### 3. Verify + +```bash +pexo-project-list.sh +``` + +If 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: + +```bash +# 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 file +``` + +### Network 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_proxy` environment 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 | diff --git a/skills/ai-video-generation/references/TROUBLESHOOTING.md b/skills/ai-video-generation/references/TROUBLESHOOTING.md new file mode 100644 index 0000000..42b48b4 --- /dev/null +++ b/skills/ai-video-generation/references/TROUBLESHOOTING.md @@ -0,0 +1,289 @@ +# 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: + +```json +{"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 script +- `error`: auth/proxy error code such as `INVALID_API_KEY` or `INTERNAL_ERROR` +- `message`: the most useful user-facing message extracted from the response +- `details`: 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 Credits +``` + + +## Auth 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_size` values 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: + +```text +Error: upload failed with HTTP +``` + +#### 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 using `pexo-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 using `pexo-upload.sh`. +- `412`: the upload session has already expired or been completed. Re-upload from scratch using `pexo-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 a `429`. +- `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.sh` exits non-zero and prints compact JSON to `stderr`, for example: + +```json +{“ok”:false,”httpCode”:200,”message”:”Insufficient credits”,”error”:”credits.insufficient_credits_err”} +``` + +Notes: + +- `pexo-chat.sh` is 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.sh` call should be followed by `pexo-project-get.sh` polling, typically every `60` seconds. + +### `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 `availableCredits` is `0`, the top-up URL is also printed to stderr. +- You generally do not need to call this script manually — `pexo-project-create.sh` and `pexo-chat.sh` call it automatically on `429`/`412` failures 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-run `pexo-asset-get.sh` to 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 set `PEXO_TMP_DIR` to a writable path. + +Notes: + +- The script downloads the file into `~/.pexo/tmp/` (or `$PEXO_TMP_DIR`) and returns both `url` and `localPath`. +- If the asset metadata exists but `downloadUrl` is absent, the script returns `localPath: null`. + +### `pexo-doctor.sh` + +- `200`: config and API key look healthy +- `401` + `INVALID_API_KEY`: API key is invalid or revoked. Update `PEXO_API_KEY` in `~/.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 Credits +``` + +If `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 `429` is likely the concurrent-project limit: the user already has an active project running. +- Re-read the `message` field 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 `. +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 `. +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 key +- `error=INTERNAL_ERROR`: treat it as a temporary service issue, not a key problem diff --git a/skills/ai-video-generation/scripts/_common.sh b/skills/ai-video-generation/scripts/_common.sh new file mode 100644 index 0000000..e8656a4 --- /dev/null +++ b/skills/ai-video-generation/scripts/_common.sh @@ -0,0 +1,430 @@ +#!/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 +} diff --git a/skills/ai-video-generation/scripts/pexo-asset-get.sh b/skills/ai-video-generation/scripts/pexo-asset-get.sh new file mode 100644 index 0000000..d03ba78 --- /dev/null +++ b/skills/ai-video-generation/scripts/pexo-asset-get.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +[ -n "${BASH_VERSION:-}" ] || exec bash "$0" "$@" + +usage() { + cat <<'EOF' +Usage: + pexo-asset-get.sh + 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}' diff --git a/skills/ai-video-generation/scripts/pexo-chat.sh b/skills/ai-video-generation/scripts/pexo-chat.sh new file mode 100644 index 0000000..4cb9b0f --- /dev/null +++ b/skills/ai-video-generation/scripts/pexo-chat.sh @@ -0,0 +1,154 @@ +#!/usr/bin/env bash +[ -n "${BASH_VERSION:-}" ] || exec bash "$0" "$@" + +usage() { + cat <<'EOF' +Usage: + pexo-chat.sh [--choice ] [--timeout ] + 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: + asset_id + asset_id + asset_id + Bare asset IDs inside the message are ignored by Pexo and rejected locally. + +Options: + --choice Send the selected preview asset ID as choices.preview_id + --timeout 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 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#((a_[1-9A-HJ-NP-Za-km-z]{7,24})|([0-9A-Z]{26}))# #g' \ + | sed -E 's#((a_[1-9A-HJ-NP-Za-km-z]{7,24})|([0-9A-Z]{26}))# #g' \ + | sed -E 's#((a_[1-9A-HJ-NP-Za-km-z]{7,24})|([0-9A-Z]{26}))# #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 must be wrapped with ..., ..., or ....' >&2 + printf 'Invalid asset reference(s): %s\n' "$joined" >&2 + echo 'Example: pexo-chat.sh "Use a_xxx 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." + }' diff --git a/skills/ai-video-generation/scripts/pexo-doctor.sh b/skills/ai-video-generation/scripts/pexo-doctor.sh new file mode 100644 index 0000000..3d4ed1e --- /dev/null +++ b/skills/ai-video-generation/scripts/pexo-doctor.sh @@ -0,0 +1,200 @@ +#!/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-"' + 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 diff --git a/skills/ai-video-generation/scripts/pexo-entitlements.sh b/skills/ai-video-generation/scripts/pexo-entitlements.sh new file mode 100644 index 0000000..dca519e --- /dev/null +++ b/skills/ai-video-generation/scripts/pexo-entitlements.sh @@ -0,0 +1,63 @@ +#!/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 diff --git a/skills/ai-video-generation/scripts/pexo-project-create.sh b/skills/ai-video-generation/scripts/pexo-project-create.sh new file mode 100644 index 0000000..41e5677 --- /dev/null +++ b/skills/ai-video-generation/scripts/pexo-project-create.sh @@ -0,0 +1,84 @@ +#!/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 + 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" diff --git a/skills/ai-video-generation/scripts/pexo-project-get.sh b/skills/ai-video-generation/scripts/pexo-project-get.sh new file mode 100644 index 0000000..898bf68 --- /dev/null +++ b/skills/ai-video-generation/scripts/pexo-project-get.sh @@ -0,0 +1,177 @@ +#!/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 [--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 [--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 diff --git a/skills/ai-video-generation/scripts/pexo-project-list.sh b/skills/ai-video-generation/scripts/pexo-project-list.sh new file mode 100644 index 0000000..d11594b --- /dev/null +++ b/skills/ai-video-generation/scripts/pexo-project-list.sh @@ -0,0 +1,80 @@ +#!/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 ] [--page-size ] + pexo-project-list.sh -h | --help + +Description: + List projects for the authenticated user. + +Options: + --page Page number (default: 1) + --page-size 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}" diff --git a/skills/ai-video-generation/scripts/pexo-upload.sh b/skills/ai-video-generation/scripts/pexo-upload.sh new file mode 100644 index 0000000..eaede81 --- /dev/null +++ b/skills/ai-video-generation/scripts/pexo-upload.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +[ -n "${BASH_VERSION:-}" ] || exec bash "$0" "$@" + +usage() { + cat <<'EOF' +Usage: + pexo-upload.sh + 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" diff --git a/skills/explainer-video/README.md b/skills/explainer-video/README.md new file mode 100644 index 0000000..4a5eebd --- /dev/null +++ b/skills/explainer-video/README.md @@ -0,0 +1,38 @@ +# Explainer Video — Pexo + +Create explainer videos with narration. Pexo writes the script, generates visuals shot by shot, +adds TTS voiceover and captions, and assembles a clear, finished explainer — no prompt +engineering, no editing. + +## Install + +```bash +npx skills add https://github.com/pexoai/pexo-skills --skill explainer-video +``` + +## Configure + +Create `~/.pexo/config`: + +``` +PEXO_BASE_URL="https://pexo.ai" +PEXO_API_KEY="sk-" +``` + +Get an API key at . + +## Use + +Ask your agent, for example: + +> Explain how our budgeting app works in a 60-second video. + +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 explainer-video scenario. All creative work (scriptwriting, +model selection, prompts, music, subtitles) happens server-side. diff --git a/skills/explainer-video/SKILL.md b/skills/explainer-video/SKILL.md new file mode 100644 index 0000000..6edf620 --- /dev/null +++ b/skills/explainer-video/SKILL.md @@ -0,0 +1,107 @@ +--- +name: explainer-video +description: > + Create an explainer video with narration using Pexo. Describe your product, concept, or process + and Pexo writes the script, generates visuals shot by shot, adds TTS voiceover and captions, and + assembles a clear, finished explainer. Use for explainer/tutorial content: "explainer video", + "explain this as a video", "tutorial video", "how it works video". NOT for ads or promos (use + the video-ad skill). +homepage: https://pexo.ai +repository: https://github.com/pexoai/pexo-skills +version: "0.1.0" +requires: + env: [PEXO_API_KEY, PEXO_BASE_URL] + runtime: [curl, jq, file] +metadata: + author: pexoai +--- + +# Explainer Video — Pexo + +**Pexo:** https://pexo.ai — get an API key, watch your project render, and buy credits there. + +Produce a clear, narrated explainer video. You relay the user's topic to the hosted Pexo agent +and deliver the result; Pexo writes the script, generates the visuals shot by shot, and adds +voiceover and captions. + +## 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-" +``` +**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 ""` → save the `project_id`. +2. **Upload any files** the user gave: `pexo-upload.sh ` → save `asset_id`, + reference it inline as `asset_id` (or `` / + ``). Tags are required — a bare `asset_id` is ignored. Pexo can't crawl URLs — + download, then upload. +3. **Send the request:** `pexo-chat.sh " "`. + Copy the user's words exactly; only add asset tags. +4. **Tell the user** (their language): submitted ✓ · ~15–20 min · `https://pexo.ai/project/`. +5. **Poll:** every ≥60s run `pexo-project-get.sh ` 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 ` per option, show the URLs (A/B/C), let the user + pick, then `pexo-chat.sh "" --choice `; for a `document` + event, mention it to the user. + - **DELIVER** → `pexo-asset-get.sh `, 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 `nextActionHint` in plain terms and offer to retry. + - **RECONNECT** → `pexo-chat.sh "continue"`, tell the user the connection + dropped and you're resuming, then keep polling. + - Never call `pexo-chat.sh` during 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 ""`, 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: "Explain how our budgeting app works in a 60-second video." + +```bash +pid=$(pexo-project-create.sh "budgeting app explainer") +pexo-chat.sh "$pid" "Explain how our budgeting app works in a 60-second video." +# 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` | `""` | `project_id` | +| `pexo-upload.sh` | ` ` | `asset_id` | +| `pexo-chat.sh` | ` "" [--choice ]` | ack (async) | +| `pexo-project-get.sh` | `` | JSON: `nextAction`, `recentMessages` | +| `pexo-asset-get.sh` | ` ` | JSON with `url` | +| `pexo-doctor.sh` | — | setup diagnostic | + +Error codes and edge cases → `references/TROUBLESHOOTING.md`. diff --git a/skills/explainer-video/references/SETUP-CHECKLIST.md b/skills/explainer-video/references/SETUP-CHECKLIST.md new file mode 100644 index 0000000..c2ee83e --- /dev/null +++ b/skills/explainer-video/references/SETUP-CHECKLIST.md @@ -0,0 +1,93 @@ +# Setup Checklist + +This guide covers first-time setup and environment diagnostics for the pexo-video skill. + +## Quick Start + +### 1. Create config file + +```bash +mkdir -p ~/.pexo +cat > ~/.pexo/config << 'EOF' +PEXO_BASE_URL="https://pexo.ai" +PEXO_API_KEY="sk-" +EOF +``` + +Get 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: **7M7TJS** + 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 + +```bash +pexo-doctor.sh +``` + +This checks: +- Config file exists and is readable +- `PEXO_BASE_URL` and `PEXO_API_KEY` are set +- `curl`, `jq`, and `file` are installed +- Network connectivity to Pexo servers +- API key is valid (attempts to list projects) + +Fix any issues reported before using other scripts. + +### 3. Verify + +```bash +pexo-project-list.sh +``` + +If 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: + +```bash +# 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 file +``` + +### Network 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_proxy` environment 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 | diff --git a/skills/explainer-video/references/TROUBLESHOOTING.md b/skills/explainer-video/references/TROUBLESHOOTING.md new file mode 100644 index 0000000..42b48b4 --- /dev/null +++ b/skills/explainer-video/references/TROUBLESHOOTING.md @@ -0,0 +1,289 @@ +# 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: + +```json +{"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 script +- `error`: auth/proxy error code such as `INVALID_API_KEY` or `INTERNAL_ERROR` +- `message`: the most useful user-facing message extracted from the response +- `details`: 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 Credits +``` + + +## Auth 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_size` values 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: + +```text +Error: upload failed with HTTP +``` + +#### 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 using `pexo-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 using `pexo-upload.sh`. +- `412`: the upload session has already expired or been completed. Re-upload from scratch using `pexo-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 a `429`. +- `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.sh` exits non-zero and prints compact JSON to `stderr`, for example: + +```json +{“ok”:false,”httpCode”:200,”message”:”Insufficient credits”,”error”:”credits.insufficient_credits_err”} +``` + +Notes: + +- `pexo-chat.sh` is 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.sh` call should be followed by `pexo-project-get.sh` polling, typically every `60` seconds. + +### `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 `availableCredits` is `0`, the top-up URL is also printed to stderr. +- You generally do not need to call this script manually — `pexo-project-create.sh` and `pexo-chat.sh` call it automatically on `429`/`412` failures 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-run `pexo-asset-get.sh` to 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 set `PEXO_TMP_DIR` to a writable path. + +Notes: + +- The script downloads the file into `~/.pexo/tmp/` (or `$PEXO_TMP_DIR`) and returns both `url` and `localPath`. +- If the asset metadata exists but `downloadUrl` is absent, the script returns `localPath: null`. + +### `pexo-doctor.sh` + +- `200`: config and API key look healthy +- `401` + `INVALID_API_KEY`: API key is invalid or revoked. Update `PEXO_API_KEY` in `~/.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 Credits +``` + +If `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 `429` is likely the concurrent-project limit: the user already has an active project running. +- Re-read the `message` field 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 `. +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 `. +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 key +- `error=INTERNAL_ERROR`: treat it as a temporary service issue, not a key problem diff --git a/skills/explainer-video/scripts/_common.sh b/skills/explainer-video/scripts/_common.sh new file mode 100644 index 0000000..e8656a4 --- /dev/null +++ b/skills/explainer-video/scripts/_common.sh @@ -0,0 +1,430 @@ +#!/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 +} diff --git a/skills/explainer-video/scripts/pexo-asset-get.sh b/skills/explainer-video/scripts/pexo-asset-get.sh new file mode 100644 index 0000000..d03ba78 --- /dev/null +++ b/skills/explainer-video/scripts/pexo-asset-get.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +[ -n "${BASH_VERSION:-}" ] || exec bash "$0" "$@" + +usage() { + cat <<'EOF' +Usage: + pexo-asset-get.sh + 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}' diff --git a/skills/explainer-video/scripts/pexo-chat.sh b/skills/explainer-video/scripts/pexo-chat.sh new file mode 100644 index 0000000..4cb9b0f --- /dev/null +++ b/skills/explainer-video/scripts/pexo-chat.sh @@ -0,0 +1,154 @@ +#!/usr/bin/env bash +[ -n "${BASH_VERSION:-}" ] || exec bash "$0" "$@" + +usage() { + cat <<'EOF' +Usage: + pexo-chat.sh [--choice ] [--timeout ] + 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: + asset_id + asset_id + asset_id + Bare asset IDs inside the message are ignored by Pexo and rejected locally. + +Options: + --choice Send the selected preview asset ID as choices.preview_id + --timeout 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 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#((a_[1-9A-HJ-NP-Za-km-z]{7,24})|([0-9A-Z]{26}))# #g' \ + | sed -E 's#((a_[1-9A-HJ-NP-Za-km-z]{7,24})|([0-9A-Z]{26}))# #g' \ + | sed -E 's#((a_[1-9A-HJ-NP-Za-km-z]{7,24})|([0-9A-Z]{26}))# #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 must be wrapped with ..., ..., or ....' >&2 + printf 'Invalid asset reference(s): %s\n' "$joined" >&2 + echo 'Example: pexo-chat.sh "Use a_xxx 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." + }' diff --git a/skills/explainer-video/scripts/pexo-doctor.sh b/skills/explainer-video/scripts/pexo-doctor.sh new file mode 100644 index 0000000..3d4ed1e --- /dev/null +++ b/skills/explainer-video/scripts/pexo-doctor.sh @@ -0,0 +1,200 @@ +#!/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-"' + 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 diff --git a/skills/explainer-video/scripts/pexo-entitlements.sh b/skills/explainer-video/scripts/pexo-entitlements.sh new file mode 100644 index 0000000..dca519e --- /dev/null +++ b/skills/explainer-video/scripts/pexo-entitlements.sh @@ -0,0 +1,63 @@ +#!/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 diff --git a/skills/explainer-video/scripts/pexo-project-create.sh b/skills/explainer-video/scripts/pexo-project-create.sh new file mode 100644 index 0000000..41e5677 --- /dev/null +++ b/skills/explainer-video/scripts/pexo-project-create.sh @@ -0,0 +1,84 @@ +#!/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 + 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" diff --git a/skills/explainer-video/scripts/pexo-project-get.sh b/skills/explainer-video/scripts/pexo-project-get.sh new file mode 100644 index 0000000..898bf68 --- /dev/null +++ b/skills/explainer-video/scripts/pexo-project-get.sh @@ -0,0 +1,177 @@ +#!/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 [--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 [--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 diff --git a/skills/explainer-video/scripts/pexo-project-list.sh b/skills/explainer-video/scripts/pexo-project-list.sh new file mode 100644 index 0000000..d11594b --- /dev/null +++ b/skills/explainer-video/scripts/pexo-project-list.sh @@ -0,0 +1,80 @@ +#!/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 ] [--page-size ] + pexo-project-list.sh -h | --help + +Description: + List projects for the authenticated user. + +Options: + --page Page number (default: 1) + --page-size 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}" diff --git a/skills/explainer-video/scripts/pexo-upload.sh b/skills/explainer-video/scripts/pexo-upload.sh new file mode 100644 index 0000000..eaede81 --- /dev/null +++ b/skills/explainer-video/scripts/pexo-upload.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +[ -n "${BASH_VERSION:-}" ] || exec bash "$0" "$@" + +usage() { + cat <<'EOF' +Usage: + pexo-upload.sh + 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" diff --git a/skills/image-to-video/README.md b/skills/image-to-video/README.md new file mode 100644 index 0000000..fd97777 --- /dev/null +++ b/skills/image-to-video/README.md @@ -0,0 +1,38 @@ +# Image to Video — Pexo + +Turn a still image into a moving, publish-ready video. Upload a photo and Pexo auto-selects the +best image-to-video model, adds natural motion and camera moves, and assembles a finished clip +with music — no prompt engineering, no model picking. + +## Install + +```bash +npx skills add https://github.com/pexoai/pexo-skills --skill image-to-video +``` + +## Configure + +Create `~/.pexo/config`: + +``` +PEXO_BASE_URL="https://pexo.ai" +PEXO_API_KEY="sk-" +``` + +Get an API key at . + +## Use + +Ask your agent, for example: + +> Animate this product photo into a 10-second clip with a slow zoom. + +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 image-to-video scenario. All creative work (scriptwriting, +model selection, prompts, music, subtitles) happens server-side. diff --git a/skills/image-to-video/SKILL.md b/skills/image-to-video/SKILL.md new file mode 100644 index 0000000..cd2ccaa --- /dev/null +++ b/skills/image-to-video/SKILL.md @@ -0,0 +1,108 @@ +--- +name: image-to-video +description: > + Animate a still image into a finished, moving video with Pexo. Upload a photo and Pexo adds + natural motion, camera moves, and transitions, auto-picks the best image-to-video model + (Seedance, Kling, Wan, and more), and returns a publish-ready clip with music. Use when the + user has an IMAGE to bring to life: "image to video", "animate this photo", "make a video from + this picture", "turn my image into a video". NOT for text-only prompts (use the text-to-video + skill) or editing an existing video. +homepage: https://pexo.ai +repository: https://github.com/pexoai/pexo-skills +version: "0.1.0" +requires: + env: [PEXO_API_KEY, PEXO_BASE_URL] + runtime: [curl, jq, file] +metadata: + author: pexoai +--- + +# Image to Video — Pexo + +**Pexo:** https://pexo.ai — get an API key, watch your project render, and buy credits there. + +Bring a still image to life as a finished video. You upload the user's image, hand the request +to the hosted Pexo agent, and deliver the result — Pexo adds the motion, camera movement, music, +and assembles the final clip. + +## 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-" +``` +**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 ""` → save the `project_id`. +2. **Upload any files** the user gave: `pexo-upload.sh ` → save `asset_id`, + reference it inline as `asset_id` (or `` / + ``). Tags are required — a bare `asset_id` is ignored. Pexo can't crawl URLs — + download, then upload. +3. **Send the request:** `pexo-chat.sh " "`. + Copy the user's words exactly; only add asset tags. +4. **Tell the user** (their language): submitted ✓ · ~15–20 min · `https://pexo.ai/project/`. +5. **Poll:** every ≥60s run `pexo-project-get.sh ` 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 ` per option, show the URLs (A/B/C), let the user + pick, then `pexo-chat.sh "" --choice `; for a `document` + event, mention it to the user. + - **DELIVER** → `pexo-asset-get.sh `, 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 `nextActionHint` in plain terms and offer to retry. + - **RECONNECT** → `pexo-chat.sh "continue"`, tell the user the connection + dropped and you're resuming, then keep polling. + - Never call `pexo-chat.sh` during 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 ""`, 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: "Animate this product photo into a 10-second clip with a slow zoom." + +```bash +pid=$(pexo-project-create.sh "animate product photo") +pexo-chat.sh "$pid" "Animate this product photo into a 10-second clip with a slow zoom." +# 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` | `""` | `project_id` | +| `pexo-upload.sh` | ` ` | `asset_id` | +| `pexo-chat.sh` | ` "" [--choice ]` | ack (async) | +| `pexo-project-get.sh` | `` | JSON: `nextAction`, `recentMessages` | +| `pexo-asset-get.sh` | ` ` | JSON with `url` | +| `pexo-doctor.sh` | — | setup diagnostic | + +Error codes and edge cases → `references/TROUBLESHOOTING.md`. diff --git a/skills/image-to-video/references/SETUP-CHECKLIST.md b/skills/image-to-video/references/SETUP-CHECKLIST.md new file mode 100644 index 0000000..512c3fd --- /dev/null +++ b/skills/image-to-video/references/SETUP-CHECKLIST.md @@ -0,0 +1,93 @@ +# Setup Checklist + +This guide covers first-time setup and environment diagnostics for the pexo-video skill. + +## Quick Start + +### 1. Create config file + +```bash +mkdir -p ~/.pexo +cat > ~/.pexo/config << 'EOF' +PEXO_BASE_URL="https://pexo.ai" +PEXO_API_KEY="sk-" +EOF +``` + +Get 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: **3LTZ9V** + 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 + +```bash +pexo-doctor.sh +``` + +This checks: +- Config file exists and is readable +- `PEXO_BASE_URL` and `PEXO_API_KEY` are set +- `curl`, `jq`, and `file` are installed +- Network connectivity to Pexo servers +- API key is valid (attempts to list projects) + +Fix any issues reported before using other scripts. + +### 3. Verify + +```bash +pexo-project-list.sh +``` + +If 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: + +```bash +# 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 file +``` + +### Network 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_proxy` environment 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 | diff --git a/skills/image-to-video/references/TROUBLESHOOTING.md b/skills/image-to-video/references/TROUBLESHOOTING.md new file mode 100644 index 0000000..42b48b4 --- /dev/null +++ b/skills/image-to-video/references/TROUBLESHOOTING.md @@ -0,0 +1,289 @@ +# 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: + +```json +{"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 script +- `error`: auth/proxy error code such as `INVALID_API_KEY` or `INTERNAL_ERROR` +- `message`: the most useful user-facing message extracted from the response +- `details`: 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 Credits +``` + + +## Auth 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_size` values 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: + +```text +Error: upload failed with HTTP +``` + +#### 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 using `pexo-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 using `pexo-upload.sh`. +- `412`: the upload session has already expired or been completed. Re-upload from scratch using `pexo-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 a `429`. +- `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.sh` exits non-zero and prints compact JSON to `stderr`, for example: + +```json +{“ok”:false,”httpCode”:200,”message”:”Insufficient credits”,”error”:”credits.insufficient_credits_err”} +``` + +Notes: + +- `pexo-chat.sh` is 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.sh` call should be followed by `pexo-project-get.sh` polling, typically every `60` seconds. + +### `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 `availableCredits` is `0`, the top-up URL is also printed to stderr. +- You generally do not need to call this script manually — `pexo-project-create.sh` and `pexo-chat.sh` call it automatically on `429`/`412` failures 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-run `pexo-asset-get.sh` to 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 set `PEXO_TMP_DIR` to a writable path. + +Notes: + +- The script downloads the file into `~/.pexo/tmp/` (or `$PEXO_TMP_DIR`) and returns both `url` and `localPath`. +- If the asset metadata exists but `downloadUrl` is absent, the script returns `localPath: null`. + +### `pexo-doctor.sh` + +- `200`: config and API key look healthy +- `401` + `INVALID_API_KEY`: API key is invalid or revoked. Update `PEXO_API_KEY` in `~/.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 Credits +``` + +If `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 `429` is likely the concurrent-project limit: the user already has an active project running. +- Re-read the `message` field 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 `. +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 `. +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 key +- `error=INTERNAL_ERROR`: treat it as a temporary service issue, not a key problem diff --git a/skills/image-to-video/scripts/_common.sh b/skills/image-to-video/scripts/_common.sh new file mode 100644 index 0000000..e8656a4 --- /dev/null +++ b/skills/image-to-video/scripts/_common.sh @@ -0,0 +1,430 @@ +#!/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 +} diff --git a/skills/image-to-video/scripts/pexo-asset-get.sh b/skills/image-to-video/scripts/pexo-asset-get.sh new file mode 100644 index 0000000..d03ba78 --- /dev/null +++ b/skills/image-to-video/scripts/pexo-asset-get.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +[ -n "${BASH_VERSION:-}" ] || exec bash "$0" "$@" + +usage() { + cat <<'EOF' +Usage: + pexo-asset-get.sh + 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}' diff --git a/skills/image-to-video/scripts/pexo-chat.sh b/skills/image-to-video/scripts/pexo-chat.sh new file mode 100644 index 0000000..4cb9b0f --- /dev/null +++ b/skills/image-to-video/scripts/pexo-chat.sh @@ -0,0 +1,154 @@ +#!/usr/bin/env bash +[ -n "${BASH_VERSION:-}" ] || exec bash "$0" "$@" + +usage() { + cat <<'EOF' +Usage: + pexo-chat.sh [--choice ] [--timeout ] + 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: + asset_id + asset_id + asset_id + Bare asset IDs inside the message are ignored by Pexo and rejected locally. + +Options: + --choice Send the selected preview asset ID as choices.preview_id + --timeout 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 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#((a_[1-9A-HJ-NP-Za-km-z]{7,24})|([0-9A-Z]{26}))# #g' \ + | sed -E 's#((a_[1-9A-HJ-NP-Za-km-z]{7,24})|([0-9A-Z]{26}))# #g' \ + | sed -E 's#((a_[1-9A-HJ-NP-Za-km-z]{7,24})|([0-9A-Z]{26}))# #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 must be wrapped with ..., ..., or ....' >&2 + printf 'Invalid asset reference(s): %s\n' "$joined" >&2 + echo 'Example: pexo-chat.sh "Use a_xxx 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." + }' diff --git a/skills/image-to-video/scripts/pexo-doctor.sh b/skills/image-to-video/scripts/pexo-doctor.sh new file mode 100644 index 0000000..3d4ed1e --- /dev/null +++ b/skills/image-to-video/scripts/pexo-doctor.sh @@ -0,0 +1,200 @@ +#!/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-"' + 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 diff --git a/skills/image-to-video/scripts/pexo-entitlements.sh b/skills/image-to-video/scripts/pexo-entitlements.sh new file mode 100644 index 0000000..dca519e --- /dev/null +++ b/skills/image-to-video/scripts/pexo-entitlements.sh @@ -0,0 +1,63 @@ +#!/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 diff --git a/skills/image-to-video/scripts/pexo-project-create.sh b/skills/image-to-video/scripts/pexo-project-create.sh new file mode 100644 index 0000000..41e5677 --- /dev/null +++ b/skills/image-to-video/scripts/pexo-project-create.sh @@ -0,0 +1,84 @@ +#!/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 + 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" diff --git a/skills/image-to-video/scripts/pexo-project-get.sh b/skills/image-to-video/scripts/pexo-project-get.sh new file mode 100644 index 0000000..898bf68 --- /dev/null +++ b/skills/image-to-video/scripts/pexo-project-get.sh @@ -0,0 +1,177 @@ +#!/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 [--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 [--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 diff --git a/skills/image-to-video/scripts/pexo-project-list.sh b/skills/image-to-video/scripts/pexo-project-list.sh new file mode 100644 index 0000000..d11594b --- /dev/null +++ b/skills/image-to-video/scripts/pexo-project-list.sh @@ -0,0 +1,80 @@ +#!/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 ] [--page-size ] + pexo-project-list.sh -h | --help + +Description: + List projects for the authenticated user. + +Options: + --page Page number (default: 1) + --page-size 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}" diff --git a/skills/image-to-video/scripts/pexo-upload.sh b/skills/image-to-video/scripts/pexo-upload.sh new file mode 100644 index 0000000..eaede81 --- /dev/null +++ b/skills/image-to-video/scripts/pexo-upload.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +[ -n "${BASH_VERSION:-}" ] || exec bash "$0" "$@" + +usage() { + cat <<'EOF' +Usage: + pexo-upload.sh + 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" diff --git a/skills/make-a-video/README.md b/skills/make-a-video/README.md new file mode 100644 index 0000000..8247b68 --- /dev/null +++ b/skills/make-a-video/README.md @@ -0,0 +1,38 @@ +# Make a Video — Pexo + +Describe a video in plain words and get a finished one back. Pexo writes the script, picks the +right model per shot, generates everything, and assembles a publish-ready video with music and +subtitles — no prompt engineering, no editing. + +## Install + +```bash +npx skills add https://github.com/pexoai/pexo-skills --skill make-a-video +``` + +## Configure + +Create `~/.pexo/config`: + +``` +PEXO_BASE_URL="https://pexo.ai" +PEXO_API_KEY="sk-" +``` + +Get an API key at . + +## 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 make-a-video scenario. All creative work (scriptwriting, +model selection, prompts, music, subtitles) happens server-side. diff --git a/skills/make-a-video/SKILL.md b/skills/make-a-video/SKILL.md new file mode 100644 index 0000000..137238a --- /dev/null +++ b/skills/make-a-video/SKILL.md @@ -0,0 +1,107 @@ +--- +name: make-a-video +description: > + Make a complete video from a simple idea with Pexo. Describe what you want in plain words and + Pexo handles everything — script, shots, model selection, music, subtitles, and final editing — + and returns a publish-ready video. Use when the user just wants to make a video: "make a video", + "make me a video about X", "create a video", "I need a video for ...". NOT for editing a video + they already have. +homepage: https://pexo.ai +repository: https://github.com/pexoai/pexo-skills +version: "0.1.0" +requires: + env: [PEXO_API_KEY, PEXO_BASE_URL] + runtime: [curl, jq, file] +metadata: + author: pexoai +--- + +# Make a Video — Pexo + +**Pexo:** https://pexo.ai — get an API key, watch your project render, and buy credits there. + +Make a finished video from a plain-language idea. You pass the user's request to the hosted Pexo +agent verbatim and deliver the result — Pexo does the script, shot selection, models, music, and +editing end to end. + +## 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-" +``` +**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 ""` → save the `project_id`. +2. **Upload any files** the user gave: `pexo-upload.sh ` → save `asset_id`, + reference it inline as `asset_id` (or `` / + ``). Tags are required — a bare `asset_id` is ignored. Pexo can't crawl URLs — + download, then upload. +3. **Send the request:** `pexo-chat.sh " "`. + Copy the user's words exactly; only add asset tags. +4. **Tell the user** (their language): submitted ✓ · ~15–20 min · `https://pexo.ai/project/`. +5. **Poll:** every ≥60s run `pexo-project-get.sh ` 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 ` per option, show the URLs (A/B/C), let the user + pick, then `pexo-chat.sh "" --choice `; for a `document` + event, mention it to the user. + - **DELIVER** → `pexo-asset-get.sh `, 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 `nextActionHint` in plain terms and offer to retry. + - **RECONNECT** → `pexo-chat.sh "continue"`, tell the user the connection + dropped and you're resuming, then keep polling. + - Never call `pexo-chat.sh` during 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 ""`, 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." + +```bash +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` | `""` | `project_id` | +| `pexo-upload.sh` | ` ` | `asset_id` | +| `pexo-chat.sh` | ` "" [--choice ]` | ack (async) | +| `pexo-project-get.sh` | `` | JSON: `nextAction`, `recentMessages` | +| `pexo-asset-get.sh` | ` ` | JSON with `url` | +| `pexo-doctor.sh` | — | setup diagnostic | + +Error codes and edge cases → `references/TROUBLESHOOTING.md`. diff --git a/skills/make-a-video/references/SETUP-CHECKLIST.md b/skills/make-a-video/references/SETUP-CHECKLIST.md new file mode 100644 index 0000000..633fe61 --- /dev/null +++ b/skills/make-a-video/references/SETUP-CHECKLIST.md @@ -0,0 +1,93 @@ +# Setup Checklist + +This guide covers first-time setup and environment diagnostics for the pexo-video skill. + +## Quick Start + +### 1. Create config file + +```bash +mkdir -p ~/.pexo +cat > ~/.pexo/config << 'EOF' +PEXO_BASE_URL="https://pexo.ai" +PEXO_API_KEY="sk-" +EOF +``` + +Get 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: **4WMZ4P** + 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 + +```bash +pexo-doctor.sh +``` + +This checks: +- Config file exists and is readable +- `PEXO_BASE_URL` and `PEXO_API_KEY` are set +- `curl`, `jq`, and `file` are installed +- Network connectivity to Pexo servers +- API key is valid (attempts to list projects) + +Fix any issues reported before using other scripts. + +### 3. Verify + +```bash +pexo-project-list.sh +``` + +If 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: + +```bash +# 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 file +``` + +### Network 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_proxy` environment 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 | diff --git a/skills/make-a-video/references/TROUBLESHOOTING.md b/skills/make-a-video/references/TROUBLESHOOTING.md new file mode 100644 index 0000000..42b48b4 --- /dev/null +++ b/skills/make-a-video/references/TROUBLESHOOTING.md @@ -0,0 +1,289 @@ +# 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: + +```json +{"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 script +- `error`: auth/proxy error code such as `INVALID_API_KEY` or `INTERNAL_ERROR` +- `message`: the most useful user-facing message extracted from the response +- `details`: 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 Credits +``` + + +## Auth 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_size` values 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: + +```text +Error: upload failed with HTTP +``` + +#### 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 using `pexo-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 using `pexo-upload.sh`. +- `412`: the upload session has already expired or been completed. Re-upload from scratch using `pexo-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 a `429`. +- `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.sh` exits non-zero and prints compact JSON to `stderr`, for example: + +```json +{“ok”:false,”httpCode”:200,”message”:”Insufficient credits”,”error”:”credits.insufficient_credits_err”} +``` + +Notes: + +- `pexo-chat.sh` is 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.sh` call should be followed by `pexo-project-get.sh` polling, typically every `60` seconds. + +### `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 `availableCredits` is `0`, the top-up URL is also printed to stderr. +- You generally do not need to call this script manually — `pexo-project-create.sh` and `pexo-chat.sh` call it automatically on `429`/`412` failures 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-run `pexo-asset-get.sh` to 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 set `PEXO_TMP_DIR` to a writable path. + +Notes: + +- The script downloads the file into `~/.pexo/tmp/` (or `$PEXO_TMP_DIR`) and returns both `url` and `localPath`. +- If the asset metadata exists but `downloadUrl` is absent, the script returns `localPath: null`. + +### `pexo-doctor.sh` + +- `200`: config and API key look healthy +- `401` + `INVALID_API_KEY`: API key is invalid or revoked. Update `PEXO_API_KEY` in `~/.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 Credits +``` + +If `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 `429` is likely the concurrent-project limit: the user already has an active project running. +- Re-read the `message` field 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 `. +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 `. +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 key +- `error=INTERNAL_ERROR`: treat it as a temporary service issue, not a key problem diff --git a/skills/make-a-video/scripts/_common.sh b/skills/make-a-video/scripts/_common.sh new file mode 100644 index 0000000..e8656a4 --- /dev/null +++ b/skills/make-a-video/scripts/_common.sh @@ -0,0 +1,430 @@ +#!/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 +} diff --git a/skills/make-a-video/scripts/pexo-asset-get.sh b/skills/make-a-video/scripts/pexo-asset-get.sh new file mode 100644 index 0000000..d03ba78 --- /dev/null +++ b/skills/make-a-video/scripts/pexo-asset-get.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +[ -n "${BASH_VERSION:-}" ] || exec bash "$0" "$@" + +usage() { + cat <<'EOF' +Usage: + pexo-asset-get.sh + 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}' diff --git a/skills/make-a-video/scripts/pexo-chat.sh b/skills/make-a-video/scripts/pexo-chat.sh new file mode 100644 index 0000000..4cb9b0f --- /dev/null +++ b/skills/make-a-video/scripts/pexo-chat.sh @@ -0,0 +1,154 @@ +#!/usr/bin/env bash +[ -n "${BASH_VERSION:-}" ] || exec bash "$0" "$@" + +usage() { + cat <<'EOF' +Usage: + pexo-chat.sh [--choice ] [--timeout ] + 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: + asset_id + asset_id + asset_id + Bare asset IDs inside the message are ignored by Pexo and rejected locally. + +Options: + --choice Send the selected preview asset ID as choices.preview_id + --timeout 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 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#((a_[1-9A-HJ-NP-Za-km-z]{7,24})|([0-9A-Z]{26}))# #g' \ + | sed -E 's#((a_[1-9A-HJ-NP-Za-km-z]{7,24})|([0-9A-Z]{26}))# #g' \ + | sed -E 's#((a_[1-9A-HJ-NP-Za-km-z]{7,24})|([0-9A-Z]{26}))# #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 must be wrapped with ..., ..., or ....' >&2 + printf 'Invalid asset reference(s): %s\n' "$joined" >&2 + echo 'Example: pexo-chat.sh "Use a_xxx 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." + }' diff --git a/skills/make-a-video/scripts/pexo-doctor.sh b/skills/make-a-video/scripts/pexo-doctor.sh new file mode 100644 index 0000000..3d4ed1e --- /dev/null +++ b/skills/make-a-video/scripts/pexo-doctor.sh @@ -0,0 +1,200 @@ +#!/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-"' + 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 diff --git a/skills/make-a-video/scripts/pexo-entitlements.sh b/skills/make-a-video/scripts/pexo-entitlements.sh new file mode 100644 index 0000000..dca519e --- /dev/null +++ b/skills/make-a-video/scripts/pexo-entitlements.sh @@ -0,0 +1,63 @@ +#!/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 diff --git a/skills/make-a-video/scripts/pexo-project-create.sh b/skills/make-a-video/scripts/pexo-project-create.sh new file mode 100644 index 0000000..41e5677 --- /dev/null +++ b/skills/make-a-video/scripts/pexo-project-create.sh @@ -0,0 +1,84 @@ +#!/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 + 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" diff --git a/skills/make-a-video/scripts/pexo-project-get.sh b/skills/make-a-video/scripts/pexo-project-get.sh new file mode 100644 index 0000000..898bf68 --- /dev/null +++ b/skills/make-a-video/scripts/pexo-project-get.sh @@ -0,0 +1,177 @@ +#!/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 [--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 [--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 diff --git a/skills/make-a-video/scripts/pexo-project-list.sh b/skills/make-a-video/scripts/pexo-project-list.sh new file mode 100644 index 0000000..d11594b --- /dev/null +++ b/skills/make-a-video/scripts/pexo-project-list.sh @@ -0,0 +1,80 @@ +#!/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 ] [--page-size ] + pexo-project-list.sh -h | --help + +Description: + List projects for the authenticated user. + +Options: + --page Page number (default: 1) + --page-size 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}" diff --git a/skills/make-a-video/scripts/pexo-upload.sh b/skills/make-a-video/scripts/pexo-upload.sh new file mode 100644 index 0000000..eaede81 --- /dev/null +++ b/skills/make-a-video/scripts/pexo-upload.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +[ -n "${BASH_VERSION:-}" ] || exec bash "$0" "$@" + +usage() { + cat <<'EOF' +Usage: + pexo-upload.sh + 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" diff --git a/skills/product-video/README.md b/skills/product-video/README.md new file mode 100644 index 0000000..5e75f84 --- /dev/null +++ b/skills/product-video/README.md @@ -0,0 +1,38 @@ +# Product Video — Pexo + +Turn product photos into a publish-ready product video. Upload your images and Pexo composes the +shots, adds motion and music, and returns a finished clip for your listing, ads, or socials — no +editing, no model picking. + +## Install + +```bash +npx skills add https://github.com/pexoai/pexo-skills --skill product-video +``` + +## Configure + +Create `~/.pexo/config`: + +``` +PEXO_BASE_URL="https://pexo.ai" +PEXO_API_KEY="sk-" +``` + +Get an API key at . + +## Use + +Ask your agent, for example: + +> Make a 20-second product video from these three sneaker photos. + +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 product-video scenario. All creative work (scriptwriting, +model selection, prompts, music, subtitles) happens server-side. diff --git a/skills/product-video/SKILL.md b/skills/product-video/SKILL.md new file mode 100644 index 0000000..5f2748a --- /dev/null +++ b/skills/product-video/SKILL.md @@ -0,0 +1,107 @@ +--- +name: product-video +description: > + Turn product photos or a store URL into a polished product video with Pexo. Upload your product + images (or download from a link, then upload) and Pexo composes the shots, adds motion, music, + and transitions, and returns a publish-ready clip for your listing, ads, or socials. Use for + e-commerce/product clips: "product video", "video for my product", "ecommerce video", "video + from product photos". NOT for explainer or non-product content. +homepage: https://pexo.ai +repository: https://github.com/pexoai/pexo-skills +version: "0.1.0" +requires: + env: [PEXO_API_KEY, PEXO_BASE_URL] + runtime: [curl, jq, file] +metadata: + author: pexoai +--- + +# Product Video — Pexo + +**Pexo:** https://pexo.ai — get an API key, watch your project render, and buy credits there. + +Turn a product's photos into a polished, publish-ready video. You upload the user's product +images and relay the request to the hosted Pexo agent, then deliver the result — Pexo composes +the shots, motion, and music. + +## 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-" +``` +**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 ""` → save the `project_id`. +2. **Upload any files** the user gave: `pexo-upload.sh ` → save `asset_id`, + reference it inline as `asset_id` (or `` / + ``). Tags are required — a bare `asset_id` is ignored. Pexo can't crawl URLs — + download, then upload. +3. **Send the request:** `pexo-chat.sh " "`. + Copy the user's words exactly; only add asset tags. +4. **Tell the user** (their language): submitted ✓ · ~15–20 min · `https://pexo.ai/project/`. +5. **Poll:** every ≥60s run `pexo-project-get.sh ` 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 ` per option, show the URLs (A/B/C), let the user + pick, then `pexo-chat.sh "" --choice `; for a `document` + event, mention it to the user. + - **DELIVER** → `pexo-asset-get.sh `, 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 `nextActionHint` in plain terms and offer to retry. + - **RECONNECT** → `pexo-chat.sh "continue"`, tell the user the connection + dropped and you're resuming, then keep polling. + - Never call `pexo-chat.sh` during 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 ""`, 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 20-second product video from these three sneaker photos." + +```bash +pid=$(pexo-project-create.sh "sneaker product video") +pexo-chat.sh "$pid" "Make a 20-second product video from these three sneaker photos." +# 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` | `""` | `project_id` | +| `pexo-upload.sh` | ` ` | `asset_id` | +| `pexo-chat.sh` | ` "" [--choice ]` | ack (async) | +| `pexo-project-get.sh` | `` | JSON: `nextAction`, `recentMessages` | +| `pexo-asset-get.sh` | ` ` | JSON with `url` | +| `pexo-doctor.sh` | — | setup diagnostic | + +Error codes and edge cases → `references/TROUBLESHOOTING.md`. diff --git a/skills/product-video/references/SETUP-CHECKLIST.md b/skills/product-video/references/SETUP-CHECKLIST.md new file mode 100644 index 0000000..95b3034 --- /dev/null +++ b/skills/product-video/references/SETUP-CHECKLIST.md @@ -0,0 +1,93 @@ +# Setup Checklist + +This guide covers first-time setup and environment diagnostics for the pexo-video skill. + +## Quick Start + +### 1. Create config file + +```bash +mkdir -p ~/.pexo +cat > ~/.pexo/config << 'EOF' +PEXO_BASE_URL="https://pexo.ai" +PEXO_API_KEY="sk-" +EOF +``` + +Get 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: **5HWGF5** + 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 + +```bash +pexo-doctor.sh +``` + +This checks: +- Config file exists and is readable +- `PEXO_BASE_URL` and `PEXO_API_KEY` are set +- `curl`, `jq`, and `file` are installed +- Network connectivity to Pexo servers +- API key is valid (attempts to list projects) + +Fix any issues reported before using other scripts. + +### 3. Verify + +```bash +pexo-project-list.sh +``` + +If 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: + +```bash +# 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 file +``` + +### Network 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_proxy` environment 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 | diff --git a/skills/product-video/references/TROUBLESHOOTING.md b/skills/product-video/references/TROUBLESHOOTING.md new file mode 100644 index 0000000..42b48b4 --- /dev/null +++ b/skills/product-video/references/TROUBLESHOOTING.md @@ -0,0 +1,289 @@ +# 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: + +```json +{"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 script +- `error`: auth/proxy error code such as `INVALID_API_KEY` or `INTERNAL_ERROR` +- `message`: the most useful user-facing message extracted from the response +- `details`: 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 Credits +``` + + +## Auth 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_size` values 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: + +```text +Error: upload failed with HTTP +``` + +#### 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 using `pexo-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 using `pexo-upload.sh`. +- `412`: the upload session has already expired or been completed. Re-upload from scratch using `pexo-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 a `429`. +- `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.sh` exits non-zero and prints compact JSON to `stderr`, for example: + +```json +{“ok”:false,”httpCode”:200,”message”:”Insufficient credits”,”error”:”credits.insufficient_credits_err”} +``` + +Notes: + +- `pexo-chat.sh` is 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.sh` call should be followed by `pexo-project-get.sh` polling, typically every `60` seconds. + +### `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 `availableCredits` is `0`, the top-up URL is also printed to stderr. +- You generally do not need to call this script manually — `pexo-project-create.sh` and `pexo-chat.sh` call it automatically on `429`/`412` failures 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-run `pexo-asset-get.sh` to 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 set `PEXO_TMP_DIR` to a writable path. + +Notes: + +- The script downloads the file into `~/.pexo/tmp/` (or `$PEXO_TMP_DIR`) and returns both `url` and `localPath`. +- If the asset metadata exists but `downloadUrl` is absent, the script returns `localPath: null`. + +### `pexo-doctor.sh` + +- `200`: config and API key look healthy +- `401` + `INVALID_API_KEY`: API key is invalid or revoked. Update `PEXO_API_KEY` in `~/.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 Credits +``` + +If `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 `429` is likely the concurrent-project limit: the user already has an active project running. +- Re-read the `message` field 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 `. +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 `. +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 key +- `error=INTERNAL_ERROR`: treat it as a temporary service issue, not a key problem diff --git a/skills/product-video/scripts/_common.sh b/skills/product-video/scripts/_common.sh new file mode 100644 index 0000000..e8656a4 --- /dev/null +++ b/skills/product-video/scripts/_common.sh @@ -0,0 +1,430 @@ +#!/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 +} diff --git a/skills/product-video/scripts/pexo-asset-get.sh b/skills/product-video/scripts/pexo-asset-get.sh new file mode 100644 index 0000000..d03ba78 --- /dev/null +++ b/skills/product-video/scripts/pexo-asset-get.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +[ -n "${BASH_VERSION:-}" ] || exec bash "$0" "$@" + +usage() { + cat <<'EOF' +Usage: + pexo-asset-get.sh + 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}' diff --git a/skills/product-video/scripts/pexo-chat.sh b/skills/product-video/scripts/pexo-chat.sh new file mode 100644 index 0000000..4cb9b0f --- /dev/null +++ b/skills/product-video/scripts/pexo-chat.sh @@ -0,0 +1,154 @@ +#!/usr/bin/env bash +[ -n "${BASH_VERSION:-}" ] || exec bash "$0" "$@" + +usage() { + cat <<'EOF' +Usage: + pexo-chat.sh [--choice ] [--timeout ] + 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: + asset_id + asset_id + asset_id + Bare asset IDs inside the message are ignored by Pexo and rejected locally. + +Options: + --choice Send the selected preview asset ID as choices.preview_id + --timeout 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 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#((a_[1-9A-HJ-NP-Za-km-z]{7,24})|([0-9A-Z]{26}))# #g' \ + | sed -E 's#((a_[1-9A-HJ-NP-Za-km-z]{7,24})|([0-9A-Z]{26}))# #g' \ + | sed -E 's#((a_[1-9A-HJ-NP-Za-km-z]{7,24})|([0-9A-Z]{26}))# #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 must be wrapped with ..., ..., or ....' >&2 + printf 'Invalid asset reference(s): %s\n' "$joined" >&2 + echo 'Example: pexo-chat.sh "Use a_xxx 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." + }' diff --git a/skills/product-video/scripts/pexo-doctor.sh b/skills/product-video/scripts/pexo-doctor.sh new file mode 100644 index 0000000..3d4ed1e --- /dev/null +++ b/skills/product-video/scripts/pexo-doctor.sh @@ -0,0 +1,200 @@ +#!/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-"' + 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 diff --git a/skills/product-video/scripts/pexo-entitlements.sh b/skills/product-video/scripts/pexo-entitlements.sh new file mode 100644 index 0000000..dca519e --- /dev/null +++ b/skills/product-video/scripts/pexo-entitlements.sh @@ -0,0 +1,63 @@ +#!/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 diff --git a/skills/product-video/scripts/pexo-project-create.sh b/skills/product-video/scripts/pexo-project-create.sh new file mode 100644 index 0000000..41e5677 --- /dev/null +++ b/skills/product-video/scripts/pexo-project-create.sh @@ -0,0 +1,84 @@ +#!/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 + 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" diff --git a/skills/product-video/scripts/pexo-project-get.sh b/skills/product-video/scripts/pexo-project-get.sh new file mode 100644 index 0000000..898bf68 --- /dev/null +++ b/skills/product-video/scripts/pexo-project-get.sh @@ -0,0 +1,177 @@ +#!/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 [--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 [--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 diff --git a/skills/product-video/scripts/pexo-project-list.sh b/skills/product-video/scripts/pexo-project-list.sh new file mode 100644 index 0000000..d11594b --- /dev/null +++ b/skills/product-video/scripts/pexo-project-list.sh @@ -0,0 +1,80 @@ +#!/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 ] [--page-size ] + pexo-project-list.sh -h | --help + +Description: + List projects for the authenticated user. + +Options: + --page Page number (default: 1) + --page-size 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}" diff --git a/skills/product-video/scripts/pexo-upload.sh b/skills/product-video/scripts/pexo-upload.sh new file mode 100644 index 0000000..eaede81 --- /dev/null +++ b/skills/product-video/scripts/pexo-upload.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +[ -n "${BASH_VERSION:-}" ] || exec bash "$0" "$@" + +usage() { + cat <<'EOF' +Usage: + pexo-upload.sh + 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" diff --git a/skills/tiktok-video-ad/README.md b/skills/tiktok-video-ad/README.md new file mode 100644 index 0000000..c7811fe --- /dev/null +++ b/skills/tiktok-video-ad/README.md @@ -0,0 +1,38 @@ +# TikTok Video Ad — Pexo + +Make native TikTok video ads — vertical 9:16, sound-on, hook-first. Pexo writes the script in +TikTok's voice, sequences fast-paced shots, and delivers a ready-to-post ad with music — no +editing. + +## Install + +```bash +npx skills add https://github.com/pexoai/pexo-skills --skill tiktok-video-ad +``` + +## Configure + +Create `~/.pexo/config`: + +``` +PEXO_BASE_URL="https://pexo.ai" +PEXO_API_KEY="sk-" +``` + +Get an API key at . + +## Use + +Ask your agent, for example: + +> Make a TikTok ad for our skincare serum. + +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 tiktok-video-ad scenario. All creative work (scriptwriting, +model selection, prompts, music, subtitles) happens server-side. diff --git a/skills/tiktok-video-ad/SKILL.md b/skills/tiktok-video-ad/SKILL.md new file mode 100644 index 0000000..0ab438d --- /dev/null +++ b/skills/tiktok-video-ad/SKILL.md @@ -0,0 +1,107 @@ +--- +name: tiktok-video-ad +description: > + Create native TikTok video ads with Pexo — vertical 9:16, sound-on, hook-first. Give it your + product and Pexo writes the script in TikTok's voice, sequences fast-paced shots, picks the + models, and delivers a ready-to-post ad with trending-style music. Use for TikTok ads: "tiktok + ad", "tiktok video ad", "ad for tiktok", "vertical video ad". NOT for landscape or long-form ads + (use the video-ad skill). +homepage: https://pexo.ai +repository: https://github.com/pexoai/pexo-skills +version: "0.1.0" +requires: + env: [PEXO_API_KEY, PEXO_BASE_URL] + runtime: [curl, jq, file] +metadata: + author: pexoai +--- + +# TikTok Video Ad — Pexo + +**Pexo:** https://pexo.ai — get an API key, watch your project render, and buy credits there. + +Produce a native, ready-to-post TikTok ad — vertical and hook-first. You relay the user's +product or brief to the hosted Pexo agent and deliver the result; Pexo writes the TikTok-style +script, sequences the shots, and sets the pacing and sound. + +## 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-" +``` +**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 ""` → save the `project_id`. +2. **Upload any files** the user gave: `pexo-upload.sh ` → save `asset_id`, + reference it inline as `asset_id` (or `` / + ``). Tags are required — a bare `asset_id` is ignored. Pexo can't crawl URLs — + download, then upload. +3. **Send the request:** `pexo-chat.sh " "`. + Copy the user's words exactly; only add asset tags. +4. **Tell the user** (their language): submitted ✓ · ~15–20 min · `https://pexo.ai/project/`. +5. **Poll:** every ≥60s run `pexo-project-get.sh ` 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 ` per option, show the URLs (A/B/C), let the user + pick, then `pexo-chat.sh "" --choice `; for a `document` + event, mention it to the user. + - **DELIVER** → `pexo-asset-get.sh `, 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 `nextActionHint` in plain terms and offer to retry. + - **RECONNECT** → `pexo-chat.sh "continue"`, tell the user the connection + dropped and you're resuming, then keep polling. + - Never call `pexo-chat.sh` during 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 ""`, 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 TikTok ad for our skincare serum." + +```bash +pid=$(pexo-project-create.sh "skincare serum TikTok ad") +pexo-chat.sh "$pid" "Make a TikTok ad for our skincare serum." +# 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` | `""` | `project_id` | +| `pexo-upload.sh` | ` ` | `asset_id` | +| `pexo-chat.sh` | ` "" [--choice ]` | ack (async) | +| `pexo-project-get.sh` | `` | JSON: `nextAction`, `recentMessages` | +| `pexo-asset-get.sh` | ` ` | JSON with `url` | +| `pexo-doctor.sh` | — | setup diagnostic | + +Error codes and edge cases → `references/TROUBLESHOOTING.md`. diff --git a/skills/tiktok-video-ad/references/SETUP-CHECKLIST.md b/skills/tiktok-video-ad/references/SETUP-CHECKLIST.md new file mode 100644 index 0000000..f8f6501 --- /dev/null +++ b/skills/tiktok-video-ad/references/SETUP-CHECKLIST.md @@ -0,0 +1,93 @@ +# Setup Checklist + +This guide covers first-time setup and environment diagnostics for the pexo-video skill. + +## Quick Start + +### 1. Create config file + +```bash +mkdir -p ~/.pexo +cat > ~/.pexo/config << 'EOF' +PEXO_BASE_URL="https://pexo.ai" +PEXO_API_KEY="sk-" +EOF +``` + +Get 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: **6CGFKM** + 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 + +```bash +pexo-doctor.sh +``` + +This checks: +- Config file exists and is readable +- `PEXO_BASE_URL` and `PEXO_API_KEY` are set +- `curl`, `jq`, and `file` are installed +- Network connectivity to Pexo servers +- API key is valid (attempts to list projects) + +Fix any issues reported before using other scripts. + +### 3. Verify + +```bash +pexo-project-list.sh +``` + +If 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: + +```bash +# 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 file +``` + +### Network 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_proxy` environment 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 | diff --git a/skills/tiktok-video-ad/references/TROUBLESHOOTING.md b/skills/tiktok-video-ad/references/TROUBLESHOOTING.md new file mode 100644 index 0000000..42b48b4 --- /dev/null +++ b/skills/tiktok-video-ad/references/TROUBLESHOOTING.md @@ -0,0 +1,289 @@ +# 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: + +```json +{"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 script +- `error`: auth/proxy error code such as `INVALID_API_KEY` or `INTERNAL_ERROR` +- `message`: the most useful user-facing message extracted from the response +- `details`: 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 Credits +``` + + +## Auth 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_size` values 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: + +```text +Error: upload failed with HTTP +``` + +#### 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 using `pexo-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 using `pexo-upload.sh`. +- `412`: the upload session has already expired or been completed. Re-upload from scratch using `pexo-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 a `429`. +- `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.sh` exits non-zero and prints compact JSON to `stderr`, for example: + +```json +{“ok”:false,”httpCode”:200,”message”:”Insufficient credits”,”error”:”credits.insufficient_credits_err”} +``` + +Notes: + +- `pexo-chat.sh` is 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.sh` call should be followed by `pexo-project-get.sh` polling, typically every `60` seconds. + +### `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 `availableCredits` is `0`, the top-up URL is also printed to stderr. +- You generally do not need to call this script manually — `pexo-project-create.sh` and `pexo-chat.sh` call it automatically on `429`/`412` failures 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-run `pexo-asset-get.sh` to 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 set `PEXO_TMP_DIR` to a writable path. + +Notes: + +- The script downloads the file into `~/.pexo/tmp/` (or `$PEXO_TMP_DIR`) and returns both `url` and `localPath`. +- If the asset metadata exists but `downloadUrl` is absent, the script returns `localPath: null`. + +### `pexo-doctor.sh` + +- `200`: config and API key look healthy +- `401` + `INVALID_API_KEY`: API key is invalid or revoked. Update `PEXO_API_KEY` in `~/.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 Credits +``` + +If `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 `429` is likely the concurrent-project limit: the user already has an active project running. +- Re-read the `message` field 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 `. +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 `. +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 key +- `error=INTERNAL_ERROR`: treat it as a temporary service issue, not a key problem diff --git a/skills/tiktok-video-ad/scripts/_common.sh b/skills/tiktok-video-ad/scripts/_common.sh new file mode 100644 index 0000000..e8656a4 --- /dev/null +++ b/skills/tiktok-video-ad/scripts/_common.sh @@ -0,0 +1,430 @@ +#!/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 +} diff --git a/skills/tiktok-video-ad/scripts/pexo-asset-get.sh b/skills/tiktok-video-ad/scripts/pexo-asset-get.sh new file mode 100644 index 0000000..d03ba78 --- /dev/null +++ b/skills/tiktok-video-ad/scripts/pexo-asset-get.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +[ -n "${BASH_VERSION:-}" ] || exec bash "$0" "$@" + +usage() { + cat <<'EOF' +Usage: + pexo-asset-get.sh + 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}' diff --git a/skills/tiktok-video-ad/scripts/pexo-chat.sh b/skills/tiktok-video-ad/scripts/pexo-chat.sh new file mode 100644 index 0000000..4cb9b0f --- /dev/null +++ b/skills/tiktok-video-ad/scripts/pexo-chat.sh @@ -0,0 +1,154 @@ +#!/usr/bin/env bash +[ -n "${BASH_VERSION:-}" ] || exec bash "$0" "$@" + +usage() { + cat <<'EOF' +Usage: + pexo-chat.sh [--choice ] [--timeout ] + 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: + asset_id + asset_id + asset_id + Bare asset IDs inside the message are ignored by Pexo and rejected locally. + +Options: + --choice Send the selected preview asset ID as choices.preview_id + --timeout 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 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#((a_[1-9A-HJ-NP-Za-km-z]{7,24})|([0-9A-Z]{26}))# #g' \ + | sed -E 's#((a_[1-9A-HJ-NP-Za-km-z]{7,24})|([0-9A-Z]{26}))# #g' \ + | sed -E 's#((a_[1-9A-HJ-NP-Za-km-z]{7,24})|([0-9A-Z]{26}))# #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 must be wrapped with ..., ..., or ....' >&2 + printf 'Invalid asset reference(s): %s\n' "$joined" >&2 + echo 'Example: pexo-chat.sh "Use a_xxx 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." + }' diff --git a/skills/tiktok-video-ad/scripts/pexo-doctor.sh b/skills/tiktok-video-ad/scripts/pexo-doctor.sh new file mode 100644 index 0000000..3d4ed1e --- /dev/null +++ b/skills/tiktok-video-ad/scripts/pexo-doctor.sh @@ -0,0 +1,200 @@ +#!/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-"' + 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 diff --git a/skills/tiktok-video-ad/scripts/pexo-entitlements.sh b/skills/tiktok-video-ad/scripts/pexo-entitlements.sh new file mode 100644 index 0000000..dca519e --- /dev/null +++ b/skills/tiktok-video-ad/scripts/pexo-entitlements.sh @@ -0,0 +1,63 @@ +#!/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 diff --git a/skills/tiktok-video-ad/scripts/pexo-project-create.sh b/skills/tiktok-video-ad/scripts/pexo-project-create.sh new file mode 100644 index 0000000..41e5677 --- /dev/null +++ b/skills/tiktok-video-ad/scripts/pexo-project-create.sh @@ -0,0 +1,84 @@ +#!/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 + 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" diff --git a/skills/tiktok-video-ad/scripts/pexo-project-get.sh b/skills/tiktok-video-ad/scripts/pexo-project-get.sh new file mode 100644 index 0000000..898bf68 --- /dev/null +++ b/skills/tiktok-video-ad/scripts/pexo-project-get.sh @@ -0,0 +1,177 @@ +#!/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 [--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 [--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 diff --git a/skills/tiktok-video-ad/scripts/pexo-project-list.sh b/skills/tiktok-video-ad/scripts/pexo-project-list.sh new file mode 100644 index 0000000..d11594b --- /dev/null +++ b/skills/tiktok-video-ad/scripts/pexo-project-list.sh @@ -0,0 +1,80 @@ +#!/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 ] [--page-size ] + pexo-project-list.sh -h | --help + +Description: + List projects for the authenticated user. + +Options: + --page Page number (default: 1) + --page-size 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}" diff --git a/skills/tiktok-video-ad/scripts/pexo-upload.sh b/skills/tiktok-video-ad/scripts/pexo-upload.sh new file mode 100644 index 0000000..eaede81 --- /dev/null +++ b/skills/tiktok-video-ad/scripts/pexo-upload.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +[ -n "${BASH_VERSION:-}" ] || exec bash "$0" "$@" + +usage() { + cat <<'EOF' +Usage: + pexo-upload.sh + 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" diff --git a/skills/video-ad/README.md b/skills/video-ad/README.md new file mode 100644 index 0000000..b25957d --- /dev/null +++ b/skills/video-ad/README.md @@ -0,0 +1,38 @@ +# Video Ad — Pexo + +Turn a product or brief into a scroll-stopping video ad. Pexo writes the hook, sequences the +shots, auto-selects the models, and delivers a finished ad with music — ready to run, no editing +required. + +## Install + +```bash +npx skills add https://github.com/pexoai/pexo-skills --skill video-ad +``` + +## Configure + +Create `~/.pexo/config`: + +``` +PEXO_BASE_URL="https://pexo.ai" +PEXO_API_KEY="sk-" +``` + +Get an API key at . + +## Use + +Ask your agent, for example: + +> Make a 15-second video ad for our new running shoes. + +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 video-ad scenario. All creative work (scriptwriting, +model selection, prompts, music, subtitles) happens server-side. diff --git a/skills/video-ad/SKILL.md b/skills/video-ad/SKILL.md new file mode 100644 index 0000000..8d82218 --- /dev/null +++ b/skills/video-ad/SKILL.md @@ -0,0 +1,107 @@ +--- +name: video-ad +description: > + Create scroll-stopping video ads with Pexo. Give it your product, brand, or a photo and Pexo + writes the hook, sequences the shots, picks the best models, and delivers a finished ad with + music — ready to run on Meta, TikTok, or YouTube. Use for advertising/promo video: "video ad", + "make an ad", "product ad", "promo video", "commercial". NOT for explainer or tutorial content + (use the explainer-video skill). +homepage: https://pexo.ai +repository: https://github.com/pexoai/pexo-skills +version: "0.1.0" +requires: + env: [PEXO_API_KEY, PEXO_BASE_URL] + runtime: [curl, jq, file] +metadata: + author: pexoai +--- + +# Video Ad — Pexo + +**Pexo:** https://pexo.ai — get an API key, watch your project render, and buy credits there. + +Produce a finished, ready-to-run video ad. You hand the user's product or brief to the hosted +Pexo agent and deliver the result — Pexo writes the hook, sequences the shots, picks the models, +and sets music and pacing built to convert. + +## 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-" +``` +**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 ""` → save the `project_id`. +2. **Upload any files** the user gave: `pexo-upload.sh ` → save `asset_id`, + reference it inline as `asset_id` (or `` / + ``). Tags are required — a bare `asset_id` is ignored. Pexo can't crawl URLs — + download, then upload. +3. **Send the request:** `pexo-chat.sh " "`. + Copy the user's words exactly; only add asset tags. +4. **Tell the user** (their language): submitted ✓ · ~15–20 min · `https://pexo.ai/project/`. +5. **Poll:** every ≥60s run `pexo-project-get.sh ` 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 ` per option, show the URLs (A/B/C), let the user + pick, then `pexo-chat.sh "" --choice `; for a `document` + event, mention it to the user. + - **DELIVER** → `pexo-asset-get.sh `, 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 `nextActionHint` in plain terms and offer to retry. + - **RECONNECT** → `pexo-chat.sh "continue"`, tell the user the connection + dropped and you're resuming, then keep polling. + - Never call `pexo-chat.sh` during 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 ""`, 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 15-second video ad for our new running shoes." + +```bash +pid=$(pexo-project-create.sh "running shoes ad") +pexo-chat.sh "$pid" "Make a 15-second video ad for our new running shoes." +# 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` | `""` | `project_id` | +| `pexo-upload.sh` | ` ` | `asset_id` | +| `pexo-chat.sh` | ` "" [--choice ]` | ack (async) | +| `pexo-project-get.sh` | `` | JSON: `nextAction`, `recentMessages` | +| `pexo-asset-get.sh` | ` ` | JSON with `url` | +| `pexo-doctor.sh` | — | setup diagnostic | + +Error codes and edge cases → `references/TROUBLESHOOTING.md`. diff --git a/skills/video-ad/references/SETUP-CHECKLIST.md b/skills/video-ad/references/SETUP-CHECKLIST.md new file mode 100644 index 0000000..aec7194 --- /dev/null +++ b/skills/video-ad/references/SETUP-CHECKLIST.md @@ -0,0 +1,93 @@ +# Setup Checklist + +This guide covers first-time setup and environment diagnostics for the pexo-video skill. + +## Quick Start + +### 1. Create config file + +```bash +mkdir -p ~/.pexo +cat > ~/.pexo/config << 'EOF' +PEXO_BASE_URL="https://pexo.ai" +PEXO_API_KEY="sk-" +EOF +``` + +Get 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: **58RYNH** + 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 + +```bash +pexo-doctor.sh +``` + +This checks: +- Config file exists and is readable +- `PEXO_BASE_URL` and `PEXO_API_KEY` are set +- `curl`, `jq`, and `file` are installed +- Network connectivity to Pexo servers +- API key is valid (attempts to list projects) + +Fix any issues reported before using other scripts. + +### 3. Verify + +```bash +pexo-project-list.sh +``` + +If 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: + +```bash +# 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 file +``` + +### Network 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_proxy` environment 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 | diff --git a/skills/video-ad/references/TROUBLESHOOTING.md b/skills/video-ad/references/TROUBLESHOOTING.md new file mode 100644 index 0000000..42b48b4 --- /dev/null +++ b/skills/video-ad/references/TROUBLESHOOTING.md @@ -0,0 +1,289 @@ +# 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: + +```json +{"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 script +- `error`: auth/proxy error code such as `INVALID_API_KEY` or `INTERNAL_ERROR` +- `message`: the most useful user-facing message extracted from the response +- `details`: 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 Credits +``` + + +## Auth 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_size` values 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: + +```text +Error: upload failed with HTTP +``` + +#### 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 using `pexo-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 using `pexo-upload.sh`. +- `412`: the upload session has already expired or been completed. Re-upload from scratch using `pexo-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 a `429`. +- `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.sh` exits non-zero and prints compact JSON to `stderr`, for example: + +```json +{“ok”:false,”httpCode”:200,”message”:”Insufficient credits”,”error”:”credits.insufficient_credits_err”} +``` + +Notes: + +- `pexo-chat.sh` is 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.sh` call should be followed by `pexo-project-get.sh` polling, typically every `60` seconds. + +### `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 `availableCredits` is `0`, the top-up URL is also printed to stderr. +- You generally do not need to call this script manually — `pexo-project-create.sh` and `pexo-chat.sh` call it automatically on `429`/`412` failures 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-run `pexo-asset-get.sh` to 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 set `PEXO_TMP_DIR` to a writable path. + +Notes: + +- The script downloads the file into `~/.pexo/tmp/` (or `$PEXO_TMP_DIR`) and returns both `url` and `localPath`. +- If the asset metadata exists but `downloadUrl` is absent, the script returns `localPath: null`. + +### `pexo-doctor.sh` + +- `200`: config and API key look healthy +- `401` + `INVALID_API_KEY`: API key is invalid or revoked. Update `PEXO_API_KEY` in `~/.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 Credits +``` + +If `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 `429` is likely the concurrent-project limit: the user already has an active project running. +- Re-read the `message` field 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 `. +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 `. +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 key +- `error=INTERNAL_ERROR`: treat it as a temporary service issue, not a key problem diff --git a/skills/video-ad/scripts/_common.sh b/skills/video-ad/scripts/_common.sh new file mode 100644 index 0000000..e8656a4 --- /dev/null +++ b/skills/video-ad/scripts/_common.sh @@ -0,0 +1,430 @@ +#!/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 +} diff --git a/skills/video-ad/scripts/pexo-asset-get.sh b/skills/video-ad/scripts/pexo-asset-get.sh new file mode 100644 index 0000000..d03ba78 --- /dev/null +++ b/skills/video-ad/scripts/pexo-asset-get.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +[ -n "${BASH_VERSION:-}" ] || exec bash "$0" "$@" + +usage() { + cat <<'EOF' +Usage: + pexo-asset-get.sh + 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}' diff --git a/skills/video-ad/scripts/pexo-chat.sh b/skills/video-ad/scripts/pexo-chat.sh new file mode 100644 index 0000000..4cb9b0f --- /dev/null +++ b/skills/video-ad/scripts/pexo-chat.sh @@ -0,0 +1,154 @@ +#!/usr/bin/env bash +[ -n "${BASH_VERSION:-}" ] || exec bash "$0" "$@" + +usage() { + cat <<'EOF' +Usage: + pexo-chat.sh [--choice ] [--timeout ] + 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: + asset_id + asset_id + asset_id + Bare asset IDs inside the message are ignored by Pexo and rejected locally. + +Options: + --choice Send the selected preview asset ID as choices.preview_id + --timeout 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 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#((a_[1-9A-HJ-NP-Za-km-z]{7,24})|([0-9A-Z]{26}))# #g' \ + | sed -E 's#((a_[1-9A-HJ-NP-Za-km-z]{7,24})|([0-9A-Z]{26}))# #g' \ + | sed -E 's#((a_[1-9A-HJ-NP-Za-km-z]{7,24})|([0-9A-Z]{26}))# #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 must be wrapped with ..., ..., or ....' >&2 + printf 'Invalid asset reference(s): %s\n' "$joined" >&2 + echo 'Example: pexo-chat.sh "Use a_xxx 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." + }' diff --git a/skills/video-ad/scripts/pexo-doctor.sh b/skills/video-ad/scripts/pexo-doctor.sh new file mode 100644 index 0000000..3d4ed1e --- /dev/null +++ b/skills/video-ad/scripts/pexo-doctor.sh @@ -0,0 +1,200 @@ +#!/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-"' + 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 diff --git a/skills/video-ad/scripts/pexo-entitlements.sh b/skills/video-ad/scripts/pexo-entitlements.sh new file mode 100644 index 0000000..dca519e --- /dev/null +++ b/skills/video-ad/scripts/pexo-entitlements.sh @@ -0,0 +1,63 @@ +#!/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 diff --git a/skills/video-ad/scripts/pexo-project-create.sh b/skills/video-ad/scripts/pexo-project-create.sh new file mode 100644 index 0000000..41e5677 --- /dev/null +++ b/skills/video-ad/scripts/pexo-project-create.sh @@ -0,0 +1,84 @@ +#!/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 + 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" diff --git a/skills/video-ad/scripts/pexo-project-get.sh b/skills/video-ad/scripts/pexo-project-get.sh new file mode 100644 index 0000000..898bf68 --- /dev/null +++ b/skills/video-ad/scripts/pexo-project-get.sh @@ -0,0 +1,177 @@ +#!/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 [--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 [--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 diff --git a/skills/video-ad/scripts/pexo-project-list.sh b/skills/video-ad/scripts/pexo-project-list.sh new file mode 100644 index 0000000..d11594b --- /dev/null +++ b/skills/video-ad/scripts/pexo-project-list.sh @@ -0,0 +1,80 @@ +#!/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 ] [--page-size ] + pexo-project-list.sh -h | --help + +Description: + List projects for the authenticated user. + +Options: + --page Page number (default: 1) + --page-size 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}" diff --git a/skills/video-ad/scripts/pexo-upload.sh b/skills/video-ad/scripts/pexo-upload.sh new file mode 100644 index 0000000..eaede81 --- /dev/null +++ b/skills/video-ad/scripts/pexo-upload.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +[ -n "${BASH_VERSION:-}" ] || exec bash "$0" "$@" + +usage() { + cat <<'EOF' +Usage: + pexo-upload.sh + 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" diff --git a/skills/youtube-short-maker/README.md b/skills/youtube-short-maker/README.md new file mode 100644 index 0000000..dfddb20 --- /dev/null +++ b/skills/youtube-short-maker/README.md @@ -0,0 +1,38 @@ +# YouTube Short Maker — Pexo + +Make YouTube Shorts that hold attention — vertical, fast-paced, hook-first. Pexo scripts, +generates, and assembles a finished Short with music and captions — no prompt engineering, no +editing. + +## Install + +```bash +npx skills add https://github.com/pexoai/pexo-skills --skill youtube-short-maker +``` + +## Configure + +Create `~/.pexo/config`: + +``` +PEXO_BASE_URL="https://pexo.ai" +PEXO_API_KEY="sk-" +``` + +Get an API key at . + +## Use + +Ask your agent, for example: + +> Make a YouTube Short on 3 quick tips for better sleep. + +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 youtube-short-maker scenario. All creative work (scriptwriting, +model selection, prompts, music, subtitles) happens server-side. diff --git a/skills/youtube-short-maker/SKILL.md b/skills/youtube-short-maker/SKILL.md new file mode 100644 index 0000000..5e8cb3f --- /dev/null +++ b/skills/youtube-short-maker/SKILL.md @@ -0,0 +1,107 @@ +--- +name: youtube-short-maker +description: > + Make YouTube Shorts that hold attention with Pexo — vertical 9:16, fast-paced, hook in the first + 3 seconds. Describe your topic and Pexo writes the script, generates the shots, picks the models, + and assembles a finished Short with music and captions. Use for YouTube Shorts: "youtube short", + "make a youtube short", "shorts video", "short-form video". NOT for long-form or landscape + video. +homepage: https://pexo.ai +repository: https://github.com/pexoai/pexo-skills +version: "0.1.0" +requires: + env: [PEXO_API_KEY, PEXO_BASE_URL] + runtime: [curl, jq, file] +metadata: + author: pexoai +--- + +# YouTube Short Maker — Pexo + +**Pexo:** https://pexo.ai — get an API key, watch your project render, and buy credits there. + +Make a finished YouTube Short built to retain — vertical and hook-first. You pass the user's +topic to the hosted Pexo agent and deliver the result; Pexo scripts it, generates the shots, and +assembles the Short with music and captions. + +## 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-" +``` +**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 ""` → save the `project_id`. +2. **Upload any files** the user gave: `pexo-upload.sh ` → save `asset_id`, + reference it inline as `asset_id` (or `` / + ``). Tags are required — a bare `asset_id` is ignored. Pexo can't crawl URLs — + download, then upload. +3. **Send the request:** `pexo-chat.sh " "`. + Copy the user's words exactly; only add asset tags. +4. **Tell the user** (their language): submitted ✓ · ~15–20 min · `https://pexo.ai/project/`. +5. **Poll:** every ≥60s run `pexo-project-get.sh ` 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 ` per option, show the URLs (A/B/C), let the user + pick, then `pexo-chat.sh "" --choice `; for a `document` + event, mention it to the user. + - **DELIVER** → `pexo-asset-get.sh `, 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 `nextActionHint` in plain terms and offer to retry. + - **RECONNECT** → `pexo-chat.sh "continue"`, tell the user the connection + dropped and you're resuming, then keep polling. + - Never call `pexo-chat.sh` during 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 ""`, 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 YouTube Short on 3 quick tips for better sleep." + +```bash +pid=$(pexo-project-create.sh "better sleep tips short") +pexo-chat.sh "$pid" "Make a YouTube Short on 3 quick tips for better sleep." +# 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` | `""` | `project_id` | +| `pexo-upload.sh` | ` ` | `asset_id` | +| `pexo-chat.sh` | ` "" [--choice ]` | ack (async) | +| `pexo-project-get.sh` | `` | JSON: `nextAction`, `recentMessages` | +| `pexo-asset-get.sh` | ` ` | JSON with `url` | +| `pexo-doctor.sh` | — | setup diagnostic | + +Error codes and edge cases → `references/TROUBLESHOOTING.md`. diff --git a/skills/youtube-short-maker/references/SETUP-CHECKLIST.md b/skills/youtube-short-maker/references/SETUP-CHECKLIST.md new file mode 100644 index 0000000..2dca43e --- /dev/null +++ b/skills/youtube-short-maker/references/SETUP-CHECKLIST.md @@ -0,0 +1,93 @@ +# Setup Checklist + +This guide covers first-time setup and environment diagnostics for the pexo-video skill. + +## Quick Start + +### 1. Create config file + +```bash +mkdir -p ~/.pexo +cat > ~/.pexo/config << 'EOF' +PEXO_BASE_URL="https://pexo.ai" +PEXO_API_KEY="sk-" +EOF +``` + +Get 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: **6VKKHB** + 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 + +```bash +pexo-doctor.sh +``` + +This checks: +- Config file exists and is readable +- `PEXO_BASE_URL` and `PEXO_API_KEY` are set +- `curl`, `jq`, and `file` are installed +- Network connectivity to Pexo servers +- API key is valid (attempts to list projects) + +Fix any issues reported before using other scripts. + +### 3. Verify + +```bash +pexo-project-list.sh +``` + +If 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: + +```bash +# 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 file +``` + +### Network 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_proxy` environment 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 | diff --git a/skills/youtube-short-maker/references/TROUBLESHOOTING.md b/skills/youtube-short-maker/references/TROUBLESHOOTING.md new file mode 100644 index 0000000..42b48b4 --- /dev/null +++ b/skills/youtube-short-maker/references/TROUBLESHOOTING.md @@ -0,0 +1,289 @@ +# 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: + +```json +{"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 script +- `error`: auth/proxy error code such as `INVALID_API_KEY` or `INTERNAL_ERROR` +- `message`: the most useful user-facing message extracted from the response +- `details`: 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 Credits +``` + + +## Auth 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_size` values 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: + +```text +Error: upload failed with HTTP +``` + +#### 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 using `pexo-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 using `pexo-upload.sh`. +- `412`: the upload session has already expired or been completed. Re-upload from scratch using `pexo-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 a `429`. +- `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.sh` exits non-zero and prints compact JSON to `stderr`, for example: + +```json +{“ok”:false,”httpCode”:200,”message”:”Insufficient credits”,”error”:”credits.insufficient_credits_err”} +``` + +Notes: + +- `pexo-chat.sh` is 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.sh` call should be followed by `pexo-project-get.sh` polling, typically every `60` seconds. + +### `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 `availableCredits` is `0`, the top-up URL is also printed to stderr. +- You generally do not need to call this script manually — `pexo-project-create.sh` and `pexo-chat.sh` call it automatically on `429`/`412` failures 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-run `pexo-asset-get.sh` to 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 set `PEXO_TMP_DIR` to a writable path. + +Notes: + +- The script downloads the file into `~/.pexo/tmp/` (or `$PEXO_TMP_DIR`) and returns both `url` and `localPath`. +- If the asset metadata exists but `downloadUrl` is absent, the script returns `localPath: null`. + +### `pexo-doctor.sh` + +- `200`: config and API key look healthy +- `401` + `INVALID_API_KEY`: API key is invalid or revoked. Update `PEXO_API_KEY` in `~/.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 Credits +``` + +If `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 `429` is likely the concurrent-project limit: the user already has an active project running. +- Re-read the `message` field 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 `. +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 `. +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 key +- `error=INTERNAL_ERROR`: treat it as a temporary service issue, not a key problem diff --git a/skills/youtube-short-maker/scripts/_common.sh b/skills/youtube-short-maker/scripts/_common.sh new file mode 100644 index 0000000..e8656a4 --- /dev/null +++ b/skills/youtube-short-maker/scripts/_common.sh @@ -0,0 +1,430 @@ +#!/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 +} diff --git a/skills/youtube-short-maker/scripts/pexo-asset-get.sh b/skills/youtube-short-maker/scripts/pexo-asset-get.sh new file mode 100644 index 0000000..d03ba78 --- /dev/null +++ b/skills/youtube-short-maker/scripts/pexo-asset-get.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +[ -n "${BASH_VERSION:-}" ] || exec bash "$0" "$@" + +usage() { + cat <<'EOF' +Usage: + pexo-asset-get.sh + 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}' diff --git a/skills/youtube-short-maker/scripts/pexo-chat.sh b/skills/youtube-short-maker/scripts/pexo-chat.sh new file mode 100644 index 0000000..4cb9b0f --- /dev/null +++ b/skills/youtube-short-maker/scripts/pexo-chat.sh @@ -0,0 +1,154 @@ +#!/usr/bin/env bash +[ -n "${BASH_VERSION:-}" ] || exec bash "$0" "$@" + +usage() { + cat <<'EOF' +Usage: + pexo-chat.sh [--choice ] [--timeout ] + 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: + asset_id + asset_id + asset_id + Bare asset IDs inside the message are ignored by Pexo and rejected locally. + +Options: + --choice Send the selected preview asset ID as choices.preview_id + --timeout 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 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#((a_[1-9A-HJ-NP-Za-km-z]{7,24})|([0-9A-Z]{26}))# #g' \ + | sed -E 's#((a_[1-9A-HJ-NP-Za-km-z]{7,24})|([0-9A-Z]{26}))# #g' \ + | sed -E 's#((a_[1-9A-HJ-NP-Za-km-z]{7,24})|([0-9A-Z]{26}))# #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 must be wrapped with ..., ..., or ....' >&2 + printf 'Invalid asset reference(s): %s\n' "$joined" >&2 + echo 'Example: pexo-chat.sh "Use a_xxx 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." + }' diff --git a/skills/youtube-short-maker/scripts/pexo-doctor.sh b/skills/youtube-short-maker/scripts/pexo-doctor.sh new file mode 100644 index 0000000..3d4ed1e --- /dev/null +++ b/skills/youtube-short-maker/scripts/pexo-doctor.sh @@ -0,0 +1,200 @@ +#!/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-"' + 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 diff --git a/skills/youtube-short-maker/scripts/pexo-entitlements.sh b/skills/youtube-short-maker/scripts/pexo-entitlements.sh new file mode 100644 index 0000000..dca519e --- /dev/null +++ b/skills/youtube-short-maker/scripts/pexo-entitlements.sh @@ -0,0 +1,63 @@ +#!/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 diff --git a/skills/youtube-short-maker/scripts/pexo-project-create.sh b/skills/youtube-short-maker/scripts/pexo-project-create.sh new file mode 100644 index 0000000..41e5677 --- /dev/null +++ b/skills/youtube-short-maker/scripts/pexo-project-create.sh @@ -0,0 +1,84 @@ +#!/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 + 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" diff --git a/skills/youtube-short-maker/scripts/pexo-project-get.sh b/skills/youtube-short-maker/scripts/pexo-project-get.sh new file mode 100644 index 0000000..898bf68 --- /dev/null +++ b/skills/youtube-short-maker/scripts/pexo-project-get.sh @@ -0,0 +1,177 @@ +#!/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 [--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 [--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 diff --git a/skills/youtube-short-maker/scripts/pexo-project-list.sh b/skills/youtube-short-maker/scripts/pexo-project-list.sh new file mode 100644 index 0000000..d11594b --- /dev/null +++ b/skills/youtube-short-maker/scripts/pexo-project-list.sh @@ -0,0 +1,80 @@ +#!/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 ] [--page-size ] + pexo-project-list.sh -h | --help + +Description: + List projects for the authenticated user. + +Options: + --page Page number (default: 1) + --page-size 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}" diff --git a/skills/youtube-short-maker/scripts/pexo-upload.sh b/skills/youtube-short-maker/scripts/pexo-upload.sh new file mode 100644 index 0000000..eaede81 --- /dev/null +++ b/skills/youtube-short-maker/scripts/pexo-upload.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +[ -n "${BASH_VERSION:-}" ] || exec bash "$0" "$@" + +usage() { + cat <<'EOF' +Usage: + pexo-upload.sh + 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"